From 09f5fd6983fc74619790a4e48ad95c338b85217c Mon Sep 17 00:00:00 2001 From: Lucas Vasconcelos Date: Wed, 27 Nov 2024 18:08:23 -0300 Subject: [PATCH] p2p: add dynamic name resolution for the dialer --- p2p/dial.go | 46 ++++++++++++++++++++----- p2p/enode/node.go | 83 ++++++++++++++++++++++++++++++++++++++++++++++ p2p/enode/urlv4.go | 36 ++++++++++---------- p2p/server.go | 14 ++++++++ 4 files changed, 154 insertions(+), 25 deletions(-) diff --git a/p2p/dial.go b/p2p/dial.go index 24d4dc2e89..9752397f03 100644 --- a/p2p/dial.go +++ b/p2p/dial.go @@ -135,6 +135,7 @@ type dialConfig struct { log log.Logger clock mclock.Clock rand *mrand.Rand + ttl time.Duration } func (cfg dialConfig) withDefaults() dialConfig { @@ -274,13 +275,15 @@ loop: case node := <-d.addStaticCh: id := node.ID() _, exists := d.static[id] - d.log.Trace("Adding static node", "id", id, "ip", node.IPAddr(), "added", !exists) + d.log.Trace("Adding static node", "id", id, "addr", node.DisplayAddr(), "ip", node.IPAddr(), "added", !exists) if exists { continue loop } task := newDialTask(node, staticDialedConn) d.static[id] = task - if d.checkDial(node) == nil { + if err := d.checkDial(node); err != nil { + d.log.Trace("Discarding dial candidate", "id", node.ID(), "addr", node.DisplayAddr(), "ip", node.IPAddr(), "reason", err) + } else { d.addToStaticPool(task) } @@ -436,7 +439,7 @@ func (d *dialScheduler) removeFromStaticPool(idx int) { // startDial runs the given dial task in a separate goroutine. func (d *dialScheduler) startDial(task *dialTask) { node := task.dest() - d.log.Trace("Starting p2p dial", "id", node.ID(), "ip", node.IPAddr(), "flag", task.flags) + d.log.Trace("Starting p2p dial", "id", node.ID(), "addr", node.DisplayAddr(), "ip", node.IPAddr(), "flag", task.flags) hkey := string(node.ID().Bytes()) d.history.add(hkey, d.clock.Now().Add(dialHistoryExpiration)) d.dialing[node.ID()] = task @@ -473,7 +476,7 @@ func (t *dialTask) dest() *enode.Node { } func (t *dialTask) run(d *dialScheduler) { - if t.needResolve() && !t.resolve(d) { + if !t.resolveIfNeeded(d) { return } @@ -488,8 +491,36 @@ func (t *dialTask) run(d *dialScheduler) { } } -func (t *dialTask) needResolve() bool { - return t.flags&staticDialedConn != 0 && !t.dest().IPAddr().IsValid() +// resolveIfNeeded attempts to resolve the node's IP address if it is invalid. +// It returns true if the node's IP address is valid after resolution attempts. +func (t *dialTask) resolveIfNeeded(d *dialScheduler) bool { + node := t.dest() + + if t.flags&staticDialedConn != 0 { + if t.resolve(d) && node.IPAddr().IsValid() { + return true + } + } + + if node.NeedsDNSResolve() { + if t.resolveDNS(d) && node.IPAddr().IsValid() { + return true + } + } + + return false +} + +// resolveDNS attempts to resolve the DNS name of the destination node. +// It returns true if resolution succeeds. +func (t *dialTask) resolveDNS(d *dialScheduler) bool { + node := t.dest() + d.log.Trace("Starting DNS resolution", "id", node.ID(), "addr", node.DisplayAddr()) + if err := node.RefreshDNS(d.dialConfig.ttl); err != nil { + d.log.Trace("DNS resolution failed", "id", node.ID(), "addr", node.DisplayAddr()) + return false + } + return true } // resolve attempts to find the current endpoint for the destination @@ -533,8 +564,7 @@ func (t *dialTask) dial(d *dialScheduler, dest *enode.Node) error { dialMeter.Mark(1) fd, err := d.dialer.Dial(d.ctx, dest) if err != nil { - addr, _ := dest.TCPEndpoint() - d.log.Trace("Dial error", "id", dest.ID(), "addr", addr, "conn", t.flags, "err", cleanupDialErr(err)) + d.log.Trace("Dial error", "id", dest.ID(), "addr", dest.DisplayAddr(), "ip", dest.IPAddr(), "conn", t.flags, "err", cleanupDialErr(err)) dialConnectionError.Mark(1) return &dialError{err} } diff --git a/p2p/enode/node.go b/p2p/enode/node.go index 4d93d3f6be..ad1aaae558 100644 --- a/p2p/enode/node.go +++ b/p2p/enode/node.go @@ -26,6 +26,7 @@ import ( "net" "net/netip" "strings" + "time" "github.com/ethereum/go-ethereum/p2p/enr" "github.com/ethereum/go-ethereum/rlp" @@ -41,6 +42,10 @@ type Node struct { ip netip.Addr udp uint16 tcp uint16 + // dns information + dnsName string + dnsResolved time.Time + dnsTTL time.Duration } // New wraps a node record. The record must be valid according to the given @@ -257,6 +262,84 @@ func (n *Node) String() string { return "enr:" + b64 } +// resolveDNS attempts to resolve a DNS name to an IP address +func (n *Node) resolveDNS(dnsName string) (netip.Addr, error) { + ips, err := net.LookupIP(dnsName) + if err != nil { + return netip.Addr{}, err + } + for _, ip := range ips { + if ip4 := ip.To4(); ip4 != nil { + addr, ok := netip.AddrFromSlice(ip4) + if ok { + return addr, nil + } + } + } + // Fall back to IPv6 if no IPv4 is available + for _, ip := range ips { + addr, ok := netip.AddrFromSlice(ip) + if ok { + return addr, nil + } + } + + return netip.Addr{}, errors.New("no valid IP address found") +} + +// SetDNS sets the DNS name and resolves it to an IP address +func (n *Node) SetDNS(dnsName string, ttl time.Duration) error { + ip, err := n.resolveDNS(dnsName) + if err != nil { + return err + } + + n.dnsName = dnsName + n.dnsResolved = time.Now() + n.dnsTTL = ttl + + if ip.Is4() { + n.setIP4(ip) + } else { + n.setIP6(ip) + } + return nil +} + +// DNSName returns the stored DNS name +func (n *Node) DNSName() string { + return n.dnsName +} + +// RefreshDNS updates the IP address from the stored DNS name +func (n *Node) RefreshDNS(ttl time.Duration) error { + if n.dnsName == "" { + return errors.New("no DNS name set") + } + return n.SetDNS(n.dnsName, ttl) +} + +// DisplayAddr returns either "hostname:port" or "ip:port" +func (n *Node) DisplayAddr() string { + addr := n.dnsName + if addr == "" { + addr = n.ip.String() + } + return fmt.Sprintf("%s:%d", addr, n.tcp) +} + +// NeedsDNSResolve returns true if the node has a DNS name that needs resolution +func (n *Node) NeedsDNSResolve() bool { + if n.dnsName == "" { + return false + } + return !n.ip.IsValid() || time.Since(n.dnsResolved) > n.dnsTTL +} + +func (n *Node) GetTTL() time.Duration { + return n.dnsTTL +} + // MarshalText implements encoding.TextMarshaler. func (n *Node) MarshalText() ([]byte, error) { return []byte(n.String()), nil diff --git a/p2p/enode/urlv4.go b/p2p/enode/urlv4.go index a55dfa6632..f2ac90664b 100644 --- a/p2p/enode/urlv4.go +++ b/p2p/enode/urlv4.go @@ -126,20 +126,6 @@ func parseComplete(rawurl string) (*Node, error) { if id, err = parsePubkey(u.User.String()); err != nil { return nil, fmt.Errorf("invalid public key (%v)", err) } - // Parse the IP address. - ip := net.ParseIP(u.Hostname()) - if ip == nil { - ips, err := lookupIPFunc(u.Hostname()) - if err != nil { - return nil, err - } - ip = ips[0] - } - // Ensure the IP is 4 bytes long for IPv4 addresses. - if ipv4 := ip.To4(); ipv4 != nil { - ip = ipv4 - } - // Parse the port numbers. if tcpPort, err = strconv.ParseUint(u.Port(), 10, 16); err != nil { return nil, errors.New("invalid port") } @@ -151,7 +137,18 @@ func parseComplete(rawurl string) (*Node, error) { return nil, errors.New("invalid discport in query") } } - return NewV4(id, ip, int(tcpPort), int(udpPort)), nil + // Check if hostname is an IP address and create node accordingly + hostname := u.Hostname() + if ip := net.ParseIP(hostname); ip != nil { + // Create node with IP + node := NewV4(id, ip, int(tcpPort), int(udpPort)) + return node, nil + } + + // Create node for DNS name + node := NewV4(id, nil, int(tcpPort), int(udpPort)) + node.dnsName = hostname + return node, nil } // parsePubkey parses a hex-encoded secp256k1 public key. @@ -184,9 +181,14 @@ func (n *Node) URLv4() string { if !n.ip.IsValid() { u.Host = nodeid } else { - addr := net.TCPAddr{IP: n.IP(), Port: n.TCP()} u.User = url.User(nodeid) - u.Host = addr.String() + // Use DNS name if available, otherwise use IP + if n.DNSName() != "" { + u.Host = fmt.Sprintf("%s:%d", n.DNSName(), n.TCP()) + } else { + addr := net.TCPAddr{IP: n.IP(), Port: n.TCP()} + u.Host = addr.String() + } if n.UDP() != n.TCP() { u.RawQuery = "discport=" + strconv.Itoa(n.UDP()) } diff --git a/p2p/server.go b/p2p/server.go index 172f0667eb..6b08858cae 100644 --- a/p2p/server.go +++ b/p2p/server.go @@ -64,6 +64,10 @@ const ( // Maximum amount of time allowed for writing a complete message. frameWriteTimeout = 20 * time.Second + + defaultTTL = 5 * time.Minute + minTTL = 1 * time.Second + maxTTL = 24 * time.Hour ) var ( @@ -122,6 +126,11 @@ type Config struct { // allowed to connect, even above the peer limit. TrustedNodes []*enode.Node + // TTL is the time-to-live for DNS discovery queries + // This value is passed to the DNS resolvers when performing name + // resolutions. + TTL time.Duration `toml:",omitempty"` + // Connectivity can be restricted to certain IP networks. // If this option is set to a non-nil value, only hosts which match one of the // IP networks contained in the list are considered. @@ -620,6 +629,11 @@ func (srv *Server) setupDialScheduler() { if config.dialer == nil { config.dialer = tcpDialer{&net.Dialer{Timeout: defaultDialTimeout}} } + if srv.TTL >= minTTL && srv.TTL <= maxTTL { + config.ttl = srv.TTL + } else { + config.ttl = defaultTTL + } srv.dialsched = newDialScheduler(config, srv.discmix, srv.SetupConn) for _, n := range srv.StaticNodes { srv.dialsched.addStatic(n)