Compare commits
No commits in common. "3f675323fa659004d284b6ee7382472595de8992" and "d9a6e2e9b4213bb0900b99deda851cb2eaaa2b1b" have entirely different histories.
3f675323fa
...
d9a6e2e9b4
|
|
@ -1,69 +0,0 @@
|
||||||
---
|
|
||||||
model: gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
session: intercepted-claude-7b9c97c4-fff7-4ac4-97fb-35720453308e
|
|
||||||
timestamp: 2026-08-18T02:17:29Z
|
|
||||||
git_ref: d9a6e2e9
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260818T021729Z_d9a6e2e9_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
> Intercept Claude session
|
|
||||||
> `7b9c97c4-fff7-4ac4-97fb-35720453308e`, pick up where it
|
|
||||||
> stopped in its open worktree, finish the tunnelled-address change
|
|
||||||
> and commit plan, and prepare any outstanding context for another
|
|
||||||
> provider.
|
|
||||||
|
|
||||||
The recovered final prompt specifically called out the `uds` versus
|
|
||||||
`unix` boundary decision, absent concrete `Address.namespace`
|
|
||||||
implementations, the corrected namespace test, and the passing
|
|
||||||
focused and wider suites.
|
|
||||||
|
|
||||||
During review, the human further required that tunnelled-maddr work
|
|
||||||
delegate to `multiaddr`'s encapsulation APIs, challenged the premature
|
|
||||||
transport-shaped listener hooks and directed their removal, corrected
|
|
||||||
the long-term bearer provisioning model, and confirmed the intended
|
|
||||||
split between discovery metadata and bindspace lifecycle code.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Recovered the transcript and matched it to `wkts/addr_unpacking`,
|
|
||||||
audited the staged implementation, and completed the interrupted
|
|
||||||
verification and commit-plan work. The audit removed premature
|
|
||||||
transport-shaped listener hooks, widened the namespace identifier
|
|
||||||
type, updated stale import documentation, and removed an
|
|
||||||
invalid-escape warning from the maddr diagram. It also preserved the
|
|
||||||
layer-C design where tractor provisions the kernel-owned tunnel
|
|
||||||
bearer without treating it as a message transport.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tractor/discovery/_tunnel.py` - tunnel specs, address wrapper, and
|
|
||||||
peeling helpers.
|
|
||||||
- `tractor/discovery/_addr.py` - wrapped-address recognition and
|
|
||||||
namespace typing.
|
|
||||||
- `tractor/discovery/__init__.py` - public tunnel API exports.
|
|
||||||
- `tests/discovery/test_tunnelled_addr.py` - delegation and boundary
|
|
||||||
regression coverage.
|
|
||||||
- `ai/tpt-backends/03_wg_tunnel_bindspace.md` - distinguish
|
|
||||||
tractor-owned bindspace provisioning from kernel socket ownership.
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
Substantial human-directed editing occurred over several review turns:
|
|
||||||
|
|
||||||
- required use of `multiaddr`'s `.encapsulate()`/`.decapsulate()`
|
|
||||||
family rather than a hand-rolled tunnel peeler
|
|
||||||
- rejected the premature `start_listener()`/`close_listener()` hooks
|
|
||||||
and directed their removal from this foundational change
|
|
||||||
- corrected the documentation so tractor retains ownership of future
|
|
||||||
bindspace provisioning while the kernel owns the bearer socket
|
|
||||||
- reviewed and accepted the placement of declarative tunnel metadata
|
|
||||||
under `tractor.discovery`, with lifecycle code kept separate
|
|
||||||
|
|
||||||
The final source lines were applied through the coding agents, but
|
|
||||||
these design corrections and deletion decisions came from the human
|
|
||||||
review and materially shaped the patch.
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
---
|
|
||||||
model: gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
timestamp: 2026-08-18T02:17:29Z
|
|
||||||
git_ref: d9a6e2e9
|
|
||||||
diff_cmd: git diff HEAD~1..HEAD
|
|
||||||
---
|
|
||||||
|
|
||||||
# Raw output - tunnelled-address handoff completion
|
|
||||||
|
|
||||||
Recovered Claude Code session
|
|
||||||
`7b9c97c4-fff7-4ac4-97fb-35720453308e` and continued its
|
|
||||||
interrupted `wkts/addr_unpacking` changes.
|
|
||||||
|
|
||||||
## Generated code
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/discovery/_tunnel.py`
|
|
||||||
|
|
||||||
Added frozen `WGTunnelSpec` and `TunnelledAddress` structs. The
|
|
||||||
wrapper delegates transport identity, validity, bindspace, and wire
|
|
||||||
serialization to its overlay while retaining tunnel metadata locally.
|
|
||||||
Added pure helpers to peel nested wrappers and enumerate their tunnel
|
|
||||||
specs. The module documents why wrappers must be peeled before
|
|
||||||
`Endpoint` selects the overlay transport backend.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/discovery/_addr.py`
|
|
||||||
|
|
||||||
Extended `is_wrapped_addr()` to recognize `TunnelledAddress` without
|
|
||||||
registering tunnels as message transports, and widened the namespace
|
|
||||||
identifier type to cover named network namespaces.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/discovery/__init__.py`
|
|
||||||
|
|
||||||
Exported the tunnel address API from `tractor.discovery` and updated
|
|
||||||
the eager-import documentation.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tests/discovery/test_tunnelled_addr.py`
|
|
||||||
|
|
||||||
Added focused coverage for delegation, serialization, rewrapping,
|
|
||||||
namespace fallback, nested peeling order, and frozen structs.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- ai/tpt-backends/03_wg_tunnel_bindspace.md`
|
|
||||||
|
|
||||||
Clarified that tractor owns the eventual bindspace lifecycle and may
|
|
||||||
provision the WireGuard iface, routes, and kernel UDP listener through
|
|
||||||
netlink/`pyroute2`. Kernel socket ownership does not make the bearer an
|
|
||||||
application `MsgTransport` endpoint.
|
|
||||||
|
|
||||||
## Verification
|
|
||||||
|
|
||||||
Focused tests:
|
|
||||||
|
|
||||||
```text
|
|
||||||
9 passed in 0.03s
|
|
||||||
```
|
|
||||||
|
|
||||||
Discovery and IPC suites:
|
|
||||||
|
|
||||||
```text
|
|
||||||
67 passed, 2 xpassed
|
|
||||||
```
|
|
||||||
|
|
||||||
The audit removed premature module-level listener hooks. Runtime
|
|
||||||
integration must peel the wrapper explicitly at bind and dial
|
|
||||||
boundaries rather than make `._tunnel` impersonate a transport
|
|
||||||
backend.
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
---
|
|
||||||
model: gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
session: tractor-addr-unpacking-followup
|
|
||||||
timestamp: 2026-08-18T07:50:31Z
|
|
||||||
git_ref: dd02c7c0
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260818T075031Z_dd02c7c0_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
The human requested the complete tunnelled-maddr parsing/composition
|
|
||||||
slice as an unattended batch, with every numbered requirement finished
|
|
||||||
and atomic commit plans prepared at the end. Existing human decisions
|
|
||||||
required native `multiaddr` encapsulation APIs, no hand-written peeler,
|
|
||||||
and preservation of tractor's future bindspace lifecycle ownership.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Implemented strict WG key codecs and native single/nested tunnel maddr
|
|
||||||
parsing and composition, integrated them into discovery APIs, migrated
|
|
||||||
the multihost example off its duplicate parser, corrected package
|
|
||||||
dependency metadata, and added focused and end-to-end parser
|
|
||||||
regressions. Verified the complete tractor suite and built both package
|
|
||||||
artifacts.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tractor/discovery/` - WG codecs, parser/composer, wrapper typing,
|
|
||||||
public exports, and discovery dispatch.
|
|
||||||
- `tests/discovery/` - key, grammar, nesting, round-trip, and public
|
|
||||||
boundary regressions.
|
|
||||||
- `examples/multihost/wg_lan/` - production parser migration and
|
|
||||||
updated usage documentation.
|
|
||||||
- `pyproject.toml`, `uv.lock` - reproducible WG codec and multibase
|
|
||||||
dependencies for checkout and package installs.
|
|
||||||
- `ai/tpt-backends/03_wg_tunnel_bindspace.md` - current layer-A state
|
|
||||||
and future bindspace ownership.
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
The human selected the five-step scope and batch execution model,
|
|
||||||
required delegation to `multiaddr`'s encapsulation APIs, rejected
|
|
||||||
transport-shaped listener placeholders in the prerequisite commit, and
|
|
||||||
clarified that tractor will eventually provision the kernel-owned
|
|
||||||
bearer through its bindspace layer. The agent implemented and tested
|
|
||||||
those decisions; no direct manual source edits were observed during
|
|
||||||
this batch.
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
---
|
|
||||||
model: gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
timestamp: 2026-08-18T07:50:31Z
|
|
||||||
git_ref: dd02c7c0
|
|
||||||
diff_cmd: git diff HEAD~1..HEAD
|
|
||||||
---
|
|
||||||
|
|
||||||
# Raw output - native WireGuard maddr integration
|
|
||||||
|
|
||||||
The human requested completion of the five-step tunnelled-maddr slice:
|
|
||||||
port the proven WireGuard parser, delegate to `py-multiaddr`'s native
|
|
||||||
tunnel APIs, integrate public parse and composition entry points, add
|
|
||||||
regressions, and return atomic commit plans after completing the batch.
|
|
||||||
|
|
||||||
## Generated code
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/discovery/_tunnel.py tractor/discovery/_multiaddr.py tractor/discovery/_addr.py tractor/discovery/__init__.py`
|
|
||||||
|
|
||||||
Added strict WireGuard standard-base64/multibase key codecs and native
|
|
||||||
WG maddr parsing/composition. Nested stacks peel the last `/wg/`
|
|
||||||
repeatedly with `.decapsulate_code()`, isolate segments through
|
|
||||||
`.split()`/`.join()`, and compose recursively with `.encapsulate()`.
|
|
||||||
Public discovery parsing, wrapping, endpoint-table parsing, and maddr
|
|
||||||
composition now preserve `TunnelledAddress` metadata locally.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- pyproject.toml uv.lock`
|
|
||||||
|
|
||||||
Declared `py-multibase` directly and pinned the unreleased
|
|
||||||
py-multiaddr#108 WG codec revision in distribution metadata. Enabled
|
|
||||||
Hatch direct-reference metadata so editable, wheel, and sdist builds
|
|
||||||
use the same dependency contract.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tests/discovery/test_multiaddr.py tests/discovery/test_tunnelled_addr.py`
|
|
||||||
|
|
||||||
Added key-codec, single/nested WG round-trip, malformed key, invalid
|
|
||||||
bearer/overlay, missing codec, missing bearer, `wrap_address()`, and
|
|
||||||
`parse_endpoints()` coverage while retaining plain TCP/UDS regressions.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- examples/multihost/wg_lan ai/tpt-backends/03_wg_tunnel_bindspace.md`
|
|
||||||
|
|
||||||
Removed the duplicate example parser, migrated callers to production
|
|
||||||
`TunnelledAddress`, retained only explicit `wg(8)` verification, and
|
|
||||||
updated dependency and future bindspace ownership documentation.
|
|
||||||
|
|
||||||
## Verification
|
|
||||||
|
|
||||||
- focused discovery files: `45 passed`
|
|
||||||
- discovery and IPC suites: `81 passed, 2 xpassed`
|
|
||||||
- full suite: `443 passed, 9 skipped, 7 xfailed, 4 xpassed`
|
|
||||||
- collection: `462 tests`
|
|
||||||
- Ruff: clean
|
|
||||||
- sdist and wheel builds: successful
|
|
||||||
|
|
||||||
## Review corrections
|
|
||||||
|
|
||||||
A review caught that `[tool.uv.sources]` alone would not constrain
|
|
||||||
built-package consumers to a WG-capable py-multiaddr revision. The
|
|
||||||
dependency was moved into PEP 621 metadata and the parser now converts
|
|
||||||
missing-codec failures into an actionable tractor error without
|
|
||||||
misclassifying Unix paths containing a `wg` directory.
|
|
||||||
|
|
@ -123,7 +123,7 @@ that the key decodes to exactly 32 bytes, so a truncated key is a
|
||||||
```
|
```
|
||||||
/ip4/192.168.1.50/udp/51820/wg/u<A_pub>/ip4/10.0.11.1/tcp/1616
|
/ip4/192.168.1.50/udp/51820/wg/u<A_pub>/ip4/10.0.11.1/tcp/1616
|
||||||
\_______ bearer __________/\__ key __/\______ overlay ______/
|
\_______ bearer __________/\__ key __/\______ overlay ______/
|
||||||
underlay, wg `ListenPort` the `MsgTransport` bind
|
underlay, wg `ListenPort` the ONLY part we bind
|
||||||
```
|
```
|
||||||
|
|
||||||
The `/wg/` segment is **infix, not suffix** — the segments
|
The `/wg/` segment is **infix, not suffix** — the segments
|
||||||
|
|
@ -150,11 +150,11 @@ Observed protocol-name lists, for writing the `match`:
|
||||||
- so the three parts have **three different owners**, and only the
|
- so the three parts have **three different owners**, and only the
|
||||||
third is an `Endpoint`:
|
third is an `Endpoint`:
|
||||||
|
|
||||||
| part | socket owner / provisioner | runtime role |
|
| part | bound by | in the runtime? |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| bearer | kernel-owned; externally provisioned in layer A, tractor bindspace-provisioned in layer C | control-plane metadata, never an `Endpoint` |
|
| bearer | kernel, via `wg-quick`/`pyroute2` | no |
|
||||||
| `/wg/u<key>` | nothing — it's an identity | parsed and explicitly verified |
|
| `/wg/u<key>` | nothing — it's an identity | no, verified out-of-band |
|
||||||
| overlay | `tractor`'s `IPCServer` | application `MsgTransport`, as `.overlay` |
|
| overlay | `tractor`'s `IPCServer` | **yes**, as `.overlay` |
|
||||||
|
|
||||||
This owner-split is the real axis of the design, *not* whether
|
This owner-split is the real axis of the design, *not* whether
|
||||||
the maddr stack is "composed" (it is).
|
the maddr stack is "composed" (it is).
|
||||||
|
|
@ -319,14 +319,6 @@ data-structure which can easily be passed to nested `@acm`s
|
||||||
which consecutively setup nested net bindspaces for binding the
|
which consecutively setup nested net bindspaces for binding the
|
||||||
endpoint addrs"*.
|
endpoint addrs"*.
|
||||||
|
|
||||||
Layer C is where tractor takes ownership of bindspace orchestration.
|
|
||||||
For a fully bootstrapped deployment it may create the netns and wg
|
|
||||||
iface, configure peers/routes, and ask the kernel to establish the
|
|
||||||
bearer's UDP `ListenPort` through netlink/`pyroute2`. "Kernel-owned"
|
|
||||||
describes the data-plane socket, not who provisions it: tractor owns
|
|
||||||
the lifecycle while `Endpoint`/`MsgTransport` remain responsible only
|
|
||||||
for the overlay application socket.
|
|
||||||
|
|
||||||
### 5.1 the composition
|
### 5.1 the composition
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
|
|
||||||
|
|
@ -48,10 +48,7 @@ dependencies = [
|
||||||
# typed IPC msging
|
# typed IPC msging
|
||||||
"msgspec>=0.20.0",
|
"msgspec>=0.20.0",
|
||||||
"bidict>=0.23.1",
|
"bidict>=0.23.1",
|
||||||
# unreleased `/wg/` codec from py-multiaddr#108
|
"multiaddr>=0.2.0",
|
||||||
"multiaddr @ git+https://github.com/multiformats/py-multiaddr.git@f86519daaa21699023d0037c58cdff600313dd09",
|
|
||||||
# encode/decode `wg` pubkeys carried by multiaddrs
|
|
||||||
"py-multibase>=2.0.0,<3",
|
|
||||||
"platformdirs>=4.4.0",
|
"platformdirs>=4.4.0",
|
||||||
# per-actor `argv[0]` proc-title for OS-level diag tools
|
# per-actor `argv[0]` proc-title for OS-level diag tools
|
||||||
# (`ps`, `top`, `psutil`-backed tooling like `acli.pytree`).
|
# (`ps`, `top`, `psutil`-backed tooling like `acli.pytree`).
|
||||||
|
|
@ -169,6 +166,17 @@ sync_pause = {requires-python = ">=3.13, <3.14"}
|
||||||
# linux kernel networking
|
# linux kernel networking
|
||||||
# 'pyroute2
|
# 'pyroute2
|
||||||
|
|
||||||
|
# XXX TEMP, the `/wg/u<key>` maddr proto is MERGED upstream (in
|
||||||
|
# py-multiaddr#108, 2026-07-28) but is in NO release yet; the
|
||||||
|
# latest `0.2.0` (2026-03-17) predates the merge by ~4 months.
|
||||||
|
# Pinned by `rev` (not `branch`) so CI stays reproducible.
|
||||||
|
#
|
||||||
|
# Drop this pin (and bump the `multiaddr` dep floor above) the
|
||||||
|
# moment a release carries the `wg` codec; the only consumer is
|
||||||
|
# `examples/multihost/wg_lan/`.
|
||||||
|
# |_https://github.com/multiformats/py-multiaddr/pull/108
|
||||||
|
multiaddr = { git = 'https://github.com/multiformats/py-multiaddr.git', rev = 'f86519daaa21699023d0037c58cdff600313dd09' }
|
||||||
|
|
||||||
# ------ tool.uv.sources ------
|
# ------ tool.uv.sources ------
|
||||||
|
|
||||||
[tool.uv]
|
[tool.uv]
|
||||||
|
|
@ -183,9 +191,6 @@ python-preference = 'system'
|
||||||
|
|
||||||
# ------ tool.uv ------
|
# ------ tool.uv ------
|
||||||
|
|
||||||
[tool.hatch.metadata]
|
|
||||||
allow-direct-references = true
|
|
||||||
|
|
||||||
[tool.hatch.build.targets.sdist]
|
[tool.hatch.build.targets.sdist]
|
||||||
include = ["tractor"]
|
include = ["tractor"]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,6 @@ from types import SimpleNamespace
|
||||||
import pytest
|
import pytest
|
||||||
from multiaddr import Multiaddr
|
from multiaddr import Multiaddr
|
||||||
|
|
||||||
from tractor.discovery import (
|
|
||||||
TunnelledAddress,
|
|
||||||
WGTunnelSpec,
|
|
||||||
mb_pubkey,
|
|
||||||
mk_wg_maddr,
|
|
||||||
parse_wg_maddr,
|
|
||||||
tunnels_of,
|
|
||||||
)
|
|
||||||
from tractor.ipc._tcp import TCPAddress
|
from tractor.ipc._tcp import TCPAddress
|
||||||
from tractor.ipc._uds import UDSAddress
|
from tractor.ipc._uds import UDSAddress
|
||||||
from tractor.discovery._multiaddr import (
|
from tractor.discovery._multiaddr import (
|
||||||
|
|
@ -30,19 +22,6 @@ from tractor.discovery._multiaddr import (
|
||||||
from tractor.discovery._addr import wrap_address
|
from tractor.discovery._addr import wrap_address
|
||||||
|
|
||||||
|
|
||||||
_WG_PUBKEY: str = (
|
|
||||||
'g3x7z0AdV1rM6UQU22CC7IL3/ivn4DzrE7ikDhCZ/Dc='
|
|
||||||
)
|
|
||||||
_WG_PUBKEY_2: str = (
|
|
||||||
'AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8='
|
|
||||||
)
|
|
||||||
_WG_MADDR: str = (
|
|
||||||
f'/ip4/192.168.1.50/udp/51820'
|
|
||||||
f'/wg/{mb_pubkey(_WG_PUBKEY)}'
|
|
||||||
f'/ip4/10.0.11.1/tcp/1616'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_tpt_proto_to_maddr_mapping():
|
def test_tpt_proto_to_maddr_mapping():
|
||||||
'''
|
'''
|
||||||
`_tpt_proto_to_maddr` maps all supported `proto_key`
|
`_tpt_proto_to_maddr` maps all supported `proto_key`
|
||||||
|
|
@ -231,181 +210,6 @@ def test_parse_maddr_unsupported():
|
||||||
parse_maddr('/ip4/127.0.0.1/udp/1234')
|
parse_maddr('/ip4/127.0.0.1/udp/1234')
|
||||||
|
|
||||||
|
|
||||||
def test_parse_wg_maddr():
|
|
||||||
'''
|
|
||||||
`parse_maddr()` previously rejected the canonical infix `/wg/`
|
|
||||||
grammar even though `py-multiaddr` parsed it. Feed a bearer,
|
|
||||||
identity, and TCP overlay through both the WG-specific and public
|
|
||||||
parsers, then prove they produce the same local-only tunnel
|
|
||||||
annotation without changing the bindable overlay.
|
|
||||||
|
|
||||||
'''
|
|
||||||
parsed = parse_wg_maddr(_WG_MADDR)
|
|
||||||
|
|
||||||
assert parse_maddr(_WG_MADDR) == parsed
|
|
||||||
assert isinstance(parsed, TunnelledAddress)
|
|
||||||
assert parsed.tunnel == WGTunnelSpec(
|
|
||||||
peer_pubkey=_WG_PUBKEY,
|
|
||||||
bearer=('192.168.1.50', 51820),
|
|
||||||
)
|
|
||||||
assert isinstance(parsed.overlay, TCPAddress)
|
|
||||||
assert parsed.overlay.unwrap() == ('10.0.11.1', 1616)
|
|
||||||
|
|
||||||
|
|
||||||
def test_mk_wg_maddr_roundtrip():
|
|
||||||
'''
|
|
||||||
`mk_maddr()` previously saw only the wrapper's delegated TCP
|
|
||||||
proto-key and silently dropped all tunnel metadata. Parse the
|
|
||||||
canonical maddr, compose it through both public entry points, and
|
|
||||||
prove bearer, key, and overlay survive byte-for-byte.
|
|
||||||
|
|
||||||
'''
|
|
||||||
parsed = parse_wg_maddr(_WG_MADDR)
|
|
||||||
|
|
||||||
assert str(mk_wg_maddr(parsed)) == _WG_MADDR
|
|
||||||
assert str(mk_maddr(parsed)) == _WG_MADDR
|
|
||||||
|
|
||||||
|
|
||||||
def test_nested_wg_maddr_roundtrip():
|
|
||||||
'''
|
|
||||||
A single first-match lookup confuses nested WG keys and bearers.
|
|
||||||
Arrange an IPv4 outer bearer around an IPv6 inner bearer, parse
|
|
||||||
from the last `/wg/` outward, and assert tunnel ordering plus an
|
|
||||||
exact re-composition of the original stack.
|
|
||||||
|
|
||||||
'''
|
|
||||||
nested_maddr: str = (
|
|
||||||
f'/ip4/192.168.1.50/udp/51820'
|
|
||||||
f'/wg/{mb_pubkey(_WG_PUBKEY)}'
|
|
||||||
f'/ip6/2001:db8::2/udp/51821'
|
|
||||||
f'/wg/{mb_pubkey(_WG_PUBKEY_2)}'
|
|
||||||
f'/ip4/10.0.11.1/tcp/1616'
|
|
||||||
)
|
|
||||||
|
|
||||||
parsed = parse_maddr(nested_maddr)
|
|
||||||
specs = tunnels_of(parsed)
|
|
||||||
|
|
||||||
assert len(specs) == 2
|
|
||||||
assert specs[0].peer_pubkey == _WG_PUBKEY
|
|
||||||
assert specs[0].bearer == ('192.168.1.50', 51820)
|
|
||||||
assert specs[1].peer_pubkey == _WG_PUBKEY_2
|
|
||||||
assert specs[1].bearer == ('2001:db8::2', 51821)
|
|
||||||
assert str(mk_maddr(parsed)) == nested_maddr
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
'maddr, match',
|
|
||||||
[
|
|
||||||
pytest.param(
|
|
||||||
(
|
|
||||||
f'/ip4/192.168.1.50/tcp/51820'
|
|
||||||
f'/wg/{mb_pubkey(_WG_PUBKEY)}'
|
|
||||||
f'/ip4/10.0.11.1/tcp/1616'
|
|
||||||
),
|
|
||||||
'Bad `wg` bearer',
|
|
||||||
id='non-udp-bearer',
|
|
||||||
),
|
|
||||||
pytest.param(
|
|
||||||
(
|
|
||||||
f'/ip4/192.168.1.50/udp/51820'
|
|
||||||
f'/wg/{mb_pubkey(_WG_PUBKEY)}'
|
|
||||||
),
|
|
||||||
'no overlay endpoint',
|
|
||||||
id='missing-overlay',
|
|
||||||
),
|
|
||||||
pytest.param(
|
|
||||||
(
|
|
||||||
f'/ip4/192.168.1.50/udp/51820'
|
|
||||||
f'/wg/{mb_pubkey(_WG_PUBKEY)}'
|
|
||||||
f'/ip4/10.0.11.1/udp/1616'
|
|
||||||
),
|
|
||||||
'Unsupported `wg` overlay',
|
|
||||||
id='non-tcp-overlay',
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_parse_wg_maddr_rejects_bad_grammar(
|
|
||||||
maddr: str,
|
|
||||||
match: str,
|
|
||||||
):
|
|
||||||
'''
|
|
||||||
Accepting an invalid bearer or overlay assigns an endpoint to the
|
|
||||||
wrong runtime owner. Exercise parseable but unsupported protocol
|
|
||||||
combinations and prove each fails before constructing a wrapper,
|
|
||||||
with an error identifying the violated WG grammar boundary.
|
|
||||||
|
|
||||||
'''
|
|
||||||
with pytest.raises(ValueError, match=match):
|
|
||||||
parse_maddr(maddr)
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_wg_maddr_rejects_malformed_key():
|
|
||||||
'''
|
|
||||||
A truncated multibase key used to be vulnerable to silent
|
|
||||||
identity corruption in hand-written parsers. Give the upstream
|
|
||||||
`/wg/` codec a short key and prove `Multiaddr()` rejects it
|
|
||||||
before tractor's wrapper parser runs.
|
|
||||||
|
|
||||||
'''
|
|
||||||
maddr: str = (
|
|
||||||
'/ip4/192.168.1.50/udp/51820'
|
|
||||||
'/wg/udG9vIHNob3J0'
|
|
||||||
'/ip4/10.0.11.1/tcp/1616'
|
|
||||||
)
|
|
||||||
with pytest.raises(ValueError):
|
|
||||||
parse_maddr(maddr)
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_wg_maddr_reports_missing_codec(
|
|
||||||
monkeypatch,
|
|
||||||
):
|
|
||||||
'''
|
|
||||||
Released `multiaddr==0.2.0` does not know `/wg/` and emits an
|
|
||||||
opaque unknown-protocol parse error. Simulate that registry and
|
|
||||||
prove an actual WG stack reports the dependency action while a
|
|
||||||
Unix path containing a `wg` directory remains ordinary UDS data.
|
|
||||||
|
|
||||||
'''
|
|
||||||
from multiaddr.exceptions import ProtocolNotFoundError
|
|
||||||
from tractor.discovery import _tunnel
|
|
||||||
|
|
||||||
def no_wg_proto(name: str):
|
|
||||||
raise ProtocolNotFoundError(name)
|
|
||||||
|
|
||||||
monkeypatch.setattr(
|
|
||||||
_tunnel,
|
|
||||||
'protocol_with_name',
|
|
||||||
no_wg_proto,
|
|
||||||
)
|
|
||||||
uds = parse_maddr('/unix/tmp/wg/service.sock')
|
|
||||||
assert isinstance(uds, UDSAddress)
|
|
||||||
|
|
||||||
with pytest.raises(
|
|
||||||
RuntimeError,
|
|
||||||
match='py-multiaddr#108',
|
|
||||||
):
|
|
||||||
parse_maddr(_WG_MADDR)
|
|
||||||
|
|
||||||
|
|
||||||
def test_mk_wg_maddr_requires_bearer():
|
|
||||||
'''
|
|
||||||
A key-only tunnel spec relies on local configuration and cannot
|
|
||||||
be reconstructed as the canonical bearer-first maddr. Build that
|
|
||||||
incomplete annotation and prove composition raises instead of
|
|
||||||
emitting a misleading overlay-only address.
|
|
||||||
|
|
||||||
'''
|
|
||||||
addr = TunnelledAddress(
|
|
||||||
overlay=TCPAddress('10.0.11.1', 1616),
|
|
||||||
tunnel=WGTunnelSpec(peer_pubkey=_WG_PUBKEY),
|
|
||||||
)
|
|
||||||
with pytest.raises(
|
|
||||||
ValueError,
|
|
||||||
match='without a bearer',
|
|
||||||
):
|
|
||||||
mk_maddr(addr)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
'addr',
|
'addr',
|
||||||
[
|
[
|
||||||
|
|
@ -448,21 +252,6 @@ def test_wrap_address_maddr_str():
|
||||||
assert result.unwrap() == ('127.0.0.1', 9999)
|
assert result.unwrap() == ('127.0.0.1', 9999)
|
||||||
|
|
||||||
|
|
||||||
def test_wrap_address_wg_maddr_str():
|
|
||||||
'''
|
|
||||||
`wrap_address()` delegates slash-prefixed strings to
|
|
||||||
`parse_maddr()`. Pass a canonical WG maddr through that public
|
|
||||||
boundary and prove it preserves the tunnel annotation rather than
|
|
||||||
rejecting the protocol stack or returning only its TCP overlay.
|
|
||||||
|
|
||||||
'''
|
|
||||||
result = wrap_address(_WG_MADDR)
|
|
||||||
|
|
||||||
assert isinstance(result, TunnelledAddress)
|
|
||||||
assert result.tunnel.peer_pubkey == _WG_PUBKEY
|
|
||||||
assert result.overlay.unwrap() == ('10.0.11.1', 1616)
|
|
||||||
|
|
||||||
|
|
||||||
# ------ parse_endpoints() tests ------
|
# ------ parse_endpoints() tests ------
|
||||||
|
|
||||||
def test_parse_endpoints_tcp_only():
|
def test_parse_endpoints_tcp_only():
|
||||||
|
|
@ -513,30 +302,6 @@ def test_parse_endpoints_mixed_tpts():
|
||||||
assert str(filedir) == '/tmp/tractor'
|
assert str(filedir) == '/tmp/tractor'
|
||||||
|
|
||||||
|
|
||||||
def test_parse_endpoints_wg_maddr():
|
|
||||||
'''
|
|
||||||
Service endpoint tables previously rejected WG protocol stacks.
|
|
||||||
Put a tunnelled maddr beside a plain TCP address and prove
|
|
||||||
`parse_endpoints()` retains input order while delivering the
|
|
||||||
wrapper needed by the future bindspace lifecycle.
|
|
||||||
|
|
||||||
'''
|
|
||||||
table = {
|
|
||||||
'registry': [
|
|
||||||
_WG_MADDR,
|
|
||||||
'/ip4/127.0.0.1/tcp/1616',
|
|
||||||
],
|
|
||||||
}
|
|
||||||
addrs = parse_endpoints(table)['registry']
|
|
||||||
|
|
||||||
assert isinstance(addrs[0], TunnelledAddress)
|
|
||||||
assert addrs[0].tunnel.bearer == (
|
|
||||||
'192.168.1.50',
|
|
||||||
51820,
|
|
||||||
)
|
|
||||||
assert isinstance(addrs[1], TCPAddress)
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_endpoints_unwrapped_tuples():
|
def test_parse_endpoints_unwrapped_tuples():
|
||||||
'''
|
'''
|
||||||
`parse_endpoints()` accept raw `(host, port)` tuples
|
`parse_endpoints()` accept raw `(host, port)` tuples
|
||||||
|
|
|
||||||
|
|
@ -1,234 +0,0 @@
|
||||||
'''
|
|
||||||
`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)
|
|
||||||
|
|
@ -18,10 +18,11 @@
|
||||||
Discovery (protocols) API for automatic addressing
|
Discovery (protocols) API for automatic addressing
|
||||||
and location management of (service) actors.
|
and location management of (service) actors.
|
||||||
|
|
||||||
NOTE: this ``__init__`` only eagerly imports the lightweight
|
NOTE: this ``__init__`` only eagerly imports the
|
||||||
``._multiaddr`` and ``._tunnel`` submodules for public re-exports.
|
``._multiaddr`` submodule (for public re-exports).
|
||||||
Heavier submodules like ``._addr`` and ``._api`` are NOT imported
|
Heavier submodules like ``._addr`` and ``._api``
|
||||||
here to avoid circular imports; use direct module paths for those.
|
are NOT imported here to avoid circular imports;
|
||||||
|
use direct module paths for those.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
from ._multiaddr import (
|
from ._multiaddr import (
|
||||||
|
|
@ -29,14 +30,3 @@ from ._multiaddr import (
|
||||||
parse_maddr as parse_maddr,
|
parse_maddr as parse_maddr,
|
||||||
mk_maddr as mk_maddr,
|
mk_maddr as mk_maddr,
|
||||||
)
|
)
|
||||||
from ._tunnel import (
|
|
||||||
TunnelledAddress as TunnelledAddress,
|
|
||||||
TunnelSpec as TunnelSpec,
|
|
||||||
WGTunnelSpec as WGTunnelSpec,
|
|
||||||
mb_pubkey as mb_pubkey,
|
|
||||||
mk_wg_maddr as mk_wg_maddr,
|
|
||||||
parse_wg_maddr as parse_wg_maddr,
|
|
||||||
strip_tunnels as strip_tunnels,
|
|
||||||
tunnels_of as tunnels_of,
|
|
||||||
wg8_pubkey as wg8_pubkey,
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,6 @@ from ..ipc._tcp import TCPAddress
|
||||||
from ..ipc._uds import UDSAddress
|
from ..ipc._uds import UDSAddress
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ._tunnel import TunnelledAddress
|
|
||||||
from ..runtime._runtime import Actor
|
from ..runtime._runtime import Actor
|
||||||
|
|
||||||
log = get_logger()
|
log = get_logger()
|
||||||
|
|
@ -94,7 +93,7 @@ class Address(Protocol):
|
||||||
|
|
||||||
# TODO, maybe `.netns` is a better name?
|
# TODO, maybe `.netns` is a better name?
|
||||||
@property
|
@property
|
||||||
def namespace(self) -> tuple[str, str|int]|None:
|
def namespace(self) -> tuple[str, int]|None:
|
||||||
'''
|
'''
|
||||||
The if-available, OS-specific "network namespace" key.
|
The if-available, OS-specific "network namespace" key.
|
||||||
|
|
||||||
|
|
@ -193,16 +192,7 @@ def get_address_cls(name: str) -> Type[Address]:
|
||||||
|
|
||||||
|
|
||||||
def is_wrapped_addr(addr: any) -> bool:
|
def is_wrapped_addr(addr: any) -> bool:
|
||||||
# XXX NOTE, a `TunnelledAddress` is genuinely "wrapped" but is
|
return type(addr) in _address_types.values()
|
||||||
# deliberately NOT in `_address_types`: it has no
|
|
||||||
# `MsgTransport` of its own (a tunnel is transparent to
|
|
||||||
# `socket(2)`), so it gets no proto-key entry. See `._tunnel`.
|
|
||||||
from ._tunnel import TunnelledAddress
|
|
||||||
return (
|
|
||||||
type(addr) in _address_types.values()
|
|
||||||
or
|
|
||||||
isinstance(addr, TunnelledAddress)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def mk_uuid() -> str:
|
def mk_uuid() -> str:
|
||||||
|
|
@ -216,8 +206,8 @@ def mk_uuid() -> str:
|
||||||
|
|
||||||
|
|
||||||
def wrap_address(
|
def wrap_address(
|
||||||
addr: UnwrappedAddress|str|Address|TunnelledAddress,
|
addr: UnwrappedAddress|str,
|
||||||
) -> Address|TunnelledAddress:
|
) -> Address:
|
||||||
'''
|
'''
|
||||||
Wrap an `UnwrappedAddress` as an `Address`-type based
|
Wrap an `UnwrappedAddress` as an `Address`-type based
|
||||||
on matching builtin python data-structures which we adhoc
|
on matching builtin python data-structures which we adhoc
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,6 @@ from multiaddr import Multiaddr
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from tractor.discovery._addr import Address
|
from tractor.discovery._addr import Address
|
||||||
from tractor.discovery._tunnel import TunnelledAddress
|
|
||||||
|
|
||||||
# map from tractor-internal `proto_key` identifiers
|
# map from tractor-internal `proto_key` identifiers
|
||||||
# to the standard multiaddr protocol name strings.
|
# to the standard multiaddr protocol name strings.
|
||||||
|
|
@ -49,7 +48,7 @@ _maddr_to_tpt_proto: dict[str, str] = {
|
||||||
|
|
||||||
|
|
||||||
def mk_maddr(
|
def mk_maddr(
|
||||||
addr: 'Address|TunnelledAddress',
|
addr: 'Address',
|
||||||
) -> Multiaddr:
|
) -> Multiaddr:
|
||||||
'''
|
'''
|
||||||
Construct a `Multiaddr` from a tractor `Address` instance,
|
Construct a `Multiaddr` from a tractor `Address` instance,
|
||||||
|
|
@ -57,13 +56,6 @@ def mk_maddr(
|
||||||
multiaddr-spec-compliant protocol path.
|
multiaddr-spec-compliant protocol path.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
from ._tunnel import (
|
|
||||||
TunnelledAddress,
|
|
||||||
mk_wg_maddr,
|
|
||||||
)
|
|
||||||
if isinstance(addr, TunnelledAddress):
|
|
||||||
return mk_wg_maddr(addr)
|
|
||||||
|
|
||||||
proto_key: str = addr.proto_key
|
proto_key: str = addr.proto_key
|
||||||
maddr_proto: str|None = _tpt_proto_to_maddr.get(proto_key)
|
maddr_proto: str|None = _tpt_proto_to_maddr.get(proto_key)
|
||||||
if maddr_proto is None:
|
if maddr_proto is None:
|
||||||
|
|
@ -98,7 +90,7 @@ def mk_maddr(
|
||||||
|
|
||||||
def parse_maddr(
|
def parse_maddr(
|
||||||
maddr_str: str,
|
maddr_str: str,
|
||||||
) -> 'Address|TunnelledAddress':
|
) -> 'Address':
|
||||||
'''
|
'''
|
||||||
Parse a multiaddr string into a tractor `Address`.
|
Parse a multiaddr string into a tractor `Address`.
|
||||||
|
|
||||||
|
|
@ -109,16 +101,7 @@ def parse_maddr(
|
||||||
from tractor.ipc._tcp import TCPAddress
|
from tractor.ipc._tcp import TCPAddress
|
||||||
from tractor.ipc._uds import UDSAddress
|
from tractor.ipc._uds import UDSAddress
|
||||||
|
|
||||||
try:
|
|
||||||
maddr = Multiaddr(maddr_str)
|
maddr = Multiaddr(maddr_str)
|
||||||
except ValueError:
|
|
||||||
# Diagnose an unavailable WG codec after upstream parsing
|
|
||||||
# fails. Pre-checking the raw string would misclassify valid
|
|
||||||
# values such as `/unix/tmp/wg/service.sock`.
|
|
||||||
if '/wg/' in maddr_str:
|
|
||||||
from ._tunnel import _wg_proto_code
|
|
||||||
_wg_proto_code()
|
|
||||||
raise
|
|
||||||
proto_names: list[str] = [
|
proto_names: list[str] = [
|
||||||
p.name for p in maddr.protocols()
|
p.name for p in maddr.protocols()
|
||||||
]
|
]
|
||||||
|
|
@ -141,10 +124,6 @@ def parse_maddr(
|
||||||
filename=sockpath.name,
|
filename=sockpath.name,
|
||||||
)
|
)
|
||||||
|
|
||||||
case _ if 'wg' in proto_names:
|
|
||||||
from ._tunnel import parse_wg_maddr
|
|
||||||
return parse_wg_maddr(maddr)
|
|
||||||
|
|
||||||
case _:
|
case _:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f'Unsupported multiaddr protocol combo: '
|
f'Unsupported multiaddr protocol combo: '
|
||||||
|
|
@ -163,11 +142,11 @@ EndpointsTable = dict[
|
||||||
list[str|tuple], # maddr strs or UnwrappedAddress
|
list[str|tuple], # maddr strs or UnwrappedAddress
|
||||||
]
|
]
|
||||||
|
|
||||||
# output table: actor/service name -> list of wrapped address
|
# output table: actor/service name -> list of wrapped
|
||||||
# declarations ready for bindspace handling.
|
# `Address` instances ready for transport binding.
|
||||||
ParsedEndpoints = dict[
|
ParsedEndpoints = dict[
|
||||||
str, # actor/service name
|
str, # actor/service name
|
||||||
list['Address|TunnelledAddress'],
|
list['Address'],
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -176,7 +155,7 @@ def parse_endpoints(
|
||||||
) -> ParsedEndpoints:
|
) -> ParsedEndpoints:
|
||||||
'''
|
'''
|
||||||
Parse a service-endpoint config table into wrapped
|
Parse a service-endpoint config table into wrapped
|
||||||
address declarations suitable for bindspace handling.
|
`Address` instances suitable for transport binding.
|
||||||
|
|
||||||
Each key is an actor/service name and each value is
|
Each key is an actor/service name and each value is
|
||||||
a list of addresses in any format accepted by
|
a list of addresses in any format accepted by
|
||||||
|
|
@ -188,8 +167,6 @@ def parse_endpoints(
|
||||||
``/uds/`` proto_key)
|
``/uds/`` proto_key)
|
||||||
- raw unwrapped tuples: ``('127.0.0.1', 1616)``
|
- raw unwrapped tuples: ``('127.0.0.1', 1616)``
|
||||||
- pre-wrapped `Address` objects (passed through)
|
- pre-wrapped `Address` objects (passed through)
|
||||||
- `wg` maddrs, returned as `TunnelledAddress` wrappers which
|
|
||||||
must be peeled at the eventual bind/dial boundary
|
|
||||||
|
|
||||||
Returns a new `dict` with the same keys, where each
|
Returns a new `dict` with the same keys, where each
|
||||||
value list contains the corresponding `Address`
|
value list contains the corresponding `Address`
|
||||||
|
|
|
||||||
|
|
@ -1,491 +0,0 @@
|
||||||
# tractor: structured concurrent "actors".
|
|
||||||
# Copyright 2018-eternity Tyler Goodlet.
|
|
||||||
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Affero General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
r'''
|
|
||||||
Tunnelled addresses: an `Address` that rides *inside* a tunnel.
|
|
||||||
|
|
||||||
A tunnel (`wg`, and later plain ip-in-udp, `veth`-in-netns, ..) is
|
|
||||||
**not** a `MsgTransport`. Its data plane is transparent to the
|
|
||||||
application's `socket(2)`, so it never gets its own entry in
|
|
||||||
`._addr._address_types` nor a `MsgpackTransport` impl. Instead it
|
|
||||||
*annotates* an existing L4 addr, and this module carries that
|
|
||||||
annotation beside it.
|
|
||||||
|
|
||||||
That does not mean tractor can never provision the tunnel. Layer A
|
|
||||||
assumes an externally configured iface; a later bindspace lifecycle
|
|
||||||
may create its iface, netns, routes, and kernel-owned UDP listener
|
|
||||||
through netlink/`pyroute2`. The distinction is that this
|
|
||||||
control-plane work does not turn the bearer into an application
|
|
||||||
`Endpoint`.
|
|
||||||
|
|
||||||
Naming follows `py-multiaddr`'s encapsulation model, where earlier
|
|
||||||
maddr segs wrap later ones (`.encapsulate()` appends):
|
|
||||||
|
|
||||||
/ip4/192.168.1.50/udp/51820/wg/u<key>/ip4/10.0.11.1/tcp/1616
|
|
||||||
\_______ bearer __________/\__ key __/\______ overlay ______/
|
|
||||||
|
|
||||||
- **bearer**: the underlay ep the tunnel iface listens on
|
|
||||||
(`wg(8)`'s `ListenPort`). The kernel owns this data-plane socket;
|
|
||||||
tractor may later provision it through a bindspace lifecycle but
|
|
||||||
never treats it as a `MsgTransport` listener.
|
|
||||||
- **overlay**: the ep `tractor` actually binds/dials, i.e. the
|
|
||||||
application IPC endpoint handled by `Endpoint`/`MsgTransport`.
|
|
||||||
|
|
||||||
We avoid `inner`/`outer` deliberately: in a *call* stack "inner"
|
|
||||||
reads as higher-up and later-called, whereas here the
|
|
||||||
encapsulated addr is bound *first* and sits deeper in the maddr.
|
|
||||||
|
|
||||||
XXX XXX READ THIS BEFORE USING XXX XXX
|
|
||||||
--------------------------------------
|
|
||||||
A `TunnelledAddress` **must be unwrapped to `.overlay` before it
|
|
||||||
reaches `Endpoint`**. `Endpoint.start_listener()` resolves its
|
|
||||||
listener fns by `inspect.getmodule(self.addr)`, so a wrapper
|
|
||||||
would resolve to *this* module rather than the transport's and
|
|
||||||
silently fail to find `start_listener()`.
|
|
||||||
|
|
||||||
If a wrapper reaches `Endpoint`, its backend lookup resolves this
|
|
||||||
module instead of the overlay transport module:
|
|
||||||
|
|
||||||
tpt_mod = inspect.getmodule(self.addr)
|
|
||||||
await tpt_mod.start_listener(addr=self.addr)
|
|
||||||
|
|
||||||
This module intentionally does not impersonate that transport API.
|
|
||||||
Unwrap at the parse or bindspace boundary; see `.overlay` and
|
|
||||||
`strip_tunnels()`.
|
|
||||||
|
|
||||||
'''
|
|
||||||
from __future__ import annotations
|
|
||||||
import base64
|
|
||||||
import ipaddress
|
|
||||||
from typing import (
|
|
||||||
ClassVar,
|
|
||||||
TYPE_CHECKING,
|
|
||||||
)
|
|
||||||
|
|
||||||
import msgspec
|
|
||||||
import multibase
|
|
||||||
from multiaddr import Multiaddr
|
|
||||||
from multiaddr.exceptions import ProtocolNotFoundError
|
|
||||||
from multiaddr.protocols import protocol_with_name
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from ._addr import (
|
|
||||||
Address,
|
|
||||||
UnwrappedAddress,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class WGTunnelSpec(
|
|
||||||
msgspec.Struct,
|
|
||||||
frozen=True,
|
|
||||||
):
|
|
||||||
'''
|
|
||||||
The `wg`-specific half of a tunnel annotation.
|
|
||||||
|
|
||||||
Everything here is an *interface-layer* concern owned by
|
|
||||||
`wg(8)`/the kernel. A later tractor bindspace lifecycle may
|
|
||||||
provision it through netlink, but it is never an application
|
|
||||||
`MsgTransport` endpoint.
|
|
||||||
|
|
||||||
'''
|
|
||||||
# tunnel peer pubkey in the std-base64 `wg(8)` form, i.e.
|
|
||||||
# directly comparable to `wg show <if> peers` output
|
|
||||||
peer_pubkey: str
|
|
||||||
|
|
||||||
# the underlay `(ip, udp-port)` the wg iface listens on, i.e.
|
|
||||||
# wg's `ListenPort`. The kernel owns the socket even when a
|
|
||||||
# tractor bindspace lifecycle provisions it. `None` when the
|
|
||||||
# maddr declared only a key (identity) and the bearer is
|
|
||||||
# implied by local cfg.
|
|
||||||
bearer: tuple[str, int]|None = None
|
|
||||||
|
|
||||||
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'
|
|
||||||
|
|
||||||
|
|
||||||
# the tunnel-spec union; grows as new tunnel kinds land
|
|
||||||
# (plain ip-in-udp, `veth`-in-netns, ..)
|
|
||||||
TunnelSpec = WGTunnelSpec
|
|
||||||
|
|
||||||
|
|
||||||
def mb_pubkey(
|
|
||||||
wg8_key: str,
|
|
||||||
) -> str:
|
|
||||||
'''
|
|
||||||
Encode a `wg(8)` public key as multibase base64url.
|
|
||||||
|
|
||||||
WireGuard public keys are exactly 32 bytes. Enforce that here
|
|
||||||
before handing the `u`-prefixed result to `py-multiaddr`'s
|
|
||||||
`/wg/` codec.
|
|
||||||
|
|
||||||
'''
|
|
||||||
raw: bytes = base64.b64decode(
|
|
||||||
wg8_key,
|
|
||||||
validate=True,
|
|
||||||
)
|
|
||||||
if (nbytes := len(raw)) != 32:
|
|
||||||
raise ValueError(
|
|
||||||
f'A `wg` public key must decode to 32 bytes, '
|
|
||||||
f'not {nbytes}!'
|
|
||||||
)
|
|
||||||
|
|
||||||
return multibase.encode(
|
|
||||||
'base64url',
|
|
||||||
raw,
|
|
||||||
).decode('ascii')
|
|
||||||
|
|
||||||
|
|
||||||
def wg8_pubkey(
|
|
||||||
mb_key: str,
|
|
||||||
) -> str:
|
|
||||||
'''
|
|
||||||
Decode a multibase public key to `wg(8)` standard base64.
|
|
||||||
|
|
||||||
'''
|
|
||||||
raw: bytes = multibase.decode(mb_key)
|
|
||||||
if (nbytes := len(raw)) != 32:
|
|
||||||
raise ValueError(
|
|
||||||
f'A `wg` public key must decode to 32 bytes, '
|
|
||||||
f'not {nbytes}!'
|
|
||||||
)
|
|
||||||
|
|
||||||
return base64.b64encode(raw).decode('ascii')
|
|
||||||
|
|
||||||
|
|
||||||
def _wg_proto_code() -> int:
|
|
||||||
'''
|
|
||||||
Deliver the installed `py-multiaddr` `/wg/` protocol code.
|
|
||||||
|
|
||||||
`wg` support is merged upstream but not yet in a release, so
|
|
||||||
fail clearly when tractor was installed without the pinned rev.
|
|
||||||
|
|
||||||
'''
|
|
||||||
try:
|
|
||||||
return protocol_with_name('wg').code
|
|
||||||
except ProtocolNotFoundError as exc:
|
|
||||||
raise RuntimeError(
|
|
||||||
'Installed `py-multiaddr` has no `/wg/` protocol!\n'
|
|
||||||
'Install py-multiaddr#108 or use tractor\'s pinned '
|
|
||||||
'dependency revision.\n'
|
|
||||||
) from exc
|
|
||||||
|
|
||||||
|
|
||||||
class TunnelledAddress(
|
|
||||||
msgspec.Struct,
|
|
||||||
frozen=True,
|
|
||||||
):
|
|
||||||
'''
|
|
||||||
An `Address` annotated with the tunnel it must be reached
|
|
||||||
*through*.
|
|
||||||
|
|
||||||
Address-level properties delegate to `.overlay`, so proto-key
|
|
||||||
guards and `.unwrap()` retain their existing meaning and
|
|
||||||
**nothing new crosses the wire**. Transport boundaries which
|
|
||||||
dispatch on exact type or declaring module must first call
|
|
||||||
`strip_tunnels()`.
|
|
||||||
|
|
||||||
'''
|
|
||||||
overlay: Address|TunnelledAddress
|
|
||||||
tunnel: TunnelSpec
|
|
||||||
|
|
||||||
# ---- delegated, so the runtime can't tell the difference ----
|
|
||||||
|
|
||||||
@property
|
|
||||||
def proto_key(self) -> str:
|
|
||||||
'''
|
|
||||||
The *overlay's* proto-key — a tunnel has no transport of
|
|
||||||
its own.
|
|
||||||
|
|
||||||
NOTE, this is a property whereas `Address.proto_key` is
|
|
||||||
spec'd as a `ClassVar`. That's deliberate: the value is
|
|
||||||
only knowable per-instance here, and this type is never
|
|
||||||
registered in `_address_types`, so no class-level access
|
|
||||||
of it should ever occur.
|
|
||||||
|
|
||||||
'''
|
|
||||||
return self.overlay.proto_key
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_valid(self) -> bool:
|
|
||||||
return self.overlay.is_valid
|
|
||||||
|
|
||||||
@property
|
|
||||||
def bindspace(self) -> str:
|
|
||||||
return self.overlay.bindspace
|
|
||||||
|
|
||||||
def unwrap(self) -> UnwrappedAddress:
|
|
||||||
'''
|
|
||||||
Delegate to `.overlay`, so the tunnel annotation is
|
|
||||||
**not** serialized and no peer needs to understand it.
|
|
||||||
|
|
||||||
'''
|
|
||||||
return self.overlay.unwrap()
|
|
||||||
|
|
||||||
# ---- the tunnel's own contribution ----
|
|
||||||
|
|
||||||
@property
|
|
||||||
def namespace(self) -> tuple[str, str|int]|None:
|
|
||||||
'''
|
|
||||||
The tunnel's netns, when it declares one.
|
|
||||||
|
|
||||||
This is the first real consumer of `Address.namespace`,
|
|
||||||
spec'd in the `Address` protocol since day one and
|
|
||||||
implemented by no backend.
|
|
||||||
|
|
||||||
XXX NOTE, "implemented by no backend" is literal: neither
|
|
||||||
`TCPAddress` nor `UDSAddress` defines `.namespace` at all,
|
|
||||||
so a plain attr access on an overlay raises
|
|
||||||
`AttributeError` rather than yielding `None`. Hence the
|
|
||||||
`getattr()` — drop it once the backends actually declare
|
|
||||||
the member.
|
|
||||||
|
|
||||||
'''
|
|
||||||
if (netns := self.tunnel.netns) is None:
|
|
||||||
return getattr(self.overlay, 'namespace', None)
|
|
||||||
|
|
||||||
return ('netns', netns)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return (
|
|
||||||
f'{type(self).__name__}(\n'
|
|
||||||
f' overlay={self.overlay!r},\n'
|
|
||||||
f' via={self.tunnel.tunnel_key!r} '
|
|
||||||
f'iface={self.tunnel.iface!r},\n'
|
|
||||||
f')'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _wg_bearer(
|
|
||||||
bearer_ma: Multiaddr,
|
|
||||||
source_ma: Multiaddr,
|
|
||||||
) -> tuple[str, int]:
|
|
||||||
'''
|
|
||||||
Parse one kernel-owned `wg` bearer endpoint.
|
|
||||||
|
|
||||||
'''
|
|
||||||
proto_names: list[str] = [
|
|
||||||
proto.name
|
|
||||||
for proto in bearer_ma.protocols()
|
|
||||||
]
|
|
||||||
match proto_names:
|
|
||||||
case [('ip4' | 'ip6') as ip_proto, 'udp']:
|
|
||||||
return (
|
|
||||||
bearer_ma.value_for_protocol(ip_proto),
|
|
||||||
int(bearer_ma.value_for_protocol('udp')),
|
|
||||||
)
|
|
||||||
|
|
||||||
case _:
|
|
||||||
raise ValueError(
|
|
||||||
f'Bad `wg` bearer, expected '
|
|
||||||
f'`/ip4|ip6/<host>/udp/<port>`\n'
|
|
||||||
f'got: {bearer_ma}\n'
|
|
||||||
f'from maddr: {source_ma}\n'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_wg_maddr(
|
|
||||||
maddr: str|Multiaddr,
|
|
||||||
) -> TunnelledAddress:
|
|
||||||
'''
|
|
||||||
Parse a `wg` maddr stack into nested tunnel annotations.
|
|
||||||
|
|
||||||
Pure: every segment operation delegates to `py-multiaddr`.
|
|
||||||
Repeated `.decapsulate_code()` calls peel the last `/wg/`
|
|
||||||
first, while `.split()` and `.join()` isolate that tunnel's
|
|
||||||
bearer without parsing slash-delimited strings ourselves.
|
|
||||||
|
|
||||||
'''
|
|
||||||
ma: Multiaddr = (
|
|
||||||
maddr
|
|
||||||
if isinstance(maddr, Multiaddr)
|
|
||||||
else Multiaddr(maddr)
|
|
||||||
)
|
|
||||||
wg_code: int = _wg_proto_code()
|
|
||||||
segs: list[Multiaddr] = ma.split()
|
|
||||||
proto_names: list[str] = [
|
|
||||||
proto.name
|
|
||||||
for seg in segs
|
|
||||||
for proto in seg.protocols()
|
|
||||||
]
|
|
||||||
if 'wg' not in proto_names:
|
|
||||||
raise ValueError(
|
|
||||||
f'Not a `wg`-tunnelled maddr; no `/wg/` segment!\n'
|
|
||||||
f'maddr: {ma}\n'
|
|
||||||
)
|
|
||||||
|
|
||||||
final_wg_i: int = len(proto_names) - 1
|
|
||||||
final_wg_i -= proto_names[::-1].index('wg')
|
|
||||||
overlay_ma: Multiaddr = Multiaddr.join(
|
|
||||||
*segs[final_wg_i + 1:]
|
|
||||||
)
|
|
||||||
overlay_names: list[str] = [
|
|
||||||
proto.name
|
|
||||||
for proto in overlay_ma.protocols()
|
|
||||||
]
|
|
||||||
match overlay_names:
|
|
||||||
case [('ip4' | 'ip6'), 'tcp']:
|
|
||||||
from ._multiaddr import parse_maddr
|
|
||||||
overlay: Address|TunnelledAddress = parse_maddr(
|
|
||||||
str(overlay_ma)
|
|
||||||
)
|
|
||||||
|
|
||||||
case []:
|
|
||||||
raise ValueError(
|
|
||||||
f'`wg` maddr declares no overlay endpoint!\n'
|
|
||||||
f'Append the endpoint tractor should bind.\n'
|
|
||||||
f'maddr: {ma}\n'
|
|
||||||
)
|
|
||||||
|
|
||||||
case _:
|
|
||||||
raise ValueError(
|
|
||||||
f'Unsupported `wg` overlay protocol combo: '
|
|
||||||
f'{overlay_names!r}\n'
|
|
||||||
f'overlay: {overlay_ma}\n'
|
|
||||||
f'from maddr: {ma}\n'
|
|
||||||
)
|
|
||||||
|
|
||||||
cursor: Multiaddr = ma
|
|
||||||
while any(
|
|
||||||
proto.name == 'wg'
|
|
||||||
for proto in cursor.protocols()
|
|
||||||
):
|
|
||||||
cursor_segs: list[Multiaddr] = cursor.split()
|
|
||||||
cursor_names: list[str] = [
|
|
||||||
proto.name
|
|
||||||
for seg in cursor_segs
|
|
||||||
for proto in seg.protocols()
|
|
||||||
]
|
|
||||||
wg_i: int = len(cursor_names) - 1
|
|
||||||
wg_i -= cursor_names[::-1].index('wg')
|
|
||||||
mb_key: str = cursor_segs[wg_i].value_for_protocol('wg')
|
|
||||||
|
|
||||||
bearer_prefix: Multiaddr = cursor.decapsulate_code(
|
|
||||||
wg_code
|
|
||||||
)
|
|
||||||
prefix_segs: list[Multiaddr] = bearer_prefix.split()
|
|
||||||
prefix_names: list[str] = [
|
|
||||||
proto.name
|
|
||||||
for seg in prefix_segs
|
|
||||||
for proto in seg.protocols()
|
|
||||||
]
|
|
||||||
prior_wg_i: int = (
|
|
||||||
len(prefix_names) - 1
|
|
||||||
- prefix_names[::-1].index('wg')
|
|
||||||
if 'wg' in prefix_names
|
|
||||||
else -1
|
|
||||||
)
|
|
||||||
bearer_ma: Multiaddr = Multiaddr.join(
|
|
||||||
*prefix_segs[prior_wg_i + 1:]
|
|
||||||
)
|
|
||||||
overlay = TunnelledAddress(
|
|
||||||
overlay=overlay,
|
|
||||||
tunnel=WGTunnelSpec(
|
|
||||||
peer_pubkey=wg8_pubkey(mb_key),
|
|
||||||
bearer=_wg_bearer(bearer_ma, ma),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
cursor = bearer_prefix
|
|
||||||
|
|
||||||
return overlay
|
|
||||||
|
|
||||||
|
|
||||||
def mk_wg_maddr(
|
|
||||||
addr: TunnelledAddress,
|
|
||||||
) -> Multiaddr:
|
|
||||||
'''
|
|
||||||
Compose nested tunnel annotations as a canonical `wg` maddr.
|
|
||||||
|
|
||||||
Only the peer key and bearer have maddr representations. Local
|
|
||||||
interface, namespace, and allowed-IP config remains local.
|
|
||||||
|
|
||||||
'''
|
|
||||||
_wg_proto_code()
|
|
||||||
if (bearer := addr.tunnel.bearer) is None:
|
|
||||||
raise ValueError(
|
|
||||||
f'Can not compose a `wg` maddr without a bearer!\n'
|
|
||||||
f'tunnel: {addr.tunnel!r}\n'
|
|
||||||
)
|
|
||||||
|
|
||||||
bindable: Address = strip_tunnels(addr)
|
|
||||||
if bindable.proto_key != 'tcp':
|
|
||||||
raise ValueError(
|
|
||||||
f'Unsupported `wg` overlay proto-key: '
|
|
||||||
f'{bindable.proto_key!r}\n'
|
|
||||||
f'overlay: {bindable!r}\n'
|
|
||||||
)
|
|
||||||
|
|
||||||
host, port = bearer
|
|
||||||
ip = ipaddress.ip_address(host)
|
|
||||||
ip_proto: str = (
|
|
||||||
'ip4'
|
|
||||||
if ip.version == 4
|
|
||||||
else 'ip6'
|
|
||||||
)
|
|
||||||
bearer_ma = Multiaddr(
|
|
||||||
f'/{ip_proto}/{host}/udp/{port}'
|
|
||||||
)
|
|
||||||
key_ma = Multiaddr(
|
|
||||||
f'/wg/{mb_pubkey(addr.tunnel.peer_pubkey)}'
|
|
||||||
)
|
|
||||||
|
|
||||||
from ._multiaddr import mk_maddr
|
|
||||||
overlay_ma: Multiaddr = mk_maddr(addr.overlay)
|
|
||||||
return (
|
|
||||||
bearer_ma
|
|
||||||
.encapsulate(key_ma)
|
|
||||||
.encapsulate(overlay_ma)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def strip_tunnels(
|
|
||||||
addr: Address|TunnelledAddress,
|
|
||||||
) -> Address:
|
|
||||||
'''
|
|
||||||
Deliver the bindable `Address`, peeling any tunnel
|
|
||||||
annotation(s).
|
|
||||||
|
|
||||||
Pure. Idempotent on an un-tunnelled `Address`, and loops so
|
|
||||||
a nested (tunnel-in-tunnel) stack collapses in one call.
|
|
||||||
|
|
||||||
Call this at every bind/dial boundary.
|
|
||||||
|
|
||||||
'''
|
|
||||||
while isinstance(addr, TunnelledAddress):
|
|
||||||
addr = addr.overlay
|
|
||||||
|
|
||||||
return addr
|
|
||||||
|
|
||||||
|
|
||||||
def tunnels_of(
|
|
||||||
addr: Address|TunnelledAddress,
|
|
||||||
) -> tuple[TunnelSpec, ...]:
|
|
||||||
'''
|
|
||||||
Deliver every tunnel spec wrapping `addr`, outermost first.
|
|
||||||
|
|
||||||
Pure; empty for an un-tunnelled `Address`.
|
|
||||||
|
|
||||||
'''
|
|
||||||
specs: list[TunnelSpec] = []
|
|
||||||
while isinstance(addr, TunnelledAddress):
|
|
||||||
specs.append(addr.tunnel)
|
|
||||||
addr = addr.overlay
|
|
||||||
|
|
||||||
return tuple(specs)
|
|
||||||
2
uv.lock
2
uv.lock
|
|
@ -1113,7 +1113,6 @@ dependencies = [
|
||||||
{ name = "multiaddr" },
|
{ name = "multiaddr" },
|
||||||
{ name = "pdbp" },
|
{ name = "pdbp" },
|
||||||
{ name = "platformdirs" },
|
{ name = "platformdirs" },
|
||||||
{ name = "py-multibase" },
|
|
||||||
{ name = "setproctitle" },
|
{ name = "setproctitle" },
|
||||||
{ name = "tricycle" },
|
{ name = "tricycle" },
|
||||||
{ name = "trio" },
|
{ name = "trio" },
|
||||||
|
|
@ -1178,7 +1177,6 @@ requires-dist = [
|
||||||
{ name = "multiaddr", git = "https://github.com/multiformats/py-multiaddr.git?rev=f86519daaa21699023d0037c58cdff600313dd09" },
|
{ name = "multiaddr", git = "https://github.com/multiformats/py-multiaddr.git?rev=f86519daaa21699023d0037c58cdff600313dd09" },
|
||||||
{ name = "pdbp", specifier = ">=1.8.2,<2" },
|
{ name = "pdbp", specifier = ">=1.8.2,<2" },
|
||||||
{ name = "platformdirs", specifier = ">=4.4.0" },
|
{ name = "platformdirs", specifier = ">=4.4.0" },
|
||||||
{ name = "py-multibase", specifier = ">=2.0.0,<3" },
|
|
||||||
{ name = "setproctitle", specifier = ">=1.3,<2" },
|
{ name = "setproctitle", specifier = ">=1.3,<2" },
|
||||||
{ name = "tricycle", specifier = ">=0.4.1,<0.5" },
|
{ name = "tricycle", specifier = ">=0.4.1,<0.5" },
|
||||||
{ name = "trio", specifier = ">0.27" },
|
{ name = "trio", specifier = ">0.27" },
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue