本机通、公网 403——容器前的域名转发层 (Loopback OK but Public 403 — Domain Forwarding Layer)
1. 触发场景
给一台跑在容器里的机器新增域名。本文是 LXD 容器(内网 192.168.52.172):nginx vhost 写好了、证书装好了、DNS A 记录也加了,公网就是不通。
2. 症状
- 公网访问:
403 Forbidden,响应头Server: openresty,证书是CN=127.0.0.1的自签证书。 - 容器内部:
openssl s_client -connect 127.0.0.1:443 -servername 新域名返回正确的证书。 - 同一台机器换成已知能用的老域名走公网:一切正常。
- 容器的 nginx 访问日志里完全没有这条请求。
这三条同时出现时,配置本身没问题,是更前面断了。
3. 决定性判据
三个一起看,30 秒定型。
① 对比回环 vs 公网,用同一个 SNI
openssl s_client -connect 127.0.0.1:443 -servername <新域名> 2>/dev/null | openssl x509 -noout -subject
openssl s_client -connect <公网IP>:443 -servername <新域名> 2>/dev/null | openssl x509 -noout -subject回环给对证书、公网给自签证书 → 中间有一层。
② 看 Server: 响应头
容器自己的 nginx 配了 server_tokens off,头是 Server: nginx;挡在前面那层返回 Server: openresty。两个不同的 server 才会给出不同的头——这一条最快。
③ 看容器的 access log
请求根本没到 → 问题不在 nginx 配置。配合日志来源 IP 一起看:如果日志里的客户端 IP 是公网 IP 而不是真实客户端,说明中间那层做了 NAT。
4. 根因
容器前面还有一层按域名转发的反向代理。本文里是同一宿主机上的另一个容器 192.168.52.2,跑 openresty。
它的转发规则是白名单式的:只把已注册的域名转进容器,没注册的一律返回它自己的 403 页和自签证书。DNS 指对了也没用,因为请求压根没进容器。
这类层通常和 NAT / 共享 IP / LXD 容器一起出现——机器自己只有内网地址,对外全靠别人转发。
5. 修法
找到那层:从容器里扫同网段,谁开着 443 就是谁。
for i in $(seq 1 20); do timeout 2 bash -c "echo > /dev/tcp/192.168.52.$i/443" 2>/dev/null && echo "192.168.52.$i:443 open"; done它往往开着一个面板端口(本文是网关
192.168.52.1:8443的 LXD API,和转发机192.168.52.2)。注意这类机器常常没有对外 SSH 端口,只能从容器内部访问。- 在它的面板/配置里加一条转发:
新域名 → 容器内网IP:443。 - 证书两层都要装——前面那层要做 TLS 终结,容器自己也要。用泛域名证书最省事。
加完实测,不要只看「保存成功」:
- 订阅/文件类路径:
curl拿到内容和正确的Content-Type WebSocket 类路径:手动发升级请求,要拿到
101 Switching Protocolscurl -s -i -N --max-time 10 \ -H "Connection: Upgrade" -H "Upgrade: websocket" \ -H "Sec-WebSocket-Version: 13" -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \ https://<新域名>/<ws路径> | head -3
- 订阅/文件类路径:
5.1 转发层配好之后的第二关:后端自己的 vhost
修好第 5 节之后还有一半:转发层只是把请求送到后端,后端得自己认领这个域名。
同一个后端上,「A 域名通、B 域名不通」而两者在转发面板里的配置一模一样时,去数后端的 vhost:
# la(宝塔)的例子
ls /www/server/panel/vhost/nginx/*.conf
# gpt.6767.chat.conf ✓
# la.6767.chat.conf ✓
# ai.6767.chat.conf ✗ ← 就是它后端没有对应 vhost 时,请求会落到默认站点(server_name _),表现是:
- 返回默认站点的页面。宝塔的默认页是「域名未绑定」,很容易被误读成「转发层没生效」。
- API 路径返回 146 字节的 nginx 404,
Server: nginx。 - 补齐 vhost 后同样的路径返回 401 + JSON(
Invalid token)—— 401 才是「找对人了,只是没带 key」。
排查顺序反过来更快:先看后端有没有这个域名的 vhost,再看转发层。默认站点的错页长得像转发失败,很容易让人在前端来回查。
为什么日志不能当证据:宝塔的 nginx.conf 里 http 段有 access_log off;,没自带 access_log 的 vhost 什么都不记。于是「访问日志里没有这条请求」既可能表示请求没到,也可能表示到了但没记 —— 不能用来下结论。看默认站点的页面内容是不是后端的,比看日志可靠。
6. 下次如何识别
- 「本机测通、外面 403」= 先怀疑中间层,不要回去翻 nginx 配置。
Server:头跟预期不符是最快的指纹,比翻日志快。- 换老域名能通、新域名不通 → 白名单问题,不是 DNS 问题。
- NAT / 共享 IP / 容器化的机器,默认假设前面有转发层。新域名第一件事就是问「这层认不认这个名字」。
- 加域名是两步:DNS 一次,转发层一次。只做 DNS 会得到「解析对了但访问 403」这种最容易误判的状态。
7. 相关
- NAT VPS 不转发 UDP 导致 QUIC 类协议全废 (NAT VPS Without UDP Forwarding Blocks QUIC Protocols) —— 同一台机器上的另一层限制;那篇讲传输层,本文讲应用层的域名路由。
- 自建代理 VPS 选购与流量计价 (VPS Selection for Self-Hosted Proxy) —— 选型时把「有没有可控的域名转发」纳入考量。
- 宝塔 nginx 的 systemctl reload 静默不生效 (BT Panel nginx systemctl reload Is a Silent No-Op) —— 同一台机器上另一个「命令返回成功但没生效」的坑。
来源:2026-09-22 在 LA 小鸡(内网192.168.52.172)新增la.6767.chat。DNS、证书、vhost 全部就位后公网仍 403;靠「回环 vs 公网 SNI 对比 +Server:头」定位到宿主机上的转发容器192.168.52.2。当时还用了「access log 里没记录」这条证据,但它是错的 —— 宝塔 http 段有access_log off,默认 vhost 本来就不记日志。后来给ai.6767.chat排查时被这条假证据带偏了一次:真实原因是后端 la 没有ai.6767.chat.conf,请求落到了默认站点(那张「域名未绑定」的页面)。补上 vhost 后同一路径从 404 变成 401。见 5.1 节。
本文归档于个人知识库 · 领域:CS & AI · 模块:04 Project_CS_AI/Network