mirror of
https://github.com/QuilibriumNetwork/ceremonyclient.git
synced 2026-02-24 11:57:25 +08:00
* 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
943 lines
28 KiB
Go
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())
|
|
})
|
|
}
|