235 lines
5.9 KiB
Python
235 lines
5.9 KiB
Python
'''
|
|
`TunnelledAddress` delegation + peeling semantics.
|
|
|
|
A tunnel annotates an existing L4 addr rather than being its own
|
|
transport, so the contract under test is mostly *delegation*: the
|
|
runtime must not be able to tell a tunnelled addr from its
|
|
overlay, and **nothing** about the tunnel may cross the wire.
|
|
|
|
See `ai/tpt-backends/03_wg_tunnel_bindspace.md` §3.1/§3.4.
|
|
|
|
'''
|
|
from __future__ import annotations
|
|
|
|
import msgspec
|
|
import pytest
|
|
|
|
from tractor.discovery import (
|
|
TunnelledAddress,
|
|
WGTunnelSpec,
|
|
mb_pubkey,
|
|
strip_tunnels,
|
|
tunnels_of,
|
|
wg8_pubkey,
|
|
)
|
|
from tractor.discovery._addr import (
|
|
is_wrapped_addr,
|
|
wrap_address,
|
|
)
|
|
from tractor.ipc._tcp import TCPAddress
|
|
|
|
|
|
# a valid-looking std-base64 `wg(8)` pubkey (32B -> 44 chars)
|
|
_PUBKEY: str = 'g3x7z0AdV1rM6UQU22CC7IL3/ivn4DzrE7ikDhCZ/Dc='
|
|
|
|
|
|
@pytest.fixture
|
|
def overlay() -> TCPAddress:
|
|
return TCPAddress('10.0.11.1', 1616)
|
|
|
|
|
|
@pytest.fixture
|
|
def spec() -> WGTunnelSpec:
|
|
return WGTunnelSpec(
|
|
peer_pubkey=_PUBKEY,
|
|
bearer=('192.168.1.50', 51820),
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def tunnelled(
|
|
overlay: TCPAddress,
|
|
spec: WGTunnelSpec,
|
|
) -> TunnelledAddress:
|
|
return TunnelledAddress(overlay=overlay, tunnel=spec)
|
|
|
|
|
|
def test_wg_pubkey_codec_roundtrip():
|
|
'''
|
|
Standard `wg(8)` base64 keys can contain `/`, which cannot be
|
|
embedded unchanged in a slash-delimited maddr. Prove the helper
|
|
emits `u`-prefixed multibase base64url without `/` and decodes
|
|
it back to the exact original 32-byte key.
|
|
|
|
'''
|
|
mb_key: str = mb_pubkey(_PUBKEY)
|
|
|
|
assert mb_key.startswith('u')
|
|
assert '/' not in mb_key
|
|
assert wg8_pubkey(mb_key) == _PUBKEY
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'key, converter',
|
|
[
|
|
pytest.param(
|
|
'dG9vIHNob3J0',
|
|
mb_pubkey,
|
|
id='wg8-base64',
|
|
),
|
|
pytest.param(
|
|
'udG9vIHNob3J0',
|
|
wg8_pubkey,
|
|
id='multibase',
|
|
),
|
|
],
|
|
)
|
|
def test_wg_pubkey_codec_rejects_wrong_size(
|
|
key: str,
|
|
converter,
|
|
):
|
|
'''
|
|
WireGuard silently-corrupt key handling would let an invalid
|
|
identity reach peer verification. Exercise both input encodings
|
|
with a short payload and prove conversion rejects it before a
|
|
tunnel spec or maddr can be constructed.
|
|
|
|
'''
|
|
with pytest.raises(
|
|
ValueError,
|
|
match='must decode to 32 bytes',
|
|
):
|
|
converter(key)
|
|
|
|
|
|
def test_proto_key_delegates(
|
|
tunnelled: TunnelledAddress,
|
|
overlay: TCPAddress,
|
|
):
|
|
'''
|
|
A tunnel has no transport of its own, so every table lookup
|
|
must see the *overlay's* proto-key.
|
|
|
|
'''
|
|
assert tunnelled.proto_key == overlay.proto_key == 'tcp'
|
|
|
|
|
|
def test_unwrap_is_identical_to_overlay(
|
|
tunnelled: TunnelledAddress,
|
|
overlay: TCPAddress,
|
|
):
|
|
'''
|
|
The whole point: nothing new crosses the wire, so a peer
|
|
never has to understand tunnels.
|
|
|
|
'''
|
|
assert tunnelled.unwrap() == overlay.unwrap()
|
|
|
|
# and it must survive msgpack as-is
|
|
enc: bytes = msgspec.msgpack.encode(tunnelled.unwrap())
|
|
assert msgspec.msgpack.decode(enc) == list(overlay.unwrap())
|
|
|
|
|
|
def test_unwrap_roundtrips_back_to_plain_overlay(
|
|
tunnelled: TunnelledAddress,
|
|
overlay: TCPAddress,
|
|
):
|
|
'''
|
|
`wrap_address()` on a tunnelled addr's unwrapped form yields
|
|
the *plain* overlay type — the tunnel is simply absent, which
|
|
is correct: it was never on the wire.
|
|
|
|
'''
|
|
rewrapped = wrap_address(tunnelled.unwrap())
|
|
assert type(rewrapped) is TCPAddress
|
|
assert rewrapped == overlay
|
|
assert not isinstance(rewrapped, TunnelledAddress)
|
|
|
|
|
|
def test_bindspace_and_validity_delegate(
|
|
tunnelled: TunnelledAddress,
|
|
overlay: TCPAddress,
|
|
):
|
|
assert tunnelled.bindspace == overlay.bindspace
|
|
assert tunnelled.is_valid == overlay.is_valid
|
|
|
|
|
|
def test_is_wrapped_addr_accepts_tunnelled(
|
|
tunnelled: TunnelledAddress,
|
|
overlay: TCPAddress,
|
|
):
|
|
'''
|
|
`TunnelledAddress` is deliberately absent from
|
|
`_address_types`, so `is_wrapped_addr()` needs its own
|
|
clause.
|
|
|
|
'''
|
|
assert is_wrapped_addr(overlay)
|
|
assert is_wrapped_addr(tunnelled)
|
|
# the unwrapped form is NOT a wrapped addr
|
|
assert not is_wrapped_addr(tunnelled.unwrap())
|
|
|
|
|
|
def test_namespace_comes_from_the_tunnel(
|
|
overlay: TCPAddress,
|
|
):
|
|
'''
|
|
First real consumer of `Address.namespace`, spec'd in the
|
|
protocol since day one and implemented by no backend.
|
|
|
|
'''
|
|
# XXX, "no backend implements it" is literal — the member
|
|
# isn't even declared, so this is `AttributeError` not `None`.
|
|
# This assert is the guard: when a backend finally declares
|
|
# `.namespace`, it fails and the `getattr()` fallback in
|
|
# `TunnelledAddress.namespace` can go.
|
|
assert not hasattr(overlay, 'namespace')
|
|
|
|
no_ns = TunnelledAddress(
|
|
overlay=overlay,
|
|
tunnel=WGTunnelSpec(peer_pubkey=_PUBKEY),
|
|
)
|
|
assert no_ns.namespace is None
|
|
|
|
in_ns = TunnelledAddress(
|
|
overlay=overlay,
|
|
tunnel=WGTunnelSpec(peer_pubkey=_PUBKEY, netns='wg-test'),
|
|
)
|
|
assert in_ns.namespace == ('netns', 'wg-test')
|
|
|
|
|
|
def test_strip_tunnels(
|
|
tunnelled: TunnelledAddress,
|
|
overlay: TCPAddress,
|
|
spec: WGTunnelSpec,
|
|
):
|
|
# idempotent on a plain addr
|
|
assert strip_tunnels(overlay) is overlay
|
|
# peels one
|
|
assert strip_tunnels(tunnelled) is overlay
|
|
# and collapses a nested stack in one call
|
|
nested = TunnelledAddress(overlay=tunnelled, tunnel=spec)
|
|
assert strip_tunnels(nested) is overlay
|
|
|
|
|
|
def test_tunnels_of_is_outermost_first(
|
|
tunnelled: TunnelledAddress,
|
|
overlay: TCPAddress,
|
|
):
|
|
assert tunnels_of(overlay) == ()
|
|
assert tunnels_of(tunnelled) == (tunnelled.tunnel,)
|
|
|
|
inner_spec = WGTunnelSpec(peer_pubkey=_PUBKEY, iface='wg1')
|
|
nested = TunnelledAddress(
|
|
overlay=tunnelled,
|
|
tunnel=inner_spec,
|
|
)
|
|
assert tunnels_of(nested) == (inner_spec, tunnelled.tunnel)
|
|
|
|
|
|
def test_frozen(
|
|
tunnelled: TunnelledAddress,
|
|
):
|
|
with pytest.raises(AttributeError):
|
|
tunnelled.overlay = TCPAddress('127.0.0.1', 1)
|