跳转至

一次 frp + nginx 内网穿透 502 (SNI) 故障的定位与原理解读

通过 nginx 进行反代,并经由内网穿透工具 frp 对内网的 HTTPS 服务进行代理时,如果遇到 502 错误,需要在 nginx 配置中添加:

proxy_ssl_server_name on;
proxy_ssl_name $host;

让 nginx 在与 frps 建立 TLS 时正确携带域名,故障即消除。

1. 场景背景

我拥有:

  • 一台【内网服务器】(运行目标业务 / HTTPS 服务)
  • 一台【公网服务器】(运行 frps + nginx,负责对外域名解析与反代)
  • 使用 frps-frpc 做内网穿透,使公网能访问内网的 HTTPS 服务

上线后,浏览器访问域名,nginx 日志/页面出现 502 Bad Gateway

2. 拓扑结构

2.1 失败时的(缺少 SNI)

sequenceDiagram
  participant UA as 浏览器
  participant N as 公网 nginx
  participant S as frps
  participant C as frpc
  participant IS as 内网HTTPS服务

  UA->>N: HTTPS 请求 (Host: example.imxcg.com)
  N->>S: TLS ClientHello (无 SNI)
  Note over S: 无法按域名选择正确证书/路由
  S-->>N: 握手异常 / 错误证书
  N-->>UA: 502 (上游握手失败)

3.2 现象与排查思路

观察点 现象 初步推断
浏览器 返回 502 上游 (nginx->frps) 链路失败
nginx error.log upstream prematurely closed / handshake error / 502 TLS 建立阶段异常
frps 日志 可能出现握手失败或未命中期望路由 需要 SNI 区分域名

常见 502 原因包括:

  1. 上游进程未监听 / 端口错(排除:端口正常)
  2. 网络不通(排除:frp 隧道其他 HTTP 服务可通)
  3. 证书/协议不兼容(重点:HTTPS + 多域)
  4. SNI / 域名路由未携带导致的错误证书或路由失败(高度可疑)

在 GitHub issue fatedier/frp#4239 中找到了同类症状与解决方案 —— 指向 SNI 未启用

3. SNI 原理速览

  • SNI (Server Name Indication) 是 TLS 扩展 (RFC 6066),客户端在 ClientHello 中声明要访问的主机名。
  • 服务器据此:
  • 选择对应虚拟主机证书(多域/多证书共用同一监听端口)
  • 或做基于域名的路由 / 分流(例如部分隧道、多租户场景)
  • 如果客户端不发 SNI:
  • 服务器可能只能返回“默认证书”,域名不匹配 → 证书校验失败
  • 某些实现直接拒绝或无法匹配到正确后端

在本案例中:nginx 作为“客户端”访问 frps。在默认情况下(proxy_ssl_server_name off),且 proxy_pass 指向的是 IP 地址 或你需要动态指定域名变量时,nginx 不会在上游握手里发送 SNI。frps 侧基于 SNI 选择证书/路由 → 失败 → 导致 502;浏览器直连之所以正常是因为浏览器始终发送 SNI。

4. nginx 关键配置差异

4.1 故障前(缺失 SNI 设置)

location / {
    add_header       X-Served-By $host;

    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $http_connection;
    proxy_http_version 1.1;

    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Scheme $scheme;
    proxy_set_header X-Forwarded-Proto  $scheme;
    proxy_set_header X-Forwarded-For    $proxy_add_x_forwarded_for;
    proxy_set_header X-Real-IP          $remote_addr;
    proxy_pass       $forward_scheme://imxcg.com$request_uri;
}

4.2 修复后(显式启用 SNI)

location / {
    # 关键配置
    proxy_ssl_server_name on;
    proxy_ssl_name $host;

    add_header       X-Served-By $host;

    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $http_connection;
    proxy_http_version 1.1;

    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Scheme $scheme;
    proxy_set_header X-Forwarded-Proto  $scheme;
    proxy_set_header X-Forwarded-For    $proxy_add_x_forwarded_for;
    proxy_set_header X-Real-IP          $remote_addr;
    proxy_pass       $forward_scheme://imxcg.com$request_uri;
}

两行配置作用:

指令 作用
proxy_ssl_server_name on; 打开向上游发送 SNI 的功能
proxy_ssl_name $host; 指定要发送的 SNI 名称(可用变量)

注意:仅 proxy_ssl_server_name on; 时,SNI 默认值是 proxy_pass 里解析出的主机名;若上游是 IP 或你需要保持与客户端 Host 完全一致,显式 proxy_ssl_name $host; 更稳妥。

5. 为什么会是 502?

502 = nginx 作为网关,连接上游过程中**收到了无效响应或连接失败**。这里属于:

  1. nginx 尝试建立 TLS → 未发送 SNI
  2. frps 无法匹配正确域名 → 返回错误证书或直接中断
  3. nginx 认为上游握手失败 → 返回 502 给浏览器

如果你抓包(openssl s_client / tcpdump)会看到第一版缺少 server_name 拓展,修复版则出现。

6. frp 在其中的角色

  • frpsfrpc 之间是**穿透隧道**,对它们来说只转发 TCP / TLS 流量(具体由配置的类型决定)
  • 当你把 nginx 反代指向 frpsHTTPS 端口 时,frps 需要充当“TLS 服务器”角色,基于 SNI 选择证书/路由
  • 若使用多域名/多证书(你目录下存在 imxcg.cn, imxcg.com, imxcg.net 各自证书),更依赖 SNI 精确匹配

7. 常见延伸问题 FAQ

问题 说明 / 处理
只单域名也要 SNI 吗? 是。关闭 SNI 虽可能临时工作,但未来多域或证书轮换会踩坑。
可以用纯 HTTP 上游吗? 可以,但内/公网之间可能明文;HTTPS 更安全(尤其含登录 / Token)。
为什么浏览器直连不会 502? 浏览器天生发送 SNI;问题出在 nginx 作为“缺省不发 SNI 的客户端”。
proxy_ssl_name 必须吗? proxy_pass 使用域名且与 $host 相同,可以只开 proxy_ssl_server_name。使用 IP 或需动态 Host 时建议显式。
仍 502 怎么办? 抓包 / openssl / 查看 frps 日志,确认不是证书权限、frp 映射配置或防火墙导致。

8. 总结

关键点 收获
故障类型 公网 nginx 反代 frps HTTPS 502
根因 上游 TLS 缺少 SNI,frps 无法正确匹配证书/路由
解决 添加 proxy_ssl_server_name on; proxy_ssl_name $host;
原理 SNI 让多域/多证书在同一监听端口上区分虚拟主机
经验 反代到多域 TLS 上游时明确启用 SNI;善用 openssl/curl 验证

小技巧

这类问题的定位关键词:nginx upstream 502 https sni frp。第一时间检查是否发送 SNI 可以少走弯路。 若你也遇到同样的 502,不妨直接核对上面这一段是否缺失。


写到这里,整起事故链路已经闭环:问题再现 → 证据指向 SNI → 修改配置 → 验证通过 → 原理总结。希望这篇记录能在你下次遇到“看似玄学”的 502 时,提供一个清晰、系统的排查范式。

欢迎继续交流改进思路。🚀