├── .gitattributes
├── LICENSE
├── README.md
├── snippets.js
└── ech-workers.go
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 badafans
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 📦 ech-snippets
2 |
3 | 这个只是简单的把股神的ech-workers改成支持snippets,这样就没有每日10w次请求的限制了,客户端修改支持使用ip:port格式的优选ip和域名,并且对应Windows/Linux/MacOS平台,如果你需要其它平台客户端请自己用ech-workers.go源码自己编译。
4 |
5 | ## 💻 客户端帮助
6 |
7 | 客户端 -h 以后的帮助文件:
8 |
9 | Usage of ./ech-workers:
10 |
11 | -dns string
12 |
13 | ECH 查询 DNS 服务器 (default "119.29.29.29:53")
14 |
15 | -ech string
16 |
17 | ECH 查询域名 (default "cloudflare-ech.com")
18 |
19 | -f string
20 |
21 | 服务端地址 (格式: x.x.workers.dev:443)
22 |
23 | -ip string
24 |
25 | 指定服务端 IP(绕过 DNS 解析)
26 |
27 | -l string
28 |
29 | 代理监听地址 (支持 SOCKS5 和 HTTP) (default "127.0.0.1:30000")
30 |
31 | -token string
32 |
33 | 身份验证令牌
34 |
35 |
36 | ## 📝 使用说明
37 |
38 | 1. **(非必需)先修改snippets.js里面的代码**
39 | - 第五行:默认的PROXYIP (默认是我的PROXYIP域名,你可以替换成你自己喜欢的PROXYIP或者域名)
40 | - 第十三行:设置一个token (建议设置,免得被别人白嫖)
41 |
42 | 2. **创建一个新的snippets片段**
43 | - 把snippets.js的内容复制进去,左上角取一个你喜欢的名字,右上角Snippet rule设置Hostname equals 你的自定义域名,保存。
44 | - 在弹出的对话框中选择创建一个对应的开启了小黄云的A记录,内容为192.0.2.1
45 |
46 | 3. **复制你刚刚的自定义域名,用客户端启动**
47 | - 默认混合代理端口运行在本机的30000端口上,比如在win11下为:
48 | - ech-workers-windows-amd64.exe -f 你的自定义域名:443 -l 127.0.0.1:30000 -token 你的token -ip 你的优选ip:端口
49 | - 我习惯使用nekobox的自定义核心来调用。
50 |
51 | ## 🔧 nekobox的调用
52 |
53 | 1. 设置-其它核心-添加,起个名字,比如ech确定,选择你的ech-workers-windows-amd64.exe所在路径,确定。
54 | 2. 在主界面空白处右键,手动输入配置-类型-自定义(其它核心),在弹出的窗口中,名称(随便起一个),地址和端口(默认即可,这个不生效),核心(选择你刚才添加的核心名字,比如ech)
55 | 3. 命令 -f 你的自定义域名:443 -l 127.0.0.1:30000 -token 你的token -ip 你的优选ip:端口 ,确定即可。
56 |
--------------------------------------------------------------------------------
/snippets.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'cloudflare:sockets';
2 |
3 | const WS_READY_STATE_OPEN = 1;
4 | const WS_READY_STATE_CLOSING = 2;
5 | const CF_FALLBACK_IPS = ['tw.william.us.ci'];
6 |
7 | // 复用 TextEncoder,避免重复创建
8 | const encoder = new TextEncoder();
9 |
10 | export default {
11 | async fetch(request) {
12 | try {
13 | const token = '';
14 | const upgradeHeader = request.headers.get('Upgrade');
15 |
16 | if (!upgradeHeader || upgradeHeader.toLowerCase() !== 'websocket') {
17 | return new URL(request.url).pathname === '/'
18 | ? new Response('Hello World', { status: 200 })
19 | : new Response('Expected WebSocket', { status: 426 });
20 | }
21 | if (token && request.headers.get('Sec-WebSocket-Protocol') !== token) {
22 | return new Response('Unauthorized', { status: 401 });
23 | }
24 | const webSocketPair = new WebSocketPair();
25 | const [client, server] = Object.values(webSocketPair);
26 | server.accept();
27 |
28 | handleSession(server).catch(() => safeCloseWebSocket(server));
29 | // 修复 spread 类型错误
30 | const responseInit = {
31 | status: 101,
32 | webSocket: client
33 | };
34 |
35 | if (token) {
36 | responseInit.headers = { 'Sec-WebSocket-Protocol': token };
37 | }
38 | return new Response(null, responseInit);
39 |
40 | } catch (err) {
41 | return new Response(err.toString(), { status: 500 });
42 | }
43 | },
44 | };
45 | async function handleSession(webSocket) {
46 | let remoteSocket, remoteWriter, remoteReader;
47 | let isClosed = false;
48 | const cleanup = () => {
49 | if (isClosed) return;
50 | isClosed = true;
51 |
52 | try { remoteWriter?.releaseLock(); } catch {}
53 | try { remoteReader?.releaseLock(); } catch {}
54 | try { remoteSocket?.close(); } catch {}
55 |
56 | remoteWriter = remoteReader = remoteSocket = null;
57 | safeCloseWebSocket(webSocket);
58 | };
59 | const pumpRemoteToWebSocket = async () => {
60 | try {
61 | while (!isClosed && remoteReader) {
62 | const { done, value } = await remoteReader.read();
63 |
64 | if (done) break;
65 | if (webSocket.readyState !== WS_READY_STATE_OPEN) break;
66 | if (value?.byteLength > 0) webSocket.send(value);
67 | }
68 | } catch {}
69 |
70 | if (!isClosed) {
71 | try { webSocket.send('CLOSE'); } catch {}
72 | cleanup();
73 | }
74 | };
75 | const parseAddress = (addr) => {
76 | if (addr[0] === '[') {
77 | const end = addr.indexOf(']');
78 | return {
79 | host: addr.substring(1, end),
80 | port: parseInt(addr.substring(end + 2), 10)
81 | };
82 | }
83 | const sep = addr.lastIndexOf(':');
84 | return {
85 | host: addr.substring(0, sep),
86 | port: parseInt(addr.substring(sep + 1), 10)
87 | };
88 | };
89 | const isCFError = (err) => {
90 | const msg = err?.message?.toLowerCase() || '';
91 | return msg.includes('proxy request') ||
92 | msg.includes('cannot connect') ||
93 | msg.includes('cloudflare');
94 | };
95 | const connectToRemote = async (targetAddr, firstFrameData) => {
96 | const original = parseAddress(targetAddr); // 解析原始的 host 和 port
97 | const attempts = [null, ...CF_FALLBACK_IPS]; // attempts[0] = null 表示用原始
98 |
99 | for (let i = 0; i < attempts.length; i++) {
100 | let attemptHost = original.host;
101 | let attemptPort = original.port;
102 |
103 | if (attempts[i] !== null) {
104 | const fallback = attempts[i];
105 | try {
106 | const parsed = parseAddress(fallback); // 尝试解析(无论格式)
107 | if (!isNaN(parsed.port)) { // 如果 port 有效,用它
108 | attemptHost = parsed.host;
109 | attemptPort = parsed.port;
110 | } else { // port 无效(NaN),用默认 443
111 | attemptHost = fallback;
112 | attemptPort = 443;
113 | }
114 | } catch { // 任何解析错误(如缺少 ':' 或无效格式),用默认 443
115 | attemptHost = fallback;
116 | attemptPort = 443;
117 | }
118 | }
119 |
120 | try {
121 | remoteSocket = connect({
122 | hostname: attemptHost,
123 | port: attemptPort
124 | });
125 |
126 | if (remoteSocket.opened) await remoteSocket.opened;
127 |
128 | remoteWriter = remoteSocket.writable.getWriter();
129 | remoteReader = remoteSocket.readable.getReader();
130 |
131 | // 发送首帧数据
132 | if (firstFrameData) {
133 | await remoteWriter.write(encoder.encode(firstFrameData));
134 | }
135 |
136 | webSocket.send('CONNECTED');
137 | pumpRemoteToWebSocket();
138 | return;
139 |
140 | } catch (err) {
141 | // 清理失败的连接
142 | try { remoteWriter?.releaseLock(); } catch {}
143 | try { remoteReader?.releaseLock(); } catch {}
144 | try { remoteSocket?.close(); } catch {}
145 | remoteWriter = remoteReader = remoteSocket = null;
146 |
147 | // 如果不是 CF 错误或已是最后尝试,抛出错误
148 | if (!isCFError(err) || i === attempts.length - 1) {
149 | throw err;
150 | }
151 | }
152 | }
153 | };
154 | webSocket.addEventListener('message', async (event) => {
155 | if (isClosed) return;
156 | try {
157 | const data = event.data;
158 | if (typeof data === 'string') {
159 | if (data.startsWith('CONNECT:')) {
160 | const sep = data.indexOf('|', 8);
161 | await connectToRemote(
162 | data.substring(8, sep),
163 | data.substring(sep + 1)
164 | );
165 | }
166 | else if (data.startsWith('DATA:')) {
167 | if (remoteWriter) {
168 | await remoteWriter.write(encoder.encode(data.substring(5)));
169 | }
170 | }
171 | else if (data === 'CLOSE') {
172 | cleanup();
173 | }
174 | }
175 | else if (data instanceof ArrayBuffer && remoteWriter) {
176 | await remoteWriter.write(new Uint8Array(data));
177 | }
178 | } catch (err) {
179 | try { webSocket.send('ERROR:' + err.message); } catch {}
180 | cleanup();
181 | }
182 | });
183 | webSocket.addEventListener('close', cleanup);
184 | webSocket.addEventListener('error', cleanup);
185 | }
186 | function safeCloseWebSocket(ws) {
187 | try {
188 | if (ws.readyState === WS_READY_STATE_OPEN ||
189 | ws.readyState === WS_READY_STATE_CLOSING) {
190 | ws.close(1000, 'Server closed');
191 | }
192 | } catch {}
193 | }
194 |
--------------------------------------------------------------------------------
/ech-workers.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bufio"
5 | "crypto/tls"
6 | "crypto/x509"
7 | "encoding/base64"
8 | "encoding/binary"
9 | "errors"
10 | "flag"
11 | "fmt"
12 | "io"
13 | "log"
14 | "net"
15 | "strings"
16 | "sync"
17 | "time"
18 |
19 | "github.com/gorilla/websocket"
20 | )
21 |
22 | // ======================== 全局参数 ========================
23 |
24 | var (
25 | listenAddr string
26 | serverAddr string
27 | serverIP string
28 | token string
29 | dnsServer string
30 | echDomain string
31 |
32 | echListMu sync.RWMutex
33 | echList []byte
34 | )
35 |
36 | func init() {
37 | flag.StringVar(&listenAddr, "l", "127.0.0.1:30000", "代理监听地址 (支持 SOCKS5 和 HTTP)")
38 | flag.StringVar(&serverAddr, "f", "", "服务端地址 (格式: x.x.workers.dev:443)")
39 | flag.StringVar(&serverIP, "ip", "", "指定服务端 IP(绕过 DNS 解析)")
40 | flag.StringVar(&token, "token", "", "身份验证令牌")
41 | flag.StringVar(&dnsServer, "dns", "119.29.29.29:53", "ECH 查询 DNS 服务器")
42 | flag.StringVar(&echDomain, "ech", "cloudflare-ech.com", "ECH 查询域名")
43 | }
44 |
45 | func main() {
46 | flag.Parse()
47 |
48 | if serverAddr == "" {
49 | log.Fatal("必须指定服务端地址 -f\n\n示例:\n ./client -l 127.0.0.1:1080 -f your-worker.workers.dev:443 -token your-token")
50 | }
51 |
52 | log.Printf("[启动] 正在获取 ECH 配置...")
53 | if err := prepareECH(); err != nil {
54 | log.Fatalf("[启动] 获取 ECH 配置失败: %v", err)
55 | }
56 |
57 | runProxyServer(listenAddr)
58 | }
59 |
60 | // ======================== 工具函数 ========================
61 |
62 | func isNormalCloseError(err error) bool {
63 | if err == nil {
64 | return false
65 | }
66 | if err == io.EOF {
67 | return true
68 | }
69 | errStr := err.Error()
70 | return strings.Contains(errStr, "use of closed network connection") ||
71 | strings.Contains(errStr, "broken pipe") ||
72 | strings.Contains(errStr, "connection reset by peer") ||
73 | strings.Contains(errStr, "normal closure")
74 | }
75 |
76 | // ======================== ECH 支持 ========================
77 |
78 | const typeHTTPS = 65
79 |
80 | func prepareECH() error {
81 | echBase64, err := queryHTTPSRecord(echDomain, dnsServer)
82 | if err != nil {
83 | return fmt.Errorf("DNS 查询失败: %w", err)
84 | }
85 | if echBase64 == "" {
86 | return errors.New("未找到 ECH 参数")
87 | }
88 | raw, err := base64.StdEncoding.DecodeString(echBase64)
89 | if err != nil {
90 | return fmt.Errorf("ECH 解码失败: %w", err)
91 | }
92 | echListMu.Lock()
93 | echList = raw
94 | echListMu.Unlock()
95 | log.Printf("[ECH] 配置已加载,长度: %d 字节", len(raw))
96 | return nil
97 | }
98 |
99 | func refreshECH() error {
100 | log.Printf("[ECH] 刷新配置...")
101 | return prepareECH()
102 | }
103 |
104 | func getECHList() ([]byte, error) {
105 | echListMu.RLock()
106 | defer echListMu.RUnlock()
107 | if len(echList) == 0 {
108 | return nil, errors.New("ECH 配置未加载")
109 | }
110 | return echList, nil
111 | }
112 |
113 | func buildTLSConfigWithECH(serverName string, echList []byte) (*tls.Config, error) {
114 | roots, err := x509.SystemCertPool()
115 | if err != nil {
116 | return nil, fmt.Errorf("加载系统根证书失败: %w", err)
117 | }
118 | return &tls.Config{
119 | MinVersion: tls.VersionTLS13,
120 | ServerName: serverName,
121 | EncryptedClientHelloConfigList: echList,
122 | EncryptedClientHelloRejectionVerify: func(cs tls.ConnectionState) error {
123 | return errors.New("服务器拒绝 ECH")
124 | },
125 | RootCAs: roots,
126 | }, nil
127 | }
128 |
129 | func queryHTTPSRecord(domain, dnsServer string) (string, error) {
130 | query := buildDNSQuery(domain, typeHTTPS)
131 |
132 | conn, err := net.Dial("udp", dnsServer)
133 | if err != nil {
134 | return "", err
135 | }
136 | defer conn.Close()
137 |
138 | if _, err = conn.Write(query); err != nil {
139 | return "", err
140 | }
141 |
142 | response := make([]byte, 4096)
143 | n, err := conn.Read(response)
144 | if err != nil {
145 | return "", err
146 | }
147 | return parseDNSResponse(response[:n])
148 | }
149 |
150 | func buildDNSQuery(domain string, qtype uint16) []byte {
151 | query := make([]byte, 0, 512)
152 | query = append(query, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
153 | for _, label := range strings.Split(domain, ".") {
154 | query = append(query, byte(len(label)))
155 | query = append(query, []byte(label)...)
156 | }
157 | query = append(query, 0x00, byte(qtype>>8), byte(qtype), 0x00, 0x01)
158 | return query
159 | }
160 |
161 | func parseDNSResponse(response []byte) (string, error) {
162 | if len(response) < 12 {
163 | return "", errors.New("响应过短")
164 | }
165 | ancount := binary.BigEndian.Uint16(response[6:8])
166 | if ancount == 0 {
167 | return "", errors.New("无应答记录")
168 | }
169 |
170 | offset := 12
171 | for offset < len(response) && response[offset] != 0 {
172 | offset += int(response[offset]) + 1
173 | }
174 | offset += 5
175 |
176 | for i := 0; i < int(ancount); i++ {
177 | if offset >= len(response) {
178 | break
179 | }
180 | if response[offset]&0xC0 == 0xC0 {
181 | offset += 2
182 | } else {
183 | for offset < len(response) && response[offset] != 0 {
184 | offset += int(response[offset]) + 1
185 | }
186 | offset++
187 | }
188 | if offset+10 > len(response) {
189 | break
190 | }
191 | rrType := binary.BigEndian.Uint16(response[offset : offset+2])
192 | offset += 8
193 | dataLen := binary.BigEndian.Uint16(response[offset : offset+2])
194 | offset += 2
195 | if offset+int(dataLen) > len(response) {
196 | break
197 | }
198 | data := response[offset : offset+int(dataLen)]
199 | offset += int(dataLen)
200 |
201 | if rrType == typeHTTPS {
202 | if ech := parseHTTPSRecord(data); ech != "" {
203 | return ech, nil
204 | }
205 | }
206 | }
207 | return "", nil
208 | }
209 |
210 | func parseHTTPSRecord(data []byte) string {
211 | if len(data) < 2 {
212 | return ""
213 | }
214 | offset := 2
215 | if offset < len(data) && data[offset] == 0 {
216 | offset++
217 | } else {
218 | for offset < len(data) && data[offset] != 0 {
219 | offset += int(data[offset]) + 1
220 | }
221 | offset++
222 | }
223 | for offset+4 <= len(data) {
224 | key := binary.BigEndian.Uint16(data[offset : offset+2])
225 | length := binary.BigEndian.Uint16(data[offset+2 : offset+4])
226 | offset += 4
227 | if offset+int(length) > len(data) {
228 | break
229 | }
230 | value := data[offset : offset+int(length)]
231 | offset += int(length)
232 | if key == 5 {
233 | return base64.StdEncoding.EncodeToString(value)
234 | }
235 | }
236 | return ""
237 | }
238 |
239 | // ======================== WebSocket 客户端 ========================
240 |
241 | func parseServerAddr(addr string) (host, port, path string, err error) {
242 | path = "/"
243 | slashIdx := strings.Index(addr, "/")
244 | if slashIdx != -1 {
245 | path = addr[slashIdx:]
246 | addr = addr[:slashIdx]
247 | }
248 |
249 | host, port, err = net.SplitHostPort(addr)
250 | if err != nil {
251 | return "", "", "", fmt.Errorf("无效的服务器地址格式: %v", err)
252 | }
253 |
254 | return host, port, path, nil
255 | }
256 |
257 | func dialWebSocketWithECH(maxRetries int) (*websocket.Conn, error) {
258 | host, port, path, err := parseServerAddr(serverAddr)
259 | if err != nil {
260 | return nil, err
261 | }
262 |
263 | wsURL := fmt.Sprintf("wss://%s:%s%s", host, port, path)
264 |
265 | for attempt := 1; attempt <= maxRetries; attempt++ {
266 | echBytes, echErr := getECHList()
267 | if echErr != nil {
268 | if attempt < maxRetries {
269 | refreshECH()
270 | continue
271 | }
272 | return nil, echErr
273 | }
274 |
275 | tlsCfg, tlsErr := buildTLSConfigWithECH(host, echBytes)
276 | if tlsErr != nil {
277 | return nil, tlsErr
278 | }
279 |
280 | dialer := websocket.Dialer{
281 | TLSClientConfig: tlsCfg,
282 | Subprotocols: func() []string {
283 | if token == "" {
284 | return nil
285 | }
286 | return []string{token}
287 | }(),
288 | HandshakeTimeout: 10 * time.Second,
289 | }
290 |
291 | if serverIP != "" {
292 | dialer.NetDial = func(network, address string) (net.Conn, error) {
293 | _, port, err := net.SplitHostPort(address)
294 | if err != nil {
295 | return nil, err
296 | }
297 |
298 | // 添加的改进代码(完整 IPv6 支持)
299 | ipHost := serverIP
300 | userHost, userPort, splitErr := net.SplitHostPort(serverIP)
301 | if splitErr == nil {
302 | // 如果 serverIP 带端口,剥离并使用它(覆盖原 port)
303 | ipHost = userHost
304 | port = userPort // 覆盖原 port(如果不想覆盖,注释此行)
305 | } // else: 无端口或无效格式,使用原 serverIP + 原 port
306 |
307 | return net.DialTimeout(network, net.JoinHostPort(ipHost, port), 10*time.Second)
308 | }
309 | }
310 |
311 | wsConn, _, dialErr := dialer.Dial(wsURL, nil)
312 | if dialErr != nil {
313 | if strings.Contains(dialErr.Error(), "ECH") && attempt < maxRetries {
314 | log.Printf("[ECH] 连接失败,尝试刷新配置 (%d/%d)", attempt, maxRetries)
315 | refreshECH()
316 | time.Sleep(time.Second)
317 | continue
318 | }
319 | return nil, dialErr
320 | }
321 |
322 | return wsConn, nil
323 | }
324 |
325 | return nil, errors.New("连接失败,已达最大重试次数")
326 | }
327 |
328 | // ======================== 统一代理服务器 ========================
329 |
330 | func runProxyServer(addr string) {
331 | listener, err := net.Listen("tcp", addr)
332 | if err != nil {
333 | log.Fatalf("[代理] 监听失败: %v", err)
334 | }
335 | defer listener.Close()
336 |
337 | log.Printf("[代理] 服务器启动: %s (支持 SOCKS5 和 HTTP)", addr)
338 | log.Printf("[代理] 后端服务器: %s", serverAddr)
339 | if serverIP != "" {
340 | log.Printf("[代理] 使用固定 IP: %s", serverIP)
341 | }
342 |
343 | for {
344 | conn, err := listener.Accept()
345 | if err != nil {
346 | log.Printf("[代理] 接受连接失败: %v", err)
347 | continue
348 | }
349 |
350 | go handleConnection(conn)
351 | }
352 | }
353 |
354 | func handleConnection(conn net.Conn) {
355 | defer conn.Close()
356 |
357 | clientAddr := conn.RemoteAddr().String()
358 | conn.SetDeadline(time.Now().Add(30 * time.Second))
359 |
360 | // 读取第一个字节判断协议
361 | buf := make([]byte, 1)
362 | n, err := conn.Read(buf)
363 | if err != nil || n == 0 {
364 | return
365 | }
366 |
367 | firstByte := buf[0]
368 |
369 | // 使用 switch 判断协议类型
370 | switch firstByte {
371 | case 0x05:
372 | // SOCKS5 协议
373 | handleSOCKS5(conn, clientAddr, firstByte)
374 | case 'C', 'G', 'P', 'H', 'D', 'O', 'T':
375 | // HTTP 协议 (CONNECT, GET, POST, HEAD, DELETE, OPTIONS, TRACE, PUT, PATCH)
376 | handleHTTP(conn, clientAddr, firstByte)
377 | default:
378 | log.Printf("[代理] %s 未知协议: 0x%02x", clientAddr, firstByte)
379 | }
380 | }
381 |
382 | // ======================== SOCKS5 处理 ========================
383 |
384 | func handleSOCKS5(conn net.Conn, clientAddr string, firstByte byte) {
385 | // 验证版本
386 | if firstByte != 0x05 {
387 | log.Printf("[SOCKS5] %s 版本错误: 0x%02x", clientAddr, firstByte)
388 | return
389 | }
390 |
391 | // 读取认证方法数量
392 | buf := make([]byte, 1)
393 | if _, err := io.ReadFull(conn, buf); err != nil {
394 | return
395 | }
396 |
397 | nmethods := buf[0]
398 | methods := make([]byte, nmethods)
399 | if _, err := io.ReadFull(conn, methods); err != nil {
400 | return
401 | }
402 |
403 | // 响应无需认证
404 | if _, err := conn.Write([]byte{0x05, 0x00}); err != nil {
405 | return
406 | }
407 |
408 | // 读取请求
409 | buf = make([]byte, 4)
410 | if _, err := io.ReadFull(conn, buf); err != nil {
411 | return
412 | }
413 |
414 | if buf[0] != 5 {
415 | return
416 | }
417 |
418 | command := buf[1]
419 | atyp := buf[3]
420 |
421 | var host string
422 | switch atyp {
423 | case 0x01: // IPv4
424 | buf = make([]byte, 4)
425 | if _, err := io.ReadFull(conn, buf); err != nil {
426 | return
427 | }
428 | host = net.IP(buf).String()
429 |
430 | case 0x03: // 域名
431 | buf = make([]byte, 1)
432 | if _, err := io.ReadFull(conn, buf); err != nil {
433 | return
434 | }
435 | domainBuf := make([]byte, buf[0])
436 | if _, err := io.ReadFull(conn, domainBuf); err != nil {
437 | return
438 | }
439 | host = string(domainBuf)
440 |
441 | case 0x04: // IPv6
442 | buf = make([]byte, 16)
443 | if _, err := io.ReadFull(conn, buf); err != nil {
444 | return
445 | }
446 | host = net.IP(buf).String()
447 |
448 | default:
449 | conn.Write([]byte{0x05, 0x08, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00})
450 | return
451 | }
452 |
453 | // 读取端口
454 | buf = make([]byte, 2)
455 | if _, err := io.ReadFull(conn, buf); err != nil {
456 | return
457 | }
458 | port := int(buf[0])<<8 | int(buf[1])
459 |
460 | var target string
461 | if atyp == 0x04 {
462 | target = fmt.Sprintf("[%s]:%d", host, port)
463 | } else {
464 | target = fmt.Sprintf("%s:%d", host, port)
465 | }
466 |
467 | if command != 0x01 {
468 | conn.Write([]byte{0x05, 0x07, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00})
469 | return
470 | }
471 |
472 | log.Printf("[SOCKS5] %s -> %s", clientAddr, target)
473 |
474 | if err := handleTunnel(conn, target, clientAddr, modeSOCKS5, ""); err != nil {
475 | if !isNormalCloseError(err) {
476 | log.Printf("[SOCKS5] %s 代理失败: %v", clientAddr, err)
477 | }
478 | }
479 | }
480 |
481 | // ======================== HTTP 处理 ========================
482 |
483 | func handleHTTP(conn net.Conn, clientAddr string, firstByte byte) {
484 | // 将第一个字节放回缓冲区
485 | reader := bufio.NewReader(io.MultiReader(
486 | strings.NewReader(string(firstByte)),
487 | conn,
488 | ))
489 |
490 | // 读取 HTTP 请求行
491 | requestLine, err := reader.ReadString('\n')
492 | if err != nil {
493 | return
494 | }
495 |
496 | parts := strings.Fields(requestLine)
497 | if len(parts) < 3 {
498 | return
499 | }
500 |
501 | method := parts[0]
502 | requestURL := parts[1]
503 | httpVersion := parts[2]
504 |
505 | // 读取所有 headers
506 | headers := make(map[string]string)
507 | var headerLines []string
508 | for {
509 | line, err := reader.ReadString('\n')
510 | if err != nil {
511 | return
512 | }
513 | line = strings.TrimRight(line, "\r\n")
514 | if line == "" {
515 | break
516 | }
517 | headerLines = append(headerLines, line)
518 | if idx := strings.Index(line, ":"); idx > 0 {
519 | key := strings.TrimSpace(line[:idx])
520 | value := strings.TrimSpace(line[idx+1:])
521 | headers[strings.ToLower(key)] = value
522 | }
523 | }
524 |
525 | switch method {
526 | case "CONNECT":
527 | // HTTPS 隧道代理 - 需要发送 200 响应
528 | log.Printf("[HTTP-CONNECT] %s -> %s", clientAddr, requestURL)
529 | if err := handleTunnel(conn, requestURL, clientAddr, modeHTTPConnect, ""); err != nil {
530 | if !isNormalCloseError(err) {
531 | log.Printf("[HTTP-CONNECT] %s 代理失败: %v", clientAddr, err)
532 | }
533 | }
534 |
535 | case "GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS", "PATCH", "TRACE":
536 | // HTTP 代理 - 直接转发,不发送 200 响应
537 | log.Printf("[HTTP-%s] %s -> %s", method, clientAddr, requestURL)
538 |
539 | var target string
540 | var path string
541 |
542 | if strings.HasPrefix(requestURL, "http://") {
543 | // 解析完整 URL
544 | urlWithoutScheme := strings.TrimPrefix(requestURL, "http://")
545 | idx := strings.Index(urlWithoutScheme, "/")
546 | if idx > 0 {
547 | target = urlWithoutScheme[:idx]
548 | path = urlWithoutScheme[idx:]
549 | } else {
550 | target = urlWithoutScheme
551 | path = "/"
552 | }
553 | } else {
554 | // 相对路径,从 Host header 获取
555 | target = headers["host"]
556 | path = requestURL
557 | }
558 |
559 | if target == "" {
560 | conn.Write([]byte("HTTP/1.1 400 Bad Request\r\n\r\n"))
561 | return
562 | }
563 |
564 | // 添加默认端口
565 | if !strings.Contains(target, ":") {
566 | target += ":80"
567 | }
568 |
569 | // 重构 HTTP 请求(去掉完整 URL,使用相对路径)
570 | var requestBuilder strings.Builder
571 | requestBuilder.WriteString(fmt.Sprintf("%s %s %s\r\n", method, path, httpVersion))
572 |
573 | // 写入 headers(过滤掉 Proxy-Connection)
574 | for _, line := range headerLines {
575 | key := strings.Split(line, ":")[0]
576 | keyLower := strings.ToLower(strings.TrimSpace(key))
577 | if keyLower != "proxy-connection" && keyLower != "proxy-authorization" {
578 | requestBuilder.WriteString(line)
579 | requestBuilder.WriteString("\r\n")
580 | }
581 | }
582 | requestBuilder.WriteString("\r\n")
583 |
584 | // 如果有请求体,需要读取并附加
585 | if contentLength := headers["content-length"]; contentLength != "" {
586 | var length int
587 | fmt.Sscanf(contentLength, "%d", &length)
588 | if length > 0 && length < 10*1024*1024 { // 限制 10MB
589 | body := make([]byte, length)
590 | if _, err := io.ReadFull(reader, body); err == nil {
591 | requestBuilder.Write(body)
592 | }
593 | }
594 | }
595 |
596 | firstFrame := requestBuilder.String()
597 |
598 | // 使用 modeHTTPProxy 模式(不发送 200 响应)
599 | if err := handleTunnel(conn, target, clientAddr, modeHTTPProxy, firstFrame); err != nil {
600 | if !isNormalCloseError(err) {
601 | log.Printf("[HTTP-%s] %s 代理失败: %v", method, clientAddr, err)
602 | }
603 | }
604 |
605 | default:
606 | log.Printf("[HTTP] %s 不支持的方法: %s", clientAddr, method)
607 | conn.Write([]byte("HTTP/1.1 405 Method Not Allowed\r\n\r\n"))
608 | }
609 | }
610 |
611 | // ======================== 通用隧道处理 ========================
612 |
613 | // 代理模式常量
614 | const (
615 | modeSOCKS5 = 1 // SOCKS5 代理
616 | modeHTTPConnect = 2 // HTTP CONNECT 隧道
617 | modeHTTPProxy = 3 // HTTP 普通代理(GET/POST等)
618 | )
619 |
620 | func handleTunnel(conn net.Conn, target, clientAddr string, mode int, firstFrame string) error {
621 | wsConn, err := dialWebSocketWithECH(2)
622 | if err != nil {
623 | sendErrorResponse(conn, mode)
624 | return err
625 | }
626 | defer wsConn.Close()
627 |
628 | var mu sync.Mutex
629 |
630 | // 保活
631 | stopPing := make(chan bool)
632 | go func() {
633 | ticker := time.NewTicker(10 * time.Second)
634 | defer ticker.Stop()
635 | for {
636 | select {
637 | case <-ticker.C:
638 | mu.Lock()
639 | wsConn.WriteMessage(websocket.PingMessage, nil)
640 | mu.Unlock()
641 | case <-stopPing:
642 | return
643 | }
644 | }
645 | }()
646 | defer close(stopPing)
647 |
648 | conn.SetDeadline(time.Time{})
649 |
650 | // 如果没有预设的 firstFrame,尝试读取第一帧数据(仅 SOCKS5)
651 | if firstFrame == "" && mode == modeSOCKS5 {
652 | _ = conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
653 | buffer := make([]byte, 32768)
654 | n, _ := conn.Read(buffer)
655 | _ = conn.SetReadDeadline(time.Time{})
656 | if n > 0 {
657 | firstFrame = string(buffer[:n])
658 | }
659 | }
660 |
661 | // 发送连接请求
662 | connectMsg := fmt.Sprintf("CONNECT:%s|%s", target, firstFrame)
663 | mu.Lock()
664 | err = wsConn.WriteMessage(websocket.TextMessage, []byte(connectMsg))
665 | mu.Unlock()
666 | if err != nil {
667 | sendErrorResponse(conn, mode)
668 | return err
669 | }
670 |
671 | // 等待响应
672 | _, msg, err := wsConn.ReadMessage()
673 | if err != nil {
674 | sendErrorResponse(conn, mode)
675 | return err
676 | }
677 |
678 | response := string(msg)
679 | if strings.HasPrefix(response, "ERROR:") {
680 | sendErrorResponse(conn, mode)
681 | return errors.New(response)
682 | }
683 | if response != "CONNECTED" {
684 | sendErrorResponse(conn, mode)
685 | return fmt.Errorf("意外响应: %s", response)
686 | }
687 |
688 | // 发送成功响应(根据模式不同而不同)
689 | if err := sendSuccessResponse(conn, mode); err != nil {
690 | return err
691 | }
692 |
693 | log.Printf("[代理] %s 已连接: %s", clientAddr, target)
694 |
695 | // 双向转发
696 | done := make(chan bool, 2)
697 |
698 | // Client -> Server
699 | go func() {
700 | buf := make([]byte, 32768)
701 | for {
702 | n, err := conn.Read(buf)
703 | if err != nil {
704 | mu.Lock()
705 | wsConn.WriteMessage(websocket.TextMessage, []byte("CLOSE"))
706 | mu.Unlock()
707 | done <- true
708 | return
709 | }
710 |
711 | mu.Lock()
712 | err = wsConn.WriteMessage(websocket.BinaryMessage, buf[:n])
713 | mu.Unlock()
714 | if err != nil {
715 | done <- true
716 | return
717 | }
718 | }
719 | }()
720 |
721 | // Server -> Client
722 | go func() {
723 | for {
724 | mt, msg, err := wsConn.ReadMessage()
725 | if err != nil {
726 | done <- true
727 | return
728 | }
729 |
730 | if mt == websocket.TextMessage {
731 | if string(msg) == "CLOSE" {
732 | done <- true
733 | return
734 | }
735 | }
736 |
737 | if _, err := conn.Write(msg); err != nil {
738 | done <- true
739 | return
740 | }
741 | }
742 | }()
743 |
744 | <-done
745 | log.Printf("[代理] %s 已断开: %s", clientAddr, target)
746 | return nil
747 | }
748 |
749 | // ======================== 响应辅助函数 ========================
750 |
751 | func sendErrorResponse(conn net.Conn, mode int) {
752 | switch mode {
753 | case modeSOCKS5:
754 | conn.Write([]byte{0x05, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00})
755 | case modeHTTPConnect, modeHTTPProxy:
756 | conn.Write([]byte("HTTP/1.1 502 Bad Gateway\r\n\r\n"))
757 | }
758 | }
759 |
760 | func sendSuccessResponse(conn net.Conn, mode int) error {
761 | switch mode {
762 | case modeSOCKS5:
763 | // SOCKS5 成功响应
764 | _, err := conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00})
765 | return err
766 | case modeHTTPConnect:
767 | // HTTP CONNECT 需要发送 200 响应
768 | _, err := conn.Write([]byte("HTTP/1.1 200 Connection Established\r\n\r\n"))
769 | return err
770 | case modeHTTPProxy:
771 | // HTTP GET/POST 等不需要发送响应,直接转发目标服务器的响应
772 | return nil
773 | }
774 | return nil
775 | }
776 |
--------------------------------------------------------------------------------