--- /dev/null
+#+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_<id>=
+- Brings =lo= down inside that namespace.
+- Ensures Unix groups:
+ - =<masu>=
+ - =<masu>-incommon=
+- Ensures Unix user:
+ - =<masu>_<subu>...= (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_<id>=
+2. =ip netns exec ns-subu_<id> 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 <endpoint>=
+2. Record server-provided key via =subu WG server_provided_public_key=
+3. Attach interface via =subu attach WG <Subu_ID> <WG_ID>=
+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_<M>= 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
--- /dev/null
+Domain modules
+
+subu.py
+
+wg.py
+
+network.py
+
+options.py
+
+exec.py (as in “execute command in subu”)
+
+
+Infrastructure modules
+
+db.py
+
+unix.py
+
+bpf.py
+
+bpf_worker.py
+
+bpf_force_egress.c
+
+schema.sql
+
+parser.py (either merged into CLI.py or deleted, see below)
#!/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
+1. CLI.py
+ dispatch.
+
+Role: parse argv, choose command, call
+CLI should not do any work beyond:
+
+ * figure out program_name (for example, manager/CLI.py or wrapper name)
+ * call the right function in dispatch
+ * print text from text.py when needed
+ * exit with the returned status code
"""
+
import sys, argparse
-from text import USAGE, HELP, EXAMPLE, VERSION
-import core
+from text import make_text
+import dispatch
-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
+def build_arg_parser(program_name):
+ """
+ Build the top level argument parser for the subu manager.
+ """
+ parser = argparse.ArgumentParser(prog=program_name, add_help=False)
+ parser.add_argument("-V","--Version", action="store_true", help="print version")
+
+ subparsers = parser.add_subparsers(dest="verb")
- p = argparse.ArgumentParser(prog="subu", add_help=False)
- p.add_argument("-V", "--Version", action="store_true", help="print version")
- sub = p.add_subparsers(dest="verb")
+ register_subu_commands(subparsers)
+ register_wireguard_commands(subparsers)
+ register_attach_commands(subparsers)
+ register_network_commands(subparsers)
+ register_option_commands(subparsers)
+ register_exec_commands(subparsers)
+ return parser
+
+
+def register_subu_commands(subparsers):
+ """
+ Register subu related commands:
+ init, make, list, info, information, lo
+ """
# init
- ap = sub.add_parser("init")
+ ap = subparsers.add_parser("init")
ap.add_argument("token", nargs="?")
- # create/list/info
- ap = sub.add_parser("create")
+ # make
+ ap = subparsers.add_parser("make")
ap.add_argument("owner")
ap.add_argument("name")
- sub.add_parser("list")
- ap = sub.add_parser("info"); ap.add_argument("subu_id")
- ap = sub.add_parser("information"); ap.add_argument("subu_id")
+ # list
+ subparsers.add_parser("list")
+
+ # info / information
+ ap = subparsers.add_parser("info")
+ ap.add_argument("subu_id")
+ ap = subparsers.add_parser("information")
+ ap.add_argument("subu_id")
# lo
- ap = sub.add_parser("lo")
+ ap = subparsers.add_parser("lo")
ap.add_argument("state", choices=["up","down"])
ap.add_argument("subu_id")
- # WG
- ap = sub.add_parser("WG")
- ap.add_argument("verb", choices=["global","create","server_provided_public_key","info","information","up","down"])
+
+def register_wireguard_commands(subparsers):
+ """
+ Register WireGuard related commands, grouped under 'WG':
+ WG global <BaseCIDR>
+ WG make <host:port>
+ WG server_provided_public_key <WG_ID> <Base64Key>
+ WG info|information <WG_ID>
+ WG up|down <WG_ID>
+ """
+ ap = subparsers.add_parser("WG")
+ ap.add_argument(
+ "wg_verb",
+ choices=[
+ "global",
+ "make",
+ "server_provided_public_key",
+ "info",
+ "information",
+ "up",
+ "down",
+ ],
+ )
ap.add_argument("arg1", nargs="?")
ap.add_argument("arg2", nargs="?")
- # attach/detach
- ap = sub.add_parser("attach")
+
+def register_attach_commands(subparsers):
+ """
+ Register attach and detach commands:
+ attach WG <Subu_ID> <WG_ID>
+ detach WG <Subu_ID>
+ """
+ ap = subparsers.add_parser("attach")
ap.add_argument("what", choices=["WG"])
ap.add_argument("subu_id")
ap.add_argument("wg_id")
- ap = sub.add_parser("detach")
+ ap = subparsers.add_parser("detach")
ap.add_argument("what", choices=["WG"])
ap.add_argument("subu_id")
- # network
- ap = sub.add_parser("network")
+
+def register_network_commands(subparsers):
+ """
+ Register network aggregate commands:
+ network up|down <Subu_ID>
+ """
+ ap = subparsers.add_parser("network")
ap.add_argument("state", choices=["up","down"])
ap.add_argument("subu_id")
- # option
- ap = sub.add_parser("option")
- ap.add_argument("verb", choices=["set","get","list"])
+
+def register_option_commands(subparsers):
+ """
+ Register option commands:
+ option set|get|list ...
+ """
+ ap = subparsers.add_parser("option")
+ ap.add_argument("action", choices=["set","get","list"])
ap.add_argument("subu_id")
ap.add_argument("name", nargs="?")
ap.add_argument("value", nargs="?")
- # exec
- ap = sub.add_parser("exec")
+
+def register_exec_commands(subparsers):
+ """
+ Register exec command:
+ exec <Subu_ID> -- <cmd> ...
+ """
+ ap = subparsers.add_parser("exec")
ap.add_argument("subu_id")
+ # Use a dedicated "--" argument so that:
+ # subu exec subu_7 -- curl -4v https://ifconfig.me
+ # works as before.
ap.add_argument("--", dest="cmd", nargs=argparse.REMAINDER, default=[])
- ns = p.parse_args(argv)
- if ns.Version:
- print(VERSION); return 0
+
+def CLI(argv=None) -> int:
+ """
+ Top level entry point for the subu manager CLI.
+ """
+ if argv is None:
+ argv = sys.argv[1:]
+
+ # For now we fix the program name to "subu".
+ # A release wrapper can later pass a different program name.
+ program_name = "subu"
+ text = make_text(program_name)
+
+ # No arguments is the same as "help".
+ if not argv:
+ print(text.help(), end="")
+ return 0
+
+ # Simple verbs that bypass argparse so they always work.
+ simple = {
+ "help": text.help,
+ "--help": text.help,
+ "-h": text.help,
+ "usage": text.usage,
+ "example": text.example,
+ "version": text.version,
+ }
+ if argv[0] in simple:
+ print(simple[argv[0]](), end="")
+ return 0
+
+ parser = build_arg_parser(program_name)
+ ns = parser.parse_args(argv)
+
+ if getattr(ns, "Version", False):
+ print(text.version(), end="")
+ return 0
try:
if ns.verb == "init":
- return core.cmd_init(ns.token)
+ return dispatch.init(ns.token)
+
+ if ns.verb == "make":
+ return dispatch.subu_make(ns.owner, ns.name)
- if ns.verb == "create":
- core.create_subu(ns.owner, ns.name); return 0
if ns.verb == "list":
- core.list_subu(); return 0
+ return dispatch.subu_list()
+
if ns.verb in ("info","information"):
- core.info_subu(ns.subu_id); return 0
+ return dispatch.subu_info(ns.subu_id)
if ns.verb == "lo":
- core.lo_toggle(ns.subu_id, ns.state); return 0
+ return dispatch.lo_toggle(ns.subu_id, ns.state)
if ns.verb == "WG":
- v = ns.verb
- if ns.arg1 is None and v in ("info","information"):
- print("WG info requires WG_ID"); return 2
+ v = ns.wg_verb
+ if v in ("info","information") and ns.arg1 is None:
+ print("WG info requires WG_ID", file=sys.stderr)
+ return 2
if v == "global":
- core.wg_global(ns.arg1); return 0
- if v == "create":
- wid = core.wg_create(ns.arg1); print(wid); return 0
+ return dispatch.wg_global(ns.arg1)
+ if v == "make":
+ return dispatch.wg_make(ns.arg1)
if v == "server_provided_public_key":
- core.wg_set_pubkey(ns.arg1, ns.arg2); return 0
+ return dispatch.wg_server_public_key(ns.arg1, ns.arg2)
if v in ("info","information"):
- core.wg_info(ns.arg1); return 0
+ return dispatch.wg_info(ns.arg1)
if v == "up":
- core.wg_up(ns.arg1); return 0
+ return dispatch.wg_up(ns.arg1)
if v == "down":
- core.wg_down(ns.arg1); return 0
+ return dispatch.wg_down(ns.arg1)
if ns.verb == "attach":
if ns.what == "WG":
- core.attach_wg(ns.subu_id, ns.wg_id); return 0
+ return dispatch.attach_wg(ns.subu_id, ns.wg_id)
if ns.verb == "detach":
if ns.what == "WG":
- core.detach_wg(ns.subu_id); return 0
+ return dispatch.detach_wg(ns.subu_id)
if ns.verb == "network":
- core.network_toggle(ns.subu_id, ns.state); return 0
+ return dispatch.network_toggle(ns.subu_id, ns.state)
if ns.verb == "option":
- if ns.verb == "option" and ns.name is None and ns.value is None and ns.verb == "list":
- core.option_list(ns.subu_id); return 0
- if ns.verb == "set":
- core.option_set(ns.subu_id, ns.name, ns.value); return 0
- if ns.verb == "get":
- core.option_get(ns.subu_id, ns.name); return 0
- if ns.verb == "list":
- core.option_list(ns.subu_id); return 0
+ if ns.action == "set":
+ return dispatch.option_set(ns.subu_id, ns.name, ns.value)
+ if ns.action == "get":
+ return dispatch.option_get(ns.subu_id, ns.name)
+ if ns.action == "list":
+ return dispatch.option_list(ns.subu_id)
if ns.verb == "exec":
if not ns.cmd:
- print("subu exec <Subu_ID> -- <cmd> ..."); return 2
- core.exec_in_subu(ns.subu_id, ns.cmd); return 0
+ print(f"{program_name} exec <Subu_ID> -- <cmd> ...", file=sys.stderr)
+ return 2
+ return dispatch.exec(ns.subu_id, ns.cmd)
+
+ # If we reach here, the verb was not recognised.
+ print(text.usage(), end="")
+ return 2
- print(USAGE); return 2
except Exception as e:
- print(f"error: {e}")
+ print(f"error: {e}", file=sys.stderr)
return 1
+
if __name__ == "__main__":
sys.exit(CLI())
+++ /dev/null
-
-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}")
-
+++ /dev/null
-// -*- 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 <linux/bpf.h>
-#include <bpf/bpf_helpers.h>
-#include <bpf/bpf_endian.h>
-
-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;
-}
+++ /dev/null
-# -*- 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
+++ /dev/null
-# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
-"""
-core.py — worker API for subu manager
-Version: 0.2.0
-"""
-import os, sqlite3, subprocess
-from pathlib import Path
-from contextlib import closing
-from text import VERSION
-from worker_bpf import ensure_mounts, install_steering, remove_steering, BpfError
-import db
-
-DB_FILE = Path("./subu.db")
-WG_GLOBAL_FILE = Path("./WG_GLOBAL")
-
-def run(cmd, check=True):
- r = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
- if check and r.returncode != 0:
- raise RuntimeError(f"cmd failed: {' '.join(cmd)}\n{r.stderr}")
- return r.stdout.strip()
-
-
+++ /dev/null
-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 <token>` 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
--- /dev/null
+#!/usr/bin/env python3
+# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
+"""
+dispatch.py
+
+Role: provide one function for each CLI verb so that:
+
+ * CLI.py can call these functions
+ * Other Python code can also import and call them directly
+
+Each function should return an integer status code where practical.
+
+Implementation note:
+
+ At this stage of the refactor, the functions are stubs. They define the
+ public interface and may raise NotImplementedError. As the domain modules
+ under domain/ are completed (subu.py, wg.py, network.py, options.py,
+ exec.py), these functions should be updated to call into those modules.
+"""
+# dispatch.py
+from domain import subu as subu_domain
+
+def init(token=None):
+ """
+ Initialize ./subu.db using schema.sql
+ token is currently unused but kept for CLI compatibility.
+ """
+ # open_db + ensure_schema via domain layer convenience
+ from infrastructure.db import open_db, ensure_schema
+ conn = open_db("subu.db")
+ try:
+ ensure_schema(conn)
+ return 0
+ finally:
+ conn.close()
+
+
+def subu_make(owner, name):
+ try:
+ s = subu_domain.make_subu(owner, name)
+ # print the made ID or username like your older CLI did
+ print(f"made subu: id={s.id} username={s.username}")
+ return 0
+ except Exception as e:
+ print(f"error creating subu: {e}", file=sys.stderr)
+ return 1
+
+
+def subu_list():
+ try:
+ subs = subu_domain.list_subu()
+ if not subs:
+ print("no subu found")
+ return 0
+ # simple table
+ print("ID OWNER NAME USERNAME CREATED_AT")
+ for s in subs:
+ print(f"{s.id} {s.owner} {s.name} {s.username} {s.made_at}")
+ return 0
+ except Exception as e:
+ print(f"error listing subu: {e}", file=sys.stderr)
+ return 1
+
+
+def subu_info(subu_id):
+ """
+ Handle: subu info|information <Subu_ID>
+ """
+ raise NotImplementedError("subu_info is not yet implemented")
+
+
+def lo_toggle(subu_id, state):
+ """
+ Handle: subu lo up|down <Subu_ID>
+ """
+ raise NotImplementedError("lo_toggle is not yet implemented")
+
+
+def wg_global(base_cidr):
+ """
+ Handle: subu WG global <BaseCIDR>
+ """
+ raise NotImplementedError("wg_global is not yet implemented")
+
+
+def wg_make(endpoint):
+ """
+ Handle: subu WG make <host:port>
+ """
+ raise NotImplementedError("wg_make is not yet implemented")
+
+
+def wg_server_public_key(wg_id, key):
+ """
+ Handle: subu WG server_provided_public_key <WG_ID> <Base64Key>
+ """
+ raise NotImplementedError("wg_server_public_key is not yet implemented")
+
+
+def wg_info(wg_id):
+ """
+ Handle: subu WG info|information <WG_ID>
+ """
+ raise NotImplementedError("wg_info is not yet implemented")
+
+
+def wg_up(wg_id):
+ """
+ Handle: subu WG up <WG_ID>
+ """
+ raise NotImplementedError("wg_up is not yet implemented")
+
+
+def wg_down(wg_id):
+ """
+ Handle: subu WG down <WG_ID>
+ """
+ raise NotImplementedError("wg_down is not yet implemented")
+
+
+def attach_wg(subu_id, wg_id):
+ """
+ Handle: subu attach WG <Subu_ID> <WG_ID>
+ """
+ raise NotImplementedError("attach_wg is not yet implemented")
+
+
+def detach_wg(subu_id):
+ """
+ Handle: subu detach WG <Subu_ID>
+ """
+ raise NotImplementedError("detach_wg is not yet implemented")
+
+
+def network_toggle(subu_id, state):
+ """
+ Handle: subu network up|down <Subu_ID>
+ """
+ raise NotImplementedError("network_toggle is not yet implemented")
+
+
+def option_set(subu_id, name, value):
+ """
+ Handle: subu option set <Subu_ID> <name> <value>
+ """
+ raise NotImplementedError("option_set is not yet implemented")
+
+
+def option_get(subu_id, name):
+ """
+ Handle: subu option get <Subu_ID> <name>
+ """
+ raise NotImplementedError("option_get is not yet implemented")
+
+
+def option_list(subu_id):
+ """
+ Handle: subu option list <Subu_ID>
+ """
+ raise NotImplementedError("option_list is not yet implemented")
+
+
+def exec(subu_id, cmd_argv):
+ """
+ Handle: subu exec <Subu_ID> -- <cmd> ...
+ """
+ raise NotImplementedError("exec is not yet implemented")
--- /dev/null
+"""
+4.5 domain/exec.py
+
+Run a command inside a subu’s namespace and UID.
+
+4.5.1 run_in_subu(subu: Subu, cmd_argv: list[str]) -> int
+"""
+def exec_in_subu(subu_id: str, cmd: list):
+ sid = int(subu_id.split("_")[1])
+ with closing(_db()) as db:
+ ns = db.execute("SELECT netns FROM subu WHERE id=?", (sid,)).fetchone()[0]
+ os.execvp("ip", ["ip","netns","exec", ns] + cmd)
--- /dev/null
+"""
+4.3 domain/network.py
+
+Netns + device wiring, including aggregate “network up/down”.
+
+4.3.1 lo_toggle(subu: Subu, state: str) -> None
+4.3.2 attach_wg(subu: Subu, wg: WG) -> None
+4.3.3 detach_wg(subu: Subu) -> None
+4.3.4 network_toggle(subu: Subu, state: str) -> None
+"""
+def network_toggle(subu_id: str, state: str):
+ sid = int(subu_id.split("_")[1])
+ with closing(_db()) as db:
+ 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 _make_netns_for_subu(subu_id_num: int, netns_name: str):
+ """
+ Create the network namespace & bring lo down.
+ """
+ # ip netns add ns-subu_<id>
+ run(["ip", "netns", "add", netns_name])
+ # ip netns exec ns-subu_<id> ip link set lo down
+ run(["ip", "netns", "exec", netns_name, "ip", "link", "set", "lo", "down"])
--- /dev/null
+"""
+4.4 domain/options.py
+
+Per-subu options, backed by DB.
+
+4.4.1 set_option(subu_id: str, name: str, value: str) -> None
+4.4.2 get_option(subu_id: str, name: str) -> str | None
+4.4.3 list_options(subu_id: str) -> dict[str, str]
+"""
+def option_set(subu_id: str, name: str, value: str):
+ sid = int(subu_id.split("_")[1])
+ with closing(_db()) as db:
+ 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}")
+
--- /dev/null
+"""
+4.1 domain/subu.py
+
+Subu objects: creation, lookup, hierarchy, netns identity.
+
+4.1.1 make_subu(owner: str, name: str) -> Subu
+4.1.2 list_subu() -> list[Subu]
+4.1.3 get_subu(subu_id: str) -> Subu
+4.1.4 ensure_unix_identity(subu: Subu) -> None
+4.1.5 ensure_netns(subu: Subu) -> None
+
+(A Subu can be a dataclass or NamedTuple.)
+"""
+
+# domain/subu.py
+from dataclasses import dataclass
+from infrastructure.db import open_db, ensure_schema
+import sqlite3
+import time
+
+DB_PATH = "subu.db"
+
+
+@dataclass
+class Subu:
+ id: int
+ owner: str
+ name: str
+ username: str
+ made_at: str
+
+
+def _make_username(owner, name):
+ # simple deterministic username: owner_name -> owner_name (no spaces)
+ owner_s = owner.replace(" ", "_")
+ name_s = name.replace(" ", "_")
+ return f"{owner_s}_{name_s}"
+
+
+def make_subu(owner: str, name: str) -> Subu:
+ """
+ Create a subu row in subu.db and return the Subu dataclass.
+ """
+ conn = open_db(DB_PATH)
+ try:
+ ensure_schema(conn)
+ cur = conn.cursor()
+ username = _make_username(owner, name)
+ made_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
+ cur.execute(
+ "INSERT INTO subu (owner, name, username, made_at) VALUES (?, ?, ?, ?)",
+ (owner, name, username, made_at),
+ )
+ conn.commit()
+ rowid = cur.lastrowid
+ row = conn.execute("SELECT id, owner, name, username, made_at FROM subu WHERE id = ?", (rowid,)).fetchone()
+ return Subu(row["id"], row["owner"], row["name"], row["username"], row["made_at"])
+ finally:
+ conn.close()
+
+
+def list_subu():
+ """
+ Return a list of Subu objects currently in the DB.
+ """
+ conn = open_db(DB_PATH)
+ try:
+ ensure_schema(conn)
+ rows = conn.execute("SELECT id, owner, name, username, made_at FROM subu ORDER BY id").fetchall()
+ return [Subu(r["id"], r["owner"], r["name"], r["username"], r["made_at"]) for r in rows]
+ finally:
+ conn.close()
+
+def info_subu(subu_id: str):
+ sid = int(subu_id.split("_")[1])
+ 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:
+ <masu>
+ <masu>-incommon
+
+ Side effects:
+ - DB row in 'subu' (id, owner, name, full_unix_name, path, netns_name, ...)
+ - netns ns-subu_<id> made with lo down
+ - Unix user made/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, made_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
+ _make_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
--- /dev/null
+"""
+4.2 domain/wg.py
+
+WireGuard objects, independent of subu.
+
+4.2.1 set_global_pool(base_cidr: str) -> None
+4.2.2 make_wg(endpoint: str) -> WG
+4.2.3 set_server_public_key(wg_id: str, key: str) -> None
+4.2.4 get_wg(wg_id: str) -> WG
+4.2.5 bring_up(wg_id: str) -> None
+4.2.6 bring_down(wg_id: str) -> None
+"""
+
+def wg_global(basecidr: str):
+ WG_GLOBAL_FILE.write_text(basecidr.strip()+"\n")
+ 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_make(endpoint: str) -> str:
+ if not WG_GLOBAL_FILE.exists():
+ raise RuntimeError("set WG base with `subu WG global <CIDR>` 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)")
+
+++ /dev/null
-
-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)
--- /dev/null
+"""
+bpf.py
+
+Compile/load the BPF program.
+
+5.3.1 compile_bpf(source_path: str, output_path: str) -> None
+5.3.2 load_bpf(obj_path: str) -> BpfHandle
+"""
+
+def attach_wg(subu_id: str, wg_id: str):
+ ensure_mounts()
+ 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}"
+ # make WG link in init ns, move to netns
+ run(["ip", "link", "add", ifname, "type", "wireguard"])
+ run(["ip", "link", "set", ifname, "netns", ns])
+ run(["ip", "-n", ns, "addr", "add", local_ip, "dev", ifname], check=False)
+ run(["ip", "-n", ns, "link", "set", "dev", ifname, "mtu", "1420"])
+ run(["ip", "-n", ns, "link", "set", "dev", ifname, "down"]) # keep engine down until `network up`
+
+ # install steering (MVP: make 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}")
+
--- /dev/null
+// -*- 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
+/*
+ bpf_force_egress.c
+
+5.5.1 no callable Python API; compiled/used via bpf.py.
+*/
+#include <linux/bpf.h>
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_endian.h>
+
+
+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;
+}
--- /dev/null
+# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
+"""
+bpf_worker.py
+
+Cgroup + BPF orchestration for per-subu steering.
+
+5.4.1 ensure_mounts() -> None
+5.4.2 install_steering(subu: Subu, wg_iface: str) -> None
+5.4.3 remove_steering(subu: Subu) -> None
+5.4.4 class BpfError(Exception)
+"""
+import os, subprocess, json
+from pathlib import Path
+
+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
--- /dev/null
+# infrastructure/db.py
+import os
+import pwd
+import grp
+import subprocess
+from contextlib import closing
+import sqlite3
+from pathlib import Path
+
+"""
+5.1 infrastructure/db.py
+
+All SQLite access.
+
+5.1.1 open_db(path: str = "subu.db") -> sqlite3.Connection
+5.1.2 ensure_schema(conn) -> None
+5.1.3 insert_subu(conn, subu: Subu) -> None
+5.1.4 fetch_subu(conn, subu_id: str) -> Subu
+5.1.5 list_subu(conn) -> list[Subu]
+5.1.6 insert_wg(conn, wg: WG) -> None
+5.1.7 fetch_wg(conn, wg_id: str) -> WG
+5.1.8 update_wg(conn, wg: WG) -> None
+5.1.9 set_option_row(conn, subu_id: str, name: str, value: str) -> None
+5.1.10 get_option_row(conn, subu_id: str, name: str) -> str | None
+5.1.11 list_option_rows(conn, subu_id: str) -> dict[str, str]
+
+(Exact breakdown can be tuned when we see schema.sql.)
+"""
+
+# infrastructure/db.py
+# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
+
+def schema_path_default(): return Path(__file__).with_name("schema.sql")
+def db_path_default(): return "."
+
+def open_db(path ="subu.db"):
+ """
+ Return a sqlite3.Connection with sensible pragmas.
+ Caller is responsible for closing.
+ """
+ conn = sqlite3.connect(path)
+ conn.row_factory = sqlite3.Row
+ conn.execute("PRAGMA foreign_keys = ON")
+ conn.execute("PRAGMA journal_mode = WAL")
+ conn.execute("PRAGMA synchronous = NORMAL")
+ return conn
+
+def ensure_schema(conn):
+ """
+ Ensure the schema in schema.sql is applied.
+ This is idempotent: executing the DDL again is acceptable.
+ """
+ sql = schema_path_default().read_text(encoding="utf-8")
+ conn.executescript(sql)
+ conn.commit()
+
+def _db():
+ if not DB_FILE.exists():
+ raise FileNotFoundError("subu.db not found; run `subu init <token>` first")
+ return sqlite3.connect(DB_FILE)
+
+def init_db(path: str = db_path_default()):
+ """
+ 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 ('made_at', datetime('now'))"
+ )
+ db.commit()
+ print(f"subu: made 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"made 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
--- /dev/null
+-- schema.sql
+--
+-- 5.6.1 read and executed by db.ensure_schema
+
+
+
+CREATE TABLE subu (
+ id INTEGER PRIMARY KEY,
+ owner TEXT NOT NULL, -- root user, e.g. 'Thomas'
+ 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
+);
--- /dev/null
+"""
+unix.py
+
+Thin wrappers for OS commands.
+
+5.2.1 run(cmd: list[str], check: bool = True) -> subprocess.CompletedProcess
+5.2.2 ip(*args: str, check: bool = True)
+5.2.3 ip_netns(*args: str, check: bool = True)
+5.2.4 wg(*args: str, check: bool = True)
+
+Optional later: logging, dry-run, etc.
+"""
+
+# ---------------- Unix users & groups ----------------
+
+def _group_exists(name: str) -> bool:
+ 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 <name>
+ run(["groupadd", name])
+
+def _ensure_user(name: str, primary_group: str):
+ if not _user_exists(name):
+ # useradd -m -g <primary_group> -s /bin/bash <name>
+ run(["useradd", "-m", "-g", primary_group, "-s", "/bin/bash", name])
+
+def _add_user_to_group(user: str, group: str):
+ run(["usermod", "-aG", group, user])
+++ /dev/null
-
-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_<id>
- run(["ip", "netns", "add", netns_name])
- # ip netns exec ns-subu_<id> ip link set lo down
- run(["ip", "netns", "exec", netns_name, "ip", "link", "set", "lo", "down"])
+++ /dev/null
-
-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}")
-
+++ /dev/null
-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)
+++ /dev/null
-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
-);
+++ /dev/null
-# ------------- 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:
- <masu>
- <masu>-incommon
-
- Side effects:
- - DB row in 'subu' (id, owner, name, full_unix_name, path, netns_name, ...)
- - netns ns-subu_<id> 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
-# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
-VERSION = "0.2.0"
+# text.py
-USAGE = """\
-subu — Subu manager (v0.2.0)
+from version import version as current_version
+
+
+class Text:
+ """
+ Program text bound to a specific command name.
+
+ Usage:
+ text_1 = Text("subu")
+ text_2 = Text("manager")
+
+ print(text_1.usage())
+ print(text_2.help())
+ """
+
+ def __init__(self, program_name ="subu"):
+ self.program_name = program_name
+
+ def usage(self):
+ program_name = self.program_name
+ return f"""{program_name} — Subu manager (v{current_version()})
Usage:
- subu # usage
- subu help # detailed help
- subu example # example workflow
- subu version # print version
+ {program_name} # usage
+ {program_name} help # detailed help
+ {program_name} example # example workflow
+ {program_name} version # print version
- subu init <TOKEN>
- subu create <owner> <name>
- subu list
- subu info <Subu_ID> | subu information <Subu_ID>
+ {program_name} init <TOKEN>
+ {program_name} make <owner> <name>
+ {program_name} list
+ {program_name} info <Subu_ID> | {program_name} information <Subu_ID>
- subu lo up|down <Subu_ID>
+ {program_name} lo up|down <Subu_ID>
- subu WG global <BaseCIDR>
- subu WG create <host:port>
- subu WG server_provided_public_key <WG_ID> <Base64Key>
- subu WG info|information <WG_ID>
- subu WG up <WG_ID>
- subu WG down <WG_ID>
+ {program_name} WG global <BaseCIDR>
+ {program_name} WG make <host:port>
+ {program_name} WG server_provided_public_key <WG_ID> <Base64Key>
+ {program_name} WG info|information <WG_ID>
+ {program_name} WG up <WG_ID>
+ {program_name} WG down <WG_ID>
- subu attach WG <Subu_ID> <WG_ID>
- subu detach WG <Subu_ID>
+ {program_name} attach WG <Subu_ID> <WG_ID>
+ {program_name} detach WG <Subu_ID>
- subu network up|down <Subu_ID>
+ {program_name} network up|down <Subu_ID>
- subu option set <Subu_ID> <name> <value>
- subu option get <Subu_ID> <name>
- subu option list <Subu_ID>
+ {program_name} option set <Subu_ID> <name> <value>
+ {program_name} option get <Subu_ID> <name>
+ {program_name} option list <Subu_ID>
- subu exec <Subu_ID> -- <cmd> ...
+ {program_name} exec <Subu_ID> -- <cmd> ...
"""
-HELP = """\
-Subu manager (v0.2.0)
+ def help(self, verbose =False):
+ program_name = self.program_name
+ return f"""Subu manager (v{current_version()})
1) Init
- subu init <TOKEN>
- Creates ./subu.db. Refuses to run if db exists.
+ {program_name} init <TOKEN>
+ Makes ./subu.db. Refuses to run if db exists.
2) Subu
- subu create <owner> <name>
- subu list
- subu info <Subu_ID>
+ {program_name} make <owner> <name>
+ {program_name} list
+ {program_name} info <Subu_ID>
3) Loopback
- subu lo up|down <Subu_ID>
+ {program_name} lo up|down <Subu_ID>
4) WireGuard objects (independent of subu)
- subu WG global <BaseCIDR> # e.g., 192.168.112.0/24
- subu WG create <host:port> # allocates next /32
- subu WG server_provided_public_key <WG_ID> <Base64Key>
- subu WG info <WG_ID>
- subu WG up <WG_ID> / subu WG down <WG_ID> # admin toggle after attached
-
-5) Attach/detach + eBPF steering
- subu attach WG <Subu_ID> <WG_ID>
- - Creates WG dev as subu_<M> inside ns-subu_<N>, 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 <Subu_ID>
- - Deletes device, removes cgroup + BPF
+ {program_name} WG global <BaseCIDR> # for example, 192.168.112.0/24
+ {program_name} WG make <host:port> # allocates next /32
+ {program_name} WG server_provided_public_key <WG_ID> <Base64Key>
+ {program_name} WG info <WG_ID>
+ {program_name} WG up <WG_ID> / {program_name} WG down <WG_ID> # administrative toggle after attached
+
+5) Attach or detach and eBPF steering
+ {program_name} attach WG <Subu_ID> <WG_ID>
+ - Makes WireGuard device as subu_<M> inside ns-subu_<N>, assigns /32, MTU 1420
+ - Installs per-subu cgroup and loads eBPF scaffold (user identifier check, metadata map)
+ - Keeps device administrative-down until `{program_name} network up`
+ {program_name} detach WG <Subu_ID>
+ - Deletes device, removes cgroup and eBPF program
6) Network aggregate
- subu network up|down <Subu_ID>
- - Ensures lo up on 'up', toggles attached WG ifaces
+ {program_name} network up|down <Subu_ID>
+ - Ensures loopback is up on 'up', toggles attached WireGuard interfaces
7) Options
- subu option set|get|list ...
+ {program_name} option set|get|list ...
8) Exec
- subu exec <Subu_ID> -- <cmd> ...
+ {program_name} exec <Subu_ID> -- <cmd> ...
"""
-EXAMPLE = """\
-# 0) Initialise the subu database (once per directory)
-subu init
-# -> created ./subu.db
-# If ./subu.db already exists, init will fail with an error and do nothing.
-
-# 1) Create a Subu “US” owned by user Thomas
-subu create Thomas US
-# -> Subu_ID: subu_7
-# -> netns: ns-subu_7 with lo (down)
-
-# 2) Define a global WireGuard address pool (once per host)
-subu WG global 192.168.112.0/24
-# -> base set; next free: 192.168.112.2/32
-
-# 3) Create a WG object with endpoint (ReasoningTechnology server)
-subu WG create 35.194.71.194:51820
-# or: subu WG create ReasoningTechnology.com:51820
-# -> WG_ID: WG_0
-# -> local IP: 192.168.112.2/32
-# -> AllowedIPs: 0.0.0.0/0
-
-# 4) Add server public key (example key)
-subu WG server_provided_public_key WG_0 ABCDEFG...xyz=
-# -> saved
-
-# 5) Attach WG to the Subu
-subu attach WG subu_7 WG_0
-# -> creates device ns-subu_7/subu_0
-# -> assigns 192.168.112.2/32, MTU 1420, accept_local=1
-# -> enforces egress steering via cgroup/eBPF for UID(s) of subu_7
-# -> warns if lo is down in the netns
-
-# 6) Bring networking up for the Subu
-subu network up subu_7
-# -> brings lo up in ns-subu_7
-# -> brings subu_0 admin up
-
-# 7) Start the WireGuard engine for this WG
-subu WG up WG_0
-# -> interface up; handshake should start if keys/endpoint are correct
-
-# 8) Run a command inside the Subu’s netns
-subu exec subu_7 -- curl -4v https://ifconfig.me
-# Traffic from this process should egress via subu_0/US tunnel.
+ def example(self):
+ program_name = self.program_name
+ return f"""# 0) Initialise the subu database (once per directory)
+{program_name} init dzkq7b
+
+# 1) Make Subu
+{program_name} make Thomas US
+# -> subu_1
+
+# 2) WireGuard pool once
+{program_name} WG global 192.168.112.0/24
+
+# 3) Make WireGuard object with endpoint
+{program_name} WG make ReasoningTechnology.com:51820
+# -> WG_1
+
+# 4) Server public key (placeholder)
+{program_name} WG server_provided_public_key WG_1 ABCDEFG...xyz=
+
+# 5) Attach device and install cgroup and eBPF steering
+{program_name} attach WG subu_1 WG_1
+
+# 6) Bring network up (loopback and WireGuard)
+{program_name} network up subu_1
+
+# 7) Test inside namespace
+{program_name} exec subu_1 -- curl -4v https://ifconfig.me
"""
-def VERSION_string():
- return VERSION
+ def version(self):
+ return current_version()
+
+
+def make_text(program_name ="subu"):
+ return Text(program_name)
--- /dev/null
+verbs = [
+ "usage",
+ "help",
+ "example",
+ "version",
+ "init",
+ "make",
+ "make",
+ "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)
+++ /dev/null
-# ---------------- 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 <name>
- run(["groupadd", name])
-
-def _ensure_user(name: str, primary_group: str):
- if not _user_exists(name):
- # useradd -m -g <primary_group> -s /bin/bash <name>
- run(["useradd", "-m", "-g", primary_group, "-s", "/bin/bash", name])
-
-def _add_user_to_group(user: str, group: str):
- run(["usermod", "-aG", group, user])
--- /dev/null
+def version():
+ return "0.3.2"
+++ /dev/null
-
-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 <CIDR>` 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)")
-
+++ /dev/null
-#+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_<id>=
-- Brings =lo= down inside that namespace.
-- Ensures Unix groups:
- - =<masu>=
- - =<masu>-incommon=
-- Ensures Unix user:
- - =<masu>_<subu>...= (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_<id>=
-2. =ip netns exec ns-subu_<id> 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 <endpoint>=
-2. Record server-provided key via =subu WG server_provided_public_key=
-3. Attach interface via =subu attach WG <Subu_ID> <WG_ID>=
-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_<M>= 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