feat: NonPublicAddrPublishing option (#3489)

Co-authored-by: Marco Munizaga <git@marcopolo.io>
This commit is contained in:
Marcin Rataj
2026-04-22 05:52:49 +02:00
committed by GitHub
parent 6572945550
commit 41462353c6
5 changed files with 194 additions and 44 deletions
+18 -15
View File
@@ -125,6 +125,8 @@ type Config struct {
DisablePing bool
DisableNonPublicAddrPublishing bool
Routing RoutingC
EnableAutoRelay bool
@@ -442,21 +444,22 @@ func (cfg *Config) addTransports() ([]fx.Option, error) {
func (cfg *Config) newBasicHost(swrm *swarm.Swarm, eventBus event.Bus, an *autonatv2.AutoNAT, o bhost.ObservedAddrsManager) (*bhost.BasicHost, error) {
h, err := bhost.NewHost(swrm, &bhost.HostOpts{
EventBus: eventBus,
ConnManager: cfg.ConnManager,
AddrsFactory: cfg.AddrsFactory,
NATManager: cfg.NATManager,
EnablePing: !cfg.DisablePing,
UserAgent: cfg.UserAgent,
ProtocolVersion: cfg.ProtocolVersion,
EnableHolePunching: cfg.EnableHolePunching,
HolePunchingOptions: cfg.HolePunchingOptions,
EnableRelayService: cfg.EnableRelayService,
RelayServiceOpts: cfg.RelayServiceOpts,
EnableMetrics: !cfg.DisableMetrics,
PrometheusRegisterer: cfg.PrometheusRegisterer,
AutoNATv2: an,
ObservedAddrsManager: o,
EventBus: eventBus,
ConnManager: cfg.ConnManager,
AddrsFactory: cfg.AddrsFactory,
NATManager: cfg.NATManager,
EnablePing: !cfg.DisablePing,
UserAgent: cfg.UserAgent,
ProtocolVersion: cfg.ProtocolVersion,
EnableHolePunching: cfg.EnableHolePunching,
HolePunchingOptions: cfg.HolePunchingOptions,
EnableRelayService: cfg.EnableRelayService,
RelayServiceOpts: cfg.RelayServiceOpts,
EnableMetrics: !cfg.DisableMetrics,
PrometheusRegisterer: cfg.PrometheusRegisterer,
DisableNonPublicAddrPublishing: cfg.DisableNonPublicAddrPublishing,
AutoNATv2: an,
ObservedAddrsManager: o,
})
if err != nil {
return nil, err
+15
View File
@@ -441,6 +441,21 @@ func Ping(enable bool) Option {
}
}
// NonPublicAddrPublishing controls whether the host advertises addresses that
// are not in a globally-routable range (RFC 1918 private, RFC 6598 CGNAT,
// link-local, loopback, ULA, IPv6 documentation/multicast/reserved space)
// through the peerstore and signed peer records. Multiaddrs without an IP
// component such as /p2p-circuit are not affected.
//
// Defaults to true for backward compatibility. Set to false on public-facing
// nodes to avoid leaking internal topology through identify and DHT records.
func NonPublicAddrPublishing(enable bool) Option {
return func(cfg *Config) error {
cfg.DisableNonPublicAddrPublishing = !enable
return nil
}
}
// Routing will configure libp2p to use routing.
func Routing(rt config.RoutingC) Option {
return func(cfg *Config) error {
+60 -21
View File
@@ -71,10 +71,11 @@ type addrsManager struct {
addrsMx sync.RWMutex
currentAddrs hostAddrs
signKey crypto.PrivKey
addrStore addrStore
signedRecordStore peerstore.CertifiedAddrBook
hostID peer.ID
signKey crypto.PrivKey
addrStore addrStore
signedRecordStore peerstore.CertifiedAddrBook
hostID peer.ID
disableNonPublicAddrPublishing bool
wg sync.WaitGroup
ctx context.Context
@@ -92,26 +93,28 @@ func newAddrsManager(
enableMetrics bool,
registerer prometheus.Registerer,
disableSignedPeerRecord bool,
disableNonPublicAddrPublishing bool,
signKey crypto.PrivKey,
addrStore addrStore,
hostID peer.ID,
) (*addrsManager, error) {
ctx, cancel := context.WithCancel(context.Background())
as := &addrsManager{
bus: bus,
listenAddrs: listenAddrs,
addCertHashes: addCertHashes,
observedAddrsManager: observedAddrsManager,
natManager: natmgr,
addrsFactory: addrsFactory,
triggerAddrsUpdateChan: make(chan chan struct{}, 1),
triggerReachabilityUpdate: make(chan struct{}, 1),
interfaceAddrs: &interfaceAddrsCache{},
signKey: signKey,
addrStore: addrStore,
hostID: hostID,
ctx: ctx,
ctxCancel: cancel,
bus: bus,
listenAddrs: listenAddrs,
addCertHashes: addCertHashes,
observedAddrsManager: observedAddrsManager,
natManager: natmgr,
addrsFactory: addrsFactory,
triggerAddrsUpdateChan: make(chan chan struct{}, 1),
triggerReachabilityUpdate: make(chan struct{}, 1),
interfaceAddrs: &interfaceAddrsCache{},
signKey: signKey,
addrStore: addrStore,
hostID: hostID,
disableNonPublicAddrPublishing: disableNonPublicAddrPublishing,
ctx: ctx,
ctxCancel: cancel,
}
unknownReachability := network.ReachabilityUnknown
as.hostReachability.Store(&unknownReachability)
@@ -343,8 +346,11 @@ func (a *addrsManager) updateAddrs(prevHostAddrs hostAddrs, relayAddrs []ma.Mult
// updatePeerStore updates the peer store for the host
func (a *addrsManager) updatePeerStore(currentAddrs []ma.Multiaddr, removedAddrs []ma.Multiaddr) {
// update host addresses in the peer store
a.addrStore.SetAddrs(a.hostID, currentAddrs, peerstore.PermanentAddrTTL)
publishedAddrs := currentAddrs
if a.disableNonPublicAddrPublishing {
publishedAddrs = filterPublicAddrs(currentAddrs)
}
a.addrStore.SetAddrs(a.hostID, publishedAddrs, peerstore.PermanentAddrTTL)
a.addrStore.SetAddrs(a.hostID, removedAddrs, 0)
var sr *record.Envelope
@@ -354,7 +360,7 @@ func (a *addrsManager) updatePeerStore(currentAddrs []ma.Multiaddr, removedAddrs
var err error
// add signed peer record to the event
// in case of an error drop this event.
sr, err = a.makeSignedPeerRecord(currentAddrs)
sr, err = a.makeSignedPeerRecord(publishedAddrs)
if err != nil {
log.Error("error creating a signed peer record from the set of current addresses", "err", err)
return
@@ -366,6 +372,39 @@ func (a *addrsManager) updatePeerStore(currentAddrs []ma.Multiaddr, removedAddrs
}
}
// filterPublicAddrs drops IP-based multiaddrs that are not in a globally
// routable range. Addrs without an IP or DNS component (e.g. /p2p-circuit)
// are kept as-is because manet.IsPublicAddr returns false for them.
// DNS components are evaluated by manet.IsPublicAddr (special-use names
// like .local, .invalid, .localhost are non-public).
func filterPublicAddrs(addrs []ma.Multiaddr) []ma.Multiaddr {
filtered := make([]ma.Multiaddr, 0, len(addrs))
for _, addr := range addrs {
if hasIPOrDNSComponent(addr) && !manet.IsPublicAddr(addr) {
continue
}
filtered = append(filtered, addr)
}
return filtered
}
// hasIPOrDNSComponent reports whether addr's leading component is an IP,
// DNS, or IP6ZONE wrapper. Transport multiaddrs encode their network layer
// at the front, so the leading component is sufficient to tell whether
// manet.IsPublicAddr can meaningfully evaluate the addr. Without this
// guard, filterPublicAddrs would also drop multiaddrs that have no
// network-layer address, such as /p2p-circuit/p2p/<id>.
func hasIPOrDNSComponent(addr ma.Multiaddr) bool {
if len(addr) == 0 {
return false
}
switch addr[0].Protocol().Code {
case ma.P_IP4, ma.P_IP6, ma.P_IP6ZONE, ma.P_DNS, ma.P_DNS4, ma.P_DNS6, ma.P_DNSADDR:
return true
}
return false
}
func (a *addrsManager) notifyAddrsUpdated(emitter event.Emitter, localAddrsEmitter event.Emitter, previous, current hostAddrs) {
if areAddrsDifferent(previous.localAddrs, current.localAddrs) {
log.Debug("host local addresses updated", "addrs", current.localAddrs)
+92 -8
View File
@@ -64,14 +64,15 @@ type addrStoreArgs struct {
}
type addrsManagerArgs struct {
NATManager NATManager
AddrsFactory AddrsFactory
ObservedAddrsManager ObservedAddrsManager
ListenAddrs func() []ma.Multiaddr
AddCertHashes func([]ma.Multiaddr) []ma.Multiaddr
AutoNATClient autonatv2Client
Bus event.Bus
AddrStoreArgs addrStoreArgs
NATManager NATManager
AddrsFactory AddrsFactory
ObservedAddrsManager ObservedAddrsManager
ListenAddrs func() []ma.Multiaddr
AddCertHashes func([]ma.Multiaddr) []ma.Multiaddr
AutoNATClient autonatv2Client
Bus event.Bus
AddrStoreArgs addrStoreArgs
DisableNonPublicAddrPublishing bool
}
type addrsManagerTestCase struct {
@@ -118,6 +119,7 @@ func newAddrsManagerTestCase(tb testing.TB, args addrsManagerArgs) addrsManagerT
true,
prometheus.DefaultRegisterer,
false,
args.DisableNonPublicAddrPublishing,
signKey,
addrStore,
pid,
@@ -488,6 +490,88 @@ func TestAddrsManagerPeerstoreUpdated(t *testing.T) {
}
func TestAddrsManagerNonPublicAddrPublishing(t *testing.T) {
publicV4 := ma.StringCast("/ip4/1.2.3.4/udp/1/quic-v1")
publicV6 := ma.StringCast("/ip6/2001:41d0:203:2ca6::/udp/4001/quic-v1")
loopback4 := ma.StringCast("/ip4/127.0.0.1/udp/1/quic-v1")
loopback6 := ma.StringCast("/ip6/::1/udp/1/quic-v1")
rfc1918 := ma.StringCast("/ip4/192.168.1.5/tcp/4001")
cgnat := ma.StringCast("/ip4/100.64.0.1/tcp/4001")
linkLocal4 := ma.StringCast("/ip4/169.254.10.10/tcp/4001")
ula := ma.StringCast("/ip6/fc00::1/tcp/4001")
linkLocal6 := ma.StringCast("/ip6/fe80::1/tcp/4001")
reservedV6 := ma.StringCast("/ip6/1e::109d:0:2:c80b/tcp/4001")
docV6 := ma.StringCast("/ip6/2001:db8::1/tcp/4001")
circuit := ma.StringCast("/p2p/12D3KooWGyVU3Z7iEFEKnLRWUZSCgZkruxXt9TafKigQv9TUx2N1/p2p-circuit")
dnsPublic := ma.StringCast("/dns4/example.com/tcp/443/wss")
dnsLocal := ma.StringCast("/dns4/foo.local/tcp/443")
zonedLinkLocal6 := ma.StringCast("/ip6zone/eth0/ip6/fe80::1/tcp/4001")
all := []ma.Multiaddr{
publicV4, publicV6,
loopback4, loopback6,
rfc1918, cgnat, linkLocal4,
ula, linkLocal6,
reservedV6, docV6,
circuit, dnsPublic, dnsLocal,
zonedLinkLocal6,
}
t.Run("publishes everything by default", func(t *testing.T) {
pstore, err := pstoremem.NewPeerstore()
require.NoError(t, err)
cab, _ := peerstore.GetCertifiedAddrBook(pstore)
signKey, _, err := crypto.GenerateEd25519Key(rand.Reader)
require.NoError(t, err)
pid, err := peer.IDFromPrivateKey(signKey)
require.NoError(t, err)
am := newAddrsManagerTestCase(t, addrsManagerArgs{
ListenAddrs: func() []ma.Multiaddr { return nil },
AddrsFactory: func([]ma.Multiaddr) []ma.Multiaddr { return all },
AddrStoreArgs: addrStoreArgs{
AddrStore: pstore,
HostID: pid,
SignKey: signKey,
},
})
defer am.Close()
require.ElementsMatch(t, all, pstore.Addrs(pid))
pr := peerRecordFromEnvelope(t, cab.GetPeerRecord(pid))
require.ElementsMatch(t, all, pr.Addrs)
})
t.Run("strips non-public IP addrs when publishing is disabled", func(t *testing.T) {
pstore, err := pstoremem.NewPeerstore()
require.NoError(t, err)
cab, _ := peerstore.GetCertifiedAddrBook(pstore)
signKey, _, err := crypto.GenerateEd25519Key(rand.Reader)
require.NoError(t, err)
pid, err := peer.IDFromPrivateKey(signKey)
require.NoError(t, err)
am := newAddrsManagerTestCase(t, addrsManagerArgs{
ListenAddrs: func() []ma.Multiaddr { return nil },
AddrsFactory: func([]ma.Multiaddr) []ma.Multiaddr { return all },
AddrStoreArgs: addrStoreArgs{
AddrStore: pstore,
HostID: pid,
SignKey: signKey,
},
DisableNonPublicAddrPublishing: true,
})
defer am.Close()
// kept: public v4/v6, /p2p-circuit (no IP), public DNS
// stripped: loopback, RFC1918, CGNAT, link-local (incl. ip6zone-wrapped), ULA, reserved/doc IPv6, .local DNS
expected := []ma.Multiaddr{publicV4, publicV6, circuit, dnsPublic}
require.ElementsMatch(t, expected, pstore.Addrs(pid))
pr := peerRecordFromEnvelope(t, cab.GetPeerRecord(pid))
require.ElementsMatch(t, expected, pr.Addrs)
})
}
func TestRemoveIfNotInSource(t *testing.T) {
addrs := make([]ma.Multiaddr, 0, 10)
for i := range 10 {
+9
View File
@@ -127,6 +127,14 @@ type HostOpts struct {
// DisableSignedPeerRecord disables the generation of Signed Peer Records on this host.
DisableSignedPeerRecord bool
// DisableNonPublicAddrPublishing excludes addresses that are not in a
// globally-routable range (e.g. RFC 1918 private, RFC 6598 CGNAT, link-local,
// loopback, ULA, IPv6 documentation/multicast/reserved space) from the
// host's peerstore entry and signed peer record. This prevents leaking
// non-public IP addresses to remote peers via identify or the DHT.
// Multiaddrs without an IP component such as /p2p-circuit are not affected.
DisableNonPublicAddrPublishing bool
// EnableHolePunching enables the peer to initiate/respond to hole punching attempts for NAT traversal.
EnableHolePunching bool
// HolePunchingOptions are options for the hole punching service
@@ -238,6 +246,7 @@ func NewHost(n network.Network, opts *HostOpts) (*BasicHost, error) {
opts.EnableMetrics,
opts.PrometheusRegisterer,
opts.DisableSignedPeerRecord,
opts.DisableNonPublicAddrPublishing,
h.Peerstore().PrivKey(h.ID()),
h.Peerstore(),
h.ID(),