Привет, Хабр! На днях AdGuard выложил в открытый доступ свой VPN-протокол, который назвал TrustTunnel. В статье разберемся, чем он отличается от того же VLESS и как устроен на самом деле.

Проблема любых VPN-протоколов

AdGuard в своем анонсе заявляют, что любой VPN-протокол - это компромисс. Он либо быстрый, но легко детектится (WireGuard, OpenVPN, etc.), либо маскируется под обычный трафик, но тормозит.

Главная проблема тормозов в классическом подходе «устойчивых» протоколов - они обертывают VPN-данные в TCP-соединение и мимикрируют под HTTPS. Выглядит это в итоге как обычный веб-трафик, но TCP добавляет проблем со своим обязательным подтверждением доставки пакетов. При таком подходе, при потере одного пакета встаёт вся очередь пакетов, что приводит к классическому head-of-line blocking, жутко замедляя соединение.

Оповещаем о действиях РКН и новых блокировках в нашем телеграм

Как это работает в VLESS

VLESS (V2Ray/Xray) - популярный выбор для обхода блокировок. Протокол лёгкий, имеет минимумом фич, которые бы его замедляли и умеет маскироваться под HTTPS через TLS.

Но есть у него и нюанс - VLESS работает поверх TCP. Конечно можно использовать WebSocket, gRPC или QUIC как транспорт, но всё же для упрощения возьмем базовую схему - это TCP-обёртка со всеми вытекающими.

Плюс VLESS сам по себе не шифрует, а полагается на внешний TLS. Это гибко, но добавляет слой абстракции, а пользователи не всегда заморачиваются с этим.

Как это хочет решить TrustTunnel?

На официальном сайте компании написано:

"Unlike traditional VPNs that operate on packets, TrustTunnel operates on data streams. This design enables packet buffering — multiple packets can be combined before transmission, dramatically reducing confirmation overhead"

Звучит так, будто они объединяют несколько VPN-пакетов в один блок и так экономят на ACK'ах (подтверждениях доставки). Грубо говоря так и есть, но с небольшими оговорками.

Как реализовали

Смотрим PROTOCOL.md:

Для TCP-соединения каждое туннелируемое TCP-соединение получает отдельный HTTP stream через стандартный метод CONNECT:

CONNECT example.com:443 HTTP/2
:method: CONNECT
:authority: example.com:443
proxy-authorization: Basic <credentials>

После ответа 200 OK протокол начинает общение по HTTP/2 стримингу двунаправленным байтовым потоком. Данные просто текут туда-сюда, не дожидаясь ACKов.

В UDP-трафике все поинтереснее: вся UDP-дата мультиплексируется в один HTTP stream (_udp2). Каждый пакет оборачивается в структуру:

[Length 4B][Src IP 16B][Src Port 2B][Dst IP 16B][Dst Port 2B][App Name Len 1B][App Name][Payload]

Аналогично для ICMP - один stream _icmp для всех пингов.

Таким образом, трафик, проходящий через туннель, смешивается с обычным легитимным, ведь по сути им и является. И, опять же, в отличии от обычных «оберток», по умолчанию оборачивает все в streamы.

А где буферизация-то?

В спецификации протокола нет собственного механизма буферизации. TrustTunnel не оверинжинирит, а использует буферизацию, в которую уже умеют HTTP streamы.

┌─────────────────────────────────────────────────────────┐
│                       TrustTunnel                       │
│          (любая дата, передаваемая через туннель)       │
└────────────────────────┬────────────────────────────────┘
                         │ write()
                         ▼
┌─────────────────────────────────────────────────────────┐
│            HTTP/2 (h2) / HTTP/3 (quinn) стрим           │
│    flow control window в 131072 байт                    │
│    Собирается фрейм с max_frame_size до 16384 байт      │
└────────────────────────┬────────────────────────────────┘
                         ▼
┌─────────────────────────────────────────────────────────┐
│                        TLS / QUIC                       │
│  + Записывают несколько запросов в один                 │
│  + Свои буферы отправки                                 │
└────────────────────────┬────────────────────────────────┘
                         ▼
┌─────────────────────────────────────────────────────────┐
│                        TCP / UDP                        │
└─────────────────────────────────────────────────────────┘

Конфигурационные параметры

В CONFIGURATION.md описаны параметры, влияющие на агрегацию:

Параметр

Значение по умолчанию

Описание

max_frame_size

