Справочник конфигурации сервера
Полный справочник всех полей в server.toml.
Поля верхнего уровня
Заголовок раздела «Поля верхнего уровня»Все поля располагаются под разделом [server] или на корневом уровне (принимаются оба варианта).
| Поле | Тип | По умолчанию | Описание |
|---|---|---|---|
bind_address | string | "0.0.0.0:443" | Адрес и порт для прослушивания. Используйте "0.0.0.0:443" для всех интерфейсов или "127.0.0.1:8443" только для localhost (за обратным прокси). |
tls_cert_file | path | "certs/cert.pem" | Путь к TLS-сертификату (формат PEM). |
tls_key_file | path | "certs/key.pem" | Путь к приватному ключу TLS (формат PEM). |
identity_key_file | path | "server_identity.key" | Путь к ключу идентификации X3DH сервера. Генерируется командой rvpn-server keygen. |
websocket_path | string | "/api/v1/ws" | Путь WebSocket-точки. Меняйте его для каждого развёртывания, чтобы затруднить обнаружение. |
http_port | integer | (отключён) | Если задан, также слушает на этом порту обычный HTTP (например, 80) для ACME-испытаний и редиректов HTTP→HTTPS. |
redirect_http_to_https | bool | true | Когда задан http_port, перенаправляет все HTTP-запросы на HTTPS. Не-WebSocket запросы получают жёстко зашитую страницу 404 в стиле nginx, чтобы сливаться с обычным веб-сервером. |
prekey_bundle_file | path | (нет) | Если задан, загружает ключи X3DH из этого файла вместо генерации новых. Используйте для восстановления сохранённой идентичности. |
[server.rate_limit]
Заголовок раздела «[server.rate_limit]»Управляет тем, сколько соединений может установить один IP-адрес.
| Поле | Тип | По умолчанию | Описание |
|---|---|---|---|
max_connections_per_ip | integer | 500 | Максимальное количество одновременных соединений с одного IP-адреса. |
max_handshakes_per_minute | integer | 2000 | Максимальное количество новых попыток рукопожатия с одного IP в минуту. Помогает предотвратить брутфорс-зондирование. |
[server.rate_limit]max_connections_per_ip = 10max_handshakes_per_minute = 20[server.network]
Заголовок раздела «[server.network]»Предпроверочные подсказки, используемые только для стартовых предупреждений — сервер не программирует NAT и не раздаёт IP из этого раздела. Фактические клиентские IP берутся из [server.tun].tun_ip; NAT настраивается вашим оператором (см. примеры iptables/pf в Установке сервера).
| Поле | Тип | По умолчанию | Описание |
|---|---|---|---|
nat_enabled | bool | true | Установите false, чтобы подавить стартовое предупреждение «клиенты могут не достать интернет», если ваш egress обрабатывается в другом месте. |
dhcp_range | string | "10.200.0.0/24" | Подсеть, для которой сервер ожидает наличие правил NAT MASQUERADE. При запуске сервер грепает iptables-save в поисках правила для этой подсети и предупреждает, если не найдено. |
[server.network]nat_enabled = truedhcp_range = "10.200.0.0/24"[server.tun]
Заголовок раздела «[server.tun]»Настройки для настоящего режима TUN-to-TUN. Когда включён, сервер создаёт TUN-интерфейс и направляет пакеты клиентов через него.
| Поле | Тип | По умолчанию | Описание |
|---|---|---|---|
enabled | bool | false | Включить TUN-режим. Когда false, сервер использует relay-режим (в стиле Brook). |
tun_ip | string | "10.200.0.1/24" | IP-адрес TUN-интерфейса и префикс подсети. Клиентские IP выдаются из этой подсети. |
mtu | integer | 1420 | MTU для TUN-интерфейса. |
interface_name | string | "tun0" | Имя создаваемого TUN-интерфейса. |
dns_servers | list | ["8.8.8.8"] | DNS-резолверы, передаваемые TUN-клиентам через туннель. |
[server.tun]enabled = truetun_ip = "10.200.0.1/24"mtu = 1420interface_name = "tun0"[server.acme]
Заголовок раздела «[server.acme]»Автоматические TLS-сертификаты через Let’s Encrypt — обратный прокси не нужен. Сервер сам получает и обновляет сертификаты через TLS-ALPN-01, поэтому тот же слушатель :443 обслуживает и живой трафик, и ACME-обмен. Порт :80 не нужен.
| Поле | Тип | По умолчанию | Описание |
|---|---|---|---|
enabled | bool | false | Включить выдачу через ACME. Когда true, файл tls_cert_file не должен существовать — сервер отказывается угадывать, какой сертификат отдавать. |
domains | list of strings | [] | Полные доменные имена, для которых получать сертификат. При включении обязателен хотя бы один. Первый — основной CN; остальные становятся SAN. |
contacts | list of strings | [] | Контактные URI для ACME-аккаунта, обычно ["mailto:[email protected]"]. Let’s Encrypt по ним шлёт предупреждения об истечении, если продление сломается. |
cache_dir | path | "acme-cache" | Каталог для хранения ключа ACME-аккаунта и выпущенных сертификатов. Должен быть записываемым. Сохраняйте между рестартами, чтобы не упереться в rate limit. |
staging | bool | false | Использовать staging-директорию Let’s Encrypt. Сертификаты не будут доверенными в браузерах, но выдача не бьёт по production rate limit’ам. Используйте при отладке DNS и firewall. |
Предусловия — проверьте, прежде чем ставить enabled = true:
- Каждое имя из
domainsдолжно резолвиться (A/AAAA) на публичный IP сервера. - Входящий TCP
:443должен быть доступен валидаторам Let’s Encrypt. За NAT — пробросьте порт. cache_dirдолжен быть записываемым и постоянным — потеря заставляет пере-выпускать сертификат и сжирает rate-limit-бюджет.
[server]bind_address = "0.0.0.0:443"# tls_cert_file / tls_key_file опущены — сертификат даёт ACME.
[server.acme]enabled = truedomains = ["hk.example.com"]cache_dir = "/var/lib/rvpn/acme"Первая загрузка задерживается на ~5–15 секунд, пока идёт ACME-обмен. Продление происходит автоматически примерно за 30 дней до истечения; фоновая задача пишет каждое событие на уровне info (ACME event: …).
Если Let’s Encrypt недоступен (сетевой сбой, кратковременная авария LE), фоновая задача логирует ошибку и повторяет — упавшее продление не убивает работающий слушатель. Следите за повторяющимися строками ACME error:, если сертификаты перестанут ротироваться.
Продления
Заголовок раздела «Продления»Продления полностью автоматические, вмешательство оператора не требуется.
- Триггер — фоновая задача следит за
notAfterсертификата и запускает новый заказ примерно за 30 дней до истечения. - Горячая замена — новый сертификат заменяет старый в резолвере в памяти. Существующие TLS-соединения продолжают жить с тем сертификатом, с которым были установлены; новые handshake’и подхватывают свежий. Никакого рестарта, никаких обрывов.
- Персистентность — свежий сертификат записывается в
cache_dirдо замены, поэтому рестарт посреди окна подгружает уже выпущенный сертификат, а не запускает новый заказ. - Никаких внешних таймеров — ничего вроде
certbot.timerнет. Вся машина состояний живёт внутри процесса rvpn-server.
Чтобы убедиться, что продление отработало (примерно через 60 дней после первой выдачи), поглядите журнал:
sudo journalctl -u rvpn-server --since "60d ago" | grep "ACME event"Должны быть первоначальная цепочка NewAccount → DeployedNewCert → CertCacheStore и ещё пара DeployedNewCert / CertCacheStore от продления.
Rate limits Let’s Encrypt
Заголовок раздела «Rate limits Let’s Encrypt»Самая болезненная точка отказа в ACME — не код, а rate limits Let’s Encrypt. Для этой интеграции важны три:
| Лимит | Что провоцирует | Как долго заблокирован |
|---|---|---|
| Failed authorizations | 5 упавших TLS-ALPN-01 challenge’ей на хост в час. Криво настроенный firewall, DNS или диспетчер ALPN сожжёт лимит за минуты. | 60 минут с последнего фейла |
| Duplicate certificates | 5 успешных выдач для того же набора хостов в неделю. Триггерится очисткой cache_dir и повторной выдачей. | 168 часов с пятой выдачи |
| New orders | 300 на аккаунт за 3 часа. Актуально только если фоновая задача уходит в жёсткий retry-цикл по многим доменам. | 3 часа |
Подробности — Let’s Encrypt rate limits.
Когда rvpn-server видит 429 от LE, он понижает лог с ERROR до WARN и печатает время следующей попытки — оператору сразу понятно, когда возвращаться:
WARN ACME rate-limited by Let's Encrypt; next retry allowed after 2026-07-11 06:01:16 UTCСначала staging (рекомендуемый workflow для новых установок)
Заголовок раздела «Сначала staging (рекомендуемый workflow для новых установок)»Staging-окружение Let’s Encrypt выдаёт недоверенные сертификаты, но по факту без rate limits. Используйте его, чтобы убедиться, что DNS + firewall + конфиг rvpn-server правильные, прежде чем переключаться на production:
- Начните со
staging = true:[server.acme]enabled = truestaging = truedomains = ["hk.example.com"]cache_dir = "/var/lib/rvpn/acme-staging" - Перезапустите rvpn-server, ищите в журнале
ACME event: DeployedNewCert, а затемACME event: CertCacheStore. Если эта пара срабатывает — всё работает. - Проверьте снаружи — браузер будет жаловаться на недоверенный staging-сертификат, это ожидаемо:
Issuer будет
Окно терминала echo | openssl s_client -connect hk.example.com:443 -servername hk.example.com 2>&1 | grep -Ei "issuer|verify return code"(STAGING) Let's Encrypt. - Переключитесь на production — измените
staging = falseи возьмите свежийcache_dir(staging-сертификаты живут в другом аккаунте и кэше):staging = falsecache_dir = "/var/lib/rvpn/acme" - Перезапустите, подождите, проверьте. Issuer теперь настоящий
Let's Encrypt.
На первом production-старте с пустым cache_dir rvpn-server печатает WARN, указывающий ровно на этот workflow — если увидите это предупреждение и не тестировали через staging, попробуйте сначала его, чтобы не сжечь rate-limit-бюджет.
Не чистите 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 = 80redirect_http_to_https = true
[server.rate_limit]max_connections_per_ip = 500max_handshakes_per_minute = 2000
[server.network]nat_enabled = truedhcp_range = "10.200.0.0/24"
[server.tun]enabled = truetun_ip = "10.200.0.1/24"mtu = 1420interface_name = "tun0"dns_servers = ["1.1.1.1", "8.8.8.8"]Ротация ключа идентификации
Заголовок раздела «Ротация ключа идентификации»Ключ идентификации сервера — то, что клиенты закрепляют при первом подключении. Чтобы ротовать его без ручного вмешательства уже закреплённых клиентов, сгенерируйте новую пару и опубликуйте новый prekey bundle, подписанный предыдущим ключом.
# Держим старый ключ достаточно долго, чтобы подписать ротацию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 в localhost-порт и позвольте прокси обрабатывать TLS. Направьте WebSocket-переброс прокси на тот же путь:
[server]bind_address = "127.0.0.1:8443"tls_cert_file = "" # not used — proxy handles TLStls_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(только точное совпадение).