跳转到内容

反向代理设置

将 rVPN 运行在反向代理后可以在代理层终止 TLS,在根路径提供诱饵网站,并在多个服务器实例之间分发连接。代理仅将 /api/* 流量转发给 rVPN——其他一切都可以提供看起来正常的网站。


在反向代理后运行时,请省略 TLS 证书并绑定到本地端口:

[server]
bind_address = "127.0.0.1:8443"
websocket_path = "/api/v1/ws"
identity_key_file = "/etc/rvpn/server_identity.key"
# No tls_cert_file / tls_key_file — TLS is terminated at the proxy

代理处理 TLS 并将所有 /api/* 请求转发到 127.0.0.1:8443。这涵盖了主 WebSocket 端点 (/api/v1/ws)、TUN 端点 (/api/v1/ws/tun) 以及 DNS 端点 (/api/v1/ws/dns)。


Caddy 是推荐的代理——它自动获取和续期 TLS 证书,并且无需额外配置即可处理 WebSocket 升级。

{
# Global server options — required for long-lived WebSocket connections.
# These don't affect normal HTTP traffic, only idle connection behavior.
servers {
timeouts {
idle 24h # WebSocket connections are long-lived
}
keepalive_idle 1m # Start TCP keepalive probes after 1m idle
keepalive_interval 30s
keepalive_count 5
}
}
your-domain.com {
tls {
protocols tls1.3
}
handle /api/* {
reverse_proxy 127.0.0.1:8443 {
transport http {
read_timeout 0
write_timeout 0
keepalive 0s # no upstream connection reuse
}
flush_interval -1 # critical for WebSocket — disables response buffering
}
}
}

在根路径提供看起来正常的网站。只有 /api/* 会被转发到 rVPN。普通访客和自动扫描器看到的是一个常规网站。

{
# Global server options — required for long-lived WebSocket connections.
servers {
timeouts {
idle 24h
}
keepalive_idle 1m
keepalive_interval 30s
keepalive_count 5
}
}
your-domain.com {
tls {
protocols tls1.3
}
# VPN traffic — all sub-paths under /api/ go to rvpn-server
handle /api/* {
reverse_proxy 127.0.0.1:8443 {
transport http {
read_timeout 0
write_timeout 0
keepalive 0s
}
flush_interval -1 # critical for WebSocket — disables response buffering
}
}
# Decoy site — everything else serves static files
handle {
root * /var/www/html
file_server
}
}

将诱饵站点文件放置在 /var/www/html。任何有效的静态 HTML 站点都可以——博客、着陆页、作品集。越逼真越好。

提示: Caddy 会自动为 your-domain.com 申请 Let’s Encrypt 证书。无需手动运行 certbot

{
servers {
idle_timeout 24h
keepalive_idle 1m
keepalive_interval 30s
keepalive_count 5
}
}
your-domain.com {
tls {
protocols tls1.3
}
handle /api/* {
reverse_proxy 10.0.0.1:8443 10.0.0.2:8443 10.0.0.3:8443 {
lb_policy ip_hash # sticky by client IP — required for per-session ratchet state
# rvpn-server has no /healthz endpoint. Use passive health checks:
# a backend is marked down after unresponsiveness during a real request.
fail_duration 30s
max_fails 3
transport http {
read_timeout 0
write_timeout 0
}
flush_interval -1 # critical for WebSocket — disables response buffering
}
}
handle {
root * /var/www/html
file_server
}
}

lb_policy ip_hash 至关重要:每个客户端的 X3DH + Double Ratchet 会话绑定到特定的服务器实例,因此来自同一客户端 IP 的连接必须始终到达同一后端。


Nginx 自身不会申请证书,但 certbot --nginx 插件同时处理首次签发和自动续期——它会编辑你的配置以添加证书路径,并且其 systemd 定时器会在每次续期后重新加载 nginx。无需手动配置续期钩子。

Terminal window
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d your-domain.com

Certbot 会自动在你的配置中添加 ssl_certificate 行。下面的示例显式包含了它们,以便让完整配置更清晰。

server {
listen 443 ssl;
server_name your-domain.com;
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
ssl_protocols TLSv1.3;
# Proxy all /api/* to rvpn-server.
# Prefix match covers /api/v1/ws, /api/v1/ws/tun, and /api/v1/ws/dns.
location /api/ {
proxy_pass http://127.0.0.1:8443;
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_read_timeout 0; # never time out — VPN connections are long-lived
}
}
# Redirect HTTP → HTTPS
server {
listen 80;
server_name your-domain.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name your-domain.com;
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
ssl_protocols TLSv1.3;
# VPN — all paths under /api/ forwarded to rvpn-server
location /api/ {
proxy_pass http://127.0.0.1:8443;
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_read_timeout 0;
}
# Decoy site — static files for everything else
root /var/www/html;
location / {
try_files $uri $uri/ =404;
}
}
# Redirect HTTP → HTTPS
server {
listen 80;
server_name your-domain.com;
return 301 https://$host$request_uri;
}

注意: proxy_read_timeout 0 会禁用 nginx 对被代理连接的读取超时。若不设置,nginx 会在 60 秒(默认)后关闭空闲的 WebSocket 连接。


HAProxy 非常适合在多个 rVPN 服务器实例之间分发连接。请使用 balance source(IP 哈希)以确保每个客户端始终落到同一后端——由于会话 ratchet 状态不在实例间共享,这是必需的。

global
log /dev/log local0
maxconn 50000
ssl-default-bind-options ssl-min-ver TLSv1.3 # TLS 1.3 only
defaults
log global
mode http
option httplog
timeout connect 10s
timeout client 30s
timeout server 30s
timeout tunnel 1h # WebSocket connections — must be long
frontend https
bind *:443 ssl crt /etc/haproxy/certs/your-domain.pem alpn h2,http/1.1 ssl-min-ver TLSv1.3
bind *:80
http-request redirect scheme https unless { ssl_fc }
# Route /api/* to the rvpn cluster
acl is_vpn path_beg /api/
use_backend rvpn_cluster if is_vpn
# Everything else → decoy site
default_backend decoy_site
backend rvpn_cluster
balance source # IP hash — sticky sessions required for ratchet state
option forwardfor
option http-server-close
# rvpn-server has no /healthz endpoint. Fall back to plain L4 checks —
# HAProxy will mark a backend down after `fall` failed TCP connects.
server rvpn1 10.0.0.1:8443 check inter 10s rise 2 fall 3
server rvpn2 10.0.0.2:8443 check inter 10s rise 2 fall 3
server rvpn3 10.0.0.3:8443 check inter 10s rise 2 fall 3
backend decoy_site
# Point at a separate host or a distinct port. If http_port = 80 is set in
# server.toml AND rvpn-server runs on the same box, sending here loops back
# into the built-in 404 page.
server web 127.0.0.1:8080 check

HAProxy 需要将证书和私钥串联到单个 PEM 文件中:

Terminal window
cat /etc/letsencrypt/live/your-domain.com/fullchain.pem \
/etc/letsencrypt/live/your-domain.com/privkey.pem \
> /etc/haproxy/certs/your-domain.pem
chmod 600 /etc/haproxy/certs/your-domain.pem

添加到 certbot 的续期钩子:

/etc/letsencrypt/renewal-hooks/deploy/haproxy-reload.sh
#!/bin/bash
cat /etc/letsencrypt/live/your-domain.com/fullchain.pem \
/etc/letsencrypt/live/your-domain.com/privkey.pem \
> /etc/haproxy/certs/your-domain.pem
systemctl reload haproxy
Terminal window
chmod +x /etc/letsencrypt/renewal-hooks/deploy/haproxy-reload.sh
Terminal window
haproxy -c -f /etc/haproxy/haproxy.cfg # validate config
systemctl reload haproxy

rVPN 为每个连接建立 X3DH 密钥协商与 Double Ratchet 会话。这些会话状态存储在处理握手的具体服务器实例的内存中。如果重新连接的客户端被路由到不同的后端,握手必须重新进行——旧会话已经丢失。

使用 ip_hash(Caddy)或 balance source(HAProxy),客户端在重连间会一致地到达同一后端。如果该后端宕机,它们会自动在另一个后端上重新握手。