0.3.2
authorThomas Walker Lynch <eknp9n@reasoningtechnology.com>
Tue, 4 Nov 2025 14:48:47 +0000 (14:48 +0000)
committerThomas Walker Lynch <eknp9n@reasoningtechnology.com>
Tue, 4 Nov 2025 14:48:47 +0000 (14:48 +0000)
34 files changed:
developer/document/manager.org [new file with mode: 0644]
developer/document/notes.txt [new file with mode: 0644]
developer/manager.tgz [deleted file]
developer/manager/CLI.py
developer/manager/bpf.py [deleted file]
developer/manager/bpf_force_egress.c [deleted file]
developer/manager/bpf_worker.py [deleted file]
developer/manager/core.py [deleted file]
developer/manager/db.py [deleted file]
developer/manager/dispatch.py [new file with mode: 0644]
developer/manager/domain/exec.py [new file with mode: 0644]
developer/manager/domain/network.py [new file with mode: 0644]
developer/manager/domain/options.py [new file with mode: 0644]
developer/manager/domain/subu.py [new file with mode: 0644]
developer/manager/domain/wg.py [new file with mode: 0644]
developer/manager/exec.py [deleted file]
developer/manager/infrastructure/bpf.py [new file with mode: 0644]
developer/manager/infrastructure/bpf_force_egress.c [new file with mode: 0644]
developer/manager/infrastructure/bpf_worker.py [new file with mode: 0644]
developer/manager/infrastructure/db.py [new file with mode: 0644]
developer/manager/infrastructure/schema.sql [new file with mode: 0644]
developer/manager/infrastructure/unix.py [new file with mode: 0644]
developer/manager/network.py [deleted file]
developer/manager/options.py [deleted file]
developer/manager/parser.py [deleted file]
developer/manager/schema.sql [deleted file]
developer/manager/subu.db [new file with mode: 0644]
developer/manager/subu.py [deleted file]
developer/manager/text.py
developer/manager/uncatelogued/parser.py [new file with mode: 0644]
developer/manager/unix.py [deleted file]
developer/manager/version.py [new file with mode: 0644]
developer/manager/wg.py [deleted file]
document/manager.org [deleted file]

diff --git a/developer/document/manager.org b/developer/document/manager.org
new file mode 100644 (file)
index 0000000..77dcebb
--- /dev/null
@@ -0,0 +1,289 @@
+#+TITLE: Subu Manager Specification
+#+AUTHOR: Reasoning Technology / Thomas Walker Lynch
+#+DATE: 2025-11-03
+#+LANGUAGE: en
+#+STARTUP: overview
+#+PROPERTY: header-args :results output
+
+* Overview
+The *Subu Manager* is a command-line orchestration tool for creating and managing
+*lightweight user-containers* (“subus”) with isolated namespaces, private
+WireGuard interfaces, and enforced network routing rules.
+
+It unifies several Linux primitives:
+
+- Unix users and groups (for identity & filesystem isolation)
+- Network namespaces (for network isolation)
+- WireGuard interfaces (for VPN / tunnel endpoints)
+- eBPF cgroup programs (for routing enforcement)
+- SQLite database (for persistence and state tracking)
+
+The manager is designed to evolve toward a full *Subu Light Container System*,
+where each user has nested subordinate users, and each subu can have its own
+network, security policies, and forwarding rules.
+
+---
+
+* Architecture Summary
+** Components
+1. =CLI.py= :: command-line interface
+2. =core.py= :: high-level orchestration logic
+3. =db.py= (planned) :: schema definition and migration
+4. =userutil.py= (planned) :: Unix account and group management helpers
+5. =netutil.py= (planned) :: namespace and interface creation
+6. =bpfutil.py= (planned) :: cgroup/eBPF setup
+
+** Data persistence
+All persistent configuration lives in =subu.db= (SQLite).
+This file contains:
+- *meta* :: creation time, schema version
+- *subu* :: all subuser accounts and their namespace info
+- *wg* :: WireGuard endpoints
+- *links* :: relationships between subu and wg interfaces
+- *options* :: boolean or key/value runtime options
+
+---
+
+* Command Overview
+Each =CLI.py= command corresponds to a top-level operation.  The CLI delegates
+to core functions in =core.py=.
+
+| Command | Description | Implementation |
+|----------+--------------+----------------|
+| =init= | Create the SQLite DB and schema | `core.init_db()` |
+| =make= | Create a new subu hierarchy (user, netns, groups) | `core.make_subu(path_tokens)` |
+| =info= / =information= | Print full record of a subu | `core.get_subu_info()` |
+| =WG= | Manage WireGuard objects and their mapping | `core.create_wg()`, `core.attach_wg()` |
+| =attach= / =detach= | Link or unlink WG interface to subu namespace | `core.attach_wg_to_subu()` |
+| =network up/down= | Bring up or down all attached ifaces | `core.network_toggle()` |
+| =lo up/down= | Bring loopback up/down in subu netns | `core.lo_toggle()` |
+| =option add/remove/list= | Manage options | `core.option_add()` etc. |
+| =exec= | Run command inside subu netns | `core.exec_in_netns()` |
+| =help= / =usage= / =example= | Documentation commands | CLI only |
+| =version= | Print program version | constant in `core.VERSION` |
+
+---
+
+* Subu Creation Flow (=make=)
+
+** Syntax
+#+begin_example
+./CLI.py make Thomas new-subu Rabbit
+#+end_example
+
+** Behavior
+- Verifies that *parent path* (all but last token) exists.
+  - If two-level (e.g. =Thomas US=), requires Unix user =Thomas= exists.
+  - If deeper (e.g. =Thomas new-subu Rabbit=), requires DB entry for
+    =Thomas_new-subu=.
+- Allocates next available subu ID (first free integer).
+- Inserts row in DB with:
+  - =id=, =owner=, =name=, =full_unix_name=, =path=, =netns_name=
+- Creates network namespace =ns-subu_<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
diff --git a/developer/document/notes.txt b/developer/document/notes.txt
new file mode 100644 (file)
index 0000000..ed89643
--- /dev/null
@@ -0,0 +1,28 @@
+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)
diff --git a/developer/manager.tgz b/developer/manager.tgz
deleted file mode 100644 (file)
index f802d65..0000000
Binary files a/developer/manager.tgz and /dev/null differ
index a79691e..23278d7 100755 (executable)
 #!/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())
