外观
家庭服务外网访问架构重构:Nginx SNI 分流 + ZeroTier 虚拟组网
背景与动机
硬件环境
- 云服务器:阿里云 2核2G
- 内网物理机:Dell R730XD(E5-2650 v4, 32G)
之前的方案:frp 内网穿透
早期使用 frp 的内网穿透方案 将内网服务暴露到外网:
用户请求 -> 阿里云nginx -> frps(云服务器) -> frpc(内网) -> 目标服务frp 方案的局限性:
- 所有流量都要经过 frp 转发,额外增加一层代理开销
- 每新增一个服务,需要同时修改云服务器和内网机的配置
- 某些协议(如 WebSocket)需要额外配置支持
架构概述
本文介绍 SNI 分流 + ZeroTier 的架构方案核心组件:
- 云服务器:拥有公网 IP,作为流量入口,负责 SNI 分流
- ZeroTier:建立云服务器与内网物理机的虚拟局域网隧道。参考Zerotier组网的简单应用
- 内网物理机:运行实际服务,通过 ZeroTier 接收转发流量
核心思路:云服务器只做流量分发,实际服务运行在内网物理机。
网络拓扑
网络请求
│
▼
阿里云
│
┌─────────────┼───────────────┐
│ │ │
▼ ▼ ▼
frp.a.com zerotier.a.com *.a.com (默认)
│ │ │
▼ ▼ ▼
frp dashboard zerotier ztncui ZeroTier 隧道
│
▼
┌─────────────────────────────────────┐
│ 内网物理机 (R730XD) │
│ ZeroTier IP: 172.xx.xx.x │
│ │
│ ┌─────────┬─────────┬──────────┐ │
│ │ service1│ service2│ service3 │ │
│ │ service4│ service5│ ... │ │
│ └─────────┴─────────┴──────────┘ │
└─────────────────────────────────────┘优势
- 内网服务不直接暴露公网,通过 ZeroTier 隧道通信
- 无需额外配置,新增/修改服务只需在内网配置,云服务器无需改动
- 降低云服务器负载压力,SNI 分流在四层完成,无需解密 HTTPS
云服务器配置
云服务器作为流量入口,使用 Nginx 的 stream 模块进行四层代理和 SNI 分流。
/etc/nginx/nginx.conf
- stream 块:处理 443 端口的 TCP 流量,根据 SNI 域名分流
- http 块:处理 80 端口的 HTTP 流量,没有SNI,但是可以直接根据域名分流转发到内网
stream {
# 内网物理机 upstream (通过 ZeroTier)
upstream home_backend {
server 172.xx.x.x:443;
}
# 本地服务 upstream
upstream local_https {
server 127.0.0.1:8443;
}
# SNI 路由表
map $ssl_preread_server_name $backend {
frp.a.com local_https;
zerotier.a.com local_https;
default home_backend;
}
server {
listen 443;
ssl_preread on; # 启用 SNI 预读取
proxy_pass $backend; # 根据域名分流
proxy_protocol on; # 传递真实 IP
}
}- HTTP 转发
/etc/nginx/nginx.conf (http 块)
server {
listen 80;
# 因为frps和zerotier配置了特定server_name,优先于泛域名匹配,因此不影响本地服务转发
server_name *.a.com a.com;
client_max_body_size 500M;
location / {
proxy_pass http://172.xx.x.x:80;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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 服务配置
/etc/nginx/conf.d/frp.a.com处理需要在云服务器本地处理的特定域名
server {
listen 8443 ssl http2 proxy_protocol;
server_name frp.a.com;
# 接收 proxy_protocol 传递的真实 IP
set_real_ip_from 127.0.0.1;
real_ip_header proxy_protocol;
location / {
proxy_pass http://127.0.0.1:6001;
}
ssl_certificate /etc/letsencrypt/live/frp.a.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/frp.a.com/privkey.pem;
}
# HTTP 重定向
server {
listen 80;
server_name frp.a.com;
return 301 https://$host$request_uri;
}内网物理机配置
内网物理机运行在家庭网络中,通过 ZeroTier 与云服务器组成虚拟局域网。
- 内网服务配置
处理阿里云转发过来的域名请求,根据子域名反向代理到不同服务:
/etc/nginx/conf.d/service1.a.com
server {
listen 443 ssl http2 proxy_protocol;
server_name service1.a.com;
# 接收 proxy_protocol 传递的真实 IP
set_real_ip_from 127.0.0.1;
real_ip_header proxy_protocol;
location / {
proxy_http_version 1.1;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_pass http://127.0.0.1:7001;
}
ssl_certificate /etc/letsencrypt/live/service1.a.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/service1.a.com/privkey.pem;
}
# HTTP 也被转发进来了,重定向
server {
listen 80;
server_name service1.a.com;
return 301 https://$host$request_uri;
}- 主配置文件
/etc/nginx/nginx.conf
内网 Nginx 使用标准的 HTTP 模块配置
user www-data;
worker_processes auto;
error_log /var/log/nginx/error.log;
http {
sendfile on;
tcp_nopush on;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
access_log /var/log/nginx/access.log;
gzip on;
# 包含所有站点配置
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/conf.d/*.a.com; # 自己添加的include
}证书管理
- 云服务器证书
为需要在本地处理的域名申请独立证书:
frp.a.com
zerotier.a.com- 内网物理机证书
直接在内网就可以为泛域名和各个子域名申请证书(二选一即可):
# 泛域名证书
*.a.com
# 独立子域名证书
service1.a.com
service2.a.com
service3.a.com
# ...由于我直接用certbot --nginx配置的证书,直接用certbot renew即可更新证书,无需手动操作。
问题
- SSL 协议错误
ERR_SSL_PROTOCOL_ERROR
由于 stream 模块配置了 proxy_protocol on,监听ssl的 Nginx 配置必须使用 proxy_protocol 协议,否则无法正常握手。
云服务的服务、内网的服务,都需要加上。
- Nginx nchan 模块导致 SSL 证书续期失败
Nginx nchan 模块导致 SSL 证书批量续期失败,可能是certbot大量重写配置文件并行重载nginx时导致的bug,参考:Nginx nchan 模块导致 SSL 证书批量续期失败的调试记录。
