4091 字
20 分钟
当深信服开始“乱敲门”,该如何学会装死

一、项目说明与问题排查#

近期在开发一个前后端分离的内网应用,因为是内网服务,所以就采用了以下方案

  • 后端 Java 服务直接对外暴露端口
  • 前端通过 Nginx 部署
  • 前端以 IP + 端口 的方式直接调用后端 API

部署完成后经过基本测试,一切正常便投入了使用

系统运行一段时间后,发现后端 Java 服务日志中开始频繁出现异常报错,大致内容如下:

o.apache.coyote.http11.Http11Processor: Error parsing HTTP request header
Note: further occurrences of HTTP request parsing errors will be logged at DEBUG level.
java.lang.IllegalArgumentException: Invalid character found in method name [0x000x000x000x00DB2DAS]. HTTP method names must be tokens

从异常信息来看,报错发生在 HTTP 请求解析阶段,甚至连合法的 HTTP Method 都没有识别出来

这意味着请求压根就不是 HTTP请求,所以更不可能是前端代码的问题(前端不背这锅)

继续翻日志,发现这些异常请求存在明显特征:

  • 来自固定的一批 IP
  • 访问频率稳定
  • 请求内容完全不像浏览器或业务系统

拿到这些 IP 和网维人员沟通后,了解到这是 深信服设备在做链路探测、存活检测、漏洞扫描,也就是说并不存在攻击行为、网络设备也是正常工作的

从开发角度来看这件事是合理且正常的,但问题在于正常业务日志中掺杂大量的错误日志,这不关影响正常日志的查阅

更重要的是对于一个有点强迫症的人来说日志里突然蹦出来一串异常,看着是真的不爽

既然不能让深信服不探测,那我让服务学会装死总可以吧

二、想法设计#

既然是想让深信服探测不到,那么最直接的办法就是 Java 服务只对本机开放

但是又需要提供外部访问,那么借助前端网页部署使用 Nginx 进行反向代理再合适不过了

既然明确了要使用反向代理,那么就需要先明确目标,避免配置错误

使用反向代理的真实目标:

  • 保护真正的后端 HTTP 服务,避免恶意访问
  • 阻止非 HTTP 协议到达后端,让异常流量在入口就失败

三、反向代理方式选择#

在 Nginx 中经常使用的反向代理有两种方案们分别是 HTTP 反向代理(http 模块)TCP 透传(stream 模块)

虽然他们都可以完成端口的转发,但是设计目的完全不同,适用场景也并不一样

3.1 HTTP 反向代理(http 模块)#

http 模块的前提非常明确:Nginx 需要明确知道当前流量是 HTTP 协议

因此它可以做到:

  • 解析 HTTP 请求和响应
  • 理解 Method、Header、Host 等信息
  • 根据域名、路径进行转发和路由
  • 添加或修改请求头(如 X-Forwarded-For)
  • 进行 HTTPS 终止
  • 对非 HTTP 请求直接拒绝,不转发给后端

从本质上看,http 模块属于七层代理(L7),它在后端服务之前,充当了一个“协议守门员”的角色

只要请求不是合法 HTTP 流量,就不会被转发到后端服务端口

3.2 TCP 透传(stream 模块)#

stream 模块的设计理念则完全不同,它并不关心协议内容,只负责转发字节流

其特点包括:不解析任何应用层协议不理解 HTTP 或 HTTPS收到什么数据就原封不动转发什么协议解析完全由后端服务负责

常见使用场景有:

  • MySQL、Redis 等数据库
  • SSH
  • MQTT
  • 自定义私有协议
  • HTTPS 直通(不在 Nginx 处做 SSL 终止)

从本质上看,stream 模块属于四层代理(L4),它的“简单直接”,既是优势,也是隐患

3.3 方式选择#

虽然两种方式都能转发端口但结果完全不同,我们需要的是拦截非 HTTP 流量,将 HTTP 流量转发到后端

因此两种方法中只有 HTTP 反向代理(http 模块) 符合我们的需求

四、进行反向代理的配置#

4.1 修改 Java 配置#

application.propertiesapplication.yml 中设置只运行本机访问并暴露端口

server.address=127.0.0.1
server.port=8080

修完完成后重新打包 jar 包并部署

4.2 修改Nginx 配置#

通常可以通过以下方式确认当前 Nginx 实际使用的配置路径:

Terminal window
nginx -t

命令输出中会包含类似信息:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

其中显示的 nginx.conf 所在路径,即当前 Nginx 正在使用的主配置文件目录

如果你没有修改过 nginx.conf 中的 include 路径

