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
5ec7cbaf98
commit
4c10ce2bd0
|
|
@ -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]: ...
|
) -> AsyncGenerator[WGTunnelSpec, None]: ...
|
||||||
```
|
```
|
||||||
|
|
||||||
`WGInterfaceConfig` is process-local, redacts its private/preshared
|
`WGInterfaceConfig` and each `WGPeerConfig` are process-local and
|
||||||
keys and is rejected by the global `ProcessLocal` wire guard. It owns
|
rejected by the global `ProcessLocal` wire guard. The interface config
|
||||||
local interface addresses, peer allowed CIDRs, listen port and
|
owns its private key, local addresses and listen port; each peer owns
|
||||||
persistent keepalive policy. `WGTunnelSpec` remains serializable and
|
its public key, allowed CIDRs, optional endpoint, preshared key and
|
||||||
contains only public maddr-derived identity/endpoint data. This split
|
keepalive. Reprs redact private/preshared keys. `WGTunnelSpec` remains
|
||||||
keeps secret material out of address declarations and actor payloads.
|
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
|
and a driver that folds a list of specs into nested contexts
|
||||||
(`contextlib.AsyncExitStack` for the N-deep case). The
|
(`contextlib.AsyncExitStack` for the N-deep case). The
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,15 @@ from __future__ import annotations
|
||||||
import msgspec
|
import msgspec
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from tractor.discovery import WGInterfaceConfig
|
from tractor.discovery import (
|
||||||
|
WGInterfaceConfig,
|
||||||
|
WGPeerConfig,
|
||||||
|
)
|
||||||
from tractor.msg import ProcessLocal
|
from tractor.msg import ProcessLocal
|
||||||
|
|
||||||
|
|
||||||
_PRIVATE_KEY: str = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='
|
_PRIVATE_KEY: str = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='
|
||||||
|
_PEER_KEY: str = 'r1LKM1pqhuY9Z6L4y5jQ2fGX67kJSrq5kRV5Jk2ywEo='
|
||||||
_PRESHARED_KEY: str = 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBA='
|
_PRESHARED_KEY: str = 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBA='
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -24,13 +28,18 @@ def test_wg_config_is_process_local_and_redacted() -> None:
|
||||||
Verify `ProcessLocal` blocks default msgpack encoding.
|
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(
|
config: WGInterfaceConfig = WGInterfaceConfig(
|
||||||
private_key=_PRIVATE_KEY,
|
private_key=_PRIVATE_KEY,
|
||||||
addresses=('10.0.0.1/24', 'fd00::1/64'),
|
addresses=('10.0.0.1/24', 'fd00::1/64'),
|
||||||
allowed_ips=('10.1.0.0/16', 'fd01::/64'),
|
|
||||||
listen_port=51820,
|
listen_port=51820,
|
||||||
preshared_key=_PRESHARED_KEY,
|
peers=(peer,),
|
||||||
persistent_keepalive=25,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
config_repr: str = repr(config)
|
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 _PRIVATE_KEY not in config_repr
|
||||||
assert _PRESHARED_KEY not in config_repr
|
assert _PRESHARED_KEY not in config_repr
|
||||||
assert config.addresses[0] 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(
|
with pytest.raises(
|
||||||
TypeError,
|
TypeError,
|
||||||
match='_ProcessLocalToken.*unsupported',
|
match='_ProcessLocalToken.*unsupported',
|
||||||
|
|
@ -54,14 +63,6 @@ def test_wg_config_is_process_local_and_redacted() -> None:
|
||||||
ValueError,
|
ValueError,
|
||||||
id='private-key',
|
id='private-key',
|
||||||
),
|
),
|
||||||
pytest.param(
|
|
||||||
{
|
|
||||||
'private_key': _PRIVATE_KEY,
|
|
||||||
'preshared_key': 'not-base64',
|
|
||||||
},
|
|
||||||
ValueError,
|
|
||||||
id='preshared-key',
|
|
||||||
),
|
|
||||||
pytest.param(
|
pytest.param(
|
||||||
{
|
{
|
||||||
'private_key': _PRIVATE_KEY,
|
'private_key': _PRIVATE_KEY,
|
||||||
|
|
@ -70,14 +71,6 @@ def test_wg_config_is_process_local_and_redacted() -> None:
|
||||||
ValueError,
|
ValueError,
|
||||||
id='address',
|
id='address',
|
||||||
),
|
),
|
||||||
pytest.param(
|
|
||||||
{
|
|
||||||
'private_key': _PRIVATE_KEY,
|
|
||||||
'allowed_ips': ('not-a-network',),
|
|
||||||
},
|
|
||||||
ValueError,
|
|
||||||
id='allowed-ip',
|
|
||||||
),
|
|
||||||
pytest.param(
|
pytest.param(
|
||||||
{
|
{
|
||||||
'private_key': _PRIVATE_KEY,
|
'private_key': _PRIVATE_KEY,
|
||||||
|
|
@ -89,10 +82,13 @@ def test_wg_config_is_process_local_and_redacted() -> None:
|
||||||
pytest.param(
|
pytest.param(
|
||||||
{
|
{
|
||||||
'private_key': _PRIVATE_KEY,
|
'private_key': _PRIVATE_KEY,
|
||||||
'persistent_keepalive': -1,
|
'peers': (
|
||||||
|
WGPeerConfig(public_key=_PEER_KEY),
|
||||||
|
WGPeerConfig(public_key=_PEER_KEY),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
ValueError,
|
ValueError,
|
||||||
id='keepalive',
|
id='duplicate-peer',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -110,3 +106,58 @@ def test_wg_config_rejects_invalid_values(
|
||||||
'''
|
'''
|
||||||
with pytest.raises(error):
|
with pytest.raises(error):
|
||||||
WGInterfaceConfig(**kwargs) # type: ignore[arg-type]
|
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,
|
TunnelSpec as TunnelSpec,
|
||||||
WGTunnelSpec as WGTunnelSpec,
|
WGTunnelSpec as WGTunnelSpec,
|
||||||
WGInterfaceConfig as WGInterfaceConfig,
|
WGInterfaceConfig as WGInterfaceConfig,
|
||||||
|
WGPeerConfig as WGPeerConfig,
|
||||||
mb_pubkey as mb_pubkey,
|
mb_pubkey as mb_pubkey,
|
||||||
mk_wg_maddr as mk_wg_maddr,
|
mk_wg_maddr as mk_wg_maddr,
|
||||||
parse_wg_maddr as parse_wg_maddr,
|
parse_wg_maddr as parse_wg_maddr,
|
||||||
|
|
|
||||||
|
|
@ -197,6 +197,86 @@ def _wg8_key_str(
|
||||||
return key
|
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(
|
class WGInterfaceConfig(
|
||||||
ProcessLocal,
|
ProcessLocal,
|
||||||
):
|
):
|
||||||
|
|
@ -210,34 +290,21 @@ class WGInterfaceConfig(
|
||||||
'''
|
'''
|
||||||
private_key: str
|
private_key: str
|
||||||
addresses: tuple[str, ...] = ()
|
addresses: tuple[str, ...] = ()
|
||||||
allowed_ips: tuple[str, ...] = ()
|
|
||||||
listen_port: int|None = None
|
listen_port: int|None = None
|
||||||
preshared_key: str|None = None
|
peers: tuple[WGPeerConfig, ...] = ()
|
||||||
persistent_keepalive: int|None = None
|
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
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)
|
_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
|
address: str
|
||||||
for address in self.addresses:
|
for address in self.addresses:
|
||||||
ipaddress.ip_interface(address)
|
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
|
listen_port: int|None = self.listen_port
|
||||||
if (
|
if (
|
||||||
listen_port is not None
|
listen_port is not None
|
||||||
|
|
@ -253,40 +320,31 @@ class WGInterfaceConfig(
|
||||||
'`1..65535` or `None`!'
|
'`1..65535` or `None`!'
|
||||||
)
|
)
|
||||||
|
|
||||||
keepalive: int|None = self.persistent_keepalive
|
peer_keys: set[str] = set()
|
||||||
if (
|
peer: WGPeerConfig
|
||||||
keepalive is not None
|
for peer in self.peers:
|
||||||
and
|
if not isinstance(peer, WGPeerConfig):
|
||||||
(
|
raise TypeError(
|
||||||
type(keepalive) is not int
|
'`WGInterfaceConfig.peers` must contain '
|
||||||
or
|
'`WGPeerConfig` values!'
|
||||||
not 0 <= keepalive <= 65535
|
)
|
||||||
)
|
if peer.public_key in peer_keys:
|
||||||
):
|
raise ValueError(
|
||||||
raise ValueError(
|
f'Duplicate WireGuard peer: {peer.public_key!r}'
|
||||||
'`WGInterfaceConfig.persistent_keepalive` '
|
)
|
||||||
'must be in '
|
peer_keys.add(peer.public_key)
|
||||||
'`0..65535` or `None`!'
|
|
||||||
)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
'''
|
'''
|
||||||
Render non-secret policy while redacting key material.
|
Render non-secret policy while redacting key material.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
preshared: str|None = (
|
|
||||||
'<redacted>'
|
|
||||||
if self.preshared_key is not None
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
return (
|
return (
|
||||||
f'{type(self).__name__}('
|
f'{type(self).__name__}('
|
||||||
f'private_key=<redacted>, '
|
f'private_key=<redacted>, '
|
||||||
f'addresses={self.addresses!r}, '
|
f'addresses={self.addresses!r}, '
|
||||||
f'allowed_ips={self.allowed_ips!r}, '
|
|
||||||
f'listen_port={self.listen_port!r}, '
|
f'listen_port={self.listen_port!r}, '
|
||||||
f'preshared_key={preshared!r}, '
|
f'peers={self.peers!r})'
|
||||||
f'persistent_keepalive={self.persistent_keepalive!r})'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue