structureing the code
authorThomas Walker Lynch <eknp9n@reasoningtechnology.com>
Tue, 4 Nov 2025 07:15:11 +0000 (07:15 +0000)
committerThomas Walker Lynch <eknp9n@reasoningtechnology.com>
Tue, 4 Nov 2025 07:15:11 +0000 (07:15 +0000)
26 files changed:
developer/manager.tgz [new file with mode: 0644]
developer/manager/bpf.py [new file with mode: 0644]
developer/manager/bpf_worker.py [new file with mode: 0644]
developer/manager/core.py
developer/manager/db.py [new file with mode: 0644]
developer/manager/exec.py [new file with mode: 0644]
developer/manager/network.py [new file with mode: 0644]
developer/manager/options.py [new file with mode: 0644]
developer/manager/parser.py [new file with mode: 0644]
developer/manager/schema.sql [new file with mode: 0644]
developer/manager/subu.py [new file with mode: 0644]
developer/manager/temp.sh [deleted file]
developer/manager/test.sh [deleted file]
developer/manager/test_0.sh [deleted file]
developer/manager/test_0_expected.sh [deleted file]
developer/manager/text.py
developer/manager/unix.py [new file with mode: 0644]
developer/manager/wg.py [new file with mode: 0644]
developer/manager/worker_bpf.py [deleted file]
document/manager.org [new file with mode: 0644]
tester/manager/test.sh [new file with mode: 0644]
tester/manager/test_0.sh [new file with mode: 0755]
tester/manager/test_0_expected.sh [new file with mode: 0644]
tester/test.sh [new file with mode: 0644]
tester/test_0.sh [new file with mode: 0755]
tester/test_0_expected.sh [new file with mode: 0644]

