Skip to content
File

Blob: tun/client/connection.go

go265 lines
1package client
2 
3import (
4 "context"
5 "fmt"
6 "sort"
7 "time"
8 
9 "go.miragespace.co/specter/spec/chord"
10 "go.miragespace.co/specter/spec/protocol"
11 "go.miragespace.co/specter/spec/rpc"
12 "go.miragespace.co/specter/spec/rtt"
13 "go.miragespace.co/specter/spec/tun"
14 
15 "github.com/avast/retry-go/v5"
16 "go.uber.org/zap"
17)
18 
19func (c *Client) bootstrap(ctx context.Context, apex string) error {
20 c.Logger.Info("Bootstraping connection to specter server", zap.String("addr", apex))
21 return c.openRPC(ctx, &protocol.Node{
22 Address: apex,
23 })
24}
25 
26func (c *Client) openRPC(ctx context.Context, node *protocol.Node) error {
27 if _, ok := c.connections.Load(node.GetAddress()); ok {
28 return nil
29 }
30 
31 callCtx, cancel := context.WithTimeout(ctx, connectTimeout)
32 defer cancel()
33 
34 resp, err := c.ping(callCtx, node)
35 if err != nil {
36 return err
37 }
38 
39 identity := resp.GetNode()
40 c.Logger.Info("Connected to specter server", zap.String("addr", identity.GetAddress()))
41 c.connections.Store(identity.GetAddress(), identity)
42 
43 return nil
44}
45 
46func retryRPC[V any](c *Client, ctx context.Context, fn func(node *protocol.Node) (V, error)) (resp V, err error) {
47 candidates := c.getConnectedNodes()
48 retrier := retry.New(
49 retry.Context(ctx),
50 retry.Attempts(2),
51 retry.LastErrorOnly(true),
52 retry.Delay(time.Millisecond*500),
53 retry.RetryIf(chord.ErrorIsRetryable),
54 )
55 err = retrier.Do(func() error {
56 var (
57 candidate *protocol.Node
58 rpcError error
59 )
60 if len(candidates) > 0 {
61 candidate, candidates = candidates[0], candidates[1:]
62 }
63 if candidate == nil {
64 return fmt.Errorf("no rpc candidates available")
65 }
66 resp, rpcError = fn(candidate)
67 return chord.ErrorMapper(rpcError)
68 })
69 return
70}
71 
72func (c *Client) getConnectedNodes() (nodes []*protocol.Node) {
73 c.connections.Range(func(_ string, node *protocol.Node) bool {
74 if len(nodes) < tun.NumRedundantLinks {
75 nodes = append(nodes, node)
76 }
77 return true
78 })
79 
80 // fast path exit if we don't have rtt enabled
81 if c.Recorder == nil {
82 return
83 }
84 
85 // sort routes based on rtt to different gateways, so hostname/1 and rpc calls
86 // always resolves to the gateway with the lowest rtt to the client
87 rttLookup := make(map[string]time.Duration)
88 for _, n := range nodes {
89 m := c.Recorder.Snapshot(rtt.MakeMeasurementKey(n), time.Second*10)
90 if m == nil {
91 continue
92 }
93 rttLookup[rtt.MakeMeasurementKey(n)] = m.Average
94 }
95 sort.SliceStable(nodes, func(i, j int) bool {
96 l, lOK := rttLookup[rtt.MakeMeasurementKey(nodes[i])]
97 r, rOK := rttLookup[rtt.MakeMeasurementKey(nodes[j])]
98 if lOK && !rOK {
99 return true
100 }
101 if !lOK && rOK {
102 return false
103 }
104 return l < r
105 })
106 
107 c.Logger.Debug("rtt information", zap.String("table", fmt.Sprint(rttLookup)))
108 
109 return nodes
110}
111 
112func (c *Client) getAliveNodes(ctx context.Context) (alive []*protocol.Node, dead int) {
113 alive = make([]*protocol.Node, 0)
114 c.connections.Range(func(addr string, node *protocol.Node) bool {
115 func() {
116 callCtx, cancel := context.WithTimeout(ctx, connectTimeout)
117 defer cancel()
118 
119 _, err := c.ping(callCtx, node)
120 if err != nil {
121 c.connections.Delete(addr)
122 dead++
123 } else {
124 alive = append(alive, node)
125 }
126 }()
127 return true
128 })
129 return
130}
131 
132func (c *Client) periodicReconnection(ctx context.Context) {
133 defer c.closeWg.Done()
134 
135 ctx, cancel := context.WithCancel(ctx)
136 defer cancel()
137 go func() {
138 select {
139 case <-c.closeCh:
140 cancel()
141 case <-ctx.Done():
142 }
143 }()
144 
145 runReconnectLoop(ctx, nil, func(ctx context.Context) (time.Duration, error) {
146 c.reconcileConnections(ctx)
147 return 0, nil
148 })
149}
150 
151// runReconnectLoop serializes maintenance and preserves the full client's ticker
152// cadence. A positive retry delay takes precedence over ticks and wakeups until
153// it fires, avoiding duplicate attempts during a complete outage.
154func runReconnectLoop(ctx context.Context, wake <-chan struct{}, reconcile func(context.Context) (time.Duration, error)) error {
155 ticker := time.NewTicker(checkInterval)
156 defer ticker.Stop()
157 retry := time.NewTimer(checkInterval)
158 retry.Stop()
159 defer retry.Stop()
160 var retryC <-chan time.Time
161 for {
162 select {
163 case <-ctx.Done():
164 return ctx.Err()
165 case <-ticker.C:
166 if retryC != nil {
167 continue
168 }
169 case <-wake:
170 if retryC != nil {
171 continue
172 }
173 case <-retryC:
174 retryC = nil
175 }
176 if ctx.Err() != nil {
177 return ctx.Err()
178 }
179 delay, err := reconcile(ctx)
180 if err != nil {
181 return err
182 }
183 if delay > 0 {
184 retry.Reset(delay)
185 retryC = retry.C
186 }
187 }
188}
189 
190func (c *Client) reconcileConnections(ctx context.Context) {
191 prev, failed := c.getAliveNodes(ctx)
192 if failed > 0 {
193 c.Logger.Info("Some connections have failed, opening more connections to specter server", zap.Int("dead", failed))
194 }
195 if err := c.maintainConnections(ctx); err != nil {
196 // A candidate lookup can fail while existing gateways still work. It
197 // must not prevent retrying previously failed tunnel publication.
198 c.Logger.Warn("Failed to maintain gateway connections", zap.Error(err))
199 }
200 now, _ := c.getAliveNodes(ctx)
201 changed := failed > 0 || endpointSignature(prev) != endpointSignature(now)
202 
203 c.syncMu.Lock()
204 defer c.syncMu.Unlock()
205 if ctx.Err() != nil {
206 return
207 }
208 c.syncStateMu.RLock()
209 retryPending := c.lastSync.pendingPublication() && !time.Now().Before(c.nextSync)
210 c.syncStateMu.RUnlock()
211 if changed {
212 clear(c.publication)
213 }
214 if changed || retryPending {
215 c.syncConfigTunnels(ctx, false)
216 }
217}
218 
219func (c *Client) maintainConnections(ctx context.Context) error {
220 if c.connections.Len() == 0 {
221 c.Logger.Info("No connected nodes, re-bootstrapping using apex")
222 c.configMu.RLock()
223 apex := c.ClientConfig.Configuration.Apex
224 c.configMu.RUnlock()
225 if err := c.bootstrap(ctx, apex); err != nil {
226 return fmt.Errorf("rebootstrapping connection to specter: %w", err)
227 }
228 }
229 
230 callCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
231 defer cancel()
232 
233 nodes, err := c.requestCandidates(callCtx)
234 if err != nil {
235 return err
236 }
237 c.Logger.Debug("Candidates for RPC connections", zap.Int("num", len(nodes)))
238 
239 for _, node := range nodes {
240 if c.connections.Len() >= tun.NumRedundantLinks {
241 return nil
242 }
243 if err := c.openRPC(ctx, node); err != nil {
244 return fmt.Errorf("connecting to specter server: %w", err)
245 }
246 }
247 
248 return nil
249}
250 
251func (c *Client) ping(ctx context.Context, node *protocol.Node) (*protocol.ClientPingResponse, error) {
252 return c.tunnelClient.Ping(rpc.WithNode(ctx, node), &protocol.ClientPingRequest{})
253}
254 
255func (c *Client) requestCandidates(ctx context.Context) ([]*protocol.Node, error) {
256 resp, err := retryRPC(c, ctx, func(node *protocol.Node) (*protocol.GetNodesResponse, error) {
257 ctx = rpc.WithNode(ctx, node)
258 return c.tunnelClient.GetNodes(ctx, &protocol.GetNodesRequest{})
259 })
260 if err != nil {
261 return nil, err
262 }
263 return resp.GetNodes(), nil
264}