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/.claude/settings.local.json +88 -34
- package/.claude/skills/icon_generate/skill.md +45 -0
- package/CLAUDE.md +3 -3
- package/README.md +28 -187
- package/client/app.py +29 -9
- package/client/build.sh +6 -0
- package/client/config.py +1 -0
- package/client/config_window.py +36 -26
- package/client/icons/app.icns +0 -0
- package/client/icons/christmas-sock_light.png +0 -0
- package/client/icons/christmas-sock_light_bar.png +0 -0
- package/client/icons/christmas-sock_light_bar_off.png +0 -0
- package/client/icons/socks_on_G.png +0 -0
- package/client/icons/socks_on_G_bar.png +0 -0
- package/client/icons/socks_on_M.png +0 -0
- package/client/icons/socks_on_M_bar.png +0 -0
- package/client/proxy_core.py +305 -21
- package/client/tests/test_proxy_core_udp.py +466 -0
- package/config.json +3 -1
- package/package.json +1 -1
- package/socks5/server.js +74 -167
- package/wiki.md +287 -0
- package/client/icons/backup/app_example.png +0 -0
- package/client/icons/backup/christmas-sock_dark.png +0 -0
- package/client/icons/backup/christmas-sock_light.png +0 -0
- package/client/icons/backup/socks_on_G.png +0 -0
- package/client/icons/backup/socks_on_M.png +0 -0
- package/client/icons/christmas-sock_dark.png +0 -0
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(
|
|
165
|
+
// 处理 UDP ASSOCIATE(UDP over TCP 隧道模式)
|
|
166
|
+
// 客户端通过 TLS TCP 连接发送帧格式的 UDP 数据:[2字节大端长度][SOCKS5 UDP payload]
|
|
167
|
+
// 服务端解帧后用 dgram 发出真实 UDP,响应封帧回传
|
|
166
168
|
function handleUdpAssociate(clientSocket) {
|
|
167
|
-
|
|
168
|
-
let clientUdpAddr = null; // 客户端的 UDP 地址(用于回包)
|
|
169
|
+
sendResponse(clientSocket, 0x00);
|
|
169
170
|
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
260
|
-
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
// 因此,我们可以安全地假设:**任何非 clientUdpAddr 的 UDP 包都是目标服务器的响应**
|
|
221
|
+
const payload = buffer.slice(2, 2 + frameLen);
|
|
222
|
+
buffer = buffer.slice(2 + frameLen);
|
|
284
223
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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 =
|
|
297
|
-
let
|
|
228
|
+
const atyp = payload[3];
|
|
229
|
+
let targetHost, targetPort, headerLen;
|
|
298
230
|
|
|
299
231
|
try {
|
|
300
232
|
if (atyp === 0x01) {
|
|
301
|
-
targetHost =
|
|
302
|
-
targetPort =
|
|
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 =
|
|
306
|
-
if (
|
|
307
|
-
targetHost =
|
|
308
|
-
targetPort =
|
|
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 (
|
|
312
|
-
const ipv6Bytes =
|
|
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 =
|
|
249
|
+
targetPort = payload.readUInt16BE(20);
|
|
318
250
|
headerLen = 22;
|
|
319
251
|
} else {
|
|
320
|
-
|
|
252
|
+
continue;
|
|
321
253
|
}
|
|
322
254
|
|
|
323
|
-
const
|
|
324
|
-
if (
|
|
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
|
|
260
|
+
console.warn(`UDP forward error to ${targetHost}:${targetPort}:`, err.message);
|
|
330
261
|
}
|
|
331
262
|
});
|
|
332
263
|
} catch (e) {
|
|
333
|
-
console.warn('UDP
|
|
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
|
-
|
|
361
|
-
console.
|
|
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 (
|
|
368
|
-
|
|
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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|