Skip to content

Commit b0b2a18

Browse files
authored
fix(autonatv2): secondary addrs inherit reachability from primary (#3435)
1 parent 20ba3c9 commit b0b2a18

File tree

2 files changed

+105
-42
lines changed

2 files changed

+105
-42
lines changed

p2p/host/basic/addrs_reachability_tracker.go

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -361,10 +361,6 @@ const (
361361
// and then a success(...S S S S F S). The confidence in the targetConfidence window will be equal to
362362
// targetConfidence, the last F and S cancel each other, and we won't probe again for maxProbeInterval.
363363
maxRecentDialsWindow = targetConfidence + 2
364-
// secondaryAddrsScalingFactor is the multiplier applied to secondary address dial outcomes. For secondary
365-
// addr, if the primary addr is reachable, a single successful dial is enough to consider the secondary addr
366-
// reachable.
367-
secondaryAddrsScalingFactor = targetConfidence
368364
// highConfidenceAddrProbeInterval is the maximum interval between probes for an address
369365
highConfidenceAddrProbeInterval = 1 * time.Hour
370366
// highConfidenceSecondaryAddrProbeInterval is the maximum interval between probes for an address
@@ -620,6 +616,19 @@ func (s *addrStatus) Reachability() network.Reachability {
620616
}
621617

622618
func (s *addrStatus) RequiredProbeCount(now time.Time) int {
619+
// Secondary addresses inherit reachability from their confirmed-public primary.
620+
// If the primary is ReachabilityPublic, the port is confirmed open at the
621+
// network level, so the secondary is also reachable (they share the socket).
622+
//
623+
// If the primary is ReachabilityPrivate, we still probe the secondary because
624+
// Private is a weaker signal - it could indicate:
625+
// - Port genuinely blocked (secondary will also fail)
626+
// - Protocol-specific issues with the primary (secondary might work)
627+
// The cost of extra probes when truly firewalled is low (quick failures).
628+
if s.primary != nil && s.primary.Reachability() == network.ReachabilityPublic {
629+
return 0
630+
}
631+
623632
if s.consecutiveRefusals >= maxConsecutiveRefusals {
624633
if now.Sub(s.lastRefusalTime) < recentProbeInterval {
625634
return 0
@@ -742,12 +751,49 @@ func (s *addrStatus) reachabilityAndCounts() (rch network.Reachability, successe
742751
}
743752
if s.primary != nil {
744753
prch, _, _ := s.primary.reachabilityAndCounts()
745-
switch prch {
746-
case network.ReachabilityPublic:
747-
successes *= secondaryAddrsScalingFactor
748-
case network.ReachabilityPrivate:
749-
failures *= secondaryAddrsScalingFactor
754+
if prch == network.ReachabilityPublic {
755+
// Secondary transports inherit Public reachability from their primary.
756+
//
757+
// This is important because not all AutoNAT v2 server implementations
758+
// support all secondary transports. As the Amino DHT gained a more
759+
// diverse set of node implementations (2025 Q4), we observed false
760+
// negatives: secondary addresses being marked unreachable when probing
761+
// peers simply didn't support the protocol, not because the port was
762+
// actually blocked.
763+
//
764+
// This handles shared-listener configurations where multiple
765+
// protocols share the same network socket:
766+
//
767+
// TCP-based (libp2p.ShareTCPListener):
768+
// Primary: /ip4/.../tcp/port
769+
// Secondary: /ip4/.../tcp/port/tls/sni/*.libp2p.direct/ws
770+
// TCP and Secure WebSocket share the same TCP listener.
771+
//
772+
// UDP/QUIC-based (quicreuse.ConnManager):
773+
// Primary: /ip4/.../udp/port/quic-v1
774+
// Secondary: /ip4/.../udp/port/quic-v1/webtransport
775+
// Secondary: /ip4/.../udp/port/webrtc-direct
776+
// QUIC, WebTransport, and WebRTC share the same UDP socket.
777+
//
778+
// AutoNAT v2 probe failures for secondary protocols typically
779+
// indicate protocol incompatibility at the probing peer, not
780+
// port unreachability:
781+
//
782+
// - Secure WebSocket: Probing peer may not support WebSockets,
783+
// or TLS handshake fails because the certificate isn't
784+
// provisioned yet (AutoTLS still obtaining cert).
785+
// - WebTransport: Probing peer supports QUIC but not HTTP/3.
786+
// - WebRTC: Probing peer supports QUIC but not DTLS-SRTP.
787+
//
788+
// Since the primary confirms the port is network-reachable, we
789+
// inherit that status. Protocol-level failures don't indicate
790+
// the address is unreachable to peers that DO support the protocol.
791+
return network.ReachabilityPublic, successes, failures
750792
}
793+
// If primary is Private or Unknown, we don't inherit - the secondary
794+
// builds its own status through probing. This is more conservative:
795+
// Private could indicate protocol-specific issues rather than port
796+
// unreachability, so we give the secondary a chance to prove itself.
751797
}
752798
if successes-failures >= minConfidence {
753799
return network.ReachabilityPublic, successes, failures

p2p/host/basic/addrs_reachability_tracker_test.go

Lines changed: 50 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -209,71 +209,88 @@ func TestProbeManager(t *testing.T) {
209209
}
210210
return res
211211
}
212-
// tcp private
213-
for range targetConfidence {
212+
213+
// Conservative inheritance: secondaries only inherit Public from primary.
214+
// If primary is Private, secondaries still get probed (Private could be
215+
// protocol-specific, not port-level).
216+
//
217+
// TCP gets Public results - websocket inherits Public (skips probing)
218+
// This is the AutoTLS use case: TCP works, WSS inherits reachability.
219+
for i := range targetConfidence {
214220
reqs := nextProbe(pm)
215-
matest.AssertEqualMultiaddrs(t, []ma.Multiaddr{tcp, quic, websocket, webrtc}, extractAddrs(reqs))
216-
pm.CompleteProbe(reqs, autonatv2.Result{Addr: tcp, Idx: 0, Reachability: network.ReachabilityPrivate}, nil)
221+
if i < minConfidence {
222+
// TCP not yet confirmed Public, all 4 addresses need probing
223+
matest.AssertEqualMultiaddrs(t, []ma.Multiaddr{tcp, quic, websocket, webrtc}, extractAddrs(reqs))
224+
} else {
225+
// TCP confirmed Public, websocket inherits - only 3 addresses
226+
matest.AssertEqualMultiaddrs(t, []ma.Multiaddr{tcp, quic, webrtc}, extractAddrs(reqs))
227+
}
228+
pm.CompleteProbe(reqs, autonatv2.Result{Addr: tcp, Idx: 0, Reachability: network.ReachabilityPublic}, nil)
217229
}
218-
// quic public
230+
231+
// QUIC gets Private results - webrtc still needs probing (no inheritance)
232+
// This tests the conservative behavior: Private doesn't propagate.
219233
for range targetConfidence {
220234
reqs := nextProbe(pm)
221-
matest.AssertEqualMultiaddrs(t, []ma.Multiaddr{quic, websocket, webrtc}, extractAddrs(reqs))
222-
pm.CompleteProbe(reqs, autonatv2.Result{Addr: quic, Idx: 0, Reachability: network.ReachabilityPublic}, nil)
235+
// websocket already inherited from tcp, but webrtc doesn't inherit Private
236+
matest.AssertEqualMultiaddrs(t, []ma.Multiaddr{quic, webrtc}, extractAddrs(reqs))
237+
pm.CompleteProbe(reqs, autonatv2.Result{Addr: quic, Idx: 0, Reachability: network.ReachabilityPrivate}, nil)
223238
}
224-
// only 1 check now required for websocket
225-
for range 1 {
226-
reqs := nextProbe(pm)
227-
matest.AssertEqualMultiaddrs(t, []ma.Multiaddr{websocket, webrtc}, extractAddrs(reqs))
228-
pm.CompleteProbe(reqs, autonatv2.Result{Addr: websocket, Idx: 0, Reachability: network.ReachabilityPrivate}, nil)
229-
}
230-
// 3 checks required for webrtc to make its reachability different from quic.
239+
240+
// webrtc still needs probing (doesn't inherit Private from quic)
241+
// Give webrtc its own Private status through probing
231242
for range targetConfidence {
232243
reqs := nextProbe(pm)
233244
matest.AssertEqualMultiaddrs(t, []ma.Multiaddr{webrtc}, extractAddrs(reqs))
234245
pm.CompleteProbe(reqs, autonatv2.Result{Addr: webrtc, Idx: 0, Reachability: network.ReachabilityPrivate}, nil)
235246
}
236247

248+
reqs := nextProbe(pm)
249+
require.Empty(t, reqs)
250+
237251
reachable, unreachable, _ := pm.AppendConfirmedAddrs(nil, nil, nil)
238-
matest.AssertMultiaddrsMatch(t, []ma.Multiaddr{quic}, reachable)
239-
matest.AssertMultiaddrsMatch(t, []ma.Multiaddr{tcp, websocket, webrtc}, unreachable)
252+
// websocket inherits Public from tcp (confirmed reachable port) - AutoTLS case
253+
// webrtc has its own Private status (probed independently)
254+
matest.AssertMultiaddrsMatch(t, []ma.Multiaddr{tcp, websocket}, reachable)
255+
matest.AssertMultiaddrsMatch(t, []ma.Multiaddr{quic, webrtc}, unreachable)
240256

241-
// Every `highConfidenceAddrsProbeInterval` we refresh the primary addr binding
257+
// After highConfidenceAddrProbeInterval (1h), only primaries need refresh.
258+
// websocket inherits from tcp (Public), webrtc has longer refresh interval (3h).
242259
for range 2 {
243260
cl.Add(highConfidenceAddrProbeInterval + 1*time.Millisecond)
244261
reqs := nextProbe(pm)
262+
// Only tcp and quic need refresh; websocket inherits, webrtc has 3h interval
245263
matest.AssertEqualMultiaddrs(t, []ma.Multiaddr{tcp, quic}, extractAddrs(reqs))
246-
pm.CompleteProbe(reqs, autonatv2.Result{Addr: tcp, Idx: 0, Reachability: network.ReachabilityPrivate}, nil)
264+
pm.CompleteProbe(reqs, autonatv2.Result{Addr: tcp, Idx: 0, Reachability: network.ReachabilityPublic}, nil)
247265
reqs = nextProbe(pm)
248266
matest.AssertEqualMultiaddrs(t, []ma.Multiaddr{quic}, extractAddrs(reqs))
249-
pm.CompleteProbe(reqs, autonatv2.Result{Addr: quic, Idx: 0, Reachability: network.ReachabilityPublic}, nil)
267+
pm.CompleteProbe(reqs, autonatv2.Result{Addr: quic, Idx: 0, Reachability: network.ReachabilityPrivate}, nil)
250268

251269
reqs = nextProbe(pm)
252270
require.Empty(t, reqs)
253271
}
254272

255-
cl.Add(highConfidenceAddrProbeInterval + 1*time.Millisecond)
256-
reqs := nextProbe(pm)
257-
matest.AssertEqualMultiaddrs(t, []ma.Multiaddr{tcp, quic, websocket, webrtc}, extractAddrs(reqs))
258-
pm.CompleteProbe(reqs, autonatv2.Result{Addr: tcp, Idx: 0, Reachability: network.ReachabilityPrivate}, nil)
259-
reqs = nextProbe(pm)
260-
matest.AssertEqualMultiaddrs(t, []ma.Multiaddr{quic, websocket, webrtc}, extractAddrs(reqs))
261-
pm.CompleteProbe(reqs, autonatv2.Result{Addr: quic, Idx: 0, Reachability: network.ReachabilityPublic}, nil)
273+
reachable, unreachable, _ = pm.AppendConfirmedAddrs(nil, nil, nil)
274+
matest.AssertMultiaddrsMatch(t, reachable, []ma.Multiaddr{tcp, websocket})
275+
matest.AssertMultiaddrsMatch(t, unreachable, []ma.Multiaddr{quic, webrtc})
262276

263-
// secondary addrs refreshed at 3*highConfidenceProbeInterval
277+
// After highConfidenceSecondaryAddrProbeInterval (3h), webrtc needs refresh too.
278+
// We've advanced 2h+2ms, need to reach 3h+ for webrtc's refresh.
279+
// Also need to exceed 1h since last tcp/quic refresh for them to need refresh.
280+
cl.Add(highConfidenceSecondaryAddrProbeInterval - 2*highConfidenceAddrProbeInterval + 1*time.Millisecond)
281+
reqs = nextProbe(pm)
282+
// tcp, quic, and webrtc need refresh; websocket still inherits from tcp
283+
matest.AssertEqualMultiaddrs(t, []ma.Multiaddr{tcp, quic, webrtc}, extractAddrs(reqs))
284+
pm.CompleteProbe(reqs, autonatv2.Result{Addr: tcp, Idx: 0, Reachability: network.ReachabilityPublic}, nil)
264285
reqs = nextProbe(pm)
265-
matest.AssertEqualMultiaddrs(t, []ma.Multiaddr{websocket, webrtc}, extractAddrs(reqs))
266-
pm.CompleteProbe(reqs, autonatv2.Result{Addr: websocket, Idx: 0, Reachability: network.ReachabilityPrivate}, nil)
286+
matest.AssertEqualMultiaddrs(t, []ma.Multiaddr{quic, webrtc}, extractAddrs(reqs))
287+
pm.CompleteProbe(reqs, autonatv2.Result{Addr: quic, Idx: 0, Reachability: network.ReachabilityPrivate}, nil)
267288
reqs = nextProbe(pm)
268289
matest.AssertEqualMultiaddrs(t, []ma.Multiaddr{webrtc}, extractAddrs(reqs))
269290
pm.CompleteProbe(reqs, autonatv2.Result{Addr: webrtc, Idx: 0, Reachability: network.ReachabilityPrivate}, nil)
270291

271292
reqs = nextProbe(pm)
272293
require.Empty(t, reqs)
273-
274-
reachable, unreachable, _ = pm.AppendConfirmedAddrs(nil, nil, nil)
275-
matest.AssertMultiaddrsMatch(t, reachable, []ma.Multiaddr{quic})
276-
matest.AssertMultiaddrsMatch(t, unreachable, []ma.Multiaddr{tcp, websocket, webrtc})
277294
})
278295
}
279296

0 commit comments

Comments
 (0)