diff --git a/p2p/host/basic/addrs_manager_test.go b/p2p/host/basic/addrs_manager_test.go index ee74a8bbbe..583485b1da 100644 --- a/p2p/host/basic/addrs_manager_test.go +++ b/p2p/host/basic/addrs_manager_test.go @@ -449,6 +449,40 @@ func TestAddrsManagerReachabilityEvent(t *testing.T) { } } +func TestAddrsManagerConfirmedAddrsIncludesSecondaryTransports(t *testing.T) { + // A node listening on every transport kubo enables by default. The + // secondary transports (ws, webrtc-direct, webtransport) inherit Public + // from their thin-waist primary, and all of them must survive into + // ConfirmedAddrs: getConfirmedAddrs feeds these slices to + // removeNotInSource, which drops entries when its input is not sorted. + tcp := ma.StringCast("/ip4/1.2.3.4/tcp/4001") + wsSNI := ma.StringCast("/ip4/1.2.3.4/tcp/4001/tls/sni/*.example.net/ws") + quic := ma.StringCast("/ip4/1.2.3.4/udp/4001/quic-v1") + webrtc := ma.StringCast("/ip4/1.2.3.4/udp/4001/webrtc-direct") + wt := ma.StringCast("/ip4/1.2.3.4/udp/4001/quic-v1/webtransport") + listenAddrs := []ma.Multiaddr{tcp, wsSNI, quic, webrtc, wt} + + am := newAddrsManagerTestCase(t, addrsManagerArgs{ + ListenAddrs: func() []ma.Multiaddr { return listenAddrs }, + AutoNATClient: mockAutoNATClient{ + F: func(_ context.Context, reqs []autonatv2.Request) (autonatv2.Result, error) { + return autonatv2.Result{Addr: reqs[0].Addr, Idx: 0, Reachability: network.ReachabilityPublic}, nil + }, + }, + }) + defer am.Close() + + require.Eventually(t, func() bool { + reachable, _, _ := am.ConfirmedAddrs() + return len(reachable) == len(listenAddrs) + }, 5*time.Second, 50*time.Millisecond, "expected all listen addrs to become confirmed reachable") + + reachable, unreachable, unknown := am.ConfirmedAddrs() + matest.AssertMultiaddrsMatch(t, listenAddrs, reachable) + require.Empty(t, unreachable) + require.Empty(t, unknown) +} + func TestAddrsManagerPeerstoreUpdated(t *testing.T) { quic1 := ma.StringCast("/ip4/1.2.3.4/udp/1234/quic-v1") quic2 := ma.StringCast("/ip4/1.2.3.5/udp/1/quic-v1") diff --git a/p2p/host/basic/addrs_reachability_tracker.go b/p2p/host/basic/addrs_reachability_tracker.go index 0f802977df..993f7c0ade 100644 --- a/p2p/host/basic/addrs_reachability_tracker.go +++ b/p2p/host/basic/addrs_reachability_tracker.go @@ -396,6 +396,9 @@ func newProbeManager(now func() time.Time) *probeManager { } // AppendConfirmedAddrs appends the current confirmed reachable and unreachable addresses. +// The returned slices are sorted by Multiaddr.Compare: addrsManager.getConfirmedAddrs +// passes them to removeNotInSource, and Addrs passes the unreachable set to +// removeInSource, both of which require sorted input. func (m *probeManager) AppendConfirmedAddrs(reachable, unreachable, unknown []ma.Multiaddr) (reachableAddrs, unreachableAddrs, unknownAddrs []ma.Multiaddr) { m.mx.Lock() defer m.mx.Unlock() @@ -425,6 +428,16 @@ func (m *probeManager) AppendConfirmedAddrs(reachable, unreachable, unknown []ma unknown = append(unknown, a) } } + + // primaryAddrs and secondaryAddrs are each sorted, but interleave in the + // buckets above (a secondary like webrtc-direct sorts before its quic-v1 + // primary). Unsorted output makes removeNotInSource silently drop + // confirmed addrs, and makes removeInSource fail to filter unreachable + // addrs out of Addrs(). + cmp := func(a, b ma.Multiaddr) int { return a.Compare(b) } + slices.SortFunc(reachable, cmp) + slices.SortFunc(unreachable, cmp) + slices.SortFunc(unknown, cmp) return reachable, unreachable, unknown } diff --git a/p2p/host/basic/addrs_reachability_tracker_test.go b/p2p/host/basic/addrs_reachability_tracker_test.go index 5e88e6c489..4558a758f9 100644 --- a/p2p/host/basic/addrs_reachability_tracker_test.go +++ b/p2p/host/basic/addrs_reachability_tracker_test.go @@ -294,6 +294,41 @@ func TestProbeManager(t *testing.T) { }) } +func TestProbeManagerConfirmedAddrsSorted(t *testing.T) { + // Secondary transports can sort before their primary: webrtc-direct + // (protocol code 280) sorts before quic-v1 (461) on the same UDP socket. + // AppendConfirmedAddrs iterates primaries then secondaries, so without a + // final sort the buckets interleave; removeNotInSource in addrsManager + // then drops confirmed addrs, and removeInSource fails to filter + // unreachable addrs out of Addrs(). + tcp := ma.StringCast("/ip4/1.2.3.4/tcp/4001") + wsSNI := ma.StringCast("/ip4/1.2.3.4/tcp/4001/tls/sni/*.example.net/ws") + quic := ma.StringCast("/ip4/1.2.3.4/udp/4001/quic-v1") + webrtc := ma.StringCast("/ip4/1.2.3.4/udp/4001/webrtc-direct") + wt := ma.StringCast("/ip4/1.2.3.4/udp/4001/quic-v1/webtransport") + addrs := []ma.Multiaddr{tcp, wsSNI, quic, webrtc, wt} + + cl := clock.NewMock() + pm := newProbeManager(cl.Now) + pm.UpdateAddrs(slices.Clone(addrs)) + + for { + reqs := pm.GetProbe() + if len(reqs) == 0 { + break + } + pm.MarkProbeInProgress(reqs) + pm.CompleteProbe(reqs, autonatv2.Result{Addr: reqs[0].Addr, Idx: 0, Reachability: network.ReachabilityPublic}, nil) + } + + reachable, unreachable, unknown := pm.AppendConfirmedAddrs(nil, nil, nil) + require.Empty(t, unreachable) + require.Empty(t, unknown) + matest.AssertMultiaddrsMatch(t, addrs, reachable) + require.True(t, slices.IsSortedFunc(reachable, func(a, b ma.Multiaddr) int { return a.Compare(b) }), + "reachable addrs not sorted: %v", reachable) +} + type mockAutoNATClient struct { F func(context.Context, []autonatv2.Request) (autonatv2.Result, error) }