From: Thomas Walker Lynch Date: Sat, 1 Nov 2025 17:46:46 +0000 (+0000) Subject: adding subu manager X-Git-Url: https://git.reasoningtechnology.com/usr/lib/python2.7/inspect.py?a=commitdiff_plain;h=4aa9c549132947cff4278d0fab1633184f6e0629;p=subu adding subu manager --- diff --git a/developer/source/manager/subu.py b/developer/source/manager/subu.py new file mode 100755 index 0000000..92aa8e7 --- /dev/null +++ b/developer/source/manager/subu.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 +# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*- + +""" +subu.py — CLI only. +- No-args prints USAGE. +- `help` / `usage` / `example` / `version` are handled *before* argparse. +- `-h` / `--help` are mapped to `help`. +- Delegates real work to subu_core.dispatch(args). +""" + +from __future__ import annotations +import argparse +import sys + +try: + from subu_version import VERSION +except Exception: + VERSION = "0.0.0-unknown" + +try: + from subu_text import USAGE, HELP, EXAMPLE +except Exception: + USAGE = "usage: subu [args]\n" + HELP = "help text unavailable (subu_text import failed)\n" + EXAMPLE = "example text unavailable (subu_text import failed)\n" + +# ------------------------------- +# Parser construction (verbs that do real work) +# ------------------------------- +def _build_parser() -> argparse.ArgumentParser: + # add_help=False so -h/--help don't get auto-bound; we intercept them manually + p = argparse.ArgumentParser( + prog="subu", + description="Manage subu containers, namespaces, and WireGuard attachments.", + add_help=False, + ) + # keep -V only; -h/--help are handled by pre-parse + p.add_argument("-V", "--version", action="store_true", + help="Print version and exit.") + + sub = p.add_subparsers(dest="verb", + metavar="{init,create,info,information,WG,attach,detach,network,lo,option,exec}", + required=False) + + sub.add_parser("init", help="Initialize new subu database (refuses if exists).") + sub.add_parser("create", help="Create a subu (defaults only).") + sub.add_parser("info", help="Show info about a subu.") + sub.add_parser("information", help="Alias of 'info'.") + sub.add_parser("WG", help="WireGuard operations.") + sub.add_parser("attach", help="Attach WG to subu (netns + cgroup/eBPF).") + sub.add_parser("detach", help="Detach WG from subu.") + sub.add_parser("network", help="Bring attached ifaces up/down in the subu netns.") + sub.add_parser("lo", help="Bring loopback up/down in the subu netns.") + sub.add_parser("option", help="Persisted options (list/get/set).") + sub.add_parser("exec", help="Execute a command inside the subu netns: subu exec -- ") + + return p + +def _print_topic_help(parser: argparse.ArgumentParser, topic: str) -> bool: + """Try to print help for a specific subparser topic. Returns True if found.""" + for action in getattr(parser, "_subparsers", [])._actions: + if isinstance(action, argparse._SubParsersAction): + if topic in action.choices: + action.choices[topic].print_help() + return True + if topic == "information" and "info" in action.choices: + action.choices["info"].print_help() + return True + return False + +# ------------------------------- +# CLI entry (parse only) +# ------------------------------- +def CLI(argv=None) -> int: + argv = sys.argv[1:] if argv is None else argv + parser = _build_parser() + + # 0) No args => USAGE + if not argv: + sys.stdout.write(USAGE) + return 0 + + # 1) Pre-parse intercepts (robust vs. argparse) + first = argv[0] + if first in ("-h", "--help", "help"): + topic = argv[1] if len(argv) > 1 and argv[0] == "help" else None + if topic: + # Topic-aware help if possible; else fall back to full HELP + if not _print_topic_help(parser, topic): + sys.stdout.write(HELP) + else: + sys.stdout.write(HELP) + return 0 + + if first in ("usage",): + sys.stdout.write(USAGE) + return 0 + + if first in ("example",): + sys.stdout.write(EXAMPLE) + return 0 + + if first in ("version",): + print(VERSION) + return 0 + + # 2) Normal parse + try: + args = parser.parse_args(argv) + except SystemExit as e: + return int(e.code) + + # 3) Global -V/--version + if getattr(args, "version", False): + print(VERSION) + return 0 + + # 4) Delegate to worker layer + try: + from subu_core import dispatch # type: ignore + except Exception as e: + sys.stderr.write(f"subu: internal error: cannot import subu_core.dispatch: {e}\n") + return 1 + + try: + rc = dispatch(args) + return int(rc) if rc is not None else 0 + except KeyboardInterrupt: + return 130 + except SystemExit as e: + return int(e.code) + except Exception as e: + sys.stderr.write(f"subu: error: {e}\n") + return 1 + +if __name__ == "__main__": + sys.exit(CLI()) diff --git a/developer/source/manager/subu_BPF_force_egress.c b/developer/source/manager/subu_BPF_force_egress.c new file mode 100644 index 0000000..15a0085 --- /dev/null +++ b/developer/source/manager/subu_BPF_force_egress.c @@ -0,0 +1,44 @@ +// -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- +// eBPF: force sockets inside this cgroup to use a specific ifindex +// Hooks: cgroup/connect4 and cgroup/sendmsg4 +// Logic: read ifindex from array map[0], then setsockopt(SO_BINDTOIFINDEX) + +#include +#include +#include + +struct { + __uint(type, BPF_MAP_TYPE_ARRAY); + __uint(max_entries, 1); + __type(key, __u32); + __type(value, __u32); // ifindex + __uint(pinning, LIBBPF_PIN_BY_NAME); +} force_ifindex_map SEC(".maps"); + +static __always_inline int force_bind(struct bpf_sock_addr *ctx) +{ + __u32 k = 0; + __u32 *ifx = bpf_map_lookup_elem(&force_ifindex_map, &k); + if (!ifx || !*ifx) + return 1; // allow pass-through if not configured + + int val = (int)*ifx; + // This sets sk->sk_bound_dev_if equivalently to userland SO_BINDTOIFINDEX. + // Ignore return (verifier- & failure-friendly). + (void)bpf_setsockopt(ctx, SOL_SOCKET, SO_BINDTOIFINDEX, &val, sizeof(val)); + return 1; +} + +SEC("cgroup/connect4") +int force_dev_connect4(struct bpf_sock_addr *ctx) +{ + return force_bind(ctx); +} + +SEC("cgroup/sendmsg4") +int force_dev_sendmsg4(struct bpf_sock_addr *ctx) +{ + return force_bind(ctx); +} + +char _license[] SEC("license") = "GPL"; diff --git a/developer/source/manager/subu_bpf.py b/developer/source/manager/subu_bpf.py new file mode 100644 index 0000000..d698a9a --- /dev/null +++ b/developer/source/manager/subu_bpf.py @@ -0,0 +1,25 @@ +# ===== File: subu_bpf.py ===== +#!/usr/bin/env python3 +# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*- + +""" +Stub for eBPF steering (cgroup/connect4+sendmsg4 hooks) to enforce sk_bound_dev_if. +Implementation notes: + * We will later compile a small eBPF C program (libbpf/bpftool) that: + - on connect4/sendmsg4: if process UID==subu UID -> sets sk_bound_dev_if to WG ifindex + * For now, we provide placeholders that pretend success. +""" + +import subu_utils as U +import subu_db as DB + + +def install_steer(subu_id: str, wg_ifindex: int): + # TODO: load BPF, attach to cgroup v2 path; store cgroup path in DB + DB.update_subu_cgroup(subu_id, "/sys/fs/cgroup/subu_placeholder") + return 0 + + +def remove_steer(subu_id: str): + # TODO: detach and unload + return 0 diff --git a/developer/source/manager/subu_core.py b/developer/source/manager/subu_core.py new file mode 100644 index 0000000..45ab787 --- /dev/null +++ b/developer/source/manager/subu_core.py @@ -0,0 +1,235 @@ +# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*- +""" +subu_core.py — main worker layer for Subu management +Version 0.1.6 +""" + +import os, sqlite3, subprocess +from pathlib import Path +from contextlib import closing +from subu_worker_bpf import install_steering, remove_steering, BpfError + +DB_FILE = Path("./subu.db") + +# --------------------------------------------------------------------- +# SQLite helpers +# --------------------------------------------------------------------- + +def db_connect(): + if not DB_FILE.exists(): + raise FileNotFoundError("subu.db not found; run `subu init ` first") + return sqlite3.connect(DB_FILE) + +def db_init(): + if DB_FILE.exists(): + raise FileExistsError("Database already exists") + 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("✅ subu.db created") + +# --------------------------------------------------------------------- +# System helpers +# --------------------------------------------------------------------- + +def run(cmd, check=True): + r = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + if check and r.returncode != 0: + raise RuntimeError(f"cmd failed: {' '.join(cmd)}\n{r.stderr}") + return r.stdout.strip() + +def create_netns(nsname: str): + run(["ip", "netns", "add", nsname]) + run(["ip", "-n", nsname, "link", "set", "lo", "down"]) + return nsname + +def delete_netns(nsname: str): + run(["ip", "netns", "delete", nsname], check=False) + +def ifindex_in_netns(nsname: str, ifname: str) -> int: + out = run(["ip", "-n", nsname, "-o", "link", "show", ifname]) + return int(out.split(":", 1)[0]) + +# --------------------------------------------------------------------- +# Subu operations +# --------------------------------------------------------------------- + +def create_subu(owner: str, name: str) -> str: + with closing(db_connect()) as db: + c = db.cursor() + c.execute("INSERT INTO subu (owner, name, netns) VALUES (?, ?, ?)", + (owner, name, f"ns-{owner}-{name}")) + subu_id = c.lastrowid + db.commit() + nsname = f"ns-subu_{subu_id}" + create_netns(nsname) + print(f"Created subu_{subu_id} ({owner}:{name}) with netns {nsname}") + return f"subu_{subu_id}" + +def list_subu(): + with closing(db_connect()) 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_connect()) as db: + for row in db.execute("SELECT * FROM subu WHERE id=?", (sid,)): + print(row) + +def lo_toggle(subu_id: str, state: str): + sid = int(subu_id.split("_")[1]) + with closing(db_connect()) as db: + row = db.execute("SELECT netns FROM subu WHERE id=?", (sid,)).fetchone() + if not row: raise ValueError("subu not found") + ns = row[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"loopback {state} in {subu_id}") + +# --------------------------------------------------------------------- +# WireGuard operations +# --------------------------------------------------------------------- + +def wg_global(basecidr: str): + Path("./WG_GLOBAL").write_text(basecidr.strip() + "\n") + print(f"Base CIDR set to {basecidr}") + +def wg_create(endpoint: str) -> str: + base = Path("./WG_GLOBAL").read_text().strip() if Path("./WG_GLOBAL").exists() else None + if not base: + raise RuntimeError("No WG global base; set with `subu WG global`") + with closing(db_connect()) as db: + c = db.cursor() + # trivial allocator: next /32 by count + idx = c.execute("SELECT COUNT(*) FROM wg").fetchone()[0] + octets = base.split(".") + octets[3] = str(2 + idx) + local_ip = ".".join(octets) + "/32" + 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"Created WG_{wid} ({endpoint}) local_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_connect()) as db: + db.execute("UPDATE wg SET pubkey=? WHERE id=?", (key, wid)) + db.commit() + print(f"Public key stored for {wg_id}") + +def wg_info(wg_id: str): + wid = int(wg_id.split("_")[1]) + with closing(db_connect()) as db: + row = db.execute("SELECT * FROM wg WHERE id=?", (wid,)).fetchone() + if not row: print("WG not found") + else: print(row) + +# --------------------------------------------------------------------- +# Attach / Detach with eBPF steering +# --------------------------------------------------------------------- + +def attach_wg(subu_id: str, wg_id: str): + sid = int(subu_id.split("_")[1]) + wid = int(wg_id.split("_")[1]) + wg_ifname = f"subu_{wid}" + netns = f"ns-{subu_id}" + + # Create WG device inside namespace + run(["ip", "link", "add", wg_ifname, "type", "wireguard"]) + run(["ip", "link", "set", wg_ifname, "netns", netns]) + # Configure MTU + accept_local + run(["ip", "-n", netns, "link", "set", wg_ifname, "mtu", "1420"]) + run(["ip", "-n", netns, "link", "set", "dev", wg_ifname, "up"]) + print(f"Attached {wg_id} as {wg_ifname} inside {netns}") + + # Install steering + try: + install_steering(subu_id, netns, wg_ifname) + print(f"Installed eBPF steering for {subu_id} via {wg_ifname}") + except BpfError as e: + print(f"warning: steering failed: {e}") + + # Update DB linkage + with closing(db_connect()) as db: + db.execute("UPDATE subu SET wg_id=? WHERE id=?", (wid, sid)) + db.commit() + +def detach_wg(subu_id: str): + sid = int(subu_id.split("_")[1]) + with closing(db_connect()) as db: + row = db.execute("SELECT wg_id, netns FROM subu WHERE id=?", (sid,)).fetchone() + if not row or row[0] is None: + print("nothing attached") + return + wid, ns = row + wg_ifname = f"subu_{wid}" + run(["ip", "-n", ns, "link", "del", wg_ifname], check=False) + db.execute("UPDATE subu SET wg_id=NULL WHERE id=?", (sid,)) + db.commit() + try: + remove_steering(subu_id) + print(f"Removed steering for {subu_id}") + except BpfError as e: + print(f"warning: remove steering failed: {e}") + +# --------------------------------------------------------------------- +# Network up/down aggregate +# --------------------------------------------------------------------- + +def network_toggle(subu_id: str, state: str): + sid = int(subu_id.split("_")[1]) + with closing(db_connect()) as db: + row = db.execute("SELECT netns, wg_id FROM subu WHERE id=?", (sid,)).fetchone() + if not row: raise ValueError("subu not found") + ns, wid = row + # bring lo up first if needed + if state == "up": + run(["ip", "netns", "exec", ns, "ip", "link", "set", "lo", "up"], check=False) + # bring attached iface + if wid: + ifname = f"subu_{wid}" + run(["ip", "-n", ns, "link", "set", "dev", ifname, state], check=False) + with closing(db_connect()) as db: + db.execute("UPDATE subu SET network_state=? WHERE id=?", (state, sid)) + db.commit() + print(f"{subu_id}: network {state}") + +# --------------------------------------------------------------------- +# Exec inside namespace +# --------------------------------------------------------------------- + +def exec_in_subu(subu_id: str, cmd: list): + sid = int(subu_id.split("_")[1]) + with closing(db_connect()) as db: + ns = db.execute("SELECT netns FROM subu WHERE id=?", (sid,)).fetchone()[0] + full = ["ip", "netns", "exec", ns] + cmd + os.execvp(full[0], full) diff --git a/developer/source/manager/subu_db.py b/developer/source/manager/subu_db.py new file mode 100644 index 0000000..40c5f25 --- /dev/null +++ b/developer/source/manager/subu_db.py @@ -0,0 +1,176 @@ +# ===== File: subu_db.py ===== +#!/usr/bin/env python3 +# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*- + +import os, sqlite3, json +import subu_utils as U + +SCHEMA = { + "meta": "CREATE TABLE IF NOT EXISTS meta (k TEXT PRIMARY KEY, v TEXT)" , + "subu": """ + CREATE TABLE IF NOT EXISTS subu ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + masu TEXT NOT NULL, + subu TEXT NOT NULL, + uid INTEGER, + netns TEXT, + cgroup_path TEXT, + UNIQUE(masu, subu) + )""", + "wg": """ + CREATE TABLE IF NOT EXISTS wg ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + remote TEXT NOT NULL, + pubkey TEXT, + dev TEXT, + addr TEXT, + state TEXT DEFAULT 'down' + )""", + "attach": """ + CREATE TABLE IF NOT EXISTS attach ( + subu_id INTEGER NOT NULL, + wg_id INTEGER NOT NULL, + PRIMARY KEY (subu_id, wg_id) + )""", +} + +class NotInitializedError(Exception): + pass + + +def require_initialized(): + if not os.path.exists(U.path_db()): + raise NotInitializedError() + + +def connect(): + return sqlite3.connect(U.path_db()) + + +def init_db(): + if os.path.exists(U.path_db()): + return False + con = sqlite3.connect(U.path_db()) + try: + cur = con.cursor() + for sql in SCHEMA.values(): + cur.execute(sql) + con.commit() + return True + finally: + con.close() + + +def put_meta(k, v): + con = connect(); cur = con.cursor() + cur.execute("INSERT OR REPLACE INTO meta(k,v) VALUES(?,?)", (k, v)) + con.commit(); con.close() + +def get_meta(k, default=None): + con = connect(); cur = con.cursor() + cur.execute("SELECT v FROM meta WHERE k=?", (k,)) + row = cur.fetchone(); con.close() + return row[0] if row else default + + +def create_subu(masu, subu): + con = connect(); cur = con.cursor() + cur.execute("INSERT INTO subu(masu,subu) VALUES(?,?)", (masu, subu)) + con.commit() + sid = cur.lastrowid + con.close() + return f"subu_{sid}" + + +def list_subu(): + con = connect(); cur = con.cursor() + cur.execute("SELECT id,masu,subu,uid,netns,cgroup_path FROM subu ORDER BY id") + rows = cur.fetchall(); con.close(); return rows + + +def subu_by_id(subu_id): + if not subu_id.startswith("subu_"): + raise ValueError("bad subu id") + sid = int(subu_id.split("_")[1]) + con = connect(); cur = con.cursor() + cur.execute("SELECT id,masu,subu,uid,netns,cgroup_path FROM subu WHERE id=?", (sid,)) + row = cur.fetchone(); con.close(); return row + + +def update_subu_netns(subu_id, netns): + sid = int(subu_id.split("_")[1]) + con = connect(); cur = con.cursor() + cur.execute("UPDATE subu SET netns=? WHERE id=?", (netns, sid)) + con.commit(); con.close() + + +def update_subu_uid(subu_id, uid): + sid = int(subu_id.split("_")[1]) + con = connect(); cur = con.cursor() + cur.execute("UPDATE subu SET uid=? WHERE id=?", (uid, sid)) + con.commit(); con.close() + + +def update_subu_cgroup(subu_id, path): + sid = int(subu_id.split("_")[1]) + con = connect(); cur = con.cursor() + cur.execute("UPDATE subu SET cgroup_path=? WHERE id=?", (path, sid)) + con.commit(); con.close() + +# WG + +def wg_set_global_base(cidr): + put_meta("wg_base_cidr", cidr) + + +def wg_create(remote): + con = connect(); cur = con.cursor() + cur.execute("INSERT INTO wg(remote) VALUES(?)", (remote,)) + con.commit(); wid = cur.lastrowid; con.close(); return f"WG_{wid}" + + +def wg_list(): + con = connect(); cur = con.cursor() + cur.execute("SELECT id,remote,pubkey,dev,addr,state FROM wg ORDER BY id") + rows = cur.fetchall(); con.close(); return rows + + +def wg_by_id(wg_id): + if not wg_id.startswith("WG_"): + raise ValueError("bad WG id") + wid = int(wg_id.split("_")[1]) + con = connect(); cur = con.cursor() + cur.execute("SELECT id,remote,pubkey,dev,addr,state FROM wg WHERE id=?", (wid,)) + row = cur.fetchone(); con.close(); return row + + +def wg_update(wg_id, **kv): + wid = int(wg_id.split("_")[1]) + con = connect(); cur = con.cursor() + cols = ",".join([f"{k}=?" for k in kv.keys()]) + cur.execute(f"UPDATE wg SET {cols} WHERE id=?", [*kv.values(), wid]) + con.commit(); con.close() + + +def attach(subu_id, wg_id): + sid = int(subu_id.split("_")[1]) + wid = int(wg_id.split("_")[1]) + con = connect(); cur = con.cursor() + cur.execute("INSERT OR REPLACE INTO attach(subu_id,wg_id) VALUES(?,?)", (sid, wid)) + con.commit(); con.close() + + +def detach(subu_id, wg_id): + sid = int(subu_id.split("_")[1]) + wid = int(wg_id.split("_")[1]) + con = connect(); cur = con.cursor() + cur.execute("DELETE FROM attach WHERE subu_id=? AND wg_id=?", (sid, wid)) + con.commit(); con.close() + + +def attached_wg_ids(sid: int): + con = connect(); cur = con.cursor() + cur.execute("SELECT wg_id FROM attach WHERE subu_id=?", (sid,)) + rows = [f"WG_{r[0]}" for r in cur.fetchall()] + con.close(); return rows + diff --git a/developer/source/manager/subu_net.py b/developer/source/manager/subu_net.py new file mode 100644 index 0000000..86182e8 --- /dev/null +++ b/developer/source/manager/subu_net.py @@ -0,0 +1,60 @@ +# ===== File: subu_net.py ===== +#!/usr/bin/env python3 +# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*- + +import subu_utils as U +import subu_db as DB + + +def ensure_netns(ns): + # create netns if missing + rc, out = U.run("ip netns list", capture=True, check=False) + if ns not in out.split(): + U.run(f"ip netns add {ns}") + + +def lo(ns, action: str): + if action == "up": + U.run(f"ip -n {ns} link set lo up") + else: + U.run(f"ip -n {ns} link set lo down") + + +def cmd_lo(sid, action) -> int: + row = DB.subu_by_id(sid) + if not row: + return U.err("unknown subu id") + ns = row[4] or sid + ensure_netns(ns) + lo(ns, action) + return 0 + + +def cmd_network_up(sid) -> int: + row = DB.subu_by_id(sid) + if not row: + return U.err("unknown subu id") + ns = row[4] or sid + ensure_netns(ns) + lo(ns, "up") + # bring all attached WG up + sid_int = int(sid.split("_")[1]) + for wid in DB.attached_wg_ids(sid_int): + import subu_wg as WG + WG.cmd_wg_up(wid) + return U.ok(f"network up for {sid}") + + +def cmd_network_down(sid) -> int: + row = DB.subu_by_id(sid) + if not row: + return U.err("unknown subu id") + ns = row[4] or sid + # bring attached WG down first + sid_int = int(sid.split("_")[1]) + for wid in DB.attached_wg_ids(sid_int): + import subu_wg as WG + WG.cmd_wg_down(wid) + # leave lo state alone per spec (no warning here) + return U.ok(f"network down for {sid}") + diff --git a/developer/source/manager/subu_text.py b/developer/source/manager/subu_text.py new file mode 100644 index 0000000..88434e4 --- /dev/null +++ b/developer/source/manager/subu_text.py @@ -0,0 +1,152 @@ +# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*- + +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 + install steering) + detach Detach WG from a subu (remove steering) + network Bring 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. +""" + +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. + + 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 + - installs eBPF steering (force egress via this device) automatically + + subu detach WG + Remove WG device/config from the subu’s netns and remove steering; keep WG object. + + subu WG up | subu WG down + Toggle interface admin state in the subu’s netns (must be attached). + On “up”, warn if loopback is currently down in that netns. + + 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 + + subu exec -- … + Run a process inside the subu’s netns. + +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). +""" + +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 + steering) +subu attach WG subu_7 WG_0 +# -> device ns-subu_7/subu_0 configured (no default route) +# -> steering installed: egress forced via subu_0 + +# 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 (warn if lo was down) + +# 8) Test from inside the subu +subu exec subu_7 -- curl -4v https://ifconfig.me +""" diff --git a/developer/source/manager/subu_utils.py b/developer/source/manager/subu_utils.py new file mode 100644 index 0000000..3594c1c --- /dev/null +++ b/developer/source/manager/subu_utils.py @@ -0,0 +1,34 @@ +# ===== File: subu_utils.py ===== +#!/usr/bin/env python3 +# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*- + +import os, sys, subprocess, shlex + + +def ok(msg: str, code: int = 0) -> int: + print(msg) + return code + +def err(msg: str, code: int = 2) -> int: + print(f"❌ {msg}") + return code + + +def run(cmd: str, check=True, capture=False, env=None, ns_enter=None): + """Run shell command. If ns_enter is a netns name, prefix with `ip netns exec`. + Returns (rc, outstr). + """ + if ns_enter: + cmd = f"ip netns exec {shlex.quote(ns_enter)} {cmd}" + p = subprocess.run(cmd, shell=True, env=env, + stdout=subprocess.PIPE if capture else None, + stderr=subprocess.STDOUT) + out = p.stdout.decode() if p.stdout else "" + if check and p.returncode != 0: + raise RuntimeError(f"command failed ({p.returncode}): {cmd}\n{out}") + return p.returncode, out + + +def path_db(): + return os.path.abspath("subu.db") + diff --git a/developer/source/manager/subu_version.py b/developer/source/manager/subu_version.py new file mode 100644 index 0000000..bccd1dd --- /dev/null +++ b/developer/source/manager/subu_version.py @@ -0,0 +1,2 @@ +# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*- +VERSION = "0.1.6" diff --git a/developer/source/manager/subu_wg.py b/developer/source/manager/subu_wg.py new file mode 100644 index 0000000..0eca02c --- /dev/null +++ b/developer/source/manager/subu_wg.py @@ -0,0 +1,139 @@ +# ===== File: subu_wg.py ===== +#!/usr/bin/env python3 +# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*- + +import ipaddress +import subu_utils as U +import subu_db as DB + +PREFIX_DEV = "subu_" # device name base; dev = f"{PREFIX_DEV}{WG_id_num}" + + +def cmd_wg_help() -> int: + print("WG commands: global | create | info | server_provided_public_key | up | down ") + return 0 + + +def cmd_wg_global(base_cidr: str) -> int: + # validate CIDR + try: + net = ipaddress.ip_network(base_cidr, strict=False) + if net.version != 4: + return U.err("only IPv4 supported for WG base") + except Exception as e: + return U.err(f"invalid cidr: {e}") + DB.wg_set_global_base(base_cidr) + return U.ok(f"WG base set to {base_cidr}") + + +def allocate_addr_for(wg_id: str) -> str: + base = DB.get_meta("wg_base_cidr") + if not base: + raise RuntimeError("WG base not set; run 'subu WG global '") + net = ipaddress.ip_network(base, strict=False) + wid = int(wg_id.split("_")[1]) + host = list(net.hosts())[wid + 1] # skip .1 for potential gateway + return f"{host}/32" + + +def ensure_device(wg_id: str): + # create device if missing and store dev+addr in DB + row = DB.wg_by_id(wg_id) + if not row: + raise RuntimeError("unknown WG id") + _, remote, pubkey, dev, addr, state = row + if not dev: + dev = f"{PREFIX_DEV}{wg_id.split('_')[1]}" + DB.wg_update(wg_id, dev=dev) + if not addr: + addr = allocate_addr_for(wg_id) + DB.wg_update(wg_id, addr=addr) + # ensure link exists in root netns + rc, out = U.run("ip link show", capture=True, check=False) + if f": {dev}:" not in out: + # create WG link skeleton; full wg config is deferred + U.run(f"ip link add {dev} type wireguard") + U.run(f"ip addr add {addr} dev {dev}") + return dev, addr + + +def move_to_netns(wg_id: str, ns: str): + dev, _ = ensure_device(wg_id) + # if already in ns, ip will refuse — treat as ok + U.run(f"ip link set {dev} netns {ns}", check=False) + + +def detach_device(wg_id: str): + row = DB.wg_by_id(wg_id) + if not row: return + dev = row[3] + if not dev: return + # best effort delete (must run either in owning ns or root if present there) + # try root first + rc, out = U.run(f"ip link del {dev}", check=False) + if rc != 0: + # try to find owning ns? (skipped for brevity) + pass + + +def cmd_wg_create(remote: str) -> int: + wid = DB.wg_create(remote) + print(wid) + return 0 + + +def cmd_wg_info() -> int: + rows = DB.wg_list() + for wid, remote, pubkey, dev, addr, state in rows: + print(f"WG_{wid}: remote={remote} dev={dev} addr={addr} state={state} pubkey={'set' if pubkey else 'unset'}") + return 0 + + +def cmd_wg_set_server_pub(wg_id: str, pub: str) -> int: + DB.wg_update(wg_id, pubkey=pub) + return U.ok(f"Set server pubkey for {wg_id}") + + +def _ns_of_wg(wg_id: str): + # discover netns from attachment + rows = DB.list_subu() + for sid, *_ in rows: + attached = DB.attached_wg_ids(sid) + if wg_id in attached: + row = DB.subu_by_id(f"subu_{sid}") + return row[4] or f"subu_{sid}" + return None + + +def cmd_wg_up(wg_id: str) -> int: + row = DB.wg_by_id(wg_id) + if not row: + return U.err("unknown WG id") + dev, addr = ensure_device(wg_id) + ns = _ns_of_wg(wg_id) + if ns: + # bring lo up silently before bringing WG up + U.run(f"ip -n {ns} link set lo up", check=False) + U.run(f"ip -n {ns} link set {dev} up") + else: + U.run(f"ip link set {dev} up") + DB.wg_update(wg_id, state="up") + return U.ok(f"WG {wg_id} up") + + +def cmd_wg_down(wg_id: str) -> int: + row = DB.wg_by_id(wg_id) + if not row: + return U.err("unknown WG id") + dev = row[3] + if not dev: + return U.err("WG device not created yet") + ns = _ns_of_wg(wg_id) + if ns: + U.run(f"ip -n {ns} link set {dev} down", check=False) + else: + U.run(f"ip link set {dev} down", check=False) + DB.wg_update(wg_id, state="down") + return U.ok(f"WG {wg_id} down") + + diff --git a/developer/source/manager/subu_worker_bpf.py b/developer/source/manager/subu_worker_bpf.py new file mode 100644 index 0000000..0c71d78 --- /dev/null +++ b/developer/source/manager/subu_worker_bpf.py @@ -0,0 +1,195 @@ +# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*- +""" +subu_worker_bpf.py — build, load, and manage eBPF steering for a Subu + +What it does: + * Compiles subu_bpf_force_egress.c -> /var/lib/subu/bpf/subu_force_egress.bpf.o + * Creates pins under /sys/fs/bpf/subu/: + force_connect4, force_sendmsg4, force_ifindex_map + * Creates a cgroup v2 node at /sys/fs/cgroup/subu/ + * Attaches programs (connect4, sendmsg4) to that cgroup + * Writes the target ifindex into map[0] + * Idempotent: re-running updates ifindex and ensures attachments + +Requirements: + * bpffs mounted at /sys/fs/bpf + * cgroup v2 mounted at /sys/fs/cgroup + * tools: clang, bpftool + * privileges: CAP_BPF + CAP_SYS_ADMIN +""" + +import os +import shutil +import subprocess +from pathlib import Path +from typing import Dict + +BPF_SRC = Path("subu_bpf_force_egress.c") +BUILD_DIR = Path("/var/lib/subu/bpf") +BPFFS_DIR = Path("/sys/fs/bpf") +BPF_PIN_BASE = BPFFS_DIR / "subu" # /sys/fs/bpf/subu//* +CGROOT = Path("/sys/fs/cgroup/subu") # /sys/fs/cgroup/subu/ + +OBJ_NAME = "subu_force_egress.bpf.o" +PROG_CONNECT_PIN = "force_connect4" +PROG_SENDMSG_PIN = "force_sendmsg4" +MAP_IFINDEX_PIN = "force_ifindex_map" # matches map name in C + +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)}\nstdout:\n{r.stdout}\nstderr:\n{r.stderr}") + return r + +def _which_or_die(tool: str): + if not shutil.which(tool): + raise BpfError(f"Missing required tool: {tool}") + +def ensure_prereqs(): + _which_or_die("clang") + _which_or_die("bpftool") + # bpffs? + if not BPFFS_DIR.exists(): + raise BpfError(f"{BPFFS_DIR} not mounted; try: mount -t bpf bpf {BPFFS_DIR}") + # cgroup v2? + cgroot = Path("/sys/fs/cgroup") + if not (cgroot / "cgroup.controllers").exists(): + raise BpfError("cgroup v2 not mounted; e.g.: mount -t cgroup2 none /sys/fs/cgroup") + +def ensure_dirs(subu_id: str) -> Dict[str, Path]: + BUILD_DIR.mkdir(parents=True, exist_ok=True) + (BPF_PIN_BASE).mkdir(parents=True, exist_ok=True) + pin_dir = BPF_PIN_BASE / subu_id + pin_dir.mkdir(parents=True, exist_ok=True) + + CGROOT.mkdir(parents=True, exist_ok=True) + cgdir = CGROOT / subu_id + cgdir.mkdir(parents=True, exist_ok=True) + + return { + "build_obj": BUILD_DIR / OBJ_NAME, + "pin_dir": pin_dir, + "pin_prog_connect": pin_dir / PROG_CONNECT_PIN, + "pin_prog_sendmsg": pin_dir / PROG_SENDMSG_PIN, + "pin_map_ifindex": pin_dir / MAP_IFINDEX_PIN, + "cgdir": cgdir, + } + +def compile_bpf(obj_path: Path): + if not BPF_SRC.exists(): + raise BpfError(f"BPF source not found: {BPF_SRC}") + cmd = [ + "clang", "-O2", "-g", + "-target", "bpf", + "-D__TARGET_ARCH_x86", + "-c", str(BPF_SRC), + "-o", str(obj_path), + ] + _run(cmd) + +def _load_prog_with_pinmaps(obj: Path, section: str, prog_pin: Path, maps_dir: Path): + # bpftool prog load OBJ PIN_PATH section
pinmaps + _run([ + "bpftool", "prog", "load", + str(obj), str(prog_pin), + "section", section, + "pinmaps", str(maps_dir), + ]) + +def load_and_pin_all(p: Dict[str, Path]): + obj = p["build_obj"] + maps_dir = p["pin_dir"] + # Load two sections; maps get pinned into maps_dir once (first load). + _load_prog_with_pinmaps(obj, "cgroup/connect4", p["pin_prog_connect"], maps_dir) + _load_prog_with_pinmaps(obj, "cgroup/sendmsg4", p["pin_prog_sendmsg"], maps_dir) + + # Ensure map exists where we expect it (pinned by name from C) + if not p["pin_map_ifindex"].exists(): + # Some bpftool/libbpf combos pin maps directly as /. + # If not present, try to locate by name and pin. + # Find map id by name: + out = _run(["bpftool", "map", "show"]).stdout.splitlines() + target_id = None + for line in out: + # sample: "123: array name force_ifindex_map flags 0x0 ..." + if " name " + MAP_IFINDEX_PIN + " " in line: + # id is before colon + try: + target_id = line.strip().split(":", 1)[0] + int(target_id) # validate + break + except Exception: + pass + if not target_id: + raise BpfError(f"Unable to find map '{MAP_IFINDEX_PIN}' to pin") + _run(["bpftool", "map", "pin", "id", target_id, str(p["pin_map_ifindex"])]) + +def attach_to_cgroup(p: Dict[str, Path]): + # Attach programs to the subu-specific cgroup + _run(["bpftool", "cgroup", "attach", str(p["cgdir"]), "connect4", "pinned", str(p["pin_prog_connect"])]) + _run(["bpftool", "cgroup", "attach", str(p["cgdir"]), "sendmsg4", "pinned", str(p["pin_prog_sendmsg"])]) + +def detach_from_cgroup(p: Dict[str, Path]): + _run(["bpftool", "cgroup", "detach", str(p["cgdir"]), "connect4"], check=False) + _run(["bpftool", "cgroup", "detach", str(p["cgdir"]), "sendmsg4"], check=False) + +def set_ifindex(p: Dict[str, Path], ifindex: int): + # bpftool map update pinned key <00 00 00 00> value + key_hex = "00 00 00 00".split() + val_hex = ifindex.to_bytes(4, "little").hex(" ").split() + _run(["bpftool", "map", "update", "pinned", str(p["pin_map_ifindex"]), "key", *key_hex, "value", *val_hex]) + +def _ifindex_in_netns(netns_name: str, ifname: str) -> int: + # ip -n -o link show -> "7: subu_0: <...>" + r = _run(["ip", "-n", netns_name, "-o", "link", "show", ifname]) + first = r.stdout.strip().split(":", 1)[0] + return int(first) + +def install_steering(subu_id: str, netns_name: str, wg_ifname: str): + """ + Build/load eBPF programs, pin them under /sys/fs/bpf/subu/, + attach to /sys/fs/cgroup/subu/, and set map[0]=ifindex(of wg_ifname in netns). + Idempotent across repeated calls. + """ + ensure_prereqs() + paths = ensure_dirs(subu_id) + # compile if missing or stale + if (not paths["build_obj"].exists()) or (paths["build_obj"].stat().st_mtime < BPF_SRC.stat().st_mtime): + compile_bpf(paths["build_obj"]) + + # if pins already exist, keep them and just ensure attached + value updated + pins_exist = all(paths[k].exists() for k in ("pin_prog_connect", "pin_prog_sendmsg")) + if not pins_exist: + load_and_pin_all(paths) + + # compute ifindex inside the subu netns + ifindex = _ifindex_in_netns(netns_name, wg_ifname) + set_ifindex(paths, ifindex) + + # ensure cgroup attachments in place + attach_to_cgroup(paths) + +def remove_steering(subu_id: str): + """ + Detach cgroup hooks and unpin programs/maps. Leaves the cgroup dir. + """ + ensure_prereqs() + paths = ensure_dirs(subu_id) + # detach (ignore failure) + detach_from_cgroup(paths) + # unpin objects + for key in ("pin_prog_connect", "pin_prog_sendmsg", "pin_map_ifindex"): + try: + p = paths[key] + if p.exists(): + p.unlink() + except Exception: + pass + # try to remove empty pin dir + try: + paths["pin_dir"].rmdir() + except Exception: + pass diff --git a/developer/source/manager/test.sh b/developer/source/manager/test.sh new file mode 100644 index 0000000..b5960f4 --- /dev/null +++ b/developer/source/manager/test.sh @@ -0,0 +1,13 @@ +#!/bin/env bash + +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 + diff --git a/developer/source/manager/test_0.sh b/developer/source/manager/test_0.sh new file mode 100755 index 0000000..ac354d3 --- /dev/null +++ b/developer/source/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/developer/source/manager/test_0_expected.sh b/developer/source/manager/test_0_expected.sh new file mode 100644 index 0000000..8e31ed5 --- /dev/null +++ b/developer/source/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