Xray-core: Add Lua script for dns and routing

https://github.com/XTLS/Xray-core/pull/6823
This commit is contained in:
Meow
2026-10-11 09:55:19 +08:00
parent 4c8f3b2b7e
commit f179efd35d
48 changed files with 4950 additions and 0 deletions
+35
View File
@@ -0,0 +1,35 @@
# Runtime Environment and Script Loading
## Lua Environment
Xray currently uses GopherLua, which supports Lua 5.1 syntax, Lua 5.2's `goto` statement, and its own [`channel`](https://github.com/yuin/gopher-lua#lua-api).
Xray loads Lua scripts according to the configuration file. The configuration location determines the script's purpose and the available Hooks. See the [Hook reference](./index.md#hook) for each entry point's Hooks and calling conventions.
## Module Loading
API modules are organized by functionality and loaded with `require`:
```lua
local geodata = require("xray.geodata")
local log = require("xray.log")
```
See [Module API](./index.md#module-api) for the module list, and each API reference for function usage.
See [Instances and Lifecycle](./guide/lifecycle.md) for when top-level initialization, such as loading Lua modules and creating objects, runs.
## Script File Paths
The `script` field in the configuration file specifies the path to a Lua script. An empty string or an omitted field disables the script; an absolute path locates the file directly.
Relative paths are searched in the following order. If a path does not exist, the search continues; if it exists but is not a regular file, an error is raised immediately:
1. The directory specified by the `XRAY_LOCATION_CONFDIR` environment variable;
2. The directory specified by the `XRAY_LOCATION_CONFIG` environment variable;
3. The Xray process's current working directory;
4. The directory containing the Xray executable.
See [Environment Variables](../../config/env.md) for these directory settings. Relative paths are not automatically resolved against the directory containing the configuration file.
For example, when the script path is `routing.lua`, Xray searches for the file in the order above. A Windows absolute path can be written as `C:/Xray/routing.lua`; if you use backslashes, escape them as required by your configuration format.
+155
View File
@@ -0,0 +1,155 @@
# DNS Scripts
When a script is specified through [`dns.script`](../../../config/dns.md#dnsobject), Lua's `HandleDNSQuery` function takes over the built-in DNS processing flow.
## Minimal Example
Add the following to an existing configuration:
```json
{
"dns": {
"script": "dns.lua",
"servers": ["1.1.1.1"]
}
}
```
Save `dns.lua`:
```lua
local dns = require("xray.dns")
local server = dns.Servers[1]
function HandleDNSQuery(domain, ipv4, ipv6, fake)
return server:Query(domain, ipv4, ipv6, fake)
end
```
Merge the fragment above into a complete configuration. See [Script File Paths](../environment.md#script-file-paths) for file lookup rules.
The example passes the parameters unchanged to the upstream and returns the query results directly. See [HandleDNSQuery](../reference/hook-dns.md) for the full parameters, return-value restrictions, and failure behavior.
## Relationship with DNS Configuration
Before a query reaches the script, the built-in DNS domain validation, global query-type restrictions, and Hosts processing still apply. The script is not called if Hosts has already supplied valid IPs or explicitly rejected the request. If Hosts replaces the domain, the script queries the replacement domain.
When the script is enabled, it controls upstream selection, so `domains`, `skipFallback`, `finalQuery`, `disableFallback`, `disableFallbackIfMatch`, and `enableParallelQuery` no longer have a practical effect.
Use [`serverObj:Query`](../reference/module-dns.md#query) to query a specific upstream. See that API for the server settings that still take effect.
::: tip
When the mapping between upstreams and filtering rules is fixed, configuring `expectedIPs` / `unexpectedIPs` directly is more convenient and efficient. You can also omit these settings and filter in Lua with methods such as [`ipMatcherObj:FilterIPs`](../reference/module-geodata.md#filterips) for greater flexibility.
:::
## Example: Domain-Based Upstream Selection and Result Filtering
The following example selects upstreams by domain category and filters Chinese IPs as needed. Each server is configured with an `id`, which the script uses for queries and fallback:
```json
{
"dns": {
"script": "dns.lua",
"tag": "dns-proxy",
"servers": [
{ "id": "cf", "address": "1.1.1.1" },
{ "id": "google", "address": "8.8.8.8" },
{ "id": "cn114", "address": "114.114.114.114", "tag": "dns-direct" },
{ "id": "cn223", "address": "223.5.5.5", "tag": "dns-direct" },
{
"id": "google-ecs",
"address": "8.8.8.8",
"clientIp": "222.85.85.85"
},
{
"id": "google-alt-ecs",
"address": "8.8.4.4",
"clientIp": "222.85.85.85"
}
]
},
"routing": {
"rules": [
{ "inboundTag": ["dns-direct"], "outboundTag": "direct" },
{ "inboundTag": ["dns-proxy"], "outboundTag": "proxy" }
]
},
"outbounds": [
{ "tag": "direct", "protocol": "freedom" },
{
"tag": "proxy",
"protocol": "vless",
"settings": {
// ...
}
}
]
}
```
`dns-direct` and `dns-proxy` are inbound tags for DNS upstream queries. The routing rules above select the direct outbound `direct` and the VLESS proxy outbound `proxy`, respectively.
The script first chooses a query order based on the domain, then queries the upstreams one by one. If a query fails or its filtered result is empty, it tries the next server:
```lua
local geodata = require("xray.geodata")
local dns = require("xray.dns")
local servers = {}
for _, server in ipairs(dns.Servers) do
servers[server.ID] = server
end
local googleDomainMatcher = geodata.BuildDomainMatcher("geosite:google")
local cnDomainMatcher = geodata.BuildDomainMatcher("geosite:cn")
local foreignDomainMatcher = geodata.BuildDomainMatcher("geosite:geolocation-!cn")
local cnIPMatcher = geodata.BuildIPMatcher("geoip:cn")
function HandleDNSQuery(domain, ipv4, ipv6, fake)
local queries
if googleDomainMatcher:MatchAny(domain) then
-- Use public DNS for Google domains.
queries = {{"cf"}, {"google"}}
elseif cnDomainMatcher:MatchAny(domain) then
-- For Chinese domains, query direct DNS, keep Chinese IPs, then fall back to proxied DNS.
queries = {
{"cn114", "cn"}, {"cn223", "cn"}, {"cf"}, {"google"}
}
elseif foreignDomainMatcher:MatchAny(domain) then
-- For non-Chinese domains, exclude Chinese IPs first, then try queries with ECS.
queries = {
{"cf", "non-cn"}, {"google", "non-cn"},
{"google-ecs"}, {"google-alt-ecs"}
}
else
-- For unlisted domains, use ECS to look for Chinese IPs, then fall back to regular public DNS.
queries = {
{"google-ecs", "cn"}, {"google-alt-ecs", "cn"},
{"cf"}, {"google"}
}
end
local lastError = "No DNS result meets the conditions"
for _, query in ipairs(queries) do
local ips, ttl, err =
servers[query[1]]:Query(domain, ipv4, ipv6, fake)
if err then
lastError = err
else
if query[2] then
local inCn, outsideCn = cnIPMatcher:FilterIPs(ips)
if query[2] == "cn" then
ips = inCn
else
ips = outsideCn
end
end
if ips and #ips > 0 then
return ips, ttl, nil
end
end
end
return nil, 0, lastError
end
```
See [Pooled Lifecycle](./lifecycle.md#pooled-lifecycle) for top-level initialization and state retention in DNS scripts.
@@ -0,0 +1,67 @@
# Instances and Lifecycle
The instance management strategy used by a script entry point determines how Lua instances are created, Hooks are called, state is retained, and instances are destroyed. This page describes these rules by lifecycle type.
## Pooled Lifecycle
Currently, [HandleRoute](../reference/hook-routing.md) in [routing scripts](./routing.md) and [HandleDNSQuery](../reference/hook-dns.md) in [DNS scripts](./dns.md) use pooled instances. The following rules apply to both Hooks.
### Instance Pools and Initialization
Routing and DNS scripts are bound through the `script` field in their respective configurations and manage separate Lua instance pools. Even when configured to use the same file, they do not share state within Lua instances.
At startup, Xray reads and compiles the script once, creates the first instance, executes its top-level code, and checks that the required handler exists. Failure to read the file, parse it, execute top-level code, or validate the handler prevents Xray from starting.
### Hook Calls and Instance Recycling
Each routing decision or DNS query exclusively uses one instance. An idle instance is reused when available; when concurrent calls require more instances, new ones are created from the compiled script and the top-level code runs again.
After a handler completes normally, the instance can return to the pool for reuse. An uncaught Lua exception or execution timeout destroys the instance. Reporting a business error through return values, or failing return-value validation, is not a Lua execution exception: the instance can still be reused. Excess idle instances are destroyed automatically.
When an idle instance is available, each Hook follows a lightweight path: **take an instance → execute the Hook → return the instance**. The script is read and compiled at Xray startup. The green nodes in the diagram show this normal path.
```mermaid
flowchart TD
LOAD["Xray startup<br/>Read and compile the main script once"] --> INIT["Create the first Lua instance<br/>Run top-level code and check the Hook"]
INIT --> POOL[("Idle instance pool")]
subgraph CALL["Pooled Hook call: lightweight reuse path"]
TAKE["Take exclusive use of an idle instance"] --> RUN["Execute HandleRoute / HandleDNSQuery"]
RUN -->|Normal completion| PUT["Return the instance, retaining state"]
end
REQUEST["Routing decision / DNS query"] --> AVAILABLE{"Idle instance available?"}
POOL -.-> AVAILABLE
AVAILABLE -->|Yes| TAKE
PUT --> POOL
AVAILABLE -. No: expand on demand .-> CREATE["Create a new instance from the compiled script<br/>Run top-level code and check the Hook"]
CREATE --> RUN
RUN -. Uncaught Lua exception / timeout .-> DESTROY["Destroy the instance"]
POOL -. Excess idle instances / Xray shutdown .-> DESTROY
classDef reuse fill:#e8f5e9,stroke:#2e7d32,color:#1b5e20
class TAKE,RUN,PUT reuse
```
### State in Pooled Instances
Global variables, top-level `local` variables, closures, and the `require` module cache belong to the current instance. They persist across calls handled by that instance and are lost when it is destroyed:
```lua
local calls = 0
function HandleRoute()
calls = calls + 1
return "direct", "instance-call-" .. calls
end
```
The counter above only counts calls handled by the current instance. Different instances have independent `calls` values, and requests are not guaranteed to use the same instance. It cannot serve as a counter shared by all connections or as persistent state for a particular connection.
Place initialization code, such as loading Lua modules, creating matchers, and saving DNS server objects, outside the Hook function at the script's top level. It runs once when each instance is created, and subsequent Hook calls on that instance can reuse the results.
### Timeouts and Script Updates
Currently, each routing and DNS instance has an initialization timeout of **120 seconds**, and each handler call has an execution timeout of **6 seconds**. The call timeout starts after an instance is acquired; these values currently have no separate configuration fields. When calling Xray APIs, cancellation also depends on whether the API responds to cancellation signals; each DNS server is additionally limited by its `timeoutMs` setting.
Xray does not automatically watch or recompile the main script. Restart Xray after changing it for the changes to take effect. Instances created later also use the main script compiled during the current startup.
+119
View File
@@ -0,0 +1,119 @@
# Routing Scripts
When a script is specified through [`routing.script`](../../../config/routing.md#routingobject), Lua's `HandleRoute` function takes over outbound selection.
## Minimal Example
The following configuration fragment specifies the script file and configures direct and blackhole outbounds:
```json
{
"routing": {
"script": "routing.lua"
},
"outbounds": [
{ "tag": "direct", "protocol": "freedom" },
{ "tag": "block", "protocol": "blackhole" }
]
}
```
Save `routing.lua`:
```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"
end
```
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](../environment.md#script-file-paths) for file lookup rules.
`HandleRoute` can also receive more parameters and return a rule name and an error. See [HandleRoute](../reference/hook-routing.md) 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`](../reference/module-router.md#router-pickoutbound):
```lua
local router = require("xray.router")
function HandleRoute(ctx)
local outboundTag, err = router:PickOutbound("balance")
return outboundTag, "lua-balance", err
end
```
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.
```json
{
"dns": {
"tag": "dns-query",
"servers": ["1.1.1.1"]
},
"routing": {
"script": "routing.lua"
},
"outbounds": [
{ "tag": "direct", "protocol": "freedom" },
{
"tag": "proxy",
"protocol": "vless",
"settings": {
// ...
}
}
]
}
```
```lua
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"
end
```
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](./lifecycle.md#pooled-lifecycle) for top-level initialization and state retention in routing scripts.
+46
View File
@@ -0,0 +1,46 @@
# Lua Scripts
Lua scripts provide a flexible way to extend Xray, implement custom functionality, and interact with the core.
## Getting Started
Start with [Runtime Environment and Script Loading](./environment.md) to learn about Lua support, module loading, and how script files are located.
## Writing Guides
Choose a guide for the functionality you need, start with the minimal example, and build your own script step by step:
| Guide | Purpose | Contents |
| ------------------------------------- | ----------------------- | ---------------------------------------------------------------------------------- |
| [Routing Scripts](./guide/routing.md) | Customize routing rules | Outbound selection, load balancing, and IP-based routing after explicit resolution |
| [DNS Scripts](./guide/dns.md) | Customize DNS queries | Upstream selection, query fallback, and result filtering |
The current routing and DNS scripts both use pooled instances. When writing scripts, read about the [pooled lifecycle](./guide/lifecycle.md#pooled-lifecycle) to understand top-level initialization and state retention.
## Reference
### Hook
A Hook is a Lua function that the core calls at a specific point. The script entry point determines the available Hooks and their calling conventions; one entry point can have multiple Hooks. Refer to the documentation for each entry point to find out which Hooks you need to implement.
The currently supported entry points and Hooks are listed below. See each Hook's reference page for parameters, return values, and failure behavior:
| Script entry point | Hook |
| ------------------ | ------------------------------------------ |
| `routing.script` | [HandleRoute](./reference/hook-routing.md) |
| `dns.script` | [HandleDNSQuery](./reference/hook-dns.md) |
### Data Types
In addition to Lua's built-in types, Xray Lua uses types such as `net.IP`, Go slices, and `error`. See [Data Types](./reference/data-types.md) for their representation and use in Lua.
### Module API
Browse the module APIs by functionality:
| Module | Purpose |
| --------------------------------------------- | ------------------------------------ |
| [xray.router](./reference/module-router.md) | Policy routing |
| [xray.dns](./reference/module-dns.md) | Domain resolution |
| [xray.geodata](./reference/module-geodata.md) | Domain and IP matching and filtering |
| [xray.log](./reference/module-log.md) | Log output |
@@ -0,0 +1,215 @@
# Data Types
This page summarizes data types shared by the modules and explains how they are represented and used in Lua. Types specific to a module are documented on that module's page.
## net.IP
| Property | Description |
| ------------------ | ------------------------- |
| Underlying Go type | `net.IP`, `common/net.IP` |
| Lua representation | `userdata` |
You usually do not need to compare individual `net.IP` values in Lua, especially on frequently executed paths. For IP matching, prefer [`IPMatcher`](./module-geodata.md#ipmatcher), which can directly handle the `net.IP` values returned by APIs.
If you need to manipulate IPs directly, remember that `net.IP` and IP address strings are different types. Use the following two methods for conversion and comparison:
### String
```lua
local text = ip:String()
```
Converts an IP to address text.
**Parameters**
No additional parameters.
**Return Values**
| Return value | Type | Description |
| ------------ | -------- | ------------------------------ |
| `text` | `string` | IP address string; never `nil` |
### Equal
```lua
local equal = ip:Equal(otherIP)
```
Checks whether two IPs are equal.
**Parameters**
| Parameter | Type | Description |
| --------- | ---------------------------- | -------------------------------------------- |
| `otherIP` | [`net.IP`](#net-ip) or `nil` | The IP to compare; explicit `nil` is allowed |
**Return Values**
| Return value | Type | Description |
| ------------ | --------- | ----------------------------------------------- |
| `equal` | `boolean` | `true` if equal, otherwise `false`; never `nil` |
**Behavior**
Comparing a valid IP with `nil` returns `false`.
**Example**
```lua
if ip ~= nil then
local text = ip:String()
local equal = ip:Equal(otherIP)
end
```
## slice
A Go slice is represented as `userdata` in Lua, retains its element type, and is indexed starting at **1**. The following two slice types share the same length, indexing, and iteration operations.
| Type | Lua representation | Element type |
| ----------------------- | ------------------ | ------------------- |
| [`[]net.IP`](#net-ip-1) | `userdata` | [`net.IP`](#net-ip) |
| [`[]uint32`](#uint32) | `userdata` | `number` |
### []net.IP
Tip: For matching or filtering IP lists, prefer [`IPMatcher`](./module-geodata.md#ipmatcher), which can directly handle `[]net.IP`.
### []uint32
Elements are 32-bit unsigned integers in the range `0 .. 4294967295`.
### #values
```lua
local count = #values
```
Returns the number of elements in a slice.
**Return Values**
| Return value | Type | Description |
| ------------ | -------- | ---------------------------------------------- |
| `count` | `number` | A non-negative integer; `0` for an empty slice |
**Behavior**
Lists returned by APIs may be `nil` or slices of length 0. `values` must be non-`nil` before these operations are used; checking only `if values then` does not tell you whether the list has elements.
### values[i]
```lua
local value = values[i]
```
Reads one element from a slice.
**Parameters**
| Parameter | Type | Description |
| --------- | -------- | -------------------------------------------------------- |
| `i` | `number` | Required; must be an integer in the range `1 .. #values` |
**Return Values**
| Return value | Type | Description |
| ------------ | ------------------------------- | ---------------------------------------------------- |
| `value` | [`net.IP`](#net-ip) or `number` | An element of `[]net.IP` or `[]uint32`, respectively |
**Behavior**
Out-of-bounds access raises a Lua exception instead of returning `nil`. Avoid modifying slices returned by APIs directly.
**Example**
```lua
if ips ~= nil and #ips > 0 then
local firstIP = ips[1]:String()
end
```
### values()
```lua
for i, value in values() do
-- Use index i and element value
end
```
Returns a slice iterator for a generic `for` loop, with indices starting at 1.
**Parameters**
No additional parameters.
**Return Values**
| Return value | Type | Description |
| ------------ | ---------- | ----------------------------------------------- |
| Iterator | `function` | Provides each index and element, as shown above |
**Behavior**
Slices do not support `pairs` or `ipairs`; a numeric `for` loop can also be used.
**Example**
```lua
if ips ~= nil then
for i = 1, #ips do
local text = ips[i]:String()
end
for i, ip in ips() do
local text = ip:String()
end
end
```
## IP List Input
When passing an IP list to `IPMatcher`, you can use the following forms:
| Input form | Lua representation | Description |
| ---------------------------- | ------------------ | ------------------------------------------------------------------------------------- |
| [`[]net.IP`](#net-ip-1) | `userdata` | An IP list; empty slices are allowed |
| Array of [`net.IP`](#net-ip) | `table` | Consecutive IPs starting at index 1; an empty array `{}` is allowed, but gaps are not |
| `nil` | `nil` | Must be passed explicitly; see each method for its result |
IP address strings are not parsed into `net.IP`. An empty string `""` is not a valid IP list; passing it to `AnyMatch`, `Matches`, or `FilterIPs` raises a Lua exception.
## error
| Property | Description |
| ------------------ | ------------------------------------------- |
| Underlying Go type | `error` |
| Lua representation | `userdata` for an error; `nil` for no error |
An API's `err` return value is `nil` when there is no error; otherwise it is an error object. An error object can be returned unchanged as a Hook's `err` value for the Xray core to handle.
**Hook Return Convention**
| Error value | Meaning |
| ---------------- | -------------------------------------------------------------------------------------- |
| `nil` | No error |
| Error `userdata` | Preserves the original error |
| `string` | An error message supplied by the script; an empty string `""` still indicates an error |
String errors apply only to Hook return values. Xray APIs themselves return an error or `nil`. See the relevant Hook for validation and failure behavior.
**Logging Error Messages**
You cannot obtain a Go error object's message with `tostring(err)`. To log it, pass `err` directly to [`xray.log`](./module-log.md).
```lua
local log = require("xray.log")
local ips, ttl, err = serverObj:Query(domain, ipv4, ipv6, fake)
if err ~= nil then
log.Warning("DNS query failed: ", err)
elseif ips ~= nil and #ips > 0 then
local firstIP = ips[1]:String()
end
```
@@ -0,0 +1,67 @@
# DNS Hook
The core calls the global `HandleDNSQuery` function in `dns.script` during built-in DNS queries. See the [DNS Scripts guide](../guide/dns.md) for configuration and complete examples.
## API Index
| Category | Member | Description |
| -------- | ---------------------------------------- | ------------------ |
| Hook | [`HandleDNSQuery(...)`](#handlednsquery) | Handle DNS queries |
## Hook Interface
### HandleDNSQuery(...)
```lua
function HandleDNSQuery(domain, ipv4, ipv6, fake)
return ips, ttl, err
end
```
Handles one DNS query, returning an IP slice, TTL, or error.
#### Parameters
The core always passes all four parameters, and none is `nil`.
| Parameter | Type | Description |
| --------- | --------- | ----------------------------------------- |
| `domain` | `string` | Domain to query; non-empty and lowercase |
| `ipv4` | `boolean` | Whether IPv4 address queries are allowed |
| `ipv6` | `boolean` | Whether IPv6 address queries are allowed |
| `fake` | `boolean` | Whether FakeDNS is allowed for this query |
`ipv4` and `ipv6` are already restricted by the global [`queryStrategy`](../../../config/dns.md#dnsobject); at least one is `true`. Normally, pass these three boolean parameters unchanged when querying an upstream.
#### Return Values
| Return value | Type | Description |
| ------------ | ---------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `ips` | [`[]net.IP`](./data-types.md#net-ip-1) or `nil` | Query results; must be a slice, not a Lua array |
| `ttl` | `number` or `nil` | TTL in seconds; when `err == nil`, must be an integer in the range `0 .. 4294967295` |
| `err` | [`error`](./data-types.md#error), `string`, or `nil` | Only `nil` means no error |
#### Behavior
The core validates `err`, `ttl`, and `ips` in that order, stopping at the first error. When `err` is non-`nil`, the other two values are ignored. When there is no error, a valid TTL must be provided even if the IP result is empty.
To return an empty response, use `return nil, 0, nil`. An error belongs in the third position, for example `return nil, nil, "query failed"`.
| Script result | Core behavior |
| ----------------------------------------------------------------- | ---------------------------------- |
| Validation succeeds and `ips` is non-empty | Query succeeds |
| `err == nil`, valid TTL, but `ips` is `nil` or an empty slice | Query fails with an empty response |
| Returned error, invalid return-value type, or execution exception | Query fails |
#### Example
You can forward all three return values from [`serverObj:Query`](./module-dns.md#query) directly:
```lua
local dns = require("xray.dns")
local serverObj = dns.Servers[1]
function HandleDNSQuery(domain, ipv4, ipv6, fake)
return serverObj:Query(domain, ipv4, ipv6, fake)
end
```
@@ -0,0 +1,181 @@
# Routing Hook
The core calls the global `HandleRoute` function in `routing.script` when selecting an outbound. See the [Routing Scripts guide](../guide/routing.md) for configuration and complete examples.
## API Index
| Category | Member | Description |
| -------- | --------------------------------------- | ------------------------------------- |
| Hook | [`HandleRoute(...)`](#handleroute) | Select an outbound for a request |
| Object | [`routing.Context`](#routing-context) | The current request's routing context |
| Method | [`ctx:GetSourceIPs()`](#getsourceips) | Get source IPs |
| Method | [`ctx:GetTargetIPs()`](#gettargetips) | Get target IPs |
| Method | [`ctx:GetLocalIPs()`](#getlocalips) | Get local IPs |
| Method | [`ctx:GetAttributes()`](#getattributes) | Get request attributes |
## Hook Interface
### HandleRoute(...)
```lua
function HandleRoute(ctx, inboundTag, sourcePort, targetPort,
localPort, targetDomain, network, protocol, user,
vlessRoute, skipDNSResolve)
return outboundTag, ruleTag, err
end
```
Selects an outbound for the current request, optionally returning a rule name or an error.
#### Parameters
The core always passes all 11 parameters in the order shown above, and none is `nil`. You can omit unused trailing parameters from the function definition; this does not change the values passed by the core.
| Parameter | Type | Description | Empty and Default Values |
| ---------------- | ------------------------------------- | --------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `ctx` | [`routing.Context`](#routing-context) | Current request context | Always valid `userdata` |
| `inboundTag` | `string` | Inbound tag | `""` if the inbound has no tag or inbound information is unavailable |
| `sourcePort` | `number` | Source port, `0 .. 65535` | `0` if no valid source address is available |
| `targetPort` | `number` | Target port, `0 .. 65535` | `0` if no valid target address is available |
| `localPort` | `number` | Local port of the inbound connection, `0 .. 65535` | `0` if no valid local address is available |
| `targetDomain` | `string` | The effective sniffed domain takes precedence over the connection's target domain; converted to lowercase | `""` if the target is an IP without an effective sniffed domain, or target information is unavailable |
| `network` | `number` | Network type; compare with constants such as [`router.NetworkTCP`](./module-router.md#constants) | `NetworkUnknown` (`0`) if outbound information is unavailable or the network type is unknown |
| `protocol` | `string` | Sniffed protocol | `""` if sniffed protocol information is unavailable |
| `user` | `string` | User email | `""` if inbound or user information is unavailable, or no email is set |
| `vlessRoute` | `number` | Routing value formed from bytes 7 and 8 of the VLESS UUID, `0 .. 65535` | `0` if unavailable; the value itself may also be 0 |
| `skipDNSResolve` | `boolean` | When `true`, DNS queries must be skipped to avoid loops | `false` if the flag or additional request information is unavailable |
#### Return Values
| Return value | Type | Description |
| ------------- | ---------------------------------------------------- | ---------------------------------------------------------- |
| `outboundTag` | `string` or `nil` | Outbound tag; `nil` or `""` means no outbound was selected |
| `ruleTag` | `string` or `nil` | Optional rule name; `nil`, omission, or `""` means no name |
| `err` | [`error`](./data-types.md#error), `string`, or `nil` | Only `nil` means no error |
#### Behavior
The core validates `err`, `outboundTag`, and `ruleTag` in that order, stopping at the first error. When `err` is non-`nil`, the first two values are ignored. When no outbound is selected, `ruleTag` is ignored.
On success, trailing `ruleTag` and `err` values may be omitted, for example `return "direct"`. An error must be in the third position, for example `return nil, nil, "routing failed"`; `return nil, "routing failed"` does not report an error.
| Script result | Core behavior |
| ----------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| Validation succeeds and `outboundTag` is non-empty | Uses the corresponding outbound; closes the connection if the tag does not exist |
| No outbound selected | Uses the default outbound |
| Returned error, invalid return-value type, or execution exception | Logs the error and tries the default outbound |
The default outbound is the first outbound in the configuration; the connection is closed if no default outbound is available. To block traffic, explicitly return a configured [`blackhole`](../../../config/outbounds/blackhole.md) outbound tag instead of relying on this behavior with a nonexistent tag.
## Related Objects
### routing.Context
`routing.Context` represents the current request's routing context, accessed through the `ctx` parameter in `HandleRoute`.
| Property | Description |
| ------------------ | ------------------------------------------------------ |
| Underlying Go type | The `routing.Context` interface in `features/routing` |
| Lua representation | `userdata` |
| Obtained from | The first argument passed by the core to `HandleRoute` |
All methods below take no additional parameters. See [Data Types](./data-types.md) for IP and slice operations.
#### GetSourceIPs
```lua
local ips = ctx:GetSourceIPs()
```
Gets the request's source IPs.
**Parameters**
No additional parameters.
**Return Values**
| Return value | Type | Description |
| ------------ | ----------------------------------------------- | ------------------------------------- |
| `ips` | [`[]net.IP`](./data-types.md#net-ip-1) or `nil` | IP list for the corresponding address |
**Behavior**
Returns `nil` if there is no inbound, the source address is invalid, or it is not an IP address.
#### GetTargetIPs
```lua
local ips = ctx:GetTargetIPs()
```
Gets the request's target IPs.
**Parameters**
No additional parameters.
**Return Values**
| Return value | Type | Description |
| ------------ | ----------------------------------------------- | ------------------------------------- |
| `ips` | [`[]net.IP`](./data-types.md#net-ip-1) or `nil` | IP list for the corresponding address |
**Behavior**
Returns `nil` if there is no outbound, the target address is invalid, or it is a domain name.
#### GetLocalIPs
```lua
local ips = ctx:GetLocalIPs()
```
Gets the local IPs of the inbound connection.
**Parameters**
No additional parameters.
**Return Values**
| Return value | Type | Description |
| ------------ | ----------------------------------------------- | ------------------------------------- |
| `ips` | [`[]net.IP`](./data-types.md#net-ip-1) or `nil` | IP list for the corresponding address |
**Behavior**
Returns `nil` if there is no inbound, the local address is invalid, or it is not an IP address.
#### GetAttributes
```lua
local attributes = ctx:GetAttributes()
```
Gets the current request's attributes.
**Parameters**
No additional parameters.
**Return Values**
| Return value | Type | Description |
| ------------ | ------------------- | ------------------------------ |
| `attributes` | `map[string]string` | Always `userdata`; never `nil` |
**Behavior**
Read attributes with `attributes[key]`, where `key` must be a string. An existing key returns a string (possibly `""`); a missing key returns `nil`.
See the [`attrs`](../../../config/routing.md#ruleobject) field in the routing configuration for the meaning and use of attributes.
**Example**
```lua
local attributes = ctx:GetAttributes()
if attributes[":method"] == "GET" then
return "direct", "lua-get"
end
```
@@ -0,0 +1,139 @@
# xray.dns
Provides interfaces for DNS queries and upstream server access.
```lua
local dns = require("xray.dns")
```
## API Index
| Category | Member | Description |
| -------- | ----------------------------------------------------- | ---------------------------------- |
| Function | [`dns.Query(domain, ipv4, ipv6, fake)`](#dns-query) | Query through Xray's DNS client |
| Field | [`dns.Servers`](#dns-servers) | Upstream server array |
| Object | [`serverObj`](#server) | A single DNS upstream server |
| Field | [`serverObj.ID`](#id) | Server identifier |
| Method | [`serverObj:Query(domain, ipv4, ipv6, fake)`](#query) | Query a specific upstream directly |
## Functions
### dns.Query
```lua
local ips, ttl, err = dns.Query(domain, ipv4, ipv6, fake)
```
Queries a domain through Xray's DNS client. Unavailable in the DNS Hook, because the script takes over that very query flow.
**Parameters**
| Parameter | Type | Description |
| --------- | --------- | -------------------------------- |
| `domain` | `string` | Domain to query |
| `ipv4` | `boolean` | Whether IPv4 queries are allowed |
| `ipv6` | `boolean` | Whether IPv6 queries are allowed |
| `fake` | `boolean` | Whether FakeDNS is allowed |
**Return Values**
| Return value | Type | Description |
| ------------ | ----------------------------------------------- | ----------------------------------------------------------- |
| `ips` | [`[]net.IP`](./data-types.md#net-ip-1) or `nil` | Query results |
| `ttl` | `number` | TTL in seconds, in the range `0 .. 4294967295`; never `nil` |
| `err` | [`error`](./data-types.md#error) or `nil` | Query error; `nil` means no error |
**Behavior**
All four parameters must be passed explicitly with the correct types; otherwise a Lua exception is raised.
The query follows the client's global query-type restrictions, Hosts, and upstream configuration. If a [DNS script](../guide/dns.md) is configured, the query is also handled by that script. System DNS is used if built-in DNS is not configured.
Returns a non-empty IP slice on success; returns `nil, 0, err` if the query fails or no address is available.
**Example**
```lua
local ips, ttl, err = dns.Query("example.com", true, true, false)
if err == nil and ips ~= nil and #ips > 0 then
local firstIP = ips[1]:String()
end
```
## Fields
### dns.Servers
```lua
local servers = dns.Servers
```
| Name | Type | Description |
| ------------- | ------- | ----------------------------------------------------------------------- |
| `dns.Servers` | `table` | Array of [`serverObj`](#server), indexed from 1, in configuration order |
When built-in DNS is used, the list contains the configured upstreams. If DNS or upstream servers are not configured, the list contains only `localhost` (system DNS).
Use `ipairs` to iterate over the list; `servers[i]` is `nil` for an out-of-range index.
**Example**
```lua
for _, serverObj in ipairs(dns.Servers) do
local id = serverObj.ID
end
```
## Objects
### server
`serverObj` represents a DNS upstream server, including its identifier and query method.
| Property | Description |
| ------------------ | ------------------------------------------------- |
| Go implementation | `luaDNSServer` in `app/dns` |
| Lua representation | `table` |
| Obtained from | An array element of [`dns.Servers`](#dns-servers) |
#### ID
```lua
local id = serverObj.ID
```
| Name | Type | Description |
| -------------- | -------- | ----------------------------------------------------------------------------------------------------- |
| `serverObj.ID` | `string` | The `id` field in the [DNS server configuration](../../../config/dns.md#dnsserverobject); never `nil` |
If `id` is omitted, it is `""`, including for servers configured as strings. The automatically added system DNS upstream has ID `"localhost"`.
If you need to distinguish servers by ID, configure unique values yourself; the core does not check for duplicates.
#### Query
```lua
local ips, ttl, err = serverObj:Query(domain, ipv4, ipv6, fake)
```
Queries the upstream represented by `serverObj`.
**Parameters and Return Values**
Parameter requirements and return-value types are the same as for [`dns.Query`](#dns-query).
**Behavior**
A normally configured upstream returns a non-empty IP slice, TTL, and `nil` on success. If the query fails, no address meets the conditions, or FakeDNS is queried with `fake == false`, returns `nil, 0, err`.
Queries this server directly, bypassing global Hosts and the DNS script. The server's own `queryStrategy`, cache, `timeoutMs`, `clientIP`, `tag`, and `expectedIPs` / `unexpectedIPs` with their corresponding `actPrior` / `actUnprior` settings still take effect.
**Example**
Return the first upstream's query result directly from the DNS Hook:
```lua
function HandleDNSQuery(domain, ipv4, ipv6, fake)
return dns.Servers[1]:Query(domain, ipv4, ipv6, fake)
end
```
@@ -0,0 +1,344 @@
# xray.geodata
Provides rule-based matching for domains and IPs.
```lua
local geodata = require("xray.geodata")
```
## API Index
| Category | Member | Description |
| -------- | ---------------------------------------------------------------- | ---------------------------------------- |
| Function | [`geodata.BuildDomainMatcher(...)`](#geodata-builddomainmatcher) | Create a `DomainMatcher` |
| Function | [`geodata.BuildIPMatcher(...)`](#geodata-buildipmatcher) | Create an `IPMatcher` |
| Object | [`DomainMatcher`](#domainmatcher) | Domain matcher |
| Method | [`domainMatcherObj:MatchAny(domain)`](#matchany) | Check whether a domain matches any rule |
| Method | [`domainMatcherObj:Match(domain)`](#match) | Get the indices of matching domain rules |
| Object | [`IPMatcher`](#ipmatcher) | IP matcher |
| Method | [`ipMatcherObj:Match(ip)`](#match-1) | Match a single IP |
| Method | [`ipMatcherObj:AnyMatch(ips)`](#anymatch) | Check whether any IP matches |
| Method | [`ipMatcherObj:Matches(ips)`](#matches) | Check whether the entire IP list matches |
| Method | [`ipMatcherObj:FilterIPs(ips)`](#filterips) | Separate matching and non-matching IPs |
| Method | [`ipMatcherObj:SetReverse(reverse)`](#setreverse) | Set the reverse flag |
| Method | [`ipMatcherObj:ToggleReverse()`](#togglereverse) | Toggle the reverse flag |
## Functions
### geodata.BuildDomainMatcher
```lua
local domainMatcherObj = geodata.BuildDomainMatcher(rule1, rule2, ...)
```
Creates a [`DomainMatcher`](#domainmatcher) from domain rules.
**Parameters**
| Parameter | Type | Description / Requirements |
| ------------------- | -------- | ------------------------------------------------------- |
| `rule1, rule2, ...` | `string` | Required; at least one domain rule, passed individually |
**Return Values**
| Return value | Type | Description |
| ------------------ | --------------------------------- | ------------------------- |
| `domainMatcherObj` | [`DomainMatcher`](#domainmatcher) | A domain matcher instance |
**Behavior**
Raises a Lua exception if no rules are supplied, an argument has the wrong type, or rule parsing, resource loading, or construction fails.
Supports `domain:`, `full:`, `keyword:`, `regexp:`, `dotless:`, `geosite:`, and `ext:` (also written as `ext-domain:` / `ext-site:`). See [routing domain rules](../../../config/routing.md#ruleobject) for the formats. GeoSite files are loaded from the [resource directory](../../../config/env.md#resource-file-path).
Strings without a prefix use `domain:` rules by default.
Except for regular expressions, rules are converted to lowercase during construction.
**Example**
```lua
local sitesObj = geodata.BuildDomainMatcher(
"example.com", "full:other.example"
)
```
### geodata.BuildIPMatcher
```lua
local ipMatcherObj = geodata.BuildIPMatcher(rule1, rule2, ...)
```
Creates an [`IPMatcher`](#ipmatcher) from IP rules.
**Parameters**
| Parameter | Type | Description / Requirements |
| ------------------- | -------- | --------------------------------------------------- |
| `rule1, rule2, ...` | `string` | Required; at least one IP rule, passed individually |
**Return Values**
| Return value | Type | Description |
| -------------- | ------------------------- | ---------------------- |
| `ipMatcherObj` | [`IPMatcher`](#ipmatcher) | An IP matcher instance |
**Behavior**
Raises a Lua exception if no rules are supplied, an argument has the wrong type, a rule is an empty string, or rule parsing, resource loading, or construction fails.
Supports IPs, CIDR, `geoip:`, `ext:` (also written as `ext-ip:`), and `!` reverse rules. See [routing IP rules](../../../config/routing.md#ruleobject) for how rules are combined. GeoIP files are loaded from the [resource directory](../../../config/env.md#resource-file-path).
**Example**
```lua
local privateIPsObj = geodata.BuildIPMatcher(
"10.0.0.0/8", "192.168.0.0/16", "fc00::/7"
)
```
## Objects
### DomainMatcher
| Property | Description |
| ------------------ | ------------------------------------------------------------------------------------ |
| Underlying Go type | An implementation of the `geodata.DomainMatcher` interface |
| Lua representation | `userdata` |
| Obtained from | The return value of [`geodata.BuildDomainMatcher(...)`](#geodata-builddomainmatcher) |
**Common Conventions**
Matching methods take one domain string. `nil`, omitted arguments, incorrect types, or an incorrect argument count raise a Lua exception.
The input domain is not automatically converted to lowercase; the script must do this as needed.
#### MatchAny
```lua
local matched = domainMatcherObj:MatchAny(domain)
```
Checks whether a domain matches at least one rule.
**Parameters**
| Parameter | Type | Description / Requirements |
| --------- | -------- | -------------------------- |
| `domain` | `string` | Required; domain to match |
**Return Values**
| Return value | Type | Description |
| ------------ | --------- | ------------------------------------ |
| `matched` | `boolean` | `true` if matched, otherwise `false` |
**Example**
```lua
local matched = sitesObj:MatchAny("www.example.com")
```
#### Match
```lua
local indices = domainMatcherObj:Match(domain)
```
Gets the indices of rules matched by the domain.
**Parameters**
| Parameter | Type | Description / Requirements |
| --------- | -------- | -------------------------- |
| `domain` | `string` | Required; domain to match |
**Return Values**
| Return value | Type | Description |
| ------------ | --------------------------------------------- | ---------------------------------------------------------------------- |
| `indices` | [`[]uint32`](./data-types.md#uint32) or `nil` | Matching rule indices; `nil` or a slice of length 0 if nothing matches |
**Behavior**
Rule indices start at **0** and correspond to the order of rules supplied during construction. Result order is not guaranteed, and duplicate indices may occur. Entries expanded from GeoSite retain the index of their parent rule.
If you store the same rules in a Lua array, use `rules[ruleNumber + 1]` to get the original rule. See [Data Types](./data-types.md#slice) for slice access.
**Example**
```lua
local indices = sitesObj:Match("www.example.com")
if indices ~= nil then
for i = 1, #indices do
local ruleNumber = indices[i]
end
end
```
### IPMatcher
| Property | Description |
| ------------------ | ---------------------------------------------------------------------------- |
| Underlying Go type | An implementation of the `geodata.IPMatcher` interface |
| Lua representation | `userdata` |
| Obtained from | The return value of [`geodata.BuildIPMatcher(...)`](#geodata-buildipmatcher) |
**Common Conventions**
For a single IP, use [`net.IP`](./data-types.md#net-ip). For matching or filtering lists, see [IP List Input](./data-types.md#ip-list-input). IP address strings are not automatically parsed into `net.IP`.
Matching and filtering methods all accept explicit `nil`. Omitted arguments, incorrect types, or an incorrect argument count raise a Lua exception.
#### Match
```lua
local matched = ipMatcherObj:Match(ip)
```
Checks whether a single IP matches the rules.
**Parameters**
| Parameter | Type | Description / Requirements |
| --------- | ------------------------------------------- | ----------------------------------- |
| `ip` | [`net.IP`](./data-types.md#net-ip) or `nil` | Required; must be passed explicitly |
**Return Values**
| Return value | Type | Description |
| ------------ | --------- | ---------------------------------------------------------------- |
| `matched` | `boolean` | `true` if a valid IP matches; `false` for `nil` or an invalid IP |
**Behavior**
Passing `""` or an empty table `{}` is treated as an empty IP and returns `false`. Strings are not parsed as IP address text.
#### AnyMatch
```lua
local matched = ipMatcherObj:AnyMatch(ips)
```
Checks whether at least one valid IP matches the rules.
**Parameters**
| Parameter | Type | Description / Requirements |
| --------- | ---------------------------------------------- | ----------------------------------------------------- |
| `ips` | [IP List Input](./data-types.md#ip-list-input) | Required; must be passed explicitly; `nil` is allowed |
**Return Values**
| Return value | Type | Description |
| ------------ | --------- | -------------------------------------------------------------------------------------- |
| `matched` | `boolean` | `true` if at least one valid IP matches; `false` for `nil`, an empty list, or no match |
#### Matches
```lua
local matched = ipMatcherObj:Matches(ips)
```
Checks whether the entire IP list matches the rules.
**Parameters**
| Parameter | Type | Description / Requirements |
| --------- | ---------------------------------------------- | ----------------------------------------------------- |
| `ips` | [IP List Input](./data-types.md#ip-list-input) | Required; must be passed explicitly; `nil` is allowed |
**Return Values**
| Return value | Type | Description |
| ------------ | --------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `matched` | `boolean` | `true` if the entire list meets the matching requirements; `false` for `nil`, an empty list, or a list containing invalid IPs |
**Behavior**
Combined rules may contain multiple internal matchers. A single internal matcher must match the entire list.
For example, with `geodata.BuildIPMatcher("192.168.1.0/24", "geoip:us")`, a list containing both `192.168.1.1` and `8.8.8.8` makes `Matches` return `false`: the first IP matches the custom CIDR, and the second matches `geoip:us`, but these belong to different internal matchers, and neither matches the entire list.
If you only need to check whether any address matches, use [`AnyMatch`](#anymatch).
#### FilterIPs
```lua
local matched, unmatched = ipMatcherObj:FilterIPs(ips)
```
Separates valid IPs into matching and non-matching groups.
**Parameters**
| Parameter | Type | Description / Requirements |
| --------- | ---------------------------------------------- | ----------------------------------------------------- |
| `ips` | [IP List Input](./data-types.md#ip-list-input) | Required; must be passed explicitly; `nil` is allowed |
**Return Values**
| Return value | Type | Description |
| ------------ | -------------------------------------- | ----------------------------- |
| `matched` | [`[]net.IP`](./data-types.md#net-ip-1) | Matching IPs; never `nil` |
| `unmatched` | [`[]net.IP`](./data-types.md#net-ip-1) | Non-matching IPs; never `nil` |
**Behavior**
A group with no results is an empty slice. For `nil`, an empty list, or a list containing only invalid IPs, both groups are empty slices.
Invalid IPs are discarded, and result order is not guaranteed to match the input order.
#### SetReverse
```lua
ipMatcherObj:SetReverse(reverse)
```
Sets the reverse flag on each internal matcher.
**Parameters**
| Parameter | Type | Description / Requirements |
| --------- | --------- | -------------------------------------------------------------- |
| `reverse` | `boolean` | Required; `true` enables reverse matching, `false` disables it |
**Return Values**
No return values.
**Behavior**
`nil`, omission, or an incorrect type raises a Lua exception. This operation overrides each internal matcher's existing reverse flag, including flags specified with `!` in rules. Reverse matching applies only to address families present in the original rules and does not match invalid IPs.
The resulting reverse state is stored in the current matcher object. If later Hook calls reuse that object, the modified state remains in effect. See [State in Pooled Instances](../guide/lifecycle.md#state-in-pooled-instances).
**Example**
Reversing `10.0.0.0/8` matches valid IPv4 addresses outside that range:
```lua
local ipMatcherObj = geodata.BuildIPMatcher("10.0.0.0/8")
ipMatcherObj:SetReverse(true)
```
#### ToggleReverse
```lua
ipMatcherObj:ToggleReverse()
```
Toggles the reverse flag on each internal matcher.
**Parameters**
No additional parameters.
**Return Values**
No return values.
**Behavior**
Each internal matcher in combined rules toggles its own reverse flag. This is not equivalent to logically negating the combined result.
The resulting reverse state is stored in the current matcher object. If later Hook calls reuse that object, the modified state remains in effect. See [State in Pooled Instances](../guide/lifecycle.md#state-in-pooled-instances).
@@ -0,0 +1,69 @@
# xray.log
Writes logs to Xray's logging system.
```lua
local log = require("xray.log")
```
## API Index
| Category | Member | Description |
| -------- | ---------------------------------- | ------------------- |
| Function | [`log.Debug(...)`](#log-debug) | Write a debug log |
| Function | [`log.Info(...)`](#log-info) | Write an info log |
| Function | [`log.Warning(...)`](#log-warning) | Write a warning log |
| Function | [`log.Error(...)`](#log-error) | Write an error log |
## Functions
### log.Debug
```lua
log.Debug(...)
```
Writes a log at the `debug` level.
**Parameters**
| Parameter | Type | Description / Requirements |
| --------- | ------------- | --------------------------------------------------------- |
| `...` | Any Lua value | Optional; any number of log values, concatenated in order |
**Return Values**
No return values.
**Argument Conversion**
Accepts no arguments, `nil`, `""`, `{}`, and empty slices. Arguments are converted to text and concatenated in order, without automatically inserting spaces or separators.
- [`error`](./data-types.md#error) values are converted to their error messages first.
- Other values with a function-valued `__tostring` use that conversion; exceptions it raises propagate to the caller.
- Remaining values use their default string representation. `nil` becomes `"nil"`, and empty strings add no text. Tables and slices are not expanded into element lists.
**Output Conditions**
Logs are prefixed with the calling script's filename. Whether they are written depends on [`log.loglevel`](../../../config/log.md#logobject). For example, at `warning`, `Debug` and `Info` produce no output and do not invoke their arguments' `__tostring` conversions.
None of the four functions returns `err`. Assigning a call to a variable gives `nil`, which cannot be used to determine whether a log was successfully written.
**Example**
```lua
log.Info("Query domain: ", domain)
log.Warning("Upstream query failed: ", err)
```
### log.Info
Same as above.
### log.Warning
Same as above.
### log.Error
Same as above.
@@ -0,0 +1,119 @@
# xray.router
Provides constants and helpers for [routing scripts](../guide/routing.md). See [Routing Hook](./hook-routing.md) for request parameters and context.
```lua
local router = require("xray.router")
```
## API Index
| Category | Member | Description |
| -------- | ---------------------------------------------------------- | --------------------------------------------------- |
| Constant | [`router.NetworkUnknown`](#constants) | Unknown network |
| Constant | [`router.NetworkTCP`](#constants) | TCP |
| Constant | [`router.NetworkUDP`](#constants) | UDP |
| Constant | [`router.NetworkUNIX`](#constants) | UNIX |
| Constant | [`router.LocalOS`](#constants) | Runtime platform |
| Function | [`router:PickOutbound(balancerTag)`](#router-pickoutbound) | Select an outbound through a balancer |
| Function | [`router.FindProcess(ctx)`](#router-findprocess) | Find the process associated with a local connection |
## Constants
| Name | Type | Value / Description |
| ----------------------- | -------- | --------------------------------------------------------------------------- |
| `router.NetworkUnknown` | `number` | `0`, unknown network |
| `router.NetworkTCP` | `number` | `2`, TCP |
| `router.NetworkUDP` | `number` | `3`, UDP |
| `router.NetworkUNIX` | `number` | `4`, UNIX |
| `router.LocalOS` | `string` | Current runtime platform, for example `"windows"`, `"linux"`, or `"darwin"` |
Compare the [Routing Hook](./hook-routing.md#handleroute)'s `network` parameter with the network constants:
```lua
if network == router.NetworkUDP then
return "direct", "lua-udp"
end
```
## Functions
### router:PickOutbound
```lua
local outboundTag, err = router:PickOutbound(balancerTag)
```
Selects an outbound through the specified balancer.
**Parameters**
| Parameter | Type | Description / Requirements |
| ------------- | -------- | ---------------------------------------------------------------------------------------------------------------- |
| `balancerTag` | `string` | The `tag` of the balancer to use, configured in [`routing.balancers`](../../../config/routing.md#balancerobject) |
**Return Values**
| Return value | Type | Description |
| ------------- | ----------------------------------------- | --------------------------------------------------- |
| `outboundTag` | `string` or `nil` | On success, the selected outbound's non-empty `tag` |
| `err` | [`error`](./data-types.md#error) or `nil` | `nil` indicates success |
**Behavior**
A string must be passed explicitly; otherwise a Lua exception is raised. An empty string `""` looks up an empty `tag`.
Check `err` before using the result. If the balancer does not exist, returns `nil, err`; if it exists but selects no outbound, returns `"", err`. When the configured `fallbackTag` takes effect, returns that outbound tag and `nil`.
**Example**
The following uses the balancer with `tag` `"proxy-pool"` and passes on the result according to the [Routing Hook's return-value convention](./hook-routing.md#return-values):
```lua
local outboundTag, err = router:PickOutbound("proxy-pool")
return outboundTag, "lua-balance", err
```
See the [Routing Scripts guide](../guide/routing.md#relationship-with-routing-configuration) for a complete example.
### router.FindProcess
```lua
local pid, name, path, err = router.FindProcess(ctx)
```
Finds the process on the local machine associated with the current connection.
**Parameters**
| Parameter | Type | Description / Requirements |
| --------- | ------------------------------------------------------ | --------------------------------------------------- |
| `ctx` | [`routing.Context`](./hook-routing.md#routing-context) | Current request context, passed to the Routing Hook |
**Return Values**
| Return value | Type | Description |
| ------------ | ----------------------------------------- | -------------------------------------- |
| `pid` | `number` | Process ID; usually `0` if unavailable |
| `name` | `string` | Process name; `""` if unavailable |
| `path` | `string` | Executable path; `""` if unavailable |
| `err` | [`error`](./data-types.md#error) or `nil` | `nil` indicates success |
**Behavior**
A valid routing context must be passed explicitly; otherwise a Lua exception is raised. Returns an error if there is no source IP, the network type is not TCP/UDP, lookup fails, or the platform is unsupported.
`pid`, `name`, and `path` are never `nil`, but may retain partial information on failure; check `err` before using them. If there is no source IP or the network type is unsupported, returns `0, "", "", err`; otherwise returns the platform lookup results unchanged.
The lookup uses the first source IP and source port. If target IPs are available, it also passes the first target IP and target port. The platform lookup implementation determines the exact matching behavior.
Lookup capabilities depend on the platform and permissions. Windows, Linux, and macOS provide built-in implementations. Android requires the runtime environment to register a lookup implementation, and even successful results may have no name or path. iOS and other unsupported platforms return an error.
**Example**
```lua
local pid, name, path, err = router.FindProcess(ctx)
if err == nil and name == "curl" then
return "direct", "lua-process"
end
```