-
Notifications
You must be signed in to change notification settings - Fork 32
Add wg (WireGuard) protocol support
#108
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| --- | ||
| model: claude-opus-4-6 | ||
| service: claude | ||
| session: py-multiaddr-wg-protocol | ||
| timestamp: 2026-04-15T00:24:53Z | ||
| git_ref: 0b6493a | ||
| scope: code | ||
| substantive: true | ||
| raw_file: 20260415T002453Z_0b6493a_prompt_io.raw.md | ||
| --- | ||
|
|
||
| ## Prompt | ||
|
|
||
| The user requested adding full support for a new "wireguard" protocol | ||
| denoted as `wg` into the py-multiaddr codebase. They referenced the | ||
| upstream spec repo links: | ||
|
|
||
| - https://github.com/multiformats/multiaddr/blob/master/protocols.csv | ||
| - https://multiformats.io/multiaddr/ | ||
|
|
||
| And noted the multicodec addition process: | ||
|
|
||
| - https://github.com/multiformats/multicodec | ||
| - https://github.com/multiformats/multicodec?tab=readme-ov-file#adding-new-multicodecs-to-the-table | ||
| - https://github.com/multiformats/multicodec/blob/master/table.csv#L130 | ||
|
|
||
| ## Response summary | ||
|
|
||
| Add WireGuard (`wg`) as a new multiaddr protocol with draft code | ||
| `0x01C7` (an unassigned slot between `noise` 0x01C6 and `shs` 0x01C8 | ||
| in the multicodec table). The codec handles 32-byte Curve25519 public | ||
| keys encoded as standard base64, matching `wg(8)` tooling conventions. | ||
| Example multiaddr: `/ip4/1.2.3.4/udp/51820/wg/<base64-pubkey>`. | ||
|
|
||
| ## Files changed | ||
|
|
||
| - `multiaddr/protocols.py` — Add `P_WG = 0x01C7` constant and | ||
| `Protocol(P_WG, "wg", "wg")` entry in `PROTOCOLS` list | ||
| - `multiaddr/codecs/wg.py` — New codec for 32-byte Curve25519 | ||
| public keys (SIZE=256, standard base64 encoding) | ||
| - `tests/test_protocols.py` — Add `wg` codec unit tests (roundtrip, | ||
| validation, error paths, registry lookup, integration) | ||
| - `tests/test_multiaddr.py` — Add valid and invalid WireGuard | ||
| multiaddr string test cases | ||
|
|
||
| ## Human edits | ||
|
|
||
| None — committed as generated. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,154 @@ | ||
| --- | ||
| model: claude-opus-4-6 | ||
| service: claude | ||
| timestamp: 2026-04-15T00:24:53Z | ||
| git_ref: 0b6493a | ||
| --- | ||
|
|
||
| Let me explore the codebase and check the upstream spec simultaneously. | ||
|
|
||
| [Launched Explore agent to thoroughly explore py-multiaddr codebase structure, protocol definitions, codec patterns, and tests] | ||
|
|
||
| [Fetched https://github.com/multiformats/multiaddr/blob/master/protocols.csv — no WireGuard/wg entry found in upstream spec] | ||
|
|
||
| The upstream `protocols.csv` doesn't yet include a `wg` entry, so we'll need to pick a code. Let me read the key files to understand the exact patterns before implementing. | ||
|
|
||
| [Read multiaddr/protocols.py, multiaddr/codecs/__init__.py, multiaddr/codecs/uint16be.py] | ||
|
|
||
| Now let me also check the upstream multicodec table for any existing WireGuard code assignment. | ||
|
|
||
| [Fetched https://raw.githubusercontent.com/multiformats/multicodec/master/table.csv — no wireguard/wg entry found. Available slots in 0x01C0-0x01D0 range: 0x01c2, 0x01c3, 0x01c4, 0x01c5, 0x01c7, 0x01c9, 0x01ca, 0x01cb, 0x01ce, 0x01cf, 0x01d0] | ||
|
|
||
| [Read tests/test_protocols.py, tests/test_multiaddr.py] | ||
|
|
||
| I have a clear picture. Here's the plan: | ||
|
|
||
| - **Protocol code**: `0x01C7` (available slot between `noise` 0x01C6 and `shs` 0x01C8 in the multicodec table) | ||
| - **Codec**: 32-byte (256-bit) Curve25519 public key, base64 encoded (standard WireGuard key format) | ||
| - **Multiaddr example**: `/ip4/1.2.3.4/udp/51820/wg/<base64-pubkey>` | ||
|
|
||
| Let me implement all three pieces. | ||
|
|
||
| ### Edit 1: multiaddr/protocols.py — Add P_WG constant | ||
|
|
||
| Added `P_WG = 0x01C7` before `P_MEMORY`. | ||
|
|
||
| ### Edit 2: multiaddr/protocols.py — Add Protocol entry | ||
|
|
||
| Added `Protocol(P_WG, "wg", "wg"),` after the `noise` entry in the PROTOCOLS list. | ||
|
|
||
| ### New file: multiaddr/codecs/wg.py | ||
|
|
||
| ```python | ||
| """ | ||
| WireGuard protocol codec. | ||
|
|
||
| Encode/decode a 32-byte Curve25519 public key as standard | ||
| base64 (the canonical format used by `wg(8)` tooling). | ||
|
|
||
| The protocol code `0x01C7` is a draft allocation not yet | ||
| present in the upstream multicodec table: | ||
| - https://github.com/multiformats/multicodec/blob/master/table.csv | ||
| - https://github.com/multiformats/multiaddr/blob/master/protocols.csv | ||
| - https://multiformats.io/multiaddr/ | ||
|
|
||
| See also the upstream multicodec addition process: | ||
| - https://github.com/multiformats/multicodec?tab=readme-ov-file#adding-new-multicodecs-to-the-table | ||
| """ | ||
|
|
||
| import base64 | ||
| from typing import Any | ||
|
|
||
| from ..codecs import CodecBase | ||
| from ..exceptions import BinaryParseError | ||
|
|
||
| SIZE = 256 # 32 bytes * 8 bits | ||
| IS_PATH = False | ||
|
|
||
| WG_KEY_LENGTH = 32 # Curve25519 public key | ||
|
|
||
|
|
||
| class Codec(CodecBase): | ||
| SIZE = SIZE | ||
| IS_PATH = IS_PATH | ||
|
|
||
| def to_bytes(self, proto: Any, string: str) -> bytes: | ||
| try: | ||
| raw = base64.b64decode(string, validate=True) | ||
| except Exception as exc: | ||
| raise ValueError( | ||
| f"invalid base64 WireGuard public key: {exc}" | ||
| ) from exc | ||
|
|
||
| if len(raw) != WG_KEY_LENGTH: | ||
| raise ValueError( | ||
| f"WireGuard public key must be {WG_KEY_LENGTH} bytes, " | ||
| f"got {len(raw)}" | ||
| ) | ||
| return raw | ||
|
|
||
| def to_string(self, proto: Any, buf: bytes) -> str: | ||
| if len(buf) != WG_KEY_LENGTH: | ||
| raise BinaryParseError( | ||
| f"WireGuard public key must be {WG_KEY_LENGTH} bytes, " | ||
| f"got {len(buf)}", | ||
| buf, | ||
| "wg", | ||
| ) | ||
| return base64.b64encode(buf).decode("ascii") | ||
|
|
||
| def validate(self, b: bytes) -> None: | ||
| if len(b) != WG_KEY_LENGTH: | ||
| raise ValueError( | ||
| f"WireGuard public key must be {WG_KEY_LENGTH} bytes, " | ||
| f"got {len(b)}" | ||
| ) | ||
| ``` | ||
|
|
||
| ### Edit 3: tests/test_protocols.py — Add wg import and tests | ||
|
|
||
| Added `wg` to codec imports. | ||
|
|
||
| Added test functions: | ||
| - `test_wg_valid_roundtrip` — base64 encode/decode of random 32-byte key | ||
| - `test_wg_zero_key_roundtrip` — all-zeros key roundtrip | ||
| - `test_wg_invalid_base64_raises` — rejects invalid base64 | ||
| - `test_wg_wrong_length_string_raises` — rejects 16-byte and 64-byte keys | ||
| - `test_wg_wrong_length_bytes_raises` — rejects wrong-length binary buffers | ||
| - `test_wg_validate` — direct validate() method testing | ||
| - `test_wg_protocol_lookup` — registry lookup by name | ||
| - `test_wg_integration` — full Multiaddr roundtrip with `/ip4/1.2.3.4/udp/51820/wg/<key>` | ||
|
|
||
| ### Edit 4: tests/test_multiaddr.py — Add invalid cases | ||
|
|
||
| Added to invalid test params: | ||
| - `/wg` — missing key | ||
| - `/wg/not-valid-base64` — invalid base64 | ||
| - `/ip4/1.2.3.4/udp/51820/wg` — missing key after full address | ||
|
|
||
| ### Edit 5: tests/test_multiaddr.py — Add valid cases | ||
|
|
||
| Added to valid test params: | ||
| - `/ip4/1.2.3.4/udp/51820/wg/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=` | ||
| - `/ip6/::1/udp/51820/wg/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=` | ||
|
|
||
| ### Test run | ||
|
|
||
| All 269 tests pass. | ||
|
|
||
| ### Summary | ||
|
|
||
| **3 files touched:** | ||
|
|
||
| 1. **`multiaddr/protocols.py`** — Added `P_WG = 0x01C7` constant and `Protocol(P_WG, "wg", "wg")` entry in the `PROTOCOLS` list (placed after `noise`). | ||
|
|
||
| 2. **`multiaddr/codecs/wg.py`** (new) — Codec for 32-byte Curve25519 public keys using standard base64 encoding (the canonical format from `wg(8)` tooling). Fixed size of 256 bits. Includes docstring referencing the upstream multicodec/multiaddr spec repos and the addition process. | ||
|
|
||
| 3. **`tests/test_protocols.py`** + **`tests/test_multiaddr.py`** — Added codec roundtrip, validation, error-path tests, protocol registry lookup, integration tests, and valid/invalid multiaddr string cases. | ||
|
|
||
| **Protocol details:** | ||
| - Code: `0x01C7` (draft, between `noise` 0x01C6 and `shs` 0x01C8 — not yet allocated upstream) | ||
| - Codec: fixed 256-bit (32-byte) Curve25519 public key, standard base64 | ||
| - Example multiaddr: `/ip4/1.2.3.4/udp/51820/wg/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=` | ||
|
|
||
| Regarding the upstream multicodec table — `0x01C7` is currently unassigned. To make this official, a PR to multiformats/multicodec adding `wg,multiaddr,0x01c7,draft` to `table.csv` would be needed per their adding new multicodecs process (similar to how `shs`/Secure Scuttlebutt got `0x01c8`). |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| # AI Prompt I/O Log — claude | ||
|
|
||
| This directory tracks prompt inputs and model | ||
| outputs for AI-assisted development using | ||
| `claude` (Claude Code). | ||
|
|
||
| ## Policy | ||
|
|
||
| Prompt logging follows the | ||
| [NLNet generative AI policy][nlnet-ai]. | ||
| All substantive AI contributions are logged | ||
| with: | ||
| - Model name and version | ||
| - Timestamps | ||
| - The prompts that produced the output | ||
| - Unedited model output (`.raw.md` files) | ||
|
|
||
| [nlnet-ai]: https://nlnet.nl/foundation/policies/generativeAI/ | ||
|
|
||
| ## Usage | ||
|
|
||
| Entries are created by the `/prompt-io` skill | ||
| or automatically via `/commit-msg` integration. | ||
|
|
||
| Human contributors remain accountable for all | ||
| code decisions. AI-generated content is never | ||
| presented as human-authored work. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| """ | ||
| WireGuard protocol codec. | ||
|
|
||
| Encode/decode a 32-byte Curve25519 public key as standard | ||
| base64 (the canonical format used by `wg(8)` tooling). | ||
|
|
||
| The protocol code `0x01C7` is a draft allocation not yet | ||
| present in the upstream multicodec table: | ||
| - https://github.com/multiformats/multicodec/blob/master/table.csv | ||
| - https://github.com/multiformats/multiaddr/blob/master/protocols.csv | ||
| - https://multiformats.io/multiaddr/ | ||
|
|
||
| See also the upstream multicodec addition process: | ||
| - https://github.com/multiformats/multicodec?tab=readme-ov-file#adding-new-multicodecs-to-the-table | ||
| """ | ||
|
|
||
| import base64 | ||
| from typing import Any | ||
|
|
||
| from ..codecs import CodecBase | ||
| from ..exceptions import BinaryParseError | ||
|
|
||
| SIZE = 256 # 32 bytes * 8 bits | ||
| IS_PATH = False | ||
|
|
||
| WG_KEY_LENGTH = 32 # Curve25519 public key | ||
|
|
||
|
|
||
| class Codec(CodecBase): | ||
| SIZE = SIZE | ||
| IS_PATH = IS_PATH | ||
|
|
||
| def to_bytes(self, proto: Any, string: str) -> bytes: | ||
| try: | ||
| raw = base64.b64decode(string, validate=True) | ||
| except Exception as exc: | ||
| raise ValueError( | ||
| f"invalid base64 WireGuard public key: {exc}" | ||
| ) from exc | ||
|
|
||
| if len(raw) != WG_KEY_LENGTH: | ||
| raise ValueError( | ||
| f"WireGuard public key must be {WG_KEY_LENGTH} bytes, " | ||
| f"got {len(raw)}" | ||
| ) | ||
| return raw | ||
|
|
||
| def to_string(self, proto: Any, buf: bytes) -> str: | ||
| if len(buf) != WG_KEY_LENGTH: | ||
| raise BinaryParseError( | ||
| f"WireGuard public key must be {WG_KEY_LENGTH} bytes, " | ||
| f"got {len(buf)}", | ||
| buf, | ||
| "wg", | ||
| ) | ||
| return base64.b64encode(buf).decode("ascii") | ||
|
|
||
| def validate(self, b: bytes) -> None: | ||
| if len(b) != WG_KEY_LENGTH: | ||
| raise ValueError( | ||
| f"WireGuard public key must be {WG_KEY_LENGTH} bytes, " | ||
| f"got {len(b)}" | ||
| ) | ||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -83,6 +83,9 @@ | |||||||
| "/dns4", | ||||||||
| "/dns6", | ||||||||
| "/cancer", | ||||||||
| "/wg", | ||||||||
| "/wg/not-valid-base64", | ||||||||
| "/ip4/1.2.3.4/udp/51820/wg", | ||||||||
| ], | ||||||||
| ) | ||||||||
| def test_invalid(addr_str): | ||||||||
|
|
@@ -139,6 +142,8 @@ def test_invalid(addr_str): | |||||||
| "/ip4/127.0.0.1/tcp/9090/http/p2p-webrtc-direct", | ||||||||
| "/ip4/127.0.0.1/tcp/127/webrtc-direct", | ||||||||
| "/ip4/127.0.0.1/tcp/127/webrtc", | ||||||||
| "/ip4/1.2.3.4/udp/51820/wg/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", | ||||||||
| "/ip6/::1/udp/51820/wg/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", | ||||||||
|
||||||||
| "/ip6/::1/udp/51820/wg/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", | |
| "/ip6/::1/udp/51820/wg/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", | |
| "/ip6/::1/udp/51820/wg/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%2F=", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wguses standard base64, which can include the '/' character. Because multiaddr strings are '/'-delimited and_from_stringsplits on '/', any key containing '/' cannot be represented or parsed, andbytes_to_string()may emit an invalid/ambiguous multiaddr. Encode reserved characters into_string(at least '/') and decode them into_bytesbefore base64 decoding (or switch to a URL-safe encoding); add a roundtrip test that includes a key whose base64 contains '/'.