One API 3000 端口突然访问不了?一次 Tailscale Serve、Docker NAT 与 loopback 的排查记录
记录一次真实故障:为了让 RAG 系统的 api.golforfun.net 用上 443,执行了 tailscale serve reset,结果 One API 原本可用的 x162-43-92-249.tail9c4209.ts.net:3000 突然失效,OpenClaw 也跟着报错。排查过程中试过 iptables 放行、Tailscale Serve 重配、Docker 端口映射,最终用「同机服务改走 loopback」彻底解决。核心教训:同机服务不要硬编码 Tailscale IP。
一、故障现象
某天,One API 的 Tailscale 访问地址突然打不开:
http://x162-43-92-249.tail9c4209.ts.net:3000/ → 无法访问
同时 OpenClaw 的 Agent 开始报错,因为它配置里硬编码了:
"baseUrl": "http://100.99.4.120:3000/v1"
但 VPS 本地检查一切正常:
curl -s -o /dev/null -w "HTTP %{http_code}\n" \
-H "Authorization: Bearer sk-..." \
http://127.0.0.1:3000/v1/models
# HTTP 200
docker ps | grep one-api
# Up 38 hours
One API 容器没挂,本地 200,但 Tailscale URL 和公网 IP 都访问不了。
二、排查过程
2.1 先确认不是 One API 本身的问题
systemctl status docker --no-pager | head -5
# active (running)
docker ps | grep one-api
# Up 38 hours
curl -s -o /dev/null -w "One API: HTTP %{http_code}\n" \
-H "Authorization: Bearer sk-..." \
http://127.0.0.1:3000/v1/models
# One API: HTTP 200
Docker、容器、本地 API 全部正常。问题在网络访问路径,不在服务本身。
2.2 检查 UFW 和 iptables
ufw status | grep 3000
# 3000/tcp ALLOW Anywhere
iptables -L ufw-user-input -n | grep 3000
# ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:3000
UFW 和 iptables 都放行了 3000。但 MacBook 上 nc -zv 162.43.92.249 3000 返回 Connection refused,不是 timeout。
refused 意味着包到了 VPS,但被主动拒绝(RST),不是 DROP。这指向两个可能:
- iptables 有 REJECT 规则
- 更外层(XServer 防火墙)回了 RST
2.3 检查 INPUT 链的完整规则
iptables -L INPUT -n --line-numbers | head -20
输出显示:
Chain INPUT (policy DROP)
1-12 ACCEPT tcp -- 172.17.0.0/16 ... tcp dpt:3000(只允许内网)
13 ts-input all -- 0.0.0.0/0 0.0.0.0/0
14-18 ufw-* 链
关键发现:INPUT 链的 policy 是 DROP,而 3000 只对 172.17.0.0/16 等 Docker 内网放行。公网 IP 的 3000 流量没有匹配到任何 ACCEPT 规则,落到最后的 DROP。
2.4 检查 ts-input 链
iptables -L ts-input -n --line-numbers
输出:
1-3 ACCEPT tcp -- 0.0.0.0/0 ... tcp dpt:3000(之前手动加的,重复三次)
4 ACCEPT all -- 100.99.4.120 0.0.0.0/0
5 ACCEPT all -- 0.0.0.0/0 0.0.0.0/0
6 ACCEPT udp -- 0.0.0.0/0 ... udp dpt:41641
7 RETURN all -- 100.115.92.0/23 0.0.0.0/0
8 DROP all -- 100.64.0.0/10 0.0.0.0/0
问题在这里:第 8 条 DROP all -- 100.64.0.0/10 是 Tailscale 的防 IP 欺骗规则,会丢弃来自 CGNAT 网段(100.64.0.0/10)的流量。MacBook 的 Tailscale IP 在这个网段里,所以访问 100.99.4.120:3000 时被这条 DROP 拦了。
第 1–3 条手动加的 ACCEPT 理论上应该优先,但包根本没走到 ts-input 链——Docker 的 DNAT 规则在 INPUT 链之前就把包转走了,或者 Tailscale 的 netstack 在更底层处理了连接。
2.5 检查 Docker 端口映射
docker inspect one-api | grep -A 5 "PortBindings"
输出:
"PortBindings": {
"3000/tcp": [
{ "HostIp": "", "HostPort": "3000" }
]
}
HostIp: "" 等价于 0.0.0.0,理论上应该覆盖所有接口。但 Docker 的 DNAT 规则通常只匹配 eth0 进来的流量,tailscale0 进来的包可能没被正确 NAT 到容器。
三、根本原因
两个原因叠加:
- tailscale serve reset 清掉了 Tailscale Serve 在 3000 上的转发规则。原本 x162-43-92-249.tail9c4209.ts.net:3000 能用,是因为 Serve 在 3000 上做了转发。reset 之后规则没了。
- Docker 的端口映射不覆盖 tailscale0 接口。即使 UFW 和 iptables 放行,从 Tailscale 进来的包也没被正确 NAT 到容器。
为什么改 443 会连带影响 3000
为了给 RAG 系统的 api.golforfun.net 配 HTTPS,做了这些操作:
- 给 api.golforfun.net 签 Let’s Encrypt 证书
- nginx 绑 162.43.92.249:443
- 因为 100.99.4.120:443 被 tailscaled 占着,nginx 只能绑公网 IP
- 期间跑过
tailscale serve reset,清掉了所有 Serve 规则
443 和 3000 本身不冲突,冲突的是 Tailscale Serve 的 HTTPS 默认端口(443)和 nginx 的 443。在解决这个冲突时,tailscale serve reset 把 3000 的转发规则也一起清掉了。
四、尝试过的方案
方案 A:手动加 iptables ACCEPT(失败)
iptables -I ts-input 1 -p tcp --dport 3000 -j ACCEPT
失败。因为包没走到 ts-input 链,或者 Docker NAT 在更底层就处理了。
方案 B:tailscale serve –bg 3000(失败)
tailscale serve --bg 3000
# 报错:foreground listener already exists for port 443
Tailscale Serve 默认用 443 做 HTTPS 入口,443 被 nginx 占了,冲突。
方案 C:tailscale serve –bg –https=8443(可行,但 URL 变了)
tailscale serve --bg --https=8443 http://127.0.0.1:3000
成功后访问 https://x162-43-92-249.tail9c4209.ts.net:8443。但 URL 变了,OpenClaw 里硬编码的 :3000 还是失效。
方案 D:改 Docker 端口映射(风险大,没采用)
把 0.0.0.0:3000->3000 改成同时绑 127.0.0.1:3000 和 100.99.4.120:3000。但这要改 docker-compose.yml,重启 One API,影响所有依赖它的服务。风险太高。
方案 E:同机服务改走 loopback(最终采用)
核心洞察:OpenClaw 和 One API 在同一台 VPS 上,127.0.0.1:3000 和 100.99.4.120:3000 在功能上完全等价,但 loopback 不经过任何网络层(iptables、Docker NAT、Tailscale)。
改 openclaw.json:
cp /root/.openclaw/openclaw.json /root/.openclaw/openclaw.json.bak.$(date +%s)
sed -i 's|http://100\.99\.4\.120:3000|http://127.0.0.1:3000|g' /root/.openclaw/openclaw.json
systemctl restart openclaw-gateway
重启后 OpenClaw 正常,100.99.4.120:3000 不再被任何服务依赖。
五、最终状态
| 服务 | 调用方式 | 状态 |
|---|---|---|
| OpenClaw | 127.0.0.1:3000 | ✅ 正常 |
| RAG (api.golforfun.net) | 127.0.0.1:3000 | ✅ 正常 |
| One API 本地 | 127.0.0.1:3000 | ✅ 200 |
| nginx 443 (api.golforfun.net) | 162.43.92.249:443 | ✅ 正常 |
| dsh 3081 | nginx 直接监听 | ✅ 正常 |
| 100.99.4.120:3000 | 不再被依赖 | 可忽略 |
| x162-43-92-249.tail9c4209.ts.net:3000 | 不再被依赖 | 可忽略 |
六、给 MacBook 配 Agent 用什么 URL
MacBook 和 VPS 不在同一个 loopback 里,不能用 127.0.0.1:3000。三种方式:
1. SSH 隧道(最安全,适合偶尔用)
# MacBook 上
ssh -L 3000:127.0.0.1:3000 root@162.43.92.249
然后 Mac 上的应用用 http://127.0.0.1:3000/v1。
2. Tailscale Serve 8443(长期用)
# VPS 上
tailscale serve --bg --https=8443 http://127.0.0.1:3000
Mac 上用 https://x162-43-92-249.tail9c4209.ts.net:8443/v1。
3. 公网 IP(不推荐)
http://162.43.92.249:3000/v1 需要 XServer パケットフィルター、UFW、iptables 三层都放行,且 One API 管理后台暴露公网有安全风险。
七、踩坑清单
| # | 坑 | 解法 |
|---|---|---|
| 1 | tailscale serve reset 清掉所有 Serve 规则,包括 3000 的转发 | 跑 reset 前确认没有其他服务依赖 Serve |
| 2 | Tailscale Serve 默认用 443,和 nginx 的 443 冲突 | 用 –https=8443 避开 |
| 3 | Docker 的 DNAT 不覆盖 tailscale0,Tailscale IP 访问不到容器 | 同机服务改走 loopback,跨机用 Serve 或 SSH 隧道 |
| 4 | 手动 iptables -I ts-input 加 ACCEPT 没生效 | 包没走到 ts-input 链,iptables 规则不起作用 |
| 5 | ts-input 链的 DROP all — 100.64.0.0/10 拦 Tailscale 流量 | 不要硬编码 Tailscale IP,用 loopback |
| 6 | OpenClaw 硬编码 100.99.4.120:3000,Serve 一 reset 就断 | 同机服务统一用 127.0.0.1:3000 |
八、核心教训
同机服务之间调 One API,一律用 127.0.0.1:3000,不要用 Tailscale IP 或公网 IP。
loopback 不经过任何网络层,不受 iptables、Docker NAT、Tailscale Serve 影响。只有从其他机器访问时才需要 Tailscale 或公网,而那种场景应该用 tailscale serve 或 SSH 隧道,不要硬编码 IP。
这条原则适用于所有自建服务:One API、数据库、Redis、内部 API。硬编码 IP 是脆弱的,网络配置一变就断。loopback 是稳定的,只要服务在同一台机器上,永远能用。
参考文件
- /root/.openclaw/openclaw.json — OpenClaw 配置,baseUrl 改为 http://127.0.0.1:3000/v1
- /opt/golf-rag/agent.py — RAG 的 ONEAPI_BASE = “http://127.0.0.1:3000/v1”
- /etc/nginx/sites-enabled/api.golforfun.net.conf — nginx 443 配置
