From e501638678cc132685bd515e65f4d2c72d890631 Mon Sep 17 00:00:00 2001 From: Thomas Walker Lynch Date: Tue, 4 Nov 2025 07:15:11 +0000 Subject: [PATCH] structureing the code --- developer/manager.tgz | Bin 0 -> 20863 bytes developer/manager/bpf.py | 52 +++ .../manager/{worker_bpf.py => bpf_worker.py} | 0 developer/manager/core.py | 234 +----------- developer/manager/db.py | 86 +++++ developer/manager/exec.py | 6 + developer/manager/network.py | 24 ++ developer/manager/options.py | 23 ++ developer/manager/parser.py | 32 ++ developer/manager/schema.sql | 11 + developer/manager/subu.py | 150 ++++++++ developer/manager/temp.sh | 40 -- developer/manager/text.py | 61 +-- developer/manager/unix.py | 28 ++ developer/manager/wg.py | 48 +++ document/manager.org | 289 ++++++++++++++ {developer => tester}/manager/test.sh | 0 {developer => tester}/manager/test_0.sh | 0 .../manager/test_0_expected.sh | 0 tester/test.sh | 13 + tester/test_0.sh | 11 + tester/test_0_expected.sh | 353 ++++++++++++++++++ 22 files changed, 1167 insertions(+), 294 deletions(-) create mode 100644 developer/manager.tgz create mode 100644 developer/manager/bpf.py rename developer/manager/{worker_bpf.py => bpf_worker.py} (100%) create mode 100644 developer/manager/db.py create mode 100644 developer/manager/exec.py create mode 100644 developer/manager/network.py create mode 100644 developer/manager/options.py create mode 100644 developer/manager/parser.py create mode 100644 developer/manager/schema.sql create mode 100644 developer/manager/subu.py delete mode 100644 developer/manager/temp.sh create mode 100644 developer/manager/unix.py create mode 100644 developer/manager/wg.py create mode 100644 document/manager.org rename {developer => tester}/manager/test.sh (100%) rename {developer => tester}/manager/test_0.sh (100%) rename {developer => tester}/manager/test_0_expected.sh (100%) create mode 100644 tester/test.sh create mode 100755 tester/test_0.sh create mode 100644 tester/test_0_expected.sh diff --git a/developer/manager.tgz b/developer/manager.tgz new file mode 100644 index 0000000000000000000000000000000000000000..f802d65b34563253940ebedc290c07c3bf6e4532 GIT binary patch literal 20863 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 a`n3h;<+FU2&+_?;KmP~cXRa6kKmh {ifname}") + except BpfError as e: + print(f"{subu_id}: steering warning: {e}") + + with closing(_db()) as db: + db.execute("UPDATE subu SET wg_id=? WHERE id=?", (wid, sid)) + db.commit() + print(f"attached {wg_id} to {subu_id} in {ns} as {ifname}") + +def detach_wg(subu_id: str): + ensure_mounts() + sid = int(subu_id.split("_")[1]) + with closing(_db()) as db: + r = db.execute("SELECT netns,wg_id FROM subu WHERE id=?", (sid,)).fetchone() + if not r: print("not found"); return + ns, wid = r + if wid is None: + print("nothing attached"); return + ifname = f"subu_{wid}" + run(["ip", "-n", ns, "link", "del", ifname], check=False) + try: + remove_steering(subu_id) + except BpfError as e: + print(f"steering remove warn: {e}") + with closing(_db()) as db: + db.execute("UPDATE subu SET wg_id=NULL WHERE id=?", (sid,)) + db.commit() + print(f"detached WG_{wid} from {subu_id}") + diff --git a/developer/manager/worker_bpf.py b/developer/manager/bpf_worker.py similarity index 100% rename from developer/manager/worker_bpf.py rename to developer/manager/bpf_worker.py diff --git a/developer/manager/core.py b/developer/manager/core.py index c363ec2..f66cdf9 100644 --- a/developer/manager/core.py +++ b/developer/manager/core.py @@ -8,6 +8,7 @@ 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") @@ -18,237 +19,4 @@ def run(cmd, check=True): raise RuntimeError(f"cmd failed: {' '.join(cmd)}\n{r.stderr}") return r.stdout.strip() -# ---------------- DB ---------------- -def _db(): - if not DB_FILE.exists(): - raise FileNotFoundError("subu.db not found; run `subu init ` first") - return sqlite3.connect(DB_FILE) -def cmd_init(token: str|None): - if DB_FILE.exists(): - raise FileExistsError("db already exists") - if not token or len(token) < 6: - raise ValueError("init requires a 6+ char token") - with closing(sqlite3.connect(DB_FILE)) as db: - c = db.cursor() - c.executescript(""" - CREATE TABLE subu ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - owner TEXT, - name TEXT, - netns TEXT, - lo_state TEXT DEFAULT 'down', - wg_id INTEGER, - network_state TEXT DEFAULT 'down' - ); - CREATE TABLE wg ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - endpoint TEXT, - local_ip TEXT, - allowed_ips TEXT, - pubkey TEXT, - state TEXT DEFAULT 'down' - ); - CREATE TABLE options ( - subu_id INTEGER, - name TEXT, - value TEXT, - PRIMARY KEY (subu_id, name) - ); - """) - db.commit() - print(f"created subu.db (v{VERSION})") - -# ------------- 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() - - # 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) - -def info_subu(subu_id: str): - sid = int(subu_id.split("_")[1]) - with closing(_db()) as db: - row = db.execute("SELECT * FROM subu WHERE id=?", (sid,)).fetchone() - if not row: - print("not found"); return - print(row) - wg = db.execute("SELECT wg_id FROM subu WHERE id=?", (sid,)).fetchone()[0] - if wg is not None: - wrow = db.execute("SELECT * FROM wg WHERE id=?", (wg,)).fetchone() - print("WG:", wrow) - opts = db.execute("SELECT name,value FROM options WHERE subu_id=?", (sid,)).fetchall() - print("Options:", opts) - -def lo_toggle(subu_id: str, state: str): - sid = int(subu_id.split("_")[1]) - with closing(_db()) as db: - ns = db.execute("SELECT netns FROM subu WHERE id=?", (sid,)).fetchone() - if not ns: raise ValueError("subu not found") - ns = ns[0] - run(["ip", "netns", "exec", ns, "ip", "link", "set", "lo", state]) - db.execute("UPDATE subu SET lo_state=? WHERE id=?", (state, sid)) - db.commit() - print(f"{subu_id}: lo {state}") - -# ------------- WG ops --------------- -def wg_global(basecidr: str): - WG_GLOBAL_FILE.write_text(basecidr.strip()+"\n") - print(f"WG pool base = {basecidr}") - -def _alloc_ip(idx: int, base: str) -> str: - # simplistic /24 allocator: base must be x.y.z.0/24 - prefix = base.split("/")[0].rsplit(".", 1)[0] - host = 2 + idx - return f"{prefix}.{host}/32" - -def wg_create(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() - with closing(_db()) as db: - c = db.cursor() - idx = c.execute("SELECT COUNT(*) FROM wg").fetchone()[0] - local_ip = _alloc_ip(idx, base) - c.execute("INSERT INTO wg (endpoint, local_ip, allowed_ips) VALUES (?, ?, ?)", - (endpoint, local_ip, "0.0.0.0/0")) - wid = c.lastrowid - db.commit() - print(f"WG_{wid} endpoint={endpoint} ip={local_ip}") - return f"WG_{wid}" - -def wg_set_pubkey(wg_id: str, key: str): - wid = int(wg_id.split("_")[1]) - with closing(_db()) as db: - db.execute("UPDATE wg SET pubkey=? WHERE id=?", (key, wid)) - db.commit() - print("ok") - -def wg_info(wg_id: str): - wid = int(wg_id.split("_")[1]) - with closing(_db()) as db: - row = db.execute("SELECT * FROM wg WHERE id=?", (wid,)).fetchone() - print(row if row else "not found") - -def wg_up(wg_id: str): - wid = int(wg_id.split("_")[1]) - # Admin-up of WG device handled via network_toggle once attached. - print(f"{wg_id}: up (noop until attached)") - -def wg_down(wg_id: str): - wid = int(wg_id.split("_")[1]) - print(f"{wg_id}: down (noop until attached)") - -# ---------- attach/detach + BPF ---------- -def attach_wg(subu_id: str, wg_id: str): - ensure_mounts() - sid = int(subu_id.split("_")[1]); wid = int(wg_id.split("_")[1]) - with closing(_db()) as db: - r = db.execute("SELECT netns FROM subu WHERE id=?", (sid,)).fetchone() - if not r: raise ValueError("subu not found") - ns = r[0] - w = db.execute("SELECT endpoint, local_ip, pubkey FROM wg WHERE id=?", (wid,)).fetchone() - if not w: raise ValueError("WG not found") - endpoint, local_ip, pubkey = w - - ifname = f"subu_{wid}" - # create 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) - try: - install_steering(subu_id, ns, ifname) - print(f"{subu_id}: eBPF steering installed -> {ifname}") - except BpfError as e: - print(f"{subu_id}: steering warning: {e}") - - with closing(_db()) as db: - db.execute("UPDATE subu SET wg_id=? WHERE id=?", (wid, sid)) - db.commit() - print(f"attached {wg_id} to {subu_id} in {ns} as {ifname}") - -def detach_wg(subu_id: str): - ensure_mounts() - sid = int(subu_id.split("_")[1]) - with closing(_db()) as db: - r = db.execute("SELECT netns,wg_id FROM subu WHERE id=?", (sid,)).fetchone() - if not r: print("not found"); return - ns, wid = r - if wid is None: - print("nothing attached"); return - ifname = f"subu_{wid}" - run(["ip", "-n", ns, "link", "del", ifname], check=False) - try: - remove_steering(subu_id) - except BpfError as e: - print(f"steering remove warn: {e}") - with closing(_db()) as db: - db.execute("UPDATE subu SET wg_id=NULL WHERE id=?", (sid,)) - db.commit() - print(f"detached WG_{wid} from {subu_id}") - -# ------------- network up/down ------------- -def network_toggle(subu_id: str, state: str): - sid = int(subu_id.split("_")[1]) - with closing(_db()) as db: - ns, wid = db.execute("SELECT netns,wg_id FROM subu WHERE id=?", (sid,)).fetchone() - # always make sure lo up on 'up' - if state == "up": - run(["ip", "netns", "exec", ns, "ip", "link", "set", "lo", "up"], check=False) - if wid is not None: - ifname = f"subu_{wid}" - run(["ip", "-n", ns, "link", "set", "dev", ifname, state], check=False) - with closing(_db()) as db: - db.execute("UPDATE subu SET network_state=? WHERE id=?", (state, sid)) - db.commit() - print(f"{subu_id}: network {state}") - -# ------------- options ---------------- -def option_set(subu_id: str, name: str, value: str): - sid = int(subu_id.split("_")[1]) - with closing(_db()) as db: - db.execute("INSERT INTO options (subu_id,name,value) VALUES(?,?,?) " - "ON CONFLICT(subu_id,name) DO UPDATE SET value=excluded.value", - (sid, name, value)) - db.commit() - print("ok") - -def option_get(subu_id: str, name: str): - sid = int(subu_id.split("_")[1]) - with closing(_db()) as db: - row = db.execute("SELECT value FROM options WHERE subu_id=? AND name=?", (sid,name)).fetchone() - print(row[0] if row else "") - -def option_list(subu_id: str): - sid = int(subu_id.split("_")[1]) - with closing(_db()) as db: - rows = db.execute("SELECT name,value FROM options WHERE subu_id=?", (sid,)).fetchall() - for n,v in rows: - print(f"{n}={v}") - -# ------------- exec ------------------- -def exec_in_subu(subu_id: str, cmd: list): - sid = int(subu_id.split("_")[1]) - with closing(_db()) as db: - ns = db.execute("SELECT netns FROM subu WHERE id=?", (sid,)).fetchone()[0] - os.execvp("ip", ["ip","netns","exec", ns] + cmd) diff --git a/developer/manager/db.py b/developer/manager/db.py new file mode 100644 index 0000000..c42b2bc --- /dev/null +++ b/developer/manager/db.py @@ -0,0 +1,86 @@ +import os +import pwd +import grp +import subprocess +from contextlib import closing + +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): + """ + Initialise subu.db if missing; refuse to overwrite existing file. + """ + if os.path.exists(path): + print(f"subu: db already exists at {path}") + return + + with closing(sqlite3.connect(path)) as db: + db.executescript(SCHEMA_SQL) + db.execute( + "INSERT INTO meta(key,value) VALUES ('created_at', datetime('now'))" + ) + db.commit() + print(f"subu: created new db at {path}") + + +def cmd_init(token: str|None): + if DB_FILE.exists(): + raise FileExistsError("db already exists") + if not token or len(token) < 6: + raise ValueError("init requires a 6+ char token") + with closing(sqlite3.connect(DB_FILE)) as db: + c = db.cursor() + c.executescript(""" + CREATE TABLE subu ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + owner TEXT, + name TEXT, + netns TEXT, + lo_state TEXT DEFAULT 'down', + wg_id INTEGER, + network_state TEXT DEFAULT 'down' + ); + CREATE TABLE wg ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + endpoint TEXT, + local_ip TEXT, + allowed_ips TEXT, + pubkey TEXT, + state TEXT DEFAULT 'down' + ); + CREATE TABLE options ( + subu_id INTEGER, + name TEXT, + value TEXT, + PRIMARY KEY (subu_id, name) + ); + """) + db.commit() + print(f"created subu.db (v{VERSION})") + +def _first_free_id(db, table: str) -> int: + """ + Return the smallest non-negative integer not in table.id. + Assumes 'id' INTEGER PRIMARY KEY in that table. + """ + rows = db.execute(f"SELECT id FROM {table} ORDER BY id ASC").fetchall() + used = {r[0] for r in rows} + i = 0 + while i in used: + i += 1 + return i + +def get_subu_by_full_unix_name(full_unix_name: str): + """ + Return the DB row for a subu with this full_unix_name, or None. + """ + with closing(open_db()) as db: + row = db.execute( + "SELECT id, owner, name, full_unix_name, path, netns_name " + "FROM subu WHERE full_unix_name = ?", + (full_unix_name,) + ).fetchone() + return row diff --git a/developer/manager/exec.py b/developer/manager/exec.py new file mode 100644 index 0000000..f823d9a --- /dev/null +++ b/developer/manager/exec.py @@ -0,0 +1,6 @@ + +def exec_in_subu(subu_id: str, cmd: list): + sid = int(subu_id.split("_")[1]) + with closing(_db()) as db: + ns = db.execute("SELECT netns FROM subu WHERE id=?", (sid,)).fetchone()[0] + os.execvp("ip", ["ip","netns","exec", ns] + cmd) diff --git a/developer/manager/network.py b/developer/manager/network.py new file mode 100644 index 0000000..000bbf8 --- /dev/null +++ b/developer/manager/network.py @@ -0,0 +1,24 @@ + +def network_toggle(subu_id: str, state: str): + sid = int(subu_id.split("_")[1]) + with closing(_db()) as db: + ns, wid = db.execute("SELECT netns,wg_id FROM subu WHERE id=?", (sid,)).fetchone() + # always make sure lo up on 'up' + if state == "up": + run(["ip", "netns", "exec", ns, "ip", "link", "set", "lo", "up"], check=False) + if wid is not None: + ifname = f"subu_{wid}" + run(["ip", "-n", ns, "link", "set", "dev", ifname, state], check=False) + with closing(_db()) as db: + db.execute("UPDATE subu SET network_state=? WHERE id=?", (state, sid)) + db.commit() + print(f"{subu_id}: network {state}") + +def _create_netns_for_subu(subu_id_num: int, netns_name: str): + """ + Create the network namespace & bring lo down. + """ + # ip netns add ns-subu_ + run(["ip", "netns", "add", netns_name]) + # ip netns exec ns-subu_ ip link set lo down + run(["ip", "netns", "exec", netns_name, "ip", "link", "set", "lo", "down"]) diff --git a/developer/manager/options.py b/developer/manager/options.py new file mode 100644 index 0000000..76b5caa --- /dev/null +++ b/developer/manager/options.py @@ -0,0 +1,23 @@ + +def option_set(subu_id: str, name: str, value: str): + sid = int(subu_id.split("_")[1]) + with closing(_db()) as db: + db.execute("INSERT INTO options (subu_id,name,value) VALUES(?,?,?) " + "ON CONFLICT(subu_id,name) DO UPDATE SET value=excluded.value", + (sid, name, value)) + db.commit() + print("ok") + +def option_get(subu_id: str, name: str): + sid = int(subu_id.split("_")[1]) + with closing(_db()) as db: + row = db.execute("SELECT value FROM options WHERE subu_id=? AND name=?", (sid,name)).fetchone() + print(row[0] if row else "") + +def option_list(subu_id: str): + sid = int(subu_id.split("_")[1]) + with closing(_db()) as db: + rows = db.execute("SELECT name,value FROM options WHERE subu_id=?", (sid,)).fetchall() + for n,v in rows: + print(f"{n}={v}") + diff --git a/developer/manager/parser.py b/developer/manager/parser.py new file mode 100644 index 0000000..d0c2f47 --- /dev/null +++ b/developer/manager/parser.py @@ -0,0 +1,32 @@ +verbs = [ + "usage", + "help", + "example", + "version", + "init", + "make", + "create", + "info", + "information", + "WG", + "attach", + "detach", + "network", + "lo", + "option", + "exec", +] + +p_make = subparsers.add_parser( + "make", + help="Create a Subu with hierarchical name + Unix user/groups + netns", +) +p_make.add_argument( + "path", + nargs="+", + help="Full Subu path, e.g. 'Thomas US' or 'Thomas new-subu Rabbit'", +) + +elif args.verb == "make": + subu_id = core.make_subu(args.path) + print(subu_id) diff --git a/developer/manager/schema.sql b/developer/manager/schema.sql new file mode 100644 index 0000000..a33ae95 --- /dev/null +++ b/developer/manager/schema.sql @@ -0,0 +1,11 @@ +CREATE TABLE subu ( + id INTEGER PRIMARY KEY, + owner TEXT NOT NULL, -- root user, e.g. 'Thomas' + name TEXT NOT NULL, -- leaf, e.g. 'US', 'Rabbit' + full_unix_name TEXT NOT NULL UNIQUE, -- e.g. 'Thomas_US_Rabbit' + path TEXT NOT NULL, -- e.g. 'Thomas US Rabbit' + netns_name TEXT NOT NULL, + wg_id INTEGER, -- nullable for now + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +); diff --git a/developer/manager/subu.py b/developer/manager/subu.py new file mode 100644 index 0000000..ea5ad0c --- /dev/null +++ b/developer/manager/subu.py @@ -0,0 +1,150 @@ +# ------------- 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() + + # 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) + +def info_subu(subu_id: str): + sid = int(subu_id.split("_")[1]) + with closing(_db()) as db: + row = db.execute("SELECT * FROM subu WHERE id=?", (sid,)).fetchone() + if not row: + print("not found"); return + print(row) + wg = db.execute("SELECT wg_id FROM subu WHERE id=?", (sid,)).fetchone()[0] + if wg is not None: + wrow = db.execute("SELECT * FROM wg WHERE id=?", (wg,)).fetchone() + print("WG:", wrow) + opts = db.execute("SELECT name,value FROM options WHERE subu_id=?", (sid,)).fetchall() + print("Options:", opts) + +def lo_toggle(subu_id: str, state: str): + sid = int(subu_id.split("_")[1]) + with closing(_db()) as db: + ns = db.execute("SELECT netns FROM subu WHERE id=?", (sid,)).fetchone() + if not ns: raise ValueError("subu not found") + ns = ns[0] + run(["ip", "netns", "exec", ns, "ip", "link", "set", "lo", state]) + db.execute("UPDATE subu SET lo_state=? WHERE id=?", (state, sid)) + db.commit() + print(f"{subu_id}: lo {state}") + +# ---------------- High-level Subu factory ---------------- + +def make_subu(path_tokens: list[str]) -> str: + """ + Create a new Subu with hierarchical name and full wiring: + + path_tokens: ['Thomas', 'US'] or ['Thomas', 'new-subu', 'Rabbit'] + + Rules: + - len(path_tokens) >= 2 + - parent path (everything except last token) must already exist + as: + * a Unix user (for len==2: just the top-level user, e.g. 'Thomas') + * and as a Subu in our DB if len > 2 (e.g. 'Thomas_new-subu') + - new Unix user name is path joined by '_', e.g. 'Thomas_new-subu_Rabbit' + - mas u(root) is path_tokens[0] + - groups: + + -incommon + + 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 + - Unix groups ensured and membership updated + + Returns: textual Subu_ID, e.g. 'subu_7'. + """ + if not path_tokens or len(path_tokens) < 2: + raise SystemExit("subu: make requires at least two path elements, e.g. 'Thomas US'") + + # Normalised pieces + path_tokens = [p.strip() for p in path_tokens if p.strip()] + if len(path_tokens) < 2: + raise SystemExit("subu: make requires at least two non-empty path elements") + + masu = path_tokens[0] # root user / owner + leaf = path_tokens[-1] # new subu leaf + parent_tokens = path_tokens[:-1] # parent path + full_unix_name = "_".join(path_tokens) # e.g. 'Thomas_new-subu_Rabbit' + parent_unix_name = "_".join(parent_tokens) + path_str = " ".join(path_tokens) # e.g. 'Thomas new-subu Rabbit' + + # 1) Enforce parent existing + + # Case A: top-level subu (e.g. ['Thomas', 'US']) + if len(path_tokens) == 2: + # Require the root user to exist as a Unix user + if not _user_exists(masu): + raise SystemExit( + f"subu: cannot make '{path_str}': root user '{masu}' does not exist" + ) + else: + # Case B: deeper subu: require parent subu exists in our DB + parent_row = get_subu_by_full_unix_name(parent_unix_name) + if not parent_row: + raise SystemExit( + f"subu: cannot make '{path_str}': parent subu '{parent_unix_name}' does not exist" + ) + + # Also forbid duplicate full_unix_name + existing = get_subu_by_full_unix_name(full_unix_name) + if existing: + raise SystemExit( + f"subu: subu with name '{full_unix_name}' already exists (id=subu_{existing[0]})" + ) + + # 2) Insert DB row and allocate ID + netns_name + + with closing(open_db()) as db: + subu_id_num = _first_free_id(db, "subu") + 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) " + "VALUES (?, ?, ?, ?, ?, ?, NULL, datetime('now'), datetime('now'))", + (subu_id_num, masu, leaf, full_unix_name, path_str, netns_name) + ) + db.commit() + + subu_id = f"subu_{subu_id_num}" + + # 3) Create netns + lo down + _create_netns_for_subu(subu_id_num, netns_name) + + # 4) Ensure Unix user + groups + + unix_user = full_unix_name + group_masu = masu + group_incommon = f"{masu}-incommon" + + _ensure_group(group_masu) + _ensure_group(group_incommon) + + _ensure_user(unix_user, group_masu) + _add_user_to_group(unix_user, group_masu) # mostly redundant but explicit + _add_user_to_group(unix_user, group_incommon) + + print(f"Created Subu {subu_id} for path '{path_str}' with Unix user '{unix_user}' " + f"and netns '{netns_name}'") + + return subu_id diff --git a/developer/manager/temp.sh b/developer/manager/temp.sh deleted file mode 100644 index 36855b6..0000000 --- a/developer/manager/temp.sh +++ /dev/null @@ -1,40 +0,0 @@ -# from: /home/Thomas/subu_data/developer/project/active/subu/developer/source/manager - -set -euo pipefail - -echo "== 1) Backup legacy-prefixed modules ==" -mkdir -p _old_prefixed -for f in subu_*.py; do - [ -f "$f" ] && mv -v "$f" _old_prefixed/ -done -[ -f subu_worker_bpf.py ] && mv -v subu_worker_bpf.py _old_prefixed/ || true - -echo "== 2) Ensure only the new module names remain ==" -# Keep these (already present in your tar): -# CLI.py core.py text.py worker_bpf.py bpf_force_egress.c -ls -1 - -echo "== 3) Make CLI runnable as 'subu' ==" -# Make sure CLI has a shebang; add if missing -if ! head -n1 CLI.py | grep -q '^#!/usr/bin/env python3'; then - (printf '%s\n' '#!/usr/bin/env python3' ; cat CLI.py) > .CLI.tmp && mv .CLI.tmp CLI.py -fi -chmod +x CLI.py -ln -sf CLI.py subu -chmod +x subu - -echo "== 4) Quick import sanity ==" -# Fail if any of the remaining files still import the old module names -bad=$(grep -R --line-number -E 'import +subu_|from +subu_' -- *.py || true) -if [ -n "$bad" ]; then - echo "Found old-style imports; please fix:" >&2 - echo "$bad" >&2 - exit 1 -fi - -echo "== 5) Show version and help ==" -./subu version || true -./subu help || true -./subu || true # should print usage by default - -echo "== Done. If this looks good, you can delete _old_prefixed when ready. ==" diff --git a/developer/manager/text.py b/developer/manager/text.py index 84f6762..d5ff982 100644 --- a/developer/manager/text.py +++ b/developer/manager/text.py @@ -78,31 +78,50 @@ Subu manager (v0.2.0) """ EXAMPLE = """\ -# 0) Init -subu init dzkq7b +# 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 Subu +# 1) Create a Subu “US” owned by user Thomas subu create Thomas US -# -> subu_1 +# -> Subu_ID: subu_7 +# -> netns: ns-subu_7 with lo (down) -# 2) WG pool once +# 2) Define a global WireGuard address pool (once per host) subu WG global 192.168.112.0/24 - -# 3) Create WG object with endpoint -subu WG create ReasoningTechnology.com:51820 -# -> WG_1 - -# 4) Pubkey (placeholder) -subu WG server_provided_public_key WG_1 ABCDEFG...xyz= - -# 5) Attach device and install cgroup+BPF steering -subu attach WG subu_1 WG_1 - -# 6) Bring network up (lo + WG) -subu network up subu_1 - -# 7) Test inside ns -subu exec subu_1 -- curl -4v https://ifconfig.me +# -> 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 VERSION_string(): diff --git a/developer/manager/unix.py b/developer/manager/unix.py new file mode 100644 index 0000000..7773e4c --- /dev/null +++ b/developer/manager/unix.py @@ -0,0 +1,28 @@ +# ---------------- Unix users & groups ---------------- + +def _group_exists(name: str) -> bool: + try: + grp.getgrnam(name) + return True + except KeyError: + return False + +def _user_exists(name: str) -> bool: + try: + pwd.getpwnam(name) + return True + except KeyError: + return False + +def _ensure_group(name: str): + if not _group_exists(name): + # groupadd + run(["groupadd", name]) + +def _ensure_user(name: str, primary_group: str): + if not _user_exists(name): + # useradd -m -g -s /bin/bash + run(["useradd", "-m", "-g", primary_group, "-s", "/bin/bash", name]) + +def _add_user_to_group(user: str, group: str): + run(["usermod", "-aG", group, user]) diff --git a/developer/manager/wg.py b/developer/manager/wg.py new file mode 100644 index 0000000..3be049c --- /dev/null +++ b/developer/manager/wg.py @@ -0,0 +1,48 @@ + +def wg_global(basecidr: str): + WG_GLOBAL_FILE.write_text(basecidr.strip()+"\n") + print(f"WG pool base = {basecidr}") + +def _alloc_ip(idx: int, base: str) -> str: + # simplistic /24 allocator: base must be x.y.z.0/24 + prefix = base.split("/")[0].rsplit(".", 1)[0] + host = 2 + idx + return f"{prefix}.{host}/32" + +def wg_create(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() + with closing(_db()) as db: + c = db.cursor() + idx = c.execute("SELECT COUNT(*) FROM wg").fetchone()[0] + local_ip = _alloc_ip(idx, base) + c.execute("INSERT INTO wg (endpoint, local_ip, allowed_ips) VALUES (?, ?, ?)", + (endpoint, local_ip, "0.0.0.0/0")) + wid = c.lastrowid + db.commit() + print(f"WG_{wid} endpoint={endpoint} ip={local_ip}") + return f"WG_{wid}" + +def wg_set_pubkey(wg_id: str, key: str): + wid = int(wg_id.split("_")[1]) + with closing(_db()) as db: + db.execute("UPDATE wg SET pubkey=? WHERE id=?", (key, wid)) + db.commit() + print("ok") + +def wg_info(wg_id: str): + wid = int(wg_id.split("_")[1]) + with closing(_db()) as db: + row = db.execute("SELECT * FROM wg WHERE id=?", (wid,)).fetchone() + print(row if row else "not found") + +def wg_up(wg_id: str): + wid = int(wg_id.split("_")[1]) + # Admin-up of WG device handled via network_toggle once attached. + print(f"{wg_id}: up (noop until attached)") + +def wg_down(wg_id: str): + wid = int(wg_id.split("_")[1]) + print(f"{wg_id}: down (noop until attached)") + diff --git a/document/manager.org b/document/manager.org new file mode 100644 index 0000000..77dcebb --- /dev/null +++ b/document/manager.org @@ -0,0 +1,289 @@ +#+TITLE: Subu Manager Specification +#+AUTHOR: Reasoning Technology / Thomas Walker Lynch +#+DATE: 2025-11-03 +#+LANGUAGE: en +#+STARTUP: overview +#+PROPERTY: header-args :results output + +* Overview +The *Subu Manager* is a command-line orchestration tool for creating and managing +*lightweight user-containers* (“subus”) with isolated namespaces, private +WireGuard interfaces, and enforced network routing rules. + +It unifies several Linux primitives: + +- Unix users and groups (for identity & filesystem isolation) +- Network namespaces (for network isolation) +- WireGuard interfaces (for VPN / tunnel endpoints) +- eBPF cgroup programs (for routing enforcement) +- SQLite database (for persistence and state tracking) + +The manager is designed to evolve toward a full *Subu Light Container System*, +where each user has nested subordinate users, and each subu can have its own +network, security policies, and forwarding rules. + +--- + +* Architecture Summary +** Components +1. =CLI.py= :: command-line interface +2. =core.py= :: high-level orchestration logic +3. =db.py= (planned) :: schema definition and migration +4. =userutil.py= (planned) :: Unix account and group management helpers +5. =netutil.py= (planned) :: namespace and interface creation +6. =bpfutil.py= (planned) :: cgroup/eBPF setup + +** Data persistence +All persistent configuration lives in =subu.db= (SQLite). +This file contains: +- *meta* :: creation time, schema version +- *subu* :: all subuser accounts and their namespace info +- *wg* :: WireGuard endpoints +- *links* :: relationships between subu and wg interfaces +- *options* :: boolean or key/value runtime options + +--- + +* Command Overview +Each =CLI.py= command corresponds to a top-level operation. The CLI delegates +to core functions in =core.py=. + +| Command | Description | Implementation | +|----------+--------------+----------------| +| =init= | Create the SQLite DB and schema | `core.init_db()` | +| =make= | Create a new subu hierarchy (user, netns, groups) | `core.make_subu(path_tokens)` | +| =info= / =information= | Print full record of a subu | `core.get_subu_info()` | +| =WG= | Manage WireGuard objects and their mapping | `core.create_wg()`, `core.attach_wg()` | +| =attach= / =detach= | Link or unlink WG interface to subu namespace | `core.attach_wg_to_subu()` | +| =network up/down= | Bring up or down all attached ifaces | `core.network_toggle()` | +| =lo up/down= | Bring loopback up/down in subu netns | `core.lo_toggle()` | +| =option add/remove/list= | Manage options | `core.option_add()` etc. | +| =exec= | Run command inside subu netns | `core.exec_in_netns()` | +| =help= / =usage= / =example= | Documentation commands | CLI only | +| =version= | Print program version | constant in `core.VERSION` | + +--- + +* Subu Creation Flow (=make=) + +** Syntax +#+begin_example +./CLI.py make Thomas new-subu Rabbit +#+end_example + +** Behavior +- Verifies that *parent path* (all but last token) exists. + - If two-level (e.g. =Thomas US=), requires Unix user =Thomas= exists. + - If deeper (e.g. =Thomas new-subu Rabbit=), requires DB entry for + =Thomas_new-subu=. +- Allocates next available subu ID (first free integer). +- Inserts row in DB with: + - =id=, =owner=, =name=, =full_unix_name=, =path=, =netns_name= +- Creates network namespace =ns-subu_= +- Brings =lo= down inside that namespace. +- Ensures Unix groups: + - == + - =-incommon= +- Ensures Unix user: + - =_...= (underscores for hierarchy) +- Adds new user to both groups. + +** Implementation +#+begin_src python +def make_subu(path_tokens: list[str]) -> str: + # 1. Validate hierarchy, check parent + # 2. Allocate ID (via _first_free_id) + # 3. Insert into DB (open_db) + # 4. Create netns (ip netns add ...) + # 5. Ensure groups/users (useradd, groupadd) + # 6. Return subu_X identifier +#+end_src + +--- + +* User and Group Management + +** Goals +Each subu is a Linux user; hierarchy is mirrored in usernames: +#+begin_example +Thomas_US +Thomas_US_Rabbit +Thomas_local +#+end_example + +Each subu belongs to: +- group =Thomas= +- group =Thomas-incommon= + +** Implementation Functions +#+begin_src python +def _group_exists(name): ... +def _user_exists(name): ... +def _ensure_group(name): ... +def _ensure_user(name, primary_group): ... +def _add_user_to_group(user, group): ... +#+end_src + +--- + +* Database Schema (summary) + +#+begin_src sql +CREATE TABLE meta ( + key TEXT PRIMARY KEY, + value TEXT +); + +CREATE TABLE subu ( + id INTEGER PRIMARY KEY, + owner TEXT NOT NULL, + name TEXT NOT NULL, + full_unix_name TEXT NOT NULL UNIQUE, + path TEXT NOT NULL, + netns_name TEXT NOT NULL, + wg_id INTEGER, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +); + +CREATE TABLE wg ( + id INTEGER PRIMARY KEY, + endpoint TEXT, + local_ip TEXT, + server_pubkey TEXT, + created_at TEXT, + updated_at TEXT +); + +CREATE TABLE links ( + subu_id INTEGER, + wg_id INTEGER, + FOREIGN KEY(subu_id) REFERENCES subu(id), + FOREIGN KEY(wg_id) REFERENCES wg(id) +); + +CREATE TABLE options ( + subu_id INTEGER, + name TEXT, + value TEXT, + FOREIGN KEY(subu_id) REFERENCES subu(id) +); +#+end_src + +--- + +* Networking and Namespaces +Each subu has a private namespace. + +** Steps +1. =ip netns add ns-subu_= +2. =ip netns exec ns-subu_ ip link set lo down= +3. Optionally attach WG interfaces (later). + +** Implementation +#+begin_src python +def _create_netns_for_subu(subu_id_num, netns_name): + run(["ip", "netns", "add", netns_name]) + run(["ip", "netns", "exec", netns_name, "ip", "link", "set", "lo", "down"]) +#+end_src + +--- + +* WireGuard Integration +Each subu may have exactly one WG interface. + +** Workflow +1. Allocate new WG object via =subu WG create = +2. Record server-provided key via =subu WG server_provided_public_key= +3. Attach interface via =subu attach WG = +4. Bring network up (includes WG admin up). + +** Implementation (planned) +#+begin_src python +def create_wg(endpoint): ... +def attach_wg_to_subu(subu_id, wg_id): ... +def wg_up(wg_id): ... +def wg_down(wg_id): ... +#+end_src + +--- + +* eBPF Steering (Planned) +The manager will attach an eBPF program to the subu’s cgroup that: + +- Hooks =connect()=, =bind()=, =sendmsg()= +- Forces =SO_BINDTOIFINDEX=subu_= for all sockets created by the subu +- Guarantees all UID traffic egresses through its WG interface +- Reuses kernel routing for MTU/GSO logic, but overrides device binding + +** Implementation Sketch +#+begin_src python +def attach_egress_bpf(subu_id, ifindex): + # load compiled eBPF ELF (bpf_prog_load) + # attach to cgroup of the subu user (BPF_PROG_ATTACH) + pass +#+end_src + +--- + +* Options and Policies + +Options are persisted flags controlling runtime behavior. + +| Option Name | Purpose | Default | +|--------------+----------+----------| +| =local_forwarding= | Enable 127/8 forwarding to WG peer | off | +| =steer_enabled= | Enable cgroup eBPF steering | on | + +** Implementation +#+begin_src python +def option_add(subu_id, name): + set_option(subu_id, name, "1") + +def option_remove(subu_id, name): + db.execute("DELETE FROM options WHERE subu_id=? AND name=?", ...) +#+end_src + +--- + +* Command Examples + +#+begin_example +# 0) Initialize +CLI.py init +# -> creates ./subu.db + +# 1) Create first subu +CLI.py make Thomas US +# -> user Thomas_US, netns ns-subu_0 + +# 2) Create hierarchical subu +CLI.py make Thomas new-subu Rabbit +# -> requires Thomas_new-subu exists + +# 3) Bring network up +CLI.py network up subu_0 + +# 4) Create WireGuard pool and object +CLI.py WG global 192.168.112.0/24 +CLI.py WG create ReasoningTechnology.com:51820 + +# 5) Attach and activate +CLI.py attach WG subu_0 WG_0 +CLI.py WG up WG_0 + +# 6) Inspect +CLI.py info subu_0 +CLI.py option list subu_0 +#+end_example + +--- + +* Future Work +1. 127/8 forwarding rewrite & mapping +2. Server-side sifter for mapped local addresses +3. GUI configuration (subu-light control panel) +4. BPF loader / verifier integration +5. Persistent daemon mode for live control +6. Automated namespace cleanup and audit +7. JSON-RPC or REST management API diff --git a/developer/manager/test.sh b/tester/manager/test.sh similarity index 100% rename from developer/manager/test.sh rename to tester/manager/test.sh diff --git a/developer/manager/test_0.sh b/tester/manager/test_0.sh similarity index 100% rename from developer/manager/test_0.sh rename to tester/manager/test_0.sh diff --git a/developer/manager/test_0_expected.sh b/tester/manager/test_0_expected.sh similarity index 100% rename from developer/manager/test_0_expected.sh rename to tester/manager/test_0_expected.sh diff --git a/tester/test.sh b/tester/test.sh new file mode 100644 index 0000000..706250b --- /dev/null +++ b/tester/test.sh @@ -0,0 +1,13 @@ +#!/bin/env bash + +set -x +./CLI # -> USAGE (exit 0) +./CLI usage # -> USAGE +./CLI -h # -> HELP +./CLI --help # -> HELP +./CLI help # -> HELP +./CLI help WG # -> WG topic help (or full HELP if topic unknown) +./CLI example # -> EXAMPLE +./CLI version # -> 0.1.4 +./CLI -V # -> 0.1.4 + diff --git a/tester/test_0.sh b/tester/test_0.sh new file mode 100755 index 0000000..ac354d3 --- /dev/null +++ b/tester/test_0.sh @@ -0,0 +1,11 @@ +set -x +./subu.py # -> USAGE (exit 0) +./subu.py usage # -> USAGE +./subu.py -h # -> HELP +./subu.py --help # -> HELP +./subu.py help # -> HELP +./subu.py help WG # -> WG topic help (or full HELP if topic unknown) +./subu.py example # -> EXAMPLE +./subu.py version # -> 0.1.4 +./subu.py -V # -> 0.1.4 +set +x diff --git a/tester/test_0_expected.sh b/tester/test_0_expected.sh new file mode 100644 index 0000000..8e31ed5 --- /dev/null +++ b/tester/test_0_expected.sh @@ -0,0 +1,353 @@ +++ ./subu.py +usage: subu [-V] [] + +Quick verbs: + usage Show this usage summary + help [topic] Detailed help; same as -h / --help + example End-to-end example session + version Print version + +Main verbs: + init Initialize a new subu database (refuses if it exists) + create Create a minimal subu record (defaults only) + info | information Show details for a subu + WG WireGuard object operations + attach Attach a WG object to a subu (netns + cgroup/eBPF) + detach Detach WG from a subu + network Bring all attached ifaces up/down inside the subu netns + lo Bring loopback up/down inside the subu netns + option Persisted options (list/set/get for future policy) + exec Run a command inside the subu netns + +Tip: `subu help` (or `subu --help`) shows detailed help; `subu help WG` shows topic help. +++ ./subu.py usage +usage: subu [-V] [] + +Quick verbs: + usage Show this usage summary + help [topic] Detailed help; same as -h / --help + example End-to-end example session + version Print version + +Main verbs: + init Initialize a new subu database (refuses if it exists) + create Create a minimal subu record (defaults only) + info | information Show details for a subu + WG WireGuard object operations + attach Attach a WG object to a subu (netns + cgroup/eBPF) + detach Detach WG from a subu + network Bring all attached ifaces up/down inside the subu netns + lo Bring loopback up/down inside the subu netns + option Persisted options (list/set/get for future policy) + exec Run a command inside the subu netns + +Tip: `subu help` (or `subu --help`) shows detailed help; `subu help WG` shows topic help. +++ ./subu.py -h +subu — manage subu containers, namespaces, and WG attachments + +2.1 Core + + subu init + Create ./subu.db (tables: subu, wg, links, options, state). + Requires a 6-char token (e.g., dzkq7b). Refuses if DB already exists. + + subu create + Make a default subu with netns ns- containing lo only (down). + Returns subu_N. + + subu list + Columns: Subu_ID, Owner, Name, NetNS, WG_Attached?, Up/Down, Steer? + + subu info | subu information + Full record + attached WG(s) + options + iface states. + +2.2 Loopback + + subu lo up | subu lo down + Toggle loopback inside the subu’s netns. + +2.3 WireGuard objects (independent) + + subu WG global + e.g., 192.168.112.0/24; allocator hands out /32 peers sequentially. + Shows current base and next free on success. + + subu WG create + Creates WG object; allocates next /32 local IP; AllowedIPs=0.0.0.0/0. + Returns WG_M. + + subu WG server_provided_public_key + Stores server’s pubkey. + + subu WG info | subu WG information + Endpoint, allocated IP, pubkey set?, link state (admin/oper). + +2.4 Link WG ↔ subu, bring up/down + + subu attach WG + Creates/configures WG device inside ns-: + - device name: subu_ (M from WG_ID) + - set local /32, MTU 1420, accept_local=1 + - (no default route is added — steering uses eBPF) + - v1: enforce one WG per Subu; error if another attached + + subu detach WG + Remove WG device/config from the subu’s netns; keep WG object. + + subu WG up | subu WG down + Toggle interface admin state in the subu’s netns (must be attached). + + subu network up | subu network down + Only toggles admin state for all attached ifaces. On “up”, loopback + is brought up first automatically. No route manipulation. + +2.5 Execution & (future) steering + + subu exec -- … + Run a process inside the subu’s netns. + + subu steer enable | subu steer disable + (Future) Attach/detach eBPF cgroup programs to force SO_BINDTOIFINDEX=subu_ + for TCP/UDP. Default: disabled. + +2.6 Options (persist only, for future policy) + + subu option list + subu option get [name] + subu option set + +2.7 Meta + + subu usage + Short usage summary (also printed when no args are given). + + subu help [topic] + This help (or per-topic help such as `subu help WG`). + + subu example + A concrete end-to-end scenario. + + subu version + Print version (same as -V / --version). +++ ./subu.py --help +subu — manage subu containers, namespaces, and WG attachments + +2.1 Core + + subu init + Create ./subu.db (tables: subu, wg, links, options, state). + Requires a 6-char token (e.g., dzkq7b). Refuses if DB already exists. + + subu create + Make a default subu with netns ns- containing lo only (down). + Returns subu_N. + + subu list + Columns: Subu_ID, Owner, Name, NetNS, WG_Attached?, Up/Down, Steer? + + subu info | subu information + Full record + attached WG(s) + options + iface states. + +2.2 Loopback + + subu lo up | subu lo down + Toggle loopback inside the subu’s netns. + +2.3 WireGuard objects (independent) + + subu WG global + e.g., 192.168.112.0/24; allocator hands out /32 peers sequentially. + Shows current base and next free on success. + + subu WG create + Creates WG object; allocates next /32 local IP; AllowedIPs=0.0.0.0/0. + Returns WG_M. + + subu WG server_provided_public_key + Stores server’s pubkey. + + subu WG info | subu WG information + Endpoint, allocated IP, pubkey set?, link state (admin/oper). + +2.4 Link WG ↔ subu, bring up/down + + subu attach WG + Creates/configures WG device inside ns-: + - device name: subu_ (M from WG_ID) + - set local /32, MTU 1420, accept_local=1 + - (no default route is added — steering uses eBPF) + - v1: enforce one WG per Subu; error if another attached + + subu detach WG + Remove WG device/config from the subu’s netns; keep WG object. + + subu WG up | subu WG down + Toggle interface admin state in the subu’s netns (must be attached). + + subu network up | subu network down + Only toggles admin state for all attached ifaces. On “up”, loopback + is brought up first automatically. No route manipulation. + +2.5 Execution & (future) steering + + subu exec -- … + Run a process inside the subu’s netns. + + subu steer enable | subu steer disable + (Future) Attach/detach eBPF cgroup programs to force SO_BINDTOIFINDEX=subu_ + for TCP/UDP. Default: disabled. + +2.6 Options (persist only, for future policy) + + subu option list + subu option get [name] + subu option set + +2.7 Meta + + subu usage + Short usage summary (also printed when no args are given). + + subu help [topic] + This help (or per-topic help such as `subu help WG`). + + subu example + A concrete end-to-end scenario. + + subu version + Print version (same as -V / --version). +++ ./subu.py help +subu — manage subu containers, namespaces, and WG attachments + +2.1 Core + + subu init + Create ./subu.db (tables: subu, wg, links, options, state). + Requires a 6-char token (e.g., dzkq7b). Refuses if DB already exists. + + subu create + Make a default subu with netns ns- containing lo only (down). + Returns subu_N. + + subu list + Columns: Subu_ID, Owner, Name, NetNS, WG_Attached?, Up/Down, Steer? + + subu info | subu information + Full record + attached WG(s) + options + iface states. + +2.2 Loopback + + subu lo up | subu lo down + Toggle loopback inside the subu’s netns. + +2.3 WireGuard objects (independent) + + subu WG global + e.g., 192.168.112.0/24; allocator hands out /32 peers sequentially. + Shows current base and next free on success. + + subu WG create + Creates WG object; allocates next /32 local IP; AllowedIPs=0.0.0.0/0. + Returns WG_M. + + subu WG server_provided_public_key + Stores server’s pubkey. + + subu WG info | subu WG information + Endpoint, allocated IP, pubkey set?, link state (admin/oper). + +2.4 Link WG ↔ subu, bring up/down + + subu attach WG + Creates/configures WG device inside ns-: + - device name: subu_ (M from WG_ID) + - set local /32, MTU 1420, accept_local=1 + - (no default route is added — steering uses eBPF) + - v1: enforce one WG per Subu; error if another attached + + subu detach WG + Remove WG device/config from the subu’s netns; keep WG object. + + subu WG up | subu WG down + Toggle interface admin state in the subu’s netns (must be attached). + + subu network up | subu network down + Only toggles admin state for all attached ifaces. On “up”, loopback + is brought up first automatically. No route manipulation. + +2.5 Execution & (future) steering + + subu exec -- … + Run a process inside the subu’s netns. + + subu steer enable | subu steer disable + (Future) Attach/detach eBPF cgroup programs to force SO_BINDTOIFINDEX=subu_ + for TCP/UDP. Default: disabled. + +2.6 Options (persist only, for future policy) + + subu option list + subu option get [name] + subu option set + +2.7 Meta + + subu usage + Short usage summary (also printed when no args are given). + + subu help [topic] + This help (or per-topic help such as `subu help WG`). + + subu example + A concrete end-to-end scenario. + + subu version + Print version (same as -V / --version). +++ ./subu.py help WG +usage: subu WG [-h] + +options: + -h, --help show this help message and exit +++ ./subu.py example +# 0) Safe init (refuses if ./subu.db exists) +subu init dzkq7b +# -> created ./subu.db + +# 1) Create Subu +subu create Thomas US +# -> Subu_ID: subu_7 +# -> netns: ns-subu_7 with lo (down) + +# 2) Define WG pool (once per host) +subu WG global 192.168.112.0/24 +# -> base set; next free: 192.168.112.2/32 + +# 3) Create WG object with endpoint +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 +subu WG server_provided_public_key WG_0 ABCDEFG...xyz= +# -> saved + +# 5) Attach WG to Subu (device created/configured in ns) +subu attach WG subu_7 WG_0 +# -> device ns-subu_7/subu_0 configured (no default route) + +# 6) Bring network up (lo first, then attached ifaces) +subu network up subu_7 +# -> lo up; subu_0 admin up + +# 7) Start the WG engine inside the netns +subu WG up WG_0 +# -> up, handshakes should start + +# 8) Test from inside the subu +subu exec subu_7 -- curl -4v https://ifconfig.me +++ ./subu.py version +0.1.3 +++ ./subu.py -V +0.1.3 +++ set +x -- 2.20.1