adding subu manager
authorThomas Walker Lynch <eknp9n@reasoningtechnology.com>
Sat, 1 Nov 2025 17:46:46 +0000 (17:46 +0000)
committerThomas Walker Lynch <eknp9n@reasoningtechnology.com>
Sat, 1 Nov 2025 17:46:46 +0000 (17:46 +0000)
14 files changed:
developer/source/manager/subu.py [new file with mode: 0755]
developer/source/manager/subu_BPF_force_egress.c [new file with mode: 0644]
developer/source/manager/subu_bpf.py [new file with mode: 0644]
developer/source/manager/subu_core.py [new file with mode: 0644]
developer/source/manager/subu_db.py [new file with mode: 0644]
developer/source/manager/subu_net.py [new file with mode: 0644]
developer/source/manager/subu_text.py [new file with mode: 0644]
developer/source/manager/subu_utils.py [new file with mode: 0644]
developer/source/manager/subu_version.py [new file with mode: 0644]
developer/source/manager/subu_wg.py [new file with mode: 0644]
developer/source/manager/subu_worker_bpf.py [new file with mode: 0644]
developer/source/manager/test.sh [new file with mode: 0644]
developer/source/manager/test_0.sh [new file with mode: 0755]
developer/source/manager/test_0_expected.sh [new file with mode: 0644]

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