mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-07-27 10:31:38 +08:00
2125 lines
67 KiB
Lua
2125 lines
67 KiB
Lua
module("luci.passwall.util_xray", package.seeall)
|
||
local api = require "luci.passwall.api"
|
||
local uci = api.uci
|
||
local sys = api.sys
|
||
local jsonc = api.jsonc
|
||
local appname = "passwall"
|
||
local fs = api.fs
|
||
|
||
local GLOBAL = {
|
||
DNS_SERVER = {},
|
||
DNS_HOSTNAME = {},
|
||
VPS_EXCLUDE = {}
|
||
}
|
||
|
||
local xray_version = api.get_app_version("xray")
|
||
|
||
local xray_min_version = "26.3.27"
|
||
|
||
local function get_domain_excluded()
|
||
local path = string.format("/usr/share/%s/rules/domains_excluded", appname)
|
||
local content = fs.readfile(path)
|
||
if not content then return nil end
|
||
local hosts = {}
|
||
string.gsub(content, '[^' .. "\n" .. ']+', function(w)
|
||
local s = api.trim(w)
|
||
if s == "" then return end
|
||
if s:find("#") and s:find("#") == 1 then return end
|
||
if not s:find("#") or s:find("#") ~= 1 then table.insert(hosts, s) end
|
||
end)
|
||
if #hosts == 0 then hosts = nil end
|
||
return hosts
|
||
end
|
||
|
||
function gen_outbound(flag, node, tag, proxy_table)
|
||
local result = nil
|
||
if node then
|
||
local node_id = node[".name"]
|
||
if tag == nil then
|
||
tag = node_id
|
||
end
|
||
local remarks = node.remarks
|
||
|
||
local proxy_tag = nil
|
||
local fragment = nil
|
||
local noise = nil
|
||
local run_socks_instance = true
|
||
if proxy_table ~= nil and type(proxy_table) == "table" then
|
||
proxy_tag = proxy_table.tag or nil
|
||
fragment = (proxy_table.fragment and not node.hysteria2_realms) and true or nil
|
||
noise = (proxy_table.noise and not node.hysteria2_realms) and true or nil
|
||
run_socks_instance = proxy_table.run_socks_instance
|
||
end
|
||
|
||
if node.type ~= "Xray" then
|
||
if node.type == "Socks" then
|
||
node.protocol = "socks"
|
||
node.transport = "tcp"
|
||
else
|
||
local relay_port = node.port
|
||
local new_port = api.get_new_port()
|
||
local config_file = string.format("%s_%s_%s.json", flag, tag, new_port)
|
||
if tag and node_id and not tag:find(node_id) then
|
||
config_file = string.format("%s_%s_%s_%s.json", flag, tag, node_id, new_port)
|
||
end
|
||
if run_socks_instance then
|
||
sys.call(string.format('/usr/share/%s/app.sh run_socks "%s"> /dev/null',
|
||
appname,
|
||
string.format("flag=%s node=%s bind=%s socks_port=%s config_file=%s relay_port=%s",
|
||
new_port, --flag
|
||
node_id, --node
|
||
"127.0.0.1", --bind
|
||
new_port, --socks port
|
||
config_file, --config file
|
||
(proxy_tag and relay_port) and tostring(relay_port) or "" --relay port
|
||
)
|
||
))
|
||
end
|
||
node = {}
|
||
node.protocol = "socks"
|
||
node.transport = "tcp"
|
||
node.address = "127.0.0.1"
|
||
node.port = new_port
|
||
end
|
||
node.stream_security = "none"
|
||
proxy_tag = "socks <- " .. node_id
|
||
else
|
||
if proxy_tag then
|
||
node.proxySettings = {
|
||
tag = proxy_tag,
|
||
transportLayer = true
|
||
}
|
||
end
|
||
end
|
||
|
||
if node.type == "Xray" then
|
||
if node.tls and node.tls == "1" then
|
||
node.stream_security = "tls"
|
||
if node.type == "Xray" and node.reality and node.reality == "1" then
|
||
node.stream_security = "reality"
|
||
end
|
||
end
|
||
end
|
||
|
||
if node.protocol == "wireguard" and node.wireguard_reserved then
|
||
local bytes = {}
|
||
if not node.wireguard_reserved:match("[^%d,]+") then
|
||
node.wireguard_reserved:gsub("%d+", function(b)
|
||
bytes[#bytes + 1] = tonumber(b)
|
||
end)
|
||
else
|
||
local result = api.base64Decode(node.wireguard_reserved)
|
||
for i = 1, #result do
|
||
bytes[i] = result:byte(i)
|
||
end
|
||
end
|
||
node.wireguard_reserved = #bytes > 0 and bytes or nil
|
||
end
|
||
|
||
if node.protocol == "hysteria2" then
|
||
node.protocol = "hysteria"
|
||
node.transport = "hysteria"
|
||
node.stream_security = "tls"
|
||
end
|
||
|
||
if remarks then
|
||
tag = tag .. ":" .. remarks
|
||
end
|
||
|
||
node.address = (node.address or ""):lower()
|
||
|
||
result = {
|
||
_id = node_id,
|
||
_flag = flag,
|
||
_flag_proxy_tag = proxy_tag,
|
||
tag = tag,
|
||
proxySettings = node.proxySettings or nil,
|
||
protocol = node.protocol,
|
||
mux = {
|
||
enabled = (node.mux == "1") and true or false,
|
||
concurrency = (node.mux == "1" and ((node.mux_concurrency) and tonumber(node.mux_concurrency) or -1)) or nil,
|
||
xudpConcurrency = (node.mux == "1" and ((node.xudp_concurrency) and tonumber(node.xudp_concurrency) or 8)) or nil
|
||
} or nil,
|
||
-- 底层传输配置
|
||
streamSettings = (node.streamSettings or node.protocol == "vmess" or node.protocol == "vless" or node.protocol == "socks" or node.protocol == "shadowsocks" or node.protocol == "trojan" or node.protocol == "hysteria") and {
|
||
sockopt = {
|
||
mark = 255,
|
||
domainStrategy = node.domain_strategy or "UseIP",
|
||
tcpFastOpen = (node.tcp_fast_open == "1") and true or nil,
|
||
tcpMptcp = (node.tcpMptcp == "1") and true or nil,
|
||
happyEyeballs = (node.happy_eyeballs == "1") and {
|
||
TryDelayMs = 250,
|
||
PrioritizeIPv6 = false,
|
||
Interleave = 1,
|
||
MaxConcurrentTry = 4
|
||
} or nil
|
||
},
|
||
[(api.compare_versions(xray_version, "<", "26.7.11")) and "network" or "method"] = node.transport, -- Todo: Remove version check and "network"
|
||
security = node.stream_security,
|
||
tlsSettings = (node.stream_security == "tls") and {
|
||
serverName = node.tls_serverName,
|
||
fingerprint = (node.type == "Xray" and node.utls == "1" and node.fingerprint and node.fingerprint ~= "") and node.fingerprint or nil,
|
||
pinnedPeerCertSha256 = node.tls_pinSHA256 or "",
|
||
verifyPeerCertByName = node.tls_CertByName or "",
|
||
echConfigList = (node.ech == "1") and node.ech_config or nil,
|
||
certificates = (node.tls_certificate == "1" and node.tls_certificate_pem ~= "") and {
|
||
certificate = api.split(node.tls_certificate_pem, "\n"),
|
||
usage = "verify"
|
||
} or nil
|
||
} or nil,
|
||
realitySettings = (node.stream_security == "reality") and {
|
||
serverName = node.tls_serverName,
|
||
publicKey = node.reality_publicKey,
|
||
shortId = node.reality_shortId or "",
|
||
spiderX = node.reality_spiderX or "/",
|
||
fingerprint = (node.type == "Xray" and node.fingerprint and node.fingerprint ~= "") and node.fingerprint or "chrome",
|
||
mldsa65Verify = (node.use_mldsa65Verify == "1") and node.reality_mldsa65Verify or nil
|
||
} or nil,
|
||
rawSettings = ((node.transport == "raw" or node.transport == "tcp") and node.protocol ~= "socks" and (node.tcp_guise and node.tcp_guise ~= "none")) and {
|
||
header = {
|
||
type = node.tcp_guise,
|
||
request = (node.tcp_guise == "http") and {
|
||
path = node.tcp_guise_http_path and (function()
|
||
local t, r = node.tcp_guise_http_path, {}
|
||
for _, v in ipairs(t) do
|
||
r[#r + 1] = (v == "" and "/" or v)
|
||
end
|
||
return r
|
||
end)() or {"/"},
|
||
headers = (node.tcp_guise_http_host or node.user_agent) and {
|
||
Host = node.tcp_guise_http_host,
|
||
["User-Agent"] = node.user_agent and {node.user_agent} or nil
|
||
} or nil
|
||
} or nil
|
||
}
|
||
} or nil,
|
||
kcpSettings = (node.transport == "mkcp") and {
|
||
mtu = (node.mkcp_mtu and node.mkcp_mtu ~= "") and tonumber(node.mkcp_mtu) or 1350,
|
||
tti = 50,
|
||
uplinkCapacity = 12,
|
||
downlinkCapacity = 100,
|
||
CwndMultiplier = 1,
|
||
MaxSendingWindow = 2 * 1024 * 1024
|
||
} or nil,
|
||
wsSettings = (node.transport == "ws") and {
|
||
path = node.ws_path or "/",
|
||
host = node.ws_host,
|
||
headers = node.user_agent and {
|
||
["User-Agent"] = node.user_agent
|
||
} or nil,
|
||
maxEarlyData = tonumber(node.ws_maxEarlyData) or nil,
|
||
earlyDataHeaderName = (node.ws_earlyDataHeaderName) and node.ws_earlyDataHeaderName or nil,
|
||
heartbeatPeriod = tonumber(node.ws_heartbeatPeriod) or nil
|
||
} or nil,
|
||
grpcSettings = (node.transport == "grpc") and {
|
||
serviceName = node.grpc_serviceName,
|
||
multiMode = (node.grpc_mode == "multi") and true or false,
|
||
idle_timeout = node.grpc_idle_timeout and (tonumber(node.grpc_idle_timeout) < 10 and 10 or tonumber(node.grpc_idle_timeout)) or nil,
|
||
health_check_timeout = tonumber(node.grpc_health_check_timeout) or nil,
|
||
permit_without_stream = (node.grpc_permit_without_stream == "1") and true or false,
|
||
initial_windows_size = node.grpc_initial_windows_size and tonumber(node.grpc_initial_windows_size) or 0,
|
||
user_agent = node.user_agent
|
||
} or nil,
|
||
httpupgradeSettings = (node.transport == "httpupgrade") and {
|
||
path = node.httpupgrade_path or "/",
|
||
host = node.httpupgrade_host,
|
||
headers = node.user_agent and {
|
||
["User-Agent"] = node.user_agent
|
||
} or nil
|
||
} or nil,
|
||
xhttpSettings = (node.transport == "xhttp") and {
|
||
mode = node.xhttp_mode or "auto",
|
||
path = node.xhttp_path or "/",
|
||
host = node.xhttp_host,
|
||
extra = (function()
|
||
local extra = {}
|
||
if node.xhttp_extra then
|
||
local ok, parsed = pcall(jsonc.parse, api.base64Decode(node.xhttp_extra))
|
||
if ok and type(parsed) == "table" then
|
||
extra = parsed.extra or parsed
|
||
end
|
||
end
|
||
-- 处理 User-Agent
|
||
if node.user_agent and node.user_agent ~= "" then
|
||
extra.headers = extra.headers or {}
|
||
if not extra.headers["User-Agent"] and not extra.headers["user-agent"] then
|
||
extra.headers["User-Agent"] = node.user_agent
|
||
end
|
||
end
|
||
return api.cleanEmptyTables(extra)
|
||
end)()
|
||
} or nil,
|
||
hysteriaSettings = (node.transport == "hysteria") and {
|
||
version = 2,
|
||
auth = node.hysteria2_auth_password
|
||
} or nil,
|
||
finalmask = (function()
|
||
local finalmask = {}
|
||
local TP = node.transport
|
||
if TP == "mkcp" then
|
||
local map = {none = "none", srtp = "srtp", utp = "utp", ["wechat-video"] = "wechat",
|
||
dtls = "dtls", wireguard = "wireguard", dns = "dns"}
|
||
local udp = {}
|
||
if node.mkcp_guise and node.mkcp_guise ~= "none" then
|
||
local g = { type = "mkcp-legacy" }
|
||
g.settings = { header = map[node.mkcp_guise] }
|
||
if node.mkcp_guise == "dns" and node.mkcp_domain and node.mkcp_domain ~= "" then
|
||
g.settings.value = node.mkcp_domain
|
||
end
|
||
udp[#udp+1] = g
|
||
end
|
||
local s = { type = "mkcp-legacy" }
|
||
if node.mkcp_seed and node.mkcp_seed ~= "" then
|
||
s.settings = { value = node.mkcp_seed }
|
||
end
|
||
udp[#udp+1] = s
|
||
finalmask.udp = udp
|
||
elseif TP == "hysteria" then
|
||
local udp = {}
|
||
if node.hysteria2_obfs_type and node.hysteria2_obfs_type ~= "" then
|
||
local o = {
|
||
type = "salamander",
|
||
settings = node.hysteria2_obfs_password and {
|
||
password = node.hysteria2_obfs_password
|
||
} or nil
|
||
}
|
||
if node.hysteria2_obfs_type == "gecko" then
|
||
local min = tonumber(node.hysteria2_obfs_MinPacketSize) or 512
|
||
local max = tonumber(node.hysteria2_obfs_MaxPacketSize) or 1200
|
||
if min <= 0 or min > max or max > 2048 then
|
||
min = 512
|
||
max = 1200
|
||
end
|
||
o.settings.packetSize = min .. "-" .. max
|
||
end
|
||
udp[#udp+1] = o
|
||
end
|
||
if node.hysteria2_realms then
|
||
local realm = api.parse_realm_uri(node.hysteria2_realm_url)
|
||
local url, stun
|
||
if realm then
|
||
url = realm.scheme .. "://" .. realm.token .. "@" .. realm.server_url .. "/" .. realm.realm_id
|
||
stun = realm.stun_servers or node.hysteria2_realm_stun
|
||
end
|
||
local r = {
|
||
type = "realm",
|
||
settings = {
|
||
url = url,
|
||
stunServers = stun
|
||
}
|
||
}
|
||
udp[#udp+1] = r
|
||
end
|
||
finalmask.udp = udp
|
||
local up = tonumber(node.hysteria2_up_mbps) or 0
|
||
local down = tonumber(node.hysteria2_down_mbps) or 0
|
||
finalmask.quicParams = {
|
||
congestion = (up <= 0 and down <= 0) and "bbr" or "brutal",
|
||
brutalUp = up > 0 and (up .. "mbps") or nil,
|
||
brutalDown = down > 0 and (down .. "mbps") or nil,
|
||
udpHop = (node.hysteria2_hop) and {
|
||
ports = string.gsub(node.hysteria2_hop, ":", "-"),
|
||
interval = (function(v)
|
||
if not v then return 30 end
|
||
if v:find("-", 1, true) then
|
||
local min, max = v:match("^(%d+)%-(%d+)$")
|
||
min = tonumber(min)
|
||
max = tonumber(max)
|
||
if min and max then
|
||
min = (min >= 5) and min or 5
|
||
max = (max >= min) and max or min
|
||
return min .. "-" .. max
|
||
end
|
||
return 30
|
||
end
|
||
v = tonumber((v or "30"):match("^%d+"))
|
||
return (v and v >= 5) and v or 30
|
||
end)(node.hysteria2_hop_interval)
|
||
} or nil,
|
||
maxIdleTimeout = (function(t)
|
||
t = tonumber(tostring(t or "30"):match("^%d+"))
|
||
return (t and t >= 4 and t <= 120) and t or 30
|
||
end)(node.hysteria2_idle_timeout),
|
||
keepAlivePeriod = (function(t)
|
||
t = tonumber(tostring(t or "0"):match("^%d+"))
|
||
return (t and t >= 2 and t <= 60) and t or nil
|
||
end)(node.hysteria2_keep_alive_period),
|
||
disablePathMTUDiscovery = tonumber(node.hysteria2_disable_mtu_discovery) == 1
|
||
}
|
||
end
|
||
if fragment and fragment_table and ({raw=1, ws=1, httpupgrade=1, grpc=1, xhttp=1})[TP] then
|
||
finalmask.tcp = finalmask.tcp or {}
|
||
finalmask.tcp[#finalmask.tcp+1] = api.clone(fragment_table)
|
||
end
|
||
if noise and noise_table and (TP == "mkcp" or (TP == "xhttp" and node.alpn == "h3")) then
|
||
finalmask.udp = finalmask.udp or {}
|
||
finalmask.udp[#finalmask.udp+1] = api.clone(noise_table)
|
||
end
|
||
if node.finalmask and node.finalmask ~= "" then
|
||
local ok, fm = pcall(jsonc.parse, api.base64Decode(node.finalmask))
|
||
if ok and type(fm) == "table" then
|
||
finalmask = fm
|
||
end
|
||
end
|
||
return api.cleanEmptyTables(finalmask)
|
||
end)()
|
||
} or nil,
|
||
settings = {
|
||
address = (node.protocol == "wireguard") and node.wireguard_local_address or node.address,
|
||
port = tonumber(node.port),
|
||
id = (node.protocol == "vmess" or node.protocol == "vless") and node.uuid or nil,
|
||
encryption = (node.protocol == "vless") and ((node.encryption and node.encryption ~= "") and node.encryption or "none") or nil,
|
||
flow = (node.protocol == "vless" and (node.tls == "1" or (node.encryption and node.encryption ~= "" and node.encryption ~= "none"))
|
||
and node.flow and node.flow ~= "") and node.flow or nil,
|
||
security = (node.protocol == "vmess") and node.security or nil,
|
||
user = (node.protocol == "socks" or node.protocol == "http") and node.username or nil,
|
||
pass = (node.protocol == "socks" or node.protocol == "http") and node.password or nil,
|
||
password = (node.protocol == "shadowsocks" or node.protocol == "trojan") and node.password or nil,
|
||
method = (node.protocol == "shadowsocks") and ((node.method == "chacha20-ietf-poly1305" and "chacha20-poly1305") or
|
||
(node.method == "xchacha20-ietf-poly1305" and "xchacha20-poly1305") or (node.method ~= "" and node.method) or nil) or nil,
|
||
secretKey = (node.protocol == "wireguard") and node.wireguard_secret_key or nil,
|
||
peers = (node.protocol == "wireguard") and {
|
||
{
|
||
publicKey = node.wireguard_public_key,
|
||
endpoint = node.address .. ":" .. node.port,
|
||
preSharedKey = node.wireguard_preSharedKey,
|
||
keepAlive = node.wireguard_keepAlive and tonumber(node.wireguard_keepAlive) or nil
|
||
}
|
||
} or nil,
|
||
mtu = (node.protocol == "wireguard" and node.wireguard_mtu) and tonumber(node.wireguard_mtu) or nil,
|
||
reserved = (node.protocol == "wireguard" and node.wireguard_reserved) and node.wireguard_reserved or nil,
|
||
version = (node.protocol == "hysteria") and 2 or nil,
|
||
level = 0
|
||
}
|
||
}
|
||
|
||
if node.protocol == "hysteria" and node.hysteria2_realms then
|
||
local realm = api.parse_realm_uri(node.hysteria2_realm_url)
|
||
if realm then
|
||
result.settings.address = realm.address
|
||
result.settings.port = realm.port
|
||
end
|
||
end
|
||
|
||
if node.protocol == "wireguard" then
|
||
result.settings.noKernelTun = true
|
||
if node.finalmask and node.finalmask ~= "" then
|
||
local ok, fm = pcall(jsonc.parse, api.base64Decode(node.finalmask))
|
||
if ok and type(fm) == "table" then
|
||
result.streamSettings = result.streamSettings or {}
|
||
result.streamSettings.finalmask = fm
|
||
end
|
||
end
|
||
end
|
||
|
||
local alpn = {}
|
||
if node.alpn and node.alpn ~= "default" then
|
||
string.gsub(node.alpn, '[^' .. "," .. ']+', function(w)
|
||
table.insert(alpn, w)
|
||
end)
|
||
end
|
||
if alpn and #alpn > 0 then
|
||
if result.streamSettings.tlsSettings then
|
||
result.streamSettings.tlsSettings.alpn = alpn
|
||
end
|
||
end
|
||
|
||
if api.datatypes.hostname(node.address) and node.domain_resolver and (node.domain_resolver_dns or node.domain_resolver_dns_https) then
|
||
local dns_proto = node.domain_resolver
|
||
local config_address
|
||
local config_port
|
||
if dns_proto == "https" then
|
||
local _a = api.parseDoH(node.domain_resolver_dns_https)
|
||
if _a then
|
||
config_address = _a.url
|
||
config_port = _a.port or 443
|
||
if _a.hostname and api.datatypes.hostname(_a.hostname) then
|
||
GLOBAL.DNS_HOSTNAME[_a.hostname] = true
|
||
end
|
||
end
|
||
else
|
||
local server_address = node.domain_resolver_dns
|
||
config_port = 53
|
||
local split = api.split(server_address, ":")
|
||
if #split > 1 then
|
||
server_address = split[1]
|
||
config_port = tonumber(split[#split])
|
||
end
|
||
config_address = server_address
|
||
if dns_proto == "tcp" then
|
||
config_address = dns_proto .. "://" .. server_address .. ":" .. config_port
|
||
end
|
||
end
|
||
local dns_key = dns_proto .. "|" .. config_address .. "|" .. tostring(config_port)
|
||
if not GLOBAL.DNS_SERVER[dns_key] then
|
||
GLOBAL.DNS_SERVER[dns_key] = {
|
||
tag = "dns-node-" .. api.gen_short_uuid(),
|
||
-- queryStrategy = node.domain_strategy or "UseIP",
|
||
address = config_address,
|
||
port = config_port,
|
||
finalQuery = true,
|
||
disableCache = false,
|
||
serveStale = true,
|
||
domains = {}
|
||
}
|
||
end
|
||
local exists
|
||
local domain = "full:" .. node.address
|
||
for _, d in ipairs(GLOBAL.DNS_SERVER[dns_key].domains) do
|
||
if d == domain then exists = true; break end
|
||
end
|
||
if not exists then table.insert(GLOBAL.DNS_SERVER[dns_key].domains, domain) end
|
||
GLOBAL.VPS_EXCLUDE[node.address] = true
|
||
end
|
||
end
|
||
return result
|
||
end
|
||
|
||
function gen_config_server(node)
|
||
local settings = nil
|
||
local routing = nil
|
||
local outbounds = {
|
||
{ protocol = "freedom", tag = "direct", settings = { finalRules = {{ action = "allow" }}}}, { protocol = "blackhole", tag = "blocked" }
|
||
}
|
||
|
||
if node.protocol == "vmess" or node.protocol == "vless" then
|
||
if node.uuid then
|
||
local users = {}
|
||
for i = 1, #node.uuid do
|
||
users[i] = {
|
||
id = node.uuid[i],
|
||
flow = (node.protocol == "vless"
|
||
and (node.tls == "1" or (node.decryption and node.decryption ~= "" and node.decryption ~= "none"))
|
||
and node.flow and node.flow ~= "") and node.flow or nil
|
||
}
|
||
end
|
||
settings = {
|
||
users = users,
|
||
decryption = (node.protocol == "vless") and ((node.decryption and node.decryption ~= "") and node.decryption or "none") or nil
|
||
}
|
||
end
|
||
elseif node.protocol == "socks" then
|
||
settings = {
|
||
udp = ("1" == node.udp_forward) and true or false,
|
||
auth = ("1" == node.auth) and "password" or "noauth",
|
||
users = ("1" == node.auth) and {
|
||
{
|
||
user = node.username,
|
||
pass = node.password
|
||
}
|
||
} or nil
|
||
}
|
||
elseif node.protocol == "http" then
|
||
settings = {
|
||
allowTransparent = false,
|
||
users = ("1" == node.auth) and {
|
||
{
|
||
user = node.username,
|
||
pass = node.password
|
||
}
|
||
} or nil
|
||
}
|
||
node.transport = "tcp"
|
||
node.tcp_guise = "none"
|
||
elseif node.protocol == "shadowsocks" then
|
||
settings = {
|
||
method = node.method,
|
||
password = node.password,
|
||
network = node.ss_network or "TCP,UDP"
|
||
}
|
||
elseif node.protocol == "trojan" then
|
||
if node.uuid then
|
||
local users = {}
|
||
for i = 1, #node.uuid do
|
||
users[i] = {
|
||
password = node.uuid[i],
|
||
}
|
||
end
|
||
settings = {
|
||
users = users
|
||
}
|
||
end
|
||
elseif node.protocol == "hysteria2" then
|
||
settings = {
|
||
version = 2,
|
||
users = node.hysteria2_auth_password and {
|
||
{ auth = node.hysteria2_auth_password }
|
||
}
|
||
}
|
||
elseif node.protocol == "dokodemo-door" then
|
||
settings = {
|
||
network = node.d_protocol,
|
||
address = node.d_address,
|
||
port = tonumber(node.d_port)
|
||
}
|
||
end
|
||
|
||
if node.fallback and node.fallback == "1" then
|
||
local fallbacks = {}
|
||
for i = 1, #node.fallback_list do
|
||
local fallbackStr = node.fallback_list[i]
|
||
if fallbackStr then
|
||
local tmp = {}
|
||
string.gsub(fallbackStr, '[^,]+', function(w)
|
||
table.insert(tmp, w)
|
||
end)
|
||
local dest = tmp[1] or ""
|
||
local path = tmp[2]
|
||
local xver = tonumber(tmp[3])
|
||
if not dest:find("%.") then
|
||
dest = tonumber(dest)
|
||
end
|
||
fallbacks[i] = {
|
||
path = path,
|
||
dest = dest,
|
||
xver = xver
|
||
}
|
||
end
|
||
end
|
||
settings.fallbacks = fallbacks
|
||
end
|
||
|
||
routing = {
|
||
domainStrategy = "IPOnDemand",
|
||
rules = {
|
||
{
|
||
ip = {"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"},
|
||
outboundTag = (node.accept_lan == nil or node.accept_lan == "0") and "blocked" or "direct"
|
||
}
|
||
}
|
||
}
|
||
|
||
if node.outbound_node then
|
||
local outbound = nil
|
||
if node.outbound_node == "_iface" and node.outbound_node_iface then
|
||
outbound = {
|
||
protocol = "freedom",
|
||
tag = "outbound",
|
||
streamSettings = {
|
||
sockopt = {
|
||
mark = 255,
|
||
interface = node.outbound_node_iface
|
||
}
|
||
},
|
||
settings = {
|
||
finalRules = {{ action = "allow" }}
|
||
}
|
||
}
|
||
sys.call(string.format("mkdir -p %s && touch %s/%s", api.TMP_IFACE_PATH, api.TMP_IFACE_PATH, node.outbound_node_iface))
|
||
else
|
||
local outbound_node_t = uci:get_all("passwall", node.outbound_node)
|
||
if node.outbound_node == "_socks" or node.outbound_node == "_http" then
|
||
outbound_node_t = {
|
||
type = node.type,
|
||
protocol = node.outbound_node:gsub("_", ""),
|
||
transport = "tcp",
|
||
address = node.outbound_node_address,
|
||
port = node.outbound_node_port,
|
||
username = (node.outbound_node_username and node.outbound_node_username ~= "") and node.outbound_node_username or nil,
|
||
password = (node.outbound_node_password and node.outbound_node_password ~= "") and node.outbound_node_password or nil,
|
||
}
|
||
end
|
||
outbound = require("luci.passwall.util_xray").gen_outbound(nil, outbound_node_t, "outbound")
|
||
end
|
||
if outbound then
|
||
table.insert(outbounds, 1, outbound)
|
||
end
|
||
end
|
||
|
||
if node.protocol == "hysteria2" then
|
||
node.protocol = "hysteria"
|
||
node.transport = "hysteria"
|
||
node.tls = "1"
|
||
end
|
||
|
||
local config = {
|
||
log = {
|
||
-- error = "/tmp/etc/passwall_server/log/" .. user[".name"] .. ".log",
|
||
loglevel = ("1" == node.log) and node.loglevel or "none"
|
||
},
|
||
-- 传入连接
|
||
inbounds = {
|
||
{
|
||
listen = (node.bind_local == "1") and "127.0.0.1" or nil,
|
||
port = tonumber(node.port),
|
||
protocol = node.protocol,
|
||
settings = settings,
|
||
streamSettings = {
|
||
[(api.compare_versions(xray_version, "<", "26.7.11")) and "network" or "method"] = node.transport, -- Todo: Remove version check and "network"
|
||
security = "none",
|
||
tlsSettings = ("1" == node.tls) and {
|
||
disableSystemRoot = false,
|
||
certificates = {
|
||
{
|
||
certificateFile = node.tls_certificateFile,
|
||
keyFile = node.tls_keyFile
|
||
}
|
||
},
|
||
echServerKeys = (node.ech == "1") and node.ech_key or nil
|
||
} or nil,
|
||
rawSettings = (node.transport == "raw" or node.transport == "tcp") and {
|
||
header = {
|
||
type = node.tcp_guise,
|
||
request = (node.tcp_guise == "http") and {
|
||
path = node.tcp_guise_http_path and (function()
|
||
local t, r = node.tcp_guise_http_path, {}
|
||
for _, v in ipairs(t) do
|
||
r[#r + 1] = (v == "" and "/" or v)
|
||
end
|
||
return r
|
||
end)() or {"/"},
|
||
headers = {
|
||
Host = node.tcp_guise_http_host or {}
|
||
}
|
||
} or nil
|
||
}
|
||
} or nil,
|
||
kcpSettings = (node.transport == "mkcp") and {
|
||
mtu = (node.mkcp_mtu and node.mkcp_mtu ~= "") and tonumber(node.mkcp_mtu) or 1350,
|
||
tti = 50,
|
||
uplinkCapacity = 12,
|
||
downlinkCapacity = 100,
|
||
CwndMultiplier = 1,
|
||
MaxSendingWindow = 2 * 1024 * 1024
|
||
} or nil,
|
||
wsSettings = (node.transport == "ws") and {
|
||
host = node.ws_host or nil,
|
||
path = node.ws_path
|
||
} or nil,
|
||
grpcSettings = (node.transport == "grpc" and node.grpc_serviceName) and {
|
||
serviceName = node.grpc_serviceName
|
||
} or nil,
|
||
httpupgradeSettings = (node.transport == "httpupgrade") and {
|
||
path = node.httpupgrade_path or "/",
|
||
host = node.httpupgrade_host
|
||
} or nil,
|
||
xhttpSettings = (node.transport == "xhttp") and {
|
||
path = node.xhttp_path or "/",
|
||
host = node.xhttp_host,
|
||
maxUploadSize = node.xhttp_maxuploadsize,
|
||
maxConcurrentUploads = node.xhttp_maxconcurrentuploads
|
||
} or nil,
|
||
hysteriaSettings = (node.transport == "hysteria") and {
|
||
version = 2
|
||
} or nil,
|
||
finalmask = (function()
|
||
local finalmask = {}
|
||
if node.transport == "mkcp" then
|
||
local map = {none = "none", srtp = "srtp", utp = "utp", ["wechat-video"] = "wechat",
|
||
dtls = "dtls", wireguard = "wireguard", dns = "dns"}
|
||
local udp = {}
|
||
if node.mkcp_guise and node.mkcp_guise ~= "none" then
|
||
local g = { type = "mkcp-legacy" }
|
||
g.settings = { header = map[node.mkcp_guise] }
|
||
if node.mkcp_guise == "dns" and node.mkcp_domain and node.mkcp_domain ~= "" then
|
||
g.settings.value = node.mkcp_domain
|
||
end
|
||
udp[#udp+1] = g
|
||
end
|
||
local s = { type = "mkcp-legacy" }
|
||
if node.mkcp_seed and node.mkcp_seed ~= "" then
|
||
s.settings = { value = node.mkcp_seed }
|
||
end
|
||
udp[#udp+1] = s
|
||
finalmask.udp = udp
|
||
elseif node.transport == "hysteria" then
|
||
local udp = {}
|
||
if node.hysteria2_obfs_type and node.hysteria2_obfs_type ~= "" then
|
||
local o = {
|
||
type = "salamander",
|
||
settings = node.hysteria2_obfs_password and {
|
||
password = node.hysteria2_obfs_password
|
||
} or nil
|
||
}
|
||
if node.hysteria2_obfs_type == "gecko" then
|
||
local min = tonumber(node.hysteria2_obfs_MinPacketSize) or 512
|
||
local max = tonumber(node.hysteria2_obfs_MaxPacketSize) or 1200
|
||
if min <= 0 or min > max or max > 2048 then
|
||
min = 512
|
||
max = 1200
|
||
end
|
||
o.settings.packetSize = min .. "-" .. max
|
||
end
|
||
udp[#udp+1] = o
|
||
end
|
||
if node.hysteria2_realms then
|
||
local realm = api.parse_realm_uri(node.hysteria2_realm_url)
|
||
local url, stun
|
||
if realm then
|
||
url = realm.scheme .. "://" .. realm.token .. "@" .. realm.server_url .. "/" .. realm.realm_id
|
||
stun = realm.stun_servers or node.hysteria2_realm_stun
|
||
end
|
||
local r = {
|
||
type = "realm",
|
||
settings = {
|
||
url = url,
|
||
stunServers = stun
|
||
}
|
||
}
|
||
udp[#udp+1] = r
|
||
end
|
||
finalmask.udp = udp
|
||
local ignore = tonumber(node.hysteria2_ignore_client_bandwidth) == 1
|
||
local up = (not ignore) and tonumber(node.hysteria2_up_mbps) or 0
|
||
local down = (not ignore) and tonumber(node.hysteria2_down_mbps) or 0
|
||
finalmask.quicParams = {
|
||
congestion = (up <= 0 and down <= 0) and "bbr" or "brutal",
|
||
brutalUp = up > 0 and (up .. "mbps") or nil,
|
||
brutalDown = down > 0 and (down .. "mbps") or nil
|
||
}
|
||
end
|
||
if node.finalmask and node.finalmask ~= "" then
|
||
local ok, fm = pcall(jsonc.parse, api.base64Decode(node.finalmask))
|
||
if ok and type(fm) == "table" then
|
||
finalmask = fm
|
||
end
|
||
end
|
||
return api.cleanEmptyTables(finalmask)
|
||
end)(),
|
||
sockopt = {
|
||
tcpFastOpen = (node.tcp_fast_open == "1") and true or nil,
|
||
acceptProxyProtocol = (node.acceptProxyProtocol and node.acceptProxyProtocol == "1") and true or false
|
||
}
|
||
}
|
||
}
|
||
},
|
||
-- 传出连接
|
||
outbounds = outbounds,
|
||
routing = routing,
|
||
version = {
|
||
min = xray_min_version
|
||
}
|
||
}
|
||
|
||
local alpn = {}
|
||
if node.alpn and node.alpn ~= "default" then
|
||
string.gsub(node.alpn, '[^' .. "," .. ']+', function(w)
|
||
table.insert(alpn, w)
|
||
end)
|
||
end
|
||
if alpn and #alpn > 0 then
|
||
if config.inbounds[1].streamSettings.tlsSettings then
|
||
config.inbounds[1].streamSettings.tlsSettings.alpn = alpn
|
||
end
|
||
end
|
||
|
||
if "1" == node.tls then
|
||
config.inbounds[1].streamSettings.security = "tls"
|
||
if "1" == node.reality then
|
||
config.inbounds[1].streamSettings.tlsSettings = nil
|
||
config.inbounds[1].streamSettings.security = "reality"
|
||
config.inbounds[1].streamSettings.realitySettings = {
|
||
show = false,
|
||
dest = node.reality_dest,
|
||
serverNames = node.reality_serverNames or {},
|
||
privateKey = node.reality_private_key,
|
||
shortIds = node.reality_shortId or "",
|
||
mldsa65Seed = (node.use_mldsa65Seed == "1") and node.reality_mldsa65Seed or nil,
|
||
minClientVer = "1.0.0"
|
||
} or nil
|
||
end
|
||
end
|
||
|
||
for index, value in ipairs(config.outbounds) do
|
||
for k, v in pairs(config.outbounds[index]) do
|
||
if k and k:find("_") == 1 then
|
||
config.outbounds[index][k] = nil
|
||
end
|
||
end
|
||
if value.protocol == "freedom" and api.compare_versions(xray_version, "<", "26.5.3") then -- Todo is to remove it
|
||
value.settings = nil
|
||
end
|
||
end
|
||
|
||
return config
|
||
end
|
||
|
||
function gen_config(var)
|
||
local flag = var["flag"]
|
||
local node_id = var["node"]
|
||
local server_host = var["server_host"]
|
||
local server_port = var["server_port"]
|
||
local tcp_proxy_way = var["tcp_proxy_way"] or "redirect"
|
||
local tcp_redir_port = var["tcp_redir_port"]
|
||
local udp_redir_port = var["udp_redir_port"]
|
||
local local_socks_address = var["local_socks_address"] or "0.0.0.0"
|
||
local local_socks_port = var["local_socks_port"]
|
||
local local_socks_username = var["local_socks_username"]
|
||
local local_socks_password = var["local_socks_password"]
|
||
local local_http_address = var["local_http_address"] or "0.0.0.0"
|
||
local local_http_port = var["local_http_port"]
|
||
local local_http_username = var["local_http_username"]
|
||
local local_http_password = var["local_http_password"]
|
||
local dns_listen_port = var["dns_listen_port"]
|
||
local dns_cache = var["dns_cache"]
|
||
local direct_dns_port = var["direct_dns_port"]
|
||
local direct_dns_udp_server = var["direct_dns_udp_server"]
|
||
local direct_dns_tcp_server = var["direct_dns_tcp_server"]
|
||
local direct_dns_query_strategy = var["direct_dns_query_strategy"]
|
||
local remote_dns_udp_server = var["remote_dns_udp_server"]
|
||
local remote_dns_udp_port = var["remote_dns_udp_port"]
|
||
local remote_dns_tcp_server = var["remote_dns_tcp_server"]
|
||
local remote_dns_tcp_port = var["remote_dns_tcp_port"]
|
||
local remote_dns_doh = var["remote_dns_doh"]
|
||
local remote_dns_client_ip = var["remote_dns_client_ip"]
|
||
local remote_dns_fake = var["remote_dns_fake"]
|
||
local remote_dns_query_strategy = var["remote_dns_query_strategy"]
|
||
local dns_socks_address = var["dns_socks_address"]
|
||
local dns_socks_port = var["dns_socks_port"]
|
||
local loglevel = var["loglevel"] or "warning"
|
||
local no_run = var["no_run"]
|
||
|
||
local dns_domain_rules = {}
|
||
local dns = nil
|
||
local fakedns = nil
|
||
local routing = nil
|
||
local observatory = nil
|
||
local burstObservatory = nil
|
||
local strategy = nil
|
||
local inbounds = {}
|
||
local outbounds = {}
|
||
local COMMON = {}
|
||
|
||
local xray_settings = uci:get_all(appname, "@global_xray[0]") or {}
|
||
|
||
if xray_settings.fragment == "1" then
|
||
local lengths, delays = {}, {}
|
||
api.trim(xray_settings.fragment_lengths):gsub("[^,]+", function(w)
|
||
w = w:gsub("%s+", "")
|
||
if w ~= "" then lengths[#lengths+1] = w end
|
||
end)
|
||
api.trim(xray_settings.fragment_delays):gsub("[^,]+", function(w)
|
||
w = w:gsub("%s+", "")
|
||
if w ~= "" then delays[#delays+1] = w end
|
||
end)
|
||
fragment_table = {
|
||
type = "fragment",
|
||
settings = {
|
||
packets = xray_settings.fragment_packets or "tlshello",
|
||
lengths = #lengths > 0 and lengths or {"3-5","6-8","10-20"},
|
||
delays = #delays > 0 and delays or {"10-20"},
|
||
maxSplit = xray_settings.fragment_maxSplit or "3-6"
|
||
}
|
||
}
|
||
end
|
||
|
||
if xray_settings.noise == "1" then
|
||
local noises = {}
|
||
uci:foreach(appname, "xray_noise_packets", function(n)
|
||
if n.enabled == "1" then
|
||
local noise = {
|
||
rand = (n.type == "rand" and n.packet) and (n.packet:find("-", 1, true) and n.packet or tonumber(n.packet)) or nil,
|
||
type = (n.type ~= "rand") and n.type or nil,
|
||
packet = (n.type ~= "rand") and n.packet or nil,
|
||
delay = n.delay and (n.delay:find("-", 1, true) and n.delay or tonumber(n.delay)) or nil
|
||
}
|
||
table.insert(noises, noise)
|
||
end
|
||
end)
|
||
noise_table = #noises > 0 and {
|
||
type = "noise",
|
||
settings = { reset = 0, noise = noises }
|
||
} or nil
|
||
end
|
||
|
||
if node_id then
|
||
local node = uci:get_all(appname, node_id)
|
||
local balancers = {}
|
||
local rules = {}
|
||
if node then
|
||
if server_host and server_port then
|
||
node.address = server_host
|
||
node.port = server_port
|
||
end
|
||
end
|
||
if local_socks_port then
|
||
local inbound = {
|
||
tag = "socks-in",
|
||
listen = local_socks_address,
|
||
port = tonumber(local_socks_port),
|
||
protocol = "socks",
|
||
settings = {auth = "noauth", udp = true},
|
||
sniffing = {
|
||
enabled = (xray_settings.sniffing_override_dest == "1") or (node and node.protocol == "_shunt") or false
|
||
}
|
||
}
|
||
if inbound.sniffing.enabled == true then
|
||
inbound.sniffing.destOverride = {"http", "tls", "quic"}
|
||
inbound.sniffing.routeOnly = xray_settings.sniffing_override_dest ~= "1" or nil
|
||
inbound.sniffing.domainsExcluded = xray_settings.sniffing_override_dest == "1" and get_domain_excluded() or nil
|
||
end
|
||
if local_socks_username and local_socks_password and local_socks_username ~= "" and local_socks_password ~= "" then
|
||
inbound.settings.auth = "password"
|
||
inbound.settings.users = {
|
||
{
|
||
user = local_socks_username,
|
||
pass = local_socks_password
|
||
}
|
||
}
|
||
end
|
||
table.insert(inbounds, inbound)
|
||
end
|
||
if local_http_port then
|
||
local inbound = {
|
||
listen = local_http_address,
|
||
port = tonumber(local_http_port),
|
||
protocol = "http",
|
||
settings = {allowTransparent = false}
|
||
}
|
||
if local_http_username and local_http_password and local_http_username ~= "" and local_http_password ~= "" then
|
||
inbound.settings.users = {
|
||
{
|
||
user = local_http_username,
|
||
pass = local_http_password
|
||
}
|
||
}
|
||
end
|
||
table.insert(inbounds, inbound)
|
||
end
|
||
|
||
|
||
function gen_socks_config_node(node_id, socks_id, remarks)
|
||
if node_id then
|
||
socks_id = node_id:sub(1 + #"Socks_")
|
||
end
|
||
local result
|
||
local socks_node = uci:get_all(appname, socks_id) or nil
|
||
if socks_node then
|
||
if not remarks then
|
||
remarks = socks_node.port
|
||
end
|
||
result = {
|
||
[".name"] = "Socksid_" .. socks_id,
|
||
remarks = remarks,
|
||
type = "Xray",
|
||
protocol = "socks",
|
||
address = "127.0.0.1",
|
||
port = socks_node.port,
|
||
transport = "tcp",
|
||
stream_security = "none"
|
||
}
|
||
end
|
||
return result
|
||
end
|
||
|
||
function get_node_by_id(node_id)
|
||
if not node_id or node_id == "" or node_id == "nil" then return nil end
|
||
if node_id:find("Socks_") then
|
||
return gen_socks_config_node(node_id)
|
||
else
|
||
return uci:get_all(appname, node_id)
|
||
end
|
||
end
|
||
|
||
function gen_loopback(outbound_tag, loopback_dst)
|
||
if not outbound_tag or outbound_tag == "" then return nil end
|
||
local inbound_tag = loopback_dst and "lo-to-" .. loopback_dst or outbound_tag .. "-lo"
|
||
local loopback_outbound = {
|
||
protocol = "loopback",
|
||
tag = outbound_tag,
|
||
settings = { inboundTag = inbound_tag }
|
||
}
|
||
local insert_index = #outbounds + 1
|
||
if outbound_tag == "default" then
|
||
insert_index = 1
|
||
end
|
||
table.insert(outbounds, insert_index, loopback_outbound)
|
||
return loopback_outbound
|
||
end
|
||
|
||
function gen_balancer(_node, loopback_tag)
|
||
local balancer_id = _node[".name"]
|
||
local balancer_tag = "balancer-" .. balancer_id
|
||
local loopback_dst = balancer_id -- route destination for the loopback outbound
|
||
if not loopback_tag or loopback_tag == "" then loopback_tag = balancer_id end
|
||
-- existing balancer
|
||
for _, v in ipairs(balancers) do
|
||
if v.tag == balancer_tag then
|
||
local loopback_outbound = gen_loopback(loopback_tag, loopback_dst)
|
||
return balancer_tag, loopback_outbound
|
||
end
|
||
end
|
||
-- new balancer
|
||
local blc_nodes
|
||
if _node.node_add_mode and _node.node_add_mode == "batch" then
|
||
blc_nodes = api.get_batch_nodes(_node)
|
||
else
|
||
blc_nodes = _node.balancing_node
|
||
end
|
||
|
||
api.log(" - 加载 Xray 负载均衡 节点【" .. (_node.remarks or "") .. "】,子节点数量:" .. #(blc_nodes or {}))
|
||
|
||
local valid_nodes = {}
|
||
for i = 1, #(blc_nodes or {}) do
|
||
local blc_node_id = blc_nodes[i]
|
||
local blc_node_tag = "blc-" .. blc_node_id
|
||
local is_new_blc_node = true
|
||
for _, outbound in ipairs(outbounds) do
|
||
if string.sub(outbound.tag, 1, #blc_node_tag) == blc_node_tag then
|
||
is_new_blc_node = false
|
||
valid_nodes[#valid_nodes + 1] = outbound.tag
|
||
break
|
||
end
|
||
end
|
||
if is_new_blc_node then
|
||
local outboundTag = gen_outbound_get_tag(flag, blc_node_id, blc_node_tag, { fragment = xray_settings.fragment == "1" or nil, noise = xray_settings.noise == "1" or nil, run_socks_instance = not no_run })
|
||
if outboundTag then
|
||
valid_nodes[#valid_nodes + 1] = outboundTag
|
||
end
|
||
end
|
||
-- Check if balancing node duplicates fallback node
|
||
if _node.fallback_node == blc_node_id then
|
||
_node.fallback_node = nil
|
||
end
|
||
end
|
||
if #valid_nodes == 0 then return nil end
|
||
|
||
-- fallback node
|
||
local fallback_node_id = _node.fallback_node
|
||
fallback_node_id = (fallback_node_id and fallback_node_id ~= "") and fallback_node_id or nil
|
||
local fallback_node_tag = (fallback_node_id == "_direct") and "direct" or "blackhole"
|
||
if fallback_node_id and fallback_node_id ~= "_direct" then
|
||
local is_new_node = true
|
||
for _, outbound in ipairs(outbounds) do
|
||
if string.sub(outbound.tag, 1, #fallback_node_id) == fallback_node_id then
|
||
is_new_node = false
|
||
fallback_node_tag = outbound.tag
|
||
break
|
||
end
|
||
end
|
||
if is_new_node then
|
||
local fallback_node = get_node_by_id(fallback_node_id)
|
||
if fallback_node then
|
||
if fallback_node.protocol ~= "_balancing" then
|
||
local outboundTag = gen_outbound_get_tag(flag, fallback_node, fallback_node_id, { fragment = xray_settings.fragment == "1" or nil, noise = xray_settings.noise == "1" or nil, run_socks_instance = not no_run })
|
||
if outboundTag then
|
||
fallback_node_tag = outboundTag
|
||
end
|
||
else
|
||
if gen_balancer(fallback_node) then
|
||
fallback_node_tag = fallback_node_id
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
if _node.balancingStrategy == "leastLoad" then
|
||
strategy = {
|
||
type = _node.balancingStrategy,
|
||
settings = {
|
||
expected = _node.expected and tonumber(_node.expected) and tonumber(_node.expected) or 2,
|
||
maxRTT = "1s",
|
||
tolerance = (function(t)
|
||
t = tonumber(t) or 0
|
||
if t < 1 then return nil end
|
||
if t > 100 then t = 100 end
|
||
return t / 100
|
||
end)(_node.tolerance)
|
||
}
|
||
}
|
||
else
|
||
strategy = { type = _node.balancingStrategy or "random" }
|
||
end
|
||
table.insert(balancers, {
|
||
tag = balancer_tag,
|
||
selector = api.clone(valid_nodes),
|
||
fallbackTag = fallback_node_tag,
|
||
strategy = strategy
|
||
})
|
||
if _node.balancingStrategy == "leastPing" or _node.balancingStrategy == "leastLoad" or fallback_node_tag then
|
||
local t = api.format_go_time(_node.probeInterval)
|
||
if t == "0s" then
|
||
t = "60s"
|
||
elseif not t:find("[hm]") and tonumber(t:match("%d+")) < 10 then
|
||
t = "10s"
|
||
end
|
||
if _node.balancingStrategy == "leastLoad" then
|
||
burstObservatory = burstObservatory or {
|
||
subjectSelector = { "blc-" },
|
||
pingConfig = {
|
||
destination = _node.useCustomProbeUrl and _node.probeUrl or nil,
|
||
interval = t,
|
||
sampling = 3,
|
||
timeout = "5s"
|
||
}
|
||
}
|
||
else
|
||
observatory = observatory or {
|
||
subjectSelector = { "blc-" },
|
||
probeUrl = _node.useCustomProbeUrl and _node.probeUrl or nil,
|
||
probeInterval = t,
|
||
enableConcurrency = true
|
||
}
|
||
end
|
||
end
|
||
local loopback_outbound = gen_loopback(loopback_tag, loopback_dst)
|
||
local inbound_tag = loopback_outbound.settings.inboundTag
|
||
table.insert(rules, { inboundTag = { inbound_tag }, balancerTag = balancer_tag })
|
||
return balancer_tag, loopback_outbound
|
||
end
|
||
|
||
function set_outbound_detour(node, outbound, outbounds_table)
|
||
if not node or not outbound or not outbounds_table then return nil end
|
||
local default_outTag = outbound.tag
|
||
local last_insert_outbound
|
||
|
||
if node.chain_proxy == "1" and node.preproxy_node then
|
||
if outbound["_flag_proxy_tag"] then
|
||
--Ignore
|
||
else
|
||
local preproxy_node = get_node_by_id(node.preproxy_node)
|
||
if preproxy_node then
|
||
local preproxy_outbound, exist
|
||
if preproxy_node.protocol == "_balancing" then
|
||
local balancer_tag, loopback_outbound = gen_balancer(preproxy_node)
|
||
if loopback_outbound then
|
||
preproxy_outbound = loopback_outbound
|
||
exist = true
|
||
end
|
||
else
|
||
preproxy_outbound = gen_outbound(node[".name"], preproxy_node)
|
||
end
|
||
if preproxy_outbound then
|
||
outbound.tag = preproxy_outbound.tag .. " -> " .. outbound.tag
|
||
outbound.proxySettings = {
|
||
tag = preproxy_outbound.tag,
|
||
transportLayer = true
|
||
}
|
||
if not exist then
|
||
last_insert_outbound = preproxy_outbound
|
||
end
|
||
default_outTag = outbound.tag
|
||
end
|
||
end
|
||
end
|
||
end
|
||
if node.chain_proxy == "2" and node.to_node then
|
||
local to_node = get_node_by_id(node.to_node)
|
||
if to_node then
|
||
-- Landing Node not support use special node.
|
||
if to_node.protocol and to_node.protocol:find("^_") then
|
||
to_node = nil
|
||
end
|
||
end
|
||
if to_node then
|
||
local to_outbound
|
||
if to_node.type ~= "Xray" then
|
||
local in_tag = "inbound_" .. to_node[".name"] .. "_" .. tostring(outbound.tag)
|
||
local new_port = api.get_new_port()
|
||
table.insert(inbounds, {
|
||
tag = in_tag,
|
||
listen = "127.0.0.1",
|
||
port = new_port,
|
||
protocol = "dokodemo-door",
|
||
settings = {network = "tcp,udp", address = to_node.address, port = tonumber(to_node.port)}
|
||
})
|
||
if to_node.tls_serverName == nil then
|
||
to_node.tls_serverName = to_node.address
|
||
end
|
||
to_node.address = "127.0.0.1"
|
||
to_node.port = new_port
|
||
table.insert(rules, 1, {
|
||
inboundTag = {in_tag},
|
||
outboundTag = outbound.tag
|
||
})
|
||
to_outbound = gen_outbound(node[".name"], to_node, to_node[".name"], {
|
||
tag = to_node[".name"],
|
||
run_socks_instance = not no_run
|
||
})
|
||
else
|
||
to_outbound = gen_outbound(node[".name"], to_node)
|
||
end
|
||
if to_outbound then
|
||
to_outbound.tag = outbound.tag .. " -> " .. to_outbound.tag
|
||
if to_node.type == "Xray" then
|
||
to_outbound.proxySettings = {
|
||
tag = outbound.tag,
|
||
transportLayer = true
|
||
}
|
||
end
|
||
table.insert(outbounds_table, to_outbound)
|
||
default_outTag = to_outbound.tag
|
||
end
|
||
end
|
||
end
|
||
if node.chain_proxy == "3" and node.outbound_iface then
|
||
if outbound.streamSettings and outbound.streamSettings.sockopt then
|
||
outbound.streamSettings.sockopt.interface = node.outbound_iface
|
||
end
|
||
end
|
||
return default_outTag, last_insert_outbound
|
||
end
|
||
|
||
function gen_outbound_get_tag(flag, node_id, tag, proxy_table)
|
||
if not node_id or node_id == "" or node_id == "nil" then return nil end
|
||
local node
|
||
if type(node_id) == "string" then
|
||
node = get_node_by_id(node_id)
|
||
elseif type(node_id) == "table" then
|
||
node = node_id
|
||
end
|
||
if not tag then tag = node[".name"] end
|
||
if node then
|
||
if proxy_table.chain_proxy == "1" or proxy_table.chain_proxy == "2" then
|
||
node.chain_proxy = proxy_table.chain_proxy
|
||
node.preproxy_node = proxy_table.chain_proxy == "1" and proxy_table.preproxy_node
|
||
node.to_node = proxy_table.chain_proxy == "2" and proxy_table.to_node
|
||
proxy_table.chain_proxy = nil
|
||
proxy_table.preproxy_node = nil
|
||
proxy_table.to_node = nil
|
||
end
|
||
local outbound, has_add_outbound
|
||
for _, _outbound in ipairs(outbounds) do
|
||
-- Avoid generating duplicate nested processes
|
||
if _outbound["_flag_proxy_tag"] and _outbound["_flag_proxy_tag"]:find("socks <- " .. node[".name"], 1, true) then
|
||
outbound = api.clone(_outbound)
|
||
outbound.tag = tag
|
||
break
|
||
end
|
||
end
|
||
if node.protocol == "_balancing" then
|
||
local balancer_tag, loopback_outbound = gen_balancer(node, tag)
|
||
if loopback_outbound then
|
||
outbound = loopback_outbound
|
||
node[".name"] = outbound.tag
|
||
has_add_outbound = true
|
||
end
|
||
elseif node.protocol == "_iface" then
|
||
if node.iface then
|
||
outbound = {
|
||
tag = tag,
|
||
protocol = "freedom",
|
||
streamSettings = {
|
||
sockopt = {
|
||
mark = 255,
|
||
interface = node.iface
|
||
}
|
||
},
|
||
settings = (api.compare_versions(xray_version, ">", "26.4.25")) and { -- Todo: Remove version check
|
||
finalRules = {{ action = "allow" }}
|
||
} or nil
|
||
}
|
||
sys.call(string.format("mkdir -p %s && touch %s/%s", api.TMP_IFACE_PATH, api.TMP_IFACE_PATH, node.iface))
|
||
end
|
||
end
|
||
if not outbound then
|
||
outbound = gen_outbound(flag, node, tag, proxy_table)
|
||
end
|
||
if outbound then
|
||
local default_outbound_tag, last_insert_outbound = set_outbound_detour(node, outbound, outbounds)
|
||
if not has_add_outbound then
|
||
local insert_index = #outbounds + 1
|
||
if tag == "default" then
|
||
insert_index = 1
|
||
end
|
||
table.insert(outbounds, insert_index, outbound)
|
||
end
|
||
if last_insert_outbound then
|
||
table.insert(outbounds, last_insert_outbound)
|
||
end
|
||
return default_outbound_tag
|
||
end
|
||
end
|
||
end
|
||
|
||
if node and node.protocol == "_shunt" then
|
||
inner_fakedns = node.fakedns or "0"
|
||
|
||
local function gen_shunt_node(rule_name, _node_id)
|
||
if not rule_name then return nil end
|
||
if not _node_id then _node_id = node[rule_name] end
|
||
if _node_id == "_direct" then
|
||
return "direct"
|
||
elseif _node_id == "_blackhole" then
|
||
return "blackhole"
|
||
elseif _node_id == "_default" and rule_name ~= "default" then
|
||
return "default"
|
||
elseif _node_id then
|
||
local proxy_table = {
|
||
fragment = xray_settings.fragment == "1",
|
||
noise = xray_settings.noise == "1",
|
||
run_socks_instance = not no_run,
|
||
}
|
||
local preproxy_node_id = node[rule_name .. "_proxy_tag"]
|
||
if preproxy_node_id == _node_id then preproxy_node_id = nil end
|
||
if preproxy_node_id then
|
||
proxy_table.chain_proxy = "2"
|
||
proxy_table.to_node = _node_id
|
||
return gen_outbound_get_tag(flag, preproxy_node_id, rule_name, proxy_table)
|
||
else
|
||
return gen_outbound_get_tag(flag, _node_id, rule_name, proxy_table)
|
||
end
|
||
end
|
||
return nil
|
||
end
|
||
|
||
--default_node
|
||
local default_node_id = node.default_node or "_direct"
|
||
local default_outboundTag = gen_shunt_node("default", default_node_id)
|
||
COMMON.default_outbound_tag = default_outboundTag
|
||
|
||
if inner_fakedns == "1" and node["default_fakedns"] == "1" then
|
||
remote_dns_fake = true
|
||
end
|
||
|
||
--shunt rule
|
||
uci:foreach(appname, "shunt_rules", function(e)
|
||
local outbound_tag = gen_shunt_node(e[".name"])
|
||
if outbound_tag and e.remarks then
|
||
if outbound_tag == "default" then
|
||
outbound_tag = default_outboundTag
|
||
end
|
||
local protocols = nil
|
||
if e["protocol"] and e["protocol"] ~= "" then
|
||
protocols = {}
|
||
string.gsub(e["protocol"], '[^' .. " " .. ']+', function(w)
|
||
table.insert(protocols, w)
|
||
end)
|
||
end
|
||
local inbound_tag = nil
|
||
if e["inbound"] and e["inbound"] ~= "" then
|
||
inbound_tag = {}
|
||
if e["inbound"]:find("tproxy") then
|
||
if tcp_redir_port then
|
||
table.insert(inbound_tag, "tcp_redir")
|
||
end
|
||
if udp_redir_port then
|
||
table.insert(inbound_tag, "udp_redir")
|
||
end
|
||
end
|
||
if e["inbound"]:find("socks") then
|
||
if local_socks_port then
|
||
table.insert(inbound_tag, "socks-in")
|
||
end
|
||
end
|
||
end
|
||
local domains = nil
|
||
if e.domain_list then
|
||
local domain_table = {
|
||
shunt_rule_name = e[".name"],
|
||
outboundTag = outbound_tag,
|
||
domain = {},
|
||
fakedns = nil,
|
||
}
|
||
domains = {}
|
||
string.gsub(e.domain_list, '[^' .. "\r\n" .. ']+', function(w)
|
||
w = api.trim(w)
|
||
if w == "" or w:find("#") == 1 then return end
|
||
if w:find("rule-set:", 1, true) == 1 or w:find("rs:") == 1 then return end
|
||
table.insert(domains, w)
|
||
table.insert(domain_table.domain, w)
|
||
end)
|
||
if inner_fakedns == "1" and node[e[".name"] .. "_fakedns"] == "1" and #domains > 0 then
|
||
domain_table.fakedns = true
|
||
end
|
||
if #domains == 0 then domains = nil end
|
||
if outbound_tag and domains then
|
||
table.insert(dns_domain_rules, api.clone(domain_table))
|
||
end
|
||
end
|
||
local ip = nil
|
||
if e.ip_list then
|
||
ip = {}
|
||
string.gsub(e.ip_list, '[^' .. "\r\n" .. ']+', function(w)
|
||
w = api.trim(w)
|
||
if w == "" or w:find("#") == 1 then return end
|
||
if w:find("rule-set:", 1, true) == 1 or w:find("rs:") == 1 then return end
|
||
table.insert(ip, w)
|
||
end)
|
||
if #ip == 0 then ip = nil end
|
||
end
|
||
local source = nil
|
||
if e.source then
|
||
source = {}
|
||
string.gsub(e.source, '[^' .. " " .. ']+', function(w)
|
||
table.insert(source, w)
|
||
end)
|
||
end
|
||
local rule = {
|
||
ruleTag = e.remarks,
|
||
inboundTag = inbound_tag,
|
||
outboundTag = outbound_tag,
|
||
network = e["network"] or "tcp,udp",
|
||
source = source,
|
||
--sourcePort = e["sourcePort"] ~= "" and e["sourcePort"] or nil,
|
||
port = e["port"] ~= "" and e["port"] or nil,
|
||
protocol = protocols
|
||
}
|
||
if domains then
|
||
local _rule = api.clone(rule)
|
||
_rule.ruleTag = _rule.ruleTag .. " Domains"
|
||
_rule.domains = domains
|
||
table.insert(rules, _rule)
|
||
end
|
||
if ip then
|
||
local _rule = api.clone(rule)
|
||
_rule.ruleTag = _rule.ruleTag .. " IP"
|
||
_rule.ip = ip
|
||
table.insert(rules, _rule)
|
||
end
|
||
if not domains and not ip and protocols then
|
||
table.insert(rules, rule)
|
||
end
|
||
end
|
||
end)
|
||
|
||
table.insert(rules, {
|
||
outboundTag = "direct",
|
||
ip = { "geoip:private" }
|
||
})
|
||
|
||
if default_outboundTag then
|
||
local rule = {
|
||
_flag = "default",
|
||
outboundTag = default_outboundTag
|
||
}
|
||
if node.domainStrategy == "IPIfNonMatch" then
|
||
rule.port = "1-65535"
|
||
else
|
||
rule.network = "tcp,udp"
|
||
end
|
||
table.insert(rules, rule)
|
||
end
|
||
|
||
routing = {
|
||
domainStrategy = node.domainStrategy or "AsIs",
|
||
domainMatcher = node.domainMatcher or "hybrid",
|
||
balancers = #balancers > 0 and balancers or nil,
|
||
rules = rules
|
||
}
|
||
else
|
||
COMMON.default_outbound_tag = gen_outbound_get_tag(flag, node or node_id, nil, {
|
||
fragment = xray_settings.fragment == "1" or nil,
|
||
noise = xray_settings.noise == "1" or nil,
|
||
run_socks_instance = not no_run
|
||
})
|
||
if COMMON.default_outbound_tag then
|
||
routing = {
|
||
domainStrategy = "AsIs",
|
||
domainMatcher = "hybrid",
|
||
balancers = #balancers > 0 and balancers or nil,
|
||
rules = rules
|
||
}
|
||
table.insert(routing.rules, {
|
||
ruleTag = "default",
|
||
network = "tcp,udp",
|
||
outboundTag = COMMON.default_outbound_tag
|
||
})
|
||
end
|
||
end
|
||
|
||
if tcp_redir_port or udp_redir_port then
|
||
local inbound = {
|
||
protocol = "dokodemo-door",
|
||
settings = {network = "tcp,udp", followRedirect = true},
|
||
streamSettings = {sockopt = {tproxy = "tproxy"}},
|
||
sniffing = {
|
||
enabled = (xray_settings.sniffing_override_dest == "1") or (node and node.protocol == "_shunt") or false
|
||
}
|
||
}
|
||
if inbound.sniffing.enabled == true then
|
||
inbound.sniffing.destOverride = {"http", "tls", "quic"}
|
||
inbound.sniffing.metadataOnly = false
|
||
inbound.sniffing.routeOnly = xray_settings.sniffing_override_dest ~= "1" or nil
|
||
inbound.sniffing.domainsExcluded = xray_settings.sniffing_override_dest == "1" and get_domain_excluded() or nil
|
||
end
|
||
if remote_dns_fake or inner_fakedns == "1" then
|
||
inbound.sniffing.enabled = true
|
||
if not inbound.sniffing.destOverride then
|
||
inbound.sniffing.destOverride = {"fakedns"}
|
||
inbound.sniffing.metadataOnly = true
|
||
else
|
||
table.insert(inbound.sniffing.destOverride, "fakedns")
|
||
inbound.sniffing.metadataOnly = false
|
||
end
|
||
end
|
||
|
||
if tcp_redir_port then
|
||
local tcp_inbound = api.clone(inbound)
|
||
tcp_inbound.tag = "tcp_redir"
|
||
tcp_inbound.settings.network = "tcp"
|
||
tcp_inbound.port = tonumber(tcp_redir_port)
|
||
tcp_inbound.streamSettings.sockopt.tproxy = tcp_proxy_way
|
||
table.insert(inbounds, tcp_inbound)
|
||
end
|
||
|
||
if udp_redir_port then
|
||
local udp_inbound = api.clone(inbound)
|
||
udp_inbound.tag = "udp_redir"
|
||
udp_inbound.settings.network = "udp"
|
||
udp_inbound.port = tonumber(udp_redir_port)
|
||
table.insert(inbounds, udp_inbound)
|
||
end
|
||
end
|
||
end
|
||
|
||
local node_dns = {}
|
||
for i, v in pairs(GLOBAL.DNS_SERVER) do
|
||
table.insert(node_dns, {
|
||
server = v,
|
||
outboundTag = "direct"
|
||
})
|
||
end
|
||
|
||
if (remote_dns_udp_server and remote_dns_udp_port) or (remote_dns_tcp_server and remote_dns_tcp_port) or remote_dns_doh or #node_dns > 0 then
|
||
if not routing then
|
||
routing = {
|
||
domainStrategy = "IPOnDemand",
|
||
rules = {}
|
||
}
|
||
end
|
||
|
||
dns = {
|
||
tag = "dns-global",
|
||
hosts = {},
|
||
disableCache = (dns_cache and dns_cache == "0") and true or false,
|
||
disableFallback = true,
|
||
disableFallbackIfMatch = true,
|
||
servers = {},
|
||
clientIp = (remote_dns_client_ip and remote_dns_client_ip ~= "") and remote_dns_client_ip or nil,
|
||
queryStrategy = "UseIP",
|
||
useSystemHosts = true
|
||
}
|
||
|
||
local _direct_dns = {}
|
||
|
||
direct_dns_udp_server = (direct_dns_udp_server and direct_dns_udp_server ~= "") and direct_dns_udp_server or nil
|
||
|
||
if direct_dns_udp_server or direct_dns_tcp_server then
|
||
_direct_dns.tag = "dns-global-direct"
|
||
_direct_dns.queryStrategy = (direct_dns_query_strategy and direct_dns_query_strategy ~= "") and direct_dns_query_strategy or "UseIP"
|
||
|
||
if direct_dns_udp_server then
|
||
local port = tonumber(direct_dns_port) or 53
|
||
_direct_dns.port = port
|
||
_direct_dns.address = direct_dns_udp_server
|
||
elseif direct_dns_tcp_server then
|
||
local port = tonumber(direct_dns_port) or 53
|
||
_direct_dns.address = "tcp://" .. direct_dns_tcp_server .. ":" .. port
|
||
end
|
||
|
||
if COMMON.default_outbound_tag == "direct" then
|
||
table.insert(dns.servers, _direct_dns)
|
||
end
|
||
end
|
||
if dns_listen_port and next(_direct_dns) then
|
||
local domain = {}
|
||
local nodes_domain_text = sys.exec([[uci show passwall | sed -n "s/.*\.\(address\|download_address\)='\([^']*\)'/\2/p" | sort -u]])
|
||
string.gsub(nodes_domain_text, '[^' .. "\r\n" .. ']+', function(w)
|
||
w = (w or ""):lower()
|
||
if not api.vps_domain_exclude(w) and api.datatypes.hostname(w) and not GLOBAL.VPS_EXCLUDE[w] then
|
||
table.insert(domain, "full:" .. w)
|
||
end
|
||
end)
|
||
if #domain > 0 then
|
||
table.insert(dns_domain_rules, 1, {
|
||
shunt_rule_name = "logic-vpslist",
|
||
outboundTag = "direct",
|
||
domain = domain
|
||
})
|
||
end
|
||
end
|
||
|
||
local _remote_dns = {}
|
||
|
||
if remote_dns_udp_server then
|
||
_remote_dns.address = remote_dns_udp_server
|
||
_remote_dns.port = tonumber(remote_dns_udp_port) or 53
|
||
|
||
elseif remote_dns_tcp_server then
|
||
_remote_dns.address = "tcp://" .. remote_dns_tcp_server .. ":" .. tonumber(remote_dns_tcp_port) or 53
|
||
|
||
elseif remote_dns_doh then
|
||
local _a = api.parseDoH(remote_dns_doh)
|
||
if _a then
|
||
_remote_dns.address = _a.url
|
||
_remote_dns.port = _a.port or 443
|
||
|
||
if api.datatypes.hostname(_a.hostname) then
|
||
if _a.hostip then
|
||
dns.hosts[_a.hostname] = _a.hostip
|
||
else
|
||
GLOBAL.DNS_HOSTNAME[_a.hostname] = true
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
if next(_remote_dns) then
|
||
-- _remote_dns.tag = "dns-global-remote"
|
||
_remote_dns.queryStrategy = (remote_dns_query_strategy and remote_dns_query_strategy ~= "") and remote_dns_query_strategy or "UseIPv4"
|
||
table.insert(dns.servers, _remote_dns)
|
||
end
|
||
|
||
local _remote_fakedns = {
|
||
--tag = "dns-global-remote-fakedns",
|
||
address = "fakedns",
|
||
}
|
||
|
||
if remote_dns_fake or inner_fakedns == "1" then
|
||
fakedns = {}
|
||
local fakedns4 = {
|
||
ipPool = "198.18.0.0/15",
|
||
poolSize = 65535
|
||
}
|
||
local fakedns6 = {
|
||
ipPool = "fc00::/18",
|
||
poolSize = 65535
|
||
}
|
||
if remote_dns_query_strategy == "UseIP" then
|
||
table.insert(fakedns, fakedns4)
|
||
table.insert(fakedns, fakedns6)
|
||
elseif remote_dns_query_strategy == "UseIPv4" then
|
||
table.insert(fakedns, fakedns4)
|
||
elseif remote_dns_query_strategy == "UseIPv6" then
|
||
table.insert(fakedns, fakedns6)
|
||
end
|
||
if remote_dns_fake and inner_fakedns ~= "1" then
|
||
table.insert(dns.servers, 1, _remote_fakedns)
|
||
end
|
||
end
|
||
|
||
local dns_outbound_tag = "direct"
|
||
if dns_socks_address and dns_socks_port then
|
||
dns_outbound_tag = "out"
|
||
table.insert(outbounds, 1, {
|
||
tag = dns_outbound_tag,
|
||
protocol = "socks",
|
||
streamSettings = {
|
||
network = "tcp",
|
||
security = "none",
|
||
sockopt = {
|
||
mark = 255
|
||
}
|
||
},
|
||
settings = {
|
||
servers = {
|
||
{
|
||
address = dns_socks_address,
|
||
port = tonumber(dns_socks_port)
|
||
}
|
||
}
|
||
}
|
||
})
|
||
else
|
||
if COMMON.default_outbound_tag then
|
||
dns_outbound_tag = COMMON.default_outbound_tag
|
||
end
|
||
end
|
||
|
||
local dns_rule_position = 1
|
||
local remote_dns_outbound
|
||
if dns_listen_port then
|
||
table.insert(inbounds, {
|
||
listen = "127.0.0.1",
|
||
port = tonumber(dns_listen_port),
|
||
protocol = "dokodemo-door",
|
||
tag = "dns-in",
|
||
settings = {
|
||
address = "0.0.0.0",
|
||
network = "tcp,udp"
|
||
}
|
||
})
|
||
|
||
-- remote dns outbound
|
||
local chn_list = uci:get(appname, "@global[0]", "chn_list") or "direct"
|
||
remote_dns_outbound = {
|
||
tag = "dns-out",
|
||
protocol = "dns",
|
||
proxySettings = dns_outbound_tag and {
|
||
tag = (dns_outbound_tag ~= "blackhole") and dns_outbound_tag or "direct"
|
||
} or nil,
|
||
settings = {
|
||
address = (chn_list ~= "proxy") and "8.8.8.8" or "223.5.5.5",
|
||
port = 53,
|
||
network = "tcp",
|
||
nonIPQuery = (api.compare_versions(xray_version, "<", "26.4.25")) and "reject" or nil, -- Todo is to remove it
|
||
rules = (api.compare_versions(xray_version, ">", "26.4.17")) and {} or nil
|
||
}
|
||
}
|
||
|
||
table.insert(routing.rules, 1, {
|
||
inboundTag = {
|
||
"dns-in"
|
||
},
|
||
outboundTag = "dns-out"
|
||
})
|
||
dns_rule_position = dns_rule_position + 1
|
||
end
|
||
|
||
if not COMMON.default_outbound_tag or COMMON.default_outbound_tag == "direct" then
|
||
if direct_dns_udp_server or direct_dns_tcp_server then
|
||
table.insert(routing.rules, dns_rule_position, {
|
||
inboundTag = {
|
||
"dns-global-direct"
|
||
},
|
||
outboundTag = "direct"
|
||
})
|
||
dns_rule_position = dns_rule_position + 1
|
||
end
|
||
end
|
||
|
||
--按分流顺序DNS
|
||
local remote_dns_out_rules = {}
|
||
if dns_domain_rules and #dns_domain_rules > 0 then
|
||
for index, value in ipairs(dns_domain_rules) do
|
||
if value.domain and value.outboundTag then
|
||
local dns_server = nil
|
||
if value.outboundTag == "direct" and _direct_dns.address then
|
||
dns_server = api.clone(_direct_dns)
|
||
else
|
||
if value.fakedns then
|
||
dns_server = api.clone(_remote_fakedns)
|
||
else
|
||
dns_server = api.clone(_remote_dns)
|
||
end
|
||
end
|
||
local outboundTag
|
||
if dns_server then
|
||
if not api.is_local_ip(dns_server.address) or value.outboundTag == "blackhole" then
|
||
outboundTag = value.outboundTag
|
||
else
|
||
outboundTag = "direct" --dns为本地ip,走直连
|
||
end
|
||
end
|
||
--[[
|
||
local dns_block_mode = "host"
|
||
if dns_block_mode == "host" and outboundTag == "blackhole" then
|
||
for d_i, d_k in ipairs(value.domain) do
|
||
dns.hosts[d_k] = "0.0.0.0"
|
||
end
|
||
dns_server = nil
|
||
end
|
||
]]--
|
||
|
||
-- remote dns outbound rules
|
||
if value.outboundTag == "blackhole" then
|
||
table.insert(remote_dns_out_rules, {
|
||
action = "return",
|
||
rCode = 0,
|
||
domain = api.clone(value.domain)
|
||
})
|
||
else
|
||
table.insert(remote_dns_out_rules, {
|
||
action = "hijack",
|
||
qType = "1,28",
|
||
domain = api.clone(value.domain)
|
||
})
|
||
end
|
||
|
||
if dns_server then
|
||
--dns_server.finalQuery = true
|
||
dns_server.domains = value.domain
|
||
if value.shunt_rule_name then
|
||
dns_server.tag = "dns-in-" .. value.shunt_rule_name
|
||
if value.shunt_rule_name == "logic-vpslist" then
|
||
dns_server.finalQuery = true
|
||
dns_server.disableCache = false
|
||
dns_server.serveStale = true
|
||
dns_server.serveExpiredTTL = 30
|
||
end
|
||
end
|
||
table.insert(dns.servers, dns_server)
|
||
table.insert(routing.rules, dns_rule_position, {
|
||
inboundTag = { dns_server.tag },
|
||
outboundTag = outboundTag
|
||
})
|
||
dns_rule_position = dns_rule_position + 1
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
local _outboundTag
|
||
if _remote_dns.address and not api.is_local_ip(_remote_dns.address) or dns_outbound_tag == "blackhole" then --dns为本地ip,不走代理
|
||
_outboundTag = dns_outbound_tag
|
||
else
|
||
_outboundTag = "direct"
|
||
end
|
||
table.insert(routing.rules, dns_rule_position, {
|
||
inboundTag = { "dns-global" },
|
||
outboundTag = _outboundTag
|
||
})
|
||
dns_rule_position = dns_rule_position + 1
|
||
|
||
local default_rule_index = nil
|
||
for index, value in ipairs(routing.rules) do
|
||
if value.ruleTag == "default" then
|
||
default_rule_index = index
|
||
break
|
||
end
|
||
end
|
||
if default_rule_index then
|
||
local default_rule = api.clone(routing.rules[default_rule_index])
|
||
table.remove(routing.rules, default_rule_index)
|
||
table.insert(routing.rules, default_rule)
|
||
end
|
||
|
||
local dns_hosts_len = 0
|
||
for key, value in pairs(dns.hosts) do
|
||
dns_hosts_len = dns_hosts_len + 1
|
||
end
|
||
|
||
if dns_hosts_len == 0 then
|
||
dns.hosts = nil
|
||
end
|
||
|
||
-- remote dns outbound
|
||
if remote_dns_outbound then
|
||
if remote_dns_outbound.settings.rules then
|
||
table.insert(remote_dns_out_rules, {
|
||
action = "hijack",
|
||
qType = "1,28"
|
||
})
|
||
table.insert(remote_dns_out_rules, {
|
||
action = "direct"
|
||
})
|
||
remote_dns_outbound.settings.rules = remote_dns_out_rules
|
||
end
|
||
table.insert(outbounds, remote_dns_outbound)
|
||
end
|
||
|
||
-- 自定义节点 DNS
|
||
if #node_dns > 0 and #dns.servers < 1 then
|
||
dns.servers = { "localhost" }
|
||
end
|
||
local idx = dns_listen_port and 2 or 1
|
||
for i = #node_dns, 1, -1 do
|
||
local value = node_dns[i]
|
||
table.insert(routing.rules, idx, {
|
||
inboundTag = {
|
||
value.server.tag
|
||
},
|
||
outboundTag = value.outboundTag,
|
||
})
|
||
table.insert(dns.servers, 2, value.server)
|
||
end
|
||
if next(GLOBAL.DNS_HOSTNAME) then
|
||
local hostname = {}
|
||
for line, _ in pairs(GLOBAL.DNS_HOSTNAME) do
|
||
table.insert(hostname, line)
|
||
end
|
||
local new_dns_server = next(_direct_dns) and api.clone(_direct_dns) or { address = "localhost" }
|
||
new_dns_server.tag = "dns-in-bootstrap"
|
||
new_dns_server.domains = hostname
|
||
table.insert(dns.servers, 2, new_dns_server)
|
||
table.insert(routing.rules, idx, {
|
||
inboundTag = { "dns-in-bootstrap" },
|
||
outboundTag = "direct"
|
||
})
|
||
end
|
||
end
|
||
|
||
if inbounds or outbounds then
|
||
local config = {
|
||
env = (function()
|
||
local asset_location = uci:get(appname, "@global_rules[0]", "v2ray_location_asset") or "/usr/share/v2ray/"
|
||
return { XRAY_LOCATION_ASSET = asset_location }
|
||
end)(),
|
||
log = {
|
||
-- error = string.format("/tmp/etc/%s/%s.log", appname, node[".name"]),
|
||
loglevel = loglevel
|
||
},
|
||
-- DNS
|
||
dns = dns,
|
||
fakedns = fakedns,
|
||
-- 传入连接
|
||
inbounds = inbounds,
|
||
-- 传出连接
|
||
outbounds = outbounds,
|
||
-- 连接观测
|
||
observatory = (not burstObservatory) and observatory or nil,
|
||
burstObservatory = burstObservatory,
|
||
-- 路由
|
||
routing = routing,
|
||
-- 本地策略
|
||
policy = {
|
||
levels = {
|
||
[0] = {
|
||
-- handshake = 4,
|
||
-- connIdle = 300,
|
||
-- uplinkOnly = 2,
|
||
-- downlinkOnly = 5,
|
||
bufferSize = xray_settings.buffer_size and tonumber(xray_settings.buffer_size) or nil,
|
||
statsUserUplink = false,
|
||
statsUserDownlink = false
|
||
}
|
||
},
|
||
-- system = {
|
||
-- statsInboundUplink = false,
|
||
-- statsInboundDownlink = false
|
||
-- }
|
||
},
|
||
version = {
|
||
min = xray_min_version
|
||
}
|
||
}
|
||
|
||
local direct_outbound = {
|
||
protocol = "freedom",
|
||
tag = "direct",
|
||
settings = {
|
||
domainStrategy = (direct_dns_query_strategy and direct_dns_query_strategy ~= "") and direct_dns_query_strategy or "UseIP",
|
||
finalRules = (api.compare_versions(xray_version, ">", "26.4.25")) and {{ action = "allow" }} or nil -- Todo: Remove version check
|
||
},
|
||
streamSettings = {
|
||
sockopt = {
|
||
mark = 255
|
||
}
|
||
}
|
||
}
|
||
if COMMON.default_outbound_tag == "direct" then
|
||
table.insert(outbounds, 1, direct_outbound)
|
||
else
|
||
table.insert(outbounds, direct_outbound)
|
||
end
|
||
|
||
local blackhole_outbound = {
|
||
protocol = "blackhole",
|
||
tag = "blackhole"
|
||
}
|
||
if COMMON.default_outbound_tag == "blackhole" then
|
||
table.insert(outbounds, 1, blackhole_outbound)
|
||
else
|
||
table.insert(outbounds, blackhole_outbound)
|
||
end
|
||
for index, value in ipairs(config.outbounds) do
|
||
local pt = value.protocol
|
||
local exclude = { blackhole=1, dns=1, freedom=1, loopback=1 }
|
||
if not value["_flag_proxy_tag"] and value["_id"] and pt and not exclude[pt] and not no_run then
|
||
sys.call(string.format("echo '%s' >> %s", value["_id"], api.TMP_PATH .. "/direct_node_list"))
|
||
end
|
||
for k, v in pairs(config.outbounds[index]) do
|
||
if k:find("_") == 1 then
|
||
config.outbounds[index][k] = nil
|
||
end
|
||
end
|
||
end
|
||
return jsonc.stringify(config, 1)
|
||
end
|
||
end
|
||
|
||
function gen_proto_config(var)
|
||
local local_socks_address = var["local_socks_address"] or "0.0.0.0"
|
||
local local_socks_port = var["local_socks_port"]
|
||
local local_socks_username = var["local_socks_username"]
|
||
local local_socks_password = var["local_socks_password"]
|
||
local local_http_address = var["local_http_address"] or "0.0.0.0"
|
||
local local_http_port = var["local_http_port"]
|
||
local local_http_username = var["local_http_username"]
|
||
local local_http_password = var["local_http_password"]
|
||
local server_proto = var["server_proto"]
|
||
local server_address = var["server_address"]
|
||
local server_port = var["server_port"]
|
||
local server_username = var["server_username"]
|
||
local server_password = var["server_password"]
|
||
|
||
local inbounds = {}
|
||
local outbounds = {}
|
||
local routing = nil
|
||
|
||
if local_socks_address and local_socks_port then
|
||
local inbound = {
|
||
listen = local_socks_address,
|
||
port = tonumber(local_socks_port),
|
||
protocol = "socks",
|
||
settings = {
|
||
udp = true,
|
||
auth = "noauth"
|
||
}
|
||
}
|
||
if local_socks_username and local_socks_password and local_socks_username ~= "" and local_socks_password ~= "" then
|
||
inbound.settings.auth = "password"
|
||
inbound.settings.users = {
|
||
{
|
||
user = local_socks_username,
|
||
pass = local_socks_password
|
||
}
|
||
}
|
||
end
|
||
table.insert(inbounds, inbound)
|
||
end
|
||
|
||
if local_http_address and local_http_port then
|
||
local inbound = {
|
||
listen = local_http_address,
|
||
port = tonumber(local_http_port),
|
||
protocol = "http",
|
||
settings = {
|
||
allowTransparent = false
|
||
}
|
||
}
|
||
if local_http_username and local_http_password and local_http_username ~= "" and local_http_password ~= "" then
|
||
inbound.settings.users = {
|
||
{
|
||
user = local_http_username,
|
||
pass = local_http_password
|
||
}
|
||
}
|
||
end
|
||
table.insert(inbounds, inbound)
|
||
end
|
||
|
||
if server_proto ~= "nil" and server_address ~= "nil" and server_port ~= "nil" then
|
||
local outbound = {
|
||
protocol = server_proto,
|
||
streamSettings = {
|
||
network = "tcp",
|
||
security = "none"
|
||
},
|
||
settings = {
|
||
servers = {
|
||
{
|
||
address = server_address,
|
||
port = tonumber(server_port),
|
||
users = (server_username and server_password) and {
|
||
{
|
||
user = server_username,
|
||
pass = server_password
|
||
}
|
||
} or nil
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if outbound then table.insert(outbounds, outbound) end
|
||
end
|
||
|
||
-- 额外传出连接
|
||
table.insert(outbounds, {
|
||
protocol = "freedom",
|
||
tag = "direct",
|
||
settings = (api.compare_versions(xray_version, ">", "26.4.25")) and { -- Todo: Remove version check
|
||
finalRules = {{ action = "allow" }}
|
||
} or nil,
|
||
sockopt = {mark = 255}
|
||
})
|
||
|
||
local config = {
|
||
log = {
|
||
loglevel = "warning"
|
||
},
|
||
-- 传入连接
|
||
inbounds = inbounds,
|
||
-- 传出连接
|
||
outbounds = outbounds,
|
||
-- 路由
|
||
routing = routing
|
||
}
|
||
return jsonc.stringify(config, 1)
|
||
end
|
||
|
||
_G.gen_config = gen_config
|
||
_G.gen_proto_config = gen_proto_config
|
||
|
||
if arg[1] then
|
||
local func =_G[arg[1]]
|
||
if func then
|
||
local var = nil
|
||
if arg[2] then
|
||
var = jsonc.parse(arg[2])
|
||
end
|
||
print(func(var))
|
||
end
|
||
end
|