Nginx + FRP 端口收敛实战指南

通过 TLS SNI 与 Nginx stream 模块实现内网服务的优雅暴露
基于 FRP v0.61+ / Nginx 1.25+


1. 问题背景与端口收敛概念

1.1 为什么需要端口收敛?

在私有部署场景中,我们经常面临这样的困境:

暴露前(混乱):
┌─────────────────────────┐
│  公网 IP / 云服务器       │
├─────────────────────────┤
│  :80   → HTTP           │
│  :443  → HTTPS         │
│  :2222 → SSH           │
│  :3306 → MySQL (危险!) │
│  :6379 → Redis (危险!) │
│  :8080 → API-1         │
│  :8081 → API-2         │
│  :9200 → ElasticSearch  │
│  :27017→ MongoDB       │
│  ...更多端口...          │
└─────────────────────────┘
  每个服务一个端口,安全隐患多,管理复杂

核心问题:

  • 端口暴露过多 = 攻击面大
  • 每个服务单独配置证书、权限复杂
  • 云服务器安全组规则难以维护
  • NAT 端口映射数量有限(云厂商通常限制 20-50 个)

1.2 什么是端口收敛?

端口收敛(Port Consolidation) 是将多个内部服务归一化到少量(甚至 1-2 个)公网端口的技术。

暴露后(收敛):
┌─────────────────────────┐
│  公网 IP / 云服务器       │
├─────────────────────────┤
│  :443  → 统一入口        │
│        ↘ SNI 路由        │
│           ├→ api.example.com  → :8080
│           ├→ db.example.com   → :3306
│           ├→ cache.example.com → :6379
│           └→ web.example.com   → :80
│  :2222 → 仅 SSH(严格限制)│
└─────────────────────────┘

1.3 本文目标

基于 TLS SNI 识别 + Nginx stream 模块四层代理,实现:

  1. 仅暴露 443 (HTTPS) 和 2222 (SSH) 两个端口
  2. 通过域名自动路由到不同内网服务
  3. 所有流量加密,证书统一管理
  4. FRP 作为底层穿越隧道

2. 技术原理:TLS SNI 与 Nginx stream 模块

2.1 TLS SNI(Server Name Indication)

SNI 是 TLS 协议扩展,客户端在 TLS 握手初期就发送域名:

Client                            Server
  │                                 │
  │──── ClientHello ────────────────│
  │      extensions[]:              │
  │        server_name: api.xxx.com│  ← 明文传输!
  │                                 │
  │◄─── ServerHello + Certificate ─│
  │                                 │

关键点:

  • SNI 在 TLS 握手 ClientHello 阶段以明文传输
  • 即使流量加密,域名也会暴露在 TLS 头中
  • 利用这一特性,可以在 不解密 的情况下识别目标服务
  • nginx stream 模块正是基于此实现四层智能路由

2.2 Nginx stream 模块 vs http 模块

Nginx 有两个核心模块:

特性 http 模块 stream 模块
层级 OSI 第七层(应用层) OSI 第四层(传输层)
协议 HTTP/1.x, HTTP/2 TCP 任意协议
能力 URL 路由、证书、gzip SNI 路由、SSL握手、TCP 转发
延迟 高(完整 HTTP 解析) 低(SNI 直接路由)
配置 server {} stream {}

2.3 工作原理图解

用户请求: https://api.example.com:443
              │
              ▼
┌─────────────────────────────────┐
│      Nginx (stream 模块)         │
│   端口: 0.0.0.0:443             │
│                                 │
│  读取 ClientHello 的 SNI        │
│  server_name: api.example.com   │
│                                 │
│  比对 map 规则                   │
│  └─── 匹配: api.example.com     │
│        → proxy_pass 127.0.0.1:9443│
│                                 │
│  未匹配 → default               │
│        → proxy_pass 127.0.0.1:9080│
└─────────────────────────────────┘
              │
              ▼
┌─────────────────────────────────┐
│  FRP (frpc) 隧道                 │
│  127.0.0.1:9443 ↔ 内网:443     │
└─────────────────────────────────┘
              │
              ▼
┌─────────────────────────────────┐
│  内网服务(需自行配置证书)        │
│  :443  API Backend              │
│  :3306 MySQL                    │
│  :6379 Redis                    │
└─────────────────────────────────┘

2.4 为什么不直接用 Nginx http 模块?

