From: Thomas Walker Lynch Date: Tue, 4 Nov 2025 07:15:11 +0000 (+0000) Subject: structureing the code X-Git-Url: https://git.reasoningtechnology.com/style/static/git-logo.png?a=commitdiff_plain;h=e501638678cc132685bd515e65f4d2c72d890631;p=subu structureing the code --- diff --git a/developer/manager.tgz b/developer/manager.tgz new file mode 100644 index 0000000..f802d65 Binary files /dev/null and b/developer/manager.tgz differ diff --git a/developer/manager/bpf.py b/developer/manager/bpf.py new file mode 100644 index 0000000..527d419 --- /dev/null +++ b/developer/manager/bpf.py @@ -0,0 +1,52 @@ + +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}") + diff --git a/developer/manager/bpf_worker.py b/developer/manager/bpf_worker.py new file mode 100644 index 0000000..96aef14 --- /dev/null +++ b/developer/manager/bpf_worker.py @@ -0,0 +1,78 @@ +# -*- 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 +""" +import os, subprocess, json +from pathlib import Path + +class BpfError(RuntimeError): pass + +def run(cmd, check=True): + r = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + if check and r.returncode != 0: + raise BpfError(f"cmd failed: {' '.join(cmd)}\n{r.stderr}") + return r.stdout.strip() + +def ensure_mounts(): + # ensure bpf and cgroup v2 are mounted + try: + Path("/sys/fs/bpf").mkdir(parents=True, exist_ok=True) + run(["mount","-t","bpf","bpf","/sys/fs/bpf"], check=False) + except Exception: + pass + try: + Path("/sys/fs/cgroup").mkdir(parents=True, exist_ok=True) + run(["mount","-t","cgroup2","none","/sys/fs/cgroup"], check=False) + except Exception: + pass + +def cgroup_path(subu_id: str) -> str: + return f"/sys/fs/cgroup/{subu_id}" + +def install_steering(subu_id: str, netns: str, ifname: str): + ensure_mounts() + cg = Path(cgroup_path(subu_id)) + cg.mkdir(parents=True, exist_ok=True) + + # compile BPF + obj = Path("./bpf_force_egress.o") + src = Path("./bpf_force_egress.c") + if not src.exists(): + raise BpfError("bpf_force_egress.c missing next to manager") + + # Build object (requires clang/llc/bpftool) + run(["clang","-O2","-g","-target","bpf","-c",str(src),"-o",str(obj)]) + + # Load program into bpffs; attach to cgroup/inet4_connect + inet4_post_bind (MVP) + pinned = f"/sys/fs/bpf/{subu_id}_egress" + run(["bpftool","prog","loadall",str(obj),pinned], check=True) + + # Attach to hooks (MVP validation hooks) + # NOTE: these are safe no-ops for now; they validate UID and stash ifindex map. + for hook in ("cgroup/connect4","cgroup/post_bind4"): + run(["bpftool","cgroup","attach",cgroup_path(subu_id),"attach",hook,"pinned",f"{pinned}/prog_0"], check=False) + + # Write metadata for ifname (saved for future prog versions) + meta = {"ifname": ifname} + Path(f"/sys/fs/bpf/{subu_id}_meta.json").write_text(json.dumps(meta)) + +def remove_steering(subu_id: str): + cg = cgroup_path(subu_id) + # Detach whatever is attached + for hook in ("cgroup/connect4","cgroup/post_bind4"): + subprocess.run(["bpftool","cgroup","detach",cg,"detach",hook], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + # Remove pinned prog dir + pinned = Path(f"/sys/fs/bpf/{subu_id}_egress") + if pinned.exists(): + subprocess.run(["bpftool","prog","detach",str(pinned)], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + try: + for p in pinned.glob("*"): p.unlink() + pinned.rmdir() + except Exception: + pass + # Remove cgroup dir + try: + Path(cg).rmdir() + except Exception: + pass 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/test.sh b/developer/manager/test.sh deleted file mode 100644 index 706250b..0000000 --- a/developer/manager/test.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/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/developer/manager/test_0.sh b/developer/manager/test_0.sh deleted file mode 100755 index ac354d3..0000000 --- a/developer/manager/test_0.sh +++ /dev/null @@ -1,11 +0,0 @@ -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/developer/manager/test_0_expected.sh b/developer/manager/test_0_expected.sh deleted file mode 100644 index 8e31ed5..0000000 --- a/developer/manager/test_0_expected.sh +++ /dev/null @@ -1,353 +0,0 @@ -++ ./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 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/developer/manager/worker_bpf.py b/developer/manager/worker_bpf.py deleted file mode 100644 index 96aef14..0000000 --- a/developer/manager/worker_bpf.py +++ /dev/null @@ -1,78 +0,0 @@ -# -*- 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 -""" -import os, subprocess, json -from pathlib import Path - -class BpfError(RuntimeError): pass - -def run(cmd, check=True): - r = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) - if check and r.returncode != 0: - raise BpfError(f"cmd failed: {' '.join(cmd)}\n{r.stderr}") - return r.stdout.strip() - -def ensure_mounts(): - # ensure bpf and cgroup v2 are mounted - try: - Path("/sys/fs/bpf").mkdir(parents=True, exist_ok=True) - run(["mount","-t","bpf","bpf","/sys/fs/bpf"], check=False) - except Exception: - pass - try: - Path("/sys/fs/cgroup").mkdir(parents=True, exist_ok=True) - run(["mount","-t","cgroup2","none","/sys/fs/cgroup"], check=False) - except Exception: - pass - -def cgroup_path(subu_id: str) -> str: - return f"/sys/fs/cgroup/{subu_id}" - -def install_steering(subu_id: str, netns: str, ifname: str): - ensure_mounts() - cg = Path(cgroup_path(subu_id)) - cg.mkdir(parents=True, exist_ok=True) - - # compile BPF - obj = Path("./bpf_force_egress.o") - src = Path("./bpf_force_egress.c") - if not src.exists(): - raise BpfError("bpf_force_egress.c missing next to manager") - - # Build object (requires clang/llc/bpftool) - run(["clang","-O2","-g","-target","bpf","-c",str(src),"-o",str(obj)]) - - # Load program into bpffs; attach to cgroup/inet4_connect + inet4_post_bind (MVP) - pinned = f"/sys/fs/bpf/{subu_id}_egress" - run(["bpftool","prog","loadall",str(obj),pinned], check=True) - - # Attach to hooks (MVP validation hooks) - # NOTE: these are safe no-ops for now; they validate UID and stash ifindex map. - for hook in ("cgroup/connect4","cgroup/post_bind4"): - run(["bpftool","cgroup","attach",cgroup_path(subu_id),"attach",hook,"pinned",f"{pinned}/prog_0"], check=False) - - # Write metadata for ifname (saved for future prog versions) - meta = {"ifname": ifname} - Path(f"/sys/fs/bpf/{subu_id}_meta.json").write_text(json.dumps(meta)) - -def remove_steering(subu_id: str): - cg = cgroup_path(subu_id) - # Detach whatever is attached - for hook in ("cgroup/connect4","cgroup/post_bind4"): - subprocess.run(["bpftool","cgroup","detach",cg,"detach",hook], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) - # Remove pinned prog dir - pinned = Path(f"/sys/fs/bpf/{subu_id}_egress") - if pinned.exists(): - subprocess.run(["bpftool","prog","detach",str(pinned)], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) - try: - for p in pinned.glob("*"): p.unlink() - pinned.rmdir() - except Exception: - pass - # Remove cgroup dir - try: - Path(cg).rmdir() - except Exception: - pass 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/tester/manager/test.sh b/tester/manager/test.sh new file mode 100644 index 0000000..706250b --- /dev/null +++ b/tester/manager/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/manager/test_0.sh b/tester/manager/test_0.sh new file mode 100755 index 0000000..ac354d3 --- /dev/null +++ b/tester/manager/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/manager/test_0_expected.sh b/tester/manager/test_0_expected.sh new file mode 100644 index 0000000..8e31ed5 --- /dev/null +++ b/tester/manager/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 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