В прошлой статье я разобрал базовую настройку v2rayN и подключение к серверу. Здесь — продолжение, уже про routing.
Именно на этом этапе обычно всплывают странные симптомы: не открывается localhost, dev-домены вроде *.test и *.project начинают идти через прокси, а Docker и локальные сервисы внезапно ловят таймауты. Проблема почти всегда не в самом сервере, а в том, как локальный трафик проходит через правила маршрутизации.
Почему после включения прокси начинает ломаться локалка
В v2rayN есть две разные вещи, которые легко перепутать. Первая — system proxy: он просто направляет трафик части программ в локальный inbound ядра. Вторая — routing внутри Xray: он уже решает, куда отправить соединение дальше, в direct или в proxy. Правила в Xray проверяются сверху вниз, срабатывает первое совпадение, а если совпадений нет, трафик уходит через первый outbound. Поэтому, если для локальных адресов и dev-доменов нет отдельного direct-правила, они вполне могут улетать в прокси.



Из этого же следует важный практический момент: routing вообще ничего не решает для приложений, которые не отправляют трафик в локальный прокси. На десктопе это обычно браузер с включённым system proxy, приложение с ручной настройкой 127.0.0.1:порт, либо TUN-режим. Если программа идёт в сеть напрямую, Xray её просто не видит.
Что в типовой схеме я поправил
Самая важная правка — не объединять домены и IP в одно правило, если вы хотите ловить и доменные запросы, и обращения по “голому” IP. В Xray это не логика “или”, а логика “и”: когда в одном правиле заполнено несколько полей, они должны совпасть одновременно. Поэтому правило, где в одном месте прописаны и domain, и ip, может сработать для my.project, который резолвится в 127.0.0.1, но не поймает запрос на 172.18.0.5, если приложение ходит туда напрямую по IP. Это как раз та причина, по которой в примере из wiki v2rayN private domains и private IP вынесены в разные правила.
Второй момент — формат доменных правил. Строки без префикса в поле domain трактуются Xray как substring match. То есть .test действительно сработает для домена вида api.test, но технически это просто поиск подстроки. Если нужен более строгий и предсказуемый вариант, лучше использовать full: и regexp:. Для поддоменов у Xray есть и форма domain:, но для локальных суффиксов удобнее регулярка по окончанию имени.
Если вы только выбираете новый dev-суффикс, я бы вообще не брал .dev и с осторожностью относился к .local. .test и .localhost зарезервированы именно для тестов и loopback-сценариев. .local имеет специальную семантику mDNS, а .dev — это реальный публичный TLD, причём он включён в HSTS preload list, то есть браузеры принудительно требуют там HTTPS. Для нового локального проекта разумный современный вариант — *.localhost или *.dev.localhost; для старых проектов можно просто оставить текущие имена и добавить их в direct-правила.
Третий момент — IPv6. В режиме IPOnDemand Xray при необходимости резолвит домены в IP и использует для сопоставления как IPv4, так и IPv6. Если в правилах есть только 127.0.0.1/32, а ::1/128 забыли, часть локальных запросов может пойти не туда только из-за того, что стек предпочёл IPv6.
Пошаговая схема, которая нормально работает
v2rayN у вас используется Xray core. Сам v2rayN умеет работать и с Xray, и с sing-box, но в интерфейсе это не одно и то же: у sing-box есть свой отдельный блок параметров, поэтому ниже речь именно про Xray-часть окна Routing Setting.
Открываем активный набор правил
Откройте Settings → Routing Setting и выберите тот набор, которым реально пользуетесь. Если у вас уже включён готовый профиль вроде RUv1-Всё, кроме РФ, его удалять не нужно. Правильная схема здесь не в том, чтобы выбросить пресет, а в том, чтобы добавить свои direct-правила выше него.

Выставляем Domain strategy
Если у вас в наборе правил есть IP-условия, AsIs для такой схемы не подходит: доменные запросы не будут использовать IP-матчинг. Для dev-машины с локальными IP и Docker я бы ставил IPOnDemand. Этот режим разрешает Xray резолвить домен при встрече с IP-правилами и использовать результат для routing. Формально IPIfNonMatch тоже возможен, но IPOnDemand в таком наборе обычно ведёт себя предсказуемее.
Создаём первое правило — локальные домены напрямую
В первом правиле заполняем только поле Domain.
Основные поля:
Rule Type:RoutingoutboundTag:direct
Строгий вариант для поля Domain:
full:localhost,regexp:.(localhost|local|lan|test|dev|project)$
Если нужен более простой и визуально понятный вариант, можно оставить и так:
localhost,.localhost,.local,.lan,.test,.dev,.project
Но здесь важно понимать разницу: второй вариант — это не “магические суффиксы v2rayN”, а обычные строки, которые Xray потом использует как substring match. Для публикации я бы всё-таки оставил строгий вариант через full: и regexp:.

Создаём второе правило — локальные IP, Docker и link-local напрямую
Во втором правиле поле Domain оставляем пустым и заполняем только IP or IP CIDR.
Основные поля те же:
Rule Type:RoutingoutboundTag:direct
Поле IP or IP CIDR:
127.0.0.1/32,::1/128,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,fc00::/7,fe80::/10
Этого списка обычно хватает для loopback, RFC1918-сетей, Docker-бриджей и link-local адресов на обычной workstation. Если у вас есть свои нестандартные сети, просто добавьте их отдельными CIDR. Поля protocol, inboundTag и network здесь не трогаем.
Протоколы и дополнительные фильтры не включаем
Вот это как раз место, где очень легко всё сузить сильнее, чем вы хотели. Если в правиле отметить http, tls или что-то ещё, правило станет матчить только этот тип трафика. В документации Xray отдельно оговорено, что protocol — это дополнительное условие, а http в этом контексте относится только к HTTP/1.0 и 1.1. Для обычного bypass локалки это не нужно.
Поднимаем оба direct-правила наверх и оставляем catch-all proxy
После создания правил поднимите их выше любых geoip:private, geosite:private и региональных пресетов. Логика Xray простая: сверху вниз, первое совпадение победило. И ещё один важный момент: внизу должен остаться итоговый proxy-rule. Если вы собираете набор с нуля, добавьте его явно, например с port: 0-65535 и outboundTag: proxy; если ничего не совпадёт и такого правила нет, Xray отправит трафик через первый outbound по умолчанию.

