`_segments()` called `Multiaddr(maddr)` purely to validate, then
swallowed every failure under `except Exception: pass`. That was
harmless pre-#108 — w/o a `wg` codec there was nothing to
validate — but now that the codec is pinned in, the swallow is
load-bearing and disabled: a malformed key sails past validation
into `wg8_pubkey()`, which happily emits a corrupt b64 str, and
the returned struct then fails its own `.maddr` round-trip. No
raise, just quietly wrong output.
Deats,
- add `_have_wg_maddr_proto()`, the gate plan-03 already
referenced but which never actually existed. Impl'd as
`protocols.protocol_with_name('wg')` under
`except ProtocolNotFoundError` and cached in a mod global,
same shape as the TIPC plan's `is_tipc_available()`.
- only validate when that gate is `True`, and let
`StringParseError` propagate — a maddr which doesn't parse
must NOT reach `wg8_pubkey()`.
- keep the degraded split for a pre-#108 install, now w/ an
explicit `XXX` naming the validation you give up.
So parsing stays pure but becomes total-or-raises. Our own
`ValueError`s (missing `/wg/` seg, bare tunnel w/o an overlay
ep) are unaffected, as is the `wg(8)` b64 round-trip.
(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
|
||
|---|---|---|
| .. | ||
| README.md | ||
| host_a_srv.py | ||
| host_b_client.py | ||
| wg_maddr.py | ||
README.md
tractor over a WireGuard tunnel, declared as one maddr
A two-host LAN setup: a tractor actor tree on host A, dialed from host B, with the endpoint declared as a single wg multiaddr.
Supersedes the example set in gh #482 — see what changed.
Why
examples/multihost/?tests/test_docs_examples.pywalksexamples/recursively and runs everything it collects as a subproc, assertingrc == 0. These need a real second host and a livewgtunnel, so they can’t satisfy that;'multihost' not in p[0]is already in the test’s exclusion list, which is what keeps them out of CI.
the maddr form
/ip4/192.168.1.50/udp/51820/wg/u<A_pub>/ip4/10.0.11.1/tcp/1616
\____ wg bearer ___________/\__ key __/\____ tractor ep _____/
underlay, wg `ListenPort` overlay, on the wg iface
(kernel/`wg(8)` owns it) (the ONLY part tractor binds)
Three parts, three different owners:
| part | who binds it | in the runtime? |
|---|---|---|
/ip4/../udp/51820 bearer |
kernel via wg-quick/pyroute2 |
no |
/wg/u<key> |
nothing — it’s an identity | no, verified out-of-band |
/ip4/../tcp/1616 overlay |
tractor’s IPCServer |
yes, as .inner |
Verified against py-multiaddr #108: this composed form parses and round-trips (['ip4','udp','wg','ip4','tcp']).
requirements
The wg proto isn’t in released py-multiaddr yet (0.2.0 has no wg codec), so until #108 lands:
uv pip install 'git+https://github.com/baudco/py-multiaddr.git@wg_support' multibasewg_maddr.py degrades to a plain segment split when the codec is absent, so the examples still run — but you lose per-segment validation. It deliberately does not hand-roll a wg codec (gh #429 was about dropping our NIH parser).
0. tunnel setup (out-of-band, both hosts)
Host A is the service host (underlay e.g. 192.168.1.50), host B your workstation. Overlay net 10.0.11.0/24.
umask 077
wg genkey | tee wg_priv.key | wg pubkey > wg_pub.key/etc/wireguard/wg0.conf on host A:
[Interface]
PrivateKey = <A_priv>
Address = 10.0.11.1/24
ListenPort = 51820[Peer]
PublicKey = <B_pub>
AllowedIPs = 10.0.11.2/32on host B:
[Interface]
PrivateKey = <B_priv>
Address = 10.0.11.2/24[Peer]
PublicKey = <A_pub>
Endpoint = 192.168.1.50:51820
AllowedIPs = 10.0.11.1/32
PersistentKeepalive = 25Note how ListenPort and Endpoint are exactly the maddr’s bearer segment, and [Interface] Address is its overlay host.
sudo wg-quick up wg0 # both hosts
ping -c1 10.0.11.1 # from B1. get your pubkey into the maddr
python -c "
import base64, multibase
key = open('wg_pub.key').read().strip()
print(multibase.encode('base64url', base64.b64decode(key)).decode())
"Paste the u... output into WG_MADDR in both scripts (they use the same string — A’s bearer, A’s key, A’s overlay ep).
2. run
# host A
python host_a_srv.py
# host B
python host_b_client.pyhost_a_srv.py must be importable on host B too, since portal.run() refs the fn by module path — standard tractor RPC semantics.
what changed vs #482
Four corrections, all from ai/tpt-backends/03_wg_tunnel_bindspace.md:
- the maddr semantics were inverted. #482 used
/ip4/10.0.11.1/tcp/1616/wg/u<key>— that parses, but it puts the overlay addr where the bearer belongs andtcpwhere wg’sudpListenPortgoes, and it declares no overlay ep at all.parse_wg_maddr()now rejects it with an actionable error. - parsing is pure. #482’s helper had the key-check adjacent to the parse;
verify_wg_peer()is now a separate, explicitly composed step that the caller invokes. A parser that shells out is a nasty surprise. - no
sudo. #482 ransudo wg show; a library/example must never escalate.wg showworks unprivileged for read on most setups; if yours needs root, run the script as root rather than embeddingsudo. - no new
Addressproto-type. The tunnel rides beside the inner addr in a frozenWGTunnelledAddr, and only.innercrosses intoopen_nursery(). #482 §6 floated aWGAddressregistered in_address_types— that table is abidict(1:1 proto-key↔︎type) and_addr_to_transportwants aMsgTransportper addr-type, whichwgdoesn’t have.
next
WGTunnelledAddr is deliberately example-local. Promoting it to tractor.discovery as a TunnelledAddress whose .proto_key/.unwrap() delegate to .inner, plus open_bindspace() @acms that create/tear down the iface + netns via pyroute2, is layers A→C of the plan doc.