ALL

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

tunnel/observer_test.go

SHA-256: 2e9b6509848da15bcda54686bd93ee43d21a970e00fabb305b95188fa5ce7330
379 lines · 14046 bytes · View on GitHub

1package main
2
3import (
4 "bytes"
5 "context"
6 "crypto/tls"
7 "math/rand/v2"
8 "net"
9 "os/exec"
10 "strings"
11 "sync"
12 "testing"
13 "time"
14)
15
16// The cleartext handshake observer (observer.go): what the node reads of the first messages of a TLS handshake, and nothing else.
17
18// recordedServerHello runs a TLS server with cfg against a client with ccfg over a pipe and returns the bytes the server sent first (until the client
19// has what it needs): the device's side of a handshake, as the node would splice it.
20func recordedServerHello(t testing.TB, scfg, ccfg *tls.Config) []byte {
21 t.Helper()
22 ln, err := net.Listen("tcp", "127.0.0.1:0")
23 if err != nil {
24 t.Fatal(err)
25 }
26 defer ln.Close()
27 var mu sync.Mutex
28 var sent bytes.Buffer
29 done := make(chan struct{})
30 go func() {
31 defer close(done)
32 raw, err := ln.Accept()
33 if err != nil {
34 return
35 }
36 defer raw.Close()
37 _ = raw.SetDeadline(time.Now().Add(5 * time.Second))
38 rec := &recordingConn{Conn: raw, onWrite: func(p []byte) { mu.Lock(); sent.Write(p); mu.Unlock() }}
39 _ = tls.Server(rec, scfg).Handshake()
40 }()
41 c, err := net.DialTimeout("tcp", ln.Addr().String(), 3*time.Second)
42 if err != nil {
43 t.Fatal(err)
44 }
45 _ = c.SetDeadline(time.Now().Add(5 * time.Second))
46 _ = tls.Client(c, ccfg).Handshake()
47 c.Close()
48 <-done
49 mu.Lock()
50 defer mu.Unlock()
51 return append([]byte(nil), sent.Bytes()...)
52}
53
54type recordingConn struct {
55 net.Conn
56 onWrite func([]byte)
57}
58
59func (r *recordingConn) Write(p []byte) (int, error) {
60 r.onWrite(p)
61 return r.Conn.Write(p)
62}
63
64func testServerConfig(t testing.TB) *tls.Config {
65 t.Helper()
66 _, cert := makeCA(t, "observer.example.test")
67 return &tls.Config{Certificates: []tls.Certificate{cert}, MinVersion: tls.VersionTLS12}
68}
69
70func TestObserver_ClientHelloGroupsAndECH(t *testing.T) {
71 hello := captureClientHello(t, &tls.Config{ServerName: "dev1.example.test", InsecureSkipVerify: true, CurvePreferences: []tls.CurveID{tls.X25519MLKEM768, tls.X25519, tls.CurveP256}})
72 scan := scanClientHello(hello)
73 shape := shapeOfClientHello(scan.hello)
74 if !shape.ok || shape.ech {
75 t.Fatalf("%+v", shape)
76 }
77 words := map[string]bool{}
78 for _, g := range shape.groups {
79 words[groupWord(g)] = true
80 }
81 for _, want := range []string{"x25519mlkem768", "x25519", "secp256r1"} {
82 if !words[want] {
83 t.Errorf("offered groups %v lack %s", shape.groups, want)
84 }
85 }
86 m := newMetrics()
87 m.observeClientHello(hello)
88 if m.obsHellos.Load() != 1 || m.obsOffered.get("x25519mlkem768") != 1 || m.obsOffered.get("x25519") != 1 || m.obsECH.Load() != 0 {
89 t.Fatalf("counters: %d hellos, %d hybrid", m.obsHellos.Load(), m.obsOffered.get("x25519mlkem768"))
90 }
91 // a hello that is cut short, or not a hello, is not observed
92 m.observeClientHello(hello[:len(hello)/2])
93 m.observeClientHello([]byte("GET / HTTP/1.1\r\n"))
94 if m.obsHellos.Load() != 1 {
95 t.Fatal("an incomplete hello was observed")
96 }
97}
98
99func TestObserver_ServerHelloOfEveryKindOfHandshake(t *testing.T) {
100 cases := map[string]struct {
101 server, client *tls.Config
102 version, cipher, group string
103 retries int64
104 }{
105 "tls 1.3 with the hybrid group": {
106 &tls.Config{}, &tls.Config{CurvePreferences: []tls.CurveID{tls.X25519MLKEM768, tls.X25519}},
107 "tls13", "tls_aes_128_gcm_sha256", "x25519mlkem768", 0,
108 },
109 "tls 1.3 with x25519 only": {
110 &tls.Config{}, &tls.Config{CurvePreferences: []tls.CurveID{tls.X25519}},
111 "tls13", "tls_aes_128_gcm_sha256", "x25519", 0,
112 },
113 "tls 1.3 with chacha": {
114 &tls.Config{CipherSuites: nil}, &tls.Config{CurvePreferences: []tls.CurveID{tls.CurveP256}},
115 "tls13", "tls_aes_128_gcm_sha256", "secp256r1", 0,
116 },
117 "tls 1.2": {
118 &tls.Config{}, &tls.Config{MaxVersion: tls.VersionTLS12},
119 "tls12", "ecdhe_ecdsa_aes_128_gcm_sha256", "none", 0,
120 },
121 "a hello retry request": {
122 &tls.Config{CurvePreferences: []tls.CurveID{tls.CurveP256}}, &tls.Config{CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256}},
123 "tls13", "tls_aes_128_gcm_sha256", "secp256r1", 1,
124 },
125 }
126 for name, c := range cases {
127 t.Run(name, func(t *testing.T) {
128 scfg := testServerConfig(t)
129 scfg.CurvePreferences, scfg.CipherSuites = c.server.CurvePreferences, c.server.CipherSuites
130 ccfg := &tls.Config{ServerName: "observer.example.test", InsecureSkipVerify: true, CurvePreferences: c.client.CurvePreferences, MaxVersion: c.client.MaxVersion}
131 stream := recordedServerHello(t, scfg, ccfg)
132 // the same bytes, whole and one at a time, give the same reading
133 for _, chunk := range []int{len(stream), 1, 7} {
134 m := newMetrics()
135 w := newServerHelloWatch(m)
136 for off := 0; off < len(stream); off += chunk {
137 w.feed(stream[off:min(off+chunk, len(stream))])
138 }
139 if m.obsVersion.get(c.version) != 1 || m.obsCipher.get(c.cipher) != 1 || m.obsGroup.get(c.group) != 1 || m.obsRetry.Load() != c.retries || m.obsUnparsed.Load() != 0 {
140 t.Fatalf("chunk %d: version %d cipher %d group %d retries %d unparsed %d\n%s", chunk, m.obsVersion.get(c.version), m.obsCipher.get(c.cipher),
141 m.obsGroup.get(c.group), m.obsRetry.Load(), m.obsUnparsed.Load(), m.renderGroups())
142 }
143 if !w.done {
144 t.Fatal("the watcher kept reading after the ServerHello")
145 }
146 }
147 })
148 }
149}
150
151func (m *metrics) renderGroups() string {
152 var b bytes.Buffer
153 m.obsVersion.write(&b)
154 m.obsCipher.write(&b)
155 m.obsGroup.write(&b)
156 return b.String()
157}
158
159func TestObserver_ReadsNothingAfterTheServerHelloAndGivesUpOnWhatIsNotOne(t *testing.T) {
160 scfg := testServerConfig(t)
161 stream := recordedServerHello(t, scfg, &tls.Config{ServerName: "observer.example.test", InsecureSkipVerify: true})
162 m := newMetrics()
163 w := newServerHelloWatch(m)
164 w.feed(stream)
165 if m.obsVersion.get("tls13") != 1 {
166 t.Fatal("not read")
167 }
168 before := len(w.buf)
169 w.feed(bytes.Repeat([]byte{22, 3, 3, 0, 4, 2, 0, 0, 0}, 100)) // more "hellos": never looked at
170 if m.obsVersion.get("tls13") != 1 || len(w.buf) != before {
171 t.Fatal("the watcher read on after the ServerHello")
172 }
173 for name, first := range map[string][]byte{
174 "an HTTP answer": []byte("HTTP/1.1 200 OK\r\n\r\n"),
175 "an alert": {21, 3, 3, 0, 2, 2, 40},
176 "application data": {23, 3, 3, 0, 5, 1, 2, 3, 4, 5},
177 "a ServerHello that lies": {22, 3, 3, 0, 8, 2, 0, 0, 40, 1, 2, 3, 4},
178 "a certificate where a hello": {22, 3, 3, 0, 6, 11, 0, 0, 2, 0, 0},
179 } {
180 m := newMetrics()
181 w := newServerHelloWatch(m)
182 w.feed(first)
183 if m.obsUnparsed.Load() != 1 || !w.done {
184 t.Errorf("%s: unparsed %d, done %v", name, m.obsUnparsed.Load(), w.done)
185 }
186 w.feed(first)
187 if m.obsUnparsed.Load() != 1 {
188 t.Errorf("%s: counted twice", name)
189 }
190 }
191 // a stream that never shows a hello in the observer's budget of bytes stops being watched
192 m2 := newMetrics()
193 w2 := newServerHelloWatch(m2)
194 for i := 0; i < 20 && !w2.done; i++ {
195 w2.feed(append([]byte{22, 3, 3, 0x03, 0xe8}, make([]byte, 500)...)) // a record that never completes
196 }
197 if !w2.done || m2.obsUnparsed.Load() != 1 || len(w2.buf) > maxObserved {
198 t.Fatalf("done %v unparsed %d buffered %d", w2.done, m2.obsUnparsed.Load(), len(w2.buf))
199 }
200}
201
202func TestObserver_NeverPanicsAndNeverReadsOutsideItsInput(t *testing.T) {
203 rng := rand.New(rand.NewPCG(7, 11))
204 hello := captureClientHello(t, &tls.Config{ServerName: "dev1.example.test", InsecureSkipVerify: true})
205 scfg := testServerConfig(t)
206 server := recordedServerHello(t, scfg, &tls.Config{ServerName: "observer.example.test", InsecureSkipVerify: true})
207 m := newMetrics()
208 for i := 0; i < 3000; i++ {
209 b := make([]byte, rng.IntN(400))
210 for j := range b {
211 b[j] = byte(rng.IntN(256))
212 }
213 shapeOfClientHello(b)
214 _, _ = parseServerHello(b)
215 newServerHelloWatch(m).feed(b)
216 // a real hello with bytes flipped and with its tail cut
217 mut := append([]byte(nil), hello...)
218 for k := 0; k < 3; k++ {
219 mut[rng.IntN(len(mut))] ^= byte(1 << rng.IntN(8))
220 }
221 m.observeClientHello(mut[:rng.IntN(len(mut))+1])
222 sv := append([]byte(nil), server...)
223 sv[rng.IntN(len(sv))] ^= byte(1 << rng.IntN(8))
224 w := newServerHelloWatch(m)
225 w.feed(sv[:rng.IntN(len(sv))+1])
226 }
227}
228
229func TestObserver_GREASEIsNotCountedAndWordsAreAClosedList(t *testing.T) {
230 for _, g := range []uint16{0x0a0a, 0x1a1a, 0xfafa} {
231 if !isGREASE(g) {
232 t.Errorf("%#x is GREASE", g)
233 }
234 }
235 if isGREASE(0x001d) || isGREASE(0x11ec) {
236 t.Error("a real group was taken for GREASE")
237 }
238 m := newMetrics()
239 m.obsOffered.inc("a group nobody has heard of")
240 m.obsOffered.inc("another")
241 if m.obsOffered.get("other") != 2 {
242 t.Fatal("an unknown word is counted as other")
243 }
244 if groupWord(0x1234) != "other" || cipherWord(0x1234) != "other" || versionWord(0x7f1c) != "other" || groupWord(0x0100) != "ffdhe" {
245 t.Fatal("the word tables")
246 }
247}
248
249func TestNode_ObserverReportsTLSVersionCipherAndGroupForAGoClientIncludingTheHybrid(t *testing.T) {
250 e := newTestEnv(t, nil)
251 d := e.newDevice()
252 dc := e.connectOK(d)
253 dc.serve(nil)
254 c, err := tls.DialWithDialer(&net.Dialer{Timeout: 3 * time.Second}, "tcp", e.httpsAddr, &tls.Config{
255 ServerName: d.host, RootCAs: d.pool, NextProtos: []string{"http/1.1"}, MinVersion: tls.VersionTLS13,
256 CurvePreferences: []tls.CurveID{tls.X25519MLKEM768, tls.X25519},
257 })
258 if err != nil {
259 t.Fatal(err)
260 }
261 if st := c.ConnectionState(); st.Version != tls.VersionTLS13 {
262 t.Fatalf("the Go handshake itself: version %#x", st.Version)
263 }
264 if _, err := postOver(c, d.host, []byte("x")); err != nil {
265 t.Fatal(err)
266 }
267 c.Close()
268 m := e.srv.met
269 if m.obsHellos.Load() != 1 || m.obsOffered.get("x25519mlkem768") != 1 || m.obsOffered.get("x25519") != 1 {
270 t.Fatalf("offered:\n%s", e.srv.renderMetrics())
271 }
272 if m.obsVersion.get("tls13") != 1 || m.obsCipher.get("tls_aes_128_gcm_sha256") != 1 || m.obsGroup.get("x25519mlkem768") != 1 {
273 t.Fatalf("selected:\n%s", e.srv.renderMetrics())
274 }
275 // the node's own conversation with a device (its name, its certificate) is not observed: it is the node's TLS, not a client's
276 if m.obsHellos.Load() != 1 {
277 t.Fatalf("the device's own connections were observed: %d", m.obsHellos.Load())
278 }
279}
280
281func openSSLWith(t *testing.T, group string) string {
282 t.Helper()
283 for _, bin := range []string{"/opt/homebrew/bin/openssl", "openssl"} {
284 path, err := exec.LookPath(bin)
285 if err != nil {
286 continue
287 }
288 out, err := exec.Command(path, "list", "-tls-groups").CombinedOutput()
289 if err == nil && strings.Contains(strings.ToLower(string(out)), strings.ToLower(group)) {
290 return path
291 }
292 }
293 return ""
294}
295
296func TestNode_ObserverReportsTheGroupsAnOpenSSLClientOffersAndWhatTheServerSelects(t *testing.T) {
297 bin := openSSLWith(t, "X25519MLKEM768")
298 if bin == "" {
299 t.Skip("no OpenSSL with X25519MLKEM768 on this machine")
300 }
301 for name, c := range map[string]struct {
302 groups string
303 offered []string
304 chosen string
305 }{
306 "the hybrid first": {"X25519MLKEM768:X25519", []string{"x25519mlkem768", "x25519"}, "x25519mlkem768"},
307 "x25519 and p-256": {"X25519:P-256", []string{"x25519", "secp256r1"}, "x25519"},
308 "p-256 only": {"P-256", []string{"secp256r1"}, "secp256r1"},
309 } {
310 t.Run(name, func(t *testing.T) {
311 e := newTestEnv(t, nil)
312 d := e.newDevice()
313 dc := e.connectOK(d)
314 dc.serve(nil)
315 ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
316 defer cancel()
317 cmd := exec.CommandContext(ctx, bin, "s_client", "-connect", e.httpsAddr, "-servername", d.host, "-tls1_3", "-groups", c.groups)
318 cmd.Stdin = strings.NewReader("")
319 out, _ := cmd.CombinedOutput()
320 if !strings.Contains(string(out), "TLSv1.3") {
321 t.Fatalf("OpenSSL did not complete a TLS 1.3 handshake:\n%s", out)
322 }
323 eventually(t, "the observer saw the handshake", 5*time.Second, func() bool { return e.srv.met.obsHellos.Load() == 1 && e.srv.met.obsVersion.get("tls13") == 1 })
324 for _, g := range c.offered {
325 if e.srv.met.obsOffered.get(g) != 1 {
326 t.Errorf("OpenSSL offered %s and the node did not count it:\n%s", g, e.srv.renderMetrics())
327 }
328 }
329 if e.srv.met.obsGroup.get(c.chosen) != 1 {
330 t.Errorf("the selected group should be %s:\n%s", c.chosen, e.srv.renderMetrics())
331 }
332 })
333 }
334}
335
336// helloWithExtensions builds a ClientHello for host with the extensions of the test (supported_groups, an ECH extension, GREASE).
337func helloWithExtensions(host string, groups []uint16, ech bool) []byte {
338 ext := sniExt(host)
339 if len(groups) > 0 {
340 list := []byte{byte(len(groups) * 2 >> 8), byte(len(groups) * 2)}
341 for _, g := range groups {
342 list = append(list, byte(g>>8), byte(g))
343 }
344 ext = append(append([]byte{0, 10, byte(len(list) >> 8), byte(len(list))}, list...), ext...)
345 }
346 if ech {
347 ext = append([]byte{0xfe, 0x0d, 0, 4, 1, 2, 3, 4}, ext...)
348 }
349 var body []byte
350 body = append(body, 3, 3)
351 body = append(body, make([]byte, 32)...)
352 body = append(body, 0)
353 body = append(body, 0, 2, 0x13, 0x01)
354 body = append(body, 1, 0)
355 body = append(body, byte(len(ext)>>8), byte(len(ext)))
356 body = append(body, ext...)
357 hs := append([]byte{1, byte(len(body) >> 16), byte(len(body) >> 8), byte(len(body))}, body...)
358 return append([]byte{22, 3, 1, byte(len(hs) >> 8), byte(len(hs))}, hs...)
359}
360
361func TestNode_ObserverCountsAnECHExtensionGREASEAndAServerThatAnswersWithoutTLS(t *testing.T) {
362 e := newTestEnv(t, nil)
363 d := e.newDevice()
364 dc := e.connectOK(d)
365 dc.serve(func(c net.Conn) { _, _ = c.Write([]byte("HTTP/1.1 400 not tls\r\n\r\n")) })
366 raw := sendRaw(t, e, helloWithExtensions(d.host, []uint16{0x0a0a, 0x11ec, 0x001d, 0x001d, 0x9999}, true), 0)
367 _ = raw.SetReadDeadline(time.Now().Add(3 * time.Second))
368 buf := make([]byte, 64)
369 _, _ = raw.Read(buf)
370 eventually(t, "the hello observed", 3*time.Second, func() bool { return e.srv.met.obsHellos.Load() == 1 })
371 m := e.srv.met
372 if m.obsECH.Load() != 1 {
373 t.Fatalf("ECH: %d", m.obsECH.Load())
374 }
375 if m.obsOffered.get("x25519mlkem768") != 1 || m.obsOffered.get("x25519") != 1 || m.obsOffered.get("other") != 1 {
376 t.Fatalf("a group listed twice counts once, GREASE not at all, an unknown one as other:\n%s", e.srv.renderMetrics())
377 }
378 eventually(t, "the answer that was not a ServerHello", 3*time.Second, func() bool { return m.obsUnparsed.Load() == 1 })
379}