block-proxy 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/socks5/server.js CHANGED
@@ -113,6 +113,8 @@ async function init() {
113
113
  }
114
114
 
115
115
  function handleTcpRequest(clientSocket, targetHost, targetPort) {
116
+ // jayli
117
+ // console.log(targetHost);
116
118
  clientSocket.setTimeout(30_000);
117
119
  clientSocket.on('timeout', () => clientSocket.destroy());
118
120
 
@@ -160,13 +162,199 @@ async function init() {
160
162
  clientSocket.on('close', () => proxySocket.destroy());
161
163
  }
162
164
 
163
- // 处理 UDP ASSOCIATE(本地 UDP 中继)
165
+ // 处理 UDP ASSOCIATE(RFC 1928 合规实现)
164
166
  function handleUdpAssociate(clientSocket) {
165
167
  const udpRelay = dgram.createSocket('udp4');
168
+ let clientUdpAddr = null; // 客户端的 UDP 地址(用于回包)
169
+
170
+ // 存储 { 'host:port': { rinfo } } 用于回包时知道发给谁
171
+ const targetToClientMap = new Map();
172
+
173
+ // 绑定到任意端口
174
+ udpRelay.bind(0, '127.0.0.1', () => {
175
+ const localAddr = udpRelay.address();
176
+ // 告诉客户端 UDP 中继地址(必须是 127.0.0.1 或公网 IP,不能是 0.0.0.0)
177
+ sendResponse(clientSocket, 0x00, 0x01, '127.0.0.1', localAddr.port);
178
+ });
179
+
180
+ // 接收来自客户端的 UDP 包(带 SOCKS5 header)
181
+ udpRelay.on('message', (msg, rinfo) => {
182
+ if (!clientUdpAddr) {
183
+ // 第一个包来自客户端,记录其地址(后续回包用)
184
+ clientUdpAddr = rinfo;
185
+ }
186
+
187
+ if (msg.length < 10) {
188
+ // 最小 UDP 请求:VER=0 + RSV=0 + FRAG=0 + ATYP=1 (IPv4) + ADDR(4) + PORT(2) = 10
189
+ return;
190
+ }
191
+
192
+ const ver = msg[0];
193
+ const frag = msg[2]; // 分片不支持
194
+ if (ver !== 0x00 || frag !== 0x00) {
195
+ return; // 不支持分片或错误版本
196
+ }
197
+
198
+ try {
199
+ const atyp = msg[3];
200
+ let headerLen = 0;
201
+ let targetHost, targetPort;
202
+
203
+ if (atyp === 0x01) {
204
+ // IPv4
205
+ targetHost = msg.slice(4, 8).join('.');
206
+ targetPort = msg.readUInt16BE(8);
207
+ headerLen = 10;
208
+ } else if (atyp === 0x03) {
209
+ // Domain
210
+ const len = msg[4];
211
+ if (msg.length < 5 + len + 2) return;
212
+ targetHost = msg.slice(5, 5 + len).toString();
213
+ targetPort = msg.readUInt16BE(5 + len);
214
+ headerLen = 5 + len + 2;
215
+ } else if (atyp === 0x04) {
216
+ // IPv6 —— 简化:只取原始字节,Node.js dgram 支持字符串格式
217
+ if (msg.length < 22) return;
218
+ const ipv6Bytes = msg.slice(4, 20);
219
+ targetHost = '[' + ipv6Bytes.reduce((acc, byte, i) => {
220
+ if (i % 2 === 0 && i > 0) acc += ':';
221
+ return acc + byte.toString(16).padStart(2, '0');
222
+ }, '').replace(/(^|:)0+([0-9a-f]+)/g, '$1$2') + ']';
223
+ targetPort = msg.readUInt16BE(20);
224
+ headerLen = 22;
225
+ } else {
226
+ return; // 不支持的地址类型
227
+ }
228
+
229
+ const payload = msg.slice(headerLen);
230
+ if (payload.length === 0) return;
231
+
232
+ // 构建目标唯一键(用于回包映射)
233
+ const targetKey = `${targetHost}:${targetPort}`;
234
+
235
+ // 创建临时 socket 发送数据(避免端口复用问题)
236
+ const outSocket = dgram.createSocket('udp4');
237
+ outSocket.send(payload, targetPort, targetHost, (err) => {
238
+ if (err) {
239
+ console.warn(`UDP forward error to ${targetHost}:${targetPort}:`, err.message);
240
+ }
241
+ outSocket.close();
242
+ });
243
+
244
+ // 记录该目标对应的客户端地址(用于响应包回传)
245
+ targetToClientMap.set(targetKey, rinfo);
246
+
247
+ // 可选:加个超时自动清理(简化起见这里省略,靠 close 清理)
248
+ } catch (e) {
249
+ console.warn('UDP parse error:', e.message);
250
+ }
251
+ });
252
+
253
+ // 接收从目标服务器返回的 UDP 响应,并转发回客户端
254
+ udpRelay.on('listening', () => {
255
+ // Node.js 不会自动监听入站响应,但我们已经在 bind 后处于 listening 状态
256
+ // 所有 inbound UDP 都会触发 'message',包括响应
257
+ });
258
+
259
+ // 注意:响应包也会触发 'message',但来源是外部服务器(不是 clientUdpAddr)
260
+ // 所以我们需要在上面的逻辑中区分:如果是来自已知 target 的响应,则回包
261
+
262
+ // 重写 message handler 以同时处理“客户端请求”和“服务器响应”
263
+ // 我们已经做了:所有包都进同一个 handler,通过 targetToClientMap 判断是否是响应
264
+
265
+ // 但我们还需要:当收到外部服务器的响应时,把它封装后发回 clientUdpAddr
266
+ // 所以上面的 handler 已经能处理请求,现在补充响应回包逻辑:
267
+
268
+ // 实际上,上面的 handler 只处理了“客户端 → 代理”的包。
269
+ // “目标服务器 → 代理”的包也会进同一个 handler,但此时 rinfo ≠ clientUdpAddr,
270
+ // 且不在 targetToClientMap 的 key 中(因为 key 是 host:port,而 rinfo 是源地址)。
271
+
272
+ // 所以我们需要换一种方式:**为每个目标创建独立的 socket?**
273
+ // 但那样太重。更高效的做法是:**用单个 relay socket,靠 targetToClientMap 映射**
274
+
275
+ // ✅ 正确做法:在收到外部响应时,根据 (rinfo.address:rinfo.port) 查找是否是我们发出的请求的目标
276
+ // 但注意:我们发的是 targetHost:targetPort,而响应来自 same address:port
277
+
278
+ // 所以我们在发送时,应该用 **rinfo.address:rinfo.port 作为 key 存 clientAddr**
279
+ // 但这样不行,因为多个客户端可能访问同一目标。
280
+
281
+ // 🚨 更健壮的方式:**每个客户端有自己的 udpRelay**(当前就是这么做的!)
282
+ // 所以在这个函数内,所有流量都属于同一个 SOCKS5 TCP 会话的客户端。
283
+ // 因此,我们可以安全地假设:**任何非 clientUdpAddr 的 UDP 包都是目标服务器的响应**
284
+
285
+ // 修改 message handler 如下(替换上面的 handler):
286
+ udpRelay.removeAllListeners('message');
166
287
  udpRelay.on('message', (msg, rinfo) => {
167
- // 注意:标准 SOCKS5 UDP 包含 header,但此处简化直接回传(适用于 DNS 等)
168
- // 生产环境建议按 RFC 1928 封装/解封装
169
- clientSocket.write(msg);
288
+ // 判断是客户端发来的请求,还是目标服务器的响应
289
+ if (clientUdpAddr && rinfo.address === clientUdpAddr.address && rinfo.port === clientUdpAddr.port) {
290
+ // ← 来自客户端的请求(带 header)
291
+ if (msg.length < 10) return;
292
+ const ver = msg[0];
293
+ const frag = msg[2];
294
+ if (ver !== 0x00 || frag !== 0x00) return;
295
+
296
+ const atyp = msg[3];
297
+ let headerLen = 0, targetHost, targetPort;
298
+
299
+ try {
300
+ if (atyp === 0x01) {
301
+ targetHost = msg.slice(4, 8).join('.');
302
+ targetPort = msg.readUInt16BE(8);
303
+ headerLen = 10;
304
+ } else if (atyp === 0x03) {
305
+ const len = msg[4];
306
+ if (msg.length < 5 + len + 2) return;
307
+ targetHost = msg.slice(5, 5 + len).toString();
308
+ targetPort = msg.readUInt16BE(5 + len);
309
+ headerLen = 5 + len + 2;
310
+ } else if (atyp === 0x04) {
311
+ if (msg.length < 22) return;
312
+ const ipv6Bytes = msg.slice(4, 20);
313
+ targetHost = '[' + ipv6Bytes.reduce((acc, byte, i) => {
314
+ if (i % 2 === 0 && i > 0) acc += ':';
315
+ return acc + byte.toString(16).padStart(2, '0');
316
+ }, '').replace(/(^|:)0+([0-9a-f]+)/g, '$1$2') + ']';
317
+ targetPort = msg.readUInt16BE(20);
318
+ headerLen = 22;
319
+ } else {
320
+ return;
321
+ }
322
+
323
+ const payload = msg.slice(headerLen);
324
+ if (payload.length === 0) return;
325
+
326
+ // 发送到目标
327
+ udpRelay.send(payload, targetPort, targetHost, (err) => {
328
+ if (err) {
329
+ console.warn(`UDP send error to ${targetHost}:${targetPort}:`, err.message);
330
+ }
331
+ });
332
+ } catch (e) {
333
+ console.warn('UDP request parse error:', e.message);
334
+ }
335
+ } else {
336
+ // ← 来自目标服务器的响应(裸 payload),需要封装后发回客户端
337
+ if (!clientUdpAddr) return; // 还没收到客户端请求
338
+
339
+ // 构建 SOCKS5 UDP response header
340
+ const respHeader = Buffer.alloc(10);
341
+ respHeader[0] = 0x00; // RSV
342
+ respHeader[1] = 0x00; // RSV
343
+ respHeader[2] = 0x00; // FRAG
344
+ respHeader[3] = 0x01; // ATYP = IPv4 (简化:统一返回 IPv4 0.0.0.0)
345
+ respHeader[4] = 0;
346
+ respHeader[5] = 0;
347
+ respHeader[6] = 0;
348
+ respHeader[7] = 0;
349
+ respHeader.writeUInt16BE(rinfo.port, 8); // 源端口作为 DST.PORT(部分客户端依赖)
350
+
351
+ const response = Buffer.concat([respHeader, msg]);
352
+ udpRelay.send(response, clientUdpAddr.port, clientUdpAddr.address, (err) => {
353
+ if (err) {
354
+ console.warn('UDP send back to client error:', err.message);
355
+ }
356
+ });
357
+ }
170
358
  });
171
359
 
172
360
  udpRelay.on('error', (err) => {
@@ -174,17 +362,16 @@ async function init() {
174
362
  clientSocket.destroy();
175
363
  });
176
364
 
177
- const localAddr = udpRelay.address();
178
- // 告诉客户端 UDP 中继地址(返回 127.0.0.1 + 端口)
179
- sendResponse(clientSocket, 0x00, 0x01, '127.0.0.1', localAddr.port);
180
-
181
365
  // 清理
182
- clientSocket.on('close', () => udpRelay.close());
183
- clientSocket.on('error', () => udpRelay.close());
366
+ const cleanup = () => {
367
+ if (!udpRelay._closed) {
368
+ udpRelay.close();
369
+ }
370
+ };
371
+ clientSocket.on('close', cleanup);
372
+ clientSocket.on('error', cleanup);
184
373
  }
185
374
 
186
- // console.log('ticketKeys length:', ticketKeys.length); // 必须是 48!
187
-
188
375
  // TLS 服务器选项
189
376
  const tlsOptions = {
190
377
  key: TLS_KEY,
@@ -297,13 +484,13 @@ async function init() {
297
484
  socket.destroy();
298
485
  }
299
486
  } catch (err) {
300
- console.error('SOCKS5 over TLS session error:', err.message);
487
+ console.error('SOCKS5 over TLS session error:', err);
301
488
  socket.destroy();
302
489
  }
303
490
  });
