From 9700b5b10eee6ea3e0f71eaa2da3875e25047c7b Mon Sep 17 00:00:00 2001 From: Chase Wright Date: Mon, 29 Jun 2026 08:55:42 -0500 Subject: [PATCH] cmd/devp2p: support dual-stack discovery listener (#35220) `devp2p discv4 listen` / `discv5 listen` is the supported replacement for the removed bootnode tool, but it bound IPv4-only and `-extaddr` took a single address, so it couldn't run a dual-stack bootnode. This binds the listener dual-stack (falling back to IPv4-only where IPv6 is unavailable) and lets `-extaddr` take a comma-separated IPv4/IPv6 pair. A single node can then advertise both `ip` and `ip6` in its ENR over one UDP port: ``` devp2p discv4 listen --nodekey --addr [::]:30301 \ --extaddr 203.0.113.10:30301,[2001:db8::1]:30301 ``` The fallback IP is only derived from the listener when no `-extaddr` is given, so a v4- or v6-only `-extaddr` no longer leaks a loopback entry. All addresses must share one UDP port (single socket). --- cmd/devp2p/discv4cmd.go | 60 ++++++++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/cmd/devp2p/discv4cmd.go b/cmd/devp2p/discv4cmd.go index 84c7ef0c44..1089210c57 100644 --- a/cmd/devp2p/discv4cmd.go +++ b/cmd/devp2p/discv4cmd.go @@ -126,7 +126,7 @@ var ( } extAddrFlag = &cli.StringFlag{ Name: "extaddr", - Usage: "UDP endpoint announced in ENR. You can provide a bare IP address or IP:port as the value of this flag.", + Usage: "UDP endpoint announced in ENR. You can provide a bare IP address or IP:port as the value of this flag. Provide a comma-separated pair to announce both an IPv4 and an IPv6 endpoint.", } crawlTimeoutFlag = &cli.DurationFlag{ Name: "timeout", @@ -344,36 +344,60 @@ func parseExtAddr(spec string) (ip net.IP, port int, ok bool) { func listen(ctx *cli.Context, ln *enode.LocalNode) *net.UDPConn { addr := ctx.String(listenAddrFlag.Name) + extAddr := ctx.String(extAddrFlag.Name) + var ( + socket net.PacketConn + err error + ) if addr == "" { - addr = "0.0.0.0:0" + // Dual-stack socket, falling back to IPv4-only where IPv6 is unavailable. + if socket, err = net.ListenPacket("udp", "[::]:0"); err != nil { + socket, err = net.ListenPacket("udp", "0.0.0.0:0") + } + } else { + socket, err = net.ListenPacket("udp", addr) } - socket, err := net.ListenPacket("udp4", addr) if err != nil { exit(err) } - // Configure UDP endpoint in ENR from listener address. + // Configure the ENR endpoint from the listener address, but only without an + // explicit -extaddr: otherwise we'd announce a fallback IP for an address + // family the user didn't specify (e.g. loopback IPv4 on an IPv6-only node). usocket := socket.(*net.UDPConn) uaddr := socket.LocalAddr().(*net.UDPAddr) - if uaddr.IP.IsUnspecified() { - ln.SetFallbackIP(net.IP{127, 0, 0, 1}) - } else { - ln.SetFallbackIP(uaddr.IP) + if extAddr == "" { + if uaddr.IP.IsUnspecified() { + ln.SetFallbackIP(net.IP{127, 0, 0, 1}) + } else { + ln.SetFallbackIP(uaddr.IP) + } } ln.SetFallbackUDP(uaddr.Port) - // If an ENR endpoint is set explicitly on the command-line, override - // the information from the listening address. Note this is careful not - // to set the UDP port if the external address doesn't have it. - extAddr := ctx.String(extAddrFlag.Name) + // Override with explicit -extaddr address(es). A static IP is set per family, + // and all specs share one UDP port because the node has a single socket. if extAddr != "" { - ip, port, ok := parseExtAddr(extAddr) - if !ok { - exit(fmt.Errorf("-%s: invalid external address %q", extAddrFlag.Name, extAddr)) + var extPort int + for spec := range strings.SplitSeq(extAddr, ",") { + spec = strings.TrimSpace(spec) + if spec == "" { + continue + } + ip, port, ok := parseExtAddr(spec) + if !ok { + exit(fmt.Errorf("-%s: invalid external address %q", extAddrFlag.Name, spec)) + } + ln.SetStaticIP(ip) + if port != 0 { + if extPort != 0 && port != extPort { + exit(fmt.Errorf("-%s: all addresses must announce the same UDP port, got %d and %d", extAddrFlag.Name, extPort, port)) + } + extPort = port + } } - ln.SetStaticIP(ip) - if port != 0 { - ln.SetFallbackUDP(port) + if extPort != 0 { + ln.SetFallbackUDP(extPort) } }