feat(l1): dual-stack IPv6 UDP discovery sockets#6377
feat(l1): dual-stack IPv6 UDP discovery sockets#6377azteca1998 wants to merge 17 commits intofeat/p2p-dual-stack-ipv6from
Conversation
🤖 Kimi Code ReviewReview SummaryThis PR adds dual-stack support for discovery by allowing multiple UDP bind addresses. The changes are well-structured and follow Rust best practices. However, there are a few issues to address: Critical Issues
Security Concerns
Performance & Correctness
Minor Issues
Suggested Fixes
if discovery_bind_addrs.len() > 2 {
panic!("Maximum 2 discovery addresses supported (IPv4 + IPv6)");
}
.map(|a| a.parse()
.map_err(|_| format!("Invalid discovery address: {}", a)))?
Automated review by Kimi (Moonshot AI) · custom prompt |
🤖 Claude Code ReviewPR #6377 —
|
🤖 Codex Code ReviewFindings:
No EVM opcode/gas/consensus/state-transition logic is touched in this diff, so no direct execution-layer correctness concerns from these changes. Automated review by OpenAI Codex · custom prompt |
Lines of code reportTotal lines added: Detailed view |
fcd9b49 to
726af77
Compare
430df2e to
858f935
Compare
726af77 to
57b6cb3
Compare
858f935 to
7d3ad8b
Compare
2f01fa6 to
ba70b26
Compare
6c0615f to
aade806
Compare
Extends the discovery layer to bind one UDP socket per configured address family, giving IPv6 peers the same discv4/discv5 reachability as IPv4 peers. - `NetworkConfig`: `discovery_bind_addr`/`discovery_external_addr` (scalar) replaced by `discovery_bind_addrs`/`discovery_external_addrs` (`Vec<IpAddr>`). `bind_udp_addr()` replaced by `bind_udp_addrs() -> Vec<SocketAddr>`. - `start_network()`: iterates over `bind_udp_addrs()` and for each entry binds a dedicated UDP socket, spawns independent discv4 and discv5 server instances (sharing the same peer table), and starts a `DiscoveryMultiplexer`. Each server's `local_node` uses the matching external address so Ping/Pong `from` endpoints are always correct for the socket's address family. - `--discovery.addr` CLI flag changed from `Option<String>` to `Vec<String>` with comma delimiter, accepting e.g. `--discovery.addr 0.0.0.0,::`. When omitted, defaults to the same address set as `--p2p.addr`, so dual-stack RLPx automatically implies dual-stack discovery. - `get_local_p2p_node()` builds `discovery_bind_addrs` / `discovery_external_addrs` applying the same per-address NAT / auto-detect logic already used for RLPx.
discv4 and discv5 servers can discover peers of both address families from bootnode neighbor responses, then attempt to send UDP packets to those peers using the current socket — which fails with EAFNOSUPPORT (errno 97) when the destination is IPv6 but the socket is IPv4-bound (or vice versa). Guard all outbound UDP send paths in both servers so packets destined for an address family that doesn't match the socket are silently skipped: - discv4: `send()`, `send_else_dispose()`, and the FindNode direct send in `lookup()` all check `addr.is_ipv6() != self.local_node.ip.is_ipv6()`. - discv5: `send_packet()` applies the same check. This fix is needed even in single-stack IPv4 deployments, where IPv6 peers appear in neighbor lists from bootnodes that serve both families.
…ery sockets UdpSocket::bind() sets no socket options, causing two problems when binding both 0.0.0.0:port and [::]:port for dual-stack discovery: - On Linux systems where net.ipv6.bindv6only=0 (the common default), the IPv6 socket claims both the IPv6 and IPv4 wildcard, so the subsequent IPv4 bind fails with EADDRINUSE. - Without SO_REUSEADDR, a fast restart after a crash also fails with EADDRINUSE while the OS-level TIME_WAIT drains. Replace the raw UdpSocket::bind call with a udp_socket() helper (mirroring the existing listener() helper for TCP) that uses socket2 to set options before binding: SO_REUSEADDR, SO_REUSEPORT (Unix), and IPV6_V6ONLY=true for IPv6 sockets so each wildcard address claims only its own address family.
Makes the IPv6 TCP listener explicitly IPv6-only, consistent with the UDP socket. Without this, on Linux systems where net.ipv6.bindv6only defaults to 0, ss would show the socket as '*:30303' (dual-stack) rather than '[::]:30303' (IPv6-only).
Store ip6/tcp6/udp6 from peer ENRs in Contact. The IPv6 discv4/discv5 servers filter get_contact_for_lookup to contacts with an IPv6 ENR address and receive the contact with node.ip rewritten to that IPv6 address, bootstrapping IPv6 discovery from dual-stack peers found via IPv4. Adds a debug log when an ENR with an IPv6 address is received. Also fix IPV6_V6ONLY on the IPv6 TCP listener using socket2 (Tokio's TcpSocket has no set_only_v6).
connection_addr() now skips 0.0.0.0 (and ::) so a peer that advertises a broken/NAT IPv4 correctly falls through to their IPv6 address. record_enr_response_received also ignores unspecified :: when storing the IPv6 relay address for the IPv6 discovery server.
Most dual-stack peers advertise ip6 but omit tcp6/udp6, implying the same port serves both families. connection_addr() now uses tcp6_port falling back to tcp_port when selecting an IPv6 TCP address, allowing RLPx to reach peers that only set ip6 without tcp6 in their ENR.
Testing confirmed IPv6 attempts reach the peer but get Connection refused — most mainnet nodes advertise ip6 in their ENR but don't actually listen on IPv6 TCP. Revert to IPv4-preferred: only rewrite node.ip to IPv6 in get_contact_to_initiate when the stored IPv4 is 0.0.0.0 (IPv6-only peer).
Adds a debug log when an outbound RLPx connection is attempted over IPv6 so we can confirm the initiator is actually dialing. Bumps TCP connection errors from debug to warn so they appear without enabling debug logging.
When record_enr_response_received changes a contact's IP (e.g. 0.0.0.0 to an IPv6 address), the node was still in already_tried_peers and the RLPx initiator would skip it indefinitely. Now we remove it from already_tried_peers whenever the IP changes so the initiator retries with the correct address.
enode_url() now wraps IPv6 addresses in brackets (e.g. [::1]:30303) so the URL is valid for both display and re-parsing as a bootnode. from_node() was adding ENR key-value pairs in non-alphabetical order (secp256k1 before ip), violating the ENR spec and producing a different signature than expected. Add sort_by to match from_network_config. Also add tcp6_port/udp6_port to the NodeRecordPairs literal in the discv5 messages test to fix the compile error from adding those fields.
ba70b26 to
d6be22a
Compare
aade806 to
0b2a359
Compare
Summary
start_network()now iterates overNetworkConfig.discovery_bind_addrsand binds a dedicated UDP socket per entry. A dual-stack node (e.g.--discovery.addr 0.0.0.0,::) gets two independent sockets, one for IPv4 and one for IPv6.Discv4ServerandDiscv5Serverinstances (backed by the same sharedPeerTable) and its ownDiscoveryMultiplexer. Responses always go back on the same-family socket they arrived on.fromendpoint: each server'slocal_nodecarries the external address for that socket's family, so discv4 Ping/Pong packets advertise the right address to remote peers.--discovery.addrbecomes a Vec: accepts comma-separated addresses (e.g.--discovery.addr 0.0.0.0,::) mirroring--p2p.addr. When omitted, defaults to the same set as--p2p.addr, so dual-stack RLPx automatically implies dual-stack discovery with no extra flags.NetworkConfigrefactor:discovery_bind_addr/discovery_external_addr(scalar) replaced bydiscovery_bind_addrs/discovery_external_addrs(Vec<IpAddr>);bind_udp_addr()replaced bybind_udp_addrs().Depends on
Usage
Single-stack IPv4 (unchanged default):
Dual-stack:
IPv6-only:
Override discovery to IPv4-only while RLPx is dual-stack:
Test plan
cargo checkpasses (verified locally)--discovery.addr 0.0.0.0,::: two UDP sockets visible vialsof -iUDP:30303