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
+5
View File
@@ -72,6 +72,11 @@ export const nav: DefaultTheme.Config["nav"] = [
text: "Protocol Details",
link: "/en/development/protocols/",
activeMatch: "^/en/development/protocols/"
},
{
text: "Lua Scripts",
link: "/en/development/lua/",
activeMatch: "^/en/development/lua/"
}
]
},
+5
View File
@@ -72,6 +72,11 @@ export const nav: DefaultTheme.Config["nav"] = [
text: "协议详解",
link: "/development/protocols/",
activeMatch: "^/development/protocols/"
},
{
text: "Lua 脚本",
link: "/development/lua/",
activeMatch: "^/development/lua/"
}
]
},
+5
View File
@@ -72,6 +72,11 @@ export const nav: DefaultTheme.Config["nav"] = [
text: "Детали протоколов",
link: "/ru/development/protocols/",
activeMatch: "^/ru/development/protocols/"
},
{
text: "Скрипты Lua",
link: "/ru/development/lua/",
activeMatch: "^/ru/development/lua/"
}
]
},
+70
View File
@@ -316,6 +316,76 @@ export const sidebar: DefaultTheme.Config["sidebar"] = {
link: "/en/development/protocols/mkcp.md"
}
]
},
{
text: "Lua Scripts",
link: "/en/development/lua/",
collapsed: true,
items: [
{
text: "Getting Started",
collapsed: true,
items: [
{ text: "Overview", link: "/en/development/lua/" },
{
text: "Runtime Environment and Script Loading",
link: "/en/development/lua/environment.md"
}
]
},
{
text: "Writing Guides",
collapsed: true,
items: [
{
text: "Routing Scripts",
link: "/en/development/lua/guide/routing.md"
},
{
text: "DNS Scripts",
link: "/en/development/lua/guide/dns.md"
},
{
text: "Instances and Lifecycle",
link: "/en/development/lua/guide/lifecycle.md"
}
]
},
{
text: "Reference",
collapsed: true,
items: [
{
text: "Routing Hook",
link: "/en/development/lua/reference/hook-routing.md"
},
{
text: "DNS Hook",
link: "/en/development/lua/reference/hook-dns.md"
},
{
text: "Data Types",
link: "/en/development/lua/reference/data-types.md"
},
{
text: "xray.router",
link: "/en/development/lua/reference/module-router.md"
},
{
text: "xray.dns",
link: "/en/development/lua/reference/module-dns.md"
},
{
text: "xray.geodata",
link: "/en/development/lua/reference/module-geodata.md"
},
{
text: "xray.log",
link: "/en/development/lua/reference/module-log.md"
}
]
}
]
}
]
}
+70
View File
@@ -271,6 +271,76 @@ export const sidebar: DefaultTheme.Config["sidebar"] = {
},
{ text: "mKCP 协议", link: "/development/protocols/mkcp.md" }
]
},
{
text: "Lua 脚本",
link: "/development/lua/",
collapsed: true,
items: [
{
text: "入门",
collapsed: true,
items: [
{ text: "概览", link: "/development/lua/" },
{
text: "运行环境与脚本加载",
link: "/development/lua/environment.md"
}
]
},
{
text: "编写指南",
collapsed: true,
items: [
{
text: "路由脚本",
link: "/development/lua/guide/routing.md"
},
{
text: "DNS 脚本",
link: "/development/lua/guide/dns.md"
},
{
text: "实例与生命周期",
link: "/development/lua/guide/lifecycle.md"
}
]
},
{
text: "参考",
collapsed: true,
items: [
{
text: "路由 Hook",
link: "/development/lua/reference/hook-routing.md"
},
{
text: "DNS Hook",
link: "/development/lua/reference/hook-dns.md"
},
{
text: "数据类型",
link: "/development/lua/reference/data-types.md"
},
{
text: "xray.router",
link: "/development/lua/reference/module-router.md"
},
{
text: "xray.dns",
link: "/development/lua/reference/module-dns.md"
},
{
text: "xray.geodata",
link: "/development/lua/reference/module-geodata.md"
},
{
text: "xray.log",
link: "/development/lua/reference/module-log.md"
}
]
}
]
}
]
}
+70
View File
@@ -349,6 +349,76 @@ export const sidebar: DefaultTheme.Config["sidebar"] = {
link: "/ru/development/protocols/mkcp.md"
}
]
},
{
text: "Скрипты Lua",
link: "/ru/development/lua/",
collapsed: true,
items: [
{
text: "Начало работы",
collapsed: true,
items: [
{ text: "Обзор", link: "/ru/development/lua/" },
{
text: "Среда выполнения и загрузка скриптов",
link: "/ru/development/lua/environment.md"
}
]
},
{
text: "Руководства по написанию",
collapsed: true,
items: [
{
text: "Скрипты маршрутизации",
link: "/ru/development/lua/guide/routing.md"
},
{
text: "Скрипты DNS",
link: "/ru/development/lua/guide/dns.md"
},
{
text: "Экземпляры и жизненный цикл",
link: "/ru/development/lua/guide/lifecycle.md"
}
]
},
{
text: "Справочник",
collapsed: true,
items: [
{
text: "Hook маршрутизации",
link: "/ru/development/lua/reference/hook-routing.md"
},
{
text: "Hook DNS",
link: "/ru/development/lua/reference/hook-dns.md"
},
{
text: "Типы данных",
link: "/ru/development/lua/reference/data-types.md"
},
{
text: "xray.router",
link: "/ru/development/lua/reference/module-router.md"
},
{
text: "xray.dns",
link: "/ru/development/lua/reference/module-dns.md"
},
{
text: "xray.geodata",
link: "/ru/development/lua/reference/module-geodata.md"
},
{
text: "xray.log",
link: "/ru/development/lua/reference/module-log.md"
}
]
}
]
}
]
}
+12
View File
@@ -31,6 +31,8 @@ Xray 内置的 DNS 模块,主要有三大用途:
域名将先执行 Hosts 映射检查(详见 `hosts` 字段),若没有查出需要的 IP,则继续使用 DNS 服务器进行查询。
如果配置了 `script`,后续 DNS 查询交给 [Lua 脚本](../development/lua/guide/dns.md),不再执行下面的内置处理流程。
而后核心将开始构建一个列表,核心会根据请求的域名,将服务器进行排序,遵循以下规则。
- 构建列表 1:包含 `domains` 字段成功命中了请求域名的服务器,顺序与配置文件中相同。
@@ -54,6 +56,7 @@ Xray 内置的 DNS 模块,主要有三大用途:
"baidu.com": "127.0.0.1",
"dns.google": ["8.8.8.8", "8.8.4.4"]
},
"script": "",
"servers": [
"8.8.8.8",
"8.8.4.4",
@@ -107,6 +110,10 @@ Xray 内置的 DNS 模块,主要有三大用途:
其匹配格式(`domain:` `full:` 等等)同常用的 [路由系统](./routing.md#ruleobject) 中的 domain. 不同的是无前缀时此处默认使用 `full:` 前缀(类似常见的 hosts 文件写法)
> `script`: string
Lua 脚本文件路径,默认为空字符串。设置后,内置 DNS 处理流程由 Lua 脚本接管;用法见[DNS 脚本指南](../development/lua/guide/dns.md)。
> `servers`: \[string | [DnsServerObject](#dnsserverobject) \]
一个 DNS 服务器列表,支持的类型有两种:DNS 地址(字符串形式)和 [DnsServerObject](#dnsserverobject) 。
@@ -267,6 +274,7 @@ DNS 回退(failover)默认是串行的,即默认仅在选中的 DNS 服务
```json
{
"id": "primary",
"address": "1.2.3.4",
"port": 5353,
"domains": ["domain:xray.com"],
@@ -281,6 +289,10 @@ DNS 回退(failover)默认是串行的,即默认仅在选中的 DNS 服务
}
```
> `id`: string
服务器标识,未配置时为空字符串,供 Lua 脚本通过 [`serverObj.ID`](../development/lua/reference/module-dns.md#id) 识别上游。
> `address`: address
一个 DNS 服务器列表,支持的类型有两种:DNS 地址(字符串形式)和 DnsServerObject 。
+5
View File
@@ -15,6 +15,7 @@
"routing": {
"domainStrategy": "AsIs",
"rules": [],
"script": "",
"balancers": []
}
}
@@ -44,6 +45,10 @@
当没有匹配到任何规则时,流量默认由第一个 outbound 发出。
:::
> `script`: string
Lua 脚本文件路径,默认为空字符串。设置后,路由选择由 Lua 脚本接管;用法见[路由脚本指南](../development/lua/guide/routing.md)。
> `balancers`: \[ [BalancerObject](#balancerobject) \]
一个数组,数组中每一项是一个负载均衡器的配置。
+35
View File
@@ -0,0 +1,35 @@
# 运行环境与脚本加载
## Lua 环境
当前使用 GopherLua,支持 Lua 5.1 语法、Lua 5.2 的 `goto` 语句,以及独有的 [`channel`](https://github.com/yuin/gopher-lua#lua-api)。
Xray 根据配置文件加载 Lua 脚本。配置的位置决定脚本用于什么功能,不同位置加载的脚本可用的 Hook 也不同。各入口可用的 Hook 及其调用约定见 [Hook 参考](./index.md#hook)。
## 模块加载
API 模块按功能组织,通过 `require` 加载:
```lua
local geodata = require("xray.geodata")
local log = require("xray.log")
```
模块列表见[模块 API](./index.md#模块-api),各函数的用法见对应 API 参考。
加载 Lua 模块、创建对象等顶层初始化的执行时机,见[实例与生命周期](./guide/lifecycle.md)。
## 脚本文件路径
配置文件中的 `script` 字段用于指定 Lua 脚本文件的路径。空字符串或未配置表示不启用脚本;绝对路径直接定位文件。
相对路径按以下顺序查找;路径不存在时继续,存在但不是普通文件时立即报错:
1. 环境变量 `XRAY_LOCATION_CONFDIR` 指定的目录;
2. 环境变量 `XRAY_LOCATION_CONFIG` 指定的目录;
3. Xray 进程的当前工作目录;
4. Xray 可执行文件所在目录。
这些目录的设置见[环境变量](../../config/env.md)。相对路径不会自动以配置文件所在目录为基准。
例如,将脚本路径设为 `routing.lua` 后,Xray 会按上述顺序查找文件。Windows 绝对路径可以写成 `C:/Xray/routing.lua`;使用反斜杠时,请按所用配置格式的要求进行转义。
+155
View File
@@ -0,0 +1,155 @@
# DNS 脚本
通过 [`dns.script`](../../../config/dns.md#dnsobject) 指定脚本后,内置 DNS 处理流程由 Lua 的 `HandleDNSQuery` 函数接管。
## 最小示例
在已有配置中加入:
```json
{
"dns": {
"script": "dns.lua",
"servers": ["1.1.1.1"]
}
}
```
保存 `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
```
上面的配置片段需要合并到完整配置中。文件定位规则见[脚本文件路径](../environment.md#脚本文件路径)。
示例将参数原样传给上游,并直接返回查询结果。完整参数、返回值限制及失败行为见 [HandleDNSQuery](../reference/hook-dns.md)。
## 与 DNS 配置的关系
查询到达脚本前,仍经过内置 DNS 的域名检查、全局查询类型限制和 Hosts 处理。Hosts 已给出有效 IP 或明确拒绝请求时,不调用脚本;Hosts 替换域名时,脚本查询替换后的域名。
启用脚本后,上游选择由脚本控制,因此 `domains`、`skipFallback`、`finalQuery`、`disableFallback`、`disableFallbackIfMatch` 和 `enableParallelQuery` 不再有实际意义。
查询指定上游使用 [`serverObj:Query`](../reference/module-dns.md#query);单台服务器仍生效的配置项见该 API。
::: tip 小提示
上游与过滤规则的对应关系不变时,直接配置 `expectedIPs` / `unexpectedIPs` 更方便快捷;也可以不配置这两项,交给 Lua 使用 [`ipMatcherObj:FilterIPs`](../reference/module-geodata.md#filterips) 等方法自行过滤,更灵活。
:::
## 示例:按域名分流并过滤解析结果
下面的示例按域名分类选择上游,并按需要过滤中国 IP。为服务器配置 `id`,脚本按标识查询和回退:
```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`、`dns-proxy` 是 DNS 上游查询的入站标识,通过上面的路由规则分别选择直连出站 `direct` 和 VLESS 代理出站 `proxy`。
脚本先按域名选择查询顺序,再逐个查询上游;查询失败或过滤后为空时,尝试下一台服务器:
```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
-- Google 域名使用公共 DNS。
queries = {{"cf"}, {"google"}}
elseif cnDomainMatcher:MatchAny(domain) then
-- 中国域名先查直连 DNS,只保留中国 IP,再回退到代理 DNS。
queries = {
{"cn114", "cn"}, {"cn223", "cn"}, {"cf"}, {"google"}
}
elseif foreignDomainMatcher:MatchAny(domain) then
-- 非中国域名先排除中国 IP,再尝试带 ECS 的查询。
queries = {
{"cf", "non-cn"}, {"google", "non-cn"},
{"google-ecs"}, {"google-alt-ecs"}
}
else
-- 未收录域名先通过 ECS 寻找中国 IP,再回退到普通公共 DNS。
queries = {
{"google-ecs", "cn"}, {"google-alt-ecs", "cn"},
{"cf"}, {"google"}
}
end
local lastError = "没有符合条件的 DNS 结果"
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
```
DNS 脚本的顶层初始化和状态保留见[池化生命周期](./lifecycle.md#池化生命周期)。
+67
View File
@@ -0,0 +1,67 @@
# 实例与生命周期
Lua 实例的创建、Hook 调用、状态保留和销毁,由脚本入口采用的实例管理方式决定。本页按生命周期类型说明这些规则。
## 池化生命周期
当前[路由脚本](./routing.md)的 [HandleRoute](../reference/hook-routing.md) 和 [DNS 脚本](./dns.md)的 [HandleDNSQuery](../reference/hook-dns.md) 使用池化实例。以下规则适用于这两个 Hook。
### 实例池与初始化
路由脚本和 DNS 脚本分别通过对应配置中的 `script` 绑定脚本,各自管理独立的 Lua 实例池。即使配置为同一个文件,也不会共享 Lua 实例内的状态。
Xray 启动时会读取并编译脚本一次,然后创建第一个实例,执行脚本顶层代码,检查必需的处理函数是否存在。文件读取、语法、顶层执行或处理函数检查失败时,Xray 启动失败。
### Hook 调用与回收
每次路由选择或 DNS 查询独占一个实例。有空闲实例时直接复用;并发调用需要更多实例时,使用已编译的脚本创建新实例,并重新执行顶层代码。
处理函数正常执行完成后,实例可归还池中复用;未捕获的 Lua 异常或执行超时会使实例被销毁。处理函数通过返回值报告业务错误,或返回值校验失败,并不等同于 Lua 执行异常,实例仍可复用。多余的空闲实例会自动被销毁。
有空闲实例时,每次 Hook 的调度路径很轻:**取出实例 → 执行 Hook → 归还实例**。脚本读取与编译在 Xray 启动时完成。图中的绿色节点表示这条常规路径。
```mermaid
flowchart TD
LOAD["Xray 启动<br/>读取并编译主脚本一次"] --> INIT["创建首个 Lua 实例<br/>执行顶层代码并检查 Hook"]
INIT --> POOL[("空闲实例池")]
subgraph CALL["池化 Hook 调用:轻量复用路径"]
TAKE["取出并独占空闲实例"] --> RUN["执行 HandleRoute / HandleDNSQuery"]
RUN -->|正常执行结束| PUT["归还实例,保留状态"]
end
REQUEST["路由选择 / DNS 查询"] --> AVAILABLE{"池中有空闲实例?"}
POOL -.-> AVAILABLE
AVAILABLE -->|有| TAKE
PUT --> POOL
AVAILABLE -. 无:按需扩容 .-> CREATE["使用已编译脚本创建新实例<br/>执行顶层代码并检查 Hook"]
CREATE --> RUN
RUN -. 未捕获 Lua 异常 / 超时 .-> DESTROY["销毁实例"]
POOL -. 多余空闲实例 / Xray 关闭 .-> DESTROY
classDef reuse fill:#e8f5e9,stroke:#2e7d32,color:#1b5e20
class TAKE,RUN,PUT reuse
```
### 池化实例内状态
全局变量、顶层 `local`、闭包和 `require` 的模块缓存属于当前实例,会在该实例处理的多次调用之间保留,实例销毁时丢失:
```lua
local calls = 0
function HandleRoute()
calls = calls + 1
return "direct", "instance-call-" .. calls
end
```
上面的计数只表示当前实例处理过的调用次数。不同实例有独立的 `calls`,请求也不保证总是分配到同一个实例。它不能用作所有连接共享的计数器或某条连接的持续状态。
建议将加载 Lua 模块、创建匹配器、保存 DNS 服务器对象等初始化代码放在 Hook 函数外(脚本顶层)。这些代码在每个实例创建时执行一次,结果可供同一实例后续的 Hook 调用复用。
### 超时与脚本更新
当前路由和 DNS 的每个实例初始化超时为 **120 秒**,每次处理函数调用的执行超时为 **6 秒**。调用超时从取得实例后开始计算;这些值目前没有独立的配置字段。调用 Xray API 时,实际取消行为还取决于 API 是否响应取消信号;单台 DNS 服务器还受其 `timeoutMs` 限制。
Xray 不自动监视或重新编译主脚本。修改主脚本后,应重启 Xray 使其生效;之后创建的新实例也使用本次启动时编译的主脚本。
+119
View File
@@ -0,0 +1,119 @@
# 路由脚本
通过 [`routing.script`](../../../config/routing.md#routingobject) 指定脚本后,路由选择由 Lua 的 `HandleRoute` 函数接管。
## 最小示例
下面的配置片段指定脚本文件,并配置直连和黑洞出站:
```json
{
"routing": {
"script": "routing.lua"
},
"outbounds": [
{ "tag": "direct", "protocol": "freedom" },
{ "tag": "block", "protocol": "blackhole" }
]
}
```
保存 `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
```
脚本通过 `ctx` 读取源 IP。源 IP 为 `127.0.0.1` 时返回 `block`,其余情况返回 `direct`,分别对应上面配置中的出站 `tag`。
上面的配置片段需要合并到完整配置中。文件定位规则见[脚本文件路径](../environment.md#脚本文件路径)。
`HandleRoute` 还可以接收更多参数,并返回规则名称和错误;完整调用约定及失败行为见 [HandleRoute](../reference/hook-routing.md)。
## 与路由配置的关系
启用路由脚本后,`rules` 和 `domainStrategy` 均不生效。脚本未选中出站或发生错误时,也不会继续匹配内置路由规则。
`balancers` 仍可配置,脚本通过 [`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
```
将示例中的 `balance` 换成 `routing.balancers` 中已配置的负载均衡器的 `tag` 值。
## 示例:主动查询 DNS 后按 IP 分流
下面的脚本先处理 DNS 上游查询流量,然后检查请求已有的目标 IP,必要时主动解析域名,再按私有地址范围选择出站。
```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("解析 ", targetDomain, " 失败:", err)
elseif privateIPMatcher:AnyMatch(resolved) then
return "direct", "lua-resolved-private"
end
end
return "proxy", "lua-default"
end
```
普通模式的 DNS 上游请求也会进入路由。示例对 `skipDNSResolve` 为 `true` 或 `inboundTag` 为 `"dns-query"` 的请求直接选择出站,避免再次发起 DNS 解析而形成回环。
`dns.Query` 的结果只用于脚本判断,不会自动改写 `ctx` 的目标 IP 或实际连接目标。示例在解析失败时选择 `proxy`;可以按需要调整为自己的策略。
路由脚本的顶层初始化和状态保留见[池化生命周期](./lifecycle.md#池化生命周期)。
+46
View File
@@ -0,0 +1,46 @@
# Lua 脚本
Lua 脚本为 Xray 提供灵活的扩展方式,可用于实现自定义功能,与核心交互。
## 入门
先阅读[运行环境与脚本加载](./environment.md),了解 Lua 支持范围、模块加载方式和脚本文件定位规则。
## 编写指南
根据需要实现的功能选择指南,从最小示例开始,逐步编写自己的脚本:
| 指南 | 作用 | 内容 |
| ------------------------------ | ------------------- | ---------------------------------------- |
| [路由脚本](./guide/routing.md) | 自定义路由规则 | 出站选择、负载均衡、主动解析后按 IP 分流 |
| [DNS 脚本](./guide/dns.md) | 自定义 DNS 查询规则 | 上游选择、查询回退、结果过滤 |
当前的路由和 DNS 脚本都使用池化实例。编写脚本时,请结合[池化生命周期](./guide/lifecycle.md#池化生命周期)理解顶层初始化和状态保留。
## 参考
### Hook
Hook 是脚本中供核心在特定时机调用的 Lua 函数。脚本入口决定可用的 Hook 及其调用约定,一个入口可以对应多个 Hook。具体需要实现哪些 Hook,以相应入口的说明为准。
目前支持的入口和 Hook 如下,参数、返回值和失败行为见各 Hook 的参考页:
| 脚本入口 | Hook |
| ---------------- | ------------------------------------------ |
| `routing.script` | [HandleRoute](./reference/hook-routing.md) |
| `dns.script` | [HandleDNSQuery](./reference/hook-dns.md) |
### 数据类型
除 Lua 自带的数据类型外,Xray Lua 还使用 `net.IP`、Go slice 和 `error` 等类型,其在 Lua 中的表示与操作方式见[数据类型](./reference/data-types.md)。
### 模块 API
按功能查阅模块 API:
| 模块 | 用途 |
| --------------------------------------------- | -------------------- |
| [xray.router](./reference/module-router.md) | 策略路由 |
| [xray.dns](./reference/module-dns.md) | 域名解析 |
| [xray.geodata](./reference/module-geodata.md) | 域名和 IP 匹配、过滤 |
| [xray.log](./reference/module-log.md) | 输出日志 |
@@ -0,0 +1,215 @@
# 数据类型
本页汇总各模块共用的数据类型,说明它们在 Lua 中的表示和操作方式。模块独有的数据类型见各自页面。
## net.IP
| 属性 | 说明 |
| ----------- | ------------------------- |
| Go 底层类型 | `net.IP`、`common/net.IP` |
| Lua 表现 | `userdata` |
通常无需在 Lua 中逐个比较 `net.IP`,尤其应避免在热路径中这样做。需要匹配 IP 时,应优先使用 [`IPMatcher`](./module-geodata.md#ipmatcher),它可以直接处理 API 返回的 `net.IP`。
如果确实需要直接操作 IP,请注意 `net.IP` 与 IP 地址字符串是不同的类型。可使用以下两个方法进行转换和比较:
### String
```lua
local text = ip:String()
```
将 IP 转为地址文本。
**参数**
无额外参数。
**返回值**
| 返回值 | 类型 | 说明 |
| ------ | -------- | ------------------------- |
| `text` | `string` | IP 地址字符串,不为 `nil` |
### Equal
```lua
local equal = ip:Equal(otherIP)
```
比较两个 IP 是否相等。
**参数**
| 参数 | 类型 | 说明 |
| --------- | ---------------------------- | ------------------------------- |
| `otherIP` | [`net.IP`](#net-ip) 或 `nil` | 待比较的 IP,可以显式传入 `nil` |
**返回值**
| 返回值 | 类型 | 说明 |
| ------- | --------- | ------------------------------------------- |
| `equal` | `boolean` | 相等时为 `true`,否则为 `false`;不为 `nil` |
**行为约定**
有效 IP 与 `nil` 比较时返回 `false`。
**示例**
```lua
if ip ~= nil then
local text = ip:String()
local equal = ip:Equal(otherIP)
end
```
## slice
Go slice 在 Lua 中表示为 `userdata`,保留元素类型,下标从 **1** 开始。以下两种 slice 共用相同的长度、索引和遍历操作。
| 类型 | Lua 表现 | 元素类型 |
| ----------------------- | ---------- | ------------------- |
| [`[]net.IP`](#net-ip-1) | `userdata` | [`net.IP`](#net-ip) |
| [`[]uint32`](#uint32) | `userdata` | `number` |
### []net.IP
提示:需要匹配或筛选 IP 列表时,应优先使用 [`IPMatcher`](./module-geodata.md#ipmatcher),它可以直接处理 `[]net.IP`。
### []uint32
元素为 32 位无符号整数,取值范围为 `0 .. 4294967295`。
### #values
```lua
local count = #values
```
取得 slice 的元素数量。
**返回值**
| 返回值 | 类型 | 说明 |
| ------- | -------- | ------------------------- |
| `count` | `number` | 非负整数;空 slice 为 `0` |
**行为约定**
API 返回的列表可能为 `nil`,也可能是长度为 0 的 slice。调用上述操作前,`values` 必须非 `nil`;仅检查 `if values then` 不能判断是否有元素。
### values[i]
```lua
local value = values[i]
```
读取 slice 中的一个元素。
**参数**
| 参数 | 类型 | 说明 |
| ---- | -------- | ---------------------------------------- |
| `i` | `number` | 必填,必须是 `1 .. #values` 范围内的整数 |
**返回值**
| 返回值 | 类型 | 说明 |
| ------- | ------------------------------- | ---------------------------------------- |
| `value` | [`net.IP`](#net-ip) 或 `number` | 分别对应 `[]net.IP` 和 `[]uint32` 的元素 |
**行为约定**
越界访问会抛出 Lua 异常,不会返回 `nil`。避免直接修改 API 返回的 slice。
**示例**
```lua
if ips ~= nil and #ips > 0 then
local firstIP = ips[1]:String()
end
```
### values()
```lua
for i, value in values() do
-- 使用下标 i 和元素 value
end
```
取得 slice 迭代器,用于泛型 `for` 遍历,下标从 1 开始。
**参数**
无额外参数。
**返回值**
| 返回值 | 类型 | 说明 |
| ------ | ---------- | ---------------------------- |
| 迭代器 | `function` | 逐项提供下标和元素,用法如上 |
**行为约定**
slice 不支持 `pairs` 或 `ipairs`,也可以通过数值 `for` 遍历。
**示例**
```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 列表输入
向 `IPMatcher` 传入 IP 列表时,可以使用以下形式:
| 输入形式 | Lua 表现 | 说明 |
| ------------------------ | ---------- | ------------------------------------------------- |
| [`[]net.IP`](#net-ip-1) | `userdata` | IP 列表,允许空 slice |
| [`net.IP`](#net-ip) 数组 | `table` | 从 1 开始连续存放 IP,允许空数组 `{}`,不能有空洞 |
| `nil` | `nil` | 须显式传入;各方法的结果见对应条目 |
IP 地址字符串不会被解析为 `net.IP`。空字符串 `""` 不是合法 IP 列表,传给 `AnyMatch`、`Matches` 或 `FilterIPs` 会抛出 Lua 异常。
## error
| 属性 | 说明 |
| ----------- | --------------------------------- |
| Go 底层类型 | `error` |
| Lua 表现 | 错误为 `userdata`,无错误为 `nil` |
API 返回的 `err` 为 `nil` 时表示没有错误,否则为错误对象。错误对象可原样作为 Hook 的 `err` 返回值,将错误交给 Xray 核心处理。
**Hook 返回约定**
| 错误值 | 含义 |
| --------------- | -------------------------------------------- |
| `nil` | 没有错误 |
| 错误 `userdata` | 保留原始错误 |
| `string` | 脚本提供的错误消息,空字符串 `""` 仍表示错误 |
字符串错误仅适用于 Hook 返回。Xray API 自身返回错误或 `nil`。具体校验及失败行为见对应 Hook 的说明。
**输出错误消息**
Go 错误对象无法通过 `tostring(err)` 取得消息;需要输出时,直接将 `err` 传给 [`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 查询失败:", err)
elseif ips ~= nil and #ips > 0 then
local firstIP = ips[1]:String()
end
```
@@ -0,0 +1,67 @@
# DNS Hook
核心在内置 DNS 查询中调用 `dns.script` 中的全局函数 `HandleDNSQuery`。配置接入和完整示例见[DNS 脚本指南](../guide/dns.md)。
## API 索引
| 分类 | 成员 | 说明 |
| ---- | ---------------------------------------- | ------------- |
| Hook | [`HandleDNSQuery(...)`](#handlednsquery) | 处理 DNS 查询 |
## Hook 接口
### HandleDNSQuery(...)
```lua
function HandleDNSQuery(domain, ipv4, ipv6, fake)
return ips, ttl, err
end
```
处理一次 DNS 查询,返回 IP slice、TTL 或错误。
#### 参数
核心始终传入全部四个参数,均非 `nil`。
| 参数 | 类型 | 说明 |
| -------- | --------- | ---------------------------- |
| `domain` | `string` | 待查询域名,非空且已转为小写 |
| `ipv4` | `boolean` | 是否允许查询 IPv4 地址 |
| `ipv6` | `boolean` | 是否允许查询 IPv6 地址 |
| `fake` | `boolean` | 本次查询是否允许 FakeDNS |
`ipv4`、`ipv6` 已受全局 [`queryStrategy`](../../../config/dns.md#dnsobject) 限制,至少一个为 `true`。向上游查询时通常原样传递这三个 boolean 参数。
#### 返回值
| 返回值 | 类型 | 说明 |
| ------ | --------------------------------------------------- | --------------------------------------------------------------- |
| `ips` | [`[]net.IP`](./data-types.md#net-ip-1) 或 `nil` | 查询结果,必须是 slice,不接受 Lua 数组 |
| `ttl` | `number` 或 `nil` | TTL(秒);`err == nil` 时必须为 `0 .. 4294967295` 范围内的整数 |
| `err` | [`error`](./data-types.md#error)、`string` 或 `nil` | 仅 `nil` 表示无错误 |
#### 行为约定
核心依次校验 `err`、`ttl`、`ips`,遇到错误即停止;`err` 非 `nil` 时忽略其余两项。无错误时,即使 IP 结果为空,也必须提供合法 TTL。
返回空响应可写 `return nil, 0, nil`;错误应放在第三个位置,例如 `return nil, nil, "查询失败"`。
| 脚本结果 | 核心处理 |
| ---------------------------------------------------- | ---------------------- |
| 校验通过且 `ips` 非空 | 本次查询成功 |
| `err == nil`、TTL 合法,但 `ips` 为 `nil` 或空 slice | 本次查询失败(空响应) |
| 返回错误、返回值类型错误或执行异常 | 本次查询失败 |
#### 示例
可以直接转发 [`serverObj:Query`](./module-dns.md#query) 的三个返回值:
```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 @@
# 路由 Hook
核心在选择出站时调用 `routing.script` 中的全局函数 `HandleRoute`。配置接入和完整示例见[路由脚本指南](../guide/routing.md)。
## API 索引
| 分类 | 成员 | 说明 |
| ---- | --------------------------------------- | -------------------- |
| Hook | [`HandleRoute(...)`](#handleroute) | 为请求选择出站 |
| 对象 | [`routing.Context`](#routing-context) | 当前请求的路由上下文 |
| 方法 | [`ctx:GetSourceIPs()`](#getsourceips) | 取得来源 IP |
| 方法 | [`ctx:GetTargetIPs()`](#gettargetips) | 取得目标 IP |
| 方法 | [`ctx:GetLocalIPs()`](#getlocalips) | 取得本地 IP |
| 方法 | [`ctx:GetAttributes()`](#getattributes) | 取得请求属性 |
## Hook 接口
### HandleRoute(...)
```lua
function HandleRoute(ctx, inboundTag, sourcePort, targetPort,
localPort, targetDomain, network, protocol, user,
vlessRoute, skipDNSResolve)
return outboundTag, ruleTag, err
end
```
为当前请求选择出站,并可返回规则名称或错误。
#### 参数
核心始终按上述顺序传入全部 11 个参数,均非 `nil`。函数定义可省略不使用的末尾形参,这不会改变核心传入的值。
| 参数 | 类型 | 说明 | 空值与缺省值 |
| ---------------- | ------------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------ |
| `ctx` | [`routing.Context`](#routing-context) | 当前请求上下文 | 始终是有效的 `userdata` |
| `inboundTag` | `string` | 入站标识 | 入站未设置 tag 或没有入站信息时为 `""` |
| `sourcePort` | `number` | 来源端口,`0 .. 65535` | 没有有效来源地址时为 `0` |
| `targetPort` | `number` | 目标端口,`0 .. 65535` | 没有有效目标地址时为 `0` |
| `localPort` | `number` | 入站连接的本地端口,`0 .. 65535` | 没有有效本地地址时为 `0` |
| `targetDomain` | `string` | 优先取生效的嗅探域名,否则取连接目标中的域名,统一转为小写 | 目标为 IP 且无生效的嗅探域名,或缺少目标信息时为 `""` |
| `network` | `number` | 网络类型,与 [`router.NetworkTCP`](./module-router.md#常量) 等常量比较 | 没有出站信息或网络类型未知时为 `NetworkUnknown`(`0`) |
| `protocol` | `string` | 嗅探得到的协议 | 没有嗅探协议信息时为 `""` |
| `user` | `string` | 用户 email | 没有入站、用户信息或未设置 email 时为 `""` |
| `vlessRoute` | `number` | VLESS UUID 第 7、8 字节组成的路由值,`0 .. 65535` | 没有该信息时为 `0`,值本身也可能为 0 |
| `skipDNSResolve` | `boolean` | 为 `true` 时必须跳过 DNS 查询,避免回环 | 没有该标记或附加请求信息时为 `false` |
#### 返回值
| 返回值 | 类型 | 说明 |
| ------------- | --------------------------------------------------- | --------------------------------------------- |
| `outboundTag` | `string` 或 `nil` | 出站标识;`nil` 或 `""` 表示未选中 |
| `ruleTag` | `string` 或 `nil` | 可选的规则名称;`nil`、省略或 `""` 表示无名称 |
| `err` | [`error`](./data-types.md#error)、`string` 或 `nil` | 仅 `nil` 表示无错误 |
#### 行为约定
核心依次校验 `err`、`outboundTag`、`ruleTag`,遇到错误即停止;`err` 非 `nil` 时忽略前两项,未选中出站时忽略 `ruleTag`。
成功时可省略末尾的 `ruleTag` 和 `err`,例如 `return "direct"`。错误必须放在第三个位置,例如 `return nil, nil, "路由失败"`;`return nil, "路由失败"` 不会报告错误。
| 脚本结果 | 核心处理 |
| ---------------------------------- | ---------------------------------- |
| 校验通过且 `outboundTag` 非空 | 使用对应出站,标识不存在则关闭连接 |
| 未选中出站 | 使用默认出站 |
| 返回错误、返回值类型错误或执行异常 | 记录错误并尝试默认出站 |
默认出站是配置中的第一个出站;没有可用默认出站时关闭连接。若要阻断流量应显式返回已配置的 [`blackhole`](../../../config/outbounds/blackhole.md) 出站 tag 而不是利用此特性返回不存在的标识。
## 关联对象
### routing.Context
`routing.Context` 表示当前请求的路由上下文,在 `HandleRoute` 中通过参数 `ctx` 访问。
| 属性 | 说明 |
| ----------- | ---------------------------------------------- |
| Go 底层类型 | `features/routing` 中的 `routing.Context` 接口 |
| Lua 表现 | `userdata` |
| 取得方式 | 核心传入的 `HandleRoute` 第一个参数 |
下列方法均无额外参数。IP 及 slice 操作统一见[数据类型](./data-types.md)。
#### GetSourceIPs
```lua
local ips = ctx:GetSourceIPs()
```
取得请求的来源 IP。
**参数**
无额外参数。
**返回值**
| 返回值 | 类型 | 说明 |
| ------ | ----------------------------------------------- | ------------------ |
| `ips` | [`[]net.IP`](./data-types.md#net-ip-1) 或 `nil` | 对应地址的 IP 列表 |
**行为约定**
没有入站、来源地址无效或来源地址不是 IP 时返回 `nil`。
#### GetTargetIPs
```lua
local ips = ctx:GetTargetIPs()
```
取得请求的目标 IP。
**参数**
无额外参数。
**返回值**
| 返回值 | 类型 | 说明 |
| ------ | ----------------------------------------------- | ------------------ |
| `ips` | [`[]net.IP`](./data-types.md#net-ip-1) 或 `nil` | 对应地址的 IP 列表 |
**行为约定**
没有出站、目标地址无效或目标地址是域名时返回 `nil`。
#### GetLocalIPs
```lua
local ips = ctx:GetLocalIPs()
```
取得入站连接的本地 IP。
**参数**
无额外参数。
**返回值**
| 返回值 | 类型 | 说明 |
| ------ | ----------------------------------------------- | ------------------ |
| `ips` | [`[]net.IP`](./data-types.md#net-ip-1) 或 `nil` | 对应地址的 IP 列表 |
**行为约定**
没有入站、本地地址无效或本地地址不是 IP 时返回 `nil`。
#### GetAttributes
```lua
local attributes = ctx:GetAttributes()
```
取得当前请求的属性。
**参数**
无额外参数。
**返回值**
| 返回值 | 类型 | 说明 |
| ------------ | ------------------- | ------------------------------- |
| `attributes` | `map[string]string` | 始终返回 `userdata`,不为 `nil` |
**行为约定**
使用 `attributes[key]` 读取属性,`key` 必须为字符串。已存在的键返回字符串(可能为 `""`),不存在的键返回 `nil`。
属性的含义和用途见路由配置中的 [`attrs`](../../../config/routing.md#ruleobject) 字段。
**示例**
```lua
local attributes = ctx:GetAttributes()
if attributes[":method"] == "GET" then
return "direct", "lua-get"
end
```
@@ -0,0 +1,139 @@
# xray.dns
提供 DNS 查询和上游服务器访问接口。
```lua
local dns = require("xray.dns")
```
## API 索引
| 分类 | 成员 | 说明 |
| ---- | ----------------------------------------------------- | ------------------------ |
| 函数 | [`dns.Query(domain, ipv4, ipv6, fake)`](#dns-query) | 通过 Xray DNS 客户端查询 |
| 字段 | [`dns.Servers`](#dns-servers) | 上游服务器数组 |
| 对象 | [`serverObj`](#server) | 单个 DNS 上游服务器 |
| 字段 | [`serverObj.ID`](#id) | 服务器标识 |
| 方法 | [`serverObj:Query(domain, ipv4, ipv6, fake)`](#query) | 直接查询指定上游 |
## 函数
### dns.Query
```lua
local ips, ttl, err = dns.Query(domain, ipv4, ipv6, fake)
```
通过 Xray 的 DNS 客户端查询域名。在 DNS Hook 中不可用,因为脚本接管的就是它。
**参数**
| 参数 | 类型 | 说明 |
| -------- | --------- | -------------------- |
| `domain` | `string` | 待查询域名 |
| `ipv4` | `boolean` | 是否允许查询 IPv4 |
| `ipv6` | `boolean` | 是否允许查询 IPv6 |
| `fake` | `boolean` | 是否允许使用 FakeDNS |
**返回值**
| 返回值 | 类型 | 说明 |
| ------ | ----------------------------------------------- | ----------------------------------------------- |
| `ips` | [`[]net.IP`](./data-types.md#net-ip-1) 或 `nil` | 查询结果 |
| `ttl` | `number` | TTL(秒),范围 `0 .. 4294967295`,始终非 `nil` |
| `err` | [`error`](./data-types.md#error) 或 `nil` | 查询错误,`nil` 表示无错误 |
**行为约定**
四个参数均须显式传入且类型正确,否则抛出 Lua 异常。
查询遵循客户端的全局查询类型限制、Hosts 和上游配置;配置了 [DNS 脚本](../guide/dns.md)时,也会交由该脚本处理。未配置内置 DNS 时使用系统 DNS。
查询成功时返回非空 IP slice;查询失败或没有可用地址时返回 `nil, 0, err`。
**示例**
```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
```
## 字段
### dns.Servers
```lua
local servers = dns.Servers
```
| 名称 | 类型 | 说明 |
| ------------- | ------- | ---------------------------------------------------------- |
| `dns.Servers` | `table` | [`serverObj`](#server) 数组,下标从 1 开始,顺序与配置相同 |
使用内置 DNS 时,列表包含配置的上游;未配置 DNS 或上游服务器时,列表仅包含 `localhost`(系统 DNS)。
可用 `ipairs` 遍历列表;下标超出范围时,`servers[i]` 为 `nil`。
**示例**
```lua
for _, serverObj in ipairs(dns.Servers) do
local id = serverObj.ID
end
```
## 对象
### server
`serverObj` 表示一个 DNS 上游服务器,包含其标识和查询方法。
| 属性 | 说明 |
| --------- | ---------------------------------------- |
| Go 侧实现 | `app/dns` 中的 `luaDNSServer` |
| Lua 表现 | `table` |
| 取得方式 | [`dns.Servers`](#dns-servers) 的数组元素 |
#### ID
```lua
local id = serverObj.ID
```
| 名称 | 类型 | 说明 |
| -------------- | -------- | ----------------------------------------------------------------------------------------- |
| `serverObj.ID` | `string` | 对应 [DNS 服务器配置](../../../config/dns.md#dnsserverobject)中的 `id` 字段,始终非 `nil` |
未填写 `id` 时为 `""`,包括用字符串形式配置的服务器;自动添加的系统 DNS 上游 ID 为 `"localhost"`。
需要通过 ID 区分服务器时,请自行配置唯一的值,核心不会检查是否重复。
#### Query
```lua
local ips, ttl, err = serverObj:Query(domain, ipv4, ipv6, fake)
```
查询 `serverObj` 对应的上游。
**参数与返回值**
参数要求和返回值类型与 [`dns.Query`](#dns-query) 相同。
**行为约定**
正常配置的上游成功时返回非空 IP slice、TTL 和 `nil`;查询失败、没有符合条件的地址,或 `fake == false` 却查询 FakeDNS 时返回 `nil, 0, err`。
直接查询该服务器,跳过全局 Hosts 和 DNS 脚本。服务器自身的 `queryStrategy`、缓存、`timeoutMs`、`clientIP`、`tag`,以及 `expectedIPs` / `unexpectedIPs` 和对应的 `actPrior` / `actUnprior` 仍然生效。
**示例**
在 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
提供域名和 IP 的规则匹配功能。
```lua
local geodata = require("xray.geodata")
```
## API 索引
| 分类 | 成员 | 说明 |
| ---- | ---------------------------------------------------------------- | ------------------------ |
| 函数 | [`geodata.BuildDomainMatcher(...)`](#geodata-builddomainmatcher) | 创建 `DomainMatcher` |
| 函数 | [`geodata.BuildIPMatcher(...)`](#geodata-buildipmatcher) | 创建 `IPMatcher` |
| 对象 | [`DomainMatcher`](#domainmatcher) | 域名匹配器 |
| 方法 | [`domainMatcherObj:MatchAny(domain)`](#matchany) | 判断域名是否命中任意规则 |
| 方法 | [`domainMatcherObj:Match(domain)`](#match) | 取得命中的域名规则编号 |
| 对象 | [`IPMatcher`](#ipmatcher) | IP 匹配器 |
| 方法 | [`ipMatcherObj:Match(ip)`](#match-1) | 匹配单个 IP |
| 方法 | [`ipMatcherObj:AnyMatch(ips)`](#anymatch) | 判断是否有 IP 命中 |
| 方法 | [`ipMatcherObj:Matches(ips)`](#matches) | 判断整个 IP 列表是否命中 |
| 方法 | [`ipMatcherObj:FilterIPs(ips)`](#filterips) | 分离命中和未命中的 IP |
| 方法 | [`ipMatcherObj:SetReverse(reverse)`](#setreverse) | 设置反选标记 |
| 方法 | [`ipMatcherObj:ToggleReverse()`](#togglereverse) | 切换反选标记 |
## 函数
### geodata.BuildDomainMatcher
```lua
local domainMatcherObj = geodata.BuildDomainMatcher(rule1, rule2, ...)
```
根据域名规则创建 [`DomainMatcher`](#domainmatcher)。
**参数**
| 参数 | 类型 | 说明 / 要求 |
| ------------------- | -------- | -------------------------------- |
| `rule1, rule2, ...` | `string` | 必填,至少一条域名规则,逐个传入 |
**返回值**
| 返回值 | 类型 | 说明 |
| ------------------ | --------------------------------- | -------------- |
| `domainMatcherObj` | [`DomainMatcher`](#domainmatcher) | 域名匹配器实例 |
**行为约定**
未传入规则、参数类型错误,或规则解析、资源加载、构建失败时,抛出 Lua 异常。
支持 `domain:`、`full:`、`keyword:`、`regexp:`、`dotless:`、`geosite:`、`ext:`(亦可写为 `ext-domain:` / `ext-site:`),格式见[路由域名规则](../../../config/routing.md#ruleobject)。GeoSite 文件从[资源目录](../../../config/env.md#资源文件路径)加载。
无前缀字符串默认使用 `domain:` 规则。
除正则表达式外,规则在构建时转为小写。
**示例**
```lua
local sitesObj = geodata.BuildDomainMatcher(
"example.com", "full:other.example"
)
```
### geodata.BuildIPMatcher
```lua
local ipMatcherObj = geodata.BuildIPMatcher(rule1, rule2, ...)
```
根据 IP 规则创建 [`IPMatcher`](#ipmatcher)。
**参数**
| 参数 | 类型 | 说明 / 要求 |
| ------------------- | -------- | -------------------------------- |
| `rule1, rule2, ...` | `string` | 必填,至少一条 IP 规则,逐个传入 |
**返回值**
| 返回值 | 类型 | 说明 |
| -------------- | ------------------------- | ------------- |
| `ipMatcherObj` | [`IPMatcher`](#ipmatcher) | IP 匹配器实例 |
**行为约定**
未传入规则、参数类型错误、规则为空字符串,或规则解析、资源加载、构建失败时,抛出 Lua 异常。
支持 IP、CIDR、`geoip:`、`ext:`(亦可写为 `ext-ip:`)及 `!` 反选规则,组合方式见[路由 IP 规则](../../../config/routing.md#ruleobject)。GeoIP 文件从[资源目录](../../../config/env.md#资源文件路径)加载。
**示例**
```lua
local privateIPsObj = geodata.BuildIPMatcher(
"10.0.0.0/8", "192.168.0.0/16", "fc00::/7"
)
```
## 对象
### DomainMatcher
| 属性 | 说明 |
| ----------- | ------------------------------------------------------------------------- |
| Go 底层类型 | `geodata.DomainMatcher` 接口的实现 |
| Lua 表现 | `userdata` |
| 取得方式 | [`geodata.BuildDomainMatcher(...)`](#geodata-builddomainmatcher) 的返回值 |
**通用约定**
匹配方法接收一个域名字符串。`nil`、省略参数、类型或参数数量错误会抛出 Lua 异常。
输入域名不会自动转为小写,需由脚本按需处理。
#### MatchAny
```lua
local matched = domainMatcherObj:MatchAny(domain)
```
判断域名是否命中至少一条规则。
**参数**
| 参数 | 类型 | 说明 / 要求 |
| -------- | -------- | ---------------- |
| `domain` | `string` | 必填,待匹配域名 |
**返回值**
| 返回值 | 类型 | 说明 |
| --------- | --------- | ------------------------------- |
| `matched` | `boolean` | 命中时为 `true`,否则为 `false` |
**示例**
```lua
local matched = sitesObj:MatchAny("www.example.com")
```
#### Match
```lua
local indices = domainMatcherObj:Match(domain)
```
取得域名命中的规则编号。
**参数**
| 参数 | 类型 | 说明 / 要求 |
| -------- | -------- | ---------------- |
| `domain` | `string` | 必填,待匹配域名 |
**返回值**
| 返回值 | 类型 | 说明 |
| --------- | --------------------------------------------- | ---------------------------------------------------- |
| `indices` | [`[]uint32`](./data-types.md#uint32) 或 `nil` | 命中的规则编号;未命中时为 `nil` 或长度为 0 的 slice |
**行为约定**
规则编号从 **0** 开始,对应构建时传入的规则顺序。结果顺序不保证,可能包含重复编号;GeoSite 展开的条目沿用其所属规则的编号。
若将同一组规则保存在 Lua 数组中,可用 `rules[ruleNumber + 1]` 取得原规则。slice 的访问方式见[数据类型](./data-types.md#slice)。
**示例**
```lua
local indices = sitesObj:Match("www.example.com")
if indices ~= nil then
for i = 1, #indices do
local ruleNumber = indices[i]
end
end
```
### IPMatcher
| 属性 | 说明 |
| ----------- | ----------------------------------------------------------------- |
| Go 底层类型 | `geodata.IPMatcher` 接口的实现 |
| Lua 表现 | `userdata` |
| 取得方式 | [`geodata.BuildIPMatcher(...)`](#geodata-buildipmatcher) 的返回值 |
**通用约定**
匹配单个 IP 时使用 [`net.IP`](./data-types.md#net-ip);匹配或筛选列表时,输入形式见 [IP 列表输入](./data-types.md#ip-列表输入)。IP 地址字符串不会自动解析为 `net.IP`。
匹配和筛选方法均接受显式的 `nil`。省略参数、类型或参数数量错误会抛出 Lua 异常。
#### Match
```lua
local matched = ipMatcherObj:Match(ip)
```
判断单个 IP 是否命中规则。
**参数**
| 参数 | 类型 | 说明 / 要求 |
| ---- | ------------------------------------------- | ---------------- |
| `ip` | [`net.IP`](./data-types.md#net-ip) 或 `nil` | 必填,须显式传入 |
**返回值**
| 返回值 | 类型 | 说明 |
| --------- | --------- | --------------------------------------------------- |
| `matched` | `boolean` | 有效 IP 命中时为 `true`;`nil` 或无效 IP 为 `false` |
**行为约定**
传入 `""` 或空 table `{}` 时,会被视为空 IP 并返回 `false`;字符串不会按 IP 地址文本解析。
#### AnyMatch
```lua
local matched = ipMatcherObj:AnyMatch(ips)
```
判断是否至少有一个有效 IP 命中规则。
**参数**
| 参数 | 类型 | 说明 / 要求 |
| ----- | ------------------------------------------ | ---------------------------- |
| `ips` | [IP 列表输入](./data-types.md#ip-列表输入) | 必填,须显式传入,允许 `nil` |
**返回值**
| 返回值 | 类型 | 说明 |
| --------- | --------- | ------------------------------------------------------------------ |
| `matched` | `boolean` | 至少一个有效 IP 命中时为 `true`;`nil`、空列表或未命中时为 `false` |
#### Matches
```lua
local matched = ipMatcherObj:Matches(ips)
```
判断整个 IP 列表是否命中规则。
**参数**
| 参数 | 类型 | 说明 / 要求 |
| ----- | ------------------------------------------ | ---------------------------- |
| `ips` | [IP 列表输入](./data-types.md#ip-列表输入) | 必填,须显式传入,允许 `nil` |
**返回值**
| 返回值 | 类型 | 说明 |
| --------- | --------- | ---------------------------------------------------------------------- |
| `matched` | `boolean` | 整个列表满足匹配要求时为 `true`;`nil`、空列表或含无效 IP 时为 `false` |
**行为约定**
组合规则可能包含多个内部匹配器,必须有一个内部匹配器命中整个列表。
例如,用 `geodata.BuildIPMatcher("192.168.1.0/24", "geoip:us")` 构建匹配器,输入列表同时包含 `192.168.1.1` 和 `8.8.8.8` 时,`Matches` 返回 `false`:前者命中 custom CIDR,后者命中 `geoip:us`,但两类规则属于不同的内部匹配器,没有一个能命中整个列表。
只需判断是否有地址命中时,使用 [`AnyMatch`](#anymatch)。
#### FilterIPs
```lua
local matched, unmatched = ipMatcherObj:FilterIPs(ips)
```
将有效 IP 分为命中和未命中两组。
**参数**
| 参数 | 类型 | 说明 / 要求 |
| ----- | ------------------------------------------ | ---------------------------- |
| `ips` | [IP 列表输入](./data-types.md#ip-列表输入) | 必填,须显式传入,允许 `nil` |
**返回值**
| 返回值 | 类型 | 说明 |
| ----------- | -------------------------------------- | ------------------------- |
| `matched` | [`[]net.IP`](./data-types.md#net-ip-1) | 命中的 IP,始终非 `nil` |
| `unmatched` | [`[]net.IP`](./data-types.md#net-ip-1) | 未命中的 IP,始终非 `nil` |
**行为约定**
没有结果的一组为空 slice;传入 `nil`、空列表或全部无效 IP 时,两组均为空 slice。
无效 IP 会被丢弃,结果顺序不保证与输入相同。
#### SetReverse
```lua
ipMatcherObj:SetReverse(reverse)
```
设置各内部匹配器的反选标记。
**参数**
| 参数 | 类型 | 说明 / 要求 |
| --------- | --------- | --------------------------------------- |
| `reverse` | `boolean` | 必填,`true` 开启反选,`false` 关闭反选 |
**返回值**
无返回值。
**行为约定**
`nil`、省略参数或类型错误会抛出 Lua 异常。此操作覆盖各内部匹配器原有的反选标记,包括规则中 `!` 指定的标记。反选只作用于原规则包含的地址族,且不会匹配无效 IP。
设置后的反选状态保存在当前匹配器对象中。如果后续 Hook 调用复用该对象,修改后的状态也会继续生效,见[池化实例内状态](../guide/lifecycle.md#池化实例内状态)。
**示例**
对 `10.0.0.0/8` 反选后,匹配该范围以外的有效 IPv4 地址:
```lua
local ipMatcherObj = geodata.BuildIPMatcher("10.0.0.0/8")
ipMatcherObj:SetReverse(true)
```
#### ToggleReverse
```lua
ipMatcherObj:ToggleReverse()
```
切换各内部匹配器的反选标记。
**参数**
无额外参数。
**返回值**
无返回值。
**行为约定**
组合规则中的各内部匹配器分别切换反选标记,结果不等同于对整个组合结果取逻辑反。
切换后的反选状态保存在当前匹配器对象中。如果后续 Hook 调用复用该对象,修改后的状态也会继续生效,见[池化实例内状态](../guide/lifecycle.md#池化实例内状态)。
@@ -0,0 +1,69 @@
# xray.log
将日志写入 Xray 的日志系统。
```lua
local log = require("xray.log")
```
## API 索引
| 分类 | 成员 | 说明 |
| ---- | ---------------------------------- | ----------------- |
| 函数 | [`log.Debug(...)`](#log-debug) | 记录 debug 日志 |
| 函数 | [`log.Info(...)`](#log-info) | 记录 info 日志 |
| 函数 | [`log.Warning(...)`](#log-warning) | 记录 warning 日志 |
| 函数 | [`log.Error(...)`](#log-error) | 记录 error 日志 |
## 函数
### log.Debug
```lua
log.Debug(...)
```
记录 `debug` 级别的日志。
**参数**
| 参数 | 类型 | 说明 / 要求 |
| ----- | ----------- | ------------------------------------ |
| `...` | 任意 Lua 值 | 可选,任意数量的日志内容,按顺序拼接 |
**返回值**
无返回值。
**参数转换**
参数接受不传参数、`nil`、`""`、`{}` 和空 slice,按顺序转为文本并拼接,不自动插入空格或分隔符。
- [`error`](./data-types.md#error) 优先转为错误消息。
- 其他具有函数类型 `__tostring` 的值使用该转换;转换抛出的异常向调用者传播。
- 其余值使用默认字符串表示。`nil` 转为 `"nil"`,空字符串不增加正文;table 和 slice 不会展开为元素列表。
**输出条件**
日志以调用脚本的文件名为前缀,是否输出取决于 [`log.loglevel`](../../../config/log.md#logobject)。例如设置为 `warning` 时,`Debug` 和 `Info` 不输出,也不会调用参数的 `__tostring` 转换。
四个函数均没有 `err` 返回值。将调用赋给变量时,该变量为 `nil`,不能据此判断日志是否成功输出。
**示例**
```lua
log.Info("查询域名:", domain)
log.Warning("上游查询失败:", err)
```
### log.Info
同上。
### log.Warning
同上。
### log.Error
同上。
@@ -0,0 +1,119 @@
# xray.router
提供[路由脚本](../guide/routing.md)使用的常量和辅助接口。请求参数与上下文见[路由 Hook](./hook-routing.md)。
```lua
local router = require("xray.router")
```
## API 索引
| 分类 | 成员 | 说明 |
| ---- | ---------------------------------------------------------- | ---------------------- |
| 常量 | [`router.NetworkUnknown`](#常量) | 未知网络 |
| 常量 | [`router.NetworkTCP`](#常量) | TCP |
| 常量 | [`router.NetworkUDP`](#常量) | UDP |
| 常量 | [`router.NetworkUNIX`](#常量) | UNIX |
| 常量 | [`router.LocalOS`](#常量) | 运行平台 |
| 函数 | [`router:PickOutbound(balancerTag)`](#router-pickoutbound) | 通过负载均衡器选择出站 |
| 函数 | [`router.FindProcess(ctx)`](#router-findprocess) | 查找本地连接对应的进程 |
## 常量
| 名称 | 类型 | 值 / 说明 |
| ----------------------- | -------- | ----------------------------------------------------- |
| `router.NetworkUnknown` | `number` | `0`,未知网络 |
| `router.NetworkTCP` | `number` | `2`,TCP |
| `router.NetworkUDP` | `number` | `3`,UDP |
| `router.NetworkUNIX` | `number` | `4`,UNIX |
| `router.LocalOS` | `string` | 当前运行平台,例如 `"windows"`、`"linux"`、`"darwin"` |
可将[路由 Hook](./hook-routing.md#handleroute) 的 `network` 参数与网络常量比较:
```lua
if network == router.NetworkUDP then
return "direct", "lua-udp"
end
```
## 函数
### router:PickOutbound
```lua
local outboundTag, err = router:PickOutbound(balancerTag)
```
通过指定的负载均衡器选择出站。
**参数**
| 参数 | 类型 | 说明 / 要求 |
| ------------- | -------- | --------------------------------------------------------------------------------------------------------- |
| `balancerTag` | `string` | 要使用的负载均衡器的 `tag` 值,在 [`routing.balancers`](../../../config/routing.md#balancerobject) 中配置 |
**返回值**
| 返回值 | 类型 | 说明 |
| ------------- | ----------------------------------------- | ---------------------------- |
| `outboundTag` | `string` 或 `nil` | 成功时为选中出站的非空 `tag` |
| `err` | [`error`](./data-types.md#error) 或 `nil` | `nil` 表示成功 |
**行为约定**
必须显式传入字符串,否则抛出 Lua 异常。空字符串 `""` 会按空 `tag` 查找。
使用结果前应先检查 `err`。负载均衡器不存在时返回 `nil, err`;存在但未选出出站时返回 `"", err`。配置的 `fallbackTag` 生效时,返回该出站标识和 `nil`。
**示例**
下面使用 `tag` 为 `"proxy-pool"` 的负载均衡器,并按[路由 Hook 的返回值约定](./hook-routing.md#返回值)传递结果:
```lua
local outboundTag, err = router:PickOutbound("proxy-pool")
return outboundTag, "lua-balance", err
```
完整示例见[路由脚本指南](../guide/routing.md#与路由配置的关系)。
### router.FindProcess
```lua
local pid, name, path, err = router.FindProcess(ctx)
```
查找本机上与当前连接对应的进程。
**参数**
| 参数 | 类型 | 说明 / 要求 |
| ----- | ------------------------------------------------------ | ---------------------------------- |
| `ctx` | [`routing.Context`](./hook-routing.md#routing-context) | 当前请求的上下文,由路由 Hook 传入 |
**返回值**
| 返回值 | 类型 | 说明 |
| ------ | ----------------------------------------- | ------------------------------- |
| `pid` | `number` | 进程 ID;未取得时通常为 `0` |
| `name` | `string` | 进程名称;未取得时为 `""` |
| `path` | `string` | 可执行文件路径;未取得时为 `""` |
| `err` | [`error`](./data-types.md#error) 或 `nil` | `nil` 表示成功 |
**行为约定**
须显式传入有效的路由上下文,否则抛出 Lua 异常。没有来源 IP、网络类型不是 TCP/UDP、查找失败或平台不支持时,返回错误。
`pid`、`name`、`path` 始终非 `nil`,但失败时可能保留部分信息,使用前应先检查 `err`。没有来源 IP 或网络类型不受支持时,返回 `0, "", "", err`;其他情况原样返回平台查找器的结果。
查询使用首个来源 IP 和来源端口;有目标 IP 时,同时传入首个目标 IP 和目标端口。具体匹配方式由平台查找器决定。
查找能力取决于运行平台和权限。Windows、Linux 和 macOS 提供内置实现;Android 需由运行环境注册查找器,成功时也可能没有名称或路径。iOS 和其他不支持的平台返回错误。
**示例**
```lua
local pid, name, path, err = router.FindProcess(ctx)
if err == nil and name == "curl" then
return "direct", "lua-process"
end
```
+15
View File
@@ -7,6 +7,7 @@ The built-in DNS module in Xray has three main purposes:
- **Routing Phase:** Resolves domain names to IPs and matches rules based on the resolved IPs for traffic splitting.
::: details Detailed explanation
Whether a domain name is resolved for IP-based routing depends on `routing.domainStrategy`. The built-in DNS server may be used for DNS queries only with the following values:
- `"IPIfNonMatch"`: If no rule matches during the first routing pass, resolution occurs whenever the target includes a domain name and at least one rule contains an `ip` condition.
- `"IPOnDemand"`: Resolution occurs when the target includes a domain name and a rule containing an `ip` condition is encountered.
@@ -14,6 +15,7 @@ The built-in DNS module in Xray has three main purposes:
- **Outbound Phase:** Resolves target domain names for connections or for sending to a remote proxy server.
::: details Detailed explanation
- For example, setting `targetStrategy` to `UseIP` in a VLESS outbound resolves the target domain of the proxied request through the local built-in DNS module, then sends the resolved IP to the remote proxy server.
- Setting `sockopt.domainStrategy` to `UseIP` in a VLESS outbound resolves the VLESS server's domain through the built-in DNS module, then connects to the resolved IP.
- Setting `sockopt.domainStrategy` to `UseIP` in a Freedom outbound resolves the request's target domain through the built-in DNS module, then connects to the resolved IP.
@@ -23,6 +25,7 @@ The built-in DNS module in Xray has three main purposes:
- **TUN/Transparent Proxy DNS Traffic Hijacking:** Combines routing with the DNS outbound to hijack DNS traffic into this module; or uses [Tunnel](./inbounds/tunnel.md) to expose port 53 and act as a recursive DNS server.
::: details Detailed explanation
- Only basic IP queries (A and AAAA records) are supported. CNAME records will be queried repeatedly until an A/AAAA record is returned. Other queries will not enter the built-in DNS server; instead, they may be discarded or transparently forwarded to other servers depending on your outbound configuration.
:::
@@ -31,6 +34,8 @@ The built-in DNS module in Xray has three main purposes:
The domain first undergoes a Hosts mapping check (see the `hosts` field). If the required IP is not found, the DNS server is used for the query.
If `script` is configured, subsequent DNS queries are handled by a [Lua script](../development/lua/guide/dns.md), and the built-in processing flow below is skipped.
The core then begins to build a list of servers, sorting them according to the requested domain based on the following rules.
- Build List 1: Contains servers where the `domains` field successfully matches the requested domain, in the order they appear in the configuration file.
@@ -54,6 +59,7 @@ When executing a DNS query, the core will query the servers in the Final Server
"baidu.com": "127.0.0.1",
"dns.google": ["8.8.8.8", "8.8.4.4"]
},
"script": "",
"servers": [
"8.8.8.8",
"8.8.4.4",
@@ -107,6 +113,10 @@ The mapping target may be a domain name. When the core finishes matching and the
The matching format (`domain:`, `full:`, etc.) is the same as the domain in the commonly used [Routing System](./routing.md#ruleobject). The difference is that without a prefix, it defaults to using the `full:` prefix (similar to the common hosts file syntax).
> `script`: string
Path to a Lua script file; defaults to an empty string. When set, the Lua script takes over the built-in DNS processing flow. See the [DNS Scripts guide](../development/lua/guide/dns.md) for usage.
> `servers`: \[string | [DnsServerObject](#dnsserverobject) \]
A list of DNS servers. Two types are supported: DNS address (string format) and [DnsServerObject](#dnsserverobject).
@@ -267,6 +277,7 @@ For query traffic generated by the built-in DNS, except for `localhost`, `fakedn
```json
{
"id": "primary",
"address": "1.2.3.4",
"port": 5353,
"domains": ["domain:xray.com"],
@@ -281,6 +292,10 @@ For query traffic generated by the built-in DNS, except for `localhost`, `fakedn
}
```
> `id`: string
Server identifier; an empty string if omitted. Lua scripts use [`serverObj.ID`](../development/lua/reference/module-dns.md#id) to identify the upstream.
> `address`: address
A list of DNS servers. Two types are supported: DNS address (string format) and DnsServerObject.
+5
View File
@@ -15,6 +15,7 @@ For a more detailed analysis of the routing function: [Analysis of Routing (Part
"routing": {
"domainStrategy": "AsIs",
"rules": [],
"script": "",
"balancers": []
}
}
@@ -44,6 +45,10 @@ For each connection, routing will judge these rules from top to bottom. When the
When no rule is matched, traffic is sent via the first outbound by default.
:::
> `script`: string
Path to a Lua script file; defaults to an empty string. When set, the Lua script takes over outbound selection. See the [Routing Scripts guide](../development/lua/guide/routing.md) for usage.
> `balancers`: \[ [BalancerObject](#balancerobject) \]
An array, where each item is a load balancer configuration.
+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
```
+15
View File
@@ -7,6 +7,7 @@
- На этапе маршрутизации: разрешение доменов в IP и сопоставление правил на основе полученных IP для разделения трафика.
::: details Подробное объяснение
Разрешение доменного имени для маршрутизации по IP зависит от значения `routing.domainStrategy`. Встроенный DNS-сервер может использоваться для запросов только при следующих значениях:
- `"IPIfNonMatch"`: если в первом проходе маршрутизации не сработало ни одно правило, разрешение выполняется при условии, что цель содержит доменное имя и хотя бы одно правило содержит условие `ip`.
- `"IPOnDemand"`: разрешение выполняется, если цель содержит доменное имя и встречается правило с условием `ip`.
@@ -14,6 +15,7 @@
- На этапе исходящего подключения: разрешение целевых доменных имен для подключения или передачи удаленному прокси-серверу.
::: details Подробное объяснение
- Например, если в исходящем подключении VLESS задать `targetStrategy` равным `UseIP`, целевой домен проксируемого запроса сначала разрешается локальным встроенным модулем DNS, затем полученный IP передается удаленному прокси-серверу.
- Если в исходящем подключении VLESS задать `sockopt.domainStrategy` равным `UseIP`, домен сервера VLESS разрешается встроенным модулем DNS, затем устанавливается соединение с полученным IP.
- Если в исходящем подключении Freedom задать `sockopt.domainStrategy` равным `UseIP`, целевой домен запроса разрешается встроенным модулем DNS, затем устанавливается соединение с полученным IP.
@@ -23,6 +25,7 @@
- Перехват DNS-трафика в режиме TUN/прозрачного прокси с помощью маршрутизации и исходящего подключения DNS для направления запросов в этот модуль; либо использование [Tunnel](./inbounds/tunnel.md) для открытия порта 53 и работы в качестве рекурсивного DNS-сервера.
::: details Подробное объяснение
- Поддерживаются только базовые IP-запросы (записи A и AAAA). Записи CNAME будут запрашиваться повторно до тех пор, пока не будет возвращена запись A/AAAA. Другие типы запросов не попадают во встроенный DNS-сервер, а либо отбрасываются, либо передаются другим серверам в зависимости от вашей конфигурации исходящего подключения.
:::
@@ -31,6 +34,8 @@
Домен сначала проходит проверку сопоставления Hosts (см. поле `hosts`). Если нужный IP не найден, для запроса используется DNS-сервер.
Если настроен `script`, дальнейшие DNS-запросы передаются [скрипту Lua](../development/lua/guide/dns.md), а описанный ниже встроенный процесс обработки не выполняется.
Затем ядро начинает строить список серверов, сортируя их в зависимости от запрашиваемого домена по следующим правилам.
- Построение списка 1: содержит серверы, у которых поле `domains` успешно совпало с запрашиваемым доменом, в порядке их появления в конфигурационном файле.
@@ -54,6 +59,7 @@
"baidu.com": "127.0.0.1",
"dns.google": ["8.8.8.8", "8.8.4.4"]
},
"script": "",
"servers": [
"8.8.8.8",
"8.8.4.4",
@@ -107,6 +113,10 @@
Формат сопоставления (`domain:`, `full:` и т.д.) аналогичен `domain` в системе [маршрутизации](./routing.md#ruleobject). Отличие в том, что без префикса здесь по умолчанию используется `full:` (аналогично стандартному файлу hosts).
> `script`: string
Путь к файлу скрипта Lua; по умолчанию пустая строка. Если задан, обработка встроенного DNS передаётся скрипту Lua. Использование описано в [руководстве по скриптам DNS](../development/lua/guide/dns.md).
> `servers`: \[string | [DnsServerObject](#dnsserverobject) \]
Список DNS-серверов. Поддерживаются два типа: адрес DNS (строка) и [DnsServerObject](#dnsserverobject).
@@ -267,6 +277,7 @@ IP-адрес, используемый в расширении EDNS Client Subn
```json
{
"id": "primary",
"address": "1.2.3.4",
"port": 5353,
"domains": ["domain:xray.com"],
@@ -281,6 +292,10 @@ IP-адрес, используемый в расширении EDNS Client Subn
}
```
> `id`: string
Идентификатор сервера; если не задан, используется пустая строка. Скрипты Lua обращаются к [`serverObj.ID`](../development/lua/reference/module-dns.md#id), чтобы определить вышестоящий сервер.
> `address`: address
Список DNS-серверов. Поддерживаются два типа: адрес DNS (строка) и DnsServerObject.
+5
View File
@@ -15,6 +15,7 @@
"routing": {
"domainStrategy": "AsIs",
"rules": [],
"script": "",
"balancers": []
}
}
@@ -44,6 +45,10 @@
Если ни одно правило не совпадает, трафик по умолчанию отправляется через первый исходящий канал.
:::
> `script`: string
Путь к файлу скрипта Lua; по умолчанию пустая строка. Если задан, выбор исходящего подключения передаётся скрипту Lua. Использование описано в [руководстве по скриптам маршрутизации](../development/lua/guide/routing.md).
> `balancers`: \[ [BalancerObject](#balancerobject) \]
Массив, каждый элемент которого является конфигурацией балансировщика нагрузки.
+35
View File
@@ -0,0 +1,35 @@
# Среда выполнения и загрузка скриптов
## Среда Lua
Сейчас используется GopherLua, который поддерживает синтаксис Lua 5.1, оператор `goto` из Lua 5.2 и собственный [`channel`](https://github.com/yuin/gopher-lua#lua-api).
Xray загружает скрипты Lua согласно файлу конфигурации. Положение настройки определяет назначение скрипта и доступные Hook. Hook каждой точки входа и соглашения об их вызове описаны в [справочнике Hook](./index.md#hook).
## Загрузка модулей
Модули API организованы по назначению и загружаются через `require`:
```lua
local geodata = require("xray.geodata")
local log = require("xray.log")
```
Список модулей приведён в разделе [API модулей](./index.md#api-модулеи), а использование функций — в соответствующих справочниках API.
Когда выполняется инициализация верхнего уровня, включая загрузку модулей Lua и создание объектов, описано в разделе [Экземпляры и жизненный цикл](./guide/lifecycle.md).
## Пути к файлам скриптов
Поле `script` в конфигурации задаёт путь к файлу скрипта Lua. Пустая строка или отсутствие поля отключает скрипт; абсолютный путь указывает файл напрямую.
Относительные пути проверяются в следующем порядке. Если путь не существует, поиск продолжается; если он существует, но не является обычным файлом, сразу возникает ошибка:
1. Каталог, указанный переменной окружения `XRAY_LOCATION_CONFDIR`;
2. Каталог, указанный переменной окружения `XRAY_LOCATION_CONFIG`;
3. Текущий рабочий каталог процесса Xray;
4. Каталог исполняемого файла Xray.
Настройка этих каталогов описана в разделе [Переменные окружения](../../config/env.md). Относительные пути не отсчитываются автоматически от каталога файла конфигурации.
Например, если путь скрипта задан как `routing.lua`, Xray ищет файл в указанном выше порядке. Абсолютный путь Windows можно записать как `C:/Xray/routing.lua`; при использовании обратной косой черты экранируйте её согласно требованиям формата конфигурации.
+155
View File
@@ -0,0 +1,155 @@
# Скрипты DNS
Когда скрипт указан через [`dns.script`](../../../config/dns.md#dnsobject), обработка встроенного DNS передаётся функции Lua `HandleDNSQuery`.
## Минимальный пример
Добавьте в существующую конфигурацию:
```json
{
"dns": {
"script": "dns.lua",
"servers": ["1.1.1.1"]
}
}
```
Сохраните `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
```
Этот фрагмент нужно объединить с полной конфигурацией. Правила поиска файла описаны в разделе [Пути к файлам скриптов](../environment.md#пути-к-фаилам-скриптов).
Пример передаёт параметры вышестоящему серверу без изменений и напрямую возвращает результат запроса. Полный список параметров, ограничения возвращаемых значений и поведение при ошибках описаны в [HandleDNSQuery](../reference/hook-dns.md).
## Связь с конфигурацией DNS
До передачи запроса скрипту по-прежнему выполняются проверка домена встроенным DNS, глобальные ограничения типов запросов и обработка Hosts. Если Hosts уже вернул допустимые IP-адреса или явно отклонил запрос, скрипт не вызывается. Если Hosts заменил домен, скрипт запрашивает заменённый домен.
При включении скрипта он управляет выбором вышестоящего сервера, поэтому `domains`, `skipFallback`, `finalQuery`, `disableFallback`, `disableFallbackIfMatch` и `enableParallelQuery` больше не имеют практического значения.
Для запроса к конкретному серверу используйте [`serverObj:Query`](../reference/module-dns.md#query). Настройки отдельного сервера, которые продолжают действовать, перечислены в описании этого API.
::: tip Совет
Если соответствие между серверами и правилами фильтрации постоянно, удобнее и быстрее настроить `expectedIPs` / `unexpectedIPs` напрямую. Можно также не задавать эти параметры и фильтровать результаты в Lua методами вроде [`ipMatcherObj:FilterIPs`](../reference/module-geodata.md#filterips), что даёт больше гибкости.
:::
## Пример: выбор сервера по домену и фильтрация результатов
Следующий пример выбирает вышестоящие серверы по категории домена и при необходимости фильтрует китайские IP-адреса. Для каждого сервера задаётся `id`, по которому скрипт выполняет запросы и выбирает резервный сервер:
```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` и `dns-proxy` — входящие теги запросов к вышестоящим DNS-серверам. Правила маршрутизации выше выбирают для них прямое исходящее подключение `direct` и прокси VLESS `proxy` соответственно.
Скрипт сначала определяет порядок запросов по домену, затем последовательно опрашивает серверы. Если запрос завершился ошибкой или результат после фильтрации пуст, он пробует следующий сервер:
```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
-- Использовать публичный DNS для доменов Google.
queries = {{"cf"}, {"google"}}
elseif cnDomainMatcher:MatchAny(domain) then
-- Для китайских доменов сначала запросить прямой DNS, оставить китайские IP, затем перейти к DNS через прокси.
queries = {
{"cn114", "cn"}, {"cn223", "cn"}, {"cf"}, {"google"}
}
elseif foreignDomainMatcher:MatchAny(domain) then
-- Для некитайских доменов сначала исключить китайские IP, затем попробовать запросы с ECS.
queries = {
{"cf", "non-cn"}, {"google", "non-cn"},
{"google-ecs"}, {"google-alt-ecs"}
}
else
-- Для доменов вне списков сначала искать китайские IP через ECS, затем перейти к обычному публичному DNS.
queries = {
{"google-ecs", "cn"}, {"google-alt-ecs", "cn"},
{"cf"}, {"google"}
}
end
local lastError = "Нет DNS-результатов, удовлетворяющих условиям"
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
```
Инициализация верхнего уровня и сохранение состояния скриптов DNS описаны в разделе [Жизненный цикл пула](./lifecycle.md#жизненныи-цикл-пула).
@@ -0,0 +1,67 @@
# Экземпляры и жизненный цикл
Способ управления экземплярами в точке входа скрипта определяет создание экземпляров Lua, вызовы Hook, сохранение состояния и уничтожение экземпляров. На этой странице правила описаны по типам жизненного цикла.
## Жизненный цикл пула
Сейчас [HandleRoute](../reference/hook-routing.md) в [скриптах маршрутизации](./routing.md) и [HandleDNSQuery](../reference/hook-dns.md) в [скриптах DNS](./dns.md) используют экземпляры из пула. Следующие правила применяются к обоим Hook.
### Пулы экземпляров и инициализация
Скрипты маршрутизации и DNS подключаются через поле `script` в соответствующих настройках и управляют отдельными пулами экземпляров Lua. Даже если указан один и тот же файл, состояние внутри экземпляров Lua не разделяется.
При запуске Xray один раз читает и компилирует скрипт, затем создаёт первый экземпляр, выполняет код верхнего уровня и проверяет наличие обязательного обработчика. Ошибка чтения файла, синтаксиса, выполнения кода верхнего уровня или проверки обработчика препятствует запуску Xray.
### Вызовы Hook и возврат экземпляров
Каждый выбор маршрута или DNS-запрос получает экземпляр в исключительное пользование. Свободный экземпляр используется повторно; если параллельным вызовам нужны дополнительные экземпляры, они создаются из скомпилированного скрипта с повторным выполнением кода верхнего уровня.
После нормального завершения обработчика экземпляр может вернуться в пул для повторного использования. Необработанное исключение Lua или превышение времени выполнения уничтожает экземпляр. Сообщение об ошибке обработки через возвращаемые значения или ошибка их проверки не равнозначны исключению выполнения Lua: экземпляр можно использовать повторно. Лишние свободные экземпляры уничтожаются автоматически.
Если есть свободный экземпляр, путь вызова каждого Hook прост: **получить экземпляр → выполнить Hook → вернуть экземпляр**. Чтение и компиляция скрипта выполняются при запуске Xray. Зелёные узлы на схеме показывают этот обычный путь.
```mermaid
flowchart TD
LOAD["Запуск Xray<br/>Однократное чтение и компиляция основного скрипта"] --> INIT["Создать первый экземпляр Lua<br/>Выполнить код верхнего уровня и проверить Hook"]
INIT --> POOL[("Пул свободных экземпляров")]
subgraph CALL["Вызов Hook из пула: простой путь повторного использования"]
TAKE["Получить свободный экземпляр в исключительное пользование"] --> RUN["Выполнить HandleRoute / HandleDNSQuery"]
RUN -->|Нормальное завершение| PUT["Вернуть экземпляр, сохранив состояние"]
end
REQUEST["Выбор маршрута / DNS-запрос"] --> AVAILABLE{"Есть свободный экземпляр?"}
POOL -.-> AVAILABLE
AVAILABLE -->|Да| TAKE
PUT --> POOL
AVAILABLE -. Нет: расширить по необходимости .-> CREATE["Создать экземпляр из скомпилированного скрипта<br/>Выполнить код верхнего уровня и проверить Hook"]
CREATE --> RUN
RUN -. Необработанное исключение Lua / тайм-аут .-> DESTROY["Уничтожить экземпляр"]
POOL -. Лишние свободные экземпляры / завершение Xray .-> DESTROY
classDef reuse fill:#e8f5e9,stroke:#2e7d32,color:#1b5e20
class TAKE,RUN,PUT reuse
```
### Состояние в экземплярах пула
Глобальные переменные, переменные `local` верхнего уровня, замыкания и кеш модулей `require` принадлежат текущему экземпляру. Они сохраняются между вызовами, которые он обрабатывает, и теряются при его уничтожении:
```lua
local calls = 0
function HandleRoute()
calls = calls + 1
return "direct", "instance-call-" .. calls
end
```
Счётчик выше отражает только число вызовов, обработанных текущим экземпляром. У разных экземпляров независимые значения `calls`, и запросы не обязательно попадают в один экземпляр. Этот счётчик нельзя использовать как общий для всех соединений или как постоянное состояние отдельного соединения.
Рекомендуется размещать код инициализации, например загрузку модулей Lua, создание объектов сопоставления и сохранение объектов DNS-серверов, вне функции Hook, на верхнем уровне скрипта. Он выполняется один раз при создании каждого экземпляра, а результаты могут повторно использоваться его последующими вызовами Hook.
### Тайм-ауты и обновление скриптов
Сейчас тайм-аут инициализации каждого экземпляра маршрутизации и DNS составляет **120 секунд**, а выполнения каждого вызова обработчика — **6 секунд**. Тайм-аут вызова отсчитывается после получения экземпляра; отдельных полей конфигурации для этих значений пока нет. При вызовах API Xray фактическая отмена также зависит от того, реагирует ли API на сигнал отмены; отдельный DNS-сервер дополнительно ограничен настройкой `timeoutMs`.
Xray не отслеживает и не перекомпилирует основной скрипт автоматически. После его изменения перезапустите Xray, чтобы изменения вступили в силу. Экземпляры, созданные позже, также используют основной скрипт, скомпилированный при текущем запуске.
+119
View File
@@ -0,0 +1,119 @@
# Скрипты маршрутизации
Когда скрипт указан через [`routing.script`](../../../config/routing.md#routingobject), выбор исходящего подключения передаётся функции Lua `HandleRoute`.
## Минимальный пример
Следующий фрагмент конфигурации задаёт файл скрипта и настраивает исходящие подключения для прямого соединения и блокировки:
```json
{
"routing": {
"script": "routing.lua"
},
"outbounds": [
{ "tag": "direct", "protocol": "freedom" },
{ "tag": "block", "protocol": "blackhole" }
]
}
```
Сохраните `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
```
Скрипт читает IP-адрес источника через `ctx`. При адресе `127.0.0.1` он возвращает `block`, в остальных случаях — `direct`. Эти значения соответствуют `tag` исходящих подключений в конфигурации выше.
Этот фрагмент нужно объединить с полной конфигурацией. Правила поиска файла описаны в разделе [Пути к файлам скриптов](../environment.md#пути-к-фаилам-скриптов).
`HandleRoute` также может получать дополнительные параметры и возвращать имя правила и ошибку. Полное соглашение о вызове и поведение при ошибках описаны в [HandleRoute](../reference/hook-routing.md).
## Связь с конфигурацией маршрутизации
При включении скрипта маршрутизации `rules` и `domainStrategy` не действуют. Если скрипт не выбрал исходящее подключение или возникла ошибка, встроенные правила маршрутизации также не проверяются.
Настройка `balancers` остаётся доступной. Скрипт выбирает исходящее подключение через [`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
```
Замените `balance` в примере значением `tag` балансировщика, настроенного в `routing.balancers`.
## Пример: явный DNS-запрос для маршрутизации по IP
Следующий скрипт сначала обрабатывает трафик запросов к вышестоящим DNS-серверам, затем проверяет имеющиеся IP-адреса назначения запроса. При необходимости он явно разрешает домен и выбирает исходящее подключение по диапазонам частных адресов.
```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("Разрешение ", targetDomain, " завершилось ошибкой: ", err)
elseif privateIPMatcher:AnyMatch(resolved) then
return "direct", "lua-resolved-private"
end
end
return "proxy", "lua-default"
end
```
Запросы к вышестоящим DNS-серверам в обычном режиме также проходят через маршрутизацию. В примере для запросов с `skipDNSResolve`, равным `true`, или `inboundTag`, равным `"dns-query"`, исходящее подключение выбирается сразу, чтобы повторный DNS-запрос не создал петлю.
Результат `dns.Query` используется только для принятия решения в скрипте. Он не меняет автоматически IP-адреса назначения в `ctx` или фактическую цель соединения. При ошибке разрешения пример выбирает `proxy`; при необходимости измените это поведение согласно своей политике.
Инициализация верхнего уровня и сохранение состояния скриптов маршрутизации описаны в разделе [Жизненный цикл пула](./lifecycle.md#жизненныи-цикл-пула).
+46
View File
@@ -0,0 +1,46 @@
# Скрипты Lua
Скрипты Lua позволяют гибко расширять Xray, реализовывать собственные функции и взаимодействовать с ядром.
## Начало работы
Сначала прочитайте [Среда выполнения и загрузка скриптов](./environment.md), чтобы узнать о поддержке Lua, загрузке модулей и правилах поиска файлов скриптов.
## Руководства по написанию
Выберите руководство для нужной функции, начните с минимального примера и постепенно создайте собственный скрипт:
| Руководство | Назначение | Содержание |
| ------------------------------------------- | ---------------------------------- | -------------------------------------------------------------------------------------------------------- |
| [Скрипты маршрутизации](./guide/routing.md) | Собственные правила маршрутизации | Выбор исходящего подключения, балансировка нагрузки и маршрутизация по IP после явного разрешения домена |
| [Скрипты DNS](./guide/dns.md) | Собственная обработка DNS-запросов | Выбор вышестоящего сервера, резервные запросы и фильтрация результатов |
Текущие скрипты маршрутизации и DNS используют экземпляры из пула. При написании скриптов учитывайте [жизненный цикл пула](./guide/lifecycle.md#жизненныи-цикл-пула), чтобы понимать, когда выполняется инициализация верхнего уровня и как сохраняется состояние.
## Справочник
### Hook
Hook — это функция Lua, которую ядро вызывает в определённый момент. Точка входа скрипта определяет доступные Hook и соглашения об их вызове; одной точке входа могут соответствовать несколько Hook. Какие именно Hook нужно реализовать, указано в описании соответствующей точки входа.
Сейчас поддерживаются следующие точки входа и Hook. Параметры, возвращаемые значения и поведение при ошибках описаны на справочных страницах каждого Hook:
| Точка входа скрипта | Hook |
| ------------------- | ------------------------------------------ |
| `routing.script` | [HandleRoute](./reference/hook-routing.md) |
| `dns.script` | [HandleDNSQuery](./reference/hook-dns.md) |
### Типы данных
Помимо встроенных типов Lua, Xray Lua использует `net.IP`, срезы Go и `error`. Их представление и способы работы с ними в Lua описаны в разделе [Типы данных](./reference/data-types.md).
### API модулей
Выберите API модуля по назначению:
| Модуль | Назначение |
| --------------------------------------------- | --------------------------------------- |
| [xray.router](./reference/module-router.md) | Маршрутизация по правилам |
| [xray.dns](./reference/module-dns.md) | Разрешение доменных имён |
| [xray.geodata](./reference/module-geodata.md) | Сопоставление и фильтрация доменов и IP |
| [xray.log](./reference/module-log.md) | Запись журналов |
@@ -0,0 +1,215 @@
# Типы данных
На этой странице собраны типы данных, общие для модулей, и описаны их представление и использование в Lua. Типы, специфичные для отдельного модуля, описаны на его странице.
## net.IP
| Свойство | Описание |
| ------------------- | ------------------------- |
| Базовый тип Go | `net.IP`, `common/net.IP` |
| Представление в Lua | `userdata` |
Обычно не нужно сравнивать значения `net.IP` по одному в Lua, особенно в часто выполняемом коде. Для сопоставления IP используйте прежде всего [`IPMatcher`](./module-geodata.md#ipmatcher), который напрямую работает со значениями `net.IP`, возвращаемыми API.
Если нужно работать с IP напрямую, учитывайте, что `net.IP` и строка IP-адреса — разные типы. Для преобразования и сравнения доступны следующие два метода:
### String
```lua
local text = ip:String()
```
Преобразует IP в текст адреса.
**Параметры**
Дополнительных параметров нет.
**Возвращаемые значения**
| Значение | Тип | Описание |
| -------- | -------- | ---------------------------------- |
| `text` | `string` | Строка IP-адреса; никогда не `nil` |
### Equal
```lua
local equal = ip:Equal(otherIP)
```
Проверяет равенство двух IP.
**Параметры**
| Параметр | Тип | Описание |
| --------- | ----------------------------- | ----------------------------------------- |
| `otherIP` | [`net.IP`](#net-ip) или `nil` | IP для сравнения; допускается явный `nil` |
**Возвращаемые значения**
| Значение | Тип | Описание |
| -------- | --------- | ----------------------------------------------------- |
| `equal` | `boolean` | `true` при равенстве, иначе `false`; никогда не `nil` |
**Поведение**
Сравнение допустимого IP с `nil` возвращает `false`.
**Пример**
```lua
if ip ~= nil then
local text = ip:String()
local equal = ip:Equal(otherIP)
end
```
## slice
Срез Go представлен в Lua как `userdata`, сохраняет тип элементов и индексируется начиная с **1**. Следующие два типа срезов используют одинаковые операции получения длины, доступа по индексу и перебора.
| Тип | Представление в Lua | Тип элемента |
| ----------------------- | ------------------- | ------------------- |
| [`[]net.IP`](#net-ip-1) | `userdata` | [`net.IP`](#net-ip) |
| [`[]uint32`](#uint32) | `userdata` | `number` |
### []net.IP
Совет: для сопоставления или фильтрации списков IP используйте прежде всего [`IPMatcher`](./module-geodata.md#ipmatcher), который напрямую работает с `[]net.IP`.
### []uint32
Элементы — 32-битные целые числа без знака в диапазоне `0 .. 4294967295`.
### #values
```lua
local count = #values
```
Возвращает число элементов среза.
**Возвращаемые значения**
| Значение | Тип | Описание |
| -------- | -------- | -------------------------------------------------- |
| `count` | `number` | Неотрицательное целое число; `0` для пустого среза |
**Поведение**
Списки, возвращаемые API, могут быть `nil` или срезами длины 0. Перед выполнением этих операций `values` не должен быть `nil`; одной проверки `if values then` недостаточно, чтобы определить наличие элементов.
### values[i]
```lua
local value = values[i]
```
Читает один элемент среза.
**Параметры**
| Параметр | Тип | Описание |
| -------- | -------- | -------------------------------------------------- |
| `i` | `number` | Обязателен; целое число в диапазоне `1 .. #values` |
**Возвращаемые значения**
| Значение | Тип | Описание |
| -------- | -------------------------------- | ------------------------------------------------ |
| `value` | [`net.IP`](#net-ip) или `number` | Элемент `[]net.IP` или `[]uint32` соответственно |
**Поведение**
Выход за границы вызывает исключение Lua, а не возвращает `nil`. Избегайте прямого изменения срезов, возвращаемых API.
**Пример**
```lua
if ips ~= nil and #ips > 0 then
local firstIP = ips[1]:String()
end
```
### values()
```lua
for i, value in values() do
-- Используйте индекс i и элемент value
end
```
Возвращает итератор среза для обобщённого цикла `for`; индексы начинаются с 1.
**Параметры**
Дополнительных параметров нет.
**Возвращаемые значения**
| Значение | Тип | Описание |
| -------- | ---------- | ----------------------------------------------------------------- |
| Итератор | `function` | Последовательно предоставляет индекс и элемент, как показано выше |
**Поведение**
Срезы не поддерживают `pairs` и `ipairs`; для перебора также можно использовать числовой цикл `for`.
**Пример**
```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
При передаче списка IP в `IPMatcher` доступны следующие формы:
| Форма ввода | Представление в Lua | Описание |
| -------------------------- | ------------------- | ------------------------------------------------------------------------------ |
| [`[]net.IP`](#net-ip-1) | `userdata` | Список IP; допускается пустой срез |
| Массив [`net.IP`](#net-ip) | `table` | IP последовательно с индекса 1; допускается пустой массив `{}`, но не пропуски |
| `nil` | `nil` | Нужно передать явно; результат указан в описании каждого метода |
Строки IP-адресов не преобразуются в `net.IP`. Пустая строка `""` не является допустимым списком IP; её передача в `AnyMatch`, `Matches` или `FilterIPs` вызывает исключение Lua.
## error
| Свойство | Описание |
| ------------------- | ---------------------------------------------- |
| Базовый тип Go | `error` |
| Представление в Lua | `userdata` при ошибке; `nil` при её отсутствии |
Если возвращаемое API значение `err` равно `nil`, ошибки нет; иначе это объект ошибки. Его можно без изменений вернуть как `err` из Hook, чтобы передать ошибку ядру Xray.
**Соглашение о возврате из Hook**
| Значение ошибки | Значение |
| ----------------- | ------------------------------------------------------------------------ |
| `nil` | Ошибки нет |
| `userdata` ошибки | Сохраняет исходную ошибку |
| `string` | Сообщение об ошибке от скрипта; пустая строка `""` также означает ошибку |
Строковые ошибки допустимы только в возвращаемых значениях Hook. Сами API Xray возвращают ошибку или `nil`. Проверка значений и поведение при ошибках описаны в соответствующем Hook.
**Вывод сообщений об ошибках**
Сообщение объекта ошибки Go нельзя получить через `tostring(err)`. Для вывода передайте `err` напрямую в [`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-запроса: ", err)
elseif ips ~= nil and #ips > 0 then
local firstIP = ips[1]:String()
end
```
@@ -0,0 +1,67 @@
# Hook DNS
Во время запросов встроенного DNS ядро вызывает глобальную функцию `HandleDNSQuery` из `dns.script`. Настройка и полные примеры приведены в [руководстве по скриптам DNS](../guide/dns.md).
## Указатель API
| Категория | Член API | Описание |
| --------- | ---------------------------------------- | ---------------------- |
| Hook | [`HandleDNSQuery(...)`](#handlednsquery) | Обработка DNS-запросов |
## Интерфейс Hook
### HandleDNSQuery(...)
```lua
function HandleDNSQuery(domain, ipv4, ipv6, fake)
return ips, ttl, err
end
```
Обрабатывает один DNS-запрос и возвращает срез IP, TTL или ошибку.
#### Параметры
Ядро всегда передаёт все четыре параметра; ни один из них не равен `nil`.
| Параметр | Тип | Описание |
| -------- | --------- | ------------------------------------------------ |
| `domain` | `string` | Запрашиваемый домен; непустой, в нижнем регистре |
| `ipv4` | `boolean` | Разрешены ли запросы адресов IPv4 |
| `ipv6` | `boolean` | Разрешены ли запросы адресов IPv6 |
| `fake` | `boolean` | Разрешён ли FakeDNS для этого запроса |
`ipv4` и `ipv6` уже ограничены глобальным [`queryStrategy`](../../../config/dns.md#dnsobject); хотя бы один равен `true`. Обычно при запросах к вышестоящему серверу эти три булевых параметра передаются без изменений.
#### Возвращаемые значения
| Значение | Тип | Описание |
| -------- | ---------------------------------------------------- | --------------------------------------------------------------------------------------- |
| `ips` | [`[]net.IP`](./data-types.md#net-ip-1) или `nil` | Результаты запроса; нужен срез, массив Lua не принимается |
| `ttl` | `number` или `nil` | TTL в секундах; при `err == nil` должен быть целым числом в диапазоне `0 .. 4294967295` |
| `err` | [`error`](./data-types.md#error), `string` или `nil` | Только `nil` означает отсутствие ошибки |
#### Поведение
Ядро последовательно проверяет `err`, `ttl` и `ips`, останавливаясь при первой ошибке. Если `err` не равен `nil`, два остальных значения игнорируются. При отсутствии ошибки нужно указать допустимый TTL, даже если результат IP пуст.
Для пустого ответа используйте `return nil, 0, nil`. Ошибка должна быть в третьей позиции, например `return nil, nil, "ошибка запроса"`.
| Результат скрипта | Действие ядра |
| ------------------------------------------------------------------ | -------------------------------------------- |
| Проверка успешна, `ips` непустой | Запрос успешен |
| `err == nil`, TTL допустим, но `ips` равен `nil` или пустому срезу | Запрос завершается неудачей с пустым ответом |
| Возвращена ошибка, неверный тип значения или исключение выполнения | Запрос завершается неудачей |
#### Пример
Можно напрямую передать все три возвращаемых значения [`serverObj:Query`](./module-dns.md#query):
```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 @@
# Hook маршрутизации
При выборе исходящего подключения ядро вызывает глобальную функцию `HandleRoute` из `routing.script`. Настройка и полные примеры приведены в [руководстве по скриптам маршрутизации](../guide/routing.md).
## Указатель API
| Категория | Член API | Описание |
| --------- | --------------------------------------- | ---------------------------------------- |
| Hook | [`HandleRoute(...)`](#handleroute) | Выбор исходящего подключения для запроса |
| Объект | [`routing.Context`](#routing-context) | Контекст маршрутизации текущего запроса |
| Метод | [`ctx:GetSourceIPs()`](#getsourceips) | Получение IP источника |
| Метод | [`ctx:GetTargetIPs()`](#gettargetips) | Получение IP назначения |
| Метод | [`ctx:GetLocalIPs()`](#getlocalips) | Получение локальных IP |
| Метод | [`ctx:GetAttributes()`](#getattributes) | Получение атрибутов запроса |
## Интерфейс Hook
### HandleRoute(...)
```lua
function HandleRoute(ctx, inboundTag, sourcePort, targetPort,
localPort, targetDomain, network, protocol, user,
vlessRoute, skipDNSResolve)
return outboundTag, ruleTag, err
end
```
Выбирает исходящее подключение для текущего запроса и при необходимости возвращает имя правила или ошибку.
#### Параметры
Ядро всегда передаёт все 11 параметров в указанном порядке; ни один из них не равен `nil`. В определении функции можно опустить неиспользуемые параметры в конце — это не меняет значения, передаваемые ядром.
| Параметр | Тип | Описание | Пустые значения и значения по умолчанию |
| ---------------- | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `ctx` | [`routing.Context`](#routing-context) | Контекст текущего запроса | Всегда допустимый `userdata` |
| `inboundTag` | `string` | Тег входящего подключения | `""`, если тег не задан или нет входящей информации |
| `sourcePort` | `number` | Порт источника, `0 .. 65535` | `0`, если нет допустимого адреса источника |
| `targetPort` | `number` | Порт назначения, `0 .. 65535` | `0`, если нет допустимого адреса назначения |
| `localPort` | `number` | Локальный порт входящего соединения, `0 .. 65535` | `0`, если нет допустимого локального адреса |
| `targetDomain` | `string` | Действующий домен, обнаруженный анализом трафика, имеет приоритет; иначе используется домен цели соединения. Значение приводится к нижнему регистру | `""`, если цель — IP без действующего обнаруженного домена или нет информации о цели |
| `network` | `number` | Тип сети; сравнивается с константами вроде [`router.NetworkTCP`](./module-router.md#константы) | `NetworkUnknown` (`0`), если нет исходящей информации или тип сети неизвестен |
| `protocol` | `string` | Протокол, обнаруженный анализом трафика | `""`, если нет информации об обнаруженном протоколе |
| `user` | `string` | Email пользователя | `""`, если нет входящей информации, данных пользователя или email не задан |
| `vlessRoute` | `number` | Значение маршрута из 7-го и 8-го байтов UUID VLESS, `0 .. 65535` | `0` при отсутствии информации; само значение также может быть 0 |
| `skipDNSResolve` | `boolean` | При `true` нужно пропустить DNS-запросы, чтобы избежать петель | `false`, если нет этого флага или дополнительных данных запроса |
#### Возвращаемые значения
| Значение | Тип | Описание |
| ------------- | ---------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| `outboundTag` | `string` или `nil` | Тег исходящего подключения; `nil` или `""` означает, что оно не выбрано |
| `ruleTag` | `string` или `nil` | Необязательное имя правила; `nil`, отсутствие значения или `""` означает отсутствие имени |
| `err` | [`error`](./data-types.md#error), `string` или `nil` | Только `nil` означает отсутствие ошибки |
#### Поведение
Ядро последовательно проверяет `err`, `outboundTag` и `ruleTag`, останавливаясь при первой ошибке. Если `err` не равен `nil`, первые два значения игнорируются. Если исходящее подключение не выбрано, `ruleTag` игнорируется.
При успехе значения `ruleTag` и `err` в конце можно опустить, например `return "direct"`. Ошибка должна быть в третьей позиции, например `return nil, nil, "ошибка маршрутизации"`; `return nil, "ошибка маршрутизации"` не сообщает об ошибке.
| Результат скрипта | Действие ядра |
| ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------- |
| Проверка успешна, `outboundTag` непустой | Использует соответствующее исходящее подключение; если тег не существует, закрывает соединение |
| Исходящее подключение не выбрано | Использует исходящее подключение по умолчанию |
| Возвращена ошибка, неверный тип значения или исключение выполнения | Записывает ошибку в журнал и пробует исходящее подключение по умолчанию |
Исходящее подключение по умолчанию — первое в конфигурации; если оно недоступно, соединение закрывается. Для блокировки трафика явно возвращайте тег настроенного исходящего подключения [`blackhole`](../../../config/outbounds/blackhole.md), а не используйте это поведение с несуществующим тегом.
## Связанные объекты
### routing.Context
`routing.Context` представляет контекст маршрутизации текущего запроса и доступен в `HandleRoute` через параметр `ctx`.
| Свойство | Описание |
| ------------------- | ------------------------------------------------- |
| Базовый тип Go | Интерфейс `routing.Context` из `features/routing` |
| Представление в Lua | `userdata` |
| Получение | Первый параметр `HandleRoute`, передаваемый ядром |
Все методы ниже не принимают дополнительных параметров. Операции с IP и срезами описаны в разделе [Типы данных](./data-types.md).
#### GetSourceIPs
```lua
local ips = ctx:GetSourceIPs()
```
Получает IP-адреса источника запроса.
**Параметры**
Дополнительных параметров нет.
**Возвращаемые значения**
| Значение | Тип | Описание |
| -------- | ------------------------------------------------ | --------------------------------- |
| `ips` | [`[]net.IP`](./data-types.md#net-ip-1) или `nil` | Список IP соответствующего адреса |
**Поведение**
Возвращает `nil`, если нет входящего подключения, адрес источника недопустим или не является IP-адресом.
#### GetTargetIPs
```lua
local ips = ctx:GetTargetIPs()
```
Получает IP-адреса назначения запроса.
**Параметры**
Дополнительных параметров нет.
**Возвращаемые значения**
| Значение | Тип | Описание |
| -------- | ------------------------------------------------ | --------------------------------- |
| `ips` | [`[]net.IP`](./data-types.md#net-ip-1) или `nil` | Список IP соответствующего адреса |
**Поведение**
Возвращает `nil`, если нет исходящего подключения, адрес назначения недопустим или является доменным именем.
#### GetLocalIPs
```lua
local ips = ctx:GetLocalIPs()
```
Получает локальные IP-адреса входящего соединения.
**Параметры**
Дополнительных параметров нет.
**Возвращаемые значения**
| Значение | Тип | Описание |
| -------- | ------------------------------------------------ | --------------------------------- |
| `ips` | [`[]net.IP`](./data-types.md#net-ip-1) или `nil` | Список IP соответствующего адреса |
**Поведение**
Возвращает `nil`, если нет входящего подключения, локальный адрес недопустим или не является IP-адресом.
#### GetAttributes
```lua
local attributes = ctx:GetAttributes()
```
Получает атрибуты текущего запроса.
**Параметры**
Дополнительных параметров нет.
**Возвращаемые значения**
| Значение | Тип | Описание |
| ------------ | ------------------- | ----------------------------------- |
| `attributes` | `map[string]string` | Всегда `userdata`; никогда не `nil` |
**Поведение**
Читайте атрибуты через `attributes[key]`, где `key` должен быть строкой. Существующий ключ возвращает строку (возможно `""`), отсутствующий — `nil`.
Значение и использование атрибутов описаны в поле [`attrs`](../../../config/routing.md#ruleobject) конфигурации маршрутизации.
**Пример**
```lua
local attributes = ctx:GetAttributes()
if attributes[":method"] == "GET" then
return "direct", "lua-get"
end
```
@@ -0,0 +1,139 @@
# xray.dns
Предоставляет интерфейсы DNS-запросов и доступа к вышестоящим серверам.
```lua
local dns = require("xray.dns")
```
## Указатель API
| Категория | Член API | Описание |
| --------- | ----------------------------------------------------- | ---------------------------------- |
| Функция | [`dns.Query(domain, ipv4, ipv6, fake)`](#dns-query) | Запрос через DNS-клиент Xray |
| Поле | [`dns.Servers`](#dns-servers) | Массив вышестоящих серверов |
| Объект | [`serverObj`](#server) | Отдельный вышестоящий DNS-сервер |
| Поле | [`serverObj.ID`](#id) | Идентификатор сервера |
| Метод | [`serverObj:Query(domain, ipv4, ipv6, fake)`](#query) | Прямой запрос к указанному серверу |
## Функции
### dns.Query
```lua
local ips, ttl, err = dns.Query(domain, ipv4, ipv6, fake)
```
Запрашивает домен через DNS-клиент Xray. В Hook DNS недоступен, поскольку скрипт заменяет именно этот процесс обработки запросов.
**Параметры**
| Параметр | Тип | Описание |
| -------- | --------- | ------------------------- |
| `domain` | `string` | Запрашиваемый домен |
| `ipv4` | `boolean` | Разрешены ли запросы IPv4 |
| `ipv6` | `boolean` | Разрешены ли запросы IPv6 |
| `fake` | `boolean` | Разрешён ли FakeDNS |
**Возвращаемые значения**
| Значение | Тип | Описание |
| -------- | ------------------------------------------------ | ------------------------------------------------------------ |
| `ips` | [`[]net.IP`](./data-types.md#net-ip-1) или `nil` | Результаты запроса |
| `ttl` | `number` | TTL в секундах, диапазон `0 .. 4294967295`; никогда не `nil` |
| `err` | [`error`](./data-types.md#error) или `nil` | Ошибка запроса; `nil` означает отсутствие ошибки |
**Поведение**
Все четыре параметра нужно передать явно с правильными типами, иначе возникает исключение Lua.
Запрос учитывает глобальные ограничения типов запросов клиента, Hosts и настройки вышестоящих серверов. Если настроен [скрипт DNS](../guide/dns.md), запрос также передаётся ему. Если встроенный DNS не настроен, используется системный DNS.
При успехе возвращается непустой срез IP; при ошибке запроса или отсутствии доступных адресов — `nil, 0, err`.
**Пример**
```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
```
## Поля
### dns.Servers
```lua
local servers = dns.Servers
```
| Имя | Тип | Описание |
| ------------- | ------- | ------------------------------------------------------------------------------ |
| `dns.Servers` | `table` | Массив [`serverObj`](#server), индексы с 1, порядок соответствует конфигурации |
При использовании встроенного DNS список содержит настроенные вышестоящие серверы. Если DNS или его серверы не настроены, список содержит только `localhost` (системный DNS).
Для перебора списка используйте `ipairs`; при выходе индекса за границы `servers[i]` равен `nil`.
**Пример**
```lua
for _, serverObj in ipairs(dns.Servers) do
local id = serverObj.ID
end
```
## Объекты
### server
`serverObj` представляет вышестоящий DNS-сервер с его идентификатором и методом запроса.
| Свойство | Описание |
| ------------------- | --------------------------------------------- |
| Реализация в Go | `luaDNSServer` из `app/dns` |
| Представление в Lua | `table` |
| Получение | Элемент массива [`dns.Servers`](#dns-servers) |
#### ID
```lua
local id = serverObj.ID
```
| Имя | Тип | Описание |
| -------------- | -------- | ------------------------------------------------------------------------------------------------ |
| `serverObj.ID` | `string` | Поле `id` в [конфигурации DNS-сервера](../../../config/dns.md#dnsserverobject); никогда не `nil` |
Если `id` не задан, значение равно `""`, в том числе у серверов, настроенных строками. Автоматически добавленный системный DNS имеет ID `"localhost"`.
Если нужно различать серверы по ID, самостоятельно задайте уникальные значения: ядро не проверяет дубликаты.
#### Query
```lua
local ips, ttl, err = serverObj:Query(domain, ipv4, ipv6, fake)
```
Запрашивает вышестоящий сервер, представленный `serverObj`.
**Параметры и возвращаемые значения**
Требования к параметрам и типы возвращаемых значений такие же, как у [`dns.Query`](#dns-query).
**Поведение**
Обычно настроенный сервер при успехе возвращает непустой срез IP, TTL и `nil`. При ошибке запроса, отсутствии адресов, удовлетворяющих условиям, или запросе FakeDNS с `fake == false` возвращается `nil, 0, err`.
Запрашивает этот сервер напрямую, обходя глобальные Hosts и скрипт DNS. Собственные настройки сервера `queryStrategy`, кеш, `timeoutMs`, `clientIP`, `tag`, а также `expectedIPs` / `unexpectedIPs` и соответствующие `actPrior` / `actUnprior` продолжают действовать.
**Пример**
Вернуть результат запроса к первому серверу напрямую из Hook DNS:
```lua
function HandleDNSQuery(domain, ipv4, ipv6, fake)
return dns.Servers[1]:Query(domain, ipv4, ipv6, fake)
end
```
@@ -0,0 +1,344 @@
# xray.geodata
Предоставляет сопоставление доменов и IP по правилам.
```lua
local geodata = require("xray.geodata")
```
## Указатель API
| Категория | Член API | Описание |
| --------- | ---------------------------------------------------------------- | ------------------------------------------- |
| Функция | [`geodata.BuildDomainMatcher(...)`](#geodata-builddomainmatcher) | Создание `DomainMatcher` |
| Функция | [`geodata.BuildIPMatcher(...)`](#geodata-buildipmatcher) | Создание `IPMatcher` |
| Объект | [`DomainMatcher`](#domainmatcher) | Объект сопоставления доменов |
| Метод | [`domainMatcherObj:MatchAny(domain)`](#matchany) | Проверка совпадения домена с любым правилом |
| Метод | [`domainMatcherObj:Match(domain)`](#match) | Получение номеров совпавших правил домена |
| Объект | [`IPMatcher`](#ipmatcher) | Объект сопоставления IP |
| Метод | [`ipMatcherObj:Match(ip)`](#match-1) | Сопоставление отдельного IP |
| Метод | [`ipMatcherObj:AnyMatch(ips)`](#anymatch) | Проверка наличия совпавшего IP |
| Метод | [`ipMatcherObj:Matches(ips)`](#matches) | Проверка совпадения всего списка IP |
| Метод | [`ipMatcherObj:FilterIPs(ips)`](#filterips) | Разделение совпавших и несовпавших IP |
| Метод | [`ipMatcherObj:SetReverse(reverse)`](#setreverse) | Установка флага инверсии |
| Метод | [`ipMatcherObj:ToggleReverse()`](#togglereverse) | Переключение флага инверсии |
## Функции
### geodata.BuildDomainMatcher
```lua
local domainMatcherObj = geodata.BuildDomainMatcher(rule1, rule2, ...)
```
Создаёт [`DomainMatcher`](#domainmatcher) из правил доменов.
**Параметры**
| Параметр | Тип | Описание / Требования |
| ------------------- | -------- | -------------------------------------------------------------------- |
| `rule1, rule2, ...` | `string` | Обязательны; хотя бы одно правило домена, каждое передаётся отдельно |
**Возвращаемые значения**
| Значение | Тип | Описание |
| ------------------ | --------------------------------- | ------------------------------- |
| `domainMatcherObj` | [`DomainMatcher`](#domainmatcher) | Экземпляр сопоставления доменов |
**Поведение**
Если правила не переданы, тип аргумента неверен или произошла ошибка разбора правил, загрузки ресурсов или создания объекта, возникает исключение Lua.
Поддерживаются `domain:`, `full:`, `keyword:`, `regexp:`, `dotless:`, `geosite:` и `ext:` (также `ext-domain:` / `ext-site:`). Форматы описаны в [правилах доменов маршрутизации](../../../config/routing.md#ruleobject). Файлы GeoSite загружаются из [каталога ресурсов](../../../config/env.md#путь-к-фаилам-ресурсов).
Строки без префикса по умолчанию используют правило `domain:`.
Кроме регулярных выражений, правила при создании приводятся к нижнему регистру.
**Пример**
```lua
local sitesObj = geodata.BuildDomainMatcher(
"example.com", "full:other.example"
)
```
### geodata.BuildIPMatcher
```lua
local ipMatcherObj = geodata.BuildIPMatcher(rule1, rule2, ...)
```
Создаёт [`IPMatcher`](#ipmatcher) из правил IP.
**Параметры**
| Параметр | Тип | Описание / Требования |
| ------------------- | -------- | ---------------------------------------------------------------- |
| `rule1, rule2, ...` | `string` | Обязательны; хотя бы одно правило IP, каждое передаётся отдельно |
**Возвращаемые значения**
| Значение | Тип | Описание |
| -------------- | ------------------------- | -------------------------- |
| `ipMatcherObj` | [`IPMatcher`](#ipmatcher) | Экземпляр сопоставления IP |
**Поведение**
Если правила не переданы, тип аргумента неверен, правило является пустой строкой или произошла ошибка разбора правил, загрузки ресурсов или создания объекта, возникает исключение Lua.
Поддерживаются IP, CIDR, `geoip:`, `ext:` (также `ext-ip:`) и правила инверсии `!`. Сочетание правил описано в [правилах IP маршрутизации](../../../config/routing.md#ruleobject). Файлы GeoIP загружаются из [каталога ресурсов](../../../config/env.md#путь-к-фаилам-ресурсов).
**Пример**
```lua
local privateIPsObj = geodata.BuildIPMatcher(
"10.0.0.0/8", "192.168.0.0/16", "fc00::/7"
)
```
## Объекты
### DomainMatcher
| Свойство | Описание |
| ------------------- | -------------------------------------------------------------------------------------- |
| Базовый тип Go | Реализация интерфейса `geodata.DomainMatcher` |
| Представление в Lua | `userdata` |
| Получение | Возвращаемое значение [`geodata.BuildDomainMatcher(...)`](#geodata-builddomainmatcher) |
**Общие соглашения**
Методы сопоставления принимают одну строку домена. `nil`, отсутствие аргумента, неверный тип или число аргументов вызывают исключение Lua.
Входной домен не приводится к нижнему регистру автоматически; при необходимости это должен делать скрипт.
#### MatchAny
```lua
local matched = domainMatcherObj:MatchAny(domain)
```
Проверяет, совпадает ли домен хотя бы с одним правилом.
**Параметры**
| Параметр | Тип | Описание / Требования |
| -------- | -------- | ----------------------------------- |
| `domain` | `string` | Обязателен; домен для сопоставления |
**Возвращаемые значения**
| Значение | Тип | Описание |
| --------- | --------- | ------------------------------------ |
| `matched` | `boolean` | `true` при совпадении, иначе `false` |
**Пример**
```lua
local matched = sitesObj:MatchAny("www.example.com")
```
#### Match
```lua
local indices = domainMatcherObj:Match(domain)
```
Получает номера правил, с которыми совпал домен.
**Параметры**
| Параметр | Тип | Описание / Требования |
| -------- | -------- | ----------------------------------- |
| `domain` | `string` | Обязателен; домен для сопоставления |
**Возвращаемые значения**
| Значение | Тип | Описание |
| --------- | ---------------------------------------------- | --------------------------------------------------------------------------- |
| `indices` | [`[]uint32`](./data-types.md#uint32) или `nil` | Номера совпавших правил; при отсутствии совпадений — `nil` или срез длины 0 |
**Поведение**
Номера правил начинаются с **0** и соответствуют порядку правил, переданных при создании. Порядок результатов не гарантируется; возможны повторяющиеся номера. Записи, развёрнутые из GeoSite, сохраняют номер исходного правила.
Если те же правила сохранены в массиве Lua, исходное правило можно получить через `rules[ruleNumber + 1]`. Доступ к срезам описан в разделе [Типы данных](./data-types.md#slice).
**Пример**
```lua
local indices = sitesObj:Match("www.example.com")
if indices ~= nil then
for i = 1, #indices do
local ruleNumber = indices[i]
end
end
```
### IPMatcher
| Свойство | Описание |
| ------------------- | ------------------------------------------------------------------------------ |
| Базовый тип Go | Реализация интерфейса `geodata.IPMatcher` |
| Представление в Lua | `userdata` |
| Получение | Возвращаемое значение [`geodata.BuildIPMatcher(...)`](#geodata-buildipmatcher) |
**Общие соглашения**
Для отдельного IP используется [`net.IP`](./data-types.md#net-ip); для сопоставления или фильтрации списков формы ввода описаны в разделе [Ввод списка IP](./data-types.md#ввод-списка-ip). Строки IP-адресов не преобразуются в `net.IP` автоматически.
Методы сопоставления и фильтрации принимают явно переданный `nil`. Отсутствие аргументов, неверный тип или число аргументов вызывают исключение Lua.
#### Match
```lua
local matched = ipMatcherObj:Match(ip)
```
Проверяет, совпадает ли отдельный IP с правилами.
**Параметры**
| Параметр | Тип | Описание / Требования |
| -------- | -------------------------------------------- | ------------------------------- |
| `ip` | [`net.IP`](./data-types.md#net-ip) или `nil` | Обязателен; нужно передать явно |
**Возвращаемые значения**
| Значение | Тип | Описание |
| --------- | --------- | ------------------------------------------------------------------------- |
| `matched` | `boolean` | `true`, если допустимый IP совпал; `false` для `nil` или недопустимого IP |
**Поведение**
`""` или пустая таблица `{}` трактуются как пустой IP и возвращают `false`. Строки не разбираются как текст IP-адреса.
#### AnyMatch
```lua
local matched = ipMatcherObj:AnyMatch(ips)
```
Проверяет, совпадает ли с правилами хотя бы один допустимый IP.
**Параметры**
| Параметр | Тип | Описание / Требования |
| -------- | ------------------------------------------------ | -------------------------------------------------- |
| `ips` | [Ввод списка IP](./data-types.md#ввод-списка-ip) | Обязателен; нужно передать явно; допускается `nil` |
**Возвращаемые значения**
| Значение | Тип | Описание |
| --------- | --------- | ----------------------------------------------------------------------------------------------------------- |
| `matched` | `boolean` | `true`, если совпал хотя бы один допустимый IP; `false` для `nil`, пустого списка или отсутствия совпадений |
#### Matches
```lua
local matched = ipMatcherObj:Matches(ips)
```
Проверяет, совпадает ли с правилами весь список IP.
**Параметры**
| Параметр | Тип | Описание / Требования |
| -------- | ------------------------------------------------ | -------------------------------------------------- |
| `ips` | [Ввод списка IP](./data-types.md#ввод-списка-ip) | Обязателен; нужно передать явно; допускается `nil` |
**Возвращаемые значения**
| Значение | Тип | Описание |
| --------- | --------- | ---------------------------------------------------------------------------------------------------------------- |
| `matched` | `boolean` | `true`, если весь список удовлетворяет условиям; `false` для `nil`, пустого списка или списка с недопустимыми IP |
**Поведение**
Сочетание правил может содержать несколько внутренних объектов сопоставления. Один внутренний объект должен сопоставить весь список.
Например, для `geodata.BuildIPMatcher("192.168.1.0/24", "geoip:us")` список с `192.168.1.1` и `8.8.8.8` даёт `false` у `Matches`: первый IP совпадает с пользовательским CIDR, второй — с `geoip:us`, но эти правила принадлежат разным внутренним объектам, и ни один не сопоставляет весь список.
Если нужно проверить лишь наличие совпавшего адреса, используйте [`AnyMatch`](#anymatch).
#### FilterIPs
```lua
local matched, unmatched = ipMatcherObj:FilterIPs(ips)
```
Разделяет допустимые IP на совпавшие и несовпавшие группы.
**Параметры**
| Параметр | Тип | Описание / Требования |
| -------- | ------------------------------------------------ | -------------------------------------------------- |
| `ips` | [Ввод списка IP](./data-types.md#ввод-списка-ip) | Обязателен; нужно передать явно; допускается `nil` |
**Возвращаемые значения**
| Значение | Тип | Описание |
| ----------- | -------------------------------------- | -------------------------------- |
| `matched` | [`[]net.IP`](./data-types.md#net-ip-1) | Совпавшие IP; никогда не `nil` |
| `unmatched` | [`[]net.IP`](./data-types.md#net-ip-1) | Несовпавшие IP; никогда не `nil` |
**Поведение**
Группа без результатов является пустым срезом. При передаче `nil`, пустого списка или только недопустимых IP обе группы — пустые срезы.
Недопустимые IP отбрасываются; порядок результатов не обязательно совпадает с порядком ввода.
#### SetReverse
```lua
ipMatcherObj:SetReverse(reverse)
```
Устанавливает флаг инверсии каждого внутреннего объекта сопоставления.
**Параметры**
| Параметр | Тип | Описание / Требования |
| --------- | --------- | ------------------------------------------------------- |
| `reverse` | `boolean` | Обязателен; `true` включает инверсию, `false` отключает |
**Возвращаемые значения**
Нет возвращаемых значений.
**Поведение**
`nil`, отсутствие аргумента или неверный тип вызывают исключение Lua. Операция заменяет прежний флаг инверсии каждого внутреннего объекта, включая флаги `!` из правил. Инверсия действует только на семейства адресов исходного правила и не сопоставляет недопустимые IP.
Установленное состояние инверсии сохраняется в текущем объекте сопоставления. Если последующие вызовы Hook повторно используют этот объект, изменённое состояние продолжает действовать. См. [Состояние в экземплярах пула](../guide/lifecycle.md#состояние-в-экземплярах-пула).
**Пример**
После инверсии `10.0.0.0/8` сопоставляются допустимые адреса IPv4 вне этого диапазона:
```lua
local ipMatcherObj = geodata.BuildIPMatcher("10.0.0.0/8")
ipMatcherObj:SetReverse(true)
```
#### ToggleReverse
```lua
ipMatcherObj:ToggleReverse()
```
Переключает флаг инверсии каждого внутреннего объекта сопоставления.
**Параметры**
Дополнительных параметров нет.
**Возвращаемые значения**
Нет возвращаемых значений.
**Поведение**
Каждый внутренний объект в сочетании правил переключает собственный флаг инверсии. Результат не равнозначен логическому отрицанию всего сочетания.
Новое состояние инверсии сохраняется в текущем объекте сопоставления. Если последующие вызовы Hook повторно используют этот объект, изменённое состояние продолжает действовать. См. [Состояние в экземплярах пула](../guide/lifecycle.md#состояние-в-экземплярах-пула).
@@ -0,0 +1,69 @@
# xray.log
Записывает сообщения в систему журналирования Xray.
```lua
local log = require("xray.log")
```
## Указатель API
| Категория | Член API | Описание |
| --------- | ---------------------------------- | ------------------------ |
| Функция | [`log.Debug(...)`](#log-debug) | Запись сообщения debug |
| Функция | [`log.Info(...)`](#log-info) | Запись сообщения info |
| Функция | [`log.Warning(...)`](#log-warning) | Запись сообщения warning |
| Функция | [`log.Error(...)`](#log-error) | Запись сообщения error |
## Функции
### log.Debug
```lua
log.Debug(...)
```
Записывает сообщение уровня `debug`.
**Параметры**
| Параметр | Тип | Описание / Требования |
| -------- | ------------------ | ------------------------------------------------------------ |
| `...` | Любое значение Lua | Необязательны; любое число значений, объединяемых по порядку |
**Возвращаемые значения**
Нет возвращаемых значений.
**Преобразование аргументов**
Допускается вызов без аргументов, а также `nil`, `""`, `{}` и пустые срезы. Аргументы преобразуются в текст и объединяются по порядку без автоматической вставки пробелов или разделителей.
- Значения [`error`](./data-types.md#error) в первую очередь преобразуются в сообщения об ошибках.
- Для остальных значений с функцией `__tostring` используется это преобразование; исключения из него передаются вызывающему коду.
- Остальные значения используют строковое представление по умолчанию. `nil` становится `"nil"`, пустая строка не добавляет текст; таблицы и срезы не разворачиваются в списки элементов.
**Условия вывода**
Сообщения имеют префикс с именем файла вызывающего скрипта. Вывод зависит от [`log.loglevel`](../../../config/log.md#logobject). Например, при уровне `warning` функции `Debug` и `Info` ничего не выводят и не вызывают преобразование аргументов через `__tostring`.
Ни одна из четырёх функций не возвращает `err`. Если присвоить результат вызова переменной, она получит `nil`; по этому значению нельзя определить, было ли сообщение успешно записано.
**Пример**
```lua
log.Info("Запрашиваемый домен: ", domain)
log.Warning("Ошибка запроса к вышестоящему серверу: ", err)
```
### log.Info
Как описано выше.
### log.Warning
Как описано выше.
### log.Error
Как описано выше.
@@ -0,0 +1,119 @@
# xray.router
Предоставляет константы и вспомогательные интерфейсы для [скриптов маршрутизации](../guide/routing.md). Параметры запроса и контекст описаны в [Hook маршрутизации](./hook-routing.md).
```lua
local router = require("xray.router")
```
## Указатель API
| Категория | Член API | Описание |
| --------- | ---------------------------------------------------------- | ------------------------------------------------ |
| Константа | [`router.NetworkUnknown`](#константы) | Неизвестная сеть |
| Константа | [`router.NetworkTCP`](#константы) | TCP |
| Константа | [`router.NetworkUDP`](#константы) | UDP |
| Константа | [`router.NetworkUNIX`](#константы) | UNIX |
| Константа | [`router.LocalOS`](#константы) | Платформа выполнения |
| Функция | [`router:PickOutbound(balancerTag)`](#router-pickoutbound) | Выбор исходящего подключения через балансировщик |
| Функция | [`router.FindProcess(ctx)`](#router-findprocess) | Поиск процесса локального соединения |
## Константы
| Имя | Тип | Значение / Описание |
| ----------------------- | -------- | ---------------------------------------------------------------------------- |
| `router.NetworkUnknown` | `number` | `0`, неизвестная сеть |
| `router.NetworkTCP` | `number` | `2`, TCP |
| `router.NetworkUDP` | `number` | `3`, UDP |
| `router.NetworkUNIX` | `number` | `4`, UNIX |
| `router.LocalOS` | `string` | Текущая платформа выполнения, например `"windows"`, `"linux"` или `"darwin"` |
Параметр `network` из [Hook маршрутизации](./hook-routing.md#handleroute) можно сравнивать с сетевыми константами:
```lua
if network == router.NetworkUDP then
return "direct", "lua-udp"
end
```
## Функции
### router:PickOutbound
```lua
local outboundTag, err = router:PickOutbound(balancerTag)
```
Выбирает исходящее подключение через указанный балансировщик.
**Параметры**
| Параметр | Тип | Описание / Требования |
| ------------- | -------- | ---------------------------------------------------------------------------------------------------------------------- |
| `balancerTag` | `string` | Значение `tag` нужного балансировщика, настроенного в [`routing.balancers`](../../../config/routing.md#balancerobject) |
**Возвращаемые значения**
| Значение | Тип | Описание |
| ------------- | ------------------------------------------ | ------------------------------------------------------------- |
| `outboundTag` | `string` или `nil` | При успехе — непустой `tag` выбранного исходящего подключения |
| `err` | [`error`](./data-types.md#error) или `nil` | `nil` означает успех |
**Поведение**
Нужно явно передать строку, иначе возникает исключение Lua. Пустая строка `""` выполняет поиск по пустому `tag`.
Перед использованием результата проверьте `err`. Если балансировщик не существует, возвращается `nil, err`; если существует, но не выбрал исходящее подключение, — `"", err`. Когда срабатывает настроенный `fallbackTag`, возвращаются этот тег исходящего подключения и `nil`.
**Пример**
Ниже используется балансировщик с `tag` `"proxy-pool"`, а результат передаётся согласно [соглашению о возвращаемых значениях Hook маршрутизации](./hook-routing.md#возвращаемые-значения):
```lua
local outboundTag, err = router:PickOutbound("proxy-pool")
return outboundTag, "lua-balance", err
```
Полный пример приведён в [руководстве по скриптам маршрутизации](../guide/routing.md#связь-с-конфигурациеи-маршрутизации).
### router.FindProcess
```lua
local pid, name, path, err = router.FindProcess(ctx)
```
Находит процесс на локальной машине, связанный с текущим соединением.
**Параметры**
| Параметр | Тип | Описание / Требования |
| -------- | ------------------------------------------------------ | -------------------------------------------------------- |
| `ctx` | [`routing.Context`](./hook-routing.md#routing-context) | Контекст текущего запроса, переданный Hook маршрутизации |
**Возвращаемые значения**
| Значение | Тип | Описание |
| -------- | ------------------------------------------ | ------------------------------------------------ |
| `pid` | `number` | ID процесса; обычно `0`, если не получен |
| `name` | `string` | Имя процесса; `""`, если не получено |
| `path` | `string` | Путь к исполняемому файлу; `""`, если не получен |
| `err` | [`error`](./data-types.md#error) или `nil` | `nil` означает успех |
**Поведение**
Нужно явно передать допустимый контекст маршрутизации, иначе возникает исключение Lua. Если нет IP источника, тип сети не TCP/UDP, поиск не удался или платформа не поддерживается, возвращается ошибка.
`pid`, `name` и `path` никогда не равны `nil`, но при ошибке могут содержать частичные данные; перед использованием проверьте `err`. Если нет IP источника или тип сети не поддерживается, возвращается `0, "", "", err`; в остальных случаях результат платформенного поиска возвращается без изменений.
Поиск использует первый IP источника и порт источника. Если есть IP назначения, также передаются первый IP назначения и порт назначения. Точный способ сопоставления определяет платформенная реализация поиска.
Возможности поиска зависят от платформы и прав доступа. Windows, Linux и macOS имеют встроенные реализации. В Android среда выполнения должна зарегистрировать реализацию поиска; даже при успехе имя или путь могут отсутствовать. iOS и другие неподдерживаемые платформы возвращают ошибку.
**Пример**
```lua
local pid, name, path, err = router.FindProcess(ctx)
if err == nil and name == "curl" then
return "direct", "lua-process"
end
```