v2.31.0-dev23 58a92f3

tls_mux.go RawHistoryFolder

1package traffic
2
3import (
4	"bytes"
5	"crypto/tls"
6	"fmt"
7	"maps"
8	"net"
9	"net/http"
10	"strings"
11	"sync"
12	"time"
13
14	"github.com/bnema/gordon/internal/domain"
15)
16
17const (
18	maxClientHelloBytes    = 64 << 10
19	clientHelloReadTimeout = 250 * time.Millisecond
20)
21
22// TLSHTTPServerConfig describes the HTTPS server attached to a tls_mux entrypoint.
23type TLSHTTPServerConfig struct {
24	Handler   http.Handler
25	TLSConfig *tls.Config
26}
27
28type tlsHTTPServers map[string]TLSHTTPServerConfig
29
30// SetTLSHTTPServer installs or removes the HTTPS server for a tls_mux entrypoint.
31func (m *Manager) SetTLSHTTPServer(entryPoint string, handler http.Handler, tlsConfig *tls.Config) {
32	m.mu.Lock()
33	current := m.loadTLSHTTPServers()
34	next := make(tlsHTTPServers, len(current)+1)
35	maps.Copy(next, current)
36	if handler == nil || tlsConfig == nil {
37		delete(next, entryPoint)
38	} else {
39		next[entryPoint] = TLSHTTPServerConfig{Handler: handler, TLSConfig: tlsConfig.Clone()}
40	}
41	m.tlsHTTPServers.Store(next)
42	runtimes := make([]*entryPointRuntime, 0, len(m.listeners))
43	for _, runtime := range m.listeners {
44		if runtime.entryPointSnapshot().Name == entryPoint {
45			runtimes = append(runtimes, runtime)
46		}
47	}
48	m.mu.Unlock()
49
50	for _, runtime := range runtimes {
51		runtime.refreshTLSHTTPServer(entryPoint)
52	}
53}
54
55func (m *Manager) tlsHTTPServer(entryPoint string) (TLSHTTPServerConfig, bool) {
56	config, ok := m.loadTLSHTTPServers()[entryPoint]
57	return config, ok
58}
59
60func (m *Manager) loadTLSHTTPServers() tlsHTTPServers {
61	value := m.tlsHTTPServers.Load()
62	if value == nil {
63		return tlsHTTPServers{}
64	}
65	return value.(tlsHTTPServers)
66}
67
68func peekClientHelloSNI(conn net.Conn) (string, net.Conn, error) {
69	return peekClientHelloSNIWithLimit(conn, maxClientHelloBytes)
70}
71
72func peekClientHelloSNIWithLimit(conn net.Conn, maxBytes int) (string, net.Conn, error) {
73	if err := conn.SetReadDeadline(time.Now().Add(clientHelloReadTimeout)); err != nil {
74		return "", nil, fmt.Errorf("set client hello read deadline: %w", err)
75	}
76	defer func() { _ = conn.SetReadDeadline(time.Time{}) }()
77
78	buf := make([]byte, 0, 4096)
79	for len(buf) < maxBytes {
80		remaining := maxBytes - len(buf)
81		readSize := min(1024, remaining)
82		tmp := make([]byte, readSize)
83		n, err := conn.Read(tmp)
84		if n > 0 {
85			buf = append(buf, tmp[:n]...)
86			sni, complete, parseErr := parseClientHelloSNI(buf)
87			if parseErr != nil {
88				return "", nil, fmt.Errorf("parse client hello sni: %w", parseErr)
89			}
90			if complete {
91				return sni, replayConn{Conn: conn, reader: bytes.NewReader(buf)}, nil
92			}
93		}
94		if err != nil {
95			return "", nil, fmt.Errorf("read client hello: %w", err)
96		}
97	}
98	return "", nil, fmt.Errorf("%w: %d bytes", domain.ErrClientHelloTooLarge, maxBytes)
99}
100
101func parseClientHelloSNI(data []byte) (string, bool, error) {
102	if len(data) < 5 {
103		return "", false, nil
104	}
105	if data[0] != 22 {
106		return "", false, fmt.Errorf("tls record is not a handshake")
107	}
108	record, consumed, complete, err := tlsRecordPayload(data, 0)
109	if err != nil || !complete {
110		return "", complete, err
111	}
112	handshake := append([]byte(nil), record...)
113	for len(handshake) < 4 {
114		if len(data) < consumed+5 {
115			return "", false, nil
116		}
117		next, nextConsumed, nextComplete, err := tlsRecordPayload(data, consumed)
118		if err != nil || !nextComplete {
119			return "", nextComplete, err
120		}
121		handshake = append(handshake, next...)
122		consumed = nextConsumed
123	}
124	if handshake[0] != 1 {
125		return "", true, fmt.Errorf("tls handshake is not client hello")
126	}
127	handshakeLen := int(handshake[1])<<16 | int(handshake[2])<<8 | int(handshake[3])
128	for len(handshake)-4 < handshakeLen {
129		if len(data) < consumed+5 {
130			return "", false, nil
131		}
132		next, nextConsumed, nextComplete, err := tlsRecordPayload(data, consumed)
133		if err != nil || !nextComplete {
134			return "", nextComplete, err
135		}
136		handshake = append(handshake, next...)
137		consumed = nextConsumed
138	}
139	sni, err := parseClientHelloExtensions(handshake[4 : 4+handshakeLen])
140	return sni, true, err
141}
142
143func tlsRecordPayload(data []byte, offset int) ([]byte, int, bool, error) {
144	if data[offset] != 22 {
145		return nil, offset, false, fmt.Errorf("tls record is not a handshake")
146	}
147	recordLen := int(data[offset+3])<<8 | int(data[offset+4])
148	if recordLen <= 0 {
149		return nil, offset, false, fmt.Errorf("tls handshake record is empty")
150	}
151	end := offset + 5 + recordLen
152	if len(data) < end {
153		return nil, offset, false, nil
154	}
155	return data[offset+5 : end], end, true, nil
156}
157
158func parseClientHelloExtensions(body []byte) (string, error) {
159	reader := bytes.NewReader(body)
160	if reader.Len() < 34 {
161		return "", fmt.Errorf("client hello is truncated")
162	}
163	_, _ = readBytes(reader, 34) // legacy_version + random
164	if err := skipLengthPrefixed(reader, 1); err != nil {
165		return "", err
166	}
167	if err := skipLengthPrefixed(reader, 2); err != nil {
168		return "", err
169	}
170	if err := skipLengthPrefixed(reader, 1); err != nil {
171		return "", err
172	}
173	if reader.Len() == 0 {
174		return "", nil
175	}
176	extLen, err := readUint16(reader)
177	if err != nil {
178		return "", err
179	}
180	if extLen > reader.Len() {
181		return "", fmt.Errorf("client hello extensions are truncated")
182	}
183	extBytes, _ := readBytes(reader, extLen)
184	extensions := bytes.NewReader(extBytes)
185	for extensions.Len() > 0 {
186		extType, err := readUint16(extensions)
187		if err != nil {
188			return "", err
189		}
190		extDataLen, err := readUint16(extensions)
191		if err != nil {
192			return "", err
193		}
194		if extDataLen > extensions.Len() {
195			return "", fmt.Errorf("client hello extension is truncated")
196		}
197		extData, _ := readBytes(extensions, extDataLen)
198		if extType == 0 {
199			return parseSNIExtension(extData)
200		}
201	}
202	return "", nil
203}
204
205func parseSNIExtension(data []byte) (string, error) {
206	reader := bytes.NewReader(data)
207	listLen, err := readUint16(reader)
208	if err != nil {
209		return "", err
210	}
211	if listLen > reader.Len() {
212		return "", fmt.Errorf("sni extension is truncated")
213	}
214	listBytes, _ := readBytes(reader, listLen)
215	list := bytes.NewReader(listBytes)
216	for list.Len() > 0 {
217		nameType, err := list.ReadByte()
218		if err != nil {
219			return "", err
220		}
221		nameLen, err := readUint16(list)
222		if err != nil {
223			return "", err
224		}
225		if nameLen > list.Len() {
226			return "", fmt.Errorf("sni name is truncated")
227		}
228		nameBytes, _ := readBytes(list, nameLen)
229		name := string(nameBytes)
230		if nameType == 0 {
231			return normalizeTLSName(name), nil
232		}
233	}
234	return "", nil
235}
236
237func skipLengthPrefixed(reader *bytes.Reader, size int) error {
238	length, err := readLength(reader, size)
239	if err != nil {
240		return err
241	}
242	if length > reader.Len() {
243		return fmt.Errorf("client hello field is truncated")
244	}
245	_, _ = readBytes(reader, length)
246	return nil
247}
248
249func readBytes(reader *bytes.Reader, size int) ([]byte, error) {
250	if reader.Len() < size {
251		return nil, fmt.Errorf("client hello field is truncated")
252	}
253	value := make([]byte, size)
254	_, _ = reader.Read(value)
255	return value, nil
256}
257
258func readLength(reader *bytes.Reader, size int) (int, error) {
259	if reader.Len() < size {
260		return 0, fmt.Errorf("client hello length is truncated")
261	}
262	value := 0
263	for range size {
264		b, _ := reader.ReadByte()
265		value = value<<8 | int(b)
266	}
267	return value, nil
268}
269
270func readUint16(reader *bytes.Reader) (int, error) {
271	return readLength(reader, 2)
272}
273
274func normalizeTLSName(value string) string {
275	return strings.TrimSuffix(strings.ToLower(strings.TrimSpace(value)), ".")
276}
277
278type replayConn struct {
279	net.Conn
280	reader *bytes.Reader
281}
282
283func (c replayConn) Read(p []byte) (int, error) {
284	if c.reader != nil && c.reader.Len() > 0 {
285		return c.reader.Read(p)
286	}
287	return c.Conn.Read(p)
288}
289
290type tlsHTTPListener struct {
291	addr   net.Addr
292	conns  chan net.Conn
293	done   chan struct{}
294	once   sync.Once
295	mu     sync.Mutex
296	closed bool
297}
298
299func newTLSHTTPListener(addr net.Addr) *tlsHTTPListener {
300	return &tlsHTTPListener{addr: addr, conns: make(chan net.Conn, 128), done: make(chan struct{})}
301}
302
303func (l *tlsHTTPListener) Accept() (net.Conn, error) {
304	select {
305	case <-l.done:
306		return nil, net.ErrClosed
307	default:
308	}
309	select {
310	case conn := <-l.conns:
311		if l.isClosed() {
312			_ = conn.Close()
313			return nil, net.ErrClosed
314		}
315		return conn, nil
316	case <-l.done:
317		return nil, net.ErrClosed
318	}
319}
320
321func (l *tlsHTTPListener) Close() error {
322	l.once.Do(func() {
323		l.mu.Lock()
324		l.closed = true
325		l.mu.Unlock()
326		close(l.done)
327		for {
328			select {
329			case conn := <-l.conns:
330				_ = conn.Close()
331			default:
332				return
333			}
334		}
335	})
336	return nil
337}
338
339func (l *tlsHTTPListener) Addr() net.Addr { return l.addr }
340
341func (l *tlsHTTPListener) serve(conn net.Conn) bool {
342	l.mu.Lock()
343	defer l.mu.Unlock()
344	if l.closed {
345		return false
346	}
347	select {
348	case l.conns <- conn:
349		return true
350	default:
351		return false
352	}
353}
354
355func (l *tlsHTTPListener) isClosed() bool {
356	l.mu.Lock()
357	defer l.mu.Unlock()
358	return l.closed
359}

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