ALL

For reading and verification only. You may not copy, use, modify or redistribute this code. Full notice on the main page.

tunnel/config.go

SHA-256: 5bf61e7144345dd41dfa0e7432deb34fad579363f24faeb28a1501a30ed8e4e4
263 lines · 15554 bytes · View on GitHub

1package main
2
3import (
4 "context"
5 "crypto/tls"
6 "fmt"
7 "io"
8 "log/slog"
9 "net"
10 "net/netip"
11 "net/url"
12 "time"
13)
14
15// Config is everything the node needs. DefaultConfig gives the production values, every one of them read from policy.json; tests override the
16// timeouts and addresses.
17type Config struct {
18 HTTPSAddr string // :443 the one public listener: own-name SNI is the control handler (ALPN all-tunnel/2), any other SNI is spliced. "" disables.
19 HTTPAddr string // :80 only the node's own ACME webroot. "" disables.
20 NodeID string // the node's id in the control plane (authorize carries it; OK says it)
21 NodeHost string // the node's own name: the SNI that reaches the control handler, and the host of the proof's URL
22 PublicPort int // the port devices dial, only for the proof's URL (443)
23 ACMEWebroot string // directory whose .well-known/acme-challenge the node's own certificate client writes; "" serves nothing on :80
24
25 // CertSource gives the node's public-CA certificate for its own name (hot reload is the certificate piece's business, the node asks at every
26 // handshake).
27 CertSource func(*tls.ClientHelloInfo) (*tls.Certificate, error)
28 // Keys are the control plane's public keys: sessions are verified offline against them.
29 Keys KeySource
30 // Control is the node's side of POST /v1/tunnel/authorize.
31 Control ControlPlane
32 // Feed is the node's side of GET /v1/tunnel/feed: what the node must stop serving (a revoke, a pause, a released label, the kill switch). Nil runs
33 // the node without a feed (the test node's fake control plane has none).
34 Feed FeedSource
35 // Health is the node's record of whether its control plane answers (the paid grace asks it); the JWKS cache and the feed loop write to the same
36 // one. Nil gets a fresh one.
37 Health *cpHealth
38 // Background are tasks that live as long as the node: the JWKS refresh, the certificate watcher. They start with the listeners and end when the
39 // node shuts down.
40 Background []func(ctx context.Context)
41
42 MaxStreams int // concurrent client connections, global
43 MaxStreamsPerHost int // concurrent client connections per device host
44 MaxUnauth int // concurrent connections that have not yet finished their first message (a ClientHello or a first line)
45 MaxStreamBytes int64 // bytes (both directions summed) per stream
46 ACMEMaxBytes int64 // bytes of a webroot challenge file served on :80
47
48 HelloTimeout time.Duration // a client must deliver a full ClientHello within this
49 AttachTimeout time.Duration // a device must ATTACH within this after OPEN
50 FirstLineTimeout time.Duration // TLS handshake + first protocol line; also the time a device has to answer CHALLENGE with PROOF
51 DeadPeer time.Duration // a control connection that says nothing for this long is dropped
52 MinRenewGap time.Duration // a RENEW sooner than this after the previous one is busy: a device cannot turn one connection into a stream of authorize calls
53 WriteTimeout time.Duration // one control-line write
54 StreamIdle time.Duration // no bytes in a direction for this long closes the stream
55 StreamMaxAge time.Duration // hard cap on one stream's lifetime
56 HalfCloseTimeout time.Duration // after one direction ends cleanly, the other has this long
57 ACMETimeout time.Duration // a whole request on :80
58 AuthorizeMax int // calls to authorize in flight at once, whoever asks
59 AuthorizeTimeout time.Duration // the control plane's time to answer authorize; beyond it the device gets control_unavailable
60 ShutdownGrace time.Duration
61 FeedPoll time.Duration // the longest the node waits between two looks at the feed, and the longest a poll asks the control plane to hold an answer
62 FeedStartWait time.Duration // how long Start waits for the feed's first answer before it accepts connections (the node starts without it when it is late)
63 Grace time.Duration // how long a paid connection outlives its valid_until while the control plane does not answer
64 DownQuiet time.Duration // how long nothing may have answered for the control plane to count as down
65 DrainQuiet time.Duration // how long a draining node keeps its open streams before it cuts them
66
67 // Hardening (limits.go, hints.go, gated.go, openai_prefixes.go, metrics.go). Every number comes from policy.json (node_limits, gated).
68 PerIPConcurrent int // connections one source holds at once: pre-hello, and the client streams it has open (an IPv6 source is its /64)
69 PerIPNewPerS int // new connections per second one source may open (a burst of this many, then this many per second)
70 IPv6PrefixBits int // the length of the IPv6 prefix that counts as one source
71 IPTableMax int // sources remembered at once; beyond it new ones share one overflow entry
72 OpenAIPerIPFactor int // the per-source limits of an address in the OpenAI prefix list are this many times larger (one egress address serves many users)
73 DevicePerIPFactor int // the same for an address that holds an authenticated control connection: a device opens one connection per stream it serves
74 UnknownSNIPerIP int // distinct names that are not a device online (or malformed ClientHellos) one source may try within UnknownSNIWindow
75 UnknownSNIWindow time.Duration //
76 UnknownSNIBan time.Duration // how long a source that went over the budget is dropped at accept (memory only)
77 LabelAnonSlice int // of MaxStreamsPerHost, what a source that is not marked good may hold; the rest is the slice of sources the device marked with HINT
78 LabelNewPerS int // new client streams per second one device host accepts from sources that are not marked good
79 LabelNewBurst int //
80 OpenAIReservedPct int // of MaxStreams, the share only sources of the openai or hint class may use
81 HintTTL time.Duration // how long a HINT keeps an address good
82 HintsPerLabel int // addresses remembered per device host
83 GatedBytesPerDay int64 // a gated device's byte budget per UTC day (both directions, all its streams)
84 GatedStreamsPerDay int // and its stream budget
85 TCPKeepalive time.Duration // keepalive period of every socket the node holds
86 MetricsAddr string // loopback address of /metrics and /healthz; "" disables
87 OpenAIPrefixesURL string // where the connector prefixes are published; "" disables the refresh
88 OpenAIPrefixesCache string // file with the last good list (public data), read at start and written at every good refresh; "" keeps it in memory only
89 OpenAIPrefixRefresh time.Duration
90 // PrefixFetch replaces the HTTPS GET of OpenAIPrefixesURL (tests, and a deployment that mirrors the list).
91 PrefixFetch func(ctx context.Context) ([]byte, error)
92 // CertNotAfter reports the end of the node's own certificate, for the metrics (nil: not reported).
93 CertNotAfter func() time.Time
94
95 // ClockSkew is how far a device's clock may be ahead of the node's: a session or proof issued up to this far in the future still verifies. The
96 // device compensates its clock with the control plane's server_time, this is the window left over.
97 ClockSkew time.Duration
98
99 Logger *slog.Logger
100
101 // Now is the node's clock (time.Now when nil), AfterFunc its timers (time.AfterFunc when nil). Every lifetime the node decides (valid_until, the
102 // grace, the feed's pace, a drain's quiet period, the JWKS cache) runs on them, so a test drives hours in an instant.
103 Now func() time.Time
104 AfterFunc func(d time.Duration, f func()) (stop func() bool)
105 // Classify overrides the class of a client's source address for the OPEN: openai, hint or anon. Nil is the node's own: openai for an address of
106 // the OpenAI prefix list, hint for one the device marked with HINT, anon for the rest.
107 Classify func(peer netip.Addr, host string) string
108 // OnHint is told of the source addresses a device marked as good with HINT, besides the node's own hint table (nil: only the table).
109 OnHint func(host string, addrs []netip.Addr)
110
111 // tap, when non-nil, is called with every payload buffer the node forwards (never set in production). Tests use it to prove what the node
112 // can and cannot see.
113 tap func(streamID, direction string, p []byte)
114 // divert, when non-nil, may take a client connection before the node opens a stream for it (never set in production: only the test-only mutation of
115 // blindmutant_test_hook.go sets it, to be a node that does what the blindness gate must catch). It reports whether it handled the connection.
116 divert func(c net.Conn, peeked []byte, log *slog.Logger) bool
117 // peerOf, when non-nil, tells the source address of an accepted connection instead of its remote address (never set in production: a test gives
118 // loopback connections the addresses of many sources).
119 peerOf func(c net.Conn) (netip.Addr, bool)
120 // trace, when non-nil, receives one event per protocol step in the format of the contracts' transcript checker (never set in production).
121 trace func(event map[string]any)
122}
123
124// DefaultConfig returns the production defaults: every window and limit comes from policy.json.
125func DefaultConfig() Config {
126 p := policyData
127 return Config{
128 HTTPSAddr: ":443",
129 HTTPAddr: ":80",
130 PublicPort: 443,
131 MaxStreams: p.NodeLimits.GlobalStreamsMax,
132 MaxStreamsPerHost: p.NodeLimits.LabelStreamsMax,
133 MaxUnauth: p.NodeLimits.MaxUnauthenticatedControl,
134 MaxStreamBytes: 512 << 20,
135 ACMEMaxBytes: 8 << 10,
136 HelloTimeout: secs(p.NodeLimits.ClientHelloDeadlineS),
137 AttachTimeout: secs(p.Protocol.AttachTimeoutS),
138 FirstLineTimeout: secs(p.Protocol.FirstLineTimeoutS),
139 DeadPeer: secs(p.Timing.DeadPeerS),
140 MinRenewGap: secs(p.Timing.BackoffMinS),
141 WriteTimeout: 5 * time.Second,
142 StreamIdle: secs(p.Timing.StreamIdleS),
143 StreamMaxAge: secs(p.Timing.StreamMaxAgeS),
144 HalfCloseTimeout: secs(p.Timing.HalfCloseS),
145 ACMETimeout: 15 * time.Second,
146 AuthorizeMax: p.Authorization.AuthorizeMaxConcur,
147 AuthorizeTimeout: 10 * time.Second,
148 ShutdownGrace: 10 * time.Second,
149 FeedPoll: secs(p.Timing.FeedPollS),
150 FeedStartWait: 3 * time.Second,
151 Grace: secs(p.Authorization.GracePaidS),
152 DownQuiet: 2 * secs(p.Timing.FeedPollS),
153 DrainQuiet: secs(p.Timing.DrainQuietS),
154 ClockSkew: secs(p.Authorization.NodeSkewS),
155 PerIPConcurrent: p.NodeLimits.PerIPConcurrent,
156 PerIPNewPerS: p.NodeLimits.PerIPNewPerS,
157 IPv6PrefixBits: p.NodeLimits.IPv6PrefixBits,
158 IPTableMax: p.NodeLimits.IPTableMax,
159 OpenAIPerIPFactor: p.NodeLimits.OpenAIPerIPFactor,
160 DevicePerIPFactor: p.NodeLimits.DevicePerIPFactor,
161 UnknownSNIPerIP: p.NodeLimits.UnknownSNIPerIP,
162 UnknownSNIWindow: secs(p.NodeLimits.UnknownSNIWindowS),
163 UnknownSNIBan: secs(p.NodeLimits.UnknownSNIBanS),
164 LabelAnonSlice: p.NodeLimits.LabelAnonSlice,
165 LabelNewPerS: p.NodeLimits.LabelNewPerS,
166 LabelNewBurst: p.NodeLimits.LabelNewBurst,
167 OpenAIReservedPct: p.NodeLimits.OpenAIReservedPct,
168 HintTTL: secs(p.NodeLimits.HintTTLS),
169 HintsPerLabel: p.NodeLimits.HintAddressesPerLabel,
170 GatedBytesPerDay: p.Gated.BytesPerDay,
171 GatedStreamsPerDay: p.Gated.StreamsPerDay,
172 TCPKeepalive: secs(p.NodeLimits.TCPKeepaliveS),
173 OpenAIPrefixRefresh: secs(p.NodeLimits.OpenAIPrefixRefreshS),
174 Logger: slog.New(slog.NewJSONHandler(io.Discard, nil)),
175 }
176}
177
178// validate refuses a configuration the node cannot run safely. It does not touch the network.
179func (c *Config) validate() error {
180 if !reNodeID.MatchString(c.NodeID) {
181 return fmt.Errorf("node id %q: want 1-16 characters of [a-z0-9]", c.NodeID)
182 }
183 if c.HTTPSAddr != "" {
184 host, ok := normalizeName(c.NodeHost)
185 if !ok || host != c.NodeHost {
186 return fmt.Errorf("node host %q is not a normalized DNS name", c.NodeHost)
187 }
188 if validFQDN(host) {
189 return fmt.Errorf("node host %q looks like a device host", host)
190 }
191 if c.CertSource == nil {
192 return fmt.Errorf("no certificate source for the node's own name")
193 }
194 }
195 if c.Keys == nil || c.Control == nil {
196 return fmt.Errorf("the node needs a key source and a control plane")
197 }
198 if c.PublicPort < 1 || c.PublicPort > 65535 {
199 return fmt.Errorf("public port %d", c.PublicPort)
200 }
201 if c.ClockSkew < 0 || c.ClockSkew > secs(policyData.Authorization.NodeSkewMaxS) {
202 return fmt.Errorf("clock skew %s is beyond the policy's %s", c.ClockSkew, secs(policyData.Authorization.NodeSkewMaxS))
203 }
204 if c.Feed != nil && (c.FeedPoll < time.Millisecond || c.FeedPoll > secs(policyData.Timing.FeedPollS)) {
205 return fmt.Errorf("feed poll %s is beyond the policy's %s", c.FeedPoll, secs(policyData.Timing.FeedPollS))
206 }
207 if c.DrainQuiet < 0 || c.DrainQuiet > 24*time.Hour {
208 return fmt.Errorf("drain quiet period %s: want 0 to 24h", c.DrainQuiet)
209 }
210 if c.Grace < 0 || c.Grace > secs(policyData.Authorization.GracePaidS) {
211 return fmt.Errorf("grace %s is beyond the policy's %s", c.Grace, secs(policyData.Authorization.GracePaidS))
212 }
213 if c.AuthorizeMax < 1 {
214 return fmt.Errorf("authorize concurrency must be at least 1")
215 }
216 if c.MaxStreams < 1 || c.MaxStreamsPerHost < 1 || c.MaxUnauth < 1 {
217 return fmt.Errorf("stream limits must be at least 1")
218 }
219 if c.PerIPConcurrent < 1 || c.PerIPNewPerS < 1 || c.IPTableMax < 1 || c.OpenAIPerIPFactor < 1 || c.DevicePerIPFactor < 1 || c.LabelAnonSlice < 1 || c.LabelNewPerS < 1 || c.LabelNewBurst < 1 {
220 return fmt.Errorf("per-source and per-device limits must be at least 1")
221 }
222 if c.IPv6PrefixBits < 1 || c.IPv6PrefixBits > 128 {
223 return fmt.Errorf("ipv6 prefix of %d bits", c.IPv6PrefixBits)
224 }
225 if c.OpenAIReservedPct < 0 || c.OpenAIReservedPct > 90 {
226 return fmt.Errorf("openai reserved share %d%%: want 0 to 90", c.OpenAIReservedPct)
227 }
228 if c.UnknownSNIPerIP < 1 || c.UnknownSNIWindow <= 0 || c.UnknownSNIBan <= 0 || c.HintTTL <= 0 || c.HintsPerLabel < 1 || c.TCPKeepalive <= 0 {
229 return fmt.Errorf("the unknown-name budget, the hint table and the keepalive need positive values")
230 }
231 if c.GatedBytesPerDay < 1 || c.GatedStreamsPerDay < 1 {
232 return fmt.Errorf("a gated device's daily budgets must be at least 1")
233 }
234 if c.MetricsAddr != "" {
235 if err := checkLoopbackAddr(c.MetricsAddr); err != nil {
236 return err
237 }
238 }
239 if c.OpenAIPrefixesURL != "" {
240 if u, err := url.Parse(c.OpenAIPrefixesURL); err != nil || u.Host == "" || u.User != nil || (u.Scheme != "https" && !(u.Scheme == "http" && isLoopbackHost(u.Hostname()))) {
241 return fmt.Errorf("prefix list URL %q: want https://<host>/<path> (plain http only for a loopback mirror)", c.OpenAIPrefixesURL)
242 }
243 }
244 return nil
245}
246
247// checkLoopbackAddr refuses a metrics address that is not loopback: the counters are for the machine's own operator, never for the network.
248func checkLoopbackAddr(addr string) error {
249 host, _, err := net.SplitHostPort(addr)
250 if err != nil {
251 return fmt.Errorf("metrics address %q: %w", addr, err)
252 }
253 if !isLoopbackHost(host) {
254 return fmt.Errorf("metrics address %q is not a loopback address: /metrics and /healthz are for this machine only", addr)
255 }
256 return nil
257}
258
259// isLoopbackHost reports whether host is a loopback IP literal.
260func isLoopbackHost(host string) bool {
261 ip, err := netip.ParseAddr(host)
262 return err == nil && ip.IsLoopback()
263}