I hit this setting up a UDP-only, dual-stack discovery bootnode and I'm not sure which behavior should be followed.
EIP-778 says that if udp6 is omitted, the udp port "applies to both IP addresses," and also that using the same port number for udp and udp6 "should be avoided." So for a node that listens on the same port for v4 and v6, the natural record is ip + ip6 + udp, with udp6 left out.
The problem is that record behaves differently depending on who reads it. go-ethereum applies the fallback: if udp6 is missing it uses udp for the v6 address too. The rust enr crate and sigp/discv5 don't. udp6() only reads the udp6 key, and get_contactable_addr won't build a v6 socket without it, so the node ends up not contactable over IPv6 at all. Same for tcp6/tcp.
So the same seemingly-compliant record is reachable over v6 from go-ethereum but not from sigp/discv5. I couldn't find this discussed anywhere, so it might just not be on anyone's radar.
Which behavior is correct? Should dual-stack nodes always set udp6/tcp6 explicitly, or is the fallback the intended reading and this should be fixed in sigp/discv5 / the enr crate?
References (line-pinned to current commits)
- EIP-778: https://eips.ethereum.org/EIPS/eip-778
- go-ethereum applies the fallback —
node.go setIP6() loads UDP6/TCP6 and falls back to UDP/TCP when absent: https://github.com/ethereum/go-ethereum/blob/a8a7116395ed435fe223ee75160d3eabddb1a73a/p2p/enode/node.go#L121-L133
sigp/discv5 requires an explicit v6 port — ipmode.rs get_contactable_addr() / canonical_ipv6_enr_addr():
|
pub fn get_contactable_addr(&self, enr: &Enr) -> Option<SocketAddr> { |
|
// A function to get a canonical ipv6 address from an Enr |
|
|
|
/// NOTE: There is nothing in the spec preventing compat/mapped addresses from being |
|
/// transmitted in the ENR. Here we choose to enforce canonical addresses since |
|
/// it simplifies the logic of matching socket_addr verification. For this we prevent |
|
/// communications with Ipv4 addresses advertised in the Ipv6 field. |
|
fn canonical_ipv6_enr_addr(enr: &Enr) -> Option<std::net::SocketAddrV6> { |
|
enr.udp6_socket().and_then(|socket_addr| { |
|
if to_ipv4_mapped(socket_addr.ip()).is_some() { |
|
None |
|
} else { |
|
Some(socket_addr) |
|
} |
|
}) |
|
} |
|
|
|
match self { |
|
Ip4 => enr.udp4_socket().map(SocketAddr::V4), |
|
Ip6 => canonical_ipv6_enr_addr(enr).map(SocketAddr::V6), |
|
DualStack => { |
|
canonical_ipv6_enr_addr(enr) |
|
.map(SocketAddr::V6) |
|
// NOTE: general consensus is that ipv6 addresses should be preferred. |
|
.or_else(|| enr.udp4_socket().map(SocketAddr::V4)) |
|
} |
|
} |
|
} |
- …which calls the
enr crate's udp6_socket(), needing both ip6 and udp6 (no fallback to udp): https://github.com/sigp/enr/blob/6bb8bdf0359f89fbc37304ab5ee6081835b1c051/src/lib.rs#L405-L429
I hit this setting up a UDP-only, dual-stack discovery bootnode and I'm not sure which behavior should be followed.
EIP-778 says that if
udp6is omitted, theudpport "applies to both IP addresses," and also that using the same port number forudpandudp6"should be avoided." So for a node that listens on the same port for v4 and v6, the natural record isip+ip6+udp, withudp6left out.The problem is that record behaves differently depending on who reads it. go-ethereum applies the fallback: if
udp6is missing it usesudpfor the v6 address too. The rustenrcrate andsigp/discv5don't.udp6()only reads theudp6key, andget_contactable_addrwon't build a v6 socket without it, so the node ends up not contactable over IPv6 at all. Same fortcp6/tcp.So the same seemingly-compliant record is reachable over v6 from go-ethereum but not from sigp/discv5. I couldn't find this discussed anywhere, so it might just not be on anyone's radar.
Which behavior is correct? Should dual-stack nodes always set
udp6/tcp6explicitly, or is the fallback the intended reading and this should be fixed insigp/discv5/ theenrcrate?References (line-pinned to current commits)
node.gosetIP6()loadsUDP6/TCP6and falls back toUDP/TCPwhen absent: https://github.com/ethereum/go-ethereum/blob/a8a7116395ed435fe223ee75160d3eabddb1a73a/p2p/enode/node.go#L121-L133sigp/discv5requires an explicit v6 port —ipmode.rsget_contactable_addr()/canonical_ipv6_enr_addr():discv5/src/ipmode.rs
Lines 48 to 75 in 29cfa52
enrcrate'sudp6_socket(), needing bothip6andudp6(no fallback toudp): https://github.com/sigp/enr/blob/6bb8bdf0359f89fbc37304ab5ee6081835b1c051/src/lib.rs#L405-L429