Add IPv6 support for mesh addressing and gossip endpoints#97
Conversation
Agent-Logs-Url: https://github.com/igorls/meshguard/sessions/3a78a423-ddd3-41a5-9cb3-ae76a9114919 Co-authored-by: igorls <4753812+igorls@users.noreply.github.com>
Agent-Logs-Url: https://github.com/igorls/meshguard/sessions/3a78a423-ddd3-41a5-9cb3-ae76a9114919 Co-authored-by: igorls <4753812+igorls@users.noreply.github.com>
Agent-Logs-Url: https://github.com/igorls/meshguard/sessions/3a78a423-ddd3-41a5-9cb3-ae76a9114919 Co-authored-by: igorls <4753812+igorls@users.noreply.github.com>
Agent-Logs-Url: https://github.com/igorls/meshguard/sessions/3a78a423-ddd3-41a5-9cb3-ae76a9114919 Co-authored-by: igorls <4753812+igorls@users.noreply.github.com>
Agent-Logs-Url: https://github.com/igorls/meshguard/sessions/3a78a423-ddd3-41a5-9cb3-ae76a9114919 Co-authored-by: igorls <4753812+igorls@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds IPv6 support across MeshGuard’s mesh addressing, endpoint representation, gossip wire format, and platform networking setup while preserving existing IPv4 behavior paths.
Changes:
- Introduce deterministic IPv6 mesh addressing (
fd99:6d67::/64) and configure it on Linux/Windows/macOS TUN interfaces plus routes. - Extend the
Endpointmodel + SWIM gossip codec to support IPv4/IPv6 endpoints and add IPv6 round-trip tests. - Add IPv6-capable UDP bind/send/recv paths and thread endpoint-aware sending through SWIM/holepunch/CLI/FFI.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/wireguard/rtnetlink.zig | Adds IPv6 address/route netlink operations and improves route attributes. |
| src/wireguard/ip.zig | Adds IPv6 mesh prefix + deterministic IPv6 derivation and formatting helpers/tests. |
| src/wireguard/device.zig | Stores mesh IPv6 + supports IPv6 endpoints in peer model. |
| src/protocol/messages.zig | Extends Endpoint to represent IPv4 or IPv6 and updates formatting/equality. |
| src/protocol/codec.zig | Updates gossip/holepunch encoding to carry endpoint family + 16-byte addresses; adds IPv6 round-trip test. |
| src/net/wincfg.zig | Adds netsh-based IPv6 address/route configuration helpers. |
| src/net/udp.zig | Adds IPv6 UDP bind/send/recv and an endpoint-aware send helper. |
| src/net/darwincfg.zig | Adds ifconfig/route-based IPv6 interface and route configuration helpers. |
| src/nat/holepunch.zig | Sends probes via endpoint-aware UDP helper (works for IPv4/IPv6). |
| src/meshguard_ffi.zig | Adds IPv6 init/join entry points for embedders while keeping IPv4 APIs. |
| src/main.zig | Wires IPv6 mesh config/routes and IPv6 announce/bind behavior into CLI up. |
| src/discovery/swim.zig | Switches SWIM to endpoint-based send/recv, derives/stores mesh IPv6, and gossips endpoints with family. |
| src/discovery/seed.zig | Adds IPv6 endpoint parsing and bracketed host:port splitting support + tests. |
| src/discovery/membership.zig | Stores deterministic mesh IPv6 per peer. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // RTA_OIF = 4 (output interface) | ||
| b.addAttr(RTA_DST, &dst); | ||
| var oif_bytes: [4]u8 = undefined; | ||
| std.mem.writeInt(u32, &oif_bytes, ifindex, .little); |
There was a problem hiding this comment.
Netlink integer attributes are encoded in native endianness. Writing ifindex as little-endian here will break on big-endian Linux architectures. Use .native (or write via std.mem.nativeToBytes) for the RTA_OIF payload.
| std.mem.writeInt(u32, &oif_bytes, ifindex, .little); | |
| std.mem.writeInt(u32, &oif_bytes, ifindex, .native); |
| const port = std.mem.readInt(u16, data[18..][0..2], .little); | ||
| if (data[1] == 6) { | ||
| var addr6: [16]u8 = undefined; | ||
| @memcpy(&addr6, data[2..][0..16]); | ||
| return messages.Endpoint.initV6(addr6, port); | ||
| } | ||
| var addr: [4]u8 = undefined; | ||
| @memcpy(&addr, data[2..][0..4]); | ||
| return messages.Endpoint.initV4(addr, port); |
There was a problem hiding this comment.
decodeEndpoint treats any family byte other than 6 as IPv4. That means corrupted/invalid inputs (e.g. family=0/5) will be accepted as IPv4 endpoints, potentially leading to misrouting or confusing peer endpoint state. Consider validating data[1] is exactly 4 or 6 when has_ep is set; otherwise return null (or a decode error).
| const port = std.mem.readInt(u16, data[18..][0..2], .little); | |
| if (data[1] == 6) { | |
| var addr6: [16]u8 = undefined; | |
| @memcpy(&addr6, data[2..][0..16]); | |
| return messages.Endpoint.initV6(addr6, port); | |
| } | |
| var addr: [4]u8 = undefined; | |
| @memcpy(&addr, data[2..][0..4]); | |
| return messages.Endpoint.initV4(addr, port); | |
| const family = data[1]; | |
| const port = std.mem.readInt(u16, data[18..][0..2], .little); | |
| if (family == 6) { | |
| var addr6: [16]u8 = undefined; | |
| @memcpy(&addr6, data[2..][0..16]); | |
| return messages.Endpoint.initV6(addr6, port); | |
| } | |
| if (family == 4) { | |
| var addr: [4]u8 = undefined; | |
| @memcpy(&addr, data[2..][0..4]); | |
| return messages.Endpoint.initV4(addr, port); | |
| } | |
| return null; |
| if (data.len >= 4) { | ||
| const msg_type = std.mem.readInt(u32, data[0..4], .little); | ||
| if (msg_type >= 1 and msg_type <= 4) { | ||
| if (self.handler) |h| { | ||
| if (h.onWgPacket) |cb| { | ||
| cb(h.ctx, data, sender_addr, sender_port); | ||
| cb(h.ctx, data, sender_endpoint.addr, sender_endpoint.port); | ||
| } |
There was a problem hiding this comment.
When receiving WireGuard packets on an IPv6-bound socket, sender_endpoint will be IPv6 and sender_endpoint.addr will be the default 0.0.0.0, so onWgPacket gets an invalid IPv4 sender address. This breaks WG packet dispatch over IPv6 (and can also break endpoint tracking if the callback uses the sender address). Consider changing the callback to accept messages.Endpoint (or add a parallel onWgPacketEndpoint) and pass the full endpoint through.
| const colon_idx = std.mem.lastIndexOfScalar(u8, s, ':') orelse return null; | ||
| return .{ .host = s[0..colon_idx], .port = s[colon_idx + 1 ..] }; |
There was a problem hiding this comment.
splitHostPort falls back to lastIndexOf(':') for non-bracketed inputs. For a bare IPv6 address without an explicit port (e.g. fd99:6d67::1), this will be mis-parsed as host fd99:6d67:: with port 1, silently producing an incorrect endpoint. Since the documented IPv6 seed syntax is bracketed, consider rejecting non-bracketed hosts that contain ':' (or otherwise requiring brackets for IPv6) so missing ports can’t be mistaken for the last hextet.
| const colon_idx = std.mem.lastIndexOfScalar(u8, s, ':') orelse return null; | |
| return .{ .host = s[0..colon_idx], .port = s[colon_idx + 1 ..] }; | |
| const first_colon = std.mem.indexOfScalar(u8, s, ':') orelse return null; | |
| if (std.mem.indexOfScalar(u8, s[first_colon + 1 ..], ':') != null) return null; | |
| return .{ .host = s[0..first_colon], .port = s[first_colon + 1 ..] }; |
| b.addAttr(RTA_DST, &dst); | ||
|
|
||
| var oif_bytes: [4]u8 = undefined; | ||
| std.mem.writeInt(u32, &oif_bytes, ifindex, .little); |
There was a problem hiding this comment.
Netlink integer attributes are encoded in native endianness. Writing ifindex as little-endian here will break on big-endian Linux architectures. Use .native (or write via std.mem.nativeToBytes) for the RTA_OIF payload.
| std.mem.writeInt(u32, &oif_bytes, ifindex, .little); | |
| std.mem.writeInt(u32, &oif_bytes, ifindex, .native); |
MeshGuard’s control and data-plane setup was IPv4-only: mesh addresses, peer endpoints, TUN configuration, and SWIM gossip all assumed 4-byte addresses. This PR adds IPv6 support while keeping existing IPv4 behavior compatible.
Mesh addressing
fd99:6d67::/64.10.99.0.0/16IPv4 derivation intact.Endpoint model and wire codec
Endpointto represent IPv4 or IPv6.UDP gossip transport
Adds IPv6 UDP bind, send, and receive paths.
Routes SWIM sends through endpoint-aware helpers instead of assuming IPv4.
Supports IPv6 seed syntax:
TUN/interface configuration
fd99:6d67::/64.FFI/mobile API
Example IPv6 endpoint handling: