Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions p2p/discover/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ package discover
import (
"context"
"fmt"
"net"
"net/netip"
"slices"
"sync"
Expand All @@ -36,6 +37,7 @@ import (
"github.com/CortexFoundation/CortexTheseus/log"
"github.com/CortexFoundation/CortexTheseus/metrics"
"github.com/CortexFoundation/CortexTheseus/p2p/enode"
"github.com/CortexFoundation/CortexTheseus/p2p/enr"
"github.com/CortexFoundation/CortexTheseus/p2p/netutil"
)

Expand Down Expand Up @@ -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)
}
Expand All @@ -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 {
Expand Down
60 changes: 60 additions & 0 deletions p2p/discover/table_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading