todo/xray/routing.md
loop-uh 08b4491545
Some checks failed
Published content check / validate (push) Failing after 3s
Завершить переезд базы знаний на Forgejo
Обновить правила репозитория и подписи исходников в 99 заметках, не затрагивая пользовательские незакоммиченные файлы. Сделать Forgejo Actions содержательным: проверять опубликованный commit, а не пустое рабочее дерево после checkout.
2026-08-07 08:27:28 +03:00

232 lines
26 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
date: 2026-07-17
tags:
- xray
- routing
- geoip
- geosite
- balancer
- config
aliases:
- Маршрутизация Xray
- Xray routing
- RoutingObject
- domainStrategy
- Балансировщик Xray
link: https://xtls.github.io/ru/config/routing.html
---
# 🧭 Маршрутизация в Xray: как трафик распределяется по outbound
> [!info] О чём заметка
> Разбор модуля **маршрутизации** (routing) в [[xray/project-x|Xray-core]] — механизма, который для каждого соединения решает, через какой исходящий канал (outbound) его отправить: напрямую, через прокси или заблокировать. Это то, что на практике позволяет открывать российские сайты мимо туннеля, а заблокированные — через сервер (см. [[xray/clients-and-routing|практику раздельной маршрутизации]]). Здесь — устройство правил и стратегий, сверенное с официальной документацией и с чтением исходного кода `app/router/` и `app/dispatcher/`. Про сами протоколы, поверх которых всё это работает — [[xray/vless|VLESS]], [[xray/reality|REALITY]], [[xray/xtls-vision|XTLS/Vision]].
## TL;DR
- Маршрутизация отправляет входящий трафик в разные outbound по **правилам**. Типичная задача — разделить внутренний и внешний трафик (`.ru` напрямую, заблокированное через прокси).
- Правила проверяются **сверху вниз, первое сработавшее выигрывает** (first match wins). Если не сработало ни одно — трафик идёт в outbound по умолчанию.
- Внутри одного правила все условия соединяются логическим **И (AND)**; внутри одного условия (список доменов, IP, портов) — **ИЛИ (OR)**.
- **`domainStrategy`** решает, резолвить ли домен в IP для сопоставления: `AsIs` (не резолвить), `IPIfNonMatch` (резолвить, если правила не сработали), `IPOnDemand` (резолвить сразу).
- Есть встроенные списки: **`geosite:`** (домены, файл `geosite.dat`) и **`geoip:`** (IP-диапазоны стран, файл `geoip.dat`) — например `geoip:cn`, `geoip:private`, `geosite:category-ads-all`.
- **Балансировщики** распределяют трафик между несколькими outbound по стратегиям `random`/`roundRobin`/`leastPing`/`leastLoad`.
- Важно: резолв домена для маршрутизации **не меняет фактический адрес соединения** — цель остаётся исходной.
## Общая картина: как соединение проходит через маршрутизацию
Когда приходит соединение, диспетчер (`app/dispatcher`) делает так:
1. Определяет цель (адрес и порт) и, при включённом **sniffing**, «подсматривает» первые байты, чтобы распознать протокол (TLS/HTTP/QUIC/BitTorrent) и извлечь домен из SNI/Host.
2. Собирает **контекст маршрутизации** — целевой адрес, домен, тег входящего канала (inbound), источник, тип сети.
3. Вызывает роутер: `Router.PickRoute()` перебирает правила сверху вниз и возвращает первое подходящее.
4. Правило указывает `outboundTag` (или балансировщик) — трафик уходит в этот исходящий канал.
Проще говоря: маршрутизатор — это диспетчер на развилке. Он смотрит на каждое соединение, сверяет его со списком правил по порядку и на первом же совпадении направляет по нужной «дороге» (outbound). Не совпало ни с чем — едет по дороге по умолчанию.
> [!note] Куда идёт трафик, если правила не сработали
> Если ни одно правило не совпало, соединение уходит в **outbound по умолчанию** — на практике это **первый** исходящий канал в конфиге. Поэтому распространённый приём — поставить первым нужный дефолт (например прямое соединение или основной прокси), либо в самом конце правил добавить «ловушку» `"network": "tcp,udp"` без других условий, которая ловит вообще всё оставшееся и явно задаёт назначение.
## RoutingObject: структура
Секция `routing` в конфиге состоит из трёх частей:
```json
{
"routing": {
"domainStrategy": "AsIs",
"rules": [],
"balancers": []
}
}
```
- `domainStrategy` — стратегия разрешения доменов (см. ниже).
- `rules` — массив правил, проверяются по порядку.
- `balancers` — конфигурации балансировщиков (опционально).
## domainStrategy: резолвить домен в IP или нет
Это, пожалуй, самая недопонятая настройка. Она определяет, **разрешает ли маршрутизатор доменное имя в IP-адрес** ради сопоставления с IP-правилами (`geoip:` и т.п.). Три значения:
- **`AsIs`** (по умолчанию) — никакого резолва. Работает только с доменом (из цели или из sniffing). IP-правила для доменной цели не сработают, потому что IP просто неоткуда взять.
- **`IPIfNonMatch`** — сначала правила прогоняются как есть (по домену). Если **ни одно не совпало**, домен резолвится в IP, и **весь список правил прогоняется заново** — уже с IP. Это даёт доменным правилам приоритет, а IP-правила служат «вторым проходом».
- **`IPOnDemand`** — домен резолвится в IP **сразу**, ещё до первого прохода, и IP доступен всем правилам с самого начала.
> [!note] Разрешение ленивое — чтобы не тормозить
> Даже в `IPOnDemand` фактический DNS-запрос не делается заранее: он откладывается до **первого правила с IP-условием**, которое реально запросит IP (в коде — `ResolvableContext.GetTargetIPs`, результат кешируется). Если раньше сработало доменное или другое правило — резолв вообще не понадобится, и задержки не будет. Результат содержит и IPv4, и IPv6; сузить семейство можно через `queryStrategy` встроенного DNS.
> [!important] Резолв НЕ меняет реальный адрес соединения
> Разрешение домена в IP используется **только для сопоставления IP-правил**. Фактическая цель запроса остаётся исходной — домен уходит на выбранный outbound и резолвится уже там (транспортом или сервером). Это подтверждается кодом: маршрутизатор держит IP в отдельном кеше поверх контекста и никогда не переписывает целевой адрес.
Отдельно: при включённых **sniffing + routeOnly** маршрутизатор видит и IP, и домен, распознанный из трафика. Домен из sniffing имеет **приоритет** над исходным целевым адресом — и при резолве, и при сопоставлении.
## RuleObject: правило и его условия
Правило описывает набор условий и цель (`outboundTag` или `balancerTag`).
> [!danger] Все условия внутри правила — логическое И
> Если в одном правиле указано несколько условий (например `domain` и `port`), они должны выполниться **одновременно**, иначе правило не срабатывает. А вот внутри одного условия (список доменов, список IP, список портов) действует **ИЛИ** — достаточно совпадения любого элемента.
### Условие по домену (`domain`)
Список шаблонов домена. Типы (по префиксу):
| Префикс | Тип | Пример | Матчит |
|---|---|---|---|
| `full:` | Полное совпадение | `full:xray.com` | только `xray.com` |
| `domain:` | Домен и поддомены (рекомендуется) | `domain:xray.com` | `xray.com`, `www.xray.com`, но не `wxray.com` |
| `keyword:` | Подстрока | `keyword:sina.com` | `sina.com`, `sina.com.cn`, `www.sina.com` |
| `regexp:` | Регулярное выражение | `regexp:\.goo.*\.com$` | `www.google.com`, но не `google.com` |
| `dotless:` | Домен без точек | `dotless:pc-` | `pc-alice` (для NetBIOS-имён во внутренней сети) |
| `geosite:` | Встроенный список | `geosite:cn` | все домены из списка `cn` |
| `ext:` | Список из файла | `ext:geosite.dat:cn` | эквивалент `geosite:cn` |
> [!warning] «Голый» домен без префикса — это keyword (подстрока), а не полное совпадение
> Если написать в `domain` просто `"baidu.com"` без префикса, ядро трактует это как **подстроку** (`keyword`), а не как точное имя. Это подтверждается кодом (тип по умолчанию — `Domain_Substr`). Чтобы поймать домен и поддомены, используйте явный `domain:baidu.com`; для точного совпадения — `full:`.
Технически на десктопе домены матчатся через MPH (minimal perfect hash) — быструю структуру; на iOS/Android — линейным перебором. Регулярки чувствительны к регистру.
### Условие по IP (`ip`, `sourceIP`, `localIP`)
Список диапазонов. Форматы: одиночный IP (`127.0.0.1`), CIDR (`10.0.0.0/8`, `::/0`), встроенный список стран (`geoip:cn`), спецзначение `geoip:private` (все приватные адреса), файл (`ext:geoip.dat:cn`).
**Инверсия `!`** — важная и хитрая деталь. `!geoip:cn` означает «всё, что НЕ входит в geoip:cn». Логика объединения по документации: несколько отрицаний соединяются через **И (AND)**, а положительные условия и совокупность отрицательных — через **ИЛИ (OR)**.
Пример: `ip: ["!geoip:cn", "!geoip:us", "geoip:telegram"]` матчит IP, которые «не Китай И не США», ИЛИ являются IP Telegram.
> [!note] Нюанс реализации инверсии
> По коду AND между отрицаниями держится строго внутри одной «корзины» — отдельно для CIDR и отдельно для geoip. То есть `!geoip:cn` + `!geoip:us` действительно объединяются через AND, но отрицаемый CIDR и отрицаемый `!geoip:cn` окажутся в разных корзинах и соединятся через OR. Для типовых конфигов (несколько `!geoip:`) поведение совпадает с документацией.
### Условия по портам и сети
- `port` — порт назначения. Форматы: `443`, диапазон `1000-2000`, смесь через запятую `53,443,1000-2000`.
- `sourcePort` — порт источника (тот же формат).
- `localPort` — порт локального inbound (полезно, когда inbound слушает диапазон портов).
- `network``tcp`, `udp` или `tcp,udp`. Условие `"network": "tcp,udp"` без других полей ловит **весь** трафик (ядро на 4-м уровне знает только TCP и UDP) — удобно как catch-all в конце списка правил.
### Прочие условия
- `sourceIP` (псевдоним `source`) — IP источника (IP/CIDR/geoip).
- `localIP` — IP, на который пришёл inbound (при `listen: 0.0.0.0`). Не работает для UDP.
- `user` — email пользователя-источника; поддерживает `regexp:`.
- `inboundTag` — тег входящего канала, через который пришло соединение.
- `protocol``http` / `tls` / `quic` / `bittorrent`. **Требует включённого sniffing**, иначе тип пуст. Матч по префиксу (правило `http` поймает и распознанный `http1`).
- `attrs` — сопоставление HTTP-заголовков и псевдозаголовков h2 (`:method`, `:path`), например `{":method": "GET"}`. Тоже требует sniffing; все указанные ключи должны совпасть (AND), значения поддерживают регулярки.
- `ruleTag` — метка правила. На маршрутизацию не влияет, но при срабатывании пишет в лог (уровень Info) строку `Hit route rule: [<ruleTag>]` — удобно для отладки, какое правило сработало.
### Условие по процессу (`process`)
Сопоставление по процессу-источнику — работает только для **локальных** соединений (Windows/Linux). Три режима: имя процесса (`curl` — ядро само отбрасывает `.exe`), абсолютный путь (`C:/Windows/System32/curl.exe`, слеши всегда прямые), папка (путь с завершающим `/` — все процессы внутри).
Два синтаксических сахара очень полезны:
- **`self/`** — матчит **сам процесс ядра Xray**. Главное применение — защита от **петель маршрутизации** (routing loops): трафик, порождённый самим ядром, не заворачивается обратно в себя.
- **`xray/`** — матчит **все процессы Xray** из того же бинарника (подставляется абсолютный путь к текущему исполняемому файлу).
На Android для сопоставления по приложениям нужно, чтобы клиентская обёртка зарегистрировала искатель процессов через `RegisterAndroidProcessFinder()`.
### vlessRoute: маршрутизация по байтам UUID
Необычная возможность: VLESS-клиент может изменить **7-й и 8-й байты своего UUID** (в записи `xxxxxxxx-xxxx-0000-xxxx-...` это выделенные нули) на любое значение и использовать их как данные маршрутизации `vlessRoute`. Это позволяет клиенту управлять частью серверной маршрутизации, **не меняя никаких внешних полей** конфига.
Значение кодируется как big-endian uint16 (проще: воспринимайте эти 4 hex-символа как шестнадцатеричное число → в десятичное: `0001`→1, `000e`→14, `38b2`→14514), а в правиле `vlessRoute` пишется в том же синтаксисе, что и порты (одиночные значения и диапазоны).
Механика (по коду): при проверке пользователя эти два байта **обнуляются** (`ProcessUUID` в `validator.go`), поэтому аутентификация по UUID не ломается — а исходное значение байтов сервер запоминает и использует для матчинга правила.
### webhook: уведомление при срабатывании правила
Опционально к правилу можно привязать `webhook` — при попадании в правило ядро шлёт **POST-запрос** с JSON о соединении (email, protocol, network, source, destination, originalTarget, routeTarget, inboundTag, outboundTag, ts и др.). Полезно для алертов и статистики (например, «зафиксирован bittorrent»).
Поля объекта: `url` (обычный HTTP(S)-адрес **или** путь Unix-сокета, в т.ч. abstract-сокет `@abstract` / `@@padded` на Linux), `deduplication` (окно в секундах — повторные события по тому же пользователю в этот период не шлются), `headers` (доп. заголовки запроса, например `X-API-Key`).
## Балансировщики нагрузки
Если правило указывает `balancerTag` вместо `outboundTag`, Xray выбирает конкретный outbound через **балансировщик**. Балансировщик описывается в `balancers`:
```json
{
"tag": "balancer",
"selector": ["out"],
"fallbackTag": "outbound",
"strategy": { "type": "roundRobin" }
}
```
- `selector` — массив префиксов тегов. Балансировщик работает с теми outbound, чьи теги **начинаются** с указанных префиксов: `["a"]` захватит `a`, `ab`, `abc`. Это префиксное совпадение (`HasPrefix`), не точное.
- `fallbackTag` — куда уйти, если по данным наблюдения все outbound недоступны.
- `strategy` — стратегия выбора.
> [!tip] Если оба тега заданы — приоритет у outboundTag
> В одном правиле можно указать `outboundTag` и `balancerTag`, но действует только один. По документации приоритет у `outboundTag`.
### Четыре стратегии
| Стратегия | Как выбирает | Нужен наблюдатель |
|---|---|---|
| `random` (по умолчанию) | Случайный из кандидатов | Опционально |
| `roundRobin` | По очереди, циклически | Опционально |
| `leastPing` | С наименьшей задержкой | Да |
| `leastLoad` | Самый стабильный (по разбросу RTT) | Да (burst) |
- **`random` / `roundRobin`** могут работать и без наблюдателя. Если наблюдатель есть (подключается при заданном `fallbackTag`), недоступные узлы исключаются, а узлы **без данных наблюдения считаются доступными**.
- **`leastPing` / `leastLoad`** требуют наблюдателя обязательно; узлы, не покрытые наблюдением, **исключаются**. Если все недоступны и `fallbackTag` не задан — берётся outbound по умолчанию.
Данные о доступности и задержке узлов поставляет **observatory** (простой health-check: HTTP-запрос через каждый outbound, даёт `Alive` + задержку) или **burstObservatory** (богаче: скользящее окно замеров со статистикой — среднее, макс/мин, доля отказов, стандартное отклонение RTT). Для `leastLoad` нужен именно burst, потому что он опирается на разброс RTT.
### Настройки leastLoad
Стратегия `leastLoad` тонко настраивается через `settings`:
- `expected` — сколько лучших узлов оставить (трафик случайно распределяется между ними).
- `maxRTT` — отбрасывать узлы с задержкой выше порога.
- `tolerance` — допустимая доля неудачных замеров (например `0.01` = 1%).
- `baselines` — пороги стандартного отклонения RTT для отбора «стабильных» узлов.
- `costs` — веса для outbound (`regexp`/`match`/`value`); чем больше `value`, тем менее вероятен выбор узла.
По коду leastLoad сортирует узлы по «стоимости отклонения RTT» (стабильность важнее среднего пинга), отбирает лучшие `expected`, а внутри них выбирает случайно — так нагрузка равномерно ложится на несколько хороших узлов.
## Встроенные списки geosite и geoip
Xray поставляется с двумя файлами-справочниками:
- **`geosite.dat`** — именованные списки доменов, используются как `geosite:имя`. Полезные: `category-ads-all` (реклама + рекламные сети), `cn` (сайты материкового Китая), `google`, `microsoft`, `apple`, `telegram`, `geolocation-cn` / `geolocation-!cn` (китайские / некитайские), `tld-cn` / `tld-!cn` (домены по TLD). Полный список — в [community-репозитории доменов](https://github.com/v2fly/domain-list-community).
- **`geoip.dat`** — списки IP-диапазонов по странам, используются как `geoip:код_страны` (почти все страны). Плюс спец-значение `geoip:private` (приватные подсети).
Именно на этих списках строятся типовые правила «китайские/российские сайты — напрямую, остальное — через прокси». Синтаксис `ext:файл:тег` позволяет подключать свои `.dat`-файлы из директории ресурсов.
## Практический смысл для обхода блокировок
Всё это — фундамент раздельной маршрутизации: правило с `geoip:ru`/`geosite:` и `outboundTag: direct` пускает российские сайты напрямую, а «ловушка» в конце гонит остальное через прокси-outbound. На стороне пользователя те же правила приезжают готовым набором (например через HAPP и `routing.help`) — как это выглядит на практике, разобрано в [[xray/clients-and-routing|заметке про клиенты и маршрутизацию]].
## 📚 См. также
- [[xray/clients-and-routing|Клиенты и маршрутизация]] — практика: готовые правила `.ru` напрямую
- [[xray/project-x|Project X (Xray-core)]] — обзор ядра и всех технологий
- [[xray/vless|Протокол VLESS]] — протокол, поверх которого работают outbound (в т.ч. `vlessRoute`)
- [[xray/reality|REALITY]] и [[xray/xtls-vision|XTLS и Vision]] — чем шифруется и оптимизируется прокси-outbound
- 🔗 [Официальная дока: маршрутизация](https://xtls.github.io/ru/config/routing.html) — первоисточник по RoutingObject
- 🔗 [v2fly/domain-list-community](https://github.com/v2fly/domain-list-community) — полный список geosite-доменов
---
> [!quote] 🤖 Эти статьи открыты — можно обучать на них ИИ
> При желании вы можете натренировать ИИ на наших статьях. Исходное форматирование и скачивание всего репозитория одним zip-архивом доступны в Forgejo: [исходник этой заметки](https://git.zapret.moe/zapretdiscordyoutube/todo/src/branch/main/xray/routing.md) · [весь репозиторий](https://git.zapret.moe/zapretdiscordyoutube/todo/src/branch/main).