diff --git a/developer/manager/bpf.py b/developer/manager/bpf.py
deleted file mode 100644 (file)
index 527d419..0000000
+++ /dev/null
@@ -1,52 +0,0 @@
-
-def attach_wg(subu_id: str, wg_id: str):
-  ensure_mounts()
-  sid = int(subu_id.split("_")[1]); wid = int(wg_id.split("_")[1])
-  with closing(_db()) as db:
-    r = db.execute("SELECT netns FROM subu WHERE id=?", (sid,)).fetchone()
-    if not r: raise ValueError("subu not found")
-    ns = r[0]
-    w = db.execute("SELECT endpoint, local_ip, pubkey FROM wg WHERE id=?", (wid,)).fetchone()
-    if not w: raise ValueError("WG not found")
-    endpoint, local_ip, pubkey = w
-
-  ifname = f"subu_{wid}"
-  # create WG link in init ns, move to netns
-  run(["ip", "link", "add", ifname, "type", "wireguard"])
-  run(["ip", "link", "set", ifname, "netns", ns])
-  run(["ip", "-n", ns, "addr", "add", local_ip, "dev", ifname], check=False)
-  run(["ip", "-n", ns, "link", "set", "dev", ifname, "mtu", "1420"])
-  run(["ip", "-n", ns, "link", "set", "dev", ifname, "down"])  # keep engine down until `network up`
-
-  # install steering (MVP: create cgroup + attach bpf program)
-  try:
-    install_steering(subu_id, ns, ifname)
-    print(f"{subu_id}: eBPF steering installed -> {ifname}")
-  except BpfError as e:
-    print(f"{subu_id}: steering warning: {e}")
-
-  with closing(_db()) as db:
-    db.execute("UPDATE subu SET wg_id=? WHERE id=?", (wid, sid))
-    db.commit()
-  print(f"attached {wg_id} to {subu_id} in {ns} as {ifname}")
-
-def detach_wg(subu_id: str):
-  ensure_mounts()
-  sid = int(subu_id.split("_")[1])
-  with closing(_db()) as db:
-    r = db.execute("SELECT netns,wg_id FROM subu WHERE id=?", (sid,)).fetchone()
-    if not r: print("not found"); return
-    ns, wid = r
-    if wid is None:
-      print("nothing attached"); return
-  ifname = f"subu_{wid}"
-  run(["ip", "-n", ns, "link", "del", ifname], check=False)
-  try:
-    remove_steering(subu_id)
-  except BpfError as e:
-    print(f"steering remove warn: {e}")
-  with closing(_db()) as db:
-    db.execute("UPDATE subu SET wg_id=NULL WHERE id=?", (sid,))
-    db.commit()
-  print(f"detached WG_{wid} from {subu_id}")
-
diff --git a/developer/manager/bpf_force_egress.c b/developer/manager/bpf_force_egress.c
deleted file mode 100644 (file)
index c3aedec..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-// -*- 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;
-}
diff --git a/developer/manager/bpf_worker.py b/developer/manager/bpf_worker.py
deleted file mode 100644 (file)
index 96aef14..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
-"""
-worker_bpf.py — create per-subu cgroups and load eBPF (MVP)
-Version: 0.2.0
-"""
-import os, subprocess, json
-from pathlib import Path
-
-class BpfError(RuntimeError): pass
-
-def run(cmd, check=True):
-  r = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
-  if check and r.returncode != 0:
-    raise BpfError(f"cmd failed: {' '.join(cmd)}\n{r.stderr}")
-  return r.stdout.strip()
-
-def ensure_mounts():
-  # ensure bpf and cgroup v2 are mounted
-  try:
-    Path("/sys/fs/bpf").mkdir(parents=True, exist_ok=True)
-    run(["mount","-t","bpf","bpf","/sys/fs/bpf"], check=False)
-  except Exception:
-    pass
-  try:
-    Path("/sys/fs/cgroup").mkdir(parents=True, exist_ok=True)
-    run(["mount","-t","cgroup2","none","/sys/fs/cgroup"], check=False)
-  except Exception:
-    pass
-
-def cgroup_path(subu_id: str) -> str:
-  return f"/sys/fs/cgroup/{subu_id}"
-
-def install_steering(subu_id: str, netns: str, ifname: str):
-  ensure_mounts()
-  cg = Path(cgroup_path(subu_id))
-  cg.mkdir(parents=True, exist_ok=True)
-
-  # compile BPF
-  obj = Path("./bpf_force_egress.o")
-  src = Path("./bpf_force_egress.c")
-  if not src.exists():
-    raise BpfError("bpf_force_egress.c missing next to manager")
-
-  # Build object (requires clang/llc/bpftool)
-  run(["clang","-O2","-g","-target","bpf","-c",str(src),"-o",str(obj)])
-
-  # Load program into bpffs; attach to cgroup/inet4_connect + inet4_post_bind (MVP)
-  pinned = f"/sys/fs/bpf/{subu_id}_egress"
-  run(["bpftool","prog","loadall",str(obj),pinned], check=True)
-
-  # Attach to hooks (MVP validation hooks)
-  # NOTE: these are safe no-ops for now; they validate UID and stash ifindex map.
-  for hook in ("cgroup/connect4","cgroup/post_bind4"):
-    run(["bpftool","cgroup","attach",cgroup_path(subu_id),"attach",hook,"pinned",f"{pinned}/prog_0"], check=False)
-
-  # Write metadata for ifname (saved for future prog versions)
-  meta = {"ifname": ifname}
-  Path(f"/sys/fs/bpf/{subu_id}_meta.json").write_text(json.dumps(meta))
-
-def remove_steering(subu_id: str):
-  cg = cgroup_path(subu_id)
-  # Detach whatever is attached
-  for hook in ("cgroup/connect4","cgroup/post_bind4"):
-    subprocess.run(["bpftool","cgroup","detach",cg,"detach",hook], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
-  # Remove pinned prog dir
-  pinned = Path(f"/sys/fs/bpf/{subu_id}_egress")
-  if pinned.exists():
-    subprocess.run(["bpftool","prog","detach",str(pinned)], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
-    try:
-      for p in pinned.glob("*"): p.unlink()
-      pinned.rmdir()
-    except Exception:
-      pass
-  # Remove cgroup dir
-  try:
-    Path(cg).rmdir()
-  except Exception:
-    pass
diff --git a/developer/manager/core.py b/developer/manager/core.py
deleted file mode 100644 (file)
index f66cdf9..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-# -*- 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()
-
-
diff --git a/developer/manager/db.py b/developer/manager/db.py
deleted file mode 100644 (file)
index c42b2bc..0000000
+++ /dev/null
@@ -1,86 +0,0 @@
-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
diff --git a/developer/manager/dispatch.py b/developer/manager/dispatch.py
new file mode 100644 (file)
index 0000000..d0cce3c
--- /dev/null
@@ -0,0 +1,167 @@
+#!/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")
diff --git a/developer/manager/domain/exec.py b/developer/manager/domain/exec.py
new file mode 100644 (file)
index 0000000..0826560
--- /dev/null
@@ -0,0 +1,12 @@
+"""
+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)
diff --git a/developer/manager/domain/network.py b/developer/manager/domain/network.py
new file mode 100644 (file)
index 0000000..2ea77b4
--- /dev/null
@@ -0,0 +1,33 @@
+"""
+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"])
diff --git a/developer/manager/domain/options.py b/developer/manager/domain/options.py
new file mode 100644 (file)
index 0000000..76fd97e
--- /dev/null
@@ -0,0 +1,31 @@
+"""
+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}")
+
diff --git a/developer/manager/domain/subu.py b/developer/manager/domain/subu.py
new file mode 100644 (file)
index 0000000..7f77f86
--- /dev/null
@@ -0,0 +1,200 @@
+"""
+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
diff --git a/developer/manager/domain/wg.py b/developer/manager/domain/wg.py
new file mode 100644 (file)
index 0000000..0cf6126
--- /dev/null
@@ -0,0 +1,60 @@
+"""
+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)")
+
diff --git a/developer/manager/exec.py b/developer/manager/exec.py
deleted file mode 100644 (file)
index f823d9a..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-
-def exec_in_subu(subu_id: str, cmd: list):
-  sid = int(subu_id.split("_")[1])
-  with closing(_db()) as db:
-    ns = db.execute("SELECT netns FROM subu WHERE id=?", (sid,)).fetchone()[0]
-  os.execvp("ip", ["ip","netns","exec", ns] + cmd)
diff --git a/developer/manager/infrastructure/bpf.py b/developer/manager/infrastructure/bpf.py
new file mode 100644 (file)
index 0000000..16c4388
--- /dev/null
@@ -0,0 +1,60 @@
+"""
+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}")
+
diff --git a/developer/manager/infrastructure/bpf_force_egress.c b/developer/manager/infrastructure/bpf_force_egress.c
new file mode 100644 (file)
index 0000000..628cc83
--- /dev/null
@@ -0,0 +1,48 @@
+// -*- 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;
+}
diff --git a/developer/manager/infrastructure/bpf_worker.py b/developer/manager/infrastructure/bpf_worker.py
new file mode 100644 (file)
index 0000000..66aaf6d
--- /dev/null
@@ -0,0 +1,84 @@
+# -*- 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
diff --git a/developer/manager/infrastructure/db.py b/developer/manager/infrastructure/db.py
new file mode 100644 (file)
index 0000000..83fb5c5
--- /dev/null
@@ -0,0 +1,136 @@
+# 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
diff --git a/developer/manager/infrastructure/schema.sql b/developer/manager/infrastructure/schema.sql
new file mode 100644 (file)
index 0000000..ab8d80a
--- /dev/null
@@ -0,0 +1,17 @@
+-- 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
+);
diff --git a/developer/manager/infrastructure/unix.py b/developer/manager/infrastructure/unix.py
new file mode 100644 (file)
index 0000000..fec6d9a
--- /dev/null
@@ -0,0 +1,41 @@
+"""
+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])
diff --git a/developer/manager/network.py b/developer/manager/network.py
deleted file mode 100644 (file)
index 000bbf8..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-
-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"])
diff --git a/developer/manager/options.py b/developer/manager/options.py
deleted file mode 100644 (file)
index 76b5caa..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-
-def option_set(subu_id: str, name: str, value: str):
-  sid = int(subu_id.split("_")[1])
-  with closing(_db()) as db:
-    db.execute("INSERT INTO options (subu_id,name,value) VALUES(?,?,?) "
-               "ON CONFLICT(subu_id,name) DO UPDATE SET value=excluded.value",
-               (sid, name, value))
-    db.commit()
-  print("ok")
-
-def option_get(subu_id: str, name: str):
-  sid = int(subu_id.split("_")[1])
-  with closing(_db()) as db:
-    row = db.execute("SELECT value FROM options WHERE subu_id=? AND name=?", (sid,name)).fetchone()
-  print(row[0] if row else "")
-
-def option_list(subu_id: str):
-  sid = int(subu_id.split("_")[1])
-  with closing(_db()) as db:
-    rows = db.execute("SELECT name,value FROM options WHERE subu_id=?", (sid,)).fetchall()
-  for n,v in rows:
-    print(f"{n}={v}")
-
diff --git a/developer/manager/parser.py b/developer/manager/parser.py
deleted file mode 100644 (file)
index d0c2f47..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-verbs = [
-    "usage",
-    "help",
-    "example",
-    "version",
-    "init",
-    "make",
-    "create",
-    "info",
-    "information",
-    "WG",
-    "attach",
-    "detach",
-    "network",
-    "lo",
-    "option",
-    "exec",
-]
-
-p_make = subparsers.add_parser(
-    "make",
-    help="Create a Subu with hierarchical name + Unix user/groups + netns",
-)
-p_make.add_argument(
-    "path",
-    nargs="+",
-    help="Full Subu path, e.g. 'Thomas US' or 'Thomas new-subu Rabbit'",
-)
-
-elif args.verb == "make":
-    subu_id = core.make_subu(args.path)
-    print(subu_id)
diff --git a/developer/manager/schema.sql b/developer/manager/schema.sql
deleted file mode 100644 (file)
index a33ae95..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-CREATE TABLE subu (
-  id            INTEGER PRIMARY KEY,
-  owner         TEXT NOT NULL,           -- root user, e.g. 'Thomas'
-  name          TEXT NOT NULL,           -- leaf, e.g. 'US', 'Rabbit'
-  full_unix_name TEXT NOT NULL UNIQUE,   -- e.g. 'Thomas_US_Rabbit'
-  path          TEXT NOT NULL,           -- e.g. 'Thomas US Rabbit'
-  netns_name    TEXT NOT NULL,
-  wg_id         INTEGER,                 -- nullable for now
-  created_at    TEXT NOT NULL,
-  updated_at    TEXT NOT NULL
-);
diff --git a/developer/manager/subu.db b/developer/manager/subu.db
new file mode 100644 (file)
index 0000000..aff27ef
Binary files /dev/null and b/developer/manager/subu.db differ
diff --git a/developer/manager/subu.py b/developer/manager/subu.py
deleted file mode 100644 (file)
index ea5ad0c..0000000
+++ /dev/null
@@ -1,150 +0,0 @@
-# ------------- 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
index d5ff982..0b1b364 100644 (file)
-# -*- 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)
diff --git a/developer/manager/uncatelogued/parser.py b/developer/manager/uncatelogued/parser.py
new file mode 100644 (file)
index 0000000..cf2dd84
--- /dev/null
@@ -0,0 +1,32 @@
+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)
diff --git a/developer/manager/unix.py b/developer/manager/unix.py
deleted file mode 100644 (file)
index 7773e4c..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-# ---------------- 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])
diff --git a/developer/manager/version.py b/developer/manager/version.py
new file mode 100644 (file)
index 0000000..ed3750c
--- /dev/null
@@ -0,0 +1,2 @@
+def version():
+  return "0.3.2"
diff --git a/developer/manager/wg.py b/developer/manager/wg.py
deleted file mode 100644 (file)
index 3be049c..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-
-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)")
-
diff --git a/document/manager.org b/document/manager.org
deleted file mode 100644 (file)
index 77dcebb..0000000
+++ /dev/null
@@ -1,289 +0,0 @@
-#+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