diff --git a/developer/manager.tgz b/developer/manager.tgz
new file mode 100644 (file)
index 0000000..f802d65
Binary files /dev/null and b/developer/manager.tgz differ
diff --git a/developer/manager/bpf.py b/developer/manager/bpf.py
new file mode 100644 (file)
index 0000000..527d419
--- /dev/null
@@ -0,0 +1,52 @@
+
+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_worker.py b/developer/manager/bpf_worker.py
new file mode 100644 (file)
index 0000000..96aef14
--- /dev/null
@@ -0,0 +1,78 @@
+# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
+"""
+worker_bpf.py — create per-subu cgroups and load eBPF (MVP)
+Version: 0.2.0
+"""
+import os, subprocess, json
+from pathlib import Path
+
+class BpfError(RuntimeError): pass
+
+def run(cmd, check=True):
+  r = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
+  if check and r.returncode != 0:
+    raise BpfError(f"cmd failed: {' '.join(cmd)}\n{r.stderr}")
+  return r.stdout.strip()
+
+def ensure_mounts():
+  # ensure bpf and cgroup v2 are mounted
+  try:
+    Path("/sys/fs/bpf").mkdir(parents=True, exist_ok=True)
+    run(["mount","-t","bpf","bpf","/sys/fs/bpf"], check=False)
+  except Exception:
+    pass
+  try:
+    Path("/sys/fs/cgroup").mkdir(parents=True, exist_ok=True)
+    run(["mount","-t","cgroup2","none","/sys/fs/cgroup"], check=False)
+  except Exception:
+    pass
+
+def cgroup_path(subu_id: str) -> str:
+  return f"/sys/fs/cgroup/{subu_id}"
+
+def install_steering(subu_id: str, netns: str, ifname: str):
+  ensure_mounts()
+  cg = Path(cgroup_path(subu_id))
+  cg.mkdir(parents=True, exist_ok=True)
+
+  # compile BPF
+  obj = Path("./bpf_force_egress.o")
+  src = Path("./bpf_force_egress.c")
+  if not src.exists():
+    raise BpfError("bpf_force_egress.c missing next to manager")
+
+  # Build object (requires clang/llc/bpftool)
+  run(["clang","-O2","-g","-target","bpf","-c",str(src),"-o",str(obj)])
+
+  # Load program into bpffs; attach to cgroup/inet4_connect + inet4_post_bind (MVP)
+  pinned = f"/sys/fs/bpf/{subu_id}_egress"
+  run(["bpftool","prog","loadall",str(obj),pinned], check=True)
+
+  # Attach to hooks (MVP validation hooks)
+  # NOTE: these are safe no-ops for now; they validate UID and stash ifindex map.
+  for hook in ("cgroup/connect4","cgroup/post_bind4"):
+    run(["bpftool","cgroup","attach",cgroup_path(subu_id),"attach",hook,"pinned",f"{pinned}/prog_0"], check=False)
+
+  # Write metadata for ifname (saved for future prog versions)
+  meta = {"ifname": ifname}
+  Path(f"/sys/fs/bpf/{subu_id}_meta.json").write_text(json.dumps(meta))
+
+def remove_steering(subu_id: str):
+  cg = cgroup_path(subu_id)
+  # Detach whatever is attached
+  for hook in ("cgroup/connect4","cgroup/post_bind4"):
+    subprocess.run(["bpftool","cgroup","detach",cg,"detach",hook], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
+  # Remove pinned prog dir
+  pinned = Path(f"/sys/fs/bpf/{subu_id}_egress")
+  if pinned.exists():
+    subprocess.run(["bpftool","prog","detach",str(pinned)], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
+    try:
+      for p in pinned.glob("*"): p.unlink()
+      pinned.rmdir()
+    except Exception:
+      pass
+  # Remove cgroup dir
+  try:
+    Path(cg).rmdir()
+  except Exception:
+    pass
index c363ec2..f66cdf9 100644 (file)
@@ -8,6 +8,7 @@ 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")
@@ -18,237 +19,4 @@ def run(cmd, check=True):
     raise RuntimeError(f"cmd failed: {' '.join(cmd)}\n{r.stderr}")
   return r.stdout.strip()
 
-# ---------------- DB ----------------
-def _db():
-  if not DB_FILE.exists():
-    raise FileNotFoundError("subu.db not found; run `subu init <token>` first")
-  return sqlite3.connect(DB_FILE)
 
-def cmd_init(token: str|None):
-  if DB_FILE.exists():
-    raise FileExistsError("db already exists")
-  if not token or len(token) < 6:
-    raise ValueError("init requires a 6+ char token")
-  with closing(sqlite3.connect(DB_FILE)) as db:
-    c = db.cursor()
-    c.executescript("""
-      CREATE TABLE subu (
-        id INTEGER PRIMARY KEY AUTOINCREMENT,
-        owner TEXT,
-        name TEXT,
-        netns TEXT,
-        lo_state TEXT DEFAULT 'down',
-        wg_id INTEGER,
-        network_state TEXT DEFAULT 'down'
-      );
-      CREATE TABLE wg (
-        id INTEGER PRIMARY KEY AUTOINCREMENT,
-        endpoint TEXT,
-        local_ip TEXT,
-        allowed_ips TEXT,
-        pubkey TEXT,
-        state TEXT DEFAULT 'down'
-      );
-      CREATE TABLE options (
-        subu_id INTEGER,
-        name TEXT,
-        value TEXT,
-        PRIMARY KEY (subu_id, name)
-      );
-    """)
-    db.commit()
-  print(f"created subu.db (v{VERSION})")
-
-# ------------- Subu ops -------------
-def create_subu(owner: str, name: str) -> str:
-  with closing(_db()) as db:
-    c = db.cursor()
-    subu_netns = f"ns-subu_tmp"  # temp; we rename after ID known
-    c.execute("INSERT INTO subu (owner, name, netns) VALUES (?, ?, ?)",
-              (owner, name, subu_netns))
-    sid = c.lastrowid
-    netns = f"ns-subu_{sid}"
-    c.execute("UPDATE subu SET netns=? WHERE id=?", (netns, sid))
-    db.commit()
-
-  # create netns
-  run(["ip", "netns", "add", netns])
-  run(["ip", "-n", netns, "link", "set", "lo", "down"])
-  print(f"Created subu_{sid} ({owner}:{name}) with netns {netns}")
-  return f"subu_{sid}"
-
-def list_subu():
-  with closing(_db()) as db:
-    for row in db.execute("SELECT id, owner, name, netns, lo_state, wg_id, network_state FROM subu"):
-      print(row)
-
-def info_subu(subu_id: str):
-  sid = int(subu_id.split("_")[1])
-  with closing(_db()) as db:
-    row = db.execute("SELECT * FROM subu WHERE id=?", (sid,)).fetchone()
-    if not row:
-      print("not found"); return
-    print(row)
-    wg = db.execute("SELECT wg_id FROM subu WHERE id=?", (sid,)).fetchone()[0]
-    if wg is not None:
-      wrow = db.execute("SELECT * FROM wg WHERE id=?", (wg,)).fetchone()
-      print("WG:", wrow)
-    opts = db.execute("SELECT name,value FROM options WHERE subu_id=?", (sid,)).fetchall()
-    print("Options:", opts)
-
-def lo_toggle(subu_id: str, state: str):
-  sid = int(subu_id.split("_")[1])
-  with closing(_db()) as db:
-    ns = db.execute("SELECT netns FROM subu WHERE id=?", (sid,)).fetchone()
-    if not ns: raise ValueError("subu not found")
-    ns = ns[0]
-    run(["ip", "netns", "exec", ns, "ip", "link", "set", "lo", state])
-    db.execute("UPDATE subu SET lo_state=? WHERE id=?", (state, sid))
-    db.commit()
-  print(f"{subu_id}: lo {state}")
-
-# ------------- WG ops ---------------
-def wg_global(basecidr: str):
-  WG_GLOBAL_FILE.write_text(basecidr.strip()+"\n")
-  print(f"WG pool base = {basecidr}")
-
-def _alloc_ip(idx: int, base: str) -> str:
-  # simplistic /24 allocator: base must be x.y.z.0/24
-  prefix = base.split("/")[0].rsplit(".", 1)[0]
-  host = 2 + idx
-  return f"{prefix}.{host}/32"
-
-def wg_create(endpoint: str) -> str:
-  if not WG_GLOBAL_FILE.exists():
-    raise RuntimeError("set WG base with `subu WG global <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)")
-
-# ---------- attach/detach + BPF ----------
-def attach_wg(subu_id: str, wg_id: str):
-  ensure_mounts()
-  sid = int(subu_id.split("_")[1]); wid = int(wg_id.split("_")[1])
-  with closing(_db()) as db:
-    r = db.execute("SELECT netns FROM subu WHERE id=?", (sid,)).fetchone()
-    if not r: raise ValueError("subu not found")
-    ns = r[0]
-    w = db.execute("SELECT endpoint, local_ip, pubkey FROM wg WHERE id=?", (wid,)).fetchone()
-    if not w: raise ValueError("WG not found")
-    endpoint, local_ip, pubkey = w
-
-  ifname = f"subu_{wid}"
-  # create WG link in init ns, move to netns
-  run(["ip", "link", "add", ifname, "type", "wireguard"])
-  run(["ip", "link", "set", ifname, "netns", ns])
-  run(["ip", "-n", ns, "addr", "add", local_ip, "dev", ifname], check=False)
-  run(["ip", "-n", ns, "link", "set", "dev", ifname, "mtu", "1420"])
-  run(["ip", "-n", ns, "link", "set", "dev", ifname, "down"])  # keep engine down until `network up`
-
-  # install steering (MVP: create cgroup + attach bpf program)
-  try:
-    install_steering(subu_id, ns, ifname)
-    print(f"{subu_id}: eBPF steering installed -> {ifname}")
-  except BpfError as e:
-    print(f"{subu_id}: steering warning: {e}")
-
-  with closing(_db()) as db:
-    db.execute("UPDATE subu SET wg_id=? WHERE id=?", (wid, sid))
-    db.commit()
-  print(f"attached {wg_id} to {subu_id} in {ns} as {ifname}")
-
-def detach_wg(subu_id: str):
-  ensure_mounts()
-  sid = int(subu_id.split("_")[1])
-  with closing(_db()) as db:
-    r = db.execute("SELECT netns,wg_id FROM subu WHERE id=?", (sid,)).fetchone()
-    if not r: print("not found"); return
-    ns, wid = r
-    if wid is None:
-      print("nothing attached"); return
-  ifname = f"subu_{wid}"
-  run(["ip", "-n", ns, "link", "del", ifname], check=False)
-  try:
-    remove_steering(subu_id)
-  except BpfError as e:
-    print(f"steering remove warn: {e}")
-  with closing(_db()) as db:
-    db.execute("UPDATE subu SET wg_id=NULL WHERE id=?", (sid,))
-    db.commit()
-  print(f"detached WG_{wid} from {subu_id}")
-
-# ------------- network up/down -------------
-def network_toggle(subu_id: str, state: str):
-  sid = int(subu_id.split("_")[1])
-  with closing(_db()) as db:
-    ns, wid = db.execute("SELECT netns,wg_id FROM subu WHERE id=?", (sid,)).fetchone()
-  # always make sure lo up on 'up'
-  if state == "up":
-    run(["ip", "netns", "exec", ns, "ip", "link", "set", "lo", "up"], check=False)
-  if wid is not None:
-    ifname = f"subu_{wid}"
-    run(["ip", "-n", ns, "link", "set", "dev", ifname, state], check=False)
-  with closing(_db()) as db:
-    db.execute("UPDATE subu SET network_state=? WHERE id=?", (state, sid))
-    db.commit()
-  print(f"{subu_id}: network {state}")
-
-# ------------- options ----------------
-def option_set(subu_id: str, name: str, value: str):
-  sid = int(subu_id.split("_")[1])
-  with closing(_db()) as db:
-    db.execute("INSERT INTO options (subu_id,name,value) VALUES(?,?,?) "
-               "ON CONFLICT(subu_id,name) DO UPDATE SET value=excluded.value",
-               (sid, name, value))
-    db.commit()
-  print("ok")
-
-def option_get(subu_id: str, name: str):
-  sid = int(subu_id.split("_")[1])
-  with closing(_db()) as db:
-    row = db.execute("SELECT value FROM options WHERE subu_id=? AND name=?", (sid,name)).fetchone()
-  print(row[0] if row else "")
-
-def option_list(subu_id: str):
-  sid = int(subu_id.split("_")[1])
-  with closing(_db()) as db:
-    rows = db.execute("SELECT name,value FROM options WHERE subu_id=?", (sid,)).fetchall()
-  for n,v in rows:
-    print(f"{n}={v}")
-
-# ------------- exec -------------------
-def exec_in_subu(subu_id: str, cmd: list):
-  sid = int(subu_id.split("_")[1])
-  with closing(_db()) as db:
-    ns = db.execute("SELECT netns FROM subu WHERE id=?", (sid,)).fetchone()[0]
-  os.execvp("ip", ["ip","netns","exec", ns] + cmd)
diff --git a/developer/manager/db.py b/developer/manager/db.py
new file mode 100644 (file)
index 0000000..c42b2bc
--- /dev/null
@@ -0,0 +1,86 @@
+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/exec.py b/developer/manager/exec.py
new file mode 100644 (file)
index 0000000..f823d9a
--- /dev/null
@@ -0,0 +1,6 @@
+
+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/network.py b/developer/manager/network.py
new file mode 100644 (file)
index 0000000..000bbf8
--- /dev/null
@@ -0,0 +1,24 @@
+
+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
new file mode 100644 (file)
index 0000000..76b5caa
--- /dev/null
@@ -0,0 +1,23 @@
+
+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
new file mode 100644 (file)
index 0000000..d0c2f47
--- /dev/null
@@ -0,0 +1,32 @@
+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
new file mode 100644 (file)
index 0000000..a33ae95
--- /dev/null
@@ -0,0 +1,11 @@
+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.py b/developer/manager/subu.py
new file mode 100644 (file)
index 0000000..ea5ad0c
--- /dev/null
@@ -0,0 +1,150 @@
+# ------------- 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
diff --git a/developer/manager/temp.sh b/developer/manager/temp.sh
deleted file mode 100644 (file)
index 36855b6..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-# from: /home/Thomas/subu_data/developer/project/active/subu/developer/source/manager
-
-set -euo pipefail
-
-echo "== 1) Backup legacy-prefixed modules =="
-mkdir -p _old_prefixed
-for f in subu_*.py; do
-  [ -f "$f" ] && mv -v "$f" _old_prefixed/
-done
-[ -f subu_worker_bpf.py ] && mv -v subu_worker_bpf.py _old_prefixed/ || true
-
-echo "== 2) Ensure only the new module names remain =="
-# Keep these (already present in your tar):
-#   CLI.py  core.py  text.py  worker_bpf.py  bpf_force_egress.c
-ls -1
-
-echo "== 3) Make CLI runnable as 'subu' =="
-# Make sure CLI has a shebang; add if missing
-if ! head -n1 CLI.py | grep -q '^#!/usr/bin/env python3'; then
-  (printf '%s\n' '#!/usr/bin/env python3' ; cat CLI.py) > .CLI.tmp && mv .CLI.tmp CLI.py
-fi
-chmod +x CLI.py
-ln -sf CLI.py subu
-chmod +x subu
-
-echo "== 4) Quick import sanity =="
-# Fail if any of the remaining files still import the old module names
-bad=$(grep -R --line-number -E 'import +subu_|from +subu_' -- *.py || true)
-if [ -n "$bad" ]; then
-  echo "Found old-style imports; please fix:" >&2
-  echo "$bad" >&2
-  exit 1
-fi
-
-echo "== 5) Show version and help =="
-./subu version || true
-./subu help    || true
-./subu         || true  # should print usage by default
-
-echo "== Done. If this looks good, you can delete _old_prefixed when ready. =="
diff --git a/developer/manager/test.sh b/developer/manager/test.sh
deleted file mode 100644 (file)
index 706250b..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/bin/env bash
-
-set -x
-./CLI                 # -> USAGE (exit 0)
-./CLI usage           # -> USAGE
-./CLI -h              # -> HELP
-./CLI --help          # -> HELP
-./CLI help            # -> HELP
-./CLI help WG         # -> WG topic help (or full HELP if topic unknown)
-./CLI example         # -> EXAMPLE
-./CLI version         # -> 0.1.4
-./CLI -V              # -> 0.1.4
-
diff --git a/developer/manager/test_0.sh b/developer/manager/test_0.sh
deleted file mode 100755 (executable)
index ac354d3..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-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/manager/test_0_expected.sh b/developer/manager/test_0_expected.sh
deleted file mode 100644 (file)
index 8e31ed5..0000000
+++ /dev/null
@@ -1,353 +0,0 @@
-++ ./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
index 84f6762..d5ff982 100644 (file)
@@ -78,31 +78,50 @@ Subu manager (v0.2.0)
 """
 
 EXAMPLE = """\
-# 0) Init
-subu init dzkq7b
+# 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 Subu
+# 1) Create a Subu “US” owned by user Thomas
 subu create Thomas US
-# -> subu_1
+# -> Subu_ID: subu_7
+# -> netns: ns-subu_7 with lo (down)
 
-# 2) WG pool once
+# 2) Define a global WireGuard address pool (once per host)
 subu WG global 192.168.112.0/24
-
-# 3) Create WG object with endpoint
-subu WG create ReasoningTechnology.com:51820
-# -> WG_1
-
-# 4) Pubkey (placeholder)
-subu WG server_provided_public_key WG_1 ABCDEFG...xyz=
-
-# 5) Attach device and install cgroup+BPF steering
-subu attach WG subu_1 WG_1
-
-# 6) Bring network up (lo + WG)
-subu network up subu_1
-
-# 7) Test inside ns
-subu exec subu_1 -- curl -4v https://ifconfig.me
+# -> 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 VERSION_string():
diff --git a/developer/manager/unix.py b/developer/manager/unix.py
new file mode 100644 (file)
index 0000000..7773e4c
--- /dev/null
@@ -0,0 +1,28 @@
+# ---------------- 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/wg.py b/developer/manager/wg.py
new file mode 100644 (file)
index 0000000..3be049c
--- /dev/null
@@ -0,0 +1,48 @@
+
+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/developer/manager/worker_bpf.py b/developer/manager/worker_bpf.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/document/manager.org b/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/tester/manager/test.sh b/tester/manager/test.sh
new file mode 100644 (file)
index 0000000..706250b
--- /dev/null
@@ -0,0 +1,13 @@
+#!/bin/env bash
+
+set -x
+./CLI                 # -> USAGE (exit 0)
+./CLI usage           # -> USAGE
+./CLI -h              # -> HELP
+./CLI --help          # -> HELP
+./CLI help            # -> HELP
+./CLI help WG         # -> WG topic help (or full HELP if topic unknown)
+./CLI example         # -> EXAMPLE
+./CLI version         # -> 0.1.4
+./CLI -V              # -> 0.1.4
+
diff --git a/tester/manager/test_0.sh b/tester/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/tester/manager/test_0_expected.sh b/tester/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
diff --git a/tester/test.sh b/tester/test.sh
new file mode 100644 (file)
index 0000000..706250b
--- /dev/null
@@ -0,0 +1,13 @@
+#!/bin/env bash
+
+set -x
+./CLI                 # -> USAGE (exit 0)
+./CLI usage           # -> USAGE
+./CLI -h              # -> HELP
+./CLI --help          # -> HELP
+./CLI help            # -> HELP
+./CLI help WG         # -> WG topic help (or full HELP if topic unknown)
+./CLI example         # -> EXAMPLE
+./CLI version         # -> 0.1.4
+./CLI -V              # -> 0.1.4
+
diff --git a/tester/test_0.sh b/tester/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/tester/test_0_expected.sh b/tester/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