Skip to content
File

Blob: tun/server/route_cache_test.go

go174 lines
1package server
2 
3import (
4 "bytes"
5 "context"
6 "errors"
7 "fmt"
8 "testing"
9 "time"
10 
11 "go.miragespace.co/specter/spec/protocol"
12 "go.miragespace.co/specter/spec/tun"
13 
14 "github.com/stretchr/testify/mock"
15 "github.com/stretchr/testify/require"
16)
17 
18func TestRouteCacheLoaderAllNotFound(t *testing.T) {
19 as := require.New(t)
20 
21 _, node, _, _, serv := getFixture(t, as)
22 
23 hostname := "all-not-found.example.com"
24 link := &protocol.Link{Hostname: hostname}
25 expected := getExpected(link)
26 
27 node.On("Get", mock.Anything, mock.MatchedBy(func(k []byte) bool {
28 return assertBytes(k, expected...)
29 })).Return([]byte{}, nil)
30 
31 ret := serv.routeCacheLoader(context.Background(), hostname)
32 
33 as.ErrorIs(ret.Value.err, tun.ErrDestinationNotFound)
34 as.Equal(routeNegativeTTL, ret.TTL)
35 as.EqualValues(int64(8), ret.Cost)
36 as.Nil(ret.Value.routes)
37 
38 node.AssertExpectations(t)
39}
40 
41func TestRouteCacheLoaderLookupFailure(t *testing.T) {
42 for numErrors := 1; numErrors <= tun.NumRedundantLinks; numErrors++ {
43 t.Run(fmt.Sprintf("%d_errors", numErrors), func(t *testing.T) {
44 as := require.New(t)
45 _, node, _, _, serv := getFixture(t, as)
46 hostname := "lookup-failure.example.com"
47 
48 for i := 1; i <= tun.NumRedundantLinks; i++ {
49 var lookupErr error
50 if i <= numErrors {
51 lookupErr = errors.New("boom")
52 }
53 node.On("Get", mock.Anything, []byte(tun.RoutingKey(hostname, i))).
54 Return(([]byte)(nil), lookupErr).Once()
55 }
56 
57 ret := serv.routeCacheLoader(context.Background(), hostname)
58 
59 as.ErrorIs(ret.Value.err, tun.ErrLookupFailed)
60 as.Equal(routeFailedTTL, ret.TTL)
61 as.EqualValues(int64(16), ret.Cost)
62 as.Nil(ret.Value.routes)
63 node.AssertExpectations(t)
64 })
65 }
66}
67 
68func TestRouteCacheLoaderSuccessPrioritizesDirect(t *testing.T) {
69 as := require.New(t)
70 
71 _, node, clientT, _, serv := getFixture(t, as)
72 cli, cht, tn := getIdentities()
73 
74 hostname := "success.example.com"
75 
76 indirectRoute := &protocol.TunnelRoute{
77 ClientDestination: cli,
78 ChordDestination: cht,
79 TunnelDestination: &protocol.Node{Address: "remote:123"},
80 Hostname: hostname,
81 }
82 indirectBuf, err := indirectRoute.MarshalVT()
83 as.NoError(err)
84 
85 directRoute := &protocol.TunnelRoute{
86 ClientDestination: cli,
87 ChordDestination: cht,
88 TunnelDestination: tn,
89 Hostname: hostname,
90 }
91 directBuf, err := directRoute.MarshalVT()
92 as.NoError(err)
93 
94 keys := [][]byte{
95 []byte(tun.RoutingKey(hostname, 1)),
96 []byte(tun.RoutingKey(hostname, 2)),
97 []byte(tun.RoutingKey(hostname, 3)),
98 }
99 
100 node.On("Get", mock.Anything, mock.MatchedBy(func(k []byte) bool {
101 return bytes.Equal(k, keys[0])
102 })).Return(indirectBuf, nil)
103 
104 node.On("Get", mock.Anything, mock.MatchedBy(func(k []byte) bool {
105 return bytes.Equal(k, keys[1])
106 })).Return(directBuf, nil)
107 
108 node.On("Get", mock.Anything, mock.MatchedBy(func(k []byte) bool {
109 return bytes.Equal(k, keys[2])
110 })).Return(([]byte)(nil), errors.New("temporary lookup failure"))
111 
112 clientT.On("Identity").Return(tn)
113 
114 ret := serv.routeCacheLoader(context.Background(), hostname)
115 
116 as.NoError(ret.Value.err)
117 as.Len(ret.Value.routes, 2)
118 as.Equal(routePositiveTTL, ret.TTL)
119 as.True(ret.Cost > 0)
120 
121 // the directly connected route (matching TunnelTransport.Identity) should be first
122 as.Equal(tn.GetAddress(), ret.Value.routes[0].GetTunnelDestination().GetAddress())
123 as.Equal("remote:123", ret.Value.routes[1].GetTunnelDestination().GetAddress())
124 
125 node.AssertExpectations(t)
126 clientT.AssertExpectations(t)
127}
128 
129func TestEphemeralRoutes(t *testing.T) {
130 s, n, _, _ := sessionFixture(t)
131 label, inv, err := tun.EphemeralLabel(s.Chord.ID(), []byte("key"))
132 require.NoError(t, err)
133 local := s.routeCacheLoader(t.Context(), label)
134 require.NoError(t, local.Value.err)
135 require.GreaterOrEqual(t, local.Cost, int64(256))
136 n.AssertNotCalled(t, "Get", mock.Anything, mock.Anything)
137 remote := uint64(10)
138 n.On("FindSuccessor", remote).Return(getVNode(&protocol.Node{Id: remote + 1}), nil).Once()
139 missing := s.ephemeralRouteLoader(t.Context(), label, remote, inv)
140 require.ErrorIs(t, missing.Value.err, tun.ErrDestinationNotFound)
141 dst := &protocol.TunnelDestination{
142 Chord: &protocol.Node{
143 Id: remote,
144 Address: "remote",
145 },
146 Tunnel: &protocol.Node{Address: "remote tunnel"},
147 }
148 data, _ := dst.MarshalVT()
149 n.On("FindSuccessor", remote).Return(getVNode(dst.Chord), nil).Once()
150 n.On("Get", mock.Anything, []byte(tun.DestinationByChordKey(dst.Chord))).Return(data, nil).Once()
151 found := s.ephemeralRouteLoader(t.Context(), label, remote, inv)
152 require.NoError(t, found.Value.err)
153 require.Equal(t, dst.Tunnel, found.Value.routes[0].TunnelDestination)
154 require.Equal(t, tun.SessionAlias(inv), found.Value.routes[0].ClientDestination.Address)
155 require.GreaterOrEqual(t, found.Cost, int64(256))
156 blocked, release := make(chan struct{}, 16), make(chan struct{})
157 n.On("FindSuccessor", uint64(11)).Run(func(mock.Arguments) { blocked <- struct{}{}; <-release }).Return(nil, errors.New("blocked lookup")).Times(16)
158 for i := range 16 {
159 go s.ephemeralRouteLoader(t.Context(), fmt.Sprint(i), 11, inv)
160 }
161 for range 16 {
162 <-blocked
163 }
164 exhausted := s.ephemeralRouteLoader(t.Context(), "17", 11, inv)
165 require.ErrorIs(t, exhausted.Value.err, tun.ErrLookupFailed)
166 require.Equal(t, routeFailedTTL, exhausted.TTL)
167 // Expired waiters must not free admission for still-running Chord traversals.
168 time.Sleep(lookupTimeout + 10*time.Millisecond)
169 require.Len(t, s.ephemeralLoads, 16)
170 close(release)
171 require.Eventually(t, func() bool { return len(s.ephemeralLoads) == 0 }, time.Second, time.Millisecond)
172 n.AssertExpectations(t)
173}