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.