ACP v1.4 新增 — DCUtR 风格 UDP 打洞,三级连接策略自动降级,用户零感知。
| 我的网络环境 | 对方网络环境 | 需要 Relay? | 接入方式 |
|---|---|---|---|
| 有公网 IP | 任意 | 不需要 | 直连(Level 1) |
| 局域网内 | 同一局域网 | 不需要 | 直连(Level 1) |
| NAT 后面 | 有公网 IP | 不需要 | 直连(Level 1) |
| NAT 后面 | NAT 后面 | 需要(初始) | 自动 UDP 打洞(Level 2) |
| CGNAT | 任意 | 需要(永久) | Relay 兜底(Level 3) |
ACP 自动按顺序尝试上面三个级别,你不需要做任何配置。
Level 1 ─ 直接连接(3s timeout)
├─ 条件:至少一方有公网 IP,或双方在同一内网
├─ 延迟:最低(< 100ms)
└─ 失败 → Level 2
Level 2 ─ UDP 打洞(DCUtR 风格,5s timeout)★ v1.4 新增
├─ 条件:双方都在 NAT 后面,但 NAT 类型兼容
├─ 流程:
│ 1. 通过已有的 Relay 连接交换双方公网地址(STUN 发现)
│ 2. 双方在约定时刻(t_punch)同时发 UDP 探测包
│ 3. NAT 设备因为出站包而开放入站规则(打洞)
│ 4. 确认对方 UDP 回包,建立直连
│ 5. Relay 连接关闭,后续通信不再经过任何中间节点
├─ 成功率:~70%(覆盖 Full Cone / Restricted Cone NAT)
├─ 延迟:+100~600ms(打洞握手一次性开销)
└─ 失败 → Level 3
Level 3 ─ Relay 永久中转(兜底)
├─ 条件:对称 NAT(Symmetric NAT)/ CGNAT / 打洞失败
├─ 机制:所有消息帧经过 Relay Server 转发(不存储)
├─ 延迟:额外 +50~200ms(取决于 Relay 位置)
└─ 成功率:100%(只要能访问 Relay)
整个过程对宿主应用完全透明——无论走哪条路径,/message:send 和 /stream API 行为完全一致。
不需要 Relay。对方可以直接通过 acp://你的IP:7801/token 连接。
启动方式:
python3 acp_relay.py --name "MyAgent" --port 7801ACP 会输出可分享的 acp:// 链接,直接发给对方即可。
v1.4 之前,这种场景必须手动指定 --relay。v1.4 起完全自动:ACP 先尝试 UDP 打洞升级到直连,失败才使用 Relay 兜底。
你仍然需要一个公网可访问的 Relay 节点作为信令服务器(用于初始握手和打洞协调)。选择:
在任意有公网 IP 的 VPS 上运行:
# 在 VPS 上启动(保持运行)
python3 acp_relay.py --name "RelayNode" --port 7801
# 它会输出:
# acp://YOUR_VPS_IP:7801/tok_xxxxx然后将这个链接分享给双方,双方各自连接即可。
使用默认公共 Relay(Cloudflare Worker):
python3 acp_relay.py --name "MyAgent" --relay
⚠️ 不建议在生产环境依赖公共 Relay——它是无状态的,不提供 SLA 保证。
python3 acp_relay.py --name "MyAgent" --relay --relay-url https://your-relay.example.com内网部署无需公网。在内网任意一台机器上启动:
python3 acp_relay.py --name "TeamRelay" --port 7801所有内网 Agent 用内网 IP 连接,不经过任何公网节点。可以同时开启 mDNS 广播,让局域网内的 Agent 自动发现彼此:
python3 acp_relay.py --name "TeamRelay" --advertise-mdnsCGNAT(电信宽带常见)下,UDP 打洞成功率极低(多级 NAT 导致地址映射不一致)。ACP 会自动检测打洞失败并永久使用 Relay 兜底,你不需要做任何额外配置,只是需要一个可访问的 Relay 节点(同情况 B)。
# 需要 Python 3.9+ 和 websockets
pip install websockets
python3 acp_relay.py --name "Relay" --port 7801输出:
13:00:00 [acp] HTTP interface: http://127.0.0.1:7901
13:00:00 [acp] WebSocket: ws://0.0.0.0:7801
13:00:00 [acp] =============================================
13:00:00 [acp] ACP v1.4 — host mode ready
13:00:00 [acp] Your link: acp://1.2.3.4:7801/tok_xxxxx
13:00:00 [acp] Share this with the other Agent to connect
13:00:00 [acp] =============================================
| 端口 | 协议 | 说明 |
|---|---|---|
7801 |
WebSocket | Agent 互连(需公网开放) |
7901 |
HTTP | 本地管理 API(仅 localhost) |
防火墙只需开放 TCP 7801(入站)。
curl http://localhost:7901/status
# → {"connected": true, "acp_version": "1.4.0", ...}
curl http://localhost:7901/.well-known/acp.json
# → AgentCard JSON# /etc/systemd/system/acp-relay.service
[Unit]
Description=ACP Relay Server
After=network.target
[Service]
Type=simple
User=acp
WorkingDirectory=/opt/acp
ExecStart=/usr/bin/python3 /opt/acp/acp_relay.py --name "ProductionRelay" --port 7801
Restart=on-failure
RestartSec=5s
StandardOutput=journal
StandardError=journal
Environment=PYTHONUNBUFFERED=1
[Install]
WantedBy=multi-user.target# 安装并启动
sudo systemctl daemon-reload
sudo systemctl enable acp-relay
sudo systemctl start acp-relay
sudo systemctl status acp-relay
# 查看日志
sudo journalctl -u acp-relay -fdocker run -d \
--name acp-relay \
--restart unless-stopped \
-p 7801:7801 \
-e PYTHONUNBUFFERED=1 \
ghcr.io/kickflip73/agent-communication-protocol/acp-relay:latest \
--name ProductionRelay --port 7801UDP 打洞(NAT Hole Punching)利用了大多数 NAT 设备的一个特性:
当你向外发出一个 UDP 包(出站),NAT 会在自身打开一个「洞」(映射记录), 允许从那个目标地址返回的 UDP 包通过。
DCUtR 的关键改进:双方在同一时刻同时发包。这确保双方的 NAT 在对方的包到达前已经打开了洞。
这些消息在 Relay WebSocket 连接上传输,用于协调打洞,不影响业务消息:
// 1. 发起方 → 响应方:发起打洞请求
{
"type": "dcutr_connect",
"addresses": ["1.2.3.4:9001", "192.168.1.1:9001"],
"session_id": "550e8400-e29b-41d4-a716-446655440000"
}
// 2. 响应方 → 发起方:同步打洞时刻
{
"type": "dcutr_sync",
"addresses": ["5.6.7.8:9002"],
"session_id": "550e8400-e29b-41d4-a716-446655440000",
"t_punch": 1711180800.500
}
// 3. 发起方 → 响应方(可选):通知结果
{
"type": "dcutr_result",
"session_id": "550e8400-e29b-41d4-a716-446655440000",
"success": true,
"direct_addr": "5.6.7.8:9002"
}ACP 使用 Google 公共 STUN 服务器(stun.l.google.com:19302)发现公网 UDP 地址。
实现为纯 stdlib(约 120 行),无需 aiortc、aioice 等第三方库。
支持 STUN RFC 5389 / RFC 8489:
- XOR-MAPPED-ADDRESS(优先)
- MAPPED-ADDRESS(兜底)
| NAT 类型 | UDP 打洞成功率 | 说明 |
|---|---|---|
| Full Cone NAT | ✅ 高(~95%) | 最友好,常见于家用路由器 |
| Restricted Cone NAT | ✅ 高(~85%) | 常见于企业防火墙 |
| Port-Restricted Cone NAT | ✅ 中(~70%) | 需要端口精确匹配 |
| Symmetric NAT | ❌ 低(~10%) | 每次出站映射不同端口,难以打洞 |
| CGNAT(运营商级) | ❌ 极低(~5%) | 多级 NAT,基本依赖 Relay |
无需手动处理。ACP 在 5 秒超时后自动降级到 Level 3(Relay 永久中转)。你的连接会继续工作,只是消息经过 Relay 中转。
检查日志输出:
[connect] Level 1 direct connect succeeded → 直连
[connect] Level 2 hole punch succeeded → UDP 打洞直连
[connect] Level 3 relay fallback (permanent) → Relay 中转
或者查询 /status 端点(未来版本将添加 connection_type 字段)。
- Level 1/2(直连)延迟高:通常是网络路由问题,与 ACP 无关
- Level 3(Relay)延迟高:Relay 地理位置远,考虑自托管更近的 Relay
- 打开调试日志:
PYTHONPATH=. python3 acp_relay.py --log-level DEBUG
完全兼容。acp:// 链接格式不变,NAT 穿透对上层透明。v1.3 的 Agent 可以和 v1.4 的 Agent 正常通信,只是不会触发 Level 2 打洞流程(对方不理解 dcutr_connect 消息,ACP 会静默降级)。
UDP(通用数据报协议)。打洞成功后,ACP 会在打洞建立的 NAT 映射上建立一个新的 WebSocket(TCP) 连接用于实际通信。UDP 只用于打洞阶段,不用于消息传输。
- Level 1(有公网 IP 端):需开放 TCP 7801 入站
- Level 2(UDP 打洞):不需要预先开放端口,NAT 在打洞时自动开放
- Level 3(Relay):只需出站 HTTPS/WebSocket,无需入站端口