ALL

Solo para leer y verificar. No puedes copiar, usar, modificar ni redistribuir este código. Aviso completo en la página principal.

tunnel/selfcheck.go

SHA-256: fe172f347bc5de5cf23394984fba5d0d630c96ee8bb2ff44376add6aaa68c126
311 líneas · 11760 bytes · Ver en GitHub

1package main
2
3// Self-check (ALL 2.0, WP26; the design plan, section 6 point 4, the migration notes): before a computer is told to reconnect, somebody OUTSIDE the
4// computer's own process dials its public name the way ChatGPT will, and says whether it works. The computer asks it of a node (the node is on the other side of
5// the internet from the computer's own network), and the same function runs from the computer itself (the device's enrolment code) and from the operator's canary.
6//
7// dial the public address, SNI = the computer's host, TLS 1.2 offered and 1.3 required
8// verify the chain against the public roots, the name and the validity at this moment
9// compare the leaf's SubjectPublicKeyInfo hash with the one the computer expects (a certificate that is not the computer's own key is not the computer)
10// GET /healthz through the same connection and read {"ok": true}
11//
12// The answer is contracts/schemas/selfcheck-result.schema.json: ok, the FIRST step that failed as one closed word (chain, expired, spki, name, tls_version, healthz,
13// unreachable, timeout), the TLS version, the leaf's SPKI hash and its not_after. No host, no address, no free text: it can be shown, stored and sent as it is.
14//
15// What this file does NOT do: it does not decide who may ask, it has no listener of its own and it does not know the label registry. NewSelfCheckHandler is the
16// HTTP shape (a POST with {v, label, expect}) for the operator's loopback port, with the per-label spacing and the cap on checks in flight that keep it from being
17// a way to make the node dial itself; the route in server.go and the policy numbers (selfcheck.timeout_s, selfcheck.min_interval_s) are the node integration's (WP17).
18
19import (
20 "bufio"
21 "context"
22 "crypto/sha256"
23 "crypto/tls"
24 "crypto/x509"
25 "encoding/base64"
26 "encoding/json"
27 "errors"
28 "io"
29 "net"
30 "net/http"
31 "os"
32 "strings"
33 "sync"
34 "time"
35)
36
37// The reasons, in the order the steps run. They are policy.enums.selfcheck_reason (the test pins that they are the same list).
38const (
39 selfCheckChain = "chain"
40 selfCheckExpired = "expired"
41 selfCheckSPKI = "spki"
42 selfCheckName = "name"
43 selfCheckTLSVersion = "tls_version"
44 selfCheckHealthz = "healthz"
45 selfCheckUnreachable = "unreachable"
46 selfCheckTimeout = "timeout"
47)
48
49// SelfCheckResult is the contract's selfcheck-result.
50type SelfCheckResult struct {
51 V int `json:"v"`
52 OK bool `json:"ok"`
53 Reason *string `json:"reason"`
54 TLSVersion *string `json:"tls_version"`
55 SPKISHA256 *string `json:"spki_sha256"`
56 NotAfter *int64 `json:"not_after"`
57 Healthz bool `json:"healthz"`
58}
59
60// SelfCheckConfig is what the check needs from outside. Nothing here has a default that is a number: the timeout is the caller's (the policy's).
61type SelfCheckConfig struct {
62 // Dial opens the transport to the public address; nil is a plain TCP dial.
63 Dial func(ctx context.Context, network, addr string) (net.Conn, error)
64 // Roots are the public roots the chain must lead to; nil is the system's.
65 Roots *x509.CertPool
66 // Now is the moment validity is judged at; nil is the clock.
67 Now func() time.Time
68 // Timeout bounds the whole check (dial, handshake and healthz); zero leaves it to the context.
69 Timeout time.Duration
70}
71
72func selfCheckFail(res *SelfCheckResult, reason string) SelfCheckResult {
73 res.OK = false
74 res.Reason = &reason
75 return *res
76}
77
78// SelfCheck dials addr ("host:port", the public address of the node or of the computer's name), speaks TLS as `host`, and judges what it finds. `expect` is the set
79// of SPKI hashes (SHA-256, base64url, no padding) the computer may be serving; empty means "any" (the operator's canary asks only whether the chain is good).
80func SelfCheck(ctx context.Context, cfg SelfCheckConfig, addr, host string, expect []string) SelfCheckResult {
81 res := SelfCheckResult{V: 1}
82 if cfg.Timeout > 0 {
83 var cancel context.CancelFunc
84 ctx, cancel = context.WithTimeout(ctx, cfg.Timeout)
85 defer cancel()
86 }
87 dial := cfg.Dial
88 if dial == nil {
89 dial = (&net.Dialer{}).DialContext
90 }
91 conn, err := dial(ctx, "tcp", addr)
92 if err != nil {
93 return selfCheckFail(&res, networkReason(ctx, err))
94 }
95 defer conn.Close()
96 if deadline, ok := ctx.Deadline(); ok {
97 _ = conn.SetDeadline(deadline)
98 }
99 now := time.Now
100 if cfg.Now != nil {
101 now = cfg.Now
102 }
103
104 // Every step that can fail inside the handshake records its reason here before it refuses the connection: the standard library would otherwise report them all as one
105 // "certificate" error, and an operator needs to know which of the four it was.
106 var found string
107 verify := func(state tls.ConnectionState) error {
108 if len(state.PeerCertificates) == 0 {
109 found = selfCheckChain
110 return errors.New("no certificate")
111 }
112 leaf := state.PeerCertificates[0]
113 sum := sha256.Sum256(leaf.RawSubjectPublicKeyInfo)
114 spki := base64.RawURLEncoding.EncodeToString(sum[:])
115 notAfter := leaf.NotAfter.Unix()
116 res.SPKISHA256, res.NotAfter = &spki, &notAfter
117 version := tlsVersionText(state.Version)
118 res.TLSVersion = &version
119 if state.Version != tls.VersionTLS13 {
120 found = selfCheckTLSVersion
121 return errors.New("tls version")
122 }
123 intermediates := x509.NewCertPool()
124 for _, extra := range state.PeerCertificates[1:] {
125 intermediates.AddCert(extra)
126 }
127 if _, verr := leaf.Verify(x509.VerifyOptions{DNSName: host, Roots: cfg.Roots, Intermediates: intermediates, CurrentTime: now()}); verr != nil {
128 found = verifyReason(verr)
129 return verr
130 }
131 if len(expect) > 0 && !containsText(expect, spki) {
132 found = selfCheckSPKI
133 return errors.New("spki")
134 }
135 return nil
136 }
137 secure := tls.Client(conn, &tls.Config{
138 ServerName: host,
139 MinVersion: tls.VersionTLS12,
140 NextProtos: []string{"http/1.1"},
141 // The verification is VerifyConnection above, so that every failure has its own reason; nothing is accepted that it refuses.
142 InsecureSkipVerify: true,
143 VerifyConnection: verify,
144 })
145 if err := secure.HandshakeContext(ctx); err != nil {
146 if found != "" {
147 return selfCheckFail(&res, found)
148 }
149 if strings.Contains(err.Error(), "protocol version") {
150 return selfCheckFail(&res, selfCheckTLSVersion)
151 }
152 return selfCheckFail(&res, networkReason(ctx, err))
153 }
154
155 req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://"+host+"/healthz", nil)
156 if err != nil {
157 return selfCheckFail(&res, selfCheckHealthz)
158 }
159 req.Header.Set("User-Agent", "all-tunnel-selfcheck")
160 req.Close = true
161 if err := req.Write(secure); err != nil {
162 return selfCheckFail(&res, networkReason(ctx, err))
163 }
164 response, err := http.ReadResponse(bufio.NewReader(io.LimitReader(secure, 64<<10)), req)
165 if err != nil {
166 return selfCheckFail(&res, networkReasonOr(ctx, err, selfCheckHealthz))
167 }
168 defer response.Body.Close()
169 body, err := io.ReadAll(io.LimitReader(response.Body, 4<<10))
170 if err != nil {
171 return selfCheckFail(&res, networkReasonOr(ctx, err, selfCheckHealthz))
172 }
173 var health struct {
174 OK *bool `json:"ok"`
175 }
176 if response.StatusCode != http.StatusOK || json.Unmarshal(body, &health) != nil || health.OK == nil || !*health.OK {
177 return selfCheckFail(&res, selfCheckHealthz)
178 }
179 res.Healthz = true
180 res.OK = true
181 return res
182}
183
184func tlsVersionText(version uint16) string {
185 if version == tls.VersionTLS13 {
186 return "1.3"
187 }
188 return "1.2"
189}
190
191func containsText(list []string, text string) bool {
192 for _, item := range list {
193 if item == text {
194 return true
195 }
196 }
197 return false
198}
199
200// verifyReason is the first of the three things x509 checks that is wrong: the validity, the name, or the chain (an unknown authority, a bad signature, a certificate
201// that is not for a server, anything else).
202func verifyReason(err error) string {
203 var invalid x509.CertificateInvalidError
204 if errors.As(err, &invalid) && invalid.Reason == x509.Expired {
205 return selfCheckExpired
206 }
207 var wrongName x509.HostnameError
208 if errors.As(err, &wrongName) {
209 return selfCheckName
210 }
211 return selfCheckChain
212}
213
214func networkReason(ctx context.Context, err error) string {
215 return networkReasonOr(ctx, err, selfCheckUnreachable)
216}
217
218// networkReasonOr is `timeout` when the time ran out (the context, a deadline on the connection, a net.Error that says so) and `otherwise` for every other way a
219// connection can end.
220func networkReasonOr(ctx context.Context, err error, otherwise string) string {
221 var netErr net.Error
222 if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, os.ErrDeadlineExceeded) || ctx.Err() != nil || (errors.As(err, &netErr) && netErr.Timeout()) {
223 return selfCheckTimeout
224 }
225 return otherwise
226}
227
228// ---- the HTTP shape --------------------------------------------------------------------------------------------------------------------------
229
230// selfCheckRequest is {"v": 1, "label": "...", "expect": ["<spki>", ...]}; nothing else is accepted.
231type selfCheckRequest struct {
232 V int `json:"v"`
233 Label string `json:"label"`
234 Expect []string `json:"expect"`
235}
236
237const (
238 selfCheckBodyMax = 1024
239 selfCheckExpectMax = 4
240 selfCheckSPKILength = 43
241 selfCheckInFlight = 4
242)
243
244// NewSelfCheckHandler is POST {v, label, expect} -> selfcheck-result, for a port that is not public (the operator's loopback listener). `lookup` says which host a label
245// stands for on this node and whether its computer is connected (an unknown or offline label is an `unreachable` answer, never an error that tells which); `labelOK`
246// is the label pattern of the policy. A label is checked at most once per `minInterval` and at most selfCheckInFlight checks run at once: a loop of requests is a
247// 429, never a flood of dials.
248func NewSelfCheckHandler(cfg SelfCheckConfig, addr string, lookup func(label string) (host string, ok bool), labelOK func(string) bool, minInterval time.Duration) http.Handler {
249 var mu sync.Mutex
250 last := map[string]time.Time{}
251 slots := make(chan struct{}, selfCheckInFlight)
252 reply := func(w http.ResponseWriter, status int, value any) {
253 w.Header().Set("Content-Type", "application/json")
254 w.Header().Set("Cache-Control", "no-store")
255 w.Header().Set("X-Content-Type-Options", "nosniff")
256 w.WriteHeader(status)
257 _ = json.NewEncoder(w).Encode(value)
258 }
259 refuse := func(w http.ResponseWriter, status int, word string) {
260 reply(w, status, map[string]any{"ok": false, "error": word})
261 }
262 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
263 if r.Method != http.MethodPost {
264 refuse(w, http.StatusMethodNotAllowed, "invalid_request")
265 return
266 }
267 decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, selfCheckBodyMax))
268 decoder.DisallowUnknownFields()
269 var asked selfCheckRequest
270 if err := decoder.Decode(&asked); err != nil || decoder.More() || asked.V != 1 || !labelOK(asked.Label) || len(asked.Expect) > selfCheckExpectMax {
271 refuse(w, http.StatusBadRequest, "invalid_request")
272 return
273 }
274 for _, spki := range asked.Expect {
275 if len(spki) != selfCheckSPKILength {
276 refuse(w, http.StatusBadRequest, "invalid_request")
277 return
278 }
279 }
280 mu.Lock()
281 if at, seen := last[asked.Label]; seen && time.Since(at) < minInterval {
282 mu.Unlock()
283 refuse(w, http.StatusTooManyRequests, "busy")
284 return
285 }
286 select {
287 case slots <- struct{}{}:
288 last[asked.Label] = time.Now()
289 if len(last) > 4096 {
290 for label, at := range last {
291 if time.Since(at) >= minInterval {
292 delete(last, label)
293 }
294 }
295 }
296 mu.Unlock()
297 default:
298 mu.Unlock()
299 refuse(w, http.StatusTooManyRequests, "busy")
300 return
301 }
302 defer func() { <-slots }()
303 host, online := lookup(asked.Label)
304 if !online {
305 reason := selfCheckUnreachable
306 reply(w, http.StatusOK, SelfCheckResult{V: 1, Reason: &reason})
307 return
308 }
309 reply(w, http.StatusOK, SelfCheck(r.Context(), cfg, addr, host, asked.Expect))
310 })
311}