Compare commits
2 Commits
ee17ed9f6e
...
d9a6e2e9b4
| Author | SHA1 | Date |
|---|---|---|
|
|
d9a6e2e9b4 | |
|
|
13588d0216 |
|
|
@ -32,9 +32,9 @@ onto `trio` as the library's sans-io layer allows.
|
||||||
latest `0.2.0` predating it. Spec registration is still tracked
|
latest `0.2.0` predating it. Spec registration is still tracked
|
||||||
by multiformats/py-multiaddr#107 and gh #483.
|
by multiformats/py-multiaddr#107 and gh #483.
|
||||||
- so **today's deployable story is declarative**: run `wg-quick`
|
- so **today's deployable story is declarative**: run `wg-quick`
|
||||||
out-of-band, parse the maddr, strip to the inner
|
out-of-band, parse the maddr, strip to the overlay
|
||||||
`(host, port)`, verify the pubkey against the live tunnel,
|
`(host, port)`, verify the pubkey against the live tunnel,
|
||||||
hand the inner addr to `registry_addrs=`/`tpt_bind_addrs=`.
|
hand the overlay addr to `registry_addrs=`/`tpt_bind_addrs=`.
|
||||||
#482 already contains working example code for exactly this.
|
#482 already contains working example code for exactly this.
|
||||||
- `Address.namespace` exists in the Protocol
|
- `Address.namespace` exists in the Protocol
|
||||||
(`_addr.py:94-101`, "the if-available OS-specific network
|
(`_addr.py:94-101`, "the if-available OS-specific network
|
||||||
|
|
@ -45,7 +45,7 @@ onto `trio` as the library's sans-io layer allows.
|
||||||
|
|
||||||
| layer | what | dep | ships |
|
| layer | what | dep | ships |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| **A. declarative** | commit #482's examples; `parse_maddr()` learns `/wg/u<key>` → inner `Address` + verified pubkey | `multiaddr` (already), `wg(8)` CLI | first |
|
| **A. declarative** | commit #482's examples; `parse_maddr()` learns `/wg/u<key>` → overlay `Address` + verified pubkey | `multiaddr` (already), `wg(8)` CLI | first |
|
||||||
| **B. `pyroute2` read/verify** | replace the `subprocess.run(['sudo','wg','show'])` shelling with netlink queries | `pyroute2` extra | second |
|
| **B. `pyroute2` read/verify** | replace the `subprocess.run(['sudo','wg','show'])` shelling with netlink queries | `pyroute2` extra | second |
|
||||||
| **C. `@acm` lifecycle** | create/configure/tear down wg ifaces + netns *from the runtime*, as nested bindspaces; implement `Address.namespace` | `pyroute2` + `CAP_NET_ADMIN` | third |
|
| **C. `@acm` lifecycle** | create/configure/tear down wg ifaces + netns *from the runtime*, as nested bindspaces; implement `Address.namespace` | `pyroute2` + `CAP_NET_ADMIN` | third |
|
||||||
|
|
||||||
|
|
@ -70,16 +70,16 @@ does not create a new address type.** Two candidate encodings;
|
||||||
msgspec.Struct,
|
msgspec.Struct,
|
||||||
frozen=True,
|
frozen=True,
|
||||||
):
|
):
|
||||||
inner: Address # e.g. TCPAddress
|
overlay: Address # e.g. TCPAddress
|
||||||
tunnel: WGTunnelSpec # proto-specific, frozen
|
tunnel: WGTunnelSpec # proto-specific, frozen
|
||||||
```
|
```
|
||||||
with `.proto_key` **delegating to `inner.proto_key`** so every
|
with `.proto_key` **delegating to `overlay.proto_key`** so every
|
||||||
existing table lookup (`_addr_to_transport`,
|
existing table lookup (`_addr_to_transport`,
|
||||||
`enable_transports` guard at `_root.py:391`,
|
`enable_transports` guard at `_root.py:391`,
|
||||||
`transport_from_addr()`) keeps working untouched, and
|
`transport_from_addr()`) keeps working untouched, and
|
||||||
`.unwrap()` delegating to `inner.unwrap()` so **nothing new
|
`.unwrap()` delegating to `overlay.unwrap()` so **nothing new
|
||||||
crosses the wire**. `.namespace` and `.bindspace` come from
|
crosses the wire**. `.namespace` and `.bindspace` come from
|
||||||
the tunnel spec. The wrapper is stripped (`→ .inner`) at the
|
the tunnel spec. The wrapper is stripped (`→ .overlay`) at the
|
||||||
moment of bind/connect.
|
moment of bind/connect.
|
||||||
- ⚠️ `is_wrapped_addr()` (`_addr.py:194`) tests
|
- ⚠️ `is_wrapped_addr()` (`_addr.py:194`) tests
|
||||||
`type(addr) in _address_types.values()` — a `bidict` of
|
`type(addr) in _address_types.values()` — a `bidict` of
|
||||||
|
|
@ -154,25 +154,48 @@ Observed protocol-name lists, for writing the `match`:
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| bearer | kernel, via `wg-quick`/`pyroute2` | no |
|
| bearer | kernel, via `wg-quick`/`pyroute2` | no |
|
||||||
| `/wg/u<key>` | nothing — it's an identity | no, verified out-of-band |
|
| `/wg/u<key>` | nothing — it's an identity | no, verified out-of-band |
|
||||||
| overlay | `tractor`'s `IPCServer` | **yes**, as `.inner` |
|
| overlay | `tractor`'s `IPCServer` | **yes**, as `.overlay` |
|
||||||
|
|
||||||
This owner-split is the real axis of the design, *not* whether
|
This owner-split is the real axis of the design, *not* whether
|
||||||
the maddr stack is "composed" (it is).
|
the maddr stack is "composed" (it is).
|
||||||
|
- ⚠️ **CORRECTION**, an earlier draft of this section specced a
|
||||||
|
hand-rolled `_peel_tunnel_segs(proto_names) -> (bearer_names,
|
||||||
|
tunnel_specs, overlay_names)`. **Do not write it.**
|
||||||
|
`py-multiaddr` already ships the whole tunnel compose/peel API
|
||||||
|
and it was simply missed here — see its README "En/decapsulate"
|
||||||
|
and "Tunneling" sections, and gh #443's 2nd bullet which links
|
||||||
|
them. Verified against the pinned rev:
|
||||||
|
|
||||||
|
| need | API |
|
||||||
|
| --- | --- |
|
||||||
|
| isolate the bearer | `ma.decapsulate_code(P_WG)` |
|
||||||
|
| drop the overlay, keep bearer+key | `ma.decapsulate(overlay_ma)` |
|
||||||
|
| per-seg maddrs | `ma.split()` |
|
||||||
|
| rejoin a seg tail | `Multiaddr.join(*segs)` |
|
||||||
|
| read the key | `ma.value_for_protocol('wg')` |
|
||||||
|
| recompose | `bearer.encapsulate(key).encapsulate(overlay)` |
|
||||||
|
|
||||||
|
`.decapsulate_code()` handles the infix `/wg/` seg cleanly
|
||||||
|
*because* it cuts on proto-code and never tries to match an
|
||||||
|
addr value — the key seg has no addr of its own. This is the
|
||||||
|
same NIH trap gh #429 existed to close, one layer up.
|
||||||
|
|
||||||
|
- ⚠️ `value_for_protocol('ip4')` on a *full* tunnelled maddr
|
||||||
|
silently returns the **first** match, i.e. the bearer's host.
|
||||||
|
Always call it on a peeled sub-maddr, never the whole stack.
|
||||||
|
|
||||||
- `parse_maddr()` gains a case on
|
- `parse_maddr()` gains a case on
|
||||||
`[('ip4'|'ip6'), 'udp', 'wg', ('ip4'|'ip6'), <inner-l4>]` →
|
`[('ip4'|'ip6'), 'udp', 'wg', ('ip4'|'ip6'), <overlay-l4>]` →
|
||||||
build the inner `Address` from the trailing segments, decode
|
peel w/ the API above, decode the multibase key to std-base64,
|
||||||
the multibase key to std-base64, and return
|
and return `TunnelledAddress(overlay=..., tunnel=WGTunnelSpec(
|
||||||
`TunnelledAddress(inner=..., tunnel=WGTunnelSpec(...))` with
|
...))` w/ the bearer recorded in the spec.
|
||||||
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 by **peeling at the tunnel segment**: split
|
- nesting (wg-in-wg) falls out of `.decapsulate_code()` cutting
|
||||||
`proto_names` at `'wg'`, hand the trailing list to the existing
|
at the *last* occurrence — peel repeatedly rather than
|
||||||
inner-stack logic, and recurse for nested tunnels. Write it as
|
recursing through a bespoke splitter.
|
||||||
a small pure fn `_peel_tunnel_segs(proto_names) ->
|
- `mk_maddr()` inverse for `TunnelledAddress` is just
|
||||||
(bearer_names, tunnel_specs, inner_names)`. This is also what
|
`.encapsulate()` composition; don't rebuild `str`s by hand.
|
||||||
makes a wg-inside-wg stack fall out for free.
|
|
||||||
- `mk_maddr()` inverse for `TunnelledAddress`.
|
|
||||||
- **pending an upstream release**: py-multiaddr#108 is merged, so
|
- **pending an upstream release**: py-multiaddr#108 is merged, so
|
||||||
`Multiaddr('/…/wg/u…')` parses — but off a `[tool.uv.sources]`
|
`Multiaddr('/…/wg/u…')` parses — but off a `[tool.uv.sources]`
|
||||||
`rev` pin, since no release carries the codec. Gate the tests
|
`rev` pin, since no release carries the codec. Gate the tests
|
||||||
|
|
@ -217,7 +240,7 @@ side-effect-free; verification is the *caller's* explicit step
|
||||||
run. Keep prose in the docs; keep the examples runnable and
|
run. Keep prose in the docs; keep the examples runnable and
|
||||||
minimal.
|
minimal.
|
||||||
- tests: maddr round-trip, `TunnelledAddress` delegation
|
- tests: maddr round-trip, `TunnelledAddress` delegation
|
||||||
(`proto_key`/`unwrap` identical to inner), `wrap_address()`
|
(`proto_key`/`unwrap` identical to overlay), `wrap_address()`
|
||||||
regression (a tunnelled maddr `str` → `TunnelledAddress`; a
|
regression (a tunnelled maddr `str` → `TunnelledAddress`; a
|
||||||
plain one → unchanged), and **a real end-to-end over a
|
plain one → unchanged), and **a real end-to-end over a
|
||||||
locally-created wg pair** gated on `CAP_NET_ADMIN` (see §5.3).
|
locally-created wg pair** gated on `CAP_NET_ADMIN` (see §5.3).
|
||||||
|
|
@ -305,7 +328,7 @@ async def open_bindspace(
|
||||||
) -> AsyncGenerator[Address, None]:
|
) -> AsyncGenerator[Address, None]:
|
||||||
'''
|
'''
|
||||||
Enter the net-bindspace implied by `addr`'s tunnel stack,
|
Enter the net-bindspace implied by `addr`'s tunnel stack,
|
||||||
yielding the *inner* `Address` ready to bind/connect.
|
yielding the *overlay* `Address` ready to bind/connect.
|
||||||
|
|
||||||
Nests: one `@acm` per tunnel segment, outermost-first, so
|
Nests: one `@acm` per tunnel segment, outermost-first, so
|
||||||
a 2-deep stack is just two nested `async with`s and the
|
a 2-deep stack is just two nested `async with`s and the
|
||||||
|
|
@ -444,7 +467,7 @@ consider doing it *first* for exactly that reason.
|
||||||
| risk | mitigation |
|
| risk | mitigation |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `to_thread` worker runs in the wrong netns | §5.3; pass `netns=` to pyroute2 or pin a worker; test-first |
|
| `to_thread` worker runs in the wrong netns | §5.3; pass `netns=` to pyroute2 or pin a worker; test-first |
|
||||||
| py-multiaddr#108 merged but unreleased | `[tool.uv.sources]` `rev` pin + `_have_wg_maddr_proto()` gate; layer A's inner-addr path works regardless |
|
| py-multiaddr#108 merged but unreleased | `[tool.uv.sources]` `rev` pin + `_have_wg_maddr_proto()` gate; layer A's overlay-addr path works regardless |
|
||||||
| `TunnelledAddress` leaks into `Endpoint` and breaks `inspect.getmodule()` | unwrap at parse/bindspace boundary; assert `not isinstance(ep.addr, TunnelledAddress)` in `Endpoint.__post_init__` |
|
| `TunnelledAddress` leaks into `Endpoint` and breaks `inspect.getmodule()` | unwrap at parse/bindspace boundary; assert `not isinstance(ep.addr, TunnelledAddress)` in `Endpoint.__post_init__` |
|
||||||
| privileged ops in a library | never `sudo`; explicit cap probe + actionable error; pre-provisioned is the default |
|
| privileged ops in a library | never `sudo`; explicit cap probe + actionable error; pre-provisioned is the default |
|
||||||
| pyroute2 0.9 asyncio core drags a loop into the actor | option (1) is a *thread*, not a loop; forbid `trio-asyncio` here (§4.1) |
|
| pyroute2 0.9 asyncio core drags a loop into the actor | option (1) is a *thread*, not a loop; forbid `trio-asyncio` here (§4.1) |
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ Three parts, three different owners:
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `/ip4/../udp/51820` bearer | kernel via `wg-quick`/`pyroute2` | no |
|
| `/ip4/../udp/51820` bearer | kernel via `wg-quick`/`pyroute2` | no |
|
||||||
| `/wg/u<key>` | nothing — it's an identity | no, verified out-of-band |
|
| `/wg/u<key>` | nothing — it's an identity | no, verified out-of-band |
|
||||||
| `/ip4/../tcp/1616` overlay | `tractor`'s `IPCServer` | **yes**, as `.inner` |
|
| `/ip4/../tcp/1616` overlay | `tractor`'s `IPCServer` | **yes**, as `.overlay` |
|
||||||
|
|
||||||
Verified against py-multiaddr
|
Verified against py-multiaddr
|
||||||
[#108](https://github.com/multiformats/py-multiaddr/pull/108):
|
[#108](https://github.com/multiformats/py-multiaddr/pull/108):
|
||||||
|
|
@ -55,12 +55,17 @@ release carries the codec. You also need `multibase`:
|
||||||
uv pip install multibase
|
uv pip install multibase
|
||||||
```
|
```
|
||||||
|
|
||||||
Without the codec `wg_maddr.py` degrades to a plain segment split
|
Without the codec `parse_wg_maddr()` raises immediately with an
|
||||||
— the examples still run, but you lose per-segment validation
|
actionable message — there is deliberately **no** degraded
|
||||||
(incl. the 32-byte key-length check), so a malformed key reaches
|
hand-split fallback. `_have_wg_maddr_proto()` is the predicate.
|
||||||
the returned struct instead of raising. `_have_wg_maddr_proto()`
|
|
||||||
is the gate. It deliberately does **not** hand-roll a `wg` codec
|
Every peel and re-compose here goes through `py-multiaddr`'s own
|
||||||
(gh #429 was about *dropping* our NIH parser).
|
tunnel API (`.decapsulate_code()`, `.split()`, `.join()`,
|
||||||
|
`.encapsulate()`, `.value_for_protocol()`) rather than any
|
||||||
|
bespoke segment slicing — see its README "En/decapsulate" and
|
||||||
|
"Tunneling" sections. gh #429 was about *dropping* our NIH
|
||||||
|
parser, and that applies to peeling a tunnel stack just as much
|
||||||
|
as to decoding one proto.
|
||||||
|
|
||||||
## 0. tunnel setup (out-of-band, both hosts)
|
## 0. tunnel setup (out-of-band, both hosts)
|
||||||
|
|
||||||
|
|
@ -156,7 +161,7 @@ Four corrections, all from
|
||||||
setups; if yours needs root, run the script as root rather
|
setups; if yours needs root, run the script as root rather
|
||||||
than embedding `sudo`.
|
than embedding `sudo`.
|
||||||
4. **no new `Address` proto-type.** The tunnel rides *beside* the
|
4. **no new `Address` proto-type.** The tunnel rides *beside* the
|
||||||
inner addr in a frozen `WGTunnelledAddr`, and only `.inner`
|
overlay addr in a frozen `WGTunnelledAddr`, and only `.overlay`
|
||||||
crosses into `open_nursery()`. #482 §6 floated a `WGAddress`
|
crosses into `open_nursery()`. #482 §6 floated a `WGAddress`
|
||||||
registered in `_address_types` — that table is a `bidict`
|
registered in `_address_types` — that table is a `bidict`
|
||||||
(1:1 proto-key↔type) and `_addr_to_transport` wants a
|
(1:1 proto-key↔type) and `_addr_to_transport` wants a
|
||||||
|
|
@ -166,6 +171,6 @@ Four corrections, all from
|
||||||
|
|
||||||
`WGTunnelledAddr` is deliberately example-local. Promoting it to
|
`WGTunnelledAddr` is deliberately example-local. Promoting it to
|
||||||
`tractor.discovery` as a `TunnelledAddress` whose
|
`tractor.discovery` as a `TunnelledAddress` whose
|
||||||
`.proto_key`/`.unwrap()` delegate to `.inner`, plus
|
`.proto_key`/`.unwrap()` delegate to `.overlay`, plus
|
||||||
`open_bindspace()` `@acm`s that create/tear down the iface +
|
`open_bindspace()` `@acm`s that create/tear down the iface +
|
||||||
netns via `pyroute2`, is layers A→C of the plan doc.
|
netns via `pyroute2`, is layers A→C of the plan doc.
|
||||||
|
|
|
||||||
|
|
@ -41,13 +41,13 @@ async def main():
|
||||||
)
|
)
|
||||||
print(
|
print(
|
||||||
f'wg bearer (kernel-owned): {addr.bearer}\n'
|
f'wg bearer (kernel-owned): {addr.bearer}\n'
|
||||||
f'tractor overlay ep: {addr.inner}\n'
|
f'tractor overlay ep: {addr.overlay}\n'
|
||||||
)
|
)
|
||||||
async with tractor.open_nursery(
|
async with tractor.open_nursery(
|
||||||
# XXX only `.inner` crosses into the runtime; the bearer
|
# XXX only `.overlay` crosses into the runtime; the bearer
|
||||||
# + key are iface-layer concerns `tractor` never binds.
|
# + key are iface-layer concerns `tractor` never binds.
|
||||||
registry_addrs=[addr.inner],
|
registry_addrs=[addr.overlay],
|
||||||
enable_transports=[addr.inner_proto],
|
enable_transports=[addr.overlay_proto],
|
||||||
) as an:
|
) as an:
|
||||||
await an.start_actor(
|
await an.start_actor(
|
||||||
'echo_srv',
|
'echo_srv',
|
||||||
|
|
|
||||||
|
|
@ -33,12 +33,12 @@ async def main():
|
||||||
async with (
|
async with (
|
||||||
tractor.open_root_actor(
|
tractor.open_root_actor(
|
||||||
name='wg_client',
|
name='wg_client',
|
||||||
registry_addrs=[addr.inner],
|
registry_addrs=[addr.overlay],
|
||||||
enable_transports=[addr.inner_proto],
|
enable_transports=[addr.overlay_proto],
|
||||||
),
|
),
|
||||||
tractor.find_actor(
|
tractor.find_actor(
|
||||||
'echo_srv',
|
'echo_srv',
|
||||||
registry_addrs=[addr.inner],
|
registry_addrs=[addr.overlay],
|
||||||
) as portal,
|
) as portal,
|
||||||
):
|
):
|
||||||
res: str = await portal.run(
|
res: str = await portal.run(
|
||||||
|
|
|
||||||
|
|
@ -2,32 +2,49 @@
|
||||||
r'''
|
r'''
|
||||||
Parse `wg`-tunnelled multiaddrs into `tractor`-ready addrs.
|
Parse `wg`-tunnelled multiaddrs into `tractor`-ready addrs.
|
||||||
|
|
||||||
The canonical form (per py-multiaddr #108, verified to parse +
|
The canonical form (per py-multiaddr #108, verified against its
|
||||||
round-trip against its upstream merge) nests the *overlay*
|
upstream merge) nests the *overlay* endpoint **after** the `/wg/`
|
||||||
endpoint **after** the `/wg/` segment:
|
segment:
|
||||||
|
|
||||||
/ip4/10.0.0.1/udp/51820/wg/u<key>/ip4/10.0.11.1/tcp/1616
|
/ip4/10.0.0.1/udp/51820/wg/u<key>/ip4/10.0.11.1/tcp/1616
|
||||||
\_______ wg bearer ______/\_ key _/\____ tractor ep _____/
|
\_______ wg bearer ______/\_ key _/\____ tractor ep _____/
|
||||||
(underlay, wg
|
(underlay, wg
|
||||||
`ListenPort`)
|
`ListenPort`)
|
||||||
|
|
||||||
- the segments *before* `/wg/` are the **bearer**: the underlay
|
Naming follows `py-multiaddr`'s own encapsulation model, where
|
||||||
|
earlier segments *wrap* later ones (`.encapsulate()` appends), so
|
||||||
|
the two roles are:
|
||||||
|
|
||||||
|
- **bearer**: the segs *before* `/wg/`, i.e. the underlay
|
||||||
`(ip, udp-port)` that `wg(8)` itself listens on. Nothing in
|
`(ip, udp-port)` that `wg(8)` itself listens on. Nothing in
|
||||||
`tractor` ever binds this — the kernel/`wg` iface owns it.
|
`tractor` ever binds this — the kernel/`wg` iface owns it.
|
||||||
- `/wg/u<key>` carries the tunnel peer's Curve25519 pubkey as
|
- **overlay**: the segs *after*, i.e. the addr `tractor` actually
|
||||||
multibase base64url (std base64 from `wg(8)` contains `/` and
|
binds/dials. The only part the runtime ever sees.
|
||||||
can't go in a `/`-delimited maddr).
|
|
||||||
- the segments *after* are the **overlay** endpoint, i.e. the
|
We deliberately avoid `inner`/`outer` for these two: in a *call*
|
||||||
addr `tractor` actually binds/dials. This is the only part the
|
stack "inner" reads as higher-up and later-called, whereas here
|
||||||
runtime sees.
|
the encapsulated addr is bound *first* and sits deeper in the
|
||||||
|
maddr — two opposite intuitions on one word.
|
||||||
|
|
||||||
|
`/wg/u<key>` itself carries the tunnel peer's Curve25519 pubkey
|
||||||
|
as multibase base64url (std base64 from `wg(8)` contains `/` and
|
||||||
|
so can't go in a `/`-delimited maddr). It binds nothing at all;
|
||||||
|
it's an identity, verified out-of-band.
|
||||||
|
|
||||||
XXX NOTE, `tractor`'s own `parse_maddr()` can't parse this yet
|
XXX NOTE, `tractor`'s own `parse_maddr()` can't parse this yet
|
||||||
(`ValueError('Unsupported multiaddr protocol combo')`), which is
|
(`ValueError('Unsupported multiaddr protocol combo')`), which is
|
||||||
why this module exists: parse here, hand `.inner` to the runtime.
|
why this module exists: peel here, hand `.overlay` to the
|
||||||
|
runtime.
|
||||||
|
|
||||||
Design rules this module follows (see
|
Design rules this module follows (see
|
||||||
`ai/tpt-backends/03_wg_tunnel_bindspace.md`):
|
`ai/tpt-backends/03_wg_tunnel_bindspace.md`):
|
||||||
|
|
||||||
|
- **let `py-multiaddr` do the parsing**. Every peel/compose goes
|
||||||
|
through `.decapsulate_code()`, `.split()`, `.join()`,
|
||||||
|
`.encapsulate()` and `.value_for_protocol()`. We hand-roll no
|
||||||
|
segment splitting whatsoever — the whole point of gh #429 was
|
||||||
|
dropping the NIH parser, and that applies to *peeling a tunnel
|
||||||
|
stack* every bit as much as to decoding a single proto.
|
||||||
- **parsing is pure**. `parse_wg_maddr()` does no I/O, no
|
- **parsing is pure**. `parse_wg_maddr()` does no I/O, no
|
||||||
`subprocess`, no netlink. A parser that shells out is a nasty
|
`subprocess`, no netlink. A parser that shells out is a nasty
|
||||||
surprise.
|
surprise.
|
||||||
|
|
@ -36,8 +53,8 @@ Design rules this module follows (see
|
||||||
- **no new `Address` proto-type**. `wg` gets no entry in
|
- **no new `Address` proto-type**. `wg` gets no entry in
|
||||||
`tractor.discovery._addr._address_types` (a `bidict`, so 1:1
|
`tractor.discovery._addr._address_types` (a `bidict`, so 1:1
|
||||||
proto-key<->type) bc it has no `MsgTransport` of its own. The
|
proto-key<->type) bc it has no `MsgTransport` of its own. The
|
||||||
tunnel is a *bindspace*, so we carry it beside the inner addr
|
tunnel is a *bindspace*, so we carry it beside the overlay
|
||||||
and strip to `.inner` at bind/dial time.
|
addr and strip to `.overlay` at bind/dial time.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
@ -46,6 +63,11 @@ import subprocess
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
import msgspec
|
import msgspec
|
||||||
|
from multiaddr import Multiaddr
|
||||||
|
from multiaddr.protocols import P_WG
|
||||||
|
|
||||||
|
|
||||||
|
IPProto = Literal['ip4', 'ip6']
|
||||||
|
|
||||||
|
|
||||||
class WGTunnelledAddr(
|
class WGTunnelledAddr(
|
||||||
|
|
@ -66,22 +88,43 @@ class WGTunnelledAddr(
|
||||||
|
|
||||||
# overlay ep: an `UnwrappedAddress` as accepted by
|
# overlay ep: an `UnwrappedAddress` as accepted by
|
||||||
# `tractor.discovery.wrap_address()`
|
# `tractor.discovery.wrap_address()`
|
||||||
inner: tuple[str, int]
|
overlay: tuple[str, int]
|
||||||
inner_proto: Literal['tcp'] = 'tcp'
|
overlay_proto: Literal['tcp'] = 'tcp'
|
||||||
|
|
||||||
|
# kept so `.as_multiaddr()` re-renders the same ip family it
|
||||||
|
# was parsed from, rather than assuming v4
|
||||||
|
bearer_ip: IPProto = 'ip4'
|
||||||
|
overlay_ip: IPProto = 'ip4'
|
||||||
|
|
||||||
|
def as_multiaddr(self) -> Multiaddr:
|
||||||
|
'''
|
||||||
|
Re-compose the canonical `Multiaddr`, bearer outward-in,
|
||||||
|
using `.encapsulate()` exactly as py-multiaddr's own
|
||||||
|
tunneling example does.
|
||||||
|
|
||||||
|
'''
|
||||||
|
b_host, b_port = self.bearer
|
||||||
|
o_host, o_port = self.overlay
|
||||||
|
return (
|
||||||
|
Multiaddr(f'/{self.bearer_ip}/{b_host}/udp/{b_port}')
|
||||||
|
.encapsulate(
|
||||||
|
Multiaddr(f'/wg/{mb_pubkey(self.peer_pubkey)}')
|
||||||
|
)
|
||||||
|
.encapsulate(
|
||||||
|
Multiaddr(
|
||||||
|
f'/{self.overlay_ip}/{o_host}'
|
||||||
|
f'/{self.overlay_proto}/{o_port}'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def maddr(self) -> str:
|
def maddr(self) -> str:
|
||||||
'''
|
'''
|
||||||
Re-render the canonical maddr `str` form.
|
The canonical maddr `str` form.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
b_host, b_port = self.bearer
|
return str(self.as_multiaddr())
|
||||||
i_host, i_port = self.inner
|
|
||||||
return (
|
|
||||||
f'/ip4/{b_host}/udp/{b_port}'
|
|
||||||
f'/wg/{mb_pubkey(self.peer_pubkey)}'
|
|
||||||
f'/ip4/{i_host}/{self.inner_proto}/{i_port}'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def mb_pubkey(wg8_key: str) -> str:
|
def mb_pubkey(wg8_key: str) -> str:
|
||||||
|
|
@ -104,68 +147,6 @@ def wg8_pubkey(mb_key: str) -> str:
|
||||||
return base64.b64encode(raw).decode('ascii')
|
return base64.b64encode(raw).decode('ascii')
|
||||||
|
|
||||||
|
|
||||||
def parse_wg_maddr(
|
|
||||||
maddr: str,
|
|
||||||
) -> WGTunnelledAddr:
|
|
||||||
'''
|
|
||||||
Split a `wg`-tunnelled maddr into its bearer/key/overlay
|
|
||||||
parts. Pure — no I/O.
|
|
||||||
|
|
||||||
Total-or-raises: with a `wg`-aware `py-multiaddr` (#108) an
|
|
||||||
unparseable maddr raises instead of yielding a struct built
|
|
||||||
from garbage segments. See `_segments()` for the degraded
|
|
||||||
pre-#108 path.
|
|
||||||
|
|
||||||
'''
|
|
||||||
segs: list[str] = _segments(maddr)
|
|
||||||
try:
|
|
||||||
wg_at: int = segs.index('wg')
|
|
||||||
except ValueError:
|
|
||||||
raise ValueError(
|
|
||||||
f'Not a `wg`-tunnelled maddr, no `/wg/` segment ??\n'
|
|
||||||
f'maddr: {maddr!r}\n'
|
|
||||||
)
|
|
||||||
|
|
||||||
bearer_segs: list[str] = segs[:wg_at]
|
|
||||||
mb_key: str = segs[wg_at + 1]
|
|
||||||
inner_segs: list[str] = segs[wg_at + 2:]
|
|
||||||
|
|
||||||
match bearer_segs:
|
|
||||||
case ['ip4'|'ip6', str() as b_host, 'udp', str() as b_port]:
|
|
||||||
bearer = (b_host, int(b_port))
|
|
||||||
case _:
|
|
||||||
raise ValueError(
|
|
||||||
f'Bad `wg` bearer, expected `/ip4|ip6/<h>/udp/<p>`\n'
|
|
||||||
f'got: {"/".join(bearer_segs)!r}\n'
|
|
||||||
f'from maddr: {maddr!r}\n'
|
|
||||||
)
|
|
||||||
|
|
||||||
match inner_segs:
|
|
||||||
case ['ip4'|'ip6', str() as i_host, 'tcp', str() as i_port]:
|
|
||||||
inner = (i_host, int(i_port))
|
|
||||||
inner_proto = 'tcp'
|
|
||||||
case []:
|
|
||||||
raise ValueError(
|
|
||||||
f'`wg` maddr declares no overlay endpoint!\n'
|
|
||||||
f'A bare `/…/wg/<key>` names only the tunnel; '
|
|
||||||
f'append the ep `tractor` should bind, e.g.\n'
|
|
||||||
f' {maddr}/ip4/10.0.11.1/tcp/1616\n'
|
|
||||||
)
|
|
||||||
case _:
|
|
||||||
raise ValueError(
|
|
||||||
f'Unsupported `wg` overlay proto combo\n'
|
|
||||||
f'got: {"/".join(inner_segs)!r}\n'
|
|
||||||
f'from maddr: {maddr!r}\n'
|
|
||||||
)
|
|
||||||
|
|
||||||
return WGTunnelledAddr(
|
|
||||||
bearer=bearer,
|
|
||||||
peer_pubkey=wg8_pubkey(mb_key),
|
|
||||||
inner=inner,
|
|
||||||
inner_proto=inner_proto,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
_wg_proto_known: bool|None = None
|
_wg_proto_known: bool|None = None
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -174,8 +155,9 @@ def _have_wg_maddr_proto() -> bool:
|
||||||
True iff the installed `py-multiaddr` knows the `/wg/` proto,
|
True iff the installed `py-multiaddr` knows the `/wg/` proto,
|
||||||
i.e. carries py-multiaddr#108.
|
i.e. carries py-multiaddr#108.
|
||||||
|
|
||||||
Merged upstream 2026-07-28 but in no release as of `0.2.0`,
|
Merged upstream 2026-07-28 (`f86519da`) but in no release as
|
||||||
hence the `[tool.uv.sources]` `rev` pin.
|
of `0.2.0`, hence the `[tool.uv.sources]` `rev` pin in
|
||||||
|
`pyproject.toml`.
|
||||||
|
|
||||||
Pure predicate; result cached since it can't change without a
|
Pure predicate; result cached since it can't change without a
|
||||||
reinstall.
|
reinstall.
|
||||||
|
|
@ -194,26 +176,95 @@ def _have_wg_maddr_proto() -> bool:
|
||||||
return _wg_proto_known
|
return _wg_proto_known
|
||||||
|
|
||||||
|
|
||||||
def _segments(maddr: str) -> list[str]:
|
def parse_wg_maddr(
|
||||||
|
maddr: str|Multiaddr,
|
||||||
|
) -> WGTunnelledAddr:
|
||||||
'''
|
'''
|
||||||
Deliver a maddr's `/`-split segments, validating via the real
|
Peel a `wg`-tunnelled maddr into its bearer/key/overlay
|
||||||
parser whenever it knows `wg`.
|
parts. Pure — no I/O.
|
||||||
|
|
||||||
|
Every cut is made by `py-multiaddr`, so a malformed maddr
|
||||||
|
(incl. a `wg` key that isn't exactly 32B) raises out of
|
||||||
|
`Multiaddr()` rather than yielding a struct quietly built
|
||||||
|
from garbage segs.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
if _have_wg_maddr_proto():
|
if not _have_wg_maddr_proto():
|
||||||
from multiaddr import Multiaddr
|
raise RuntimeError(
|
||||||
# the real thing: validates every proto + value, incl.
|
f'Installed `py-multiaddr` has no `/wg/` proto!\n'
|
||||||
# that the `wg` key decodes to exactly 32 bytes. Let it
|
f'Needs py-multiaddr#108, merged upstream but not\n'
|
||||||
# raise — a maddr that doesn't parse must NOT reach
|
f'yet released; a `uv sync` picks up the pinned rev.\n'
|
||||||
# `wg8_pubkey()`, which would happily emit a corrupt key.
|
f'maddr: {maddr!r}\n'
|
||||||
Multiaddr(maddr)
|
)
|
||||||
|
|
||||||
# XXX, degraded path for a pre-#108 `py-multiaddr` ONLY: no
|
ma: Multiaddr = (
|
||||||
# per-segment validation, so a malformed key survives to the
|
maddr
|
||||||
# returned struct. We deliberately DON'T hand-roll a `wg`
|
if isinstance(maddr, Multiaddr)
|
||||||
# codec (the whole point of gh #429 was dropping the NIH
|
else Multiaddr(maddr)
|
||||||
# parser) — install the pinned rev to get validation back.
|
)
|
||||||
return [s for s in maddr.split('/') if s]
|
segs: list[Multiaddr] = ma.split()
|
||||||
|
names: list[str] = [
|
||||||
|
proto.name
|
||||||
|
for seg in segs
|
||||||
|
for proto in seg.protocols()
|
||||||
|
]
|
||||||
|
if 'wg' not in names:
|
||||||
|
raise ValueError(
|
||||||
|
f'Not a `wg`-tunnelled maddr, no `/wg/` segment ??\n'
|
||||||
|
f'maddr: {ma}\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
# NOTE, `.decapsulate_code()` cuts at the LAST occurrence of
|
||||||
|
# the proto and keeps the *prefix*, which is exactly the
|
||||||
|
# bearer. It handles `/wg/` cleanly precisely bc it cuts on
|
||||||
|
# proto-code and never tries to match an addr value — the
|
||||||
|
# key seg has no addr of its own.
|
||||||
|
bearer_ma: Multiaddr = ma.decapsulate_code(P_WG)
|
||||||
|
overlay_ma: Multiaddr = Multiaddr.join(
|
||||||
|
*segs[names.index('wg') + 1:]
|
||||||
|
)
|
||||||
|
|
||||||
|
match [proto.name for proto in bearer_ma.protocols()]:
|
||||||
|
case [('ip4' | 'ip6') as b_ip, 'udp']:
|
||||||
|
bearer = (
|
||||||
|
bearer_ma.value_for_protocol(b_ip),
|
||||||
|
int(bearer_ma.value_for_protocol('udp')),
|
||||||
|
)
|
||||||
|
case _:
|
||||||
|
raise ValueError(
|
||||||
|
f'Bad `wg` bearer, expected `/ip4|ip6/<h>/udp/<p>`\n'
|
||||||
|
f'got: {bearer_ma}\n'
|
||||||
|
f'from maddr: {ma}\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
match [proto.name for proto in overlay_ma.protocols()]:
|
||||||
|
case [('ip4' | 'ip6') as o_ip, ('tcp') as l4]:
|
||||||
|
overlay = (
|
||||||
|
overlay_ma.value_for_protocol(o_ip),
|
||||||
|
int(overlay_ma.value_for_protocol(l4)),
|
||||||
|
)
|
||||||
|
case []:
|
||||||
|
raise ValueError(
|
||||||
|
f'`wg` maddr declares no overlay endpoint!\n'
|
||||||
|
f'A bare `/…/wg/<key>` names only the tunnel; '
|
||||||
|
f'append the ep `tractor` should bind, e.g.\n'
|
||||||
|
f' {ma}/ip4/10.0.11.1/tcp/1616\n'
|
||||||
|
)
|
||||||
|
case _:
|
||||||
|
raise ValueError(
|
||||||
|
f'Unsupported `wg` overlay proto combo\n'
|
||||||
|
f'got: {overlay_ma}\n'
|
||||||
|
f'from maddr: {ma}\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
return WGTunnelledAddr(
|
||||||
|
bearer=bearer,
|
||||||
|
peer_pubkey=wg8_pubkey(ma.value_for_protocol('wg')),
|
||||||
|
overlay=overlay,
|
||||||
|
overlay_proto=l4,
|
||||||
|
bearer_ip=b_ip,
|
||||||
|
overlay_ip=o_ip,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def verify_wg_peer(
|
def verify_wg_peer(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue