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

Почему IPv6-прокси не работают на сайтах с dual-stack: диагностика через AAAA-записи

Почему IPv6-прокси не работают на сайтах с dual-stack: диагностика через AAAA-записи

Что происходит при подключении к dual-stack сайту

Клиент резолвит домен. Получает A и AAAA. Дальше начинается самое интересное — поведение зависит от операционной системы, версии glibc, настроек резолвера и того, какой сокет открывает приложение.

Стандартная логика из RFC 6724 говорит: IPv6 в приоритете. Если у клиента есть глобальный IPv6-адрес и AAAA-запись резолвится — соединение пойдёт по IPv6. Прокси тут ни при чём, если он не умеет перехватывать IPv6-трафик.

Типичная картина: у вас настроен SOCKS5-прокси на IPv4-адресе, браузер или curl его используют, но при этом система имеет native IPv6. Приложение резолвит AAAA, видит IPv6-адрес назначения и... идёт напрямую. Прокси остаётся в стороне. Трафик утекает мимо.

```

\$ curl -v --socks5 127.0.0.1:1080 https://example.com

* Trying 127.0.0.1:1080...

* SOCKS5 connect to [2606:2800:220:1:248:1893:25c8:1946]:443

```

Вот оно. curl через SOCKS5 попытался подключиться к IPv6-адресу. Если прокси не поддерживает IPv6-направление — соединение отвалится с ошибкой `SOCKS5 connect failed` или зависнет на таймауте.

Почему AAAA-запись ломает маршрутизацию через прокси

Прокси-сервер получает от клиента команду CONNECT с адресом назначения. Адрес может быть в формате IPv4 или IPv6. SOCKS5 поддерживает оба типа через разные ATYP: 0x01 для IPv4, 0x04 для IPv6, 0x03 для доменного имени.

Проблема в том, что многие прокси-серверы (особенно старые сборки на базе Dante, 3proxy, squid) собираются без флага `--enable-ipv6` или его аналогов. Они физически не могут открыть исходящий сокет к IPv6-адресу. Клиент присылает ATYP=0x04, сервер возвращает `0x07 Command not supported` или просто рвёт соединение.

Второй сценарий — прокси работает, но на IPv4-only хосте. Клиент шлёт IPv6-адрес назначения, прокси не может его зарезолвить в IPv4 и отдаёт ошибку. При этом A-запись у домена есть, но клиент её уже не спрашивает — он выбрал AAAA.

Третий, самый коварный случай: прокси поддерживает и IPv4, и IPv6, но выходной интерфейс имеет только IPv4-адрес. Соединение к IPv6-назначению уходит в никуда, таймаут 30 секунд, потом ошибка. Пользователь видит «сайт не открывается», хотя проблема в одной строке конфига.

Диагностика через dig и getent

Первое, что нужно сделать — понять, что вообще резолвится.

```bash

dig A example.com +short

dig AAAA example.com +short

```

Если AAAA есть — половина проблем уже понятна. Дальше смотрим, что видит само приложение. `getent` использует ту же цепочку NSS, что и большинство программ.

```bash

getent ahosts example.com

```

Порядок вывода покажет приоритет. Если IPv6 идёт первым — приложение по умолчанию пойдёт туда.

Проверить, какой адрес реально выбирается, можно через `strace` на открытие сокета:

```bash

strace -f -e trace=connect curl -s https://example.com -o /dev/null 2>&1 | grep connect

```

Увидите `sin6_family=AF_INET6` или `sin_family=AF_INET`. Это снимает все догадки.

Настройка gai.conf и приоритетов

Linux использует `/etc/gai.conf` для управления порядком адресов из getaddrinfo. По умолчанию RFC 6724 даёт IPv6 приоритет 40, IPv4 — 10. Можно поменять.

```

precedence ::ffff:0:0/96 100

```

Эта строка заставляет glibc предпочитать IPv4-mapped адреса, то есть фактически IPv4. Костыль, но работает без пересборки приложений.

Проблема: не все программы используют getaddrinfo. Go, например, имеет собственный резолвер, если не собран с тегом `netgo`. Java тоже резолвит по-своему через `java.net.preferIPv4Stack`. Так что gai.conf — не серебряная пуля.

Проверить, какой резолвер использует бинарник:

```bash

ldd \$(which curl) | grep -i resolv

```

Если линковка с libc — gai.conf подействует. Если статическая сборка или Go — нет.

Кейс: nginx 1.24 как reverse proxy перед IPv6-бэкендом

Сервер на Ubuntu 22.04, nginx 1.24.0. Апстрим — приложение на `[::1]:8080`. Клиенты ходят через IPv4-прокси. Симптом: 502 Bad Gateway на половине запросов.

Причина: nginx резолвит имя апстрима через getaddrinfo, получает и A, и AAAA. В конфиге `proxy_pass http://backend;` без указания семейства. Nginx выбирает первый адрес из списка — иногда IPv4, иногда IPv6. Если выбран IPv6, а `resolver` не настроен для динамического резолва — соединение падает.

Решение: явно указать семейство.

```nginx

upstream backend {

server 127.0.0.1:8080;

server [::1]:8080; # убрать или закомментировать

}

server {

listen 80;

location / {

proxy_pass http://backend;

proxy_next_upstream error timeout;

}

}

```

Либо использовать переменную с явным resolver:

```nginx

resolver 127.0.0.53 valid=30s ipv6=off;

set \$backend "http://backend.example.com";

proxy_pass \$backend;

```

