NGINX + HAProxy:基于 ALPN 的 SSL 单端口多路复用

NGINX ALPN 分流方案

目标

服务端只开放一个对外端口 443,使用同一张 SSL 证书,同时承载两类流量:

  • 浏览器发来的普通 HTTPS 请求,直接返回网站内容
  • 设备发来的隧道流量(socks5 over tls),透传到后端业务端口 8300

两类流量从外部看完全一样,都是标准 SSL 连接,无法通过端口或协议特征区分。区分手段是 TLS 握手中客户端主动声明的 ALPN 值。

原理概述

客户端连接 443 端口时,TLS 握手的第一个包(ClientHello)包含两个明文可见的字段:

  • SNI:客户端要连接的域名
  • ALPN:客户端声明的应用层协议,如 h2http/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 配置

1
apt install 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_prereadssl(SSL 终止)不能在同一个 server 块中同时使用。443 那层只负责读 ALPN 和透传,SSL 握手必须在后端(4433 / 4444)完成。

ALPN 值可以是任意字符串,不限于 IANA 注册的标准值。只要客户端与服务端约定一致即可。避免使用 h2http/1.1 等标准值,防止与正常浏览器流量误匹配。