← Назад в базу знаний

IPv6 прокси: MTU фрагментация и Path MTU Discovery

IPv6 прокси: MTU фрагментация и Path MTU Discovery

Суть проблемы

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.

✔️Купить прокси