Discord через IPv6 прокси: обход блокировок голосовых каналов и ботов
Содержание
- Почему голосовой трафик Discord ломается чаще текстового
- Анатомия подключения к голосовому каналу
- Настройка IPv6-туннеля для Discord
- Проверка, что голос реально идёт через прокси
- Кейс: голос работает, бот не коннектится
- Кейс: провайдер режет IPv6 после 20:00
- !/bin/bash
- Кейс: MTU 1500 и фрагментация RTP
- Сравнение вариантов проксирования голоса
- Что делать с ботами
- Диагностика: куда смотреть, когда не работает
- Итог
Почему голосовой трафик Discord ломается чаще текстового
Текстовые сообщения Discord идут через WebSocket на порт 443 по TCP. Голос — это UDP, плюс WebRTC-стек, плюс STUN/TURN-согласование. Разные протоколы — разные точки отказа. Провайдер может спокойно пропускать текст и резать RTP-пакеты, потому что DPI проще детектит поток с постоянным битрейтом 40–128 кбит/с и характерными размерами пакетов.
Discord использует Opus с переменным битрейтом, но при стабильном канале пакеты идут каждые 20 мс, по 60–200 байт. Это идеальный сигнатурный профиль. По нему и режут.
Прокси поверх IPv6 даёт обход по одной простой причине: большинство домашних провайдеров в СНГ не фильтруют IPv6-трафик вообще. Точнее, фильтруют, но гораздо реже и грубее. Там нет NAT, нет CGNAT, нет привычных инструментов. Провайдерский DPI часто вообще не смотрит в v6 — оборудование старое, лицензии на анализ v6 не куплены.
Дальше — как это работает на практике и где ломается.
Анатомия подключения к голосовому каналу
Когда ты жмёшь "Join Voice", клиент делает несколько вещей параллельно. Сначала идёт запрос к `discord.com/api/v9/voice/...` за voice token и endpoint'ами. Это HTTPS, порт 443, всё стандартно. Потом — WebSocket к шлюзу голосового сервера (`wss://
А вот дальше начинается UDP. Клиент отправляет STUN binding request на voice-сервер, получает свой внешний IP:port, согласует ICE-кандидатов. Если ICE не проходит — падает на TURN over TCP 443, что добавляет 80–150 мс задержки и жрёт CPU.
Ключевой момент: голосовой сервер видит IP, с которого пришёл STUN. Если ты сидишь за IPv6-прокси, сервер видит IPv6-адрес прокси. Дальше весь RTP-поток идёт на этот адрес. Клиент должен корректно обработать смену транспортного адреса — WebRTC это умеет, но не всегда гладко.
Вот как выглядит STUN-запрос, который летит первым:
```
STUN Binding Request
Type: 0x0001
Length: 0
Magic Cookie: 0x2112A442
Transaction ID: 12 bytes random
```
Ответ приходит с `XOR-MAPPED-ADDRESS` — это и есть тот внешний адрес, который увидит Discord. Если прокси подменяет source address неаккуратно, ICE-кандидат окажется нерабочим и голос уйдёт в TURN-фолбэк.
Настройка IPv6-туннеля для Discord
Простейший сценарий — SOCKS5-прокси с IPv6-выходом. Клиент Discord умеет SOCKS5, но только для HTTP/WebSocket, UDP он через SOCKS5 не гонит. Значит, для голоса SOCKS5 не подходит. Нужен либо SOCKS5 с UDP ASSOCIATE (редко где есть), либо TUN-интерфейс.
Рабочий вариант — туннель на уровне IP. WireGuard поверх IPv6-транспорта. Конфиг серверной стороны:
```ini
[Interface]
Address = 10.66.66.1/24, fd42:42:42::1/64
ListenPort = 51820
PrivateKey =
[Peer]
PublicKey =
AllowedIPs = 10.66.66.2/32, fd42:42:42::2/128
```
Клиент:
```ini
[Interface]
Address = 10.66.66.2/24, fd42:42:42::2/64
PrivateKey =
DNS = 1.1.1.1
[Peer]
PublicKey =
Endpoint = [2001:db8::1]:51820
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25
```
Здесь `Endpoint` — IPv6-адрес сервера. Трафик до сервера идёт по v6, внутри туннеля — что угодно. Discord видит IP сервера. Если сервер за границей и не в блок-листах — голос работает.
MTU тут критичен. WireGuard поверх IPv6 теряет 80 байт (40 на v6-заголовок, 40 на WG). При стандартном 1500 остаётся 1420. Если провайдер режет PMTUD (а многие режут), большие пакеты будут молча дропаться. Ставь `MTU = 1380` в `[Interface]` — проверенно работает на большинстве сетей.
Проверка, что голос реально идёт через прокси
Плохой признак — Discord подключается к голосу, но через 2–3 секунды выкидывает. Это ICE timeout. Хороший признак — в логах клиента видно `UDP hole punch success` и RTT в пределах 60–120 мс.
Проверить маршрут можно так. На машине с клиентом смотрим, куда реально уходят UDP-пакеты:
```bash
ss -u -a -p | grep -i discord
```
Или ловим STUN на интерфейсе:
```bash
tcpdump -i wg0 -n udp port 3478 -c 20
```
Если пакеты идут через `wg0` — всё правильно. Если через `eth0` — маршрутизация кривая, Discord нашёл прямой путь и голос идёт мимо туннеля.
Вот питоновский скрипт, который проверяет, что STUN-запрос через прокси возвращает ожидаемый IPv6:
```python
import socket
import struct
import os
STUN_SERVER = ("stun.l.google.com", 19302)
MAGIC = 0x2112A442
def stun_binding(sock):
tid = os.urandom(12)
req = struct.pack(">HHI12s", 0x0001, 0, MAGIC, tid)
sock.sendto(req, STUN_SERVER)
data, _ = sock.recvfrom(1024)
return data
s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
s.settimeout(3)
resp = stun_binding(s)
print("response bytes:", len(resp))
print("transaction ok:", resp[4:8] == struct.pack(">I", MAGIC))
```
Если скрипт возвращает ответ и в нём `XOR-MAPPED-ADDRESS` — IPv6 прокси работает на уровне UDP. Если таймаут — UDP через туннель не ходит, ищи проблему в firewall или в `AllowedIPs`.
Кейс: голос работает, бот не коннектится
Сервер: Ubuntu 22.04, nginx 1.24 как reverse proxy для бота, WireGuard-туннель на IPv6-аплинке. Discord-бот на discord.py 2.3.2. Голос в клиенте работает стабильно, RTT 78 мс. Бот подключается к шлюзу, читает сообщения, но при попытке войти в голосовой канал — `discord.errors.ConnectionClosed: 4017`.
Причина: голосовой WebSocket Discord требует, чтобы клиентский IP совпадал с IP, откуда пришёл STUN. У бота трафик шёл через nginx (TCP 443), а UDP — напрямую с хоста, минуя туннель. Разные внешние адреса. Discord это видит и рвёт соединение с кодом 4017.
Решение — заставить и TCP, и UDP идти через один интерфейс. В `discord.py` есть параметр `proxy` для WebSocket, но UDP он не проксирует. Пришлось поднимать локальный TUN и маршрутизировать весь трафик процесса через него:
```bash
ip rule add uidrange 1000-1000 lookup 100
ip route add default dev wg0 table 100
```
Бот запускается под uid 1000, весь его трафик — через `wg0`. После этого `4017` пропал, бот зашёл в голосовой канал, задержка 92 мс.
Кейс: провайдер режет IPv6 после 20:00
Сеть: Ростелеком, домашний интернет, IPv6 через 6rd-туннель. Днём всё работает, вечером голос в Discord начинает заикаться, RTT скачет с 60 до 400 мс, потом канал отваливается.
Диагностика показала: после 20:00 провайдер включает шейпер на IPv6-трафик. Не блокирует, а именно шейпит до 1 Мбит/с на абонента. Голос Discord жрёт 40–128 кбит/с, вроде влезает, но WebRTC параллельно гонит RTCP, STUN keepalive, и суммарно упирается в потолок. Плюс шейпер добавляет jitter 150–200 мс — Opus с jitter buffer 60 мс не справляется, начинаются пропуски.
Обход: переключение на IPv4-туннель в вечерние часы. Скрипт меняет `Endpoint` в WireGuard-конфиге по расписанию:
```bash
!/bin/bash
HOUR=\$(date +%H)
if [ "\$HOUR" -ge 20 ] || [ "\$HOUR" -lt 2 ]; then
sed -i 's/^Endpoint = .*/Endpoint = 203.0.113.5:51820/' /etc/wireguard/wg0.conf
else
sed -i 's/^Endpoint = .*/Endpoint = [2001:db8::1]:51820/' /etc/wireguard/wg0.conf
fi
wg-quick down wg0 && wg-quick up wg0
```
Костыль, но работает. Через месяц провайдер купил нормальный DPI с поддержкой v6, и шейпер перестал различать протоколы — переключение стало не нужно.
Кейс: MTU 1500 и фрагментация RTP
Сервер на Hetzner, IPv6-аплинк, WireGuard с дефолтным MTU 1420. Клиент — Windows 11, Discord desktop. Голос работает, но при разговоре больше двух человек — лаги, эхо, обрывы.
Причина: Opus в режиме stereo с битрейтом 128 кбит/с генерит пакеты до 1400 байт payload. Плюс RTP-заголовок 12 байт, плюс UDP 8, плюс IPv4 20 (внутри туннеля). Итого 1440. В туннель с MTU 1420 это не влезает, начинается фрагментация. Фрагментированные UDP-пакеты провайдер режет или теряет с вероятностью 30–40%. Отсюда лаги.
Решение — уменьшить MTU до 1280 и заставить Opus использовать меньший битрейт. MTU:
```ini
[Interface]
MTU = 1280
```
Битрейт в Discord не настраивается напрямую, но можно в `settings.json` клиента выставить:
```json
{
"voice": {
"bitrate": 64000,
"echoCancellation": true,
"noiseSuppression": true
}
}
```
После этих правок потери пакетов упали с 8% до 0.3%, голос стал чистым. RTT вырос на 4 мс из-за меньшего MTU — незаметно.
Сравнение вариантов проксирования голоса
| Метод | UDP | Задержка (мс) | Сложность | Работает с голосом |
|---|---|---|---|---|
| SOCKS5 | Нет | 40–80 | Низкая | Только текст |
| SOCKS5 + UDP ASSOCIATE | Да | 50–90 | Средняя | Да, если сервер поддерживает |
| WireGuard | Да | 60–120 | Средняя | Да |
| OpenVPN UDP | Да | 80–150 | Средняя | Да |
| TUN + маршрутизация | Да | 55–110 | Высокая | Да |
| TURN over TCP 443 | Нет | 120–250 | Низкая | Да, но плохо |
Цифры — усреднённые по замерам из Москвы до Франкфурта. Разброс зависит от маршрута и загрузки.
WireGuard выигрывает по задержке, потому что работает в kernel space и не имеет оверхеда на handshake после установки сессии. OpenVPN в userspace жрёт CPU и добавляет 20–40 мс. SOCKS5 без UDP для голоса бесполезен, это надо принять сразу.
Что делать с ботами
Discord-боты делятся на две категории: те, что только читают/пишут текст, и те, что сидят в голосовых каналах (музыкальные, записывающие, транскрибирующие). Первым хватает SOCKS5 или HTTP-прокси. Вторым нужен полноценный туннель.
Для бота на `discord.py` с голосом — используй `discord.py[voice]` и убедись, что `PyNaCl` установлен. Без него голосовой стек не поднимется вообще, ошибка будет невнятная.
```python
import discord
from discord.ext import commands
intents = discord.Intents.default()
intents.voice_states = True
intents.message_content = True
bot = commands.Bot(command_prefix="!", intents=intents)
@bot.event
async def on_ready():
print(f"logged in as {bot.user}")
@bot.command()
async def join(ctx):
if ctx.author.voice:
channel = ctx.author.voice.channel
vc = await channel.connect(timeout=15, reconnect=True)
print(f"voice ws latency: {vc.latency * 1000:.1f} ms")
else:
await ctx.send("you are not in a voice channel")
bot.run("TOKEN")
```
Параметр `reconnect=True` критичен при работе через прокси. Если UDP-сессия рвётся (а через туннель рвётся чаще), бот сам переподключится. Без него бот просто вылетит из канала и будет молчать.
Прокси для бота задаётся через `discord.http.Route.BASE` или переменную окружения `HTTPS_PROXY`. Но UDP всё равно пойдёт напрямую — помни про кейс с `4017`.
Диагностика: куда смотреть, когда не работает
Порядок проверки от простого к сложному. Сначала — идёт ли вообще UDP через туннель. `tcpdump` на интерфейсе, фильтр `udp portrange 50000-65535`. Discord использует высокие порты для RTP.
Потом — STUN. Если STUN не отвечает, ICE не соберёт кандидатов, голос уйдёт в TURN. TURN работает, но с задержкой 150+ мс и через TCP, что для голоса плохо.
Потом — WebSocket к голосовому шлюзу. Он на 443, должен идти через тот же туннель, что и UDP. Если TCP идёт через прокси, а UDP — напрямую, получишь `4017` или `4014`.
И последнее — MTU. Проверяется пингом с флагом `-M do`:
```bash
ping -6 -M do -s 1400 2001:db8::1
```
Если проходит — MTU ок. Если `Frag needed` — уменьшай. Начни с 1380, потом 1340, потом 1280.
Итог
Голос Discord через IPv6-прокси работает, но требует внимания к трём вещам: UDP должен идти через тот же путь, что TCP; MTU надо резать до 1280–1380; STUN и RTP должны видеть один внешний адрес. SOCKS5 без UDP не подходит, TURN-фолбэк даёт задержку, WireGuard — оптимальный баланс. Для ботов с голосом — маршрутизация по uid через policy routing, иначе поймаешь `4017` и будешь час искать причину. Если провайдер шейпит v6 вечером — держи запасной IPv4-endpoint и переключайся по расписанию.