fatedier / frp

A fast reverse proxy to help you expose a local server behind a NAT or firewall to the internet.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

nginx 转发请求到 frps 的 HTTPS 时报错 502

ks4na opened this issue · comments

Bug Description

由于 frp 内网穿透的 HTTPS 服务,需要携带 vhostHTTPSPort 指定的端口号(例如:20443),所以想要在 frps 所在的机器上通过 nginx 反向代理来隐藏端口号。

所以就有了如下的 nginx 请求转发配置(*.frps.example.com 已经配置了解析到当前服务器):

server {
    listen       80;
    listen  [::]:80;
    server_name *.frps.example.com;

    return 301 https://$host$request_uri;
}

server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;
  server_name *.frps.example.com;

  ssl_certificate certs/self_signed/server.crt;
  ssl_certificate_key certs/self_signed/server.key;

  include ssl/ssl_options.conf;

  location / {
    proxy_pass https://127.0.0.1:20443; # 转发到本机的 frps 监听 HTTPS 服务的地址

    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  }
}

想要实现当访问 https://web-https.frps.example.com 时,转发请求到 frps 监听 HTTPS 服务的地址 https://127.0.0.1:20443,然后根据 frpc.toml 中的配置:

user = "some_user"

serverAddr = "frps.example.com"
serverPort  = 20000

# 内网 HTTPS 服务
[[proxies]]
name = "web-https"
type = "https"
localPort = 443
customDomains = ["web-https.frps.example.com"]

将请求转发到内网服务器。

但是这样做会报错 502, nginx 反向代理的日志输出如下:

192.168.1.10 - - [23/May/2024:15:23:01 +0000] "GET /favicon.ico HTTP/2.0" 502 559 "https://web-https.frps.example.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36" "-"
2024/05/23 15:23:13 [crit] 49#49: *29 SSL_do_handshake() failed (SSL: error:14094458:SSL routines:ssl3_read_bytes:tlsv1 unrecognized name:SSL alert number 112) while SSL handshaking to upstream, client: 192.168.1.10, server: web-https.frps.example.com, request: "GET / HTTP/2.0", upstream: "https://127.0.0.1:20443/", host: "web-https.frps.example.com"

报错的原因是 unrecognized name

frps 的日志输出如下(调整日志等级 log.level = debug):

2024/05/23 15:23:01 [D] [vhost.go:216] http request for host [] path [] httpUser [] not found

对比携带 20443 端口的请求时的日志输出:

2024/05/23 15:28:34 [D] [vhost.go:247] [868cad529d201650] [some_user.web-https] new request host [web-https.frps.example.com] path [] httpUser []

发现少了 host 的值。

frpc Version

0.52.3

frps Version

0.52.3

System Architecture

linux/amd64

Configurations

frps.toml:

bindPort = 20000

vhostHTTPPort = 20080
vhostHTTPSPort = 20443

frpc.toml:

user = "some_user"

serverAddr = "frps.example.com"
serverPort  = 20000

# 内网 HTTPS 服务
[[proxies]]
name = "web-https"
type = "https"
localPort = 443
customDomains = ["web-https.frps.example.com"]

Logs

No response

Steps to reproduce

...

Affected area

  • Docs
  • Installation
  • Performance and Scalability
  • Security
  • User Experience
  • Test and Release
  • Developer Infrastructure
  • Client Plugin
  • Server Plugin
  • Extensions
  • Others

翻看了相关 issue,发现似乎并没有比较好的解决方案:

  • #610 中设置 proxy_pass 为域名,这样要走DNS,并且防火墙要开端口,并且写死了访问域名无法适应泛域名;作者回复可以使用 proxy_ssl_server_name on;,但是只加上这个配置也没有效果
  • #671 中的解决方案需要本机搭建 dnsmasq

其他一些相关 issue ( #359#520 )也没有给出解决方案。

查看源码 + 询问 chatgpt ,最终找到了解决方案,在此记录一下,方便后续有人出现同样问题时参考。

从源码找到获取 host 的位置:https.go 48 行reqInfoMap["Host"] = clientHello.ServerName。 询问 chatgpt ,说是需要添加额外 nginx 配置以在反向代理请求时保留并传递客户端提供的 SNI 信息:

server {
    listen       80;
    listen  [::]:80;
    server_name *.frps.example.com;

    return 301 https://$host$request_uri;
}

server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;
  server_name *.frps.example.com;

  ssl_certificate certs/self_signed/server.crt;
  ssl_certificate_key certs/self_signed/server.key;

  include ssl/ssl_options.conf;

  location / {
    proxy_pass https://127.0.0.1:20443; # 转发到本机的 frps 监听 HTTPS 服务的地址

    # 设置 SNI 信息
    proxy_ssl_server_name on;
    # 设置 SNI 名称为客户端请求的主机名
    proxy_ssl_name $host;

    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  }
}

解释:

  • proxy_ssl_server_name on;:启用 SNI 传递。
  • proxy_ssl_name $host;:设置 SNI 名称为客户端请求的主机名。

添加上面的配置项,reload nginx 之后,确实可以正常访问了。

之前的 issue #610 中作者回复中提到过使用 proxy_ssl_server_name on;,但是当时添加了测试无效,于是又查了一下 nginx 文档 proxy_ssl_name ,发现默认值是 $proxy_host,By default, the host part of the proxy_pass URL is used.

由于我这里 proxy_pass 填写的是 127.0.0.1,所以无法获取到 host,而chatgpt 的答案中 proxy_ssl_name 设置为客户端请求的主机名 $host,所以 frp 可以正常获取到 host

贴一篇博客供参考 - Nginx反向代理,当后端为Https时的一些细节和原理