block-proxy 0.1.14 → 0.1.16

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
@@ -162,211 +162,118 @@ async function init() {
162
162
  clientSocket.on('close', () => proxySocket.destroy());
163
163
  }
164
164
 
165
- // 处理 UDP ASSOCIATE(RFC 1928 合规实现)
165
+ // 处理 UDP ASSOCIATE(UDP over TCP 隧道模式)
166
+ // 客户端通过 TLS TCP 连接发送帧格式的 UDP 数据:[2字节大端长度][SOCKS5 UDP payload]
167
+ // 服务端解帧后用 dgram 发出真实 UDP,响应封帧回传
166
168
  function handleUdpAssociate(clientSocket) {
167
- const udpRelay = dgram.createSocket('udp4');
168
- let clientUdpAddr = null; // 客户端的 UDP 地址(用于回包)
169
+ sendResponse(clientSocket, 0x00);
169
170
 
170
- // 存储 { 'host:port': { rinfo } } 用于回包时知道发给谁
171
- const targetToClientMap = new Map();
171
+ const udpSocket = dgram.createSocket('udp4');
172
+ let buffer = Buffer.alloc(0);
173
+ let idleTimer = null;
174
+ const UDP_IDLE_TIMEOUT = 120_000;
172
175
 
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
- });
176
+ function resetIdleTimer() {
177
+ if (idleTimer) clearTimeout(idleTimer);
178
+ idleTimer = setTimeout(() => {
179
+ clientSocket.destroy();
180
+ }, UDP_IDLE_TIMEOUT);
181
+ }
252
182
 
