Repository: libp2p/go-libp2p
Pull request #3075 (2024-12-01 13:37:25)

addrsmanager: extract out addressing logic from basichost
The goal is to keep modifying it and removing the dependency on basichost by relying on events. At that point, we can expose this as a separate service usable by both basic and blank hosts

Part of #2229 

NOTE: There's no fuzzing here. I am thinking of fuzzing the external facing components like observed address manager etc, as opposed to fuzzing something here. @MarcoPolo  Do you think we should be defensive and parse return values form each of those components here and filter for nil etc? 


Patch:
diff --git a/p2p/host/basic/addrs_manager.go b/p2p/host/basic/addrs_manager.go
new file mode 100644
index 00000000..6b984a9d
--- /dev/null
+++ b/p2p/host/basic/addrs_manager.go
@@ -0,0 +1,549 @@
+package basichost
+
+import (
+	"context"
+	"fmt"
+	"net"
+	"slices"
+	"sync"
+	"sync/atomic"
+	"time"
+
+	"github.com/libp2p/go-libp2p/core/event"
+	"github.com/libp2p/go-libp2p/core/network"
+	"github.com/libp2p/go-libp2p/core/transport"
+	"github.com/libp2p/go-libp2p/p2p/host/basic/internal/backoff"
+	libp2pwebrtc "github.com/libp2p/go-libp2p/p2p/transport/webrtc"
+	libp2pwebtransport "github.com/libp2p/go-libp2p/p2p/transport/webtransport"
+	"github.com/libp2p/go-netroute"
+	ma "github.com/multiformats/go-multiaddr"
+	manet "github.com/multiformats/go-multiaddr/net"
+)
+
+const maxObservedAddrsPerListenAddr = 5
+
+type observedAddrsManager interface {
+	OwnObservedAddrs() []ma.Multiaddr
+	ObservedAddrsFor(local ma.Multiaddr) []ma.Multiaddr
+}
+
+type addrsManager struct {
+	eventbus              event.Bus
+	natManager            NATManager
+	addrsFactory          AddrsFactory
+	listenAddrs           func() []ma.Multiaddr
+	transportForListening func(ma.Multiaddr) transport.Transport
+	observedAddrsManager  observedAddrsManager
+	interfaceAddrs        *interfaceAddrsCache
+
+	// triggerAddrsUpdateChan is used to trigger an addresses update.
+	triggerAddrsUpdateChan chan struct{}
+	// addrsUpdatedChan is notified when addresses change.
+	addrsUpdatedChan chan struct{}
+	hostReachability atomic.Pointer[network.Reachability]
+
+	addrsMx    sync.RWMutex // protects fields below
+	localAddrs []ma.Multiaddr
+	relayAddrs []ma.Multiaddr
+
+	wg        sync.WaitGroup
+	ctx       context.Context
+	ctxCancel context.CancelFunc
+}
+
+func newAddrsManager(
+	eventbus event.Bus,
+	natmgr NATManager,
+	addrsFactory AddrsFactory,
+	listenAddrs func() []ma.Multiaddr,
+	transportForListening func(ma.Multiaddr) transport.Transport,
+	observedAddrsManager observedAddrsManager,
+	addrsUpdatedChan chan struct{},
+) (*addrsManager, error) {
+	ctx, cancel := context.WithCancel(context.Background())
+	as := &addrsManager{
+		eventbus:               eventbus,
+		listenAddrs:            listenAddrs,
+		transportForListening:  transportForListening,
+		observedAddrsManager:   observedAddrsManager,
+		natManager:             natmgr,
+		addrsFactory:           addrsFactory,
+		triggerAddrsUpdateChan: make(chan struct{}, 1),
+		addrsUpdatedChan:       addrsUpdatedChan,
+		interfaceAddrs:         &interfaceAddrsCache{},
+		ctx:                    ctx,
+		ctxCancel:              cancel,
+	}
+	unknownReachability := network.ReachabilityUnknown
+	as.hostReachability.Store(&unknownReachability)
+	return as, nil
+}
+
+func (a *addrsManager) Start() error {
+	return a.background()
+}
+
+func (a *addrsManager) Close() {
+	a.ctxCancel()
+	if a.natManager != nil {
+		err := a.natManager.Close()
+		if err != nil {
+			log.Warnf("error closing natmgr: %s", err)
+		}
+	}
+	a.wg.Wait()
+}
+
+func (a *addrsManager) NetNotifee() network.Notifiee {
+	return &network.NotifyBundle{
+		ListenF:      func(network.Network, ma.Multiaddr) { a.triggerAddrsUpdate() },
+		ListenCloseF: func(network.Network, ma.Multiaddr) { a.triggerAddrsUpdate() },
+	}
+}
+
+func (a *addrsManager) triggerAddrsUpdate() {
+	// This is ugly, we update here *and* in the background loop, but this ensures the nice property
+	// that host.Addrs after host.Network().Listen(...) will return the recently added listen address.
+	a.updateLocalAddrs()
+	select {
+	case a.triggerAddrsUpdateChan <- struct{}{}:
+	default:
+	}
+}
+
+func (a *addrsManager) background() error {
+	autoRelayAddrsSub, err := a.eventbus.Subscribe(new(event.EvtAutoRelayAddrsUpdated))
+	if err != nil {
+		return fmt.Errorf("error subscribing to auto relay addrs: %s", err)
+	}
+
+	autonatReachabilitySub, err := a.eventbus.Subscribe(new(event.EvtLocalReachabilityChanged))
+	if err != nil {
+		return fmt.Errorf("error subscribing to autonat reachability: %s", err)
+	}
+
+	// ensure that we have the correct address after returning from Start()
+	// update local addrs
+	a.updateLocalAddrs()
+	// update relay addrs in case we're private
+	select {
+	case e := <-autoRelayAddrsSub.Out():
+		if evt, ok := e.(event.EvtAutoRelayAddrsUpdated); ok {
+			a.updateRelayAddrs(evt.RelayAddrs)
+		}
+	default:
+	}
+	select {
+	case e := <-autonatReachabilitySub.Out():
+		if evt, ok := e.(event.EvtLocalReachabilityChanged); ok {
+			a.hostReachability.Store(&evt.Reachability)
+		}
+	default:
+	}
+
+	a.wg.Add(1)
+	go func() {
+		defer a.wg.Done()
+		defer func() {
+			err := autoRelayAddrsSub.Close()
+			if err != nil {
+				log.Warnf("error closing auto relay addrs sub: %s", err)
+			}
+		}()
+		defer func() {
+			err := autonatReachabilitySub.Close()
+			if err != nil {
+				log.Warnf("error closing autonat reachability sub: %s", err)
+			}
+		}()
+
+		ticker := time.NewTicker(addrChangeTickrInterval)
+		defer ticker.Stop()
+		var prev []ma.Multiaddr
+		for {
+			a.updateLocalAddrs()
+			curr := a.Addrs()
+			if a.areAddrsDifferent(prev, curr) {
+				log.Debugf("host addresses updated: %s", curr)
+				select {
+				case a.addrsUpdatedChan <- struct{}{}:
+				default:
+				}
+			}
+			prev = curr
+			select {
+			case <-ticker.C:
+			case <-a.triggerAddrsUpdateChan:
+			case e := <-autoRelayAddrsSub.Out():
+				if evt, ok := e.(event.EvtAutoRelayAddrsUpdated); ok {
+					a.updateRelayAddrs(evt.RelayAddrs)
+				}
+			case e := <-autonatReachabilitySub.Out():
+				if evt, ok := e.(event.EvtLocalReachabilityChanged); ok {
+					a.hostReachability.Store(&evt.Reachability)
+				}
+			case <-a.ctx.Done():
+				return
+			}
+		}
+	}()
+	return nil
+}
+
+// Addrs returns the node's dialable addresses both public and private.
+// If autorelay is enabled and node reachability is private, it returns
+// the node's relay addresses and private network addresses.
+func (a *addrsManager) Addrs() []ma.Multiaddr {
+	addrs := a.DirectAddrs()
+	rch := a.hostReachability.Load()
+	if rch != nil && *rch == network.ReachabilityPrivate {
+		a.addrsMx.RLock()
+		// Delete public addresses if the node's reachability is private, and we have relay addresses
+		if len(a.relayAddrs) > 0 {
+			addrs = slices.DeleteFunc(addrs, manet.IsPublicAddr)
+			addrs = append(addrs, a.relayAddrs...)
+		}
+		a.addrsMx.RUnlock()
+	}
+	// Make a copy. Consumers can modify the slice elements
+	addrs = slices.Clone(a.addrsFactory(addrs))
+	// Add certhashes for the addresses provided by the user via address factory.
+	addrs = a.addCertHashes(ma.Unique(addrs))
+	slices.SortFunc(addrs, func(a, b ma.Multiaddr) int { return a.Compare(b) })
+	return addrs
+}
+
+// HolePunchAddrs returns the node's public direct listen addresses for hole punching.
+func (a *addrsManager) HolePunchAddrs() []ma.Multiaddr {
+	addrs := a.DirectAddrs()
+	addrs = slices.Clone(a.addrsFactory(addrs))
+	// AllAddrs may ignore observed addresses in favour of NAT mappings.
+	// Use both for hole punching.
+	if a.observedAddrsManager != nil {
+		addrs = append(addrs, a.observedAddrsManager.OwnObservedAddrs()...)
+	}
+	addrs = ma.Unique(addrs)
+	return slices.DeleteFunc(addrs, func(a ma.Multiaddr) bool { return !manet.IsPublicAddr(a) })
+}
+
+// DirectAddrs returns all the addresses the host is listening on except circuit addresses.
+func (a *addrsManager) DirectAddrs() []ma.Multiaddr {
+	a.addrsMx.RLock()
+	defer a.addrsMx.RUnlock()
+	return slices.Clone(a.localAddrs)
+}
+
+func (a *addrsManager) updateRelayAddrs(addrs []ma.Multiaddr) {
+	a.addrsMx.Lock()
+	defer a.addrsMx.Unlock()
+	a.relayAddrs = append(a.relayAddrs[:0], addrs...)
+}
+
+var p2pCircuitAddr = ma.StringCast("/p2p-circuit")
+
+func (a *addrsManager) updateLocalAddrs() {
+	localAddrs := a.getLocalAddrs()
+	slices.SortFunc(localAddrs, func(a, b ma.Multiaddr) int { return a.Compare(b) })
+
+	a.addrsMx.Lock()
+	a.localAddrs = localAddrs
+	a.addrsMx.Unlock()
+}
+
+func (a *addrsManager) getLocalAddrs() []ma.Multiaddr {
+	listenAddrs := a.listenAddrs()
+	if len(listenAddrs) == 0 {
+		return nil
+	}
+
+	finalAddrs := make([]ma.Multiaddr, 0, 8)
+	finalAddrs = a.appendPrimaryInterfaceAddrs(finalAddrs, listenAddrs)
+	finalAddrs = a.appendNATAddrs(finalAddrs, listenAddrs, a.interfaceAddrs.All())
+
+	finalAddrs = ma.Unique(finalAddrs)
+
+	// Remove "/p2p-circuit" addresses from the list.
+	// The p2p-circuit listener reports its address as just /p2p-circuit. This is
+	// useless for dialing. Users need to manage their circuit addresses themselves,
+	// or use AutoRelay.
+	finalAddrs = slices.DeleteFunc(finalAddrs, func(a ma.Multiaddr) bool {
+		return a.Equal(p2pCircuitAddr)
+	})
+
+	// Remove any unspecified address from the list
+	finalAddrs = slices.DeleteFunc(finalAddrs, func(a ma.Multiaddr) bool {
+		return manet.IsIPUnspecified(a)
+	})
+
+	// Add certhashes for /webrtc-direct, /webtransport, etc addresses discovered
+	// using identify.
+	finalAddrs = a.addCertHashes(finalAddrs)
+	return finalAddrs
+}
+
+// appendPrimaryInterfaceAddrs appends the primary interface addresses to `dst`.
+func (a *addrsManager) appendPrimaryInterfaceAddrs(dst []ma.Multiaddr, listenAddrs []ma.Multiaddr) []ma.Multiaddr {
+	// resolving any unspecified listen addressees to use only the primary
+	// interface to avoid advertising too many addresses.
+	if resolved, err := manet.ResolveUnspecifiedAddresses(listenAddrs, a.interfaceAddrs.Filtered()); err != nil {
+		log.Warnw("failed to resolve listen addrs", "error", err)
+	} else {
+		dst = append(dst, resolved...)
+	}
+	return dst
+}
+
+// appendNATAddrs appends the NAT-ed addrs for the listenAddrs. For unspecified listen addrs it appends the
+// public address for all the interfaces.
+// Inferring WebTransport from QUIC depends on the observed address manager.
+//
+// TODO: Merge the natmgr and identify.ObservedAddrManager in to one NatMapper module.
+func (a *addrsManager) appendNATAddrs(dst []ma.Multiaddr, listenAddrs []ma.Multiaddr, ifaceAddrs []ma.Multiaddr) []ma.Multiaddr {
+	var obsAddrs []ma.Multiaddr
+	for _, listenAddr := range listenAddrs {
+		var natAddr ma.Multiaddr
+		if a.natManager != nil {
+			natAddr = a.natManager.GetMapping(listenAddr)
+		}
+
+		// The order of the cases below is important.
+		switch {
+		case natAddr == nil: // no nat mapping
+			dst = a.appendObservedAddrs(dst, listenAddr, ifaceAddrs)
+		case manet.IsIPUnspecified(natAddr):
+			log.Infof("NAT device reported an unspecified IP as it's external address: %s", natAddr)
+			_, natRest := ma.SplitFirst(natAddr)
+			obsAddrs = a.appendObservedAddrs(obsAddrs[:0], listenAddr, ifaceAddrs)
+			for _, addr := range obsAddrs {
+				obsIP, _ := ma.SplitFirst(addr)
+				if obsIP != nil && manet.IsPublicAddr(obsIP.Multiaddr()) {
+					dst = append(dst, obsIP.Encapsulate(natRest))
+				}
+			}
+		// This is !Public as opposed to IsPrivate intentionally.
+		// Public is a more restrictive classification in some cases, like IPv6 addresses which only
+		// consider unicast IPv6 addresses allocated so far as public(2000::/3).
+		case !manet.IsPublicAddr(natAddr): // nat reported non public addr(maybe CGNAT?)
+			// use both NAT and observed addr
+			dst = append(dst, natAddr)
+			dst = a.appendObservedAddrs(dst, listenAddr, ifaceAddrs)
+		default: // public addr
+			dst = append(dst, natAddr)
+		}
+	}
+	return dst
+}
+
+func (a *addrsManager) appendObservedAddrs(dst []ma.Multiaddr, listenAddr ma.Multiaddr, ifaceAddrs []ma.Multiaddr) []ma.Multiaddr {
+	if a.observedAddrsManager == nil {
+		return dst
+	}
+	// Add it for the listenAddr first.
+	// listenAddr maybe unspecified. That's okay as connections on UDP transports
+	// will have the unspecified address as the local address.
+	obsAddrs := a.observedAddrsManager.ObservedAddrsFor(listenAddr)
+	if len(obsAddrs) > maxObservedAddrsPerListenAddr {
+		obsAddrs = obsAddrs[:maxObservedAddrsPerListenAddr]
+	}
+	dst = append(dst, obsAddrs...)
+
+	// if it can be resolved into more addresses, add them too
+	resolved, err := manet.ResolveUnspecifiedAddress(listenAddr, ifaceAddrs)
+	if err != nil {
+		log.Warnf("failed to resolve listen addr %s, %s: %s", listenAddr, ifaceAddrs, err)
+		return dst
+	}
+	for _, addr := range resolved {
+		obsAddrs = a.observedAddrsManager.ObservedAddrsFor(addr)
+		if len(obsAddrs) > maxObservedAddrsPerListenAddr {
+			obsAddrs = obsAddrs[:maxObservedAddrsPerListenAddr]
+		}
+		dst = append(dst, obsAddrs...)
+	}
+	return dst
+}
+
+func (a *addrsManager) addCertHashes(addrs []ma.Multiaddr) []ma.Multiaddr {
+	if a.transportForListening == nil {
+		return addrs
+	}
+
+	// TODO(sukunrt): Move this to swarm.
+	// There are two parts to determining our external address
+	// 1. From the NAT device, or identify, or other such STUN like mechanism.
+	// All that matters here is (internal_ip, internal_port, tcp) => (external_ip, external_port, tcp)
+	// The rest of the address should be cut and appended to the external one.
+	// 2. The user provides us with the address (/ip4/1.2.3.4/udp/1/webrtc-direct) and we add the certhash.
+	// This API should be where the transports are, i.e. swarm.
+	//
+	// It would have been nice to remove this completely and just work with
+	// mapping the interface thinwaist addresses (tcp, 192.168.18.18:4000 => 1.2.3.4:4577)
+	// but that is only convenient if we're using the same port for listening on
+	// all transports which share the same thinwaist protocol. If you listen
+	// on 4001 for tcp, and 4002 for websocket, then it's a terrible API.
+	type addCertHasher interface {
+		AddCertHashes(m ma.Multiaddr) (ma.Multiaddr, bool)
+	}
+
+	for i, addr := range addrs {
+		wtOK, wtN := libp2pwebtransport.IsWebtransportMultiaddr(addr)
+		webrtcOK, webrtcN := libp2pwebrtc.IsWebRTCDirectMultiaddr(addr)
+		if (wtOK && wtN == 0) || (webrtcOK && webrtcN == 0) {
+			t := a.transportForListening(addr)
+			if t == nil {
+				continue
+			}
+			tpt, ok := t.(addCertHasher)
+			if !ok {
+				continue
+			}
+			addrWithCerthash, added := tpt.AddCertHashes(addr)
+			if !added {
+				log.Warnf("Couldn't add certhashes to multiaddr: %s", addr)
+				continue
+			}
+			addrs[i] = addrWithCerthash
+		}
+	}
+	return addrs
+}
+
+func (a *addrsManager) areAddrsDifferent(prev, current []ma.Multiaddr) bool {
+	// TODO: make the sorted nature of ma.Unique a guarantee in multiaddrs
+	prev = ma.Unique(prev)
+	current = ma.Unique(current)
+	if len(prev) != len(current) {
+		return true
+	}
+	slices.SortFunc(prev, func(a, b ma.Multiaddr) int { return a.Compare(b) })
+	slices.SortFunc(current, func(a, b ma.Multiaddr) int { return a.Compare(b) })
+	for i := range prev {
+		if !prev[i].Equal(current[i]) {
+			return true
+		}
+	}
+	return false
+}
+
+const interfaceAddrsCacheTTL = time.Minute
+
+type interfaceAddrsCache struct {
+	mx                     sync.RWMutex
+	filtered               []ma.Multiaddr
+	all                    []ma.Multiaddr
+	updateLocalIPv4Backoff backoff.ExpBackoff
+	updateLocalIPv6Backoff backoff.ExpBackoff
+	lastUpdated            time.Time
+}
+
+func (i *interfaceAddrsCache) Filtered() []ma.Multiaddr {
+	i.mx.RLock()
+	if time.Now().After(i.lastUpdated.Add(interfaceAddrsCacheTTL)) {
+		i.mx.RUnlock()
+		return i.update(true)
+	}
+	defer i.mx.RUnlock()
+	return i.filtered
+}
+
+func (i *interfaceAddrsCache) All() []ma.Multiaddr {
+	i.mx.RLock()
+	if time.Now().After(i.lastUpdated.Add(interfaceAddrsCacheTTL)) {
+		i.mx.RUnlock()
+		return i.update(false)
+	}
+	defer i.mx.RUnlock()
+	return i.all
+}
+
+func (i *interfaceAddrsCache) update(filtered bool) []ma.Multiaddr {
+	i.mx.Lock()
+	defer i.mx.Unlock()
+	if !time.Now().After(i.lastUpdated.Add(interfaceAddrsCacheTTL)) {
+		if filtered {
+			return i.filtered
+		}
+		return i.all
+	}
+	i.updateUnlocked()
+	i.lastUpdated = time.Now()
+	if filtered {
+		return i.filtered
+	}
+	return i.all
+}
+
+func (i *interfaceAddrsCache) updateUnlocked() {
+	i.filtered = nil
+	i.all = nil
+
+	// Try to use the default ipv4/6 addresses.
+	// TODO: Remove this. We should advertise all interface addresses.
+	if r, err := netroute.New(); err != nil {
+		log.Debugw("failed to build Router for kernel's routing table", "error", err)
+	} else {
+
+		var localIPv4 net.IP
+		var ran bool
+		err, ran = i.updateLocalIPv4Backoff.Run(func() error {
+			_, _, localIPv4, err = r.Route(net.IPv4zero)
+			return err
+		})
+
+		if ran && err != nil {
+			log.Debugw("failed to fetch local IPv4 address", "error", err)
+		} else if ran && localIPv4.IsGlobalUnicast() {
+			maddr, err := manet.FromIP(localIPv4)
+			if err == nil {
+				i.filtered = append(i.filtered, maddr)
+			}
+		}
+
+		var localIPv6 net.IP
+		err, ran = i.updateLocalIPv6Backoff.Run(func() error {
+			_, _, localIPv6, err = r.Route(net.IPv6unspecified)
+			return err
+		})
+
+		if ran && err != nil {
+			log.Debugw("failed to fetch local IPv6 address", "error", err)
+		} else if ran && localIPv6.IsGlobalUnicast() {
+			maddr, err := manet.FromIP(localIPv6)
+			if err == nil {
+				i.filtered = append(i.filtered, maddr)
+			}
+		}
+	}
+
+	// Resolve the interface addresses
+	ifaceAddrs, err := manet.InterfaceMultiaddrs()
+	if err != nil {
+		// This usually shouldn't happen, but we could be in some kind
+		// of funky restricted environment.
+		log.Errorw("failed to resolve local interface addresses", "error", err)
+
+		// Add the loopback addresses to the filtered addrs and use them as the non-filtered addrs.
+		// Then bail. There's nothing else we can do here.
+		i.filtered = append(i.filtered, manet.IP4Loopback, manet.IP6Loopback)
+		i.all = i.filtered
+		return
+	}
+
+	// remove link local ipv6 addresses
+	i.all = slices.DeleteFunc(ifaceAddrs, manet.IsIP6LinkLocal)
+
+	// If netroute failed to get us any interface addresses, use all of
+	// them.
+	if len(i.filtered) == 0 {
+		// Add all addresses.
+		i.filtered = i.all
+	} else {
+		// Only add loopback addresses. Filter these because we might
+		// not _have_ an IPv6 loopback address.
+		for _, addr := range i.all {
+			if manet.IsIPLoopback(addr) {
+				i.filtered = append(i.filtered, addr)
+			}
+		}
+	}
+}
diff --git a/p2p/host/basic/basic_host.go b/p2p/host/basic/basic_host.go
index a1e0aa42..b4db8c20 100644
--- a/p2p/host/basic/basic_host.go
+++ b/p2p/host/basic/basic_host.go
@@ -5,10 +5,8 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"net"
 	"slices"
 	"sync"
