Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions ai/prompt-io/claude/20260415T002453Z_0b6493a_prompt_io.md
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.
154 changes: 154 additions & 0 deletions ai/prompt-io/claude/20260415T002453Z_0b6493a_prompt_io.raw.md
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`).
27 changes: 27 additions & 0 deletions ai/prompt-io/claude/README.md
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.
87 changes: 87 additions & 0 deletions gh/wg_spec_submission_plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# `wg` multiaddr protocol: upstream spec submission plan

Plan for registering the `wg` (WireGuard) protocol upstream
once the `py-multiaddr` impl (PR
[multiformats/py-multiaddr#108][pr108]) is settled.

## Settled impl semantics (post-review)

- protocol name: `wg`
- code: `0x01c7` (decimal `455`) — the unassigned slot
between `noise` (`0x01c6`) and `shs` (`0x01c8`) in the
`multiaddr` tag range.
- binary form: exactly 32 bytes — a raw Curve25519 public
key; fixed size `256` bits.
- string form: URL-safe base64 (RFC 4648 section 5) **with
padding** — 44 chars ending in `=`.
* standard base64 (as printed by `wg(8)`) is rejected
since `/` collides with the multiaddr delimiter and `+`
is reserved to the std alphabet; transliterate via
`tr '+/' '-_'`.
* precedent: `garlic64` uses alt-alphabet base64
(`-~`) for the same delimiter-collision reason;
`certhash` uses multibase `base64url`.
- example:
`/ip4/1.2.3.4/udp/51820/wg/__________________________________________8=`

## Step 1: multicodec PR (reserves the code)

Repo: <https://github.com/multiformats/multicodec>

Add one row to `table.csv` (cols:
`name, tag, code, status, description`), keeping the file's
column alignment, sorted by code next to `noise`/`shs`:

```csv
wg, multiaddr, 0x01c7, draft, WireGuard tunnel endpoint - 32-byte Curve25519 public key
```

- run the repo's table validation locally before pushing
(`make` / `npm test` per their CONTRIBUTING docs).
- per their addition process, `draft` status entries for
unclaimed codes are routinely accepted via small PRs.
- PR body: link WireGuard cryptokey-routing docs, py-multiaddr
PR #108 as the first implementation, and the overlay-network
use case from issue #107.

## Step 2: multiaddr spec PR (defines the protocol)

Repo: <https://github.com/multiformats/multiaddr>

1. Add a row to `protocols.csv` (cols:
`code,\tsize,\tname,\tcomment`), sorted by code after
`noise` (`454`):

```csv
455, 256, wg, WireGuard tunnel endpoint (Curve25519 public key)
```

2. If requested by maintainers, add a short protocol
description to the README/spec covering:
* value = 32-byte Curve25519 public key (binary),
padded URL-safe base64 (string).
* rationale for the URL-safe alphabet (delimiter
collision) + the `tr '+/' '-_'` conversion from
`wg(8)` output.
* canonical composition: `/ip{4,6}/<host>/udp/<port>/wg/<key>`.

Open this PR referencing the multicodec PR from step 1 so
both land with the same code.

## Step 3: circle back to py-multiaddr

After (or alongside) the upstream PRs:

- update `multiaddr/codecs/wg.py` module docstring + PR #108
body to point at the upstream PRs instead of calling the
code a speculative draft.
- tick the "Submit a draft multicodec addition PR upstream"
TODO checkbox in the PR #108 description.

## Follow-ups (optional)

- propose the same protocol to `go-multiaddr` /
`js-multiaddr` once the spec rows land, so the string/binary
forms stay interoperable across impls.

[pr108]: https://github.com/multiformats/py-multiaddr/pull/108
77 changes: 77 additions & 0 deletions multiaddr/codecs/wg.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""
WireGuard protocol codec.

Encode/decode a 32-byte Curve25519 public key as URL-safe
base64 (RFC 4648 section 5, with padding).

`wg(8)` tooling renders keys as *standard* base64 whose
alphabet includes '/', a char which collides with the
multiaddr protocol delimiter and thus can't appear in a
multiaddr string segment. The URL-safe alphabet differs
only by two chars ('+' -> '-', '/' -> '_') so converting a
`wg(8)` key is a simple transliteration:

tr '+/' '-_' <<< "$WG_PUBKEY"

The same alternate-alphabet approach is used by the
`garlic64` codec for i2p addrs.

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:
# explicitly reject the standard-base64 alphabet:
# '/' collides with the multiaddr delimiter, so keys
# copied from `wg(8)` output must be transliterated
# to the URL-safe alphabet first.
if "+" in string or "/" in string:
raise ValueError(
"WireGuard public key must be URL-safe base64 "
"(RFC 4648 section 5): replace '+' with '-' "
"and '/' with '_'"
)

try:
raw = base64.b64decode(string, altchars=b"-_", 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, 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, got {len(buf)}",
buf,
"wg",
)
return base64.urlsafe_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, got {len(b)}")
Loading
Loading