Движок маршрутизации
Маршрутизатор оценивает входящие соединения по списку правил и выбирает, какой исходящий обработчик должен обработать трафик. Он поддерживает сопоставление по доменам/IP/портам, базы данных GeoIP/GeoSite и балансировку нагрузки.
Исходный код: app/router/router.go, app/router/condition.go, app/router/strategy_*.go
Структура маршрутизатора
// app/router/router.go
type Router struct {
domainStrategy Config_DomainStrategy
rules []*Rule
balancers map[string]*Balancer
dns dns.Client
ctx context.Context
ohm outbound.Manager
dispatcher routing.Dispatcher
}Оценка правил
Правила оцениваются последовательно — первое совпавшее правило побеждает:
func (r *Router) pickRouteInternal(ctx routing.Context) (*Rule, routing.Context, error) {
// Применение стратегии доменов (может выполнить DNS-разрешение)
ctx = r.applyDomainStrategy(ctx)
for _, rule := range r.rules {
if rule.Apply(ctx) {
return rule, ctx, nil
}
}
return nil, ctx, common.ErrNoClue
}Стратегия доменов
Стратегия доменов определяет, разрешает ли маршрутизатор доменные имена перед оценкой правил:
| Стратегия | Поведение |
|---|---|
AsIs | Использовать домен как есть; не разрешать DNS |
IPIfNonMatch | Сначала попробовать домен; если ни одно правило не совпало, разрешить в IP и повторить |
IPOnDemand | Разрешать домен в IP только когда правило требует сопоставления по IP |
func (r *Router) applyDomainStrategy(ctx routing.Context) routing.Context {
switch r.domainStrategy {
case Config_IpIfNonMatch:
// Первый проход: попробовать с доменом
// Если нет совпадения: разрешить домен->IP, попробовать снова
case Config_IpOnDemand:
// Разрешать только при встрече правил на основе IP
}
}Условия правил
Каждое правило имеет Condition, которое проверяет контекст маршрутизации:
type Rule struct {
Condition Condition
Tag string // тег исходящего
RuleTag string // идентификатор правила для логирования
Balancer *Balancer // или nil
}
type Condition interface {
Apply(ctx routing.Context) bool
}Типы условий
| Условие | Поле | Описание |
|---|---|---|
DomainMatcher | domain | Сопоставление целевого домена (full, substr, regex, domain) |
GeoIPMatcher | ip | Сопоставление целевого IP по базе GeoIP |
MultiGeoIPMatcher | geoip | Несколько GeoIP-матчеров (коды стран) |
PortMatcher | port | Сопоставление целевого порта или диапазона портов |
PortRangeMatcher | portList | Сопоставление порта по диапазонам |
NetworkMatcher | network | TCP, UDP или оба |
ProtocolMatcher | protocol | Перехваченный протокол (http, tls, bittorrent) |
UserMatcher | user | Email аутентифицированного пользователя |
InboundTagMatcher | inboundTag | Тег входящего обработчика |
AttributeMatcher | attrs | HTTP-атрибуты (перехваченные) |
ConditionChan | (составной) | AND из нескольких условий |
Контекст маршрутизации
Контекст маршрутизации предоставляет все поля, по которым правила могут выполнять сопоставление:
// features/routing/session/context.go
type Context struct {
Inbound *session.Inbound // источник, тег, пользователь
Outbound *session.Outbound // цель, routeTarget
Content *session.Content // перехваченный протокол, атрибуты
}
func (ctx *Context) GetTargetDomain() string
func (ctx *Context) GetTargetIPs() []net.IP
func (ctx *Context) GetSourceIPs() []net.IP
func (ctx *Context) GetInboundTag() string
func (ctx *Context) GetUser() string
func (ctx *Context) GetProtocol() string
func (ctx *Context) GetAttributes() map[string]stringСопоставление доменов
Сопоставление доменов использует пакет strmatcher, предоставляющий эффективные матчеры:
Типы матчеров
// common/strmatcher/strmatcher.go
const (
Full Type = 0 // Точное совпадение: "example.com"
Substr Type = 1 // Содержит: "example" совпадает с "test.example.com"
Domain Type = 2 // Суффикс домена: "example.com" совпадает с "a.b.example.com"
Regex Type = 3 // Регулярное выражение
)Оптимизация MPH (Minimal Perfect Hash)
Для больших списков доменов (GeoSite) Xray-core использует функцию Minimal Perfect Hash:
// common/strmatcher/mph_matcher.go
type MphIndexMatcher struct {
rules []matcherGroup // группы матчеров по хеш-корзинам
values []uint32 // хеш-таблица
level0 []uint32
level1 []uint32
// ... таблицы поиска MPH
}Это обеспечивает поиск за O(1) для паттернов точного совпадения и совпадения по суффиксу домена, что значительно быстрее линейного сканирования.
GeoIP / GeoSite
GeoIP
Гео-сопоставление по IP использует отсортированный список CIDR-диапазонов с бинарным поиском:
// app/router/condition_geoip.go
type GeoIPMatcher struct {
ip4 []ipv6 // отсортированные IPv4-диапазоны (приведённые к IPv6)
ip6 []ipv6 // отсортированные IPv6-диапазоны
}
func (m *GeoIPMatcher) Match(ip net.IP) bool {
// Бинарный поиск в отсортированном списке диапазонов
}GeoSite
Гео-сопоставление по доменам загружается из файлов .dat (сериализованных в protobuf):
type GeoSiteList struct {
Entry []*GeoSite // код страны -> список доменов
}
type GeoSite struct {
CountryCode string
Domain []*Domain // с Type (full/domain/substr/regex)
}Балансировка нагрузки
Когда правило указывает на балансировщик вместо прямого тега:
type Balancer struct {
selectors []string // шаблоны тегов исходящих
strategy BalancingStrategy // round-robin, random, least-ping, least-load
ohm outbound.Manager
}Стратегии
| Стратегия | Описание |
|---|---|
random | Случайный выбор среди подходящих исходящих |
roundRobin | Последовательная ротация |
leastPing | Наименьший RTT по данным зондирования observatory |
leastLoad | Наименьшее количество активных соединений / лучшее состояние |
func (b *Balancer) PickOutbound() (string, error) {
candidates := b.getMatchingOutbounds()
return b.strategy.Pick(candidates)
}Поток контекста маршрутизации
flowchart TB
Dispatch["Диспетчер получает<br/>(ctx, destination)"]
Sniff["Перехват: определение домена<br/>из TLS SNI / HTTP Host"]
Dispatch --> Sniff
Sniff --> SetCtx["Установка контекста маршрутизации:<br/>домен, IP, порт, протокол,<br/>тег входящего, пользователь"]
SetCtx --> Strategy{Стратегия доменов?}
Strategy -->|AsIs| Eval["Последовательная оценка правил"]
Strategy -->|IPIfNonMatch| Eval
Strategy -->|IPOnDemand| Eval
Eval --> Match{Правило совпало?}
Match -->|Да| Check{Есть балансировщик?}
Check -->|Да| Balance["Balancer.Pick()"]
Check -->|Нет| Tag["Использовать rule.Tag"]
Balance --> Handler["Получить исходящий обработчик"]
Tag --> Handler
Match -->|Нет, есть ещё правила| Eval
Match -->|Правил не осталось + IPIfNonMatch| Resolve["Разрешить домен->IP"]
Resolve --> Eval2["Повторная оценка с IP"]
Match -->|Правил не осталось| Default["Использовать исходящий по умолчанию"]
Eval2 --> Match2{Правило совпало?}
Match2 -->|Да| Check
Match2 -->|Нет| DefaultЗаметки по реализации
Порядок правил важен: Побеждает первое совпадение. Пользователи ожидают, что их правила оцениваются сверху вниз.
Стратегия доменов критична:
IPIfNonMatchделает два прохода — сначала с доменом, затем с разрешёнными IP. Это влияет на производительность (DNS-запрос при отсутствии совпадения).Бинарный поиск для GeoIP: IP-диапазоны предварительно сортируются при загрузке. Используйте бинарный поиск, а не линейное сканирование.
MPH для доменов: Для 100K+ доменных правил (типично для GeoSite) MPH даёт O(1) против O(n). Без этого сопоставление доменов становится узким местом.
RouteTarget и Target: При использовании перехвата с
routeOnlyмаршрутизатор видитRouteTarget(перехваченный домен), но исходящий используетTarget(исходный IP).Состояние балансировщика:
leastPingиleastLoadзависят от функции Observatory, которая периодически зондирует состояние исходящих обработчиков.