Files
2026-10-31 18:07:25 +08:00

813 lines
31 KiB
Lua
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
local api = require "luci.passwall.api"
local var = api.get_args(arg)
local FLAG = var["-FLAG"]
local DIRECT_DNS = var["-DIRECT_DNS"]
local LOCAL_PORT = tonumber(var["-LOCAL_PORT"]) or 0
local REMOTE_PROXY_SERVER = var["-REMOTE_PROXY_SERVER"]
local USE_DEFAULT_DNS = var["-USE_DEFAULT_DNS"]
local REMOTE_DNS = var["-REMOTE_DNS"]
local TUN_DNS = var["-TUN_DNS"]
local DNS_MODE = var["-DNS_MODE"]
local NODE = var["-NODE"]
local USE_DIRECT_LIST = var["-USE_DIRECT_LIST"]
local USE_PROXY_LIST = var["-USE_PROXY_LIST"]
local USE_BLOCK_LIST = var["-USE_BLOCK_LIST"]
local USE_GFW_LIST = var["-USE_GFW_LIST"]
local CHN_LIST = var["-CHN_LIST"]
local DEFAULT_PROXY_MODE = var["-DEFAULT_PROXY_MODE"]
local NO_PROXY_IPV6 = var["-NO_PROXY_IPV6"]
local NO_LOGIC_LOG = var["-NO_LOGIC_LOG"]
local NFTFLAG = var["-NFTFLAG"]
local SUBNET = var["-SUBNET"]
local LISTEN_PORT = var["-LISTEN_PORT"]
local DNS_REDIRECT = var["-DNS_REDIRECT"] == "1"
local LOG = var["-LOG"] == "1"
local LOG_LEVEL = var["-LOG_LEVEL"] or "error"
local LOG_SIZE = tonumber(var["-LOG_SIZE"]) or 1
local UI = var["-UI"] == "1"
local UI_PORT = tonumber(var["-UI_PORT"]) or 9880
local UI_DATA_DIR = var["-UI_DATA_DIR"]
local UI_LOG_MAX_AGE = tonumber(var["-UI_LOG_MAX_AGE"]) or 7
local sys = api.sys
local fs = api.fs
local datatypes = api.datatypes
local TMP_PATH = api.TMP_PATH
local RULES_PATH = "/usr/share/passwall/rules"
local USER_RULES_PATH = "/etc/passwall/rules"
local CACHE_RULES_PATH = api.CACHE_PATH .. "/user_rules"
local FLAG_PATH = TMP_PATH .. "/acl/" .. FLAG
local TMP_CONF_FILE = FLAG_PATH .. "/smartdns.conf"
local SMARTDNS_DATA_PATH = api.CACHE_PATH .. "/smartdns"
local config_lines = {}
local tmp_lines = {}
local USE_GEOVIEW = api.uci_get_c("@global_rules[0]", "enable_geoview")
local IS_SHUNT_NODE = api.uci_get_c(NODE, "protocol") == "_shunt"
if not api.is_finded("geoview") then
USE_GEOVIEW = "0"
end
local function log(...)
if NO_LOGIC_LOG == "1" then
return
end
api.log(...)
end
local function is_file_nonzero(path)
if path and #path > 1 and (fs.stat(path, "size") or 0) > 0 then
return true
end
return false
end
local function insert_unique(dest_table, value, lookup_table)
if not lookup_table[value] then
table.insert(dest_table, value)
lookup_table[value] = true
end
end
local function merge_array(array1, array2)
for i, line in ipairs(array2) do
table.insert(array1, #array1 + 1, line)
end
end
local function insert_array_before(array1, array2, target) --将array2插入到array1的target前面,target不存在则追加
for i, line in ipairs(array1) do
if line == target then
for j = #array2, 1, -1 do
table.insert(array1, i, array2[j])
end
return
end
end
merge_array(array1, array2)
end
local function insert_array_after(array1, array2, target) --将array2插入到array1的target后面,target不存在则追加
for i, line in ipairs(array1) do
if line == target then
for j = 1, #array2 do
table.insert(array1, i + j, array2[j])
end
return
end
end
merge_array(array1, array2)
end
local function get_geosite(list_arg, out_path)
local geosite_path = api.uci_get_c("@global_rules[0]", "v2ray_location_asset") or "/usr/share/v2ray/"
geosite_path = geosite_path:match("^(.*)/") .. "/geosite.dat"
if not is_file_nonzero(geosite_path) then return 1, "File geosite.dat not found" end
if not list_arg or list_arg == "" then return 1, "Site list cannot be empty" end
if not out_path or out_path == "" then return 1, "Output path cannot be empty" end
local bin = api.finded_com("geoview")
local cmd = string.format("%q -type geosite -append=true -input %q -list %q -output %q -lowmem=true",
bin, geosite_path, list_arg, out_path)
return api.exec_call(cmd)
end
sys.call("mkdir -p %s" % FLAG_PATH)
sys.call("mkdir -p %s" % CACHE_RULES_PATH)
sys.call("mkdir -p " .. api.util.shellquote(SMARTDNS_DATA_PATH))
local LOG_LEVELS = {off = true, fatal = true, error = true, warn = true, notice = true, info = true, debug = true}
if not LOG_LEVELS[LOG_LEVEL] then LOG_LEVEL = "error" end
if LOG_SIZE < 1 or LOG_SIZE % 1 ~= 0 then LOG_SIZE = 1 end
if UI_PORT < 1 or UI_PORT > 65535 or UI_PORT % 1 ~= 0 then UI_PORT = 9880 end
if UI_LOG_MAX_AGE < 1 or UI_LOG_MAX_AGE % 1 ~= 0 then UI_LOG_MAX_AGE = 7 end
if not UI_DATA_DIR or UI_DATA_DIR == "" then UI_DATA_DIR = SMARTDNS_DATA_PATH end
local UI_PLUGIN = "/usr/lib/smartdns_ui.so"
if not fs.access(UI_PLUGIN) then UI_PLUGIN = "/usr/lib/smartdns/smartdns_ui.so" end
local ENABLE_UI = UI and fs.access(UI_PLUGIN)
local data_dir = ENABLE_UI and UI_DATA_DIR or SMARTDNS_DATA_PATH
if data_dir ~= SMARTDNS_DATA_PATH then
sys.call("mkdir -p " .. api.util.shellquote(data_dir))
end
-- 统一生成直连、远程上游,协议和默认端口由 SmartDNS 根据 URL 识别。
local function make_server_param(endpoint)
local dns = api.split(api.trim(endpoint), ",")
local address = api.trim(dns[1] or "")
if address == "" then return nil end
local protocol, host_address = address:match("^(%w+)://(.+)$")
protocol = protocol and protocol:lower()
address = host_address or address
local host, port = address:match("^(.-)#(%d+)$")
if host then
if host:find(":", 1, true) and not host:find("[", 1, true) then host = "[" .. host .. "]" end
address = host .. ":" .. port
elseif datatypes.ip6addr(address) and not address:find("[", 1, true) then
address = "[" .. address .. "]"
end
local server_param = "server " .. (protocol and (protocol .. "://") or "") .. address
if dns[2] and dns[2] ~= "" then
-- 保留 URL 中的域名,固定连接 IP 交给 SmartDNS 处理,避免手动替换影响 SNI 和 HTTP Host。
server_param = server_param .. " -host-ip " .. api.trim(dns[2]):gsub("^%[(.*)%]$", "%1")
end
return server_param
end
local DIRECT_SERVERS = {}
for endpoint in string.gmatch(DIRECT_DNS or "", "[^|]+") do
local server_param = make_server_param(endpoint)
if server_param then
-- 复用 PassWall 的直连标记,使所有协议及动态域名上游绕过本机透明代理。
table.insert(DIRECT_SERVERS, server_param .. " -set-mark 255")
end
end
if #DIRECT_SERVERS == 0 then
log(" * 注意:SmartDNS 没有可用的直连 DNS 服务器,DNS 将无法正常工作!!!")
os.exit(1)
end
local AUTO_GROUP = "auto"
local auto_config_lines = {}
-- 直连规则显式覆盖远程兜底的缓存、IPv6、测速和双栈优选设置。
local DIRECT_RULE_OPTIONS = " -enable-cache -address -6 -speed-check-mode ping,tcp:80,tcp:443 -response-mode first-ping -d yes"
--设置默认 DNS 分组(兜底组),全局代理模式仍使用远程组。
local DEFAULT_DNS_GROUP = (USE_DEFAULT_DNS == "direct" and "direct") or (USE_DEFAULT_DNS == "remote" and "proxy") or (USE_DEFAULT_DNS == "auto" and AUTO_GROUP)
local only_global = (DEFAULT_PROXY_MODE == "proxy" and CHN_LIST == "0" and USE_GFW_LIST == "0") and 1
if only_global == 1 then
DEFAULT_DNS_GROUP = "proxy"
end
local AUTO_DIRECT_PORT = 0
if DEFAULT_DNS_GROUP == AUTO_GROUP then
AUTO_DIRECT_PORT = api.get_new_port(15350)
-- 避开同一进程尚未启动的主监听和直连监听端口。
for _ = 1, 2 do
if not AUTO_DIRECT_PORT or (AUTO_DIRECT_PORT ~= tonumber(LISTEN_PORT) and AUTO_DIRECT_PORT ~= LOCAL_PORT) then break end
AUTO_DIRECT_PORT = api.get_new_port(AUTO_DIRECT_PORT + 1)
end
end
local force_https_soa = api.uci_get_c("@global[0]", "force_https_soa") or 0
local force_qtype_soa = tonumber(force_https_soa) == 1 and "force-qtype-SOA 65" or "force-qtype-SOA -,65"
local proxy_server_name = "psw-proxy-server"
local listen_address = "[::]:" .. LISTEN_PORT .. (DNS_REDIRECT and "" or "@lo")
local local_domain = DNS_REDIRECT and api.uci_get("dhcp", "@dnsmasq[0]", "domain")
config_lines = {
tonumber(LISTEN_PORT) ~= 0 and ("bind " .. listen_address) or "",
tonumber(LISTEN_PORT) ~= 0 and ("bind-tcp " .. listen_address) or "",
LOCAL_PORT > 0 and ("bind [::]:" .. LOCAL_PORT .. "@lo -group direct -no-rules") or "",
LOCAL_PORT > 0 and ("bind-tcp [::]:" .. LOCAL_PORT .. "@lo -group direct -no-rules") or "",
AUTO_DIRECT_PORT > 0 and ("bind [::]:" .. AUTO_DIRECT_PORT .. "@lo -group auto-direct") or "",
"log-level " .. LOG_LEVEL,
"log-num " .. (LOG and "1" or "0"),
LOG and string.format("log-file %q", FLAG_PATH .. "/smartdns.log") or "",
LOG and ("log-size " .. LOG_SIZE .. "M") or "",
string.format("cache-file %q", SMARTDNS_DATA_PATH .. "/smartdns.cache"),
string.format("data-dir %q", data_dir),
"server 223.5.5.5 -bootstrap-dns -set-mark 255",
-- Set the suffix before loading hosts/leases so initial records are expanded too.
(local_domain and local_domain ~= "") and "local-domain " .. local_domain or "",
DNS_REDIRECT and "hosts-file /tmp/hosts/*" or "",
is_file_nonzero("/etc/hosts") and "hosts-file /etc/hosts" or "",
DNS_MODE == "socks" and string.format("proxy-server socks5://%s -name %s", REMOTE_PROXY_SERVER, proxy_server_name) or ""
}
if ENABLE_UI then
merge_array(config_lines, {
"plugin " .. UI_PLUGIN,
"smartdns-ui.www-root /usr/share/smartdns/wwwroot",
"smartdns-ui.ip http://[::]:" .. UI_PORT,
"smartdns-ui.max-query-log-age " .. (UI_LOG_MAX_AGE * 86400)
})
end
if DNS_REDIRECT then
local hosts = {}
api.uci:foreach("dhcp", "host", function(s)
if datatypes.hostname(s.name or "") and datatypes.ip4addr(s.ip or "") then
hosts[#hosts + 1] = s.ip .. " " .. s.name
end
end)
local hosts_file = FLAG_PATH .. "/dhcp-static-hosts"
if fs.writefile(hosts_file, table.concat(hosts, "\n") .. "\n") and #hosts > 0 then
table.insert(config_lines, string.format("hosts-file %q", hosts_file))
end
-- SmartDNS monitors the lease file and updates local records without a restart.
local lease_file = api.uci_get("dhcp", "@dnsmasq[0]", "leasefile")
if not lease_file or lease_file == "" then lease_file = "/tmp/dhcp.leases" end
table.insert(config_lines, string.format("dnsmasq-lease-file %q", lease_file))
local odhcpd_lease = api.uci_get("dhcp", "odhcpd", "leasefile")
if odhcpd_lease and odhcpd_lease ~= "" then
table.insert(config_lines, string.format("odhcpd-lease-file %q", odhcpd_lease))
end
end
for _, server_param in ipairs(DIRECT_SERVERS) do
table.insert(config_lines, server_param .. " -group direct -exclude-default-group")
end
if DEFAULT_DNS_GROUP == AUTO_GROUP then
-- 自动组先通过独立入口去除 CNAME,再按原逻辑筛选 IP。
table.insert(auto_config_lines, "server 127.0.0.1:" .. AUTO_DIRECT_PORT .. " -group " .. AUTO_GROUP .. " -exclude-default-group -whitelist-ip")
table.insert(auto_config_lines, "group-begin auto-direct -inherit none")
merge_array(auto_config_lines, DIRECT_SERVERS)
merge_array(auto_config_lines, {"force-no-CNAME yes", "group-end"})
end
-- 由下方 DNS_MODE 分支调用,服务器参数来自 PassWall 的 REMOTE_DNS 或 TUN_DNS。
local function add_remote_server(server_param)
table.insert(config_lines, server_param .. " -group proxy -exclude-default-group")
if DEFAULT_DNS_GROUP == AUTO_GROUP then
-- 远程副本作为自动组的后备服务器,保留代理、ECS 和传输协议参数。
local auto_param = server_param .. " -group " .. AUTO_GROUP .. " -exclude-default-group -fallback"
if NO_PROXY_IPV6 == "1" then auto_param = auto_param .. " -blacklist-ip" end
table.insert(auto_config_lines, auto_param)
end
end
if DNS_MODE == "socks" then
for w in string.gmatch(REMOTE_DNS, '[^|]+') do
local server_param = make_server_param(w)
if not api.is_local_ip(w) then
server_param = server_param .. " -proxy " .. proxy_server_name
end
if SUBNET and SUBNET ~= "" and SUBNET ~= "0" then
server_param = server_param .. " -subnet " .. SUBNET
end
add_remote_server(server_param)
end
else
add_remote_server("server " .. TUN_DNS:gsub("#", ":"))
log(" - " .. DNS_MODE:gsub("^%l",string.upper) .. " " .. TUN_DNS .. " -> 分组(proxy)")
end
if DEFAULT_DNS_GROUP == AUTO_GROUP then
-- 仅自动组的服务器启用 IP 过滤,普通直连和代理组保持各自行为。
if NO_PROXY_IPV6 == "1" then
table.insert(auto_config_lines, "ip-rules ::/0 -blacklist-ip")
end
for _, name in ipairs({"chnroute", "chnroute6"}) do
local path = RULES_PATH .. "/" .. name
if is_file_nonzero(path) then
local set_name = AUTO_GROUP .. "-" .. name
table.insert(auto_config_lines, string.format("ip-set -name %s -file %s", set_name, path))
local ip_rules = "ip-rules ip-set:" .. set_name .. " -whitelist-ip"
if name == "chnroute6" and NO_PROXY_IPV6 == "1" then
-- 最长前缀匹配时,大陆 IPv6 网段也需要黑名单标记才能过滤远程结果。
ip_rules = ip_rules .. " -blacklist-ip"
end
table.insert(auto_config_lines, ip_rules)
else
log(" * 注意:SmartDNS 自动模式大陆 IP 列表不存在或为空:" .. path)
end
end
merge_array(config_lines, auto_config_lines)
end
table.insert(config_lines, force_qtype_soa)
if DEFAULT_DNS_GROUP then
local domain_rules_str = "domain-rules /./ -nameserver " .. DEFAULT_DNS_GROUP
if DEFAULT_DNS_GROUP == "proxy" then
-- 代理仍禁止写入缓存,去掉会被直连继承且无法反向覆盖的过期缓存限制。
domain_rules_str = domain_rules_str .. " -no-cache -speed-check-mode none -d no"
domain_rules_str = domain_rules_str .. " -address " .. ((only_global and IS_SHUNT_NODE) and "-6" or (NO_PROXY_IPV6 == "1" and "#6" or "-6"))
end
table.insert(config_lines, domain_rules_str)
end
local setflag = (NFTFLAG == "1") and "inet#passwall#" or ""
local set_type = (NFTFLAG == "1") and "-nftset" or "-ipset"
--预设排序标签(越往后优先级越高)
for i = 1, 10 do
table.insert(config_lines, "#--" .. i)
end
--屏蔽列表
local file_block_host = CACHE_RULES_PATH .. "/block_host"
if not fs.access(file_block_host .. "_ok") then
api.remove(file_block_host .. "*")
end
local block_geosite_flag = fs.access(file_block_host) and fs.access(file_block_host .. "_ok") and fs.access(file_block_host .. "_geo")
if USE_BLOCK_LIST == "1" and not fs.access(file_block_host) then
local block_domain, lookup_block_domain = {}, {}
local geosite_arg = ""
local f = io.open(USER_RULES_PATH .. "/block_host")
if f then
for line in f:lines() do
if not line:find("#") and line:find("geosite:") then
line = string.match(line, ":([^:]+)$")
geosite_arg = geosite_arg .. (geosite_arg ~= "" and "," or "") .. line
else
line = api.get_std_domain(line)
if line ~= "" and not line:find("#") then
insert_unique(block_domain, line, lookup_block_domain)
end
end
end
f:close()
end
if #block_domain > 0 then
local f_out = io.open(file_block_host, "w")
for i = 1, #block_domain do
f_out:write(block_domain[i] .. "\n")
end
f_out:close()
end
local success = true
if USE_GEOVIEW == "1" and geosite_arg ~= "" then
local code, out = get_geosite(geosite_arg, file_block_host)
if code == 0 then
log(" - 解析[屏蔽列表] Geosite 到屏蔽域名表(blocklist)完成")
sys.call("touch " .. file_block_host .. "_geo")
else
log(" - 解析[屏蔽列表] Geosite 到屏蔽域名表(blocklist)失败![" .. out .. "]")
success = false
end
end
if success and fs.access(file_block_host) then
sys.call("touch " .. file_block_host .. "_ok")
end
end
if USE_BLOCK_LIST == "1" and is_file_nonzero(file_block_host) then
if block_geosite_flag then
log(" - [屏蔽列表] Geosite 使用缓存结果,跳过重复解析")
end
local domain_set_name = "psw-block"
tmp_lines = {
string.format("domain-set -name %s -file %s", domain_set_name, file_block_host),
string.format("domain-rules /domain-set:%s/ -a #", domain_set_name)
}
insert_array_after(config_lines, tmp_lines, "#--8")
end
--始终用国内DNS解析节点域名
local file_vpslist = TMP_PATH .. "/vpslist"
if not is_file_nonzero(file_vpslist) then
local f_out = io.open(file_vpslist, "w")
local written_domains = {}
local function process_address(address)
address = (address or ""):lower()
if api.vps_domain_exclude(address) then return end
if datatypes.hostname(address) and not written_domains[address] then
f_out:write(address .. "\n")
written_domains[address] = true
end
end
api.uci_foreach_c("nodes", function(t)
process_address(t.address)
process_address(t.download_address)
end)
f_out:close()
end
if is_file_nonzero(file_vpslist) then
local domain_set_name = "psw-vpslist"
tmp_lines = {
string.format("domain-set -name %s -file %s", domain_set_name, file_vpslist)
}
local sets = {
"#4:" .. setflag .. "psw_vps",
"#6:" .. setflag .. "psw_vps6"
}
local domain_rules_str = string.format('domain-rules /domain-set:%s/ -nameserver direct', domain_set_name) .. DIRECT_RULE_OPTIONS
-- 节点域名最多缓存 30 秒,到期后重新查询,避免继续返回过期的节点 IP。
domain_rules_str = domain_rules_str .. " -rr-ttl-max 30 -no-serve-expired"
domain_rules_str = domain_rules_str .. " " .. set_type .. " " .. table.concat(sets, ",")
table.insert(tmp_lines, domain_rules_str)
insert_array_after(config_lines, tmp_lines, "#--9")
log(" - 节点列表中的域名(vpslist)使用分组:direct")
end
--直连(白名单)列表
local file_direct_host = CACHE_RULES_PATH .. "/direct_host"
if not fs.access(file_direct_host .. "_ok") then
api.remove(file_direct_host .. "*")
end
local direct_geosite_flag = fs.access(file_direct_host) and fs.access(file_direct_host .. "_ok") and fs.access(file_direct_host .. "_geo")
if USE_DIRECT_LIST == "1" and not fs.access(file_direct_host) then
local direct_domain, lookup_direct_domain = {}, {}
local geosite_arg = ""
local f = io.open(USER_RULES_PATH .. "/direct_host")
if f then
for line in f:lines() do
if not line:find("#") and line:find("geosite:") then
line = string.match(line, ":([^:]+)$")
geosite_arg = geosite_arg .. (geosite_arg ~= "" and "," or "") .. line
else
line = api.get_std_domain(line)
if line ~= "" and not line:find("#") and not line:find(":") then
insert_unique(direct_domain, line, lookup_direct_domain)
end
end
end
f:close()
end
if #direct_domain > 0 then
local f_out = io.open(file_direct_host, "w")
for i = 1, #direct_domain do
f_out:write(direct_domain[i] .. "\n")
end
f_out:close()
end
local success = true
if USE_GEOVIEW == "1" and geosite_arg ~= "" then
local code, out = get_geosite(geosite_arg, file_direct_host)
if code == 0 then
log(" - 解析[直连列表] Geosite 到域名白名单(whitelist)完成")
sys.call("touch " .. file_direct_host .. "_geo")
else
log(" - 解析[直连列表] Geosite 到域名白名单(whitelist)失败![" .. out .. "]")
success = false
end
end
if success and fs.access(file_direct_host) then
sys.call("touch " .. file_direct_host .. "_ok")
end
end
if USE_DIRECT_LIST == "1" and is_file_nonzero(file_direct_host) then
if direct_geosite_flag then
log(" - [直连列表] Geosite 使用缓存结果,跳过重复解析")
end
local domain_set_name = "psw-directlist"
tmp_lines = {
string.format("domain-set -name %s -file %s", domain_set_name, file_direct_host)
}
local sets = {
"#4:" .. setflag .. "psw_white",
"#6:" .. setflag .. "psw_white6"
}
local domain_rules_str = string.format('domain-rules /domain-set:%s/ -nameserver direct', domain_set_name) .. DIRECT_RULE_OPTIONS
domain_rules_str = domain_rules_str .. " " .. set_type .. " " .. table.concat(sets, ",")
table.insert(tmp_lines, domain_rules_str)
insert_array_after(config_lines, tmp_lines, "#--7")
log(" - 域名白名单(whitelist)使用分组:direct")
end
--代理(黑名单)列表
local file_proxy_host = CACHE_RULES_PATH .. "/proxy_host"
if not fs.access(file_proxy_host .. "_ok") then
api.remove(file_proxy_host .. "*")
end
local proxy_geosite_flag = fs.access(file_proxy_host) and fs.access(file_proxy_host .. "_ok") and fs.access(file_proxy_host .. "_geo")
if USE_PROXY_LIST == "1" and not fs.access(file_proxy_host) then
local proxy_domain, lookup_proxy_domain = {}, {}
local geosite_arg = ""
local f = io.open(USER_RULES_PATH .. "/proxy_host")
if f then
for line in f:lines() do
if not line:find("#") and line:find("geosite:") then
line = string.match(line, ":([^:]+)$")
geosite_arg = geosite_arg .. (geosite_arg ~= "" and "," or "") .. line
else
line = api.get_std_domain(line)
if line ~= "" and not line:find("#") and not line:find(":") then
insert_unique(proxy_domain, line, lookup_proxy_domain)
end
end
end
f:close()
end
if #proxy_domain > 0 then
local f_out = io.open(file_proxy_host, "w")
for i = 1, #proxy_domain do
f_out:write(proxy_domain[i] .. "\n")
end
f_out:close()
end
local success = true
if USE_GEOVIEW == "1" and geosite_arg ~= "" then
local code, out = get_geosite(geosite_arg, file_proxy_host)
if code == 0 then
log(" - 解析[代理列表] Geosite 到代理域名表(blacklist)完成")
sys.call("touch " .. file_proxy_host .. "_geo")
else
log(" - 解析[代理列表] Geosite 到代理域名表(blacklist)失败![" .. out .. "]")
success = false
end
end
if success and fs.access(file_proxy_host) then
sys.call("touch " .. file_proxy_host .. "_ok")
end
end
if USE_PROXY_LIST == "1" and is_file_nonzero(file_proxy_host) then
if proxy_geosite_flag then
log(" - [代理列表] Geosite 使用缓存结果,跳过重复解析")
end
local domain_set_name = "psw-proxylist"
tmp_lines = {
string.format("domain-set -name %s -file %s", domain_set_name, file_proxy_host)
}
local domain_rules_str = string.format('domain-rules /domain-set:%s/ -nameserver proxy', domain_set_name)
domain_rules_str = domain_rules_str .. " -no-cache -speed-check-mode none -d no"
local sets = {
"#4:" .. setflag .. "psw_black"
}
if NO_PROXY_IPV6 == "1" then
domain_rules_str = domain_rules_str .. " -address #6 " .. set_type .. " " .. table.concat(sets, ",")
else
table.insert(sets, "#6:" .. setflag .. "psw_black6")
domain_rules_str = domain_rules_str .. " -address -6 " .. set_type .. " " .. table.concat(sets, ",")
end
table.insert(tmp_lines, domain_rules_str)
insert_array_after(config_lines, tmp_lines, "#--6")
log(" - 代理域名表(blacklist)使用分组:proxy")
end
--GFW列表
if USE_GFW_LIST == "1" and is_file_nonzero(RULES_PATH .. "/gfwlist") then
local domain_set_name = "psw-gfwlist"
tmp_lines = {
string.format("domain-set -name %s -file %s", domain_set_name, RULES_PATH .. "/gfwlist")
}
local domain_rules_str = string.format('domain-rules /domain-set:%s/ -nameserver proxy', domain_set_name)
domain_rules_str = domain_rules_str .. " -no-cache -speed-check-mode none -d no"
local sets = {
"#4:" .. setflag .. "psw_gfw"
}
if NO_PROXY_IPV6 == "1" then
domain_rules_str = domain_rules_str .. " -address #6 " .. set_type .. " " .. table.concat(sets, ",")
else
table.insert(sets, "#6:" .. setflag .. "psw_gfw6")
domain_rules_str = domain_rules_str .. " -address -6 " .. set_type .. " " .. table.concat(sets, ",")
end
table.insert(tmp_lines, domain_rules_str)
insert_array_after(config_lines, tmp_lines, "#--5")
log(" - 防火墙域名表(gfwlist)使用分组:proxy")
end
--中国列表
if CHN_LIST ~= "0" and is_file_nonzero(RULES_PATH .. "/chnlist") then
local domain_set_name = "psw-chnlist"
tmp_lines = {
string.format("domain-set -name %s -file %s", domain_set_name, RULES_PATH .. "/chnlist")
}
if CHN_LIST == "direct" then
local sets = {
"#4:" .. setflag .. "psw_chn",
"#6:" .. setflag .. "psw_chn6"
}
local domain_rules_str = string.format('domain-rules /domain-set:%s/ -nameserver direct', domain_set_name) .. DIRECT_RULE_OPTIONS
domain_rules_str = domain_rules_str .. " " .. set_type .. " " .. table.concat(sets, ",")
table.insert(tmp_lines, domain_rules_str)
insert_array_after(config_lines, tmp_lines, "#--4")
log(" - 中国域名表(chnlist)使用分组:direct")
end
--回中国模式
if CHN_LIST == "proxy" then
local domain_rules_str = string.format('domain-rules /domain-set:%s/ -nameserver proxy', domain_set_name)
domain_rules_str = domain_rules_str .. " -no-cache -speed-check-mode none -d no"
local sets = {
"#4:" .. setflag .. "psw_chn"
}
if NO_PROXY_IPV6 == "1" then
domain_rules_str = domain_rules_str .. " -address #6 " .. set_type .. " " .. table.concat(sets, ",")
else
table.insert(sets, "#6:" .. setflag .. "psw_chn6")
domain_rules_str = domain_rules_str .. " -address -6 " .. set_type .. " " .. table.concat(sets, ",")
end
table.insert(tmp_lines, domain_rules_str)
insert_array_after(config_lines, tmp_lines, "#--4")
log(" - 中国域名表(chnlist)使用分组:proxy")
end
end
--分流规则
if IS_SHUNT_NODE and not only_global then
local direct_domain, lookup_direct_domain = {}, {}
local proxy_domain, lookup_proxy_domain = {}, {}
local black_domain, lookup_black_domain = {}, {}
local CACHE_FLAG_PATH = CACHE_RULES_PATH .. "/" .. FLAG
local shunt_direct_host = CACHE_FLAG_PATH .. "/shunt_direct_host"
local shunt_proxy_host = CACHE_FLAG_PATH .. "/shunt_proxy_host"
local shunt_black_host = CACHE_FLAG_PATH .. "/shunt_black_host"
local geosite_direct_arg, geosite_proxy_arg, geosite_black_arg = "", "", ""
local SHUNT_LIST = ""
local t = api.uci_get_c(NODE)
local default_node_id = t["default_node"] or "_direct"
api.uci_foreach_c("shunt_rules", function(s)
local _node_id = t[s[".name"]]
if _node_id and t["shunt_group"] == s.group then
if _node_id == "_default" then
_node_id = default_node_id
end
local domain_list = s.domain_list or ""
for line in string.gmatch(domain_list, "[^\r\n]+") do
if line ~= "" and not line:find("#") and not line:find("regexp:") and not line:find("ext:") and not line:find("rule-set:") and not line:find("rs:") then
if line:find("geosite:") then
line = string.match(line, ":([^:]+)$")
if _node_id == "_direct" then
geosite_direct_arg = geosite_direct_arg .. (geosite_direct_arg ~= "" and "," or "") .. line
elseif _node_id == "_blackhole" then
geosite_black_arg = geosite_black_arg .. (geosite_black_arg ~= "" and "," or "") .. line
else
geosite_proxy_arg = geosite_proxy_arg .. (geosite_proxy_arg ~= "" and "," or "") .. line
end
else
if line:find("domain:") or line:find("full:") then
line = string.match(line, ":([^:]+)$")
end
line = api.get_std_domain(line)
if line ~= "" and not line:find("#") then
if _node_id == "_direct" then
insert_unique(direct_domain, line, lookup_direct_domain)
elseif _node_id == "_blackhole" then
insert_unique(black_domain, line, lookup_black_domain)
else
insert_unique(proxy_domain, line, lookup_proxy_domain)
end
end
end
end
end
SHUNT_LIST = SHUNT_LIST .. domain_list .. (_node_id:sub(1, 1) == "_" and "not-node" or "node")
if _node_id ~= "_direct" and _node_id ~= "_blackhole" then
log(string.format(" - Sing-Box/Xray分流规则(%s)使用分组:proxy", s.remarks))
end
end
end)
local MD5_FILE = CACHE_FLAG_PATH .. "/md5.txt"
local cache_md5 = ""
local USE_CACHE = true
if fs.access(MD5_FILE) then
cache_md5 = fs.readfile(MD5_FILE)
end
local new_md5 = api.md5_string(SHUNT_LIST)
if cache_md5 == "" or new_md5 == "" or cache_md5 ~= new_md5 then
api.remove(CACHE_FLAG_PATH)
sys.call("mkdir -p %s" % CACHE_FLAG_PATH)
fs.writefile(MD5_FILE, new_md5)
USE_CACHE = false
end
if not is_file_nonzero(shunt_direct_host) then
if #direct_domain > 0 then
local f_out = io.open(shunt_direct_host, "w")
for i = 1, #direct_domain do
f_out:write(direct_domain[i] .. "\n")
end
f_out:close()
end
end
if not is_file_nonzero(shunt_proxy_host) then
if #proxy_domain > 0 then
local f_out = io.open(shunt_proxy_host, "w")
for i = 1, #proxy_domain do
f_out:write(proxy_domain[i] .. "\n")
end
f_out:close()
end
end
if not is_file_nonzero(shunt_black_host) then
if #black_domain > 0 then
local f_out = io.open(shunt_black_host, "w")
for i = 1, #black_domain do
f_out:write(black_domain[i] .. "\n")
end
f_out:close()
end
end
if not USE_CACHE and USE_GEOVIEW == "1" then
local ok = true
local function resolve(arg, dest)
if arg == "" then return end
local code, out = get_geosite(arg, dest)
if code ~= 0 then
ok = false
log(" - 解析[分流节点] Geosite 失败![" .. out .. "]")
end
end
resolve(geosite_direct_arg, shunt_direct_host)
resolve(geosite_proxy_arg, shunt_proxy_host)
resolve(geosite_black_arg, shunt_black_host)
if ok then
log(" - 解析[分流节点] Geosite 完成")
else
api.remove(MD5_FILE)
end
end
if USE_CACHE and USE_GEOVIEW == "1" and (geosite_direct_arg ~= "" or geosite_proxy_arg ~= "" or geosite_black_arg ~= "") then
log(" - [分流节点] Geosite 使用缓存结果,跳过重复解析")
end
if is_file_nonzero(shunt_direct_host) then
local domain_set_name = "psw-shunt-direct"
tmp_lines = {
string.format("domain-set -name %s -file %s", domain_set_name, shunt_direct_host)
}
local domain_rules_str = string.format('domain-rules /domain-set:%s/ -nameserver proxy', domain_set_name)
domain_rules_str = domain_rules_str .. " -no-cache -speed-check-mode none -d no"
local sets = {
"#4:" .. setflag .. "psw_shunt",
"#6:" .. setflag .. "psw_shunt6"
}
domain_rules_str = domain_rules_str .. " -address -6 " .. set_type .. " " .. table.concat(sets, ",")
table.insert(tmp_lines, domain_rules_str)
insert_array_after(config_lines, tmp_lines, "#--1")
end
if is_file_nonzero(shunt_proxy_host) then
local domain_set_name = "psw-shunt-proxy"
tmp_lines = {
string.format("domain-set -name %s -file %s", domain_set_name, shunt_proxy_host)
}
local domain_rules_str = string.format('domain-rules /domain-set:%s/ -nameserver proxy', domain_set_name)
domain_rules_str = domain_rules_str .. " -no-cache -speed-check-mode none -d no"
local sets = {
"#4:" .. setflag .. "psw_shunt",
"#6:" .. setflag .. "psw_shunt6"
}
domain_rules_str = domain_rules_str .. " -address -6 " .. set_type .. " " .. table.concat(sets, ",")
table.insert(tmp_lines, domain_rules_str)
insert_array_after(config_lines, tmp_lines, "#--2")
end
if is_file_nonzero(shunt_black_host) then
local domain_set_name = "psw-shunt-black"
tmp_lines = {
string.format("domain-set -name %s -file %s", domain_set_name, shunt_black_host)
}
local domain_rules_str = string.format('domain-rules /domain-set:%s/ -nameserver proxy', domain_set_name)
domain_rules_str = domain_rules_str .. " -no-cache -speed-check-mode none -d no"
local sets = {
"#4:" .. setflag .. "psw_shunt",
"#6:" .. setflag .. "psw_shunt6"
}
domain_rules_str = domain_rules_str .. " -address -6 " .. set_type .. " " .. table.concat(sets, ",")
table.insert(tmp_lines, domain_rules_str)
insert_array_after(config_lines, tmp_lines, "#--3")
end
end
if #config_lines > 0 then
local f_out = io.open(TMP_CONF_FILE, "w")
for i = 1, #config_lines do
line = config_lines[i]
if line ~= "" and not line:find("^#--") then
f_out:write(line .. "\n")
end
end
f_out:close()
end
if DEFAULT_DNS_GROUP then
log(string.format(" - 默认 DNS 分组:%s", DEFAULT_DNS_GROUP == AUTO_GROUP and "自动" or DEFAULT_DNS_GROUP))
end
if DNS_REDIRECT then
log(" - DNS 重定向直接使用 SmartDNS,DHCP 租约由 SmartDNS 自动加载。")
else
log(" - SmartDNS已作为Dnsmasq上游,如果你自行配置了错误的DNS流程,将会导致域名(直连/代理)分流失效!!!")
end