Add wg (WireGuard) protocol support#108
Conversation
Introduce a new `wg` multiaddr protocol using draft code `0x01C7` (unassigned slot between `noise` 0x01C6 and `shs` 0x01C8 in the multicodec table). Deats, - `multiaddr/codecs/wg.py`: fixed 256-bit codec for Curve25519 public keys using standard base64 (matching `wg(8)` tooling conventions) - `P_WG = 0x01C7` constant and `Protocol` entry registered in the default `PROTOCOLS` list - codec unit tests: roundtrip, validation, error paths, registry lookup, full `Multiaddr` integration - valid/invalid multiaddr string test cases for both IPv4 and IPv6 stacks (e.g. `/ip4/1.2.3.4/udp/51820/wg/<b64-pubkey>`) Further, the `0x01C7` allocation is not yet in the upstream multicodec table; an addition PR per https://github.com/multiformats/multicodec?tab=readme-ov-file#adding-new-multicodecs-to-the-table is needed to make it official. Prompt-IO: ai/prompt-io/claude/20260415T002453Z_0b6493a_prompt_io.md (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code
There was a problem hiding this comment.
Pull request overview
Adds initial WireGuard (wg) protocol support to py-multiaddr by introducing a new protocol code (0x01C7) and a corresponding fixed-size codec for 32-byte Curve25519 public keys.
Changes:
- Register
wgas a new protocol (P_WG = 0x01C7) in the default protocol registry. - Add
multiaddr/codecs/wg.pyimplementing base64 encode/decode for 32-byte public keys. - Extend test suite with
wgcodec/unit/integration cases and multiaddr valid/invalid string fixtures.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
multiaddr/protocols.py |
Adds P_WG constant and registers the wg protocol in PROTOCOLS. |
multiaddr/codecs/wg.py |
New codec for WireGuard public keys (32 bytes, base64). |
tests/test_protocols.py |
Adds wg codec tests and a Multiaddr integration test. |
tests/test_multiaddr.py |
Adds wg to invalid/valid multiaddr string parameter sets. |
ai/prompt-io/claude/README.md |
Documents prompt logging policy for Claude-assisted changes. |
ai/prompt-io/claude/20260415T002453Z_0b6493a_prompt_io*.md |
Stores the prompt I/O log for this change. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| 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") |
There was a problem hiding this comment.
wg uses standard base64, which can include the '/' character. Because multiaddr strings are '/'-delimited and _from_string splits on '/', any key containing '/' cannot be represented or parsed, and bytes_to_string() may emit an invalid/ambiguous multiaddr. Encode reserved characters in to_string (at least '/') and decode them in to_bytes before base64 decoding (or switch to a URL-safe encoding); add a roundtrip test that includes a key whose base64 contains '/'.
| # A valid 32-byte Curve25519 public key (random) | ||
| VALID_WG_KEY_BYTES = os.urandom(32) | ||
| VALID_WG_KEY_STRING = base64.b64encode(VALID_WG_KEY_BYTES).decode("ascii") | ||
|
|
||
| # A well-known test key (all zeros) | ||
| ZERO_WG_KEY_BYTES = b"\x00" * 32 | ||
| ZERO_WG_KEY_STRING = base64.b64encode(ZERO_WG_KEY_BYTES).decode("ascii") |
There was a problem hiding this comment.
The tests build VALID_WG_KEY_STRING from os.urandom(32) at import time. Since standard base64 may contain '/', this can make /.../wg/<key> unparsable and cause nondeterministic failures. Use a deterministic test key (and ideally one that exercises the chosen escaping/encoding strategy) instead of randomness.
| "/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=", |
There was a problem hiding this comment.
The added wg valid cases only cover base64 strings without reserved multiaddr delimiters. Once wg supports escaping (or uses URL-safe encoding), add a valid case that includes a key whose encoded form would contain '/' to ensure string parsing and str(ma) roundtrip are reliable.
| "/ip6/::1/udp/51820/wg/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", | |
| "/ip6/::1/udp/51820/wg/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", | |
| "/ip6/::1/udp/51820/wg/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%2F=", |
|
I'm going to wait for real human reviewers before addressing the copilot review if that's ok with y'all 🙏🏼 |
|
Hello @goodboy thanks for this PR. Here's a review and some thoughts about this PR. AI PR Review: #108 — Add
|
| Risk | Impact | Mitigation |
|---|---|---|
| Parsing treats part of a public key as separate path components | Medium (failed connections, confusing errors; not memory corruption) | Fix encoding so / cannot appear inside the wg value (see section 9); add regression tests. |
Broad except Exception around base64 decode |
Low | Narrow exceptions; avoids accidentally swallowing KeyboardInterrupt in odd call paths (unlikely here). |
| Error strings include decoder details | Low | Acceptable; avoid logging full multiaddr with live keys in application code (call-site concern). |
No DNS/resolver/subprocess/file-system changes in the core feature; scope is local validation and serialization.
6. Documentation and examples
- Module docstring in
multiaddr/codecs/wg.pyis helpful and links to multicodec/multiaddr processes. - Sphinx
make docs-cisucceeds; autodoc picks up the new module. - End-user README / examples do not yet document
wg; consider adding after the string encoding is fixed.
7. Newsfragment requirement
- Missing: No
newsfragments/107.feature.rst(or108.feature.rstif the project uses PR numbers when no issue exists — here Add wireguard support (with key'wg'?) #107 exists and is closed by the PR). - Action: Add a fragment with ReST body, trailing newline, and type
feature.
8. Tests and validation
| Check | Result | Log |
|---|---|---|
make lint |
Failed — ruff format reformats multiaddr/codecs/wg.py |
downloads/AI-PR-REVIEWS/108/lint_output.log |
make typecheck |
Passed | downloads/AI-PR-REVIEWS/108/typecheck_output.log |
make test |
Failed — tests/test_protocols.py::test_wg_integration (base64 contained /) |
downloads/AI-PR-REVIEWS/108/test_output.log |
make docs-ci |
Passed | downloads/AI-PR-REVIEWS/108/docs_output.log |
Note: Logs were captured on the PR branch as merged from GitHub (wg_support); working tree restored after ruff format auto-changes.
9. Recommendations for improvement
- Fix delimiter safety for the textual multiaddr form (this is both a correctness and CI stability issue). Align with whatever go-multiaddr / js-multiaddr or the eventual IETF/multiformats spec chooses, if any precedent exists.
- Replace import-time
os.urandomin tests with fixed vectors; add a test that requires a/in standard base64 once you support it safely. - Add
newsfragments/107.feature.rst. - Run ruff format and re-run
make lint. - Resolve whether
ai/prompt-io/should ship inmultiformats/py-multiaddr.
Ecosystem-aligned encoding (refined recommendation)
-
Document explicitly that textual
wgvalues MUST use an encoding that cannot contain/in the value (for example multibase consistent with other binary components such as/certhash, or base64url with stated padding rules). Prefer aligning padding and multibase code with go-multiaddr / existing multiaddr spec text rather than inventing a one-off variant. -
Codec behavior (input vs output):
- What
wg(8)gives you: Tools print the Curve25519 public key as standard base64, whose alphabet includes/(and+). That is fine as opaque text, but/must not appear inside a single multiaddr path component, because implementations split the address string on/. - Convenience on input: It is still useful to let users paste exactly what WireGuard printed. That can be supported via a narrow, documented path (e.g. a small helper like
from_wg_public_key(...), or a documented rule that applies only when building aMultiaddrfrom rawwgoutput). Decode standard base64 there, then store 32 raw bytes internally as today. - Canonical on output: When serializing to the multiaddr string (
str(ma)/to_string), thewgsegment should always use the safe, specified encoding from item 1 above (e.g. base64url or multibase—never raw standard base64). So: same key bytes everywhere, but the only form allowed inside a printed multiaddr is the normalized one. That waystr(ma)round-trips undersplit("/")and matches what Go, JS, Rust, etc. can parse if they follow the same rule.
- What
-
Tests: Use fixed vectors (no import-time
os.urandom). Include at least one key whose standard base64 contains/, proving the stored/transmitted multiaddr form does not embed raw/inside thewgcomponent and that parse +str()round-trip works.
10. Questions for the author
- Is there an emerging multiformats convention for
wg(name, codepoint, encoding) beyond this draft, or should py-multiaddr track a specific reference implementation? - For textual multiaddrs, do you prefer base64url, percent-encoding, or hex for interoperability with other stacks?
- Should the prompt I/O files remain in the upstream repo, or live elsewhere per maintainer preference?
- Has a multicodec table PR been opened for
0x01C7/wg?
11. Overall assessment
| Dimension | Rating |
|---|---|
| Quality | Needs work — core idea is sound; string form and tests are not yet reliable. |
| Security impact | Low (no classic memory/sandbox issues; logic/parsing bug with operational impact). |
| Merge readiness | Needs fixes — failing tests, lint/format, missing newsfragment, and critical string-encoding design gap. |
| Confidence | High — failure reproduced locally; root cause is clear from parser structure and failing trace. |
Bottom line: The PR addresses #107 in spirit, but standard base64 in a /-delimited string is incompatible with how py-multiaddr parses strings today. Until that is resolved, real WireGuard keys cannot be represented reliably in textual multiaddr form, and CI can fail randomly. GitHub Copilot’s review on the same point is directionally correct.
Appendix: relevant code references
String parsing uses / splitting:
parts_list = addr.strip("/").split("/")Tests use nondeterministic key material at import time:
# A valid 32-byte Curve25519 public key (random)
VALID_WG_KEY_BYTES = os.urandom(32)
VALID_WG_KEY_STRING = base64.b64encode(VALID_WG_KEY_BYTES).decode("ascii")def test_wg_integration():
ma = Multiaddr(f"/ip4/1.2.3.4/udp/51820/wg/{VALID_WG_KEY_STRING}")
assert str(ma) == f"/ip4/1.2.3.4/udp/51820/wg/{VALID_WG_KEY_STRING}"
assert ma.value_for_protocol(protocols.P_WG) == VALID_WG_KEY_STRINGAddendum: libp2p, WireGuard, and ecosystem projects
Context for PR #108 / issue #107: why a wg component in multiaddrs might matter even though WireGuard is not a first-class libp2p transport.
Core libp2p vs WireGuard as a transport
There is no official, actively maintained “WireGuard transport” wired into the core libp2p stack the way TCP, QUIC, or WebRTC are. Libp2p and WireGuard still meet often in real deployments: libp2p excels at peer discovery, NAT traversal, and relay semantics; WireGuard excels at kernel-fast, modern crypto tunnels for the data plane.
Noise (shared cryptographic lineage)
Libp2p’s default encrypted transports (e.g. the Noise-based handshake in go-libp2p) and WireGuard both draw from the Noise Protocol Framework—the same design space (modern DH, AEAD, simple handshake patterns). They are not the same wire protocol: patterns, framing, and integration differ. The point is only that “libp2p crypto” and “WireGuard crypto” are cousins, not that libp2p runs inside a WG tunnel by default.
Projects that combine libp2p-style networking with WireGuard (or similar goals)
These are illustrative community / product efforts; listing them does not imply endorsement or that py-multiaddr must track any one of them.
| Project | Role of libp2p / P2P | Role of WireGuard (or analogue) |
|---|---|---|
| wgmesh (example community implementation) | Peer discovery and TLS-style peer control traffic | WireGuard for the actual mesh / tunnel data plane |
| Webmesh | Zero-config mesh; uses libp2p circuit relays (and WebRTC / ICE) to negotiate connectivity | Manages a WireGuard mesh across OSes |
| Hyprspace | Lightweight VPN over IPFS + libp2p (DHT, hole-punching) | Not WireGuard under the hood; WG-inspired; addresses “everyone behind NAT” scenarios libp2p is good at |
Several other repos use the name wgmesh; treat the table as archetypes (control plane vs data plane) rather than a single canonical package.
Historical note: go-wireguard in the libp2p org
The archived/experimental repo github.com/libp2p/go-wireguard was an early Go helper around WireGuard interfaces and packet queues (~2016). It was not a maintained, plug-in libp2p transport comparable to today’s stack, and it has not seen active development in years. It is useful mainly as historical context for “WireGuard has been on the radar near libp2p for a long time.”
Tie-in to this review
Overlays that use libp2p (or multiaddr-bearing tooling) for signaling and WireGuard for tunnels benefit from a stable, interoperable way to pin a peer’s WireGuard public key into an address or descriptor. That is the practical motivation for a draft /wg/<key> component—provided the textual encoding cannot break /-delimited parsing (see section 9).
|
Ok so reading this all, I feel like i'm concluding this isn't a totally terrible idea and presuming we resolve the @acul71 ? |
Add
wg(WireGuard) protocol supportCloses #107.
WireGuard has no entry in the upstream multiaddr
protocols.csv or the multicodec table yet,
but there's clear demand for declaring WireGuard tunnel endpoints
using multiaddr notation — particularly for overlay-network use cases
where distributed processes communicate over
wginterfaces.This patch adds a draft
wgprotocol so that multiaddrs like/ip4/1.2.3.4/udp/51820/wg/<b64-pubkey>can be parsed, validated,and round-tripped through the existing codec machinery.
Src of research
The following provide ctx on the upstream spec landscape and the
multicodec addition process,
https://github.com/multiformats/multiaddr
https://github.com/multiformats/multicodec
shsat0x01C8)Summary of changes
(8af8da0) Add
wgprotocol with draft codeP_WG = 0x01C7(unassigned slot betweennoise0x01C6andshs0x01C8in the multicodec table).
multiaddr/codecs/wg.pycodec: fixed 256-bit (32-byte)Curve25519 public key using standard base64 — matching
wg(8)tooling conventions.
Protocol(P_WG, "wg", "wg")entry registered in the defaultPROTOCOLSlist.wrong-length string/bytes,
validate(), registry lookup, fullMultiaddrintegration.and IPv6 stacks.
Scopes changed
multiaddr.protocolsP_WG = 0x01C7constant added alongside existing protocol codes.Protocol(P_WG, "wg", "wg")appended afternoiseinPROTOCOLS.multiaddr.codecs.wg(new)Codecclass:SIZE=256,IS_PATH=False.to_bytes(): base64-decode + 32-byte length check.to_string(): 32-byte length check + base64-encode.validate(): byte-length assertion.tests.test_protocolswgcodec import and 8 new test fns.tests.test_multiaddrTODOs before landing
wg,multiaddr,0x01c7,draftintable.csvper the additionprocess — this patch uses the code speculatively until
that lands.
Reviewers
cc @acul71 @pacrob
(this pr content was generated in some part by
claude-code)