From 41d08d04a60628f43e45682104b181a967a285ef Mon Sep 17 00:00:00 2001 From: goodboy Date: Wed, 12 Aug 2026 19:37:19 -0400 Subject: [PATCH] Fix the `wg` maddr grammar, `/wg/` is *infix* MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The prior revision (and gh #482's examples) had it as a suffix, `/ip4/10.0.11.1/tcp/1616/wg/u`. Wrong: verified against `baudco/py-multiaddr@wg_support` (py-multiaddr#108) installed in a throwaway venv, the canonical form is /ip4/192.168.1.50/udp/51820/wg/u/ip4/10.0.11.1/tcp/1616 where segs *before* `/wg/` are the **bearer** — the underlay `(ip, udp-port)` `wg(8)` itself listens on (`ListenPort`), per the codec docstring's own example — and segs *after* are the **overlay** ep, the only part we ever bind. The suffix form does parse, which is why it slipped through, but it's semantically inverted: overlay addr where the bearer belongs, `tcp` where wg's `udp` goes, and no overlay ep declared at all. Records the observed `[p.name for p in m.protocols()]` lists so the `match` can be written against fact, and replaces the "composed vs not" framing w/ what's actually the design axis: three parts, three **owners** — bearer bound by the kernel via `wg-quick`/`pyroute2`, `/wg/u` bound by nothing (it's an identity, verified out-of-band), overlay bound by our `IPCServer` as `.inner`. `_peel_tunnel_segs()` correspondingly grows a 3rd return, splitting *at* the tunnel seg so nested tunnels fall out for free. Also hoists the netns conclusion to the top of §5.3 where it can't be missed: netns is a **runtime-level config API, not an actor-app-code one**. It's a spawn/boot-time input alongside `enable_transports`/`tpt_bind_addrs`, deliberately w/ no `await actor.enter_netns(...)`, because `setns(2)` neither moves already-created sockets nor applies beyond the calling thread — so a mid-life API would silently leave the IPC server bound in the old ns. (this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`)) --- ai/tpt-backends/03_wg_tunnel_bindspace.md | 74 +++++++++++++++++++---- 1 file changed, 61 insertions(+), 13 deletions(-) diff --git a/ai/tpt-backends/03_wg_tunnel_bindspace.md b/ai/tpt-backends/03_wg_tunnel_bindspace.md index 5f7427e1..6f44c7a9 100644 --- a/ai/tpt-backends/03_wg_tunnel_bindspace.md +++ b/ai/tpt-backends/03_wg_tunnel_bindspace.md @@ -112,25 +112,61 @@ class WGTunnelSpec( ### 3.2 `parse_maddr()`/`mk_maddr()` -Grammar (matches py-multiaddr#108): the `wg` segment is a -*suffix* whose value is the multibase `u` pubkey. +Grammar — **verified** against py-multiaddr#108 +(`baudco/py-multiaddr@wg_support`, installed in a throwaway venv; +all three forms below parse *and* round-trip): ``` -/ip4/10.0.11.1/tcp/1616/wg/u +/ip4/192.168.1.50/udp/51820/wg/u/ip4/10.0.11.1/tcp/1616 +\_______ bearer __________/\__ key __/\______ overlay ______/ + underlay, wg `ListenPort` the ONLY part we bind ``` -- `parse_maddr()` gains - `case [('ip4'|'ip6'), 'tcp', 'wg']:` → build the inner - `TCPAddress`, decode the multibase key to std-base64, return - `TunnelledAddress(inner=..., tunnel=WGTunnelSpec(...))`. +The `/wg/` segment is **infix, not suffix** — the segments +*before* it are the wg **bearer** (the underlay `(ip, udp-port)` +that `wg(8)` itself listens on, per the codec docstring's own +`/ip4/1.2.3.4/udp/51820/wg/{key}` example), and the segments +*after* are the **overlay** endpoint that `tractor` binds. + +⚠️ **CORRECTION** — an earlier revision of this plan (and the +examples in gh #482) used a *suffix* form +`/ip4/10.0.11.1/tcp/1616/wg/u`. That parses, but it is +semantically inverted: it puts the overlay addr where the bearer +belongs, `tcp` where wg's `udp` `ListenPort` goes, and declares +no overlay endpoint at all. `parse_wg_maddr()` in +`examples/wg_lan/` now rejects it with an actionable error. +Observed protocol-name lists, for writing the `match`: + +| maddr | `[p.name for p in m.protocols()]` | +| --- | --- | +| `/ip4/1.2.3.4/udp/51820/wg/u` | `['ip4','udp','wg']` | +| `/ip4/../udp/../wg/u/ip4/../tcp/..` | `['ip4','udp','wg','ip4','tcp']` | + +- so the three parts have **three different owners**, and only the + third is an `Endpoint`: + + | part | bound by | in the runtime? | + | --- | --- | --- | + | bearer | kernel, via `wg-quick`/`pyroute2` | no | + | `/wg/u` | nothing — it's an identity | no, verified out-of-band | + | overlay | `tractor`'s `IPCServer` | **yes**, as `.inner` | + + This owner-split is the real axis of the design, *not* whether + the maddr stack is "composed" (it is). +- `parse_maddr()` gains a case on + `[('ip4'|'ip6'), 'udp', 'wg', ('ip4'|'ip6'), ]` → + build the inner `Address` from the trailing segments, decode + the multibase key to std-base64, and return + `TunnelledAddress(inner=..., tunnel=WGTunnelSpec(...))` with + the bearer recorded in the spec. - keep the existing 2-proto cases byte-identical; add the new case *after* them. -- generalize the match so the *inner* stack is parsed by the - existing logic and `wg` is peeled off first — this is what - makes `/…/udp/…/quic-v1/wg/u` work later without another - case (plan 02 §3.4). Write it as a small pure function: - `_peel_tunnel_segs(proto_names) -> (inner_names, - tunnel_specs)`. +- generalize by **peeling at the tunnel segment**: split + `proto_names` at `'wg'`, hand the trailing list to the existing + inner-stack logic, and recurse for nested tunnels. Write it as + a small pure fn `_peel_tunnel_segs(proto_names) -> + (bearer_names, tunnel_specs, inner_names)`. This is also what + makes a wg-inside-wg stack fall out for free. - `mk_maddr()` inverse for `TunnelledAddress`. - **blocked on upstream**: `Multiaddr('/…/wg/u…')` only parses once py-multiaddr#108 lands. Until then: pin the branch in the @@ -297,6 +333,18 @@ entries. Extend it to carry the tunnel stack, not to *enter* it. ### 5.3 the netns/process reality — read this before designing +**The headline consequence, stated up front**: netns is a +**runtime-level config API, not an actor-app-code API.** It is +declared as part of how an actor process is *brought up* — a +spawn-time/boot-time input alongside `enable_transports` and +`tpt_bind_addrs` — and it is **not** dynamically re-enterable by +app code once the actor is live. There is deliberately no +`await actor.enter_netns(...)`. Two hard reasons, both below: +`setns(2)` doesn't retroactively move existing sockets, and it's +per-thread rather than per-process. Anything that *looks* like a +mid-life API here would be a footgun that silently leaves the IPC +server bound in the old namespace. + - `setns(2)` with `CLONE_NEWNET` affects **the calling thread only**, and sockets already created keep their original netns. A trio actor is effectively single-threaded for our purposes,