需求很简单:给一台机器开一个「用 IP + 端口访问」的入口,反代到本机的一个容器。按习惯写了:
server {
listen 888 default_server;
server_name _;
location / { proxy_pass http://127.0.0.1:21080; ... }
}
nginx -t 过了,reload 也正常。然后所有请求 403。
从响应头里揪出中间层
403 页面的模板是 nginx 的风格,但响应头里带着一个 Set-Cookie: server_session_30288668=…。这个 cookie 名我在应用的正常响应里从来没见过——它不属于后端,也不像 nginx 自己会发的东西。
那就顺着它找:
grep -rl 'server_session' /www/server/ 2>/dev/null
命中一个文件:宝塔 WAF(btwaf)的 public/public.lua。也就是说 403 是这层 Lua 发的,不是 nginx 默认行为,也不是后端。
四处对照,锁定一行
WAF 里判 403 的地方在 waf.lua:
if ngx.ctx.server_name == "未绑定域名" then
ngx.exit(403)
end
ngx.ctx.server_name 的来源在 public.lua 的 get_server_name_waf():它取的是 ngx.var.server_name,然后——
local c_name = ngx.var.server_name
...
if c_name == '_' then
c_name = "未绑定域名"
end
四个对照实验确认这条因果链,而不是猜:
curl -H 'Host: www.cccn.xyz' http://127.0.0.1:888/→ 403(换 Host 没用)curl http://127.0.0.1:21080/直连容器 → 200(后端是好的)- 把
server_name改成真实域名、default_server保留 → 同样的 curl 立刻 200 - 同一份配置在 888 之外另开一个端口,行为完全一致
被踩中的那个语义
$server_name 不是 Host 头。它是「被匹配到的那个 server 块里,server_name 指令写了什么」。
所以 server_name _; 的块,无论请求带什么 Host,$server_name 永远是 _。而 _ 在 nginx 里没有任何魔法——它只是一个永远匹配不上的字面量,是社区约定俗成的「这里没名字」。
任何拿 $server_name 做判断的东西都会把 _ 当成一个真的叫 `_` 的站点:WAF 的白名单、按站点的配置查找、访问日志里的站点标识、缓存 key。这次撞上的就是第一个。
改法:把两件事拆开
server {
listen 888 default_server; /* 决定:Host 不匹配时谁接 */
server_name www.cccn.xyz; /* 决定:$server_name 是什么 */
...
}
default_server 和 server_name 是两个正交的东西。前者管兜底,后者管”我是谁”。用 IP + 端口访问时,Host 是 1.2.3.4:888,匹配不上任何 server_name,于是落到 default_server 这个块——但 $server_name 仍然是有名字的域名,WAF 放行。
顺带一个副作用值得知道:这个块成了该端口的默认站点后,访问日志、任何按 $server_name 归类的统计,都会记在真实域名名下而不是 IP 名下。对排查来说是好事。
我现在排 403 的顺序
- 先看响应里有没有不属于任何应用层的痕迹:奇怪的 cookie 名、
Server头、错误页模板的字数。有就顺着字符串 grep 整台机器。 - 对比”经过反代”和”直连后端”两条路的响应码。后端 200、前面 403,问题必在中间。
- 最后才怀疑
deny、allow、防火墙。这次从头到尾 IP 都不是因素,WAF 拦的是名字,不是地址。