Some checks failed
Published content check / validate (push) Failing after 3s
Обновить правила репозитория и подписи исходников в 99 заметках, не затрагивая пользовательские незакоммиченные файлы. Сделать Forgejo Actions содержательным: проверять опубликованный commit, а не пустое рабочее дерево после checkout.
232 lines
26 KiB
Markdown
232 lines
26 KiB
Markdown
---
|
||
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).
|