DNS и проверка
DNS и routing — это разные уровни. hosts и DNS-серверы отвечают за то, во что вообще резолвится имя. Routing отвечает за то, через какой outbound пойдёт уже найденный адрес. Встроенный DNS Xray сначала проверяет hosts, а затем обращается к списку servers. Для routing-этапа он начинает участвовать, когда в routing.domainStrategy выбран IPIfNonMatch или IPOnDemand. Поэтому hosts может помочь локальному имени найти нужный IP, но сам по себе hosts не заменяет direct-правило.
Если локальный домен не живёт в системном hosts или локальном DNS, можно добавить его в Settings → DNS Settings → V2ray Custom DNS:
{
"hosts": {
"my.project": "127.0.0.1"
},
"servers": [
"1.1.1.1",
"8.8.8.8",
"https://dns.google/dns-query"
]
}

Проверять routing лучше не “голым” curl, а трафиком, который реально идёт в Xray: браузером при включённом system proxy, приложением с ручным локальным прокси или CLI с явно выставленными proxy env vars. В логе нужно увидеть что-то вроде:
accepted //my.project:443 [socks -> direct]
accepted //www.google.com:443 [socks -> proxy]
Если в логе уже видно [socks -> direct], а сайт всё равно не открывается, routing свою часть сделал правильно, и дальше имеет смысл смотреть уже в nginx, dev-сервер или контейнер, а не в прокси-правила. Архитектурно это уже другая проблема.

Если вы на Linux
На Linux кнопка Set system proxy в v2rayN не делает из вашей shell-сессии “проксированную”. В v2rayN это настраивается через скрипт, который пишет прокси в настройки рабочего окружения через gsettings и kwriteconfig; поэтому браузер или часть GUI-приложений могут использовать прокси, а curl, npm, docker pull и прочие CLI-утилиты — нет. На GitHub у v2rayN это видно и по самому скрипту, и по типичным Linux-issue, где пользователи отдельно отмечают, что http_proxy после Set system proxy не появляется.
Если вам нужно, чтобы терминал тоже ходил через v2rayN, прокси-переменные придётся выставлять явно. Для curl и большинства утилит это стандартный механизм: http_proxy, HTTPS_PROXY, общий ALL_PROXY и список исключений NO_PROXY. npm тоже умеет читать HTTP_PROXY/HTTPS_PROXY, а Docker daemon читает HTTP_PROXY/HTTPS_PROXY/NO_PROXY на старте или те же значения из daemon.json.
Если у вас стандартный локальный mixed-порт — 10808, то минимальный набор выглядит так:
export http_proxy="http://127.0.0.1:10808"
export https_proxy="http://127.0.0.1:10808"
export ALL_PROXY="socks5://127.0.0.1:10808"
export NO_PROXY="localhost,127.0.0.1,::1,.localhost,.local,.lan,.test,.dev,.project"
У NO_PROXY для доменных суффиксов имеет смысл оставлять значения с точкой впереди — так исключение распространяется на весь домен и его поддомены, а не только на один точный host.
Если у вас Docker должен тянуть внешние образы через прокси, а локальные вещи обходить, daemon можно настроить отдельно. Пример минимального daemon.json:
{
"proxies": {
"http-proxy": "http://127.0.0.1:10808",
"https-proxy": "http://127.0.0.1:10808",
"no-proxy": "localhost,127.0.0.1,::1,.localhost,.local,.lan,.test,.dev,.project,127.0.0.0/8"
}
}
После изменения этой конфигурации Docker daemon нужно перезапустить.
С TUN-режимом я бы для рабочего dev-хоста не спешил, особенно если параллельно подключён корпоративный VPN. В документации Xray TUN описан как отдельный виртуальный интерфейс, для которого маршрутами нужно управлять аккуратно, а ещё отдельно предупреждается про риск route loop. На практике это означает, что TUN — это уже почти отдельная сетевая топология, а не просто “галочка, чтобы всё заработало”. Для первой стабильной схемы proxy + system proxy + нормальный routing обычно намного проще.
Что получится в итоге
Если собрать схему именно так, она становится заметно предсказуемее. Локальные домены уходят в direct отдельным правилом. Локальные IP, Docker и приватные сети — тоже отдельным правилом. Внешний трафик забирает финальный proxy. DNS при этом лишь помогает имени найти адрес, но не подменяет сам routing. А на Linux терминальные утилиты получают прокси отдельно через переменные окружения, а не “магически” от desktop proxy.
Именно эту часть я бы и менял в исходной статье. Главное исправление здесь одно: вместо одного объединённого правила сделать два верхних direct-правила — для доменов и для IP. Всё остальное уже скорее доводка: строгий формат доменных масок, IPv6-префиксы, аккуратная DNS-настройка и отдельный Linux-блок для CLI. После этого v2rayN перестаёт мешать разработке и начинает делать ровно то, что от него и ждёшь.