tractor/examples/multihost/wg_lan/wg_maddr.py

64 lines
1.7 KiB
Python
Raw Permalink Normal View History

# tractor: distributed structured concurrency.
r'''
Verify `wg` peers declared by tractor's multiaddr parser.
`tractor.discovery.parse_wg_maddr()` owns pure parsing and delegates
all tunnel peeling to `py-multiaddr`. This example keeps only the
explicit impure probe used by the two-host demo; parsing never shells
out or verifies local interface state implicitly.
The canonical maddr form is:
/ip4/10.0.0.1/udp/51820/wg/u<key>/ip4/10.0.11.1/tcp/1616
\_______ wg bearer ______/\_ key _/\____ tractor ep _____/
Peel `wg` maddrs w/ `py-multiaddr`'s own tunnel API `py-multiaddr` already ships the entire tunnel compose/peel surface and this module was reimplementing it — a raw `maddr.split('/')` plus index arithmetic, sitting directly under a comment congratulating itself for not hand-rolling a parser. Same NIH trap gh #429 existed to close, just one layer up. The API was linked from gh #443's own 2nd bullet the whole time. So every cut now goes through the real thing, | need | API | | --- | --- | | isolate the bearer | `.decapsulate_code(P_WG)` | | per-seg maddrs | `.split()` | | rejoin a seg tail | `Multiaddr.join()` | | read the key | `.value_for_protocol('wg')` | | recompose | `.encapsulate()` | `.decapsulate_code()` turns out to handle 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, which was the exact thing I'd assumed would need bespoke handling. Deats, - rename the role fields `inner`/`inner_proto` -> `overlay`/`overlay_proto`, matching `py-multiaddr`'s encapsulation model (earlier segs wrap later ones) and #443's owner table. `inner` collided head-on w/ call-stack `inner`, where it reads as higher-up + later-called, while here the encapsulated addr is bound *first* and sits deeper. - drop `_segments()` and its degraded hand-split path entirely. W/o the codec there's now one actionable `RuntimeError` instead of a silent downgrade, superseding the swallow fix in 7d6e7955. - add `.as_multiaddr()` so callers can stay in `Multiaddr` land; `.maddr` is now just `str()` of it. - accept `str|Multiaddr` on the way in. - carry `bearer_ip`/`overlay_ip` so a v6 stack re-renders as v6 — the old `.maddr` hardcoded `/ip4/` and would silently mangle it. - both host scripts follow the rename to `.overlay`. ⚠️ `value_for_protocol('ip4')` on a *full* tunnelled maddr silently returns the **first** match, i.e. the bearer's host, so it's only ever called here on an already-peeled sub-maddr. (this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
2026-08-17 21:25:07 +00:00
The kernel owns the bearer socket. A future tractor bindspace may
provision it through netlink, but only the overlay is an application
`MsgTransport` endpoint.
'''
from __future__ import annotations
import subprocess
from tractor.discovery import (
TunnelledAddress,
WGTunnelSpec,
)
def verify_wg_peer(
addr: TunnelledAddress,
iface: str|None = None,
) -> bool:
'''
Check the outer tunnel's key against one local `wg` iface.
IMPURE + explicit by design: neither `parse_wg_maddr()` nor
`tractor.discovery.parse_maddr()` calls this probe.
?TODO, per plan-03 layer B, swap this body for `pyroute2`
while retaining the explicit verification boundary.
'''
spec = addr.tunnel
if not isinstance(spec, WGTunnelSpec):
raise TypeError(
f'Unsupported tunnel spec: {type(spec)!r}'
)
iface = iface or spec.iface
def _wg(*args: str) -> str:
return subprocess.run(
['wg', 'show', iface, *args],
capture_output=True,
text=True,
check=True,
).stdout
return (
spec.peer_pubkey in _wg('peers').split()
or
spec.peer_pubkey == _wg('public-key').strip()
)