IPv6 прокси: MTU фрагментация и Path MTU Discovery
Содержание
- Суть проблемы
- Как работает Path MTU Discovery в IPv6
- MTU на прокси: цифры и замеры
- Кейс: WireGuard на lexic.ml
- TCP MSS Clamping: как обойти грабли
- Принудительный MSS
- Кейс: VoIP через IPv6 прокси
- Что делать, если ICMP блокирован
- /etc/systemd/network/50-eth0.network
- Кейс: HTTP/2 мультиплексирование
- Таблица: MTU для разных туннелей
- Почему 1280 — магическое число
- Проверка MTU на лету
- !/usr/bin/env python3
- Отправляем SYN с указанием MSS
- Итог
Суть проблемы
IPv6 запрещает фрагментацию на промежуточных узлах. В отличие от IPv4, где маршрутизаторы могли резать пакеты на лету, IPv6 перекладывает эту ответственность на отправителя. Звучит логично. На практике — адские грабли.
Когда ваш сервер шлёт пакет через прокси, а MTU на каком-то участке пути меньше размера пакета — пакет тупо дропается. Отправитель получает ICMPv6 Packet Too Big. Если не умеет обрабатывать — привет, чёрный экран.
Как работает Path MTU Discovery в IPv6
RFC 8201 описывает механизм. Отправитель шлёт пакеты с флагом DF (Don't Fragment). На самом деле в IPv6 этого флага нет — фрагментация просто запрещена на маршрутизаторах.
Когда пакет упирается в линк с меньшим MTU, маршрутизатор шлёт ICMPv6 Type 2 (Packet Too Big) с указанием MTU этого линка. Отправитель должен:
1. Запомнить новый MTU для этой сессии
2. Переслать пакет с меньшим размером
3. Периодически пробовать увеличить MTU обратно
Проблема: ICMP-пакеты часто блокируются файрволами. Особенно в облачных сетях.
MTU на прокси: цифры и замеры
Возьмём типичную цепочку: клиент → IPv6 прокси → целевой сервер. MTU ethernet — 1500 байт. Но добавляем PPPoE — 1492. GRE туннель — 1476. WireGuard — 1420.
```
curl -6 https://api.lexic.ml/ip -o /dev/null -w "%{time_total}\n" --interface eth0
```
На прокси мы видим MTU 1500. Но реальный путь через WireGuard даёт 1420. Если пакет больше — дроп.
Проверка реального MTU:
```bash
ping -6 -c 3 -M do -s 1472 google.com
```
-s 1472 + 28 (ICMP заголовки) = 1500. Если пинг проходит — MTU 1500. Если нет — уменьшаем.
Кейс: WireGuard на lexic.ml
Пример: сервер на Debian 12, WireGuard 1.0.20220627. MTU интерфейса wg0 — 1420 байт. Клиенты шлют HTTP запросы через корзину.
Проблема: сайты с большими TLS сертификатами (цепочка > 4 КБ) не открывались. TCP сессия стартовала, но на handshake зависала.
Причина: SYN-пакеты проходили (маленькие), а вот ответ сервера с Certificate (размер ~4500 байт) фрагментировался на 3 пакета по 1420 байт. Но клиентский файрвол блокировал ICMPv6 Packet Too Big.
Решение: принудительно установить MTU на клиенте:
```bash
ip link set dev wg0 mtu 1280
```
1280 — минимальный MTU для IPv6 (RFC 2460). Работает везде. Цена — 10% оверхед на фрагментацию.
TCP MSS Clamping: как обойти грабли
Лучше, чем костыль с MTU. На прокси режем TCP MSS (Maximum Segment Size) в SYN-пакетах.
```nginx
server {
listen [::]:443 ssl;
tcp_nopush on;
tcp_nodelay on;
proxy_http_version 1.1;
Принудительный MSS
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_next_upstream error timeout invalid_header http_500;
}
```
Но nginx не умеет резать MSS. Используем iptables:
```bash
iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu
ip6tables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu
```
Это заставляет ядро автоматически вычислять MSS на основе MTU выходного интерфейса. Работает для IPv4 и IPv6.
Кейс: VoIP через IPv6 прокси
SIP и RTP протоколы. Типичный MTU для RTP — 1500. Но через прокси с WireGuard — 1420.
Проблема: пакеты с G.722 кодеком (битрейт 64 кбит/с) имеют полезную нагрузку ~200 байт. Всё ок. Но DTMF тоны (RFC 4733) генерируют пакеты до 1400 байт.
Причина: SIP информирует о поддержке DTMF через RTP payload. Клиент шлёт большие пакеты. Прокси их дропает.
Решение: на прокси настроить QoS и принудительный MTU для RTP трафика:
```bash
tc qdisc add dev wg0 root handle 1: htb default 12
tc class add dev wg0 parent 1: classid 1:1 htb rate 100mbit ceil 100mbit
tc filter add dev wg0 protocol ipv6 parent 1:0 prio 1 u32 match ip6 protocol 17 0xff flowid 1:1
```
И заодно режем MTU до 1280 на интерфейсе прокси. Качество звонков не страдает — RTP пакеты маленькие.
Что делать, если ICMP блокирован
Половина хостинг-провайдеров дропает ICMPv6. Особенно Hetzner, DigitalOcean, AWS. Решение — принудительный MTU на всех интерфейсах.
```bash
/etc/systemd/network/50-eth0.network
[Network]
DHCP=ipv6
LinkLocalAddressing=yes
[Link]
MTUBytes=1280
```
Или через sysctl:
```bash
sysctl -w net.ipv6.conf.all.mtu=1280
```
Жёстко, но работает. Платите 10% пропускной способности за стабильность.
Кейс: HTTP/2 мультиплексирование
HTTP/2 использует одно TCP соединение для множества запросов. Проблема: если один поток вызывает фрагментацию — страдают все.
Пример: сервер nginx 1.26, HTTP/2, через прокси с MTU 1420. Клиент загружает страницу с 50 ресурсами. Один ресурс — JS-бандл 2 МБ.
Проблема: TCP сессия одна. Все 50 потоков идут через неё. Когда сервер шлёт большой JS, он фрагментируется. ICMPv6 блокирован. TCP retransmission каждые 200 мс. Через 3 секунды — таймаут.
Решение: включить HTTP/2 initial window size:
```nginx
http2_max_field_size 4k;
http2_max_header_size 8k;
http2_body_preread_size 64k;
```
И принудительный MSS на прокси:
```bash
ip6tables -t mangle -A POSTROUTING -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1360
```
1360 = 1420 (MTU) - 60 (IPv6 заголовок). Работает как часы.
Таблица: MTU для разных туннелей
| Тип туннеля | MTU | Оверхед | Примечание |
|-------------|-----|---------|------------|
| Ethernet | 1500 | 0 | Базовый |
| PPPoE | 1492 | 8 | Домашние сети |
| GRE | 1476 | 24 | Стандартный туннель |
| IPIP | 1480 | 20 | Простой туннель |
| WireGuard | 1420 | 80 | Современный VPN |
| OpenVPN (UDP) | 1500 | 0 | Если не сжимает |
| OpenVPN (TCP) | 1300 | 200 | Двойная инкапсуляция |
| IPv6 over IPv4 | 1480 | 20 | 6to4 туннель |
Почему 1280 — магическое число
RFC 2460 говорит: IPv6 обязателен к поддержке на всех линках с MTU >= 1280 байт. Это минимальный гарантированный MTU для IPv6.
Если выставить MTU 1280 на всех интерфейсах — гарантированно не будет проблем с фрагментацией. Даже через старые PPPoE, даже через туннели.
Цена: 14% оверхед на заголовки (1500 vs 1280). Для большинства сайтов — незаметно. Для файловых серверов — ощутимо.
Проверка MTU на лету
Скрипт для тестирования реального MTU до целевого хоста:
```python
!/usr/bin/env python3
import socket
import struct
import time
def check_mtu(host, port=443):
sizes = [1500, 1492, 1476, 1420, 1400, 1380, 1360, 1340, 1320, 1300, 1280]
for mtu in sizes:
try:
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
sock.settimeout(2)
sock.connect((host, port))
Отправляем SYN с указанием MSS
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_MAXSEG, mtu - 40)
sock.send(b"GET / HTTP/1.1\r\nHost: " + host.encode() + b"\r\n\r\n")
data = sock.recv(1024)
print(f"MTU {mtu}: OK")
sock.close()
return mtu
except:
print(f"MTU {mtu}: FAIL")
return 1280
check_mtu("api.lexic.ml")
```
Итог
MTU фрагментация в IPv6 — не баг, а фича. Но фича, которая ломает 30% сайтов при первом подключении через прокси.
Три рабочих способа:
1. Принудительный MTU 1280 на всех интерфейсах — просто, надёжно, 14% потерь
2. TCP MSS clamping на прокси — прозрачно для клиентов
3. Path MTU Discovery с корректной обработкой ICMP — идеально, но требует настройки файрволов
Выбирайте по ситуации. Для продакшена — комбинируйте 1 и 2.