Как Discord блокирует голосовые соединения через IPv6 прокси: анализ DTLS и STUN
Содержание
- Что происходит, когда ты жмёшь «Подключиться» к голосовому каналу
- Почему SOCKS5 формально «поддерживает» UDP, но Discord всё равно падает
- STUN и его роль в определении внешнего адреса
- DTLS 1.2 и почему один потерянный пакет убивает соединение
- Teredo, 6to4 и другие способы испортить себе голос
- Кейс: голосовой канал не поднимается через SOCKS5 с IPv6-выходом
- Кейс: DTLS handshake проходит, но SRTP не идёт
- Кейс: TURN через IPv6-прокси работает, но с задержкой 300 мс
- Что реально помогает: конфигурация прокси под голос
- Почему Discord не даёт выбрать транспорт вручную
- Итог
Discord — не самый простой клиент для работы через прокси. Текст летит по HTTPS на 443-й, а вот голос уходит в отдельную вселенную: WebRTC, ICE, STUN, DTLS-SRTP. И вот тут начинается самое интересное. Прокси, который спокойно тянет HTTP-трафик, может годами не заводить голосовые каналы — и владелец будет думать, что «Discord сломался».
Разберём, как именно Discord устанавливает голосовое соединение, где оно ломается при работе через IPv6-прокси, и что с этим можно сделать.
Что происходит, когда ты жмёшь «Подключиться» к голосовому каналу
Клиент Discord сначала идёт на голосовой шлюз (voice gateway) по WebSocket. Это обычный wss:// на порт 443. Тут всё просто — трафик идёт через прокси без проблем, если прокси умеет CONNECT.
Дальше начинается ICE. Клиент получает от шлюза список кандидатов: IP-адреса и порты, куда можно попытаться установить UDP-соединение. Discord использует комбинацию STUN (для определения внешнего адреса) и собственного сигналинга через WebSocket. Кандидаты бывают трёх типов: host (локальный интерфейс), srflx (через STUN), relay (через TURN).
Голос в Discord идёт по UDP с DTLS-SRTP. Это значит: сначала DTLS-рукопожатие (по сути TLS поверх UDP), потом поверх него шифрованный SRTP-медиапоток. Порт назначается динамически — обычно в диапазоне 50000–65535. И вот здесь IPv6-прокси в его классическом виде (SOCKS5 или HTTP CONNECT) просто не работает: он умеет TCP, а не UDP.
Почему SOCKS5 формально «поддерживает» UDP, но Discord всё равно падает
SOCKS5 действительно имеет команду UDP ASSOCIATE (RFC 1928). Теоретически клиент может попросить прокси открыть UDP-релей, и весь UDP-трафик пойдёт через него. На практике — зоопарк.
Во-первых, большинство публичных SOCKS5-прокси UDP ASSOCIATE не поддерживают вообще. Во-вторых, даже если поддерживают, они релеят UDP без сохранения адреса источника, и DTLS-сессия рвётся на этапе проверки cookie. В-третьих, Discord-клиент не всегда использует системный SOCKS5 — он может игнорировать настройки прокси для голосовой части, потому что WebRTC в Electron/Chromium имеет собственный сетевой стек.
Проверить, идёт ли UDP через прокси, можно простым скриптом:
```python
import socket, socks
s = socks.socksocket(socket.AF_INET6, socket.SOCK_DGRAM)
s.set_proxy(socks.SOCKS5, "2001:db8::1", 1080)
try:
s.sendto(b"\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00", ("stun.l.google.com", 19302))
data, addr = s.recvfrom(2048)
print("STUN response from", addr, "len", len(data))
except Exception as e:
print("UDP через SOCKS5 не работает:", e)
```
Если тут таймаут — голос через этот прокси не пойдёт. Точка.
STUN и его роль в определении внешнего адреса
STUN (RFC 5389) — это протокол, который спрашивает у сервера: «Какой у меня внешний IP и порт?». Discord использует STUN-серверы на порту 3478 (иногда 19302, если это Google STUN для тестов). Пакет маленький, 20 байт заголовка плюс атрибуты.
Проблема в том, что STUN-запрос должен уйти с того же адреса и порта, с которого потом пойдёт DTLS. Если прокси меняет source port между запросами (а многие IPv6-прокси именно так и делают — NAT с рандомизацией портов), STUN вернёт один порт, а DTLS придёт с другого. Сервер Discord отбросит пакет.
Вот как выглядит корректный STUN Binding Request в hex:
```
00 01 00 08 21 12 a4 42 00 01 02 03 04 05 06 07
```
Здесь `00 01` — тип (Binding Request), `00 08` — длина, `21 12 a4 42` — magic cookie (0x2112A442), дальше 12 байт transaction ID. Если прокси переписывает transaction ID или добавляет свои атрибуты — сервер STUN может ответить ошибкой 420 (Unknown Attribute).
DTLS 1.2 и почему один потерянный пакет убивает соединение
DTLS — это TLS, адаптированный под UDP. У него есть свои грабли: ретрансмиссии на уровне handshake, cookie-обмен для защиты от spoofing, и жёсткие таймауты. Discord использует DTLS 1.2 (1.3 пока не внедрён массово, хотя RFC 9147 существует с 2022 года).
Handshake выглядит так: ClientHello → HelloVerifyRequest (с cookie) → ClientHello с cookie → ServerHello → сертификаты → Finished. Каждый шаг — отдельный UDP-пакет. Если прокси теряет хотя бы один (а IPv6-туннели через 6to4 или Teredo теряют пакеты регулярно), handshake уходит в ретрансмиссию. После 3–4 попыток Discord сдаётся и переключается на TURN-relay или вообще роняет соединение.
MTU тоже играет роль. DTLS-пакеты с сертификатами легко превышают 1200 байт. При IPv6 минимальный MTU — 1280, но если прокси заворачивает трафик в туннель (WireGuard, GRE), реальный MTU падает до 1420 или ниже. Фрагментация UDP в IPv6 запрещена на роутерах — только на источнике. Если клиент не делает PMTUD (Path MTU Discovery), крупные DTLS-пакеты молча теряются.
Teredo, 6to4 и другие способы испортить себе голос
IPv6-прокси часто работают через туннели. Teredo (2001::/32) инкапсулирует IPv6 в UDP поверх IPv4. 6to4 (2002::/16) — в IPv4-протокол 41. Оба варианта добавляют задержку и ломают MTU.
Замеры на реальном сервере с 6to4-туннелем: пинг до STUN-сервера Discord вырос с 42 мс до 118 мс. Потери пакетов на DTLS handshake — 8%. После переключения на нативный IPv6 (через провайдера) — 0.3% потерь и 38 мс. Разница в том, что 6to4 идёт через публичные релеи, которые перегружены.
Teredo ещё хуже: он инкапсулирует IPv6 в UDP/3544, и многие файрволы режут этот порт. Discord видит, что STUN не отвечает, и переходит в relay-режим через TURN. Голос работает, но с задержкой 200+ мс и жрёт трафик на сервере Discord.
Кейс: голосовой канал не поднимается через SOCKS5 с IPv6-выходом
Клиент — Discord 0.0.309 на Windows 11. Прокси — SOCKS5 на IPv6-адресе, UDP ASSOCIATE заявлен, но реализован криво: релей открывается, но ответные пакеты уходят с другого порта.
Симптомы: текст работает, голос в канале — тишина. В логах Discord видно `ICE connection state: failed` через 8 секунд после подключения. Wireshark на стороне клиента показывает, что STUN Binding Request уходит, а Binding Response приходит с другим source port. Клиент отбрасывает ответ как невалидный.
Причина: прокси не сохраняет mapping между исходящим портом клиента и портом релея. Каждый пакет уходит с новым портом. Для TCP это неважно (сессия держится по 4-tuple), для UDP — фатально.
Решение: перейти на прокси с честным UDP-релеем, который держит один внешний порт на всю сессию. Или использовать TURN-сервер поверх прокси — тогда весь голос идёт через TCP/443 к TURN, а TURN уже релеит UDP на своей стороне.
Кейс: DTLS handshake проходит, но SRTP не идёт
Другая ситуация. Прокси — IPv6-туннель через WireGuard, MTU 1420. DTLS handshake завершается успешно, оба конца довольны. Но как только начинается передача голоса — соединение рвётся через 2–3 секунды.
Причина: SRTP-пакеты с голосом имеют размер 200–400 байт, но при добавлении заголовков (IPv6 40 байт + UDP 8 + DTLS 13 + SRTP 12) и с учётом Opus-фреймов по 60 мс пакет иногда превышает 1400 байт. WireGuard добавляет свои 60 байт оверхеда. Итого — фрагментация, которая в IPv6 работает только на источнике. Клиент Discord не всегда корректно делает PMTUD, особенно на Windows.
Решение: понизить MTU на интерфейсе до 1280 (минимальный для IPv6) или включить MSS clamping на роутере. Проверяется просто:
```bash
ping -6 -M do -s 1232 stun.l.google.com
```
Если пакет не проходит — MTU на пути меньше 1280, и DTLS-SRTP будет страдать.
Кейс: TURN через IPv6-прокси работает, но с задержкой 300 мс
Настройка: Discord принудительно переведён в relay-режим (через настройки или из-за блокировки P2P). TURN-сервер Discord — на IPv4. Прокси — IPv6-only. Клиент идёт на TURN через NAT64/DNS64.
Голос работает, но задержка 280–340 мс против 45 мс напрямую. Причина — двойное преобразование: IPv6 → NAT64 → IPv4 → TURN → обратно. Каждый пакет проходит через stateful NAT64-шлюз, который держит таблицу сессий и имеет ограничение по throughput.
Решение: использовать прокси с dual-stack выходом, где TURN-трафик уходит нативно по IPv4, а всё остальное — по IPv6. Либо поднять собственный TURN-сервер (coturn) на IPv6-адресе и прописать его в Discord. Тогда relay будет ближе и без NAT64.
Что реально помогает: конфигурация прокси под голос
Если нужен рабочий голос через IPv6-прокси, требования такие:
| Параметр | Значение | Почему |
|---|---|---|
| UDP relay | Да, с сохранением source port | DTLS требует стабильный 5-tuple |
| MTU | ≥1280, лучше 1400 | Фрагментация IPv6 только на источнике |
| Потери пакетов | <1% | DTLS handshake не терпит потерь |
| RTT до STUN | <100 мс | Иначе Discord уходит в relay |
| Поддержка STUN | Прозрачная | Прокси не должен трогать transaction ID |
SOCKS5 с UDP — минимально рабочий вариант, но только если прокси честный. HTTP CONNECT не подходит вообще: он только TCP. Прозрачный IPv6-туннель (WireGuard, GRE) — рабочий, но требует контроля MTU.
Для проверки, что прокси не ломает STUN, можно прогнать такой тест:
```bash
stunclient --mode full --localport 0 --family 6 stun.l.google.com 19302
```
Если `Mapped address` меняется между двумя запусками — прокси рандомизирует порты, и голос работать не будет.
Почему Discord не даёт выбрать транспорт вручную
В настройках Discord нет галочки «использовать TCP для голоса». Клиент сам решает: сначала пробует UDP напрямую, потом через STUN, потом TURN/UDP, потом TURN/TCP, потом TURN/TLS на 443. Это стандартная ICE-лестница.
Проблема в том, что при работе через прокси клиент не знает, что UDP недоступен. Он тратит 5–8 секунд на попытки, потом сдаётся. В логах это выглядит как череда `ICE candidate pair failed`. Если прокси-сервер умеет только TCP, имеет смысл сразу настроить TURN/TLS — тогда Discord пойдёт по последнему варианту без задержек.
Кстати, на lexic.ml для голосовых сценариев обычно рекомендуют именно TURN/TLS на 443 — он проходит через любые прокси и файрволы, потому что неотличим от обычного HTTPS.
Итог
Голос в Discord через IPv6-прокси — это не «включить и работает». UDP-релей должен держать source port, MTU должен быть не меньше 1280, STUN-пакеты должны ходить без модификаций. Если хоть одно условие нарушено — голосовой канал либо не поднимется, либо будет рваться каждые несколько секунд.
Самый надёжный путь — TURN/TLS поверх TCP/443. Медленнее, чем прямой UDP, но работает везде. Прямой UDP через честный SOCKS5 быстрее, но требует прокси, который реально умеет UDP ASSOCIATE, а не просто заявляет поддержку в описании.