253
- // 接收从目标服务器返回的 UDP 响应,并转发回客户端
254
- udpRelay.on('listening', () => {
255
- // Node.js 不会自动监听入站响应,但我们已经在 bind 后处于 listening 状态
256
- // 所有 inbound UDP 都会触发 'message',包括响应
183
+ resetIdleTimer();
184
+
185
+ udpSocket.on('message', (msg, rinfo) => {
186
+ if (clientSocket.destroyed) return;
187
+ resetIdleTimer();
188
+
189
+ // 构建 SOCKS5 UDP response header(源地址填入 rinfo)
190
+ const addrParts = rinfo.address.split('.');
191
+ const header = Buffer.alloc(10);
192
+ header[0] = 0x00; // RSV
193
+ header[1] = 0x00; // RSV
194
+ header[2] = 0x00; // FRAG
195
+ header[3] = 0x01; // ATYP = IPv4
196
+ header[4] = parseInt(addrParts[0]);
197
+ header[5] = parseInt(addrParts[1]);
198
+ header[6] = parseInt(addrParts[2]);
199
+ header[7] = parseInt(addrParts[3]);
200
+ header.writeUInt16BE(rinfo.port, 8);
201
+
202
+ const payload = Buffer.concat([header, msg]);
203
+ const frame = Buffer.alloc(2 + payload.length);
204
+ frame.writeUInt16BE(payload.length, 0);
205
+ payload.copy(frame, 2);
206
+ clientSocket.write(frame);
257
207
  });
258
208
 
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 映射**
209
+ clientSocket.on('data', (chunk) => {
210
+ resetIdleTimer();
211
+ buffer = Buffer.concat([buffer, chunk]);
274
212
 
275
- // 正确做法:在收到外部响应时,根据 (rinfo.address:rinfo.port) 查找是否是我们发出的请求的目标
276
- // 但注意:我们发的是 targetHost:targetPort,而响应来自 same address:port
277
-
278
- // 所以我们在发送时,应该用 **rinfo.address:rinfo.port 作为 key 存 clientAddr**
279
- // 但这样不行,因为多个客户端可能访问同一目标。
213
+ while (buffer.length >= 2) {
214
+ const frameLen = buffer.readUInt16BE(0);
215
+ if (frameLen === 0 || frameLen > 65535) {
216
+ clientSocket.destroy();
217
+ return;
218
+ }
219
+ if (buffer.length < 2 + frameLen) break;
280
220
 
281
- // 🚨 更健壮的方式:**每个客户端有自己的 udpRelay**(当前就是这么做的!)
282
- // 所以在这个函数内,所有流量都属于同一个 SOCKS5 TCP 会话的客户端。
283
- // 因此,我们可以安全地假设:**任何非 clientUdpAddr 的 UDP 包都是目标服务器的响应**
221
+ const payload = buffer.slice(2, 2 + frameLen);
222
+ buffer = buffer.slice(2 + frameLen);
284
223
 
285
- // 修改 message handler 如下(替换上面的 handler):
286
- udpRelay.removeAllListeners('message');
287
- udpRelay.on('message', (msg, rinfo) => {
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;
224
+ if (payload.length < 10) continue;
225
+ if (payload[0] !== 0x00 || payload[1] !== 0x00) continue;
226
+ if (payload[2] !== 0x00) continue; // 不支持分片
295
227
 
296
- const atyp = msg[3];
297
- let headerLen = 0, targetHost, targetPort;
228
+ const atyp = payload[3];
229
+ let targetHost, targetPort, headerLen;
298
230
 
299
231
  try {
300
232
  if (atyp === 0x01) {
301
- targetHost = msg.slice(4, 8).join('.');
302
- targetPort = msg.readUInt16BE(8);
233
+ targetHost = payload.slice(4, 8).join('.');
234
+ targetPort = payload.readUInt16BE(8);
303
235
  headerLen = 10;
304
236
  } 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);
237
+ const len = payload[4];
238
+ if (payload.length < 5 + len + 2) continue;
239
+ targetHost = payload.slice(5, 5 + len).toString();
240
+ targetPort = payload.readUInt16BE(5 + len);
309
241
  headerLen = 5 + len + 2;
310
242
  } else if (atyp === 0x04) {
311
- if (msg.length < 22) return;
312
- const ipv6Bytes = msg.slice(4, 20);
243
+ if (payload.length < 22) continue;
244
+ const ipv6Bytes = payload.slice(4, 20);
313
245
  targetHost = '[' + ipv6Bytes.reduce((acc, byte, i) => {
314
246
  if (i % 2 === 0 && i > 0) acc += ':';
315
247
  return acc + byte.toString(16).padStart(2, '0');
316
248
  }, '').replace(/(^|:)0+([0-9a-f]+)/g, '$1$2') + ']';
317
- targetPort = msg.readUInt16BE(20);
249
+ targetPort = payload.readUInt16BE(20);
318
250
  headerLen = 22;
319
251
  } else {
320
- return;
252
+ continue;
321
253
  }
322
254
 
323
- const payload = msg.slice(headerLen);
324
- if (payload.length === 0) return;
255
+ const data = payload.slice(headerLen);
256
+ if (data.length === 0) continue;
325
257
 
326
- // 发送到目标
327
- udpRelay.send(payload, targetPort, targetHost, (err) => {
258
+ udpSocket.send(data, targetPort, targetHost, (err) => {
328
259
  if (err) {
329
- console.warn(`UDP send error to ${targetHost}:${targetPort}:`, err.message);
260
+ console.warn(`UDP forward error to ${targetHost}:${targetPort}:`, err.message);
330
261
  }
331
262
  });
332
263
  } catch (e) {
333
- console.warn('UDP request parse error:', e.message);
264
+ console.warn('UDP frame parse error:', e.message);
334
265
  }
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
266
  }
358
267
  });
359
268
 
360
- udpRelay.on('error', (err) => {
361
- console.error('UDP relay error:', err);
269
+ udpSocket.on('error', (err) => {
270
+ console.warn('UDP socket error:', err.message);
362
271
  clientSocket.destroy();
363
272
  });
364
273
 
365
- // 清理
366
274
  const cleanup = () => {
367
- if (!udpRelay._closed) {
368
- udpRelay.close();
369
- }
275
+ if (idleTimer) clearTimeout(idleTimer);
276
+ try { udpSocket.close(); } catch (e) {}
370
277
  };
371
278
  clientSocket.on('close', cleanup);
372
279
  clientSocket.on('error', cleanup);
package/wiki.md ADDED
@@ -0,0 +1,287 @@
1
+ # Block-Proxy Wiki
2
+
3
+ ## 1)Docker 构建与发布
4
+
5
+ ### 构建命令
6
+
7
+ | 命令 | 说明 |
8
+ |:-----|:------|
9
+ | `npm run docker:build` | 本地构建 amd64 镜像 |
10
+ | `npm run docker:build:arm` | 本地构建 arm64 镜像 |
11
+ | `npm run docker:push` | 构建并推送 amd64 + arm64 双架构到 ACR |
12
+ | `npm run docker:push:amd64` | 仅推送 amd64 |
13
+ | `npm run docker:push:arm64` | 仅推送 arm64 |
14
+
15
+ ### 首次推送前登录 ACR
16
+
17
+ ```bash
18
+ docker login --username=hi50078584@aliyun.com crpi-x1zji86f6jpcd7t1.cn-hangzhou.personal.cr.aliyuncs.com
19
+ ```
20
+
21
+ > 如果打包时磁盘空间不够:`docker system prune -a --volumes`
22
+
23
+ ### Docker 部署 - host 网络模式(推荐)
24
+
25
+ 网关里为了方便获取子网机器 IP 和 MAC 地址,容器需要和宿主机共享同一个网络:
26
+
27
+ ```bash
28
+ docker run --init -d --restart=unless-stopped \
29
+ -e TZ=Asia/Shanghai --network=host \
30
+ --user=root \
31
+ --log-driver local \
32
+ --log-opt max-size=10m \
33
+ --log-opt max-file=3 \
34
+ --cpus="5" \
35
+ --memory 400m \
36
+ -v "$(pwd)/":/app/config \
37
+ --name block-proxy \
38
+ crpi-x1zji86f6jpcd7t1.cn-hangzhou.personal.cr.aliyuncs.com/lijing00333/block-proxy:latest
39
+ ```
40
+
41
+ ### 挂载配置文件(config.json 和 rule.js)
42
+
43
+ Docker 启动命令中的 `-v "$(pwd)/":/app/config` 将宿主机当前目录映射到容器内的 `/app/config`。代理启动时会自动从该目录加载配置。
44
+
45
+ #### config.json
46
+
47
+ 容器首次启动时,如果挂载目录下没有 `config.json`,代理会自动生成一份默认配置。你也可以预先创建:
48
+
49
+ ```json
50
+ {
51
+ "block_hosts": [],
52
+ "proxy_port": 8001,
53
+ "socks5_port": 8002,
54
+ "auth_username": "admin",
55
+ "auth_password": "your-password",
56
+ "enable_express": "1",
57
+ "enable_socks5": "1"
58
+ }
59
+ ```
60
+
61
+ 配置修改后通过后台面板(`:8003`)保存,或重启容器后生效。
62
+
63
+ #### rule.js(自定义 MITM 规则)
64
+
65
+ 在挂载目录下创建 `rule.js`,代理启动时会自动加载并与内置规则合并。参照 [`example/rule.js`](https://github.com/jayli/block-proxy/blob/main/example/rule.js) 格式:
66
+
67
+ ```js
68
+ module.exports = {
69
+ MyRule: [{
70
+ type: 'beforeSendRequest',
71
+ host: 'example.com',
72
+ regexp: '^https?://example\\.com/blocked',
73
+ callback: async function(url, request, response) {
74
+ return {
75
+ response: {
76
+ statusCode: 403,
77
+ header: { 'Content-Type': 'text/plain' },
78
+ body: 'Blocked by custom rule'
79
+ }
80
+ };
81
+ }
82
+ }]
83
+ };
84
+ ```
85
+
86
+ 规则支持两种类型:
87
+ - `beforeSendRequest` — 在请求发出前拦截,可返回本地响应阻断请求
88
+ - `beforeSendResponse` — 在响应返回前修改,可改写响应体
89
+
90
+ 修改 `rule.js` 后需重启容器使规则生效。
91
+
92
+ > 注意:`config.json` 是运行时配置文件,通过后台面板修改后由代理自动写入。`rule.js` 需要手动编辑,容器只读取不写入。
93
+
94
+ ### Docker 部署 - 端口绑定模式(Windows/Mac)
95
+
96
+ ```bash
97
+ docker run --init -d --restart=unless-stopped --user=root \
98
+ -v "$(pwd)/":/app/config \
99
+ -e TZ=Asia/Shanghai -p 8001:8001 -p 8002:8002 \
100
+ --name block-proxy \
101
+ crpi-x1zji86f6jpcd7t1.cn-hangzhou.personal.cr.aliyuncs.com/lijing00333/block-proxy:latest
102
+ ```
103
+
104
+ ---
105
+
106
+ ## 2)服务端配置
107
+
108
+ ### 端口说明
109
+
110
+ | 端口 | 说明 | 可否关闭 |
111
+ |:----:|:------|:------:|
112
+ | 3000 | 调试端口(仅 `npm run dev` 时启用) | 生产环境不启用 |
113
+ | 8001 | HTTP 代理端口 | 不可禁用 |
114
+ | 8002 | Socks5 over TLS 代理端口 | 可禁用 |
115
+ | 8003 | 后台配置面板端口 | 可禁用 |
116
+
117
+
118
+ ### 配置面板
119
+
120
+ - 后台配置:访问 `http://server-ip:8003`
121
+ - 关闭/启用配置面板:`http://server-ip:8001`
122
+
123
+ 首次启动后访问 `http://代理IP:8001` 根据提示操作。block-proxy 可以配置只启动 proxy 而不启动后台面板。
124
+
125
+ ### 路由表
126
+
127
+ 路由表每两小时自动刷新一次。如果新入网的设备未显示,在后台手动刷新路由表,添加限制条件后点击「重启代理」即可。
128
+
129
+ ---
130
+
131
+ ## 3)客户端配置
132
+
133
+ ### 代理端口
134
+
135
+ - **8001**:HTTP 代理
136
+ - **8002**:Socks5 over TLS
137
+
138
+ > ⚠️ Socks5 代理不支持对 MAC 地址的定向拦截,MAC 地址拦截仅对局域网内 HTTP 代理绑定生效。建议局域网绑定 HTTP 代理,公网绑定 Socks5 代理。
139
+
140
+ > ⚠️ 使用小火箭的 Socks5 over TLS 代理时,TLS 选项中勾选「允许不安全」。
141
+
142
+ ### 证书安装(iOS)
143
+
144
+ 1. 进入后台配置面板,扫码下载证书
145
+ 2. 手机设置中安装该证书
146
+ 3. 设置 → 通用 → 关于本机 → 证书信任设置 → 打开对 AnyProxy 的完全信任
147
+
148
+ ### 代理设置(iPhone/iPad)
149
+
150
+ 设置 → 无线局域网 → 点击当前网络 → HTTP 代理/配置代理 → 手动 → 填写服务器地址和端口。
151
+
152
+ ### 固定 MAC 地址
153
+
154
+ 如果要通过 MAC 地址拦截小朋友上网,在小朋友设备中把 MAC 地址固定下来(关闭私有 Wi-Fi 地址):
155
+
156
+ - iOS:设置 → 无线局域网 → 点击当前网络 → 关闭「私有 Wi-Fi 地址」
157
+
158
+ ### macOS 客户端
159
+
160
+ 提供 SocksClient.app 桌面端连接工具,支持 macOS(M 系列芯片)。
161
+
162
+ 下载地址:[GitHub Release](https://github.com/jayli/block-proxy/releases/latest)
163
+
164
+ ---
165
+
166
+ ## 4)防火墙配置:禁止设备直连
167
+
168
+ 防止小朋友修改 Wi-Fi 连接绕过代理,在网关配置防火墙规则禁止设备直连:
169
+
170
+ ```bash
171
+ iptables -I FORWARD -m mac --mac-source D2:9E:8D:1B:F1:4E -j REJECT
172
+ ip6tables -I forwarding_rule -m mac --mac-source D2:9E:8D:1B:F1:4E -j REJECT
173
+ ```
174
+
175
+ 执行后重启防火墙生效。
176
+
177
+ ---
178
+
179
+ ## 5)MITM 规则
180
+
181
+ ### 生效条件
182
+
183
+ 1. 客户端设备必须安装 AnyProxy 证书
184
+ 2. 服务需要根据 IP 反查 MAC 地址,需要部署在可扫描子网的节点(推荐 OpenWrt 网关)
185
+ 3. 路由表每 2 小时更新一次,新入网设备建议在后台手动刷新并重启代理
186
+ 4. 所有拦截规则仅在 HTTP 代理中生效;Socks5 over TLS 是反向代理,MAC 地址拦截只对直连 HTTP 代理生效
187
+
188
+ ### YouTube 去广告
189
+
190
+ 在后台配置中添加以下 4 条 reject 规则:
191
+
192
+ | 域名 | Match Rule |
193
+ |:-----|:-----------|
194
+ | `youtube.com` | `^https?:\/\/(www\|s)\.youtube\.com\/(pagead\|ptracking)` |
195
+ | `youtube.com` | `^https?:\/\/s\.youtube\.com\/api\/stats\/qoe\?adcontext` |
196
+ | `youtube.com` | `^https?:\/\/(www\|s)\.youtube\.com\/api\/stats\/ads` |
197
+ | `googlevideo.com` | `^https?:\/\/[\w-]+\.googlevideo\.com\/(?!(dclk_video_ads\|videoplayback\?)).+&oad` |
198
+
199
+ 另外两条规则在源码中自动生效:[`proxy/mitm/rule.js`](https://github.com/jayli/block-proxy/blob/main/proxy/mitm/rule.js)。
200
+
201
+ ### 有道词典会员
202
+
203
+ 已内置,无需额外配置。
204
+
205
+ ### 自定义规则
206
+
207
+ 参照 [`example/rule.js`](https://github.com/jayli/block-proxy/blob/main/example/rule.js) 编写配置文件,启动时通过 `-c` 参数加载:
208
+
209
+ ```bash
210
+ block-proxy -c rule.js
211
+ ```
212
+
213
+ ---
214
+
215
+ ## 6)开发指南
216
+
217
+ ### 环境准备
218
+
219
+ ```bash
220
+ git clone https://github.com/jayli/block-proxy.git
221
+ cd block-proxy
222
+ pnpm i
223
+ ```
224
+
225
+ ### 开发命令
226
+
227
+ | 命令 | 说明 |
228
+ |:-----|:------|
229
+ | `npm run dev` | 开发模式,启动全部服务(代理 + 后台 + React HMR :3000) |
230
+ | `npm run craco` | 仅启动 React 开发服务器(端口 3000) |
231
+ | `npm run start` | 生产模式启动 |
232
+ | `npm run proxy` | 仅启动代理,不启动后台面板 |
233
+ | `npm run socks5` | 仅启动 Socks5 服务 |
234
+ | `npm run build` | 构建 React 前端 |
235
+
236
+ ### 测试
237
+
238
+ ```bash
239
+ npm run test:proxy # 代理连通性/性能/吞吐量测试(需先启动代理服务)
240
+ ```
241
+
242
+ ### 代码结构
243
+
244
+ ```
245
+ ├── proxy/ # 代理核心:AnyProxy 集成、拦截逻辑、MITM 规则
246
+ │ └── mitm/ # MITM 规则定义(YouTube 去广告、有道词典 VIP)
247
+ ├── socks5/ # SOCKS5 over TLS 实现
248
+ ├── server/ # Express 后台 API + 管理面板
249
+ ├── src/ # React 前端(CRA + CRACO)
250
+ ├── client/ # macOS 客户端(Python)
251
+ ├── test/ # 测试套件
252
+ ├── cert/ # TLS/CA 证书
253
+ ├── bin/start.js # CLI 入口(block-proxy 命令)
254
+ └── config.json # 运行时配置
255
+ ```
256
+
257
+ ---
258
+
259
+ ## 7)性能测试
260
+
261
+ ### 并发测试
262
+
263
+ | 直连 | 代理 |
264
+ |:----:|:----:|
265
+ | <img height="400" alt="直连测试" src="https://github.com/user-attachments/assets/8268bc5c-956f-4b67-89c1-cdd5725114b3" /> | <img height="400" alt="代理测试" src="https://github.com/user-attachments/assets/abf4bfa1-c8b8-4907-ba0e-bcc76e8899fa" /> |
266
+
267
+ ### 网速测试
268
+
269
+ <img width="544" alt="网速测试" src="https://github.com/user-attachments/assets/67c61e34-67ae-4345-97ca-d266cd35ddf4" />
270
+
271
+ ---
272
+
273
+ ## 8)已知问题
274
+
275
+ ### iOS Safari 安全限制
276
+
277
+ iOS Safari 不支持带认证的代理与网关使用相同 IP 地址。两种解决方案:
278
+
279
+ 1. 不填写代理认证用户名和密码(留空则验证始终通过)
280
+ 2. 给 OpenWrt LAN 口额外绑定一个 IP,iOS 设备使用该 IP 作为代理地址
281
+
282
+ <img width="300" alt="openwrt IP 绑定" src="https://github.com/user-attachments/assets/0f46d6b4-00b1-44aa-9be7-fa23a09bb199" />
283
+
284
+
285
+ ## 9)网络拓扑
286
+
287
+ <img width="600" alt="image" src="https://github.com/user-attachments/assets/2d8a4ec7-8ced-446d-8777-6eaa30e27bc0" />
Binary file
Binary file
Binary file