Skip to content

Routing Engine

The router evaluates incoming connections against a list of rules and selects which outbound handler should process the traffic. It supports domain/IP/port matching, GeoIP/GeoSite databases, and load balancing.

Source: app/router/router.go, app/router/condition.go, app/router/strategy_*.go

Router Structure

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
}

Rule Evaluation

Rules are evaluated sequentially — the first matching rule wins:

go
func (r *Router) pickRouteInternal(ctx routing.Context) (*Rule, routing.Context, error) {
    // Apply domain strategy (may resolve DNS)
    ctx = r.applyDomainStrategy(ctx)

    for _, rule := range r.rules {
        if rule.Apply(ctx) {
            return rule, ctx, nil
        }
    }
    return nil, ctx, common.ErrNoClue
}

Domain Strategy

The domain strategy controls whether the router resolves domain names before evaluating rules:

StrategyBehavior
AsIsUse domain as-is; don't resolve DNS
IPIfNonMatchTry domain first; if no rule matches, resolve to IP and retry
IPOnDemandResolve domain to IP only when a rule needs IP matching
go
func (r *Router) applyDomainStrategy(ctx routing.Context) routing.Context {
    switch r.domainStrategy {
    case Config_IpIfNonMatch:
        // First pass: try with domain
        // If no match: resolve domain→IP, try again
    case Config_IpOnDemand:
        // Resolve only when IP-based rules are encountered
    }
}

Rule Conditions

Each rule has a Condition that checks the routing context:

go
type Rule struct {
    Condition Condition
    Tag       string     // outbound tag
    RuleTag   string     // rule identifier for logging
    Balancer  *Balancer  // or nil
}

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

Condition Types

ConditionFieldDescription
DomainMatcherdomainMatches target domain (full, substr, regex, domain)
GeoIPMatcheripMatches target IP against GeoIP database
MultiGeoIPMatchergeoipMultiple GeoIP matchers (country codes)
PortMatcherportMatches target port or port range
PortRangeMatcherportListMatches port against ranges
NetworkMatchernetworkTCP, UDP, or both
ProtocolMatcherprotocolSniffed protocol (http, tls, bittorrent)
UserMatcheruserAuthenticated user email
InboundTagMatcherinboundTagInbound handler tag
AttributeMatcherattrsHTTP attributes (sniffed)
ConditionChan(composite)AND of multiple conditions

Routing Context

The routing context provides all the fields that rules can match against:

go
// features/routing/session/context.go
type Context struct {
    Inbound  *session.Inbound   // source, tag, user
    Outbound *session.Outbound  // target, routeTarget
    Content  *session.Content   // sniffed protocol, attributes
}

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

Domain Matching

Domain matching uses the strmatcher package which provides efficient matchers:

Matcher Types

go
// common/strmatcher/strmatcher.go
const (
    Full    Type = 0  // Exact match: "example.com"
    Substr  Type = 1  // Contains: "example" matches "test.example.com"
    Domain  Type = 2  // Domain suffix: "example.com" matches "a.b.example.com"
    Regex   Type = 3  // Regular expression
)

MPH (Minimal Perfect Hash) Optimization

For large domain lists (GeoSite), Xray-core uses a Minimal Perfect Hash function:

go
// common/strmatcher/mph_matcher.go
type MphIndexMatcher struct {
    rules   []matcherGroup  // matcher groups per hash bucket
    values  []uint32        // hash table
    level0  []uint32
    level1  []uint32
    // ... MPH lookup tables
}

This provides O(1) lookup for full-match and domain-suffix-match patterns, dramatically faster than linear scanning.

GeoIP / GeoSite

GeoIP

IP-based geo matching uses a sorted list of CIDR ranges with binary search:

go
// app/router/condition_geoip.go
type GeoIPMatcher struct {
    ip4 []ipv6   // sorted IPv4 ranges (mapped to IPv6)
    ip6 []ipv6   // sorted IPv6 ranges
}

func (m *GeoIPMatcher) Match(ip net.IP) bool {
    // Binary search in sorted range list
}

GeoSite

Domain-based geo matching loads from .dat files (protobuf-serialized):

go
type GeoSiteList struct {
    Entry []*GeoSite  // country code → domain list
}
type GeoSite struct {
    CountryCode string
    Domain      []*Domain  // with Type (full/domain/substr/regex)
}

Load Balancing

When a rule points to a balancer instead of a direct tag:

go
type Balancer struct {
    selectors []string          // outbound tag patterns
    strategy  BalancingStrategy // round-robin, random, least-ping, least-load
    ohm       outbound.Manager
}

Strategies

StrategyDescription
randomRandom selection among matching outbounds
roundRobinSequential rotation
leastPingLowest RTT from observatory probes
leastLoadLeast active connections / best health
go
func (b *Balancer) PickOutbound() (string, error) {
    candidates := b.getMatchingOutbounds()
    return b.strategy.Pick(candidates)
}

Routing Context Flow

mermaid
flowchart TB
    Dispatch["Dispatcher receives<br/>(ctx, destination)"]
    Sniff["Sniffing: detect domain<br/>from TLS SNI / HTTP Host"]

    Dispatch --> Sniff
    Sniff --> SetCtx["Set routing context:<br/>domain, IPs, port, protocol,<br/>inbound tag, user"]

    SetCtx --> Strategy{Domain Strategy?}

    Strategy -->|AsIs| Eval["Evaluate rules sequentially"]
    Strategy -->|IPIfNonMatch| Eval
    Strategy -->|IPOnDemand| Eval

    Eval --> Match{Rule matches?}
    Match -->|Yes| Check{Has balancer?}
    Check -->|Yes| Balance["Balancer.Pick()"]
    Check -->|No| Tag["Use rule.Tag"]
    Balance --> Handler["Get outbound handler"]
    Tag --> Handler

    Match -->|No, more rules| Eval
    Match -->|No rules left + IPIfNonMatch| Resolve["Resolve domain→IP"]
    Resolve --> Eval2["Re-evaluate with IPs"]
    Match -->|No rules left| Default["Use default outbound"]

    Eval2 --> Match2{Rule matches?}
    Match2 -->|Yes| Check
    Match2 -->|No| Default

Implementation Notes

  1. Rule order matters: First match wins. Users expect their rules to be evaluated top-to-bottom.

  2. Domain strategy is critical: IPIfNonMatch does two passes — first with domain, then with resolved IPs. This affects performance (DNS lookup on miss).

  3. GeoIP binary search: The IP ranges are pre-sorted at load time. Use binary search, not linear scan.

  4. MPH for domains: For 100K+ domain rules (common with GeoSite), MPH gives O(1) vs O(n). Without it, domain matching becomes a bottleneck.

  5. RouteTarget vs Target: When routeOnly sniffing is used, the router sees RouteTarget (sniffed domain) but the outbound uses Target (original IP).

  6. Balancer health: leastPing and leastLoad depend on the Observatory feature probing outbound health periodically.

Technical analysis for re-implementation purposes.