From: Thomas Walker Lynch Date: Mon, 15 Sep 2025 05:34:22 +0000 (-0700) Subject: a little house cleaning X-Git-Url: https://git.reasoningtechnology.com/style/rt_dark_doc.css?a=commitdiff_plain;h=8a4a77267ba8eac35523181d4d278d772bd3bfa2;p=subu a little house cleaning --- diff --git a/developer/source/DNS/deloy.sh b/developer/source/DNS/deloy.sh deleted file mode 100644 index 7a033a8..0000000 --- a/developer/source/DNS/deloy.sh +++ /dev/null @@ -1,8 +0,0 @@ -sudo ./install_staged_tree.py -sudo systemctl daemon-reload -# enable nft snippet (once): -sudo sed -i '1{/table inet filter {/!{h;s/.*/include "\/etc\/nftables.d\/30-dnsredir.nft"/;H;x}}' /etc/nftables.conf || true -sudo nft -f /etc/nftables.conf - -# bring up Unbound instances (they wait for wg links): -sudo systemctl enable --now unbound@US unbound@x6 diff --git a/developer/source/DNS/deploy.py b/developer/source/DNS/deploy.py new file mode 100644 index 0000000..33f07e0 --- /dev/null +++ b/developer/source/DNS/deploy.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +""" +deploy_dns.py — installs staged DNS artifacts (Unbound, nftables snippet) +without starting/stopping services. Prints next-step commands. +""" + +from __future__ import annotations +from pathlib import Path +import os, sys + +def main(argv=None) -> int: + root = Path(__file__).resolve().parent + stage = root / "stage" + issues = [] + if os.geteuid() != 0: + issues.append("must be run as root (sudo)") + + for rel in [ + "etc/unbound/unbound-US.conf", + "etc/unbound/unbound-x6.conf", + "etc/systemd/system/unbound@.service", + "etc/nftables.d/30-dnsredir.nft", + ]: + if not (stage / rel).exists(): + issues.append(f"missing staged file: stage/{rel}") + + try: + import install_staged_tree as ist + except Exception as e: + issues.append(f"failed to import install_staged_tree: {e}") + + if issues: + print("❌ deploy preflight found issue(s):") + for i in issues: print(f" - {i}") + return 2 + + dest_root = Path("/") + staged = ist.install_staged_tree(stage_root=stage, dest_root=dest_root) + # Paths printed by install_staged_tree; keep our output short. + print("\nNext steps:") + print(" sudo systemctl daemon-reload") + print(' # ensure nft snippet included in /etc/nftables.conf:') + print(' # include "/etc/nftables.d/30-dnsredir.nft"') + print(" sudo nft -f /etc/nftables.conf") + print(" sudo install -d -m 0755 /var/lib/unbound") + print(" sudo unbound-anchor -a /var/lib/unbound/root.key") + print(" sudo systemctl enable --now unbound@US unbound@x6") + print("\nVerify:") + print(" sudo ss -ltnup '( sport = :5301 or sport = :5302 )'") + print(" sudo -u Thomas-US dig example.com +short") + print(" sudo -u Thomas-x6 dig example.com +short") + return 0 + +if __name__ == "__main__": + sys.exit(main()) diff --git a/developer/source/DNS/scratchpad/.gitignore b/developer/source/DNS/scratchpad/.gitignore new file mode 100644 index 0000000..120f485 --- /dev/null +++ b/developer/source/DNS/scratchpad/.gitignore @@ -0,0 +1,2 @@ +* +!/.gitignore diff --git a/developer/source/DNS_bundle.tgz b/developer/source/DNS_bundle.tgz new file mode 100644 index 0000000..1635cc2 Binary files /dev/null and b/developer/source/DNS_bundle.tgz differ diff --git a/developer/source/tunnel-client/mothball/stage/.gitignore b/developer/source/tunnel-client/mothball/stage/.gitignore deleted file mode 100644 index 53642ce..0000000 --- a/developer/source/tunnel-client/mothball/stage/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ - -* -!.gitignore - diff --git a/developer/source/tunnel-client/mothball/stage_IP_routes_script.py b/developer/source/tunnel-client/mothball/stage_IP_routes_script.py deleted file mode 100755 index d1ec126..0000000 --- a/developer/source/tunnel-client/mothball/stage_IP_routes_script.py +++ /dev/null @@ -1,118 +0,0 @@ -#!/usr/bin/env python3 -# stage_IP_route_script.py — emit /usr/local/bin/route_init_.sh from DB -# Purpose at runtime of the emitted script: -# 1) Ensure default + blackhole default in the dedicated route table. -# 2) Pin the peer endpoint (/32 via GW on NIC, metric 5) outside the tunnel so the handshake cannot vanish. -# 3) Apply any extra route from the route table (on_up=1). -# -# Usage: stage_IP_route_script.py -# Output: stage/usr/local/bin/route_init_.sh (chmod 500) -# Idempotence (runtime): uses `ip -4 route replace` -# Failure modes (runtime): if DNS resolution fails, step (2) is skipped; (1) and (3) still apply. - -from __future__ import annotations -import sys, sqlite3 -from pathlib import Path -import incommon as ic # open_db(), rows() - -def _bash_single_quote(s: str) -> str: - # Safe single-quoted literal for bash - return "'" + s.replace("'", "'\"'\"'") + "'" - -def stage_ip_route_script(iface: str) -> Path: - # Resolve DB data - with ic.open_db() as conn: - row = conn.execute( - "SELECT id, rt_table_name_eff FROM v_client_effective WHERE iface=? LIMIT 1;", - (iface,) - ).fetchone() - if not row: - raise RuntimeError(f"iface not found in DB: {iface}") - iface_id, rtname = int(row[0]), str(row[1]) - - # Preferred server: lowest priority, then lowest id - srow = conn.execute( - """ - SELECT s.endpoint_host, s.endpoint_port - FROM server s - JOIN Iface c ON c.id=s.iface_id - WHERE c.id=? - ORDER BY s.priority ASC, s.id ASC - LIMIT 1; - """, - (iface_id,) - ).fetchone() - ep_host = str(srow[0]) if srow and srow[0] else "" - ep_port = str(srow[1]) if srow and srow[1] else "" - - # Extra route for on_up - extra = ic.rows(conn, """ - SELECT cidr, COALESCE(via,''), COALESCE(table_name,''), COALESCE(metric,'') - FROM route - WHERE iface_id=? AND on_up=1 - ORDER BY id; - """, (iface_id,)) - - # Paths - out_path = Path(__file__).resolve().parent / "stage" / "usr" / "local" / "bin" / f"route_init_{iface}.sh" - out_path.parent.mkdir(parents=True, exist_ok=True) - - # Emit script - lines: list[str] = [] - lines.append("#!/usr/bin/env bash") - lines.append("set -euo pipefail") - lines.append(f"table={_bash_single_quote(rtname)}") - lines.append(f"dev={_bash_single_quote(iface)}") - lines.append(f"endpoint_host={_bash_single_quote(ep_host)}") - lines.append(f"endpoint_port={_bash_single_quote(ep_port)}") - lines.append("") - lines.append("# 1) Default in dedicated table") - lines.append('ip -4 route replace default dev "$dev" table "$table"') - lines.append('ip -4 route replace blackhole default metric 32767 table "$table"') - lines.append("") - lines.append("# 2) Keep peer endpoint reachable outside the tunnel") - lines.append('ep_ip=$(getent ahostsv4 "$endpoint_host" | awk \'NR==1{print $1}\')') - lines.append('if [[ -n "$ep_ip" ]]; then') - lines.append(' gw=$(ip -4 route get "$ep_ip" | awk \'/ via /{print $3; exit}\')') - lines.append(' nic=$(ip -4 route get "$ep_ip" | awk \'/ dev /{for(i=1;i<=NF;i++) if ($i=="dev"){print $(i+1); exit}}\')') - lines.append(' if [[ -n "$gw" && -n "$nic" ]]; then') - lines.append(' ip -4 route replace "${ep_ip}/32" via "$gw" dev "$nic" metric 5') - lines.append(' fi') - lines.append('fi') - lines.append("") - lines.append("# 3) Extra route from DB") - for cidr, via, tbl, met in extra: - cidr = str(cidr) - via = str(via or "") - tbl = str(tbl or rtname) - met = str(met or "") - cmd = ["ip -4 route replace", cidr] - if via: cmd += ["via", via] - cmd += ['table', f'"{tbl}"'] - if met: cmd += ['metric', met] - lines.append(" ".join(cmd)) - - out_path.write_text("\n".join(lines) + "\n") - out_path.chmod(0o500) - - return out_path - -def main(argv: list[str]) -> int: - if len(argv) != 1: - print(f"Usage: {Path(sys.argv[0]).name} ", file=sys.stderr) - return 2 - iface = argv[0] - try: - out = stage_ip_route_script(iface) - except (sqlite3.Error, FileNotFoundError, RuntimeError) as e: - print(f"❌ {e}", file=sys.stderr); return 1 - # Print relative-to-CWD as requested style: 'stage/...' - try: - rel = out.relative_to(Path.cwd()) - print(f"staged: {rel}") - except ValueError: - print(f"staged: {out}") - return 0 - -if __name__ == "__main__": - sys.exit(main(sys.argv[1:])) diff --git a/developer/source/tunnel-client/mothball/stage_IP_rules_script.py b/developer/source/tunnel-client/mothball/stage_IP_rules_script.py deleted file mode 100755 index 7fae716..0000000 --- a/developer/source/tunnel-client/mothball/stage_IP_rules_script.py +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env python3 -""" -stage_IP_rules.py — stage a runtime script to enforce IPv4 rules for all subu - -- Reads subu_cidr from DB.meta -- For each client: adds FROM → and per-UID rules -- Appends a final PROHIBIT for subu_cidr to enforce hard containment -- Writes: stage/usr/local/bin/ (no args at runtime) -""" - -from __future__ import annotations -import sys -from pathlib import Path -from typing import Optional, Sequence, Dict, List -import incommon as ic - -OUTPUT_SCRIPT_NAME = "set_subu_IP_rules.sh" - -def stage_set_subu_ip_rules(ifaces: Optional[Sequence[str]] = None) -> tuple[Path, str]: - with ic.open_db() as conn: # ← no path arg - client = ic.fetch_client(conn, ifaces) # expects id, iface, rtname, addr from v_client_effective - if not client: raise RuntimeError("no client selected") - subu = ic.subu_cidr(conn, "10.0.0.0/24") - ic.validate_unique_hosts(client, subu) - uid_map: Dict[int, List[int]] = {int(c["id"]): ic.collect_uids(conn, int(c["id"])) for c in client} - - out = ic.STAGE_ROOT / "usr" / "local" / "bin" / OUTPUT_SCRIPT_NAME - - lines: List[str] = [] - - lines += [ - "#!/usr/bin/env bash", - "# Enforce IPv4 rules for all subu; idempotent per rule.", - "set -euo pipefail", - "", - 'add_IP_rule_if_not_exists(){ local search_phrase=$1; shift; if ! ip -4 rule list | grep -F -q -- "$search_phrase"; then ip -4 rule add "$@"; fi; }', - "" - ] - - for c in client: - table = c["rtname"]; src_cidr = c["addr"]; cid = int(c["id"]) - lines += [f"# client: iface={c['iface']} table={table} src={src_cidr} id={cid}"] - lines += [f'add_IP_rule_if_not_exists "from {src_cidr} lookup {table}" from "{src_cidr}" lookup "{table}" pref 17000'] - for u in uid_map[cid]: - lines += [f'add_IP_rule_if_not_exists "from {src_cidr} lookup {table}" from "{src_cidr}" lookup "{table}" pref 17000'] - lines += [""] - - lines += [ - "# hard containment for subu space", - f'add_IP_rule_if_not_exists "from {subu} prohibit" from "{subu}" prohibit pref 18050', - "" - ] - - ic.write_exec_quiet(out, "\n".join(lines)) - - per_iface = ", ".join( - f"{c['iface']}:[{','.join(str(u) for u in uid_map[int(c['id'])]) or '-'}]" - for c in client - ) - total_uid_rules = sum(len(uid_map[int(c["id"])]) for c in client) - summary = f"client={len(client)}, uid_rules={total_uid_rules} ({per_iface})" - return out, summary - -def main(argv: Sequence[str]) -> int: - ifaces = list(argv) if argv else None - try: - path, summary = stage_set_subu_ip_rules(ifaces) - except Exception as e: - print(f"❌ {e}", file=sys.stderr); return 1 - try: - rel = "stage/" + path.relative_to(ic.STAGE_ROOT).as_posix() - except Exception: - rel = path.as_posix().replace(ic.ROOT.as_posix() + "/", "") - print(f"staged: {rel} — {summary}") - return 0 - -if __name__ == "__main__": - sys.exit(main(sys.argv[1:])) diff --git a/developer/source/tunnel-client/mothball/stage_StanleyPark.py b/developer/source/tunnel-client/mothball/stage_StanleyPark.py deleted file mode 100644 index c374029..0000000 --- a/developer/source/tunnel-client/mothball/stage_StanleyPark.py +++ /dev/null @@ -1,139 +0,0 @@ -#!/usr/bin/env python3 -# stage_StanleyPark.py — stage artifacts for this client machine only -# Chooses just the ifaces we run here (x6, US) and reuses existing business funcs. - -from __future__ import annotations -import sys, sqlite3, shutil -from pathlib import Path -import incommon as ic - -# Reuse business modules (no logic duplication) -import stage_clean as stclean -import stage_wg_conf as stconf -import stage_preferred_server as stpref -import stage_IP_route_script as striproute -import stage_IP_rules_script as striprules -import stage_wg_unit_IP_scripts as stdrop - -# Ifaces for THIS machine (adjust if needed) -IFACES = ["x6", "US"] - -def msg_wrapped_call(title: str, fn=None, *args, **kwargs): - print(f"→ {title}", flush=True) - res = fn(*args, **kwargs) if fn else None - print(f"✔ {title}" + (f": {res}" if res not in (None, "") else ""), flush=True) - return res - -def fetch_client_by_iface(conn: sqlite3.Connection, iface: str) -> dict | None: - conn.row_factory = sqlite3.Row - # Prefer the effective-view; fall back if missing - try: - r = conn.execute(""" - SELECT c.id, c.iface, v.rt_table_name_eff AS rtname, - COALESCE(c.rt_table_id,'') AS rtid, - c.local_address_cidr AS addr, - c.private_key AS priv, - COALESCE(c.mtu,'') AS mtu, - COALESCE(c.fwmark,'') AS fwmark, - c.dns_mode AS dns_mode, - COALESCE(c.dns_servers,'') AS dns_servers, - c.autostart AS autostart - FROM Iface c - JOIN v_client_effective v ON v.id=c.id - WHERE c.iface=? LIMIT 1; - """,(iface,)).fetchone() - except sqlite3.Error: - r = conn.execute(""" - SELECT id, iface, COALESCE(rt_table_name,iface) AS rtname, - COALESCE(rt_table_id,'') AS rtid, - local_address_cidr AS addr, - private_key AS priv, - COALESCE(mtu,'') AS mtu, - COALESCE(fwmark,'') AS fwmark, - dns_mode AS dns_mode, - COALESCE(dns_servers,'') AS dns_servers, - autostart AS autostart - FROM Iface WHERE iface=? LIMIT 1; - """,(iface,)).fetchone() - return (dict(r) if r else None) - -def stage_for_ifaces(ifaces: list[str], clean_mode: str | None) -> int: - # 0) Clean stage dir - if clean_mode == "--clean": - msg_wrapped_call("stage clean (--yes)", stclean.clean, yes=True, dry_run=False, hard=False) - elif clean_mode == "--no-clean": - Path(stclean.stage_root()).mkdir(parents=True, exist_ok=True) - else: - msg_wrapped_call("stage clean (interactive)", stclean.clean, yes=False, dry_run=False, hard=False) - - root = Path(__file__).resolve().parent - stage_root = root / "stage" - (stage_root / "wireguard").mkdir(parents=True, exist_ok=True) - (stage_root / "systemd").mkdir(parents=True, exist_ok=True) - (stage_root / "usr" / "local" / "bin").mkdir(parents=True, exist_ok=True) - - # Optional helper carry-over (kept same behavior) - ip_rule_add = root / "IP_rule_add_UID.sh" - if ip_rule_add.exists(): - dst = stage_root / "usr" / "local" / "bin" / "IP_rule_add_UID.sh" - shutil.copy2(ip_rule_add, dst); dst.chmod(0o500) - print(f"staged: {dst.relative_to(root)}") - - # 1) Global policy script — limit to selected ifaces (so rules are scoped) - msg_wrapped_call(f"stage global set_subu_IP_rules.sh for {ifaces}", - striprules.stage_set_subu_ip_rules, ifaces) - - # 2) Per-iface artifacts - with ic.open_db() as conn: - for iface in ifaces: - c = fetch_client_by_iface(conn, iface) - if not c: - print(f"⚠️ iface '{iface}' not in DB; skipping"); continue - - cid = int(c["id"]) - addr = str(c["addr"]) - priv = str(c["priv"]) - mtu = str(c["mtu"]) - fw = str(c["fwmark"]) - dns_m = str(c["dns_mode"]) - dns_s = str(c["dns_servers"]) - - srow = stpref.preferred_server_row(cid) - if not srow: - print(f"⚠️ No server for client '{iface}' (id={cid}). Skipping.") - continue - (s_name, s_pub, s_psk, s_host, s_port, s_allow, s_ka, s_route) = srow - - # WG conf - conf_out = stage_root / "wireguard" / f"{iface}.conf" - msg_wrapped_call(f"wg conf for {iface}", - stconf.write_wg_conf, conf_out, addr, priv, mtu, fw, dns_m, dns_s, - s_pub, s_psk, s_host, str(s_port), s_allow, str(s_ka or "") - ) - - # Per-iface route script - msg_wrapped_call(f"route_init for {iface}", striproute.stage_ip_route_script, iface) - - # Systemd override referencing global rules + per-iface route - msg_wrapped_call(f"wg-quick override for {iface}", stdrop.stage_dropin, iface) - - print(f"✔ Staged: {iface}") - - print(f"✅ Stage generation complete in: {stage_root}") - return 0 - -def main(argv: list[str]) -> int: - clean_mode = None - if argv and argv[0] in ("--clean","--no-clean"): - clean_mode = argv[0] - argv = argv[1:] - if argv: - print(f"Usage: {Path(sys.argv[0]).name} [--clean|--no-clean]", file=sys.stderr) - return 2 - try: - return stage_for_ifaces(IFACES, clean_mode) - except (sqlite3.Error, FileNotFoundError, RuntimeError) as e: - print(f"❌ {e}", file=sys.stderr); return 1 - -if __name__ == "__main__": - sys.exit(main(sys.argv[1:])) diff --git a/developer/source/tunnel-client/mothball/stage_UID_routes.py b/developer/source/tunnel-client/mothball/stage_UID_routes.py deleted file mode 100755 index 7dfeb31..0000000 --- a/developer/source/tunnel-client/mothball/stage_UID_routes.py +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env python3 -# stage_UID_route.py — emit /usr/local/bin/set_subu_UID_route.sh from DB - -from __future__ import annotations -import sys, sqlite3, ipaddress -from pathlib import Path -import incommon as ic - -OUT = Path(__file__).resolve().parent / "stage" / "usr" / "local" / "bin" / "set_subu_UID_route.sh" - -def main(argv: list[str]) -> int: - try: - with ic.open_db() as conn: - rows = ic.rows(conn, """ - SELECT c.iface, c.rt_table_name_eff AS rtname, c.local_address_cidr, - ub.uid - FROM Iface c - LEFT JOIN user_binding ub ON ub.iface_id=c.id - ORDER BY c.iface, ub.uid; - """) - - meta = dict(ic.rows(conn, "SELECT key, value FROM meta;")) - subu_cidr = meta.get("subu_cidr", "10.0.0.0/24") - except (sqlite3.Error, FileNotFoundError) as e: - print(f"❌ {e}", file=sys.stderr); return 1 - - OUT.parent.mkdir(parents=True, exist_ok=True) - - lines = [] - lines.append("#!/usr/bin/env bash") - lines.append("# Set per-UID policy routing; idempotent.") - lines.append("set -euo pipefail") - lines.append('ensure(){ local n=\"$1\"; shift; if ! ip -4 rule list | grep -F -q -- \"$n\"; then ip -4 rule add \"$@\"; fi; }') - lines.append('ensureroute(){ local tbl=\"$1\"; shift; ip route replace \"$@\" table \"$tbl\"; }') - lines.append("") - - seen = set() - for iface, rtname, cidr, uid in rows: - if not iface: continue - try: src_ip = str(ipaddress.IPv4Interface(cidr).ip) - except: continue - # table name per UID (avoid rt_tables entries by using numeric if you prefer) - if uid is None: continue - tname = f"{rtname}_u{uid}" - key = (iface, uid) - if key in seen: continue - seen.add(key) - - # route: default via iface with pinned src - lines.append(f"# uid {uid} on {iface} → src {src_ip} via table {tname}") - lines.append(f'ensureroute "{tname}" default dev {iface} src {src_ip}') - lines.append(f'ensure "uidrange {uid}-{uid} lookup {tname}" uidrange "{uid}-{uid}" lookup "{tname}" pref 17010') - # symmetry guard for already-sourced packets - lines.append(f'ensure "from {src_ip}/32 lookup {tname}" from "{src_ip}/32" lookup "{tname}" pref 17000') - lines.append("") - - # global hard containment for subu space - lines.append(f'# hard containment for subu space {subu_cidr}') - lines.append(f'ensure "from {subu_cidr} prohibit" from "{subu_cidr}" prohibit pref 18050') - content = "\n".join(lines) + "\n" - OUT.write_text(content) - OUT.chmod(0o500) - print(f"staged: {OUT.relative_to(Path(__file__).resolve().parent)}") - return 0 - -if __name__ == "__main__": - sys.exit(main(sys.argv[1:])) diff --git a/developer/source/tunnel-client/mothball/stage_list_clients.py b/developer/source/tunnel-client/mothball/stage_list_clients.py deleted file mode 100755 index a36657a..0000000 --- a/developer/source/tunnel-client/mothball/stage_list_clients.py +++ /dev/null @@ -1,92 +0,0 @@ -#!/usr/bin/env python3 -# stage_list_client.py — emit one line per client with fields needed for staging -# Output format (pipe-separated, no header): -# id|iface|rt_table_name|rt_table_id|addr|priv|mtu|fwmark|dns_mode|dns_servers|autostart - -from __future__ import annotations -import sys, sqlite3 -from pathlib import Path - -def rows(conn: sqlite3.Connection, sql: str, params: tuple = ()) -> list[sqlite3.Row]: - conn.row_factory = sqlite3.Row - cur = conn.execute(sql, params) - return cur.fetchall() - -def list_client(db_path: Path) -> int: - try: - conn = sqlite3.connect(str(db_path)) - except sqlite3.Error as e: - print(f"❌ sqlite open failed: {e}", file=sys.stderr) - return 1 - - try: - # Prefer the view (effective rt_table_name); fall back to COALESCE if view missing. - try_sql = """ - SELECT c.id, - c.iface, - v.rt_table_name_eff AS rt_table_name, - COALESCE(c.rt_table_id, '') AS rt_table_id, - c.local_address_cidr AS addr, - c.private_key AS priv, - COALESCE(c.mtu, '') AS mtu, - COALESCE(c.fwmark, '') AS fwmark, - c.dns_mode, - COALESCE(c.dns_servers, '') AS dns_servers, - c.autostart - FROM Iface c - JOIN v_client_effective v ON v.id = c.id - ORDER BY c.id; - """ - try: - R = rows(conn, try_sql) - except sqlite3.Error: - # Fallback without the view - fallback_sql = """ - SELECT id, - iface, - COALESCE(rt_table_name, iface) AS rt_table_name, - COALESCE(rt_table_id, '') AS rt_table_id, - local_address_cidr AS addr, - private_key AS priv, - COALESCE(mtu, '') AS mtu, - COALESCE(fwmark, '') AS fwmark, - dns_mode, - COALESCE(dns_servers, '') AS dns_servers, - autostart - FROM Iface - ORDER BY id; - """ - R = rows(conn, fallback_sql) - - for r in R: - fields = [ - r["id"], - r["iface"], - r["rt_table_name"], - r["rt_table_id"], - r["addr"], - r["priv"], - r["mtu"], - r["fwmark"], - r["dns_mode"], - r["dns_servers"], - r["autostart"], - ] - print("|".join("" if v is None else str(v) for v in fields)) - return 0 - finally: - conn.close() - -def main(argv: list[str]) -> int: - if len(argv) != 1: - prog = Path(sys.argv[0]).name - print(f"Usage: {prog} /path/to/db", file=sys.stderr) - return 2 - db_path = Path(argv[0]) - if not db_path.exists(): - print(f"❌ DB not found: {db_path}", file=sys.stderr) - return 1 - return list_client(db_path) - -if __name__ == "__main__": - sys.exit(main(sys.argv[1:])) diff --git a/developer/source/tunnel-client/mothball/stage_list_uid.py b/developer/source/tunnel-client/mothball/stage_list_uid.py deleted file mode 100644 index 5acf312..0000000 --- a/developer/source/tunnel-client/mothball/stage_list_uid.py +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env python3 -# stage_list_uid.py — print Uid (one per line) bound to a iface_id - -from __future__ import annotations -import sys, sqlite3 -from pathlib import Path -import incommon as ic - -def list_uid(iface_id: int) -> int: - try: - with ic.open_db() as conn: - rows = conn.execute(""" - SELECT ub.uid - FROM user_binding ub - WHERE ub.iface_id=? AND ub.uid IS NOT NULL AND ub.uid!='' - ORDER BY ub.uid; - """,(iface_id,)).fetchall() - except (sqlite3.Error, FileNotFoundError) as e: - print(f"❌ {e}", file=sys.stderr); return 1 - for (uid,) in rows: - print(uid) - return 0 - -def main(argv): - if len(argv)!=1: - print(f"Usage: {Path(sys.argv[0]).name} ", file=sys.stderr) - return 2 - return list_uid(int(argv[0])) - -if __name__ == "__main__": - sys.exit(main(sys.argv[1:])) diff --git a/developer/source/tunnel-client/mothball/stage_populate.py b/developer/source/tunnel-client/mothball/stage_populate.py deleted file mode 100644 index bcb803a..0000000 --- a/developer/source/tunnel-client/mothball/stage_populate.py +++ /dev/null @@ -1,133 +0,0 @@ -#!/usr/bin/env python3 -# stage_populate.py — orchestrate stage generation (no business logic here) - -from __future__ import annotations -import sys, sqlite3, shutil -from pathlib import Path -import incommon as ic - -# imports of our freshly Pythonized helpers -import stage_clean as stclean -import stage_wg_conf as stconf -import stage_preferred_server as stpref -import stage_list_uids as stuids -import stage_IP_route_script as striproute -import stage_IP_rules_script as striprules -import stage_wg_unit_IP_scripts as stdrop - -def msg_wrapped_call(title: str, fn=None, *args, **kwargs): - print(f"→ {title}", flush=True) - res = fn(*args, **kwargs) if fn else None - print(f"✔ {title}" + (f": {res}" if res not in (None, "") else ""), flush=True) - return res - -def list_client(conn: sqlite3.Connection) -> list[sqlite3.Row]: - conn.row_factory = sqlite3.Row - try: - sql = """ - SELECT c.id, c.iface, v.rt_table_name_eff AS rtname, - COALESCE(c.rt_table_id,'') AS rtid, - c.local_address_cidr AS addr, - c.private_key AS priv, - COALESCE(c.mtu,'') AS mtu, - COALESCE(c.fwmark,'') AS fwmark, - c.dns_mode AS dns_mode, - COALESCE(c.dns_servers,'') AS dns_servers, - c.autostart AS autostart - FROM Iface c - JOIN v_client_effective v ON v.id=c.id - ORDER BY c.id; - """ - return list(conn.execute(sql)) - except sqlite3.Error: - # fallback if view missing - sql = """ - SELECT id, iface, COALESCE(rt_table_name,iface) AS rtname, - COALESCE(rt_table_id,'') AS rtid, - local_address_cidr AS addr, - private_key AS priv, - COALESCE(mtu,'') AS mtu, - COALESCE(fwmark,'') AS fwmark, - dns_mode, COALESCE(dns_servers,'') AS dns_servers, - autostart - FROM Iface ORDER BY id; - """ - return list(conn.execute(sql)) - -def stage_populate(clean_mode: str | None) -> int: - # 0) clean stage - if clean_mode == "--clean": - msg_wrapped_call("stage clean (--yes)", stclean.clean, yes=True, dry_run=False, hard=False) - elif clean_mode == "--no-clean": - Path(stclean.stage_root()).mkdir(parents=True, exist_ok=True) - else: - # interactive prompt like original - msg_wrapped_call("stage clean (interactive)", stclean.clean, yes=False, dry_run=False, hard=False) - - # base dirs - root = Path(__file__).resolve().parent - stage_root = root / "stage" - (stage_root / "wireguard").mkdir(parents=True, exist_ok=True) - (stage_root / "systemd").mkdir(parents=True, exist_ok=True) - (stage_root / "usr" / "local" / "bin").mkdir(parents=True, exist_ok=True) - - # 1) optional helper copy - ip_rule_add = root / "IP_rule_add_UID.sh" - if ip_rule_add.exists(): - dst = stage_root / "usr" / "local" / "bin" / "IP_rule_add_UID.sh" - shutil.copy2(ip_rule_add, dst) - dst.chmod(0o500) - print(f"staged: {dst.relative_to(root)}") - - # 2) stage global policy script once (replaces per-iface policy_init_*.sh) - msg_wrapped_call("stage global set_subu_IP_rules.sh", striprules.stage_set_subu_ip_rules) - - # 3) per-client staging - with ic.open_db() as conn: - for r in list_client(conn): - cid = int(r["id"]); iface = str(r["iface"]) - rt = str(r["rtname"]) - addr = str(r["addr"]); priv = str(r["priv"]) - mtu = str(r["mtu"]); fw = str(r["fwmark"]) - dns_m = str(r["dns_mode"]); dns_s = str(r["dns_servers"]) - - # 3a) preferred server - srow = stpref.preferred_server_row(cid) - if not srow: - print(f"⚠️ No server for client '{iface}' (id={cid}). Skipping.") - continue - (s_name, s_pub, s_psk, s_host, s_port, s_allow, s_ka, s_route) = srow - - # 3b) WG conf - conf_out = stage_root / "wireguard" / f"{iface}.conf" - msg_wrapped_call(f"wg conf for {iface}", - stconf.write_wg_conf, conf_out, addr, priv, mtu, fw, dns_m, dns_s, - s_pub, s_psk, s_host, str(s_port), s_allow, str(s_ka or "") - ) - - # 3c) route init script - msg_wrapped_call(f"route_init for {iface}", striproute.stage_ip_route_script, iface) - - # 3d) systemd override referencing global rules + per-iface route - msg_wrapped_call(f"wg-quick override for {iface}", stdrop.stage_dropin, iface) - - print(f"✔ Staged: {iface}") - - print(f"✅ Stage generation complete in: {stage_root}") - return 0 - -def main(argv): - clean_mode = None - if argv: - if argv[0] in ("--clean","--no-clean"): - clean_mode = argv[0] - argv = argv[1:] - if argv: - print(f"Usage: {Path(sys.argv[0]).name} [--clean|--no-clean]", file=sys.stderr); return 2 - try: - return stage_populate(clean_mode) - except (sqlite3.Error, FileNotFoundError, RuntimeError) as e: - print(f"❌ {e}", file=sys.stderr); return 1 - -if __name__ == "__main__": - sys.exit(main(sys.argv[1:])) diff --git a/developer/source/tunnel-client/mothball/stage_preferred_server.py b/developer/source/tunnel-client/mothball/stage_preferred_server.py deleted file mode 100644 index 8e39d4d..0000000 --- a/developer/source/tunnel-client/mothball/stage_preferred_server.py +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env python3 -# stage_preferred_server.py — emit the preferred server row for a iface_id -# Output: name|peer_pub|psk|endpoint_host|endpoint_port|allowed_ips|keepalive|route_allowed_ips - -from __future__ import annotations -import sys, sqlite3 -from pathlib import Path -import incommon as ic - -def preferred_server_row(iface_id: int) -> tuple | None: - with ic.open_db() as conn: - r = conn.execute(""" - SELECT name, public_key, COALESCE(preshared_key,''), - endpoint_host, endpoint_port, allowed_ips, - COALESCE(keepalive_s,''), route_allowed_ips - FROM server - WHERE iface_id=? - ORDER BY priority ASC, id ASC - LIMIT 1; - """,(iface_id,)).fetchone() - return tuple(r) if r else None - -def main(argv): - if len(argv)!=1: - print(f"Usage: {Path(sys.argv[0]).name} ", file=sys.stderr) - return 2 - row = preferred_server_row(int(argv[0])) - if not row: - # empty stdout on "no server" just like the shell version - return 0 - print("|".join("" if v is None else str(v) for v in row)) - return 0 - -if __name__ == "__main__": - sys.exit(main(sys.argv[1:])) diff --git a/developer/source/tunnel-client/mothball/stage_wg_conf.py b/developer/source/tunnel-client/mothball/stage_wg_conf.py deleted file mode 100644 index a395db2..0000000 --- a/developer/source/tunnel-client/mothball/stage_wg_conf.py +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env python3 -# stage_wg_conf.py — write stage/wireguard/.conf - -from __future__ import annotations -import sys, argparse -from pathlib import Path - -def write_wg_conf(out: Path, addr: str, priv: str, mtu: str, fwmark: str, - dns_mode: str, dns_servers: str, - peer_pub: str, psk: str, host: str, port: str, - allowed: str, keepalive: str) -> Path: - out.parent.mkdir(parents=True, exist_ok=True) - lines = [ - "[Interface]", - f"Address = {addr}", - f"PrivateKey = {priv}", - ] - if mtu: lines.append(f"MTU = {mtu}") - if fwmark: lines.append(f"FwMark = {fwmark}") - if dns_mode == "static" and dns_servers: - lines.append(f"DNS = {dns_servers}") - lines.append("Table = off") # policy routing handled outside wg-quick - lines += [ - "", - "[Peer]", - f"PublicKey = {peer_pub}", - ] - if psk: lines.append(f"PresharedKey = {psk}") - lines += [ - f"Endpoint = {host}:{port}", - f"AllowedIPs = {allowed}", - ] - if keepalive: lines.append(f"PersistentKeepalive = {keepalive}") - - out.write_text("\n".join(lines) + "\n") - out.chmod(0o400) - return out - -def main(argv): - ap = argparse.ArgumentParser() - ap.add_argument("out"); ap.add_argument("addr"); ap.add_argument("priv") - ap.add_argument("mtu"); ap.add_argument("fwmark") - ap.add_argument("dns_mode"); ap.add_argument("dns_servers") - ap.add_argument("peer_pub"); ap.add_argument("psk") - ap.add_argument("host"); ap.add_argument("port") - ap.add_argument("allowed"); ap.add_argument("keepalive") - args = ap.parse_args(argv) - out = write_wg_conf(Path(args.out), args.addr, args.priv, args.mtu, args.fwmark, - args.dns_mode, args.dns_servers, args.peer_pub, args.psk, - args.host, args.port, args.allowed, args.keepalive) - print(f"staged: {out}") - return 0 - -if __name__ == "__main__": - sys.exit(main(sys.argv[1:])) diff --git a/developer/source/tunnel-client/mothball/stage_wg_unit_IP_scripts.py b/developer/source/tunnel-client/mothball/stage_wg_unit_IP_scripts.py deleted file mode 100644 index cef5abb..0000000 --- a/developer/source/tunnel-client/mothball/stage_wg_unit_IP_scripts.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env python3 -# stage_wg_unit_IP_scripts.py — write systemd unit override for wg-quick@IFACE - -from __future__ import annotations -import sys -from pathlib import Path - -def stage_dropin(iface: str) -> Path: - root = Path(__file__).resolve().parent - stage_root = root / "stage" - dropin_dir = stage_root / "etc" / "systemd" / f"wg-quick@{iface}.service.d" - dropin_dir.mkdir(parents=True, exist_ok=True) - conf = dropin_dir / "10-postup-IP-scripts.conf" - conf.write_text( - "[Service]\n" - "Restart=on-failure\n" - "RestartSec=5\n" - f"ExecStartPre=-/usr/sbin/ip link delete {iface}\n" - f"ExecStartPost=+/usr/local/bin/set_subu_IP_rules.sh\n" - f"ExecStartPost=+/usr/local/bin/route_init_{iface}.sh\n" - f"ExecStartPost=+/usr/bin/logger 'wg-quick@{iface} up: rules+route applied'\n" - ) - return conf - -def main(argv): - if len(argv)!=1: - print(f"Usage: {Path(sys.argv[0]).name} ", file=sys.stderr); return 2 - p = stage_dropin(argv[0]) - # print a "stage/..." relative path for consistency - root = Path(__file__).resolve().parent - rel = p.as_posix().replace(root.as_posix() + "/", "") - print(f"staged: {rel}") - return 0 - -if __name__ == "__main__": - sys.exit(main(sys.argv[1:])) diff --git a/developer/source/tunnel-client/mothball/stage_wipe.py b/developer/source/tunnel-client/mothball/stage_wipe.py deleted file mode 100755 index 161ab79..0000000 --- a/developer/source/tunnel-client/mothball/stage_wipe.py +++ /dev/null @@ -1,97 +0,0 @@ -#!/usr/bin/env python3 -# stage_wipe.py — safely wipe ./stage contents (keeps hidden files by default) -# Usage: -# ./stage_wipe.py [--yes] [--dry-run] [--hard] -# -# Notes: -# - Default (no --hard): removes ONLY non-hidden entries in ./stage, keeps dotfiles like .gitignore. -# - --hard: removes the stage directory itself (this will remove hidden files as well), then recreates it. - -from __future__ import annotations -import argparse, shutil, sys, subprocess -from pathlib import Path - -def stage_root() -> Path: - return Path(__file__).resolve().parent / "stage" - -def human_count_and_size(p: Path) -> tuple[int, str]: - try: - count = sum(1 for _ in p.rglob("*")) - except Exception: - count = 0 - try: - cp = subprocess.run(["du", "-sh", p.as_posix()], text=True, capture_output=True) - size = cp.stdout.split()[0] if cp.returncode == 0 and cp.stdout else "?" - except Exception: - size = "?" - return count, size - -def wipe(yes: bool, dry_run: bool, hard: bool) -> int: - st = stage_root() - if not st.exists(): - print(f"Nothing to wipe: {st} does not exist.") - return 0 - - # Path safety guard - safe_root = Path(__file__).resolve().parent / "stage" - if st.resolve() != safe_root.resolve(): - print(f"Refusing: STAGE path looks unsafe: {st}", file=sys.stderr) - return 1 - - count, size = human_count_and_size(st) - - if dry_run: - if hard: - print(f"DRY RUN — would remove the entire directory: {st} (items: {count}, size: ~{size})") - else: - print(f"DRY RUN — would remove NON-HIDDEN contents of: {st} (items: {count}, size: ~{size})") - for p in sorted(st.iterdir()): - if not p.name.startswith('.'): - print(" " + p.as_posix()) - return 0 - - if not yes: - prompt = f"Permanently delete {'ALL of ' if hard else 'non-hidden entries in '}{st} (items: {count}, size: ~{size})? [y/N] " - try: - ans = input(prompt).strip() - except EOFError: - ans = "" - if ans.lower() not in ("y", "yes"): - print("Aborted.") - return 0 - - if hard: - # Remove entire stage directory (hidden files included), then recreate it - try: - shutil.rmtree(st) - print(f"Removed stage dir: {st}") - except Exception as e: - print(f"WARN: rmtree failed: {e}", file=sys.stderr) - st.mkdir(parents=True, exist_ok=True) - else: - # Remove only non-hidden entries; keep dotfiles like .gitignore - for p in list(st.iterdir()): - if p.name.startswith('.'): - continue # preserve hidden files/dirs - try: - if p.is_dir(): - shutil.rmtree(p) - else: - p.unlink(missing_ok=True) - except Exception as e: - print(f"WARN: failed to remove {p}: {e}", file=sys.stderr) - print(f"Cleared non-hidden contents of: {st}") - - print("✅ Done.") - return 0 - -def main(argv): - ap = argparse.ArgumentParser(description="Wipe the stage directory (keeps hidden files unless --hard).") - ap.add_argument("--yes", action="store_true", help="do not prompt") - ap.add_argument("--dry-run", action="store_true", help="show what would be removed, then exit") - ap.add_argument("--hard", action="store_true", help="remove the stage dir itself (also removes hidden files)") - args = ap.parse_args(argv) - return wipe(args.yes, args.dry_run, args.hard) - -if __name__ == "__main__": - sys.exit(main(sys.argv[1:]))