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

Discord через IPv6 прокси: обход блокировок голосовых каналов и ботов

Discord через IPv6 прокси: обход блокировок голосовых каналов и ботов

Почему голосовой трафик 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://.discord.gg` или похожий). Тоже 443.

А вот дальше начинается 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 и переключайся по расписанию.

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