Как заставить браузер не утекать реальный IP при отключении IPv6 прокси: настройка fail-closed и тест на утечки
Содержание
- Почему браузер вообще утекает
- Архитектура fail-closed: три уровня защиты
- Маршрутизация через nftables с kill switch
- Настройка браузера: PAC-файл против ручных настроек
- Блокировка WebRTC
- DNS-утечки и их закрытие
- Тест на утечки: что и как проверять
- Кейс: nginx как прокси-шлюз с проверкой
- Кейс: Firefox с жёсткой политикой
- Кейс: SOCKS5 через lexic.ml с проверкой на утечки
- Чек-лист проверки
- Почему fail-open так популярен
- Что делать при падении прокси
- !/bin/bash
Прокси упал. Браузер молча переключился на прямой маршрут. Реальный IP улетел на сайт. Через 40 секунд пришёл бан или стук в дверь. Знакомая история.
Проблема в том, что большинство прокси-клиентов работают по модели fail-open: не смогли подключиться через прокси — идут напрямую. Удобно для обычного пользователя, катастрофа для тех, кому нужно скрывать адрес. Разбираем, как построить fail-closed схему и не оставить щелей.
Почему браузер вообще утекает
Браузер понятия не имеет, что вы сидите за прокси. Он видит системные настройки или расширение и пытается им следовать. Если прокси-сервер не отвечает на TCP-хендшейк, браузер ждёт таймаут (обычно 10-30 секунд), потом выдаёт ошибку — но только для конкретного запроса. Следующий запрос может уйти уже напрямую, если настройки слетели или расширение отключилось.
Хуже с WebRTC. Этот протокол устанавливает peer-to-peer соединения в обход HTTP-прокси. Даже при активном SOCKS5-туннеле WebRTC может раскрыть локальный и публичный IP через STUN-запросы. Chrome исторически этим славился, Firefox закрыл дыру только в версии 96 (2022 год). Safari до сих пор отдаёт локальные адреса в некоторых сценариях.
Ещё один канал — DNS. Прокси-клиент может туннелировать HTTP, но DNS-запросы уходят через системный резолвер. Провайдер видит, какие домены вы резолвите. И даже если трафик идёт через прокси, DNS-утечка выдаёт паттерн поведения.
Третий канал — IPv6. Многие прокси-сервисы поддерживают только IPv4. Если сайт имеет AAAA-запись, браузер предпочтёт IPv6-маршрут (RFC 6724), который пойдёт напрямую мимо прокси. Классическая утечка.
Архитектура fail-closed: три уровня защиты
Fail-closed означает: нет прокси — нет соединения. Никаких прямых маршрутов. Реализуется на трёх уровнях, и все три нужны.
Первый уровень — маршрутизация на уровне ОС. Если трафик физически не может уйти мимо туннеля, браузер не сможет утечь даже при желании. Второй уровень — настройки прокси в браузере, чтобы он не пытался обходить туннель. Третий — блокировка WebRTC и прочих обходных каналов.
Начинать надо с ОС, потому что браузерные настройки легко слетают. Расширение обновилось — сбросилось. Профиль повредился — сбросилось. Пользователь нажал «отключить» — сбросилось. А маршрутизация через iptables или nftables работает независимо от того, что творится в браузере.
Маршрутизация через nftables с kill switch
Самый надёжный способ — заблокировать весь исходящий трафик, кроме туннельного интерфейса. На Linux это делается через nftables. Пример для WireGuard-интерфейса `wg0`:
```bash
nft add table inet fw
nft add chain inet fw output '{ type filter hook output priority 0; policy drop; }'
nft add rule inet fw output oifname "lo" accept
nft add rule inet fw output oifname "wg0" accept
nft add rule inet fw output ct state established,related accept
nft add rule inet fw output ip daddr 10.8.0.1 udp dport 51820 accept
```
Последнее правило разрешает установку самого туннеля. Без него WireGuard не сможет подключиться, и вы останетесь без сети вообще. Это и есть fail-closed: туннель упал — весь трафик дропается, браузер получает `ENETUNREACH` вместо реального соединения.
Для SOCKS5-прокси схема другая. Прокси-клиент обычно слушает localhost:1080, а приложение ходит туда. Блокировать надо всё, кроме localhost и адреса прокси-сервера:
```bash
nft add rule inet fw output ip daddr 127.0.0.1 accept
nft add rule inet fw output ip daddr
nft add rule inet fw output oifname "lo" accept
```
Обратите внимание: правила разрешают соединение только с конкретным IP прокси. Если прокси-сервер имеет несколько адресов (например, балансировка), нужно разрешить все. Иначе после переключения на резервный узел трафик заблокируется.
Настройка браузера: PAC-файл против ручных настроек
Ручные настройки прокси в браузере — это зоопарк. Chrome использует системные настройки, Firefox — свои собственные, Safari — свои. Расширения вроде FoxyProxy добавляют ещё один слой. Легко запутаться, что где настроено.
PAC-файл (Proxy Auto-Config) даёт централизованный контроль. Это JavaScript-функция `FindProxyForURL`, которая возвращает строку с типом прокси. Для fail-closed нужна конструкция, которая возвращает `DIRECT` только для явно разрешённых адресов, а для всего остального — `PROXY`:
```javascript
function FindProxyForURL(url, host) {
if (isPlainHostName(host) || shExpMatch(host, "*.local")) {
return "DIRECT";
}
if (isInNet(dnsResolve(host), "10.0.0.0", "255.0.0.0")) {
return "DIRECT";
}
return "SOCKS5 127.0.0.1:1080; SOCKS5 127.0.0.1:1081";
}
```
Два адреса в строке — резервные. Если первый прокси не отвечает, браузер попробует второй. Если оба упали — соединение не установится. Никакого `DIRECT` в конце. Это ключевой момент: многие PAC-файлы заканчиваются на `DIRECT`, что автоматически делает схему fail-open.
PAC-файл можно раздавать через локальный HTTP-сервер или через `file://`. Второй вариант проще, но некоторые браузеры (Chrome) не принимают `file://` в настройках прокси без флага. Придётся поднять nginx на localhost:
```nginx
server {
listen 127.0.0.1:8080;
location /proxy.pac {
alias /etc/proxy/proxy.pac;
add_header Content-Type application/x-ns-proxy-autoconfig;
}
}
```
Блокировка WebRTC
WebRTC — главная дыра в любой прокси-схеме. Протокол использует UDP и STUN для обнаружения внешнего адреса. Прокси тут ни при чём: браузер сам устанавливает соединение и сообщает сайту локальный и публичный IP.
В Firefox есть настройка `media.peerconnection.ice.default_address_only` в `about:config`. Она заставляет WebRTC использовать только адрес, полученный через прокси. Ещё одна — `media.peerconnection.ice.no_host` — полностью отключает передачу локальных адресов. Обе нужно выставить в `true`.
В Chrome такой настройки нет. Расширения вроде WebRTC Leak Prevent работают через API `chrome.privacy.network.webRTCIPHandlingPolicy`. Значения: `default`, `default_public_and_private_interfaces`, `default_public_interface_only`, `disable_non_proxied_udp`. Последнее — то, что нужно: WebRTC будет использовать только UDP через прокси.
Но расширения — это костыль. Они могут отключиться, обновиться, сломаться. Надёжнее заблокировать STUN-трафик на уровне сети. STUN работает через UDP на порту 3478 (и 5349 для TLS). Правило в nftables:
```bash
nft add rule inet fw output udp dport 3478 drop
nft add rule inet fw output udp dport 5349 drop
```
Если WebRTC нужен для работы (видеозвонки), придётся разрешить STUN через прокси-интерфейс. Но тогда браузер может утечь адрес туннеля, а не реальный — это уже приемлемо.
DNS-утечки и их закрытие
DNS-запросы — отдельная история. Браузер может резолвить домены сам, через системный резолвер, через DoH (DNS over HTTPS) или через прокси. В fail-closed схеме все запросы должны идти через туннель.
В Firefox настройка `network.proxy.socks_remote_dns` в `about:config` заставляет резолвить имена на стороне прокси. Это правильно: DNS-сервер провайдера не видит, какие домены вы запрашиваете. В Chrome аналогичная настройка называется «Use secure DNS» и включается через `chrome://settings/security`. Но она может использовать публичные DoH-серверы (Cloudflare, Google), которые видят ваши запросы.
Лучший вариант — направить DNS через сам туннель. Если у вас WireGuard, DNS-сервер внутри туннеля (например, `10.8.0.1`) резолвит всё. Если SOCKS5 — используйте `socks_remote_dns` и не настраивайте DoH вообще. Провайдер увидит только зашифрованный трафик к прокси.
Проверить утечку DNS можно через `dnsleaktest.com` или `ipleak.net`. Оба сервиса показывают, какие DNS-серверы видны сайту. Если там ваш провайдер — утечка есть.
Тест на утечки: что и как проверять
Простой тест: отключите прокси и посмотрите, работает ли интернет. Если работает — fail-closed не настроен. Это базовая проверка, но она не покрывает все каналы.
Более серьёзный тест — с имитацией падения прокси. Остановите прокси-сервер (`systemctl stop wg-quick@wg0` или `kill` процесса SOCKS5) и проверьте, что браузер не может открыть ни одну страницу. Затем проверьте через `curl`, что трафик реально дропается:
```bash
curl -v --max-time 5 https://ifconfig.me
```
Если соединение висит и падает по таймауту — хорошо. Если возвращает IP — плохо, трафик уходит напрямую.
Отдельно проверьте WebRTC. Откройте `chrome://webrtc-internals` или `about:webrtc` в Firefox и посмотрите, какие адреса видны. Там не должно быть вашего реального IP. Только адреса внутри туннеля или `0.0.0.0`.
Ещё один тест — IPv6. Многие прокси не поддерживают IPv6, и браузер может использовать его в обход. Проверьте через `test-ipv6.com`: если сайт видит ваш IPv6-адрес, а прокси работает только по IPv4 — утечка. Решение: отключить IPv6 на сетевом интерфейсе или добавить правило в nftables:
```bash
nft add rule inet fw output ip6 daddr ::/0 drop
```
Кейс: nginx как прокси-шлюз с проверкой
Пример из практики. Сервер на nginx 1.24 выступает в роли forward-прокси для группы из 12 машин. Задача — не пускать трафик напрямую при падении upstream. Конфигурация:
```nginx
stream {
upstream proxy_pool {
server 10.0.1.10:1080 max_fails=2 fail_timeout=10s;
server 10.0.1.11:1080 backup;
}
server {
listen 127.0.0.1:1080;
proxy_pass proxy_pool;
proxy_connect_timeout 3s;
proxy_timeout 30s;
proxy_next_upstream on;
}
}
```
`proxy_next_upstream on` переключает на резервный узел при падении основного. Если оба упали — соединение не устанавливается, клиент получает ошибку. Важно: `proxy_next_upstream` не должен быть `off`, иначе при падении первого узла клиент сразу получит отказ, а failover не сработает.
Проблема была в том, что клиенты использовали `curl` без `--proxy-fail`. При падении прокси curl пытался идти напрямую, если в окружении был `HTTP_PROXY` и `NO_PROXY` с широкой маской. Исправление — явное указание `--proxy` и удаление `NO_PROXY`:
```bash
curl --proxy socks5h://127.0.0.1:1080 --noproxy "" https://api.example.com
```
`--noproxy ""` отключает все исключения. `socks5h` (с буквой h) заставляет резолвить DNS на стороне прокси. Без `h` curl резолвит локально и утекает DNS.
Кейс: Firefox с жёсткой политикой
Второй пример — корпоративный парк из 200 машин на Firefox ESR 115. Нужно было запретить любые прямые соединения. Решение — `policies.json` в директории установки:
```json
{
"policies": {
"Proxy": {
"Mode": "auto",
"AutoConfigURL": "http://127.0.0.1:8080/proxy.pac",
"Locked": true
},
"Preferences": {
"network.proxy.socks_remote_dns": {
"Value": true,
"Status": "locked"
},
"media.peerconnection.ice.no_host": {
"Value": true,
"Status": "locked"
},
"media.peerconnection.ice.default_address_only": {
"Value": true,
"Status": "locked"
}
}
}
}
```
`Locked: true` запрещает пользователю менять настройки через интерфейс. `Status: "locked"` делает то же для `about:config`. Пользователь не может отключить прокси или включить WebRTC-утечку, даже если очень захочет.
Проблема всплыла на машинах с IPv6. Firefox предпочитал IPv6-маршрут, который шёл мимо PAC-файла. Пришлось добавить в `policies.json` ещё одну настройку — `network.dns.disableIPv6: true`. После этого утечки прекратились. Проверяли через `ipleak.net` на всех 200 машинах — ни одного реального адреса.
Кейс: SOCKS5 через lexic.ml с проверкой на утечки
Третий пример — индивидуальный пользователь, которому нужен стабильный IPv6-прокси для работы с зарубежными сервисами. Настройка через lexic.ml даёт пул IPv6-адресов, но клиент должен быть настроен на fail-closed, иначе при падении туннеля реальный адрес утечёт. Конфигурация клиента на Python с проверкой:
```python
import requests
import socket
PROXY = "socks5h://user:pass@proxy.lexic.ml:1080"
TEST_URL = "https://ifconfig.me/ip"
def check_leak():
try:
r = requests.get(TEST_URL, proxies={"https": PROXY}, timeout=5)
return r.text.strip()
except requests.exceptions.ProxyError:
return None
except requests.exceptions.Timeout:
return None
ip = check_leak()
if ip is None:
print("proxy down, connection blocked")
else:
print(f"exit ip: {ip}")
```
Ключевой момент — `socks5h` вместо `socks5`. Без `h` библиотека `requests` резолвит DNS локально, и провайдер видит запросы. С `h` резолв идёт на стороне прокси. Проверка через `ifconfig.me` показывает IPv6-адрес из пула, а не реальный.
Дополнительно настроили блокировку IPv6 на локальной машине через `sysctl`:
```bash
sysctl -w net.ipv6.conf.all.disable_ipv6=1
sysctl -w net.ipv6.conf.default.disable_ipv6=1
```
Это гарантирует, что браузер не попытается использовать IPv6-маршрут в обход прокси. Минус — IPv6-сайты станут недоступны напрямую, но через прокси они работают, потому что туннель поддерживает IPv6.
Чек-лист проверки
Собрать всё вместе. После настройки fail-closed прогоните эти проверки:
| Проверка | Инструмент | Ожидаемый результат |
|---|---|---|
| Падение прокси | `systemctl stop wg-quick@wg0` | Браузер не открывает страницы |
| Утечка IP | `ipleak.net` | Только адрес прокси |
| Утечка DNS | `dnsleaktest.com` | DNS-сервер прокси, не провайдера |
| WebRTC | `chrome://webrtc-internals` | Нет реального IP |
| IPv6 | `test-ipv6.com` | Адрес прокси или недоступность |
| Прямой запрос | `curl --max-time 5 https://ifconfig.me` | Таймаут |
Если хоть один пункт не проходит — схема дырявая. Каждая утечка — это канал, через который уходит реальный адрес. Fail-closed означает, что все каналы закрыты, а не только основной.
Почему fail-open так популярен
Провайдеры и разработчики прокси-клиентов выбирают fail-open по простой причине: пользователь не любит, когда интернет «ломается». Если прокси упал, fail-open даёт хоть какой-то доступ. Пользователь не звонит в поддержку. Метрики хорошие.
Но для задач анонимности это неприемлемо. Один запрос напрямую — и адрес раскрыт. Никакие последующие действия не помогут: сайт уже знает, откуда вы. Логи сохраняются, IP попадает в чёрные списки, и восстановить анонимность невозможно.
Поэтому в схемах, где важна приватность, fail-closed — не опция, а обязательное требование. Настройка занимает час, проверка — ещё час. Зато потом можно спать спокойно.
Что делать при падении прокси
Даже с fail-closed остаётся вопрос: что делать, когда прокси упал? Ответ простой: ждать восстановления. Или переключиться на резервный узел, если он настроен. Но ни в коем случае не отключать блокировку «на минутку, чтобы проверить почту». Именно в эту минутку и утекает всё.
Хорошая практика — мониторинг доступности прокси. Простой скрипт на cron каждые 30 секунд проверяет соединение и логирует падения:
```bash
!/bin/bash
if ! curl -s --max-time 5 --proxy socks5h://127.0.0.1:1080 https://ifconfig.me > /dev/null; then
logger -t proxy-monitor "proxy down at \$(date -Iseconds)"
fi
```
Логи покажут, как часто падает туннель. Если чаще раза в сутки — пора менять провайдера или настраивать резервирование. Стабильность прокси — это часть fail-closed схемы, потому что нестабильный прокси означает постоянные отказы в работе.