Директива `ipv6=off` в resolver — ключевая. Без неё nginx будет пытаться резолвить AAAA и подключаться по IPv6.

Python requests и urllib3: скрытый выбор

`requests` использует `urllib3`, который через `socket.create_connection` вызывает getaddrinfo и перебирает адреса по порядку. Если первый — IPv6 и он недоступен, будет таймаут, потом fallback на IPv4. Но таймаут по умолчанию — 30 секунд на попытку.

```python

import socket

import requests

Принудительно IPv4

orig = socket.getaddrinfo

def ipv4_only(host, port, family=0, type=0, proto=0, flags=0):

return orig(host, port, socket.AF_INET, type, proto, flags)

socket.getaddrinfo = ipv4_only

r = requests.get("https://example.com", timeout=5)

print(r.status_code)

```

Этот хак работает, но ломает всё остальное в процессе. Лучше — использовать кастомный адаптер или `urllib3` с `socket_options`.

Более чистый путь — пропатчить resolver через `requests_toolbelt` или использовать `httpx` с явным указанием `local_address`:

```python

import httpx

transport = httpx.HTTPTransport(local_address="0.0.0.0")

client = httpx.Client(transport=transport, timeout=5.0)

r = client.get("https://example.com")

```

`local_address="0.0.0.0"` заставляет сокет биндиться на IPv4, что автоматически исключает IPv6-подключения.

SOCKS5 и ATYP: где рвётся

Разберём пакет SOCKS5 подробнее. Клиент отправляет:

```

VER=0x05, CMD=0x01, RSV=0x00, ATYP=0x04, ADDR=16 bytes, PORT=2 bytes

```

ATYP=0x04 означает IPv6. Прокси-сервер должен уметь это обработать. Если в коде сервера есть только ветка для ATYP=0x01 и 0x03 — соединение отклоняется с кодом 0x08 (Address type not supported).

Проверить, что именно шлёт клиент:

```bash

tcpdump -i lo -X 'tcp port 1080' -c 10

```

В дампе увидите байты после handshake. ATYP=0x04 — приговор для IPv4-only прокси.

Решение на стороне клиента: заставить резолвить только A-записи. В curl это `--ipv4`:

```bash

curl --ipv4 --socks5 127.0.0.1:1080 https://example.com

```

В браузере — флаг `network.dns.disableIPv6` в about:config для Firefox, или `--disable-ipv6` для Chromium.

Прокси-серверы с поддержкой IPv6: сравнение

| Прокси | IPv6 out | ATYP 0x04 | Резолв AAAA |

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

| Dante 1.4.2 | да (с --enable-ipv6) | да | да |

| 3proxy 0.9.4 | да | да | да |

| Squid 5.7 | да | да | да |

| Privoxy 3.0.34 | нет | нет | нет |

| Tinyproxy 1.11 | частично | нет | нет |

Tinyproxy до версии 1.11 не умел IPv6 вообще. В 1.11 добавили, но с оговорками — ATYP 0x04 обрабатывается только если собран с `--enable-ipv6`.

Dante — самый предсказуемый вариант. Конфиг:

```

socksmethod: none

clientmethod: none

external: eth0

internal: eth1

socks pass {

from: 0.0.0.0/0 to: 0.0.0.0/0

protocol: tcp udp

}

```

Строка `external: eth0` без указания семейства заставит Dante использовать оба стека. Если нужен только IPv4 — `external: 0.0.0.0`.

Когда IPv6-прокси оправдан

Если целевой сайт имеет только AAAA (например, некоторые CDN-ноды или внутренние сервисы), IPv4-прокси бесполезен. Нужен именно IPv6-выход. Такие прокси арендуют у провайдеров с native IPv6 — Hetzner, OVH, Scaleway.

Проверить, что прокси реально отдаёт IPv6-трафик:

```bash

curl -6 --proxy socks5h://user:pass@proxy:1080 https://ifconfig.co

```

Флаг `-6` заставляет curl использовать IPv6 для подключения к прокси. `socks5h` означает, что резолв делает прокси, а не клиент. Без `h` клиент резолвит сам и может выбрать не тот адрес.

Сервис lexic.ml даёт IPv6-прокси с 2015 года, и там как раз решена проблема dual-stack — трафик маршрутизируется через IPv6-пул, а клиент может быть на любом стеке. Это не реклама, а техническая деталь: при работе с dual-stack сайтами важно, чтобы прокси-сервер имел и IPv4, и IPv6 интерфейсы, иначе половина запросов будет отваливаться.

Практический чеклист диагностики

Порядок действий при проблеме «через прокси не открывается, без прокси открывается»:

1. `dig AAAA` — есть ли AAAA у домена

2. `getent ahosts` — что видит система

3. `strace -e connect` — куда реально идёт соединение

4. `tcpdump` на порту прокси — какой ATYP шлёт клиент

5. Проверка прокси на IPv6: `curl -6 --proxy socks5h://... https://ifconfig.co`

6. Логи прокси-сервера — есть ли ошибка «address type not supported»

Каждый шаг отсекает половину гипотез. В 80% случаев проблема в том, что клиент резолвит AAAA и идёт мимо прокси или шлёт IPv6-адрес в IPv4-only прокси.

Оставшиеся 20% — конфиг прокси без IPv6-выхода. Лечится либо пересборкой с `--enable-ipv6`, либо переключением клиента на IPv4-only режим. Второе быстрее, первое правильнее.

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