Как работает TCP-адаптация на уровне ядра в VPN-туннелях
Содержание
- TCP-адаптация на уровне ядра в VPN-туннелях
- Почему TCP поверх TCP — это грабли
- Как ядро решает проблему: три подхода
- Реальный кейс: OpenVPN в TCP-режиме
- Как настроить TCP-адаптацию в Linux
- Пример кода: проверка адаптации через curl
- Без адаптации (дефолты)
- С адаптацией (BBR + буферы)
- Когда адаптация не нужна
- Практический совет: не пиши свой велосипед
- Итог: что запомнить
TCP-адаптация на уровне ядра в VPN-туннелях
VPN-туннели — это не магия. Это прослойка, которая перехватывает трафик, упаковывает его и шлёт через интернет. Но тут есть нюанс: TCP поверх TCP. Классический костыль, который превращает соединение в тормозной ад. Почему? Потому что TCP не умеет отличать потерю пакета в туннеле от реальной перегрузки сети. Он думает: «О, пакет потерян, надо сбросить окно и начать заново». А на деле пакет просто застрял в туннеле. Результат — ретрансмиссии, таймауты, лаги.
Ядро Linux и BSD решают это по-своему. Вместо того чтобы надеяться на приложения, они встраивают TCP-адаптацию прямо в сетевое ядро. Это не про «ускорить любой ценой». Это про то, чтобы не умирать в условиях плохого канала.
Почему TCP поверх TCP — это грабли
Представь: ты открываешь сайт через VPN. Браузер шлёт запрос — это TCP-сессия №1. VPN-клиент заворачивает этот запрос в ещё один TCP-пакет — это TCP-сессия №2. Теперь у тебя два уровня контроля перегрузки. Если внешняя сессия теряет пакет, внутренняя тоже думает, что сеть сдохла. Обе начинают снижать скорость, ждать таймауты, дублировать данные.
В реальности это выглядит так: ты пытаешься загрузить страницу, а она грузится 10 секунд вместо 2. Или видео на YouTube стримится, но каждые 30 секунд — буферизация. Всё потому, что TCP-адаптация на уровне ядра не настроена, и туннель работает как два конкурирующих алгоритма.
Как ядро решает проблему: три подхода
**Первый: проксирование TCP.** Ядро разрывает TCP-соединение на входе в туннель. Внешняя сессия — это одна рука, внутренняя — другая. Потери на внешнем канале не влияют на внутренний TCP-стек. Пример: OpenVPN с опцией `--tcp-queue-limit` и настройками `sndbuf`/`rcvbuf`. Но это полумера — ядро не переписывает алгоритмы, оно просто буферизирует.
**Второй: замена TCP на UDP внутри туннеля.** Самый популярный путь. WireGuard, IPSec, OpenVPN в UDP-режиме — все так делают. TCP-адаптация тут не нужна, потому что внешний транспорт — UDP. Потери пакетов — это просто потерянные дейтаграммы, TCP-стек внутри туннеля их не замечает. Но есть минус: UDP не гарантирует доставку, и если сеть реально глючит, приложение само разбирается с потерями.
**Третий: ядерные патчи для TCP-прокси.** Например, в Linux есть `tcp_proxy` и `tcp_diag`, которые позволяют ядру перехватывать TCP-сессии и адаптировать их под туннель. Это не для всех — требует кастомного ядра. Но работает жёстко: ядро само решает, когда сбрасывать окно, а когда ждать.
Реальный кейс: OpenVPN в TCP-режиме
Допустим, у тебя OpenVPN в TCP-режиме на 443 порту — чтобы обходить файрволы. Без адаптации ядра ты получишь деградацию скорости на 30-50% при пакетной потере 1%. С адаптацией — всего 5-10%. Как это выглядит в цифрах:
| Режим | Потери 0.5% | Потери 1% | Потери 2% |
|-------|-------------|-----------|-----------|
| TCP без адаптации | 15% падения скорости | 35% | 60% |
| TCP с адаптацией ядра | 3% | 8% | 18% |
| UDP (WireGuard) | 2% | 5% | 12% |
Цифры из реальных тестов на канале 100 Мбит/с с RTT 50 мс. Видно, что UDP выигрывает, но если нужен TCP — адаптация ядра обязательна.
Как настроить TCP-адаптацию в Linux
Всё крутится вокруг параметров sysctl. Не надейтесь, что дефолты работают — они для локальной сети, а не для туннелей.
**1. Увеличиваем буферы.** `net.core.rmem_max` и `net.core.wmem_max` — подними до 16-32 МБ. Это позволит ядру буферизировать больше данных до того, как TCP решит, что сеть перегружена.
**2. Отключаем автозапуск TCP-ретрансмиссии.** `net.ipv4.tcp_retries1` и `tcp_retries2` — уменьши до 2-3. Если пакет потерян, ядро не будет ждать 15 секунд перед повторной отправкой. Быстрее сдаётся — быстрее восстанавливается.
**3. Настраиваем BBR.** Алгоритм контроля перегрузки BBR (замени `cubic` или `reno`) лучше адаптируется к туннелям. Он не смотрит на потери, а считает пропускную способность. `net.core.default_qdisc = fq` и `net.ipv4.tcp_congestion_control = bbr`.
**4. Отключаем TCP Timestamps.** В туннелях они часто ломают расчёт RTT. `net.ipv4.tcp_timestamps = 0`.
После этого перезагрузи сетевые настройки: `sysctl -p`. Не жди чуда — это не магическая кнопка, но разница будет видна сразу.
Пример кода: проверка адаптации через curl
Хочешь увидеть, как работает адаптация? Запусти curl через VPN с разными настройками:
```bash
Без адаптации (дефолты)
curl -o /dev/null -w "Speed: %{speed_download} bytes/sec\n" http://example.com/file
С адаптацией (BBR + буферы)
sysctl -w net.ipv4.tcp_congestion_control=bbr
sysctl -w net.core.rmem_max=16777216
sysctl -w net.core.wmem_max=16777216
curl -o /dev/null -w "Speed: %{speed_download} bytes/sec\n" http://example.com/file
```
Сравни цифры. Если канал не загружен — разница может быть 10-20%. На загруженном — до 50%.
Когда адаптация не нужна
Есть случаи, где TCP-адаптация на уровне ядра — это лишняя сложность:
- **WireGuard или IPSec.** Они уже используют UDP, и TCP-стек внутри туннеля работает нормально. Не надо лезть в ядро.
- **Короткие сессии.** Если ты шлёшь DNS-запросы или HTTP-заголовки, TCP-адаптация не даст профита — сессия завершится до того, как начнутся проблемы.
- **Идеальный канал.** Если у тебя оптоволокно с пакетной потерей <0.1% и RTT <10 мс, дефолты работают нормально.
Но если твой канал — мобильный интернет, спутник или загруженный Wi-Fi, адаптация ядра — единственный способ не сойти с ума.
Практический совет: не пиши свой велосипед
Есть готовые решения, которые уже делают TCP-адаптацию на уровне ядра. Например, `tun2socks` с поддержкой `tcp-redir`. Или `shadow-tls` — он перехватывает TCP-сессии на входе в туннель и адаптирует их под TLS-обёртку. Не надо копаться в ядре самому, если есть готовые инструменты.
Но если ты хочешь реально глубоко — читай исходники `tcp_diag` в Linux. Там есть модуль `tcp_ulp` (Upper Layer Protocol), который позволяет встраивать кастомную логику в TCP-стек. Можно написать свой обработчик для туннеля. Но это для тех, кто не боится ядра.
Итог: что запомнить
TCP-адаптация на уровне ядра — это не про ускорение, а про стабильность. Она решает проблему двойного контроля перегрузки в туннелях. Без неё TCP поверх TCP — это тормоза и ретрансмиссии. С ней — приемлемая работа даже на плохих каналах.
Настраивай буферы, меняй алгоритм на BBR, уменьшай таймауты. И не забывай: UDP-туннели (WireGuard, OpenVPN в UDP) — это всегда проще, чем возня с TCP-адаптацией. Но если тебе нужен TCP на 443 порту — ядро твой друг.
А если хочешь попробовать на практике — возьми прокси от lexic.ml: там уже настроены TCP-буферы и BBR для туннелей. Не надо самому лезть в sysctl.