那么可以在 /etc/nginx/conf.d 文件夹中创建一个独立的配置文件

例如创建一个名为 nginx-8090-proxy-local-8080.conf 的配置文件

名字的意思为通过 Nginx 暴露 8090 端口代理本地 8080 端口

由于 Java 服务需要拿到客户端的 IP 进行日记账记录,所以配置文件内容如下

server {
# 对外监听端口
listen 8090;
# Nginx 所在机器的 IP,用于匹配客户端请求里的 Host
server_name 10.0.0.2;
location / {
# 转发至本机后端 Java 服务
proxy_pass http://127.0.0.1:8080;
# 使用 HTTP/1.1 与后端通信
proxy_http_version 1.1;
# 透传原始 Host
proxy_set_header Host $host;
# 传递客户端真实 IP
proxy_set_header X-Real-IP $remote_addr;
# 记录完整的客户端 IP 访问链
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
IMPORTANT

Host 需要显式设置
否则 Nginx 转发到后端时会将Host默认修改为上游地址(即 proxy_pass 指向的主机 127.0.0.1:8080)而不是客户端实际访问入口 10.0.0.2:8090

配置完成以后使用一下命令加载配置变动

Terminal window
nginx -s reload

4.3 配置说明#

Nginx 必须先成功解析 HTTP,只有合法 HTTP 请求才会被转发

非 HTTP 请求在 Nginx 层直接失败,不会进入后端

后端所获取到的信息和直接访问 Java 服务的 IP+端口 一致

外部请求流量
Nginx :8090(HTTP 入口)
├─ 非 HTTP 流量:直接丢弃
127.0.0.1:8080(后端 Java 服务)

4.4 stream(TCP 透传)(错误方案扩展)#

stream {
server {
# 对外监听端口
listen 8090;
# 转发至本机后端 Java 服务
proxy_pass 127.0.0.1:8080;
}
}

配置说明#

Nginx 不解析任何协议, 不管是 HTTP、二进制还是私有协议,怎么来的怎么进行转发

深信服的非 HTTP 协议被 原封不动转发到后端,实际等于 Nginx 帮 Java 服务换了一个端口,后端将会继续报错

五、浏览器汇报 CORS ?#

上线该配置后,前端开始出现 CORS 报错,一开始以为 Nginx 影响了跨域的配置

但是经过查看发现存在响应头,且响应头包含 Server: nginxContent-Type: text/html ,但是没有任何 Access-Control-Allow-*

这意味着浏览器拿到的并不是 Java 的正常业务响应,而是 Nginx 自己生成的错误响应

因为在跨域场景下,只要响应里缺少 Access-Control-Allow-Origin 等 CORS 头,浏览器就会把该响应视为不可被 JS 读取,从而表现为 CORS 失败

由于设计到内网文件的下载,Chrome 因为过于严格的安全策略一直提示不安全,所以最近一直在 Edge 上进行开发

但是在 Edge 上调试半天只发现一直报告 CORS 错误,但是没有发现 OPTIONS 请求,也没有错误代码,所以果断换到 Chrome 上进行调试,都不需要复杂的排查,一个请求就显示出了结果

发现出现 CORS 拦截的接口是文件上传接口,这个接口不是简单请求,需要先发起 OPTIONS 进行预检,询问 JAVA 服务是否允许跨域请求

继续查看信息发现 OPTIONS 预检成功了,也返回了相关的响应头,说明跨域策略本身并没有阻止任何请求

继续查看发现实际发生报错是上传接口,发送请求后立即接受到返回,响应的Status Code是 413 Request Entity Too Large

搭配响应头包含 Server: nginx 一切都明了了,这实际上是上传的请求体超过了 Nginx 默认允许的大小,导致在 Nginx 层就直接报错了,根本还没有进入到后端

也就是说实际上并不存在 CORS 问题,而是浏览器在跨域场景下对不带 CORS 头的错误响应进行了安全屏蔽

Edge 的开发者工具在该场景下没有直观展示真实 HTTP 状态码,导致排查方向被误导,果然开发环境还是需要使用 Chrome

解决方法#

解决 413 Request Entity Too Large 的方法也很直接,只需要在 Nginx 中提升允许的请求体的大小 client_max_body_size 即可

# 仅上传接口放宽到 100MB
client_max_body_size 100m;

对 server 下所有 location 生效#

server {
# 对外监听端口
listen 8090;
# Nginx 所在机器的 IP,用于匹配客户端请求里的 Host
server_name 10.0.0.2;
# 仅上传接口放宽到 100MB
client_max_body_size 100m;
location / {
# 转发至本机后端 Java 服务
proxy_pass http://127.0.0.1:8080;
# 使用 HTTP/1.1 与后端通信
proxy_http_version 1.1;
# 透传原始 Host
proxy_set_header Host $host;
# 传递客户端真实 IP
proxy_set_header X-Real-IP $remote_addr;
# 记录完整的客户端 IP 访问链
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

只对上传接口放开#

server {
# 对外监听端口
listen 8090;
# Nginx 所在机器的 IP,用于匹配客户端请求里的 Host
server_name 10.0.0.2;
location / {
# 转发至本机后端 Java 服务
proxy_pass http://127.0.0.1:8080;
# 使用 HTTP/1.1 与后端通信
proxy_http_version 1.1;
# 透传原始 Host
proxy_set_header Host $host;
# 传递客户端真实 IP
proxy_set_header X-Real-IP $remote_addr;
# 记录完整的客户端 IP 访问链
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /api/upload {
# 仅上传接口放宽到 100MB
client_max_body_size 100m;
# 转发至本机后端 Java 服务
proxy_pass http://127.0.0.1:8080;
# 使用 HTTP/1.1 与后端通信
proxy_http_version 1.1;
# 透传原始 Host
proxy_set_header Host $host;
# 传递客户端真实 IP
proxy_set_header X-Real-IP $remote_addr;
# 记录完整的客户端 IP 访问链
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

配置完成以后使用一下命令加载配置变动

Terminal window
nginx -s reload

额,不错,深信服请求进不来了,这下美滋滋了

六、不是吧!!!又来??#

6.1 错误的配置#

是的,没看错,深信服这次又双叒叕通过 Nginx 请求到了 Java 服务

o.apache.coyote.http11.Http11Processor : The host [_] is not valid
Note: further occurrences of request parsing errors will be logged at DEBUG level.

不过这是我配置上的问题

我一开始的配置如下,我没有使用 Nginx 所在机器的 IP 作为 server_name ,而是使用了自定义的 _

server {
# 对外监听端口
listen 8090;
server_name _;
# 仅上传接口放宽到 100MB
client_max_body_size 100m;
location / {
# 转发至本机后端 Java 服务
proxy_pass http://127.0.0.1:8080;
# 使用 HTTP/1.1 与后端通信
proxy_http_version 1.1;
# 透传原始 Host
proxy_set_header Host $host;
# 传递客户端真实 IP
proxy_set_header X-Real-IP $remote_addr;
# 记录完整的客户端 IP 访问链
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

对于正常浏览器请求,例如访问 http://10.0.0.2:8090,浏览器会自动带上合法的 Host: 10.0.0.2:8090 请求头,此时 Nginx 变量 $host 会被设为 10.0.0.2(注意:$host 不包含端口),后端 Java 服务能正常处理

而对于异常探测流量,情况分两种:

  1. 完全不携带 Host (如某些原始 TCP 探测): 此时 Nginx 会将 $host 回退为匹配到的 server_name。如果你配置了 server_name _;,那么 $host 就会变成 _
  2. 携带非法但语法存在的 Host (如 Host: _Host: \x00abc 等): 虽然内容非法,但因为 HTTP 报文结构完整,Nginx 仍将其视为合法 HTTP 请求,并直接使用请求中的 Host 值作为 $host不会回退到 server_name

无论哪种情况,只要 $host 的值是 _ 或其他非法字符串,再通过 proxy_set_header Host $host; 透传给后端,Tomcat 在解析时就会报错

6.2 解决方案(最终版)#

针对这种非正常 HTTP 客户端发起的探测请求,最简单也最直接的处理方式,就是在 Nginx 入口层对 Host 做一次强校验

正常浏览器请求会携带合法的 Host(如 10.0.0.2:8090),而后端能正确处理

但深信服等探测设备往往会:

  • 完全不带 Host → Nginx 用 server_name 填充 $host
  • 带非法 Host 头(如 Host: _ → Nginx 直接使用该非法值作为 $host不会回退

因此,只需在入口处判断客户端实际发送的 Host 头(即 $http_host)是否符合预即可

if ( $http_host != 10.0.0.2:8090) { return 444; }
TIP

return 444 是 Nginx 特有状态码,表示立即关闭连接且不返回任何响应。
相比 403,它更‘安静’,不会暴露服务存在,适合用于丢弃探测流量

NOTE

该方案仅适用于固定 IP + 固定端口 + HTTP 的简单场景
生产环境推荐使用基于 $host$server_port 的白名单模型

说明:这里使用 $http_host 是为了精确匹配 Host:port 形式的入口;
如果请求是 Host: 10.0.0.2(不带端口),将会被视为不符合预期而返回 444

完成配置如下

server {
# 对外监听端口
listen 8090;
# Nginx 所在机器的 IP,用于匹配客户端请求里的 Host
server_name 10.0.0.2;
# 仅上传接口放宽到 100MB
client_max_body_size 100m;
if ( $http_host != 10.0.0.2:8090) { return 444; }
location / {
# 转发至本机后端 Java 服务
proxy_pass http://127.0.0.1:8080;
# 使用 HTTP/1.1 与后端通信
proxy_http_version 1.1;
# 透传原始 Host
proxy_set_header Host $host;
# 传递客户端真实 IP
proxy_set_header X-Real-IP $remote_addr;
# 记录完整的客户端 IP 访问链
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

七、HTTPS 扩展(支持多 IP + 端口 + HTTP / HTTPS)#

在后续的拓展中,访问场景可能不再局限于单一的 IP + 端口 + HTTP,而是可能会演进为:

  • 多个前端来源(不同 IP / 不同端口)
  • 同时存在 HTTP 与 HTTPS

并且HTTPS 场景下 Host 通常不携带端口,如果仍然使用如下方式进行拦截:

if ($http_host != 10.0.0.2:8090) { return 444; }

就会面临两个问题:

  1. 扩展性差:每增加一个入口都需要修改 if
  2. HTTPS 误杀:HTTPS 请求的 Host 通常为域名,不包含端口

因此需要将硬编码判断升级为可扩展的 Host 白名单模型

7.1 使用 map 定义 Host 白名单#

nginx.confhttp 级别统一定义允许的 Host:

http {
# 定义的 Host 和 允许状态(0不允许,1允许)
map $host $host_allowed {
default 0;
# ===== HTTP:IP =====
10.0.0.2 1;
10.0.0.3 1;
# ===== HTTP:域名 =====
api.example.com 1;
# ===== HTTPS:域名(默认 443,不带端口)=====
apis.example.com 1;
apis2.example.com 1;
}
# 定义的 Host 和 允许状态(0不允许,1允许)
map $server_port $port_allowed {
default 0;
# ===== HTTP:端口 =====
80 1;
8090 1;
# ===== HTTPS:域名(默认 443,不带端口)=====
443 1;
}
...
}
NOTE

说明:在多入口(尤其 HTTPS)场景下,白名单更适合基于 $host(主机名)做匹配;
端口校验单独交给 $server_port$port_allowed 处理

7.2 HTTP HTTPS统一配置#

server {
# 对外监听端口
listen 80;
listen 8090;
listen 443 ssl http2;
# 对外访问入口(IP / 域名),用于匹配客户端请求中的 Host
server_name 10.0.0.2 10.0.0.3 api.example.com apis.example.com apis2.example.com;
if ($host_allowed = 0) { return 444; }
if ($port_allowed = 0) { return 444; }
# 域名证书必须覆盖定义的 HTTPS 请求的域名
# 443 建议只用域名访问;用 IP 访问 443 可能因为 SNI/证书不匹配而失败,这是正常现象
ssl_certificate certs/apis_apis2_fullchain.pem;
ssl_certificate_key certs/apis_apis2_privkey.pem;
location / {
# 转发至本机后端 Java 服务
proxy_pass http://127.0.0.1:8080;
# 使用 HTTP/1.1 与后端通信
proxy_http_version 1.1;
# 透传原始 Host
proxy_set_header Host $host;
# 传递客户端真实 IP
proxy_set_header X-Real-IP $remote_addr;
# 记录完整的客户端 IP 访问链
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 传递真实 HTTP 协议
proxy_set_header X-Forwarded-Proto $scheme;
}
}

这样设计之后

  • HTTP / HTTPS 使用同一套 Host 白名单逻辑
  • 是否允许访问,取决于 $host_allowed$port_allowed(分别由 $host$server_port 映射得到)
  • 非 HTTP / 非预期 Host 的流量在 Nginx 入口直接失败
  • 后端 Java 服务只接收来自 Nginx 的合法 HTTP 请求

八、提示说明#

可能存在错误内配置或说明,该内容仅是此次配置中的流水账记录

当深信服开始“乱敲门”,该如何学会装死
https://www.self4m.com/posts/nginx-block-non-http-traffic-with-reverse-proxy/
✍️作者
Self4m
📅发布于
2026-01-04
©️许可协议
CC BY-NC-SA 4.0

商业用途必须事先获得作者授权;
非商业用途可以使用,但必须注明出处;
若有改编需采用相同许可协议发布。