---
tags: [network, security]
verified: 2026-09-02
---

# OpenVPN против WireGuard — выбор под раздельную маршрутизацию

## Обзор

Сравнение двух VPN-протоколов почти всегда начинается и заканчивается производительностью: WireGuard живёт в ядре, не копирует данные в пространство пользователя, укладывается в четыре тысячи строк кода — и на синтетическом тесте одного соединения уверенно обходит OpenVPN. Отсюда расхожий вывод «WireGuard всегда лучше».

Вывод верен ровно до тех пор, пока задача формулируется как «прогнать максимум байт через туннель». Если задача другая — держать VPN включённым постоянно, но выпускать часть трафика мимо него, и список исключений время от времени меняется, — критерием становится не пропускная способность, а модель управления маршрутами и конфигурацией. По этому критерию протоколы устроены принципиально по-разному, и разница не в пользу WireGuard.

Заметка разбирает именно этот срез: как каждый протокол выражает исключение из туннеля, как доставляет конфигурацию на клиентов и во что это обходится при сопровождении. Общий контекст маршрутизации и firewall — в [заметке о сети Linux](./linux-networking.md).

## Сценарий

Постоянно включённый VPN, полный туннель по умолчанию, но набор конкретных сервисов должен ходить напрямую через провайдера. Причины бывают разные: сервис блокирует иностранные адреса, банк не пускает сессию из другой страны, платёжный шлюз завязан на геолокацию. Важно не то, какие именно это сервисы, а два свойства сценария:

- список исключений не статичен и пополняется по мере надобности;
- клиентов несколько, и среди них мобильные, в том числе iOS.

Оба свойства и определяют исход сравнения.

## Производительность: аргумента нет

Главный довод в пользу WireGuard в этом сценарии не работает, и это стоит показать числами, а не рассуждением.

Порядок величин на типичном одноядерном VPS (AMD EPYC, около гигабайта памяти, OpenVPN 2.6 с `AES-128-GCM`, `auth SHA256` и `tls-crypt`), обслуживающем пять клиентов:

```text
Аптайм                        ~50 суток
CPU, потреблённый демоном     ~17 ч        →  ~1.4 % одного ядра в среднем
Трафик через tun              ~1.3 ТБ суммарно
Резидентная память процесса   ~4 МБ
```

Терабайт трафика обошёлся в полтора процента ядра. Причина — аппаратное ускорение AES-NI: с `AES-GCM` шифрование почти бесплатно, узким местом остаётся сеть, а не процессор. WireGuard дал бы выше пиковую скорость одного соединения, но выигрывать нечего: процессор простаивает в обоих случаях.

Числа привязаны к конкретному железу и профилю нагрузки, и на другом сервере будут другими. Существенно не само значение, а вывод: при аппаратном AES и нагрузке уровня домашнего или небольшого офисного шлюза производительность из списка критериев выпадает. Она вернётся туда на гигабитных скоростях, на процессорах без AES-NI (там разрыв в пользу ChaCha20 у WireGuard становится большим) или при сотнях одновременных клиентов.

Отдельно стоит держать в голове, что довод «WireGuard в ядре, а OpenVPN нет» перестал быть точным. Модуль data channel offload — `ovpn` — вошёл в основное ядро Linux начиная с 6.16, а OpenVPN 2.7 умеет работать только с его новым API: демон сам определяет наличие модуля и включает offload, выключается тот опцией `--disable-dco`. Шифрование и пересылка данных при этом уходят в ядро, пользовательский процесс остаётся плоскостью управления; платой служат ограничения offload — только `dev tun`, только AEAD-шифры (AES-GCM и ChaCha20-Poly1305), без компрессии, на сервере только `topology subnet`.

Где разница реальна и на малой нагрузке — мобильные клиенты. WireGuard экономнее по батарее, поскольку не держит собственных таймеров поверх простаивающего туннеля, и переживает смену сети (Wi-Fi ↔ LTE) мгновенно: сессия привязана к ключу пира, а не к паре адресов. OpenVPN при смене адреса проходит рукопожатие заново, с задержкой в единицы секунд.

