diff --git a/config/config.go b/config/config.go index 602e17e88..386536ee8 100644 --- a/config/config.go +++ b/config/config.go @@ -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 diff --git a/options.go b/options.go index 6bf0e2cc2..8a4577371 100644 --- a/options.go +++ b/options.go @@ -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 { diff --git a/p2p/host/basic/addrs_manager.go b/p2p/host/basic/addrs_manager.go index 8fb19455a..fd3dbdeae 100644 --- a/p2p/host/basic/addrs_manager.go +++ b/p2p/host/basic/addrs_manager.go @@ -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/. +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) diff --git a/p2p/host/basic/addrs_manager_test.go b/p2p/host/basic/addrs_manager_test.go index 67f7c74fb..ee74a8bbb 100644 --- a/p2p/host/basic/addrs_manager_test.go +++ b/p2p/host/basic/addrs_manager_test.go @@ -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 { diff --git a/p2p/host/basic/basic_host.go b/p2p/host/basic/basic_host.go index abd18795c..46fe082df 100644 --- a/p2p/host/basic/basic_host.go +++ b/p2p/host/basic/basic_host.go @@ -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(),