HTTP 模块也可以做反向代理,但:

场景 http 模块 stream + SNI
非 HTTP 服务(MySQL/Redis) ❌ 无法直接代理 原生支持
SNI 路由(不解密直接转发) ❌ 需要终止 TLS 原生支持
WebSocket ✅ 支持 ✅ 支持
gRPC ✅ 支持 ✅ 支持
性能开销 较高(完整 HTTP 解析) 更低

结论:混合架构才是最优解

  • HTTP/HTTPS Web 服务 → http 模块(需要 URL 路由时)
  • 所有服务统一入口 → stream 模块 + SNI 路由
  • SSH 等非 TLS 服务 → stream 模块单独端口

3. 最终架构图与数据流

3.1 完整架构图

┌──────────────────────────────────────────────────────────────────────────────┐
│                          公网服务器 (VPS/云)                              │
│                                                                       │
│   443/tcp ────────────────────────────────┐                            │
│   2222/tcp ───────────────────┐          │                            │
│                                │          │                            │
│                    ┌───────────┴───────────┴───────────┐               │
│                    │         Nginx (stream)             │               │
│                    │                                    │               │
│                    │  stream {                           │               │
│                    │    server {                        │               │
│                    │      listen 443;                    │               │
│                    │      ssl_preread on;                │               │
│                    │      proxy_pass ...;               │               │
│                    │    }                                │               │
│                    │  }                                 │               │
│                    └───────────┬───────────┬───────────┘               │
│                                │           │                            │
│                                ▼           ▼                            │
│                         SNI 路由      SSH 转发                         │
│                        (根据域名)    (直接转发)                         │
│                                                                       │
│   22 ◄────────────────────────────────────────────────────────── SSH  │
│                                                                       │
└──────────────────────────────────────────────────────────────────────────────┘
                                │
                    ┌───────────┴───────────┐
                    │    FRP (frpc)         │
                    │  TCP 隧道 + TLS 加密   │
                    └───────────┬───────────┘
                                │
              ┌─────────────────┼─────────────────┐
              │                 │                 │
              ▼                 ▼                 ▼
     ┌────────────┐      ┌────────────┐     ┌────────────┐
     │ 内网机器 1  │      │ 内网机器 2  │     │ 内网机器 3  │
     │            │      │            │     │            │
     │ :443 API   │      │ :3306 MySQL│     │ :6379 Redis│
     │ :80  Web   │      │ :22 SSH    │     │            │
     └────────────┘      └────────────┘     └────────────┘

3.2 数据流详解

场景一:访问 https://api.example.com
1. 用户浏览器
   └─► api.example.com:443 (TLS ClientHello with SNI: api.example.com)

2. 公网 Nginx (stream)
   └─► 读取 SNI → 匹配 map → proxy_pass → 127.0.0.1:9443 (FRP隧道入口)

3. 公网 FRP Server (frps)
   └─► 接收加密流量 → 转发给内网 FRP Client

4. 内网 FRP Client (frpc)
   └─► 解密 → 转发给内网服务 10.0.0.2:443

5. 内网 API 服务(需配置 api.example.com 的证书)
   └─► 处理请求 → 响应原路返回
场景二:SSH 登录内网机器
1. 用户 SSH 客户端
   └─► 公网IP:2222

2. 公网 Nginx (stream - SSH server 块)
   └─► 直接 TCP 代理 → 127.0.0.1:2223 (FRP SSH 隧道) → 内网 10.0.0.2:22

3. 内网机器 SSH 服务
   └─► 验证密钥 → 建立会话

4. 详细配置步骤

4.1 环境说明

角色 主机 组件
公网服务器 1.2.3.4 (VPS) Nginx, FRP Server (frps)
内网机器 192.168.1.100 (家庭/公司内网) FRP Client (frpc), 各种服务

4.2 公网服务器配置

步骤 1:安装 Nginx(包含 stream 模块)
# Ubuntu/Debian
sudo apt update
sudo apt install nginx -y

# 验证 stream 模块已启用(更准确的检查方式)
nginx -V 2>&1 | grep -- '--with-stream'
# 输出包含 "--with-stream" 即成功

