Skip to content

Commit e470703

Browse files
Tonnodoubtclaude
andcommitted
refactor(p2p): 移除 FRP 第三层,与后端两层架构对齐
后端已在 ae784ba 中移除内置 FRP 中继,仅保留 LAN 直连 + STUN 打洞。 同步清理移动端: - 删除 DevConnectionConfig/UdpP2PClient/CloudRegistryService 中的 frpIp/frpPort 字段 - 删除 UdpP2PClient.connect() 中 Layer 3 FRP 回退逻辑 - 更新文档为两层架构描述 - 保留 connectionHelp 中用户自建隧道推荐(cpolar/natapp) 净减少约 100 行代码。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ad684df commit e470703

8 files changed

Lines changed: 40 additions & 138 deletions

File tree

docs/README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,7 @@ docs/
7373
│ └── ...
7474
7575
├── solutions/ # 解决方案
76-
│ ├── p2p-solution.md
77-
│ └── frp-reverse-proxy.md
76+
│ └── p2p-solution.md
7877
7978
├── strategy/ # 策略文档
8079
│ ├── cross-platform-components.md

docs/solutions/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
| 文档 | 描述 |
1010
|------|------|
11-
| [p2p-solution.md](./p2p-solution.md) | P2P 连接方案设计(含同WiFi直连、UPnP打洞、FRP中继|
11+
| [p2p-solution.md](./p2p-solution.md) | P2P 连接方案设计(含同WiFi直连、STUN打洞|
1212

1313
---
1414

docs/solutions/p2p-solution.md

Lines changed: 25 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# P2P 网络方案(修订版)
22

3-
**核心目标**:同WiFi直连,跨网先尝试UPnP打洞,失败就走FRP中继
3+
**核心目标**:同WiFi直连,跨网通过STUN打洞连接。打洞失败时用户可自行使用 cpolar/Tailscale 等隧道工具
44

55
---
66

@@ -9,20 +9,20 @@
99
主服务绑定 `localhost:48911` 无法直接对外。启动一个轻量代理,绑定当前 WiFi 的局域网 IP(如 `192.168.1.10:48920`),通过 Token 鉴权保证安全。
1010

1111
```
12-
手机连接策略(三层降级):
12+
手机连接策略(两层降级):
1313
1414
同WiFi? ──► 是 ──► 直连局域网IP(10ms,免费)
1515
16-
└──► 否 ──► UPnP打洞成功? ──► 是 ──► P2P直连(30-80ms,免费)
16+
└──► 否 ──► STUN打洞成功? ──► 是 ──► P2P直连(30-80ms,免费)
1717
18-
└──► 否 ──► FRP中继(100-300ms,付费
18+
└──► 否 ──► 用户自行搭建隧道(cpolar/Tailscale等
1919
```
2020

2121
| 场景 | 连接方式 | 延迟 | 成本 |
2222
|------|---------|------|------|
2323
| 同WiFi | 局域网直连 | <10ms | 免费 |
24-
| 跨网+UPnP成功 | P2P直连 | 30-80ms | 免费 |
25-
| 跨网+UPnP失败 | FRP中继 | 100-300ms | 按需付费 |
24+
| 跨网+STUN成功 | P2P直连 | 30-80ms | 免费 |
25+
| 跨网+STUN失败 | 用户自建隧道 | 视工具而定 | 视工具而定 |
2626

2727
---
2828

@@ -293,74 +293,22 @@ const apiBase = config.p2p?.token
293293
294294
---
295295

296-
## FRP中继兜底(Layer 3)
296+
## 跨网备选方案
297297

298-
UPnP 失败时(CGNAT、路由器不支持等)
298+
当 STUN 打洞失败时(Symmetric NAT 等),用户可自行使用第三方隧道工具
299299

300-
```
301-
手机(4G/5G) ──► 云服务器(FRP) ──► 电脑
302-
转发流量
303-
```
304-
305-
### FRP 架构
306-
307-
后端主服务以 `127.0.0.1:48911` 启动,外部不可直接访问。云端 frps 对手机暴露公网端口,桌面端 frpc 将隧道流量转发至本地 `lan_proxy``127.0.0.1:48920`),由 lan_proxy 执行与 LAN 直连相同的 Token 验证与剥离后,再转发至 Main Server,保持安全模型一致。
308-
309-
> **注意**:FRP 模式下,`lan_proxy` 需额外绑定 `127.0.0.1:48920`(当前仅绑定 LAN IP),以便 frpc 通过本地回环连接。手机请求中的 `?token=xxx` 由 frps/frpc 隧道透传,到达 lan_proxy 后执行验证。
310-
311-
```
312-
手机 App ──HTTP/WS+token──▶ 云端 frps (公网:48920)
313-
314-
frps ↔ frpc 隧道 (bindPort=7000)
315-
316-
frpc ──▶ lan_proxy (127.0.0.1:48920)
317-
318-
Token 验证 + 剥离
319-
320-
Main Server (127.0.0.1:48911)
321-
```
322-
323-
### 配置项
324-
325-
| 环境变量 | 默认值 | 说明 |
326-
|---|---|---|
327-
| `NEKO_FRP_BIND_PORT` | 7000 | frps 内部通信端口 |
328-
| `NEKO_FRP_PROXY_PORT` | 48920 | 对外代理端口(手机连这个) |
329-
| `NEKO_FRP_TOKEN` | neko-frp-default | FRP 认证 token |
300+
- **cpolar**(推荐): cpolar.com — 将本地 48920 端口映射到公网
301+
- **Tailscale**: 免费组网工具,支持 NAT 穿透
302+
- **Cloudflare Tunnel**: 通过 Cloudflare 代理
330303

331-
所有端口支持自动冲突检测和 fallback。
332-
333-
### 使用方式
334-
335-
**后端启动**
336-
```bash
337-
# 直接启动,首次运行会自动下载当前平台的 FRP 二进制
338-
python launcher.py
339-
```
340-
341-
启动成功后会看到:
342-
```
343-
🎉 所有服务器已启动完成!
344-
345-
现在你可以:
346-
1. 启动 lanlan_frd.exe 使用系统
347-
2. 在浏览器访问 http://localhost:48911
348-
3. 手机端连接 <电脑IP>:48920
349-
```
350-
351-
**RN App 端**
352-
在设置页面中,将连接地址设为 `<电脑局域网IP>:48920`
353-
354-
**收费策略**
355-
- 免费用户:每月 3 小时额度
356-
- 付费用户:不限时
304+
安装后将本地端口 48920 映射到公网,用获取的公网地址重新扫码连接即可。
357305

358306
---
359307

360-
## 移动端连接管理(三层降级
308+
## 移动端连接管理(两层降级
361309

362310
```typescript
363-
// 三层降级状态机(简化伪代码)
311+
// 两层降级状态机(简化伪代码)
364312
async function connect(config: ConnectionConfig) {
365313
// 1. 同WiFi → 直连代理
366314
if (config.p2p?.token) {
@@ -371,20 +319,17 @@ async function connect(config: ConnectionConfig) {
371319
// 2. 从云端获取地址
372320
const info = await fetchRelayInfo(config.deviceId);
373321

374-
// 3. 尝试 UPnP 直连
375-
if (info.upnp_ip) {
322+
// 3. 尝试 STUN 直连
323+
if (info.stun_ip) {
376324
try {
377-
const wsUrl = `ws://${info.upnp_ip}:${info.upnp_port}/ws/${info.character}?token=${info.token}`;
325+
const wsUrl = `ws://${info.stun_ip}:${info.stun_port}/ws/${info.character}?token=${info.token}`;
378326
return createNativeRealtimeClient({ url: wsUrl, reconnect: { enabled: true } });
379-
} catch { /* 降级到 FRP */ }
327+
} catch { /* 连接失败,提示用户使用隧道工具 */ }
380328
}
381-
382-
// 4. FRP 兜底
383-
return connectViaFRP(config.deviceId);
384329
}
385330
```
386331

387-
> 注意三层都用 `createNativeRealtimeClient`,同一套代码路径,只是 URL 不同。
332+
> 注意两层都用 `createNativeRealtimeClient`,同一套代码路径,只是 URL 不同。
388333
389334
---
390335

@@ -452,7 +397,7 @@ def lookup(device_id: str):
452397
```
453398
1. 连接断开检测(WebSocket onclose)
454399
2. 自动重连(realtime 库内置)
455-
3. 若 WiFi → 4G:重连到代理失败 → 查云端 → UPnP/FRP
400+
3. 若 WiFi → 4G:重连到代理失败 → 查云端 → STUN
456401
4. 若 4G → WiFi:重连到代理成功 → 直连恢复
457402
```
458403

@@ -470,7 +415,7 @@ def lookup(device_id: str):
470415
| UPnP映射+续期 | ~200行 | 端口映射、心跳 |
471416
| 云端API | ~100行 | FastAPI+Redis |
472417
| 移动端改动 | ~20行 | URL 构造 + config 解析 |
473-
| **合计** | **~440行** | 不含FRP集成 |
418+
| **合计** | **~440行** | |
474419

475420
---
476421

@@ -501,15 +446,15 @@ def lookup(device_id: str):
501446
- 云端地址交换
502447
- 移动端降级连接(URL 不同,client 相同)
503448

504-
### Phase 3:FRP兜底
505-
- FRP集成
506-
- 付费套餐
449+
### Phase 3:用户自建隧道(文档指引)
450+
- 提供 cpolar/Tailscale/Cloudflare Tunnel 使用指引
451+
- 在 App 中添加连接帮助提示
507452

508453
---
509454

510455
## 与原版对比
511456

512-
| | 原版四层 | 当前三层方案 |
457+
| | 原版四层 | 当前两层方案 |
513458
|--|---------|-------------|
514459
| 代理协议 | - | HTTP/WebSocket |
515460
| 移动端额外代码 | - | ~20行 |

hooks/useUdpP2PConnection.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* UDP P2P 连接 Hook
33
*
4-
* 自动处理 UDP P2P 三层连接,并更新 config 中的 host 和 port
4+
* 自动处理 UDP P2P 两层连接,并更新 config 中的 host 和 port
55
*/
66

77
import { useEffect, useRef, useState } from 'react';
@@ -103,8 +103,6 @@ export function useUdpP2PConnection(
103103
lanPort: config.p2p!.lanPort,
104104
stunIp: config.p2p!.stunIp,
105105
stunPort: config.p2p!.stunPort,
106-
frpIp: config.p2p!.frpIp,
107-
frpPort: config.p2p!.frpPort,
108106
cloudRegistryUrl: process.env.EXPO_PUBLIC_CLOUD_REGISTRY_URL,
109107
});
110108

@@ -116,7 +114,6 @@ export function useUdpP2PConnection(
116114
});
117115

118116
addLog(`stunIp=${config.p2p!.stunIp} stunPort=${config.p2p!.stunPort}`);
119-
addLog(`frpIp=${config.p2p!.frpIp} frpPort=${config.p2p!.frpPort}`);
120117

121118
const tcpEndpoint = await client.connect();
122119

@@ -130,7 +127,7 @@ export function useUdpP2PConnection(
130127
setStatus('connected');
131128
setEndpoint(tcpEndpoint);
132129
} else {
133-
addLog('❌ 所有三层连接方式都失败');
130+
addLog('❌ 所有连接方式都失败');
134131
setStatus('failed');
135132
setLayer(null);
136133
}

services/CloudRegistryService.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@ export type CloudDeviceInfo = {
1414
token: string;
1515
stun_ip?: string;
1616
stun_port?: number;
17-
frp_ip?: string;
18-
frp_port?: number;
1917
character?: string;
2018
created_at: number;
2119
};
@@ -67,10 +65,6 @@ export async function queryDeviceInfo(deviceId: string): Promise<DevConnectionCo
6765
// 第2层:STUN 打洞
6866
stunIp: data.stun_ip,
6967
stunPort: data.stun_port,
70-
71-
// 第3层:FRP 中转
72-
frpIp: data.frp_ip,
73-
frpPort: data.frp_port,
7468
},
7569
};
7670

services/DevConnectionStorage.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ function sanitizePartial(input: any): Partial<DevConnectionConfig> {
1717
if (isValidPort(input?.port)) out.port = input.port;
1818
if (isNonEmptyString(input?.characterName)) out.characterName = input.characterName.trim();
1919

20-
// 支持 P2P 配置 (v3: 完整的三层连接信息)
20+
// 支持 P2P 配置 (v3: 完整的两层连接信息)
2121
if (input?.p2p && typeof input.p2p === 'object') {
2222
const p2p = input.p2p;
2323
if (isNonEmptyString(p2p.token)) {
@@ -30,9 +30,6 @@ function sanitizePartial(input: any): Partial<DevConnectionConfig> {
3030
// 第2层:STUN 打洞
3131
stunIp: isNonEmptyString(p2p.stunIp) ? p2p.stunIp : undefined,
3232
stunPort: isValidPort(p2p.stunPort) ? p2p.stunPort : undefined,
33-
// 第3层:FRP 中转
34-
frpIp: isNonEmptyString(p2p.frpIp) ? p2p.frpIp : undefined,
35-
frpPort: isValidPort(p2p.frpPort) ? p2p.frpPort : undefined,
3633
};
3734
}
3835
}

services/UdpP2PClient.ts

Lines changed: 7 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
/**
2-
* UDP P2P 客户端 - v3 三层连接回退
2+
* UDP P2P 客户端 - v3 两层连接回退
33
*
44
* 工作流程:
5-
* 1. UDP 探测(NAT 穿透)- 三层回退
5+
* 1. UDP 探测(NAT 穿透)- 两层回退
66
* 2. 获取 TCP endpoint(HTTP 代理地址)
77
* 3. 切换到 TCP 连接(标准 HTTP API)
88
*/
@@ -20,8 +20,6 @@ export type P2PConfig = {
2020
lanPort?: number;
2121
stunIp?: string;
2222
stunPort?: number;
23-
frpIp?: string;
24-
frpPort?: number;
2523
cloudRegistryUrl?: string; // 云注册中心地址,用于双向打洞协调
2624
};
2725

@@ -42,11 +40,11 @@ export class UdpP2PClient extends EventEmitter {
4240
}
4341

4442
/**
45-
* 尝试建立 P2P 连接(三层回退
43+
* 尝试建立 P2P 连接(两层回退
4644
* @returns TCP endpoint(用于后续 HTTP 连接)或 null
4745
*/
4846
async connect(): Promise<TcpEndpoint | null> {
49-
this.emit('log', '开始三层连接尝试');
47+
this.emit('log', '开始两层连接尝试');
5048

5149
// 第1层:LAN 直连(由 hook 层处理,这里跳过)
5250

@@ -65,25 +63,7 @@ export class UdpP2PClient extends EventEmitter {
6563
this.connected = true;
6664
return endpoint;
6765
}
68-
this.emit('log', '⏱️ 第2层超时,尝试 FRP...');
69-
}
70-
71-
// 第3层:FRP 中转
72-
if (this.config.frpIp && this.config.frpPort) {
73-
this.emit('log', `第3层:FRP 中转 ${this.config.frpIp}:${this.config.frpPort}`);
74-
const endpoint = await this._tryConnect(
75-
this.config.frpIp,
76-
this.config.frpPort,
77-
10000,
78-
3,
79-
'FRP'
80-
);
81-
if (endpoint) {
82-
this.tcpEndpoint = endpoint;
83-
this.connected = true;
84-
return endpoint;
85-
}
86-
this.emit('log', '⏱️ 第3层超时');
66+
this.emit('log', '⏱️ 第2层超时');
8767
}
8868

8969
this.emit('log', '❌ 所有连接方式失败');
@@ -252,10 +232,8 @@ export class UdpP2PClient extends EventEmitter {
252232
this.socket.on('listening', async () => {
253233
this.emit('log', 'Socket 就绪,查询公网地址...');
254234

255-
// 复用 FRP UDP 端口查询公网地址,避免依赖 coturn 3478(可能被 WiFi 封锁)
256-
const stunServer = this.config.frpIp || ip;
257-
const stunQueryPort = this.config.frpPort || 48920;
258-
const myPublicAddr = await this._queryStunAddress(stunServer, stunQueryPort);
235+
// 复用目标服务器查询公网地址
236+
const myPublicAddr = await this._queryStunAddress(ip, port);
259237

260238
if (myPublicAddr && this.config.deviceId && this.config.cloudRegistryUrl) {
261239
this.emit('log', `我的公网地址: ${myPublicAddr.ip}:${myPublicAddr.port}`);

utils/devConnectionConfig.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ export type DevConnectionConfig = {
22
host: string;
33
port: number;
44
characterName: string;
5-
// P2P 连接配置(v3: 三层回退
5+
// P2P 连接配置(v3: 两层回退
66
p2p?: {
77
token: string;
88
deviceId?: string; // 设备 ID(用于云端查询)
@@ -14,10 +14,6 @@ export type DevConnectionConfig = {
1414
// 第2层:STUN 打洞
1515
stunIp?: string; // STUN 公网 IP
1616
stunPort?: number; // STUN 公网端口
17-
18-
// 第3层:FRP 中转
19-
frpIp?: string; // FRP 中转 IP
20-
frpPort?: number; // FRP 中转端口
2117
};
2218
};
2319

@@ -38,13 +34,13 @@ export function parseDevConnectionConfig(raw: string): Partial<DevConnectionConf
3834
if (obj && typeof obj === 'object') {
3935
const out: Partial<DevConnectionConfig> = {};
4036

41-
// 检测 P2P 格式(包含 lan_ip 和 token)- v3 架构:三层回退
37+
// 检测 P2P 格式(包含 lan_ip 和 token)- v3 架构:两层回退
4238
if (typeof obj.lan_ip === 'string' && obj.lan_ip.trim() && typeof obj.token === 'string') {
4339
out.host = obj.lan_ip.trim();
4440
out.port = typeof obj.port === 'number' ? obj.port : 48920;
4541
out.characterName = obj.character || obj.name || 'test';
4642

47-
// v3: 完整的三层连接信息
43+
// v3: 完整的两层连接信息
4844
out.p2p = {
4945
token: obj.token,
5046
deviceId: obj.device_id,
@@ -56,10 +52,6 @@ export function parseDevConnectionConfig(raw: string): Partial<DevConnectionConf
5652
// 第2层:STUN 打洞
5753
stunIp: obj.stun_ip,
5854
stunPort: obj.stun_port,
59-
60-
// 第3层:FRP 中转
61-
frpIp: obj.frp_ip,
62-
frpPort: obj.frp_port,
6355
};
6456
return out;
6557
}

0 commit comments

Comments
 (0)