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()`
|
### 3.2 `parse_maddr()`/`mk_maddr()`
|
||||||
|
|
||||||
Grammar (matches py-multiaddr#108): the `wg` segment is a
|
Grammar — **verified** against py-multiaddr#108
|
||||||
*suffix* whose value is the multibase `u<base64url>` pubkey.
|
(`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
|
The `/wg/` segment is **infix, not suffix** — the segments
|
||||||
`case [('ip4'|'ip6'), 'tcp', 'wg']:` → build the inner
|
*before* it are the wg **bearer** (the underlay `(ip, udp-port)`
|
||||||
`TCPAddress`, decode the multibase key to std-base64, return
|
that `wg(8)` itself listens on, per the codec docstring's own
|
||||||
`TunnelledAddress(inner=..., tunnel=WGTunnelSpec(...))`.
|
`/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
|
- keep the existing 2-proto cases byte-identical; add the new
|
||||||
case *after* them.
|
case *after* them.
|
||||||
- generalize the match so the *inner* stack is parsed by the
|
- generalize by **peeling at the tunnel segment**: split
|
||||||
existing logic and `wg` is peeled off first — this is what
|
`proto_names` at `'wg'`, hand the trailing list to the existing
|
||||||
makes `/…/udp/…/quic-v1/wg/u<key>` work later without another
|
inner-stack logic, and recurse for nested tunnels. Write it as
|
||||||
case (plan 02 §3.4). Write it as a small pure function:
|
a small pure fn `_peel_tunnel_segs(proto_names) ->
|
||||||
`_peel_tunnel_segs(proto_names) -> (inner_names,
|
(bearer_names, tunnel_specs, inner_names)`. This is also what
|
||||||
tunnel_specs)`.
|
makes a wg-inside-wg stack fall out for free.
|
||||||
- `mk_maddr()` inverse for `TunnelledAddress`.
|
- `mk_maddr()` inverse for `TunnelledAddress`.
|
||||||
- **blocked on upstream**: `Multiaddr('/…/wg/u…')` only parses
|
- **blocked on upstream**: `Multiaddr('/…/wg/u…')` only parses
|
||||||
once py-multiaddr#108 lands. Until then: pin the branch in the
|
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
|
### 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
|
- `setns(2)` with `CLONE_NEWNET` affects **the calling thread
|
||||||
only**, and sockets already created keep their original netns.
|
only**, and sockets already created keep their original netns.
|
||||||
A trio actor is effectively single-threaded for our purposes,
|
A trio actor is effectively single-threaded for our purposes,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue