mirror of
https://github.com/XTLS/Xray-docs-next.git
synced 2026-10-12 09:18:19 +03:00
Xray-core: Add Lua script for dns and routing
https://github.com/XTLS/Xray-core/pull/6823
This commit is contained in:
@@ -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#池化生命周期)。
|
||||
@@ -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 使其生效;之后创建的新实例也使用本次启动时编译的主脚本。
|
||||
@@ -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#池化生命周期)。
|
||||
Reference in New Issue
Block a user