For reading and verification only. You may not copy, use, modify or redistribute this code. Full notice on the main page.
tunnel/config.go
| 1 | package main |
| 2 | |
| 3 | import ( |
| 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. |
| 17 | type 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. |
| 125 | func 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. |
| 179 | func (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. |
| 248 | func 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. |
| 260 | func isLoopbackHost(host string) bool { |
| 261 | ip, err := netip.ParseAddr(host) |
| 262 | return err == nil && ip.IsLoopback() |
| 263 | } |