Separate local WireGuard configuration
Add process-local `WGInterfaceConfig` for key material, interface addresses and peer-routing policy required by future provisioning. Deats, - redact private and preshared keys from representation - block config from default actor-IPC encoding via `ProcessLocal` - validate keys, interface CIDRs, allowed CIDRs and bounded integers - keep public endpoint and peer identity in `WGTunnelSpec` - move allowed-IP policy out of the serializable tunnel declaration Prompt-IO: ai/prompt-io/opencode/20260825T234631Z_b973e78c_prompt_io.md (this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
parent
b973e78c2a
commit
dcdf4d82ad
|
|
@ -0,0 +1,42 @@
|
|||
---
|
||||
model: gpt-5.6-sol
|
||||
service: opencode
|
||||
session: tractor-addr-unpacking
|
||||
timestamp: 2026-08-25T23:46:31Z
|
||||
git_ref: b973e78c
|
||||
scope: code
|
||||
substantive: true
|
||||
raw_file: 20260825T234631Z_b973e78c_prompt_io.raw.md
|
||||
---
|
||||
|
||||
## Prompt
|
||||
|
||||
The human pointed out that the agent had again produced no worktree
|
||||
changes after the latest commit and expected actual next-layer work.
|
||||
|
||||
## Response summary
|
||||
|
||||
Corrected the sequencing mistake and added a process-local WireGuard
|
||||
interface configuration boundary. Secret/routing policy is separated
|
||||
from serializable maddr-derived tunnel declarations, redacted from
|
||||
repr and blocked from default wire encoding.
|
||||
Staged review clarified that address/network parser calls validate
|
||||
every declaration and do not select or consume a first address.
|
||||
|
||||
## Files changed
|
||||
|
||||
- `tractor/discovery/_tunnel.py` - local WG config and validation.
|
||||
- `tractor/discovery/__init__.py` - public config export.
|
||||
- `tests/discovery/test_wg_config.py` - secrecy and validation coverage.
|
||||
- `ai/tpt-backends/03_wg_tunnel_bindspace.md` - secret/config boundary
|
||||
for future interface provisioning.
|
||||
|
||||
## Human edits
|
||||
|
||||
The human identified the second sequencing failure where the agent
|
||||
summarized an already committed slice instead of implementing the next
|
||||
one. That correction directly caused this new WG configuration layer
|
||||
to be implemented. During staged review, the human requested explicit
|
||||
explanation of the address and allowed-route validation loops. The
|
||||
agent applied that human-directed clarification; no direct manual edits
|
||||
were observed.
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
model: gpt-5.6-sol
|
||||
service: opencode
|
||||
timestamp: 2026-08-25T23:46:31Z
|
||||
git_ref: b973e78c
|
||||
diff_cmd: git diff HEAD~1..HEAD
|
||||
---
|
||||
|
||||
# Raw output - separate local WireGuard configuration
|
||||
|
||||
The human again noticed the agent had re-reported already committed
|
||||
work without changing the worktree. The agent confirmed the sequencing
|
||||
mistake and moved to the next unimplemented dependency.
|
||||
|
||||
> `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 `WGInterfaceConfig` for private/preshared keys,
|
||||
local interface CIDRs, peer allowed CIDRs, listen port and persistent
|
||||
keepalive. Secrets are redacted from repr and the global
|
||||
`ProcessLocal` marker blocks default wire encoding.
|
||||
|
||||
Removed the unused serialized `WGTunnelSpec.maybe_allowed_ips`
|
||||
placeholder so maddr-derived declarations retain only public identity,
|
||||
endpoint and interface selection. Added validation for keys, CIDRs,
|
||||
ports and keepalive before future kernel mutation.
|
||||
|
||||
Ruff and lock checks passed. Focused WG coverage passed 25 tests;
|
||||
discovery plus message coverage passed 145 tests with 2 xpasses.
|
||||
|
|
@ -508,11 +508,19 @@ async def open_netns(
|
|||
@acm
|
||||
async def open_wg_iface(
|
||||
spec: WGTunnelSpec,
|
||||
config: WGInterfaceConfig,
|
||||
bindspace: BindspaceHandle,
|
||||
role: Literal['listen', 'dial'],
|
||||
) -> 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.
|
||||
|
||||
and a driver that folds a list of specs into nested contexts
|
||||
(`contextlib.AsyncExitStack` for the N-deep case). The
|
||||
`parse_endpoints()` API (`_multiaddr.py:153`) is the front door:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,112 @@
|
|||
'''
|
||||
Process-local WireGuard interface configuration contracts.
|
||||
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
import msgspec
|
||||
import pytest
|
||||
|
||||
from tractor.discovery import WGInterfaceConfig
|
||||
from tractor.msg import ProcessLocal
|
||||
|
||||
|
||||
_PRIVATE_KEY: str = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='
|
||||
_PRESHARED_KEY: str = 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBA='
|
||||
|
||||
|
||||
def test_wg_config_is_process_local_and_redacted() -> None:
|
||||
'''
|
||||
Private WireGuard configuration must neither print nor cross IPC.
|
||||
|
||||
Construct a complete local config and prove its public routing
|
||||
policy remains inspectable while both keys are absent from repr.
|
||||
Verify `ProcessLocal` blocks default msgpack encoding.
|
||||
|
||||
'''
|
||||
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,
|
||||
)
|
||||
|
||||
config_repr: str = repr(config)
|
||||
assert isinstance(config, ProcessLocal)
|
||||
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
|
||||
with pytest.raises(
|
||||
TypeError,
|
||||
match='_ProcessLocalToken.*unsupported',
|
||||
):
|
||||
msgspec.msgpack.encode(config)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
('kwargs', 'error'),
|
||||
(
|
||||
pytest.param(
|
||||
{'private_key': 'not-base64'},
|
||||
ValueError,
|
||||
id='private-key',
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
'private_key': _PRIVATE_KEY,
|
||||
'preshared_key': 'not-base64',
|
||||
},
|
||||
ValueError,
|
||||
id='preshared-key',
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
'private_key': _PRIVATE_KEY,
|
||||
'addresses': ('not-an-interface',),
|
||||
},
|
||||
ValueError,
|
||||
id='address',
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
'private_key': _PRIVATE_KEY,
|
||||
'allowed_ips': ('not-a-network',),
|
||||
},
|
||||
ValueError,
|
||||
id='allowed-ip',
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
'private_key': _PRIVATE_KEY,
|
||||
'listen_port': 65536,
|
||||
},
|
||||
ValueError,
|
||||
id='listen-port',
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
'private_key': _PRIVATE_KEY,
|
||||
'persistent_keepalive': -1,
|
||||
},
|
||||
ValueError,
|
||||
id='keepalive',
|
||||
),
|
||||
),
|
||||
)
|
||||
def test_wg_config_rejects_invalid_values(
|
||||
kwargs: dict[str, object],
|
||||
error: type[Exception],
|
||||
) -> None:
|
||||
'''
|
||||
Invalid config must fail before kernel mutation.
|
||||
|
||||
Parameterize every validated input class and prove direct msgspec
|
||||
construction cannot carry malformed configuration into a future
|
||||
pyroute2 interface lifecycle.
|
||||
|
||||
'''
|
||||
with pytest.raises(error):
|
||||
WGInterfaceConfig(**kwargs) # type: ignore[arg-type]
|
||||
|
|
@ -45,6 +45,7 @@ from ._tunnel import (
|
|||
TunnelledAddress as TunnelledAddress,
|
||||
TunnelSpec as TunnelSpec,
|
||||
WGTunnelSpec as WGTunnelSpec,
|
||||
WGInterfaceConfig as WGInterfaceConfig,
|
||||
mb_pubkey as mb_pubkey,
|
||||
mk_wg_maddr as mk_wg_maddr,
|
||||
parse_wg_maddr as parse_wg_maddr,
|
||||
|
|
|
|||
|
|
@ -83,6 +83,8 @@ from multiaddr.exceptions import ProtocolNotFoundError
|
|||
from multiaddr.protocols import protocol_with_name
|
||||
import trio
|
||||
|
||||
from ..msg._local import ProcessLocal
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ._addr import (
|
||||
Address,
|
||||
|
|
@ -117,9 +119,6 @@ class WGTunnelSpec(
|
|||
iface: str = 'wg0'
|
||||
netns: str|None = None
|
||||
|
||||
# layer-C-only fields, unset in layer A
|
||||
maybe_allowed_ips: tuple[str, ...] = ()
|
||||
|
||||
# the `multiaddr` proto name for this tunnel kind
|
||||
tunnel_key: ClassVar[str] = 'wg'
|
||||
|
||||
|
|
@ -195,6 +194,99 @@ def _wg8_key_str(
|
|||
return key
|
||||
|
||||
|
||||
class WGInterfaceConfig(
|
||||
ProcessLocal,
|
||||
):
|
||||
'''
|
||||
Process-local secrets and routing inputs for one WireGuard iface.
|
||||
|
||||
Public peer identity, endpoint and iface selection remain in
|
||||
`WGTunnelSpec`; private key material and local routing policy do
|
||||
not belong in an maddr-derived serializable declaration.
|
||||
|
||||
'''
|
||||
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
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
'''
|
||||
Validate secrets, CIDRs and bounded WireGuard integers.
|
||||
|
||||
'''
|
||||
_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.
|
||||
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
|
||||
and
|
||||
(
|
||||
type(listen_port) is not int
|
||||
or
|
||||
not 1 <= listen_port <= 65535
|
||||
)
|
||||
):
|
||||
raise ValueError(
|
||||
'`WGInterfaceConfig.listen_port` must be in '
|
||||
'`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
|
||||
)
|
||||
):
|
||||
raise ValueError(
|
||||
'`WGInterfaceConfig.persistent_keepalive` '
|
||||
'must be in '
|
||||
'`0..65535` or `None`!'
|
||||
)
|
||||
|
||||
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})'
|
||||
)
|
||||
|
||||
|
||||
def _sync_read_wg_keys(
|
||||
iface: str,
|
||||
netns: str|None,
|
||||
|
|
|
|||
Loading…
Reference in New Issue