## Как выражается исключение из туннеля

Здесь модели расходятся принципиально, и расходятся на уровне протокола, а не реализации.

### OpenVPN: исключение как отдельный маршрут

В OpenVPN маршруты — самостоятельные объекты, и среди целевых шлюзов есть псевдоимя `net_gateway` — «шлюз по умолчанию, существовавший до подключения VPN, прочитанный из таблицы маршрутизации». Исключение задаётся одной строкой:

```conf
push "route 203.0.113.0 255.255.255.0 net_gateway"
```

Отсюда два следствия.

Исключение аддитивно. Добавить сеть — дописать строку; общая директива `redirect-gateway def1` не трогается, остальные исключения не пересчитываются. Маршрут с более длинной маской выигрывает у дефолтного, и этого достаточно.

Исключение раздаётся с сервера. Директива `push` доставляет маршруты клиенту в момент подключения, по управляющему каналу. Клиентские `.ovpn` при этом не меняются никогда — в них нет ни одного из исключений. Реальный ответ сервера выглядит так:

```text
PUSH_REPLY
  redirect-gateway def1 bypass-dhcp
  route 203.0.113.0 255.255.255.0 net_gateway
  route 192.0.2.0 255.255.255.0 net_gateway
  route-gateway 10.8.0.1
```

Правка конфига на сервере и перезапуск сервиса — и все устройства получают новый список сами при переподключении.

### WireGuard: исключение как дополнение множества

В WireGuard отдельных маршрутов нет вовсе. Маршрутизацию задаёт единственный параметр `AllowedIPs` в секции `[Peer]`, и он перечисляет, что идёт в туннель: полный туннель — это `AllowedIPs = 0.0.0.0/0`. Параметр служит одновременно и криптографической привязкой (какие адреса разрешено принимать от пира), и таблицей маршрутов, которую `wg-quick` разворачивает в системную.

Синтаксиса исключения не существует — ни `DisallowedIPs`, ни отрицания префикса. Чтобы вывести сеть из туннеля, приходится выписывать дополнение множества: весь адресный диапазон, разбитый на блоки так, чтобы исключаемая сеть в них не попала.

Объём работы считается точно: дополнение префикса длины N внутри `0.0.0.0/0` даёт ровно N блоков CIDR. Одна исключённая `/22` — это 22 строки, `/24` — 24 строки. Три сети средних размеров дают порядка семидесяти строк, и при каждом изменении списка весь набор пересчитывается целиком: блоки соседних исключений перекрываются, дописать одну строку нельзя. Ровно для этого существует отдельный жанр онлайн-калькуляторов `AllowedIPs`.

Управляющего канала для конфигурации нет. WireGuard принципиально не передаёт настройки от сервера клиенту: в протоколе нет ни этапа, ни сообщения для этого — отсюда и отсутствие серверной логики как таковой, пир симметричен пиру. Конфиг правится на каждом устройстве отдельно, вручную, при каждом изменении списка.

### Сопоставление

| | OpenVPN | WireGuard |
|---|---|---|
| Запись исключения | `route <сеть> net_gateway` | дополнение множества, N блоков на префикс `/N` |
| Добавить одну сеть | одна строка на сервере | пересчёт всего набора |
| Доставка на клиенты | `push` при подключении | вручную на каждом устройстве |
| Правка на пяти устройствах | не требуется | обязательна при каждом изменении |

Добавление одного сервиса — это либо строка в серверном конфиге, либо пересчёт семидесяти строк и разнос результата по всем клиентам.

### Обходной путь для WireGuard и его граница

На десктопах `wg-quick` умеет выполнять произвольные команды на подъёме интерфейса, и логика становится похожей на OpenVPN:

```ini
PostUp = ip route add 198.51.100.0/24 via <исходный шлюз>
```

Маршрут добавляется мимо `AllowedIPs`, дополнение множества считать не нужно. Ограничений два. Во-первых, исходный шлюз надо откуда-то взять — на статической сети он известен, на ноутбуке, кочующем между Wi-Fi, его придётся вычислять в том же скрипте. Во-вторых, и это решающее: мобильные приложения WireGuard скрипты не выполняют, там доступен только `AllowedIPs`. Для парка со смартфонами путь закрыт.

### Где выигрывает WireGuard

Приложение WireGuard под Android умеет разделение по программам: в настройках туннеля задаётся список приложений, которые в туннель включаются или, наоборот, из него исключаются. Для формулировки «банковское приложение мимо VPN, остальное через» это радикально лучше любых маршрутов — критерий не зависит от IP-адресов вообще, а значит, не ломается при смене адресов сервиса и не требует сопровождения списка сетей.

Механизм опирается на Android VpnService (`addAllowedApplication` / `addDisallowedApplication`), то есть на платформу, а не на протокол. На iOS эквивалента в приложении нет: per-app VPN там существует, но управляется MDM-профилем, а не пользователем. Если в парке есть iPhone, разделение по приложениям как общее решение не выстраивается.

## Ограничения, общие для обоих

Маршрутизация работает по IP, а не по доменам. Строки «пустить `example.ru` напрямую» не существует ни в одном из решений: и `route`, и `AllowedIPs` оперируют префиксами. Задача всегда сводится к списку сетей, а список сетей приходится получать из DNS и данных о принадлежности адресов.

Сервисы за общими CDN не отделяются. Если сервис живёт на Cloudflare, Akamai или Fastly, его адреса общие с половиной интернета, и вывести их из туннеля означает вывести заодно всё остальное. Чисто отделяются только сервисы с собственной автономной системой.

Крупные блоки требуют сужения. Домен вполне может резолвиться в адрес внутри `/18` магистрального провайдера — это 16 тысяч адресов, и выпуск такого блока целиком выводит из туннеля массу постороннего. Проверять принадлежность префикса стоит всегда, а в спорных случаях спускаться до `/32`.

DNS решается отдельно. Если клиентам раздаётся зарубежный резолвер, запросы уходят через туннель и выглядят как из страны сервера. Для сервисов, отдающих один и тот же адрес всем, это безразлично; для сервисов с геозависимым DNS ответ может не совпасть со списком исключений, и трафик пойдёт не туда, куда рассчитывали. Исключения на IP и политика DNS должны согласовываться.

Масштаб ограничен у обоих, но по-разному. У OpenVPN опция `--max-routes` (исторический лимит в сотню маршрутов) удалена в версии 2.4 вместе с самим лимитом, но остался размер бандла пушнутых опций — `PUSH_BUNDLE_SIZE` в 1024 байта при буфере управляющего канала `TLS_CHANNEL_BUF_SIZE` в 2048. Длинный ответ режется на части механизмом `push-continuation`, так что десятки маршрутов проходят нормально, а вот списки в тысячи префиксов через `push` не раздают: их кладут в клиентские конфиги, со всеми последствиями по рассылке и обновлению. У WireGuard тысячи префиксов в `AllowedIPs` технически помещаются, но ровно так же требуют доставки на каждое устройство.

## Устойчивость к DPI

Вопрос не связан с маршрутизацией напрямую, но на выбор влияет, а расхожее мнение здесь снова неверно.

WireGuard считается «незаметнее» из-за минимализма, тогда как на деле у него фиксированная сигнатура: первый байт рукопожатия — тип сообщения из короткого набора, сами сообщения имеют постоянную длину, а размеры пакетов в сессии укладываются в узкий повторяющийся профиль. Это удобная мишень для DPI. OpenVPN с `tls-crypt` в этом смысле не проще: управляющий канал зашифрован целиком, и статических полей в открытом виде там нет — хотя и его давно научились опознавать по поведению.

