Skip to content

Движок маршрутизации

Маршрутизатор оценивает входящие соединения по списку правил и выбирает, какой исходящий обработчик должен обработать трафик. Он поддерживает сопоставление по доменам/IP/портам, базы данных GeoIP/GeoSite и балансировку нагрузки.

Исходный код: app/router/router.go, app/router/condition.go, app/router/strategy_*.go

Структура маршрутизатора

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
}

Оценка правил

Правила оцениваются последовательно — первое совпавшее правило побеждает:

go
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
go
func (r *Router) applyDomainStrategy(ctx routing.Context) routing.Context {
    switch r.domainStrategy {
    case Config_IpIfNonMatch:
        // Первый проход: попробовать с доменом
        // Если нет совпадения: разрешить домен->IP, попробовать снова
    case Config_IpOnDemand:
        // Разрешать только при встрече правил на основе IP
    }
}

Условия правил

Каждое правило имеет Condition, которое проверяет контекст маршрутизации:

go
type Rule struct {
    Condition Condition
    Tag       string     // тег исходящего
    RuleTag   string     // идентификатор правила для логирования
    Balancer  *Balancer  // или nil
}

type Condition interface {
    Apply(ctx routing.Context) bool
}

Типы условий

УсловиеПолеОписание
DomainMatcherdomainСопоставление целевого домена (full, substr, regex, domain)
GeoIPMatcheripСопоставление целевого IP по базе GeoIP
MultiGeoIPMatchergeoipНесколько GeoIP-матчеров (коды стран)
PortMatcherportСопоставление целевого порта или диапазона портов
PortRangeMatcherportListСопоставление порта по диапазонам
NetworkMatchernetworkTCP, UDP или оба
ProtocolMatcherprotocolПерехваченный протокол (http, tls, bittorrent)
UserMatcheruserEmail аутентифицированного пользователя
InboundTagMatcherinboundTagТег входящего обработчика
AttributeMatcherattrsHTTP-атрибуты (перехваченные)
ConditionChan(составной)AND из нескольких условий

Контекст маршрутизации

Контекст маршрутизации предоставляет все поля, по которым правила могут выполнять сопоставление:

go
// 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, предоставляющий эффективные матчеры:

Типы матчеров

go
// 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:

go
// common/strmatcher/mph_matcher.go
type MphIndexMatcher struct {
    rules   []matcherGroup  // группы матчеров по хеш-корзинам
    values  []uint32        // хеш-таблица
    level0  []uint32
    level1  []uint32
    // ... таблицы поиска MPH
}

Это обеспечивает поиск за O(1) для паттернов точного совпадения и совпадения по суффиксу домена, что значительно быстрее линейного сканирования.

GeoIP / GeoSite

GeoIP

Гео-сопоставление по IP использует отсортированный список CIDR-диапазонов с бинарным поиском:

go
// 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):

go
type GeoSiteList struct {
    Entry []*GeoSite  // код страны -> список доменов
}
type GeoSite struct {
    CountryCode string
    Domain      []*Domain  // с Type (full/domain/substr/regex)
}

Балансировка нагрузки

Когда правило указывает на балансировщик вместо прямого тега:

go
type Balancer struct {
    selectors []string          // шаблоны тегов исходящих
    strategy  BalancingStrategy // round-robin, random, least-ping, least-load
    ohm       outbound.Manager
}

Стратегии

СтратегияОписание
randomСлучайный выбор среди подходящих исходящих
roundRobinПоследовательная ротация
leastPingНаименьший RTT по данным зондирования observatory
leastLoadНаименьшее количество активных соединений / лучшее состояние
go
func (b *Balancer) PickOutbound() (string, error) {
    candidates := b.getMatchingOutbounds()
    return b.strategy.Pick(candidates)
}

Поток контекста маршрутизации

mermaid
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

Заметки по реализации

  1. Порядок правил важен: Побеждает первое совпадение. Пользователи ожидают, что их правила оцениваются сверху вниз.

  2. Стратегия доменов критична: IPIfNonMatch делает два прохода — сначала с доменом, затем с разрешёнными IP. Это влияет на производительность (DNS-запрос при отсутствии совпадения).

  3. Бинарный поиск для GeoIP: IP-диапазоны предварительно сортируются при загрузке. Используйте бинарный поиск, а не линейное сканирование.

  4. MPH для доменов: Для 100K+ доменных правил (типично для GeoSite) MPH даёт O(1) против O(n). Без этого сопоставление доменов становится узким местом.

  5. RouteTarget и Target: При использовании перехвата с routeOnly маршрутизатор видит RouteTarget (перехваченный домен), но исходящий использует Target (исходный IP).

  6. Состояние балансировщика: leastPing и leastLoad зависят от функции Observatory, которая периодически зондирует состояние исходящих обработчиков.

Технический анализ для целей повторной реализации.