Discord через IPv6 прокси: нестабильность WebSocket и обходные пути
Содержание
- Почему Discord и IPv6 — это боль
- Как устроен WebSocket-шлюз Discord
- Что ломается на IPv6-прокси
- Проверка соединения вручную
- Python-скрипт для мониторинга heartbeat
- Настройка прокси: что крутить
- Сравнение вариантов проксирования
- Голосовые каналы: отдельная история
- Реальный пример: сервер с nginx 1.24
- Когда прокси — не решение
- Что в итоге
Почему Discord и IPv6 — это боль
Discord держит четыре постоянных WebSocket-соединения к шлюзу. Голосовые каналы виснут отдельно — через UDP. И если у вас IPv6-прокси, вы почти гарантированно ловите разрывы каждые 2-5 минут. Не потому что прокси плохой, а потому что Discord жёстко привязан к TCP-сессиям и не любит, когда их кто-то трогает извне.
Шлюз Discord живёт на `gateway.discord.gg`. Резолвится в пул адресов, среди которых есть и AAAA-записи. Клиент выбирает узел, поднимает WebSocket поверх TLS, шлёт IDENTIFY — и ждёт heartbeat-подтверждения. Каждые ~41 секунда клиент отправляет heartbeat-пакет. Если сервер не отвечает в течение 2 циклов — соединение рвётся. Вот здесь и начинается веселье с IPv6-прокси.
Как устроен WebSocket-шлюз Discord
Шлюз — это не обычный HTTP-сервер. Это stateful-соединение с фиксированным жизненным циклом. После `IDENTIFY` сервер отправляет `HELLO` с интервалом heartbeat. Клиент обязан отвечать. Пропустил два — disconnect. Никаких retry на уровне приложения, только переподключение с нуля.
Discord использует opcode 10 для HELLO, opcode 1 для heartbeat, opcode 11 для ACK. Если ACK не приходит — клиент закрывает сокет с кодом 4000 и переподключается. Это поведение зашито в библиотеки discord.js, discord.py, а также в нативные клиенты.
Проблема в том, что IPv6-прокси добавляет промежуточный слой. TCP-сессия теперь проходит через цепочку: клиент → прокси → Discord. Любая задержка на этом пути ломает тайминги heartbeat.
Что ломается на IPv6-прокси
Главный враг — NAT и таймауты состояния. IPv6 в теории не требует NAT, но провайдеры всё равно ставят stateful-фаерволы. Они отслеживают соединения и дропают «простаивающие» сессии. Discord шлёт heartbeat раз в 41 секунду, но если прокси не пропускает исходящие пакеты в нужный момент — таймер проваливается.
Второй враг — MTU. IPv6 требует минимум 1280 байт, но реальный MTU на туннелях часто 1400-1480. Фрагментация WebSocket-фреймов на прокси приводит к тому, что крупные пакеты (например, при загрузке истории сообщений) теряются. Discord шлёт `RESUME` и пытается восстановить состояние — иногда успешно, иногда нет.
Третий — DNS. Если прокси резолвит `gateway.discord.gg` в IPv6-адрес, а маршрут до него нестабилен, клиент будет переподключаться к разным узлам. Каждое переподключение — новый `IDENTIFY`, новый rate limit.
Проверка соединения вручную
Прежде чем лезть в настройки прокси, проверьте, как выглядит путь до шлюза. Возьмите реальный адрес из DNS и посмотрите задержки.
```bash
dig +short AAAA gateway.discord.gg
ping6 -c 10 <полученный_адрес>
```
Если потери пакетов выше 1% — проблема на маршруте, а не в прокси. Если RTT скачет больше чем на 100 мс — тоже плохой знак.
Проверить WebSocket-хендшейк можно через curl, но с оговоркой: curl не умеет держать соединение долго. Он покажет только факт установки.
```bash
curl -v --http1.1 \
-H "Connection: Upgrade" \
-H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" \
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
https://gateway.discord.gg/?v=10&encoding=json
```
Ответ 101 Switching Protocols — хорошо. Ответ 400 или таймаут — плохо. Но даже успешный 101 не гарантирует стабильность через 5 минут.
Python-скрипт для мониторинга heartbeat
Вот рабочий скрипт, который подключается к шлюзу и логирует каждый heartbeat. Запускать через прокси, чтобы увидеть реальную картину.
```python
import asyncio
import json
import time
import websockets
GATEWAY = "wss://gateway.discord.gg/?v=10&encoding=json"
async def monitor():
async with websockets.connect(GATEWAY, ping_interval=None) as ws:
hello = json.loads(await ws.recv())
interval = hello["d"]["heartbeat_interval"] / 1000
print(f"heartbeat interval: {interval}s")
seq = None
last_ack = time.time()
async def heartbeat():
nonlocal seq
while True:
await asyncio.sleep(interval)
payload = {"op": 1, "d": seq}
t0 = time.time()
await ws.send(json.dumps(payload))
print(f"sent heartbeat, seq={seq}, t={t0:.3f}")
hb_task = asyncio.create_task(heartbeat())
try:
async for msg in ws:
data = json.loads(msg)
if data.get("op") == 11:
rtt = (time.time() - last_ack) * 1000
print(f"ACK received, rtt={rtt:.1f}ms")
last_ack = time.time()
if data.get("s"):
seq = data["s"]
finally:
hb_task.cancel()
asyncio.run(monitor())
```
Запустите без прокси и с прокси. Сравните RTT и количество разрывов. Если через прокси RTT стабильно выше на 150+ мс и разрывы каждые 3-4 минуты — прокси дропает соединение по таймауту.
Настройка прокси: что крутить
Первое — keepalive. TCP keepalive на прокси должен быть меньше, чем таймаут фаервола провайдера. Обычно это 60-120 секунд. Ставьте 30.
Второе — MTU. Если прокси на Linux, проверьте:
```bash
ip link show dev eth0 | grep mtu
```
Для IPv6-туннелей ставьте 1400. Для прямого IPv6 — 1480. Не 1500, потому что заголовки IPv6 на 20 байт больше, чем IPv4.
Третье — буферизация. WebSocket-фреймы нельзя буферизовать. Если прокси на nginx — обязательно `proxy_buffering off`. Иначе heartbeat-пакеты застрянут в буфере и уйдут пачкой.
```nginx
location / {
proxy_pass https://gateway.discord.gg;
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
proxy_set_header Host gateway.discord.gg;
}
```
`proxy_read_timeout` и `proxy_send_timeout` — критично. По умолчанию 60 секунд. Discord шлёт heartbeat раз в 41 секунду, но если сеть дёрнулась — таймаут сработает раньше, чем придёт следующий пакет. Ставьте 300.
Сравнение вариантов проксирования
| Тип прокси | Средний RTT | Разрывы за час | Поддержка UDP |
|---|---|---|---|
| Прямое IPv4 | 45 мс | 0 | да |
| IPv6 SOCKS5 | 78 мс | 2-3 | частично |
| IPv6 HTTP CONNECT | 92 мс | 5-8 | нет |
| IPv6 + WireGuard | 65 мс | 0-1 | да |
Цифры усреднённые по европейским маршрутам. HTTP CONNECT худший вариант: он не умеет UDP, а голосовые каналы Discord работают именно через UDP. Без голоса Discord ещё живёт, с голосом — нет.
SOCKS5 умеет UDP, но не все клиенты его правильно пробрасывают. discord.py через `aiohttp-socks` работает, но голос всё равно часто отваливается.
Голосовые каналы: отдельная история
Голос Discord идёт через UDP на порты 50000-65535. IPv6-прокси должен пропускать UDP-трафик. HTTP-прокси этого не делают. SOCKS5 — умеют, но с оговорками.
Проверить, идёт ли голос через прокси:
```bash
ss -u -a | grep -E '5[0-9]{4}'
```
Если видите established UDP-соединения на высоких портах — голос идёт. Если нет — голос использует прямой маршрут, минуя прокси. Это может быть как хорошо (меньше задержка), так и плохо (утечка реального IP).
Для голоса лучше WireGuard. Он инкапсулирует и TCP, и UDP, и не ломает тайминги. Настройка сложнее, но результат стабильнее.
Реальный пример: сервер с nginx 1.24
Сервер на Ubuntu 22.04, nginx 1.24.0, IPv6-адрес от Hetzner. Проксирует Discord для 12 клиентов одновременно. Изначально — разрывы каждые 90 секунд у всех клиентов.
Причина: `proxy_read_timeout` стоял по умолчанию 60 секунд. Discord шлёт heartbeat раз в 41 секунду, но при загрузке канала (например, при открытии сервера с 500+ сообщениями) пауза между heartbeat достигала 70 секунд. Nginx рвал соединение.
Решение: `proxy_read_timeout 300s`, `proxy_send_timeout 300s`, `proxy_buffering off`. После этого разрывы прекратились. RTT вырос с 45 до 78 мс — плата за проксирование. Но стабильность важнее.
Второй момент: `resolver` в nginx. Если не указать DNS-сервер, nginx кэширует IP шлюза навсегда. Discord меняет адреса. Добавьте:
```nginx
resolver 1.1.1.1 8.8.8.8 valid=30s ipv6=on;
```
`valid=30s` — обновлять DNS каждые 30 секунд. Без этого клиенты будут стучаться в мёртвый узел.
Когда прокси — не решение
Если провайдер режет IPv6-трафик или ставит DPI — прокси не поможет. DPI видит WebSocket-хендшейк и может его дропать. В этом случае нужен обфусцированный туннель: WireGuard с obfuscation, Shadowsocks, или VLESS с XTLS.
Второй случай — если Discord сам блокирует IP прокси. Такое бывает с дешёвыми VPS. Проверяется просто: подключитесь к шлюзу без прокси с того же IP. Если работает — прокси виноват. Если нет — IP в блоке.
Третий случай — rate limit на `IDENTIFY`. Discord разрешает 1000 IDENTIFY в сутки на аккаунт. Если прокси постоянно рвёт соединение, клиент переподключается и жжёт лимит. Через час — бан на 24 часа. Здесь помогает только стабильный прокси или отказ от него.
Что в итоге
Discord через IPv6-прокси работает, но требует настройки. Ключевые параметры: keepalive 30 секунд, MTU 1400, `proxy_buffering off`, таймауты 300 секунд. Без этого — разрывы каждые 1-3 минуты.
Для текстовых каналов хватит SOCKS5 или HTTP CONNECT. Для голоса — только WireGuard или прямой маршрут. HTTP-прокси голос не пропустит.
Если нужен IPv6-прокси с поддержкой UDP и без rate limit на соединения — lexic.ml держит такие с 2015 года, там же можно проверить свои тайминги через их тестовый эндпоинт. Но перед этим убедитесь, что проблема именно в прокси, а не в маршруте до Discord.