main
go 177 lines 4.73 KB
Raw
1 package discovery
2
3 import (
4 "sync/atomic"
5 "time"
6
7 "github.com/gosuda/portal-tunnel/v2/types"
8 "github.com/montanaflynn/stats"
9 )
10
11 const (
12 DiscoveryDescriptorTTL = 5 * time.Minute
13 defaultDirectRecoveryBackoff = 1 * time.Minute
14 maxDirectRecoveryBackoff = 5 * time.Minute
15 activeDropTTL = 72 * time.Hour
16 relayPoolBanTTL = 72 * time.Hour
17
18 // MaxAnnouncedRelays is the hard ceiling on the number of relay entries
19 // the local set will retain. When exceeded, eviction prefers the oldest
20 // non-bootstrap, non-confirmed entries by LastSeenAt. Bootstrap and
21 // listener-confirmed entries are pinned and never evicted by capacity.
22 MaxAnnouncedRelays = 1024
23
24 // AnnounceClockSkewTolerance bounds how far in the future a descriptor's
25 // IssuedAt may sit relative to local time. Anything beyond this is
26 // rejected as clock-skewed or maliciously post-dated.
27 AnnounceClockSkewTolerance = 5 * time.Minute
28
29 // AnnounceMaxValidity bounds the maximum (ExpiresAt - IssuedAt) window
30 // for an accepted announce. Honest relays sign with the discovery TTL,
31 // so a 24h cap leaves ample headroom while preventing attackers from
32 // minting year-long descriptors.
33 AnnounceMaxValidity = 24 * time.Hour
34 )
35
36 type PercentileTracker struct {
37 samples []float64
38 }
39
40 func (pt *PercentileTracker) Add(rtt time.Duration) {
41 pt.samples = append(pt.samples, float64(rtt))
42 if len(pt.samples) > 100 { // Keep last 100 samples
43 pt.samples = pt.samples[1:]
44 }
45 }
46
47 func (pt *PercentileTracker) Get(p float64) time.Duration {
48 if len(pt.samples) == 0 {
49 return 0
50 }
51 // stats.Percentile uses a highly optimized internal implementation
52 val, err := stats.Percentile(pt.samples, p*100)
53 if err != nil {
54 return 0
55 }
56 return time.Duration(val)
57 }
58
59 type RelayState struct {
60 Descriptor types.RelayDescriptor
61 Bootstrap bool
62 Confirmed bool
63 Banned bool
64 LastSeenAt time.Time
65
66 DiscoveryRTT time.Duration
67 DiscoveryRTTAt time.Time
68 EWMARTT time.Duration
69 RTTTracker PercentileTracker
70
71 // SLIT LoadState
72 LoadFactor float64
73 FailureRate float64
74 IsSaturated bool
75 loadFixed uint32
76 saturated uint32
77
78 discoveryFailures int
79 activeFailures int
80 unhealthySince time.Time
81 nextDiscoveryRefreshAt time.Time
82 suppressActiveUntil time.Time
83 }
84
85 const (
86 relayMetricScale = 10000
87 relaySaturationEnterLoad = 8000
88 relaySaturationExitLoad = 6000
89 )
90
91 func fixedLoad(load float64) uint32 {
92 if load <= 0 {
93 return 0
94 }
95 if load >= 1 {
96 return relayMetricScale
97 }
98 return uint32(load*relayMetricScale + 0.5)
99 }
100
101 // StoreLoadFactor records load as fixed-point telemetry.
102 func (state *RelayState) StoreLoadFactor(loadFixed uint32) {
103 if loadFixed > relayMetricScale {
104 loadFixed = relayMetricScale
105 }
106 atomic.StoreUint32(&state.loadFixed, loadFixed)
107 state.LoadFactor = float64(loadFixed) / relayMetricScale
108 }
109
110 func (state *RelayState) inheritAdaptiveTelemetry(existing RelayState) {
111 load := atomic.LoadUint32(&existing.loadFixed)
112 if load == 0 && existing.LoadFactor != 0 {
113 load = fixedLoad(existing.LoadFactor)
114 }
115 state.StoreLoadFactor(load)
116 state.IsSaturated = existing.IsSaturated || atomic.LoadUint32(&existing.saturated) == 1
117 if state.IsSaturated {
118 atomic.StoreUint32(&state.saturated, 1)
119 }
120 }
121
122 // EvaluateSaturation applies load hysteresis:
123 // saturated above 0.8, active below 0.6, unchanged in the guard band.
124 func (state *RelayState) EvaluateSaturation() {
125 load := atomic.LoadUint32(&state.loadFixed)
126 if load == 0 && state.LoadFactor != 0 {
127 load = fixedLoad(state.LoadFactor)
128 atomic.StoreUint32(&state.loadFixed, load)
129 }
130 if state.IsSaturated {
131 atomic.StoreUint32(&state.saturated, 1)
132 }
133
134 saturated := atomic.LoadUint32(&state.saturated)
135 if load > relaySaturationEnterLoad {
136 saturated = 1
137 } else if load < relaySaturationExitLoad {
138 saturated = 0
139 }
140 atomic.StoreUint32(&state.saturated, saturated)
141 state.IsSaturated = saturated == 1
142 }
143
144 func (state *RelayState) UpdateEWMARTT(newRTT time.Duration) {
145 const alpha = 0.3
146 if state.EWMARTT == 0 {
147 state.EWMARTT = newRTT
148 } else {
149 state.EWMARTT = time.Duration(float64(state.EWMARTT)*(1-alpha) + float64(newRTT)*alpha)
150 }
151 state.RTTTracker.Add(newRTT)
152 }
153
154 func newRelayState(relayURL string) RelayState {
155 return RelayState{
156 Descriptor: types.RelayDescriptor{
157 APIHTTPSAddr: relayURL,
158 },
159 }
160 }
161
162 func (state RelayState) hasObservedDescriptor() bool {
163 return !state.LastSeenAt.IsZero()
164 }
165
166 type RouteState struct {
167 ExplicitRelayURLs []string
168 // MaxActiveRelays caps auto-selected relays. Zero or negative values use
169 // the selection default of 3.
170 MaxActiveRelays int
171 MultiHopDepth int
172 RequireUDP bool
173 RequireTCP bool
174 // LocalAddress is the ingress identity address used by MOLS route selection to
175 // derive a deterministic row index into the GF(64) MOLS grid.
176 LocalAddress string
177 }