Skip to content

Dual-stack ENRs: should udp6/tcp6 always be set explicitly? go-ethereum and sigp/discv5 read EIP-778 differently #307

Description

@MysticRyuujin

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():

    discv5/src/ipmode.rs

    Lines 48 to 75 in 29cfa52

    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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions