From c7e00fb722a5dee41400ebe69dfcd55210d20059 Mon Sep 17 00:00:00 2001 From: Thomas Walker Lynch Date: Tue, 4 Nov 2025 14:48:47 +0000 Subject: [PATCH] 0.3.2 --- {document => developer/document}/manager.org | 0 developer/document/notes.txt | 28 ++ developer/manager.tgz | Bin 20863 -> 0 bytes developer/manager/CLI.py | 250 +++++++++++++----- developer/manager/core.py | 22 -- developer/manager/dispatch.py | 167 ++++++++++++ developer/manager/{ => domain}/exec.py | 6 + developer/manager/{ => domain}/network.py | 11 +- developer/manager/{ => domain}/options.py | 8 + developer/manager/{ => domain}/subu.py | 96 +++++-- developer/manager/{ => domain}/wg.py | 14 +- developer/manager/{ => infrastructure}/bpf.py | 12 +- .../{ => infrastructure}/bpf_force_egress.c | 7 +- .../{ => infrastructure}/bpf_worker.py | 10 +- developer/manager/{ => infrastructure}/db.py | 58 +++- .../manager/{ => infrastructure}/schema.sql | 6 + .../manager/{ => infrastructure}/unix.py | 13 + developer/manager/subu.db | Bin 0 -> 12288 bytes developer/manager/text.py | 201 +++++++------- .../manager/{ => uncatelogued}/parser.py | 2 +- developer/manager/version.py | 2 + 21 files changed, 686 insertions(+), 227 deletions(-) rename {document => developer/document}/manager.org (100%) create mode 100644 developer/document/notes.txt delete mode 100644 developer/manager.tgz delete mode 100644 developer/manager/core.py create mode 100644 developer/manager/dispatch.py rename developer/manager/{ => domain}/exec.py (63%) rename developer/manager/{ => domain}/network.py (72%) rename developer/manager/{ => domain}/options.py (77%) rename developer/manager/{ => domain}/subu.py (68%) rename developer/manager/{ => domain}/wg.py (81%) rename developer/manager/{ => infrastructure}/bpf.py (87%) rename developer/manager/{ => infrastructure}/bpf_force_egress.c (94%) rename developer/manager/{ => infrastructure}/bpf_worker.py (92%) rename developer/manager/{ => infrastructure}/db.py (54%) rename developer/manager/{ => infrastructure}/schema.sql (87%) rename developer/manager/{ => infrastructure}/unix.py (70%) create mode 100644 developer/manager/subu.db rename developer/manager/{ => uncatelogued}/parser.py (97%) create mode 100644 developer/manager/version.py diff --git a/document/manager.org b/developer/document/manager.org similarity index 100% rename from document/manager.org rename to developer/document/manager.org diff --git a/developer/document/notes.txt b/developer/document/notes.txt new file mode 100644 index 0000000..ed89643 --- /dev/null +++ b/developer/document/notes.txt @@ -0,0 +1,28 @@ +Domain modules + +subu.py + +wg.py + +network.py + +options.py + +exec.py (as in “execute command in subu”) + + +Infrastructure modules + +db.py + +unix.py + +bpf.py + +bpf_worker.py + +bpf_force_egress.c + +schema.sql + +parser.py (either merged into CLI.py or deleted, see below) diff --git a/developer/manager.tgz b/developer/manager.tgz deleted file mode 100644 index f802d65b34563253940ebedc290c07c3bf6e4532..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20863 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 a`n3h;<+FU2&+_?;KmP~cXRa6kKmh int: - argv = argv or sys.argv[1:] - if not argv: - print(USAGE) - return 0 - # simple verbs that bypass argparse (so `help/version/example` always work) - simple = {"help": HELP, "--help": HELP, "-h": HELP, "usage": USAGE, "example": EXAMPLE, "version": VERSION} - if argv[0] in simple: - out = simple[argv[0]] - print(out if isinstance(out, str) else out()) - return 0 +def build_arg_parser(program_name): + """ + Build the top level argument parser for the subu manager. + """ + parser = argparse.ArgumentParser(prog=program_name, add_help=False) + parser.add_argument("-V","--Version", action="store_true", help="print version") + + subparsers = parser.add_subparsers(dest="verb") - p = argparse.ArgumentParser(prog="subu", add_help=False) - p.add_argument("-V", "--Version", action="store_true", help="print version") - sub = p.add_subparsers(dest="verb") + register_subu_commands(subparsers) + register_wireguard_commands(subparsers) + register_attach_commands(subparsers) + register_network_commands(subparsers) + register_option_commands(subparsers) + register_exec_commands(subparsers) + return parser + + +def register_subu_commands(subparsers): + """ + Register subu related commands: + init, make, list, info, information, lo + """ # init - ap = sub.add_parser("init") + ap = subparsers.add_parser("init") ap.add_argument("token", nargs="?") - # create/list/info - ap = sub.add_parser("create") + # make + ap = subparsers.add_parser("make") ap.add_argument("owner") ap.add_argument("name") - sub.add_parser("list") - ap = sub.add_parser("info"); ap.add_argument("subu_id") - ap = sub.add_parser("information"); ap.add_argument("subu_id") + # list + subparsers.add_parser("list") + + # info / information + ap = subparsers.add_parser("info") + ap.add_argument("subu_id") + ap = subparsers.add_parser("information") + ap.add_argument("subu_id") # lo - ap = sub.add_parser("lo") + ap = subparsers.add_parser("lo") ap.add_argument("state", choices=["up","down"]) ap.add_argument("subu_id") - # WG - ap = sub.add_parser("WG") - ap.add_argument("verb", choices=["global","create","server_provided_public_key","info","information","up","down"]) + +def register_wireguard_commands(subparsers): + """ + Register WireGuard related commands, grouped under 'WG': + WG global + WG make + WG server_provided_public_key + WG info|information + WG up|down + """ + ap = subparsers.add_parser("WG") + ap.add_argument( + "wg_verb", + choices=[ + "global", + "make", + "server_provided_public_key", + "info", + "information", + "up", + "down", + ], + ) ap.add_argument("arg1", nargs="?") ap.add_argument("arg2", nargs="?") - # attach/detach - ap = sub.add_parser("attach") + +def register_attach_commands(subparsers): + """ + Register attach and detach commands: + attach WG + detach WG + """ + ap = subparsers.add_parser("attach") ap.add_argument("what", choices=["WG"]) ap.add_argument("subu_id") ap.add_argument("wg_id") - ap = sub.add_parser("detach") + ap = subparsers.add_parser("detach") ap.add_argument("what", choices=["WG"]) ap.add_argument("subu_id") - # network - ap = sub.add_parser("network") + +def register_network_commands(subparsers): + """ + Register network aggregate commands: + network up|down + """ + ap = subparsers.add_parser("network") ap.add_argument("state", choices=["up","down"]) ap.add_argument("subu_id") - # option - ap = sub.add_parser("option") - ap.add_argument("verb", choices=["set","get","list"]) + +def register_option_commands(subparsers): + """ + Register option commands: + option set|get|list ... + """ + ap = subparsers.add_parser("option") + ap.add_argument("action", choices=["set","get","list"]) ap.add_argument("subu_id") ap.add_argument("name", nargs="?") ap.add_argument("value", nargs="?") - # exec - ap = sub.add_parser("exec") + +def register_exec_commands(subparsers): + """ + Register exec command: + exec -- ... + """ + ap = subparsers.add_parser("exec") ap.add_argument("subu_id") + # Use a dedicated "--" argument so that: + # subu exec subu_7 -- curl -4v https://ifconfig.me + # works as before. ap.add_argument("--", dest="cmd", nargs=argparse.REMAINDER, default=[]) - ns = p.parse_args(argv) - if ns.Version: - print(VERSION); return 0 + +def CLI(argv=None) -> int: + """ + Top level entry point for the subu manager CLI. + """ + if argv is None: + argv = sys.argv[1:] + + # For now we fix the program name to "subu". + # A release wrapper can later pass a different program name. + program_name = "subu" + text = make_text(program_name) + + # No arguments is the same as "help". + if not argv: + print(text.help(), end="") + return 0 + + # Simple verbs that bypass argparse so they always work. + simple = { + "help": text.help, + "--help": text.help, + "-h": text.help, + "usage": text.usage, + "example": text.example, + "version": text.version, + } + if argv[0] in simple: + print(simple[argv[0]](), end="") + return 0 + + parser = build_arg_parser(program_name) + ns = parser.parse_args(argv) + + if getattr(ns, "Version", False): + print(text.version(), end="") + return 0 try: if ns.verb == "init": - return core.cmd_init(ns.token) + return dispatch.init(ns.token) + + if ns.verb == "make": + return dispatch.subu_make(ns.owner, ns.name) - if ns.verb == "create": - core.create_subu(ns.owner, ns.name); return 0 if ns.verb == "list": - core.list_subu(); return 0 + return dispatch.subu_list() + if ns.verb in ("info","information"): - core.info_subu(ns.subu_id); return 0 + return dispatch.subu_info(ns.subu_id) if ns.verb == "lo": - core.lo_toggle(ns.subu_id, ns.state); return 0 + return dispatch.lo_toggle(ns.subu_id, ns.state) if ns.verb == "WG": - v = ns.verb - if ns.arg1 is None and v in ("info","information"): - print("WG info requires WG_ID"); return 2 + v = ns.wg_verb + if v in ("info","information") and ns.arg1 is None: + print("WG info requires WG_ID", file=sys.stderr) + return 2 if v == "global": - core.wg_global(ns.arg1); return 0 - if v == "create": - wid = core.wg_create(ns.arg1); print(wid); return 0 + return dispatch.wg_global(ns.arg1) + if v == "make": + return dispatch.wg_make(ns.arg1) if v == "server_provided_public_key": - core.wg_set_pubkey(ns.arg1, ns.arg2); return 0 + return dispatch.wg_server_public_key(ns.arg1, ns.arg2) if v in ("info","information"): - core.wg_info(ns.arg1); return 0 + return dispatch.wg_info(ns.arg1) if v == "up": - core.wg_up(ns.arg1); return 0 + return dispatch.wg_up(ns.arg1) if v == "down": - core.wg_down(ns.arg1); return 0 + return dispatch.wg_down(ns.arg1) if ns.verb == "attach": if ns.what == "WG": - core.attach_wg(ns.subu_id, ns.wg_id); return 0 + return dispatch.attach_wg(ns.subu_id, ns.wg_id) if ns.verb == "detach": if ns.what == "WG": - core.detach_wg(ns.subu_id); return 0 + return dispatch.detach_wg(ns.subu_id) if ns.verb == "network": - core.network_toggle(ns.subu_id, ns.state); return 0 + return dispatch.network_toggle(ns.subu_id, ns.state) if ns.verb == "option": - if ns.verb == "option" and ns.name is None and ns.value is None and ns.verb == "list": - core.option_list(ns.subu_id); return 0 - if ns.verb == "set": - core.option_set(ns.subu_id, ns.name, ns.value); return 0 - if ns.verb == "get": - core.option_get(ns.subu_id, ns.name); return 0 - if ns.verb == "list": - core.option_list(ns.subu_id); return 0 + if ns.action == "set": + return dispatch.option_set(ns.subu_id, ns.name, ns.value) + if ns.action == "get": + return dispatch.option_get(ns.subu_id, ns.name) + if ns.action == "list": + return dispatch.option_list(ns.subu_id) if ns.verb == "exec": if not ns.cmd: - print("subu exec -- ..."); return 2 - core.exec_in_subu(ns.subu_id, ns.cmd); return 0 + print(f"{program_name} exec -- ...", file=sys.stderr) + return 2 + return dispatch.exec(ns.subu_id, ns.cmd) + + # If we reach here, the verb was not recognised. + print(text.usage(), end="") + return 2 - print(USAGE); return 2 except Exception as e: - print(f"error: {e}") + print(f"error: {e}", file=sys.stderr) return 1 + if __name__ == "__main__": sys.exit(CLI()) diff --git a/developer/manager/core.py b/developer/manager/core.py deleted file mode 100644 index f66cdf9..0000000 --- a/developer/manager/core.py +++ /dev/null @@ -1,22 +0,0 @@ -# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*- -""" -core.py — worker API for subu manager -Version: 0.2.0 -""" -import os, sqlite3, subprocess -from pathlib import Path -from contextlib import closing -from text import VERSION -from worker_bpf import ensure_mounts, install_steering, remove_steering, BpfError -import db - -DB_FILE = Path("./subu.db") -WG_GLOBAL_FILE = Path("./WG_GLOBAL") - -def run(cmd, check=True): - r = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) - if check and r.returncode != 0: - raise RuntimeError(f"cmd failed: {' '.join(cmd)}\n{r.stderr}") - return r.stdout.strip() - - diff --git a/developer/manager/dispatch.py b/developer/manager/dispatch.py new file mode 100644 index 0000000..d0cce3c --- /dev/null +++ b/developer/manager/dispatch.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 +# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*- +""" +dispatch.py + +Role: provide one function for each CLI verb so that: + + * CLI.py can call these functions + * Other Python code can also import and call them directly + +Each function should return an integer status code where practical. + +Implementation note: + + At this stage of the refactor, the functions are stubs. They define the + public interface and may raise NotImplementedError. As the domain modules + under domain/ are completed (subu.py, wg.py, network.py, options.py, + exec.py), these functions should be updated to call into those modules. +""" +# dispatch.py +from domain import subu as subu_domain + +def init(token=None): + """ + Initialize ./subu.db using schema.sql + token is currently unused but kept for CLI compatibility. + """ + # open_db + ensure_schema via domain layer convenience + from infrastructure.db import open_db, ensure_schema + conn = open_db("subu.db") + try: + ensure_schema(conn) + return 0 + finally: + conn.close() + + +def subu_make(owner, name): + try: + s = subu_domain.make_subu(owner, name) + # print the made ID or username like your older CLI did + print(f"made subu: id={s.id} username={s.username}") + return 0 + except Exception as e: + print(f"error creating subu: {e}", file=sys.stderr) + return 1 + + +def subu_list(): + try: + subs = subu_domain.list_subu() + if not subs: + print("no subu found") + return 0 + # simple table + print("ID OWNER NAME USERNAME CREATED_AT") + for s in subs: + print(f"{s.id} {s.owner} {s.name} {s.username} {s.made_at}") + return 0 + except Exception as e: + print(f"error listing subu: {e}", file=sys.stderr) + return 1 + + +def subu_info(subu_id): + """ + Handle: subu info|information + """ + raise NotImplementedError("subu_info is not yet implemented") + + +def lo_toggle(subu_id, state): + """ + Handle: subu lo up|down + """ + raise NotImplementedError("lo_toggle is not yet implemented") + + +def wg_global(base_cidr): + """ + Handle: subu WG global + """ + raise NotImplementedError("wg_global is not yet implemented") + + +def wg_make(endpoint): + """ + Handle: subu WG make + """ + raise NotImplementedError("wg_make is not yet implemented") + + +def wg_server_public_key(wg_id, key): + """ + Handle: subu WG server_provided_public_key + """ + raise NotImplementedError("wg_server_public_key is not yet implemented") + + +def wg_info(wg_id): + """ + Handle: subu WG info|information + """ + raise NotImplementedError("wg_info is not yet implemented") + + +def wg_up(wg_id): + """ + Handle: subu WG up + """ + raise NotImplementedError("wg_up is not yet implemented") + + +def wg_down(wg_id): + """ + Handle: subu WG down + """ + raise NotImplementedError("wg_down is not yet implemented") + + +def attach_wg(subu_id, wg_id): + """ + Handle: subu attach WG + """ + raise NotImplementedError("attach_wg is not yet implemented") + + +def detach_wg(subu_id): + """ + Handle: subu detach WG + """ + raise NotImplementedError("detach_wg is not yet implemented") + + +def network_toggle(subu_id, state): + """ + Handle: subu network up|down + """ + raise NotImplementedError("network_toggle is not yet implemented") + + +def option_set(subu_id, name, value): + """ + Handle: subu option set + """ + raise NotImplementedError("option_set is not yet implemented") + + +def option_get(subu_id, name): + """ + Handle: subu option get + """ + raise NotImplementedError("option_get is not yet implemented") + + +def option_list(subu_id): + """ + Handle: subu option list + """ + raise NotImplementedError("option_list is not yet implemented") + + +def exec(subu_id, cmd_argv): + """ + Handle: subu exec -- ... + """ + raise NotImplementedError("exec is not yet implemented") diff --git a/developer/manager/exec.py b/developer/manager/domain/exec.py similarity index 63% rename from developer/manager/exec.py rename to developer/manager/domain/exec.py index f823d9a..0826560 100644 --- a/developer/manager/exec.py +++ b/developer/manager/domain/exec.py @@ -1,4 +1,10 @@ +""" +4.5 domain/exec.py +Run a command inside a subu’s namespace and UID. + +4.5.1 run_in_subu(subu: Subu, cmd_argv: list[str]) -> int +""" def exec_in_subu(subu_id: str, cmd: list): sid = int(subu_id.split("_")[1]) with closing(_db()) as db: diff --git a/developer/manager/network.py b/developer/manager/domain/network.py similarity index 72% rename from developer/manager/network.py rename to developer/manager/domain/network.py index 000bbf8..2ea77b4 100644 --- a/developer/manager/network.py +++ b/developer/manager/domain/network.py @@ -1,4 +1,13 @@ +""" +4.3 domain/network.py +Netns + device wiring, including aggregate “network up/down”. + +4.3.1 lo_toggle(subu: Subu, state: str) -> None +4.3.2 attach_wg(subu: Subu, wg: WG) -> None +4.3.3 detach_wg(subu: Subu) -> None +4.3.4 network_toggle(subu: Subu, state: str) -> None +""" def network_toggle(subu_id: str, state: str): sid = int(subu_id.split("_")[1]) with closing(_db()) as db: @@ -14,7 +23,7 @@ def network_toggle(subu_id: str, state: str): db.commit() print(f"{subu_id}: network {state}") -def _create_netns_for_subu(subu_id_num: int, netns_name: str): +def _make_netns_for_subu(subu_id_num: int, netns_name: str): """ Create the network namespace & bring lo down. """ diff --git a/developer/manager/options.py b/developer/manager/domain/options.py similarity index 77% rename from developer/manager/options.py rename to developer/manager/domain/options.py index 76b5caa..76fd97e 100644 --- a/developer/manager/options.py +++ b/developer/manager/domain/options.py @@ -1,4 +1,12 @@ +""" +4.4 domain/options.py +Per-subu options, backed by DB. + +4.4.1 set_option(subu_id: str, name: str, value: str) -> None +4.4.2 get_option(subu_id: str, name: str) -> str | None +4.4.3 list_options(subu_id: str) -> dict[str, str] +""" def option_set(subu_id: str, name: str, value: str): sid = int(subu_id.split("_")[1]) with closing(_db()) as db: diff --git a/developer/manager/subu.py b/developer/manager/domain/subu.py similarity index 68% rename from developer/manager/subu.py rename to developer/manager/domain/subu.py index ea5ad0c..7f77f86 100644 --- a/developer/manager/subu.py +++ b/developer/manager/domain/subu.py @@ -1,25 +1,75 @@ -# ------------- Subu ops ------------- -def create_subu(owner: str, name: str) -> str: - with closing(_db()) as db: - c = db.cursor() - subu_netns = f"ns-subu_tmp" # temp; we rename after ID known - c.execute("INSERT INTO subu (owner, name, netns) VALUES (?, ?, ?)", - (owner, name, subu_netns)) - sid = c.lastrowid - netns = f"ns-subu_{sid}" - c.execute("UPDATE subu SET netns=? WHERE id=?", (netns, sid)) - db.commit() +""" +4.1 domain/subu.py + +Subu objects: creation, lookup, hierarchy, netns identity. + +4.1.1 make_subu(owner: str, name: str) -> Subu +4.1.2 list_subu() -> list[Subu] +4.1.3 get_subu(subu_id: str) -> Subu +4.1.4 ensure_unix_identity(subu: Subu) -> None +4.1.5 ensure_netns(subu: Subu) -> None + +(A Subu can be a dataclass or NamedTuple.) +""" + +# domain/subu.py +from dataclasses import dataclass +from infrastructure.db import open_db, ensure_schema +import sqlite3 +import time + +DB_PATH = "subu.db" + + +@dataclass +class Subu: + id: int + owner: str + name: str + username: str + made_at: str + + +def _make_username(owner, name): + # simple deterministic username: owner_name -> owner_name (no spaces) + owner_s = owner.replace(" ", "_") + name_s = name.replace(" ", "_") + return f"{owner_s}_{name_s}" + + +def make_subu(owner: str, name: str) -> Subu: + """ + Create a subu row in subu.db and return the Subu dataclass. + """ + conn = open_db(DB_PATH) + try: + ensure_schema(conn) + cur = conn.cursor() + username = _make_username(owner, name) + made_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) + cur.execute( + "INSERT INTO subu (owner, name, username, made_at) VALUES (?, ?, ?, ?)", + (owner, name, username, made_at), + ) + conn.commit() + rowid = cur.lastrowid + row = conn.execute("SELECT id, owner, name, username, made_at FROM subu WHERE id = ?", (rowid,)).fetchone() + return Subu(row["id"], row["owner"], row["name"], row["username"], row["made_at"]) + finally: + conn.close() - # create netns - run(["ip", "netns", "add", netns]) - run(["ip", "-n", netns, "link", "set", "lo", "down"]) - print(f"Created subu_{sid} ({owner}:{name}) with netns {netns}") - return f"subu_{sid}" def list_subu(): - with closing(_db()) as db: - for row in db.execute("SELECT id, owner, name, netns, lo_state, wg_id, network_state FROM subu"): - print(row) + """ + Return a list of Subu objects currently in the DB. + """ + conn = open_db(DB_PATH) + try: + ensure_schema(conn) + rows = conn.execute("SELECT id, owner, name, username, made_at FROM subu ORDER BY id").fetchall() + return [Subu(r["id"], r["owner"], r["name"], r["username"], r["made_at"]) for r in rows] + finally: + conn.close() def info_subu(subu_id: str): sid = int(subu_id.split("_")[1]) @@ -68,8 +118,8 @@ def make_subu(path_tokens: list[str]) -> str: Side effects: - DB row in 'subu' (id, owner, name, full_unix_name, path, netns_name, ...) - - netns ns-subu_ created with lo down - - Unix user created/ensured + - netns ns-subu_ made with lo down + - Unix user made/ensured - Unix groups ensured and membership updated Returns: textual Subu_ID, e.g. 'subu_7'. @@ -120,7 +170,7 @@ def make_subu(path_tokens: list[str]) -> str: netns_name = f"ns-subu_{subu_id_num}" db.execute( - "INSERT INTO subu(id, owner, name, full_unix_name, path, netns_name, wg_id, created_at, updated_at) " + "INSERT INTO subu(id, owner, name, full_unix_name, path, netns_name, wg_id, made_at, updated_at) " "VALUES (?, ?, ?, ?, ?, ?, NULL, datetime('now'), datetime('now'))", (subu_id_num, masu, leaf, full_unix_name, path_str, netns_name) ) @@ -129,7 +179,7 @@ def make_subu(path_tokens: list[str]) -> str: subu_id = f"subu_{subu_id_num}" # 3) Create netns + lo down - _create_netns_for_subu(subu_id_num, netns_name) + _make_netns_for_subu(subu_id_num, netns_name) # 4) Ensure Unix user + groups diff --git a/developer/manager/wg.py b/developer/manager/domain/wg.py similarity index 81% rename from developer/manager/wg.py rename to developer/manager/domain/wg.py index 3be049c..0cf6126 100644 --- a/developer/manager/wg.py +++ b/developer/manager/domain/wg.py @@ -1,3 +1,15 @@ +""" +4.2 domain/wg.py + +WireGuard objects, independent of subu. + +4.2.1 set_global_pool(base_cidr: str) -> None +4.2.2 make_wg(endpoint: str) -> WG +4.2.3 set_server_public_key(wg_id: str, key: str) -> None +4.2.4 get_wg(wg_id: str) -> WG +4.2.5 bring_up(wg_id: str) -> None +4.2.6 bring_down(wg_id: str) -> None +""" def wg_global(basecidr: str): WG_GLOBAL_FILE.write_text(basecidr.strip()+"\n") @@ -9,7 +21,7 @@ def _alloc_ip(idx: int, base: str) -> str: host = 2 + idx return f"{prefix}.{host}/32" -def wg_create(endpoint: str) -> str: +def wg_make(endpoint: str) -> str: if not WG_GLOBAL_FILE.exists(): raise RuntimeError("set WG base with `subu WG global ` first") base = WG_GLOBAL_FILE.read_text().strip() diff --git a/developer/manager/bpf.py b/developer/manager/infrastructure/bpf.py similarity index 87% rename from developer/manager/bpf.py rename to developer/manager/infrastructure/bpf.py index 527d419..16c4388 100644 --- a/developer/manager/bpf.py +++ b/developer/manager/infrastructure/bpf.py @@ -1,3 +1,11 @@ +""" +bpf.py + +Compile/load the BPF program. + +5.3.1 compile_bpf(source_path: str, output_path: str) -> None +5.3.2 load_bpf(obj_path: str) -> BpfHandle +""" def attach_wg(subu_id: str, wg_id: str): ensure_mounts() @@ -11,14 +19,14 @@ def attach_wg(subu_id: str, wg_id: str): endpoint, local_ip, pubkey = w ifname = f"subu_{wid}" - # create WG link in init ns, move to netns + # make WG link in init ns, move to netns run(["ip", "link", "add", ifname, "type", "wireguard"]) run(["ip", "link", "set", ifname, "netns", ns]) run(["ip", "-n", ns, "addr", "add", local_ip, "dev", ifname], check=False) run(["ip", "-n", ns, "link", "set", "dev", ifname, "mtu", "1420"]) run(["ip", "-n", ns, "link", "set", "dev", ifname, "down"]) # keep engine down until `network up` - # install steering (MVP: create cgroup + attach bpf program) + # install steering (MVP: make cgroup + attach bpf program) try: install_steering(subu_id, ns, ifname) print(f"{subu_id}: eBPF steering installed -> {ifname}") diff --git a/developer/manager/bpf_force_egress.c b/developer/manager/infrastructure/bpf_force_egress.c similarity index 94% rename from developer/manager/bpf_force_egress.c rename to developer/manager/infrastructure/bpf_force_egress.c index c3aedec..628cc83 100644 --- a/developer/manager/bpf_force_egress.c +++ b/developer/manager/infrastructure/bpf_force_egress.c @@ -1,10 +1,15 @@ // -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 8 -*- // bpf_force_egress.c — MVP scaffold to validate UID and prep metadata -// Version 0.2.0 +/* + bpf_force_egress.c + +5.5.1 no callable Python API; compiled/used via bpf.py. +*/ #include #include #include + char LICENSE[] SEC("license") = "GPL"; struct { diff --git a/developer/manager/bpf_worker.py b/developer/manager/infrastructure/bpf_worker.py similarity index 92% rename from developer/manager/bpf_worker.py rename to developer/manager/infrastructure/bpf_worker.py index 96aef14..66aaf6d 100644 --- a/developer/manager/bpf_worker.py +++ b/developer/manager/infrastructure/bpf_worker.py @@ -1,7 +1,13 @@ # -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*- """ -worker_bpf.py — create per-subu cgroups and load eBPF (MVP) -Version: 0.2.0 +bpf_worker.py + +Cgroup + BPF orchestration for per-subu steering. + +5.4.1 ensure_mounts() -> None +5.4.2 install_steering(subu: Subu, wg_iface: str) -> None +5.4.3 remove_steering(subu: Subu) -> None +5.4.4 class BpfError(Exception) """ import os, subprocess, json from pathlib import Path diff --git a/developer/manager/db.py b/developer/manager/infrastructure/db.py similarity index 54% rename from developer/manager/db.py rename to developer/manager/infrastructure/db.py index c42b2bc..83fb5c5 100644 --- a/developer/manager/db.py +++ b/developer/manager/infrastructure/db.py @@ -1,15 +1,65 @@ +# infrastructure/db.py import os import pwd import grp import subprocess from contextlib import closing +import sqlite3 +from pathlib import Path + +""" +5.1 infrastructure/db.py + +All SQLite access. + +5.1.1 open_db(path: str = "subu.db") -> sqlite3.Connection +5.1.2 ensure_schema(conn) -> None +5.1.3 insert_subu(conn, subu: Subu) -> None +5.1.4 fetch_subu(conn, subu_id: str) -> Subu +5.1.5 list_subu(conn) -> list[Subu] +5.1.6 insert_wg(conn, wg: WG) -> None +5.1.7 fetch_wg(conn, wg_id: str) -> WG +5.1.8 update_wg(conn, wg: WG) -> None +5.1.9 set_option_row(conn, subu_id: str, name: str, value: str) -> None +5.1.10 get_option_row(conn, subu_id: str, name: str) -> str | None +5.1.11 list_option_rows(conn, subu_id: str) -> dict[str, str] + +(Exact breakdown can be tuned when we see schema.sql.) +""" + +# infrastructure/db.py +# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*- + +def schema_path_default(): return Path(__file__).with_name("schema.sql") +def db_path_default(): return "." + +def open_db(path ="subu.db"): + """ + Return a sqlite3.Connection with sensible pragmas. + Caller is responsible for closing. + """ + conn = sqlite3.connect(path) + conn.row_factory = sqlite3.Row + conn.execute("PRAGMA foreign_keys = ON") + conn.execute("PRAGMA journal_mode = WAL") + conn.execute("PRAGMA synchronous = NORMAL") + return conn + +def ensure_schema(conn): + """ + Ensure the schema in schema.sql is applied. + This is idempotent: executing the DDL again is acceptable. + """ + sql = schema_path_default().read_text(encoding="utf-8") + conn.executescript(sql) + conn.commit() def _db(): if not DB_FILE.exists(): raise FileNotFoundError("subu.db not found; run `subu init ` first") return sqlite3.connect(DB_FILE) -def init_db(path: str = DB_PATH): +def init_db(path: str = db_path_default()): """ Initialise subu.db if missing; refuse to overwrite existing file. """ @@ -20,10 +70,10 @@ def init_db(path: str = DB_PATH): with closing(sqlite3.connect(path)) as db: db.executescript(SCHEMA_SQL) db.execute( - "INSERT INTO meta(key,value) VALUES ('created_at', datetime('now'))" + "INSERT INTO meta(key,value) VALUES ('made_at', datetime('now'))" ) db.commit() - print(f"subu: created new db at {path}") + print(f"subu: made new db at {path}") def cmd_init(token: str|None): @@ -59,7 +109,7 @@ def cmd_init(token: str|None): ); """) db.commit() - print(f"created subu.db (v{VERSION})") + print(f"made subu.db (v{VERSION})") def _first_free_id(db, table: str) -> int: """ diff --git a/developer/manager/schema.sql b/developer/manager/infrastructure/schema.sql similarity index 87% rename from developer/manager/schema.sql rename to developer/manager/infrastructure/schema.sql index a33ae95..ab8d80a 100644 --- a/developer/manager/schema.sql +++ b/developer/manager/infrastructure/schema.sql @@ -1,3 +1,9 @@ +-- schema.sql +-- +-- 5.6.1 read and executed by db.ensure_schema + + + CREATE TABLE subu ( id INTEGER PRIMARY KEY, owner TEXT NOT NULL, -- root user, e.g. 'Thomas' diff --git a/developer/manager/unix.py b/developer/manager/infrastructure/unix.py similarity index 70% rename from developer/manager/unix.py rename to developer/manager/infrastructure/unix.py index 7773e4c..fec6d9a 100644 --- a/developer/manager/unix.py +++ b/developer/manager/infrastructure/unix.py @@ -1,3 +1,16 @@ +""" +unix.py + +Thin wrappers for OS commands. + +5.2.1 run(cmd: list[str], check: bool = True) -> subprocess.CompletedProcess +5.2.2 ip(*args: str, check: bool = True) +5.2.3 ip_netns(*args: str, check: bool = True) +5.2.4 wg(*args: str, check: bool = True) + +Optional later: logging, dry-run, etc. +""" + # ---------------- Unix users & groups ---------------- def _group_exists(name: str) -> bool: diff --git a/developer/manager/subu.db b/developer/manager/subu.db new file mode 100644 index 0000000000000000000000000000000000000000..aff27efb17400cac36b1bf4e4127f441ada4252a GIT binary patch literal 12288 zcmeI#K}*9h6bJCMippTSP$)WKd(5`k84+X5)^$VId9n1(=Dee?roXD# ztafjecR!e2p0M&{_kGm_yoUe;AOHafKmY;|fB*y_0D(0HUSF*3{Yu4p>nhQUWR~}G zQ|$}St@|$d_4B4n<_R5?h{8S zG@I4MW1^!YN|QuUp2@UIvi49T$A3(QB6D<*SPbQI@Lxe9DF*ZTLC2|*<_nQj9@Chn6v7sp4#wgAVmA4I?&s z;04())}qzy`#Q~7FUahZIe}B0jP&IDR0<{gT%X5(ik(08e|udzbnLDD(~8ByxG%@q zbEMDqT;wX5_PN=1er#HEuHP|#RM|9pra^%K1Rwwb2tWV=5P$##AOHafK;XX$l+40# e{{OGri*-Q&0uX=z1Rwwb2tWV=5P$##rUIX-Xq+Vg literal 0 HcmV?d00001 diff --git a/developer/manager/text.py b/developer/manager/text.py index d5ff982..0b1b364 100644 --- a/developer/manager/text.py +++ b/developer/manager/text.py @@ -1,128 +1,133 @@ -# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*- -VERSION = "0.2.0" +# text.py -USAGE = """\ -subu — Subu manager (v0.2.0) +from version import version as current_version + + +class Text: + """ + Program text bound to a specific command name. + + Usage: + text_1 = Text("subu") + text_2 = Text("manager") + + print(text_1.usage()) + print(text_2.help()) + """ + + def __init__(self, program_name ="subu"): + self.program_name = program_name + + def usage(self): + program_name = self.program_name + return f"""{program_name} — Subu manager (v{current_version()}) Usage: - subu # usage - subu help # detailed help - subu example # example workflow - subu version # print version + {program_name} # usage + {program_name} help # detailed help + {program_name} example # example workflow + {program_name} version # print version - subu init - subu create - subu list - subu info | subu information + {program_name} init + {program_name} make + {program_name} list + {program_name} info | {program_name} information - subu lo up|down + {program_name} lo up|down - subu WG global - subu WG create - subu WG server_provided_public_key - subu WG info|information - subu WG up - subu WG down + {program_name} WG global + {program_name} WG make + {program_name} WG server_provided_public_key + {program_name} WG info|information + {program_name} WG up + {program_name} WG down - subu attach WG - subu detach WG + {program_name} attach WG + {program_name} detach WG - subu network up|down + {program_name} network up|down - subu option set - subu option get - subu option list + {program_name} option set + {program_name} option get + {program_name} option list - subu exec -- ... + {program_name} exec -- ... """ -HELP = """\ -Subu manager (v0.2.0) + def help(self, verbose =False): + program_name = self.program_name + return f"""Subu manager (v{current_version()}) 1) Init - subu init - Creates ./subu.db. Refuses to run if db exists. + {program_name} init + Makes ./subu.db. Refuses to run if db exists. 2) Subu - subu create - subu list - subu info + {program_name} make + {program_name} list + {program_name} info 3) Loopback - subu lo up|down + {program_name} lo up|down 4) WireGuard objects (independent of subu) - subu WG global # e.g., 192.168.112.0/24 - subu WG create # allocates next /32 - subu WG server_provided_public_key - subu WG info - subu WG up / subu WG down # admin toggle after attached - -5) Attach/detach + eBPF steering - subu attach WG - - Creates WG dev as subu_ inside ns-subu_, assigns /32, MTU 1420 - - Installs per-subu cgroup + loads eBPF scaffold (UID check, metadata map) - - Keeps device admin-down until `subu network up` - subu detach WG - - Deletes device, removes cgroup + BPF + {program_name} WG global # for example, 192.168.112.0/24 + {program_name} WG make # allocates next /32 + {program_name} WG server_provided_public_key + {program_name} WG info + {program_name} WG up / {program_name} WG down # administrative toggle after attached + +5) Attach or detach and eBPF steering + {program_name} attach WG + - Makes WireGuard device as subu_ inside ns-subu_, assigns /32, MTU 1420 + - Installs per-subu cgroup and loads eBPF scaffold (user identifier check, metadata map) + - Keeps device administrative-down until `{program_name} network up` + {program_name} detach WG + - Deletes device, removes cgroup and eBPF program 6) Network aggregate - subu network up|down - - Ensures lo up on 'up', toggles attached WG ifaces + {program_name} network up|down + - Ensures loopback is up on 'up', toggles attached WireGuard interfaces 7) Options - subu option set|get|list ... + {program_name} option set|get|list ... 8) Exec - subu exec -- ... + {program_name} exec -- ... """ -EXAMPLE = """\ -# 0) Initialise the subu database (once per directory) -subu init -# -> created ./subu.db -# If ./subu.db already exists, init will fail with an error and do nothing. - -# 1) Create a Subu “US” owned by user Thomas -subu create Thomas US -# -> Subu_ID: subu_7 -# -> netns: ns-subu_7 with lo (down) - -# 2) Define a global WireGuard address pool (once per host) -subu WG global 192.168.112.0/24 -# -> base set; next free: 192.168.112.2/32 - -# 3) Create a WG object with endpoint (ReasoningTechnology server) -subu WG create 35.194.71.194:51820 -# or: subu WG create ReasoningTechnology.com:51820 -# -> WG_ID: WG_0 -# -> local IP: 192.168.112.2/32 -# -> AllowedIPs: 0.0.0.0/0 - -# 4) Add server public key (example key) -subu WG server_provided_public_key WG_0 ABCDEFG...xyz= -# -> saved - -# 5) Attach WG to the Subu -subu attach WG subu_7 WG_0 -# -> creates device ns-subu_7/subu_0 -# -> assigns 192.168.112.2/32, MTU 1420, accept_local=1 -# -> enforces egress steering via cgroup/eBPF for UID(s) of subu_7 -# -> warns if lo is down in the netns - -# 6) Bring networking up for the Subu -subu network up subu_7 -# -> brings lo up in ns-subu_7 -# -> brings subu_0 admin up - -# 7) Start the WireGuard engine for this WG -subu WG up WG_0 -# -> interface up; handshake should start if keys/endpoint are correct - -# 8) Run a command inside the Subu’s netns -subu exec subu_7 -- curl -4v https://ifconfig.me -# Traffic from this process should egress via subu_0/US tunnel. + def example(self): + program_name = self.program_name + return f"""# 0) Initialise the subu database (once per directory) +{program_name} init dzkq7b + +# 1) Make Subu +{program_name} make Thomas US +# -> subu_1 + +# 2) WireGuard pool once +{program_name} WG global 192.168.112.0/24 + +# 3) Make WireGuard object with endpoint +{program_name} WG make ReasoningTechnology.com:51820 +# -> WG_1 + +# 4) Server public key (placeholder) +{program_name} WG server_provided_public_key WG_1 ABCDEFG...xyz= + +# 5) Attach device and install cgroup and eBPF steering +{program_name} attach WG subu_1 WG_1 + +# 6) Bring network up (loopback and WireGuard) +{program_name} network up subu_1 + +# 7) Test inside namespace +{program_name} exec subu_1 -- curl -4v https://ifconfig.me """ -def VERSION_string(): - return VERSION + def version(self): + return current_version() + + +def make_text(program_name ="subu"): + return Text(program_name) diff --git a/developer/manager/parser.py b/developer/manager/uncatelogued/parser.py similarity index 97% rename from developer/manager/parser.py rename to developer/manager/uncatelogued/parser.py index d0c2f47..cf2dd84 100644 --- a/developer/manager/parser.py +++ b/developer/manager/uncatelogued/parser.py @@ -5,7 +5,7 @@ verbs = [ "version", "init", "make", - "create", + "make", "info", "information", "WG", diff --git a/developer/manager/version.py b/developer/manager/version.py new file mode 100644 index 0000000..ed3750c --- /dev/null +++ b/developer/manager/version.py @@ -0,0 +1,2 @@ +def version(): + return "0.3.2" -- 2.20.1