p2p/discover: resolve DNS hostnames for bootstrap nodes (#34101)

Fixes #31208
This commit is contained in:
Charles Dusek 2026-03-28 05:37:39 -05:00 committed by GitHub
parent c3467dd8b5
commit a2496852e9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 105 additions and 0 deletions

View file

@ -25,6 +25,7 @@ package discover
import (
"context"
"fmt"
"net"
"net/netip"
"slices"
"sync"
@ -36,6 +37,7 @@ import (
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/metrics"
"github.com/ethereum/go-ethereum/p2p/enode"
"github.com/ethereum/go-ethereum/p2p/enr"
"github.com/ethereum/go-ethereum/p2p/netutil"
)
@ -205,6 +207,13 @@ func (tab *Table) close() {
func (tab *Table) setFallbackNodes(nodes []*enode.Node) error {
nursery := make([]*enode.Node, 0, len(nodes))
for _, n := range nodes {
if n.Hostname() != "" && !n.IPAddr().IsValid() {
resolved, err := resolveBootnodeHostname(n, tab.log)
if err != nil {
return fmt.Errorf("bad bootstrap node %q: %v", n, err)
}
n = resolved
}
if err := n.ValidateComplete(); err != nil {
return fmt.Errorf("bad bootstrap node %q: %v", n, err)
}
@ -218,6 +227,42 @@ func (tab *Table) setFallbackNodes(nodes []*enode.Node) error {
return nil
}
// resolveBootnodeHostname resolves the DNS hostname of a bootstrap node to an IP address.
func resolveBootnodeHostname(n *enode.Node, logger log.Logger) (*enode.Node, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
ips, err := net.DefaultResolver.LookupNetIP(ctx, "ip", n.Hostname())
if err != nil {
return nil, fmt.Errorf("DNS lookup failed for %q: %v", n.Hostname(), err)
}
var ip4, ip6 netip.Addr
for _, ip := range ips {
if ip.Is4() && !ip4.IsValid() {
ip4 = ip
}
if ip.Is6() && !ip6.IsValid() {
ip6 = ip
}
}
if !ip4.IsValid() && !ip6.IsValid() {
return nil, fmt.Errorf("no IP addresses found for hostname %q", n.Hostname())
}
rec := n.Record()
if ip4.IsValid() {
rec.Set(enr.IPv4Addr(ip4))
}
if ip6.IsValid() {
rec.Set(enr.IPv6Addr(ip6))
}
rec.SetSeq(n.Seq())
resolved := enode.SignNull(rec, n.ID()).WithHostname(n.Hostname())
logger.Debug("Resolved bootstrap node hostname", "name", n.Hostname(), "ip", resolved.IP())
return resolved, nil
}
// isInitDone returns whether the table's initial seeding procedure has completed.
func (tab *Table) isInitDone() bool {
select {

View file

@ -490,6 +490,66 @@ func quickcfg() *quick.Config {
}
}
func TestSetFallbackNodes_DNSHostname(t *testing.T) {
// Create a node with a DNS hostname but no IP, simulating an enode URL
// like enode://<key>@localhost:30303.
key := newkey()
node := enode.NewV4(&key.PublicKey, nil, 30303, 30303).WithHostname("localhost")
// Verify the node has a hostname but no valid IP.
if node.Hostname() != "localhost" {
t.Fatal("expected hostname to be set")
}
if node.IPAddr().IsValid() {
t.Fatal("expected no IP address")
}
// Create a table and set the hostname node as a bootnode.
// This should resolve the hostname to an IP address.
db, _ := enode.OpenDB(t.TempDir() + "/node.db")
defer db.Close()
cfg := Config{Log: testlog.Logger(t, log.LvlTrace)}
cfg = cfg.withDefaults()
tab := &Table{
cfg: cfg,
log: cfg.Log,
refreshReq: make(chan chan struct{}),
revalResponseCh: make(chan revalidationResponse),
addNodeCh: make(chan addNodeOp),
addNodeHandled: make(chan bool),
trackRequestCh: make(chan trackRequestOp),
initDone: make(chan struct{}),
closeReq: make(chan struct{}),
closed: make(chan struct{}),
ips: netutil.DistinctNetSet{Subnet: tableSubnet, Limit: tableIPLimit},
}
for i := range tab.buckets {
tab.buckets[i] = &bucket{
index: i,
ips: netutil.DistinctNetSet{Subnet: bucketSubnet, Limit: bucketIPLimit},
}
}
err := tab.setFallbackNodes([]*enode.Node{node})
if err != nil {
t.Fatalf("setFallbackNodes failed: %v", err)
}
if len(tab.nursery) != 1 {
t.Fatalf("expected 1 nursery node, got %d", len(tab.nursery))
}
// The resolved node should have a valid IP and retain the hostname.
resolved := tab.nursery[0]
if !resolved.IPAddr().IsValid() {
t.Fatal("expected resolved node to have a valid IP")
}
if resolved.Hostname() != "localhost" {
t.Errorf("expected hostname to be preserved, got %q", resolved.Hostname())
}
t.Logf("resolved localhost to %v", resolved.IPAddr())
}
func newkey() *ecdsa.PrivateKey {
key, err := crypto.GenerateKey()
if err != nil {