v3.0.0 f19f453

tls_mux_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close