Add a `wg`-tunnelled 2-host example set
Re-renders gh #482's examples w/ the corrected (infix) maddr
grammar, as the "layer A" slice of the wg plan: declarative
maddrs only, tunnel pre-provisioned out-of-band, zero runtime
changes.
- `wg_maddr.py`: a `frozen=True` `msgspec.Struct` addr carrying
`bearer`/`peer_pubkey`/`inner` (+ `inner_proto`), a `.maddr`
property that re-renders the canonical form, and pure
`mb_pubkey()`/`wg8_pubkey()`/`parse_wg_maddr()`. The parser
rejects #482's inverted suffix form w/ an actionable error and
stays **side-effect free** — `verify_wg_peer()` is a separate,
explicitly impure step the caller composes, never something a
parse path shells out to.
- `host_a_srv.py`/`host_b_client.py`: the two-host runs, passing
only `addr.inner` into `open_nursery()`/`open_root_actor()`,
which is the whole point — the bearer + key layers are already
established before any bind happens.
- `README.md`: the grammar + the 3-owners table, the `#108`
branch install line, tunnel setup, and a "what changed vs
#482" section enumerating the corrections.
Runnable-shaped but **not yet run against a live tunnel**; that's
next, and the reason these sit on the planning branch rather than
in `examples/` proper. `_segments()` marks its stopgap for when
the `wg` codec isn't installed.
(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
2026-08-12 23:37:29 +00:00
|
|
|
# tractor: distributed structured concurrency.
|
|
|
|
|
'''
|
|
|
|
|
Host B: workstation dialing host A's actor tree through the
|
|
|
|
|
`wg` tunnel.
|
|
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import tractor
|
|
|
|
|
import trio
|
|
|
|
|
|
|
|
|
|
from host_a_srv import echo # noqa: F401 (RPC refs it by mod path)
|
|
|
|
|
from wg_maddr import (
|
|
|
|
|
parse_wg_maddr,
|
|
|
|
|
verify_wg_peer,
|
|
|
|
|
WGTunnelledAddr,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# same maddr as host A: A's bearer, A's key, A's overlay ep
|
|
|
|
|
WG_MADDR: str = (
|
|
|
|
|
'/ip4/192.168.1.50/udp/51820'
|
|
|
|
|
'/wg/u<A_pub_b64url>'
|
|
|
|
|
'/ip4/10.0.11.1/tcp/1616'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def main():
|
|
|
|
|
addr: WGTunnelledAddr = parse_wg_maddr(WG_MADDR)
|
|
|
|
|
assert verify_wg_peer(addr), (
|
|
|
|
|
f'wg pubkey from maddr not a peer on wg0 !\n'
|
|
|
|
|
f'maddr: {WG_MADDR}\n'
|
|
|
|
|
)
|
|
|
|
|
async with (
|
|
|
|
|
tractor.open_root_actor(
|
|
|
|
|
name='wg_client',
|
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
|
|
|
registry_addrs=[addr.overlay],
|
|
|
|
|
enable_transports=[addr.overlay_proto],
|
Add a `wg`-tunnelled 2-host example set
Re-renders gh #482's examples w/ the corrected (infix) maddr
grammar, as the "layer A" slice of the wg plan: declarative
maddrs only, tunnel pre-provisioned out-of-band, zero runtime
changes.
- `wg_maddr.py`: a `frozen=True` `msgspec.Struct` addr carrying
`bearer`/`peer_pubkey`/`inner` (+ `inner_proto`), a `.maddr`
property that re-renders the canonical form, and pure
`mb_pubkey()`/`wg8_pubkey()`/`parse_wg_maddr()`. The parser
rejects #482's inverted suffix form w/ an actionable error and
stays **side-effect free** — `verify_wg_peer()` is a separate,
explicitly impure step the caller composes, never something a
parse path shells out to.
- `host_a_srv.py`/`host_b_client.py`: the two-host runs, passing
only `addr.inner` into `open_nursery()`/`open_root_actor()`,
which is the whole point — the bearer + key layers are already
established before any bind happens.
- `README.md`: the grammar + the 3-owners table, the `#108`
branch install line, tunnel setup, and a "what changed vs
#482" section enumerating the corrections.
Runnable-shaped but **not yet run against a live tunnel**; that's
next, and the reason these sit on the planning branch rather than
in `examples/` proper. `_segments()` marks its stopgap for when
the `wg` codec isn't installed.
(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
2026-08-12 23:37:29 +00:00
|
|
|
),
|
|
|
|
|
tractor.find_actor(
|
|
|
|
|
'echo_srv',
|
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
|
|
|
registry_addrs=[addr.overlay],
|
Add a `wg`-tunnelled 2-host example set
Re-renders gh #482's examples w/ the corrected (infix) maddr
grammar, as the "layer A" slice of the wg plan: declarative
maddrs only, tunnel pre-provisioned out-of-band, zero runtime
changes.
- `wg_maddr.py`: a `frozen=True` `msgspec.Struct` addr carrying
`bearer`/`peer_pubkey`/`inner` (+ `inner_proto`), a `.maddr`
property that re-renders the canonical form, and pure
`mb_pubkey()`/`wg8_pubkey()`/`parse_wg_maddr()`. The parser
rejects #482's inverted suffix form w/ an actionable error and
stays **side-effect free** — `verify_wg_peer()` is a separate,
explicitly impure step the caller composes, never something a
parse path shells out to.
- `host_a_srv.py`/`host_b_client.py`: the two-host runs, passing
only `addr.inner` into `open_nursery()`/`open_root_actor()`,
which is the whole point — the bearer + key layers are already
established before any bind happens.
- `README.md`: the grammar + the 3-owners table, the `#108`
branch install line, tunnel setup, and a "what changed vs
#482" section enumerating the corrections.
Runnable-shaped but **not yet run against a live tunnel**; that's
next, and the reason these sit on the planning branch rather than
in `examples/` proper. `_segments()` marks its stopgap for when
the `wg` codec isn't installed.
(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
2026-08-12 23:37:29 +00:00
|
|
|
) as portal,
|
|
|
|
|
):
|
|
|
|
|
res: str = await portal.run(
|
|
|
|
|
echo,
|
|
|
|
|
msg='hello over wg!',
|
|
|
|
|
)
|
|
|
|
|
print(res)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
|
trio.run(main)
|