1package traffic
2
3import (
4 "bufio"
5 "context"
6 "crypto/ecdsa"
7 "crypto/elliptic"
8 "crypto/rand"
9 "crypto/tls"
10 "crypto/x509"
11 "crypto/x509/pkix"
12 "encoding/pem"
13 "errors"
14 "fmt"
15 "io"
16 "math/big"
17 "net"
18 "net/http"
19 "strings"
20 "testing"
21 "time"
22
23 "github.com/stretchr/testify/assert"
24 "github.com/stretchr/testify/require"
25
26 "github.com/bnema/gordon/internal/domain"
27)
28
29func TestClientHelloTooLargeUsesDomainSentinel(t *testing.T) {
30 client, server := net.Pipe()
31 go func() {
32 _, _ = client.Write(clientHelloBytes(t, "large.example.com"))
33 _ = client.Close()
34 }()
35 _, _, err := peekClientHelloSNIWithLimit(server, 1)
36 require.Error(t, err)
37 assert.True(t, errors.Is(err, domain.ErrClientHelloTooLarge))
38 _ = server.Close()
39}
40
41func TestClientHelloPeek(t *testing.T) {
42 t.Run("valid sni returns replayable bytes", func(t *testing.T) {
43 sni, replayed := peekHelloFromTLSClient(t, "raw.example.com")
44 assert.Equal(t, "raw.example.com", sni)
45 buf := make([]byte, 5)
46 n, err := replayed.Read(buf)
47 require.NoError(t, err)
48 assert.Equal(t, 5, n)
49 assert.Equal(t, byte(22), buf[0])
50 })
51
52 t.Run("no sni", func(t *testing.T) {
53 sni, _ := peekHelloFromTLSClient(t, "")
54 assert.Empty(t, sni)
55 })
56
57 t.Run("malformed", func(t *testing.T) {
58 client, server := net.Pipe()
59 go func() {
60 _, _ = client.Write([]byte("not tls"))
61 _ = client.Close()
62 }()
63 _, _, err := peekClientHelloSNI(server)
64 require.Error(t, err)
65 })
66
67 t.Run("fragmented", func(t *testing.T) {
68 hello := clientHelloBytes(t, "fragmented.example.com")
69 client, server := net.Pipe()
70 go func() {
71 for _, b := range hello {
72 _, _ = client.Write([]byte{b})
73 }
74 _ = client.Close()
75 }()
76 sni, replayed, err := peekClientHelloSNI(server)
77 require.NoError(t, err)
78 assert.Equal(t, "fragmented.example.com", sni)
79 buf := make([]byte, len(hello))
80 _, err = io.ReadFull(replayed, buf)
81 require.NoError(t, err)
82 assert.Equal(t, hello, buf)
83 })
84
85 t.Run("client_hello_across_multiple_tls_records", func(t *testing.T) {
86 hello := splitClientHelloAcrossRecords(t, clientHelloBytes(t, "multi-record.example.com"), 10)
87 client, server := net.Pipe()
88 go func() {
89 _, _ = client.Write(hello)
90 _ = client.Close()
91 }()
92 sni, replayed, err := peekClientHelloSNI(server)
93 require.NoError(t, err)
94 assert.Equal(t, "multi-record.example.com", sni)
95 buf := make([]byte, len(hello))
96 _, err = io.ReadFull(replayed, buf)
97 require.NoError(t, err)
98 assert.Equal(t, hello, buf)
99 })
100
101 t.Run("client_hello_header_across_multiple_tls_records", func(t *testing.T) {
102 hello := splitClientHelloAcrossRecords(t, clientHelloBytes(t, "split-header.example.com"), 2)
103 client, server := net.Pipe()
104 go func() {
105 _, _ = client.Write(hello)
106 _ = client.Close()
107 }()
108 sni, replayed, err := peekClientHelloSNI(server)
109 require.NoError(t, err)
110 assert.Equal(t, "split-header.example.com", sni)
111 buf := make([]byte, len(hello))
112 _, err = io.ReadFull(replayed, buf)
113 require.NoError(t, err)
114 assert.Equal(t, hello, buf)
115 })
116}
117
118func splitClientHelloAcrossRecords(t *testing.T, hello []byte, firstPayloadLen int) []byte {
119 t.Helper()
120 require.GreaterOrEqual(t, len(hello), 15)
121 require.Equal(t, byte(22), hello[0])
122 recordLen := int(hello[3])<<8 | int(hello[4])
123 require.Greater(t, recordLen, firstPayloadLen)
124 first := append([]byte{}, hello[:5]...)
125 first[3] = byte(firstPayloadLen >> 8)
126 first[4] = byte(firstPayloadLen)
127 first = append(first, hello[5:5+firstPayloadLen]...)
128 remainingPayloadLen := recordLen - firstPayloadLen
129 second := append([]byte{22, hello[1], hello[2], byte(remainingPayloadLen >> 8), byte(remainingPayloadLen)}, hello[5+firstPayloadLen:5+recordLen]...)
130 out := append(first, second...)
131 out = append(out, hello[5+recordLen:]...)
132 return out
133}
134
135func TestTLSPassthrough(t *testing.T) {
136 backendA := startTLSGreetingServer(t, "backend-a", "backend-a\n")
137 backendB := startTLSGreetingServer(t, "backend-b", "backend-b\n")
138 backendExact := startTLSGreetingServer(t, "backend-exact", "backend-exact\n")
139
140 graph := tlsGraph(t, freeTCPAddress(t), []tlsRoute{
141 {name: "raw", sni: "raw.example.com", service: "raw", backend: backendA.address},
142 {name: "wild", sni: "*.example.com", service: "wild", backend: backendB.address},
143 {name: "exact", sni: "api.example.com", service: "exact", backend: backendExact.address},
144 })
145 manager := NewManager()
146 require.NoError(t, manager.Apply(context.Background(), &graph))
147 defer shutdownManager(t, manager)
148
149 assertTLSGreeting(t, graph.EntryPoints[0].Address, "raw.example.com", "backend-a\n")
150 assertTLSGreeting(t, graph.EntryPoints[0].Address, "shop.example.com", "backend-b\n")
151 assertTLSGreeting(t, graph.EntryPoints[0].Address, "api.example.com", "backend-exact\n")
152}
153
154func TestTLSMuxUnknownClosesWithoutHTTPSRoute(t *testing.T) {
155 backend := startTLSGreetingServer(t, "backend", "backend\n")
156 graph := tlsGraph(t, freeTCPAddress(t), []tlsRoute{{name: "raw", sni: "raw.example.com", service: "raw", backend: backend.address}})
157 manager := NewManager()
158 require.NoError(t, manager.Apply(context.Background(), &graph))
159 defer shutdownManager(t, manager)
160
161 _, err := tls.DialWithDialer(&net.Dialer{Timeout: time.Second}, "tcp", graph.EntryPoints[0].Address, &tls.Config{ServerName: "unknown.example.com", InsecureSkipVerify: true})
162 require.Error(t, err)
163 assert.Eventually(t, func() bool { return manager.Status().Counters.TotalRefused == 1 }, time.Second, 10*time.Millisecond)
164}
165
166func TestTLSMuxIncompleteClientHelloDoesNotHangShutdown(t *testing.T) {
167 graph := domain.TrafficGraph{
168 Options: domain.TrafficOptions{TCP: domain.TCPOptions{DialTimeout: time.Second, IdleTimeout: time.Minute, DrainTimeout: 50 * time.Millisecond}},
169 EntryPoints: []domain.EntryPoint{{Name: "websecure", Address: freeTCPAddress(t), Protocol: domain.EntryPointProtocolTLSMux}},
170 }
171 manager := NewManager()
172 require.NoError(t, manager.Apply(context.Background(), &graph))
173
174 conn := dialTCP(t, graph.EntryPoints[0].Address)
175 _, err := conn.Write([]byte{22})
176 require.NoError(t, err)
177
178 ctx, cancel := context.WithTimeout(context.Background(), 250*time.Millisecond)
179 defer cancel()
180 started := time.Now()
181 require.NoError(t, manager.Shutdown(ctx))
182 assert.Less(t, time.Since(started), 250*time.Millisecond)
183 _ = conn.Close()
184}
185
186func TestTLSMuxHTTPServerRefreshesActiveRuntime(t *testing.T) {
187 graph := tlsGraph(t, freeTCPAddress(t), nil)
188 manager := NewManager()
189 manager.SetTLSHTTPServer("websecure", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
190 _, _ = w.Write([]byte("before"))
191 }), testTLSConfig(t, "app.example.com"))
192 require.NoError(t, manager.Apply(context.Background(), &graph))
193 defer shutdownManager(t, manager)
194 assertHTTPSBody(t, graph.EntryPoints[0].Address, "app.example.com", "before")
195
196 manager.SetTLSHTTPServer("websecure", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
197 _, _ = w.Write([]byte("after"))
198 }), testTLSConfig(t, "app.example.com"))
199
200 assertHTTPSBody(t, graph.EntryPoints[0].Address, "app.example.com", "after")
201}
202
203func TestTLSHTTPListenerServeRejectsAfterClose(t *testing.T) {
204 for range 1000 {
205 addr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:443")
206 require.NoError(t, err)
207 listener := newTLSHTTPListener(addr)
208 require.NoError(t, listener.Close())
209 client, server := net.Pipe()
210 accepted := listener.serve(server)
211 _ = client.Close()
212 _ = server.Close()
213 require.False(t, accepted)
214 }
215}
216
217func TestTLSHTTPListenerCloseDrainsQueuedConnections(t *testing.T) {
218 addr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:443")
219 require.NoError(t, err)
220 listener := newTLSHTTPListener(addr)
221 client, server := net.Pipe()
222 defer client.Close()
223 require.True(t, listener.serve(server))
224 require.NoError(t, listener.Close())
225 _, err = listener.Accept()
226 require.ErrorIs(t, err, net.ErrClosed)
227 _, err = server.Write([]byte("closed"))
228 require.Error(t, err)
229}
230
231func TestTLSMuxHTTPSRoute(t *testing.T) {
232 backend := startTLSGreetingServer(t, "raw-backend", "raw\n")
233 graph := tlsGraph(t, freeTCPAddress(t), []tlsRoute{{name: "raw", sni: "raw.example.com", service: "raw", backend: backend.address}})
234 manager := NewManager()
235 manager.SetTLSHTTPServer("websecure", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
236 _, _ = w.Write([]byte("https route"))
237 }), testTLSConfig(t, "app.example.com"))
238 require.NoError(t, manager.Apply(context.Background(), &graph))
239 defer shutdownManager(t, manager)
240
241 assertTLSGreeting(t, graph.EntryPoints[0].Address, "raw.example.com", "raw\n")
242 assertHTTPSBody(t, graph.EntryPoints[0].Address, "app.example.com", "https route")
243 assertHTTPSBodyNoSNI(t, graph.EntryPoints[0].Address, "https route")
244
245 conn := dialTCP(t, graph.EntryPoints[0].Address)
246 _, err := conn.Write([]byte("malformed"))
247 require.NoError(t, err)
248 _ = conn.Close()
249 assert.Eventually(t, func() bool { return manager.Status().Counters.TotalErrors >= 1 }, time.Second, 10*time.Millisecond)
250}
251
252func TestTLSMuxHTTP2Route(t *testing.T) {
253 graph := tlsGraph(t, freeTCPAddress(t), nil)
254 manager := NewManager()
255 tlsConfig := testTLSConfig(t, "app.example.com")
256 tlsConfig.NextProtos = []string{"h2", "http/1.1"}
257 manager.SetTLSHTTPServer("websecure", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
258 _, _ = w.Write([]byte(r.Proto))
259 }), tlsConfig)
260 require.NoError(t, manager.Apply(context.Background(), &graph))
261 defer shutdownManager(t, manager)
262
263 assertHTTPSBody(t, graph.EntryPoints[0].Address, "app.example.com", "HTTP/2.0")
264}
265
266func peekHelloFromTLSClient(t *testing.T, serverName string) (string, net.Conn) {
267 t.Helper()
268 client, server := net.Pipe()
269 done := make(chan struct{})
270 go func() {
271 defer close(done)
272 _ = tls.Client(client, &tls.Config{ServerName: serverName, InsecureSkipVerify: true}).Handshake()
273 _ = client.Close()
274 }()
275 sni, replayed, err := peekClientHelloSNI(server)
276 require.NoError(t, err)
277 t.Cleanup(func() {
278 _ = replayed.Close()
279 <-done
280 })
281 return sni, replayed
282}
283
284func clientHelloBytes(t *testing.T, serverName string) []byte {
285 t.Helper()
286 _, replayed := peekHelloFromTLSClient(t, serverName)
287 buf := make([]byte, maxClientHelloBytes)
288 n, err := replayed.Read(buf)
289 require.NoError(t, err)
290 return append([]byte{}, buf[:n]...)
291}
292
293type tlsRoute struct {
294 name string
295 sni string
296 service string
297 backend string
298}
299
300func tlsGraph(t *testing.T, listenAddress string, routes []tlsRoute) domain.TrafficGraph {
301 t.Helper()
302 graph := domain.TrafficGraph{
303 Options: domain.TrafficOptions{TCP: domain.TCPOptions{DialTimeout: time.Second, IdleTimeout: time.Minute, DrainTimeout: time.Second}},
304 EntryPoints: []domain.EntryPoint{{Name: "websecure", Address: listenAddress, Protocol: domain.EntryPointProtocolTLSMux}},
305 }
306 for _, route := range routes {
307 backend, err := backendFromAddress(route.service+":tls", route.backend)
308 require.NoError(t, err)
309 ref := serviceRef(route.service, "tls")
310 graph.Routers = append(graph.Routers, domain.TrafficRouter{Name: route.name, EntryPoint: "websecure", Protocol: domain.RouterProtocolTLSPassthrough, Rule: domain.TrafficRule{SNI: route.sni}, Service: ref})
311 graph.Services = append(graph.Services, domain.TrafficService{Name: ref, Backends: []domain.TrafficBackend{backend}})
312 }
313 require.NoError(t, graph.Validate())
314 return graph
315}
316
317func assertTLSGreeting(t *testing.T, address string, serverName string, want string) {
318 t.Helper()
319 conn, err := tls.DialWithDialer(&net.Dialer{Timeout: time.Second}, "tcp", address, &tls.Config{ServerName: serverName, InsecureSkipVerify: true})
320 require.NoError(t, err)
321 defer conn.Close()
322 line, err := bufio.NewReader(conn).ReadString('\n')
323 require.NoError(t, err)
324 assert.Equal(t, want, line)
325}
326
327func assertHTTPSBody(t *testing.T, address string, serverName string, wantBody string) {
328 t.Helper()
329 transport := &http.Transport{
330 TLSClientConfig: &tls.Config{ServerName: serverName, InsecureSkipVerify: true},
331 ForceAttemptHTTP2: true,
332 }
333 client := &http.Client{Transport: transport, Timeout: time.Second}
334 defer transport.CloseIdleConnections()
335
336 req, err := http.NewRequest(http.MethodGet, "https://"+address+"/", nil)
337 require.NoError(t, err)
338 req.Host = serverName
339 resp, err := client.Do(req)
340 require.NoError(t, err)
341 defer resp.Body.Close()
342 body, err := io.ReadAll(resp.Body)
343 require.NoError(t, err)
344 assert.Contains(t, string(body), wantBody)
345}
346
347func assertHTTPSBodyNoSNI(t *testing.T, address string, wantBody string) {
348 t.Helper()
349 conn, err := tls.DialWithDialer(&net.Dialer{Timeout: time.Second}, "tcp", address, &tls.Config{InsecureSkipVerify: true})
350 require.NoError(t, err)
351 defer conn.Close()
352 _, err = conn.Write([]byte("GET / HTTP/1.1\r\nHost: app.example.com\r\nConnection: close\r\n\r\n"))
353 require.NoError(t, err)
354 body, err := io.ReadAll(conn)
355 require.NoError(t, err)
356 assert.Contains(t, string(body), wantBody)
357}
358
359type tlsGreetingServer struct{ address string }
360
361func startTLSGreetingServer(t *testing.T, commonName string, greeting string) tlsGreetingServer {
362 t.Helper()
363 listener, err := tls.Listen("tcp", "127.0.0.1:0", testTLSConfig(t, commonName))
364 require.NoError(t, err)
365 done := make(chan struct{})
366 go func() {
367 defer close(done)
368 for {
369 conn, err := listener.Accept()
370 if err != nil {
371 return
372 }
373 go func() {
374 defer conn.Close()
375 _, _ = conn.Write([]byte(greeting))
376 }()
377 }
378 }()
379 t.Cleanup(func() {
380 _ = listener.Close()
381 <-done
382 })
383 return tlsGreetingServer{address: listener.Addr().String()}
384}
385
386func testTLSConfig(t *testing.T, commonName string) *tls.Config {
387 t.Helper()
388 cert, err := testCertificate(commonName)
389 require.NoError(t, err)
390 return &tls.Config{Certificates: []tls.Certificate{cert}, MinVersion: tls.VersionTLS12}
391}
392
393func testCertificate(commonName string) (tls.Certificate, error) {
394 key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
395 if err != nil {
396 return tls.Certificate{}, fmt.Errorf("generate test certificate key: %w", err)
397 }
398 serial, err := rand.Int(rand.Reader, big.NewInt(1<<62))
399 if err != nil {
400 return tls.Certificate{}, fmt.Errorf("generate test certificate serial: %w", err)
401 }
402 template := x509.Certificate{
403 SerialNumber: serial,
404 Subject: pkix.Name{CommonName: commonName},
405 DNSNames: []string{commonName},
406 NotBefore: time.Now().Add(-time.Hour),
407 NotAfter: time.Now().Add(time.Hour),
408 KeyUsage: x509.KeyUsageDigitalSignature,
409 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
410 }
411 der, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
412 if err != nil {
413 return tls.Certificate{}, fmt.Errorf("create test certificate: %w", err)
414 }
415 certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
416 keyBytes, err := x509.MarshalECPrivateKey(key)
417 if err != nil {
418 return tls.Certificate{}, fmt.Errorf("marshal test certificate key: %w", err)
419 }
420 keyPEM := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes})
421 cert, err := tls.X509KeyPair(certPEM, keyPEM)
422 if err != nil {
423 return tls.Certificate{}, fmt.Errorf("parse test certificate key pair: %w", err)
424 }
425 return cert, nil
426}
427
428func TestHostMatchesTLSWildcard(t *testing.T) {
429 assert.True(t, hostMatchesTLSWildcard("api.example.com", "*.example.com"))
430 assert.False(t, hostMatchesTLSWildcard("example.com", "*.example.com"))
431 assert.False(t, hostMatchesTLSWildcard("v1.api.example.com", "*.example.com"))
432 assert.False(t, hostMatchesTLSWildcard("api.other.com", "*.example.com"))
433 assert.True(t, strings.EqualFold(normalizeTLSName("API.EXAMPLE.COM."), "api.example.com"))
434}