-	"sync/atomic"
 	"time"
 
 	"github.com/libp2p/go-libp2p/core/connmgr"
@@ -22,7 +20,6 @@ import (
 	"github.com/libp2p/go-libp2p/core/record"
 	"github.com/libp2p/go-libp2p/core/transport"
 	"github.com/libp2p/go-libp2p/p2p/host/autonat"
-	"github.com/libp2p/go-libp2p/p2p/host/basic/internal/backoff"
 	"github.com/libp2p/go-libp2p/p2p/host/eventbus"
 	"github.com/libp2p/go-libp2p/p2p/host/pstoremanager"
 	"github.com/libp2p/go-libp2p/p2p/host/relaysvc"
@@ -35,8 +32,6 @@ import (
 	libp2pwebtransport "github.com/libp2p/go-libp2p/p2p/transport/webtransport"
 	"github.com/prometheus/client_golang/prometheus"
 
-	"github.com/libp2p/go-netroute"
-
 	logging "github.com/ipfs/go-log/v2"
 	ma "github.com/multiformats/go-multiaddr"
 	manet "github.com/multiformats/go-multiaddr/net"
@@ -81,13 +76,10 @@ type BasicHost struct {
 	ids          identify.IDService
 	hps          *holepunch.Service
 	pings        *ping.PingService
-	natmgr       NATManager
 	cmgr         connmgr.ConnManager
 	eventbus     event.Bus
 	relayManager *relaysvc.RelayManager
 
-	AddrsFactory AddrsFactory
-
 	negtimeout time.Duration
 
 	emitters struct {
@@ -95,23 +87,16 @@ type BasicHost struct {
 		evtLocalAddrsUpdated     event.Emitter
 	}
 
-	addrChangeChan chan struct{}
-
-	addrMu                 sync.RWMutex // protects the following fields
-	updateLocalIPv4Backoff backoff.ExpBackoff
-	updateLocalIPv6Backoff backoff.ExpBackoff
-	filteredInterfaceAddrs []ma.Multiaddr
-	allInterfaceAddrs      []ma.Multiaddr
-
-	relayAddrs atomic.Pointer[[]ma.Multiaddr]
-
 	disableSignedPeerRecord bool
 	signKey                 crypto.PrivKey
 	caBook                  peerstore.CertifiedAddrBook
 
-	autoNat autonat.AutoNAT
+	autoNATMx sync.RWMutex
+	autoNat   autonat.AutoNAT
 
-	autonatv2 *autonatv2.AutoNAT
+	autonatv2        *autonatv2.AutoNAT
+	addressManager   *addrsManager
+	addrsUpdatedChan chan struct{}
 }
 
 var _ host.Host = (*BasicHost)(nil)
@@ -195,18 +180,13 @@ func NewHost(n network.Network, opts *HostOpts) (*BasicHost, error) {
 		psManager:               psManager,
 		mux:                     msmux.NewMultistreamMuxer[protocol.ID](),
 		negtimeout:              DefaultNegotiationTimeout,
-		AddrsFactory:            DefaultAddrsFactory,
 		eventbus:                opts.EventBus,
-		addrChangeChan:          make(chan struct{}, 1),
 		ctx:                     hostCtx,
 		ctxCancel:               cancel,
 		disableSignedPeerRecord: opts.DisableSignedPeerRecord,
+		addrsUpdatedChan:        make(chan struct{}, 1),
 	}
 
-	var relayAddrs []ma.Multiaddr
-	h.relayAddrs.Store(&relayAddrs)
-	h.updateLocalIpAddr()
-
 	if h.emitters.evtLocalProtocolsUpdated, err = h.eventbus.Emitter(&event.EvtLocalProtocolsUpdated{}, eventbus.Stateful); err != nil {
 		return nil, err
 	}
@@ -214,32 +194,6 @@ func NewHost(n network.Network, opts *HostOpts) (*BasicHost, error) {
 		return nil, err
 	}
 
-	if !h.disableSignedPeerRecord {
-		cab, ok := peerstore.GetCertifiedAddrBook(n.Peerstore())
-		if !ok {
-			return nil, errors.New("peerstore should also be a certified address book")
-		}
-		h.caBook = cab
-
-		h.signKey = h.Peerstore().PrivKey(h.ID())
-		if h.signKey == nil {
-			return nil, errors.New("unable to access host key")
-		}
-
-		// persist a signed peer record for self to the peerstore.
-		rec := peer.PeerRecordFromAddrInfo(peer.AddrInfo{
-			ID:    h.ID(),
-			Addrs: h.Addrs(),
-		})
-		ev, err := record.Seal(rec, h.signKey)
-		if err != nil {
-			return nil, fmt.Errorf("failed to create signed record for self: %w", err)
-		}
-		if _, err := cab.ConsumePeerRecord(ev, peerstore.PermanentAddrTTL); err != nil {
-			return nil, fmt.Errorf("failed to persist signed record to peerstore: %w", err)
-		}
-	}
-
 	if opts.MultistreamMuxer != nil {
 		h.mux = opts.MultistreamMuxer
 	}
@@ -267,6 +221,29 @@ func NewHost(n network.Network, opts *HostOpts) (*BasicHost, error) {
 		return nil, fmt.Errorf("failed to create Identify service: %s", err)
 	}
 
+	addrFactory := DefaultAddrsFactory
+	if opts.AddrsFactory != nil {
+		addrFactory = opts.AddrsFactory
+	}
+
+	var natmgr NATManager
+	if opts.NATManager != nil {
+		natmgr = opts.NATManager(h.Network())
+	}
+	var tfl func(ma.Multiaddr) transport.Transport
+	if s, ok := h.Network().(interface {
+		TransportForListening(ma.Multiaddr) transport.Transport
+	}); ok {
+		tfl = s.TransportForListening
+	}
+	h.addressManager, err = newAddrsManager(h.eventbus, natmgr, addrFactory, h.Network().ListenAddresses, tfl, h.ids, h.addrsUpdatedChan)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create address service: %w", err)
+	}
+	// register to be notified when the network's listen addrs change,
+	// so we can update our address set and push events if needed
+	h.Network().Notify(h.addressManager.NetNotifee())
+
 	if opts.EnableHolePunching {
 		if opts.EnableMetrics {
 			hpOpts := []holepunch.Option{
@@ -274,16 +251,7 @@ func NewHost(n network.Network, opts *HostOpts) (*BasicHost, error) {
 			opts.HolePunchingOptions = append(hpOpts, opts.HolePunchingOptions...)
 
 		}
-		h.hps, err = holepunch.NewService(h, h.ids, func() []ma.Multiaddr {
-			addrs := h.AllAddrs()
-			if opts.AddrsFactory != nil {
-				addrs = slices.Clone(opts.AddrsFactory(addrs))
-			}
-			// AllAddrs may ignore observed addresses in favour of NAT mappings. Use both for hole punching.
-			addrs = append(addrs, h.ids.OwnObservedAddrs()...)
-			addrs = ma.Unique(addrs)
-			return slices.DeleteFunc(addrs, func(a ma.Multiaddr) bool { return !manet.IsPublicAddr(a) })
-		}, opts.HolePunchingOptions...)
+		h.hps, err = holepunch.NewService(h, h.ids, h.addressManager.HolePunchAddrs, opts.HolePunchingOptions...)
 		if err != nil {
 			return nil, fmt.Errorf("failed to create hole punch service: %w", err)
 		}
@@ -293,14 +261,6 @@ func NewHost(n network.Network, opts *HostOpts) (*BasicHost, error) {
 		h.negtimeout = opts.NegotiationTimeout
 	}
 
-	if opts.AddrsFactory != nil {
-		h.AddrsFactory = opts.AddrsFactory
-	}
-
-	if opts.NATManager != nil {
-		h.natmgr = opts.NATManager(n)
-	}
-
 	if opts.ConnManager == nil {
 		h.cmgr = &connmgr.NullConnMgr{}
 	} else {
@@ -334,114 +294,55 @@ func NewHost(n network.Network, opts *HostOpts) (*BasicHost, error) {
 		}
 	}
 
-	n.SetStreamHandler(h.newStreamHandler)
-
-	// register to be notified when the network's listen addrs change,
-	// so we can update our address set and push events if needed
-	listenHandler := func(network.Network, ma.Multiaddr) {
-		h.SignalAddressChange()
-	}
-	n.Notify(&network.NotifyBundle{
-		ListenF:      listenHandler,
-		ListenCloseF: listenHandler,
-	})
-
-	return h, nil
-}
-
-func (h *BasicHost) updateLocalIpAddr() {
-	h.addrMu.Lock()
-	defer h.addrMu.Unlock()
-
-	h.filteredInterfaceAddrs = nil
-	h.allInterfaceAddrs = nil
-
-	// Try to use the default ipv4/6 addresses.
-
-	if r, err := netroute.New(); err != nil {
-		log.Debugw("failed to build Router for kernel's routing table", "error", err)
-	} else {
-
-		var localIPv4 net.IP
-		var ran bool
-		err, ran = h.updateLocalIPv4Backoff.Run(func() error {
-			_, _, localIPv4, err = r.Route(net.IPv4zero)
-			return err
-		})
-
-		if ran && err != nil {
-			log.Debugw("failed to fetch local IPv4 address", "error", err)
-		} else if ran && localIPv4.IsGlobalUnicast() {
-			maddr, err := manet.FromIP(localIPv4)
-			if err == nil {
-				h.filteredInterfaceAddrs = append(h.filteredInterfaceAddrs, maddr)
-			}
+	if !h.disableSignedPeerRecord {
+		h.signKey = h.Peerstore().PrivKey(h.ID())
+		cab, ok := peerstore.GetCertifiedAddrBook(h.Peerstore())
+		if !ok {
+			return nil, errors.New("peerstore should also be a certified address book")
 		}
+		h.caBook = cab
 
-		var localIPv6 net.IP
-		err, ran = h.updateLocalIPv6Backoff.Run(func() error {
-			_, _, localIPv6, err = r.Route(net.IPv6unspecified)
-			return err
-		})
-
-		if ran && err != nil {
-			log.Debugw("failed to fetch local IPv6 address", "error", err)
-		} else if ran && localIPv6.IsGlobalUnicast() {
-			maddr, err := manet.FromIP(localIPv6)
-			if err == nil {
-				h.filteredInterfaceAddrs = append(h.filteredInterfaceAddrs, maddr)
-			}
+		rec, err := h.makeSignedPeerRecord(h.addressManager.Addrs())
+		if err != nil {
+			return nil, fmt.Errorf("failed to create signed record for self: %w", err)
 		}
-	}
-
-	// Resolve the interface addresses
-	ifaceAddrs, err := manet.InterfaceMultiaddrs()
-	if err != nil {
-		// This usually shouldn't happen, but we could be in some kind
-		// of funky restricted environment.
-		log.Errorw("failed to resolve local interface addresses", "error", err)
-
-		// Add the loopback addresses to the filtered addrs and use them as the non-filtered addrs.
-		// Then bail. There's nothing else we can do here.
-		h.filteredInterfaceAddrs = append(h.filteredInterfaceAddrs, manet.IP4Loopback, manet.IP6Loopback)
-		h.allInterfaceAddrs = h.filteredInterfaceAddrs
-		return
-	}
-
-	for _, addr := range ifaceAddrs {
-		// Skip link-local addrs, they're mostly useless.
-		if !manet.IsIP6LinkLocal(addr) {
-			h.allInterfaceAddrs = append(h.allInterfaceAddrs, addr)
+		if _, err := h.caBook.ConsumePeerRecord(rec, peerstore.PermanentAddrTTL); err != nil {
+			return nil, fmt.Errorf("failed to persist signed record to peerstore: %w", err)
 		}
 	}
+	n.SetStreamHandler(h.newStreamHandler)
 
-	// If netroute failed to get us any interface addresses, use all of
-	// them.
-	if len(h.filteredInterfaceAddrs) == 0 {
-		// Add all addresses.
-		h.filteredInterfaceAddrs = h.allInterfaceAddrs
-	} else {
-		// Only add loopback addresses. Filter these because we might
-		// not _have_ an IPv6 loopback address.
-		for _, addr := range h.allInterfaceAddrs {
-			if manet.IsIPLoopback(addr) {
-				h.filteredInterfaceAddrs = append(h.filteredInterfaceAddrs, addr)
-			}
-		}
-	}
+	return h, nil
 }
 
 // Start starts background tasks in the host
+// TODO: Return error and handle it in the caller?
 func (h *BasicHost) Start() {
 	h.psManager.Start()
-	h.refCount.Add(1)
-	h.ids.Start()
 	if h.autonatv2 != nil {
 		err := h.autonatv2.Start()
 		if err != nil {
 			log.Errorf("autonat v2 failed to start: %s", err)
 		}
 	}
+	if err := h.addressManager.Start(); err != nil {
+		log.Errorf("address service failed to start: %s", err)
+	}
+
+	if !h.disableSignedPeerRecord {
+		// Ensure we have the correct peer record after Start returns
+		rec, err := h.makeSignedPeerRecord(h.addressManager.Addrs())
+		if err != nil {
+			log.Errorf("failed to create signed record: %w", err)
+		}
+		if _, err := h.caBook.ConsumePeerRecord(rec, peerstore.PermanentAddrTTL); err != nil {
+			log.Errorf("failed to persist signed record to peerstore: %w", err)
+		}
+	}
+
+	h.ids.Start()
+
+	h.refCount.Add(1)
 	go h.background()
 }
 
@@ -493,16 +394,6 @@ func (h *BasicHost) newStreamHandler(s network.Stream) {
 	handle(protoID, s)
 }
 
-// SignalAddressChange signals to the host that it needs to determine whether our listen addresses have recently
-// changed.
-// Warning: this interface is unstable and may disappear in the future.
-func (h *BasicHost) SignalAddressChange() {
-	select {
-	case h.addrChangeChan <- struct{}{}:
-	default:
-	}
-}
-
 func (h *BasicHost) makeUpdatedAddrEvent(prev, current []ma.Multiaddr) *event.EvtLocalAddressesUpdated {
 	if prev == nil && current == nil {
 		return nil
@@ -601,39 +492,13 @@ func (h *BasicHost) background() {
 		}
 	}
 
-	addrSub, err := h.EventBus().Subscribe(new(event.EvtAutoRelayAddrsUpdated), eventbus.Name("basic host address loop"))
-	if err != nil {
-		log.Errorf("failed to subscribe to the EvtAutoRelayAddrs: %s", err)
-		return
-	}
-	defer addrSub.Close()
-
-	// periodically schedules an IdentifyPush to update our peers for changes
-	// in our address set (if needed)
-	ticker := time.NewTicker(addrChangeTickrInterval)
-	defer ticker.Stop()
-
 	for {
-		// Update our local IP addresses before checking our current addresses.
-		if len(h.network.ListenAddresses()) > 0 {
-			h.updateLocalIpAddr()
-		}
 		curr := h.Addrs()
 		emitAddrChange(curr, lastAddrs)
 		lastAddrs = curr
 
 		select {
-		case <-ticker.C:
-		case <-h.addrChangeChan:
-		case e := <-addrSub.Out():
-			if evt, ok := e.(event.EvtAutoRelayAddrsUpdated); ok {
-				// Copy to a new slice. Copying to the slice pointed to by relayAddrs
-				// will introduce a race.
-				addrs := slices.Clone(evt.RelayAddrs)
-				h.relayAddrs.Store(&addrs)
-			} else {
-				log.Errorf("received unexpected event: %T %v", e, e)
-			}
+		case <-h.addrsUpdatedChan:
 		case <-h.ctx.Done():
 			return
 		}
@@ -864,19 +729,7 @@ func (h *BasicHost) ConnManager() connmgr.ConnManager {
 // When used with AutoRelay, and if the host is not publicly reachable,
 // this will only have host's private, relay, and no public addresses.
 func (h *BasicHost) Addrs() []ma.Multiaddr {
-	addrs := h.AllAddrs()
-	// Make a copy. Consumers can modify the slice elements
-	if h.GetAutoNat() != nil && h.GetAutoNat().Status() == network.ReachabilityPrivate {
-		relayAddrsPtr := h.relayAddrs.Load()
-		// Only remove public addresses if we have relay addresses.
-		if relayAddrsPtr != nil && len(*relayAddrsPtr) > 0 {
-			addrs = slices.DeleteFunc(addrs, func(a ma.Multiaddr) bool { return manet.IsPublicAddr(a) })
-			addrs = append(addrs, *relayAddrsPtr...)
-		}
-	}
-	addrs = slices.Clone(h.AddrsFactory(addrs))
-	// Add certhashes for the addresses provided by the user via address factory.
-	return h.addCertHashes(ma.Unique(addrs))
+	return h.addressManager.Addrs()
 }
 
 // NormalizeMultiaddr returns a multiaddr suitable for equality checks.
@@ -896,149 +749,9 @@ func (h *BasicHost) NormalizeMultiaddr(addr ma.Multiaddr) ma.Multiaddr {
 	return addr
 }
 
-var p2pCircuitAddr = ma.StringCast("/p2p-circuit")
-
 // AllAddrs returns all the addresses the host is listening on except circuit addresses.
 func (h *BasicHost) AllAddrs() []ma.Multiaddr {
-	listenAddrs := h.Network().ListenAddresses()
-	if len(listenAddrs) == 0 {
-		return nil
-	}
-
-	h.addrMu.RLock()
-	filteredIfaceAddrs := h.filteredInterfaceAddrs
-	allIfaceAddrs := h.allInterfaceAddrs
-	h.addrMu.RUnlock()
-
-	// Iterate over all _unresolved_ listen addresses, resolving our primary
-	// interface only to avoid advertising too many addresses.
-	finalAddrs := make([]ma.Multiaddr, 0, 8)
-	if resolved, err := manet.ResolveUnspecifiedAddresses(listenAddrs, filteredIfaceAddrs); err != nil {
-		// This can happen if we're listening on no addrs, or listening
-		// on IPv6 addrs, but only have IPv4 interface addrs.
-		log.Debugw("failed to resolve listen addrs", "error", err)
-	} else {
-		finalAddrs = append(finalAddrs, resolved...)
-	}
-
-	finalAddrs = ma.Unique(finalAddrs)
-
-	// use nat mappings if we have them
-	if h.natmgr != nil && h.natmgr.HasDiscoveredNAT() {
-		// We have successfully mapped ports on our NAT. Use those
-		// instead of observed addresses (mostly).
-		// Next, apply this mapping to our addresses.
-		for _, listen := range listenAddrs {
-			extMaddr := h.natmgr.GetMapping(listen)
-			if extMaddr == nil {
-				// not mapped
-				continue
-			}
-
-			// if the router reported a sane address
-			if !manet.IsIPUnspecified(extMaddr) {
-				// Add in the mapped addr.
-				finalAddrs = append(finalAddrs, extMaddr)
-			} else {
-				log.Warn("NAT device reported an unspecified IP as it's external address")
-			}
-
-			// Did the router give us a routable public addr?
-			if manet.IsPublicAddr(extMaddr) {
-				// well done
-				continue
-			}
-
-			// No.
-			// in case the router gives us a wrong address or we're behind a double-NAT.
-			// also add observed addresses
-			resolved, err := manet.ResolveUnspecifiedAddress(listen, allIfaceAddrs)
-			if err != nil {
-				// This can happen if we try to resolve /ip6/::/...
-				// without any IPv6 interface addresses.
-				continue
-			}
-
-			for _, addr := range resolved {
-				// Now, check if we have any observed addresses that
-				// differ from the one reported by the router. Routers
-				// don't always give the most accurate information.
-				observed := h.ids.ObservedAddrsFor(addr)
-
-				if len(observed) == 0 {
-					continue
-				}
-
-				// Drop the IP from the external maddr
-				_, extMaddrNoIP := ma.SplitFirst(extMaddr)
-
-				for _, obsMaddr := range observed {
-					// Extract a public observed addr.
-					ip, _ := ma.SplitFirst(obsMaddr)
-					if ip == nil || !manet.IsPublicAddr(ip.Multiaddr()) {
-						continue
-					}
-
-					finalAddrs = append(finalAddrs, ip.Encapsulate(extMaddrNoIP))
-				}
-			}
-		}
-	} else {
-		var observedAddrs []ma.Multiaddr
-		if h.ids != nil {
-			observedAddrs = h.ids.OwnObservedAddrs()
-		}
-		finalAddrs = append(finalAddrs, observedAddrs...)
-	}
-	finalAddrs = ma.Unique(finalAddrs)
-	// Remove /p2p-circuit addresses from the list.
-	// The p2p-circuit tranport listener reports its address as just /p2p-circuit
-	// This is useless for dialing. Users need to manage their circuit addresses themselves,
-	// or use AutoRelay.
-	finalAddrs = slices.DeleteFunc(finalAddrs, func(a ma.Multiaddr) bool {
-		return a.Equal(p2pCircuitAddr)
-	})
-	// Add certhashes for /webrtc-direct, /webtransport, etc addresses discovered
-	// using identify.
-	finalAddrs = h.addCertHashes(finalAddrs)
-	return finalAddrs
-}
-
-// addCertHashes adds certhashes to the relevant addresses. It modifies `addrs` in place.
-func (h *BasicHost) addCertHashes(addrs []ma.Multiaddr) []ma.Multiaddr {
-	// This is a temporary workaround/hack that fixes #2233. Once we have a
-	// proper address pipeline, rework this. See the issue for more context.
-	type transportForListeninger interface {
-		TransportForListening(a ma.Multiaddr) transport.Transport
-	}
-
-	type addCertHasher interface {
-		AddCertHashes(m ma.Multiaddr) (ma.Multiaddr, bool)
-	}
-
-	s, ok := h.Network().(transportForListeninger)
-	if !ok {
-		return addrs
-	}
-
-	for i, addr := range addrs {
-		wtOK, wtN := libp2pwebtransport.IsWebtransportMultiaddr(addr)
-		webrtcOK, webrtcN := libp2pwebrtc.IsWebRTCDirectMultiaddr(addr)
-		if (wtOK && wtN == 0) || (webrtcOK && webrtcN == 0) {
-			t := s.TransportForListening(addr)
-			tpt, ok := t.(addCertHasher)
-			if !ok {
-				continue
-			}
-			addrWithCerthash, added := tpt.AddCertHashes(addr)
-			if !added {
-				log.Debugf("Couldn't add certhashes to multiaddr: %s", addr)
-				continue
-			}
-			addrs[i] = addrWithCerthash
-		}
-	}
-	return addrs
+	return h.addressManager.DirectAddrs()
 }
 
 func trimHostAddrList(addrs []ma.Multiaddr, maxSize int) []ma.Multiaddr {
@@ -1095,30 +808,37 @@ func trimHostAddrList(addrs []ma.Multiaddr, maxSize int) []ma.Multiaddr {
 
 // SetAutoNat sets the autonat service for the host.
 func (h *BasicHost) SetAutoNat(a autonat.AutoNAT) {
-	h.addrMu.Lock()
-	defer h.addrMu.Unlock()
+	h.autoNATMx.Lock()
+	defer h.autoNATMx.Unlock()
 	if h.autoNat == nil {
 		h.autoNat = a
 	}
 }
 
 // GetAutoNat returns the host's AutoNAT service, if AutoNAT is enabled.
+//
+// Deprecated: Use `BasicHost.Reachability` to get the host's reachability.
 func (h *BasicHost) GetAutoNat() autonat.AutoNAT {
-	h.addrMu.Lock()
-	defer h.addrMu.Unlock()
+	h.autoNATMx.Lock()
+	defer h.autoNATMx.Unlock()
 	return h.autoNat
 }
 
+// Reachability returns the host's reachability status.
+func (h *BasicHost) Reachability() network.Reachability {
+	return *h.addressManager.hostReachability.Load()
+}
+
 // Close shuts down the Host's services (network, etc).
 func (h *BasicHost) Close() error {
 	h.closeSync.Do(func() {
 		h.ctxCancel()
-		if h.natmgr != nil {
-			h.natmgr.Close()
-		}
 		if h.cmgr != nil {
 			h.cmgr.Close()
 		}
+
+		h.addressManager.Close()
+
 		if h.ids != nil {
 			h.ids.Close()
 		}


Test patch:
diff --git a/libp2p_test.go b/libp2p_test.go
index b3bef5d2..f10bbb9b 100644
--- a/libp2p_test.go
+++ b/libp2p_test.go
@@ -811,3 +811,17 @@ func TestCustomTCPDialer(t *testing.T) {
 	})
 	require.ErrorContains(t, err, expectedErr.Error())
 }
+
+func BenchmarkAllAddrs(b *testing.B) {
+	h, err := New()
+
+	addrsHost := h.(interface{ AllAddrs() []ma.Multiaddr })
+	require.NoError(b, err)
+	defer h.Close()
+
+	b.ReportAllocs()
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		addrsHost.AllAddrs()
+	}
+}
diff --git a/p2p/host/basic/addrs_manager_test.go b/p2p/host/basic/addrs_manager_test.go
new file mode 100644
index 00000000..49e46f25
--- /dev/null
+++ b/p2p/host/basic/addrs_manager_test.go
@@ -0,0 +1,441 @@
+package basichost
+
+import (
+	"fmt"
+	"testing"
+	"time"
+
+	"github.com/libp2p/go-libp2p/core/event"
+	"github.com/libp2p/go-libp2p/core/network"
+	"github.com/libp2p/go-libp2p/p2p/host/eventbus"
+	ma "github.com/multiformats/go-multiaddr"
+	manet "github.com/multiformats/go-multiaddr/net"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestAppendNATAddrs(t *testing.T) {
+	if1, if2 := ma.StringCast("/ip4/192.168.0.100"), ma.StringCast("/ip4/1.1.1.1")
+	ifaceAddrs := []ma.Multiaddr{if1, if2}
+	tcpListenAddr, udpListenAddr := ma.StringCast("/ip4/0.0.0.0/tcp/1"), ma.StringCast("/ip4/0.0.0.0/udp/2/quic-v1")
+	cases := []struct {
+		Name        string
+		Listen      ma.Multiaddr
+		Nat         ma.Multiaddr
+		ObsAddrFunc func(ma.Multiaddr) []ma.Multiaddr
+		Expected    []ma.Multiaddr
+	}{
+		{
+			Name: "nat map success",
+			// nat mapping success, obsaddress ignored
+			Listen: ma.StringCast("/ip4/0.0.0.0/udp/1/quic-v1"),
+			Nat:    ma.StringCast("/ip4/1.1.1.1/udp/10/quic-v1"),
+			ObsAddrFunc: func(m ma.Multiaddr) []ma.Multiaddr {
+				return []ma.Multiaddr{ma.StringCast("/ip4/2.2.2.2/udp/100/quic-v1")}
+			},
+			Expected: []ma.Multiaddr{ma.StringCast("/ip4/1.1.1.1/udp/10/quic-v1")},
+		},
+		{
+			Name: "nat map failure",
+			// nat mapping fails, obs addresses added
+			Listen: ma.StringCast("/ip4/0.0.0.0/tcp/1"),
+			Nat:    nil,
+			ObsAddrFunc: func(a ma.Multiaddr) []ma.Multiaddr {
+				ipC, _ := ma.SplitFirst(a)
+				ip := ipC.Multiaddr()
+				switch {
+				case ip.Equal(if1):
+					return []ma.Multiaddr{ma.StringCast("/ip4/2.2.2.2/tcp/100")}
+				case ip.Equal(if2):
+					return []ma.Multiaddr{ma.StringCast("/ip4/3.3.3.3/tcp/100")}
+				default:
+					return []ma.Multiaddr{}
+				}
+			},
+			Expected: []ma.Multiaddr{ma.StringCast("/ip4/2.2.2.2/tcp/100"), ma.StringCast("/ip4/3.3.3.3/tcp/100")},
+		},
+		{
+			Name: "if addrs ignored if not listening on unspecified",
+			// nat mapping fails, obs addresses added
+			Listen: ma.StringCast("/ip4/192.168.1.1/tcp/1"),
+			Nat:    nil,
+			ObsAddrFunc: func(a ma.Multiaddr) []ma.Multiaddr {
+				ipC, _ := ma.SplitFirst(a)
+				ip := ipC.Multiaddr()
+				switch {
+				case ip.Equal(if1):
+					return []ma.Multiaddr{ma.StringCast("/ip4/2.2.2.2/tcp/100")}
+				case ip.Equal(if2):
+					return []ma.Multiaddr{ma.StringCast("/ip4/3.3.3.3/tcp/100")}
+				case ip.Equal(ma.StringCast("/ip4/192.168.1.1")):
+					return []ma.Multiaddr{ma.StringCast("/ip4/4.4.4.4/tcp/100")}
+				default:
+					return []ma.Multiaddr{}
+				}
+			},
+			Expected: []ma.Multiaddr{ma.StringCast("/ip4/4.4.4.4/tcp/100")},
+		},
+		{
+			Name: "nat map success but CGNAT",
+			// nat addr added, obs address added with nat provided port
+			Listen: tcpListenAddr,
+			Nat:    ma.StringCast("/ip4/100.100.1.1/tcp/100"),
+			ObsAddrFunc: func(a ma.Multiaddr) []ma.Multiaddr {
+				ipC, _ := ma.SplitFirst(a)
+				ip := ipC.Multiaddr()
+				if ip.Equal(if1) {
+					return []ma.Multiaddr{ma.StringCast("/ip4/2.2.2.2/tcp/20")}
+				}
+				return []ma.Multiaddr{ma.StringCast("/ip4/3.3.3.3/tcp/30")}
+			},
+			Expected: []ma.Multiaddr{
+				ma.StringCast("/ip4/100.100.1.1/tcp/100"),
+				ma.StringCast("/ip4/2.2.2.2/tcp/20"),
+				ma.StringCast("/ip4/3.3.3.3/tcp/30"),
+			},
+		},
+		{
+			Name: "uses unspecified address for obs address",
+			// observed address manager should be queries with both specified and unspecified addresses
+			// udp observed addresses are mapped to unspecified addresses
+			Listen: udpListenAddr,
+			Nat:    nil,
+			ObsAddrFunc: func(a ma.Multiaddr) []ma.Multiaddr {
+				if manet.IsIPUnspecified(a) {
+					return []ma.Multiaddr{ma.StringCast("/ip4/3.3.3.3/udp/20/quic-v1")}
+				}
+				return []ma.Multiaddr{ma.StringCast("/ip4/2.2.2.2/udp/20/quic-v1")}
+			},
+			Expected: []ma.Multiaddr{
+				ma.StringCast("/ip4/2.2.2.2/udp/20/quic-v1"),
+				ma.StringCast("/ip4/3.3.3.3/udp/20/quic-v1"),
+			},
+		},
+	}
+	for _, tc := range cases {
+		t.Run(tc.Name, func(t *testing.T) {
+			as := &addrsManager{
+				natManager: &mockNatManager{
+					GetMappingFunc: func(addr ma.Multiaddr) ma.Multiaddr {
+						return tc.Nat
+					},
+				},
+				observedAddrsManager: &mockObservedAddrs{
+					ObservedAddrsForFunc: tc.ObsAddrFunc,
+				},
+			}
+			res := as.appendNATAddrs(nil, []ma.Multiaddr{tc.Listen}, ifaceAddrs)
+			res = ma.Unique(res)
+			require.ElementsMatch(t, tc.Expected, res, "%s\n%s", tc.Expected, res)
+		})
+	}
+}
+
+type mockNatManager struct {
+	GetMappingFunc func(addr ma.Multiaddr) ma.Multiaddr
+}
+
+func (m *mockNatManager) Close() error {
+	return nil
+}
+
+func (m *mockNatManager) GetMapping(addr ma.Multiaddr) ma.Multiaddr {
+	if m.GetMappingFunc == nil {
+		return nil
+	}
+	return m.GetMappingFunc(addr)
+}
+
+func (m *mockNatManager) HasDiscoveredNAT() bool {
+	return true
+}
+
+var _ NATManager = &mockNatManager{}
+
+type mockObservedAddrs struct {
+	OwnObservedAddrsFunc func() []ma.Multiaddr
+	ObservedAddrsForFunc func(ma.Multiaddr) []ma.Multiaddr
+}
+
+func (m *mockObservedAddrs) OwnObservedAddrs() []ma.Multiaddr {
+	return m.OwnObservedAddrsFunc()
+}
+
+func (m *mockObservedAddrs) ObservedAddrsFor(local ma.Multiaddr) []ma.Multiaddr {
+	return m.ObservedAddrsForFunc(local)
+}
+
+type addrsManagerArgs struct {
+	NATManager           NATManager
+	AddrsFactory         AddrsFactory
+	ObservedAddrsManager observedAddrsManager
+	ListenAddrs          func() []ma.Multiaddr
+}
+
+type addrsManagerTestCase struct {
+	*addrsManager
+	PushRelay        func(relayAddrs []ma.Multiaddr)
+	PushReachability func(rch network.Reachability)
+}
+
+func newAddrsManagerTestCase(t *testing.T, args addrsManagerArgs) addrsManagerTestCase {
+	eb := eventbus.NewBus()
+	if args.AddrsFactory == nil {
+		args.AddrsFactory = func(addrs []ma.Multiaddr) []ma.Multiaddr { return addrs }
+	}
+	addrsUpdatedChan := make(chan struct{}, 1)
+	am, err := newAddrsManager(
+		eb, args.NATManager, args.AddrsFactory, args.ListenAddrs, nil, args.ObservedAddrsManager, addrsUpdatedChan,
+	)
+	require.NoError(t, err)
+
+	require.NoError(t, am.Start())
+	raEm, err := eb.Emitter(new(event.EvtAutoRelayAddrsUpdated), eventbus.Stateful)
+	require.NoError(t, err)
+
+	rchEm, err := eb.Emitter(new(event.EvtLocalReachabilityChanged), eventbus.Stateful)
+	require.NoError(t, err)
+
+	return addrsManagerTestCase{
+		addrsManager: am,
+		PushRelay: func(relayAddrs []ma.Multiaddr) {
+			err := raEm.Emit(event.EvtAutoRelayAddrsUpdated{RelayAddrs: relayAddrs})
+			require.NoError(t, err)
+		},
+		PushReachability: func(rch network.Reachability) {
+			err := rchEm.Emit(event.EvtLocalReachabilityChanged{Reachability: rch})
+			require.NoError(t, err)
+		},
+	}
+}
+
+func TestAddrsManager(t *testing.T) {
+	lhquic := ma.StringCast("/ip4/127.0.0.1/udp/1/quic-v1")
+	lhtcp := ma.StringCast("/ip4/127.0.0.1/tcp/1")
+
+	publicQUIC := ma.StringCast("/ip4/1.2.3.4/udp/1/quic-v1")
+	publicTCP := ma.StringCast("/ip4/1.2.3.4/tcp/1")
+
+	t.Run("only nat", func(t *testing.T) {
+		am := newAddrsManagerTestCase(t, addrsManagerArgs{
+			NATManager: &mockNatManager{
+				GetMappingFunc: func(addr ma.Multiaddr) ma.Multiaddr {
+					if _, err := addr.ValueForProtocol(ma.P_UDP); err == nil {
+						return publicQUIC
+					}
+					return nil
+				},
+			},
+			ListenAddrs: func() []ma.Multiaddr { return []ma.Multiaddr{lhquic, lhtcp} },
+		})
+		am.triggerAddrsUpdate()
+		require.EventuallyWithT(t, func(collect *assert.CollectT) {
+			expected := []ma.Multiaddr{publicQUIC, lhquic, lhtcp}
+			assert.ElementsMatch(collect, am.Addrs(), expected, "%s\n%s", am.Addrs(), expected)
+		}, 5*time.Second, 50*time.Millisecond)
+	})
+
+	t.Run("nat and observed addrs", func(t *testing.T) {
+		// nat mapping for udp, observed addrs for tcp
+		am := newAddrsManagerTestCase(t, addrsManagerArgs{
+			NATManager: &mockNatManager{
+				GetMappingFunc: func(addr ma.Multiaddr) ma.Multiaddr {
+					if _, err := addr.ValueForProtocol(ma.P_UDP); err == nil {
+						return publicQUIC
+					}
+					return nil
+				},
+			},
+			ObservedAddrsManager: &mockObservedAddrs{
+				ObservedAddrsForFunc: func(addr ma.Multiaddr) []ma.Multiaddr {
+					if _, err := addr.ValueForProtocol(ma.P_TCP); err == nil {
+						return []ma.Multiaddr{publicTCP}
+					}
+					return nil
+				},
+			},
+			ListenAddrs: func() []ma.Multiaddr { return []ma.Multiaddr{lhquic, lhtcp} },
+		})
+		require.EventuallyWithT(t, func(collect *assert.CollectT) {
+			expected := []ma.Multiaddr{lhquic, lhtcp, publicQUIC, publicTCP}
+			assert.ElementsMatch(collect, am.Addrs(), expected, "%s\n%s", am.Addrs(), expected)
+		}, 5*time.Second, 50*time.Millisecond)
+	})
+	t.Run("nat returns unspecified addr", func(t *testing.T) {
+		quicPort1 := ma.StringCast("/ip4/3.3.3.3/udp/1/quic-v1")
+		quicPort2 := ma.StringCast("/ip4/3.3.3.3/udp/2/quic-v1")
+		// port from nat, IP from observed addr
+		am := newAddrsManagerTestCase(t, addrsManagerArgs{
+			NATManager: &mockNatManager{
+				GetMappingFunc: func(addr ma.Multiaddr) ma.Multiaddr {
+					if addr.Equal(lhquic) {
+						return ma.StringCast("/ip4/0.0.0.0/udp/2/quic-v1")
+					}
+					return nil
+				},
+			},
+			ObservedAddrsManager: &mockObservedAddrs{
+				ObservedAddrsForFunc: func(addr ma.Multiaddr) []ma.Multiaddr {
+					if addr.Equal(lhquic) {
+						return []ma.Multiaddr{quicPort1}
+					}
+					return nil
+				},
+			},
+			ListenAddrs: func() []ma.Multiaddr { return []ma.Multiaddr{lhquic} },
+		})
+		expected := []ma.Multiaddr{lhquic, quicPort2}
+		require.EventuallyWithT(t, func(collect *assert.CollectT) {
+			assert.ElementsMatch(collect, am.Addrs(), expected, "%s\n%s", am.Addrs(), expected)
+		}, 5*time.Second, 50*time.Millisecond)
+	})
+	t.Run("only observed addrs", func(t *testing.T) {
+		am := newAddrsManagerTestCase(t, addrsManagerArgs{
+			ObservedAddrsManager: &mockObservedAddrs{
+				ObservedAddrsForFunc: func(addr ma.Multiaddr) []ma.Multiaddr {
+					if addr.Equal(lhtcp) {
+						return []ma.Multiaddr{publicTCP}
+					}
+					if addr.Equal(lhquic) {
+						return []ma.Multiaddr{publicQUIC}
+					}
+					return nil
+				},
+			},
+			ListenAddrs: func() []ma.Multiaddr { return []ma.Multiaddr{lhquic, lhtcp} },
+		})
+		am.triggerAddrsUpdate()
+		expected := []ma.Multiaddr{lhquic, lhtcp, publicTCP, publicQUIC}
+		require.EventuallyWithT(t, func(collect *assert.CollectT) {
+			assert.ElementsMatch(collect, am.Addrs(), expected, "%s\n%s", am.Addrs(), expected)
+		}, 5*time.Second, 50*time.Millisecond)
+	})
+
+	t.Run("observed addrs limit", func(t *testing.T) {
+		quicAddrs := []ma.Multiaddr{
+			ma.StringCast("/ip4/1.2.3.4/udp/1/quic-v1"),
+			ma.StringCast("/ip4/1.2.3.4/udp/2/quic-v1"),
+			ma.StringCast("/ip4/1.2.3.4/udp/3/quic-v1"),
+			ma.StringCast("/ip4/1.2.3.4/udp/4/quic-v1"),
+			ma.StringCast("/ip4/1.2.3.4/udp/5/quic-v1"),
+			ma.StringCast("/ip4/1.2.3.4/udp/6/quic-v1"),
+			ma.StringCast("/ip4/1.2.3.4/udp/7/quic-v1"),
+			ma.StringCast("/ip4/1.2.3.4/udp/8/quic-v1"),
+			ma.StringCast("/ip4/1.2.3.4/udp/9/quic-v1"),
+			ma.StringCast("/ip4/1.2.3.4/udp/10/quic-v1"),
+		}
+		am := newAddrsManagerTestCase(t, addrsManagerArgs{
+			ObservedAddrsManager: &mockObservedAddrs{
+				ObservedAddrsForFunc: func(addr ma.Multiaddr) []ma.Multiaddr {
+					return quicAddrs
+				},
+			},
+			ListenAddrs: func() []ma.Multiaddr { return []ma.Multiaddr{lhquic} },
+		})
+		am.triggerAddrsUpdate()
+		expected := []ma.Multiaddr{lhquic}
+		expected = append(expected, quicAddrs[:maxObservedAddrsPerListenAddr]...)
+		require.EventuallyWithT(t, func(collect *assert.CollectT) {
+			assert.ElementsMatch(collect, am.Addrs(), expected, "%s\n%s", am.Addrs(), expected)
+		}, 5*time.Second, 50*time.Millisecond)
+	})
+	t.Run("public addrs removed when private", func(t *testing.T) {
+		am := newAddrsManagerTestCase(t, addrsManagerArgs{
+			ObservedAddrsManager: &mockObservedAddrs{
+				ObservedAddrsForFunc: func(addr ma.Multiaddr) []ma.Multiaddr {
+					return []ma.Multiaddr{publicQUIC}
+				},
+			},
+			ListenAddrs: func() []ma.Multiaddr { return []ma.Multiaddr{lhquic, lhtcp} },
+		})
+
+		// remove public addrs
+		am.PushReachability(network.ReachabilityPrivate)
+		relayAddr := ma.StringCast("/ip4/1.2.3.4/udp/1/quic-v1/p2p/QmdXGaeGiVA745XorV1jr11RHxB9z4fqykm6xCUPX1aTJo/p2p-circuit")
+		am.PushRelay([]ma.Multiaddr{relayAddr})
+
+		expectedAddrs := []ma.Multiaddr{relayAddr, lhquic, lhtcp}
+		expectedAllAddrs := []ma.Multiaddr{publicQUIC, lhquic, lhtcp}
+		require.EventuallyWithT(t, func(collect *assert.CollectT) {
+			assert.ElementsMatch(collect, am.Addrs(), expectedAddrs, "%s\n%s", am.Addrs(), expectedAddrs)
+			assert.ElementsMatch(collect, am.DirectAddrs(), expectedAllAddrs, "%s\n%s", am.DirectAddrs(), expectedAllAddrs)
+		}, 5*time.Second, 50*time.Millisecond)
+
+		// add public addrs
+		am.PushReachability(network.ReachabilityPublic)
+
+		expectedAddrs = expectedAllAddrs
+		require.EventuallyWithT(t, func(collect *assert.CollectT) {
+			assert.ElementsMatch(collect, am.Addrs(), expectedAddrs, "%s\n%s", am.Addrs(), expectedAddrs)
+			assert.ElementsMatch(collect, am.DirectAddrs(), expectedAllAddrs, "%s\n%s", am.DirectAddrs(), expectedAllAddrs)
+		}, 5*time.Second, 50*time.Millisecond)
+	})
+
+	t.Run("addrs factory gets relay addrs", func(t *testing.T) {
+		relayAddr := ma.StringCast("/ip4/1.2.3.4/udp/1/quic-v1/p2p/QmdXGaeGiVA745XorV1jr11RHxB9z4fqykm6xCUPX1aTJo/p2p-circuit")
+		publicQUIC2 := ma.StringCast("/ip4/1.2.3.4/udp/2/quic-v1")
+		am := newAddrsManagerTestCase(t, addrsManagerArgs{
+			AddrsFactory: func(addrs []ma.Multiaddr) []ma.Multiaddr {
+				for _, a := range addrs {
+					if a.Equal(relayAddr) {
+						return []ma.Multiaddr{publicQUIC2}
+					}
+				}
+				return nil
+			},
+			ObservedAddrsManager: &mockObservedAddrs{
+				ObservedAddrsForFunc: func(addr ma.Multiaddr) []ma.Multiaddr {
+					return []ma.Multiaddr{publicQUIC}
+				},
+			},
+			ListenAddrs: func() []ma.Multiaddr { return []ma.Multiaddr{lhquic, lhtcp} },
+		})
+		am.PushReachability(network.ReachabilityPrivate)
+		am.PushRelay([]ma.Multiaddr{relayAddr})
+
+		expectedAddrs := []ma.Multiaddr{publicQUIC2}
+		expectedAllAddrs := []ma.Multiaddr{publicQUIC, lhquic, lhtcp}
+		require.EventuallyWithT(t, func(collect *assert.CollectT) {
+			assert.ElementsMatch(collect, am.Addrs(), expectedAddrs, "%s\n%s", am.Addrs(), expectedAddrs)
+			assert.ElementsMatch(collect, am.DirectAddrs(), expectedAllAddrs, "%s\n%s", am.DirectAddrs(), expectedAllAddrs)
+		}, 5*time.Second, 50*time.Millisecond)
+	})
+
+	t.Run("updates addresses on signaling", func(t *testing.T) {
+		updateChan := make(chan struct{})
+		am := newAddrsManagerTestCase(t, addrsManagerArgs{
+			AddrsFactory: func(addrs []ma.Multiaddr) []ma.Multiaddr {
+				select {
+				case <-updateChan:
+					return []ma.Multiaddr{publicQUIC}
+				default:
+					return []ma.Multiaddr{publicTCP}
+				}
+			},
+			ListenAddrs: func() []ma.Multiaddr { return []ma.Multiaddr{lhquic, lhtcp} },
+		})
+		require.Contains(t, am.Addrs(), publicTCP)
+		require.NotContains(t, am.Addrs(), publicQUIC)
+		close(updateChan)
+		am.triggerAddrsUpdate()
+		require.EventuallyWithT(t, func(collect *assert.CollectT) {
+			assert.Contains(collect, am.Addrs(), publicQUIC)
+			assert.NotContains(collect, am.Addrs(), publicTCP)
+		}, 1*time.Second, 50*time.Millisecond)
+	})
+}
+
+func BenchmarkAreAddrsDifferent(b *testing.B) {
+	var addrs [10]ma.Multiaddr
+	for i := 0; i < len(addrs); i++ {
+		addrs[i] = ma.StringCast(fmt.Sprintf("/ip4/1.1.1.%d/tcp/1", i))
+	}
+	am := &addrsManager{}
+	b.Run("areAddrsDifferent", func(b *testing.B) {
+		b.ReportAllocs()
+		b.ResetTimer()
+		for i := 0; i < b.N; i++ {
+			am.areAddrsDifferent(addrs[:], addrs[:])
+		}
+	})
+}
diff --git a/p2p/host/basic/basic_host_test.go b/p2p/host/basic/basic_host_test.go
index 895c5548..80a1d3dd 100644
--- a/p2p/host/basic/basic_host_test.go
+++ b/p2p/host/basic/basic_host_test.go
@@ -8,6 +8,7 @@ import (
 	"reflect"
 	"strings"
 	"sync"
+	"sync/atomic"
 	"testing"
 	"time"
 
@@ -206,29 +207,6 @@ func TestHostAddrsFactory(t *testing.T) {
 	}
 }
 
-func TestLocalIPChangesWhenListenAddrChanges(t *testing.T) {
-	// no listen addrs
-	h, err := NewHost(swarmt.GenSwarm(t, swarmt.OptDialOnly), nil)
-	require.NoError(t, err)
-	h.Start()
-	defer h.Close()
-
-	h.addrMu.Lock()
-	h.filteredInterfaceAddrs = nil
-	h.allInterfaceAddrs = nil
-	h.addrMu.Unlock()
-
-	// change listen addrs and verify local IP addr is not nil again
-	require.NoError(t, h.Network().Listen(ma.StringCast("/ip4/0.0.0.0/tcp/0")))
-	h.SignalAddressChange()
-	time.Sleep(1 * time.Second)
-
-	h.addrMu.RLock()
-	defer h.addrMu.RUnlock()
-	require.NotEmpty(t, h.filteredInterfaceAddrs)
-	require.NotEmpty(t, h.allInterfaceAddrs)
-}
-
 func TestAllAddrs(t *testing.T) {
 	// no listen addrs
 	h, err := NewHost(swarmt.GenSwarm(t, swarmt.OptDialOnly), nil)
@@ -620,8 +598,13 @@ func TestAddrChangeImmediatelyIfAddressNonEmpty(t *testing.T) {
 	ctx := context.Background()
 	taddrs := []ma.Multiaddr{ma.StringCast("/ip4/1.2.3.4/tcp/1234")}
 
-	starting := make(chan struct{})
+	starting := make(chan struct{}, 1)
+	var count atomic.Int32
 	h, err := NewHost(swarmt.GenSwarm(t), &HostOpts{AddrsFactory: func(addrs []ma.Multiaddr) []ma.Multiaddr {
+		// The first call here is made from the constructor. Don't block.
+		if count.Add(1) == 1 {
+			return addrs
+		}
 		<-starting
 		return taddrs
 	}})
@@ -629,11 +612,11 @@ func TestAddrChangeImmediatelyIfAddressNonEmpty(t *testing.T) {
 	defer h.Close()
 
 	sub, err := h.EventBus().Subscribe(&event.EvtLocalAddressesUpdated{})
-	close(starting)
 	if err != nil {
 		t.Error(err)
 	}
 	defer sub.Close()
+	close(starting)
 	h.Start()
 
 	expected := event.EvtLocalAddressesUpdated{
@@ -752,7 +735,7 @@ func TestHostAddrChangeDetection(t *testing.T) {
 		lk.Lock()
 		currentAddrSet = i
 		lk.Unlock()
-		h.SignalAddressChange()
+		h.addressManager.triggerAddrsUpdate()
 		evt := waitForAddrChangeEvent(ctx, sub, t)
 		if !updatedAddrEventsEqual(expectedEvents[i-1], evt) {
 			t.Errorf("change events not equal: \n\texpected: %v \n\tactual: %v", expectedEvents[i-1], evt)
diff --git a/p2p/host/routed/routed_test.go b/p2p/host/routed/routed_test.go
index 66b8054e..78ada28a 100644
--- a/p2p/host/routed/routed_test.go
+++ b/p2p/host/routed/routed_test.go
@@ -33,6 +33,7 @@ func TestRoutedHostConnectToObsoleteAddresses(t *testing.T) {
 	h2, err := basic.NewHost(swarmt.GenSwarm(t), nil)
 	require.NoError(t, err)
 	defer h2.Close()
+	h2.Start()
 
 	// assemble the AddrInfo struct to use for the connection attempt
 	pi := peer.AddrInfo{