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`))ng_tpts_planning
parent
41d08d04a6
commit
bf974c9870
|
|
@ -0,0 +1,153 @@
|
||||||
|
# `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](https://github.com/goodboy/tractor/issues/482) — see
|
||||||
|
[what changed](#what-changed-vs-482).
|
||||||
|
|
||||||
|
## 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](https://github.com/multiformats/py-multiaddr/pull/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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv pip install 'git+https://github.com/baudco/py-multiaddr.git@wg_support' multibase
|
||||||
|
```
|
||||||
|
|
||||||
|
`wg_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`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
umask 077
|
||||||
|
wg genkey | tee wg_priv.key | wg pubkey > wg_pub.key
|
||||||
|
```
|
||||||
|
|
||||||
|
`/etc/wireguard/wg0.conf` on **host A**:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Interface]
|
||||||
|
PrivateKey = <A_priv>
|
||||||
|
Address = 10.0.11.1/24
|
||||||
|
ListenPort = 51820
|
||||||
|
```
|
||||||
|
```ini
|
||||||
|
[Peer]
|
||||||
|
PublicKey = <B_pub>
|
||||||
|
AllowedIPs = 10.0.11.2/32
|
||||||
|
```
|
||||||
|
|
||||||
|
on **host B**:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Interface]
|
||||||
|
PrivateKey = <B_priv>
|
||||||
|
Address = 10.0.11.2/24
|
||||||
|
```
|
||||||
|
```ini
|
||||||
|
[Peer]
|
||||||
|
PublicKey = <A_pub>
|
||||||
|
Endpoint = 192.168.1.50:51820
|
||||||
|
AllowedIPs = 10.0.11.1/32
|
||||||
|
PersistentKeepalive = 25
|
||||||
|
```
|
||||||
|
|
||||||
|
Note how `ListenPort` and `Endpoint` are exactly the maddr's
|
||||||
|
bearer segment, and `[Interface] Address` is its overlay host.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo wg-quick up wg0 # both hosts
|
||||||
|
ping -c1 10.0.11.1 # from B
|
||||||
|
```
|
||||||
|
|
||||||
|
## 1. get your pubkey into the maddr
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# host A
|
||||||
|
python host_a_srv.py
|
||||||
|
|
||||||
|
# host B
|
||||||
|
python host_b_client.py
|
||||||
|
```
|
||||||
|
|
||||||
|
`host_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`:
|
||||||
|
|
||||||
|
1. **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 and `tcp` where
|
||||||
|
wg's `udp` `ListenPort` goes, and it declares no overlay ep at
|
||||||
|
all. `parse_wg_maddr()` now rejects it with an actionable
|
||||||
|
error.
|
||||||
|
2. **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.
|
||||||
|
3. **no `sudo`.** #482 ran `sudo wg show`; a library/example must
|
||||||
|
never escalate. `wg show` works unprivileged for read on most
|
||||||
|
setups; if yours needs root, run the script as root rather
|
||||||
|
than embedding `sudo`.
|
||||||
|
4. **no new `Address` proto-type.** The tunnel rides *beside* the
|
||||||
|
inner addr in a frozen `WGTunnelledAddr`, and only `.inner`
|
||||||
|
crosses into `open_nursery()`. #482 §6 floated a `WGAddress`
|
||||||
|
registered in `_address_types` — that table is a `bidict`
|
||||||
|
(1:1 proto-key↔type) and `_addr_to_transport` wants a
|
||||||
|
`MsgTransport` per addr-type, which `wg` doesn'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()` `@acm`s that create/tear down the iface +
|
||||||
|
netns via `pyroute2`, is layers A→C of the plan doc.
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
# tractor: distributed structured concurrency.
|
||||||
|
'''
|
||||||
|
Host A: the service host, reachable over a `wg` tunnel.
|
||||||
|
|
||||||
|
Binds `tractor`'s registrar + an `echo_srv` sub-actor on the
|
||||||
|
tunnel's *overlay* addr, declared as a single `wg` maddr.
|
||||||
|
|
||||||
|
'''
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import tractor
|
||||||
|
import trio
|
||||||
|
|
||||||
|
from wg_maddr import (
|
||||||
|
parse_wg_maddr,
|
||||||
|
verify_wg_peer,
|
||||||
|
WGTunnelledAddr,
|
||||||
|
)
|
||||||
|
|
||||||
|
# bearer = host A's underlay `(ip, wg ListenPort)`
|
||||||
|
# key = host A's OWN tunnel pubkey
|
||||||
|
# overlay = the ep `tractor` binds, on the wg iface's addr
|
||||||
|
WG_MADDR: str = (
|
||||||
|
'/ip4/192.168.1.50/udp/51820'
|
||||||
|
'/wg/u<A_pub_b64url>'
|
||||||
|
'/ip4/10.0.11.1/tcp/1616'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def echo(msg: str) -> str:
|
||||||
|
actor = tractor.current_actor()
|
||||||
|
return f'{actor.aid.name!r} echoes: {msg}'
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
addr: WGTunnelledAddr = parse_wg_maddr(WG_MADDR)
|
||||||
|
assert verify_wg_peer(addr), (
|
||||||
|
f'wg pubkey from maddr not active on wg0 !\n'
|
||||||
|
f'maddr: {WG_MADDR}\n'
|
||||||
|
f'key: {addr.peer_pubkey}\n'
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
f'wg bearer (kernel-owned): {addr.bearer}\n'
|
||||||
|
f'tractor overlay ep: {addr.inner}\n'
|
||||||
|
)
|
||||||
|
async with tractor.open_nursery(
|
||||||
|
# XXX only `.inner` crosses into the runtime; the bearer
|
||||||
|
# + key are iface-layer concerns `tractor` never binds.
|
||||||
|
registry_addrs=[addr.inner],
|
||||||
|
enable_transports=[addr.inner_proto],
|
||||||
|
) as an:
|
||||||
|
await an.start_actor(
|
||||||
|
'echo_srv',
|
||||||
|
enable_modules=[__name__],
|
||||||
|
)
|
||||||
|
print(f'echo_srv up on\n {addr.maddr}\n')
|
||||||
|
await trio.sleep_forever()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
trio.run(main)
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
# 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',
|
||||||
|
registry_addrs=[addr.inner],
|
||||||
|
enable_transports=[addr.inner_proto],
|
||||||
|
),
|
||||||
|
tractor.find_actor(
|
||||||
|
'echo_srv',
|
||||||
|
registry_addrs=[addr.inner],
|
||||||
|
) as portal,
|
||||||
|
):
|
||||||
|
res: str = await portal.run(
|
||||||
|
echo,
|
||||||
|
msg='hello over wg!',
|
||||||
|
)
|
||||||
|
print(res)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
trio.run(main)
|
||||||
|
|
@ -0,0 +1,215 @@
|
||||||
|
# tractor: distributed structured concurrency.
|
||||||
|
r'''
|
||||||
|
Parse `wg`-tunnelled multiaddrs into `tractor`-ready addrs.
|
||||||
|
|
||||||
|
The canonical form (per py-multiaddr PR #108, verified to parse +
|
||||||
|
round-trip on that branch) nests the *overlay* endpoint **after**
|
||||||
|
the `/wg/` segment:
|
||||||
|
|
||||||
|
/ip4/10.0.0.1/udp/51820/wg/u<key>/ip4/10.0.11.1/tcp/1616
|
||||||
|
\_______ wg bearer ______/\_ key _/\____ tractor ep _____/
|
||||||
|
(underlay, wg
|
||||||
|
`ListenPort`)
|
||||||
|
|
||||||
|
- the segments *before* `/wg/` are the **bearer**: the underlay
|
||||||
|
`(ip, udp-port)` that `wg(8)` itself listens on. Nothing in
|
||||||
|
`tractor` ever binds this — the kernel/`wg` iface owns it.
|
||||||
|
- `/wg/u<key>` carries the tunnel peer's Curve25519 pubkey as
|
||||||
|
multibase base64url (std base64 from `wg(8)` contains `/` and
|
||||||
|
can't go in a `/`-delimited maddr).
|
||||||
|
- the segments *after* are the **overlay** endpoint, i.e. the
|
||||||
|
addr `tractor` actually binds/dials. This is the only part the
|
||||||
|
runtime sees.
|
||||||
|
|
||||||
|
XXX NOTE, `tractor`'s own `parse_maddr()` can't parse this yet
|
||||||
|
(`ValueError('Unsupported multiaddr protocol combo')`), which is
|
||||||
|
why this module exists: parse here, hand `.inner` to the runtime.
|
||||||
|
|
||||||
|
Design rules this module follows (see
|
||||||
|
`ai/tpt-backends/03_wg_tunnel_bindspace.md`):
|
||||||
|
|
||||||
|
- **parsing is pure**. `parse_wg_maddr()` does no I/O, no
|
||||||
|
`subprocess`, no netlink. A parser that shells out is a nasty
|
||||||
|
surprise.
|
||||||
|
- **verification is an explicit, separate step**. The caller
|
||||||
|
composes `verify_wg_peer()` when it wants it; nothing implicit.
|
||||||
|
- **no new `Address` proto-type**. `wg` gets no entry in
|
||||||
|
`tractor.discovery._addr._address_types` (a `bidict`, so 1:1
|
||||||
|
proto-key<->type) bc it has no `MsgTransport` of its own. The
|
||||||
|
tunnel is a *bindspace*, so we carry it beside the inner addr
|
||||||
|
and strip to `.inner` at bind/dial time.
|
||||||
|
|
||||||
|
'''
|
||||||
|
from __future__ import annotations
|
||||||
|
import base64
|
||||||
|
import subprocess
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
import msgspec
|
||||||
|
|
||||||
|
|
||||||
|
class WGTunnelledAddr(
|
||||||
|
msgspec.Struct,
|
||||||
|
frozen=True,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
A `wg`-tunnelled endpoint: the underlay bearer, the tunnel
|
||||||
|
peer key, and the overlay addr `tractor` binds/dials.
|
||||||
|
|
||||||
|
'''
|
||||||
|
# underlay, owned by `wg(8)`/the kernel — NEVER bound by us
|
||||||
|
bearer: tuple[str, int]
|
||||||
|
|
||||||
|
# tunnel peer pubkey in the std-base64 `wg(8)` form, i.e.
|
||||||
|
# directly comparable to `wg show <if> peers` output
|
||||||
|
peer_pubkey: str
|
||||||
|
|
||||||
|
# overlay ep: an `UnwrappedAddress` as accepted by
|
||||||
|
# `tractor.discovery.wrap_address()`
|
||||||
|
inner: tuple[str, int]
|
||||||
|
inner_proto: Literal['tcp'] = 'tcp'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def maddr(self) -> str:
|
||||||
|
'''
|
||||||
|
Re-render the canonical maddr `str` form.
|
||||||
|
|
||||||
|
'''
|
||||||
|
b_host, b_port = self.bearer
|
||||||
|
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:
|
||||||
|
'''
|
||||||
|
`wg(8)` std-base64 pubkey -> multibase base64url (`u`-prefixed).
|
||||||
|
|
||||||
|
'''
|
||||||
|
import multibase
|
||||||
|
raw: bytes = base64.b64decode(wg8_key)
|
||||||
|
return multibase.encode('base64url', raw).decode('ascii')
|
||||||
|
|
||||||
|
|
||||||
|
def wg8_pubkey(mb_key: str) -> str:
|
||||||
|
'''
|
||||||
|
Inverse of `mb_pubkey()`: multibase -> `wg(8)` std-base64.
|
||||||
|
|
||||||
|
'''
|
||||||
|
import multibase
|
||||||
|
raw: bytes = multibase.decode(mb_key)
|
||||||
|
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.
|
||||||
|
|
||||||
|
Uses `py-multiaddr` when it knows the `wg` proto (PR #108),
|
||||||
|
else falls back to a minimal segment split.
|
||||||
|
|
||||||
|
'''
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _segments(maddr: str) -> list[str]:
|
||||||
|
'''
|
||||||
|
Deliver a maddr's `/`-split segments, preferring the real
|
||||||
|
parser when it supports `wg`.
|
||||||
|
|
||||||
|
'''
|
||||||
|
from multiaddr import Multiaddr
|
||||||
|
try:
|
||||||
|
# the real thing: validates every proto + value
|
||||||
|
Multiaddr(maddr)
|
||||||
|
except Exception:
|
||||||
|
# XXX STOPGAP, only until py-multiaddr#108 lands; then
|
||||||
|
# this branch is dead and `Multiaddr` is authoritative.
|
||||||
|
# We deliberately DON'T hand-roll a `wg` codec (the whole
|
||||||
|
# point of gh #429 was dropping the NIH parser).
|
||||||
|
pass
|
||||||
|
return [s for s in maddr.split('/') if s]
|
||||||
|
|
||||||
|
|
||||||
|
def verify_wg_peer(
|
||||||
|
addr: WGTunnelledAddr,
|
||||||
|
iface: str = 'wg0',
|
||||||
|
) -> bool:
|
||||||
|
'''
|
||||||
|
True iff `addr.peer_pubkey` is a configured peer (or our own
|
||||||
|
pubkey) on `iface`.
|
||||||
|
|
||||||
|
IMPURE + explicit by design: never called from
|
||||||
|
`parse_wg_maddr()`.
|
||||||
|
|
||||||
|
?TODO, per plan-03 layer B, swap this body for `pyroute2`
|
||||||
|
(keeping the signature) — and note `setns(2)` is *per-thread*,
|
||||||
|
so a query issued via `trio.to_thread` lands in the ORIGINAL
|
||||||
|
netns unless `netns=` is passed down.
|
||||||
|
|
||||||
|
'''
|
||||||
|
def _wg(*args: str) -> str:
|
||||||
|
return subprocess.run(
|
||||||
|
['wg', 'show', iface, *args],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True,
|
||||||
|
).stdout
|
||||||
|
|
||||||
|
return (
|
||||||
|
addr.peer_pubkey in _wg('peers').split()
|
||||||
|
or
|
||||||
|
addr.peer_pubkey == _wg('public-key').strip()
|
||||||
|
)
|
||||||
Loading…
Reference in New Issue