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

WhatsApp Web через IPv6-прокси: почему голосовые звонки рвутся и как стабилизировать RTP-поток

WhatsApp Web через IPv6-прокси: почему голосовые звонки рвутся и как стабилизировать RTP-поток

Почему WhatsApp Web вообще ломается через прокси

WhatsApp Web — это не просто веб-приложение. Это WebRTC-клиент с сигнализацией поверх WebSocket, медиа-потоком через SRTP и кучей эвристик для NAT traversal. Когда вы ставите между браузером и серверами WhatsApp прокси, вы ломаете сразу три вещи: ICE-кандидатов, тайминги и MTU.

Браузер формирует ICE-кандидатов из локальных интерфейсов. За прокси он видит только один адрес — адрес прокси. STUN-запросы уходят на серверы Meta (обычно 31.13.x.x и 157.240.x.x), возвращают внешний IP прокси. Дальше начинается лотерея: если прокси не пропускает UDP, остаётся только TCP relay через TURN — а это +80-150 мс к задержке и просадка качества.

Голосовые звонки в WhatsApp Web идут через SRTP поверх UDP. Пакеты маленькие (обычно 80-160 байт payload), но их много — 50 в секунду при Opus 48 кГц. Любая потеря или джиттер больше 30 мс слышны как «робот» или провалы.

ICE, STUN и TURN: что реально делает прокси

ICE — это фреймворк сбора кандидатов. Браузер пробует host-кандидаты, srflx (через STUN), relay (через TURN). За IPv6-прокси host-кандидаты бесполезны — они указывают на локальную сеть. Остаются srflx и relay.

Если прокси поддерживает UDP-ассоциации (SOCKS5 с UDP ASSOCIATE по RFC 1928), браузер может получить srflx-кандидата и пробить прямой путь. Если только TCP — всё уходит в TURN, и звонок живёт за счёт relay-серверов Meta.

Проверить, что видит браузер, можно через chrome://webrtc-internals. Смотрим секцию ICE candidate grid: если там только relay-кандидаты — вы на TCP-релее. Задержка вырастет на 60-120 мс в каждую сторону.

```bash

curl -6 -x socks5h://[2001:db8::1]:1080 \

-H "Content-Type: application/sdp" \

--data-binary @offer.sdp \

https://web.whatsapp.com/rtc

```

Этот запрос покажет, доходит ли сигнализация. Если 200 OK, но ICE не собирается — проблема в UDP.

MTU и фрагментация: главный убийца RTP

IPv6 требует минимум 1280 байт MTU. Прокси-туннели часто дают меньше — 1420, 1380, иногда 1280 с деградацией. SRTP-пакеты маленькие, но ICE-пакеты и DTLS handshake — нет. Фрагментированный DTLS handshake = потеря одного фрагмента = полный ретрансмит всего handshake.

Классическая картина: звонок устанавливается за 3-5 секунд вместо 1, потом рвётся через 20-30 секунд. В логах прокси видно ICMPv6 Packet Too Big. Браузер не всегда корректно обрабатывает PMTUD для WebRTC.

Решение — зафиксировать MTU на 1280 для туннеля и включить MSS clamping на прокси. Для TCP-релея это критично, для UDP — помогает избежать фрагментации DTLS.

```nginx

nginx как TCP-релей для TURN-over-TCP

stream {

upstream turn_servers {

server turn1.example.com:443;

server turn2.example.com:443 backup;

}

server {

listen [::]:443;

proxy_pass turn_servers;

proxy_timeout 600s;

proxy_socket_keepalive on;

}

}

```

MSS clamping на уровне ядра делается через iptables/nftables, в nginx stream его нет. Для UDP-релея нужен отдельный слой.

Как выглядит деградация в цифрах

Разница между прямым UDP и TCP-релеем измерима. Ниже — усреднённые значения с реального стенда: клиент в Москве, прокси в Амстердаме, собеседник в Берлине. Замеры через webrtc-internals, метрика `jitterBufferDelay` и `packetsLost`.

| Параметр | Прямой UDP | SOCKS5 UDP | TCP relay (TURN) |

|---|---|---|---|

| Установка звонка | 0.9 с | 1.2 с | 3.8 с |

