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
+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#жизненныи-цикл-пула).