Add explicit `verify_wg_peer()` inspection
Validate a declared tunnel key against one `pyroute2` snapshot containing the iface's own key and configured peers. Deats, - share worker offload across all WireGuard key readers - forward `WGTunnelSpec.iface` and `.netns` to the read - reject malformed declarations before netlink I/O - export the async helper and cover local, peer and absent keys - replace multihost's `wg show` subprocess probe Prompt-IO: ai/prompt-io/opencode/20260822T023226Z_59a8ecfd_prompt_io.md (this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))wkt/wg_pyroute2_read
parent
62119cf132
commit
9ec5b1e702
|
|
@ -0,0 +1,46 @@
|
|||
---
|
||||
model: gpt-5.6-sol
|
||||
service: opencode
|
||||
session: tractor-addr-unpacking
|
||||
timestamp: 2026-08-22T02:32:26Z
|
||||
git_ref: 59a8ecfd
|
||||
scope: code
|
||||
substantive: true
|
||||
raw_file: 20260822T023226Z_59a8ecfd_prompt_io.raw.md
|
||||
---
|
||||
|
||||
## Prompt
|
||||
|
||||
After committing the read-only pyroute2 helpers and `wgman` design
|
||||
update, the human authorized the next isolated Layer B change:
|
||||
explicit `verify_wg_peer()` composition over WireGuard inspection.
|
||||
|
||||
## Response summary
|
||||
|
||||
Added and exported async `verify_wg_peer()` using one validated
|
||||
WireGuard key snapshot. It recognizes local-interface and configured
|
||||
peer identities without coupling kernel inspection to address
|
||||
parsing. Updated the multihost examples to use the production helper
|
||||
and removed their subprocess-based probe.
|
||||
|
||||
## Files changed
|
||||
|
||||
- `tractor/discovery/_tunnel.py` - shared async snapshot reader and
|
||||
explicit verification helper.
|
||||
- `tractor/discovery/__init__.py` - public verification export.
|
||||
- `tests/discovery/test_wg_inspection.py` - local, peer, absent and
|
||||
malformed-key verification coverage.
|
||||
- `examples/multihost/wg_lan/host_a_srv.py` - async local-key check.
|
||||
- `examples/multihost/wg_lan/host_b_client.py` - async peer-key check.
|
||||
- `examples/multihost/wg_lan/wg_maddr.py` - removed obsolete
|
||||
subprocess probe.
|
||||
- `examples/multihost/wg_lan/README.md` - pyroute2 requirements and
|
||||
verification workflow.
|
||||
- `ai/tpt-backends/03_wg_tunnel_bindspace.md` - async API contract.
|
||||
|
||||
## Human edits
|
||||
|
||||
The human selected this pre-agreed verification layer as the next
|
||||
atomic change after reviewing and committing the preceding read and
|
||||
architecture changes. The agent implemented the source changes; no
|
||||
direct manual edits or follow-up corrections were observed.
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
model: gpt-5.6-sol
|
||||
service: opencode
|
||||
timestamp: 2026-08-22T02:32:26Z
|
||||
git_ref: 59a8ecfd
|
||||
diff_cmd: git diff HEAD~1..HEAD
|
||||
---
|
||||
|
||||
# Raw output - verify declared WireGuard identities
|
||||
|
||||
The human authorized the next incremental Layer B change after
|
||||
committing the read-only pyroute2 helpers and first-child `wgman`
|
||||
design update.
|
||||
|
||||
> `git diff HEAD~1..HEAD -- tractor/discovery/_tunnel.py tractor/discovery/__init__.py tests/discovery/test_wg_inspection.py examples/multihost/wg_lan ai/tpt-backends/03_wg_tunnel_bindspace.md`
|
||||
|
||||
Added async `verify_wg_peer()` over one WireGuard key snapshot. It
|
||||
validates the declared `WGTunnelSpec.peer_pubkey` before I/O, forwards
|
||||
the spec's iface/netns, and accepts either the local interface key for
|
||||
a source/listen declaration or a configured peer key for a
|
||||
destination/dial declaration.
|
||||
|
||||
Refactored worker offload behind one shared async reader so
|
||||
verification cannot compare two different netlink snapshots. Exported
|
||||
the helper, added local/peer/absent/malformed-key coverage, and moved
|
||||
the multihost examples from their local `wg show` subprocess probe to
|
||||
the production API.
|
||||
|
||||
Ruff and lock checks passed. Focused WireGuard/tunnel/multiaddr
|
||||
coverage passed 51 tests; the complete discovery suite passed 92
|
||||
tests with 2 xpasses.
|
||||
|
|
@ -32,8 +32,10 @@ onto `trio` as the library's sans-io layer allows.
|
|||
by multiformats/py-multiaddr#107 and gh #483.
|
||||
- **today's deployable story remains declarative**: run `wg-quick`
|
||||
out-of-band, parse the maddr, strip its wrapper to the overlay
|
||||
`(host, port)`, verify the pubkey in its host-specific role,
|
||||
hand the overlay addr to `registry_addrs=`/`tpt_bind_addrs=`.
|
||||
`(host, port)`, explicitly verify the declared pubkey against the
|
||||
local interface key or configured peers with async
|
||||
`verify_wg_peer()`, then hand the overlay addr to
|
||||
`registry_addrs=`/`tpt_bind_addrs=`.
|
||||
The repaired `examples/multihost/wg_lan/` implementation derives
|
||||
from and supersedes #482's original example.
|
||||
- `Address.namespace` exists in the Protocol
|
||||
|
|
@ -200,31 +202,25 @@ Observed protocol-name lists, for writing the `match`:
|
|||
|
||||
### 3.3 pure parser helpers + explicit verification
|
||||
|
||||
The parser/key-codec helpers live in
|
||||
`tractor/discovery/_tunnel.py`; the impure verifier remains
|
||||
example-local until layer B:
|
||||
The parser/key-codec helpers and async production verifier live in
|
||||
`tractor/discovery/_tunnel.py`; parsing remains pure while verification
|
||||
is an explicit, impure caller step:
|
||||
|
||||
```python
|
||||
def parse_wg_maddr(maddr: str|Multiaddr) -> TunnelledAddress: ...
|
||||
def mb_pubkey(wg8_key: str) -> str: ...
|
||||
def wg8_pubkey(multibase_key: str) -> str: ...
|
||||
async def verify_wg_key(
|
||||
addr: TunnelledAddress,
|
||||
role: Literal['local', 'peer'],
|
||||
iface: str|None = None,
|
||||
timeout: float = 5,
|
||||
inspection: str|None = None,
|
||||
) -> bool: ... # example-local impure probe
|
||||
async def verify_wg_peer(spec: WGTunnelSpec) -> bool: ... # layer B
|
||||
```
|
||||
|
||||
In layer A `verify_wg_key()` may shell out to role-specific
|
||||
`wg show <if> public-key|peers` queries, but it must be a *single*
|
||||
async, time-bounded function so it never blocks trio's run thread
|
||||
and layer B swaps only its body. It verifies key presence only,
|
||||
not `Endpoint`, `AllowedIPs`, handshake state, or routing. Never
|
||||
run `tractor` as root: privileged inspection stays a separate
|
||||
step whose public-key output can be passed as `inspection`. Never
|
||||
call it implicitly from
|
||||
Layer A's example-local `verify_wg_key()` used role-specific
|
||||
`wg show <if> public-key|peers` queries. Layer B replaces it with
|
||||
`verify_wg_peer()`, backed by one pyroute2 key snapshot selected by
|
||||
`spec.iface` and `spec.netns`. It validates the declared key before
|
||||
I/O and accepts either the interface's own public key or a configured
|
||||
peer key. It does not enforce a host-specific role and verifies key
|
||||
presence only, not `Endpoint`, `AllowedIPs`, handshake state, or
|
||||
routing. Never call it implicitly from
|
||||
`wrap_address()`/`parse_maddr()` — parsing must stay pure and
|
||||
side-effect-free; verification is the *caller's* explicit step
|
||||
(and later, the bindspace `@acm`'s).
|
||||
|
|
@ -285,13 +281,13 @@ Three integration options, in increasing trio-nativeness:
|
|||
- (3) reimplement the codecs. Never.
|
||||
|
||||
**Recommended split**: ship (1) first so layer B is a small,
|
||||
reviewable, behaviour-preserving swap of `verify_wg_key()`'s
|
||||
body; then land (2) as a follow-up commit for the read path
|
||||
(`wg get`, `link get`) where the sans-io surface is smallest,
|
||||
and keep (1) for the privileged mutating ops. Measure before
|
||||
converting anything else — there is no perf argument here, only
|
||||
a "no foreign event loop in a trio actor" argument, which (1)
|
||||
already satisfies (a thread is not an event loop).
|
||||
reviewable replacement of the example-local verification probe with
|
||||
production `verify_wg_peer()`; then land (2) as a follow-up commit for
|
||||
the read path (`wg get`, `link get`) where the sans-io surface is
|
||||
smallest, and keep (1) for the privileged mutating ops. Measure before
|
||||
converting anything else — there is no perf argument here, only a "no
|
||||
foreign event loop in a trio actor" argument, which (1) already
|
||||
satisfies (a thread is not an event loop).
|
||||
|
||||
Explicitly **do not** pull in `trio-asyncio` for pyroute2 or infect
|
||||
every wg-using actor merely to service one-shot netlink calls. A
|
||||
|
|
@ -313,7 +309,8 @@ async def read_wg_peers(
|
|||
async def read_wg_pubkey(iface: str = 'wg0', ...) -> str: ...
|
||||
```
|
||||
|
||||
and `verify_wg_key()` becomes a thin composition over the two.
|
||||
and `verify_wg_peer()` becomes a thin composition over one shared key
|
||||
snapshot.
|
||||
Note the pure-getter rule: no `read_wg_peers(..., create=True)`.
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -45,12 +45,13 @@ no `wg` codec. So `pyproject.toml` temporarily pins the merge commit
|
|||
in its PEP 621 dependency metadata, and a plain
|
||||
|
||||
```bash
|
||||
uv sync
|
||||
uv sync --extra wg
|
||||
```
|
||||
|
||||
gets you a `wg`-aware `multiaddr`. That pin goes away once a
|
||||
release carries the codec. `py-multibase` is a direct project
|
||||
dependency, so no separate install command is needed.
|
||||
gets you a `wg`-aware `multiaddr` plus pyroute2's Linux netlink API.
|
||||
The multiaddr pin goes away once a release carries the codec.
|
||||
`py-multibase` is a direct project dependency, so no separate install
|
||||
command is needed.
|
||||
|
||||
Without the codec `parse_wg_maddr()` raises immediately with an
|
||||
actionable message — there is deliberately **no** degraded
|
||||
|
|
@ -130,27 +131,21 @@ the same string — A's bearer, A's key, A's overlay ep).
|
|||
|
||||
## 2. verify the keys
|
||||
|
||||
Interface inspection commonly needs `CAP_NET_ADMIN`. Keep that
|
||||
privileged operation separate from the `tractor` processes:
|
||||
Both scripts explicitly call `await verify_wg_peer(addr.tunnel)`
|
||||
before starting `tractor`. The helper validates the maddr's declared
|
||||
key, reads one `wg0` key snapshot through pyroute2's Linux
|
||||
generic-netlink API, and accepts the key when it is either the
|
||||
interface's own public key or one of its configured peers.
|
||||
|
||||
```bash
|
||||
# host A: output must equal the maddr's A_pub key
|
||||
export WG_KEY_INSPECTION="$(sudo wg show wg0 public-key)"
|
||||
This establishes key presence only. It does not enforce a
|
||||
host-specific local/peer role and does not verify `Endpoint`,
|
||||
`AllowedIPs`, a recent handshake, or routing.
|
||||
|
||||
# host B: output must contain the maddr's A_pub key
|
||||
export WG_KEY_INSPECTION="$(sudo wg show wg0 peers)"
|
||||
```
|
||||
|
||||
These checks establish only that host A uses the declared local
|
||||
key and host B has that key as a configured peer. They do not
|
||||
verify `Endpoint`, `AllowedIPs`, a recent handshake, or routing.
|
||||
The exported text contains public keys only. Each script passes it
|
||||
to `verify_wg_key()` with its host-specific role before starting
|
||||
`tractor`. Callers that already have permission to inspect the
|
||||
interface may omit that argument; the helper's direct query is
|
||||
async and requests cancellation after five seconds. Trio's
|
||||
subprocess termination escalation can make final process cleanup
|
||||
take longer than that cancellation deadline.
|
||||
Interface inspection commonly requires `CAP_NET_ADMIN` in the user
|
||||
namespace that owns the target network namespace. Run each program in
|
||||
a security context that already has the required inspection authority.
|
||||
The helper never invokes `sudo` or `wg(8)`, escalates privileges, or
|
||||
creates a namespace.
|
||||
|
||||
## 3. run
|
||||
|
||||
|
|
@ -162,11 +157,10 @@ python host_a_srv.py
|
|||
python host_b_client.py
|
||||
```
|
||||
|
||||
Run both `tractor` programs as the normal application account,
|
||||
not as root. Privilege is needed only for tunnel setup and the
|
||||
separate inspection above. If using that preflight, keep the
|
||||
host-specific `WG_KEY_INSPECTION` value exported in each
|
||||
program's shell.
|
||||
Run both `tractor` programs as the normal application account in a
|
||||
security context with the inspection authority described above. No
|
||||
`WG_KEY_INSPECTION` export or subprocess preflight is used; tunnel
|
||||
setup remains out-of-band. Do not run the applications as root.
|
||||
|
||||
The client binds its own actor listener to `10.0.11.2:0`, while
|
||||
the service actor binds to host A's `10.0.11.1` overlay host with
|
||||
|
|
@ -189,12 +183,13 @@ Four corrections, all from
|
|||
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_key()` is now a separate, explicitly
|
||||
composed step for inspection-capable callers. A parser that
|
||||
shells out is a nasty surprise.
|
||||
3. **no `sudo`.** #482 ran `sudo wg show`; a library/example must
|
||||
never escalate or run `tractor` as root. Privileged tunnel
|
||||
setup and key inspection are separate shell steps.
|
||||
to the parse; async `verify_wg_peer()` is now a separate,
|
||||
explicitly composed step that the caller invokes. Implicit
|
||||
kernel inspection from a parser is a nasty surprise.
|
||||
3. **no `sudo` or subprocess.** #482 ran `sudo wg show`; tractor's
|
||||
helper reads generic netlink through pyroute2 and never attempts
|
||||
privilege escalation or namespace creation. The caller must
|
||||
already have the required inspection authority.
|
||||
4. **no new `Address` proto-type.** The tunnel rides *beside* the
|
||||
overlay addr in a frozen `TunnelledAddress`, and only `.overlay`
|
||||
crosses into `open_nursery()`. #482 §6 floated a `WGAddress`
|
||||
|
|
@ -205,7 +200,7 @@ Four corrections, all from
|
|||
|
||||
## next
|
||||
|
||||
Layer A's `TunnelledAddress` and native maddr parser now live in
|
||||
`tractor.discovery`. Next, replace this example's `wg(8)` verification
|
||||
probe with `pyroute2`, then add `open_bindspace()` `@acm`s which
|
||||
create/tear down the iface and netns.
|
||||
Layer A's `TunnelledAddress` and native maddr parser plus Layer B's
|
||||
explicit pyroute2 verification now live in `tractor.discovery`. Next,
|
||||
add `open_bindspace()` `@acm`s which create/tear down the iface and
|
||||
netns.
|
||||
|
|
|
|||
|
|
@ -8,18 +8,15 @@ tunnel's *overlay* addr, declared as a single `wg` maddr.
|
|||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
import tractor
|
||||
import trio
|
||||
from tractor.discovery import (
|
||||
TunnelledAddress,
|
||||
mk_maddr,
|
||||
parse_wg_maddr,
|
||||
verify_wg_peer,
|
||||
)
|
||||
|
||||
from wg_maddr import verify_wg_key
|
||||
|
||||
# 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
|
||||
|
|
@ -37,12 +34,7 @@ async def echo(msg: str) -> str:
|
|||
|
||||
async def main():
|
||||
addr: TunnelledAddress = parse_wg_maddr(WG_MADDR)
|
||||
inspection: str | None = os.environ.get('WG_KEY_INSPECTION')
|
||||
if not await verify_wg_key(
|
||||
addr,
|
||||
role='local',
|
||||
inspection=inspection,
|
||||
):
|
||||
if not await verify_wg_peer(addr.tunnel):
|
||||
raise RuntimeError(
|
||||
f'Maddr key is not wg0 local public key!\n'
|
||||
f'maddr: {WG_MADDR}\n'
|
||||
|
|
|
|||
|
|
@ -6,17 +6,15 @@ Host B: workstation dialing host A's actor tree through the
|
|||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
import tractor
|
||||
import trio
|
||||
from tractor.discovery import (
|
||||
TunnelledAddress,
|
||||
parse_wg_maddr,
|
||||
verify_wg_peer,
|
||||
)
|
||||
|
||||
from host_a_srv import echo # noqa: F401 (RPC refs it by mod path)
|
||||
from wg_maddr import verify_wg_key
|
||||
|
||||
# same maddr as host A: A's bearer, A's key, A's overlay ep
|
||||
WG_MADDR: str = (
|
||||
|
|
@ -29,12 +27,7 @@ LOCAL_OVERLAY_BIND: tuple[str, int] = ('10.0.11.2', 0)
|
|||
|
||||
async def main():
|
||||
addr: TunnelledAddress = parse_wg_maddr(WG_MADDR)
|
||||
inspection: str | None = os.environ.get('WG_KEY_INSPECTION')
|
||||
if not await verify_wg_key(
|
||||
addr,
|
||||
role='peer',
|
||||
inspection=inspection,
|
||||
):
|
||||
if not await verify_wg_peer(addr.tunnel):
|
||||
raise RuntimeError(
|
||||
f'Maddr key is not a configured wg0 peer!\n'
|
||||
f'maddr: {WG_MADDR}\n'
|
||||
|
|
|
|||
|
|
@ -1,85 +0,0 @@
|
|||
# tractor: distributed structured concurrency.
|
||||
r'''
|
||||
Verify `wg` keys 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 _____/
|
||||
|
||||
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
|
||||
from typing import Literal
|
||||
|
||||
import trio
|
||||
|
||||
from tractor.discovery import (
|
||||
TunnelledAddress,
|
||||
WGTunnelSpec,
|
||||
)
|
||||
|
||||
|
||||
async def verify_wg_key(
|
||||
addr: TunnelledAddress,
|
||||
role: Literal['local', 'peer'],
|
||||
iface: str | None = None,
|
||||
timeout: float = 5,
|
||||
inspection: str | None = None,
|
||||
) -> bool:
|
||||
'''
|
||||
Verify the declared key in the role required on this host.
|
||||
|
||||
A bearer host uses `role='local'`; a dialer uses `role='peer'`.
|
||||
This verifies only key presence. It does not inspect the peer's
|
||||
endpoint, AllowedIPs, handshake state, or iface addresses.
|
||||
|
||||
`inspection` accepts output captured by a separate privileged
|
||||
`wg show` step. Without it, query asynchronously for callers
|
||||
which already have interface-inspection permission.
|
||||
|
||||
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
|
||||
|
||||
match role:
|
||||
case 'local':
|
||||
field = 'public-key'
|
||||
case 'peer':
|
||||
field = 'peers'
|
||||
case _:
|
||||
raise ValueError(
|
||||
f'Unknown WireGuard key role: {role!r}'
|
||||
)
|
||||
|
||||
if inspection is None:
|
||||
with trio.fail_after(timeout):
|
||||
proc = await trio.run_process(
|
||||
['wg', 'show', iface, field],
|
||||
capture_stdout=True,
|
||||
check=True,
|
||||
)
|
||||
inspection = proc.stdout.decode()
|
||||
|
||||
if role == 'local':
|
||||
return spec.peer_pubkey == inspection.strip()
|
||||
return spec.peer_pubkey in inspection.split()
|
||||
|
|
@ -16,7 +16,10 @@ import trio
|
|||
from tractor.discovery import (
|
||||
read_wg_peers,
|
||||
read_wg_pubkey,
|
||||
verify_wg_peer,
|
||||
WGTunnelSpec,
|
||||
)
|
||||
from tractor.discovery import _tunnel
|
||||
|
||||
pyroute2: Any = pytest.importorskip('pyroute2')
|
||||
|
||||
|
|
@ -24,6 +27,7 @@ pyroute2: Any = pytest.importorskip('pyroute2')
|
|||
_PUBKEY: str = 'g3x7z0AdV1rM6UQU22CC7IL3/ivn4DzrE7ikDhCZ/Dc='
|
||||
_PEER_1: str = '7PClzcj8o1yAjyPJb0zL2Gt0s2J7yZ6c0JXYqNBGr0E='
|
||||
_PEER_2: str = 'H7bJbl1bpY7VzDlB5wI3KjA7JsiYoMWGDJd8dYgc5iw='
|
||||
_MISSING_KEY: str = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='
|
||||
|
||||
|
||||
class Attrs:
|
||||
|
|
@ -215,3 +219,87 @@ def test_wg_client_closes_when_read_fails(
|
|||
|
||||
assert instance is not None
|
||||
assert instance.closed
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
('declared_key', 'expected'),
|
||||
(
|
||||
(_PUBKEY, True),
|
||||
(_PEER_2, True),
|
||||
(_MISSING_KEY, False),
|
||||
),
|
||||
)
|
||||
def test_verify_wg_peer(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
declared_key: str,
|
||||
expected: bool,
|
||||
) -> None:
|
||||
'''
|
||||
A tunnel declaration can identify either side of one local iface.
|
||||
|
||||
Return one stable key snapshot from the async reader, then prove
|
||||
a local interface key and configured peer both verify while an
|
||||
absent key does not. Also prove the spec selects the iface/netns
|
||||
supplied to the read instead of silently using process defaults.
|
||||
|
||||
'''
|
||||
reads: list[tuple[str, str|None]] = []
|
||||
|
||||
async def read_keys(
|
||||
iface: str,
|
||||
netns: str|None,
|
||||
) -> tuple[str, tuple[str, ...]]:
|
||||
'''
|
||||
Return one deterministic WireGuard key snapshot.
|
||||
|
||||
'''
|
||||
reads.append((iface, netns))
|
||||
return _PUBKEY, (_PEER_1, _PEER_2)
|
||||
|
||||
monkeypatch.setattr(
|
||||
_tunnel,
|
||||
'_read_wg_keys',
|
||||
read_keys,
|
||||
)
|
||||
spec: WGTunnelSpec = WGTunnelSpec(
|
||||
peer_pubkey=declared_key,
|
||||
iface='wg-test',
|
||||
netns='actor-net',
|
||||
)
|
||||
|
||||
assert trio.run(verify_wg_peer, spec) is expected
|
||||
assert reads == [('wg-test', 'actor-net')]
|
||||
|
||||
|
||||
def test_verify_wg_peer_validates_before_read(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
'''
|
||||
A directly constructed tunnel spec can contain a malformed key.
|
||||
|
||||
Install a reader which would fail if called, pass malformed
|
||||
base64, and prove validation rejects the declaration before any
|
||||
kernel-state inspection occurs.
|
||||
|
||||
'''
|
||||
async def unexpected_read(
|
||||
iface: str,
|
||||
netns: str|None,
|
||||
) -> NoReturn:
|
||||
'''
|
||||
Fail if malformed-key validation reaches the read boundary.
|
||||
|
||||
'''
|
||||
raise AssertionError('WireGuard read must not run')
|
||||
|
||||
monkeypatch.setattr(
|
||||
_tunnel,
|
||||
'_read_wg_keys',
|
||||
unexpected_read,
|
||||
)
|
||||
spec: WGTunnelSpec = WGTunnelSpec(
|
||||
peer_pubkey='not-base64',
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
trio.run(verify_wg_peer, spec)
|
||||
|
|
|
|||
|
|
@ -40,5 +40,6 @@ from ._tunnel import (
|
|||
read_wg_pubkey as read_wg_pubkey,
|
||||
strip_tunnels as strip_tunnels,
|
||||
tunnels_of as tunnels_of,
|
||||
verify_wg_peer as verify_wg_peer,
|
||||
wg8_pubkey as wg8_pubkey,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -198,7 +198,7 @@ def _wg8_key_str(
|
|||
return key
|
||||
|
||||
|
||||
def _read_wg_keys(
|
||||
def _sync_read_wg_keys(
|
||||
iface: str,
|
||||
netns: str|None,
|
||||
) -> tuple[str, tuple[str, ...]]:
|
||||
|
|
@ -275,6 +275,22 @@ def _read_wg_keys(
|
|||
)
|
||||
|
||||
|
||||
async def _read_wg_keys(
|
||||
iface: str,
|
||||
netns: str|None,
|
||||
) -> tuple[str, tuple[str, ...]]:
|
||||
'''
|
||||
Read one WireGuard key snapshot without blocking Trio.
|
||||
|
||||
'''
|
||||
return await trio.to_thread.run_sync(
|
||||
_sync_read_wg_keys,
|
||||
iface,
|
||||
netns,
|
||||
abandon_on_cancel=False,
|
||||
)
|
||||
|
||||
|
||||
async def read_wg_pubkey(
|
||||
iface: str = 'wg0',
|
||||
netns: str|None = None,
|
||||
|
|
@ -286,11 +302,9 @@ async def read_wg_pubkey(
|
|||
keys: tuple[
|
||||
str,
|
||||
tuple[str, ...],
|
||||
] = await trio.to_thread.run_sync(
|
||||
_read_wg_keys,
|
||||
] = await _read_wg_keys(
|
||||
iface,
|
||||
netns,
|
||||
abandon_on_cancel=False,
|
||||
)
|
||||
return keys[0]
|
||||
|
||||
|
|
@ -306,15 +320,39 @@ async def read_wg_peers(
|
|||
keys: tuple[
|
||||
str,
|
||||
tuple[str, ...],
|
||||
] = await trio.to_thread.run_sync(
|
||||
_read_wg_keys,
|
||||
] = await _read_wg_keys(
|
||||
iface,
|
||||
netns,
|
||||
abandon_on_cancel=False,
|
||||
)
|
||||
return keys[1]
|
||||
|
||||
|
||||
async def verify_wg_peer(
|
||||
spec: WGTunnelSpec,
|
||||
) -> bool:
|
||||
'''
|
||||
Verify a declared WireGuard identity against local kernel state.
|
||||
|
||||
A source/listen maddr names the local interface key, while a
|
||||
destination/dial maddr names one configured peer. Accept either
|
||||
match without making verification an implicit part of parsing.
|
||||
|
||||
'''
|
||||
declared_key: str = _wg8_key_str(spec.peer_pubkey)
|
||||
keys: tuple[
|
||||
str,
|
||||
tuple[str, ...],
|
||||
] = await _read_wg_keys(
|
||||
spec.iface,
|
||||
spec.netns,
|
||||
)
|
||||
return (
|
||||
declared_key == keys[0]
|
||||
or
|
||||
declared_key in keys[1]
|
||||
)
|
||||
|
||||
|
||||
def _wg_proto_code() -> int:
|
||||
'''
|
||||
Deliver the installed `py-multiaddr` `/wg/` protocol code.
|
||||
|
|
|
|||
Loading…
Reference in New Issue