Repository: libp2p/go-libp2p
Pull request #3071 (2024-11-29 09:57:56)

autorelay: send addresses on eventbus; dont wrap address factory
Part of #2229 


Patch:
diff --git a/config/config.go b/config/config.go
index ea94c067..b5d42eed 100644
--- a/config/config.go
+++ b/config/config.go
@@ -532,7 +532,7 @@ func (cfg *Config) NewNode() (host.Host, error) {
 	}
 	fxopts = append(fxopts, transportOpts...)
 
-	// Configure routing and autorelay
+	// Configure routing
 	if cfg.Routing != nil {
 		fxopts = append(fxopts,
 			fx.Provide(cfg.Routing),
diff --git a/core/event/addrs.go b/core/event/addrs.go
index 312a2fad..67849aed 100644
--- a/core/event/addrs.go
+++ b/core/event/addrs.go
@@ -81,3 +81,8 @@ type EvtLocalAddressesUpdated struct {
 	// wrapped in a record.Envelope and signed by the Host's private key.
 	SignedPeerRecord *record.Envelope
 }
+
+// EvtAutoRelayAddrsUpdated is sent by the autorelay when the node's relay addresses are updated
+type EvtAutoRelayAddrsUpdated struct {
+	RelayAddrs []ma.Multiaddr
+}
diff --git a/p2p/host/autorelay/addrsplosion.go b/p2p/host/autorelay/addrsplosion.go
index 710dab14..13e6274b 100644
--- a/p2p/host/autorelay/addrsplosion.go
+++ b/p2p/host/autorelay/addrsplosion.go
@@ -9,6 +9,9 @@ import (
 
 // This function cleans up a relay's address set to remove private addresses and curtail
 // addrsplosion.
+// TODO: Remove this, we don't need this. The current method tries to select the
+// best address for the relay. Instead we should rely on the addresses provided by the
+// relay in response to the reservation request.
 func cleanupAddressSet(addrs []ma.Multiaddr) []ma.Multiaddr {
 	var public, private []ma.Multiaddr
 
@@ -17,7 +20,7 @@ func cleanupAddressSet(addrs []ma.Multiaddr) []ma.Multiaddr {
 			continue
 		}
 
-		if manet.IsPublicAddr(a) || isDNSAddr(a) {
+		if manet.IsPublicAddr(a) {
 			public = append(public, a)
 			continue
 		}
@@ -51,16 +54,6 @@ func isRelayAddr(a ma.Multiaddr) bool {
 	return isRelay
 }
 
-func isDNSAddr(a ma.Multiaddr) bool {
-	if first, _ := ma.SplitFirst(a); first != nil {
-		switch first.Protocol().Code {
-		case ma.P_DNS, ma.P_DNS4, ma.P_DNS6, ma.P_DNSADDR:
-			return true
-		}
-	}
-	return false
-}
-
 // we have addrsplosion if for some protocol we advertise multiple ports on
 // the same base address.
 func hasAddrsplosion(addrs []ma.Multiaddr) bool {
diff --git a/p2p/host/autorelay/autorelay.go b/p2p/host/autorelay/autorelay.go
index b3130209..a8ac286d 100644
--- a/p2p/host/autorelay/autorelay.go
+++ b/p2p/host/autorelay/autorelay.go
@@ -3,16 +3,15 @@ package autorelay
 import (
 	"context"
 	"errors"
+	"fmt"
 	"sync"
 
 	"github.com/libp2p/go-libp2p/core/event"
 	"github.com/libp2p/go-libp2p/core/host"
 	"github.com/libp2p/go-libp2p/core/network"
-	basic "github.com/libp2p/go-libp2p/p2p/host/basic"
 	"github.com/libp2p/go-libp2p/p2p/host/eventbus"
 
 	logging "github.com/ipfs/go-log/v2"
-	ma "github.com/multiformats/go-multiaddr"
 )
 
 var log = logging.Logger("autorelay")
@@ -22,8 +21,6 @@ type AutoRelay struct {
 	ctx       context.Context
 	ctxCancel context.CancelFunc
 
-	conf *config
-
 	mx     sync.Mutex
 	status network.Reachability
 
@@ -34,9 +31,9 @@ type AutoRelay struct {
 	metricsTracer MetricsTracer
 }
 
-func NewAutoRelay(bhost *basic.BasicHost, opts ...Option) (*AutoRelay, error) {
+func NewAutoRelay(host host.Host, opts ...Option) (*AutoRelay, error) {
 	r := &AutoRelay{
-		host:   bhost,
+		host:   host,
 		status: network.ReachabilityUnknown,
 	}
 	conf := defaultConfig
@@ -46,25 +43,12 @@ func NewAutoRelay(bhost *basic.BasicHost, opts ...Option) (*AutoRelay, error) {
 		}
 	}
 	r.ctx, r.ctxCancel = context.WithCancel(context.Background())
-	r.conf = &conf
-	r.relayFinder = newRelayFinder(bhost, conf.peerSource, &conf)
-	r.metricsTracer = &wrappedMetricsTracer{conf.metricsTracer}
-
-	// Update the host address factory to use autorelay addresses if we're private
-	//
-	// TODO: Don't update host address factory. Instead send our relay addresses on the eventbus.
-	// The host can decide how to handle those.
-	addrF := bhost.AddrsFactory
-	bhost.AddrsFactory = func(addrs []ma.Multiaddr) []ma.Multiaddr {
-		addrs = addrF(addrs)
-		r.mx.Lock()
-		defer r.mx.Unlock()
-
-		if r.status != network.ReachabilityPrivate {
-			return addrs
-		}
-		return r.relayFinder.relayAddrs(addrs)
+	rf, err := newRelayFinder(host, &conf)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create autorelay: %w", err)
 	}
+	r.relayFinder = rf
+	r.metricsTracer = &wrappedMetricsTracer{conf.metricsTracer}
 
 	return r, nil
 }
@@ -93,7 +77,6 @@ func (r *AutoRelay) background() {
 			if !ok {
 				return
 			}
-			// TODO: push changed addresses
 			evt := ev.(event.EvtLocalReachabilityChanged)
 			switch evt.Reachability {
 			case network.ReachabilityPrivate, network.ReachabilityUnknown:
diff --git a/p2p/host/autorelay/relay.go b/p2p/host/autorelay/relay.go
deleted file mode 100644
index 2ae5bf24..00000000
--- a/p2p/host/autorelay/relay.go
+++ /dev/null
@@ -1,19 +0,0 @@
-package autorelay
-
-import (
-	ma "github.com/multiformats/go-multiaddr"
-)
-
-// Filter filters out all relay addresses.
-//
-// Deprecated: It is trivial for a user to implement this if they need this.
-func Filter(addrs []ma.Multiaddr) []ma.Multiaddr {
-	raddrs := make([]ma.Multiaddr, 0, len(addrs))
-	for _, addr := range addrs {
-		if isRelayAddr(addr) {
-			continue
-		}
-		raddrs = append(raddrs, addr)
-	}
-	return raddrs
-}
diff --git a/p2p/host/autorelay/relay_finder.go b/p2p/host/autorelay/relay_finder.go
index 54e38a15..009629a5 100644
--- a/p2p/host/autorelay/relay_finder.go
+++ b/p2p/host/autorelay/relay_finder.go
@@ -1,25 +1,27 @@
 package autorelay
 
 import (
+	"bytes"
 	"context"
 	"errors"
 	"fmt"
 	"math/rand"
+	"slices"
 	"sync"
 	"time"
 
 	"golang.org/x/sync/errgroup"
 
 	"github.com/libp2p/go-libp2p/core/event"
+	"github.com/libp2p/go-libp2p/core/host"
 	"github.com/libp2p/go-libp2p/core/network"
 	"github.com/libp2p/go-libp2p/core/peer"
-	basic "github.com/libp2p/go-libp2p/p2p/host/basic"
 	"github.com/libp2p/go-libp2p/p2p/host/eventbus"
 	circuitv2 "github.com/libp2p/go-libp2p/p2p/protocol/circuitv2/client"
 	circuitv2_proto "github.com/libp2p/go-libp2p/p2p/protocol/circuitv2/proto"
+	"github.com/libp2p/go-libp2p/p2p/protocol/identify"
 
 	ma "github.com/multiformats/go-multiaddr"
-	manet "github.com/multiformats/go-multiaddr/net"
 )
 
 const protoIDv2 = circuitv2_proto.ProtoIDv2Hop
@@ -27,7 +29,8 @@ const protoIDv2 = circuitv2_proto.ProtoIDv2Hop
 // Terminology:
 // Candidate: Once we connect to a node and it supports relay protocol,
 // we call it a candidate, and consider using it as a relay.
-// Relay: Out of the list of candidates, we select a relay to connect to.
+//
+// Relay: Out of the list candidates, the ones we have a reservation with.
 // Currently, we just randomly select a candidate, but we can employ more sophisticated
 // selection strategies here (e.g. by facotring in the RTT).
 
@@ -35,7 +38,8 @@ const (
 	rsvpRefreshInterval = time.Minute
 	rsvpExpirationSlack = 2 * time.Minute
 
-	autorelayTag = "autorelay"
+	autorelayTag  = "autorelay"
+	maxRelayAddrs = 100
 )
 
 type candidate struct {
@@ -47,7 +51,7 @@ type candidate struct {
 // relayFinder is a Host that uses relays for connectivity when a NAT is detected.
 type relayFinder struct {
 	bootTime time.Time
-	host     *basic.BasicHost
+	host     host.Host
 
 	conf *config
 
@@ -70,31 +74,37 @@ type relayFinder struct {
 	// * a candidate is deleted due to its age
 	maybeRequestNewCandidates chan struct{} // cap: 1.
 
-	relayUpdated chan struct{}
+	relayReservationUpdated chan struct{}
 
 	relayMx sync.Mutex
 	relays  map[peer.ID]*circuitv2.Reservation
 
-	cachedAddrs       []ma.Multiaddr
-	cachedAddrsExpiry time.Time
+	circuitAddrs []ma.Multiaddr
 
 	// A channel that triggers a run of `runScheduledWork`.
 	triggerRunScheduledWork chan struct{}
 	metricsTracer           MetricsTracer
+
+	emitter event.Emitter
 }
 
 var errAlreadyRunning = errors.New("relayFinder already running")
 
-func newRelayFinder(host *basic.BasicHost, peerSource PeerSource, conf *config) *relayFinder {
-	if peerSource == nil {
+func newRelayFinder(host host.Host, conf *config) (*relayFinder, error) {
+	if conf.peerSource == nil {
 		panic("Can not create a new relayFinder. Need a Peer Source fn or a list of static relays. Refer to the documentation around `libp2p.EnableAutoRelay`")
 	}
 
+	emitter, err := host.EventBus().Emitter(new(event.EvtAutoRelayAddrsUpdated), eventbus.Stateful)
+	if err != nil {
+		return nil, err
+	}
+
 	return &relayFinder{
 		bootTime:                   conf.clock.Now(),
 		host:                       host,
 		conf:                       conf,
-		peerSource:                 peerSource,
+		peerSource:                 conf.peerSource,
 		candidates:                 make(map[peer.ID]*candidate),
 		backoff:                    make(map[peer.ID]time.Time),
 		candidateFound:             make(chan struct{}, 1),
@@ -102,9 +112,10 @@ func newRelayFinder(host *basic.BasicHost, peerSource PeerSource, conf *config)
 		maybeRequestNewCandidates:  make(chan struct{}, 1),
 		triggerRunScheduledWork:    make(chan struct{}, 1),
 		relays:                     make(map[peer.ID]*circuitv2.Reservation),
-		relayUpdated:               make(chan struct{}, 1),
+		relayReservationUpdated:    make(chan struct{}, 1),
 		metricsTracer:              &wrappedMetricsTracer{conf.metricsTracer},
-	}
+		emitter:                    emitter,
+	}, nil
 }
 
 type scheduledWorkTimes struct {
@@ -147,7 +158,7 @@ func (rf *relayFinder) cleanupDisconnectedPeers(ctx context.Context) {
 			rf.relayMx.Unlock()
 
 			if push {
-				rf.clearCachedAddrsAndSignalAddressChange()
+				rf.notifyRelayReservationUpdated()
 				rf.metricsTracer.ReservationEnded(1)
 			}
 		}
@@ -198,14 +209,16 @@ func (rf *relayFinder) background(ctx context.Context) {
 
 	go rf.cleanupDisconnectedPeers(ctx)
 
+	// update addrs on starting the relay finder.
+	rf.updateAddrs()
 	for {
 		select {
 		case <-rf.candidateFound:
 			rf.notifyMaybeConnectToRelay()
 		case <-bootDelayTimer.Ch():
 			rf.notifyMaybeConnectToRelay()
-		case <-rf.relayUpdated:
-			rf.clearCachedAddrsAndSignalAddressChange()
+		case <-rf.relayReservationUpdated:
+			rf.updateAddrs()
 		case now := <-workTimer.Ch():
 			// Note: `now` is not guaranteed to be the current time. It's the time
 			// that the timer was fired. This is okay because we'll schedule
@@ -221,13 +234,39 @@ func (rf *relayFinder) background(ctx context.Context) {
 	}
 }
 
-func (rf *relayFinder) clearCachedAddrsAndSignalAddressChange() {
-	rf.relayMx.Lock()
-	rf.cachedAddrs = nil
-	rf.relayMx.Unlock()
-	rf.host.SignalAddressChange()
+func (rf *relayFinder) updateAddrs() {
+	oldAddrs := rf.circuitAddrs
+	rf.circuitAddrs = rf.getCircuitAddrs()
+
+	if areSortedAddrsDifferent(rf.circuitAddrs, oldAddrs) {
+		log.Debug("relay addresses updated", rf.circuitAddrs)
+		rf.metricsTracer.RelayAddressUpdated()
+		rf.metricsTracer.RelayAddressCount(len(rf.circuitAddrs))
+		if err := rf.emitter.Emit(event.EvtAutoRelayAddrsUpdated{RelayAddrs: slices.Clone(rf.circuitAddrs)}); err != nil {
+			log.Error("failed to emit event.EvtAutoRelayAddrs with RelayAddrs", rf.circuitAddrs, err)
+		}
+	}
+}
+
+// This function returns the p2p-circuit addrs for the host.
+// The returned addresses are of the form <relay's-addr>/p2p/<relay's-id>/p2p-circuit.
+func (rf *relayFinder) getCircuitAddrs() []ma.Multiaddr {
+	raddrs := make([]ma.Multiaddr, 0, 4*len(rf.relays)+4)
+	for p := range rf.relays {
+		addrs := cleanupAddressSet(rf.host.Peerstore().Addrs(p))
+		circuit := ma.StringCast(fmt.Sprintf("/p2p/%s/p2p-circuit", p))
+		for _, addr := range addrs {
+			pub := addr.Encapsulate(circuit)
+			raddrs = append(raddrs, pub)
+		}
+	}
 
-	rf.metricsTracer.RelayAddressUpdated()
+	// Sort the addresses. We depend on this order for checking diffs to send address update events.
+	slices.SortStableFunc(raddrs, func(a, b ma.Multiaddr) int { return bytes.Compare(a.Bytes(), b.Bytes()) })
+	if len(raddrs) > maxRelayAddrs {
+		raddrs = raddrs[:maxRelayAddrs]
+	}
+	return raddrs
 }
 
 func (rf *relayFinder) runScheduledWork(ctx context.Context, now time.Time, scheduledWork *scheduledWorkTimes, peerSourceRateLimiter chan<- struct{}) time.Time {
@@ -236,7 +275,7 @@ func (rf *relayFinder) runScheduledWork(ctx context.Context, now time.Time, sche
 	if now.After(scheduledWork.nextRefresh) {
 		scheduledWork.nextRefresh = now.Add(rsvpRefreshInterval)
 		if rf.refreshReservations(ctx, now) {
-			rf.clearCachedAddrsAndSignalAddressChange()
+			rf.notifyRelayReservationUpdated()
 		}
 	}
 
@@ -427,6 +466,13 @@ func (rf *relayFinder) notifyNewCandidate() {
 	}
 }
 
+func (rf *relayFinder) notifyRelayReservationUpdated() {
+	select {
+	case rf.relayReservationUpdated <- struct{}{}:
+	default:
+	}
+}
+
 // handleNewNode tests if a peer supports circuit v2.
 // This method is only run on private nodes.
 // If a peer does, it is added to the candidates map.
@@ -483,11 +529,16 @@ func (rf *relayFinder) tryNode(ctx context.Context, pi peer.AddrInfo) (supportsR
 	}
 
 	// wait for identify to complete in at least one conn so that we can check the supported protocols
+	hi, ok := rf.host.(interface{ IDService() identify.IDService })
+	if !ok {
+		// if we don't have identify, assume the peer supports relay.
+		return true, nil
+	}
 	ready := make(chan struct{}, 1)
 	for _, conn := range conns {
 		go func(conn network.Conn) {
 			select {
-			case <-rf.host.IDService().IdentifyWait(conn):
+			case <-hi.IDService().IdentifyWait(conn):
 				select {
 				case ready <- struct{}{}:
 				default:
@@ -581,10 +632,7 @@ func (rf *relayFinder) maybeConnectToRelay(ctx context.Context) {
 
 		rf.host.ConnManager().Protect(id, autorelayTag) // protect the connection
 
-		select {
-		case rf.relayUpdated <- struct{}{}:
-		default:
-		}
+		rf.notifyRelayReservationUpdated()
 
 		rf.metricsTracer.ReservationRequestFinished(false, nil)
 
@@ -642,7 +690,6 @@ func (rf *relayFinder) refreshReservations(ctx context.Context, now time.Time) b
 		g.Go(func() error {
 			err := rf.refreshRelayReservation(ctx, p)
 			rf.metricsTracer.ReservationRequestFinished(true, err)
-
 			return err
 		})
 	}
@@ -717,49 +764,6 @@ func (rf *relayFinder) selectCandidates() []*candidate {
 	return candidates
 }
 
-// This function is computes the NATed relay addrs when our status is private:
-//   - The public addrs are removed from the address set.
-//   - The non-public addrs are included verbatim so that peers behind the same NAT/firewall
-//     can still dial us directly.
-//   - On top of those, we add the relay-specific addrs for the relays to which we are
-//     connected. For each non-private relay addr, we encapsulate the p2p-circuit addr
-//     through which we can be dialed.
-func (rf *relayFinder) relayAddrs(addrs []ma.Multiaddr) []ma.Multiaddr {
-	rf.relayMx.Lock()
-	defer rf.relayMx.Unlock()
-
-	if rf.cachedAddrs != nil && rf.conf.clock.Now().Before(rf.cachedAddrsExpiry) {
-		return rf.cachedAddrs
-	}
-
-	raddrs := make([]ma.Multiaddr, 0, 4*len(rf.relays)+4)
-
-	// only keep private addrs from the original addr set
-	for _, addr := range addrs {
-		if manet.IsPrivateAddr(addr) {
-			raddrs = append(raddrs, addr)
-		}
-	}
-
-	// add relay specific addrs to the list
-	relayAddrCnt := 0
-	for p := range rf.relays {
-		addrs := cleanupAddressSet(rf.host.Peerstore().Addrs(p))
-		relayAddrCnt += len(addrs)
-		circuit := ma.StringCast(fmt.Sprintf("/p2p/%s/p2p-circuit", p))
-		for _, addr := range addrs {
-			pub := addr.Encapsulate(circuit)
-			raddrs = append(raddrs, pub)
-		}
-	}
-
-	rf.cachedAddrs = raddrs
-	rf.cachedAddrsExpiry = rf.conf.clock.Now().Add(30 * time.Second)
-
-	rf.metricsTracer.RelayAddressCount(relayAddrCnt)
-	return raddrs
-}
-
 func (rf *relayFinder) Start() error {
 	rf.ctxCancelMx.Lock()
 	defer rf.ctxCancelMx.Unlock()
@@ -818,3 +822,15 @@ func (rf *relayFinder) resetMetrics() {
 	rf.metricsTracer.RelayAddressCount(0)
 	rf.metricsTracer.ScheduledWorkUpdated(&scheduledWorkTimes{})
 }
+
+func areSortedAddrsDifferent(a, b []ma.Multiaddr) bool {
+	if len(a) != len(b) {
+		return true
+	}
+	for i, aa := range a {
+		if !aa.Equal(b[i]) {
+			return true
+		}
+	}
+	return false
+}
diff --git a/p2p/host/basic/basic_host.go b/p2p/host/basic/basic_host.go
index a85d1978..202b4460 100644
--- a/p2p/host/basic/basic_host.go
+++ b/p2p/host/basic/basic_host.go
@@ -8,6 +8,7 @@ import (
 	"net"
 	"slices"
 	"sync"
+	"sync/atomic"
 	"time"
 
 	"github.com/libp2p/go-libp2p/core/connmgr"
@@ -96,12 +97,14 @@ type BasicHost struct {
 
 	addrChangeChan chan struct{}
 
-	addrMu                 sync.RWMutex
+	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
@@ -185,6 +188,7 @@ func NewHost(n network.Network, opts *HostOpts) (*BasicHost, error) {
 	if err != nil {
 		return nil, err
 	}
+
 	hostCtx, cancel := context.WithCancel(context.Background())
 	h := &BasicHost{
 		network:                 n,
@@ -199,6 +203,8 @@ func NewHost(n network.Network, opts *HostOpts) (*BasicHost, error) {
 		disableSignedPeerRecord: opts.DisableSignedPeerRecord,
 	}
 
+	var relayAddrs []ma.Multiaddr
+	h.relayAddrs.Store(&relayAddrs)
 	h.updateLocalIpAddr()
 
 	if h.emitters.evtLocalProtocolsUpdated, err = h.eventbus.Emitter(&event.EvtLocalProtocolsUpdated{}, eventbus.Stateful); err != nil {
@@ -595,6 +601,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)
@@ -612,6 +625,15 @@ func (h *BasicHost) background() {
 		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.ctx.Done():
 			return
 		}
@@ -841,8 +863,17 @@ 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
-	addrs := slices.Clone(h.AddrsFactory(h.AllAddrs()))
+	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))
 }


Test patch:
diff --git a/libp2p_test.go b/libp2p_test.go
index 723c3af4..b3bef5d2 100644
--- a/libp2p_test.go
+++ b/libp2p_test.go
@@ -466,7 +466,6 @@ func TestDialCircuitAddrWithWrappedResourceManager(t *testing.T) {
 		),
 		peerstore.TempAddrTTL,
 	)
-	require.NoError(t, err)
 
 	require.Eventually(t, func() bool {
 		ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
diff --git a/p2p/host/autorelay/autorelay_test.go b/p2p/host/autorelay/autorelay_test.go
index 410f8cb6..a2186b51 100644
--- a/p2p/host/autorelay/autorelay_test.go
+++ b/p2p/host/autorelay/autorelay_test.go
@@ -3,12 +3,14 @@ package autorelay_test
 import (
 	"context"
 	"fmt"
+	"slices"
 	"strings"
 	"sync/atomic"
 	"testing"
 	"time"
 
 	"github.com/libp2p/go-libp2p"
+	"github.com/libp2p/go-libp2p/core/event"
 	"github.com/libp2p/go-libp2p/core/host"
 	"github.com/libp2p/go-libp2p/core/network"
 	"github.com/libp2p/go-libp2p/core/peer"
@@ -17,6 +19,7 @@ import (
 	circuitv2_proto "github.com/libp2p/go-libp2p/p2p/protocol/circuitv2/proto"
 
 	ma "github.com/multiformats/go-multiaddr"
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
@@ -96,7 +99,10 @@ func newRelay(t *testing.T) host.Host {
 				saddr := addr.String()
 				if strings.HasPrefix(saddr, "/ip4/127.0.0.1/") {
 					addrNoIP := strings.TrimPrefix(saddr, "/ip4/127.0.0.1")
-					addrs[i] = ma.StringCast("/dns4/localhost" + addrNoIP)
+					// .internal is classified as a public address as users
+					// are free to map this dns to a public ip address for
+					// use within a LAN
+					addrs[i] = ma.StringCast("/dns/libp2p.internal" + addrNoIP)
 				}
 			}
 			return addrs
@@ -517,3 +523,81 @@ func TestNoBusyLoop0MinInterval(t *testing.T) {
 	val := atomic.LoadUint64(&calledTimes)
 	require.Less(t, val, uint64(2))
 }
+func TestAutoRelayAddrsEvent(t *testing.T) {
+	cl := newMockClock()
+	relays := []host.Host{newRelay(t), newRelay(t), newRelay(t), newRelay(t), newRelay(t)}
+	t.Cleanup(func() {
+		for _, r := range relays {
+			r.Close()
+		}
+	})
+
+	relayIDFromP2PAddr := func(a ma.Multiaddr) peer.ID {
+		r, c := ma.SplitLast(a)
+		if c.Protocol().Code != ma.P_CIRCUIT {
+			return ""
+		}
+		if id, err := peer.IDFromP2PAddr(r); err == nil {
+			return id
+		}
+		return ""
+	}
+
+	checkAddrsContainsPeersAsRelay := func(addrs []ma.Multiaddr, peers ...peer.ID) bool {
+		for _, p := range peers {
+			if !slices.ContainsFunc(addrs, func(a ma.Multiaddr) bool { return relayIDFromP2PAddr(a) == p }) {
+				return false
+			}
+		}
+		return true
+	}
+	peerChan := make(chan peer.AddrInfo, 5)
+	h := newPrivateNode(t,
+		func(context.Context, int) <-chan peer.AddrInfo {
+			return peerChan
+		},
+		autorelay.WithClock(cl),
+		autorelay.WithMinCandidates(1),
+		autorelay.WithMaxCandidates(10),
+		autorelay.WithNumRelays(5),
+		autorelay.WithBootDelay(1*time.Second),
+		autorelay.WithMinInterval(time.Hour),
+	)
+	defer h.Close()
+
+	sub, err := h.EventBus().Subscribe(new(event.EvtAutoRelayAddrsUpdated))
+	require.NoError(t, err)
+
+	peerChan <- peer.AddrInfo{ID: relays[0].ID(), Addrs: relays[0].Addrs()}
+	cl.AdvanceBy(time.Second)
+
+	require.EventuallyWithT(t, func(collect *assert.CollectT) {
+		e := <-sub.Out()
+		evt := e.(event.EvtAutoRelayAddrsUpdated)
+		if !checkAddrsContainsPeersAsRelay(evt.RelayAddrs, relays[0].ID()) {
+			collect.Errorf("expected %s to be in %v", relays[0].ID(), evt.RelayAddrs)
+		}
+		if checkAddrsContainsPeersAsRelay(evt.RelayAddrs, relays[1].ID()) {
+			collect.Errorf("expected %s to not be in %v", relays[1].ID(), evt.RelayAddrs)
+		}
+	}, 5*time.Second, 50*time.Millisecond)
+	for _, r := range relays[1:] {
+		peerChan <- peer.AddrInfo{ID: r.ID(), Addrs: r.Addrs()}
+	}
+	require.EventuallyWithT(t, func(c *assert.CollectT) {
+		e := <-sub.Out()
+		evt := e.(event.EvtAutoRelayAddrsUpdated)
+		relayIds := []peer.ID{}
+		for _, r := range relays[1:] {
+			relayIds = append(relayIds, r.ID())
+		}
+		if !checkAddrsContainsPeersAsRelay(evt.RelayAddrs, relayIds...) {
+			c.Errorf("expected %s to be in %v", relayIds, evt.RelayAddrs)
+		}
+	}, 5*time.Second, 50*time.Millisecond)
+	select {
+	case e := <-sub.Out():
+		t.Fatal("expected no more events after all reservations obtained; got: ", e.(event.EvtAutoRelayAddrsUpdated))
+	case <-time.After(1 * time.Second):
+	}
+}