ceremonyclient/go-libp2p/p2p/host/basic/addrs_reachability_tracker_test.go
Cassandra Heart dbd95bd9e9
v2.1.0 (#439)
* v2.1.0 [omit consensus and adjacent] - this commit will be amended with the full release after the file copy is complete

* 2.1.0 main node rollup
2025-09-30 02:48:15 -05:00

943 lines
28 KiB
Go

package basichost
import (
"context"
"encoding/binary"
"errors"
"fmt"
"math/rand"
"net"
"net/netip"
"slices"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/benbjohnson/clock"
"github.com/libp2p/go-libp2p/core/network"
"github.com/libp2p/go-libp2p/p2p/protocol/autonatv2"
ma "github.com/multiformats/go-multiaddr"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestProbeManager(t *testing.T) {
pub1 := tStringCast("/ip4/1.1.1.1/tcp/1")
pub2 := tStringCast("/ip4/1.1.1.2/tcp/1")
pub3 := tStringCast("/ip4/1.1.1.3/tcp/1")
cl := clock.NewMock()
nextProbe := func(pm *probeManager) []autonatv2.Request {
reqs := pm.GetProbe()
if len(reqs) != 0 {
pm.MarkProbeInProgress(reqs)
}
return reqs
}
makeNewProbeManager := func(addrs []ma.Multiaddr) *probeManager {
pm := newProbeManager(cl.Now)
pm.UpdateAddrs(addrs)
return pm
}
t.Run("addrs updates", func(t *testing.T) {
pm := newProbeManager(cl.Now)
pm.UpdateAddrs([]ma.Multiaddr{pub1, pub2})
for {
reqs := nextProbe(pm)
if len(reqs) == 0 {
break
}
pm.CompleteProbe(reqs, autonatv2.Result{Addr: reqs[0].Addr, Idx: 0, Reachability: network.ReachabilityPublic}, nil)
}
reachable, _, _ := pm.AppendConfirmedAddrs(nil, nil, nil)
require.Equal(t, reachable, []ma.Multiaddr{pub1, pub2})
pm.UpdateAddrs([]ma.Multiaddr{pub3})
reachable, _, _ = pm.AppendConfirmedAddrs(nil, nil, nil)
require.Empty(t, reachable)
require.Len(t, pm.statuses, 1)
})
t.Run("inprogress", func(t *testing.T) {
pm := makeNewProbeManager([]ma.Multiaddr{pub1, pub2})
reqs1 := pm.GetProbe()
reqs2 := pm.GetProbe()
require.Equal(t, reqs1, reqs2)
for range targetConfidence {
reqs := nextProbe(pm)
require.Equal(t, reqs, []autonatv2.Request{{Addr: pub1, SendDialData: true}, {Addr: pub2, SendDialData: true}})
}
for range targetConfidence {
reqs := nextProbe(pm)
require.Equal(t, reqs, []autonatv2.Request{{Addr: pub2, SendDialData: true}, {Addr: pub1, SendDialData: true}})
}
reqs := pm.GetProbe()
require.Empty(t, reqs)
})
t.Run("refusals", func(t *testing.T) {
pm := makeNewProbeManager([]ma.Multiaddr{pub1, pub2})
var probes [][]autonatv2.Request
for range targetConfidence {
reqs := nextProbe(pm)
require.Equal(t, reqs, []autonatv2.Request{{Addr: pub1, SendDialData: true}, {Addr: pub2, SendDialData: true}})
probes = append(probes, reqs)
}
// first one refused second one successful
for _, p := range probes {
pm.CompleteProbe(p, autonatv2.Result{Addr: pub2, Idx: 1, Reachability: network.ReachabilityPublic}, nil)
}
// the second address is validated!
probes = nil
for range targetConfidence {
reqs := nextProbe(pm)
require.Equal(t, reqs, []autonatv2.Request{{Addr: pub1, SendDialData: true}})
probes = append(probes, reqs)
}
reqs := pm.GetProbe()
require.Empty(t, reqs)
for _, p := range probes {
pm.CompleteProbe(p, autonatv2.Result{AllAddrsRefused: true}, nil)
}
// all requests refused; no more probes for too many refusals
reqs = pm.GetProbe()
require.Empty(t, reqs)
cl.Add(recentProbeInterval)
reqs = pm.GetProbe()
require.Equal(t, reqs, []autonatv2.Request{{Addr: pub1, SendDialData: true}})
})
t.Run("successes", func(t *testing.T) {
pm := makeNewProbeManager([]ma.Multiaddr{pub1, pub2})
for j := 0; j < 2; j++ {
for i := 0; i < targetConfidence; i++ {
reqs := nextProbe(pm)
pm.CompleteProbe(reqs, autonatv2.Result{Addr: reqs[0].Addr, Idx: 0, Reachability: network.ReachabilityPublic}, nil)
}
}
// all addrs confirmed
reqs := pm.GetProbe()
require.Empty(t, reqs)
cl.Add(highConfidenceAddrProbeInterval + time.Millisecond)
reqs = nextProbe(pm)
require.Equal(t, reqs, []autonatv2.Request{{Addr: pub1, SendDialData: true}, {Addr: pub2, SendDialData: true}})
reqs = nextProbe(pm)
require.Equal(t, reqs, []autonatv2.Request{{Addr: pub2, SendDialData: true}, {Addr: pub1, SendDialData: true}})
})
t.Run("throttling on indeterminate reachability", func(t *testing.T) {
pm := makeNewProbeManager([]ma.Multiaddr{pub1, pub2})
reachability := network.ReachabilityPublic
nextReachability := func() network.Reachability {
if reachability == network.ReachabilityPublic {
reachability = network.ReachabilityPrivate
} else {
reachability = network.ReachabilityPublic
}
return reachability
}
// both addresses are indeterminate
for range 2 * maxRecentDialsPerAddr {
reqs := nextProbe(pm)
pm.CompleteProbe(reqs, autonatv2.Result{Addr: reqs[0].Addr, Idx: 0, Reachability: nextReachability()}, nil)
}
reqs := pm.GetProbe()
require.Empty(t, reqs)
cl.Add(recentProbeInterval + time.Millisecond)
reqs = pm.GetProbe()
require.Equal(t, reqs, []autonatv2.Request{{Addr: pub1, SendDialData: true}, {Addr: pub2, SendDialData: true}})
for range 2 * maxRecentDialsPerAddr {
reqs := nextProbe(pm)
pm.CompleteProbe(reqs, autonatv2.Result{Addr: reqs[0].Addr, Idx: 0, Reachability: nextReachability()}, nil)
}
reqs = pm.GetProbe()
require.Empty(t, reqs)
})
t.Run("reachabilityUpdate", func(t *testing.T) {
pm := makeNewProbeManager([]ma.Multiaddr{pub1, pub2})
for range 2 * targetConfidence {
reqs := nextProbe(pm)
if reqs[0].Addr.Equal(pub1) {
pm.CompleteProbe(reqs, autonatv2.Result{Addr: pub1, Idx: 0, Reachability: network.ReachabilityPublic}, nil)
} else {
pm.CompleteProbe(reqs, autonatv2.Result{Addr: pub2, Idx: 0, Reachability: network.ReachabilityPrivate}, nil)
}
}
reachable, unreachable, _ := pm.AppendConfirmedAddrs(nil, nil, nil)
require.Equal(t, reachable, []ma.Multiaddr{pub1})
require.Equal(t, unreachable, []ma.Multiaddr{pub2})
})
t.Run("expiry", func(t *testing.T) {
pm := makeNewProbeManager([]ma.Multiaddr{pub1})
for range 2 * targetConfidence {
reqs := nextProbe(pm)
pm.CompleteProbe(reqs, autonatv2.Result{Addr: pub1, Idx: 0, Reachability: network.ReachabilityPublic}, nil)
}
reachable, unreachable, _ := pm.AppendConfirmedAddrs(nil, nil, nil)
require.Equal(t, reachable, []ma.Multiaddr{pub1})
require.Empty(t, unreachable)
cl.Add(maxProbeResultTTL + 1*time.Second)
reachable, unreachable, _ = pm.AppendConfirmedAddrs(nil, nil, nil)
require.Empty(t, reachable)
require.Empty(t, unreachable)
})
}
type mockAutoNATClient struct {
F func(context.Context, []autonatv2.Request) (autonatv2.Result, error)
}
func (m mockAutoNATClient) GetReachability(ctx context.Context, reqs []autonatv2.Request) (autonatv2.Result, error) {
return m.F(ctx, reqs)
}
var _ autonatv2Client = mockAutoNATClient{}
func TestAddrsReachabilityTracker(t *testing.T) {
pub1 := tStringCast("/ip4/1.1.1.1/tcp/1")
pub2 := tStringCast("/ip4/1.1.1.2/tcp/1")
pub3 := tStringCast("/ip4/1.1.1.3/tcp/1")
pri := tStringCast("/ip4/192.168.1.1/tcp/1")
assertFirstEvent := func(t *testing.T, tr *addrsReachabilityTracker, addrs []ma.Multiaddr) {
select {
case <-tr.reachabilityUpdateCh:
case <-time.After(200 * time.Millisecond):
t.Fatal("expected first event quickly")
}
reachable, unreachable, unknown := tr.ConfirmedAddrs()
require.Empty(t, reachable)
require.Empty(t, unreachable)
require.ElementsMatch(t, unknown, addrs, "%s %s", unknown, addrs)
}
newTracker := func(cli mockAutoNATClient, cl clock.Clock) *addrsReachabilityTracker {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
if cl == nil {
cl = clock.New()
}
tr := &addrsReachabilityTracker{
ctx: ctx,
cancel: cancel,
client: cli,
newAddrs: make(chan []ma.Multiaddr, 1),
reachabilityUpdateCh: make(chan struct{}, 1),
maxConcurrency: 3,
newAddrsProbeDelay: 0 * time.Second,
probeManager: newProbeManager(cl.Now),
clock: cl,
}
err := tr.Start()
require.NoError(t, err)
t.Cleanup(func() {
err := tr.Close()
assert.NoError(t, err)
})
return tr
}
t.Run("simple", func(t *testing.T) {
// pub1 reachable, pub2 unreachable, pub3 ignored
mockClient := mockAutoNATClient{
F: func(_ context.Context, reqs []autonatv2.Request) (autonatv2.Result, error) {
for i, req := range reqs {
if req.Addr.Equal(pub1) {
return autonatv2.Result{Addr: pub1, Idx: i, Reachability: network.ReachabilityPublic}, nil
} else if req.Addr.Equal(pub2) {
return autonatv2.Result{Addr: pub2, Idx: i, Reachability: network.ReachabilityPrivate}, nil
}
}
return autonatv2.Result{AllAddrsRefused: true}, nil
},
}
tr := newTracker(mockClient, nil)
tr.UpdateAddrs([]ma.Multiaddr{pub2, pub1, pri})
assertFirstEvent(t, tr, []ma.Multiaddr{pub1, pub2})
select {
case <-tr.reachabilityUpdateCh:
case <-time.After(2 * time.Second):
t.Fatal("expected reachability update")
}
reachable, unreachable, unknown := tr.ConfirmedAddrs()
require.Equal(t, reachable, []ma.Multiaddr{pub1}, "%s %s", reachable, pub1)
require.Equal(t, unreachable, []ma.Multiaddr{pub2}, "%s %s", unreachable, pub2)
require.Empty(t, unknown)
tr.UpdateAddrs([]ma.Multiaddr{pub3, pub1, pub2, pri})
select {
case <-tr.reachabilityUpdateCh:
case <-time.After(2 * time.Second):
t.Fatal("expected reachability update")
}
reachable, unreachable, unknown = tr.ConfirmedAddrs()
t.Logf("Second probe - Reachable: %v, Unreachable: %v, Unknown: %v", reachable, unreachable, unknown)
require.Equal(t, reachable, []ma.Multiaddr{pub1}, "%s %s", reachable, pub1)
require.Equal(t, unreachable, []ma.Multiaddr{pub2}, "%s %s", unreachable, pub2)
require.Equal(t, unknown, []ma.Multiaddr{pub3}, "%s %s", unknown, pub3)
})
t.Run("confirmed addrs ordering", func(t *testing.T) {
mockClient := mockAutoNATClient{
F: func(_ context.Context, reqs []autonatv2.Request) (autonatv2.Result, error) {
return autonatv2.Result{Addr: reqs[0].Addr, Idx: 0, Reachability: network.ReachabilityPublic}, nil
},
}
tr := newTracker(mockClient, nil)
var addrs []ma.Multiaddr
for i := 0; i < 10; i++ {
addrs = append(addrs, tStringCast(fmt.Sprintf("/ip4/1.1.1.1/tcp/%d", i)))
}
slices.SortFunc(addrs, func(a, b ma.Multiaddr) int { return -a.Compare(b) }) // sort in reverse order
tr.UpdateAddrs(addrs)
assertFirstEvent(t, tr, addrs)
select {
case <-tr.reachabilityUpdateCh:
case <-time.After(2 * time.Second):
t.Fatal("expected reachability update")
}
reachable, unreachable, _ := tr.ConfirmedAddrs()
require.Empty(t, unreachable)
orderedAddrs := slices.Clone(addrs)
slices.Reverse(orderedAddrs)
require.Equal(t, reachable, orderedAddrs, "%s %s", reachable, addrs)
})
t.Run("backoff", func(t *testing.T) {
notify := make(chan struct{}, 1)
drainNotify := func() bool {
found := false
for {
select {
case <-notify:
found = true
default:
return found
}
}
}
var allow atomic.Bool
mockClient := mockAutoNATClient{
F: func(_ context.Context, reqs []autonatv2.Request) (autonatv2.Result, error) {
select {
case notify <- struct{}{}:
default:
}
if !allow.Load() {
return autonatv2.Result{}, autonatv2.ErrNoPeers
}
if reqs[0].Addr.Equal(pub1) {
return autonatv2.Result{Addr: pub1, Idx: 0, Reachability: network.ReachabilityPublic}, nil
}
return autonatv2.Result{AllAddrsRefused: true}, nil
},
}
cl := clock.NewMock()
tr := newTracker(mockClient, cl)
// update addrs and wait for initial checks
tr.UpdateAddrs([]ma.Multiaddr{pub1})
assertFirstEvent(t, tr, []ma.Multiaddr{pub1})
// need to update clock after the background goroutine processes the new addrs
time.Sleep(100 * time.Millisecond)
cl.Add(1)
time.Sleep(100 * time.Millisecond)
require.True(t, drainNotify()) // check that we did receive probes
backoffInterval := backoffStartInterval
for i := 0; i < 4; i++ {
drainNotify()
cl.Add(backoffInterval / 2)
select {
case <-notify:
t.Fatal("unexpected call")
case <-time.After(50 * time.Millisecond):
}
cl.Add(backoffInterval/2 + 1) // +1 to push it slightly over the backoff interval
backoffInterval *= 2
select {
case <-notify:
case <-time.After(1 * time.Second):
t.Fatal("expected probe")
}
reachable, unreachable, _ := tr.ConfirmedAddrs()
require.Empty(t, reachable)
require.Empty(t, unreachable)
}
allow.Store(true)
drainNotify()
cl.Add(backoffInterval + 1)
select {
case <-tr.reachabilityUpdateCh:
case <-time.After(1 * time.Second):
t.Fatal("unexpected reachability update")
}
reachable, unreachable, _ := tr.ConfirmedAddrs()
require.Equal(t, reachable, []ma.Multiaddr{pub1})
require.Empty(t, unreachable)
})
t.Run("event update", func(t *testing.T) {
// allow minConfidence probes to pass
called := make(chan struct{}, minConfidence)
notify := make(chan struct{})
mockClient := mockAutoNATClient{
F: func(_ context.Context, _ []autonatv2.Request) (autonatv2.Result, error) {
select {
case called <- struct{}{}:
notify <- struct{}{}
return autonatv2.Result{Addr: pub1, Idx: 0, Reachability: network.ReachabilityPublic}, nil
default:
return autonatv2.Result{AllAddrsRefused: true}, nil
}
},
}
tr := newTracker(mockClient, nil)
tr.UpdateAddrs([]ma.Multiaddr{pub1})
assertFirstEvent(t, tr, []ma.Multiaddr{pub1})
for i := 0; i < minConfidence; i++ {
select {
case <-notify:
case <-time.After(1 * time.Second):
t.Fatal("expected call to autonat client")
}
}
select {
case <-tr.reachabilityUpdateCh:
reachable, unreachable, _ := tr.ConfirmedAddrs()
require.Equal(t, reachable, []ma.Multiaddr{pub1})
require.Empty(t, unreachable)
case <-time.After(1 * time.Second):
t.Fatal("expected reachability update")
}
tr.UpdateAddrs([]ma.Multiaddr{pub1}) // same addrs shouldn't get update
select {
case <-tr.reachabilityUpdateCh:
t.Fatal("didn't expect reachability update")
case <-time.After(100 * time.Millisecond):
}
tr.UpdateAddrs([]ma.Multiaddr{pub2})
select {
case <-tr.reachabilityUpdateCh:
reachable, unreachable, _ := tr.ConfirmedAddrs()
require.Empty(t, reachable)
require.Empty(t, unreachable)
case <-time.After(1 * time.Second):
t.Fatal("expected reachability update")
}
})
t.Run("refresh after reset interval", func(t *testing.T) {
notify := make(chan struct{}, 1)
drainNotify := func() bool {
found := false
for {
select {
case <-notify:
found = true
default:
return found
}
}
}
mockClient := mockAutoNATClient{
F: func(_ context.Context, reqs []autonatv2.Request) (autonatv2.Result, error) {
select {
case notify <- struct{}{}:
default:
}
if reqs[0].Addr.Equal(pub1) {
return autonatv2.Result{Addr: pub1, Idx: 0, Reachability: network.ReachabilityPublic}, nil
}
return autonatv2.Result{AllAddrsRefused: true}, nil
},
}
cl := clock.NewMock()
tr := newTracker(mockClient, cl)
// update addrs and wait for initial checks
tr.UpdateAddrs([]ma.Multiaddr{pub1})
assertFirstEvent(t, tr, []ma.Multiaddr{pub1})
// need to update clock after the background goroutine processes the new addrs
time.Sleep(100 * time.Millisecond)
cl.Add(1)
time.Sleep(100 * time.Millisecond)
require.True(t, drainNotify()) // check that we did receive probes
cl.Add(highConfidenceAddrProbeInterval / 2)
select {
case <-notify:
t.Fatal("unexpected call")
case <-time.After(50 * time.Millisecond):
}
cl.Add(highConfidenceAddrProbeInterval/2 + defaultReachabilityRefreshInterval) // defaultResetInterval for the next probe time
select {
case <-notify:
case <-time.After(1 * time.Second):
t.Fatal("expected probe")
}
})
}
func TestRefreshReachability(t *testing.T) {
pub1 := tStringCast("/ip4/1.1.1.1/tcp/1")
pub2 := tStringCast("/ip4/1.1.1.1/tcp/2")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
newTracker := func(client autonatv2Client, pm *probeManager) *addrsReachabilityTracker {
return &addrsReachabilityTracker{
probeManager: pm,
client: client,
clock: clock.New(),
maxConcurrency: 3,
ctx: ctx,
cancel: cancel,
}
}
t.Run("backoff on ErrNoValidPeers", func(t *testing.T) {
mockClient := mockAutoNATClient{
F: func(_ context.Context, _ []autonatv2.Request) (autonatv2.Result, error) {
return autonatv2.Result{}, autonatv2.ErrNoPeers
},
}
addrTracker := newProbeManager(time.Now)
addrTracker.UpdateAddrs([]ma.Multiaddr{pub1})
r := newTracker(mockClient, addrTracker)
res := r.refreshReachability()
require.True(t, <-res.BackoffCh)
require.Equal(t, addrTracker.InProgressProbes(), 0)
})
t.Run("returns backoff on errTooManyConsecutiveFailures", func(t *testing.T) {
// Create a client that always returns ErrDialRefused
mockClient := mockAutoNATClient{
F: func(_ context.Context, _ []autonatv2.Request) (autonatv2.Result, error) {
return autonatv2.Result{}, errors.New("test error")
},
}
pm := newProbeManager(time.Now)
pm.UpdateAddrs([]ma.Multiaddr{pub1})
r := newTracker(mockClient, pm)
result := r.refreshReachability()
require.True(t, <-result.BackoffCh)
require.Equal(t, pm.InProgressProbes(), 0)
})
t.Run("quits on cancellation", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
block := make(chan struct{})
mockClient := mockAutoNATClient{
F: func(_ context.Context, _ []autonatv2.Request) (autonatv2.Result, error) {
block <- struct{}{}
return autonatv2.Result{}, nil
},
}
pm := newProbeManager(time.Now)
pm.UpdateAddrs([]ma.Multiaddr{pub1})
r := &addrsReachabilityTracker{
ctx: ctx,
cancel: cancel,
client: mockClient,
probeManager: pm,
clock: clock.New(),
}
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
result := r.refreshReachability()
assert.False(t, <-result.BackoffCh)
assert.Equal(t, pm.InProgressProbes(), 0)
}()
cancel()
time.Sleep(50 * time.Millisecond) // wait for the cancellation to be processed
outer:
for i := 0; i < defaultMaxConcurrency; i++ {
select {
case <-block:
default:
break outer
}
}
select {
case <-block:
t.Fatal("expected no more requests")
case <-time.After(50 * time.Millisecond):
}
wg.Wait()
})
t.Run("handles refusals", func(t *testing.T) {
pub1, _ := ma.NewMultiaddr("/ip4/1.1.1.1/tcp/1")
mockClient := mockAutoNATClient{
F: func(_ context.Context, reqs []autonatv2.Request) (autonatv2.Result, error) {
for i, req := range reqs {
if req.Addr.Equal(pub1) {
return autonatv2.Result{Addr: pub1, Idx: i, Reachability: network.ReachabilityPublic}, nil
}
}
return autonatv2.Result{AllAddrsRefused: true}, nil
},
}
pm := newProbeManager(time.Now)
pm.UpdateAddrs([]ma.Multiaddr{pub2, pub1})
r := newTracker(mockClient, pm)
result := r.refreshReachability()
require.False(t, <-result.BackoffCh)
reachable, unreachable, _ := pm.AppendConfirmedAddrs(nil, nil, nil)
require.Equal(t, reachable, []ma.Multiaddr{pub1})
require.Empty(t, unreachable)
require.Equal(t, pm.InProgressProbes(), 0)
})
t.Run("handles completions", func(t *testing.T) {
mockClient := mockAutoNATClient{
F: func(_ context.Context, reqs []autonatv2.Request) (autonatv2.Result, error) {
for i, req := range reqs {
if req.Addr.Equal(pub1) {
return autonatv2.Result{Addr: pub1, Idx: i, Reachability: network.ReachabilityPublic}, nil
}
if req.Addr.Equal(pub2) {
return autonatv2.Result{Addr: pub2, Idx: i, Reachability: network.ReachabilityPrivate}, nil
}
}
return autonatv2.Result{AllAddrsRefused: true}, nil
},
}
pm := newProbeManager(time.Now)
pm.UpdateAddrs([]ma.Multiaddr{pub2, pub1})
r := newTracker(mockClient, pm)
result := r.refreshReachability()
require.False(t, <-result.BackoffCh)
reachable, unreachable, _ := pm.AppendConfirmedAddrs(nil, nil, nil)
require.Equal(t, reachable, []ma.Multiaddr{pub1})
require.Equal(t, unreachable, []ma.Multiaddr{pub2})
require.Equal(t, pm.InProgressProbes(), 0)
})
}
func TestAddrStatusProbeCount(t *testing.T) {
cases := []struct {
inputs string
wantRequiredProbes int
wantReachability network.Reachability
}{
{
inputs: "",
wantRequiredProbes: 3,
wantReachability: network.ReachabilityUnknown,
},
{
inputs: "S",
wantRequiredProbes: 2,
wantReachability: network.ReachabilityUnknown,
},
{
inputs: "SS",
wantRequiredProbes: 1,
wantReachability: network.ReachabilityPublic,
},
{
inputs: "SSS",
wantRequiredProbes: 0,
wantReachability: network.ReachabilityPublic,
},
{
inputs: "SSSSSSSF",
wantRequiredProbes: 1,
wantReachability: network.ReachabilityPublic,
},
{
inputs: "SFSFSSSS",
wantRequiredProbes: 0,
wantReachability: network.ReachabilityPublic,
},
{
inputs: "SSSSSFSF",
wantRequiredProbes: 2,
wantReachability: network.ReachabilityUnknown,
},
{
inputs: "FF",
wantRequiredProbes: 1,
wantReachability: network.ReachabilityPrivate,
},
}
for _, c := range cases {
t.Run(c.inputs, func(t *testing.T) {
now := time.Time{}.Add(1 * time.Second)
ao := addrStatus{}
for _, r := range c.inputs {
if r == 'S' {
ao.AddOutcome(now, network.ReachabilityPublic, 5)
} else {
ao.AddOutcome(now, network.ReachabilityPrivate, 5)
}
now = now.Add(1 * time.Second)
}
require.Equal(t, ao.RequiredProbeCount(now), c.wantRequiredProbes)
require.Equal(t, ao.Reachability(), c.wantReachability)
if c.wantRequiredProbes == 0 {
now = now.Add(highConfidenceAddrProbeInterval + 10*time.Microsecond)
require.Equal(t, ao.RequiredProbeCount(now), 1)
}
now = now.Add(1 * time.Second)
ao.RemoveBefore(now)
require.Len(t, ao.outcomes, 0)
})
}
}
func BenchmarkAddrTracker(b *testing.B) {
cl := clock.NewMock()
t := newProbeManager(cl.Now)
addrs := make([]ma.Multiaddr, 20)
for i := range addrs {
addrs[i] = tStringCast(fmt.Sprintf("/ip4/1.1.1.1/tcp/%d", rand.Intn(1000)))
}
t.UpdateAddrs(addrs)
b.ReportAllocs()
b.ResetTimer()
p := t.GetProbe()
for i := 0; i < b.N; i++ {
pp := t.GetProbe()
if len(pp) == 0 {
pp = p
}
t.MarkProbeInProgress(pp)
t.CompleteProbe(pp, autonatv2.Result{Addr: pp[0].Addr, Idx: 0, Reachability: network.ReachabilityPublic}, nil)
}
}
func FuzzAddrsReachabilityTracker(f *testing.F) {
type autonatv2Response struct {
Result autonatv2.Result
Err error
}
newMockClient := func(b []byte) mockAutoNATClient {
count := 0
return mockAutoNATClient{
F: func(_ context.Context, reqs []autonatv2.Request) (autonatv2.Result, error) {
if len(b) == 0 {
return autonatv2.Result{}, nil
}
count = (count + 1) % len(b)
if b[count]%3 == 0 {
// some address confirmed
c1 := (count + 1) % len(b)
c2 := (count + 2) % len(b)
rch := network.Reachability(b[c1] % 3)
n := int(b[c2]) % len(reqs)
return autonatv2.Result{
Addr: reqs[n].Addr,
Idx: n,
Reachability: rch,
}, nil
}
outcomes := []autonatv2Response{
{Result: autonatv2.Result{AllAddrsRefused: true}},
{Err: errors.New("test error")},
{Err: autonatv2.ErrPrivateAddrs},
{Err: autonatv2.ErrNoPeers},
{Result: autonatv2.Result{}, Err: nil},
{Result: autonatv2.Result{Addr: reqs[0].Addr, Idx: 0, Reachability: network.ReachabilityPublic}},
{Result: autonatv2.Result{
Addr: reqs[0].Addr,
Idx: 0,
Reachability: network.ReachabilityPublic,
AllAddrsRefused: true,
}},
{Result: autonatv2.Result{
Addr: reqs[0].Addr,
Idx: len(reqs) - 1, // invalid idx
Reachability: network.ReachabilityPublic,
AllAddrsRefused: false,
}},
}
outcome := outcomes[int(b[count])%len(outcomes)]
return outcome.Result, outcome.Err
},
}
}
// TODO: Move this to go-multiaddrs
getProto := func(protos []byte) ma.Multiaddr {
protoType := 0
if len(protos) > 0 {
protoType = int(protos[0])
}
port1, port2 := 0, 0
if len(protos) > 1 {
port1 = int(protos[1])
}
if len(protos) > 2 {
port2 = int(protos[2])
}
protoTemplates := []string{
"/tcp/%d/",
"/udp/%d/",
"/udp/%d/quic-v1/",
"/udp/%d/quic-v1/tcp/%d",
"/udp/%d/quic-v1/webtransport/",
"/udp/%d/webrtc/",
"/udp/%d/webrtc-direct/",
"/unix/hello/",
}
s := protoTemplates[protoType%len(protoTemplates)]
port1 %= (1 << 16)
if strings.Count(s, "%d") == 1 {
return tStringCast(fmt.Sprintf(s, port1))
}
port2 %= (1 << 16)
return tStringCast(fmt.Sprintf(s, port1, port2))
}
getIP := func(ips []byte) ma.Multiaddr {
ipType := 0
if len(ips) > 0 {
ipType = int(ips[0])
}
ips = ips[1:]
var x, y int64
split := 128 / 8
if len(ips) < split {
split = len(ips)
}
var b [8]byte
copy(b[:], ips[:split])
x = int64(binary.LittleEndian.Uint64(b[:]))
clear(b[:])
copy(b[:], ips[split:])
y = int64(binary.LittleEndian.Uint64(b[:]))
var ip netip.Addr
switch ipType % 3 {
case 0:
ip = netip.AddrFrom4([4]byte{byte(x), byte(x >> 8), byte(x >> 16), byte(x >> 24)})
return tStringCast(fmt.Sprintf("/ip4/%s/", ip))
case 1:
pubIP := net.ParseIP("2005::") // Public IP address
x := int64(binary.LittleEndian.Uint64(pubIP[0:8]))
ip = netip.AddrFrom16([16]byte{
byte(x), byte(x >> 8), byte(x >> 16), byte(x >> 24),
byte(x >> 32), byte(x >> 40), byte(x >> 48), byte(x >> 56),
byte(y), byte(y >> 8), byte(y >> 16), byte(y >> 24),
byte(y >> 32), byte(y >> 40), byte(y >> 48), byte(y >> 56),
})
return tStringCast(fmt.Sprintf("/ip6/%s/", ip))
default:
ip := netip.AddrFrom16([16]byte{
byte(x), byte(x >> 8), byte(x >> 16), byte(x >> 24),
byte(x >> 32), byte(x >> 40), byte(x >> 48), byte(x >> 56),
byte(y), byte(y >> 8), byte(y >> 16), byte(y >> 24),
byte(y >> 32), byte(y >> 40), byte(y >> 48), byte(y >> 56),
})
return tStringCast(fmt.Sprintf("/ip6/%s/", ip))
}
}
getAddr := func(addrType int, ips, protos []byte) ma.Multiaddr {
switch addrType % 4 {
case 0:
return getIP(ips).Encapsulate(getProto(protos))
case 1:
return getProto(protos)
case 2:
return nil
default:
return getIP(ips).Encapsulate(getProto(protos))
}
}
getDNSAddr := func(hostNameBytes, protos []byte) ma.Multiaddr {
hostName := strings.ReplaceAll(string(hostNameBytes), "\\", "")
hostName = strings.ReplaceAll(hostName, "/", "")
if hostName == "" {
hostName = "localhost"
}
dnsType := 0
if len(hostNameBytes) > 0 {
dnsType = int(hostNameBytes[0])
}
dnsProtos := []string{"dns", "dns4", "dns6", "dnsaddr"}
da := tStringCast(fmt.Sprintf("/%s/%s/", dnsProtos[dnsType%len(dnsProtos)], hostName))
return da.Encapsulate(getProto(protos))
}
const maxAddrs = 1000
getAddrs := func(numAddrs int, ips, protos, hostNames []byte) []ma.Multiaddr {
if len(ips) == 0 || len(protos) == 0 || len(hostNames) == 0 {
return nil
}
numAddrs = ((numAddrs % maxAddrs) + maxAddrs) % maxAddrs
addrs := make([]ma.Multiaddr, numAddrs)
ipIdx := 0
protoIdx := 0
for i := range numAddrs {
addrs[i] = getAddr(i, ips[ipIdx:], protos[protoIdx:])
ipIdx = (ipIdx + 1) % len(ips)
protoIdx = (protoIdx + 1) % len(protos)
}
maxDNSAddrs := 10
protoIdx = 0
for i := 0; i < len(hostNames) && i < maxDNSAddrs; i += 2 {
ed := min(i+2, len(hostNames))
addrs = append(addrs, getDNSAddr(hostNames[i:ed], protos[protoIdx:]))
protoIdx = (protoIdx + 1) % len(protos)
}
return addrs
}
cl := clock.NewMock()
f.Fuzz(func(t *testing.T, numAddrs int, ips, protos, hostNames, autonatResponses []byte) {
tr := newAddrsReachabilityTracker(newMockClient(autonatResponses), nil, cl, nil)
require.NoError(t, tr.Start())
tr.UpdateAddrs(getAddrs(numAddrs, ips, protos, hostNames))
// fuzz tests need to finish in 10 seconds for some reason
// https://github.com/golang/go/issues/48157
// https://github.com/golang/go/commit/5d24203c394e6b64c42a9f69b990d94cb6c8aad4#diff-4e3b9481b8794eb058998e2bec389d3db7a23c54e67ac0f7259a3a5d2c79fd04R474-R483
const maxIters = 20
for range maxIters {
cl.Add(5 * time.Minute)
time.Sleep(100 * time.Millisecond)
}
require.NoError(t, tr.Close())
})
}