NGINX ALPN 分流方案
目标
服务端只开放一个对外端口 443,使用同一张 SSL 证书,同时承载两类流量:
- 浏览器发来的普通 HTTPS 请求,直接返回网站内容
- 设备发来的隧道流量(socks5 over tls),透传到后端业务端口 8300
两类流量从外部看完全一样,都是标准 SSL 连接,无法通过端口或协议特征区分。区分手段是 TLS 握手中客户端主动声明的 ALPN 值。
原理概述
客户端连接 443 端口时,TLS 握手的第一个包(ClientHello)包含两个明文可见的字段:
- SNI:客户端要连接的域名
- ALPN:客户端声明的应用层协议,如
h2、http/1.1,或自定义值
NGINX 的 ssl_preread 功能可以在不终止 SSL 的情况下读取这两个字段,然后根据值将原始 SSL 流量透传给不同的后端。后端再各自完成 SSL 握手和协议处理。
服务端架构
1 2 3 4 5 6 7
| socks5 over tls (ALPN=webrtc) → 443 ssl_preread 读 ALPN,不终止 SSL → 4444 终止 SSL,转发明文 TCP → 8300
浏览器 (ALPN=h2 / http/1.1 / 空) → 443 ssl_preread 读 ALPN,不终止 SSL → 4433 终止 SSL,提供 HTTPS 内容
|
端口说明:
443:对外唯一入口,只读 ALPN,不做 SSL 终止
4433:内部 HTTPS server,服务普通浏览器请求
4444:内部 stream server,终止 SSL 后转发到 8300
8300:实际业务服务(ss-server)
服务端 NGINX 配置
/etc/nginx/nginx.conf 完整内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
| user www-data; worker_processes auto; pid /run/nginx.pid; error_log /var/log/nginx/error.log; include /etc/nginx/modules-enabled/*.conf;
events { worker_connections 768; }
http { sendfile on; tcp_nopush on; types_hash_max_size 2048; server_tokens off;
include /etc/nginx/mime.types; default_type application/octet-stream;
ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers off;
access_log /var/log/nginx/access.log; gzip on;
include /etc/nginx/conf.d/*.conf; include /etc/nginx/sites-enabled/*;
# HTTP 重定向到 HTTPS server { listen 80; server_name blog.li-chunli.top; return 301 https://$host$request_uri; }
# 普通浏览器流量的落点:终止 SSL,提供 HTTP 内容 server { listen 4433 ssl; server_name blog.li-chunli.top; ssl_certificate /etc/nginx/blog.li-chunli.top_cert_chain.pem; ssl_certificate_key /etc/nginx/blog.li-chunli.top_key.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5;
location / { root /var/www/html/; index index.html index.htm; } } }
stream { # ALPN=webrtc 路由到 4444,其他流量路由到 4433 map $ssl_preread_alpn_protocols $backend { ~\bwebrtc\b 127.0.0.1:4444; default 127.0.0.1:4433; }
# 对外 443:只读 ALPN,原始 SSL 流量透传 server { listen 443; ssl_preread on; proxy_pass $backend; }
# ALPN=webrtc 的落点:终止 SSL,转发明文 TCP 到 8300 server { listen 4444 ssl; ssl_certificate /etc/nginx/blog.li-chunli.top_cert_chain.pem; ssl_certificate_key /etc/nginx/blog.li-chunli.top_key.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_session_cache shared:SOCKS:10m; ssl_session_timeout 10m; proxy_pass 127.0.0.1:8300; } }
|
部署命令:
1 2
| nginx -t systemctl reload nginx
|
socks5 over tls HAProxy 配置
发送端机器上,HAProxy 监听本地 55555 端口,接收 socks5,封装成 SSL(带 ALPN=webrtc)后发往服务端 443。
/etc/haproxy/haproxy.cfg:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| global log /dev/log local0 maxconn 4096
defaults log global mode tcp timeout connect 5s timeout client 60s timeout server 60s
frontend ft_in bind *:55555 default_backend bk_nginx
backend bk_nginx server nginx <服务端IP>:443 ssl alpn webrtc verify none sni str(blog.li-chunli.top)
|
server 行参数说明:
ssl:HAProxy 作为 socks5 over tls,对外连接时启用加密
alpn webrtc:TLS ClientHello 中写入 ALPN=webrtc
sni str(blog.li-chunli.top):TLS ClientHello 中写入 SNI,与连接目标 IP 无关,可以用 IP 连接同时保留域名 SNI
verify none:不验证服务端证书;如需验证改为 verify required ca-file /etc/ssl/certs/ca-certificates.crt
部署命令:
1 2
| haproxy -c -f /etc/haproxy/haproxy.cfg systemctl restart haproxy
|
完整链路
1 2 3 4 5 6
| 本地应用 → 发送端 55555 (HAProxy, 明文 TCP) → SSL 封装 ALPN=webrtc SNI=blog.li-chunli.top → 服务端 443 (NGINX ssl_preread) → 识别 ALPN=webrtc → 4444 (NGINX stream SSL 终止) → 127.0.0.1:8300 (ss-server, 明文 TCP)
|
关键限制
ssl_preread 与 ssl(SSL 终止)不能在同一个 server 块中同时使用。443 那层只负责读 ALPN 和透传,SSL 握手必须在后端(4433 / 4444)完成。
ALPN 值可以是任意字符串,不限于 IANA 注册的标准值。只要客户端与服务端约定一致即可。避免使用 h2、http/1.1 等标准值,防止与正常浏览器流量误匹配。