From: Thomas Walker Lynch Date: Mon, 3 Nov 2025 02:41:22 +0000 (+0000) Subject: renaming modules X-Git-Url: https://git.reasoningtechnology.com/style/static/gitweb.css?a=commitdiff_plain;h=d559e21cec481587cd14c22d6f2d5ce97552e233;p=subu renaming modules --- diff --git a/developer/source/manager.tgz b/developer/source/manager.tgz new file mode 100644 index 0000000..d585797 Binary files /dev/null and b/developer/source/manager.tgz differ diff --git a/developer/source/manager/CLI.py b/developer/source/manager/CLI.py new file mode 100644 index 0000000..a79691e --- /dev/null +++ b/developer/source/manager/CLI.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*- +""" +CLI.py — thin command-line harness +Version: 0.2.0 +""" +import sys, argparse +from text import USAGE, HELP, EXAMPLE, VERSION +import core + +def CLI(argv=None) -> int: + argv = argv or sys.argv[1:] + if not argv: + print(USAGE) + return 0 + + # simple verbs that bypass argparse (so `help/version/example` always work) + simple = {"help": HELP, "--help": HELP, "-h": HELP, "usage": USAGE, "example": EXAMPLE, "version": VERSION} + if argv[0] in simple: + out = simple[argv[0]] + print(out if isinstance(out, str) else out()) + return 0 + + p = argparse.ArgumentParser(prog="subu", add_help=False) + p.add_argument("-V", "--Version", action="store_true", help="print version") + sub = p.add_subparsers(dest="verb") + + # init + ap = sub.add_parser("init") + ap.add_argument("token", nargs="?") + + # create/list/info + ap = sub.add_parser("create") + ap.add_argument("owner") + ap.add_argument("name") + + sub.add_parser("list") + ap = sub.add_parser("info"); ap.add_argument("subu_id") + ap = sub.add_parser("information"); ap.add_argument("subu_id") + + # lo + ap = sub.add_parser("lo") + ap.add_argument("state", choices=["up","down"]) + ap.add_argument("subu_id") + + # WG + ap = sub.add_parser("WG") + ap.add_argument("verb", choices=["global","create","server_provided_public_key","info","information","up","down"]) + ap.add_argument("arg1", nargs="?") + ap.add_argument("arg2", nargs="?") + + # attach/detach + ap = sub.add_parser("attach") + ap.add_argument("what", choices=["WG"]) + ap.add_argument("subu_id") + ap.add_argument("wg_id") + + ap = sub.add_parser("detach") + ap.add_argument("what", choices=["WG"]) + ap.add_argument("subu_id") + + # network + ap = sub.add_parser("network") + ap.add_argument("state", choices=["up","down"]) + ap.add_argument("subu_id") + + # option + ap = sub.add_parser("option") + ap.add_argument("verb", choices=["set","get","list"]) + ap.add_argument("subu_id") + ap.add_argument("name", nargs="?") + ap.add_argument("value", nargs="?") + + # exec + ap = sub.add_parser("exec") + ap.add_argument("subu_id") + ap.add_argument("--", dest="cmd", nargs=argparse.REMAINDER, default=[]) + + ns = p.parse_args(argv) + if ns.Version: + print(VERSION); return 0 + + try: + if ns.verb == "init": + return core.cmd_init(ns.token) + + if ns.verb == "create": + core.create_subu(ns.owner, ns.name); return 0 + if ns.verb == "list": + core.list_subu(); return 0 + if ns.verb in ("info","information"): + core.info_subu(ns.subu_id); return 0 + + if ns.verb == "lo": + core.lo_toggle(ns.subu_id, ns.state); return 0 + + if ns.verb == "WG": + v = ns.verb + if ns.arg1 is None and v in ("info","information"): + print("WG info requires WG_ID"); return 2 + if v == "global": + core.wg_global(ns.arg1); return 0 + if v == "create": + wid = core.wg_create(ns.arg1); print(wid); return 0 + if v == "server_provided_public_key": + core.wg_set_pubkey(ns.arg1, ns.arg2); return 0 + if v in ("info","information"): + core.wg_info(ns.arg1); return 0 + if v == "up": + core.wg_up(ns.arg1); return 0 + if v == "down": + core.wg_down(ns.arg1); return 0 + + if ns.verb == "attach": + if ns.what == "WG": + core.attach_wg(ns.subu_id, ns.wg_id); return 0 + + if ns.verb == "detach": + if ns.what == "WG": + core.detach_wg(ns.subu_id); return 0 + + if ns.verb == "network": + core.network_toggle(ns.subu_id, ns.state); return 0 + + if ns.verb == "option": + if ns.verb == "option" and ns.name is None and ns.value is None and ns.verb == "list": + core.option_list(ns.subu_id); return 0 + if ns.verb == "set": + core.option_set(ns.subu_id, ns.name, ns.value); return 0 + if ns.verb == "get": + core.option_get(ns.subu_id, ns.name); return 0 + if ns.verb == "list": + core.option_list(ns.subu_id); return 0 + + if ns.verb == "exec": + if not ns.cmd: + print("subu exec -- ..."); return 2 + core.exec_in_subu(ns.subu_id, ns.cmd); return 0 + + print(USAGE); return 2 + except Exception as e: + print(f"error: {e}") + return 1 + +if __name__ == "__main__": + sys.exit(CLI()) diff --git a/developer/source/manager/bpf_force_egress.c b/developer/source/manager/bpf_force_egress.c new file mode 100644 index 0000000..c3aedec --- /dev/null +++ b/developer/source/manager/bpf_force_egress.c @@ -0,0 +1,43 @@ +// -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 8 -*- +// bpf_force_egress.c — MVP scaffold to validate UID and prep metadata +// Version 0.2.0 +#include +#include +#include + +char LICENSE[] SEC("license") = "GPL"; + +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __type(key, __u32); // tgid + __type(value, __u32); // reserved (target ifindex placeholder) + __uint(max_entries, 1024); +} subu_tgid2if SEC(".maps"); + +// Helper: return 0 = allow, <0 reject +static __always_inline int allow_uid(struct bpf_sock_addr *ctx) { + // MVP: just accept everyone; you can gate on UID 2017 with bpf_get_current_uid_gid() + // __u32 uid = (__u32)(bpf_get_current_uid_gid() & 0xffffffff); + // if (uid != 2017) return -1; + return 0; +} + +// Hook: cgroup/connect4 — runs before connect(2) proceeds +SEC("cgroup/connect4") +int subu_connect4(struct bpf_sock_addr *ctx) +{ + if (allow_uid(ctx) < 0) return -1; + // Future: read pinned map/meta, set SO_* via bpf_setsockopt when permitted + return 0; +} + +// Hook: cgroup/post_bind4 — runs after a local bind is chosen +SEC("cgroup/post_bind4") +int subu_post_bind4(struct bpf_sock *sk) +{ + // Future: enforce bound dev if kernel helper allows; record tgid->ifindex + __u32 tgid = bpf_get_current_pid_tgid() >> 32; + __u32 val = 0; + bpf_map_update_elem(&subu_tgid2if, &tgid, &val, BPF_ANY); + return 0; +} diff --git a/developer/source/manager/core.py b/developer/source/manager/core.py new file mode 100644 index 0000000..c363ec2 --- /dev/null +++ b/developer/source/manager/core.py @@ -0,0 +1,254 @@ +# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*- +""" +core.py — worker API for subu manager +Version: 0.2.0 +""" +import os, sqlite3, subprocess +from pathlib import Path +from contextlib import closing +from text import VERSION +from worker_bpf import ensure_mounts, install_steering, remove_steering, BpfError + +DB_FILE = Path("./subu.db") +WG_GLOBAL_FILE = Path("./WG_GLOBAL") + +def run(cmd, check=True): + r = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + if check and r.returncode != 0: + raise RuntimeError(f"cmd failed: {' '.join(cmd)}\n{r.stderr}") + return r.stdout.strip() + +# ---------------- 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/source/manager/subu.py b/developer/source/manager/subu.py deleted file mode 100755 index 92aa8e7..0000000 --- a/developer/source/manager/subu.py +++ /dev/null @@ -1,138 +0,0 @@ -#!/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 deleted file mode 100644 index 15a0085..0000000 --- a/developer/source/manager/subu_BPF_force_egress.c +++ /dev/null @@ -1,44 +0,0 @@ -// -*- 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_core.py b/developer/source/manager/subu_core.py deleted file mode 100644 index 45ab787..0000000 --- a/developer/source/manager/subu_core.py +++ /dev/null @@ -1,235 +0,0 @@ -# -*- 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/temp.sh b/developer/source/manager/temp.sh new file mode 100644 index 0000000..36855b6 --- /dev/null +++ b/developer/source/manager/temp.sh @@ -0,0 +1,40 @@ +# 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/source/manager/text.py b/developer/source/manager/text.py new file mode 100644 index 0000000..84f6762 --- /dev/null +++ b/developer/source/manager/text.py @@ -0,0 +1,109 @@ +# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*- +VERSION = "0.2.0" + +USAGE = """\ +subu — Subu manager (v0.2.0) + +Usage: + subu # usage + subu help # detailed help + subu example # example workflow + subu version # print version + + subu init + subu create + subu list + subu info | subu information + + subu lo up|down + + subu WG global + subu WG create + subu WG server_provided_public_key + subu WG info|information + subu WG up + subu WG down + + subu attach WG + subu detach WG + + subu network up|down + + subu option set + subu option get + subu option list + + subu exec -- ... +""" + +HELP = """\ +Subu manager (v0.2.0) + +1) Init + subu init + Creates ./subu.db. Refuses to run if db exists. + +2) Subu + subu create + subu list + subu info + +3) Loopback + subu lo up|down + +4) WireGuard objects (independent of subu) + subu WG global # e.g., 192.168.112.0/24 + subu WG create # allocates next /32 + subu WG server_provided_public_key + subu WG info + subu WG up / subu WG down # admin toggle after attached + +5) Attach/detach + eBPF steering + subu attach WG + - Creates WG dev as subu_ inside ns-subu_, assigns /32, MTU 1420 + - Installs per-subu cgroup + loads eBPF scaffold (UID check, metadata map) + - Keeps device admin-down until `subu network up` + subu detach WG + - Deletes device, removes cgroup + BPF + +6) Network aggregate + subu network up|down + - Ensures lo up on 'up', toggles attached WG ifaces + +7) Options + subu option set|get|list ... + +8) Exec + subu exec -- ... +""" + +EXAMPLE = """\ +# 0) Init +subu init dzkq7b + +# 1) Create Subu +subu create Thomas US +# -> subu_1 + +# 2) WG pool once +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 +""" + +def VERSION_string(): + return VERSION diff --git a/developer/source/manager/worker_bpf.py b/developer/source/manager/worker_bpf.py new file mode 100644 index 0000000..96aef14 --- /dev/null +++ b/developer/source/manager/worker_bpf.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