Routing Scripts
When a script is specified through routing.script, Lua's HandleRoute function takes over outbound selection.
Minimal Example
The following configuration fragment specifies the script file and configures direct and blackhole outbounds:
{
"routing": {
"script": "routing.lua"
},
"outbounds": [
{ "tag": "direct", "protocol": "freedom" },
{ "tag": "block", "protocol": "blackhole" }
]
}2
3
4
5
6
7
8
9
Save routing.lua:
function HandleRoute(ctx)
local sourceIPs = ctx:GetSourceIPs()
if sourceIPs and #sourceIPs > 0
and sourceIPs[1]:String() == "127.0.0.1" then
return "block"
end
return "direct"
end2
3
4
5
6
7
8
The script reads the source IP through ctx. It returns block when the source IP is 127.0.0.1, and direct otherwise, corresponding to the outbound tag values configured above.
Merge the fragment above into a complete configuration. See Script File Paths for file lookup rules.
HandleRoute can also receive more parameters and return a rule name and an error. See HandleRoute for the full calling convention and failure behavior.
Relationship with Routing Configuration
When a routing script is enabled, neither rules nor domainStrategy takes effect. If the script does not select an outbound or encounters an error, the built-in routing rules are not evaluated either.
You can still configure balancers. The script selects an outbound from a balancer through router:PickOutbound:
local router = require("xray.router")
function HandleRoute(ctx)
local outboundTag, err = router:PickOutbound("balance")
return outboundTag, "lua-balance", err
end2
3
4
5
6
Replace balance in the example with the tag of a balancer configured in routing.balancers.
Example: Explicit DNS Queries for IP-Based Routing
The following script first handles DNS upstream query traffic, then checks the request's existing target IPs. When needed, it explicitly resolves the domain, and selects an outbound based on private address ranges.
{
"dns": {
"tag": "dns-query",
"servers": ["1.1.1.1"]
},
"routing": {
"script": "routing.lua"
},
"outbounds": [
{ "tag": "direct", "protocol": "freedom" },
{
"tag": "proxy",
"protocol": "vless",
"settings": {
// ...
}
}
]
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
local dns = require("xray.dns")
local geodata = require("xray.geodata")
local log = require("xray.log")
local privateIPMatcher = geodata.BuildIPMatcher("geoip:private")
function HandleRoute(ctx, inboundTag, sourcePort, targetPort,
localPort, targetDomain, network, protocol, user,
vlessRoute, skipDNSResolve)
if skipDNSResolve or inboundTag == "dns-query" then
return "direct", "lua-dns"
end
local ips = ctx:GetTargetIPs()
if privateIPMatcher:AnyMatch(ips) then
return "direct", "lua-private"
end
if targetDomain ~= "" then
local resolved, ttl, err =
dns.Query(targetDomain, true, true, false)
if err ~= nil then
log.Warning("Resolution of ", targetDomain, " failed: ", err)
elseif privateIPMatcher:AnyMatch(resolved) then
return "direct", "lua-resolved-private"
end
end
return "proxy", "lua-default"
end2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
DNS upstream requests in normal mode also pass through routing. The example directly selects an outbound for requests where skipDNSResolve is true or inboundTag is "dns-query", avoiding another DNS lookup that would create a loop.
The result of dns.Query is only used for decisions in the script. It does not automatically change the target IPs in ctx or the actual connection destination. The example selects proxy if resolution fails; adjust this to your own policy as needed.
See Pooled Lifecycle for top-level initialization and state retention in routing scripts.