Практический вывод: если задача — не раздельная маршрутизация, а обход блокировок, ни один из двух протоколов её не решает. Работают производные и посторонние инструменты — AmneziaWG (форк WireGuard, добавляющий подмену полей заголовка, мусорные пакеты перед рукопожатием и имитацию посторонних UDP-протоколов) или VLESS+Reality через Xray, воспроизводящий рукопожатие настоящего TLS-сайта. В AmneziaWG версии 2.0 имитация оформлена в отдельную систему Custom Protocol Signature: сигнатурные пакеты, уходящие перед рукопожатием, описываются пользователем прямо в конфиге, а не выбираются из зашитого набора.

Отдельно стоит запомнить, почему наивная маскировка не работает: SNI сверяется с владельцем адреса. Прокси, заявляющий в рукопожатии имя крупного облачного сервиса, но живущий на адресе хостинг-провайдера, для современного DPI выглядит явной аномалией — сверка дешёвая и делается на лету. Если маскировка нужна, домен прикрытия должен честно резолвиться в адрес сервера, а Reality решает ту же проблему иначе: перенаправляет непрошеных клиентов на настоящий сайт, чей хендшейк имитирует.

## Вывод

Для постоянно включённого VPN с растущим списком точечных исключений OpenVPN подходит существенно лучше, и решает это не производительность, а модель управления: аддитивные исключения плюс раздача конфигурации с сервера. Разница масштабируется с числом клиентов и частотой правок — на одном десктопе её почти нет, на пяти устройствах и правке раз в месяц она становится определяющей.

WireGuard оправдан, когда:

- клиентов мало и это десктопы, где доступен `PostUp`;
- список исключений статичен и меняется раз в год;
- критична экономия батареи и бесшовный роуминг на мобильных;
- парк на Android и подходит разделение по приложениям вместо маршрутов;
- нужна пиковая скорость на процессорах без AES-NI.

Во всех остальных случаях выигрыш WireGuard не покрывает стоимости ручного сопровождения конфигов.

## Практика: добавление сервиса в обход туннеля

Определить сеть по адресу сервиса и убедиться, что префикс принадлежит самому сервису, а не транзитному провайдеру:

```bash
getent ahostsv4 example.ru | awk '{print $1}' | sort -u

curl -s "https://stat.ripe.net/data/prefix-overview/data.json?resource=<адрес>" \
  | grep -oP '"(resource|holder)":\s*"\K[^"]+'
```

Первое поле ответа — агрегированный префикс, второе — владелец. Собственная автономная система сервиса означает, что префикс можно выпускать целиком; имя магистрального оператора — что нужно сужать до конкретных адресов.

Добавить маршрут в серверный конфиг и перезапустить демон:

```conf
push "route 203.0.113.0 255.255.255.0 net_gateway"
```

```bash
systemctl restart openvpn-server@server
```

Убедиться, что маршрут доехал до клиента:

```bash
journalctl -u openvpn-server@server --since "-5min" -o cat \
  | grep -oP "PUSH_REPLY[^']*" | tail -1 | tr ',' '\n'
```

Перезапуск разрывает клиентские сессии на несколько секунд, после чего клиенты переподключаются сами и получают обновлённый список. Имя юнита зависит от упаковки: `openvpn-server@` в современных systemd-дистрибутивах, `openvpn@` в старых.

Начиная с 2.7 разрыв не обязателен: сервер умеет доставить новые опции в уже открытую сессию сообщением `PUSH_UPDATE`, которое отправляется через управляющий интерфейс командами `push-update-broad` (всем клиентам) и `push-update-cid` (одному, по client id). Среди обновляемых опций есть `route` и `redirect-gateway`, но обновление не инкрементально: клиент удаляет прежние значения опции и ставит присланные целиком, а префикс `-` перед именем опцию снимает. Оговорка существенная — при активном DCO механизм отключён с обеих сторон, так что offload и правка маршрутов без переподключения пока взаимоисключающи.

## Источники

