Router¶
Request routing through a default outbound or a restricted Lua script.
Without a script, requests use router.default-outbound, or the first
configured outbound if omitted. This works without the plugin feature.
Omit router or use router: {} to use the first outbound directly.
Script routing requires the plugin Cargo feature. Configure either inline
source in src or a script file in path, never both. A configured script
selects the outbound instead of default-outbound.
dns:
- tag: dns-in
type: dns-udp
bind-addr: 0.0.0.0:5553
router:
src: |
return function(ctx)
if ctx.dst_domain and ctx.dst_domain:match("%.example$") then
return "special-proxy"
end
if ctx.dst_ip_v4 and ctx.dst_ip_v4:sub(1, #"192.168") == "192.168" then
return "sq-home"
end
if ctx.dst_port == 53 and ctx.network_type == "udp" then
return "dns-in" -- dns hijacking
end
return "direct"
end
Alternatively, load a file (relative paths resolve from the working directory):
File changes reload automatically for subsequent requests, including when an editor replaces the file. Failed reloads keep the last working script. A successful reload resets Lua state; existing connections are unaffected. Inline scripts are not watched. Routing is asynchronous: suspended calls keep their original script runtime when a reload occurs, while new calls use the replacement.
The script must return a function. Each request passes one context userdata:
| Field | Value |
|---|---|
inbound_tag |
Tag of the inbound listener |
dns_query |
Array of tables with name and numeric record_type; empty when no DNS metadata is attached |
network_type |
"tcp" or "udp" |
dst_domain, dst_ip_v4, dst_ip_v6 |
Destination name or IP string; unused fields are nil |
dst_port |
Destination port number |
src_addr, src_ip_v4, src_ip_v6 |
Source address strings, or nil when unavailable |
src_port |
Source port number, or nil when unavailable |
stats_context |
Table with username and conn_id for authenticated QUIC requests, otherwise nil |
Scripts may update dst_domain, dst_ip_v4, dst_ip_v6, and dst_port only
for TCP requests. Writing these fields for UDP requests raises an error,
including assigning nil.
Setting a destination name or IP clears the other destination address fields.
dns_query is a snapshot: changing its tables does not modify the request.
Return a configured outbound tag to route the request, or nil, error_message
to reject it. Routing errors do not fall back to router.default-outbound.
Scripts have base language functions and string, table, math, and bit helpers. Filesystem, process, module loading, and dynamic code loading are unavailable.
| API | Behavior | Availability |
|---|---|---|
print(...) |
Writes console output | Script loading and routing |
info(message) |
Accepts a string and emits a tracing::info! log using the application's logging filters |
Script loading and routing |
lookup(dns_tag, domain) |
Returns an array of IP strings and may block router. | Inside the returned routing function; requires dns-server |
reverse_lookup(dns_tag, ip) |
Returns an array of PTR hostname strings and may block router | Inside the returned routing function; requires dns-server |
lookup_cache(domain) |
Returns an array of cached IP strings, or an empty array on a miss; domain names are case insensitive | Script loading and routing; requires dns-server |
reverse_lookup_cache(ip) |
Returns the most recently cached hostname from matching A/AAAA or PTR answers, or nil on a miss; invalid IP strings raise a Lua error | Script loading and routing; requires dns-server |
find_ip_v4(tag, list, ip) |
Returns whether an IPv4 address belongs to a country list in the tagged database | Script loading and routing; requires router-db |
find_ip_v6(tag, list, ip) |
Returns whether an IPv6 address belongs to a country list in the tagged database | Script loading and routing; requires router-db |
find_domain(tag, list, domain) |
Returns whether a domain matches a Geosite list in the tagged database | Script loading and routing; requires router-db |
Cache lookups use the shared DNS cache, ignore expired entries, and perform
no network I/O or asynchronous suspension. They do not take a DNS service tag.
lookup and reverse_lookup suspend the routing function and raise Lua errors
on failure. Route DNS upstream requests
Database membership helpers require router.database entries (see
super::RouterDatabaseCfg). All three helpers return booleans.
Missing databases download through an internal inbound with the database tag.
Route that traffic before calling helpers. Unavailable databases raise Lua
errors; use pcall for an explicit fallback while downloading. Existing redb
files are reused, including across script reloads.
The exposed router context to script can be seen in [crate::plugin::router::RouteContext]
Fields¶
default-outbound¶
- Type:
string(optional) - Required: no
Outbound tag used when no routing script is configured. Defaults to the
first configured outbound. Does not require the plugin feature.
database¶
- Type: list of
RouterDatabaseCfg - Required: no
- Default: (type default)
Persistent databases available to Lua membership helpers.
src¶
- Type:
string(optional) - Required: no
Inline Lua source returning a routing function. Mutually exclusive with path.
path¶
- Type:
path(optional) - Required: no
Path to a Lua script, watched for changes. Mutually exclusive with src.