# CentOS/RHEL
sudo yum install nginx -y
步骤 2:下载 FRP(Server 端)
cd /tmp
wget https://github.com/fatedier/frp/releases/download/v0.61.1/frp_0.61.1_linux_amd64.tar.gz
tar -xzf frp_0.61.1_linux_amd64.tar.gz
sudo mv frp_0.61.1_linux_amd64 /opt/frps
cd /opt/frps
步骤 3:配置 FRP Server (/opt/frps/frps.toml)
# frps.toml - FRP Server 配置 (v0.61+ 标准语法)
bindAddr = "0.0.0.0"
bindPort = 7000

# Web 管理界面(可选)
webServer.addr = "0.0.0.0"
webServer.port = 7500
webServer.user = "admin"
webServer.password = "your_admin_password"

# 认证配置
[auth]
method = "token"
token  = "your_very_strong_token_here_at_least_32_chars"

# TLS 传输配置(强制加密)
[transport]
tls.enable = true
tls.certFile = "/opt/frps/tls/frp.crt"
tls.keyFile  = "/opt/frps/tls/frp.key"

# 心跳与多路复用
heartbeatInterval = 30
heartbeatTimeout = 90
tcpMux = true

# 安全:代理端口仅绑定本地(防止暴露到公网)
proxyBindAddr = "127.0.0.1"

说明

  • proxyBindAddr = "127.0.0.1" 使得 frps 只监听本地地址转发流量,避免代理端口(如 9443)被公网直接访问。
  • tcpMux 大幅减少连接数,提高穿透稳定性。
步骤 4:生成 FRP TLS 证书(自签名)
sudo mkdir -p /opt/frps/tls
cd /opt/frps/tls

# 生成自签名证书(有效期10年)
openssl req -new -newkey rsa:4096 -days 3650 -nodes -x509 \
  -subj "/CN=*.frp.internal" \
  -keyout frp.key -out frp.crt

sudo chmod 600 frp.key frp.crt
步骤 5:配置 Nginx stream(核心配置)

创建独立的配置文件(推荐):

sudo nano /etc/nginx/conf.d/stream-sni.conf

完整配置(已验证语法正确):

# /etc/nginx/conf.d/stream-sni.conf
stream {
    # 日志格式 - 包含 SNI 信息
    log_format sni '$remote_addr:$remote_port [$time_local] '
                   '$protocol -> $upstream_addr '
                   'sni="$ssl_preread_server_name" '
                   'bytes_s=$bytes_sent bytes_r=$bytes_received '
                   'upstream_connect_time=$upstream_connect_time';

    access_log /var/log/nginx/stream_sni.log sni;
    error_log  /var/log/nginx/stream_sni.error.log warn;

    # SNI → 本地 FRP 代理端口映射(直接返回 IP:Port,无需 upstream 块)
    map $ssl_preread_server_name $target_backend {
        api.example.com    127.0.0.1:9443;
        db.example.com     127.0.0.1:9444;
        cache.example.com  127.0.0.1:9445;
        web.example.com    127.0.0.1:9446;
        default            127.0.0.1:9080;   # 未匹配的域名走默认
    }

    # ===== HTTPS 统一入口(SNI 路由) =====
    server {
        listen 443 reuseport;
        ssl_preread on;          # 关键:开启 SNI 读取
        proxy_connect_timeout 10s;
        proxy_timeout 3600s;
        proxy_pass $target_backend;
    }

    # ===== SSH 独立入口 =====
    server {
        listen 2222;
        proxy_connect_timeout 10s;
        proxy_timeout 1800s;
        proxy_pass 127.0.0.1:2223;   # FRP 映射的 SSH 端口
    }
}

