ALL

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

tunnel/server_test.go

SHA-256: 80fb7d46e02002e1a00d6cf82c958517b42f8779da11b37e2a2af46bc2be37e2
1450 lines · 57033 bytes · View on GitHub

1package main
2
3import (
4 "bufio"
5 "bytes"
6 "crypto/ed25519"
7 "crypto/rand"
8 "crypto/sha256"
9 "crypto/tls"
10 "encoding/hex"
11 "fmt"
12 "io"
13 "net"
14 "net/netip"
15 "os"
16 "path/filepath"
17 "reflect"
18 "regexp"
19 "strings"
20 "sync"
21 "sync/atomic"
22 "testing"
23 "time"
24)
25
26// ------------------------------------------------------------------------------------------------------------------------------------
27// The handshake and the label binding
28// ------------------------------------------------------------------------------------------------------------------------------------
29
30func TestHello_OKRelaysWhatTheControlPlaneDecided(t *testing.T) {
31 e := newTestEnv(t, nil)
32 // valid_until is relayed exactly as the control plane gave it, never trimmed by the node: paid 14400 s, running trial 900 s, gated 900 s
33 for name, tc := range map[string]struct {
34 validFor time.Duration
35 status string
36 plan string
37 }{
38 "paid": {secs(ctPolInt("authorization.valid_until_paid_s")), statusActive, "paid"},
39 "trial": {secs(ctPolInt("authorization.valid_until_trial_s")), statusActive, "trial"},
40 "gated": {secs(ctPolInt("authorization.valid_until_gated_s")), statusGated, "trial"},
41 "a trial that ends in 90 seconds": {90 * time.Second, statusActive, "trial"},
42 } {
43 d := e.newDevice()
44 *e.plan(d) = devicePlan{host: d.host, validFor: tc.validFor, status: tc.status, plan: tc.plan}
45 before := e.now().Unix()
46 dc := e.connectOK(d)
47 after := e.now().Unix()
48 body := dc.ok["body"].(map[string]any)
49 if body["host"] != d.host || body["node"] != testNodeID || body["status"] != tc.status {
50 t.Fatalf("%s: OK body %v", name, body)
51 }
52 until := int64(body["valid_until"].(float64))
53 if until < before+int64(tc.validFor/time.Second) || until > after+int64(tc.validFor/time.Second) {
54 t.Fatalf("%s: valid_until %d, want about %d", name, until, before+int64(tc.validFor/time.Second))
55 }
56 calls := e.cp.callLog()
57 last := calls[len(calls)-1]
58 if last.DeviceID != d.id || last.Reason != "hello" || last.Node != testNodeID {
59 t.Fatalf("%s: authorize call %+v", name, last)
60 }
61 // the windows are the policy's own
62 pol := body["policy"].(map[string]any)
63 if pol["dead_peer_s"] != float64(ctPolInt("timing.dead_peer_s")) || pol["renew_before_valid_until_s"] != float64(ctPolInt("timing.renew_before_valid_until_s")) {
64 t.Fatalf("%s: policy block %v", name, pol)
65 }
66 }
67 e.checkTranscript()
68}
69
70func TestLabelBinding_ASessionOfOneDeviceNeverBringsUpTheHostOfAnother(t *testing.T) {
71 e := newTestEnv(t, nil)
72 a, b := e.newDevice(), e.newDevice()
73 sessionA := e.session(a, nil)
74
75 attempts := map[string]string{
76 "host as a third field": "HELLO 2 " + sessionA + " " + b.host,
77 "host in the version slot": "HELLO " + b.host + " " + sessionA,
78 "host before the session": "HELLO 2 " + b.host + " " + sessionA,
79 "label as an extra field": "HELLO 2 " + sessionA + " " + b.label,
80 "a host command": "HOST " + b.host,
81 "a session for another key": "HELLO 2 " + e.sessionSignedBy(b.key, b, nil),
82 }
83 for name, line := range attempts {
84 dc := e.connect(a)
85 dc.send(line)
86 reply := dc.recv()
87 if !strings.HasPrefix(reply, "ERR ") {
88 t.Fatalf("%s: answered %q", name, reply)
89 }
90 dc.expectClosed(2 * time.Second)
91 }
92 // a proof that names B, or is signed by B's key, for A's session
93 for name, mutate := range map[string]func(h, c map[string]any){
94 "proof of B's subject": func(h, c map[string]any) { c["sub"] = b.id },
95 "proof with B's key": func(h, c map[string]any) {
96 h["jwk"] = map[string]any{"kty": "OKP", "crv": "Ed25519", "x": b64Encode(b.key.Public().(ed25519.PublicKey))}
97 },
98 "proof with a host claim": func(h, c map[string]any) { c["host"] = b.host; c["sub"] = b.id },
99 } {
100 dc := e.connect(a)
101 reply := dc.hello(func() string { return sessionA }, func(s, n string) string { return e.proof(a, s, n, mutate) })
102 if !strings.HasPrefix(reply, "ERR ") {
103 t.Fatalf("%s: answered %q", name, reply)
104 }
105 dc.expectClosed(2 * time.Second)
106 }
107 if n := len(e.cp.callLog()); n != 0 {
108 t.Fatalf("the control plane was asked %d times about a session that never proved itself", n)
109 }
110 if e.srv.Online(b.host) || e.srv.Online(a.host) {
111 t.Fatal("a host is up although no device authenticated")
112 }
113
114 // A, honestly: a valid session that carries extra claims naming B's host still binds only A's own host
115 dc := e.connect(a)
116 reply := dc.hello(
117 func() string {
118 return e.session(a, func(h, c map[string]any) { c["host"] = b.host; c["label"] = b.label; c["hostname"] = b.host })
119 },
120 func(s, n string) string { return e.proof(a, s, n, nil) })
121 if m, err := parseLine(reply, dirN2D); err != nil || m.str("verb") != "OK" || m["body"].(map[string]any)["host"] != a.host {
122 t.Fatalf("A's handshake: %q", reply)
123 }
124 dsA := dc.serve(nil)
125 if !e.srv.Online(a.host) || e.srv.Online(b.host) {
126 t.Fatal("the node serves the wrong host")
127 }
128 // a client of B's host while only A is connected: refused with an alert, A is never asked to open anything
129 if _, err := e.clientTLS(b); err == nil {
130 t.Fatal("a client of B's host got a TLS session")
131 }
132 time.Sleep(100 * time.Millisecond)
133 if dsA.openCount() != 0 {
134 t.Fatal("A was asked to open a stream for B's host")
135 }
136 c, err := e.clientTLS(a)
137 if err != nil {
138 t.Fatal(err)
139 }
140 if resp, err := postOver(c, a.host, []byte("hi")); err != nil || string(resp) != "echo:hi" {
141 t.Fatalf("A's own host: %q %v", resp, err)
142 }
143 _ = c.Close()
144 e.checkTranscript()
145}
146
147func TestLabelBinding_AHostIsNeverGivenToTwoDevices(t *testing.T) {
148 e := newTestEnv(t, nil)
149 a, b := e.newDevice(), e.newDevice()
150 // a control plane that (wrongly) answers the same host for two devices: the first holds it, the second is refused
151 e.plan(b).host = a.host
152 dcA := e.connectOK(a)
153 dsA := dcA.serve(nil)
154
155 dcB := e.connect(b)
156 reply := dcB.hello(func() string { return e.session(b, nil) }, func(s, n string) string { return e.proof(b, s, n, nil) })
157 if reply != "ERR auth" {
158 t.Fatalf("second device for the same host: %q", reply)
159 }
160 dcB.expectClosed(2 * time.Second)
161
162 // traffic for the host still reaches A
163 c, err := e.clientTLS(a)
164 if err != nil {
165 t.Fatal(err)
166 }
167 if resp, err := postOver(c, a.host, []byte("still-a")); err != nil || string(resp) != "echo:still-a" {
168 t.Fatalf("%q %v", resp, err)
169 }
170 _ = c.Close()
171 if dsA.openCount() != 1 {
172 t.Fatalf("A got %d opens", dsA.openCount())
173 }
174 e.checkTranscript()
175}
176
177func TestLabelBinding_ARenewThatChangesTheHostClosesTheConnection(t *testing.T) {
178 e := newTestEnv(t, nil)
179 d := e.newDevice()
180 dc := e.connectOK(d)
181 // the control plane now says this device owns another host (its label was retired): the old host must stop being served at once
182 e.plan(d).host = label20() + "." + testZone
183 reply := dc.renew()
184 _ = reply
185 lines := append([]string{reply}, dc.expectClosed(2*time.Second)...)
186 if !containsLine(lines, "CLOSE revoked") {
187 t.Fatalf("lines after the renew: %q", lines)
188 }
189 eventually(t, "old host unbound", 2*time.Second, func() bool { return !e.srv.Online(d.host) })
190}
191
192func containsLine(lines []string, want string) bool {
193 for _, l := range lines {
194 if l == want {
195 return true
196 }
197 }
198 return false
199}
200
201// ------------------------------------------------------------------------------------------------------------------------------------
202// Proofs, sessions and what is refused
203// ------------------------------------------------------------------------------------------------------------------------------------
204
205func TestAuth_ExpiredWrongAudienceWrongKidReplayedAndWrongHTUAreRefused(t *testing.T) {
206 e := newTestEnv(t, nil)
207 d := e.newDevice()
208 _, rogue, _ := ed25519.GenerateKey(rand.Reader)
209 other := e.newDevice()
210 otherSession := e.session(other, nil)
211
212 type tc struct {
213 name string
214 session func() string
215 proof func(session, nonce string) string
216 want string // the ERR line
217 }
218 goodSession := func() string { return e.session(d, nil) }
219 goodProof := func(s, n string) string { return e.proof(d, s, n, nil) }
220 proofWith := func(mutate func(h, c map[string]any)) func(s, n string) string {
221 return func(s, n string) string { return e.proof(d, s, n, mutate) }
222 }
223 sessionWith := func(mutate func(h, c map[string]any)) func() string {
224 return func() string { return e.session(d, mutate) }
225 }
226 cases := []tc{
227 {"expired session", sessionWith(func(h, c map[string]any) { c["iat"], c["exp"] = e.now().Unix()-400, e.now().Unix()-100 }), goodProof, "ERR auth"},
228 {"wrong lifetime", sessionWith(func(h, c map[string]any) { c["exp"] = c["iat"].(int64) + 3000 }), goodProof, "ERR auth"},
229 {"audience all-relay", sessionWith(func(h, c map[string]any) { c["aud"] = "all-relay" }), goodProof, "ERR auth"},
230 {"audience all-control", sessionWith(func(h, c map[string]any) { c["aud"] = "all-control" }), goodProof, "ERR auth"},
231 {"no audience", sessionWith(func(h, c map[string]any) { delete(c, "aud") }), goodProof, "ERR auth"},
232 {"wrong kid", sessionWith(func(h, c map[string]any) { h["kid"] = "cp-unknown" }), goodProof, "ERR auth"},
233 {"signed by a forger", func() string { return e.sessionSignedBy(rogue, d, nil) }, goodProof, "ERR auth"},
234 {"alg none", func() string {
235 s := e.session(d, nil)
236 parts := strings.Split(s, ".")
237 return b64Encode([]byte(`{"alg":"none","typ":"device-session","kid":"`+testKID+`"}`)) + "." + parts[1] + "."
238 }, goodProof, "ERR bad_hello"}, // no signature segment: not even the shape of a token
239 {"alg none with a signature part", func() string {
240 s := e.session(d, nil)
241 parts := strings.Split(s, ".")
242 return b64Encode([]byte(`{"alg":"none","typ":"device-session","kid":"`+testKID+`"}`)) + "." + parts[1] + "." + parts[2]
243 }, goodProof, "ERR auth"},
244 {"alg HS256", sessionWith(func(h, c map[string]any) { h["alg"] = "HS256" }), goodProof, "ERR auth"},
245 {"wrong issuer", sessionWith(func(h, c map[string]any) { c["iss"] = "https://evil.example" }), goodProof, "ERR auth"},
246 {"ver 1", sessionWith(func(h, c map[string]any) { c["ver"] = 1 }), goodProof, "ERR auth"},
247 {"a clock far ahead", sessionWith(func(h, c map[string]any) { c["iat"], c["exp"] = e.now().Unix()+120, e.now().Unix()+420 }), goodProof, "ERR clock_skew"},
248
249 {"proof for another node's URL", goodSession, proofWith(func(h, c map[string]any) {
250 c["htu"] = "https://t2.nodes.all-e2e.test" + policyData.Protocol.ProofHTUPath
251 }), "ERR bad_proof"},
252 {"proof with a port in the URL", goodSession, proofWith(func(h, c map[string]any) {
253 c["htu"] = "https://" + testNodeHost + ":8443" + policyData.Protocol.ProofHTUPath
254 }), "ERR bad_proof"},
255 {"proof for the node's host and another path", goodSession, proofWith(func(h, c map[string]any) { c["htu"] = "https://" + testNodeHost + "/" }), "ERR bad_proof"},
256 {"proof with another nonce", goodSession, proofWith(func(h, c map[string]any) { c["nonce"] = "AAAAAAAAAAAAAAAAAAAAAA" }), "ERR bad_proof"},
257 {"proof for another session", goodSession, proofWith(func(h, c map[string]any) { c["ath"] = sha256B64([]byte(otherSession)) }), "ERR bad_proof"},
258 {"stale proof", goodSession, proofWith(func(h, c map[string]any) { c["iat"] = e.now().Unix() - 200 }), "ERR clock_skew"},
259 {"proof from the future", goodSession, proofWith(func(h, c map[string]any) { c["iat"] = e.now().Unix() + 200 }), "ERR clock_skew"},
260 {"proof with POST", goodSession, proofWith(func(h, c map[string]any) { c["htm"] = "POST" }), "ERR bad_proof"},
261 {"proof for the relay audience", goodSession, proofWith(func(h, c map[string]any) { c["aud"] = "all-relay" }), "ERR auth"},
262 {"proof of another subject", goodSession, proofWith(func(h, c map[string]any) { c["sub"] = other.id }), "ERR bad_proof"},
263 {"proof signed by another key", goodSession, func(s, n string) string { return e.proof(other, s, n, func(h, c map[string]any) { c["sub"] = d.id }) }, "ERR bad_proof"},
264 {"proof with a private part in the jwk", goodSession, proofWith(func(h, c map[string]any) { h["jwk"].(map[string]any)["d"] = "AAAA" }), "ERR bad_hello"},
265 {"proof of the wrong type", goodSession, proofWith(func(h, c map[string]any) { h["typ"] = "JWT" }), "ERR bad_proof"},
266 {"proof without a jti", goodSession, proofWith(func(h, c map[string]any) { delete(c, "jti") }), "ERR bad_proof"},
267 {"proof that is not a token", goodSession, func(s, n string) string { return "AAAA.BBBB.CCCC" }, "ERR bad_hello"},
268 }
269 for _, c := range cases {
270 dc := e.connect(d)
271 reply := dc.hello(c.session, c.proof)
272 if reply != c.want {
273 t.Errorf("%s: answered %q, want %q", c.name, reply, c.want)
274 }
275 dc.expectClosed(2 * time.Second)
276 if e.srv.Online(d.host) {
277 t.Fatalf("%s: the host is up", c.name)
278 }
279 }
280 if n := len(e.cp.callLog()); n != 0 {
281 t.Fatalf("authorize was called %d times for refused sessions and proofs", n)
282 }
283
284 // replay: the very proof jti of an accepted handshake is refused on another connection, with a fresh nonce
285 var spent string
286 dc := e.connect(d)
287 reply := dc.hello(goodSession, func(s, n string) string {
288 p := e.proof(d, s, n, nil)
289 spent = p
290 return p
291 })
292 if !strings.HasPrefix(reply, "OK ") {
293 t.Fatalf("baseline handshake: %q", reply)
294 }
295 _ = dc.c.Close()
296 jti := jtiOf(t, spent)
297 dc = e.connect(d)
298 reply = dc.hello(goodSession, func(s, n string) string { return e.proof(d, s, n, func(h, c map[string]any) { c["jti"] = jti }) })
299 if reply != "ERR bad_proof" {
300 t.Fatalf("replayed proof jti: %q", reply)
301 }
302 // the same proof text on a connection whose nonce differs fails on the nonce, and a session jti is NOT single use: a cached session reconnects
303 session := e.session(d, nil)
304 for i := 0; i < 2; i++ {
305 dc = e.connect(d)
306 reply = dc.hello(func() string { return session }, goodProof)
307 if !strings.HasPrefix(reply, "OK ") {
308 t.Fatalf("a cached session must reconnect (round %d): %q", i, reply)
309 }
310 _ = dc.c.Close()
311 }
312 e.checkTranscript()
313}
314
315func jtiOf(t *testing.T, token string) string {
316 t.Helper()
317 j, err := splitJWS(token, 1<<16)
318 if err != nil {
319 t.Fatal(err)
320 }
321 return j.claims["jti"].(string)
322}
323
324func TestAuth_ClockSkewWindowIsConfigurable(t *testing.T) {
325 ahead := func(e *testEnv, d *testDevice, by int64) string {
326 dc := e.connect(d)
327 return dc.hello(
328 func() string {
329 return e.session(d, func(h, c map[string]any) { c["iat"], c["exp"] = e.now().Unix()+by, e.now().Unix()+by+300 })
330 },
331 func(s, n string) string {
332 return e.proof(d, s, n, func(h, c map[string]any) { c["iat"] = e.now().Unix() + by })
333 })
334 }
335 e := newTestEnv(t, nil) // the policy's own window: a few seconds
336 d := e.newDevice()
337 if r := ahead(e, d, 3); !strings.HasPrefix(r, "OK ") {
338 t.Fatalf("a device clock 3 s fast: %q", r)
339 }
340 if r := ahead(e, d, 30); r != "ERR clock_skew" {
341 t.Fatalf("a device clock 30 s fast: %q", r)
342 }
343 // a device whose clock is slow still authenticates while its tokens are fresh
344 dc := e.connect(d)
345 r := dc.hello(
346 func() string {
347 return e.session(d, func(h, c map[string]any) { c["iat"], c["exp"] = e.now().Unix()-20, e.now().Unix()+280 })
348 },
349 func(s, n string) string {
350 return e.proof(d, s, n, func(h, c map[string]any) { c["iat"] = e.now().Unix() - 20 })
351 })
352 if !strings.HasPrefix(r, "OK ") {
353 t.Fatalf("a device clock 20 s slow: %q", r)
354 }
355
356 wide := newTestEnv(t, func(c *Config) { c.ClockSkew = 20 * time.Second })
357 dw := wide.newDevice()
358 if r := ahead(wide, dw, 15); !strings.HasPrefix(r, "OK ") {
359 t.Fatalf("a device clock 15 s fast inside a 20 s window: %q", r)
360 }
361 if r := ahead(wide, dw, 25); r != "ERR clock_skew" {
362 t.Fatalf("a device clock 25 s fast outside a 20 s window: %q", r)
363 }
364
365 // the window cannot be opened beyond the policy's maximum
366 cfg := DefaultConfig()
367 cfg.NodeID, cfg.NodeHost, cfg.Keys, cfg.Control = testNodeID, testNodeHost, e.keys, e.cp
368 cfg.CertSource = func(*tls.ClientHelloInfo) (*tls.Certificate, error) { return nil, nil }
369 cfg.ClockSkew = secs(policyData.Authorization.NodeSkewMaxS) + time.Second
370 if _, err := NewServer(cfg); err == nil {
371 t.Fatal("a skew window beyond the policy's maximum was accepted")
372 }
373}
374
375func TestAuth_ControlPlaneRefusalsBecomeClosedWords(t *testing.T) {
376 e := newTestEnv(t, func(c *Config) { c.AuthorizeTimeout = 400 * time.Millisecond })
377 d := e.newDevice()
378 label := label20()
379 for name, tc := range map[string]struct {
380 plan devicePlan
381 want string
382 }{
383 "license ended": {devicePlan{err: &ControlError{Status: 402, Code: "license_inactive"}}, "ERR license_inactive"},
384 "device revoked": {devicePlan{err: &ControlError{Status: 401, Code: "device_revoked"}}, "ERR device_revoked"},
385 "unknown 4xx": {devicePlan{err: &ControlError{Status: 404, Code: "not_enrolled"}}, "ERR auth"},
386 "control plane 503": {devicePlan{err: &ControlError{Status: 503, Code: "subscription_verification_unavailable"}}, "ERR control_unavailable"},
387 "too many 429": {devicePlan{err: &ControlError{Status: 429, Code: "busy"}}, "ERR control_unavailable"},
388 "transport failure": {devicePlan{err: fmt.Errorf("connection refused")}, "ERR control_unavailable"},
389 "too slow": {devicePlan{delay: 3 * time.Second, host: d.host, validFor: time.Hour}, "ERR control_unavailable"},
390 "grant already over": {devicePlan{host: d.host, validFor: -5 * time.Second}, "ERR auth"},
391 "answer for another device": {devicePlan{answer: func(int) AuthorizeResponse {
392 return AuthorizeResponse{DeviceID: randomHex(16), Label: label, Host: label + "." + testZone, Status: "active", Plan: "paid", ValidUntil: e.now().Unix() + 600,
393 ServerTime: e.now().Unix(), Policy: okPolicy{20, 45, 1800, 14400, 300}}
394 }}, "ERR control_unavailable"},
395 "answer with a host that is not a device host": {devicePlan{answer: func(int) AuthorizeResponse {
396 return AuthorizeResponse{DeviceID: d.id, Label: "evil", Host: "evil.example.com", Status: "active", Plan: "paid", ValidUntil: e.now().Unix() + 600,
397 ServerTime: e.now().Unix(), Policy: okPolicy{20, 45, 1800, 14400, 300}}
398 }}, "ERR control_unavailable"},
399 } {
400 plan := tc.plan
401 if plan.host == "" && plan.err == nil && plan.answer == nil {
402 plan.host = d.host
403 }
404 e.cp.set(d.id, &plan)
405 dc := e.connect(d)
406 reply := dc.hello(func() string { return e.session(d, nil) }, func(s, n string) string { return e.proof(d, s, n, nil) })
407 if reply != tc.want {
408 t.Errorf("%s: %q, want %q", name, reply, tc.want)
409 }
410 dc.expectClosed(2 * time.Second)
411 if e.srv.Online(d.host) {
412 t.Fatalf("%s: the host is up", name)
413 }
414 }
415 e.checkTranscript()
416}
417
418// ------------------------------------------------------------------------------------------------------------------------------------
419// The lifetime: valid_until, RENEW, replacement
420// ------------------------------------------------------------------------------------------------------------------------------------
421
422func TestLifetime_WithoutRenewalTheConnectionClosesAtValidUntil(t *testing.T) {
423 e := newTestEnv(t, nil)
424 d := e.newDevice()
425 e.plan(d).validFor = 2 * time.Second
426 dc := e.connectOK(d)
427 ds := dc.serve(nil)
428 until := int64(dc.ok["body"].(map[string]any)["valid_until"].(float64))
429
430 c, err := e.clientTLS(d)
431 if err != nil {
432 t.Fatal(err)
433 }
434 if resp, err := postOver(c, d.host, []byte("before")); err != nil || string(resp) != "echo:before" {
435 t.Fatalf("%q %v", resp, err)
436 }
437 _ = c.Close()
438
439 eventually(t, "CLOSE expired", 4*time.Second, func() bool { return containsLine(ds.otherLines(), "CLOSE expired") })
440 if now := e.now().Unix(); now < until {
441 t.Fatalf("closed at %d, before valid_until %d", now, until)
442 }
443 <-ds.done
444 if e.srv.Online(d.host) {
445 t.Fatal("the host is still up after valid_until")
446 }
447 if _, err := e.clientTLS(d); err == nil {
448 t.Fatal("a client got through after valid_until")
449 }
450 e.checkTranscript()
451}
452
453func TestLifetime_RenewExtendsTheConnectionAndCallsAuthorizeAgain(t *testing.T) {
454 e := newTestEnv(t, nil)
455 d := e.newDevice()
456 e.plan(d).validFor = 2 * time.Second
457 dc := e.connectOK(d)
458 firstUntil := int64(dc.ok["body"].(map[string]any)["valid_until"].(float64))
459
460 // the control plane's answer for the renewal is longer, and the trial turned into the paywall: gated
461 e.plan(d).validFor, e.plan(d).status, e.plan(d).plan = 5*time.Second, statusGated, "trial"
462 reply := dc.renew()
463 m, err := parseLine(reply, dirN2D)
464 if err != nil || m.str("verb") != "OK" {
465 t.Fatalf("RENEW answered %q", reply)
466 }
467 body := m["body"].(map[string]any)
468 if int64(body["valid_until"].(float64)) <= firstUntil || body["status"] != "gated" {
469 t.Fatalf("renewed body %v (first valid_until %d)", body, firstUntil)
470 }
471 // the renewal called authorize again, with its own session's jti and the reason renew
472 if e.cp.callsFor(d.id, "hello") != 1 || e.cp.callsFor(d.id, "renew") != 1 {
473 t.Fatalf("authorize calls: %+v", e.cp.callLog())
474 }
475 if last := e.cp.callLog()[1]; last.SessionJTI != jtiOf(t, dc.session) || last.Reason != "renew" {
476 t.Fatalf("the renew call %+v", last)
477 }
478
479 // past the FIRST valid_until the connection is still there and serves (gated still opens streams)
480 ds := dc.serve(nil)
481 time.Sleep(time.Until(time.Unix(firstUntil, 0)) + 500*time.Millisecond)
482 if !e.srv.Online(d.host) {
483 t.Fatal("the renewed connection ended at the old valid_until")
484 }
485 c, err := e.clientTLS(d)
486 if err != nil {
487 t.Fatalf("a client after the old valid_until: %v", err)
488 }
489 if resp, err := postOver(c, d.host, []byte("renewed")); err != nil || string(resp) != "echo:renewed" {
490 t.Fatalf("%q %v", resp, err)
491 }
492 _ = c.Close()
493 // and without a second renewal it ends at the NEW valid_until
494 eventually(t, "CLOSE expired after the renewal", 8*time.Second, func() bool { return containsLine(ds.otherLines(), "CLOSE expired") })
495 if now := e.now().Unix(); now < int64(body["valid_until"].(float64)) {
496 t.Fatalf("closed at %d, before the renewed valid_until %v", now, body["valid_until"])
497 }
498 e.checkTranscript()
499}
500
501func TestLifetime_ARefusedRenewKeepsTheConnectionAndItsOldValidUntil(t *testing.T) {
502 e := newTestEnv(t, nil)
503 d := e.newDevice()
504 other := e.newDevice()
505 e.plan(d).validFor = 3 * time.Second
506 dc := e.connectOK(d)
507 ds := dc.serve(nil)
508 _ = ds
509
510 // the control plane refuses the renewal
511 e.plan(d).err = &ControlError{Status: 402, Code: "license_inactive"}
512 if r := dc.renew(); r != "ERR license_inactive" {
513 t.Fatalf("refused RENEW answered %q", r)
514 }
515 e.plan(d).err = nil
516 // a RENEW with the session of another device
517 dc.send("RENEW " + e.session(other, nil))
518 if r := dc.recv(); r != "ERR auth" {
519 t.Fatalf("RENEW of another subject: %q", r)
520 }
521 // a RENEW whose proof is bad
522 dc.send("RENEW " + e.session(d, nil))
523 if line := dc.recv(); !strings.HasPrefix(line, "CHALLENGE ") {
524 t.Fatalf("%q", line)
525 }
526 dc.send("PROOF AAAA.BBBB.CCCC")
527 if r := dc.recv(); !strings.HasPrefix(r, "ERR ") {
528 t.Fatalf("%q", r)
529 }
530 // a session that is expired
531 dc.send("RENEW " + e.session(d, func(h, c map[string]any) { c["iat"], c["exp"] = e.now().Unix()-400, e.now().Unix()-100 }))
532 if r := dc.recv(); r != "ERR auth" {
533 t.Fatalf("%q", r)
534 }
535 // the connection is alive and answers PING: nothing above extended or ended it
536 dc.send("PING")
537 deadline := time.Now().Add(2 * time.Second)
538 for !containsLine(ds.otherLines(), "PONG") && time.Now().Before(deadline) {
539 time.Sleep(20 * time.Millisecond)
540 }
541 if !e.srv.Online(d.host) {
542 t.Fatal("a refused RENEW ended the connection")
543 }
544 eventually(t, "CLOSE expired at the OLD valid_until", 5*time.Second, func() bool { return containsLine(ds.otherLines(), "CLOSE expired") })
545 e.checkTranscript()
546}
547
548func TestLifetime_ARenewThatNeverGetsItsProofIsDroppedNotTheConnection(t *testing.T) {
549 e := newTestEnv(t, func(c *Config) { c.FirstLineTimeout = 400 * time.Millisecond })
550 d := e.newDevice()
551 dc := e.connectOK(d)
552 dc.send("RENEW " + e.session(d, nil))
553 if line := dc.recv(); !strings.HasPrefix(line, "CHALLENGE ") {
554 t.Fatalf("%q", line)
555 }
556 time.Sleep(900 * time.Millisecond)
557 // the abandoned exchange's PROOF finds no nonce; a fresh RENEW works
558 dc.send("PING")
559 if line := dc.recv(); line != "PONG" {
560 t.Fatalf("%q", line)
561 }
562 if r := dc.renew(); !strings.HasPrefix(r, "OK ") {
563 t.Fatalf("a new RENEW after the abandoned one: %q", r)
564 }
565}
566
567func TestLifetime_ASecondHelloOfTheSameSubjectReplacesTheFirst(t *testing.T) {
568 e := newTestEnv(t, nil)
569 d := e.newDevice()
570 first := e.connectOK(d)
571 dsFirst := first.serve(nil)
572
573 // a stream that is open on the first connection when it is replaced keeps running
574 c, err := e.clientTLS(d)
575 if err != nil {
576 t.Fatal(err)
577 }
578 if resp, err := postOver(c, d.host, []byte("one")); err != nil || string(resp) != "echo:one" {
579 t.Fatalf("%q %v", resp, err)
580 }
581 if dsFirst.openCount() != 1 {
582 t.Fatalf("first opens %d", dsFirst.openCount())
583 }
584
585 second := e.connectOK(d)
586 dsSecond := second.serve(nil)
587 eventually(t, "CLOSE replaced on the first connection", 3*time.Second, func() bool { return containsLine(dsFirst.otherLines(), "CLOSE replaced") })
588 <-dsFirst.done
589
590 if resp, err := postOver(c, d.host, []byte("two")); err != nil || string(resp) != "echo:two" {
591 t.Fatalf("the open stream after the replacement: %q %v", resp, err)
592 }
593 _ = c.Close()
594 c2, err := e.clientTLS(d)
595 if err != nil {
596 t.Fatal(err)
597 }
598 if resp, err := postOver(c2, d.host, []byte("three")); err != nil || string(resp) != "echo:three" {
599 t.Fatalf("%q %v", resp, err)
600 }
601 _ = c2.Close()
602 if dsSecond.openCount() != 1 || dsFirst.openCount() != 1 {
603 t.Fatalf("opens: first %d second %d", dsFirst.openCount(), dsSecond.openCount())
604 }
605 e.checkTranscript()
606}
607
608func TestLifetime_ManyConcurrentHellosOfOneSubjectLeaveExactlyOneConnection(t *testing.T) {
609 e := newTestEnv(t, nil)
610 d := e.newDevice()
611 const n = 24
612 var wg sync.WaitGroup
613 conns := make([]*devConn, n)
614 var failures atomic.Int32
615 for i := 0; i < n; i++ {
616 c, err := e.dialNode()
617 if err != nil {
618 t.Fatal(err)
619 }
620 conns[i] = &devConn{t: t, e: e, d: d, c: c, br: bufio.NewReader(c)}
621 t.Cleanup(func() { _ = c.Close() })
622 }
623 for i := 0; i < n; i++ {
624 wg.Add(1)
625 go func(dc *devConn) {
626 defer wg.Done()
627 dc.session = e.session(d, nil)
628 fmt.Fprintf(dc.c, "HELLO 2 %s\n", dc.session)
629 line, err := dc.br.ReadString('\n')
630 if err != nil || !strings.HasPrefix(line, "CHALLENGE ") {
631 t.Logf("challenge: %q %v", line, err)
632 failures.Add(1)
633 return
634 }
635 fmt.Fprintf(dc.c, "PROOF %s\n", e.proof(d, dc.session, strings.TrimSpace(strings.TrimPrefix(line, "CHALLENGE ")), nil))
636 if line, err = dc.br.ReadString('\n'); err != nil || !strings.HasPrefix(line, "OK ") {
637 t.Logf("ok: %q %v", line, err)
638 failures.Add(1)
639 }
640 }(conns[i])
641 }
642 wg.Wait()
643 if failures.Load() != 0 {
644 t.Fatalf("%d handshakes failed", failures.Load())
645 }
646 // exactly one connection is left open; every other one was told it was replaced
647 open := 0
648 for _, dc := range conns {
649 _ = dc.c.SetReadDeadline(time.Now().Add(1500 * time.Millisecond))
650 line, err := dc.br.ReadString('\n')
651 if err == nil && line == "CLOSE replaced\n" {
652 continue
653 }
654 open++
655 }
656 if open != 1 {
657 t.Fatalf("%d connections left open", open)
658 }
659 if !e.srv.Online(d.host) {
660 t.Fatal("the host is not served by the survivor")
661 }
662 e.srv.routes.mu.Lock()
663 if len(e.srv.routes.bySub) != 1 || len(e.srv.routes.byHost) != 1 {
664 t.Errorf("the table holds %d subjects and %d hosts", len(e.srv.routes.bySub), len(e.srv.routes.byHost))
665 }
666 e.srv.routes.mu.Unlock()
667 e.checkTranscript()
668}
669
670func TestLifetime_APausedDeviceKeepsItsConnectionButOpensNothing(t *testing.T) {
671 e := newTestEnv(t, nil)
672 d := e.newDevice()
673 e.plan(d).status = statusPaused
674 dc := e.connectOK(d)
675 ds := dc.serve(nil)
676 if dc.ok["body"].(map[string]any)["status"] != "paused" {
677 t.Fatalf("%v", dc.ok)
678 }
679 if _, err := e.clientTLS(d); err == nil {
680 t.Fatal("a client got a stream of a paused device")
681 }
682 time.Sleep(100 * time.Millisecond)
683 if ds.openCount() != 0 {
684 t.Fatal("a paused device was asked to open a stream")
685 }
686 // resumed: the renewal says active and streams flow again, on the same connection
687 e.plan(d).status = statusActive
688 r := dc.renew()
689 if !strings.HasPrefix(r, "OK ") {
690 t.Fatalf("%q", r)
691 }
692 c, err := e.clientTLS(d)
693 if err != nil {
694 t.Fatal(err)
695 }
696 _ = c.Close()
697 e.checkTranscript()
698}
699
700func TestLifetime_ADeadPeerIsDroppedAndAPingKeepsItAlive(t *testing.T) {
701 e := newTestEnv(t, func(c *Config) { c.DeadPeer = 600 * time.Millisecond })
702 d := e.newDevice()
703 dc := e.connectOK(d)
704 for i := 0; i < 4; i++ {
705 time.Sleep(250 * time.Millisecond)
706 dc.send("PING")
707 if line := dc.recv(); line != "PONG" {
708 t.Fatalf("%q", line)
709 }
710 }
711 if !e.srv.Online(d.host) {
712 t.Fatal("a pinging device was dropped")
713 }
714 lines := dc.expectClosed(3 * time.Second)
715 if !containsLine(lines, "CLOSE idle") {
716 t.Fatalf("lines before the close: %q", lines)
717 }
718 eventually(t, "host unbound", time.Second, func() bool { return !e.srv.Online(d.host) })
719}
720
721// ------------------------------------------------------------------------------------------------------------------------------------
722// ATTACH
723// ------------------------------------------------------------------------------------------------------------------------------------
724
725// openOne makes a client arrive and returns its OPEN line, parsed; the client's goroutine ends when done is closed or the handshake gives up.
726func openOne(t *testing.T, e *testEnv, d *testDevice, dc *devConn) (message, chan<- struct{}) {
727 t.Helper()
728 stop := make(chan struct{})
729 go func() {
730 c, err := e.clientTLS(d)
731 if err == nil {
732 <-stop
733 _ = c.Close()
734 }
735 }()
736 line := dc.recv()
737 m, err := parseLine(line, dirN2D)
738 if err != nil || m.str("verb") != "OPEN" {
739 t.Fatalf("expected an OPEN, got %q", line)
740 }
741 return m, stop
742}
743
744func attachLine(id, secret string) string { return "ATTACH " + id + " " + secret }
745
746func TestAttach_AWrongOrReusedSecretFailsAndTheRightOneWorksOnce(t *testing.T) {
747 e := newTestEnv(t, nil)
748 d := e.newDevice()
749 dc := e.connectOK(d)
750 open, stop := openOne(t, e, d, dc)
751 defer close(stop)
752 id, secret := open.str("stream_id"), open.str("secret")
753
754 rawAttach := func(line string) (reply string, closed bool) {
755 c, err := e.dialNode()
756 if err != nil {
757 t.Fatal(err)
758 }
759 defer c.Close()
760 fmt.Fprintf(c, "%s\n", line)
761 _ = c.SetReadDeadline(time.Now().Add(3 * time.Second))
762 br := bufio.NewReader(c)
763 b, _ := br.ReadString('\n')
764 _, err = br.ReadByte() // the node closes after an ERR
765 return strings.TrimSpace(b), err != nil
766 }
767 wrong := secret[:len(secret)-1] + map[bool]string{true: "B", false: "A"}[secret[len(secret)-1] == 'A']
768 if r, closed := rawAttach(attachLine(id, wrong)); r != "ERR auth" || !closed {
769 t.Fatalf("wrong secret: %q closed=%v", r, closed)
770 }
771 if r, _ := rawAttach(attachLine(id, strings.Repeat("A", 43))); r != "ERR auth" {
772 t.Fatalf("zero secret: %q", r)
773 }
774 // a wrong secret does not consume the stream: the right one still works
775 c, err := e.dialNode()
776 if err != nil {
777 t.Fatal(err)
778 }
779 defer c.Close()
780 fmt.Fprintf(c, "%s\n", attachLine(id, secret))
781 // success has no reply: the next byte is the client's ClientHello (a TLS handshake record)
782 _ = c.SetReadDeadline(time.Now().Add(3 * time.Second))
783 first := make([]byte, 1)
784 if _, err := io.ReadFull(c, first); err != nil || first[0] != 0x16 {
785 t.Fatalf("after a good ATTACH the stream's first byte is % x, err %v", first, err)
786 }
787 // used: the same id and secret again is an unknown stream
788 if r, closed := rawAttach(attachLine(id, secret)); r != "ERR unknown_stream" || !closed {
789 t.Fatalf("reused secret: %q", r)
790 }
791 if r, _ := rawAttach(attachLine(randomHex(16), secret)); r != "ERR unknown_stream" {
792 t.Fatalf("unknown id: %q", r)
793 }
794 for _, bad := range []string{"ATTACH", "ATTACH " + id, "ATTACH " + strings.ToUpper(id) + " " + secret, "ATTACH " + id + " short", "ATTACH " + id + " " + secret + " extra"} {
795 if r, _ := rawAttach(bad); r != "ERR bad_attach" && r != "ERR bad_command" {
796 t.Errorf("%q answered %q", bad, r)
797 }
798 }
799 e.checkTranscript()
800}
801
802func TestAttach_AfterTheTimeoutTheStreamIsGone(t *testing.T) {
803 e := newTestEnv(t, func(c *Config) { c.AttachTimeout = 300 * time.Millisecond })
804 d := e.newDevice()
805 dc := e.connectOK(d)
806 open, stop := openOne(t, e, d, dc)
807 defer close(stop)
808 time.Sleep(700 * time.Millisecond)
809 c, err := e.dialNode()
810 if err != nil {
811 t.Fatal(err)
812 }
813 defer c.Close()
814 fmt.Fprintf(c, "%s\n", attachLine(open.str("stream_id"), open.str("secret")))
815 _ = c.SetReadDeadline(time.Now().Add(3 * time.Second))
816 if b, _ := bufio.NewReader(c).ReadString('\n'); strings.TrimSpace(b) != "ERR unknown_stream" {
817 t.Fatalf("attach after the timeout: %q", b)
818 }
819 e.waitIdle()
820 // (no transcript check here: the checker judges a late ATTACH by the policy's attach window, this node runs a shorter one)
821}
822
823func TestAttach_EveryStreamGetsItsOwnIdAndSecret(t *testing.T) {
824 e := newTestEnv(t, nil)
825 d := e.newDevice()
826 dc := e.connectOK(d)
827 ds := dc.serve(func(c net.Conn) { time.Sleep(50 * time.Millisecond) })
828 for i := 0; i < 40; i++ {
829 c, err := tls.DialWithDialer(&net.Dialer{Timeout: time.Second}, "tcp", e.httpsAddr, &tls.Config{ServerName: d.host, InsecureSkipVerify: true})
830 if c != nil {
831 _ = c.Close()
832 }
833 _ = err
834 }
835 eventually(t, "40 opens", 5*time.Second, func() bool { return ds.openCount() >= 40 })
836 ids, secrets := map[string]bool{}, map[string]bool{}
837 for _, m := range ds.openList() {
838 ids[m.str("stream_id")], secrets[m.str("secret")] = true, true
839 if !reStreamID.MatchString(m.str("stream_id")) || !reSecret.MatchString(m.str("secret")) {
840 t.Fatalf("OPEN %v", m)
841 }
842 }
843 if len(ids) != 40 || len(secrets) != 40 {
844 t.Fatalf("%d distinct ids, %d distinct secrets for 40 streams", len(ids), len(secrets))
845 }
846}
847
848// ------------------------------------------------------------------------------------------------------------------------------------
849// OPEN only after a complete, well-formed ClientHello
850// ------------------------------------------------------------------------------------------------------------------------------------
851
852// helloOf builds a ClientHello for host with a padding extension of pad bytes, as records of at most recordSize payload bytes.
853func helloOf(host string, pad, recordSize int) []byte {
854 ext := sniExt(host)
855 if pad > 0 {
856 ext = append(append([]byte{0, 21, byte(pad >> 8), byte(pad)}, make([]byte, pad)...), ext...)
857 }
858 var body []byte
859 body = append(body, 3, 3)
860 body = append(body, make([]byte, 32)...)
861 body = append(body, 0)
862 body = append(body, 0, 2, 0x13, 0x01)
863 body = append(body, 1, 0)
864 body = append(body, byte(len(ext)>>8), byte(len(ext)))
865 body = append(body, ext...)
866 hs := append([]byte{1, byte(len(body) >> 16), byte(len(body) >> 8), byte(len(body))}, body...)
867 var out []byte
868 for len(hs) > 0 {
869 k := min(recordSize, len(hs))
870 out = append(out, 22, 3, 1, byte(k>>8), byte(k))
871 out = append(out, hs[:k]...)
872 hs = hs[k:]
873 }
874 return out
875}
876
877// sendRaw opens a connection, writes b in pieces of size (0 = at once) and returns it.
878func sendRaw(t *testing.T, e *testEnv, b []byte, size int) net.Conn {
879 t.Helper()
880 c, err := net.DialTimeout("tcp", e.httpsAddr, 3*time.Second)
881 if err != nil {
882 t.Fatal(err)
883 }
884 t.Cleanup(func() { _ = c.Close() })
885 var w net.Conn = c
886 if size > 0 {
887 w = chunkConn{Conn: c, size: size, delay: time.Millisecond}
888 }
889 _, _ = w.Write(b)
890 return c
891}
892
893func TestClientHello_NothingThatIsNotACompleteWellFormedHelloProducesAnOpen(t *testing.T) {
894 e := newTestEnv(t, func(c *Config) { c.HelloTimeout = 400 * time.Millisecond })
895 d := e.newDevice()
896 dc := e.connectOK(d)
897 ds := dc.serve(nil)
898 ds.noAttach = true
899 full := helloOf(d.host, 0, 16000)
900
901 t.Run("truncated at every interesting length", func(t *testing.T) {
902 for _, n := range []int{1, 2, 3, 4, 5, 6, 9, 20, 43, 44, len(full) / 2, len(full) - 1} {
903 c := sendRaw(t, e, full[:n], 0)
904 _ = c.Close() // the client goes away mid-hello
905 }
906 })
907 t.Run("stalls half way", func(t *testing.T) {
908 c := sendRaw(t, e, full[:len(full)/2], 0)
909 _ = c.SetReadDeadline(time.Now().Add(3 * time.Second))
910 start := time.Now()
911 if n, err := c.Read(make([]byte, 1)); n != 0 || err == nil {
912 t.Fatalf("n=%d err=%v", n, err)
913 }
914 if time.Since(start) > 2*time.Second {
915 t.Fatal("the hello deadline is not enforced")
916 }
917 })
918 t.Run("a hello whose record length lies", func(t *testing.T) {
919 lie := append([]byte(nil), full...)
920 lie[3], lie[4] = 0xff, 0xff
921 sendRaw(t, e, lie, 0)
922 })
923 t.Run("a handshake that is not a ClientHello", func(t *testing.T) {
924 notHello := append([]byte(nil), full...)
925 notHello[5] = 2
926 sendRaw(t, e, notHello, 0)
927 })
928 t.Run("not TLS at all", func(t *testing.T) {
929 sendRaw(t, e, []byte("GET / HTTP/1.1\r\nHost: "+d.host+"\r\n\r\n"), 0)
930 })
931 t.Run("no SNI", func(t *testing.T) {
932 sendRaw(t, e, realHello(t, ""), 0)
933 })
934 t.Run("an address as the name", func(t *testing.T) {
935 sendRaw(t, e, buildHello(sniExt("192.0.2.3"), true), 0)
936 })
937 t.Run("another host", func(t *testing.T) {
938 sendRaw(t, e, helloOf(label20()+"."+testZone, 0, 16000), 0)
939 })
940 t.Run("a name with an underscore, a double dot, a space", func(t *testing.T) {
941 for _, name := range []string{"a_b." + testZone, "a..b." + testZone, "a b." + testZone, d.label + "." + testZone + ".."} {
942 sendRaw(t, e, helloOf(name, 0, 16000), 0)
943 }
944 })
945 time.Sleep(900 * time.Millisecond)
946 if n := ds.openCount(); n != 0 {
947 t.Fatalf("%d OPEN lines for hellos that were not complete and well formed", n)
948 }
949 // teeth: the same bytes, whole, do produce one
950 sendRaw(t, e, full, 0)
951 eventually(t, "the OPEN of the complete hello", 3*time.Second, func() bool { return ds.openCount() == 1 })
952 e.checkTranscript()
953}
954
955func TestClientHello_FragmentedHugeCasedAndDottedHellosAreServed(t *testing.T) {
956 e := newTestEnv(t, nil)
957 d := e.newDevice()
958 dc := e.connectOK(d)
959 ds := dc.serve(nil)
960 ds.noAttach = true
961
962 variants := map[string][]byte{
963 "split in 100 byte records": helloOf(d.host, 0, 100),
964 "in 7 byte records": helloOf(d.host, 0, 7),
965 "64 KB": helloOf(d.host, 65000, 16384),
966 "upper case": helloOf(strings.ToUpper(d.host), 0, 16000),
967 "mixed case": helloOf(strings.ToUpper(d.label[:5])+d.label[5:]+"."+strings.ToUpper(testZone[:3])+testZone[3:], 0, 16000),
968 "trailing dot": helloOf(d.host+".", 0, 16000),
969 "a real client's hello": realHello(t, d.host),
970 "a client hello offering X25519MLKEM768": captureClientHello(t, &tls.Config{ServerName: d.host, InsecureSkipVerify: true, CurvePreferences: []tls.CurveID{tls.X25519MLKEM768, tls.X25519}}),
971 }
972 want := 0
973 for name, hello := range variants {
974 _ = name
975 want++
976 sendRaw(t, e, hello, 0)
977 }
978 eventually(t, "an OPEN per variant", 5*time.Second, func() bool { return ds.openCount() == want })
979 // arriving one byte at a time (over a pipe so the pieces really are separate reads)
980 sendRaw(t, e, helloOf(d.host, 0, 16000), 1)
981 eventually(t, "the byte-by-byte hello", 10*time.Second, func() bool { return ds.openCount() == want+1 })
982 e.checkTranscript()
983}
984
985func TestClientHello_ThePeerAddressAndTheClassTravelInTheOpen(t *testing.T) {
986 cases := map[string]struct {
987 classify func(netip.Addr, string) string
988 want string
989 }{
990 "no classifier": {nil, "anon"},
991 "openai": {func(netip.Addr, string) string { return "openai" }, "openai"},
992 "hint": {func(netip.Addr, string) string { return "hint" }, "hint"},
993 "a class that is not": {func(netip.Addr, string) string { return "vip" }, "anon"},
994 }
995 for name, tc := range cases {
996 var seenPeer atomic.Value
997 e := newTestEnv(t, func(c *Config) {
998 if tc.classify != nil {
999 c.Classify = func(peer netip.Addr, host string) string {
1000 seenPeer.Store(peer.String() + " " + host)
1001 return tc.classify(peer, host)
1002 }
1003 }
1004 })
1005 d := e.newDevice()
1006 dc := e.connectOK(d)
1007 ds := dc.serve(nil)
1008 ds.noAttach = true
1009 sendRaw(t, e, helloOf(d.host, 0, 16000), 0)
1010 eventually(t, name+": the OPEN", 3*time.Second, func() bool { return ds.openCount() == 1 })
1011 open := ds.openList()[0]
1012 if open.str("peer_ip") != "127.0.0.1" || open.str("class") != tc.want {
1013 t.Errorf("%s: OPEN %v", name, open)
1014 }
1015 if tc.classify != nil && seenPeer.Load() != "127.0.0.1 "+d.host {
1016 t.Errorf("%s: the classifier was asked about %v", name, seenPeer.Load())
1017 }
1018 e.checkTranscript()
1019 }
1020}
1021
1022// ------------------------------------------------------------------------------------------------------------------------------------
1023// Port 80 and the listeners
1024// ------------------------------------------------------------------------------------------------------------------------------------
1025
1026func acmeGet(t *testing.T, e *testEnv, method, path, host string) string {
1027 t.Helper()
1028 return rawExchange(t, e.httpAddr, method+" "+path+" HTTP/1.1\r\nHost: "+host+"\r\nConnection: close\r\n\r\n")
1029}
1030
1031func TestPort80_ServesOnlyTheNodesOwnACMEWebroot(t *testing.T) {
1032 webroot := t.TempDir()
1033 dir := filepath.Join(webroot, ".well-known", "acme-challenge")
1034 if err := os.MkdirAll(dir, 0o755); err != nil {
1035 t.Fatal(err)
1036 }
1037 must := func(err error) {
1038 t.Helper()
1039 if err != nil {
1040 t.Fatal(err)
1041 }
1042 }
1043 must(os.WriteFile(filepath.Join(dir, "tok_OK-1"), []byte("tok_OK-1.thumbprint"), 0o644))
1044 must(os.WriteFile(filepath.Join(dir, "huge"), bytes.Repeat([]byte("x"), 9000), 0o644))
1045 outside := filepath.Join(t.TempDir(), "secret")
1046 must(os.WriteFile(outside, []byte("not for the internet"), 0o644))
1047 must(os.Symlink(outside, filepath.Join(dir, "linked")))
1048 must(os.WriteFile(filepath.Join(webroot, "private.txt"), []byte("private"), 0o644))
1049
1050 e := newTestEnv(t, func(c *Config) { c.ACMEWebroot = webroot })
1051 d := e.newDevice()
1052 dc := e.connectOK(d)
1053 ds := dc.serve(nil)
1054 ds.noAttach = true
1055
1056 if out := acmeGet(t, e, "GET", "/.well-known/acme-challenge/tok_OK-1", testNodeHost); !strings.HasPrefix(out, "HTTP/1.1 200 OK") || !strings.HasSuffix(out, "tok_OK-1.thumbprint") {
1057 t.Fatalf("the node's own challenge: %q", out)
1058 }
1059 if out := acmeGet(t, e, "GET", "/.well-known/acme-challenge/tok_OK-1", strings.ToUpper(testNodeHost)+":80"); !strings.HasPrefix(out, "HTTP/1.1 200 OK") {
1060 t.Fatalf("the node's own name, upper case with a port: %q", out)
1061 }
1062 if out := acmeGet(t, e, "HEAD", "/.well-known/acme-challenge/tok_OK-1", testNodeHost); !strings.HasPrefix(out, "HTTP/1.1 200 OK") || strings.Contains(out, "thumbprint") {
1063 t.Fatalf("HEAD: %q", out)
1064 }
1065 for name, req := range map[string][2]string{
1066 "a device's host": {"/.well-known/acme-challenge/tok_OK-1", d.host},
1067 "another host": {"/.well-known/acme-challenge/tok_OK-1", "example.com"},
1068 "no such token": {"/.well-known/acme-challenge/nope", testNodeHost},
1069 "the webroot itself": {"/", testNodeHost},
1070 "a file outside the path": {"/private.txt", testNodeHost},
1071 "traversal": {"/.well-known/acme-challenge/../../private.txt", testNodeHost},
1072 "encoded traversal": {"/.well-known/acme-challenge/%2e%2e/private.txt", testNodeHost},
1073 "a query": {"/.well-known/acme-challenge/tok_OK-1?x=1", testNodeHost},
1074 "a file that is too large": {"/.well-known/acme-challenge/huge", testNodeHost},
1075 "a symlink": {"/.well-known/acme-challenge/linked", testNodeHost},
1076 "a metrics path": {"/metrics", testNodeHost},
1077 "healthz": {"/healthz", testNodeHost},
1078 } {
1079 if out := acmeGet(t, e, "GET", req[0], req[1]); !strings.HasPrefix(out, "HTTP/1.1 404 ") || strings.Contains(out, "thumbprint") || strings.Contains(out, "private") || strings.Contains(out, "internet") {
1080 t.Errorf("%s: %q", name, out)
1081 }
1082 }
1083 if out := acmeGet(t, e, "POST", "/.well-known/acme-challenge/tok_OK-1", testNodeHost); !strings.HasPrefix(out, "HTTP/1.1 404 ") {
1084 t.Errorf("POST: %q", out)
1085 }
1086 if out := rawExchange(t, e.httpAddr, "GET /.well-known/acme-challenge/tok_OK-1 HTTP/1.0\r\n\r\n"); out != "" && !strings.HasPrefix(out, "HTTP/1.1 404 ") {
1087 t.Errorf("no Host: %q", out)
1088 }
1089 time.Sleep(100 * time.Millisecond)
1090 if ds.openCount() != 0 {
1091 t.Fatal("port 80 asked a device to open a stream: it must never reach one")
1092 }
1093}
1094
1095func TestPort80_WithoutAWebrootEverythingIs404(t *testing.T) {
1096 e := newTestEnv(t, nil)
1097 if out := acmeGet(t, e, "GET", "/.well-known/acme-challenge/anything", testNodeHost); !strings.HasPrefix(out, "HTTP/1.1 404 ") {
1098 t.Fatalf("%q", out)
1099 }
1100}
1101
1102func TestListeners_ThereIsNoPort8443AndNoSelfSignedPin(t *testing.T) {
1103 e := newTestEnv(t, nil)
1104 if e.srv.httpsLn == nil || e.srv.httpLn == nil {
1105 t.Fatal("the two listeners are not up")
1106 }
1107 // the node holds exactly two listeners (the field set of the server says it: no control listener exists)
1108 typ := reflect.TypeOf(e.srv).Elem()
1109 for i := 0; i < typ.NumField(); i++ {
1110 if name := typ.Field(i).Name; strings.Contains(strings.ToLower(name), "ctrlln") || strings.Contains(strings.ToLower(name), "controlln") {
1111 t.Fatalf("the server has a control listener field %s", name)
1112 }
1113 }
1114 cfgType := reflect.TypeOf(Config{})
1115 for i := 0; i < cfgType.NumField(); i++ {
1116 if name := cfgType.Field(i).Name; strings.Contains(name, "ControlAddr") || strings.Contains(name, "StateDir") || strings.Contains(name, "Devices") {
1117 t.Fatalf("Config still has %s", name)
1118 }
1119 }
1120 // nothing in the sources speaks of :8443, a device token or a pinned fingerprint any more
1121 files, _ := filepath.Glob("*.go")
1122 forbidden := regexp.MustCompile(`8443|ControlAddr|control_cert_sha256|server\.fingerprint|loadOrCreateServerCert|checkToken`)
1123 for _, f := range files {
1124 if strings.HasSuffix(f, "_test.go") {
1125 continue
1126 }
1127 b, _ := os.ReadFile(f)
1128 if loc := forbidden.FindIndex(b); loc != nil {
1129 t.Errorf("%s still mentions %q", f, b[loc[0]:loc[1]])
1130 }
1131 }
1132}
1133
1134// ------------------------------------------------------------------------------------------------------------------------------------
1135// The device's connection to the node's own name
1136// ------------------------------------------------------------------------------------------------------------------------------------
1137
1138func TestOwnName_OnlyAllTunnel2OverTLS13AndOnlyHelloOrAttach(t *testing.T) {
1139 e := newTestEnv(t, nil)
1140
1141 reply := func(line string) string {
1142 c, err := e.dialNode()
1143 if err != nil {
1144 t.Fatal(err)
1145 }
1146 defer c.Close()
1147 _, _ = io.WriteString(c, line+"\n")
1148 _ = c.SetReadDeadline(time.Now().Add(3 * time.Second))
1149 b, _ := bufio.NewReader(c).ReadString('\n')
1150 return strings.TrimSpace(b)
1151 }
1152 for line, want := range map[string]string{
1153 "hello 2 AAAA.BBBB.CCCC": "ERR bad_command",
1154 "PING": "ERR bad_command",
1155 "PROOF not-a-token": "ERR bad_command", // a malformed PROOF/RENEW as the FIRST line is not HELLO or ATTACH (contract section 2)
1156 "PROOF AAAA.BBBB.CCCC": "ERR bad_command",
1157 "PROOF": "ERR bad_command",
1158 "RENEW not-a-token": "ERR bad_command",
1159 "RENEW AAAA.BBBB.CCCC": "ERR bad_command",
1160 "OPEN " + randomHex(16): "ERR bad_command",
1161 "HELLO 3 AAAA.BBBB.CCCC": "ERR unsupported_version",
1162 "HELLO 1 AAAA.BBBB.CCCC": "ERR unsupported_version",
1163 "HELLO two AAAA.BBBB.CCCC": "ERR bad_command",
1164 "HELLO 2": "ERR bad_command",
1165 "HELLO 2 not-a-token": "ERR bad_hello",
1166 "HELLO 2 AAAA.BBBB.CCCC extra": "ERR bad_command",
1167 "HELLO 2 AAAA.BBBB.CCCC": "ERR bad_command",
1168 "HELLO 2 AAAA.BBBB.CCCC\r": "ERR bad_command",
1169 "GET / HTTP/1.1": "ERR bad_command",
1170 "ATTACH": "ERR bad_command",
1171 "ATTACH short short": "ERR bad_attach",
1172 strings.Repeat("A", 9000): "ERR line_too_long",
1173 "HELLO 2 " + strings.Repeat("A", 5000) + ".B.C": "ERR bad_hello",
1174 } {
1175 if got := reply(line); got != want {
1176 t.Errorf("%.40q: %q, want %q", line, got, want)
1177 }
1178 }
1179
1180 dial := func(cfg *tls.Config) error {
1181 cfg.ServerName, cfg.RootCAs = testNodeHost, e.nodePool
1182 c, err := tls.DialWithDialer(&net.Dialer{Timeout: 3 * time.Second}, "tcp", e.httpsAddr, cfg)
1183 if err == nil {
1184 _ = c.SetReadDeadline(time.Now().Add(2 * time.Second))
1185 _, err = c.Read(make([]byte, 1)) // the node closes: it does not talk to a client that did not offer the protocol
1186 _ = c.Close()
1187 }
1188 return err
1189 }
1190 if err := dial(&tls.Config{NextProtos: []string{"h2"}, MinVersion: tls.VersionTLS13}); err == nil {
1191 t.Error("a client offering only h2 got a session")
1192 }
1193 if err := dial(&tls.Config{NextProtos: []string{"http/1.1"}, MinVersion: tls.VersionTLS13}); err == nil {
1194 t.Error("a client offering only http/1.1 stayed")
1195 }
1196 if err := dial(&tls.Config{MinVersion: tls.VersionTLS13}); err == nil {
1197 t.Error("a client offering no ALPN stayed")
1198 }
1199 if err := dial(&tls.Config{NextProtos: []string{policyData.Protocol.ALPN}, MaxVersion: tls.VersionTLS12}); err == nil {
1200 t.Error("a TLS 1.2 client got a session")
1201 }
1202 // ... and the one that speaks the protocol is not hurt by the refusals
1203 d := e.newDevice()
1204 _ = e.connectOK(d)
1205}
1206
1207func TestOwnName_UnauthenticatedConnectionsAreCapped(t *testing.T) {
1208 e := newTestEnv(t, func(c *Config) {
1209 c.MaxUnauth = 4
1210 c.FirstLineTimeout = 700 * time.Millisecond
1211 c.HelloTimeout = 700 * time.Millisecond
1212 })
1213 var held []net.Conn
1214 for i := 0; i < 4; i++ {
1215 c, err := net.Dial("tcp", e.httpsAddr)
1216 if err != nil {
1217 t.Fatal(err)
1218 }
1219 held = append(held, c)
1220 defer c.Close()
1221 }
1222 time.Sleep(150 * time.Millisecond)
1223 // the fifth is turned away without a byte
1224 c, err := net.Dial("tcp", e.httpsAddr)
1225 if err != nil {
1226 t.Fatal(err)
1227 }
1228 defer c.Close()
1229 _ = c.SetReadDeadline(time.Now().Add(2 * time.Second))
1230 if n, err := c.Read(make([]byte, 1)); n != 0 || err == nil {
1231 t.Fatalf("n=%d err=%v", n, err)
1232 }
1233 // once the held ones time out, the node serves again
1234 time.Sleep(900 * time.Millisecond)
1235 d := e.newDevice()
1236 _ = e.connectOK(d)
1237 _ = held
1238}
1239
1240// A control plane that is slow to answer must not starve what the node does without it: a device that proved itself is no longer an
1241// unauthenticated connection, so the clients (and the ATTACHes) of devices that are already connected go on.
1242func TestOwnName_SlowAuthorizeDoesNotStarveClientsOfConnectedDevices(t *testing.T) {
1243 e := newTestEnv(t, func(c *Config) { c.MaxUnauth = 4 })
1244 d := e.newDevice()
1245 dc := e.connectOK(d)
1246 _ = dc.serve(nil)
1247
1248 var pending []<-chan string
1249 for i := 0; i < 4; i++ {
1250 slow := e.newDevice()
1251 e.plan(slow).delay = 2 * time.Second
1252 pending = append(pending, e.helloAsync(slow))
1253 }
1254 eventually(t, "the slow authorize calls are in flight", 3*time.Second, func() bool { return len(e.cp.callLog()) >= 5 })
1255
1256 // a client of the connected device is served at once (its stream's ATTACH goes through the same gate)
1257 c, err := e.clientTLS(d)
1258 if err != nil {
1259 t.Fatalf("a client of a connected device was turned away while authorize was slow: %v", err)
1260 }
1261 defer c.Close()
1262 if resp, err := postOver(c, d.host, []byte("hello")); err != nil || !bytes.Contains(resp, []byte("hello")) {
1263 t.Fatalf("resp=%q err=%v", resp, err)
1264 }
1265 for _, p := range pending {
1266 if got := <-p; !strings.HasPrefix(got, "OK ") {
1267 t.Fatalf("a slow device ended with %q", got)
1268 }
1269 }
1270 e.checkTranscript()
1271}
1272
1273// At most AuthorizeMax calls to the control plane run at once; the ones that wait for a turn are bounded too, and beyond that the device is told
1274// control_unavailable.
1275func TestAuthorize_ConcurrencyAndTheWaitingAreBounded(t *testing.T) {
1276 e := newTestEnv(t, func(c *Config) { c.AuthorizeMax = 2 })
1277 var pending []<-chan string
1278 for i := 0; i < 6; i++ {
1279 d := e.newDevice()
1280 e.plan(d).delay = 300 * time.Millisecond
1281 pending = append(pending, e.helloAsync(d))
1282 }
1283 for _, p := range pending {
1284 if got := <-p; !strings.HasPrefix(got, "OK ") {
1285 t.Fatalf("%q", got)
1286 }
1287 }
1288 e.cp.mu.Lock()
1289 most := e.cp.maxInflight
1290 e.cp.mu.Unlock()
1291 if most != 2 {
1292 t.Fatalf("%d authorize calls at once, want exactly the limit of 2", most)
1293 }
1294
1295 full := newTestEnv(t, func(c *Config) { c.AuthorizeMax, c.MaxUnauth = 1, 2 })
1296 pending = nil
1297 for i := 0; i < 3; i++ {
1298 d := full.newDevice()
1299 full.plan(d).delay = time.Second
1300 pending = append(pending, full.helloAsync(d))
1301 }
1302 ok, refused := 0, 0
1303 for _, p := range pending {
1304 switch got := <-p; {
1305 case strings.HasPrefix(got, "OK "):
1306 ok++
1307 case got == "ERR control_unavailable":
1308 refused++
1309 default:
1310 t.Fatalf("%q", got)
1311 }
1312 }
1313 if ok != 2 || refused != 1 {
1314 t.Fatalf("%d OK and %d refused, want 2 and 1", ok, refused)
1315 }
1316}
1317
1318// What the node records of two connections of one subject is in the order it routed them: the OK is recorded under the routing table's lock.
1319func TestRoutes_ActivationIsRecordedUnderTheTableLock(t *testing.T) {
1320 rt := newRouteTable()
1321 a := &ctrl{}
1322 underLock := false
1323 if _, err := rt.activate(a, "sub", "h.example", func() {
1324 if rt.mu.TryLock() {
1325 rt.mu.Unlock()
1326 return
1327 }
1328 underLock = true
1329 }); err != nil || !underLock {
1330 t.Fatalf("err=%v, recorded under the lock: %v", err, underLock)
1331 }
1332 if _, err := rt.activate(&ctrl{}, "other", "h.example", func() { t.Error("recorded a refused activation") }); err != errHostTaken {
1333 t.Fatalf("err=%v", err)
1334 }
1335}
1336
1337func TestHint_AddressesReachTheHookAndABadHintClosesTheConnection(t *testing.T) {
1338 type hint struct {
1339 host string
1340 addrs []netip.Addr
1341 }
1342 got := make(chan hint, 4)
1343 e := newTestEnv(t, func(c *Config) { c.OnHint = func(host string, addrs []netip.Addr) { got <- hint{host, addrs} } })
1344 d := e.newDevice()
1345 dc := e.connectOK(d)
1346 dc.send("HINT 203.0.113.9 2001:db8::7")
1347 select {
1348 case h := <-got:
1349 if h.host != d.host || len(h.addrs) != 2 || h.addrs[0].String() != "203.0.113.9" || h.addrs[1].String() != "2001:db8::7" {
1350 t.Fatalf("%+v", h)
1351 }
1352 case <-time.After(3 * time.Second):
1353 t.Fatal("no hint")
1354 }
1355 dc.send("HINT 203.0.113.9 not-an-ip")
1356 if r := dc.recv(); r != "ERR bad_command" {
1357 t.Fatalf("%q", r)
1358 }
1359 dc.expectClosed(2 * time.Second)
1360 e.checkTranscript()
1361}
1362
1363// ------------------------------------------------------------------------------------------------------------------------------------
1364// helpers shared by the splice tests
1365// ------------------------------------------------------------------------------------------------------------------------------------
1366
1367func fingerprintOf(der []byte) string {
1368 sum := sha256.Sum256(der)
1369 return hex.EncodeToString(sum[:])
1370}
1371
1372func TestLifetime_ARenewFloodIsBusyNotAStreamOfAuthorizeCalls(t *testing.T) {
1373 e := newTestEnv(t, func(c *Config) { c.MinRenewGap = 700 * time.Millisecond })
1374 d := e.newDevice()
1375 dc := e.connectOK(d)
1376 if r := dc.renew(); !strings.HasPrefix(r, "OK ") {
1377 t.Fatalf("first renew: %q", r)
1378 }
1379 for i := 0; i < 5; i++ {
1380 dc.send("RENEW " + e.session(d, nil))
1381 if r := dc.recv(); r != "ERR busy" {
1382 t.Fatalf("a renew right after another: %q", r)
1383 }
1384 }
1385 if n := e.cp.callsFor(d.id, "renew"); n != 1 {
1386 t.Fatalf("the flood reached the control plane %d times", n)
1387 }
1388 time.Sleep(800 * time.Millisecond)
1389 if r := dc.renew(); !strings.HasPrefix(r, "OK ") {
1390 t.Fatalf("a renew after the gap: %q", r)
1391 }
1392 if !e.srv.Online(d.host) {
1393 t.Fatal("the connection did not survive the flood")
1394 }
1395}
1396
1397func TestClientHello_ADualStackListenerSeesAnIPv4ClientAsIPv4(t *testing.T) {
1398 e := newTestEnv(t, func(c *Config) { c.HTTPSAddr = "[::]:0" })
1399 d := e.newDevice()
1400 dc := e.connectOK(d)
1401 ds := dc.serve(nil)
1402 ds.noAttach = true
1403 port := e.srv.HTTPSAddr()[strings.LastIndex(e.srv.HTTPSAddr(), ":")+1:]
1404 e.httpsAddr = "127.0.0.1:" + port
1405 sendRaw(t, e, helloOf(d.host, 0, 16000), 0)
1406 eventually(t, "the OPEN", 3*time.Second, func() bool { return ds.openCount() == 1 })
1407 if ip := ds.openList()[0].str("peer_ip"); ip != "127.0.0.1" {
1408 t.Fatalf("peer_ip %q: an IPv4 client must not be written as an IPv4-mapped IPv6 address", ip)
1409 }
1410 e.checkTranscript()
1411}
1412
1413func TestAuth_AKeyRotationOfTheControlPlaneIsFollowedAndARevokedKeyIsNotTrusted(t *testing.T) {
1414 e := newTestEnv(t, nil)
1415 d := e.newDevice()
1416 oldKey := e.cpKey
1417 oldPub := oldKey.Public().(ed25519.PublicKey)
1418 newPub, newPriv, _ := ed25519.GenerateKey(rand.Reader)
1419
1420 handshake := func(signer ed25519.PrivateKey, kid string) string {
1421 dc := e.connect(d)
1422 return dc.hello(
1423 func() string { return e.sessionSignedBy(signer, d, func(h, c map[string]any) { h["kid"] = kid }) },
1424 func(s, n string) string { return e.proof(d, s, n, nil) })
1425 }
1426 if r := handshake(oldKey, testKID); !strings.HasPrefix(r, "OK ") {
1427 t.Fatalf("before the rotation: %q", r)
1428 }
1429 // both keys are published during a rotation
1430 e.keys.Set(StaticKeys{testKID: oldPub, "cp-test-2": newPub})
1431 if r := handshake(newPriv, "cp-test-2"); !strings.HasPrefix(r, "OK ") {
1432 t.Fatalf("the new key during the rotation: %q", r)
1433 }
1434 if r := handshake(oldKey, testKID); !strings.HasPrefix(r, "OK ") {
1435 t.Fatalf("the old key during the rotation: %q", r)
1436 }
1437 // the old key is withdrawn: its sessions are refused, the new key's are not
1438 e.keys.Set(StaticKeys{"cp-test-2": newPub})
1439 if r := handshake(oldKey, testKID); r != "ERR auth" {
1440 t.Fatalf("a withdrawn key: %q", r)
1441 }
1442 if r := handshake(newPriv, "cp-test-2"); !strings.HasPrefix(r, "OK ") {
1443 t.Fatalf("the new key after the old one was withdrawn: %q", r)
1444 }
1445 // a key source that vouches for nothing (stale beyond its window) refuses every session
1446 e.keys.Set(nil)
1447 if r := handshake(newPriv, "cp-test-2"); r != "ERR auth" {
1448 t.Fatalf("an empty key set: %q", r)
1449 }
1450}