v3.3.0 b164fe9

sniff.go RawHistoryFolder

1package traffic
2
3import (
4	"bytes"
5	"errors"
6	"fmt"
7	"net"
8	"strings"
9	"time"
10)
11
12type smartTCPDispatchKind string
13
14const (
15	dispatchHTTP         smartTCPDispatchKind = "http"
16	dispatchH2C          smartTCPDispatchKind = "h2c"
17	dispatchTLS          smartTCPDispatchKind = "tls"
18	dispatchUnknown      smartTCPDispatchKind = "unknown"
19	dispatchRejectPROXY  smartTCPDispatchKind = "reject_proxy"
20	dispatchSniffTimeout smartTCPDispatchKind = "sniff_timeout"
21	dispatchReject       smartTCPDispatchKind = "reject"
22	dispatchRejectLarge  smartTCPDispatchKind = "reject_large"
23)
24
25type smartTCPSniffResult struct {
26	kind smartTCPDispatchKind
27	conn net.Conn
28}
29
30const (
31	maxSmartTCPSniffBytes = 4096
32	h2cPreface            = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
33	proxyV2Signature      = "\r\n\r\n\x00\r\nQUIT\n"
34)
35
36func sniffSmartTCP(conn net.Conn, timeout time.Duration) (smartTCPSniffResult, error) {
37	if timeout > 0 {
38		if err := conn.SetReadDeadline(time.Now().Add(timeout)); err != nil {
39			return smartTCPSniffResult{}, fmt.Errorf("set sniff read deadline: %w", err)
40		}
41	}
42
43	buf := make([]byte, 0, 32)
44	for len(buf) < maxSmartTCPSniffBytes {
45		remaining := maxSmartTCPSniffBytes - len(buf)
46		readSize := min(32, remaining)
47		tmp := make([]byte, readSize)
48		n, err := conn.Read(tmp)
49		if n > 0 {
50			buf = append(buf, tmp[:n]...)
51			if kind, done := classifySmartTCPPrefix(buf); done {
52				return sniffResult(conn, buf, kind)
53			}
54		}
55		if err != nil {
56			if isTimeoutError(err) && len(buf) > 0 {
57				return sniffResult(conn, buf, dispatchSniffTimeout)
58			}
59			return smartTCPSniffResult{}, fmt.Errorf("read smart tcp prefix: %w", err)
60		}
61	}
62	return sniffResult(conn, buf, dispatchRejectLarge)
63}
64
65func sniffResult(conn net.Conn, buf []byte, kind smartTCPDispatchKind) (smartTCPSniffResult, error) {
66	if err := conn.SetReadDeadline(time.Time{}); err != nil {
67		return smartTCPSniffResult{}, fmt.Errorf("clear sniff read deadline: %w", err)
68	}
69	return smartTCPSniffResult{kind: kind, conn: replayConn{Conn: conn, reader: bytes.NewReader(buf)}}, nil
70}
71
72func classifySmartTCPPrefix(buf []byte) (smartTCPDispatchKind, bool) {
73	if bytes.Equal(buf, []byte(proxyV2Signature)) || (len(buf) >= len(proxyV2Signature) && bytes.HasPrefix(buf, []byte(proxyV2Signature))) {
74		return dispatchRejectPROXY, true
75	}
76	if bytes.Equal(buf, []byte("PROXY ")) || (len(buf) >= len("PROXY ") && bytes.HasPrefix(buf, []byte("PROXY "))) {
77		return dispatchRejectPROXY, true
78	}
79	if isCompleteH2CPreface(buf) {
80		return dispatchH2C, true
81	}
82	if hasAnyPrefix([]byte(h2cPreface), buf) {
83		return "", false
84	}
85	if isHTTPRequestLine(buf) {
86		return dispatchHTTP, true
87	}
88	if isMalformedHTTPLookingPrefix(buf) {
89		return dispatchReject, true
90	}
91	if isTLSClientHelloPrefix(buf) {
92		return dispatchTLS, true
93	}
94	if isMalformedTLSRecordHeader(buf) {
95		return dispatchReject, true
96	}
97	if isAmbiguousSmartTCPPrefix(buf) {
98		return "", false
99	}
100	return dispatchUnknown, true
101}
102
103func isCompleteH2CPreface(buf []byte) bool {
104	return len(buf) >= len(h2cPreface) && bytes.HasPrefix(buf, []byte(h2cPreface))
105}
106
107func isHTTPRequestLine(buf []byte) bool {
108	lineBytes, _, ok := bytes.Cut(buf, []byte("\r\n"))
109	if !ok {
110		return false
111	}
112	line := string(lineBytes)
113	parts := strings.Split(line, " ")
114	return len(parts) == 3 && parts[0] != "" && isHTTPToken(parts[0]) && parts[1] != "" && strings.HasPrefix(parts[2], "HTTP/")
115}
116
117func isHTTPToken(token string) bool {
118	if token == "" {
119		return false
120	}
121	for _, r := range token {
122		if r <= 32 || r >= 127 || strings.ContainsRune("()<>@,;:\\\"/[]?={}\t", r) {
123			return false
124		}
125	}
126	return true
127}
128
129func isTLSClientHelloPrefix(buf []byte) bool {
130	if len(buf) < 5 || buf[0] != 22 {
131		return false
132	}
133	if buf[1] != 3 || buf[2] > 4 {
134		return false
135	}
136	recordLen := int(buf[3])<<8 | int(buf[4])
137	return recordLen > 0
138}
139
140func isMalformedTLSRecordHeader(buf []byte) bool {
141	if len(buf) < 5 || buf[0] != 22 {
142		return false
143	}
144	if buf[1] != 3 || buf[2] > 4 {
145		return false
146	}
147	recordLen := int(buf[3])<<8 | int(buf[4])
148	return recordLen == 0
149}
150
151func isAmbiguousSmartTCPPrefix(buf []byte) bool {
152	return hasAnyPrefix([]byte(proxyV2Signature), buf) || hasAnyPrefix([]byte(h2cPreface), buf) || hasAnyPrefix([]byte("PROXY "), buf) || couldBecomeHTTPRequest(buf) || couldBecomeTLS(buf)
153}
154
155func hasAnyPrefix(full []byte, partial []byte) bool {
156	return len(partial) < len(full) && bytes.HasPrefix(full, partial)
157}
158
159func isMalformedHTTPLookingPrefix(buf []byte) bool {
160	lineBytes, _, ok := bytes.Cut(buf, []byte("\r\n"))
161	if !ok || !bytes.Contains(lineBytes, []byte(" ")) {
162		return false
163	}
164	method, _, _ := bytes.Cut(lineBytes, []byte(" "))
165	return isKnownHTTPMethod(string(method))
166}
167
168func isKnownHTTPMethod(method string) bool {
169	switch method {
170	case "GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PROPFIND", "MKCOL":
171		return true
172	default:
173		return false
174	}
175}
176
177func couldBecomeHTTPRequest(buf []byte) bool {
178	if len(buf) == 0 || bytes.Contains(buf, []byte{0}) {
179		return false
180	}
181	if bytes.Contains(buf, []byte("\r\n")) {
182		return false
183	}
184	method, _, _ := bytes.Cut(buf, []byte(" "))
185	return isHTTPToken(string(method))
186}
187
188func couldBecomeTLS(buf []byte) bool {
189	if len(buf) == 0 || buf[0] != 22 {
190		return false
191	}
192	if len(buf) == 1 {
193		return true
194	}
195	if buf[1] != 3 {
196		return false
197	}
198	if len(buf) == 2 {
199		return true
200	}
201	if buf[2] > 4 {
202		return false
203	}
204	return len(buf) < 5
205}
206
207func isTimeoutError(err error) bool {
208	var netErr net.Error
209	return errors.As(err, &netErr) && netErr.Timeout()
210}

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