Repository: libp2p/go-libp2p
Pull request #2093 (2023-02-14 08:42:40)

relaymanager: do not start new relay if one already exists
fixes: https://github.com/libp2p/go-libp2p/issues/2091


Patch:
diff --git a/.github/ISSUE_TEMPLATE/release.md b/.github/ISSUE_TEMPLATE/release.md
index e23d61a5..5f77e72c 100644
--- a/.github/ISSUE_TEMPLATE/release.md
+++ b/.github/ISSUE_TEMPLATE/release.md
@@ -27,5 +27,7 @@ about: 'Start a new libp2p release.'
   - [ ] Update the examples to the final release
   - [ ] Update the upstream dependencies to the final release and create PRs.
     - [ ] [filecoin-project/lotus](https://github.com/filecoin-project/lotus)
+    - [ ] [go-libp2p-kad-dht](https://github.com/libp2p/go-libp2p-kad-dht/)
+    - [ ] [go-libp2p-pubsub](https://github.com/libp2p/go-libp2p-pubsub) (In case of breaking changes.)
     - [ ] [ipfs/kubo](https://github.com/ipfs/kubo)
 - [ ] Make required changes to the release process.
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 00000000..b6e7d2c0
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,125 @@
+# Table Of Contents <!-- omit in toc -->
+- [v0.26.0](#v0260)
+- [v0.25.1](#v0251)
+- [v0.25.0](#v0250)
+
+# [v0.26.0](https://github.com/libp2p/go-libp2p/releases/tag/v0.26.0)
+
+## 🔦 Highlights <!-- omit in toc -->
+
+## 🐞 Bugfixes <!-- omit in toc -->
+
+**Full Changelog**: https://github.com/libp2p/go-libp2p/compare/v0.25.1...v0.26.0
+
+# [v0.25.1](https://github.com/libp2p/go-libp2p/releases/tag/v0.25.1)
+
+Fix some test-utils used by https://github.com/libp2p/go-libp2p-kad-dht
+
+* mocknet: Start host in mocknet by @MarcoPolo in https://github.com/libp2p/go-libp2p/pull/2078
+* chore: update go-multistream by @MarcoPolo in https://github.com/libp2p/go-libp2p/pull/2081
+
+**Full Changelog**: https://github.com/libp2p/go-libp2p/compare/v0.25.0...v0.25.1
+
+# [v0.25.0](https://github.com/libp2p/go-libp2p/releases/tag/v0.25.0)
+
+## 🔦 Highlights <!-- omit in toc -->
+
+### Metrics <!-- omit in toc -->
+
+We've started instrumenting the entire stack. In this release, we're adding metrics for:
+* the swarm: tracking incoming and outgoing connections, transports, security protocols and stream multiplexers in sue: (https://github.com/libp2p/go-libp2p/blob/master/p2p/net/swarm/grafana-dashboards/swarm.json)
+* the event bus: tracking how different events are propagated through the stack and to external consumers (https://github.com/libp2p/go-libp2p/blob/master/p2p/host/eventbus/grafana-dashboards/eventbus.json)
+
+Our metrics effort is still ongoing, see https://github.com/libp2p/go-libp2p/issues/1356 for progress. We'll add metrics and dashboards for more libp2p components in a future release.
+
+### Switching to Google's official Protobuf compiler <!-- omit in toc -->
+
+So far, we were using GoGo Protobuf to compile our Protobuf definitions to Go code. However, this library was deprecated in October last year: https://twitter.com/awalterschulze/status/1584553056100057088. We [benchmarked](https://github.com/libp2p/go-libp2p/issues/1976#issuecomment-1371527732) serialization and deserialization, and found that it's (only) 20% slower than GoGo. Since the vast majority of go-libp2p's CPU time is spent in code paths other than Protobuf handling, switching to the official compiler seemed like a worthwhile tradeoff.
+
+### Removal of OpenSSL <!-- omit in toc -->
+
+Before this release, go-libp2p had an option to use OpenSSL bindings for certain cryptographic primitives, mostly to speed up the generation of signatures and their verification. When building go-libp2p using `go build`, we'd use the standard library crypto packages. OpenSSL was only used when passing in a build tag: `go build -tags openssl`.
+Maintaining our own fork of the long unmaintained [go-openssl package](https://github.com/libp2p/go-openssl) has proven to place a larger than expected maintenance burden on the libp2p stewards, and when we recently discovered a range of new bugs ([this](https://github.com/libp2p/go-openssl/issues/38) and [this](https://github.com/libp2p/go-libp2p/issues/1892) and [this](https://github.com/libp2p/go-libp2p/issues/1951)), we decided to re-evaluate if this code path is really worth it. The results surprised us, it turns out that:
+* The Go standard library is faster than OpenSSL for all key types that are not RSA.
+* Verifying RSA signatures is as fast as Ed25519 signatures using the Go standard library, and even faster in OpenSSL.
+* Generating RSA signatures is painfully slow, both using Go standard library crypto and using OpenSSL (but even slower using Go standard library).
+
+Now the good news is, that if your node is not using an RSA key, it will never create any RSA signatures (it might need to verify them though, when it connects to a node that uses RSA keys). If you're concerned about CPU performance, it's a good idea to avoid RSA keys (the same applies to bandwidth, RSA keys are huge!). Even for nodes using RSA keys, it turns out that generating the signatures is not a significant part of their CPU load, as verified by profiling one of Kubo's bootstrap nodes.
+
+We therefore concluded that it's safe to drop this code path altogether, and thereby reduce our maintenance burden.
+
+### New Resource Manager types <!-- omit in toc -->
+
+* Introduces a new type `LimitVal` which can explicitly specify "use default", "unlimited", "block all", as well as any positive number. The zero value of `LimitVal` (the value when you create the object in Go) is "Use default".
+  * The JSON marshalling of this is straightforward.
+* Introduces a new `ResourceLimits` type which uses `LimitVal` instead of ints so it can encode the above for the resources.
+* Changes `LimitConfig` to `PartialLimitConfig` and uses `ResourceLimits`. This along with the marshalling changes means you can now marshal the fact that some resource limit is set to block all.
+  * Because the default is to use the defaults, this avoids the footgun of initializing the resource manager with 0 limits (that would block everything).
+  
+In general, you can go from a resource config with defaults to a concrete one with `.Build()`. e.g. `ResourceLimits.Build() => BaseLimit`, `PartialLimitConfig.Build() => ConcreteLimitConfig`, `LimitVal.Build() => int`. See PR #2000 for more details.
+
+If you're using the defaults for the resource manager, there should be no changes needed.
+
+### Other Breaking Changes <!-- omit in toc -->
+
+We've cleaned up our API to consistently use `protocol.ID` for libp2p and application protocols. Specifically, this means that the peer store now uses `protocol.ID`s, and the host's `SetStreamHandler` as well.
+
+## What's Changed <!-- omit in toc -->
+* chore: use generic LRU cache by @muXxer in https://github.com/libp2p/go-libp2p/pull/1980
+* core/crypto: drop all OpenSSL code paths by @marten-seemann in https://github.com/libp2p/go-libp2p/pull/1953
+* add WebTransport to the list of default transports by @marten-seemann in https://github.com/libp2p/go-libp2p/pull/1915
+* identify: remove old code targeting Go 1.17 by @marten-seemann in https://github.com/libp2p/go-libp2p/pull/1964
+* core: remove introspection package by @marten-seemann in https://github.com/libp2p/go-libp2p/pull/1978
+* identify: remove support for Identify Delta by @marten-seemann in https://github.com/libp2p/go-libp2p/pull/1975
+* roadmap: remove optimizations of the TCP-based handshake by @marten-seemann in https://github.com/libp2p/go-libp2p/pull/1959
+* circuitv2: correctly set the transport in the ConnectionState by @marten-seemann in https://github.com/libp2p/go-libp2p/pull/1972
+* switch to Google's Protobuf library, make protobufs compile with go generate by @marten-seemann in https://github.com/libp2p/go-libp2p/pull/1979
+* ci: run go generate as part of the go-check workflow by @marten-seemann in https://github.com/libp2p/go-libp2p/pull/1986
+* ci: use GitHub token to install protoc by @marten-seemann in https://github.com/libp2p/go-libp2p/pull/1996
+* feat: add some users to the readme by @p-shahi in https://github.com/libp2p/go-libp2p/pull/1981
+* CI: Fast multidimensional Interop tests by @MarcoPolo in https://github.com/libp2p/go-libp2p/pull/1991
+* Fix: Ignore zero values when marshalling Limits. by @ajnavarro in https://github.com/libp2p/go-libp2p/pull/1998
+* feat: add ci flakiness score to readme by @MarcoPolo in https://github.com/libp2p/go-libp2p/pull/2002
+* peerstore: make it possible to use an empty peer ID by @marten-seemann in https://github.com/libp2p/go-libp2p/pull/2006
+* feat: rcmgr: Export resource manager errors by @MarcoPolo in https://github.com/libp2p/go-libp2p/pull/2008
+* feat: ci test-plans: Parse test timeout parameter for interop test by @MarcoPolo in https://github.com/libp2p/go-libp2p/pull/2014
+* Clean addresses with peer id before adding to addrbook by @sukunrt in https://github.com/libp2p/go-libp2p/pull/2007
+* Expose muxer ids by @aschmahmann in https://github.com/libp2p/go-libp2p/pull/2012
+* swarm: add a basic metrics tracer by @marten-seemann in https://github.com/libp2p/go-libp2p/pull/1973
+* consistently use protocol.ID instead of strings by @sukunrt in https://github.com/libp2p/go-libp2p/pull/2004
+* swarm metrics: fix datasource for dashboard by @MarcoPolo in https://github.com/libp2p/go-libp2p/pull/2024
+* chore: remove textual roadmap in favor for Starmap by @p-shahi in https://github.com/libp2p/go-libp2p/pull/2036
+* rcmgr: *: Always close connscope by @MarcoPolo in https://github.com/libp2p/go-libp2p/pull/2037
+* chore: remove license files from the eventbus package by @marten-seemann in https://github.com/libp2p/go-libp2p/pull/2042
+* Migrate to test-plan composite action by @thomaseizinger in https://github.com/libp2p/go-libp2p/pull/2039
+* use quic-go and webtransport-go from quic-go organization by @marten-seemann in https://github.com/libp2p/go-libp2p/pull/2040
+* holepunch: fix flaky test by not removing holepunch protocol handler by @marten-seemann in https://github.com/libp2p/go-libp2p/pull/1948
+* quic / webtransport: extend test to test dialing a draft-29 and a v1  by @marten-seemann in https://github.com/libp2p/go-libp2p/pull/1957
+* p2p/test: add test for EvtLocalAddressesUpdated event by @marten-seemann in https://github.com/libp2p/go-libp2p/pull/2016
+* quic, tcp: only register Prometheus counters when metrics are enabled by @marten-seemann in https://github.com/libp2p/go-libp2p/pull/1971
+* p2p/test: fix flaky notification test by @marten-seemann in https://github.com/libp2p/go-libp2p/pull/2051
+* quic: disable sending of Version Negotiation packets by @marten-seemann in https://github.com/libp2p/go-libp2p/pull/2015
+* eventbus: add metrics by @sukunrt in https://github.com/libp2p/go-libp2p/pull/2038
+* metrics: use a single slice pool for all metrics tracer by @marten-seemann in https://github.com/libp2p/go-libp2p/pull/2054
+* webtransport: tidy up some test output by @MarcoPolo in https://github.com/libp2p/go-libp2p/pull/2053
+* set names for eventbus event subscriptions by @marten-seemann in https://github.com/libp2p/go-libp2p/pull/2057
+* autorelay: Split libp2p.EnableAutoRelay into 2 functions by @sukunrt in https://github.com/libp2p/go-libp2p/pull/2022
+* rcmgr: Use prometheus SDK for rcmgr metrics by @MarcoPolo in https://github.com/libp2p/go-libp2p/pull/2044
+* websocket: Replace gorilla websocket transport with nhooyr websocket transport by @MarcoPolo in https://github.com/libp2p/go-libp2p/pull/1982
+* rcmgr: add libp2p prefix to all metrics by @marten-seemann in https://github.com/libp2p/go-libp2p/pull/2063
+* chore: git-ignore various flavors of qlog files by @marten-seemann in https://github.com/libp2p/go-libp2p/pull/2064
+* interop: Update interop test to match spec by @MarcoPolo in https://github.com/libp2p/go-libp2p/pull/2049
+* chore: update webtransport-go to v0.5.1 by @marten-seemann in https://github.com/libp2p/go-libp2p/pull/2072
+* identify: refactor sending of Identify pushes by @marten-seemann in https://github.com/libp2p/go-libp2p/pull/1984
+* feat!: rcmgr: Change LimitConfig to use LimitVal type by @MarcoPolo in https://github.com/libp2p/go-libp2p/pull/2000
+* p2p/test/quic: use contexts with a timeout for Connect calls by @marten-seemann in https://github.com/libp2p/go-libp2p/pull/2070
+* identify: add some basic metrics by @marten-seemann in https://github.com/libp2p/go-libp2p/pull/2069
+* chore: Release v0.25.0 by @MarcoPolo in https://github.com/libp2p/go-libp2p/pull/2077
+
+## New Contributors <!-- omit in toc -->
+* @muXxer made their first contribution in https://github.com/libp2p/go-libp2p/pull/1980
+* @ajnavarro made their first contribution in https://github.com/libp2p/go-libp2p/pull/1998
+* @sukunrt made their first contribution in https://github.com/libp2p/go-libp2p/pull/2007
+* @thomaseizinger made their first contribution in https://github.com/libp2p/go-libp2p/pull/2039
+
+**Full Changelog**: https://github.com/libp2p/go-libp2p/compare/v0.24.2...v0.25.0
diff --git a/p2p/host/autonat/autonat.go b/p2p/host/autonat/autonat.go
index eee5481a..dd451545 100644
--- a/p2p/host/autonat/autonat.go
+++ b/p2p/host/autonat/autonat.go
@@ -286,10 +286,13 @@ func (as *AmbientAutoNAT) scheduleProbe() time.Duration {
 // Update the current status based on an observed result.
 func (as *AmbientAutoNAT) recordObservation(observation autoNATResult) {
 	currentStatus := as.status.Load()
+
 	if observation.Reachability == network.ReachabilityPublic {
 		log.Debugf("NAT status is public")
 		changed := false
 		if currentStatus.Reachability != network.ReachabilityPublic {
+			// Aggressively switch to public from other states ignoring confidence
+
 			// we are flipping our NATStatus, so confidence drops to 0
 			as.confidence = 0
 			if as.service != nil {
@@ -299,21 +302,13 @@ func (as *AmbientAutoNAT) recordObservation(observation autoNATResult) {
 		} else if as.confidence < 3 {
 			as.confidence++
 		}
-		if observation.address != nil {
-			if !changed && currentStatus.address != nil && !observation.address.Equal(currentStatus.address) {
-				as.confidence--
-			}
-			if currentStatus.address == nil || !observation.address.Equal(currentStatus.address) {
-				changed = true
-			}
-			as.status.Store(&observation)
-		}
-		if observation.address != nil && changed {
+		as.status.Store(&observation)
+		if changed {
 			as.emitStatus()
 		}
 	} else if observation.Reachability == network.ReachabilityPrivate {
 		log.Debugf("NAT status is private")
-		if currentStatus.Reachability == network.ReachabilityPublic {
+		if currentStatus.Reachability != network.ReachabilityPrivate {
 			if as.confidence > 0 {
 				as.confidence--
 			} else {
@@ -328,9 +323,6 @@ func (as *AmbientAutoNAT) recordObservation(observation autoNATResult) {
 		} else if as.confidence < 3 {
 			as.confidence++
 			as.status.Store(&observation)
-			if currentStatus.Reachability != network.ReachabilityPrivate {
-				as.emitStatus()
-			}
 		}
 	} else if as.confidence > 0 {
 		// don't just flip to unknown, reduce confidence first
diff --git a/p2p/host/eventbus/basic.go b/p2p/host/eventbus/basic.go
index ae477887..42365a79 100644
--- a/p2p/host/eventbus/basic.go
+++ b/p2p/host/eventbus/basic.go
@@ -50,7 +50,7 @@ func (e *emitter) Close() error {
 	if !e.closed.CompareAndSwap(false, true) {
 		return fmt.Errorf("closed an emitter more than once")
 	}
-	if atomic.AddInt32(&e.n.nEmitters, -1) == 0 {
+	if e.n.nEmitters.Add(-1) == 0 {
 		e.dropper(e.typ)
 	}
 	return nil
@@ -100,7 +100,7 @@ func (b *basicBus) tryDropNode(typ reflect.Type) {
 	}
 
 	n.lk.Lock()
-	if atomic.LoadInt32(&n.nEmitters) > 0 || len(n.sinks) > 0 {
+	if n.nEmitters.Load() > 0 || len(n.sinks) > 0 {
 		n.lk.Unlock()
 		b.lk.Unlock()
 		return // still in use
@@ -178,7 +178,7 @@ func (s *sub) Close() error {
 			}
 		}
 
-		tryDrop := len(n.sinks) == 0 && atomic.LoadInt32(&n.nEmitters) == 0
+		tryDrop := len(n.sinks) == 0 && n.nEmitters.Load() == 0
 
 		n.lk.Unlock()
 
@@ -294,7 +294,7 @@ func (b *basicBus) Emitter(evtType interface{}, opts ...event.EmitterOpt) (e eve
 	typ = typ.Elem()
 
 	b.withNode(typ, func(n *node) {
-		atomic.AddInt32(&n.nEmitters, 1)
+		n.nEmitters.Add(1)
 		n.keepLast = n.keepLast || settings.makeStateful
 		e = &emitter{n: n, typ: typ, dropper: b.tryDropNode, w: b.wildcard, metricsTracer: b.metricsTracer}
 	}, nil)
@@ -319,13 +319,13 @@ func (b *basicBus) GetAllEventTypes() []reflect.Type {
 
 type wildcardNode struct {
 	sync.RWMutex
-	nSinks        int32
+	nSinks        atomic.Int32
 	sinks         []*namedSink
 	metricsTracer MetricsTracer
 }
 
 func (n *wildcardNode) addSink(sink *namedSink) {
-	atomic.AddInt32(&n.nSinks, 1) // ok to do outside the lock
+	n.nSinks.Add(1) // ok to do outside the lock
 	n.Lock()
 	n.sinks = append(n.sinks, sink)
 	n.Unlock()
@@ -336,7 +336,7 @@ func (n *wildcardNode) addSink(sink *namedSink) {
 }
 
 func (n *wildcardNode) removeSink(ch chan interface{}) {
-	atomic.AddInt32(&n.nSinks, -1) // ok to do outside the lock
+	n.nSinks.Add(-1) // ok to do outside the lock
 	n.Lock()
 	for i := 0; i < len(n.sinks); i++ {
 		if n.sinks[i].ch == ch {
@@ -349,7 +349,7 @@ func (n *wildcardNode) removeSink(ch chan interface{}) {
 }
 
 func (n *wildcardNode) emit(evt interface{}) {
-	if atomic.LoadInt32(&n.nSinks) == 0 {
+	if n.nSinks.Load() == 0 {
 		return
 	}
 
@@ -372,7 +372,7 @@ type node struct {
 	typ reflect.Type
 
 	// emitter ref count
-	nEmitters int32
+	nEmitters atomic.Int32
 
 	keepLast bool
 	last     interface{}
diff --git a/p2p/host/eventbus/opts.go b/p2p/host/eventbus/opts.go
index 3922be0a..837a0683 100644
--- a/p2p/host/eventbus/opts.go
+++ b/p2p/host/eventbus/opts.go
@@ -12,7 +12,7 @@ type subSettings struct {
 	name   string
 }
 
-var subCnt int64
+var subCnt atomic.Int64
 
 var subSettingsDefault = subSettings{
 	buffer: 16,
@@ -34,7 +34,7 @@ func newSubSettings() subSettings {
 		}
 		settings.name = fmt.Sprintf("%s-L%d", file, line)
 	} else {
-		settings.name = fmt.Sprintf("subscriber-%d", atomic.AddInt64(&subCnt, 1))
+		settings.name = fmt.Sprintf("subscriber-%d", subCnt.Add(1))
 	}
 	return settings
 }
diff --git a/p2p/host/relaysvc/relay.go b/p2p/host/relaysvc/relay.go
index bea8aa73..f9bbc758 100644
--- a/p2p/host/relaysvc/relay.go
+++ b/p2p/host/relaysvc/relay.go
@@ -65,14 +65,19 @@ func (m *RelayManager) background(ctx context.Context) {
 func (m *RelayManager) reachabilityChanged(r network.Reachability) error {
 	switch r {
 	case network.ReachabilityPublic:
+		m.mutex.Lock()
+		defer m.mutex.Unlock()
+		// This could happen if two consecutive EvtLocalReachabilityChanged report the same reachability.
+		// This shouldn't happen, but it's safer to double-check.
+		if m.relay != nil {
+			return nil
+		}
 		relay, err := relayv2.New(m.host, m.opts...)
 		if err != nil {
 			return err
 		}
-		m.mutex.Lock()
-		defer m.mutex.Unlock()
 		m.relay = relay
-	case network.ReachabilityPrivate:
+	default:
 		m.mutex.Lock()
 		defer m.mutex.Unlock()
 		if m.relay != nil {
diff --git a/p2p/net/connmgr/connmgr.go b/p2p/net/connmgr/connmgr.go
index c124c6bb..df3d96d5 100644
--- a/p2p/net/connmgr/connmgr.go
+++ b/p2p/net/connmgr/connmgr.go
@@ -38,7 +38,7 @@ type BasicConnMgr struct {
 
 	// channel-based semaphore that enforces only a single trim is in progress
 	trimMutex sync.Mutex
-	connCount int32
+	connCount atomic.Int32
 	// to be accessed atomically. This is mimicking the implementation of a sync.Once.
 	// Take care of correct alignment when modifying this struct.
 	trimCount uint64
@@ -158,7 +158,7 @@ func NewConnManager(low, hi int, opts ...Option) (*BasicConnMgr, error) {
 // We don't pay attention to the silence period or the grace period.
 // We try to not kill protected connections, but if that turns out to be necessary, not connection is safe!
 func (cm *BasicConnMgr) memoryEmergency() {
-	connCount := int(atomic.LoadInt32(&cm.connCount))
+	connCount := int(cm.connCount.Load())
 	target := connCount - cm.cfg.lowWater
 	if target < 0 {
 		log.Warnw("Low on memory, but we only have a few connections", "num", connCount, "low watermark", cm.cfg.lowWater)
@@ -346,7 +346,7 @@ func (cm *BasicConnMgr) background() {
 	for {
 		select {
 		case <-ticker.C:
-			if atomic.LoadInt32(&cm.connCount) < int32(cm.cfg.highWater) {
+			if cm.connCount.Load() < int32(cm.cfg.highWater) {
 				// Below high water, skip.
 				continue
 			}
@@ -456,7 +456,7 @@ func (cm *BasicConnMgr) getConnsToClose() []network.Conn {
 		return nil
 	}
 
-	if int(atomic.LoadInt32(&cm.connCount)) <= cm.cfg.lowWater {
+	if int(cm.connCount.Load()) <= cm.cfg.lowWater {
 		log.Info("open connection count below limit")
 		return nil
 	}
@@ -632,7 +632,7 @@ func (cm *BasicConnMgr) GetInfo() CMInfo {
 		LowWater:    cm.cfg.lowWater,
 		LastTrim:    lastTrim,
 		GracePeriod: cm.cfg.gracePeriod,
-		ConnCount:   int(atomic.LoadInt32(&cm.connCount)),
+		ConnCount:   int(cm.connCount.Load()),
 	}
 }
 
@@ -686,7 +686,7 @@ func (nn *cmNotifee) Connected(n network.Network, c network.Conn) {
 	}
 
 	pinfo.conns[c] = cm.clock.Now()
-	atomic.AddInt32(&cm.connCount, 1)
+	cm.connCount.Add(1)
 }
 
 // Disconnected is called by notifiers to inform that an existing connection has been closed or terminated.
@@ -715,7 +715,7 @@ func (nn *cmNotifee) Disconnected(n network.Network, c network.Conn) {
 	if len(cinf.conns) == 0 {
 		delete(s.peers, p)
 	}
-	atomic.AddInt32(&cm.connCount, -1)
+	cm.connCount.Add(-1)
 }
 
 // Listen is no-op in this implementation.
diff --git a/p2p/net/mock/mock_conn.go b/p2p/net/mock/mock_conn.go
index 487db8c7..b6fe8f2d 100644
--- a/p2p/net/mock/mock_conn.go
+++ b/p2p/net/mock/mock_conn.go
@@ -14,7 +14,7 @@ import (
 	manet "github.com/multiformats/go-multiaddr/net"
 )
 
-var connCounter int64
+var connCounter atomic.Int64
 
 // conn represents one side's perspective of a
 // live connection between two peers.
@@ -49,7 +49,7 @@ func newConn(ln, rn *peernet, l *link, dir network.Direction) *conn {
 	c.local = ln.peer
 	c.remote = rn.peer
 	c.stat.Direction = dir
-	c.id = atomic.AddInt64(&connCounter, 1)
+	c.id = connCounter.Add(1)
 
 	c.localAddr = ln.ps.Addrs(ln.peer)[0]
 	for _, a := range rn.ps.Addrs(rn.peer) {
diff --git a/p2p/net/mock/mock_stream.go b/p2p/net/mock/mock_stream.go
index f6cb842e..c85cca54 100644
--- a/p2p/net/mock/mock_stream.go
+++ b/p2p/net/mock/mock_stream.go
@@ -13,7 +13,7 @@ import (
 	"github.com/libp2p/go-libp2p/core/protocol"
 )
 
-var streamCounter int64
+var streamCounter atomic.Int64
 
 // stream implements network.Stream
 type stream struct {
@@ -57,7 +57,7 @@ func newStream(w *io.PipeWriter, r *io.PipeReader, dir network.Direction) *strea
 	s := &stream{
 		read:      r,
 		write:     w,
-		id:        atomic.AddInt64(&streamCounter, 1),
+		id:        streamCounter.Add(1),
 		reset:     make(chan struct{}, 1),
 		close:     make(chan struct{}, 1),
 		closed:    make(chan struct{}),
diff --git a/p2p/protocol/circuitv1/relay/relay.go b/p2p/protocol/circuitv1/relay/relay.go
index 296ef306..6e6e5b03 100644
--- a/p2p/protocol/circuitv1/relay/relay.go
+++ b/p2p/protocol/circuitv1/relay/relay.go
@@ -309,10 +309,10 @@ func (r *Relay) handleHopStream(s network.Stream, msg *pb.CircuitRelay) {
 	// reset deadline
 	bs.SetDeadline(time.Time{})
 
-	goroutines := new(int32)
-	*goroutines = 2
+	var goroutines atomic.Int32
+	goroutines.Store(2)
 	done := func() {
-		if atomic.AddInt32(goroutines, -1) == 0 {
+		if goroutines.Add(-1) == 0 {
 			s.Close()
 			bs.Close()
 			cleanup()
diff --git a/p2p/protocol/circuitv2/relay/relay.go b/p2p/protocol/circuitv2/relay/relay.go
index e24cd7f3..ca50d424 100644
--- a/p2p/protocol/circuitv2/relay/relay.go
+++ b/p2p/protocol/circuitv2/relay/relay.go
@@ -377,11 +377,11 @@ func (r *Relay) handleConnect(s network.Stream, msg *pbv2.HopMessage) {
 
 	log.Infof("relaying connection from %s to %s", src, dest.ID)
 
-	goroutines := new(int32)
-	*goroutines = 2
+	var goroutines atomic.Int32
+	goroutines.Store(2)
 
 	done := func() {
-		if atomic.AddInt32(goroutines, -1) == 0 {
+		if goroutines.Add(-1) == 0 {
 			s.Close()
 			bs.Close()
 			cleanup()
diff --git a/p2p/protocol/internal/circuitv1-deprecated/relay.go b/p2p/protocol/internal/circuitv1-deprecated/relay.go
index f3a491d2..3d67bc07 100644
--- a/p2p/protocol/internal/circuitv1-deprecated/relay.go
+++ b/p2p/protocol/internal/circuitv1-deprecated/relay.go
@@ -54,8 +54,8 @@ type Relay struct {
 	incoming chan *Conn
 
 	// atomic counters
-	streamCount  int32
-	liveHopCount int32
+	streamCount  atomic.Int32
+	liveHopCount atomic.Int32
 
 	// per peer hop counters
 	mx       sync.Mutex
@@ -130,7 +130,7 @@ func NewRelay(h host.Host, upgrader transport.Upgrader, opts ...RelayOpt) (*Rela
 // sides of the hop stream. This ensures that connections with many hop streams will be protected
 // from pruning, thus minimizing disruption from connection trimming in a relay node.
 func (r *Relay) addLiveHop(from, to peer.ID) {
-	atomic.AddInt32(&r.liveHopCount, 1)
+	r.liveHopCount.Add(1)
 	r.host.ConnManager().UpsertTag(from, "relay-hop-stream", incrementTag)
 	r.host.ConnManager().UpsertTag(to, "relay-hop-stream", incrementTag)
 }
@@ -138,14 +138,14 @@ func (r *Relay) addLiveHop(from, to peer.ID) {
 // Decrement the live hpo count and decrement the connection manager tags for the two sides
 // of the hop stream.
 func (r *Relay) rmLiveHop(from, to peer.ID) {
-	atomic.AddInt32(&r.liveHopCount, -1)
+	r.liveHopCount.Add(-1)
 	r.host.ConnManager().UpsertTag(from, "relay-hop-stream", decrementTag)
 	r.host.ConnManager().UpsertTag(to, "relay-hop-stream", decrementTag)
 
 }
 
 func (r *Relay) GetActiveHops() int32 {
-	return atomic.LoadInt32(&r.liveHopCount)
+	return r.liveHopCount.Load()
 }
 
 func (r *Relay) DialPeer(ctx context.Context, relay peer.AddrInfo, dest peer.AddrInfo) (*Conn, error) {
@@ -280,9 +280,9 @@ func (r *Relay) handleHopStream(s network.Stream, msg *pb.CircuitRelay) {
 		return
 	}
 
-	streamCount := atomic.AddInt32(&r.streamCount, 1)
-	liveHopCount := atomic.LoadInt32(&r.liveHopCount)
-	defer atomic.AddInt32(&r.streamCount, -1)
+	streamCount := r.streamCount.Add(1)
+	liveHopCount := r.liveHopCount.Load()
+	defer r.streamCount.Add(-1)
 
 	if (streamCount + liveHopCount) > int32(HopStreamLimit) {
 		log.Warn("hop stream limit exceeded; resetting stream")
@@ -391,10 +391,10 @@ func (r *Relay) handleHopStream(s network.Stream, msg *pb.CircuitRelay) {
 
 	r.addLiveHop(src.ID, dst.ID)
 
-	goroutines := new(int32)
-	*goroutines = 2
+	var goroutines atomic.Int32
+	goroutines.Store(2)
 	done := func() {
-		if atomic.AddInt32(goroutines, -1) == 0 {
+		if goroutines.Add(-1) == 0 {
 			s.Close()
 			bs.Close()
 			r.rmLiveHop(src.ID, dst.ID)


Test patch:
diff --git a/core/test/crypto.go b/core/test/crypto.go
index 5032485b..ff09cfac 100644
--- a/core/test/crypto.go
+++ b/core/test/crypto.go
@@ -7,11 +7,11 @@ import (
 	ci "github.com/libp2p/go-libp2p/core/crypto"
 )
 
-var globalSeed int64
+var globalSeed atomic.Int64
 
 func RandTestKeyPair(typ, bits int) (ci.PrivKey, ci.PubKey, error) {
 	// workaround for low time resolution
-	seed := atomic.AddInt64(&globalSeed, 1)
+	seed := globalSeed.Add(1)
 	return SeededTestKeyPair(typ, bits, seed)
 }
 
diff --git a/p2p/host/autonat/autonat_test.go b/p2p/host/autonat/autonat_test.go
index 6a08680b..49dec633 100644
--- a/p2p/host/autonat/autonat_test.go
+++ b/p2p/host/autonat/autonat_test.go
@@ -209,19 +209,6 @@ func TestAutoNATObservationRecording(t *testing.T) {
 		t.Fatalf("failed to subscribe to event EvtLocalRoutabilityPublic, err=%s", err)
 	}
 
-	// pubic observation without address should be ignored.
-	an.recordObservation(autoNATResult{network.ReachabilityPublic, nil})
-	if an.Status() != network.ReachabilityUnknown {
-		t.Fatalf("unexpected transition")
-	}
-
-	select {
-	case <-s.Out():
-		t.Fatal("not expecting a public reachability event")
-	default:
-		// expected
-	}
-
 	addr, _ := ma.NewMultiaddr("/ip4/127.0.0.1/udp/1234")
 	an.recordObservation(autoNATResult{network.ReachabilityPublic, addr})
 	if an.Status() != network.ReachabilityPublic {
@@ -252,6 +239,17 @@ func TestAutoNATObservationRecording(t *testing.T) {
 		t.Fatalf("too-extreme private transition.")
 	}
 
+	// don't emit events on observed address change
+	newAddr, _ := ma.NewMultiaddr("/ip4/127.0.0.1/udp/12345")
+	an.recordObservation(autoNATResult{network.ReachabilityPublic, newAddr})
+	if an.Status() != network.ReachabilityPublic {
+		t.Fatalf("reachability should stay public")
+	}
+	select {
+	case <-s.Out():
+		t.Fatal("received event without state transition")
+	case <-time.After(300 * time.Millisecond):
+	}
 }
 
 func TestStaticNat(t *testing.T) {
diff --git a/p2p/host/autorelay/autorelay_test.go b/p2p/host/autorelay/autorelay_test.go
index 768c8ec2..53d7d67e 100644
--- a/p2p/host/autorelay/autorelay_test.go
+++ b/p2p/host/autorelay/autorelay_test.go
@@ -251,17 +251,17 @@ func TestBackoff(t *testing.T) {
 	)
 	require.NoError(t, err)
 	defer r.Close()
-	var reservations int32
+	var reservations atomic.Int32
 	r.SetStreamHandler(protoIDv2, func(str network.Stream) {
-		atomic.AddInt32(&reservations, 1)
+		reservations.Add(1)
 		str.Reset()
 	})
 
-	var counter int32 // to be used atomically
+	var counter atomic.Int32
 	h := newPrivateNode(t,
 		func(context.Context, int) <-chan peer.AddrInfo {
 			// always return the same node, and make sure we don't try to connect to it too frequently
-			atomic.AddInt32(&counter, 1)
+			counter.Add(1)
 			peerChan := make(chan peer.AddrInfo, 1)
 			peerChan <- peer.AddrInfo{ID: r.ID(), Addrs: r.Addrs()}
 			close(peerChan)
@@ -275,16 +275,16 @@ func TestBackoff(t *testing.T) {
 	)
 	defer h.Close()
 
-	require.Eventually(t, func() bool { return atomic.LoadInt32(&reservations) == 1 }, 3*time.Second, 20*time.Millisecond)
+	require.Eventually(t, func() bool { return reservations.Load() == 1 }, 3*time.Second, 20*time.Millisecond)
 	// make sure we don't add any relays yet
 	for i := 0; i < 2; i++ {
 		cl.Add(backoff / 3)
-		require.Equal(t, 1, int(atomic.LoadInt32(&reservations)))
+		require.Equal(t, 1, int(reservations.Load()))
 	}
 	cl.Add(backoff / 2)
-	require.Eventually(t, func() bool { return atomic.LoadInt32(&reservations) == 2 }, 3*time.Second, 20*time.Millisecond)
-	require.Less(t, int(atomic.LoadInt32(&counter)), 100) // just make sure we're not busy-looping
-	require.Equal(t, 2, int(atomic.LoadInt32(&reservations)))
+	require.Eventually(t, func() bool { return reservations.Load() == 2 }, 3*time.Second, 20*time.Millisecond)
+	require.Less(t, int(counter.Load()), 100) // just make sure we're not busy-looping
+	require.Equal(t, 2, int(reservations.Load()))
 }
 
 func TestStaticRelays(t *testing.T) {
diff --git a/p2p/host/eventbus/basic_test.go b/p2p/host/eventbus/basic_test.go
index d2ede36a..57362ce9 100644
--- a/p2p/host/eventbus/basic_test.go
+++ b/p2p/host/eventbus/basic_test.go
@@ -198,7 +198,7 @@ func TestClosingRaces(t *testing.T) {
 func TestSubMany(t *testing.T) {
 	bus := NewBus()
 
-	var r int32
+	var r atomic.Int32
 
 	n := getN()
 	var wait sync.WaitGroup
@@ -215,7 +215,7 @@ func TestSubMany(t *testing.T) {
 			defer sub.Close()
 
 			ready.Done()
-			atomic.AddInt32(&r, int32((<-sub.Out()).(EventB)))
+			r.Add(int32((<-sub.Out()).(EventB)))
 			wait.Done()
 		}()
 	}
@@ -231,7 +231,7 @@ func TestSubMany(t *testing.T) {
 	em.Emit(EventB(7))
 	wait.Wait()
 
-	if int(r) != 7*n {
+	if int(r.Load()) != 7*n {
 		t.Error("got wrong result")
 	}
 }
@@ -488,7 +488,7 @@ func testMany(t testing.TB, subs, emits, msgs int, stateful bool) {
 
 	bus := NewBus()
 
-	var r int64
+	var r atomic.Int64
 
 	var wait sync.WaitGroup
 	var ready sync.WaitGroup
@@ -509,7 +509,7 @@ func testMany(t testing.TB, subs, emits, msgs int, stateful bool) {
 				if !ok {
 					panic("wat")
 				}
-				atomic.AddInt64(&r, int64(e.(EventB)))
+				r.Add(int64(e.(EventB)))
 			}
 			wait.Done()
 		}()
@@ -538,7 +538,7 @@ func testMany(t testing.TB, subs, emits, msgs int, stateful bool) {
 
 	wait.Wait()
 
-	if int(r) != 97*subs*emits*msgs {
+	if int(r.Load()) != 97*subs*emits*msgs {
 		t.Fatal("got wrong result")
 	}
 }
diff --git a/p2p/host/relaysvc/relay_test.go b/p2p/host/relaysvc/relay_test.go
new file mode 100644
index 00000000..d995cac2
--- /dev/null
+++ b/p2p/host/relaysvc/relay_test.go
@@ -0,0 +1,67 @@
+package relaysvc
+
+import (
+	"testing"
+	"time"
+
+	"github.com/libp2p/go-libp2p/core/event"
+	"github.com/libp2p/go-libp2p/core/network"
+	bhost "github.com/libp2p/go-libp2p/p2p/host/blank"
+	"github.com/libp2p/go-libp2p/p2p/host/eventbus"
+	swarmt "github.com/libp2p/go-libp2p/p2p/net/swarm/testing"
+	relayv2 "github.com/libp2p/go-libp2p/p2p/protocol/circuitv2/relay"
+	"github.com/stretchr/testify/require"
+)
+
+func TestReachabilityChangeEvent(t *testing.T) {
+	h := bhost.NewBlankHost(swarmt.GenSwarm(t))
+	rmgr := NewRelayManager(h)
+	emitter, err := rmgr.host.EventBus().Emitter(new(event.EvtLocalReachabilityChanged), eventbus.Stateful)
+	if err != nil {
+		t.Fatal(err)
+	}
+	evt := event.EvtLocalReachabilityChanged{Reachability: network.ReachabilityPublic}
+	emitter.Emit(evt)
+	require.Eventually(
+		t,
+		func() bool { rmgr.mutex.Lock(); defer rmgr.mutex.Unlock(); return rmgr.relay != nil },
+		1*time.Second,
+		100*time.Millisecond,
+		"relay should be set on public reachability")
+
+	evt = event.EvtLocalReachabilityChanged{Reachability: network.ReachabilityPrivate}
+	emitter.Emit(evt)
+	require.Eventually(
+		t,
+		func() bool { rmgr.mutex.Lock(); defer rmgr.mutex.Unlock(); return rmgr.relay == nil },
+		1*time.Second,
+		100*time.Millisecond,
+		"relay should be nil on private reachability")
+
+	evt = event.EvtLocalReachabilityChanged{Reachability: network.ReachabilityPublic}
+	emitter.Emit(evt)
+	evt = event.EvtLocalReachabilityChanged{Reachability: network.ReachabilityUnknown}
+	emitter.Emit(evt)
+	require.Eventually(
+		t,
+		func() bool { rmgr.mutex.Lock(); defer rmgr.mutex.Unlock(); return rmgr.relay == nil },
+		1*time.Second,
+		100*time.Millisecond,
+		"relay should be nil on unknown reachability")
+
+	evt = event.EvtLocalReachabilityChanged{Reachability: network.ReachabilityPublic}
+	emitter.Emit(evt)
+	var relay *relayv2.Relay
+	require.Eventually(
+		t,
+		func() bool { rmgr.mutex.Lock(); defer rmgr.mutex.Unlock(); relay = rmgr.relay; return relay != nil },
+		1*time.Second,
+		100*time.Millisecond,
+		"relay should be set on public event")
+	emitter.Emit(evt)
+	require.Never(t,
+		func() bool { rmgr.mutex.Lock(); defer rmgr.mutex.Unlock(); return relay != rmgr.relay },
+		1*time.Second,
+		100*time.Millisecond,
+		"relay should not be updated on receiving the same event")
+}
diff --git a/p2p/net/connmgr/connmgr_test.go b/p2p/net/connmgr/connmgr_test.go
index 39db7b54..7468f671 100644
--- a/p2p/net/connmgr/connmgr_test.go
+++ b/p2p/net/connmgr/connmgr_test.go
@@ -367,7 +367,7 @@ func TestDoubleConnection(t *testing.T) {
 	not.Connected(nil, conn)
 	cm.TagPeer(conn.RemotePeer(), "foo", 10)
 	not.Connected(nil, conn)
-	if cm.connCount != 1 {
+	if cm.connCount.Load() != 1 {
 		t.Fatal("unexpected number of connections")
 	}
 	if cm.segments.get(conn.RemotePeer()).peers[conn.RemotePeer()].value != 10 {
@@ -386,7 +386,7 @@ func TestDisconnected(t *testing.T) {
 	cm.TagPeer(conn.RemotePeer(), "foo", 10)
 
 	not.Disconnected(nil, randConn(t, nil))
-	if cm.connCount != 1 {
+	if cm.connCount.Load() != 1 {
 		t.Fatal("unexpected number of connections")
 	}
 	if cm.segments.get(conn.RemotePeer()).peers[conn.RemotePeer()].value != 10 {
@@ -394,7 +394,7 @@ func TestDisconnected(t *testing.T) {
 	}
 
 	not.Disconnected(nil, &tconn{peer: conn.RemotePeer()})
-	if cm.connCount != 1 {
+	if cm.connCount.Load() != 1 {
 		t.Fatal("unexpected number of connections")
 	}
 	if cm.segments.get(conn.RemotePeer()).peers[conn.RemotePeer()].value != 10 {
@@ -402,7 +402,7 @@ func TestDisconnected(t *testing.T) {
 	}
 
 	not.Disconnected(nil, conn)
-	if cm.connCount != 0 {
+	if cm.connCount.Load() != 0 {
 		t.Fatal("unexpected number of connections")
 	}
 	if cm.segments.countPeers() != 0 {
@@ -495,7 +495,7 @@ func TestQuickBurstRespectsSilencePeriod(t *testing.T) {
 	if closed > 20 {
 		t.Fatalf("should have closed at most 20 connections, closed: %d", closed)
 	}
-	if total := closed + int(cm.connCount); total != 30 {
+	if total := closed + int(cm.connCount.Load()); total != 30 {
 		t.Fatalf("expected closed connections + open conn count to equal 30, value: %d", total)
 	}
 }
diff --git a/p2p/net/upgrader/listener_test.go b/p2p/net/upgrader/listener_test.go
index c6b1b885..be4a1f46 100644
--- a/p2p/net/upgrader/listener_test.go
+++ b/p2p/net/upgrader/listener_test.go
@@ -261,11 +261,11 @@ func TestAcceptQueueBacklogged(t *testing.T) {
 	defer ln.Close()
 
 	// setup AcceptQueueLength connections, but don't accept any of them
-	var counter int32 // to be used atomically
+	var counter atomic.Int32
 	doDial := func() {
 		conn, err := dial(t, u, ln.Multiaddr(), id, &network.NullScope{})
 		require.NoError(err)
-		atomic.AddInt32(&counter, 1)
+		counter.Add(1)
 		t.Cleanup(func() { conn.Close() })
 	}
 
@@ -273,20 +273,20 @@ func TestAcceptQueueBacklogged(t *testing.T) {
 		go doDial()
 	}
 
-	require.Eventually(func() bool { return int(atomic.LoadInt32(&counter)) == upgrader.AcceptQueueLength }, 2*time.Second, 50*time.Millisecond)
+	require.Eventually(func() bool { return int(counter.Load()) == upgrader.AcceptQueueLength }, 2*time.Second, 50*time.Millisecond)
 
 	// dial a new connection. This connection should not complete setup, since the queue is full
 	go doDial()
 
 	time.Sleep(100 * time.Millisecond)
-	require.Equal(int(atomic.LoadInt32(&counter)), upgrader.AcceptQueueLength)
+	require.Equal(int(counter.Load()), upgrader.AcceptQueueLength)
 
 	// accept a single connection. Now the new connection should be set up, and fill the queue again
 	conn, err := ln.Accept()
 	require.NoError(err)
 	require.NoError(conn.Close())
 
-	require.Eventually(func() bool { return int(atomic.LoadInt32(&counter)) == upgrader.AcceptQueueLength+1 }, 2*time.Second, 50*time.Millisecond)
+	require.Eventually(func() bool { return int(counter.Load()) == upgrader.AcceptQueueLength+1 }, 2*time.Second, 50*time.Millisecond)
 }
 
 func TestListenerConnectionGater(t *testing.T) {
diff --git a/p2p/test/resource-manager/rcmgr_test.go b/p2p/test/resource-manager/rcmgr_test.go
index 017b4929..c953c5c7 100644
--- a/p2p/test/resource-manager/rcmgr_test.go
+++ b/p2p/test/resource-manager/rcmgr_test.go
@@ -265,10 +265,10 @@ func TestResourceManagerServicePeerInbound(t *testing.T) {
 
 func waitForBarrier(count int32, timeout time.Duration) func() error {
 	ready := make(chan struct{})
-	wait := new(int32)
-	*wait = count
+	var wait atomic.Int32
+	wait.Store(count)
 	return func() error {
-		if atomic.AddInt32(wait, -1) == 0 {
+		if wait.Add(-1) == 0 {
 			close(ready)
 		}