mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-12 09:51:36 +00:00
p2p/discover: resolve DNS hostnames for bootstrap nodes
Since PR #30822 moved DNS resolution from parse-time to dial-time, bootstrap nodes specified with DNS hostnames (e.g. enode://<key>@hostname:30303) fail with "missing IP address" because setFallbackNodes calls ValidateComplete which requires a valid IP. This is a regression from v1.14.x that blocks multiple L2 networks (Omni, Optimism, Unichain) from upgrading to v1.15+. Fix by resolving DNS hostnames in setFallbackNodes before validation, mirroring the dnsResolveHostname pattern used for static nodes in the dial scheduler. The resolved node retains its hostname for potential future periodic re-resolution. Fixes #31208
This commit is contained in:
parent
acdd139717
commit
0962686a6e
2 changed files with 105 additions and 0 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue