跳转到内容

服务器配置参考

server.toml 所有字段的完整参考。


所有字段位于 [server] 段之下,或位于根级(两种都被接受)。

字段类型默认值描述
bind_addressstring"0.0.0.0:443"监听的地址和端口。使用 "0.0.0.0:443" 监听所有接口,或使用 "127.0.0.1:8443" 仅监听本机(用于反向代理后运行)。
tls_cert_filepath"certs/cert.pem"TLS 证书路径(PEM 格式)。
tls_key_filepath"certs/key.pem"TLS 私钥路径(PEM 格式)。
identity_key_filepath"server_identity.key"服务器 X3DH 身份密钥的路径。通过 rvpn-server keygen 生成。
websocket_pathstring"/api/v1/ws"WebSocket 端点路径。为避免被检测,请为每次部署更改。
http_portinteger(disabled)若设置,则同时在该端口监听明文 HTTP(例如 80),用于 ACME 校验和 HTTP→HTTPS 重定向。
redirect_http_to_httpsbooltrue当设置了 http_port 时,将所有 HTTP 请求重定向到 HTTPS。非 WebSocket 请求会收到硬编码的类 nginx 404 以伪装成普通 Web 服务器。
prekey_bundle_filepath(none)若设置,则从此文件加载 X3DH 密钥而非生成新密钥。用于恢复已备份的身份。

控制单个 IP 地址可建立的连接数。

字段类型默认值描述
max_connections_per_ipinteger500单个 IP 地址的最大并发连接数。
max_handshakes_per_minuteinteger2000单个 IP 每分钟允许的新握手尝试次数。有助于防止暴力探测。
[server.rate_limit]
max_connections_per_ip = 10
max_handshakes_per_minute = 20

仅用于启动警告的预检提示——服务器不会根据本段编写 NAT 或分配 IP。实际的客户端 IP 来自 [server.tun].tun_ip;NAT 由运维人员设置(参见 服务器安装 中的 iptables/pf 示例)。

字段类型默认值描述
nat_enabledbooltrue若你的出口在别处处理,可设为 false 以抑制”客户端可能无法访问互联网”的启动警告。
dhcp_rangestring"10.200.0.0/24"服务器期望 NAT MASQUERADE 规则覆盖的子网。启动时服务器会在 iptables-save 中匹配此子网的规则,若未找到则发出警告。
[server.network]
nat_enabled = true
dhcp_range = "10.200.0.0/24"

真正的 TUN 到 TUN 模式设置。启用时,服务器创建 TUN 接口并通过它路由客户端数据包。

字段类型默认值描述
enabledboolfalse启用 TUN 模式。为 false 时,服务器使用中继模式(Brook 风格)。
tun_ipstring"10.200.0.1/24"TUN 接口 IP 地址和子网前缀。客户端 IP 从此子网中分配。
mtuinteger1420TUN 接口的 MTU。
interface_namestring"tun0"要创建的 TUN 接口名。
dns_serverslist["8.8.8.8"]通过隧道推送给 TUN 客户端的 DNS 解析器。
[server.tun]
enabled = true
tun_ip = "10.200.0.1/24"
mtu = 1420
interface_name = "tun0"

通过 Let’s Encrypt 自动获取 TLS 证书 — 不需要反向代理。服务器使用 TLS-ALPN-01 挑战自行获取并续订证书,因此同一个 :443 监听器同时处理正常流量和 ACME 握手。不需要 :80 端口。

字段类型默认值描述
enabledboolfalse启用 ACME 签发。当 true 时,tls_cert_file 不能存在于磁盘上 — 服务器拒绝猜测该提供哪个证书。
domainslist of strings[]要签发证书的完全限定域名。启用时必须至少有一个。第一个是主 CN;其余作为 SAN 附加。
contactslist of strings[]ACME 账户的联系 URI,通常是 ["mailto:[email protected]"]。如果续订停滞,Let’s Encrypt 会通过这些地址发送到期警告。
cache_dirpath"acme-cache"持久保存 ACME 账户密钥和已签发证书的目录。必须可写。跨重启保留以避免触发速率限制。
stagingboolfalse使用 Let’s Encrypt 的 staging 目录。证书不会被浏览器信任,但签发不计入生产速率限制。在测试 DNS 和防火墙设置时使用。

前置条件 — 在设置 enabled = true 之前请确认:

  1. domains 中的每个主机名必须解析(A/AAAA)到本服务器的公网 IP。
  2. 入站 TCP :443 必须能被 Let’s Encrypt 的验证器访问。如果在 NAT 网关后面,请转发端口。
  3. cache_dir 必须可写且持久 — 丢失它会强制重新签发并消耗速率限制预算。
[server]
bind_address = "0.0.0.0:443"
# tls_cert_file / tls_key_file 已省略 — ACME 提供证书。
[server.acme]
enabled = true
domains = ["hk.example.com"]
contacts = ["mailto:[email protected]"]
cache_dir = "/var/lib/rvpn/acme"

首次启动会有 ~5–15 秒的延迟,期间 ACME 握手完成。续订在到期前约 30 天自动进行;驱动任务以 info 级别记录每个事件(ACME event: …)。

如果 Let’s Encrypt 无法访问(网络故障、LE 临时中断),驱动任务记录错误并重试 — 一次失败的续订不会拆除正在运行的监听器。如果证书停止轮换,请留意日志中反复出现的 ACME error: 行。

续订完全自动,无需操作员介入。

  • 触发 — 驱动任务监控证书的 notAfter,并在到期前约 30 天开始新的订单。
  • 热切换 — 新证书替换内存解析器中的旧证书。现有 TLS 连接保留建立时的证书;新握手拿到新证书。无重启,无连接中断。
  • 持久化 — 新证书在切换前写入 cache_dir,因此在续订窗口中间重启会加载已签发的证书,而不是触发新订单。
  • 无外部定时器 — 没有类似 certbot.timer 的东西。整个状态机都在 rvpn-server 进程内。

要确认续订正常触发(首次签发后约 60 天),请查看 journal:

Terminal window
sudo journalctl -u rvpn-server --since "60d ago" | grep "ACME event"

应该能看到最初的 NewAccountDeployedNewCertCertCacheStore 链,以及续订的第二对 DeployedNewCert / CertCacheStore

ACME 最痛的失败模式不是代码,而是 Let’s Encrypt 的速率限制。此集成涉及三个:

限制触发条件锁定时长
Failed authorizations(失败授权)每主机每小时 5 次失败的 TLS-ALPN-01 挑战。配置错误的防火墙、DNS 或 ALPN 分派器会在几分钟内耗尽额度。从最后一次失败起 60 分钟
Duplicate certificates(重复证书)对同一组主机名每周 5 次成功签发。清空 cache_dir 后反复重新签发会触发。从第 5 次签发起 168 小时
New orders(新订单)每账户每 3 小时 300 个。仅在驱动任务对大量域名进入紧密重试循环时才相关。3 小时

详情见 Let’s Encrypt rate limits

当 rvpn-server 看到 LE 返回 429 时,会把日志从 ERROR 降为 WARN,并打印重试时间,让运维知道何时再看:

WARN ACME rate-limited by Let's Encrypt; next retry allowed after 2026-07-11 06:01:16 UTC

Let’s Encrypt 的 staging 环境签发不被信任的证书,但基本没有速率限制。用它来确认你的 DNS + 防火墙 + rvpn-server 配置无误,然后再切换到生产:

  1. 先设 staging = true:
    [server.acme]
    enabled = true
    staging = true
    domains = ["hk.example.com"]
    contacts = ["mailto:[email protected]"]
    cache_dir = "/var/lib/rvpn/acme-staging"
  2. 重启 rvpn-server,在 journal 中留意 ACME event: DeployedNewCert,随后是 ACME event: CertCacheStore。如果这对事件触发,说明一切正常。
  3. 从外部验证 — 浏览器会抱怨不受信任的 staging 证书,这是预期行为:
    Terminal window
    echo | openssl s_client -connect hk.example.com:443 -servername hk.example.com 2>&1 | grep -Ei "issuer|verify return code"
    Issuer 会是 (STAGING) Let's Encrypt
  4. 切换到生产 — 改为 staging = false,使用全新的 cache_dir(staging 证书在另一个账户和缓存中):
    staging = false
    cache_dir = "/var/lib/rvpn/acme"
  5. 重启,等待,验证。Issuer 现在是真实的 Let's Encrypt

首次在生产模式启动且 cache_dir 为空时,rvpn-server 会打印一条 WARN,正好指向这个流程 — 如果看到这条警告而没有先用 staging 测试过,请先测,以免烧掉速率限制配额。

不要轻易清空 cache_dir 每次删除 + 重新签发都会计入每周 5 次的 duplicate certificates 限制。跨重启保留该目录,并与其他服务器状态一起备份。


[server]
bind_address = "0.0.0.0:443"
tls_cert_file = "/etc/rvpn/certs/cert.pem"
tls_key_file = "/etc/rvpn/certs/key.pem"
identity_key_file = "/etc/rvpn/server_identity.key"
websocket_path = "/api/v1/ws"
http_port = 80
redirect_http_to_https = true
[server.rate_limit]
max_connections_per_ip = 500
max_handshakes_per_minute = 2000
[server.network]
nat_enabled = true
dhcp_range = "10.200.0.0/24"
[server.tun]
enabled = true
tun_ip = "10.200.0.1/24"
mtu = 1420
interface_name = "tun0"
dns_servers = ["1.1.1.1", "8.8.8.8"]

服务器的身份密钥是客户端首次连接时会固定的密钥。轮换时请生成新密钥对, 并发布一个由身份签名的新 prekey bundle,让已固定的客户端能够接受 本次轮换而无需人工干预。

Terminal window
# 让旧身份保留足够长的时间来签署轮换
mv server_identity.key old_identity.key
# 生成新身份
rvpn-server keygen --output server_identity.key
# 发布由旧身份签名的 v2 bundle
rvpn-server prekey-bundle \
--identity server_identity.key \
--output prekey-bundle.json \
--rotate-from old_identity.key \
--from-version 1

--rotate-from--from-version 必须同时提供 —— 服务器不会发布未签名 的轮换。有关客户端如何处理轮换后的 bundle,参见 服务器身份固定


当运行在 Caddy、nginx 或其他终止 TLS 的反向代理之后时,将 bind_address 设为本机端口并让代理处理 TLS。将代理的 WebSocket 转发指向同一路径:

[server]
bind_address = "127.0.0.1:8443"
tls_cert_file = "" # not used — proxy handles TLS
tls_key_file = ""
websocket_path = "/api/v1/ws"

Caddy 示例:

your.domain.com {
handle /api/* {
reverse_proxy 127.0.0.1:8443
}
handle {
root * /var/www/html
file_server
}
}

路径匹配符必须为 /api/*(前缀匹配),而非 /api(仅精确匹配)。