OpenClaw 网关跑在 192.168.1.244 上,我习惯通过 wenkangnas.local (192.168.1.4) 的反向代理访问它:浏览器打开 https://192.168.1.4:18789/,由 DSM 的反代转给虚拟机里的网关。某天这个页面突然只返回一段 JSON:

{"error":{"message":"Proxy client attribution is required. Configure gateway.trustedProxies narrowly and make the proxy overwrite or safely rebuild forwarded client headers.","type":"proxy_attribution_required"}}

之前 NAS 侧的 agent 已经"修"过一次——往 gateway.trustedProxies 里加了 192.168.1.4——没用,它自己当时也说"可能不会解决报错"。这次我让 Claude Code 上去看。

一、先确认是哪一层在拒绝

错误文本出自 OpenClaw 自己,不是 nginx。上去查三样东西:

  1. 网关配置里 gateway.trustedProxies 现在是什么;
  2. 群晖反代这条规则往后端带了哪些头;
  3. OpenClaw 判定"能不能确定客户端是谁"的代码。

网关配置里已经有一堆 trusted proxy,其中包含 NAS,所以问题不是"NAS 没被信任"。继续看代码(dist 里 grep proxy_attribution_required 能找到判定逻辑)和 NAS 的 nginx 配置,两边对照后发现是两处叠加。

二、根因:两个条件,缺一都不会 403

1. DSM 反代默认只设置 X-Real-IP,不设置 X-Forwarded-For。 去看 /etc/nginx/proxy.conf,DSM 生成的反代配置里没有 X-Forwarded-For。而 OpenClaw 默认只认 X-Forwarded-For 来确定客户端 IP。

2. trustedProxies 写得太宽。 原来的值是三个整段:192.168.0.0/16、10.0.0.0/8、172.16.0.0/12。我的 Mac 也在 192.168.0.0/16 里,于是客户端自己的地址也算"受信代理"。网关把受信代理当成链路上的中转、会跳过去找真正的客户端,这样就算 NAS 把客户端 IP 传过来了,网关也会把这个地址跳过,最后还是找不到客户端,归因失败,403。(这一段是我读代码和 AI 分析得出的解释,没有单独写测试复现。)

所以上一次的修法没用:192.168.1.4 本来就在 192.168.0.0/16 里,加进去等于没加。

一个检验思路:信任列表越宽,“受信代理"和"真正的客户端"越容易重叠。反代场景下,列表里应该只有反代那一台。

三、修复:两边各改一处

网关侧(备份后改,改动即时生效,没有重启):

cp ~/.openclaw/openclaw.json ~/.openclaw/openclaw.json.bak-20261020-proxyfix
openclaw config set gateway.trustedProxies '["192.168.1.4"]' --strict-json
openclaw config set gateway.allowRealIpFallback true --strict-json

trustedProxies 收窄到只有 NAS 这一个地址;allowRealIpFallback 打开后,拿不到 X-Forwarded-For 时会退而使用 X-Real-IP。改完立刻 curl -sk https://192.168.1.4:18789/ 返回 200。

NAS 侧,给反代规则补上头,避免客户端自己伪造:

在 DSM 的反向代理规则(我这条规则叫 “OpenClaw”)的自定义标头里加一条:

X-Forwarded-For: $remote_addr

这里用的是覆盖而不是追加($remote_addr 是 nginx 看到的真实连接地址),所以客户端自己带的 X-Forwarded-For 传不到网关。

我在 GUI 之外用了 synowebapi 操作,因为要先备份 /usr/syno/etc/www/ReverseProxy.json,再用 SYNO.Core.AppPortal.ReverseProxy 的 list/set 改那一条的 customize_headers。nginx 的站点配置在 /usr/local/etc/nginx/sites-available/*.w3conf,那是 DSM 生成出来的文件;我的理解是应该改 DSM 自己的规则而不是直接改这些文件(没有实际测过直接改会不会被覆盖)。

四、怎么验证的,以及没验证的

  • 不带头请求:200,/healthz 也是 200。
  • 带 X-Forwarded-For: 8.8.8.8 和 X-Forwarded-For: 127.0.0.1 伪造请求:都正常返回。

没验证的:我没有去看网关最终把客户端归因成了哪个 IP,所以"伪造头真的被 NAS 覆盖了"这件事,依据是规则里设的就是覆盖,而不是在网关一侧看到了实际归因结果。以后在意这点,可以在网关日志里对一下。

五、顺带:页面打开后让输入 token

修好之后页面变成要填 Gateway secret 的连接框,原因是网关配置的是 token 认证,浏览器第一次连接要输入一次。token 我是让 AI 直接复制进剪贴板(pbcopy),没在对话里显示;URL 保持 wss://192.168.1.4:18789,粘贴后连接,浏览器会存下来。

小结

  • 症状是 403 一类的归因错误,根因是两个条件叠加:代理没传 X-Forwarded-For,且信任列表宽到把客户端也算进去。单修其中一个,要看另一个有没有把你堵住。
  • 信任列表只放反代自己的地址;反代侧用覆盖的方式设置转发头。
  • 记住一条备忘:以后在 DSM 里新建指向 OpenClaw 的反代规则,要手动加上 X-Forwarded-For 这个自定义头,默认不会有。