重要说明

  • 此配置为 TLS 透传(Passthrough),Nginx 仅读取 SNI,不终止 TLS 连接。
  • 内网目标服务(如 api.example.com:443必须自行配置该域名的有效证书,否则浏览器会报证书错误。
  • 若需要记录真实客户端 IP,可配合 proxy_protocol(见第 10 节扩展)。
步骤 6:启动 FRP Server
# 创建 systemd 服务
sudo nano /etc/systemd/system/frps.service
[Unit]
Description=FRP Server
After=network.target

[Service]
ExecStart=/opt/frps/frps -c /opt/frps/frps.toml
Restart=on-failure
RestartSec=5s
User=root
LimitNOFILE=1048576

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable frps
sudo systemctl start frps
sudo systemctl status frps

4.3 内网机器配置

步骤 1:下载 FRP Client
cd /tmp
wget https://github.com/fatedier/frp/releases/download/v0.61.1/frp_0.61.1_linux_amd64.tar.gz
tar -xzf frp_0.61.1_linux_amd64.tar.gz
sudo mv frp_0.61.1_linux_amd64 /opt/frpc
cd /opt/frpc
步骤 2:配置 FRP Client (/opt/frpc/frpc.toml)
# frpc.toml - FRP Client 配置 (v0.61+ 标准语法)
serverAddr = "1.2.3.4"
serverPort = 7000

# 认证(必须与服务端一致)
[auth]
method = "token"
token  = "your_very_strong_token_here_at_least_32_chars"

# TLS 传输配置
[transport]
tls.enable = true
# 自签名证书需指定 CA/证书文件
tls.trustedCaFile = "/opt/frpc/tls/frp.crt"
tcpMux = true
heartbeatInterval = 30
heartbeatTimeout = 90

# ===== 定义代理(注意使用 [[proxies]] 官方复数形式)=====

[[proxies]]
name = "ssh-tunnel"
type = "tcp"
localIP = "127.0.0.1"
localPort = 22
remotePort = 2223   # 对应 Nginx 2222 代理的目标

[[proxies]]
name = "api-https"
type = "tcp"
localIP = "127.0.0.1"
localPort = 443      # 内网 API 服务的 HTTPS 端口
remotePort = 9443    # 对应 Nginx map 中的 127.0.0.1:9443

[[proxies]]
name = "mysql"
type = "tcp"
localIP = "192.168.1.101"   # 若 MySQL 在其他机器
localPort = 3306
remotePort = 9444

[[proxies]]
name = "redis"
type = "tcp"
localIP = "127.0.0.1"
localPort = 6379
remotePort = 9445

[[proxies]]
name = "web-http"
type = "tcp"
localIP = "127.0.0.1"
localPort = 80
remotePort = 9446

注意

  • tls.trustedCaFile 指向服务端证书的 CA 或自签名证书本身,用于客户端验证服务端身份。
  • 若内网服务分布在多台机器,修改对应 localIP 即可。
步骤 3:复制 TLS 证书到内网机器
# 在公网服务器导出证书
sudo cat /opt/frps/tls/frp.crt

# 通过安全方式(scp/rsync)传输到内网机器
# 假设内网机器 IP 为 192.168.1.100
scp /opt/frps/tls/frp.crt user@192.168.1.100:/tmp/

# 在内网机器上放置证书
sudo mkdir -p /opt/frpc/tls
sudo mv /tmp/frp.crt /opt/frpc/tls/
sudo chmod 644 /opt/frpc/tls/frp.crt
步骤 4:启动 FRP Client
sudo nano /etc/systemd/system/frpc.service
[Unit]
Description=FRP Client
After=network.target

[Service]
ExecStart=/opt/frpc/frpc -c /opt/frpc/frpc.toml
Restart=on-failure
RestartSec=5s
User=root
LimitNOFILE=1048576

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable frpc
sudo systemctl start frpc
sudo systemctl status frpc

4.4 多内网机器扩展

当有多个内网机器时,在每台机器上运行独立的 frpc,并分配不同的 remotePort。例如:

机器 A (192.168.1.100) - frpc 配置:

[[proxies]]
name = "machine-a-api"
type = "tcp"
localIP = "127.0.0.1"
localPort = 443
remotePort = 9443

机器 B (192.168.1.101) - frpc 配置:

[[proxies]]
name = "machine-b-mysql"
type = "tcp"
localIP = "127.0.0.1"
localPort = 3306
remotePort = 9444

公网 Nginx 的 map 保持不变,只要 remotePortproxy_pass 的后端端口一致即可。


5. 遇到的典型问题及解决方法

问题 1:SNI 路由不生效,流量全部打到 default

症状curl -v --resolve api.example.com:443:1.2.3.4 https://api.example.com 访问到默认后端。

排查步骤

# 1. 确认客户端发送了 SNI(抓包)
sudo tcpdump -i any -nn 'tcp port 443' -A | grep -i "server.name"

# 2. 查看 Nginx stream 日志(已配置 SNI 字段)
sudo tail -f /var/log/nginx/stream_sni.log

# 3. 确认 ssl_preread 已开启
grep -A5 "listen 443" /etc/nginx/conf.d/stream-sni.conf

常见原因

  • 客户端使用 IP 直接访问 HTTPS(浏览器地址栏输入 https://1.2.3.4 不会发送 SNI)
  • Nginx 中 ssl_preread on; 未配置或拼写错误
  • map 中的域名与实际 SNI 不匹配(注意大小写,Nginx 默认区分,可用 ~* 修饰符)

解决方法

# 使用不区分大小写的匹配
map $ssl_preread_server_name $target_backend {
    ~*^api\.example\.com$   127.0.0.1:9443;
    default                 127.0.0.1:9080;
}

问题 2:FRP TLS 握手失败

症状

frpc[12345]: [WARN] tls: failed to dial remote: tls: first record does not look like a TLS handshake

原因:服务端未开启 TLS,客户端强制 TLS 连接。

解决方法:确保服务端 [transport]tls.enable = true,且证书路径正确。若使用自签名,客户端必须指定 tls.trustedCaFile

问题 3:MySQL 连接被拒绝(字符编码/协议问题)

症状ERROR 2013: Lost connection to MySQL server at 'reading initial communication packet'

原因:Nginx stream 代理 MySQL,但 FRP 端口映射错误或 MySQL 绑定了本地地址。

排查

# 确认 FRP 代理状态
sudo journalctl -u frpc -f

# 确认 Nginx 的 proxy_pass 端口与 FRP remotePort 一致
# 例如 Nginx 中 proxy_pass 127.0.0.1:9444,FRP 中 remotePort = 9444

解决方法:统一端口配置;若 MySQL 只监听 127.0.0.1,需改为 0.0.0.0 或内网 IP。

问题 4:SSH 连接超时

症状ssh: connect to host 1.2.3.4 port 2222: Connection timed out

排查

# 1. 确认防火墙放行
sudo iptables -L -n | grep 2222
# 或
sudo ufw status

# 2. 确认云安全组放行 2222/tcp

# 3. 确认 Nginx stream 的 SSH server 块配置正确

问题 5:gRPC 或 HTTP/2 无法正常工作(证书错误)

症状:gRPC 客户端报错 SSL_ERROR_BAD_CERT_DOMAINhttp2: unexpected greeting

根本原因:由于本方案是 TLS 透传,Nginx 不终止 TLS,内网服务必须自己配置与访问域名匹配的证书。如果内网服务使用自签名证书或证书域名与 api.example.com 不符,浏览器或 gRPC 客户端会拒绝连接。

解决方法

  • 为内网服务(如内网 Nginx、API 进程)配置正确的证书(可用 Let’s Encrypt 或内部 CA)。
  • 测试时可用 curl -k 忽略证书错误,但生产环境必须使用有效证书。

6. 验证方法

6.1 基础连通性测试

# 公网服务器本地测试
nc -zv 127.0.0.1 2222

# 公网测试 SSH 端口
nc -zv 1.2.3.4 2222

# 公网测试 HTTPS 端口(验证 SNI 路由)
openssl s_client -connect 1.2.3.4:443 -servername api.example.com

6.2 SNI 路由验证

# 测试特定域名的 SNI 路由(使用 curl 指定域名)
curl -I https://api.example.com \
  --resolve api.example.com:443:1.2.3.4 \
  -w "\n%{http_code}\n" 2>/dev/null

# 查看 Nginx 日志确认 SNI 值
tail -f /var/log/nginx/stream_sni.log

6.3 FRP 隧道验证

# 查看 FRP 连接状态(如果开启了 webServer)
curl -s http://1.2.3.4:7500/api/status | jq

# 或在公网服务器查看日志
sudo journalctl -u frps -f

# 在内网机器查看客户端状态
/opt/frpc/frpc status -c /opt/frpc/frpc.toml

6.4 端到端服务测试

# 测试 API 服务
curl https://api.example.com/api/v1/health \
  --resolve api.example.com:443:1.2.3.4

# 测试 MySQL 连接(注意使用 Nginx 暴露的端口? 不,应直接连接 FRP 端口?)
# 实际上 MySQL 已通过 SNI 路由,访问 1.2.3.4:443 并发送 db.example.com SNI 即可
mysql -h 1.2.3.4 -P 443 -u root -p --ssl-mode=REQUIRED

# 测试 SSH 隧道
ssh -p 2222 -o StrictHostKeyChecking=no user@1.2.3.4

6.5 日志分析

# Nginx stream 访问日志
tail -f /var/log/nginx/stream_sni.log

# FRP Server 日志
journalctl -u frps -f

# FRP Client 日志
journalctl -u frpc -f

7. 扩展与备选方案

7.1 多域名泛解析 SNI 路由

map $ssl_preread_server_name $target_backend {
    ~^(?<app>.+)\.example\.com$   127.0.0.1:9${app}_port;  # 动态映射(需配合脚本)
    default                       127.0.0.1:9080;
}

更实用的方式:使用 map 配合正则和变量,但需提前定义好端口映射。

7.2 记录真实客户端 IP(Proxy Protocol)

若内网服务支持 Proxy Protocol(如 Nginx、HAProxy),可获取真实客户端 IP。

步骤

  1. 公网 Nginx stream 配置添加 proxy_protocol on;
  2. FRP 代理配置添加 proxyProtocolVersion = "v2"
  3. 内网 Nginx 或服务监听时启用 proxy_protocol

示例(仅展示增量配置):

# Nginx stream server 块中
server {
    listen 443 reuseport proxy_protocol;   # 接收 PROXY 协议
    ssl_preread on;
    proxy_pass $target_backend;
}
# frpc.toml 的 proxy 条目中
[[proxies]]
name = "api-https"
type = "tcp"
localIP = "127.0.0.1"
localPort = 443
remotePort = 9443
proxyProtocolVersion = "v2"    # 发送 PROXY 协议给内网

内网 Nginx 配置:

server {
    listen 443 ssl http2 proxy_protocol;
    set_real_ip_from 127.0.0.1;
    real_ip_header proxy_protocol;
    ...
}

7.3 替代方案对比

方案 优点 缺点 适用场景
Nginx + FRP(本文) 成熟稳定,SNI 路由灵活 配置较复杂 追求性价比,多服务场景
Cloudflare Tunnel 零配置,CDN 加速 依赖 CF,流量经过第三方 快速部署,不在意延迟
WireGuard + Nginx 内网 VPN 更安全 不支持 SNI 路由 纯内网访问,无公网暴露
HAProxy + FRP 性能更高 配置更复杂 超高并发场景
Traefik 自动 HTTPS,Let’s Encrypt 主要面向 HTTP 纯 Web 服务

8. 安全建议

8.1 必做安全措施

✅ 1. FRP 强制 TLS
     - frps.toml: [transport] tls.enable = true
     - frpc.toml: [transport] tls.enable = true
     - 所有流量加密,防止中间人攻击

✅ 2. FRP 强认证 Token
     - auth.token = "长随机字符串(≥32 字符)"
     - 使用密码管理器生成

✅ 3. 限制 FRP 代理端口仅本地监听
     - frps.toml: proxyBindAddr = "127.0.0.1"
     - 防止代理端口直接暴露公网

✅ 4. SSH 密钥登录
     - 禁止密码登录
     - 使用 ED25519 或 RSA-4096 密钥

✅ 5. 云服务器安全组最小化
     - 只放行 443/tcp, 2222/tcp
     - 按需放行 ICMP(ping)
     - 绝对不放行 MySQL/Redis 端口

✅ 6. Nginx 限流(防止 DDoS)

8.2 Nginx 安全配置

stream {
    # 连接限流
    limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
    
    server {
        listen 443 reuseport;
        ssl_preread on;
        limit_conn conn_limit 100;
        proxy_connect_timeout 15s;
        proxy_timeout 3600s;
        proxy_buffer_size 64k;
    }
    
    server {
        listen 2222;
        limit_conn conn_limit 10;
        proxy_connect_timeout 10s;
        proxy_timeout 1800s;
    }
}

8.3 日志轮转(防止磁盘爆满)

创建 /etc/logrotate.d/nginx-frp

/var/log/nginx/stream_sni.log
/var/log/nginx/stream_sni.error.log
{
    daily
    rotate 7
    compress
    delaycompress
    missingok
    notifempty
    create 0640 nginx adm
    sharedscripts
    postrotate
        [ -f /var/run/nginx.pid ] && kill -USR1 `cat /var/run/nginx.pid`
    endscript
}

FRP 日志(若使用 systemd 管理的 journald 会自动轮转,无需额外配置)。

8.4 定期维护

# 每月更新 FRP/Nginx
sudo apt update && sudo apt upgrade nginx frp

# 定期检查证书有效期
sudo find /opt -name "*.crt" -exec openssl x509 -noout -dates -in {} \;

# 查看异常登录
sudo journalctl -u frps | grep -i "authentication\|fail\|error"

9. 总结与核心要点

9.1 一图总结

┌──────────────┐      ┌──────────────┐      ┌──────────────┐
│   用户视角   │      │  公网 VPS   │      │   内网视角  │
│              │      │              │      │              │
│ HTTPS://api │ ───► │ Nginx:443    │ ───► │ FRP Client  │
│ .example.com│      │ (SNI路由)    │      │             │
│              │      │              │      │             │
│ SSH 公网IP  │ ───► │ Nginx:2222   │ ───► │ FRP Client  │
│ :2222       │      │ (TCP转发)    │      │             │
└──────────────┘      └──────────────┘      └──────────────┘
     :443                  TLS                   :443/:22
     :2222              解密/转发               /:3306/:6379

9.2 核心要点速查表

要点 说明
只暴露 2 个端口 443 (HTTPS) + 2222 (SSH)
SNI 是关键 TLS ClientHello 中的 server_name 字段路由
ssl_preread on 必须开启才能用 stream 模块做 SNI 路由
FRP 强制 TLS tls.enable = true
Token 认证 FRP 双向认证 token
proxyBindAddr = “127.0.0.1” 防止 FRP 代理端口暴露公网
内网服务需自备证书 透传模式下 Nginx 不提供证书
map 直接返回 IP:Port 避免 upstream 混用错误
测试要完整 从公网测试,不要只本地测试

9.3 快速命令清单

# 重启 Nginx
sudo nginx -t && sudo systemctl reload nginx

# 重启 FRP Server
sudo systemctl restart frps

# 重启 FRP Client
sudo systemctl restart frpc

# 查看 FRP 状态
curl -s http://1.2.3.4:7500/api/status

# 测试 SNI 路由
openssl s_client -connect 1.2.3.4:443 -servername api.example.com

# 查看连接状态
ss -tlnp | grep -E "443|2222"

9.4 适用场景判断

用这套方案 ✅ 如果:

  • 有多台内网服务需要暴露
  • 云服务器端口数有限制
  • 想统一管理证书(内网服务各自证书,公网不存私钥)
  • 有一定 Linux 运维能力

考虑备选方案 🔄 如果:

  • 只是临时测试 → Cloudflare Tunnel
  • 纯 Web 服务且想自动化证书 → Let’s Encrypt + Nginx http 模块
  • 需要最高性能 → HAProxy + FRP
  • 完全不信任公网 → WireGuard 全隧道

10. 生产环境额外注意事项

  1. 证书归属:本方案为 TLS 透传,Nginx 不持有任何域名证书。请确保内网服务(如 api.example.com:443)已正确配置 api.example.com 的证书(可用 Let’s Encrypt 或内部 CA)。

  2. SNI 盲区处理:某些老旧客户端或不支持 SNI 的工具可能不发送 server_name,建议 default 目标指向一个友好的提示服务(如返回 “SNI required” 的静态页面)。

  3. FRP 心跳与 TCP 多路复用:已在配置中加入 heartbeatIntervalheartbeatTimeouttcpMux,可有效应对家庭宽带 NAT 超时问题。

  4. 日志轮转:务必配置 logrotate(见 8.3),否则高流量下 /var/log/nginx/stream_sni.log 可能撑爆磁盘。

  5. 系统限制:在 /etc/systemd/system/frps.servicefrpc.service 中已添加 LimitNOFILE=1048576,防止高并发下文件描述符耗尽。

  6. 公网安全组:仅需开放 443/tcp2222/tcp不可开放 FRP 的 7000 端口(管理端口)或代理端口(如 9443)到公网。

  7. web服务:若你需要在同一台公网服务器上额外运行 Nginx http 模块,反向代理某些 Web 服务,并且希望对外表现为标准 443 端口(实际监听非标端口如 8443),请添加以下配置:

server {
    listen 127.0.0.1:8443 ssl http2;
    server_name files.example.com;

    # 阻止 Nginx 在自动生成的重定向中加入端口号(8443)
    port_in_redirect off;

    ssl_certificate /path/to/fullchain.pem;
    ssl_certificate_key /path/to/privkey.pem;

    location / {
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header X-Forwarded-Port 443;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_pass http://127.0.0.1:8080;
    }
}

文档版本:v2.0
适用 FRP 版本:v0.61+
适用 Nginx 版本:1.25+ (stream 模块已稳定)
最后更新:2026-04-26

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