Flask app progress

This commit is contained in:
Matthew Grotke 2026-05-17 03:26:01 -04:00
parent c4fe022d42
commit b0994069ad
38 changed files with 6631 additions and 220 deletions

View file

@ -41,6 +41,7 @@ import sys
import argparse
from pathlib import Path
from datetime import datetime, timezone
from validation import ip as validate_ip
SCRIPT_DIR = Path(__file__).parent
DHCP_CONFIG_FILE = SCRIPT_DIR / "core.json"
@ -48,12 +49,12 @@ DDNS_CONFIG_FILE = SCRIPT_DIR / "ddns.json"
WG_DIR = Path("/etc/wireguard")
KEEPALIVE = 25
# ------------------------------------------------------------------------------
# ===================================================================
# Helpers
# ------------------------------------------------------------------------------
# ===================================================================
def die(msg):
print(f"ERROR: {msg}")
print(f"ERROR: {msg}", file=sys.stderr)
sys.exit(1)
def check_root():
@ -63,7 +64,7 @@ def check_root():
def chown_to_script_dir_owner(path):
"""Chown a file to the owner of the script directory.
Keeps SCRIPT_DIR files user-owned even when running as root.
/etc/wireguard files are intentionally excluded they stay root-owned.
/etc/wireguard files are intentionally excluded - they stay root-owned.
"""
try:
stat = SCRIPT_DIR.stat()
@ -124,9 +125,9 @@ def _fmt_bytes(n):
else:
return f"{n / 1024**3:.2f} GB"
# ------------------------------------------------------------------------------
# ===================================================================
# Load core.json / dotfiles
# ------------------------------------------------------------------------------
# ===================================================================
def load_dhcp():
if not DHCP_CONFIG_FILE.exists():
@ -169,9 +170,9 @@ def save_peers(iface, peers):
path.chmod(0o600)
chown_to_script_dir_owner(path)
# ------------------------------------------------------------------------------
# ===================================================================
# IP allocation
# ------------------------------------------------------------------------------
# ===================================================================
def next_available_ip(vlan, peers):
"""
@ -197,9 +198,9 @@ def next_available_ip(vlan, peers):
die(f"No available IPs in VPN subnet {network} (all .2-.254 allocated).")
# ------------------------------------------------------------------------------
# ===================================================================
# Key management
# ------------------------------------------------------------------------------
# ===================================================================
def generate_server_key(iface):
"""Generate server private key and store at WG_DIR/<iface>.key (600)."""
@ -228,9 +229,9 @@ def generate_peer_keypair():
).stdout.strip()
return private, public
# ------------------------------------------------------------------------------
# ===================================================================
# Endpoint resolution
# ------------------------------------------------------------------------------
# ===================================================================
def resolve_endpoint(listen_port):
"""
@ -294,9 +295,9 @@ def resolve_endpoint(listen_port):
entry = f"{entry}:{listen_port}"
return entry
# ------------------------------------------------------------------------------
# ===================================================================
# Split-tunnel route computation
# ------------------------------------------------------------------------------
# ===================================================================
def split_tunnel_routes(dhcp_data):
"""
@ -316,9 +317,9 @@ def split_tunnel_routes(dhcp_data):
routes.append(str(net))
return routes
# ------------------------------------------------------------------------------
# ===================================================================
# Client config
# ------------------------------------------------------------------------------
# ===================================================================
def build_client_conf(peer, private_key, server_public_key, endpoint,
allowed_ips, dns, domain, mtu):
@ -348,9 +349,9 @@ def write_client_conf(peer, private_key, server_public_key, endpoint,
chown_to_script_dir_owner(conf_path)
return conf_path
# ------------------------------------------------------------------------------
# ===================================================================
# WireGuard server conf
# ------------------------------------------------------------------------------
# ===================================================================
def build_wg_conf(vlan, peers, server_private_key):
iface = vlan["interface"]
@ -381,9 +382,9 @@ def build_wg_conf(vlan, peers, server_private_key):
]
return "\n".join(lines)
# ------------------------------------------------------------------------------
# ===================================================================
# Live peer sync
# ------------------------------------------------------------------------------
# ===================================================================
def sync_peers_live(iface, peers):
"""
@ -418,9 +419,9 @@ def sync_peers_live(iface, peers):
run(["wg", "set", iface, "peer", key, "remove"])
print(f" Removed peer: {key[:16]}...")
# ------------------------------------------------------------------------------
# ===================================================================
# Interface selection
# ------------------------------------------------------------------------------
# ===================================================================
def validate_wg_vlans(wg_vlans):
"""Die with a clear message if any wg VLAN is missing a valid vpn_information block."""
@ -432,8 +433,11 @@ def validate_wg_vlans(wg_vlans):
f"Add: \"vpn_information\": {{\"listen_port\": 51820, \"gateway\": \"...\"}}")
if not isinstance(info.get("listen_port"), int):
die(f"Interface '{iface}' vpn_information is missing a valid listen_port in core.json.")
if not info.get("gateway"):
gw = info.get("gateway", "")
if not gw:
die(f"Interface '{iface}' vpn_information is missing gateway in core.json.")
elif not validate_ip(gw):
die(f"Interface '{iface}' vpn_information.gateway '{gw}' is not a valid IP address.")
def pick_wg_interface(wg_vlans):
"""
@ -459,9 +463,9 @@ def pick_wg_interface(wg_vlans):
pass
print(" Invalid selection.")
# ------------------------------------------------------------------------------
# ===================================================================
# --add-peer
# ------------------------------------------------------------------------------
# ===================================================================
def cmd_add_peer(dhcp_data):
check_root()
@ -569,9 +573,9 @@ def cmd_add_peer(dhcp_data):
print(" sudo python3 vpn.py --apply")
print()
# ------------------------------------------------------------------------------
# ===================================================================
# --list-peers
# ------------------------------------------------------------------------------
# ===================================================================
def cmd_list_peers(dhcp_data):
check_root()
@ -718,9 +722,9 @@ def cmd_list_peers(dhcp_data):
print(" sudo python3 vpn.py --apply")
print()
# ------------------------------------------------------------------------------
# ===================================================================
# --apply
# ------------------------------------------------------------------------------
# ===================================================================
def cmd_apply(dhcp_data):
check_root()
@ -808,9 +812,9 @@ def cmd_apply(dhcp_data):
else:
print(f"WARNING: {core_py} not found -- run core.py --apply manually to load VPN firewall rules.")
# ------------------------------------------------------------------------------
# ===================================================================
# --disable
# ------------------------------------------------------------------------------
# ===================================================================
def cmd_disable(dhcp_data):
check_root()
@ -825,9 +829,9 @@ def cmd_disable(dhcp_data):
else:
print(f"WireGuard service {svc} stopped and disabled.")
# ------------------------------------------------------------------------------
# ===================================================================
# --status
# ------------------------------------------------------------------------------
# ===================================================================
def cmd_status(dhcp_data):
check_root()
@ -844,8 +848,8 @@ def cmd_status(dhcp_data):
r_enabled = run(["systemctl", "is-enabled", svc], check=False)
active = r_active.stdout.strip()
enabled = r_enabled.stdout.strip()
active_sym = "" if active == "active" else ""
enabled_sym = "" if enabled == "enabled" else ""
active_sym = "+" if active == "active" else "x"
enabled_sym = "+" if enabled == "enabled" else "x"
print(f" {svc:<45} {active_sym} {active:<10} {enabled_sym} {enabled}")
if active == "active":
@ -869,9 +873,9 @@ def cmd_status(dhcp_data):
enabled_peers = [p for p in peers if p.get("enabled") is True]
print(f" peers: {len(enabled_peers)} configured, {info.get('peers', 0)} connected")
# ------------------------------------------------------------------------------
# ===================================================================
# --logs
# ------------------------------------------------------------------------------
# ===================================================================
def cmd_logs(dhcp_data):
check_root()
@ -940,9 +944,9 @@ def cmd_logs(dhcp_data):
print()
# ------------------------------------------------------------------------------
# ===================================================================
# Main
# ------------------------------------------------------------------------------
# ===================================================================
def main():
parser = argparse.ArgumentParser(