cmd/devp2p: support dual-stack discovery listener (#35220)
Some checks are pending
/ Linux Build (push) Waiting to run
/ Linux Build (arm) (push) Waiting to run
/ Keeper Build (push) Waiting to run
/ Windows Build (push) Waiting to run
/ Docker Image (push) Waiting to run

`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 <key> --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).
This commit is contained in:
Chase Wright 2026-06-29 08:55:42 -05:00 committed by GitHub
parent a0568b1dbf
commit 9700b5b10e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -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)
}
}