Skip to content

Implement IPv6 external address auto-discovery via DiscV5 newAddressHandler #9874

@usmansaleem

Description

@usmansaleem

Description

When a node receives PONG responses from remote peers during DiscV5 discovery, the discovery library extracts the IP/port the peer observed the local node connecting from. Once ≥2 independent peers confirm the same external address (multi-peer consensus), the newAddressHandler callback is invoked with the peer-observed address.

Besu currently returns Optional.empty() from this handler, ignoring all peer feedback:

// PeerDiscoveryAgentV5.java
.newAddressHandler((nodeRecord, newAddress) -> Optional.empty())

This is safe for IPv4 because NatService (UPnP/NAT-PMP) already handles external address discovery. However, for IPv6, NatService does not apply — IPv6 addresses are globally routable and there is no NAT translation. Peer-observed addresses would be the only auto-discovery mechanism for a node that has not explicitly configured --p2p-host-ipv6.

Proposed behaviour

  • If the peer-observed address is IPv6 and --p2p-host-ipv6 is not pinned by the operator → accept the peer-suggested address, update the local ENR, re-sign and persist
  • If --p2p-host-ipv6 is explicitly configured → continue ignoring peer feedback (operator has pinned the address intentionally)
  • IPv4 peer feedback → continue ignoring (NatService handles it)

Required changes

NodeRecordManager

Add a method to update the advertised IPv6 host in-place so the existing localNodeRecordListenerupdateNodeRecord() chain picks it up and persists correctly:

public void updateAdvertisedIpv6Host(final String newHost) {
    ipv6Endpoint = ipv6Endpoint.map(ep ->
        new HostEndpoint(newHost, ep.discoveryPort(), ep.tcpPort()));
}

PeerDiscoveryAgentV5

Replace the Optional.empty() no-op with conditional logic:

.newAddressHandler((oldRecord, newAddress) -> {
    if (isIpv6(newAddress) && !operatorHasPinnedIpv6Host()) {
        nodeRecordManager.updateAdvertisedIpv6Host(newAddress.getAddress().getHostAddress());
        return nodeRecordManager.getLocalNode()
            .flatMap(DiscoveryPeer::getNodeRecord)
            .or(() -> Optional.of(oldRecord));
    }
    return Optional.empty();
})

Update flow

newAddressHandler fires (≥2 peers confirm peer-observed IPv6 address)
  → nodeRecordManager.updateAdvertisedIpv6Host(newHost)
  → return Optional.of(rebuilt record)
  → discovery library stores new local record
    → localNodeRecordListener fires
      → nodeRecordManager.updateNodeRecord()  ← re-signs, persists to disk

Relationship to other issues

Metadata

Metadata

Assignees

Type

No type

Projects

Status

Todo

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions