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 DisablePing bool
DisableNonPublicAddrPublishing bool
Routing RoutingC Routing RoutingC
EnableAutoRelay bool 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) { 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{ h, err := bhost.NewHost(swrm, &bhost.HostOpts{
EventBus: eventBus, EventBus: eventBus,
ConnManager: cfg.ConnManager, ConnManager: cfg.ConnManager,
AddrsFactory: cfg.AddrsFactory, AddrsFactory: cfg.AddrsFactory,
NATManager: cfg.NATManager, NATManager: cfg.NATManager,
EnablePing: !cfg.DisablePing, EnablePing: !cfg.DisablePing,
UserAgent: cfg.UserAgent, UserAgent: cfg.UserAgent,
ProtocolVersion: cfg.ProtocolVersion, ProtocolVersion: cfg.ProtocolVersion,
EnableHolePunching: cfg.EnableHolePunching, EnableHolePunching: cfg.EnableHolePunching,
HolePunchingOptions: cfg.HolePunchingOptions, HolePunchingOptions: cfg.HolePunchingOptions,
EnableRelayService: cfg.EnableRelayService, EnableRelayService: cfg.EnableRelayService,
RelayServiceOpts: cfg.RelayServiceOpts, RelayServiceOpts: cfg.RelayServiceOpts,
EnableMetrics: !cfg.DisableMetrics, EnableMetrics: !cfg.DisableMetrics,
PrometheusRegisterer: cfg.PrometheusRegisterer, PrometheusRegisterer: cfg.PrometheusRegisterer,
AutoNATv2: an, DisableNonPublicAddrPublishing: cfg.DisableNonPublicAddrPublishing,
ObservedAddrsManager: o, AutoNATv2: an,
ObservedAddrsManager: o,
}) })
if err != nil { if err != nil {
return nil, err 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. // Routing will configure libp2p to use routing.
func Routing(rt config.RoutingC) Option { func Routing(rt config.RoutingC) Option {
return func(cfg *Config) error { return func(cfg *Config) error {
+60 -21
View File
@@ -71,10 +71,11 @@ type addrsManager struct {
addrsMx sync.RWMutex addrsMx sync.RWMutex
currentAddrs hostAddrs currentAddrs hostAddrs
signKey crypto.PrivKey signKey crypto.PrivKey
addrStore addrStore addrStore addrStore
signedRecordStore peerstore.CertifiedAddrBook signedRecordStore peerstore.CertifiedAddrBook
hostID peer.ID hostID peer.ID
disableNonPublicAddrPublishing bool
wg sync.WaitGroup wg sync.WaitGroup
ctx context.Context ctx context.Context
@@ -92,26 +93,28 @@ func newAddrsManager(
enableMetrics bool, enableMetrics bool,
registerer prometheus.Registerer, registerer prometheus.Registerer,
disableSignedPeerRecord bool, disableSignedPeerRecord bool,
disableNonPublicAddrPublishing bool,
signKey crypto.PrivKey, signKey crypto.PrivKey,
addrStore addrStore, addrStore addrStore,
hostID peer.ID, hostID peer.ID,
) (*addrsManager, error) { ) (*addrsManager, error) {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
as := &addrsManager{ as := &addrsManager{
bus: bus, bus: bus,
listenAddrs: listenAddrs, listenAddrs: listenAddrs,
addCertHashes: addCertHashes, addCertHashes: addCertHashes,
observedAddrsManager: observedAddrsManager, observedAddrsManager: observedAddrsManager,
natManager: natmgr, natManager: natmgr,
addrsFactory: addrsFactory, addrsFactory: addrsFactory,
triggerAddrsUpdateChan: make(chan chan struct{}, 1), triggerAddrsUpdateChan: make(chan chan struct{}, 1),
triggerReachabilityUpdate: make(chan struct{}, 1), triggerReachabilityUpdate: make(chan struct{}, 1),
interfaceAddrs: &interfaceAddrsCache{}, interfaceAddrs: &interfaceAddrsCache{},
signKey: signKey, signKey: signKey,
addrStore: addrStore, addrStore: addrStore,
hostID: hostID, hostID: hostID,
ctx: ctx, disableNonPublicAddrPublishing: disableNonPublicAddrPublishing,
ctxCancel: cancel, ctx: ctx,
ctxCancel: cancel,
} }
unknownReachability := network.ReachabilityUnknown unknownReachability := network.ReachabilityUnknown
as.hostReachability.Store(&unknownReachability) 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 // updatePeerStore updates the peer store for the host
func (a *addrsManager) updatePeerStore(currentAddrs []ma.Multiaddr, removedAddrs []ma.Multiaddr) { func (a *addrsManager) updatePeerStore(currentAddrs []ma.Multiaddr, removedAddrs []ma.Multiaddr) {
// update host addresses in the peer store publishedAddrs := currentAddrs
a.addrStore.SetAddrs(a.hostID, currentAddrs, peerstore.PermanentAddrTTL) if a.disableNonPublicAddrPublishing {
publishedAddrs = filterPublicAddrs(currentAddrs)
}
a.addrStore.SetAddrs(a.hostID, publishedAddrs, peerstore.PermanentAddrTTL)
a.addrStore.SetAddrs(a.hostID, removedAddrs, 0) a.addrStore.SetAddrs(a.hostID, removedAddrs, 0)
var sr *record.Envelope var sr *record.Envelope
@@ -354,7 +360,7 @@ func (a *addrsManager) updatePeerStore(currentAddrs []ma.Multiaddr, removedAddrs
var err error var err error
// add signed peer record to the event // add signed peer record to the event
// in case of an error drop this event. // in case of an error drop this event.
sr, err = a.makeSignedPeerRecord(currentAddrs) sr, err = a.makeSignedPeerRecord(publishedAddrs)
if err != nil { if err != nil {
log.Error("error creating a signed peer record from the set of current addresses", "err", err) log.Error("error creating a signed peer record from the set of current addresses", "err", err)
return 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) { func (a *addrsManager) notifyAddrsUpdated(emitter event.Emitter, localAddrsEmitter event.Emitter, previous, current hostAddrs) {
if areAddrsDifferent(previous.localAddrs, current.localAddrs) { if areAddrsDifferent(previous.localAddrs, current.localAddrs) {
log.Debug("host local addresses updated", "addrs", 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 { type addrsManagerArgs struct {
NATManager NATManager NATManager NATManager
AddrsFactory AddrsFactory AddrsFactory AddrsFactory
ObservedAddrsManager ObservedAddrsManager ObservedAddrsManager ObservedAddrsManager
ListenAddrs func() []ma.Multiaddr ListenAddrs func() []ma.Multiaddr
AddCertHashes func([]ma.Multiaddr) []ma.Multiaddr AddCertHashes func([]ma.Multiaddr) []ma.Multiaddr
AutoNATClient autonatv2Client AutoNATClient autonatv2Client
Bus event.Bus Bus event.Bus
AddrStoreArgs addrStoreArgs AddrStoreArgs addrStoreArgs
DisableNonPublicAddrPublishing bool
} }
type addrsManagerTestCase struct { type addrsManagerTestCase struct {
@@ -118,6 +119,7 @@ func newAddrsManagerTestCase(tb testing.TB, args addrsManagerArgs) addrsManagerT
true, true,
prometheus.DefaultRegisterer, prometheus.DefaultRegisterer,
false, false,
args.DisableNonPublicAddrPublishing,
signKey, signKey,
addrStore, addrStore,
pid, 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) { func TestRemoveIfNotInSource(t *testing.T) {
addrs := make([]ma.Multiaddr, 0, 10) addrs := make([]ma.Multiaddr, 0, 10)
for i := range 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 disables the generation of Signed Peer Records on this host.
DisableSignedPeerRecord bool 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 enables the peer to initiate/respond to hole punching attempts for NAT traversal.
EnableHolePunching bool EnableHolePunching bool
// HolePunchingOptions are options for the hole punching service // HolePunchingOptions are options for the hole punching service
@@ -238,6 +246,7 @@ func NewHost(n network.Network, opts *HostOpts) (*BasicHost, error) {
opts.EnableMetrics, opts.EnableMetrics,
opts.PrometheusRegisterer, opts.PrometheusRegisterer,
opts.DisableSignedPeerRecord, opts.DisableSignedPeerRecord,
opts.DisableNonPublicAddrPublishing,
h.Peerstore().PrivKey(h.ID()), h.Peerstore().PrivKey(h.ID()),
h.Peerstore(), h.Peerstore(),
h.ID(), h.ID(),