- [OpenVPN man page: network options](https://github.com/OpenVPN/openvpn/blob/master/doc/man-sections/vpn-network-options.rst) — `route`, `redirect-gateway` и псевдоним `net_gateway`
- [OpenVPN man page: unsupported options](https://github.com/OpenVPN/openvpn/blob/master/doc/man-sections/unsupported-options.rst) — `--max-routes` удалена в 2.4 вместе с лимитом
- [OpenVPN: `src/openvpn/common.h`](https://github.com/OpenVPN/openvpn/blob/master/src/openvpn/common.h) — `PUSH_BUNDLE_SIZE` и `TLS_CHANNEL_BUF_SIZE`
- [OpenVPN ChangeLog](https://github.com/OpenVPN/openvpn/blob/release/2.7/ChangeLog) — состояние текущей стабильной ветки 2.7
- [OpenVPN: `Changes.rst`](https://github.com/OpenVPN/openvpn/blob/master/Changes.rst) — обзор нововведений 2.7, включая `PUSH_UPDATE`
- [OpenVPN: `README.dco.md`](https://github.com/OpenVPN/openvpn/blob/master/README.dco.md) — модуль `ovpn` в ядре 6.16 и ограничения data channel offload
- [OpenVPN: `doc/management-notes.txt`](https://github.com/OpenVPN/openvpn/blob/master/doc/management-notes.txt) — команды `push-update-broad` и `push-update-cid`
- [WireGuard: Cryptokey Routing](https://www.wireguard.com/#cryptokey-routing) — `AllowedIPs` как таблица маршрутов и криптографическая привязка
- [WireGuard: Protocol & Cryptography](https://www.wireguard.com/protocol/) — формат и фиксированные размеры сообщений рукопожатия
- [wg-quick(8)](https://man7.org/linux/man-pages/man8/wg-quick.8.html) — `PostUp`, `PostDown` и разворачивание маршрутов
- [Pro Custodibus: WireGuard AllowedIPs Calculator](https://www.procustodibus.com/blog/2021/03/wireguard-allowedips-calculator/) — расчёт дополнения множества
- [Android: VpnService.Builder](https://developer.android.com/reference/android/net/VpnService.Builder) — `addAllowedApplication` и `addDisallowedApplication`
- [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) — обфускация поверх WireGuard
- [Xray: VLESS + REALITY](https://xtls.github.io/en/config/inbounds/vless.html) — маскировка под настоящий TLS-сайт
- [RIPEstat: Prefix Overview](https://stat.ripe.net/docs/02.data-api/prefix-overview.html) — API определения префикса и владельца

---

## Связанные заметки

- [Linux — сеть: стек, конфигурация, firewall](./linux-networking.md) — таблица маршрутизации, метрики и nftables вокруг туннеля
- [Обратный прокси и TLS: nginx vs Caddy](./reverse-proxy-tls.md) — вторая половина публикации сервисов: TLS, SNI и терминация
- [Linux — безопасность: hardening, SELinux/AppArmor](./linux-security-hardening.md) — VPN-демон в общей модели защиты хоста
- [Самохостинг за NAT: VPS как точка входа](./selfhosting-behind-nat.md) — WireGuard в роли постоянного канала до входной точки
- [AEAD: аутентифицированное шифрование, nonce и лимиты](./crypto-aead.md) — почему DCO принимает только AEAD-шифры и чем AES-GCM отличается от ChaCha20-Poly1305 по цене ошибки
- [ChaCha20 и Poly1305: устройство связки и почему она такая](./crypto-chacha20-poly1305.md) — единственный шифр WireGuard изнутри и откуда взялся его разрыв с AES на процессорах без аппаратного ускорения
- [Обмен ключами: X25519, KEM, вывод ключей и привязка к транскрипту](./crypto-key-exchange.md) — рукопожатие Noise в WireGuard против PKI управляющего канала OpenVPN
- [AES-GCM против ChaCha20-Poly1305: как выбирать](./crypto-aes-vs-chacha20.md) — откуда берётся разрыв между шифрами на шлюзе с AES-NI и без него, с измерением
