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
// 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:
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:
| Strategy | Behavior |
|---|---|
AsIs | Use domain as-is; don't resolve DNS |
IPIfNonMatch | Try domain first; if no rule matches, resolve to IP and retry |
IPOnDemand | Resolve domain to IP only when a rule needs IP matching |
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:
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
| Condition | Field | Description |
|---|---|---|
DomainMatcher | domain | Matches target domain (full, substr, regex, domain) |
GeoIPMatcher | ip | Matches target IP against GeoIP database |
MultiGeoIPMatcher | geoip | Multiple GeoIP matchers (country codes) |
PortMatcher | port | Matches target port or port range |
PortRangeMatcher | portList | Matches port against ranges |
NetworkMatcher | network | TCP, UDP, or both |
ProtocolMatcher | protocol | Sniffed protocol (http, tls, bittorrent) |
UserMatcher | user | Authenticated user email |
InboundTagMatcher | inboundTag | Inbound handler tag |
AttributeMatcher | attrs | HTTP attributes (sniffed) |
ConditionChan | (composite) | AND of multiple conditions |
Routing Context
The routing context provides all the fields that rules can match against:
// 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]stringDomain Matching
Domain matching uses the strmatcher package which provides efficient matchers:
Matcher Types
// 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:
// 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:
// 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):
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:
type Balancer struct {
selectors []string // outbound tag patterns
strategy BalancingStrategy // round-robin, random, least-ping, least-load
ohm outbound.Manager
}Strategies
| Strategy | Description |
|---|---|
random | Random selection among matching outbounds |
roundRobin | Sequential rotation |
leastPing | Lowest RTT from observatory probes |
leastLoad | Least active connections / best health |
func (b *Balancer) PickOutbound() (string, error) {
candidates := b.getMatchingOutbounds()
return b.strategy.Pick(candidates)
}Routing Context Flow
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| DefaultImplementation Notes
Rule order matters: First match wins. Users expect their rules to be evaluated top-to-bottom.
Domain strategy is critical:
IPIfNonMatchdoes two passes — first with domain, then with resolved IPs. This affects performance (DNS lookup on miss).GeoIP binary search: The IP ranges are pre-sorted at load time. Use binary search, not linear scan.
MPH for domains: For 100K+ domain rules (common with GeoSite), MPH gives O(1) vs O(n). Without it, domain matching becomes a bottleneck.
RouteTarget vs Target: When
routeOnlysniffing is used, the router seesRouteTarget(sniffed domain) but the outbound usesTarget(original IP).Balancer health:
leastPingandleastLoaddepend on the Observatory feature probing outbound health periodically.