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}