Model explicit WireGuard peers
Add process-local `WGPeerConfig` entries and make `WGInterfaceConfig` own a unique peer tuple for listener and dial provisioning. Deats, - carry peer public keys, allowed CIDRs and optional endpoints - redact per-peer preshared keys while blocking wire encoding - validate peer routes, endpoint ports and keepalive intervals - reject duplicate peers before future kernel mutation - support multi-peer listeners without overloading tunnel identity Prompt-IO: ai/prompt-io/opencode/20260826T001442Z_dcdf4d82_prompt_io.md (this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
parent
d68b101717
commit
9dfdd54e0f
|
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
model: gpt-5.6-sol
|
||||
service: opencode
|
||||
session: tractor-addr-unpacking
|
||||
timestamp: 2026-08-26T00:14:42Z
|
||||
git_ref: dcdf4d82
|
||||
scope: code
|
||||
substantive: true
|
||||
raw_file: 20260826T001442Z_dcdf4d82_prompt_io.raw.md
|
||||
---
|
||||
|
||||
## Prompt
|
||||
|
||||
The human selected an explicit peer-list model rather than a
|
||||
dial-only single-peer shortcut before `open_wg_iface()`.
|
||||
|
||||
## Response summary
|
||||
|
||||
Added process-local per-peer configuration and refactored interface
|
||||
configuration to own a unique peer tuple. This supports multi-peer
|
||||
listeners and dial targets without overloading role-dependent tunnel
|
||||
maddr identity.
|
||||
|
||||
## Files changed
|
||||
|
||||
- `tractor/discovery/_tunnel.py` - peer model and interface peer list.
|
||||
- `tractor/discovery/__init__.py` - public peer-config export.
|
||||
- `tests/discovery/test_wg_config.py` - peer secrecy and validation.
|
||||
- `ai/tpt-backends/03_wg_tunnel_bindspace.md` - explicit multi-peer
|
||||
provisioning contract.
|
||||
|
||||
## Human edits
|
||||
|
||||
The human chose explicit peer lists over a dial-only implementation so
|
||||
listeners can represent multiple client public keys and routing
|
||||
policies. The agent implemented that human-selected design; no direct
|
||||
manual source edits were observed.
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
model: gpt-5.6-sol
|
||||
service: opencode
|
||||
timestamp: 2026-08-26T00:14:42Z
|
||||
git_ref: dcdf4d82
|
||||
diff_cmd: git diff HEAD~1..HEAD
|
||||
---
|
||||
|
||||
# Raw output - model explicit WireGuard peers
|
||||
|
||||
After committing local WG interface configuration, the human selected
|
||||
an explicit peer-list model before interface provisioning.
|
||||
|
||||
> `git diff HEAD~1..HEAD -- tractor/discovery/_tunnel.py tractor/discovery/__init__.py tests/discovery/test_wg_config.py ai/tpt-backends/03_wg_tunnel_bindspace.md`
|
||||
|
||||
Added process-local `WGPeerConfig` with public key, allowed CIDRs,
|
||||
optional endpoint, preshared key and keepalive. Refactored
|
||||
`WGInterfaceConfig` to own a tuple of unique peers beside its private
|
||||
key, local addresses and listen port.
|
||||
|
||||
Peer PSKs remain redacted and nested `ProcessLocal` sentinels prevent
|
||||
default wire encoding. Validation covers keys, routes, endpoints,
|
||||
ports, keepalive and duplicate peers.
|
||||
|
||||
Ruff and lock checks passed. Focused peer/config coverage passed 11
|
||||
tests. A broad unrelated registrar cancellation case timed out after
|
||||
86 passes and 2 xpasses; its isolated rerun passed.
|
||||
|
|
@ -514,12 +514,13 @@ async def open_wg_iface(
|
|||
) -> AsyncGenerator[WGTunnelSpec, None]: ...
|
||||
```
|
||||
|
||||
`WGInterfaceConfig` is process-local, redacts its private/preshared
|
||||
keys and is rejected by the global `ProcessLocal` wire guard. It owns
|
||||
local interface addresses, peer allowed CIDRs, listen port and
|
||||
persistent keepalive policy. `WGTunnelSpec` remains serializable and
|
||||
contains only public maddr-derived identity/endpoint data. This split
|
||||
keeps secret material out of address declarations and actor payloads.
|
||||
`WGInterfaceConfig` and each `WGPeerConfig` are process-local and
|
||||
rejected by the global `ProcessLocal` wire guard. The interface config
|
||||
owns its private key, local addresses and listen port; each peer owns
|
||||
its public key, allowed CIDRs, optional endpoint, preshared key and
|
||||
keepalive. Reprs redact private/preshared keys. `WGTunnelSpec` remains
|
||||
serializable public maddr-derived identity/endpoint data. This split
|
||||
supports multi-peer listeners without overloading the tunnel maddr.
|
||||
|
||||
and a driver that folds a list of specs into nested contexts
|
||||
(`contextlib.AsyncExitStack` for the N-deep case). The
|
||||
|
|
|
|||
|
|
@ -7,11 +7,15 @@ from __future__ import annotations
|
|||
import msgspec
|
||||
import pytest
|
||||
|
||||
from tractor.discovery import WGInterfaceConfig
|
||||
from tractor.discovery import (
|
||||
WGInterfaceConfig,
|
||||
WGPeerConfig,
|
||||
)
|
||||
from tractor.msg import ProcessLocal
|
||||
|
||||
|
||||
_PRIVATE_KEY: str = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='
|
||||
_PEER_KEY: str = 'r1LKM1pqhuY9Z6L4y5jQ2fGX67kJSrq5kRV5Jk2ywEo='
|
||||
_PRESHARED_KEY: str = 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBA='
|
||||
|
||||
|
||||
|
|
@ -24,13 +28,18 @@ def test_wg_config_is_process_local_and_redacted() -> None:
|
|||
Verify `ProcessLocal` blocks default msgpack encoding.
|
||||
|
||||
'''
|
||||
peer: WGPeerConfig = WGPeerConfig(
|
||||
public_key=_PEER_KEY,
|
||||
allowed_ips=('10.1.0.0/16', 'fd01::/64'),
|
||||
endpoint=('192.0.2.1', 51820),
|
||||
preshared_key=_PRESHARED_KEY,
|
||||
persistent_keepalive=25,
|
||||
)
|
||||
config: WGInterfaceConfig = WGInterfaceConfig(
|
||||
private_key=_PRIVATE_KEY,
|
||||
addresses=('10.0.0.1/24', 'fd00::1/64'),
|
||||
allowed_ips=('10.1.0.0/16', 'fd01::/64'),
|
||||
listen_port=51820,
|
||||
preshared_key=_PRESHARED_KEY,
|
||||
persistent_keepalive=25,
|
||||
peers=(peer,),
|
||||
)
|
||||
|
||||
config_repr: str = repr(config)
|
||||
|
|
@ -38,7 +47,7 @@ def test_wg_config_is_process_local_and_redacted() -> None:
|
|||
assert _PRIVATE_KEY not in config_repr
|
||||
assert _PRESHARED_KEY not in config_repr
|
||||
assert config.addresses[0] in config_repr
|
||||
assert config.allowed_ips[0] in config_repr
|
||||
assert peer.allowed_ips[0] in config_repr
|
||||
with pytest.raises(
|
||||
TypeError,
|
||||
match='_ProcessLocalToken.*unsupported',
|
||||
|
|
@ -54,14 +63,6 @@ def test_wg_config_is_process_local_and_redacted() -> None:
|
|||
ValueError,
|
||||
id='private-key',
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
'private_key': _PRIVATE_KEY,
|
||||
'preshared_key': 'not-base64',
|
||||
},
|
||||
ValueError,
|
||||
id='preshared-key',
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
'private_key': _PRIVATE_KEY,
|
||||
|
|
@ -70,14 +71,6 @@ def test_wg_config_is_process_local_and_redacted() -> None:
|
|||
ValueError,
|
||||
id='address',
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
'private_key': _PRIVATE_KEY,
|
||||
'allowed_ips': ('not-a-network',),
|
||||
},
|
||||
ValueError,
|
||||
id='allowed-ip',
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
'private_key': _PRIVATE_KEY,
|
||||
|
|
@ -89,10 +82,13 @@ def test_wg_config_is_process_local_and_redacted() -> None:
|
|||
pytest.param(
|
||||
{
|
||||
'private_key': _PRIVATE_KEY,
|
||||
'persistent_keepalive': -1,
|
||||
'peers': (
|
||||
WGPeerConfig(public_key=_PEER_KEY),
|
||||
WGPeerConfig(public_key=_PEER_KEY),
|
||||
),
|
||||
},
|
||||
ValueError,
|
||||
id='keepalive',
|
||||
id='duplicate-peer',
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -110,3 +106,58 @@ def test_wg_config_rejects_invalid_values(
|
|||
'''
|
||||
with pytest.raises(error):
|
||||
WGInterfaceConfig(**kwargs) # type: ignore[arg-type]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'kwargs',
|
||||
(
|
||||
pytest.param(
|
||||
{'public_key': 'not-base64'},
|
||||
id='public-key',
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
'public_key': _PEER_KEY,
|
||||
'preshared_key': 'not-base64',
|
||||
},
|
||||
id='preshared-key',
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
'public_key': _PEER_KEY,
|
||||
'allowed_ips': ('not-a-network',),
|
||||
},
|
||||
id='allowed-ip',
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
'public_key': _PEER_KEY,
|
||||
'endpoint': ('not-an-ip', 51820),
|
||||
},
|
||||
id='endpoint-host',
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
'public_key': _PEER_KEY,
|
||||
'endpoint': ('192.0.2.1', 65536),
|
||||
},
|
||||
id='endpoint-port',
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
'public_key': _PEER_KEY,
|
||||
'persistent_keepalive': -1,
|
||||
},
|
||||
id='keepalive',
|
||||
),
|
||||
),
|
||||
)
|
||||
def test_wg_peer_config_rejects_invalid_values(
|
||||
kwargs: dict[str, object],
|
||||
) -> None:
|
||||
'''
|
||||
Invalid peer policy must fail before kernel mutation.
|
||||
|
||||
'''
|
||||
with pytest.raises(ValueError):
|
||||
WGPeerConfig(**kwargs) # type: ignore[arg-type]
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ from ._tunnel import (
|
|||
TunnelSpec as TunnelSpec,
|
||||
WGTunnelSpec as WGTunnelSpec,
|
||||
WGInterfaceConfig as WGInterfaceConfig,
|
||||
WGPeerConfig as WGPeerConfig,
|
||||
mb_pubkey as mb_pubkey,
|
||||
mk_wg_maddr as mk_wg_maddr,
|
||||
parse_wg_maddr as parse_wg_maddr,
|
||||
|
|
|
|||
|
|
@ -197,6 +197,86 @@ def _wg8_key_str(
|
|||
return key
|
||||
|
||||
|
||||
class WGPeerConfig(
|
||||
ProcessLocal,
|
||||
):
|
||||
'''
|
||||
Process-local configuration for one WireGuard peer.
|
||||
|
||||
'''
|
||||
public_key: str
|
||||
allowed_ips: tuple[str, ...] = ()
|
||||
endpoint: tuple[str, int]|None = None
|
||||
preshared_key: str|None = None
|
||||
persistent_keepalive: int|None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
'''
|
||||
Validate peer identity, routes, endpoint and secret policy.
|
||||
|
||||
'''
|
||||
_wg8_key_str(self.public_key)
|
||||
if self.preshared_key is not None:
|
||||
_wg8_key_str(self.preshared_key)
|
||||
|
||||
# Validate each route. `strict=False` accepts host bits;
|
||||
# pyroute2 will still receive each original declared string.
|
||||
allowed_ip: str
|
||||
for allowed_ip in self.allowed_ips:
|
||||
ipaddress.ip_network(
|
||||
allowed_ip,
|
||||
strict=False,
|
||||
)
|
||||
|
||||
endpoint: tuple[str, int]|None = self.endpoint
|
||||
if endpoint is not None:
|
||||
host, port = endpoint
|
||||
ipaddress.ip_address(host)
|
||||
if (
|
||||
type(port) is not int
|
||||
or
|
||||
not 1 <= port <= 65535
|
||||
):
|
||||
raise ValueError(
|
||||
'`WGPeerConfig.endpoint` port must be in '
|
||||
'`1..65535`!'
|
||||
)
|
||||
|
||||
keepalive: int|None = self.persistent_keepalive
|
||||
if (
|
||||
keepalive is not None
|
||||
and
|
||||
(
|
||||
type(keepalive) is not int
|
||||
or
|
||||
not 0 <= keepalive <= 65535
|
||||
)
|
||||
):
|
||||
raise ValueError(
|
||||
'`WGPeerConfig.persistent_keepalive` must be in '
|
||||
'`0..65535` or `None`!'
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
'''
|
||||
Render public peer policy while redacting its preshared key.
|
||||
|
||||
'''
|
||||
preshared: str|None = (
|
||||
'<redacted>'
|
||||
if self.preshared_key is not None
|
||||
else None
|
||||
)
|
||||
return (
|
||||
f'{type(self).__name__}('
|
||||
f'public_key={self.public_key!r}, '
|
||||
f'allowed_ips={self.allowed_ips!r}, '
|
||||
f'endpoint={self.endpoint!r}, '
|
||||
f'preshared_key={preshared!r}, '
|
||||
f'persistent_keepalive={self.persistent_keepalive!r})'
|
||||
)
|
||||
|
||||
|
||||
class WGInterfaceConfig(
|
||||
ProcessLocal,
|
||||
):
|
||||
|
|
@ -210,34 +290,21 @@ class WGInterfaceConfig(
|
|||
'''
|
||||
private_key: str
|
||||
addresses: tuple[str, ...] = ()
|
||||
allowed_ips: tuple[str, ...] = ()
|
||||
listen_port: int|None = None
|
||||
preshared_key: str|None = None
|
||||
persistent_keepalive: int|None = None
|
||||
peers: tuple[WGPeerConfig, ...] = ()
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
'''
|
||||
Validate secrets, CIDRs and bounded WireGuard integers.
|
||||
Validate private identity, local CIDRs and peer uniqueness.
|
||||
|
||||
'''
|
||||
_wg8_key_str(self.private_key)
|
||||
if self.preshared_key is not None:
|
||||
_wg8_key_str(self.preshared_key)
|
||||
|
||||
# Validate every local CIDR; no entry is selected or consumed.
|
||||
# Validate each local CIDR; no address is selected.
|
||||
address: str
|
||||
for address in self.addresses:
|
||||
ipaddress.ip_interface(address)
|
||||
|
||||
# Validate every peer route. `strict=False` accepts host bits;
|
||||
# pyroute2 will still receive each original declared string.
|
||||
allowed_ip: str
|
||||
for allowed_ip in self.allowed_ips:
|
||||
ipaddress.ip_network(
|
||||
allowed_ip,
|
||||
strict=False,
|
||||
)
|
||||
|
||||
listen_port: int|None = self.listen_port
|
||||
if (
|
||||
listen_port is not None
|
||||
|
|
@ -253,40 +320,31 @@ class WGInterfaceConfig(
|
|||
'`1..65535` or `None`!'
|
||||
)
|
||||
|
||||
keepalive: int|None = self.persistent_keepalive
|
||||
if (
|
||||
keepalive is not None
|
||||
and
|
||||
(
|
||||
type(keepalive) is not int
|
||||
or
|
||||
not 0 <= keepalive <= 65535
|
||||
peer_keys: set[str] = set()
|
||||
peer: WGPeerConfig
|
||||
for peer in self.peers:
|
||||
if not isinstance(peer, WGPeerConfig):
|
||||
raise TypeError(
|
||||
'`WGInterfaceConfig.peers` must contain '
|
||||
'`WGPeerConfig` values!'
|
||||
)
|
||||
):
|
||||
if peer.public_key in peer_keys:
|
||||
raise ValueError(
|
||||
'`WGInterfaceConfig.persistent_keepalive` '
|
||||
'must be in '
|
||||
'`0..65535` or `None`!'
|
||||
f'Duplicate WireGuard peer: {peer.public_key!r}'
|
||||
)
|
||||
peer_keys.add(peer.public_key)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
'''
|
||||
Render non-secret policy while redacting key material.
|
||||
|
||||
'''
|
||||
preshared: str|None = (
|
||||
'<redacted>'
|
||||
if self.preshared_key is not None
|
||||
else None
|
||||
)
|
||||
return (
|
||||
f'{type(self).__name__}('
|
||||
f'private_key=<redacted>, '
|
||||
f'addresses={self.addresses!r}, '
|
||||
f'allowed_ips={self.allowed_ips!r}, '
|
||||
f'listen_port={self.listen_port!r}, '
|
||||
f'preshared_key={preshared!r}, '
|
||||
f'persistent_keepalive={self.persistent_keepalive!r})'
|
||||
f'peers={self.peers!r})'
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue