├── .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 | --------------------------------------------------------------------------------