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 到容器。

三、根本原因

两个原因叠加:

  1. tailscale serve reset 清掉了 Tailscale Serve 在 3000 上的转发规则。原本 x162-43-92-249.tail9c4209.ts.net:3000 能用,是因为 Serve 在 3000 上做了转发。reset 之后规则没了。
  2. 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 配置

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注