tractor + wg multiaddr: LAN deployment examples
Draft examples for a small today-deployable setup: a tractor
actor tree on another LAN host reachable over a WireGuard tunnel,
with endpoints declared as wg multiaddrs (py-multiaddr PR
#108, key form u<base64url> per commit 8be3a8b).
Facts these examples rely on (verified against tractor@main):
tractor accepts maddr strings anywhere an addr is taken
(wrap_address() dispatches on a leading / →
tractor/discovery/_addr.py:262), but parse_maddr()
(tractor/discovery/_multiaddr.py) only understands
/ip4|ip6/<h>/tcp/<p> and /unix/<path> combos — a
.../wg/u<key> maddr raises
ValueError('Unsupported multiaddr protocol combo') today.
- so: WG runs at the iface layer (transparent to sockets) and
the wg maddr segment is used declaratively — parsed via
py-multiaddr then stripped to the inner (host, port) for
tractor, with the pubkey checked against the live tunnel.
- relevant kwargs:
open_root_actor(registry_addrs=..., tpt_bind_addrs=..., enable_transports=['tcp']) (all forwarded
by open_nursery()); client side tractor.find_actor(name, registry_addrs=[...]).
- roadmap for first-class support:
tractor/ai/prompt-io/prompts/multiaddr_declare_eps.md
(tunnelled-maddr nested-bindspace @acms + pyroute2 netns).
0. WireGuard tunnel setup (both hosts)
Host A = service host on your LAN (underlay e.g. 192.168.1.50),
host B = your workstation. Overlay net 10.0.11.0/24.
# on each host
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/32
on 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 = 25
Bring up + smoke test:
sudo wg-quick up wg0 # both hosts
ping -c1 10.0.11.1 # from B
1. Declaring the endpoint as a wg maddr
The canonical endpoint string for host A's registrar, carrying
the tunnel pubkey for verification:
/ip4/10.0.11.1/tcp/1616/wg/u<A_pub-as-base64url>
Convert a wg(8) pubkey (std base64) to the multibase form:
python -c "
import base64, multibase
key: str = open('wg_pub.key').read().strip()
print(
multibase.encode(
'base64url',
base64.b64decode(key),
).decode('ascii')
)
"
2. Shared helper: wg maddr -> tractor addr + key check
wg_maddr.py (drop next to both scripts below):
'''
Parse `/ip4/<h>/tcp/<p>/wg/u<key>` maddrs into `tractor`
addr tuples, verifying the embedded WireGuard pubkey
against the live `wg0` tunnel state.
'''
import base64
import subprocess
import multibase
from multiaddr import Multiaddr
def parse_wg_maddr(
maddr: str,
) -> tuple[tuple[str, int], str]:
'''
Split a wg-suffixed maddr into
`((host, port), wg8_pubkey)` where the key is
re-encoded to the std-base64 `wg(8)` form.
'''
ma = Multiaddr(maddr)
host: str = ma.value_for_protocol('ip4')
port = int(ma.value_for_protocol('tcp'))
key_mb: str = ma.value_for_protocol('wg')
raw: bytes = multibase.decode(key_mb)
wg8_key: str = base64.b64encode(raw).decode('ascii')
return (host, port), wg8_key
def verify_wg_peer(
wg8_key: str,
iface: str = 'wg0',
) -> bool:
'''
True iff `wg8_key` is a configured peer (or our own
pubkey) on `iface`.
'''
peers: str = subprocess.run(
['sudo', 'wg', 'show', iface, 'peers'],
capture_output=True,
text=True,
check=True,
).stdout
own: str = subprocess.run(
['sudo', 'wg', 'show', iface, 'public-key'],
capture_output=True,
text=True,
check=True,
).stdout
return (
wg8_key in peers.split()
or
wg8_key == own.strip()
)
3. Host A: registrar root + service daemon on the wg iface
host_a_srv.py:
import tractor
import trio
from wg_maddr import parse_wg_maddr, verify_wg_peer
WG_MADDR: str = (
'/ip4/10.0.11.1/tcp/1616/wg/u<A_pub_b64url>'
)
async def echo(msg: str) -> str:
actor = tractor.current_actor()
return f'{actor.uid!r} echoes: {msg}'
async def main():
reg_addr, wg8_key = parse_wg_maddr(WG_MADDR)
assert verify_wg_peer(wg8_key), (
f'wg pubkey from maddr not active on wg0!\n'
f'maddr: {WG_MADDR}\n'
)
async with tractor.open_nursery(
# registrar binds the wg-overlay IP; subactors
# get random ports in the same bindspace so
# they stay wg-reachable too.
registry_addrs=[reg_addr],
enable_transports=['tcp'],
) as an:
await an.start_actor(
'echo_srv',
enable_modules=['host_a_srv'],
)
print(f'echo_srv up on maddr:\n {WG_MADDR}\n')
await trio.sleep_forever()
if __name__ == '__main__':
trio.run(main)
Run on host A (from a venv with tractor + this py-multiaddr
branch installed):
4. Host B: client dialing through the tunnel
host_b_client.py:
import tractor
import trio
from host_a_srv import echo
from wg_maddr import parse_wg_maddr, verify_wg_peer
# same maddr string as host A: the *service* host's
# overlay addr + *its* tunnel pubkey.
WG_MADDR: str = (
'/ip4/10.0.11.1/tcp/1616/wg/u<A_pub_b64url>'
)
async def main():
reg_addr, wg8_key = parse_wg_maddr(WG_MADDR)
assert verify_wg_peer(wg8_key), (
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',
registry_addrs=[reg_addr],
enable_transports=['tcp'],
),
tractor.find_actor(
'echo_srv',
registry_addrs=[reg_addr],
) as portal,
):
res: str = await portal.run(
echo,
msg='hello over wg!',
)
print(res)
if __name__ == '__main__':
trio.run(main)
Note: host_a_srv.py must be importable on host B too (same
file on both hosts) since portal.run() refs the fn by module
path — standard tractor RPC semantics.
5. Variant: pure-maddr config (no wg segment)
Since tractor already parses plain tcp maddr strings, the
minimal no-helper variant just drops the /wg/... suffix and
trusts the tunnel implicitly:
async with tractor.open_nursery(
registry_addrs=['/ip4/10.0.11.1/tcp/1616'],
enable_transports=['tcp'],
) as an:
...
Useful as a fallback if the helper's key-check gets in the way
during first bring-up.
6. Follow-up: first-class wg proto in tractor
For the future (per multiaddr_declare_eps.md): teach
tractor/discovery/_multiaddr.py::parse_maddr() a
[('ip4'|'ip6'), 'tcp', 'wg'] case that returns a
TCPAddress annotated with the peer pubkey (or a new
WGAddress type registered in _address_types /
_addr_to_transport), doing the sec. 2 helper's
verification inside the runtime's bind/connect paths, and
eventually pyroute2-managed netns + tunnel lifecycle as
nested bindspace @acms.
tractor+wgmultiaddr: LAN deployment examplesDraft examples for a small today-deployable setup: a
tractoractor tree on another LAN host reachable over a WireGuard tunnel,
with endpoints declared as
wgmultiaddrs (py-multiaddr PR#108, key form
u<base64url>per commit8be3a8b).Facts these examples rely on (verified against
tractor@main):tractoraccepts maddr strings anywhere an addr is taken(
wrap_address()dispatches on a leading/→tractor/discovery/_addr.py:262), butparse_maddr()(
tractor/discovery/_multiaddr.py) only understands/ip4|ip6/<h>/tcp/<p>and/unix/<path>combos — a.../wg/u<key>maddr raisesValueError('Unsupported multiaddr protocol combo')today.the
wgmaddr segment is used declaratively — parsed viapy-multiaddrthen stripped to the inner(host, port)fortractor, with the pubkey checked against the live tunnel.open_root_actor(registry_addrs=..., tpt_bind_addrs=..., enable_transports=['tcp'])(all forwardedby
open_nursery()); client sidetractor.find_actor(name, registry_addrs=[...]).tractor/ai/prompt-io/prompts/multiaddr_declare_eps.md(tunnelled-maddr nested-bindspace
@acms +pyroute2netns).0. WireGuard tunnel setup (both hosts)
Host A = service host on your LAN (underlay e.g.
192.168.1.50),host B = your workstation. Overlay net
10.0.11.0/24./etc/wireguard/wg0.confon host A:on host B:
Bring up + smoke test:
1. Declaring the endpoint as a
wgmaddrThe canonical endpoint string for host A's registrar, carrying
the tunnel pubkey for verification:
Convert a
wg(8)pubkey (std base64) to the multibase form:2. Shared helper:
wgmaddr -> tractor addr + key checkwg_maddr.py(drop next to both scripts below):3. Host A: registrar root + service daemon on the wg iface
host_a_srv.py:Run on host A (from a venv with
tractor+ thispy-multiaddrbranch installed):
4. Host B: client dialing through the tunnel
host_b_client.py:Note:
host_a_srv.pymust be importable on host B too (samefile on both hosts) since
portal.run()refs the fn by modulepath — standard
tractorRPC semantics.5. Variant: pure-maddr config (no wg segment)
Since
tractoralready parses plain tcp maddr strings, theminimal no-helper variant just drops the
/wg/...suffix andtrusts the tunnel implicitly:
Useful as a fallback if the helper's key-check gets in the way
during first bring-up.
6. Follow-up: first-class
wgproto in tractorFor the future (per
multiaddr_declare_eps.md): teachtractor/discovery/_multiaddr.py::parse_maddr()a[('ip4'|'ip6'), 'tcp', 'wg']case that returns aTCPAddressannotated with the peer pubkey (or a newWGAddresstype registered in_address_types/_addr_to_transport), doing the sec. 2 helper'sverification inside the runtime's bind/connect paths, and
eventually
pyroute2-managed netns + tunnel lifecycle asnested bindspace
@acms.