304
491
 
305
492
  server.on('clientError', (err, socket) => {
306
- console.warn('TLS client error during handshake:', err.message);
493
+ console.warn('TLS client error during handshake:', err);
307
494
  socket?.end(); // 安全关闭
308
495
  });
309
496
 
package/src/App.css CHANGED
@@ -271,9 +271,11 @@ span.host-text {
271
271
  }
272
272
 
273
273
  .setting-row label {
274
- /*width: 120px;*/
274
+ width: 220px; /* 固定宽度,足够容纳最长的标签 */
275
+ text-align: right;
275
276
  font-weight: bold;
276
277
  color: #555;
278
+ flex-shrink: 0; /* 防止被压缩 */
277
279
  }
278
280
 
279
281
  .setting-row input {
package/src/App.js CHANGED
@@ -541,7 +541,7 @@ function App() {
541
541
  onChange={(e) => updateFilterTime(index, e.target.value, getFilterTimes(host).end)}
542
542
  />
543
543
  </label>
544
- <label>~</label>
544
+ <label>~</label>
545
545
  <label>
546
546
  <input
547
547
  type="time"
@@ -564,7 +564,6 @@ function App() {
564
564
 
565
565
  <div className="config-section">
566
566
  <h2>HTTP/Socks5 端口设置,验证信息,下游 VPN_Proxy 代理</h2>
567
- {/*<p><span>配置页端口默认 8004</span></p> */}
568
567
  <div className="setting-row">
569
568
  <label>Anyproxy HTTP 代理端口:</label>
570
569
  <input
@@ -594,38 +593,38 @@ function App() {
594
593
  <div className="setting-row">
595
594
  <label>代理用户名:</label>
596
595
  <input
597
- type="string"
596
+ type="text"
598
597
  value={config.auth_username}
599
598
  onChange={(e) => setConfig({...config, auth_username: e.target.value || ""})}
600
599
  />
601
600
  </div>
602
601
  <div className="setting-row">
603
- <label>代理密码:&nbsp;&nbsp;&nbsp;&nbsp;</label>
602
+ <label>代理密码:</label>
604
603
  <input
605
- type="string"
604
+ type="text"
606
605
  value={config.auth_password}
607
606
  onChange={(e) => setConfig({...config, auth_password: e.target.value || ""})}
608
607
  />
609
608
  </div>
610
609
 
611
- <div className="setting-row">
610
+ <div className="setting-row full-width">
612
611
  <label>公网域名:</label>
613
- <div>如果要公网可访问,OpenWRT 配置相同的端口转发到 AnyProxy 代理端口,这里写公网域名<br />(仅在浏览器通过公网域名+端口查看系统水位时防止回环)</div>
614
612
  <input
615
613
  type="text"
616
614
  value={config.your_domain}
617
615
  onChange={(e) => setConfig({...config, your_domain: e.target.value || ""})}
618
616
  />
617
+ <div className="help-text">如果要公网可访问,OpenWRT 配置相同的端口转发到 AnyProxy 代理端口,这里写公网域名<br />(仅在浏览器通过公网域名+端口查看系统水位时防止回环)</div>
619
618
  </div>
620
619
 
621
- <div className="setting-row">
620
+ <div className="setting-row full-width">
622
621
  <label>VPN_PROXY 设置(留空):</label>
623
- <div>(格式:“127.0.0.1:1087”,仅调试用)</div>
624
622
  <input
625
623
  type="text"
626
624
  value={config.vpn_proxy}
627
625
  onChange={(e) => setConfig({...config, vpn_proxy: e.target.value || ""})}
628
626
  />
627
+ <div className="help-text">(格式:“127.0.0.1:1087”,仅调试用)</div>
629
628
  </div>
630
629
  <div className="setting-row actions">
631
630
  <button
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env sh
2
+
3
+ # --- 参数检查 ---
4
+ if [ $# -eq 0 ]; then
5
+ echo "用法: $0 <URL> [TARGET_IP]"
6
+ echo "说明: 若提供 TARGET_IP,则绕过 DNS,直连该 IP 并使用 URL 中的域名作为 SNI/Host"
7
+ echo "示例:"
8
+ echo " $0 https://www.taobao.com"
9
+ echo " $0 https://www.taobao.com 211.100.8.95"
10
+ exit 1
11
+ fi
12
+
13
+ URL="$1"
14
+ TARGET_IP="${2:-}" # 第二个参数可选
15
+ PROXY="socks5://127.0.0.1:1081"
16
+ TUNNEL_PID=""
17
+ SOCAT_LOG="/tmp/socat_error.log"
18
+
19
+ # --- 从 URL 提取主机名(用于 --resolve 和 SNI)---
20
+ # 移除协议头,再截断路径和端口
21
+ HOST=$(echo "$URL" | sed -E 's|^[^:]+://||' | sed -E 's|/.*$||' | sed -E 's/:.*$//')
22
+ PORT="443"
23
+
24
+ cleanup() {
25
+ if [ -n "$TUNNEL_PID" ]; then
26
+ # 检查进程是否存在
27
+ if kill -0 "$TUNNEL_PID" 2>/dev/null; then
28
+ echo "" >&2
29
+ # echo "🛑 正在终止 socat 隧道 (PID: $TUNNEL_PID)..." >&2
30
+ kill "$TUNNEL_PID" 2>/dev/null
31
+
32
+ # 等待最多 1 秒让它优雅退出
33
+ i=0
34
+ while kill -0 "$TUNNEL_PID" 2>/dev/null && [ $i -lt 10 ]; do
35
+ sleep 0.1
36
+ i=$((i + 1))
37
+ done
38
+
39
+ # 如果还在,强制杀死
40
+ if kill -0 "$TUNNEL_PID" 2>/dev/null; then
41
+ echo "⚠️ 强制终止 socat..." >&2
42
+ kill -9 "$TUNNEL_PID" 2>/dev/null
43
+ fi
44
+
45
+ # 清理僵尸进程
46
+ wait "$TUNNEL_PID" 2>/dev/null
47
+ fi
48
+ fi
49
+ rm -f "$SOCAT_LOG"
50
+ exit "${1:-0}"
51
+ }
52
+
53
+ trap cleanup EXIT INT TERM
54
+
55
+ # --- 检查本地端口 1081 是否已被占用 ---
56
+ if command -v ss >/dev/null 2>&1; then
57
+ if ss -tuln 2>/dev/null | grep -q ':1081\b'; then
58
+ echo "⚠️ 警告: 本地端口 1081 已被占用,socat 可能启动失败。"
59
+ fi
60
+ elif command -v netstat >/dev/null 2>&1; then
61
+ if netstat -tuln 2>/dev/null | grep -q ':1081\b'; then
62
+ echo "⚠️ 警告: 本地端口 1081 已被占用,socat 可能启动失败。"
63
+ fi
64
+ fi
65
+
66
+ # --- 启动 socat 隧道,并记录错误 ---
67
+ echo "🔌 正在启动隧道: socat → OPENSSL:yui.cool:8002 ..."
68
+ socat TCP-LISTEN:1081,fork,bind=127.0.0.1 OPENSSL:yui.cool:8002,verify=0 >"$SOCAT_LOG" 2>&1 &
69
+ TUNNEL_PID=$!
70
+ sleep 0.5
71
+
72
+ # --- 检查 socat 是否仍在运行 ---
73
+ if ! kill -0 "$TUNNEL_PID" 2>/dev/null; then
74
+ echo "❌ 隧道启动失败!socat 报错如下:"
75
+ if [ -s "$SOCAT_LOG" ]; then
76
+ cat "$SOCAT_LOG"
77
+ else
78
+ echo "(无详细错误,可能进程立即崩溃)"
79
+ fi
80
+ exit 1
81
+ else
82
+ : > "$SOCAT_LOG" # 清空日志
83
+ fi
84
+
85
+ # --- 构建 curl 命令 ---
86
+ CURL_CMD="curl -k -I --proxy '$PROXY'"
87
+
88
+ if [ -n "$TARGET_IP" ]; then
89
+ CURL_CMD="$CURL_CMD --resolve '$HOST:$PORT:$TARGET_IP'"
90
+ echo "🌐 绕过 DNS: 直连 $TARGET_IP,SNI = $HOST"
91
+ else
92
+ echo "🌐 使用代理解析 DNS(常规模式)"
93
+ fi
94
+
95
+ echo "📡 请求: $URL via $PROXY"
96
+
97
+ # --- 执行 curl 并计时 ---
98
+ FINAL_CMD="$CURL_CMD $URL"
99
+
100
+ echo "$FINAL_CMD"
101
+
102
+ { time_output=$( { time eval "$FINAL_CMD"; } 2>&1 1>&3 ); } 3>&1
103
+ exit_code=$?
104
+
105
+ # --- 提取状态行 ---
106
+ status_line=$(printf "%s\n" "$time_output" | head -n 1)
107
+
108
+ if [ "$exit_code" -eq 0 ]; then
109
+ echo "✅ 响应状态: $status_line"
110
+ else
111
+ echo "❌ 请求失败(退出码: $exit_code)"
112
+ [ -n "$status_line" ] && echo "⚠️ 部分响应: $status_line"
113
+ fi
114
+
115
+ # --- 显示耗时 ---
116
+ real_time=$(printf "%s\n" "$time_output" | grep "^real" | awk '{print $2}')
117
+ if [ -n "$real_time" ]; then
118
+ echo "⏱️ 耗时: $real_time"
119
+ else
120
+ echo "⚠️ 无法获取耗时(shell 不支持 time 内置命令的格式)"
121
+ fi