218 lines
5.2 KiB
Python
218 lines
5.2 KiB
Python
'''
|
|
Read-only WireGuard netlink inspection tests.
|
|
|
|
'''
|
|
from __future__ import annotations
|
|
|
|
import threading
|
|
from typing import (
|
|
Any,
|
|
NoReturn,
|
|
)
|
|
|
|
import pytest
|
|
import trio
|
|
|
|
from tractor.discovery import (
|
|
read_wg_peers,
|
|
read_wg_pubkey,
|
|
)
|
|
|
|
pyroute2: Any = pytest.importorskip('pyroute2')
|
|
|
|
|
|
_PUBKEY: str = 'g3x7z0AdV1rM6UQU22CC7IL3/ivn4DzrE7ikDhCZ/Dc='
|
|
_PEER_1: str = '7PClzcj8o1yAjyPJb0zL2Gt0s2J7yZ6c0JXYqNBGr0E='
|
|
_PEER_2: str = 'H7bJbl1bpY7VzDlB5wI3KjA7JsiYoMWGDJd8dYgc5iw='
|
|
|
|
|
|
class Attrs:
|
|
'''
|
|
Minimal pyroute2 netlink-attribute message fake.
|
|
|
|
'''
|
|
def __init__(
|
|
self,
|
|
**attrs: Any,
|
|
) -> None:
|
|
'''
|
|
Store attributes for `.get_attr()` lookups.
|
|
|
|
'''
|
|
self._attrs: dict[str, Any] = attrs
|
|
|
|
def get_attr(
|
|
self,
|
|
name: str,
|
|
) -> Any:
|
|
'''
|
|
Return the named fake netlink attribute.
|
|
|
|
'''
|
|
return self._attrs.get(name)
|
|
|
|
|
|
def test_read_wg_keys_in_worker_thread(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
'''
|
|
Pyroute2's synchronous `WireGuard` API owns a private asyncio
|
|
loop. Running it in the Trio thread would either block Trio or
|
|
introduce that foreign loop into the actor runtime.
|
|
|
|
Replace `pyroute2.WireGuard` with a fake which records thread,
|
|
iface, netns and close state. Return a multipart dump containing
|
|
duplicate peers, then prove both public helpers execute
|
|
off-thread, preserve named-netns selection, validate keys,
|
|
deduplicate peers in kernel order and close every netlink client.
|
|
|
|
'''
|
|
trio_thread: int = threading.get_ident()
|
|
|
|
class FakeWireGuard:
|
|
'''
|
|
Record each read-only `pyroute2.WireGuard` interaction.
|
|
|
|
'''
|
|
def __init__(
|
|
self,
|
|
*,
|
|
netns: str|None,
|
|
flags: int,
|
|
) -> None:
|
|
'''
|
|
Record namespace selection without opening netlink.
|
|
|
|
'''
|
|
self.netns = netns
|
|
self.flags = flags
|
|
self.closed = False
|
|
self.thread_id: int|None = None
|
|
self.iface: str|None = None
|
|
instances.append(self)
|
|
|
|
def info(
|
|
self,
|
|
iface: str,
|
|
) -> tuple[Attrs, Attrs]:
|
|
'''
|
|
Return a multipart WireGuard device dump.
|
|
|
|
'''
|
|
self.thread_id = threading.get_ident()
|
|
self.iface = iface
|
|
peer_1: Attrs = Attrs(
|
|
WGPEER_A_PUBLIC_KEY=_PEER_1.encode(),
|
|
)
|
|
peer_2: Attrs = Attrs(
|
|
WGPEER_A_PUBLIC_KEY=_PEER_2.encode(),
|
|
)
|
|
return (
|
|
Attrs(
|
|
WGDEVICE_A_PUBLIC_KEY=_PUBKEY.encode(),
|
|
WGDEVICE_A_PEERS=[peer_1],
|
|
),
|
|
Attrs(
|
|
WGDEVICE_A_PUBLIC_KEY=_PUBKEY.encode(),
|
|
WGDEVICE_A_PEERS=[peer_2, peer_1],
|
|
),
|
|
)
|
|
|
|
def close(self) -> None:
|
|
'''
|
|
Record netlink-client cleanup.
|
|
|
|
'''
|
|
self.closed = True
|
|
|
|
instances: list[FakeWireGuard] = []
|
|
monkeypatch.setattr(
|
|
pyroute2,
|
|
'WireGuard',
|
|
FakeWireGuard,
|
|
)
|
|
|
|
async def main() -> None:
|
|
'''
|
|
Read both key views from Trio's run thread.
|
|
|
|
'''
|
|
assert await read_wg_pubkey(
|
|
iface='wg-test',
|
|
netns='actor-net',
|
|
) == _PUBKEY
|
|
assert await read_wg_peers(
|
|
iface='wg-test',
|
|
netns='actor-net',
|
|
) == (_PEER_1, _PEER_2)
|
|
|
|
trio.run(main)
|
|
|
|
assert len(instances) == 2
|
|
instance: FakeWireGuard
|
|
for instance in instances:
|
|
assert instance.netns == 'actor-net'
|
|
assert instance.flags == 0
|
|
assert instance.iface == 'wg-test'
|
|
assert instance.thread_id != trio_thread
|
|
assert instance.closed
|
|
|
|
|
|
def test_wg_client_closes_when_read_fails(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
'''
|
|
A failed netlink read must not leak pyroute2's socket or private
|
|
event loop. Raise from the fake `.info()` call and prove the same
|
|
error reaches the Trio caller only after `.close()` runs.
|
|
|
|
'''
|
|
class FakeWireGuard:
|
|
'''
|
|
Raise during device inspection and record cleanup.
|
|
|
|
'''
|
|
def __init__(
|
|
self,
|
|
*,
|
|
netns: str|None,
|
|
flags: int,
|
|
) -> None:
|
|
'''
|
|
Publish this fake instance for the cleanup assertion.
|
|
|
|
'''
|
|
nonlocal instance
|
|
self.closed = False
|
|
instance = self
|
|
|
|
def info(self, iface: str) -> NoReturn:
|
|
'''
|
|
Simulate a failing netlink device read.
|
|
|
|
'''
|
|
raise OSError('netlink read failed')
|
|
|
|
def close(self) -> None:
|
|
'''
|
|
Record cleanup after the failed read.
|
|
|
|
'''
|
|
self.closed = True
|
|
|
|
instance: FakeWireGuard|None = None
|
|
monkeypatch.setattr(
|
|
pyroute2,
|
|
'WireGuard',
|
|
FakeWireGuard,
|
|
)
|
|
|
|
with pytest.raises(
|
|
OSError,
|
|
match='netlink read failed',
|
|
):
|
|
trio.run(read_wg_pubkey)
|
|
|
|
assert instance is not None
|
|
assert instance.closed
|