16384 байт

Максимальный размер HTTP/2 фрейма

recv_udp_payload_size

1350 байт

Размер UDP-датаграммы для QUIC

send_udp_payload_size

1350 байт

Размер исходящих UDP-пакетов

header_table_size

65536 байт

Таблица сжатия HPACK

initial_stream_window_size

131072 байт

Окно flow control

И сколько пакетов влезает в один фрейм?

Окей, возьмем значения по умолчанию.
Раз max_frame_size = 16384 байт, можно посчитать:

HTTP/2 со стандартными пакетами (~1400 байт MTU):

16384 / 1400 ≈ 11 пакетов на фрейм

HTTP/2 с мелкими пакетами (VoIP, ~200 байт):

16384 / 200 ≈ 81 пакет теоретически

(Но на практике VoIP-пакеты не буферизируются так агрессивно из-за требований к latency - отправляются по 1-3 штуки)

QUIC с крупными пакетами:

1350 / 1400 < 1 — нужна фрагментация

QUIC с мелкими пакетами:

1350 / 200 ≈ 6-7 пакетов

Динамический батчинг

Тут важно понимать: такой расчет типа «сколько пакетов отправится за раз» - это то, сколько может быть отправлено их максимально в одном фрейме. На деле же это динамическая величина.

TrustTunnel написан на Rust с асинхронным Tokio. По сути, всю софтварную задержку этим убрали:

  1. Нет блокирующего потока, который «сидит и ждёт» N пакетов

  2. Все данные сразу копируются в буфер библиотеки h2/quinn

  3. Сама библиотека решает, когда делать flush и отправлять данные в фрейме

  4. При медленной же сети TCP-буфер заполняется, а пакеты накапливаются в RAM. Когда сеть освобождается, TrustTunnel может отправить сразу серию фреймов

В конфиге max_frame_size (что очевидно из названия, такто!) задаёт верхний предел одного фрейма данных, а не фиксированный размер батча.

Выводы и сравнения

Просуммируем весь кайф протокола, который уменьшает его задержку еще раз:

  1. Мультиплексирует много TCP-соединений в одну TLS-сессию. Не надо делать отдельный TLS handshake на каждое соединение.

  2. Поддержка QUIC (HTTP/3) при использовании QUIC получаем отсутствие head-of-line blocking. Потеря пакета в одном stream не блокирует другие.

  3. То, за что изначально боролась компания - энергоэффективность на мобильных устройствах - агрегация в HTTP стримы позволяет LTE-модему передавать данные одним разом и быстрее уйти в режим пониженного энергопотребления, вместо того чтобы посылать по одному пакету и не давать продыху.

  4. И при всем этом он идеально маскируется под HTTPS: ведь буквально весь трафик выглядит как обычные CONNECT-запросы. DPI видит стандартный HTTP/2 over TLS.

И всё это теперь открыто под лицензией Apache 2.0 - можно форкать и использовать. Красота.

Сравнение с VLESS

VLESS + TCP/TLS

TrustTunnel

Маскировка

Похож на HTTPS

Стандартный HTTP/2 CONNECT

Транспорт

TCP (+ WS/gRPC/QUIC)

HTTP/2 или QUIC

TCP-туннели

Своя реализация

HTTP CONNECT per connection

UDP

Своя реализация (XUDP)

Мультиплекс через один stream

Head-of-line blocking

Есть (на TCP)

Нет (при QUIC)

Шифрование

Внешнее TLS

TLS 1.2+ обязателен

Макс. размер батча

Зависит от транспорта

16384 байт (HTTP/2) / 1350 байт (QUIC)

Буферизация

Своя или транспорта

Полностью на уровне HTTP/2/QUIC

Таймауты по умолчанию

  • Connection timeout: 30 сек

  • Health check: 7 сек

  • TCP idle: 2 часа

  • UDP idle: 120 сек

TrustTunnel - это HTTP/2 CONNECT прокси с поддержкой UDP/ICMP через мультиплексированные streams. Главное преимущество - не какая-то революционная архитектура, а то что протокол использует стандартные HTTP-методы (CONNECT), что делает трафик максимально похожим на обычное проксирование, а сам трафик идет через стримы.

Полезные ссылки: Спецификация протокола, Конфигурация, TrustTunnel на GitHub, Сайт проекта, Обсуждение на HN

Оповещаем о действиях РКН и новых блокировках в нашем телеграм