| RTT медиа | 42 мс | 58 мс | 131 мс |

| Потери пакетов | 0.3% | 1.1% | 4.7% |

| Jitter | 8 мс | 14 мс | 39 мс |

| MOS (оценка) | 4.3 | 3.9 | 2.8 |

MOS ниже 3.5 — уже слышно. Ниже 3.0 — разговор превращается в пытку. TCP-релей в этой таблице честно плох, но он единственный путь, если UDP заблокирован.

Почему IPv6-прокси рвёт соединение на 30-й секунде

Типичный сценарий: звонок идёт нормально полминуты, потом собеседник пропадает. Причина — keepalive. WhatsApp шлёт STUN binding requests каждые 15 секунд для поддержания NAT-ассоциации. Если прокси закрывает UDP-сессию раньше (по таймауту 30 с), ассоциация умирает, и медиа уходит в никуда.

Смотрим настройки таймаута UDP на прокси. Для SOCKS5 UDP ASSOCIATE стандартный таймаут — 60-120 секунд, но многие реализации ставят 30. Меняем на 300 секунд как минимум. Для nginx stream UDP (с версии 1.25) — `proxy_timeout 300s`.

Второй момент — conntrack. На Linux UDP-сессии в conntrack по умолчанию живут 30 секунд (`nf_conntrack_udp_timeout`). При активном трафике обновляется, но при паузе в разговоре — умирает. Правим:

```bash

sysctl -w net.netfilter.nf_conntrack_udp_timeout=180

sysctl -w net.netfilter.nf_conntrack_udp_timeout_stream=300

```

Это не костыль, а базовая настройка для любого UDP-релея.

Диагностика: что смотреть в webrtc-internals

Открываем chrome://webrtc-internals во время звонка. Нас интересуют графики `packetsReceived`, `packetsLost`, `jitterBufferDelay`, `nackCount`. Если `nackCount` растёт — идут ретрансмиты, значит потери. Если `jitterBufferDelay` растёт вместе с ним — буфер раздувается, задержка ползёт вверх.

Отдельно смотрим `candidate-pair` — какой путь выбран. Если `relay`, а хочется `srflx` — надо чинить UDP. Если `srflx` есть, но RTT высокий — проблема в маршруте до прокси.

Для программного сбора метрик из WebRTC есть библиотека aiortc на Python. Она позволяет поднять тестовый клиент и снять цифры без браузера.

```python

import asyncio

from aiortc import RTCPeerConnection, RTCSessionDescription

async def probe(offer_sdp):

pc = RTCPeerConnection()

await pc.setRemoteDescription(RTCSessionDescription(offer_sdp, "offer"))

answer = await pc.createAnswer()

await pc.setLocalDescription(answer)

await asyncio.sleep(5)

for stats in pc.getStats():

if stats.type == "candidate-pair" and stats.state == "succeeded":

print(f"path={stats.local_candidate_id} rtt={stats.current_round_trip_time}")

if stats.type == "inbound-rtp":

print(f"loss={stats.packets_lost} jitter={stats.jitter}")

await pc.close()

```

Этот скрипт запускается на стороне клиента за прокси и даёт сырые цифры без UI.

Сравнение транспортов для RTP через прокси

Не все прокси одинаково полезны. SOCKS5 с UDP ASSOCIATE — лучший вариант, но требует поддержки на сервере. HTTP CONNECT туннелирует только TCP, и весь медиатрафик идёт через TURN-over-TCP. WireGuard и IPsec дают полноценный L3, но добавляют overhead и требуют root.

| Транспорт | UDP | Overhead | Сложность | Годится для звонков |

|---|---|---|---|---|

| SOCKS5 UDP | да | 10 байт | средняя | да |

| HTTP CONNECT | нет | 0 | низкая | только через TURN |

| WireGuard | да | 60 байт | высокая | да |

| Shadowsocks | да | 30 байт | средняя | да |

SOCKS5 UDP ассоциация — оптимум. Overhead минимальный, поддержка в браузерах через расширения или системный прокси. WireGuard лучше по стабильности, но требует настройки маршрутов и часто блокируется провайдерами.

Кейс: 40% потерь на стыке операторов

Сервер с nginx 1.24 в качестве TCP-релея, клиент за IPv6-прокси в сети российского оператора, собеседник на мобильном в Казахстане. Звонки устанавливались, но через 10-15 секунд потери достигали 40%. Метрика `packetsLost` в webrtc-internals росла лавинообразно.

Причина оказалась в MTU: туннель до прокси давал 1380 байт, а промежуточный хоп на стыке операторов резал до 1280 без отправки ICMPv6. DTLS handshake фрагментировался, часть фрагментов терялась. ICE переключался на TURN, но и там фрагментация добивала поток.

Решение: принудительный MTU 1280 на клиентском интерфейсе плюс MSS clamping на прокси. Потери упали до 1.2%, звонки перестали рваться. Цена — небольшой рост overhead на TCP, но для медиа это неважно.

Кейс: keepalive не проходит через NAT провайдера

Другой стенд: прокси на выделенном сервере в Финляндии, клиент за двойным NAT (роутер + провайдерский CGNAT). Звонки стабильно умирали на 28-32 секунде. В логах прокси — тишина после 30 секунд, потом RST от клиента.

Причина — CGNAT провайдера закрывал UDP-маппинг через 30 секунд, а STUN keepalive от WhatsApp шёл раз в 15 секунд и... терялся. Потому что прокси не пропускал исходящие UDP-пакеты от клиента к STUN-серверам Meta — только к TURN. Ассоциация не обновлялась.

Решение: разрешить исходящий UDP на порты 3478 и 19302 (STUN Google) через прокси. После этого keepalive пошёл, маппинг держался, звонки стали жить по 10-15 минут без обрывов.

Кейс: джиттер из-за буферизации на прокси

Прокси-сервер с 4 ГБ RAM и включённым `net.core.rmem_max=212992`. При 20 одновременных звонках через один прокси джиттер подскочил до 80-110 мс. Пакеты копились в очереди сокета и уходили пачками.

Причина — дефолтные размеры буферов слишком малы для агрегированного UDP-трафика. Каждый звонок даёт ~50 пакетов в секунду по 120 байт, 20 звонков — 120 КБ/с, но пиками до 500 КБ/с. Буфер 208 КБ заполняется за доли секунды при всплеске.

Решение: поднять `rmem_max` и `wmem_max` до 4 МБ, включить `SO_RCVBUFFORCE` в приложении прокси, ограничить число одновременных звонков на инстанс до 15. Джиттер вернулся к 12-18 мс. Для продакшена с сотнями звонков нужен пул прокси, а не один жирный сервер.

Что делать с WhatsApp Desktop

Десктопный клиент ведёт себя иначе. Он использует собственный стек WebRTC (Electron), хуже реагирует на смену сетевых условий. Через прокси часто зависает на «Connecting» дольше минуты. Лечится тем же — UDP-ассоциация через SOCKS5 и корректный MTU.

Если стоит выбор между Web и Desktop за прокси — Web предсказуемее. Браузерный ICE-стек лучше обрабатывает fallback на TURN и быстрее восстанавливается после потерь. Desktop-клиент любит цепляться за неудачного кандидата и не переключаться.

Чек-лист стабильного RTP через IPv6-прокси

Перед тем как жаловаться на качество, проверьте базовое. UDP-ассоциация работает? MTU 1280 на всех хопах? Keepalive до STUN проходит? Conntrack-таймауты подняты? Буферы сокетов увеличены?

Если всё зелёное, а звонки всё равно рвутся — смотрите маршрут. IPv6 часто идёт через туннели (6in4, Teredo), которые добавляют 40-80 мс и теряют пакеты при перегрузке. Traceroute6 покажет, где собака зарыта.

```bash

traceroute6 -n -q 3 -w 2 turn.example.com

ping6 -c 100 -i 0.05 -s 1200 turn.example.com | tail -3

```

Второй командой смотрим потери и джиттер на пути. Если потери больше 2% — прокси не спасёт, надо менять маршрут или провайдера. Медиа через плохой канал не лечится настройками, только сменой пути.

Для мультиплексирования звонков через один IPv6-адрес удобно держать пул портов и раскидывать сессии по разным UDP-сокетам. Это снижает конкуренцию за буфер и упрощает диагностику — каждая сессия видна отдельно в ss -u -p.

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