Fix the `wg` maddr grammar, `/wg/` is *infix*
The prior revision (and gh #482's examples) had it as a suffix, `/ip4/10.0.11.1/tcp/1616/wg/u<key>`. 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<A_pub>/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<key>` 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`))ng_tpts_planning
parent
208876cc3b
commit
41d08d04a6
|
|
@ -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<base64url>` 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<A_pub-b64url>
|
||||
/ip4/192.168.1.50/udp/51820/wg/u<A_pub>/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<key>`. 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<k>` | `['ip4','udp','wg']` |
|
||||
| `/ip4/../udp/../wg/u<k>/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<key>` | 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'), <inner-l4>]` →
|
||||
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<key>` 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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue