mirror of
https://github.com/caiwx86/small-packages.git
synced 2026-11-02 05:03:56 +08:00
update 2026-10-27 11:22:48
This commit is contained in:
1 parent
a4d6470641
commit
63a5dffe55
4907 files changed
+2036791
No files matched your search
@@ -0,0 +1,233 @@
|
||||
|
||||
config global
|
||||
option enabled '0'
|
||||
option socks_enabled '0'
|
||||
option node_socks_port '1070'
|
||||
option filter_proxy_ipv6 '1'
|
||||
option dns_shunt 'chinadns-ng'
|
||||
option dns_mode 'tcp'
|
||||
option remote_dns '1.1.1.1'
|
||||
list smartdns_remote_dns 'https://1.1.1.1/dns-query'
|
||||
option use_default_dns 'direct'
|
||||
option chinadns_ng_default_tag 'none'
|
||||
option dns_redirect '1'
|
||||
option use_direct_list '1'
|
||||
option use_proxy_list '1'
|
||||
option use_block_list '1'
|
||||
option use_gfw_list '1'
|
||||
option chn_list 'direct'
|
||||
option tcp_proxy_mode 'proxy'
|
||||
option udp_proxy_mode 'proxy'
|
||||
option localhost_proxy '1'
|
||||
option client_proxy '1'
|
||||
option acl_enable '0'
|
||||
option log_node '0'
|
||||
option loglevel 'error'
|
||||
option log_chinadns_ng '0'
|
||||
|
||||
config global_haproxy
|
||||
option balancing_enable '0'
|
||||
|
||||
config global_delay
|
||||
option start_daemon '1'
|
||||
option start_delay '60'
|
||||
|
||||
config global_forwarding
|
||||
option tcp_no_redir_ports 'disable'
|
||||
option udp_no_redir_ports 'disable'
|
||||
option tcp_proxy_drop_ports 'disable'
|
||||
option udp_proxy_drop_ports '443'
|
||||
option tcp_redir_ports '22,25,53,80,143,443,465,587,853,873,993,995,5222,8080,8443,9418'
|
||||
option udp_redir_ports '1:65535'
|
||||
option accept_icmp '0'
|
||||
option prefer_nft '1'
|
||||
option tcp_proxy_way 'redirect'
|
||||
option ipv6_tproxy '0'
|
||||
|
||||
config global_xray
|
||||
option sniffing_override_dest '0'
|
||||
|
||||
config global_singbox
|
||||
|
||||
config global_other
|
||||
option auto_detection_time 'tcping'
|
||||
option show_node_info '0'
|
||||
|
||||
config global_rules
|
||||
option chnlist_update '1'
|
||||
option chnroute_update '1'
|
||||
option chnroute6_update '1'
|
||||
option gfwlist_update '1'
|
||||
option geosite_update '0'
|
||||
option geoip_update '0'
|
||||
list gfwlist_url 'https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/gfw.txt'
|
||||
list chnroute_url 'https://ispip.clang.cn/all_cn.txt'
|
||||
list chnroute_url 'https://cdn.jsdelivr.net/gh/gaoyifan/china-operator-ip@ip-lists/china.txt'
|
||||
list chnroute6_url 'https://ispip.clang.cn/all_cn_ipv6.txt'
|
||||
list chnroute6_url 'https://cdn.jsdelivr.net/gh/gaoyifan/china-operator-ip@ip-lists/china6.txt'
|
||||
list chnlist_url 'https://cdn.jsdelivr.net/gh/felixonmars/dnsmasq-china-list/accelerated-domains.china.conf'
|
||||
list chnlist_url 'https://cdn.jsdelivr.net/gh/felixonmars/dnsmasq-china-list/apple.china.conf'
|
||||
option v2ray_location_asset '/usr/share/v2ray/'
|
||||
option geoip_url 'https://github.com/Loyalsoldier/geoip/releases/latest/download/geoip.dat'
|
||||
option geosite_url 'https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat'
|
||||
|
||||
config global_app
|
||||
option geoview_file '/usr/bin/geoview'
|
||||
option sing_box_file '/usr/bin/sing-box'
|
||||
option xray_file '/usr/bin/xray'
|
||||
option hysteria_file '/usr/bin/hysteria'
|
||||
|
||||
config global_subscribe
|
||||
option filter_keyword_mode '1'
|
||||
list filter_discard_list '距离下次重置剩余'
|
||||
list filter_discard_list '套餐到期'
|
||||
list filter_discard_list '过期时间'
|
||||
list filter_discard_list '剩余流量'
|
||||
list filter_discard_list 'QQ群'
|
||||
list filter_discard_list '官网'
|
||||
|
||||
config nodes 'myshunt'
|
||||
option remarks '分流总节点'
|
||||
option type 'Xray'
|
||||
option protocol '_shunt'
|
||||
option DirectGame '_direct'
|
||||
option ProxyGame '_default'
|
||||
option AIGC '_default'
|
||||
option Streaming '_default'
|
||||
option Proxy '_default'
|
||||
option Direct '_direct'
|
||||
option default_node '_direct'
|
||||
option domainStrategy 'IPOnDemand'
|
||||
|
||||
config shunt_rules 'DirectGame'
|
||||
option remarks 'DirectGame'
|
||||
option network 'tcp,udp'
|
||||
option domain_list '# 国内游戏geosite域名
|
||||
geosite:category-games@cn'
|
||||
option ip_list '# steam直连IP
|
||||
45.121.184.0/24
|
||||
103.10.124.0/23
|
||||
103.28.54.0/24
|
||||
146.66.152.0/24
|
||||
146.66.155.0/24
|
||||
153.254.86.0/24
|
||||
155.133.224.0/22
|
||||
155.133.230.0/24
|
||||
155.133.232.0/23
|
||||
155.133.234.0/24
|
||||
155.133.236.0/22
|
||||
155.133.240.0/23
|
||||
155.133.244.0/23
|
||||
155.133.246.0/24
|
||||
155.133.248.0/21
|
||||
162.254.192.0/21
|
||||
185.25.182.0/23
|
||||
190.217.32.0/22
|
||||
192.69.96.0/22
|
||||
205.196.6.0/24
|
||||
208.64.200.0/22
|
||||
208.78.164.0/22
|
||||
205.185.194.0/24'
|
||||
|
||||
config shunt_rules 'ProxyGame'
|
||||
option remarks 'ProxyGame'
|
||||
option network 'tcp,udp'
|
||||
option domain_list '# steam 商店/客服/聊天/网页布局/API/二维码/Google云同步 代理URL
|
||||
steamcommunity.com
|
||||
www.steamcommunity.com
|
||||
store.steampowered.com
|
||||
checkout.steampowered.com
|
||||
api.steampowered.com
|
||||
help.steampowered.com
|
||||
login.steampowered.com
|
||||
store.akamai.steamstatic.com
|
||||
steambroadcast.akamaized.net
|
||||
steamvideo-a.akamaihd.net
|
||||
steamusercontent-a.akamaihd.net
|
||||
steamstore-a.akamaihd.net
|
||||
steamcommunity-a.akamaihd.net
|
||||
steamcdn-a.akamaihd.net
|
||||
steamuserimages-a.akamaihd.net
|
||||
community.akamai.steamstatic.com
|
||||
avatars.akamai.steamstatic.com
|
||||
community.steamstatic.com
|
||||
cdn.akamai.steamstatic.com
|
||||
avatars.steamstatic.com
|
||||
shared.akamai.steamstatic.com
|
||||
clan.akamai.steamstatic.com
|
||||
cdn.cloudflare.steamstatic.com
|
||||
community.cloudflare.steamstatic.com
|
||||
store.cloudflare.steamstatic.com
|
||||
avatars.cloudflare.steamstatic.com
|
||||
clan.cloudflare.steamstatic.com
|
||||
shared.cloudflare.steamstatic.com
|
||||
steam-chat.com
|
||||
steamcloud-ugc.storage.googleapis.com
|
||||
steamcloud-eu-ams.storage.googleapis.com
|
||||
steamcloud-eu-fra.storage.googleapis.com
|
||||
steamcloud-finland.storage.googleapis.com
|
||||
steamcloud-saopaulo.storage.googleapis.com
|
||||
steamcloud-singapore.storage.googleapis.com
|
||||
steamcloud-sydney.storage.googleapis.com
|
||||
steamcloud-taiwan.storage.googleapis.com
|
||||
steamcloud-eu.storage.googleapis.com
|
||||
domain:blizzard.com
|
||||
#Steam支付
|
||||
domain:checkout.steampowered.com
|
||||
domain:globalapi.smart2pay.com
|
||||
geosite:category-games'
|
||||
|
||||
config shunt_rules 'AIGC'
|
||||
option remarks 'AIGC'
|
||||
option network 'tcp,udp'
|
||||
option domain_list 'geosite:category-ai-!cn
|
||||
geosite:apple-intelligence'
|
||||
|
||||
config shunt_rules 'Streaming'
|
||||
option remarks 'Streaming'
|
||||
option network 'tcp,udp'
|
||||
option domain_list 'geosite:netflix
|
||||
geosite:disney'
|
||||
|
||||
config shunt_rules 'Proxy'
|
||||
option remarks 'Proxy'
|
||||
option network 'tcp,udp'
|
||||
option domain_list 'geosite:geolocation-!cn'
|
||||
option ip_list '149.154.160.0/20
|
||||
91.108.4.0/22
|
||||
91.108.56.0/24
|
||||
109.239.140.0/24
|
||||
67.198.55.0/24
|
||||
8.8.4.4
|
||||
8.8.8.8
|
||||
208.67.222.222
|
||||
208.67.220.220
|
||||
1.1.1.1
|
||||
1.1.1.2
|
||||
1.0.0.1
|
||||
9.9.9.9
|
||||
149.112.112.112
|
||||
2001:67c:4e8::/48
|
||||
2001:b28:f23c::/48
|
||||
2001:b28:f23d::/48
|
||||
2001:b28:f23f::/48
|
||||
2001:b28:f242::/48
|
||||
2001:4860:4860::8888
|
||||
2001:4860:4860::8844
|
||||
2606:4700:4700::1111
|
||||
2606:4700:4700::1001'
|
||||
|
||||
config shunt_rules 'Direct'
|
||||
option remarks 'Direct'
|
||||
option network 'tcp,udp'
|
||||
option domain_list 'geosite:cn'
|
||||
option ip_list '223.5.5.5/32
|
||||
223.6.6.6/32
|
||||
119.29.29.29/32
|
||||
180.76.76.76/32
|
||||
114.114.114.114/32
|
||||
114.114.115.115/32
|
||||
1.12.12.12/32
|
||||
120.53.53.53/32
|
||||
geoip:cn
|
||||
geoip:private'
|
||||
+1963
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,709 @@
|
||||
#!/usr/bin/lua
|
||||
|
||||
-- Copyright(c): lwb1978 2025–2027
|
||||
|
||||
local api = require("luci.passwall.api")
|
||||
local base64 = api.base64Encode
|
||||
local json = api.jsonc
|
||||
|
||||
local domain_redir = {}
|
||||
|
||||
local function urlencode(value)
|
||||
return api.UrlEncode(value == nil and "" or tostring(value))
|
||||
end
|
||||
|
||||
local function add_param(params, key, value)
|
||||
if value ~= nil and value ~= "" then
|
||||
if type(value) == "boolean" then value = value and "1" or "0" end
|
||||
params[#params + 1] = key .. "=" .. urlencode(value)
|
||||
end
|
||||
end
|
||||
|
||||
local function build_ech(node)
|
||||
local opts = node["ech-opts"]
|
||||
if type(opts) ~= "table" or opts.enable ~= true then return nil end
|
||||
if opts.config and opts.config ~= "" then return opts.config end
|
||||
local name = opts["query-server-name"]
|
||||
if not name or name == "" then name = node.servername or node.sni or node.server end
|
||||
if name == "" then name = node.server end
|
||||
return name .. "+https://223.5.5.5/dns-query"
|
||||
end
|
||||
|
||||
local function bandwidth_mbps(value)
|
||||
if value == nil then return nil end
|
||||
local number = tonumber(value)
|
||||
if number then return number end
|
||||
local amount, prefix, unit = tostring(value):match("^%s*(%d+)%s*([KMGT]?)([Bb])ps%s*$")
|
||||
local scales = { [""] = 1, K = 1000, M = 1000000, G = 1000000000, T = 1000000000000 }
|
||||
if not amount then return nil end
|
||||
return tonumber(amount) * scales[prefix] * (unit == "B" and 8 or 1) / 1000000
|
||||
end
|
||||
|
||||
local function inherit(base, overrides)
|
||||
local result = {}
|
||||
for key, value in pairs(base or {}) do result[key] = value end
|
||||
for key, value in pairs(overrides or {}) do result[key] = value end
|
||||
return result
|
||||
end
|
||||
|
||||
local function build_xhttp_extra(opts)
|
||||
local extra = {}
|
||||
local fields = {
|
||||
headers = "headers", ["x-padding-bytes"] = "xPaddingBytes",
|
||||
["no-grpc-header"] = "noGRPCHeader", ["x-padding-obfs-mode"] = "xPaddingObfsMode",
|
||||
["x-padding-key"] = "xPaddingKey", ["x-padding-header"] = "xPaddingHeader",
|
||||
["x-padding-placement"] = "xPaddingPlacement", ["x-padding-method"] = "xPaddingMethod",
|
||||
["uplink-http-method"] = "uplinkHTTPMethod", ["session-placement"] = "sessionIDPlacement",
|
||||
["session-key"] = "sessionIDKey", ["session-table"] = "sessionIDTable",
|
||||
["session-length"] = "sessionIDLength", ["seq-placement"] = "seqPlacement", ["seq-key"] = "seqKey",
|
||||
["uplink-data-placement"] = "uplinkDataPlacement", ["uplink-data-key"] = "uplinkDataKey",
|
||||
["uplink-chunk-size"] = "uplinkChunkSize", ["sc-max-each-post-bytes"] = "scMaxEachPostBytes",
|
||||
["sc-min-posts-interval-ms"] = "scMinPostsIntervalMs"
|
||||
}
|
||||
for source, target in pairs(fields) do extra[target] = opts[source] end
|
||||
-- jsonc treats empty tables as arrays and repeated table references as null.
|
||||
if type(extra.headers) == "table" then
|
||||
extra.headers = next(extra.headers) and inherit(extra.headers) or nil
|
||||
end
|
||||
local reuse = opts["reuse-settings"]
|
||||
if reuse then
|
||||
local xmux = {}
|
||||
local ranges = {
|
||||
["max-concurrency"] = "maxConcurrency", ["max-connections"] = "maxConnections",
|
||||
["c-max-reuse-times"] = "cMaxReuseTimes", ["h-max-request-times"] = "hMaxRequestTimes",
|
||||
["h-max-reusable-secs"] = "hMaxReusableSecs"
|
||||
}
|
||||
for source, target in pairs(ranges) do
|
||||
local value = reuse[source]
|
||||
if value ~= nil then xmux[target] = tonumber(value) or value end
|
||||
end
|
||||
xmux.hKeepAlivePeriod = tonumber(reuse["h-keep-alive-period"])
|
||||
if next(xmux) then extra.xmux = xmux end
|
||||
end
|
||||
return extra
|
||||
end
|
||||
|
||||
local function host_format(host)
|
||||
if not host then return "" end
|
||||
local str = host:match("%[(.-)%]") or host
|
||||
if api.datatypes.ip6addr(str) then
|
||||
return "[" .. str .. "]"
|
||||
end
|
||||
return host
|
||||
end
|
||||
|
||||
local function build_alpn(alpn) -- 排序+去重
|
||||
if not alpn then return nil end
|
||||
|
||||
local seen = {}
|
||||
local order = { "h3", "h2", "http/1.1" }
|
||||
|
||||
if type(alpn) == "table" then
|
||||
for _, v in ipairs(alpn) do
|
||||
if v then seen[v] = true end
|
||||
end
|
||||
else
|
||||
seen[tostring(alpn)] = true
|
||||
end
|
||||
|
||||
local t = {}
|
||||
for _, v in ipairs(order) do
|
||||
if seen[v] then table.insert(t, v) end
|
||||
end
|
||||
|
||||
if #t == 0 then return nil end
|
||||
return table.concat(t, ",")
|
||||
end
|
||||
|
||||
local function build_common(node)
|
||||
local o = {}
|
||||
|
||||
o.server = host_format(node.server)
|
||||
o.port = node.port
|
||||
o.name = node.name
|
||||
|
||||
-- ===== TLS =====
|
||||
o.tls = {}
|
||||
|
||||
if node["reality-opts"] then
|
||||
o.tls.security = "reality"
|
||||
o.tls.pbk = node["reality-opts"]["public-key"]
|
||||
o.tls.sid = node["reality-opts"]["short-id"]
|
||||
elseif node.tls then
|
||||
o.tls.security = "tls"
|
||||
end
|
||||
|
||||
o.tls.sni = node.servername or node.sni
|
||||
o.tls.fp = node["client-fingerprint"]
|
||||
o.tls.pcs = node.fingerprint
|
||||
o.tls.vcn = node["name-cert-verify"]
|
||||
o.tls.insecure = node["skip-cert-verify"] == true
|
||||
|
||||
o.tls.alpn = build_alpn(node.alpn)
|
||||
|
||||
o.tls.ech = build_ech(node)
|
||||
|
||||
-- ===== transport =====
|
||||
o.transport = {}
|
||||
local net = node.network or "tcp"
|
||||
o.transport.type = net
|
||||
|
||||
local function get_first(v)
|
||||
if type(v) == "table" then return v[1] end
|
||||
return v
|
||||
end
|
||||
|
||||
if net == "ws" then
|
||||
local opts = node["ws-opts"]
|
||||
if opts then
|
||||
local path = opts.path or "/"
|
||||
if opts["v2ray-http-upgrade"] ~= true and opts["max-early-data"] then
|
||||
path = path .. (path:find("?", 1, true) and "&ed=" or "?ed=") .. opts["max-early-data"]
|
||||
end
|
||||
if opts["v2ray-http-upgrade"] ~= true and opts["early-data-header-name"] then
|
||||
path = path .. (path:find("?", 1, true) and "&eh=" or "?eh=") .. opts["early-data-header-name"]
|
||||
end
|
||||
o.transport.path = path
|
||||
o.transport.host = opts.headers and opts.headers.Host
|
||||
if opts["v2ray-http-upgrade"] == true then o.transport.type = "httpupgrade" end
|
||||
end
|
||||
|
||||
elseif net == "grpc" then
|
||||
local opts = node["grpc-opts"]
|
||||
if opts then
|
||||
o.transport.serviceName = opts["grpc-service-name"]
|
||||
end
|
||||
|
||||
elseif net == "http" then
|
||||
-- Use TCP + HTTP camouflage so each core selects its matching transport.
|
||||
o.transport.type = "tcp"
|
||||
o.transport.headerType = "http"
|
||||
o.transport.host = node.server
|
||||
o.transport.path = "/"
|
||||
local opts = node["http-opts"]
|
||||
if opts then
|
||||
-- Clash http-opts carries the camouflage Host under headers.Host
|
||||
-- (a list), like ws-opts; opts.host is not standard for http.
|
||||
o.transport.host = get_first(opts.host) or (opts.headers and get_first(opts.headers.Host)) or o.transport.host
|
||||
o.transport.path = get_first(opts.path) or o.transport.path
|
||||
end
|
||||
|
||||
elseif net == "h2" then
|
||||
local opts = node["h2-opts"]
|
||||
if opts then
|
||||
o.transport.host = get_first(opts.host)
|
||||
o.transport.path = opts.path
|
||||
end
|
||||
|
||||
elseif net == "xhttp" then
|
||||
local opts = node["xhttp-opts"]
|
||||
if opts then
|
||||
o.transport.host = opts.host
|
||||
o.transport.path = opts.path
|
||||
o.transport.mode = opts.mode
|
||||
|
||||
local extra = build_xhttp_extra(opts)
|
||||
|
||||
-- download-settings
|
||||
if opts["download-settings"] then
|
||||
local d = opts["download-settings"]
|
||||
local effective = inherit(node, d)
|
||||
local ds = {
|
||||
address = domain_redir[effective.server] or effective.server,
|
||||
port = effective.port,
|
||||
network = "xhttp",
|
||||
security = effective.tls and "tls" or "none"
|
||||
}
|
||||
local sni = effective.servername or effective.sni or effective.server
|
||||
local alpn = build_alpn(effective.alpn)
|
||||
if effective.tls or effective["reality-opts"] then
|
||||
ds.tlsSettings = {
|
||||
serverName = sni,
|
||||
allowInsecure = effective["skip-cert-verify"] == true,
|
||||
fingerprint = effective["client-fingerprint"],
|
||||
pinnedPeerCertSha256 = effective.fingerprint and api.sha256_sb_xray(effective.fingerprint) or nil,
|
||||
verifyPeerCertByName = effective["name-cert-verify"],
|
||||
alpn = alpn and api.split(alpn, ",") or nil,
|
||||
echConfigList = build_ech(effective)
|
||||
}
|
||||
local reality = effective["reality-opts"]
|
||||
if reality then
|
||||
ds.security = "reality"
|
||||
ds.realitySettings = {
|
||||
serverName = sni, fingerprint = effective["client-fingerprint"] or "chrome",
|
||||
publicKey = reality["public-key"], shortId = reality["short-id"] or ""
|
||||
}
|
||||
ds.tlsSettings = nil
|
||||
end
|
||||
end
|
||||
local xs = build_xhttp_extra(inherit(opts, d))
|
||||
xs.path = d.path or opts.path or "/"
|
||||
xs.host = d.host or opts.host or sni
|
||||
xs.mode = opts.mode or "auto"
|
||||
ds.xhttpSettings = xs
|
||||
|
||||
extra.downloadSettings = ds
|
||||
end
|
||||
|
||||
if next(extra) then
|
||||
o.transport.extra = json.stringify(extra)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return o
|
||||
end
|
||||
|
||||
-- VLESS
|
||||
local function encode_vless(node)
|
||||
local o = build_common(node)
|
||||
|
||||
local link = "vless://" .. node.uuid .. "@" .. o.server .. ":" .. o.port
|
||||
local p = {}
|
||||
|
||||
if type(node.flow) == "string" and node.flow:sub(1, 5) == "xtls-" then add_param(p, "flow", node.flow) end
|
||||
add_param(p, "encryption", node.encryption)
|
||||
|
||||
-- TLS
|
||||
add_param(p, "security", o.tls.security)
|
||||
add_param(p, "pbk", o.tls.pbk)
|
||||
add_param(p, "sid", o.tls.sid)
|
||||
add_param(p, "sni", o.tls.sni)
|
||||
add_param(p, "fp", o.tls.fp)
|
||||
add_param(p, "alpn", o.tls.alpn)
|
||||
add_param(p, "ech", o.tls.ech)
|
||||
add_param(p, "pcs", o.tls.pcs)
|
||||
add_param(p, "vcn", o.tls.vcn)
|
||||
add_param(p, "allowInsecure", o.tls.insecure)
|
||||
|
||||
-- transport
|
||||
add_param(p, "type", o.transport.type)
|
||||
add_param(p, "headerType", o.transport.headerType)
|
||||
add_param(p, "host", o.transport.host)
|
||||
add_param(p, "path", o.transport.path)
|
||||
add_param(p, "serviceName", o.transport.serviceName)
|
||||
add_param(p, "mode", o.transport.mode)
|
||||
add_param(p, "extra", o.transport.extra)
|
||||
|
||||
if #p > 0 then
|
||||
link = link .. "?" .. table.concat(p, "&")
|
||||
end
|
||||
|
||||
return 0, link .. "#" .. urlencode(o.name or "")
|
||||
end
|
||||
|
||||
-- Trojan
|
||||
local function encode_trojan(node)
|
||||
local o = build_common(node)
|
||||
|
||||
local link = "trojan://" .. urlencode(node.password) .. "@" .. o.server .. ":" .. o.port
|
||||
local p = {}
|
||||
|
||||
add_param(p, "security", o.tls.security)
|
||||
add_param(p, "pbk", o.tls.pbk)
|
||||
add_param(p, "sid", o.tls.sid)
|
||||
add_param(p, "sni", o.tls.sni)
|
||||
add_param(p, "fp", o.tls.fp)
|
||||
add_param(p, "alpn", o.tls.alpn)
|
||||
add_param(p, "ech", o.tls.ech)
|
||||
add_param(p, "pcs", o.tls.pcs)
|
||||
add_param(p, "vcn", o.tls.vcn)
|
||||
add_param(p, "allowInsecure", o.tls.insecure)
|
||||
add_param(p, "type", o.transport.type)
|
||||
add_param(p, "headerType", o.transport.headerType)
|
||||
add_param(p, "host", o.transport.host)
|
||||
add_param(p, "path", o.transport.path)
|
||||
add_param(p, "serviceName", o.transport.serviceName)
|
||||
|
||||
if #p > 0 then
|
||||
link = link .. "?" .. table.concat(p, "&")
|
||||
end
|
||||
|
||||
return 0, link .. "#" .. urlencode(o.name or "")
|
||||
end
|
||||
|
||||
-- VMess
|
||||
local function encode_vmess(node)
|
||||
local o = build_common(node)
|
||||
|
||||
local obj = {
|
||||
v = "2",
|
||||
ps = o.name,
|
||||
add = node.server,
|
||||
port = tostring(node.port),
|
||||
id = node.uuid,
|
||||
aid = tostring(node.alterId or 0),
|
||||
net = o.transport.type,
|
||||
security = node.cipher,
|
||||
scy = node.cipher,
|
||||
type = o.transport.headerType or "none",
|
||||
host = o.transport.host or "",
|
||||
path = urlencode(o.transport.path or ""),
|
||||
tls = o.tls.security == "tls" and "tls" or "",
|
||||
sni = o.tls.sni,
|
||||
alpn = o.tls.alpn,
|
||||
fp = o.tls.fp,
|
||||
ech = o.tls.ech,
|
||||
pcs = o.tls.pcs,
|
||||
vcn = o.tls.vcn,
|
||||
insecure = o.tls.insecure and "1" or "0",
|
||||
tfo = node.tfo and "1" or "0"
|
||||
}
|
||||
|
||||
if o.transport.type == "grpc" then
|
||||
obj.path = urlencode(o.transport.serviceName or "")
|
||||
end
|
||||
|
||||
return 0, "vmess://" .. base64(json.stringify(obj))
|
||||
end
|
||||
|
||||
-- SS
|
||||
local function encode_ss(node)
|
||||
local ss_method = {
|
||||
["none"]=1, ["aes-128-gcm"]=1, ["aes-192-gcm"]=1, ["aes-256-gcm"]=1, ["chacha20-ietf-poly1305"]=1, ["xchacha20-ietf-poly1305"]=1, ["2022-blake3-aes-128-gcm"]=1, ["2022-blake3-aes-256-gcm"]=1, ["2022-blake3-chacha20-poly1305"]=1,
|
||||
["aes-128-ctr"]=1, ["aes-192-ctr"]=1, ["aes-256-ctr"]=1, ["aes-128-cfb"]=1, ["aes-192-cfb"]=1, ["aes-256-cfb"]=1, ["rc4-md5"]=1, ["chacha20-ietf"]=1, ["xchacha20"]=1,
|
||||
}
|
||||
if not ss_method[node.cipher or ""] then
|
||||
return 1, "订阅转换 → 丢弃 SS 节点:" .. (node.name or "") .. ",因 Core 不支持 " .. (node.cipher or "") .. " 加密方式"
|
||||
end
|
||||
|
||||
local userinfo = node.cipher .. ":" .. node.password
|
||||
local base = userinfo .. "@" .. host_format(node.server) .. ":" .. node.port
|
||||
local link = "ss://" .. base64(base)
|
||||
|
||||
local p = {}
|
||||
|
||||
if node.udp then add_param(p, "udp", true) end
|
||||
if node["udp-over-tcp"] then add_param(p, "uot", true) end
|
||||
|
||||
if node.plugin then
|
||||
local plugin = (node.plugin == "obfs") and "obfs-local" or node.plugin
|
||||
if plugin == "shadow-tls" then
|
||||
local shadow_tls = base64(json.stringify(node["plugin-opts"] or {}))
|
||||
add_param(p, "shadow-tls", shadow_tls)
|
||||
elseif plugin == "obfs-local" or plugin == "v2ray-plugin" then
|
||||
local opts = {}
|
||||
for k, v in pairs(node["plugin-opts"] or {}) do
|
||||
if plugin == "obfs-local" then
|
||||
if k == "mode" then k = "obfs" end
|
||||
if k == "host" then k = "obfs-host" end
|
||||
elseif plugin == "v2ray-plugin" then
|
||||
if k == "mode" and v == "websocket" then
|
||||
v = nil
|
||||
elseif type(v) == "boolean" then
|
||||
if v == true then
|
||||
table.insert(opts, k)
|
||||
end
|
||||
v = nil
|
||||
end
|
||||
end
|
||||
if v ~= nil then
|
||||
if type(v) == "boolean" then
|
||||
v = v and "1" or "0"
|
||||
end
|
||||
table.insert(opts, k .. "=" .. v)
|
||||
end
|
||||
end
|
||||
if #opts > 0 then plugin = plugin .. ";" .. table.concat(opts, ";") end
|
||||
add_param(p, "plugin", plugin)
|
||||
else
|
||||
return 1, "订阅转换 → 丢弃 SS 节点:" .. (node.name or "") .. ",因 Core 不支持 " .. plugin .. " 插件"
|
||||
end
|
||||
end
|
||||
|
||||
if #p > 0 then
|
||||
link = link .. "?" .. table.concat(p, "&")
|
||||
end
|
||||
|
||||
return 0, link .. "#" .. urlencode(node.name or "")
|
||||
end
|
||||
|
||||
-- Hysteria
|
||||
local function encode_hysteria(node)
|
||||
if node.protocol and node.protocol ~= "udp" then
|
||||
return 1, "订阅转换 → 丢弃 Hysteria 节点:" .. node.name .. ",当前 Core 不支持 " .. node.protocol .. " 传输"
|
||||
end
|
||||
local link = "hysteria://" .. host_format(node.server) .. ":" .. (node.port or "")
|
||||
local p = {}
|
||||
|
||||
add_param(p, "auth", node["auth-str"])
|
||||
add_param(p, "mport", node.ports)
|
||||
add_param(p, "obfsParam", node.obfs)
|
||||
add_param(p, "sni", node.sni)
|
||||
add_param(p, "upmbps", bandwidth_mbps(node.up))
|
||||
add_param(p, "downmbps", bandwidth_mbps(node.down))
|
||||
add_param(p, "insecure", node["skip-cert-verify"] == true)
|
||||
add_param(p, "vcn", node["name-cert-verify"])
|
||||
add_param(p, "pinSHA256", node.fingerprint)
|
||||
|
||||
add_param(p, "alpn", build_alpn(node.alpn))
|
||||
|
||||
if #p > 0 then
|
||||
link = link .. "?" .. table.concat(p, "&")
|
||||
end
|
||||
|
||||
return 0, link .. "#" .. urlencode(node.name or "")
|
||||
end
|
||||
|
||||
-- Hysteria2
|
||||
local function encode_hysteria2(node)
|
||||
-- Hopping subscriptions may omit port; keep a valid fallback for all cores.
|
||||
local port = node.port or tostring(node.ports or ""):match("^%s*(%d+)")
|
||||
local link = "hysteria2://" .. urlencode(node.password or "") .. "@" .. host_format(node.server) .. ":" .. port
|
||||
local p = {}
|
||||
|
||||
add_param(p, "mport", node.ports)
|
||||
add_param(p, "hopInterval", node["hop-interval"])
|
||||
add_param(p, "obfs", node.obfs)
|
||||
add_param(p, "obfs-password", node["obfs-password"])
|
||||
add_param(p, "minPacketSize", node["obfs-min-packet-size"])
|
||||
add_param(p, "maxPacketSize", node["obfs-max-packet-size"])
|
||||
add_param(p, "upmbps", bandwidth_mbps(node.up))
|
||||
add_param(p, "downmbps", bandwidth_mbps(node.down))
|
||||
add_param(p, "ech", build_ech(node))
|
||||
add_param(p, "vcn", node["name-cert-verify"])
|
||||
add_param(p, "sni", node.sni)
|
||||
add_param(p, "insecure", node["skip-cert-verify"] == true)
|
||||
add_param(p, "pinSHA256", node.fingerprint)
|
||||
|
||||
if #p > 0 then
|
||||
link = link .. "?" .. table.concat(p, "&")
|
||||
end
|
||||
|
||||
return 0, link .. "#" .. urlencode(node.name or "")
|
||||
end
|
||||
|
||||
-- TUIC
|
||||
local function encode_tuic(node)
|
||||
if node.token then
|
||||
return 1, "订阅转换 → 丢弃 TUIC 节点:" .. node.name .. ",当前 Core 不支持 TUIC v4"
|
||||
end
|
||||
if not node.uuid or node.password == nil then
|
||||
return 1, "订阅转换 → 丢弃 TUIC 节点:" .. node.name .. ",认证信息不完整。"
|
||||
end
|
||||
local link = "tuic://" .. urlencode(node.uuid) .. ":" .. urlencode(node.password) .. "@" .. host_format(node.server) .. ":" .. node.port
|
||||
local p = {}
|
||||
|
||||
add_param(p, "congestion_control", node["congestion-controller"])
|
||||
add_param(p, "alpn", build_alpn(node.alpn))
|
||||
add_param(p, "sni", node.sni)
|
||||
if node["disable-sni"] then add_param(p, "disable_sni", true) end
|
||||
add_param(p, "allowInsecure", node["skip-cert-verify"] == true)
|
||||
add_param(p, "udp_relay_mode", node["udp-relay-mode"])
|
||||
add_param(p, "pcs", node.fingerprint)
|
||||
add_param(p, "vcn", node["name-cert-verify"])
|
||||
|
||||
if #p > 0 then
|
||||
link = link .. "?" .. table.concat(p, "&")
|
||||
end
|
||||
|
||||
return 0, link .. "#" .. urlencode(node.name or "")
|
||||
end
|
||||
|
||||
-- AnyTLS
|
||||
local function encode_anytls(node)
|
||||
local err_msg
|
||||
if node["shadow-tls-opts"] then
|
||||
err_msg = "ShadowTLS"
|
||||
elseif node["restls-opts"] then
|
||||
err_msg = "ResTLS"
|
||||
elseif node["jls-opts"] then
|
||||
err_msg = "JLS"
|
||||
end
|
||||
if err_msg then
|
||||
err_msg = "订阅转换 → 丢弃 AnyTLS 节点:" .. (node.name or "") .. ",因 Sing-Box 不支持 AnyTLS + " .. err_msg
|
||||
return 1, err_msg
|
||||
end
|
||||
|
||||
local o = build_common(node)
|
||||
|
||||
local link = "anytls://" .. urlencode(node.password or "") .. "@" .. host_format(node.server) .. ":" .. node.port
|
||||
local p = {}
|
||||
|
||||
add_param(p, "security", o.tls.security)
|
||||
add_param(p, "pbk", o.tls.pbk)
|
||||
add_param(p, "sid", o.tls.sid)
|
||||
add_param(p, "sni", o.tls.sni)
|
||||
add_param(p, "alpn", o.tls.alpn)
|
||||
add_param(p, "fp", o.tls.fp)
|
||||
add_param(p, "ech", o.tls.ech)
|
||||
add_param(p, "pcs", o.tls.pcs)
|
||||
add_param(p, "insecure", o.tls.insecure)
|
||||
|
||||
if #p > 0 then
|
||||
link = link .. "?" .. table.concat(p, "&")
|
||||
end
|
||||
|
||||
return 0, link .. "#" .. urlencode(node.name or "")
|
||||
end
|
||||
|
||||
-- SSR
|
||||
local function encode_ssr(node)
|
||||
local link = host_format(node.server) .. ":" .. node.port .. ":" .. (node.protocol or "") .. ":" ..
|
||||
(node.cipher or "") .. ":" .. (node.obfs or "") .. ":" .. base64(node.password)
|
||||
local p = {}
|
||||
|
||||
if node["obfs-param"] then table.insert(p, "obfsparam=" .. base64(node["obfs-param"])) end
|
||||
if node["protocol-param"] then table.insert(p, "protoparam=" .. base64(node["protocol-param"])) end
|
||||
table.insert(p, "remarks=" .. base64(node.name))
|
||||
|
||||
if #p > 0 then
|
||||
link = link .. "?" .. table.concat(p, "&")
|
||||
end
|
||||
|
||||
return 0, "ssr://" .. base64(link)
|
||||
end
|
||||
|
||||
-- snell
|
||||
local function encode_snell(node)
|
||||
local err_msg
|
||||
local obfs = node["obfs-opts"] or {}
|
||||
if obfs.mode and obfs.mode ~= "http" and obfs.mode ~= "none" then
|
||||
err_msg = obfs.mode
|
||||
end
|
||||
if err_msg then
|
||||
err_msg = "订阅转换 → 丢弃 Snell 节点:" .. (node.name or "") .. ",因 Sing-Box 不支持 Snell + " .. err_msg
|
||||
return 1, err_msg
|
||||
end
|
||||
local version = node.version and tonumber(node.version) or 4
|
||||
version = (version == 5) and 4 or version
|
||||
if version < 4 then
|
||||
err_msg = "订阅转换 → 丢弃 Snell 节点:" .. (node.name or "") .. ",因 Sing-Box 不支持 Snell 版本小于 4"
|
||||
return 1, err_msg
|
||||
end
|
||||
|
||||
local link = "snell://" .. host_format(node.server) .. ":" .. node.port
|
||||
local p = {}
|
||||
|
||||
add_param(p, "psk", node.psk)
|
||||
add_param(p, "version", version)
|
||||
if obfs.mode == "http" then
|
||||
add_param(p, "obfs", "http")
|
||||
add_param(p, "obfs-host", obfs.host)
|
||||
end
|
||||
add_param(p, "reuse", node.reuse and "1" or "0")
|
||||
|
||||
if #p > 0 then
|
||||
link = link .. "?" .. table.concat(p, "&")
|
||||
end
|
||||
|
||||
return 0, link .. "#" .. urlencode(node.name or "")
|
||||
end
|
||||
|
||||
local function encode_node(node)
|
||||
if type(node) ~= "table" or not node.type or not node.name then
|
||||
return 1, "订阅转换 → 跳过格式不完整的节点。"
|
||||
end
|
||||
if not node.server or node.server == "" or not (node.port or (node.type == "hysteria2" and node.ports)) then
|
||||
return 1, "订阅转换 → 丢弃节点:" .. tostring(node.name) .. ",节点信息不完整。"
|
||||
end
|
||||
node = inherit(node)
|
||||
|
||||
local t = node.type
|
||||
if t == "hysteria" or t == "hysteria2" then
|
||||
local up, down = bandwidth_mbps(node.up), bandwidth_mbps(node.down)
|
||||
if (node.up ~= nil and up == nil) or (node.down ~= nil and down == nil) then
|
||||
return 1, "订阅转换 → 丢弃节点:" .. tostring(node.name) .. ",带宽格式不正确。"
|
||||
end
|
||||
node.up, node.down = up, down
|
||||
end
|
||||
|
||||
node.server = domain_redir[node.server] or node.server
|
||||
|
||||
if t == "vless" then return encode_vless(node)
|
||||
elseif t == "trojan" then return encode_trojan(node)
|
||||
elseif t == "vmess" then return encode_vmess(node)
|
||||
elseif t == "ss" then return encode_ss(node)
|
||||
elseif t == "hysteria" then return encode_hysteria(node)
|
||||
elseif t == "hysteria2" then return encode_hysteria2(node)
|
||||
elseif t == "tuic" then return encode_tuic(node)
|
||||
elseif t == "anytls" then return encode_anytls(node)
|
||||
elseif t == "ssr" then return encode_ssr(node)
|
||||
elseif t == "snell" then return encode_snell(node)
|
||||
else api.log("订阅转换 → 丢弃不支持的节点:" .. node.name .. ",节点类型:" .. t)
|
||||
end
|
||||
end
|
||||
|
||||
function parseClashNode(raw, remark)
|
||||
if not raw then return "" end
|
||||
domain_redir = {}
|
||||
local ok, lyaml = pcall(require, "lyaml")
|
||||
if not ok then return raw end
|
||||
|
||||
local parsed, data = pcall(lyaml.load, raw)
|
||||
if not parsed then
|
||||
api.log('订阅转换 → YAML 解析失败:' .. tostring(remark or ""))
|
||||
return ""
|
||||
end
|
||||
if not data or type(data) ~= "table" then return raw end
|
||||
if type(data.proxies) ~= "table" then return "" end
|
||||
|
||||
api.log('检测到 Clash 订阅,正在进行转换 ...')
|
||||
|
||||
-- Some airports use hosts domain redirect to disguise the real node domain name.
|
||||
for k, v in pairs(type(data.hosts) == "table" and data.hosts or {}) do
|
||||
if type(k) == "string" and type(v) == "string" then
|
||||
if api.datatypes.hostname(k) and api.datatypes.hostname(v) then
|
||||
domain_redir[k] = v
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local links = {}
|
||||
for _, node in ipairs(data.proxies) do
|
||||
local converted, err, link = pcall(encode_node, node)
|
||||
if not converted then
|
||||
local name = type(node) == "table" and node.name or ""
|
||||
api.log('订阅转换 → 丢弃节点:' .. tostring(name or "") .. ',转换失败,已跳过。')
|
||||
elseif err == 1 and link then
|
||||
api.log(link)
|
||||
elseif link then
|
||||
table.insert(links, link)
|
||||
end
|
||||
end
|
||||
|
||||
return #links > 0 and table.concat(links, "\n") or ""
|
||||
end
|
||||
|
||||
function parse_clash_sub_info(headers)
|
||||
local userinfo = headers:match("[Ss]ubscription%-userinfo:%s*([^\r\n]+)")
|
||||
if not userinfo then return nil end
|
||||
|
||||
local upload = tonumber(userinfo:match("upload=(%d+)")) or 0
|
||||
local download = tonumber(userinfo:match("download=(%d+)")) or 0
|
||||
local total = tonumber(userinfo:match("total=(%d+)")) or 0
|
||||
local expire = tonumber(userinfo:match("expire=(%d+)"))
|
||||
local remain = total - (upload + download)
|
||||
if remain < 0 then remain = 0 end
|
||||
|
||||
local function format_size(bytes)
|
||||
local units = { "B", "KB", "MB", "GB", "TB", "PB" }
|
||||
local i = 1
|
||||
while bytes >= 1024 and i < #units do
|
||||
bytes = bytes / 1024
|
||||
i = i + 1
|
||||
end
|
||||
if bytes >= 100 then
|
||||
return string.format("%.0f%s", bytes, units[i])
|
||||
elseif bytes >= 10 then
|
||||
return string.format("%.1f%s", bytes, units[i])
|
||||
else
|
||||
return string.format("%.2f%s", bytes, units[i])
|
||||
end
|
||||
end
|
||||
|
||||
local rem_traffic = format_size(remain)
|
||||
|
||||
local expired_date
|
||||
if expire and expire > 0 then
|
||||
local t = os.date("*t", expire)
|
||||
expired_date = string.format("%d-%d-%d", t.year, t.month, t.day)
|
||||
else
|
||||
expired_date = "长期有效"
|
||||
end
|
||||
|
||||
return {
|
||||
rem_traffic = rem_traffic,
|
||||
expired_date = expired_date
|
||||
}
|
||||
end
|
||||
@@ -0,0 +1,288 @@
|
||||
#!/usr/bin/lua
|
||||
|
||||
local api = require ("luci.passwall.api")
|
||||
local appname = api.appname
|
||||
local fs = api.fs
|
||||
local uci = api.uci
|
||||
local sys = api.sys
|
||||
|
||||
local log = api.log
|
||||
|
||||
function get_ip_port_from(str)
|
||||
local result_port = sys.exec("echo -n " .. str .. " | sed -n 's/^.*[:#]\\([0-9]*\\)$/\\1/p'")
|
||||
local result_ip = sys.exec(string.format("__host=%s;__varport=%s;", str, result_port) .. "echo -n ${__host%%${__varport:+[:#]${__varport}*}}")
|
||||
return result_ip, result_port
|
||||
end
|
||||
|
||||
local var = api.get_args(arg)
|
||||
local haproxy_path = var["-path"]
|
||||
local haproxy_conf = var["-conf"]
|
||||
local haproxy_dns = "127.0.0.1"
|
||||
|
||||
local cpu_thread = sys.exec('echo -n $(cat /proc/cpuinfo | grep "processor" | wc -l)') or "1"
|
||||
local health_check_type = api.uci_get_c("@global_haproxy[0]", "health_check_type") or "tcp"
|
||||
local health_check_inter = api.uci_get_c("@global_haproxy[0]", "health_check_inter") or "60"
|
||||
local balancingStrategy = api.uci_get_c("@global_haproxy[0]", "balancingStrategy") or "roundrobin"
|
||||
local console_port = api.uci_get_c("@global_haproxy[0]", "console_port") or "1188"
|
||||
local bind_local = api.uci_get_c("@global_haproxy[0]", "bind_local") or "0"
|
||||
local bind_address = "0.0.0.0"
|
||||
if bind_local == "1" then bind_address = "127.0.0.1" end
|
||||
|
||||
log("HAPROXY 负载均衡:")
|
||||
log(string.format(" * 控制台端口:%s", console_port))
|
||||
fs.mkdir(haproxy_path)
|
||||
local haproxy_file = haproxy_path .. "/" .. haproxy_conf
|
||||
|
||||
local f_out = io.open(haproxy_file, "a")
|
||||
|
||||
local haproxy_config = [[
|
||||
global
|
||||
daemon
|
||||
log 127.0.0.1 local2
|
||||
maxconn 60000
|
||||
stats socket {{path}}/haproxy.sock
|
||||
nbthread {{nbthread}}
|
||||
external-check
|
||||
insecure-fork-wanted
|
||||
spread-checks 20
|
||||
tune.max-checks-per-thread 5
|
||||
|
||||
defaults
|
||||
mode tcp
|
||||
log global
|
||||
option tcplog
|
||||
option dontlognull
|
||||
option abortonclose
|
||||
#option forwardfor except 127.0.0.0/8
|
||||
option redispatch
|
||||
retries 2
|
||||
timeout http-request 10s
|
||||
timeout queue 1m
|
||||
timeout connect 10s
|
||||
timeout client 1m
|
||||
timeout server 1m
|
||||
timeout http-keep-alive 10s
|
||||
timeout check 10s
|
||||
maxconn 3000
|
||||
|
||||
resolvers mydns
|
||||
resolve_retries 1
|
||||
timeout resolve 5s
|
||||
hold valid 600s
|
||||
{{dns}}
|
||||
]]
|
||||
|
||||
haproxy_config = haproxy_config:gsub("{{path}}", haproxy_path)
|
||||
haproxy_config = haproxy_config:gsub("{{nbthread}}", cpu_thread)
|
||||
|
||||
local mydns = ""
|
||||
local index = 0
|
||||
string.gsub(haproxy_dns, '[^' .. "," .. ']+', function(w)
|
||||
index = index + 1
|
||||
local s = w:gsub("#", ":")
|
||||
if not s:find(":") then
|
||||
s = s .. ":53"
|
||||
end
|
||||
mydns = mydns .. (index > 1 and "\n" or "") .. " " .. string.format("nameserver dns%s %s", index, s)
|
||||
end)
|
||||
haproxy_config = haproxy_config:gsub("{{dns}}", mydns)
|
||||
|
||||
f_out:write(haproxy_config)
|
||||
|
||||
local listens = {}
|
||||
|
||||
api.uci_foreach_c("haproxy_config", function(t)
|
||||
if t.enabled == "1" then
|
||||
local server_remark
|
||||
local server_address
|
||||
local server_port
|
||||
local lbss = t.lbss
|
||||
local listen_port = tonumber(t.haproxy_port) or 0
|
||||
local server_node = api.uci_get_c(lbss)
|
||||
local hop = (health_check_type == "script_logic") and server_node and (server_node.hysteria_hop or server_node.hysteria2_hop) or nil
|
||||
hop = hop and hop:gsub(":", "-") or nil
|
||||
if server_node and server_node.address and (server_node.port or hop) then
|
||||
server_remark = server_node.address .. ":" .. (server_node.port or hop)
|
||||
server_address = server_node.address
|
||||
server_port = server_node.port or hop
|
||||
t.origin_address = server_address
|
||||
t.origin_port = server_port
|
||||
if health_check_type == "script_logic" then
|
||||
if server_node.type ~= "Socks" then
|
||||
local new_port
|
||||
local cache = api.get_socks_port_by_cache(server_node[".name"])
|
||||
if cache then
|
||||
new_port = cache
|
||||
else
|
||||
new_port = api.get_new_port()
|
||||
local config_file = string.format("%s_%s.json", t[".name"], new_port)
|
||||
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",
|
||||
new_port, --flag
|
||||
server_node[".name"], --node
|
||||
"127.0.0.1", --bind
|
||||
new_port, --socks port
|
||||
config_file --config file
|
||||
)
|
||||
)
|
||||
)
|
||||
api.set_socks_port_to_cache(server_node[".name"], new_port)
|
||||
end
|
||||
server_address = "127.0.0.1"
|
||||
server_port = new_port
|
||||
end
|
||||
end
|
||||
else
|
||||
server_address, server_port = get_ip_port_from(lbss)
|
||||
server_remark = server_address .. ":" .. server_port
|
||||
t.origin_address = server_address
|
||||
t.origin_port = server_port
|
||||
end
|
||||
if server_address and server_port and listen_port > 0 then
|
||||
if not listens[listen_port] then
|
||||
listens[listen_port] = {}
|
||||
end
|
||||
t.server_remark = server_remark
|
||||
t.server_address = server_address
|
||||
t.server_port = server_port
|
||||
table.insert(listens[listen_port], t)
|
||||
else
|
||||
log(" - 丢弃1个明显无效的节点")
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
local inter = tonumber(health_check_inter:match("^%d+")) or 60
|
||||
inter = math.max(inter, 20)
|
||||
-- 限制过渡状态的确认间隔,避免正常检测间隔较长时摘除或恢复过慢。
|
||||
local fastinter = math.max(10, math.min(15, math.floor(inter / 2)))
|
||||
local downinter = math.ceil(inter * 1.5)
|
||||
local inter_arg = string.format("inter %ds fastinter %ds downinter %ds", inter, fastinter, downinter)
|
||||
|
||||
local sortTable = {}
|
||||
for i in pairs(listens) do
|
||||
if i ~= nil then
|
||||
table.insert(sortTable, i)
|
||||
end
|
||||
end
|
||||
table.sort(sortTable, function(a,b) return (a < b) end)
|
||||
|
||||
local function server_endpoint(o)
|
||||
local address = o.server_address:gsub("^%[(.*)%]$", "%1")
|
||||
if address:find(":", 1, true) then address = "[" .. address .. "]" end
|
||||
return address .. ":" .. o.server_port
|
||||
end
|
||||
|
||||
local check_counts = {}
|
||||
if health_check_type == "script_logic" then
|
||||
for _, port in ipairs(sortTable) do
|
||||
for _, o in ipairs(listens[port]) do
|
||||
-- 域名可能解析到不同 IP,仅对相同的实际代理端点共享检测。
|
||||
if api.is_ip(o.server_address) then
|
||||
local endpoint = server_endpoint(o)
|
||||
check_counts[endpoint] = (check_counts[endpoint] or 0) + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
local shared_checks = {}
|
||||
local shared_check_names = {}
|
||||
|
||||
for i, port in ipairs(sortTable) do
|
||||
log(" + 入口 %s:%s" % {bind_address, port})
|
||||
|
||||
f_out:write("\n" .. string.format([[
|
||||
listen %s
|
||||
bind %s:%s
|
||||
mode tcp
|
||||
balance %s
|
||||
]], port, bind_address, port, balancingStrategy))
|
||||
|
||||
if health_check_type == "script_logic" then
|
||||
f_out:write(string.format([[
|
||||
option external-check
|
||||
external-check command "/usr/share/%s/haproxy_check.sh"
|
||||
]], appname))
|
||||
end
|
||||
|
||||
local count_M, count_B = 1, 1
|
||||
for i, o in ipairs(listens[port]) do
|
||||
local remark = (o.server_remark or ""):gsub("[%[%]]", "")
|
||||
-- 防止重名导致无法运行
|
||||
if tostring(o.backup) ~= "1" then
|
||||
remark = "M" .. count_M .. "-" .. remark
|
||||
count_M = count_M + 1
|
||||
else
|
||||
remark = "B" .. count_B .. "-" .. remark
|
||||
count_B = count_B + 1
|
||||
end
|
||||
local server = server_endpoint(o)
|
||||
local check_arg = "check " .. inter_arg .. " rise 2 fall 2"
|
||||
if (check_counts[server] or 0) > 1 then
|
||||
local name = shared_check_names[server]
|
||||
if not name then
|
||||
name = "check_" .. (#shared_checks + 1)
|
||||
shared_check_names[server] = name
|
||||
table.insert(shared_checks, {name = name, server = server})
|
||||
end
|
||||
check_arg = "track " .. appname .. "_health_checks/" .. name
|
||||
end
|
||||
local server_conf = "server {{remark}} {{server}} weight {{weight}} {{resolvers}} {{check_arg}} {{backup}}"
|
||||
server_conf = server_conf:gsub("{{remark}}", remark)
|
||||
server_conf = server_conf:gsub("{{server}}", server)
|
||||
server_conf = server_conf:gsub("{{weight}}", o.lbweight)
|
||||
local resolvers = "resolvers mydns"
|
||||
if api.is_ip(o.server_address) then
|
||||
resolvers = ""
|
||||
end
|
||||
server_conf = server_conf:gsub("{{resolvers}}", resolvers)
|
||||
server_conf = server_conf:gsub("{{check_arg}}", check_arg)
|
||||
server_conf = server_conf:gsub("{{backup}}", tostring(o.backup) == "1" and "backup" or "")
|
||||
|
||||
f_out:write(" " .. api.trim(server_conf) .. "\n")
|
||||
|
||||
if o.export ~= "0" then
|
||||
sys.call(string.format(". /usr/share/%s/utils.sh ; add_ip2route %s %s", appname, o.origin_address, o.export))
|
||||
end
|
||||
|
||||
log(string.format(" | - 出口节点:%s:%s,权重:%s", o.origin_address, o.origin_port, o.lbweight))
|
||||
end
|
||||
end
|
||||
|
||||
-- 独立检测 backend,避免入口服务器的手动维护状态传播到其他入口。
|
||||
if #shared_checks > 0 then
|
||||
f_out:write(string.format([[
|
||||
|
||||
backend %s_health_checks
|
||||
mode tcp
|
||||
option external-check
|
||||
external-check command "/usr/share/%s/haproxy_check.sh"
|
||||
]], appname, appname))
|
||||
for _, check in ipairs(shared_checks) do
|
||||
f_out:write(string.format("\tserver %s %s check %s rise 2 fall 2\n", check.name, check.server, inter_arg))
|
||||
end
|
||||
end
|
||||
|
||||
--控制台配置
|
||||
local console_user = api.uci_get_c("@global_haproxy[0]", "console_user")
|
||||
local console_password = api.uci_get_c("@global_haproxy[0]", "console_password")
|
||||
local str = [[
|
||||
listen console
|
||||
bind 0.0.0.0:%s
|
||||
mode http
|
||||
stats refresh 30s
|
||||
stats uri /
|
||||
stats admin if TRUE
|
||||
%s
|
||||
]]
|
||||
f_out:write("\n" .. string.format(str, console_port, (console_user and console_user ~= "" and console_password and console_password ~= "") and "stats auth " .. console_user .. ":" .. console_password or ""))
|
||||
|
||||
f_out:close()
|
||||
|
||||
--passwall内置健康检查URL
|
||||
if health_check_type == "script_logic" then
|
||||
local probeUrl = api.trim(api.uci_get_c("@global_haproxy[0]", "health_probe_url") or "")
|
||||
if probeUrl == "" then probeUrl = "https://www.google.com/generate_204" end
|
||||
api.set_cache_var("HAPROXY_PROBE_URL", probeUrl)
|
||||
end
|
||||
@@ -0,0 +1,34 @@
|
||||
#!/bin/sh
|
||||
|
||||
export PATH=/usr/sbin:/usr/bin:/sbin:/bin:/root/bin
|
||||
. /usr/share/passwall/utils.sh
|
||||
|
||||
listen_address=$1
|
||||
listen_port=$2
|
||||
server_address=$3
|
||||
server_port=$4
|
||||
|
||||
busybox pgrep -af "${CONFIG}/" | grep -E 'app\.sh.*(start|stop)|nftables\.sh|iptables\.sh|subscribe\.lua' >/dev/null && {
|
||||
# 特定任务执行中不检测
|
||||
exit 0
|
||||
}
|
||||
|
||||
probeUrl=$(get_cache_var "HAPROXY_PROBE_URL")
|
||||
probeUrl="${probeUrl:-https://www.google.com/generate_204}"
|
||||
|
||||
case "$server_address" in
|
||||
\[*\]) ;;
|
||||
*:*) server_address="[$server_address]" ;;
|
||||
esac
|
||||
|
||||
# 每轮只探测一次,连续成功/失败由 HAProxy 的 rise/fall 判定。
|
||||
status=$(/usr/bin/curl -q -I -o /dev/null -sk --noproxy "" -x "socks5h://${server_address}:${server_port}" --connect-timeout 3 --max-time 6 -w "%{http_code}" "$probeUrl") || exit 1
|
||||
|
||||
case "$status" in
|
||||
200|204)
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,644 @@
|
||||
local api = require "luci.passwall.api"
|
||||
|
||||
local var = api.get_args(arg)
|
||||
local FLAG = var["-FLAG"]
|
||||
local LISTEN_PORT = var["-LISTEN_PORT"]
|
||||
local DNS_LOCAL = var["-DNS_LOCAL"]
|
||||
local DNS_TRUST = var["-DNS_TRUST"]
|
||||
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 GFWLIST = var["-GFWLIST"]
|
||||
local CHNLIST = var["-CHNLIST"]
|
||||
local NO_IPV6_TRUST = var["-NO_IPV6_TRUST"]
|
||||
local DEFAULT_MODE = var["-DEFAULT_MODE"]
|
||||
local DEFAULT_TAG = var["-DEFAULT_TAG"]
|
||||
local NO_LOGIC_LOG = var["-NO_LOGIC_LOG"]
|
||||
local NODE = var["-NODE"]
|
||||
local NFTFLAG = var["-NFTFLAG"]
|
||||
local FILTER_HTTPS = var["-FILTER_HTTPS"]
|
||||
local LOG_FILE = var["-LOG_FILE"]
|
||||
|
||||
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 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)
|
||||
|
||||
local setflag = (NFTFLAG == "1") and "inet@passwall@" or ""
|
||||
|
||||
local only_global = (DEFAULT_MODE == "proxy" and CHNLIST == "0" and GFWLIST == "0") and 1
|
||||
|
||||
config_lines = {
|
||||
LOG_FILE ~= "/dev/null" and "verbose" or "",
|
||||
"bind-addr ::",
|
||||
"bind-port " .. LISTEN_PORT,
|
||||
"china-dns " .. DNS_LOCAL,
|
||||
"trust-dns " .. DNS_TRUST,
|
||||
tonumber(FILTER_HTTPS) == 1 and "filter-qtype 65" or ""
|
||||
}
|
||||
|
||||
for i = 1, 6 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
|
||||
tmp_lines = {
|
||||
"group null",
|
||||
"group-dnl " .. file_block_host
|
||||
}
|
||||
insert_array_after(config_lines, tmp_lines, "#--5")
|
||||
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 sets = {
|
||||
setflag .. "psw_vps",
|
||||
setflag .. "psw_vps6"
|
||||
}
|
||||
tmp_lines = {
|
||||
"group vpslist",
|
||||
"group-dnl " .. file_vpslist,
|
||||
"group-upstream " .. DNS_LOCAL,
|
||||
"group-ipset " .. table.concat(sets, ",")
|
||||
}
|
||||
insert_array_after(config_lines, tmp_lines, "#--6")
|
||||
log(string.format(" - 节点列表中的域名(vpslist):%s", DNS_LOCAL or "默认"))
|
||||
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 sets = {
|
||||
setflag .. "psw_white",
|
||||
setflag .. "psw_white6"
|
||||
}
|
||||
tmp_lines = {
|
||||
"group directlist",
|
||||
"group-dnl " .. file_direct_host,
|
||||
"group-upstream " .. DNS_LOCAL,
|
||||
"group-ipset " .. table.concat(sets, ",")
|
||||
}
|
||||
insert_array_after(config_lines, tmp_lines, "#--4")
|
||||
log(string.format(" - 域名白名单(whitelist):%s", DNS_LOCAL or "默认"))
|
||||
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 sets = {
|
||||
setflag .. "psw_black",
|
||||
setflag .. "psw_black6"
|
||||
}
|
||||
if FLAG ~= "default" then
|
||||
sets = {
|
||||
setflag .. "psw_" .. FLAG .. "_black",
|
||||
setflag .. "psw_" .. FLAG .. "_black6"
|
||||
}
|
||||
end
|
||||
tmp_lines = {
|
||||
"group proxylist",
|
||||
"group-dnl " .. file_proxy_host,
|
||||
"group-upstream " .. DNS_TRUST,
|
||||
"group-ipset " .. table.concat(sets, ",")
|
||||
}
|
||||
if NO_IPV6_TRUST == "1" then table.insert(tmp_lines, "no-ipv6 tag:proxylist") end
|
||||
insert_array_after(config_lines, tmp_lines, "#--3")
|
||||
log(string.format(" - 代理域名表(blacklist):%s", DNS_TRUST or "默认"))
|
||||
end
|
||||
|
||||
--内置组(chn/gfw)优先级在自定义组后
|
||||
--GFW列表
|
||||
if GFWLIST == "1" and is_file_nonzero(RULES_PATH .. "/gfwlist") then
|
||||
local sets = {
|
||||
setflag .. "psw_gfw",
|
||||
setflag .. "psw_gfw6"
|
||||
}
|
||||
if FLAG ~= "default" then
|
||||
sets = {
|
||||
setflag .. "psw_" .. FLAG .. "_gfw",
|
||||
setflag .. "psw_" .. FLAG .. "_gfw6"
|
||||
}
|
||||
end
|
||||
tmp_lines = {
|
||||
"gfwlist-file " .. RULES_PATH .. "/gfwlist",
|
||||
"add-taggfw-ip " .. table.concat(sets, ",")
|
||||
}
|
||||
if NO_IPV6_TRUST == "1" then table.insert(tmp_lines, "no-ipv6 tag:gfw") end
|
||||
merge_array(config_lines, tmp_lines)
|
||||
log(string.format(" - 防火墙域名表(gfwlist):%s", DNS_TRUST or "默认"))
|
||||
end
|
||||
|
||||
--中国列表
|
||||
if CHNLIST ~= "0" and is_file_nonzero(RULES_PATH .. "/chnlist") then
|
||||
if CHNLIST == "direct" then
|
||||
local sets = {
|
||||
setflag .. "psw_chn",
|
||||
setflag .. "psw_chn6"
|
||||
}
|
||||
local suffix = (NFTFLAG == "1") and "_static" or ""
|
||||
tmp_lines = {
|
||||
"chnlist-file " .. RULES_PATH .. "/chnlist",
|
||||
"ipset-name4 " .. setflag .. "psw_chn" .. suffix,
|
||||
"ipset-name6 " .. setflag .. "psw_chn6" .. suffix,
|
||||
"add-tagchn-ip" .. ((NFTFLAG == "1") and (" " .. table.concat(sets, ",")) or "")
|
||||
}
|
||||
merge_array(config_lines, tmp_lines)
|
||||
log(string.format(" - 中国域名表(chnlist):%s", DNS_LOCAL or "默认"))
|
||||
end
|
||||
|
||||
--回中国模式
|
||||
if CHNLIST == "proxy" then
|
||||
local sets = {
|
||||
setflag .. "psw_chn",
|
||||
setflag .. "psw_chn6"
|
||||
}
|
||||
tmp_lines = {
|
||||
"group chn_proxy",
|
||||
"group-dnl " .. RULES_PATH .. "/chnlist",
|
||||
"group-upstream " .. DNS_TRUST,
|
||||
"group-ipset " .. table.concat(sets, ",")
|
||||
}
|
||||
if NO_IPV6_TRUST == "1" then table.insert(tmp_lines, "no-ipv6 tag:chn_proxy") end
|
||||
insert_array_after(config_lines, tmp_lines, "#--1")
|
||||
log(string.format(" - 中国域名表(chnlist):%s", DNS_TRUST or "默认"))
|
||||
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_direct_host_tmp = shunt_direct_host .. "_tmp"
|
||||
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):%s", s.remarks, DNS_TRUST or "默认"))
|
||||
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_tmp) then
|
||||
if #direct_domain > 0 then
|
||||
local f_out = io.open(shunt_direct_host_tmp, "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_tmp)
|
||||
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
|
||||
|
||||
-- 与 chnlist 比对
|
||||
if is_file_nonzero(shunt_direct_host_tmp) and CHNLIST ~= "0" and is_file_nonzero(RULES_PATH .. "/chnlist") then
|
||||
local chn_domain_set = {}
|
||||
local f_chn = io.open(RULES_PATH .. "/chnlist", "r")
|
||||
if f_chn then
|
||||
for line in f_chn:lines() do
|
||||
if line ~= "" then
|
||||
chn_domain_set[line] = true
|
||||
end
|
||||
end
|
||||
f_chn:close()
|
||||
end
|
||||
local f_in = io.open(shunt_direct_host_tmp, "r")
|
||||
local f_out = io.open(shunt_direct_host, "w")
|
||||
if f_in and f_out then
|
||||
for line in f_in:lines() do
|
||||
line = string.lower(line)
|
||||
-- 排除 cn、.cn 等缺少有效域名标签的条目
|
||||
local valid_domain = line ~= "" and line:find("%.") and line:sub(1,1) ~= "."
|
||||
if valid_domain and not chn_domain_set[line] then
|
||||
f_out:write(line .. "\n")
|
||||
end
|
||||
end
|
||||
end
|
||||
if f_in then f_in:close() end
|
||||
if f_out then f_out:close() end
|
||||
api.remove(shunt_direct_host_tmp)
|
||||
elseif is_file_nonzero(shunt_direct_host_tmp) then
|
||||
os.rename(shunt_direct_host_tmp, shunt_direct_host)
|
||||
end
|
||||
|
||||
local shunt_file
|
||||
if is_file_nonzero(shunt_black_host) then
|
||||
shunt_file = shunt_black_host
|
||||
end
|
||||
if is_file_nonzero(shunt_direct_host) then
|
||||
shunt_file = (shunt_file and shunt_file .. "," or "") .. shunt_direct_host
|
||||
end
|
||||
if is_file_nonzero(shunt_proxy_host) then
|
||||
shunt_file = (shunt_file and shunt_file .. "," or "") .. shunt_proxy_host
|
||||
end
|
||||
|
||||
if shunt_file then
|
||||
local sets = {
|
||||
setflag .. "psw_shunt",
|
||||
setflag .. "psw_shunt6"
|
||||
}
|
||||
if FLAG ~= "default" then
|
||||
sets = {
|
||||
setflag .. "psw_" .. FLAG .. "_shunt",
|
||||
setflag .. "psw_" .. FLAG .. "_shunt6"
|
||||
}
|
||||
end
|
||||
tmp_lines = {
|
||||
"group shuntlist",
|
||||
"group-dnl " .. shunt_file,
|
||||
"group-upstream " .. DNS_TRUST,
|
||||
"group-ipset " .. table.concat(sets, ",")
|
||||
}
|
||||
insert_array_after(config_lines, tmp_lines, "#--2")
|
||||
end
|
||||
end
|
||||
|
||||
--只使用gfwlist模式,GFW列表以外的域名及默认使用本地DNS
|
||||
if GFWLIST == "1" and CHNLIST == "0" then DEFAULT_TAG = "chn" end
|
||||
|
||||
--回中国模式,中国列表以外的域名及默认使用本地DNS
|
||||
if CHNLIST == "proxy" then DEFAULT_TAG = "chn" end
|
||||
|
||||
--全局模式,默认使用远程DNS
|
||||
if only_global then
|
||||
DEFAULT_TAG = "gfw"
|
||||
if NO_IPV6_TRUST == "1" and not IS_SHUNT_NODE then
|
||||
table.insert(config_lines, "no-ipv6")
|
||||
end
|
||||
end
|
||||
|
||||
--是否接受直连 DNS 空响应
|
||||
if DEFAULT_TAG == "none_noip" then table.insert(config_lines, "noip-as-chnip") end
|
||||
|
||||
if not DEFAULT_TAG or DEFAULT_TAG == "smart" or DEFAULT_TAG == "none_noip" then DEFAULT_TAG = "none" end
|
||||
|
||||
table.insert(config_lines, "default-tag " .. DEFAULT_TAG)
|
||||
|
||||
if DEFAULT_TAG == "none" then
|
||||
table.insert(config_lines, "verdict-cache 5000")
|
||||
end
|
||||
|
||||
table.insert(config_lines, "hosts")
|
||||
|
||||
if DEFAULT_TAG == "chn" then
|
||||
log(string.format(" - 默认 DNS :%s", DNS_LOCAL))
|
||||
elseif DEFAULT_TAG == "gfw" then
|
||||
log(string.format(" - 默认 DNS :%s", DNS_TRUST))
|
||||
else
|
||||
log(string.format(" - 默认 DNS :%s", "智能匹配"))
|
||||
end
|
||||
|
||||
--输出配置文件
|
||||
if #config_lines > 0 then
|
||||
for i = 1, #config_lines do
|
||||
line = config_lines[i]
|
||||
if line ~= "" and not line:find("^#--") then
|
||||
print(line)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
log(" - ChinaDNS-NG已作为Dnsmasq上游,如果你自行配置了错误的DNS流程,将会导致域名(直连/代理)分流失效!!!")
|
||||
@@ -0,0 +1,834 @@
|
||||
local api = require "luci.passwall.api"
|
||||
local sys = api.sys
|
||||
local fs = api.fs
|
||||
local datatypes = api.datatypes
|
||||
local TMP = {}
|
||||
|
||||
local function tinsert(table_name, val)
|
||||
if table_name and type(table_name) == "table" then
|
||||
if not TMP[table_name] then
|
||||
TMP[table_name] = {}
|
||||
end
|
||||
if TMP[table_name][val] then
|
||||
return false
|
||||
end
|
||||
table.insert(table_name, val)
|
||||
TMP[table_name][val] = true
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function backup_servers()
|
||||
local DNSMASQ_DNS = api.uci_get("dhcp", "@dnsmasq[0]", "server")
|
||||
if DNSMASQ_DNS and #DNSMASQ_DNS > 0 then
|
||||
api.uci_set_c("@global[0]", "dnsmasq_servers", DNSMASQ_DNS)
|
||||
api.uci_save_c(true)
|
||||
end
|
||||
end
|
||||
|
||||
local function restore_servers()
|
||||
local dns_table = {}
|
||||
local DNSMASQ_DNS = api.uci_get("dhcp", "@dnsmasq[0]", "server")
|
||||
if DNSMASQ_DNS and #DNSMASQ_DNS > 0 then
|
||||
for k, v in ipairs(DNSMASQ_DNS) do
|
||||
tinsert(dns_table, v)
|
||||
end
|
||||
end
|
||||
local OLD_SERVER = api.uci_get(api.c_config, "@global[0]", "dnsmasq_servers")
|
||||
if OLD_SERVER and #OLD_SERVER > 0 then
|
||||
for k, v in ipairs(OLD_SERVER) do
|
||||
tinsert(dns_table, v)
|
||||
end
|
||||
api.uci_del_c("@global[0]", "dnsmasq_servers")
|
||||
api.uci_save_c(true)
|
||||
end
|
||||
if dns_table and #dns_table > 0 then
|
||||
api.uci_set("dhcp", "@dnsmasq[0]", "server", dns_table)
|
||||
api.uci_save(nil, "dhcp", true)
|
||||
end
|
||||
end
|
||||
|
||||
function stretch()
|
||||
local dnsmasq_server = api.uci_get("dhcp", "@dnsmasq[0]", "server")
|
||||
local dnsmasq_noresolv = api.uci_get("dhcp", "@dnsmasq[0]", "noresolv")
|
||||
local _flag
|
||||
if dnsmasq_server and #dnsmasq_server > 0 then
|
||||
for k, v in ipairs(dnsmasq_server) do
|
||||
if not v:find("/") then
|
||||
_flag = true
|
||||
end
|
||||
end
|
||||
end
|
||||
if not _flag and dnsmasq_noresolv == "1" then
|
||||
api.uci_del("dhcp", "@dnsmasq[0]", "noresolv")
|
||||
local RESOLVFILE = "/tmp/resolv.conf.d/resolv.conf.auto"
|
||||
if (fs.stat(RESOLVFILE, "size") or 0) == 0 then
|
||||
RESOLVFILE = "/tmp/resolv.conf.auto"
|
||||
end
|
||||
api.uci_set("dhcp", "@dnsmasq[0]", "resolvfile", RESOLVFILE)
|
||||
api.uci_save(nil, "dhcp", true)
|
||||
end
|
||||
end
|
||||
|
||||
function restart(var)
|
||||
local LOG = var["-LOG"]
|
||||
sys.call("/etc/init.d/dnsmasq restart >/dev/null 2>&1")
|
||||
if LOG == "1" then
|
||||
api.log("重启 dnsmasq 服务")
|
||||
end
|
||||
end
|
||||
|
||||
function logic_restart(var)
|
||||
local LOG = var["-LOG"]
|
||||
local DEFAULT_DNS = api.get_cache_var("DEFAULT_DNS")
|
||||
if DEFAULT_DNS then
|
||||
backup_servers()
|
||||
--sys.call("sed -i '/list server/d' /etc/config/dhcp >/dev/null 2>&1")
|
||||
local dns_table = {}
|
||||
local dnsmasq_server = api.uci_get("dhcp", "@dnsmasq[0]", "server")
|
||||
if dnsmasq_server and #dnsmasq_server > 0 then
|
||||
for k, v in ipairs(dnsmasq_server) do
|
||||
if v:find("/") then
|
||||
tinsert(dns_table, v)
|
||||
end
|
||||
end
|
||||
api.uci_set("dhcp", "@dnsmasq[0]", "server", dns_table)
|
||||
api.uci_save(nil, "dhcp", true)
|
||||
end
|
||||
sys.call("/etc/init.d/dnsmasq restart >/dev/null 2>&1")
|
||||
restore_servers()
|
||||
else
|
||||
sys.call("/etc/init.d/dnsmasq restart >/dev/null 2>&1")
|
||||
end
|
||||
if LOG == "1" then
|
||||
api.log("重启 dnsmasq 服务")
|
||||
end
|
||||
end
|
||||
|
||||
function copy_instance(var)
|
||||
local LISTEN_PORT = var["-LISTEN_PORT"]
|
||||
local TMP_DNSMASQ_PATH = var["-TMP_DNSMASQ_PATH"]
|
||||
local conf_lines = {}
|
||||
local DEFAULT_DNSMASQ_CFGID = sys.exec("echo -n $(uci -q show dhcp.@dnsmasq[0] | awk 'NR==1 {split($0, conf, /[.=]/); print conf[2]}')")
|
||||
local conf_file = "/var/etc/dnsmasq.conf." .. DEFAULT_DNSMASQ_CFGID
|
||||
|
||||
local retry = 5
|
||||
while not fs.access(conf_file) and retry > 0 do
|
||||
api.nixio.nanosleep(1, 0)
|
||||
retry = retry - 1
|
||||
end
|
||||
|
||||
if fs.access(conf_file) then
|
||||
for line in io.lines(conf_file) do
|
||||
local filter
|
||||
if line:find("passwall") then filter = true end
|
||||
if line:find("ubus") then filter = true end
|
||||
if line:find("dhcp") then filter = true end
|
||||
if line:find("server=") == 1 then filter = true end
|
||||
if line:find("port=") == 1 then filter = true end
|
||||
if line:find("min%-cache%-ttl") == 1 then filter = true end
|
||||
if line:find("conf%-dir=") == 1 then
|
||||
filter = true
|
||||
if TMP_DNSMASQ_PATH then
|
||||
local tmp_path = line:sub(1 + #"conf-dir=")
|
||||
sys.call(string.format("cp -r %s/* %s/ 2>/dev/null", tmp_path, TMP_DNSMASQ_PATH))
|
||||
end
|
||||
end
|
||||
if line:find("address=") == 1 or (line:find("server=") == 1 and line:find("/")) then filter = nil end
|
||||
if not filter then
|
||||
tinsert(conf_lines, line)
|
||||
end
|
||||
end
|
||||
else
|
||||
sys.call("logger -t passwall 'ERROR: dnsmasq config " .. conf_file .. " not found after 5s wait! DNS hijacking will fail.'")
|
||||
end
|
||||
|
||||
tinsert(conf_lines, "port=" .. LISTEN_PORT)
|
||||
if TMP_DNSMASQ_PATH then
|
||||
api.remove(TMP_DNSMASQ_PATH .. "/*passwall*")
|
||||
end
|
||||
if var["-return"] == "1" then
|
||||
return conf_lines
|
||||
end
|
||||
if #conf_lines > 0 then
|
||||
local DNSMASQ_CONF = var["-DNSMASQ_CONF"]
|
||||
local conf_out = io.open(DNSMASQ_CONF, "a")
|
||||
conf_out:write(table.concat(conf_lines, "\n"))
|
||||
conf_out:write("\n")
|
||||
conf_out:close()
|
||||
end
|
||||
end
|
||||
|
||||
function add_rule(var)
|
||||
local FLAG = var["-FLAG"]
|
||||
local TMP_DNSMASQ_PATH = var["-TMP_DNSMASQ_PATH"]
|
||||
local DNSMASQ_CONF_FILE = var["-DNSMASQ_CONF_FILE"]
|
||||
local LISTEN_PORT = var["-LISTEN_PORT"]
|
||||
local DEFAULT_DNS = var["-DEFAULT_DNS"]
|
||||
local LOCAL_DNS = var["-LOCAL_DNS"]
|
||||
local TUN_DNS = var["-TUN_DNS"]
|
||||
local USE_DEFAULT_DNS = var["-USE_DEFAULT_DNS"]
|
||||
local CHINADNS_DNS = var["-CHINADNS_DNS"]
|
||||
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 CACHE_PATH = api.CACHE_PATH
|
||||
local CACHE_FLAG = "dnsmasq_" .. FLAG
|
||||
local CACHE_DNS_PATH = CACHE_PATH .. "/" .. CACHE_FLAG
|
||||
local CACHE_TEXT_FILE = CACHE_DNS_PATH .. ".txt"
|
||||
local USE_CHINADNS_NG = "0"
|
||||
local IS_SHUNT_NODE = api.uci_get_c(NODE, "protocol") == "_shunt"
|
||||
local USE_GEOVIEW = api.uci_get_c("@global_rules[0]", "enable_geoview")
|
||||
|
||||
local list1 = {}
|
||||
local excluded_domain = {}
|
||||
|
||||
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 check_dns(domain, dns)
|
||||
if domain == "" or domain:find("#") then
|
||||
return false
|
||||
end
|
||||
if not dns then
|
||||
return
|
||||
end
|
||||
for k,v in ipairs(list1[domain].dns) do
|
||||
if dns == v then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function check_ipset(domain, ipset)
|
||||
if domain == "" or domain:find("#") then
|
||||
return false
|
||||
end
|
||||
if not ipset then
|
||||
return
|
||||
end
|
||||
for k,v in ipairs(list1[domain].ipsets) do
|
||||
if ipset == v then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function set_domain_address(domain, address)
|
||||
if domain == "" or domain:find("#") then
|
||||
return
|
||||
end
|
||||
if not list1[domain] then
|
||||
list1[domain] = {
|
||||
dns = {},
|
||||
ipsets = {}
|
||||
}
|
||||
end
|
||||
if not list1[domain].address then
|
||||
list1[domain].address = address
|
||||
end
|
||||
end
|
||||
|
||||
local function set_domain_dns(domain, dns)
|
||||
if domain == "" or domain:find("#") then
|
||||
return
|
||||
end
|
||||
if not dns then
|
||||
return
|
||||
end
|
||||
if not list1[domain] then
|
||||
list1[domain] = {
|
||||
dns = {},
|
||||
ipsets = {}
|
||||
}
|
||||
end
|
||||
for line in string.gmatch(dns, '[^' .. "," .. ']+') do
|
||||
if not check_dns(domain, line) then
|
||||
table.insert(list1[domain].dns, line)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function set_domain_ipset(domain, ipset)
|
||||
if domain == "" or domain:find("#") then
|
||||
return
|
||||
end
|
||||
if not ipset then
|
||||
return
|
||||
end
|
||||
if not list1[domain] then
|
||||
list1[domain] = {
|
||||
dns = {},
|
||||
ipsets = {}
|
||||
}
|
||||
end
|
||||
for line in string.gmatch(ipset, '[^' .. "," .. ']+') do
|
||||
if not check_ipset(domain, line) then
|
||||
table.insert(list1[domain].ipsets, line)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function add_excluded_domain(domain)
|
||||
if domain == "" or domain:find("#") then
|
||||
return
|
||||
end
|
||||
excluded_domain[domain] = true
|
||||
end
|
||||
|
||||
local function check_excluded_domain(domain)
|
||||
if domain == "" or domain:find("#") then
|
||||
return false
|
||||
end
|
||||
if excluded_domain[domain] then
|
||||
return true
|
||||
end
|
||||
local pos = domain:find(".", 1, true)
|
||||
while pos do
|
||||
if excluded_domain[domain:sub(pos + 1)] then
|
||||
return true
|
||||
end
|
||||
pos = domain:find(".", pos + 1, true)
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function foreach_geosite(list_arg, callback)
|
||||
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 fs.access(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
|
||||
local bin = api.finded_com("geoview")
|
||||
local cmd = string.format("%q -type geosite -action extract -input %q -list %q -lowmem=true", bin, geosite_path, list_arg)
|
||||
local code, out = api.exec_call(cmd)
|
||||
if code ~= 0 then return code, out end
|
||||
for line in out:gmatch("[^\r\n]+") do
|
||||
callback(line)
|
||||
end
|
||||
return 0
|
||||
end
|
||||
|
||||
local function get_dns_config_key()
|
||||
local address_md5 = api.md5_string(sys.exec([[uci show passwall | grep -E '\.(address|download_address)=' | cut -d "'" -f 2 | sort -u]]))
|
||||
local new_rules = sys.exec([[
|
||||
for f in \
|
||||
/usr/share/passwall/rules/chnlist \
|
||||
/usr/share/passwall/rules/gfwlist \
|
||||
/etc/passwall/rules/direct_host \
|
||||
/etc/passwall/rules/proxy_host \
|
||||
/etc/passwall/rules/block_host
|
||||
do
|
||||
[ -f "$f" ] && md5sum "$f" | awk '{printf "%s", $1}'
|
||||
done
|
||||
]])
|
||||
local SHUNT_LIST = ""
|
||||
if IS_SHUNT_NODE then
|
||||
local t = api.uci_get_c(NODE)
|
||||
api.uci_foreach_c("shunt_rules", function(s)
|
||||
local _node_id = t[s[".name"]]
|
||||
if _node_id and t["shunt_group"] == s.group then
|
||||
SHUNT_LIST = SHUNT_LIST .. (s.domain_list or "") .. (_node_id:sub(1, 1) == "_" and "not-node" or "node")
|
||||
end
|
||||
end)
|
||||
end
|
||||
new_rules = new_rules .. api.md5_string(SHUNT_LIST)
|
||||
return TMP_DNSMASQ_PATH .. DNSMASQ_CONF_FILE .. DEFAULT_DNS .. LOCAL_DNS .. TUN_DNS .. USE_DEFAULT_DNS .. CHINADNS_DNS .. USE_DIRECT_LIST .. USE_PROXY_LIST .. USE_BLOCK_LIST .. USE_GFW_LIST .. CHN_LIST .. DEFAULT_PROXY_MODE .. NO_PROXY_IPV6 .. address_md5 .. new_rules .. NFTFLAG
|
||||
end
|
||||
|
||||
local dnsmasq_default_dns
|
||||
if USE_DEFAULT_DNS ~= "nil" then
|
||||
if USE_DEFAULT_DNS == "direct" then
|
||||
dnsmasq_default_dns = LOCAL_DNS
|
||||
end
|
||||
if USE_DEFAULT_DNS == "remote" then
|
||||
dnsmasq_default_dns = TUN_DNS
|
||||
end
|
||||
if USE_DEFAULT_DNS == "remote" and CHN_LIST == "direct" then
|
||||
dnsmasq_default_dns = TUN_DNS
|
||||
end
|
||||
end
|
||||
|
||||
local only_global
|
||||
if DEFAULT_PROXY_MODE == "proxy" and CHN_LIST == "0" and USE_GFW_LIST == "0" then
|
||||
--没有启用中国列表和GFW列表时
|
||||
dnsmasq_default_dns = TUN_DNS
|
||||
only_global = 1
|
||||
end
|
||||
if USE_DEFAULT_DNS == "chinadns_ng" and CHINADNS_DNS ~= "0" then
|
||||
dnsmasq_default_dns = CHINADNS_DNS
|
||||
USE_CHINADNS_NG = "1"
|
||||
end
|
||||
|
||||
local setflag_4= (NFTFLAG == "1") and "4#inet#passwall#" or ""
|
||||
local setflag_6= (NFTFLAG == "1") and "6#inet#passwall#" or ""
|
||||
|
||||
local new_text = ""
|
||||
if USE_CHINADNS_NG == "0" then
|
||||
local cache_text = ""
|
||||
if fs.access(CACHE_TEXT_FILE) then
|
||||
for line in io.lines(CACHE_TEXT_FILE) do
|
||||
cache_text = line
|
||||
end
|
||||
end
|
||||
new_text = get_dns_config_key()
|
||||
if cache_text == "" or new_text == "" or cache_text ~= new_text then
|
||||
api.remove(CACHE_DNS_PATH .. "*")
|
||||
end
|
||||
else
|
||||
api.remove(CACHE_DNS_PATH .. "*")
|
||||
end
|
||||
|
||||
if not fs.access(CACHE_DNS_PATH) then
|
||||
fs.mkdir(CACHE_DNS_PATH)
|
||||
local GEO_SUCCESS = true
|
||||
|
||||
--屏蔽列表
|
||||
if USE_CHINADNS_NG == "0" and USE_BLOCK_LIST == "1" then
|
||||
local geosite_arg = ""
|
||||
local f = io.open("/etc/passwall/rules/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("#") and not line:find(":") then
|
||||
set_domain_address(line, "")
|
||||
end
|
||||
end
|
||||
end
|
||||
f:close()
|
||||
end
|
||||
if USE_GEOVIEW == "1" and geosite_arg ~= "" then
|
||||
local code, out = foreach_geosite(geosite_arg, function(line)
|
||||
set_domain_address(line, "")
|
||||
end)
|
||||
if code == 0 then
|
||||
log(" - 解析[屏蔽列表] Geosite 到屏蔽域名表(blocklist)完成")
|
||||
else
|
||||
log(" - 解析[屏蔽列表] Geosite 到屏蔽域名表(blocklist)失败![" .. out .. "]")
|
||||
GEO_SUCCESS = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local fwd_dns
|
||||
local no_ipv6
|
||||
|
||||
--始终用国内DNS解析节点域名
|
||||
if true then
|
||||
fwd_dns = LOCAL_DNS
|
||||
if USE_CHINADNS_NG == "1" then
|
||||
fwd_dns = nil
|
||||
else
|
||||
local sets = {
|
||||
setflag_4 .. "psw_vps",
|
||||
setflag_6 .. "psw_vps6"
|
||||
}
|
||||
local function process_address(address)
|
||||
address = (address or ""):lower()
|
||||
if api.vps_domain_exclude(address) then return end
|
||||
if datatypes.hostname(address) then
|
||||
set_domain_dns(address, fwd_dns)
|
||||
set_domain_ipset(address, table.concat(sets, ","))
|
||||
end
|
||||
end
|
||||
api.uci_foreach_c("nodes", function(t)
|
||||
process_address(t.address)
|
||||
process_address(t.download_address)
|
||||
end)
|
||||
log(string.format(" - 节点列表中的域名(vpslist):%s", fwd_dns or "默认"))
|
||||
end
|
||||
end
|
||||
|
||||
--直连(白名单)列表
|
||||
if USE_DIRECT_LIST == "1" then
|
||||
fwd_dns = LOCAL_DNS
|
||||
if USE_CHINADNS_NG == "1" then
|
||||
fwd_dns = nil
|
||||
end
|
||||
if fwd_dns then
|
||||
local sets = {
|
||||
setflag_4 .. "psw_white",
|
||||
setflag_6 .. "psw_white6"
|
||||
}
|
||||
--始终用国内DNS解析直连(白名单)列表
|
||||
local geosite_arg = ""
|
||||
local f = io.open("/etc/passwall/rules/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
|
||||
add_excluded_domain(line)
|
||||
set_domain_dns(line, fwd_dns)
|
||||
set_domain_ipset(line, table.concat(sets, ","))
|
||||
end
|
||||
end
|
||||
end
|
||||
f:close()
|
||||
log(string.format(" - 域名白名单(whitelist):%s", fwd_dns or "默认"))
|
||||
end
|
||||
if USE_GEOVIEW == "1" and geosite_arg ~= "" then
|
||||
local code, out = foreach_geosite(geosite_arg, function(line)
|
||||
add_excluded_domain(line)
|
||||
set_domain_dns(line, fwd_dns)
|
||||
set_domain_ipset(line, table.concat(sets, ","))
|
||||
end)
|
||||
if code == 0 then
|
||||
log(" - 解析[直连列表] Geosite 到域名白名单(whitelist)完成")
|
||||
else
|
||||
log(" - 解析[直连列表] Geosite 到域名白名单(whitelist)失败![" .. out .. "]")
|
||||
GEO_SUCCESS = false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--代理(黑名单)列表
|
||||
if USE_PROXY_LIST == "1" then
|
||||
fwd_dns = TUN_DNS
|
||||
if USE_CHINADNS_NG == "1" then
|
||||
fwd_dns = nil
|
||||
end
|
||||
if fwd_dns then
|
||||
local set_name = "psw_black"
|
||||
local set6_name = "psw_black6"
|
||||
if FLAG ~= "default" then
|
||||
set_name = "psw_" .. FLAG .. "_black"
|
||||
set6_name = "psw_" .. FLAG .. "_black6"
|
||||
end
|
||||
local sets = {
|
||||
setflag_4 .. set_name
|
||||
}
|
||||
if NO_PROXY_IPV6 ~= "1" then
|
||||
table.insert(sets, setflag_6 .. set6_name)
|
||||
end
|
||||
--始终使用远程DNS解析代理(黑名单)列表
|
||||
local geosite_arg = ""
|
||||
local f = io.open("/etc/passwall/rules/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
|
||||
add_excluded_domain(line)
|
||||
if NO_PROXY_IPV6 == "1" then
|
||||
set_domain_address(line, "::")
|
||||
end
|
||||
set_domain_dns(line, fwd_dns)
|
||||
set_domain_ipset(line, table.concat(sets, ","))
|
||||
end
|
||||
end
|
||||
end
|
||||
f:close()
|
||||
log(string.format(" - 代理域名表(blacklist):%s", fwd_dns or "默认"))
|
||||
end
|
||||
if USE_GEOVIEW == "1" and geosite_arg ~= "" then
|
||||
local code, out = foreach_geosite(geosite_arg, function(line)
|
||||
add_excluded_domain(line)
|
||||
if NO_PROXY_IPV6 == "1" then
|
||||
set_domain_address(line, "::")
|
||||
end
|
||||
set_domain_dns(line, fwd_dns)
|
||||
set_domain_ipset(line, table.concat(sets, ","))
|
||||
end)
|
||||
if code == 0 then
|
||||
log(" - 解析[代理列表] Geosite 到代理域名表(blacklist)完成")
|
||||
else
|
||||
log(" - 解析[代理列表] Geosite 到代理域名表(blacklist)失败![" .. out .. "]")
|
||||
GEO_SUCCESS = false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--GFW列表
|
||||
if USE_GFW_LIST == "1" then
|
||||
fwd_dns = TUN_DNS
|
||||
if USE_CHINADNS_NG == "1" then
|
||||
fwd_dns = nil
|
||||
end
|
||||
if fwd_dns then
|
||||
local set_name = "psw_gfw"
|
||||
local set6_name = "psw_gfw6"
|
||||
if FLAG ~= "default" then
|
||||
set_name = "psw_" .. FLAG .. "_gfw"
|
||||
set6_name = "psw_" .. FLAG .. "_gfw6"
|
||||
end
|
||||
local sets = {
|
||||
setflag_4 .. set_name
|
||||
}
|
||||
if NO_PROXY_IPV6 ~= "1" then
|
||||
table.insert(sets, setflag_6 .. set6_name)
|
||||
end
|
||||
local f = io.open("/usr/share/passwall/rules/gfwlist")
|
||||
if f then
|
||||
for line in f:lines() do
|
||||
if line ~= "" and not line:find("#") and not check_excluded_domain(line) then
|
||||
if NO_PROXY_IPV6 == "1" then
|
||||
set_domain_address(line, "::")
|
||||
end
|
||||
if dnsmasq_default_dns == fwd_dns then
|
||||
fwd_dns = nil
|
||||
else
|
||||
set_domain_dns(line, fwd_dns)
|
||||
end
|
||||
set_domain_ipset(line, table.concat(sets, ","))
|
||||
end
|
||||
end
|
||||
f:close()
|
||||
log(string.format(" - 防火墙域名表(gfwlist):%s", fwd_dns or "默认"))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--中国列表
|
||||
if CHN_LIST ~= "0" then
|
||||
fwd_dns = nil
|
||||
if CHN_LIST == "direct" then
|
||||
fwd_dns = LOCAL_DNS
|
||||
end
|
||||
if CHN_LIST == "proxy" then
|
||||
fwd_dns = TUN_DNS
|
||||
end
|
||||
if USE_CHINADNS_NG == "1" then
|
||||
fwd_dns = nil
|
||||
end
|
||||
if fwd_dns then
|
||||
local sets = {
|
||||
setflag_4 .. "psw_chn",
|
||||
setflag_6 .. "psw_chn6"
|
||||
}
|
||||
if CHN_LIST == "proxy" then
|
||||
if NO_PROXY_IPV6 == "1" then
|
||||
sets = {
|
||||
setflag_4 .. "psw_chn"
|
||||
}
|
||||
end
|
||||
end
|
||||
local f = io.open("/usr/share/passwall/rules/chnlist")
|
||||
if f then
|
||||
for line in f:lines() do
|
||||
if line ~= "" and not line:find("#") and not check_excluded_domain(line) then
|
||||
if CHN_LIST == "proxy" and NO_PROXY_IPV6 == "1" then
|
||||
set_domain_address(line, "::")
|
||||
end
|
||||
if dnsmasq_default_dns == fwd_dns then
|
||||
fwd_dns = nil
|
||||
else
|
||||
set_domain_dns(line, fwd_dns)
|
||||
end
|
||||
set_domain_ipset(line, table.concat(sets, ","))
|
||||
end
|
||||
end
|
||||
f:close()
|
||||
log(string.format(" - 中国域名表(chnlist):%s", fwd_dns or "默认"))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--分流规则
|
||||
if IS_SHUNT_NODE and USE_CHINADNS_NG == "0" and only_global ~= 1 then
|
||||
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
|
||||
|
||||
fwd_dns = TUN_DNS
|
||||
|
||||
local sets = {
|
||||
setflag_4 .. "psw_shunt",
|
||||
setflag_6 .. "psw_shunt6"
|
||||
}
|
||||
if FLAG ~= "default" then
|
||||
sets = {
|
||||
setflag_4 .. "psw_" .. FLAG .. "_shunt",
|
||||
setflag_6 .. "psw_" .. FLAG .. "_shunt6"
|
||||
}
|
||||
end
|
||||
|
||||
local domain_list = s.domain_list or ""
|
||||
local geosite_arg = ""
|
||||
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, ":([^:]+)$")
|
||||
geosite_arg = geosite_arg .. (geosite_arg ~= "" and "," or "") .. line
|
||||
else
|
||||
if line:find("domain:") or line:find("full:") then
|
||||
line = string.match(line, ":([^:]+)$")
|
||||
end
|
||||
line = api.get_std_domain(line)
|
||||
add_excluded_domain(line)
|
||||
set_domain_dns(line, fwd_dns)
|
||||
set_domain_ipset(line, table.concat(sets, ","))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if USE_GFW_LIST == "1" and CHN_LIST == "0" and USE_GEOVIEW == "1" and geosite_arg ~= "" then --仅GFW模式解析geosite
|
||||
local code, out = foreach_geosite(geosite_arg, function(line)
|
||||
add_excluded_domain(line)
|
||||
set_domain_dns(line, fwd_dns)
|
||||
set_domain_ipset(line, table.concat(sets, ","))
|
||||
end)
|
||||
if code == 0 then
|
||||
log(string.format(" - 解析分流规则(%s) Geosite 完成", s.remarks))
|
||||
else
|
||||
log(string.format(" - 解析分流规则(%s) Geosite 失败![" .. out .. "]", s.remarks))
|
||||
GEO_SUCCESS = false
|
||||
end
|
||||
end
|
||||
|
||||
if _node_id ~= "_direct" and _node_id ~= "_blackhole" then
|
||||
log(string.format(" - Sing-Box/Xray分流规则(%s):%s", s.remarks, fwd_dns or "默认"))
|
||||
end
|
||||
end
|
||||
end)
|
||||
elseif only_global == 1 and NO_PROXY_IPV6 == "1" then
|
||||
--节点:固定节点
|
||||
--代理模式:全局模式
|
||||
--过滤代理域名 IPv6:启用
|
||||
--禁止解析所有IPv6记录
|
||||
list1["#"] = {
|
||||
dns = {},
|
||||
ipsets = {},
|
||||
address = "::"
|
||||
}
|
||||
end
|
||||
|
||||
if list1 and next(list1) then
|
||||
local address_out = io.open(CACHE_DNS_PATH .. "/000-address.conf", "a")
|
||||
local server_out = io.open(CACHE_DNS_PATH .. "/001-server.conf", "a")
|
||||
local ipset_out = io.open(CACHE_DNS_PATH .. "/ipset.conf", "a")
|
||||
local set_name = "ipset"
|
||||
if NFTFLAG == "1" then
|
||||
set_name = "nftset"
|
||||
end
|
||||
for key, value in pairs(list1) do
|
||||
if value.address then
|
||||
local domain = "." .. key
|
||||
if key == "#" then
|
||||
domain = key
|
||||
end
|
||||
address_out:write(string.format("address=/%s/%s", domain, value.address) .. "\n")
|
||||
end
|
||||
if value.dns and #value.dns > 0 then
|
||||
for i, dns in ipairs(value.dns) do
|
||||
server_out:write(string.format("server=/.%s/%s", key, dns) .. "\n")
|
||||
end
|
||||
end
|
||||
if value.ipsets and #value.ipsets > 0 then
|
||||
local ipsets_str = ""
|
||||
for i, ipset in ipairs(value.ipsets) do
|
||||
ipsets_str = ipsets_str .. ipset .. ","
|
||||
end
|
||||
ipsets_str = ipsets_str:sub(1, #ipsets_str - 1)
|
||||
ipset_out:write(string.format("%s=/.%s/%s", set_name, key, ipsets_str) .. "\n")
|
||||
end
|
||||
end
|
||||
address_out:close()
|
||||
server_out:close()
|
||||
ipset_out:close()
|
||||
end
|
||||
|
||||
if not GEO_SUCCESS then new_text = "" end
|
||||
local f_out = io.open(CACHE_TEXT_FILE, "a")
|
||||
f_out:write(new_text)
|
||||
f_out:close()
|
||||
else
|
||||
log(" - 从缓存加载 Dnsmasq 域名分流规则。")
|
||||
end
|
||||
|
||||
if USE_CHINADNS_NG == "0" then
|
||||
api.remove(TMP_DNSMASQ_PATH)
|
||||
fs.symlink(CACHE_DNS_PATH, TMP_DNSMASQ_PATH)
|
||||
end
|
||||
|
||||
if DNSMASQ_CONF_FILE ~= "nil" then
|
||||
local conf_lines = {}
|
||||
if LISTEN_PORT then
|
||||
--Copy dnsmasq instance
|
||||
conf_lines = copy_instance({["-LISTEN_PORT"] = LISTEN_PORT, ["-TMP_DNSMASQ_PATH"] = TMP_DNSMASQ_PATH, ["-return"] = "1"})
|
||||
--dhcp.leases to hosts
|
||||
local hosts = api.CACHE_PATH .. "/dhcp-hosts"
|
||||
sys.call("touch " .. hosts)
|
||||
tinsert(conf_lines, "addn-hosts=" .. hosts)
|
||||
else
|
||||
--Modify the default dnsmasq service
|
||||
end
|
||||
if USE_CHINADNS_NG == "0" then
|
||||
tinsert(conf_lines, string.format("conf-dir=%s", TMP_DNSMASQ_PATH))
|
||||
end
|
||||
if dnsmasq_default_dns then
|
||||
for s in string.gmatch(dnsmasq_default_dns, '[^' .. "," .. ']+') do
|
||||
tinsert(conf_lines, string.format("server=%s", s))
|
||||
end
|
||||
tinsert(conf_lines, "all-servers")
|
||||
tinsert(conf_lines, "no-poll")
|
||||
tinsert(conf_lines, "no-resolv")
|
||||
if USE_CHINADNS_NG == "0" then
|
||||
log(string.format(" - 默认:%s", dnsmasq_default_dns))
|
||||
end
|
||||
|
||||
if FLAG == "default" then
|
||||
api.set_cache_var("DEFAULT_DNS", DEFAULT_DNS)
|
||||
end
|
||||
end
|
||||
if #conf_lines > 0 then
|
||||
local conf_out = io.open(DNSMASQ_CONF_FILE, "a")
|
||||
if conf_out then
|
||||
conf_out:write(table.concat(conf_lines, "\n"))
|
||||
conf_out:write("\n")
|
||||
conf_out:close()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if USE_CHINADNS_NG == "0" then
|
||||
log(" - PassWall必须依赖于Dnsmasq,如果你自行配置了错误的DNS流程,将会导致域名(直连/代理)分流失效!!!")
|
||||
end
|
||||
end
|
||||
|
||||
_G.stretch = stretch
|
||||
_G.restart = restart
|
||||
_G.logic_restart = logic_restart
|
||||
_G.copy_instance = copy_instance
|
||||
_G.add_rule = add_rule
|
||||
|
||||
if arg[1] then
|
||||
local func =_G[arg[1]]
|
||||
if func then
|
||||
func(api.get_function_args(arg))
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,814 @@
|
||||
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
|
||||
+1637
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,98 @@
|
||||
#!/bin/sh
|
||||
|
||||
# DHCP leases and static hosts to hosts
|
||||
|
||||
. /lib/functions.sh
|
||||
. /usr/share/passwall/utils.sh
|
||||
LOCK_FILE=${LOCK_PATH}/${CONFIG}_lease2hosts.lock
|
||||
DNSMASQ_LEASE_FILE=$(uci -q get 'dhcp.@dnsmasq[0].leasefile')
|
||||
DNSMASQ_LEASE_FILE=${DNSMASQ_LEASE_FILE:-/tmp/dhcp.leases}
|
||||
ODHCPD_LEASE_FILE=$(uci -q get dhcp.odhcpd.leasefile)
|
||||
ODHCPD_LEASE_FILE=${ODHCPD_LEASE_FILE:-/tmp/odhcpd.leases}
|
||||
HOSTS_FILE="$TMP_PATH2/dhcp-hosts"
|
||||
TMP_FILE="/tmp/dhcp-hosts.tmp"
|
||||
|
||||
exec 9>"$LOCK_FILE"
|
||||
flock -n 9 || exit 0
|
||||
|
||||
read_static_host() {
|
||||
local name ip
|
||||
config_get name "$1" name
|
||||
config_get ip "$1" ip
|
||||
[ -n "$name" ] && [ "$name" != "*" ] && [ -n "$ip" ] && [ "$ip" != "ignore" ] || return
|
||||
printf '%s %s\n' "$ip" "$name"
|
||||
}
|
||||
|
||||
# Static DHCP configuration only needs to be read once at startup.
|
||||
STATIC_HOSTS=$(
|
||||
config_load dhcp
|
||||
config_foreach read_static_host host
|
||||
)
|
||||
|
||||
read_odhcpd_leases() {
|
||||
[ -f "$ODHCPD_LEASE_FILE" ] || return
|
||||
awk -v now="$(date +%s)" '
|
||||
function valid_name(name) {
|
||||
return name ~ /^[a-zA-Z0-9_][a-zA-Z0-9_.-]*$/
|
||||
}
|
||||
# State records: # interface DUID IAID hostname expiry assigned length address/prefix ...
|
||||
$1 == "#" {
|
||||
if (NF < 9 || !valid_name($5) || ($6 != -1 && $6 <= now))
|
||||
next
|
||||
# Only host addresses, never delegated IPv6 prefixes.
|
||||
if (!(($4 == "ipv4" && $8 == 32) || ($4 != "ipv4" && $8 == 128)))
|
||||
next
|
||||
for (i = 9; i <= NF; i++) {
|
||||
split($i, addr, "/")
|
||||
print addr[1] " " $5
|
||||
}
|
||||
next
|
||||
}
|
||||
# Older odhcpd versions also include hosts-format lines, with optional aliases.
|
||||
NF >= 2 && $1 ~ /^[0-9a-fA-F:.]+$/ {
|
||||
for (i = 2; i <= NF && $i !~ /^#/; i++)
|
||||
if (valid_name($i))
|
||||
print $1 " " $i
|
||||
}
|
||||
' "$ODHCPD_LEASE_FILE"
|
||||
}
|
||||
|
||||
reload_dnsmasq_pids() {
|
||||
local pidfile pid
|
||||
find $TMP_PATH/acl -type f -name 'dnsmasq.pid' 2>/dev/null | while read pidfile; do
|
||||
if [ -s "$pidfile" ]; then
|
||||
read pid < "$pidfile"
|
||||
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
||||
kill -HUP "$pid"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
while true; do
|
||||
|
||||
{
|
||||
[ -n "$STATIC_HOSTS" ] && printf '%s\n' "$STATIC_HOSTS"
|
||||
if [ -f "$DNSMASQ_LEASE_FILE" ]; then
|
||||
awk 'NF >= 4 && $4 != "*" {print $3" "$4}' "$DNSMASQ_LEASE_FILE"
|
||||
fi
|
||||
read_odhcpd_leases
|
||||
} | sort -u > "$TMP_FILE"
|
||||
if [ -s "$TMP_FILE" ]; then
|
||||
if [ ! -f "$HOSTS_FILE" ] || ! cmp -s "$TMP_FILE" "$HOSTS_FILE"; then
|
||||
mv "$TMP_FILE" "$HOSTS_FILE"
|
||||
reload_dnsmasq_pids
|
||||
else
|
||||
rm -f "$TMP_FILE"
|
||||
fi
|
||||
else
|
||||
if [ -s "$HOSTS_FILE" ]; then
|
||||
: > "$HOSTS_FILE"
|
||||
reload_dnsmasq_pids
|
||||
fi
|
||||
rm -f "$TMP_FILE"
|
||||
fi
|
||||
|
||||
sleep 60 9>&-
|
||||
|
||||
done 2>/dev/null
|
||||
@@ -0,0 +1,31 @@
|
||||
#!/bin/sh
|
||||
CONFIG=passwall
|
||||
dir="$1"
|
||||
status="$2"
|
||||
manager="$3"
|
||||
force="$4"
|
||||
|
||||
if [ "$manager" = apk ]; then
|
||||
extra=""
|
||||
[ "$force" = 1 ] && extra="--force-reinstall --force-overwrite"
|
||||
apk add --allow-untrusted $extra "$dir/luci-app-${CONFIG}.apk" "$dir/luci-i18n-${CONFIG}-zh-cn.apk"
|
||||
result=$?
|
||||
else
|
||||
extra=""
|
||||
[ "$force" = 1 ] && extra="--force-reinstall"
|
||||
opkg install $extra "$dir/luci-app-${CONFIG}.ipk" "$dir/luci-i18n-${CONFIG}-zh-cn.ipk"
|
||||
result=$?
|
||||
fi
|
||||
|
||||
rm -rf "$dir"
|
||||
if [ "$result" = 0 ]; then
|
||||
rm -f "/tmp/etc/${CONFIG}_tmp/${CONFIG}_version" /tmp/luci-indexcache /tmp/luci-indexcache.*
|
||||
if [ -x /etc/init.d/rpcd ]; then
|
||||
/etc/init.d/rpcd restart >/dev/null 2>&1
|
||||
fi
|
||||
if [ -x /etc/init.d/uhttpd ]; then
|
||||
/etc/init.d/uhttpd reload >/dev/null 2>&1
|
||||
fi
|
||||
fi
|
||||
[ "$result" = 0 ] && echo '{"code":0}' > "$status" || echo '{"code":1}' > "$status"
|
||||
(sleep 60; rm -f "$status") >/dev/null 2>&1 &
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
#!/bin/sh
|
||||
|
||||
DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
. $DIR/utils.sh
|
||||
LOCK_FILE=${LOCK_PATH}/${CONFIG}_monitor.lock
|
||||
|
||||
MAX_RESTART_COUNT=10
|
||||
RESTART_STATS_DIR="${TMP_PATH}/script_rstats"
|
||||
mkdir -p "$RESTART_STATS_DIR"
|
||||
|
||||
[ "$(get_cache_var "ENABLED_DEFAULT_ACL")" != 1 ] && [ "$(get_cache_var "ENABLED_ACLS")" != 1 ] && exit 1
|
||||
ENABLED=$(config_n_get @global_delay[0] start_daemon 0)
|
||||
[ "$ENABLED" != 1 ] && exit 1
|
||||
exec 9>"$LOCK_FILE"
|
||||
flock -n 9 || exit 0
|
||||
sleep 58s 9>&-
|
||||
last_cleanup_date=$(date +%Y%m%d)
|
||||
while [ "$ENABLED" -eq 1 ]; do
|
||||
for file in "$TMP_SCRIPT_FUNC_PATH"/*; do
|
||||
[ -f "$file" ] || continue
|
||||
{
|
||||
IFS= read -r pid
|
||||
IFS= read -r start_time
|
||||
} < "$file"
|
||||
cmd=$(sed '1,2d' "$file")
|
||||
[ -z "$cmd" ] && continue
|
||||
|
||||
filename=$(basename "$file")
|
||||
stats_file="${RESTART_STATS_DIR}/${filename}.count"
|
||||
if [ -s "$stats_file" ]; then
|
||||
read restart_count < "$stats_file"
|
||||
[ -z "$restart_count" ] && restart_count=0
|
||||
else
|
||||
restart_count=0
|
||||
fi
|
||||
# 检查是否超过最大重启次数
|
||||
[ "$restart_count" -ge "$MAX_RESTART_COUNT" ] && continue
|
||||
|
||||
current_start_time=$(get_process_start_time "$pid")
|
||||
if [ -z "$current_start_time" ] || [ "$current_start_time" != "$start_time" ]; then
|
||||
restart_count=$((restart_count + 1))
|
||||
echo "$restart_count" > "$stats_file"
|
||||
#echo "${cmd} 进程挂掉,重启" >> /tmp/log/passwall.log
|
||||
pid=$(sh -c "nohup ${cmd} & echo \$!" 9>&-)
|
||||
{
|
||||
printf '%s\n' "$pid" "$(get_process_start_time "$pid")"
|
||||
printf '%s\n' "$cmd"
|
||||
} > "$file"
|
||||
sleep 1 9>&-
|
||||
fi
|
||||
done
|
||||
|
||||
# 每天清理一次统计文件(跨天后执行一次)
|
||||
current_date=$(date +%Y%m%d)
|
||||
if [ "$current_date" != "$last_cleanup_date" ]; then
|
||||
rm -f "${RESTART_STATS_DIR:?}"/* 2>/dev/null
|
||||
last_cleanup_date="$current_date"
|
||||
fi
|
||||
|
||||
sleep 58s 9>&-
|
||||
done
|
||||
+1641
File diff suppressed because it is too large.
Load diff
+902
@@ -0,0 +1,902 @@
|
||||
#!/usr/bin/lua
|
||||
|
||||
local api = require ("luci.passwall.api")
|
||||
local appname = api.appname
|
||||
local sys = api.sys
|
||||
local jsonc = api.jsonc
|
||||
local fs = api.fs
|
||||
local uci, uci_get, uci_set, uci_del, uci_foreach, uci_save = api.uci, api.uci_get_c, api.uci_set_c, api.uci_del_c, api.uci_foreach_c, api.uci_save_c
|
||||
|
||||
-- [[ 逻辑锁 Start ]] --
|
||||
-- Both update scripts use the same lockf lock; the kernel releases it on exit.
|
||||
local update_lock = api.nixio.open(api.LOCK_PREFIX .. "_sub_rule.lock", api.nixio.open_flags("rdwr", "creat"))
|
||||
if not update_lock then
|
||||
api.log("无法打开订阅/规则更新锁,退出。")
|
||||
os.exit(1)
|
||||
end
|
||||
-- Scheduled updates wait for the lock; manual requests may skip a busy lock.
|
||||
if not update_lock:lock("tlock") then
|
||||
if arg[3] ~= "cron" then
|
||||
api.log("订阅或规则更新正在运行,本次请求跳过。")
|
||||
os.exit(0)
|
||||
end
|
||||
local running = ({s = "订阅", r = "规则更新"})[update_lock:read(1)] or "订阅/规则更新"
|
||||
update_lock:seek(0)
|
||||
api.log("[" .. running .. "]实例正在运行,[规则更新]进入队列等待...")
|
||||
if not update_lock:lock("lock") then
|
||||
api.log("无法取得更新锁,计划任务失败。")
|
||||
os.exit(1)
|
||||
end
|
||||
end
|
||||
-- Record the running task in the same file: s = subscription, r = rule update.
|
||||
update_lock:write("r")
|
||||
update_lock:seek(0)
|
||||
api.uci:revert(api.c_config)
|
||||
math.randomseed(os.time() + math.floor(os.clock() * 1000))
|
||||
-- [[ 逻辑锁 End ]] --
|
||||
|
||||
local arg1 = arg[1]
|
||||
local arg2 = arg[2]
|
||||
local arg3 = arg[3]
|
||||
|
||||
local nftable_name = "inet passwall"
|
||||
local rule_path = "/usr/share/passwall/rules"
|
||||
local reboot = 0
|
||||
local gfwlist_update = "0"
|
||||
local chnroute_update = "0"
|
||||
local chnroute6_update = "0"
|
||||
local chnlist_update = "0"
|
||||
local geoip_update = "0"
|
||||
local geosite_update = "0"
|
||||
|
||||
local excluded_domain = {"apple.com","sina.cn","sina.com.cn","baidu.com","byr.cn","jlike.com","weibo.com","zhongsou.com","youdao.com","sogou.com","so.com","soso.com","aliyun.com","taobao.com","jd.com","qq.com","bing.com"}
|
||||
|
||||
local gfwlist_url = uci_get("@global_rules[0]", "gfwlist_url") or {"https://fastly.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/gfw.txt"}
|
||||
local chnroute_url = uci_get("@global_rules[0]", "chnroute_url") or {"https://ispip.clang.cn/all_cn.txt"}
|
||||
local chnroute6_url = uci_get("@global_rules[0]", "chnroute6_url") or {"https://ispip.clang.cn/all_cn_ipv6.txt"}
|
||||
local chnlist_url = uci_get("@global_rules[0]", "chnlist_url") or {"https://fastly.jsdelivr.net/gh/felixonmars/dnsmasq-china-list/accelerated-domains.china.conf","https://fastly.jsdelivr.net/gh/felixonmars/dnsmasq-china-list/apple.china.conf","https://fastly.jsdelivr.net/gh/felixonmars/dnsmasq-china-list/google.china.conf"}
|
||||
local geoip_url = uci_get("@global_rules[0]", "geoip_url") or "https://github.com/Loyalsoldier/geoip/releases/latest/download/geoip.dat"
|
||||
local geosite_url = uci_get("@global_rules[0]", "geosite_url") or "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat"
|
||||
local asset_location = uci_get("@global_rules[0]", "v2ray_location_asset") or "/usr/share/v2ray/"
|
||||
local geo2rule = uci_get("@global_rules[0]", "geo2rule") or "0"
|
||||
local geoip_update_ok, geosite_update_ok = false, false
|
||||
asset_location = asset_location:match("/$") and asset_location or (asset_location .. "/")
|
||||
local backup_path = "/tmp/bak_v2ray/"
|
||||
local rollback = false
|
||||
|
||||
if arg3 == "cron" then
|
||||
arg2 = nil
|
||||
elseif arg3 == "rollback" then
|
||||
rollback, geoip_update_ok, geosite_update_ok = true, true, true
|
||||
end
|
||||
|
||||
local log = function(...)
|
||||
if arg1 then
|
||||
if arg1 == "log" then
|
||||
api.log(...)
|
||||
elseif arg1 == "print" then
|
||||
local result = os.date("%Y-%m-%d %H:%M:%S: ") .. table.concat({...}, " ")
|
||||
print(result)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--gen cache for nftset from file
|
||||
local function gen_cache(set_name, ip_type, input_file, output_file)
|
||||
local tmp_set_name = set_name .. "_tmp_" .. os.time()
|
||||
local final_set_name = set_name .. "_static"
|
||||
local f_in = io.open(input_file, "r")
|
||||
if not f_in then return false end
|
||||
local nft_pipe = io.popen("nft -f -", "w")
|
||||
if not nft_pipe then
|
||||
f_in:close()
|
||||
return false
|
||||
end
|
||||
nft_pipe:write('#!/usr/sbin/nft -f\n')
|
||||
nft_pipe:write(string.format('add table %s\n', nftable_name))
|
||||
nft_pipe:write(string.format('add set %s %s { type %s; flags interval; auto-merge; }\n', nftable_name, tmp_set_name, ip_type))
|
||||
nft_pipe:write(string.format('add element %s %s { ', nftable_name, tmp_set_name))
|
||||
local count = 0
|
||||
local batch_size = 500
|
||||
for line in f_in:lines() do
|
||||
local ip = line:match("^%s*(.-)%s*$")
|
||||
if ip and ip ~= "" then
|
||||
nft_pipe:write(ip, ", ")
|
||||
count = count + 1
|
||||
if count % batch_size == 0 then
|
||||
nft_pipe:write("}\n")
|
||||
nft_pipe:write(string.format('add element %s %s { ', nftable_name, tmp_set_name))
|
||||
end
|
||||
end
|
||||
end
|
||||
nft_pipe:write("}\n")
|
||||
f_in:close()
|
||||
|
||||
local success = nft_pipe:close()
|
||||
if not (success == true or success == 0) then
|
||||
os.execute(string.format('nft delete set %s %s 2>/dev/null', nftable_name, tmp_set_name))
|
||||
return false
|
||||
end
|
||||
os.execute(string.format('nft list set %s %s | sed "s/%s/%s/g" > %s', nftable_name, tmp_set_name, tmp_set_name, final_set_name, output_file))
|
||||
os.execute(string.format('nft delete set %s %s 2>/dev/null', nftable_name, tmp_set_name))
|
||||
end
|
||||
|
||||
-- curl
|
||||
local function curl(url, file)
|
||||
local http_code = 0
|
||||
local header_str = ""
|
||||
local args = {
|
||||
"-fskL",
|
||||
"--retry 3",
|
||||
"--connect-timeout 3",
|
||||
"--max-time 300",
|
||||
"--speed-limit 51200 --speed-time 15",
|
||||
"-H 'Accept: */*'",
|
||||
'-A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36"',
|
||||
"--dump-header -",
|
||||
"-w '\\n%{http_code}'"
|
||||
}
|
||||
if file then
|
||||
args[#args + 1] = "-o " .. file
|
||||
end
|
||||
local return_code, result = api.curl_auto(url, nil, args)
|
||||
if result and result ~= "" then
|
||||
local body, code = result:match("^(.-)%s*([0-9]+)$")
|
||||
if code then
|
||||
http_code = tonumber(code) or 0
|
||||
header_str = body
|
||||
else
|
||||
http_code = tonumber(result:match("(%d+)%s*$")) or 0
|
||||
end
|
||||
end
|
||||
if header_str ~= "" then
|
||||
header_str = header_str:gsub("\r", "")
|
||||
end
|
||||
return return_code, http_code, header_str
|
||||
end
|
||||
|
||||
--check excluded domain
|
||||
local excluded_map = {}
|
||||
for _, d in ipairs(excluded_domain) do
|
||||
excluded_map[d] = true
|
||||
end
|
||||
local function check_excluded_domain(value)
|
||||
if not value or value == "" then return false end
|
||||
value = value:lower()
|
||||
local eq_pos = value:find("=", 1, true)
|
||||
if eq_pos then
|
||||
value = value:sub(eq_pos + 1)
|
||||
end
|
||||
if value:sub(1,1) == "/" then
|
||||
value = value:sub(2)
|
||||
end
|
||||
local slash_pos = value:find("/", 1, true)
|
||||
local colon_pos = value:find(":", 1, true)
|
||||
local cut_pos
|
||||
if slash_pos and colon_pos then
|
||||
cut_pos = (slash_pos < colon_pos) and slash_pos or colon_pos
|
||||
else
|
||||
cut_pos = slash_pos or colon_pos
|
||||
end
|
||||
if cut_pos then
|
||||
value = value:sub(1, cut_pos - 1)
|
||||
end
|
||||
value = value:gsub("^%.*", ""):gsub("%.*$", "")
|
||||
while value do
|
||||
if excluded_map[value] then
|
||||
return true
|
||||
end
|
||||
local dot_pos = value:find(".", 1, true)
|
||||
if not dot_pos then
|
||||
break
|
||||
end
|
||||
value = value:sub(dot_pos + 1)
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- 替代 string.find 查找 "^[#!\\[@]+"
|
||||
local function is_comment_line(s)
|
||||
if not s or s == "" then return false end
|
||||
local b = s:byte(1)
|
||||
-- '#' = 35, '!' = 33, '\' = 92, '[' = 91, '@' = 64
|
||||
if b == 35 or b == 33 or b == 92 or b == 91 or b == 64 then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- IPv4 检测,替代 string.find "^%d+%.%d+%.%d+%.%d+"
|
||||
-- IPv4 cidr检测,替代 string.find "^%d+%.%d+%.%d+%.%d+[%/][%d]+$"
|
||||
local function is_ipv4(s, check_cidr)
|
||||
local dot = 0
|
||||
local seg_start = 1
|
||||
local len = #s
|
||||
local mask_start = nil
|
||||
local i = 1
|
||||
while i <= len do
|
||||
local b = s:byte(i)
|
||||
if b >= 48 and b <= 57 then
|
||||
-- 数字,继续
|
||||
elseif b == 46 then -- "."
|
||||
dot = dot + 1
|
||||
if dot > 3 or i == seg_start then return false end
|
||||
local seg = tonumber(s:sub(seg_start, i - 1))
|
||||
if not seg or seg > 255 then return false end
|
||||
seg_start = i + 1
|
||||
elseif b == 47 then -- "/"
|
||||
if not check_cidr then return false end
|
||||
if dot ~= 3 or i == seg_start then return false end
|
||||
local seg = tonumber(s:sub(seg_start, i - 1))
|
||||
if not seg or seg > 255 then return false end
|
||||
mask_start = i + 1
|
||||
break
|
||||
else
|
||||
return false
|
||||
end
|
||||
i = i + 1
|
||||
end
|
||||
-- 如果没有 CIDR,则检查最后一段即可
|
||||
if not check_cidr or not mask_start then
|
||||
if dot ~= 3 or seg_start > len then return false end
|
||||
local seg = tonumber(s:sub(seg_start))
|
||||
return seg and seg <= 255 or false
|
||||
end
|
||||
-- CIDR 掩码检查
|
||||
if mask_start > len then return false end
|
||||
local mask = tonumber(s:sub(mask_start))
|
||||
return mask and mask >= 0 and mask <= 32 or false
|
||||
end
|
||||
|
||||
local function is_ipv4_cidr(s)
|
||||
return is_ipv4(s, true)
|
||||
end
|
||||
|
||||
local function is_ipv6(s, check_cidr)
|
||||
local first = s:byte(1)
|
||||
local last = s:byte(#s)
|
||||
if first == 91 and last == 93 then -- "[" and "]"
|
||||
s = s:sub(2, -2)
|
||||
end
|
||||
local len = #s
|
||||
local i = 1
|
||||
local seg_len = 0
|
||||
local segs = 0
|
||||
local saw_dc = false -- 是否出现 "::"
|
||||
local b
|
||||
while i <= len do
|
||||
b = s:byte(i)
|
||||
-- CIDR 部分
|
||||
if b == 47 then -- '/'
|
||||
if not check_cidr then
|
||||
return false
|
||||
end
|
||||
-- 处理 "/" 之前的段
|
||||
if seg_len > 0 then segs = segs + 1 end
|
||||
if (not saw_dc and segs ~= 8) or (saw_dc and segs > 8) then return false end
|
||||
-- 解析掩码
|
||||
i = i + 1
|
||||
if i > len then return false end
|
||||
local mask = 0
|
||||
while i <= len do
|
||||
b = s:byte(i)
|
||||
if b < 48 or b > 57 then return false end
|
||||
mask = mask * 10 + (b - 48)
|
||||
if mask > 128 then return false end
|
||||
i = i + 1
|
||||
end
|
||||
-- CIDR 解析成功
|
||||
return true
|
||||
end
|
||||
-- 冒号处理(: 或 ::)
|
||||
if b == 58 then
|
||||
local nextb = (i+1 <= len) and s:byte(i+1) or 0
|
||||
-- "::"
|
||||
if nextb == 58 then
|
||||
if saw_dc then return false end
|
||||
saw_dc = true
|
||||
if seg_len > 0 then segs = segs + 1 end
|
||||
seg_len = 0
|
||||
i = i + 2
|
||||
else
|
||||
-- 普通 ":"
|
||||
if seg_len == 0 then return false end
|
||||
segs = segs + 1
|
||||
seg_len = 0
|
||||
i = i + 1
|
||||
end
|
||||
else
|
||||
-- hex 数字
|
||||
local is_hex =
|
||||
(b >= 48 and b <= 57) or -- 0-9
|
||||
(b >= 65 and b <= 70) or -- A-F
|
||||
(b >= 97 and b <= 102) -- a-f
|
||||
if not is_hex then return false end
|
||||
seg_len = seg_len + 1
|
||||
if seg_len > 4 then return false end
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
if seg_len > 0 then segs = segs + 1 end
|
||||
if not saw_dc then return segs == 8 end
|
||||
return segs <= 8
|
||||
end
|
||||
|
||||
-- IPv6 cidr检测,替代 string.find ":-[%x]+%:+[%x]-[%/][%d]+$"
|
||||
local function is_ipv6_cidr(s)
|
||||
return is_ipv6(s, true)
|
||||
end
|
||||
|
||||
-- 检测是否含有冒号,替代 string.find(line, ":")
|
||||
local function has_colon(s)
|
||||
for i = 1, #s do
|
||||
if s:byte(i) == 58 then -- ':'
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- 域名提取,替代 string.match "([%w%-]+%.[%w%.%-]+)[%/%*]*"
|
||||
local function extract_domain(s)
|
||||
if not s or s == "" then return nil end
|
||||
local len = #s
|
||||
local start = nil
|
||||
local last_dot = nil
|
||||
for i = 1, len do
|
||||
local b = s:byte(i)
|
||||
-- 允许的域名字符:a-zA-Z0-9.-
|
||||
if (b >= 48 and b <= 57) or (b >= 65 and b <= 90) or (b >= 97 and b <= 122) or b == 45 or b == 46 then
|
||||
if not start then start = i end
|
||||
if b == 46 then last_dot = i end
|
||||
else
|
||||
if start then
|
||||
if last_dot and last_dot > start then
|
||||
local domain = s:sub(start, i - 1)
|
||||
while domain:byte(1) == 46 do
|
||||
domain = domain:sub(2)
|
||||
end
|
||||
return domain
|
||||
else
|
||||
start = nil
|
||||
last_dot = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if start and last_dot and last_dot > start then
|
||||
local domain = s:sub(start)
|
||||
while domain:byte(1) == 46 do
|
||||
domain = domain:sub(2)
|
||||
end
|
||||
return domain
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function non_file_check(file_path, header_content)
|
||||
local remote_file_size = nil
|
||||
local local_file_size = tonumber(fs.stat(file_path, "size") or 0)
|
||||
if local_file_size == 0 then
|
||||
log("下载文件为空或读取出错。")
|
||||
return true
|
||||
end
|
||||
if header_content and header_content ~= "" then
|
||||
for size in header_content:gmatch("[Cc]ontent%-[Ll]ength:%s*(%d+)") do
|
||||
local s = tonumber(size)
|
||||
if s and s > 0 then
|
||||
remote_file_size = s
|
||||
end
|
||||
end
|
||||
end
|
||||
if remote_file_size and remote_file_size ~= local_file_size then
|
||||
log(string.format("校验出错:远程 %dB, 下载 %dB", remote_file_size, local_file_size))
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- 检查 Geo 所有分类的边界,每个分类抽查首条规则,仅用于识别 Geo 格式。
|
||||
local function validate_geo_file(file_path, geo_type)
|
||||
if geo_type ~= "geoip" and geo_type ~= "geosite" then return false end
|
||||
local file = io.open(file_path, "rb")
|
||||
if not file then return false end
|
||||
local size = file:seek("end")
|
||||
if not size or size == 0 or not file:seek("set", 0) then file:close(); return false end
|
||||
local pos, rules = 0, 0
|
||||
-- 使用文件自身的读取缓冲,跳过内容时直接定位,无需维护额外缓冲区。
|
||||
local function skip(length, limit)
|
||||
assert(length <= limit - pos)
|
||||
pos = pos + length
|
||||
assert(file:seek("set", pos))
|
||||
end
|
||||
-- 未指定数值上限时,仅检查并跳过变长整数,避免 64 位属性值的精度问题。
|
||||
local function varint(limit, maximum)
|
||||
local value, factor = 0, 1
|
||||
for i = 1, 10 do
|
||||
assert(pos < limit)
|
||||
local byte = assert(file:read(1)):byte()
|
||||
pos = pos + 1
|
||||
assert(i < 10 or byte <= 1)
|
||||
if maximum then
|
||||
local digit = byte % 128
|
||||
assert(digit <= math.floor((maximum - value) / factor))
|
||||
value = value + digit * factor
|
||||
end
|
||||
if byte < 128 then return value end
|
||||
factor = factor * 128
|
||||
end
|
||||
error(false)
|
||||
end
|
||||
-- 字段定义:编码类型、子消息类型(可选)、数值上限(可选)。
|
||||
local schema = {
|
||||
list = {{2, geo_type}},
|
||||
geoip = {{2}, {2, "cidr"}, {0}},
|
||||
geosite = {{2}, {2, "domain"}},
|
||||
cidr = {{2}, {0, nil, 4294967295}},
|
||||
domain = {{0, nil, 3}, {2}, {2, "attribute"}},
|
||||
attribute = {{2}, {0}, {0}}
|
||||
}
|
||||
local parse
|
||||
parse = function(kind, limit)
|
||||
local values, entries = {}, 0
|
||||
local is_entry = kind == "geoip" or kind == "geosite"
|
||||
while pos < limit do
|
||||
if is_entry and values[1] and values[1] > 0 and entries > 0 then
|
||||
skip(limit - pos, limit)
|
||||
return
|
||||
end
|
||||
local tag = varint(limit, 4294967295)
|
||||
local field, wire = math.floor(tag / 8), tag % 8
|
||||
local spec = schema[kind][field]
|
||||
assert(field > 0 and (not spec or wire == spec[1]))
|
||||
local value
|
||||
if wire == 0 then
|
||||
value = varint(limit, spec and spec[3])
|
||||
elseif wire == 2 then
|
||||
value = varint(limit, limit - pos)
|
||||
assert(value <= limit - pos)
|
||||
if spec and spec[2] and not (is_entry and entries > 0) then
|
||||
parse(spec[2], pos + value)
|
||||
else
|
||||
skip(value, limit)
|
||||
end
|
||||
if spec and spec[2] then entries = entries + 1 end
|
||||
elseif wire == 1 or wire == 5 then
|
||||
skip(wire == 1 and 8 or 4, limit)
|
||||
else
|
||||
-- Geo 格式不使用 Protobuf 分组字段。
|
||||
error(false)
|
||||
end
|
||||
if spec then values[field] = value end
|
||||
end
|
||||
if kind == "list" then
|
||||
assert(entries > 0 and rules > 0)
|
||||
elseif kind == "geoip" or kind == "geosite" or kind == "attribute" then
|
||||
assert(values[1] and values[1] > 0)
|
||||
elseif kind == "cidr" then
|
||||
assert(values[1] == 4 or values[1] == 16)
|
||||
assert((values[2] or 0) <= (values[1] == 4 and 32 or 128))
|
||||
rules = rules + 1
|
||||
elseif kind == "domain" then
|
||||
assert(values[2] and values[2] > 0)
|
||||
rules = rules + 1
|
||||
end
|
||||
end
|
||||
local valid = pcall(parse, "list", size)
|
||||
file:close()
|
||||
return valid
|
||||
end
|
||||
|
||||
local function GeoToRule(rule_name, rule_type, out_path)
|
||||
local bin = api.finded_com("geoview")
|
||||
if not (bin and api.compare_versions(api.get_app_version("geoview"), ">=", "0.1.10")) then
|
||||
log("[警告] Geoview 组件缺失或版本过低,规则生成流程已被跳过。")
|
||||
return false
|
||||
end
|
||||
local geo_type = (rule_type == "domain") and "geosite" or "geoip"
|
||||
local geo_tag = (rule_type == "domain" and rule_name == "gfwlist") and "gfw" or "cn"
|
||||
local file_path = asset_location .. geo_type .. ".dat"
|
||||
if sys.call(string.format('"%s" -type %s -input "%s" | grep -q "^%s$"', bin, geo_type, file_path, geo_tag:upper())) ~= 0 then
|
||||
log(string.format("%s 生成失败:%s.dat 中缺少 %s 标签。", rule_name, geo_type, geo_tag))
|
||||
return false
|
||||
end
|
||||
local geo_arg = "-type " .. geo_type .. " -list " .. geo_tag
|
||||
if rule_type == "ip4" then
|
||||
geo_arg = geo_arg .. " -ipv6=false"
|
||||
elseif rule_type == "ip6" then
|
||||
geo_arg = geo_arg .. " -ipv4=false"
|
||||
end
|
||||
local cmd = string.format(bin .. " -input '%s' %s -lowmem=true -output '%s'", file_path, geo_arg, out_path)
|
||||
local code, out = api.exec_call(cmd)
|
||||
local local_file_size = tonumber(fs.stat(out_path, "size") or 0)
|
||||
if code ~= 0 or local_file_size == 0 then
|
||||
api.remove(out_path)
|
||||
log(rule_name .. " 生成失败,原因:" .. out)
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--fetch rule
|
||||
local function fetch_rule(rule_name, rule_type, url, exclude_domain, max_retries)
|
||||
local sret = 0
|
||||
local max_attempts = max_retries or 2
|
||||
local rule_dataset = {}
|
||||
local file_tmp = "/tmp/" .. rule_name .. "_tmp"
|
||||
local rule_final_path = rule_path .. "/" .. rule_name
|
||||
if geo2rule == "1" then
|
||||
url = {"geo2rule"}
|
||||
log(rule_name.. " 开始生成...")
|
||||
else
|
||||
log(rule_name.. " 开始更新...")
|
||||
end
|
||||
|
||||
for k, v in ipairs(url) do
|
||||
local current_file = "/tmp/" .. rule_name .. "_dl" .. k
|
||||
local success = false
|
||||
|
||||
if v ~= "geo2rule" then
|
||||
for i = 1, max_attempts do
|
||||
local return_code, http_code, header = curl(v, current_file)
|
||||
if return_code == 0 and not non_file_check(current_file, header) then
|
||||
success = true
|
||||
break
|
||||
end
|
||||
api.remove(current_file)
|
||||
log(string.format("%s 第%d条规则下载失败 (HTTP:%s),正在进行第%d次尝试...", rule_name, k, tostring(http_code), i))
|
||||
end
|
||||
else
|
||||
if not GeoToRule(rule_name, rule_type, current_file) then return 1 end
|
||||
success = true
|
||||
end
|
||||
|
||||
if success then
|
||||
local f = io.open(current_file, "r")
|
||||
if f then
|
||||
if rule_name == "gfwlist" and geo2rule == "0" then
|
||||
local decode = api.base64Decode(f:read("*all"))
|
||||
for line in string.gmatch(decode, "[^\r\n]+") do
|
||||
line = line:gsub("full:", "")
|
||||
if not (is_comment_line(line) or is_ipv4(line) or has_colon(line) or (exclude_domain and check_excluded_domain(line))) then
|
||||
local match = extract_domain(line)
|
||||
if match and not is_ipv4(match) then
|
||||
rule_dataset[match] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
for line in f:lines() do
|
||||
if rule_type == "domain" then
|
||||
line = line:gsub("full:", "")
|
||||
if not (is_comment_line(line) or is_ipv4(line) or has_colon(line) or (exclude_domain and check_excluded_domain(line))) then
|
||||
local match = extract_domain(line)
|
||||
if match and not is_ipv4(match) then
|
||||
rule_dataset[match] = true
|
||||
end
|
||||
end
|
||||
elseif rule_type == "ip4" then
|
||||
local function is_0dot(s) -- "^0%..*"
|
||||
return s and s:byte(1)==48 and s:byte(2)==46
|
||||
end
|
||||
if is_ipv4_cidr(line) and not is_0dot(line) then
|
||||
rule_dataset[line] = true
|
||||
end
|
||||
elseif rule_type == "ip6" then
|
||||
local function is_double_colon_cidr(s) -- "^::(/%d+)?$"
|
||||
if not s or s:byte(1)~=58 or s:byte(2)~=58 then return false end
|
||||
local l = #s
|
||||
if l==2 then return true end
|
||||
if l==3 or s:byte(3)~=47 then return false end
|
||||
for i=4,l do
|
||||
local b=s:byte(i)
|
||||
if b<48 or b>57 then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
if is_ipv6_cidr(line) and not is_double_colon_cidr(line) then
|
||||
rule_dataset[line] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
f:close()
|
||||
end
|
||||
else
|
||||
sret = 1
|
||||
log(string.format("%s 第%d条规则: %s 下载失败!", rule_name, k, v))
|
||||
end
|
||||
api.remove(current_file)
|
||||
end
|
||||
|
||||
if sret == 0 then
|
||||
local result_list = {}
|
||||
for line, _ in pairs(rule_dataset) do table.insert(result_list, line) end
|
||||
table.sort(result_list)
|
||||
|
||||
local out = io.open(file_tmp, "w")
|
||||
if out then
|
||||
for _, line in ipairs(result_list) do out:write(line .. "\n") end
|
||||
out:close()
|
||||
end
|
||||
|
||||
local old_md5 = api.md5_file(rule_final_path)
|
||||
local new_md5 = api.md5_file(file_tmp)
|
||||
|
||||
if old_md5 ~= new_md5 then
|
||||
if api.is_finded("fw4") and (rule_type == "ip4" or rule_type == "ip6") then
|
||||
local nft_file = file_tmp .. ".nft"
|
||||
local set_name = "psw_" .. rule_name
|
||||
if rule_name == "chnroute" then set_name = "psw_chn"
|
||||
elseif rule_name == "chnroute6" then set_name = "psw_chn6" end
|
||||
|
||||
local addr_type = (rule_type == "ip4") and "ipv4_addr" or "ipv6_addr"
|
||||
gen_cache(set_name, addr_type, file_tmp, nft_file)
|
||||
os.execute(string.format("mv -f %s %s.nft", nft_file, rule_final_path))
|
||||
end
|
||||
os.execute(string.format("mv -f %s %s", file_tmp, rule_final_path))
|
||||
if not rollback then reboot = 1 end
|
||||
log(string.format("%s 更新成功,总规则数 %d 条。", rule_name, #result_list))
|
||||
else
|
||||
log(rule_name .. " 版本一致,无需更新。")
|
||||
api.remove(file_tmp)
|
||||
end
|
||||
else
|
||||
log(rule_name .. " 更新失败(部分或全部资源无法下载)。")
|
||||
api.remove(file_tmp)
|
||||
end
|
||||
return 0
|
||||
end
|
||||
|
||||
local function fetch_geofile(geo_name, geo_type, url)
|
||||
local tmp_path = "/tmp/" .. geo_name
|
||||
local asset_path = asset_location .. geo_name
|
||||
local down_filename = url:match("^.*/([^/?#]+)")
|
||||
local sha_url = url:gsub((down_filename:gsub("(%W)", "%%%1")), down_filename .. ".sha256sum")
|
||||
local sha_path = tmp_path .. ".sha256sum"
|
||||
|
||||
local digest
|
||||
local sha_verify = select(1, curl(sha_url, sha_path)) == 0
|
||||
if sha_verify then
|
||||
local f = io.open(sha_path, "r")
|
||||
if f then
|
||||
local content = f:read("*l")
|
||||
f:close()
|
||||
local hash = content and content:match("^(%x+)%s")
|
||||
if hash and #hash == 64 then
|
||||
digest = "sha256:" .. hash
|
||||
end
|
||||
end
|
||||
if not digest then
|
||||
log(geo_type .. " SHA256 校验信息无效,更新失败。")
|
||||
return 1
|
||||
end
|
||||
if fs.access(asset_path) and api.verify_sha256(asset_path, digest) then
|
||||
log(geo_type .. " 版本一致,无需更新。")
|
||||
return 0
|
||||
end
|
||||
end
|
||||
|
||||
local sret_tmp, _, header = curl(url, tmp_path)
|
||||
if sret_tmp == 0 and non_file_check(tmp_path, header) then
|
||||
log(geo_type .. " 下载文件过程出错,尝试重新下载。")
|
||||
api.remove(tmp_path)
|
||||
sret_tmp, _, header= curl(url, tmp_path)
|
||||
if sret_tmp == 0 and non_file_check(tmp_path, header) then
|
||||
sret_tmp = 1
|
||||
log(geo_type .. " 下载文件过程出错,请检查网络或下载链接后重试!")
|
||||
end
|
||||
end
|
||||
if sret_tmp == 0 then
|
||||
if sha_verify then
|
||||
if api.verify_sha256(tmp_path, digest) then
|
||||
sys.call(string.format("mkdir -p %s && mv -f %s %s", backup_path, asset_path, backup_path))
|
||||
sys.call(string.format("mkdir -p %s && mv -f %s %s", asset_location, tmp_path, asset_path))
|
||||
reboot = 1
|
||||
log(geo_type .. " 更新成功。")
|
||||
if geo_type == "geoip" then
|
||||
geoip_update_ok = true
|
||||
else
|
||||
geosite_update_ok = true
|
||||
end
|
||||
else
|
||||
log(geo_type .. " 更新失败,请稍后重试或更换更新URL。")
|
||||
return 1
|
||||
end
|
||||
else
|
||||
if not validate_geo_file(tmp_path, geo_type) then
|
||||
log(geo_type .. " 文件格式校验错误,更新失败。")
|
||||
return 1
|
||||
end
|
||||
if fs.access(asset_path) and sys.call(string.format("cmp -s %s %s", tmp_path, asset_path)) == 0 then
|
||||
log(geo_type .. " 版本一致,无需更新。")
|
||||
return 0
|
||||
end
|
||||
sys.call(string.format("mkdir -p %s && mv -f %s %s", backup_path, asset_path, backup_path))
|
||||
sys.call(string.format("mkdir -p %s && mv -f %s %s", asset_location, tmp_path, asset_path))
|
||||
reboot = 1
|
||||
log(geo_type .. " 更新成功。")
|
||||
if geo_type == "geoip" then
|
||||
geoip_update_ok = true
|
||||
else
|
||||
geosite_update_ok = true
|
||||
end
|
||||
end
|
||||
else
|
||||
log(geo_type .. " 更新失败,请稍后重试或更换更新URL。")
|
||||
return 1
|
||||
end
|
||||
return 0
|
||||
end
|
||||
|
||||
local function fetch_gfwlist()
|
||||
fetch_rule("gfwlist","domain",gfwlist_url,true)
|
||||
end
|
||||
|
||||
local function fetch_chnroute()
|
||||
fetch_rule("chnroute","ip4",chnroute_url,false)
|
||||
end
|
||||
|
||||
local function fetch_chnroute6()
|
||||
fetch_rule("chnroute6","ip6",chnroute6_url,false)
|
||||
end
|
||||
|
||||
local function fetch_chnlist()
|
||||
fetch_rule("chnlist","domain",chnlist_url,false)
|
||||
end
|
||||
|
||||
local function fetch_geoip()
|
||||
fetch_geofile("geoip.dat","geoip",geoip_url)
|
||||
end
|
||||
|
||||
local function fetch_geosite()
|
||||
fetch_geofile("geosite.dat","geosite",geosite_url)
|
||||
end
|
||||
|
||||
if arg2 then
|
||||
string.gsub(arg2, '[^' .. "," .. ']+', function(w)
|
||||
if w == "gfwlist" then
|
||||
gfwlist_update = "1"
|
||||
end
|
||||
if w == "chnroute" then
|
||||
chnroute_update = "1"
|
||||
end
|
||||
if w == "chnroute6" then
|
||||
chnroute6_update = "1"
|
||||
end
|
||||
if w == "chnlist" then
|
||||
chnlist_update = "1"
|
||||
end
|
||||
if w == "geoip" then
|
||||
geoip_update = "1"
|
||||
end
|
||||
if w == "geosite" then
|
||||
geosite_update = "1"
|
||||
end
|
||||
end)
|
||||
if rollback then arg2 = nil end
|
||||
else
|
||||
gfwlist_update = uci_get("@global_rules[0]", "gfwlist_update") or "1"
|
||||
chnroute_update = uci_get("@global_rules[0]", "chnroute_update") or "1"
|
||||
chnroute6_update = uci_get("@global_rules[0]", "chnroute6_update") or "1"
|
||||
chnlist_update = uci_get("@global_rules[0]", "chnlist_update") or "1"
|
||||
geoip_update = uci_get("@global_rules[0]", "geoip_update") or "1"
|
||||
geosite_update = uci_get("@global_rules[0]", "geosite_update") or "1"
|
||||
end
|
||||
if geo2rule ~= "1" and gfwlist_update == "0" and chnroute_update == "0" and chnroute6_update == "0" and chnlist_update == "0" and geoip_update == "0" and geosite_update == "0" then
|
||||
update_lock:close()
|
||||
return
|
||||
end
|
||||
|
||||
log("开始更新规则...")
|
||||
local function safe_call(func, err_msg)
|
||||
xpcall(func, function(e)
|
||||
log(e)
|
||||
log(debug.traceback())
|
||||
log(err_msg)
|
||||
end)
|
||||
end
|
||||
|
||||
local function remove_tmp_geofile(name)
|
||||
api.remove("/tmp/" .. name .. ".dat")
|
||||
api.remove("/tmp/" .. name .. ".dat.sha256sum")
|
||||
end
|
||||
|
||||
if geo2rule == "1" then
|
||||
if geoip_update == "1" and not rollback then
|
||||
log("geoip 开始更新...")
|
||||
safe_call(fetch_geoip, "更新geoip发生错误...")
|
||||
remove_tmp_geofile("geoip")
|
||||
end
|
||||
|
||||
if geosite_update == "1" and not rollback then
|
||||
log("geosite 开始更新...")
|
||||
safe_call(fetch_geosite, "更新geosite发生错误...")
|
||||
remove_tmp_geofile("geosite")
|
||||
end
|
||||
|
||||
-- 如果是手动更新(arg2存在)始终生成规则
|
||||
if arg2 then
|
||||
geoip_update_ok, geosite_update_ok = true, true
|
||||
end
|
||||
if not rollback then
|
||||
chnroute_update, chnroute6_update, gfwlist_update, chnlist_update = "1", "1", "1", "1"
|
||||
end
|
||||
|
||||
if geoip_update_ok then
|
||||
if fs.access(asset_location .. "geoip.dat") then
|
||||
if chnroute_update == "1" then
|
||||
safe_call(fetch_chnroute, "生成chnroute发生错误...")
|
||||
end
|
||||
if chnroute6_update == "1" then
|
||||
safe_call(fetch_chnroute6, "生成chnroute6发生错误...")
|
||||
end
|
||||
else
|
||||
log("geoip.dat 文件不存在,跳过规则生成。")
|
||||
end
|
||||
end
|
||||
|
||||
if geosite_update_ok then
|
||||
if fs.access(asset_location .. "geosite.dat") then
|
||||
if gfwlist_update == "1" then
|
||||
safe_call(fetch_gfwlist, "生成gfwlist发生错误...")
|
||||
end
|
||||
if chnlist_update == "1" then
|
||||
safe_call(fetch_chnlist, "生成chnlist发生错误...")
|
||||
end
|
||||
else
|
||||
log("geosite.dat 文件不存在,跳过规则生成。")
|
||||
end
|
||||
end
|
||||
else
|
||||
if gfwlist_update == "1" then
|
||||
safe_call(fetch_gfwlist, "更新gfwlist发生错误...")
|
||||
end
|
||||
|
||||
if chnroute_update == "1" then
|
||||
safe_call(fetch_chnroute, "更新chnroute发生错误...")
|
||||
end
|
||||
|
||||
if chnroute6_update == "1" then
|
||||
safe_call(fetch_chnroute6, "更新chnroute6发生错误...")
|
||||
end
|
||||
|
||||
if chnlist_update == "1" then
|
||||
safe_call(fetch_chnlist, "更新chnlist发生错误...")
|
||||
end
|
||||
|
||||
if geoip_update == "1" then
|
||||
log("geoip 开始更新...")
|
||||
safe_call(fetch_geoip, "更新geoip发生错误...")
|
||||
remove_tmp_geofile("geoip")
|
||||
end
|
||||
|
||||
if geosite_update == "1" then
|
||||
log("geosite 开始更新...")
|
||||
safe_call(fetch_geosite, "更新geosite发生错误...")
|
||||
remove_tmp_geofile("geosite")
|
||||
end
|
||||
end
|
||||
|
||||
if not rollback then
|
||||
uci_set("@global_rules[0]", "gfwlist_update", gfwlist_update)
|
||||
uci_set("@global_rules[0]", "chnroute_update", chnroute_update)
|
||||
uci_set("@global_rules[0]", "chnroute6_update", chnroute6_update)
|
||||
uci_set("@global_rules[0]", "chnlist_update", chnlist_update)
|
||||
uci_set("@global_rules[0]", "geoip_update", geoip_update)
|
||||
uci_set("@global_rules[0]", "geosite_update", geosite_update)
|
||||
uci_save(true)
|
||||
end
|
||||
|
||||
if reboot == 1 then
|
||||
log("重启服务,应用新的规则。")
|
||||
uci_set("@global[0]", "flush_set", "1")
|
||||
uci_save(true)
|
||||
end
|
||||
log("规则更新完毕...\n")
|
||||
|
||||
update_lock:close()
|
||||
if reboot == 1 then
|
||||
local action = arg3 == "cron" and " cron" or ""
|
||||
luci.sys.call("/etc/init.d/passwall restart" .. action .. " > /dev/null 2>&1 &")
|
||||
end
|
||||
Whitespace-only changes.
Whitespace-only changes.
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,33 @@
|
||||
apple.com
|
||||
microsoft.com
|
||||
dyndns.com
|
||||
douyucdn.cn
|
||||
douyucdn2.cn
|
||||
location.services.mozilla.com
|
||||
|
||||
#steam
|
||||
steamcontent.com
|
||||
dl.steam.clngaa.com
|
||||
dl.steam.ksyna.com
|
||||
st.dl.bscstorage.net
|
||||
st.dl.eccdnx.com
|
||||
st.dl.pinyuncloud.com
|
||||
cdn.mileweb.cs.steampowered.com.8686c.com
|
||||
cdn-ws.content.steamchina.com
|
||||
cdn-qc.content.steamchina.com
|
||||
cdn-ali.content.steamchina.com
|
||||
epicgames-download1-1251447533.file.myqcloud.com
|
||||
|
||||
#DoT/DoH
|
||||
dns.alidns.com
|
||||
doh.pub
|
||||
dot.pub
|
||||
doh.360.cn
|
||||
dot.360.cn
|
||||
|
||||
#GitHub Proxy
|
||||
gh-proxy.com
|
||||
gh-proxy.org
|
||||
ghproxy.cc
|
||||
ghfast.top
|
||||
ghproxy.com
|
||||
@@ -0,0 +1,10 @@
|
||||
114.114.114.114
|
||||
114.114.115.115
|
||||
223.5.5.5
|
||||
223.6.6.6
|
||||
119.29.29.29
|
||||
180.76.76.76
|
||||
1.12.12.12
|
||||
120.53.53.53
|
||||
180.184.1.1
|
||||
180.184.2.2
|
||||
@@ -0,0 +1,26 @@
|
||||
courier.push.apple.com
|
||||
rbsxbxp-mim.vivox.com
|
||||
rbsxbxp.www.vivox.com
|
||||
rbsxbxp-ws.vivox.com
|
||||
rbspsxp.www.vivox.com
|
||||
rbspsxp-mim.vivox.com
|
||||
rbspsxp-ws.vivox.com
|
||||
rbswxp.www.vivox.com
|
||||
rbswxp-mim.vivox.com
|
||||
disp-rbspsp-5-1.vivox.com
|
||||
disp-rbsxbp-5-1.vivox.com
|
||||
proxy.rbsxbp.vivox.com
|
||||
proxy.rbspsp.vivox.com
|
||||
proxy.rbswp.vivox.com
|
||||
rbswp.vivox.com
|
||||
rbsxbp.vivox.com
|
||||
rbspsp.vivox.com
|
||||
rbspsp.www.vivox.com
|
||||
rbswp.www.vivox.com
|
||||
rbsxbp.www.vivox.com
|
||||
rbsxbxp.vivox.com
|
||||
rbspsxp.vivox.com
|
||||
rbswxp.vivox.com
|
||||
Mijia Cloud
|
||||
dlg.io.mi.com
|
||||
regexp:(^|.*\.)c2c\.wechat\.com$
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,16 @@
|
||||
0.0.0.0/8
|
||||
10.0.0.0/8
|
||||
100.64.0.0/10
|
||||
127.0.0.0/8
|
||||
169.254.0.0/16
|
||||
172.16.0.0/12
|
||||
192.0.0.0/24
|
||||
192.0.2.0/24
|
||||
192.88.99.0/24
|
||||
192.168.0.0/16
|
||||
198.51.100.0/24
|
||||
203.0.113.0/24
|
||||
224.0.0.0/4
|
||||
233.252.0.0/24
|
||||
240.0.0.0/4
|
||||
255.255.255.255/32
|
||||
@@ -0,0 +1,16 @@
|
||||
::/128
|
||||
::1/128
|
||||
::ffff:0:0/96
|
||||
::ffff:0:0:0/96
|
||||
64:ff9b::/96
|
||||
64:ff9b:1::/48
|
||||
100::/64
|
||||
2001::/32
|
||||
2001:20::/28
|
||||
2001:db8::/32
|
||||
2002::/16
|
||||
3fff::/20
|
||||
5f00::/16
|
||||
fc00::/7
|
||||
fe80::/64
|
||||
ff00::/8
|
||||
@@ -0,0 +1,27 @@
|
||||
engage.cloudflareclient.com
|
||||
github.com
|
||||
bing.com
|
||||
c.mi.com
|
||||
apple-relay.apple.com
|
||||
|
||||
#google
|
||||
googleapis.cn
|
||||
googleapis.com
|
||||
google.com.tw
|
||||
google.com.hk
|
||||
gstatic.com
|
||||
xn--ngstr-lra8j.com
|
||||
|
||||
#DNS-Leak
|
||||
dnsleaktest.com
|
||||
dnsleak.com
|
||||
expressvpn.com
|
||||
nordvpn.com
|
||||
surfshark.com
|
||||
ipleak.net
|
||||
perfect-privacy.com
|
||||
browserleaks.com
|
||||
browserleaks.org
|
||||
vpnunlimited.com
|
||||
whoer.net
|
||||
whrq.net
|
||||
@@ -0,0 +1,21 @@
|
||||
149.154.160.0/20
|
||||
91.108.4.0/22
|
||||
91.108.56.0/24
|
||||
109.239.140.0/24
|
||||
67.198.55.0/24
|
||||
8.8.4.4
|
||||
8.8.8.8
|
||||
208.67.222.222
|
||||
208.67.220.220
|
||||
104.16.249.249
|
||||
104.16.248.249
|
||||
1.1.1.1
|
||||
1.1.1.2
|
||||
1.0.0.1
|
||||
9.9.9.9
|
||||
149.112.112.112
|
||||
2001:67c:4e8::/48
|
||||
2001:b28:f23c::/48
|
||||
2001:b28:f23d::/48
|
||||
2001:b28:f23f::/48
|
||||
2001:b28:f242::/48
|
||||
@@ -0,0 +1,228 @@
|
||||
#!/bin/sh
|
||||
|
||||
. /usr/share/passwall/utils.sh
|
||||
APP_FILE=${APP_PATH}/app.sh
|
||||
|
||||
check_process() {
|
||||
while busybox pgrep -af "${CONFIG}/" | grep -E 'app\.sh.*(start|stop)|nftables\.sh|iptables\.sh|subscribe\.lua' >/dev/null; do
|
||||
sleep 6s 8>&- 9>&-
|
||||
done
|
||||
}
|
||||
|
||||
test_url() {
|
||||
local url="$1"
|
||||
local try="${2:-1}"
|
||||
local timeout="${3:-2}"
|
||||
local extra_params="$4"
|
||||
local repeat="$5"
|
||||
|
||||
if [ -z "$curl_retry_all_errors" ]; then
|
||||
if /usr/bin/curl --help all | grep -q "\-\-retry-all-errors"; then
|
||||
curl_retry_all_errors=1
|
||||
fi
|
||||
fi
|
||||
[ "$curl_retry_all_errors" = "1" ] && extra_params="--retry-all-errors ${extra_params}"
|
||||
|
||||
local max_time=$((timeout + 3))
|
||||
local retry_max_time=$((max_time * (try + 1) + try))
|
||||
curl_test() {
|
||||
/usr/bin/curl -skIL -o /dev/null ${extra_params} --max-time ${max_time} --connect-timeout ${timeout} --retry ${try} --retry-delay 1 --retry-max-time ${retry_max_time} -w "%{http_code}" "$url" 8>&- 9>&-
|
||||
}
|
||||
|
||||
local status
|
||||
status=$(curl_test) || status=000
|
||||
[ "$status" = "204" ] && status=200
|
||||
if [ "$status" = "200" ] && [ "$repeat" = "1" ]; then
|
||||
sleep 3s 8>&- 9>&-
|
||||
status=$(curl_test) || status=000
|
||||
[ "$status" = "204" ] && status=200
|
||||
fi
|
||||
printf '%s' "$status"
|
||||
}
|
||||
|
||||
test_proxy() {
|
||||
local result=0
|
||||
local status=$(test_url "${probe_url}" ${retry_num} ${connect_timeout} "-x socks5h://127.0.0.1:${socks_port}")
|
||||
if [ "$status" = "200" ]; then
|
||||
result=0
|
||||
else
|
||||
local status2=$(test_url "https://www.baidu.com" ${retry_num} ${connect_timeout})
|
||||
if [ "$status2" = "200" ]; then
|
||||
result=1
|
||||
else
|
||||
result=2
|
||||
ping -c 3 -W 1 223.5.5.5 > /dev/null 2>&1 8>&- 9>&-
|
||||
[ $? -eq 0 ] && {
|
||||
result=1
|
||||
}
|
||||
fi
|
||||
fi
|
||||
echo $result
|
||||
}
|
||||
|
||||
test_node() {
|
||||
local node_id=$1
|
||||
local test_flag="test_node_${id}_${node_id}"
|
||||
local _type _tmp_port curlx _proxy_status pid_file
|
||||
(
|
||||
exec 9>&-
|
||||
exec 8>"${LOCK_PATH}/${CONFIG}_socks_auto_switch_test.lock"
|
||||
flock -x 8 || exit 1
|
||||
_type=$(echo $(config_n_get ${node_id} type) | tr 'A-Z' 'a-z')
|
||||
[ -n "${_type}" ] && {
|
||||
check_process
|
||||
_tmp_port=$(get_new_port 48800 tcp,udp) || exit 1
|
||||
NO_REC_PROCESS=1 "$APP_FILE" run_socks flag="$test_flag" node="$node_id" bind=127.0.0.1 socks_port="$_tmp_port" config_file="${test_flag}.json" 8>&- 9>&-
|
||||
sleep 2s 8>&- 9>&-
|
||||
curlx="socks5h://127.0.0.1:${_tmp_port}"
|
||||
_proxy_status=$(test_url "${probe_url}" ${retry_num} ${connect_timeout} "-x $curlx" 1)
|
||||
# 结束 SS 插件进程
|
||||
pid_file="${TMP_PATH}/${test_flag}_plugin.pid"
|
||||
[ -s "$pid_file" ] && kill -9 "$(head -n 1 "$pid_file")" >/dev/null 2>&1
|
||||
busybox pgrep -af "${TMP_PATH}/${test_flag}[._]" | awk '! /socks_auto_switch\.sh/{print $1}' | xargs -r kill -9 >/dev/null 2>&1
|
||||
rm -f "$TMP_PATH/${test_flag}"*.*
|
||||
if [ "${_proxy_status}" -eq 200 ]; then
|
||||
exit 0
|
||||
fi
|
||||
}
|
||||
exit 1
|
||||
)
|
||||
}
|
||||
|
||||
try_switch_backup() {
|
||||
local b_nodes="$1"
|
||||
local now_node="$2"
|
||||
local total tried
|
||||
local new_node msg
|
||||
local first_node found node
|
||||
|
||||
# 只有一个后备节点时,与主节点轮询
|
||||
if [ "$backup_node_num" -eq 1 ]; then
|
||||
b_nodes="$b_nodes $main_node"
|
||||
fi
|
||||
|
||||
total=$(printf "%s\n" "$b_nodes" | wc -w)
|
||||
tried=0
|
||||
|
||||
while [ "$tried" -lt "$total" ]; do
|
||||
new_node=""
|
||||
first_node=""
|
||||
found=""
|
||||
for node in $b_nodes; do
|
||||
[ -z "$first_node" ] && first_node="$node" # 记录第一个节点
|
||||
[ "$found" = "1" ] && { new_node="$node"; break; } # 找到当前节点后取下一个
|
||||
[ "$node" = "$now_node" ] && found=1 # 标记找到当前节点
|
||||
done
|
||||
# 如果没找到当前节点,或者当前节点是最后一个,就取第一个节点
|
||||
[ -z "$new_node" ] && new_node="$first_node"
|
||||
|
||||
local node_role node_type node_remarks
|
||||
if [ "$new_node" = "$main_node" ]; then
|
||||
node_role="主节点"
|
||||
else
|
||||
node_role="后备节点"
|
||||
fi
|
||||
node_type=$(config_n_get $new_node type)
|
||||
node_remarks=$(config_n_get $new_node remarks)
|
||||
echolog "Socks切换检测:端口[${socks_port}] 尝试${node_role}【${node_type}:[$node_remarks]】"
|
||||
|
||||
if test_node ${new_node}; then
|
||||
check_process
|
||||
echolog "Socks切换检测:端口[${socks_port}]【${node_type}:[$node_remarks]】正常,切换到此节点!"
|
||||
if NO_REC_PROCESS=1 "$APP_FILE" socks_node_switch flag="$id" new_node="$new_node" 9>&-; then
|
||||
echolog "Socks切换检测:端口[${socks_port}] 节点切换完毕!"
|
||||
return 0
|
||||
fi
|
||||
echolog "Socks切换检测:端口[${socks_port}] 节点切换失败,继续尝试其他节点!"
|
||||
else
|
||||
echolog "Socks切换检测:端口[${socks_port}]【${node_type}:[$node_remarks]】异常。"
|
||||
fi
|
||||
now_node="$new_node"
|
||||
tried=$((tried + 1))
|
||||
done
|
||||
|
||||
echolog "Socks切换检测:端口[${socks_port}] 所有节点均不可用或切换失败!"
|
||||
return 1
|
||||
}
|
||||
|
||||
test_auto_switch() {
|
||||
local b_nodes=$1
|
||||
local now_node=$2
|
||||
[ -z "$now_node" ] && {
|
||||
if [ -n "$(get_cache_var "${id}")" ]; then
|
||||
now_node=$(get_cache_var "${id}")
|
||||
else
|
||||
#echolog "Socks切换检测:未知错误"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
local status=$(test_proxy 9>&-)
|
||||
if [ "$status" = "2" ]; then
|
||||
echolog "Socks切换检测:无法连接到网络,请检查网络是否正常!"
|
||||
return 2
|
||||
fi
|
||||
|
||||
#检测主节点是否能使用
|
||||
if [ "$restore_switch" = "1" ] && [ -n "$main_node" ] && [ "$now_node" != "$main_node" ]; then
|
||||
test_node ${main_node}
|
||||
[ $? -eq 0 ] && {
|
||||
check_process
|
||||
echolog "Socks切换检测:端口[${socks_port}] 主节点【$(config_n_get $main_node type):[$(config_n_get $main_node remarks)]】正常,切换到主节点!"
|
||||
if NO_REC_PROCESS=1 "$APP_FILE" socks_node_switch flag="$id" new_node="$main_node" 9>&-; then
|
||||
echolog "Socks切换检测:端口[${socks_port}] 节点切换完毕!"
|
||||
return 0
|
||||
fi
|
||||
echolog "Socks切换检测:端口[${socks_port}] 恢复主节点失败!"
|
||||
status=1
|
||||
}
|
||||
fi
|
||||
|
||||
[ "$status" = "0" ] && {
|
||||
#echolog "Socks切换检测:${id}【$(config_n_get $now_node type):[$(config_n_get $now_node remarks)]】正常。"
|
||||
return 0
|
||||
}
|
||||
|
||||
echolog "Socks切换检测:端口[${socks_port}]【$(config_n_get $now_node type):[$(config_n_get $now_node remarks)]】异常。"
|
||||
try_switch_backup "$b_nodes" "$now_node"
|
||||
return $?
|
||||
}
|
||||
|
||||
start() {
|
||||
id=$1
|
||||
LOCK_FILE=${LOCK_PATH}/${CONFIG}_socks_auto_switch_${id}.lock
|
||||
exec 9>"$LOCK_FILE"
|
||||
flock -n 9 || return 0
|
||||
main_node=$(config_n_get $id node)
|
||||
socks_port=$(config_n_get $id port 0)
|
||||
delay=$(config_n_get $id autoswitch_testing_time 30)
|
||||
connect_timeout=$(config_n_get $id autoswitch_connect_timeout 3)
|
||||
retry_num=$(config_n_get $id autoswitch_retry_num 1)
|
||||
restore_switch=$(config_n_get $id autoswitch_restore_switch 0)
|
||||
probe_url=$(config_n_get $id autoswitch_probe_url "https://www.google.com/generate_204")
|
||||
backup_node=$(lua_api "get_socks_backup_nodes(\"${id}\")")
|
||||
if [ -n "$backup_node" ]; then
|
||||
backup_node_num=$(printf "%s\n" "$backup_node" | wc -w)
|
||||
echolog " - Socks切换检测:端口[${socks_port}] 后备节点数量:${backup_node_num}"
|
||||
if [ "$backup_node_num" -eq 1 ]; then
|
||||
[ "$main_node" = "$backup_node" ] && return
|
||||
elif [ "$backup_node_num" -gt 1 ]; then
|
||||
[ "$restore_switch" != "1" ] && {
|
||||
case " $backup_node " in
|
||||
*" $main_node "*) ;;
|
||||
*) backup_node="${backup_node} ${main_node}" ;;
|
||||
esac
|
||||
}
|
||||
fi
|
||||
else
|
||||
echolog " - Socks切换检测:端口[${socks_port}] 后备节点数量:0"
|
||||
return
|
||||
fi
|
||||
while [ -n "$backup_node" ]; do
|
||||
check_process
|
||||
test_auto_switch "$backup_node"
|
||||
sleep ${delay} 9>&-
|
||||
done
|
||||
}
|
||||
|
||||
start $@
|
||||
+2379
File diff suppressed because it is too large.
Load diff
+67
@@ -0,0 +1,67 @@
|
||||
#!/bin/sh
|
||||
|
||||
## 循环更新脚本
|
||||
|
||||
. /usr/share/passwall/utils.sh
|
||||
LOCK_FILE=${LOCK_PATH}/${CONFIG}_tasks.lock
|
||||
|
||||
CFG_UPDATE_INT=0
|
||||
|
||||
exec 99>"$LOCK_FILE"
|
||||
flock -n 99 || exit 0
|
||||
|
||||
while true
|
||||
do
|
||||
|
||||
if [ "$CFG_UPDATE_INT" -ne 0 ]; then
|
||||
|
||||
restart_week_mode=$(config_n_get @global_delay[0] restart_week_mode)
|
||||
restart_interval_mode=$(config_n_get @global_delay[0] restart_interval_mode)
|
||||
restart_interval_mode=$(expr "$restart_interval_mode" \* 60)
|
||||
if [ -n "$restart_week_mode" ]; then
|
||||
[ "$restart_week_mode" = "8" ] && {
|
||||
[ "$(expr "$CFG_UPDATE_INT" % "$restart_interval_mode")" -eq 0 ] && { /etc/init.d/$CONFIG restart cron > /dev/null 2>&1 99>&- & }
|
||||
}
|
||||
fi
|
||||
|
||||
rules_update_week_mode=$(config_n_get @global_rules[0] update_week_mode)
|
||||
rules_update_interval_mode=$(config_n_get @global_rules[0] update_interval_mode)
|
||||
rules_update_interval_mode=$(expr "$rules_update_interval_mode" \* 60)
|
||||
if [ -n "$rules_update_week_mode" ]; then
|
||||
[ "$rules_update_week_mode" = "8" ] && {
|
||||
[ "$(expr "$CFG_UPDATE_INT" % "$rules_update_interval_mode")" -eq 0 ] && { lua "$APP_PATH/rule_update.lua" log all cron > /dev/null 2>&1 99>&- & }
|
||||
}
|
||||
fi
|
||||
|
||||
TMP_SUB_PATH=$TMP_PATH/sub_tasks
|
||||
mkdir -p $TMP_SUB_PATH
|
||||
for item in $(uci show ${CONFIG} | grep "=subscribe_list" | cut -d '.' -sf 2 | cut -d '=' -sf 1); do
|
||||
sub_update_week_mode=$(config_n_get $item update_week_mode)
|
||||
if [ -n "$sub_update_week_mode" ] && [ "$sub_update_week_mode" = "8" ]; then
|
||||
sub_update_interval_mode=$(config_n_get $item update_interval_mode)
|
||||
[ -n "$sub_update_interval_mode" ] && {
|
||||
echo "$item" >> $TMP_SUB_PATH/${sub_update_week_mode}_${sub_update_interval_mode}
|
||||
}
|
||||
fi
|
||||
done
|
||||
|
||||
[ -d "${TMP_SUB_PATH}" ] && {
|
||||
for name in $(ls ${TMP_SUB_PATH}); do
|
||||
sub_update_week_mode=${name%_*}
|
||||
sub_update_interval_mode=${name#*_}
|
||||
sub_update_interval_mode=$(expr "$sub_update_interval_mode" \* 60)
|
||||
cfgids=$(tr '\n' ',' < "${TMP_SUB_PATH}/${name}" | sed 's/,$//')
|
||||
[ "$sub_update_week_mode" = "8" ] && {
|
||||
[ "$(expr "$CFG_UPDATE_INT" % "$sub_update_interval_mode")" -eq 0 ] && { lua "$APP_PATH/subscribe.lua" start "$cfgids" cron > /dev/null 2>&1 99>&- & }
|
||||
}
|
||||
done
|
||||
rm -rf $TMP_SUB_PATH
|
||||
}
|
||||
|
||||
fi
|
||||
|
||||
CFG_UPDATE_INT=$(expr "$CFG_UPDATE_INT" + 10)
|
||||
|
||||
sleep 600 99>&-
|
||||
|
||||
done 2>/dev/null
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
#!/bin/sh
|
||||
|
||||
. /usr/share/passwall/utils.sh
|
||||
|
||||
test_url() {
|
||||
local url=$1
|
||||
local try=1
|
||||
[ -n "$2" ] && try=$2
|
||||
local timeout=2
|
||||
[ -n "$3" ] && timeout=$3
|
||||
local extra_params=$4
|
||||
local status
|
||||
local max_time=$((timeout + 3))
|
||||
local retry_max_time=$((max_time * (try + 1) + try))
|
||||
curl --help all | grep "\-\-retry-all-errors" > /dev/null
|
||||
[ $? == 0 ] && extra_params="--retry-all-errors ${extra_params}"
|
||||
status=$(/usr/bin/curl -I -o /dev/null -skL $extra_params --connect-timeout ${timeout} --max-time ${max_time} --retry ${try} --retry-delay 1 --retry-max-time ${retry_max_time} -w %{http_code} "$url") || status=000
|
||||
case "$status" in
|
||||
204|\
|
||||
200)
|
||||
status=200
|
||||
;;
|
||||
esac
|
||||
printf '%s' "$status"
|
||||
}
|
||||
|
||||
test_proxy() {
|
||||
result=0
|
||||
status=$(test_url "https://www.google.com/generate_204" ${retry_num} ${connect_timeout})
|
||||
if [ "$status" = "200" ]; then
|
||||
result=0
|
||||
else
|
||||
status2=$(test_url "https://www.baidu.com" ${retry_num} ${connect_timeout})
|
||||
if [ "$status2" = "200" ]; then
|
||||
result=1
|
||||
else
|
||||
result=2
|
||||
ping -c 3 -W 1 223.5.5.5 > /dev/null 2>&1
|
||||
[ $? -eq 0 ] && {
|
||||
result=1
|
||||
}
|
||||
fi
|
||||
fi
|
||||
echo $result
|
||||
}
|
||||
|
||||
url_test_node() {
|
||||
local result=0
|
||||
local node_id=$1
|
||||
local test_flag="url_test_${node_id}_$$"
|
||||
local _address _port curlx _username _password _tmp_port probeUrl pid_file
|
||||
local _type=$(config_n_get "$node_id" type | tr 'A-Z' 'a-z')
|
||||
|
||||
set --
|
||||
[ -n "${_type}" ] && {
|
||||
if [ "${_type}" == "socks" ]; then
|
||||
# 防止socks节点密码包含 #、% 等字符,将地址和认证信息分开传递
|
||||
_address=$(config_n_get "$node_id" address)
|
||||
_port=$(config_n_get "$node_id" port)
|
||||
[ -n "$_address" ] && [ -n "$_port" ] || { echo 0; return 1; }
|
||||
case "$_address" in
|
||||
\[*\]) ;;
|
||||
*:*) _address="[$_address]" ;;
|
||||
esac
|
||||
curlx="socks5h://${_address}:${_port}"
|
||||
_username=$(config_n_get "$node_id" username)
|
||||
_password=$(config_n_get "$node_id" password)
|
||||
[ -n "${_username}" ] && [ -n "${_password}" ] && set -- --proxy-user "${_username}:${_password}"
|
||||
else
|
||||
_tmp_port=$(get_new_port $((48900 + $$ % 201)) tcp,udp) || { echo 0; return 1; }
|
||||
NO_REC_PROCESS=1 /usr/share/${CONFIG}/app.sh run_socks flag="$test_flag" node="$node_id" bind=127.0.0.1 socks_port="$_tmp_port" config_file="${test_flag}.json"
|
||||
curlx="socks5h://127.0.0.1:${_tmp_port}"
|
||||
fi
|
||||
sleep 2s
|
||||
probeUrl=$(config_n_get @global_other[0] url_test_url https://www.google.com/generate_204)
|
||||
result=$(curl --connect-timeout 3 --max-time 5 -o /dev/null -I -sk -w "%{http_code}:%{time_pretransfer}:%{time_starttransfer}" "$@" -x "$curlx" "${probeUrl}") || result=0
|
||||
# 结束 SS 插件进程
|
||||
if [ "$_type" != "socks" ]; then
|
||||
pid_file="${TMP_PATH}/${test_flag}_plugin.pid"
|
||||
[ -s "$pid_file" ] && kill -9 "$(head -n 1 "$pid_file")" >/dev/null 2>&1
|
||||
busybox pgrep -af "${TMP_PATH}/${test_flag}[._]" | awk '! /test\.sh/{print $1}' | xargs -r kill -9 >/dev/null 2>&1
|
||||
rm -f "$TMP_PATH/${test_flag}".* "$TMP_PATH/${test_flag}"_*.*
|
||||
fi
|
||||
}
|
||||
printf '%s' "$result"
|
||||
}
|
||||
|
||||
arg1=$1
|
||||
shift
|
||||
case $arg1 in
|
||||
test_url)
|
||||
test_url "$@"
|
||||
;;
|
||||
url_test_node)
|
||||
url_test_node "$@"
|
||||
;;
|
||||
esac
|
||||
+568
@@ -0,0 +1,568 @@
|
||||
#!/bin/sh
|
||||
# Copyright (C) 2022-2025 xiaorouji
|
||||
# Copyright (C) 2026 Openwrt-Passwall Organization
|
||||
|
||||
CONFIG=passwall
|
||||
DIRECT_GID=9911
|
||||
APP_PATH=/usr/share/${CONFIG}
|
||||
TMP_PATH=/tmp/etc/${CONFIG}
|
||||
TMP_PATH2=${TMP_PATH}_tmp
|
||||
LOCK_PATH=/var/lock
|
||||
LOG_FILE=/tmp/log/${CONFIG}.log
|
||||
TMP_ACL_PATH=${TMP_PATH}/acl
|
||||
TMP_BIN_PATH=${TMP_PATH}/bin
|
||||
TMP_IFACE_PATH=${TMP_PATH}/iface
|
||||
TMP_ROUTE_PATH=${TMP_PATH}/route
|
||||
TMP_SCRIPT_FUNC_PATH=${TMP_PATH}/script_func
|
||||
RULES_PATH=/usr/share/${CONFIG}/rules
|
||||
USER_RULES_PATH=/etc/${CONFIG}/rules
|
||||
|
||||
IPv6_REGEX="([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|"
|
||||
IPv6_REGEX="${IPv6_REGEX}([0-9a-fA-F]{1,4}:){1,7}:|"
|
||||
IPv6_REGEX="${IPv6_REGEX}([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|"
|
||||
IPv6_REGEX="${IPv6_REGEX}([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|"
|
||||
IPv6_REGEX="${IPv6_REGEX}([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|"
|
||||
IPv6_REGEX="${IPv6_REGEX}([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|"
|
||||
IPv6_REGEX="${IPv6_REGEX}([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|"
|
||||
IPv6_REGEX="${IPv6_REGEX}[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|"
|
||||
IPv6_REGEX="${IPv6_REGEX}:((:[0-9a-fA-F]{1,4}){1,7}|:)|"
|
||||
IPv6_REGEX="${IPv6_REGEX}fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|"
|
||||
IPv6_REGEX="${IPv6_REGEX}::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|"
|
||||
IPv6_REGEX="${IPv6_REGEX}([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])"
|
||||
IPv4_REGEX="((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)"
|
||||
|
||||
. /lib/functions/network.sh
|
||||
|
||||
echolog() {
|
||||
local d="$(date "+%Y-%m-%d %H:%M:%S")"
|
||||
echo -e "$d: $*" >>$LOG_FILE
|
||||
}
|
||||
|
||||
clean_log() {
|
||||
logsnum=$(cat $LOG_FILE 2>/dev/null | wc -l)
|
||||
[ "$logsnum" -gt 1000 ] && {
|
||||
echo "" > $LOG_FILE
|
||||
echolog "日志文件过长,清空处理!"
|
||||
}
|
||||
}
|
||||
|
||||
config_get_type() {
|
||||
local ret=$(uci -q get "${CONFIG}.${1}" 2>/dev/null)
|
||||
printf '%s' "${ret:=$2}"
|
||||
}
|
||||
|
||||
config_n_get() {
|
||||
local ret=$(uci -q get "${CONFIG}.${1}.${2}" 2>/dev/null)
|
||||
printf '%s' "${ret:=$3}"
|
||||
}
|
||||
|
||||
config_t_get() {
|
||||
local index=${4:-0}
|
||||
local ret=$(uci -q get "${CONFIG}.@${1}[${index}].${2}" 2>/dev/null)
|
||||
printf '%s' "${ret:=${3}}"
|
||||
}
|
||||
|
||||
eval_set_val() {
|
||||
for i in $@; do
|
||||
for j in $i; do
|
||||
eval $j
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
eval_unset_val() {
|
||||
for i in $@; do
|
||||
for j in $i; do
|
||||
eval unset $j
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
lua_api() {
|
||||
local func=${1}
|
||||
[ -z "${func}" ] && {
|
||||
echo "nil"
|
||||
return
|
||||
}
|
||||
echo $(lua -e "local api = require 'luci.passwall.api' print(api.${func})")
|
||||
}
|
||||
|
||||
eval_cache_var() {
|
||||
[ -s "$TMP_PATH/var" ] && . "$TMP_PATH/var"
|
||||
}
|
||||
|
||||
del_cache_var() {
|
||||
local key="${1}"
|
||||
[ -n "${key}" ] && [ -f "${TMP_PATH}/var" ] && {
|
||||
sed -i "/^${key}=/d" "$TMP_PATH/var" >/dev/null 2>&1
|
||||
}
|
||||
}
|
||||
|
||||
set_cache_var() {
|
||||
local key="${1}"
|
||||
shift 1
|
||||
[ -n "${key}" ] && {
|
||||
del_cache_var ${key}
|
||||
local val="$@"
|
||||
[ -n "${val}" ] && {
|
||||
[ ! -d $TMP_PATH ] && mkdir -p $TMP_PATH
|
||||
printf '%s=%s\n' "$key" "$(shell_quote "$val")" >> "$TMP_PATH/var"
|
||||
eval "$key=\$val"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get_cache_var() {
|
||||
[ -n "$1" ] && [ -s "$TMP_PATH/var" ] && (
|
||||
. "$TMP_PATH/var"
|
||||
eval "printf '%s' \"\${$1-}\""
|
||||
)
|
||||
}
|
||||
|
||||
first_type() {
|
||||
[ "${1#/}" != "$1" ] && [ -x "$1" ] && echo "$1" && return
|
||||
for p in "/bin/$1" "/usr/bin/$1" "${TMP_BIN_PATH:-/tmp}/$1"; do
|
||||
[ -x "$p" ] && echo "$p" && return
|
||||
done
|
||||
command -v "$1" 2>/dev/null || command -v "$2" 2>/dev/null
|
||||
}
|
||||
|
||||
get_enabled_anonymous_secs() {
|
||||
uci -q show "${CONFIG}" | grep "${1}\[.*\.enabled='1'" | cut -d '.' -sf2
|
||||
}
|
||||
|
||||
get_geoip() {
|
||||
local geo_output_path="$TMP_PATH2/geo_output"
|
||||
mkdir -p ${geo_output_path}
|
||||
local geoip_code="$1"
|
||||
local geoip_type_flag=""
|
||||
local output_path="${geo_output_path}/geoip-${geoip_code}-$2"
|
||||
[ ! -s "${output_path}" ] && {
|
||||
local geoip_path="$(config_n_get @global_rules[0] v2ray_location_asset)"
|
||||
geoip_path="${geoip_path%*/}/geoip.dat"
|
||||
local bin="$(first_type $(config_n_get @global_app[0] geoview_file) geoview)"
|
||||
[ -n "$bin" ] && [ -s "$geoip_path" ] || { echo ""; return; }
|
||||
case "$2" in
|
||||
"ipv4") geoip_type_flag="-ipv6=false" ;;
|
||||
"ipv6") geoip_type_flag="-ipv4=false" ;;
|
||||
esac
|
||||
"$bin" -input "$geoip_path" -list "$geoip_code" $geoip_type_flag -lowmem=true -output ${output_path}
|
||||
}
|
||||
[ -s "${output_path}" ] && cat "${output_path}"
|
||||
}
|
||||
|
||||
get_host_ip() {
|
||||
local host=$2
|
||||
local count=$3
|
||||
[ -z "$count" ] && count=3
|
||||
local isip=""
|
||||
local ip=""
|
||||
if [ "$1" = "ipv6" ]; then
|
||||
host=${host#[}
|
||||
host=${host%]}
|
||||
isip=$(printf '%s' "$host" | grep -E "^($IPv6_REGEX)$")
|
||||
[ -n "$isip" ] && ip=$isip
|
||||
else
|
||||
isip=$(printf '%s' "$host" | grep -E "^($IPv4_REGEX)$")
|
||||
[ -n "$isip" ] && ip=$isip
|
||||
fi
|
||||
[ -z "$isip" ] && {
|
||||
local t=4
|
||||
[ "$1" = "ipv6" ] && t=6
|
||||
local vpsrip=$(resolveip -$t -t "$count" "$host" | awk 'NR==1{print}')
|
||||
ip=$vpsrip
|
||||
}
|
||||
[ -n "$ip" ] && echo "$ip"
|
||||
}
|
||||
|
||||
get_node_host_ip() {
|
||||
local ip
|
||||
local address=$(config_n_get $1 address)
|
||||
[ -n "$address" ] && {
|
||||
local use_ipv6=$(config_n_get $1 use_ipv6)
|
||||
local network_type="ipv4"
|
||||
[ "$use_ipv6" = "1" ] && network_type="ipv6"
|
||||
ip=$(get_host_ip $network_type $address)
|
||||
}
|
||||
echo $ip
|
||||
}
|
||||
|
||||
get_ip_port_from() {
|
||||
local __host=${1}; shift 1
|
||||
local __ipv=${1}; shift 1
|
||||
local __portv=${1}; shift 1
|
||||
local __ucipriority=${1}; shift 1
|
||||
|
||||
local val1="$__host" val2
|
||||
case "$__host" in
|
||||
*\#*) val1=${__host%#*}; val2=${__host##*#} ;;
|
||||
\[*\]:*) val1=${__host%\]:*}; val2=${__host##*:} ;;
|
||||
*:*:*) ;;
|
||||
*:*) val1=${__host%:*}; val2=${__host##*:} ;;
|
||||
esac
|
||||
if [ -n "${__ucipriority}" ]; then
|
||||
val2=$(config_n_get "$__host" port "$val2")
|
||||
val1=$(config_n_get "$__host" address "$val1")
|
||||
fi
|
||||
val1=${val1#[}
|
||||
val1=${val1%]}
|
||||
eval "${__ipv}=\$val1; ${__portv}=\$val2"
|
||||
}
|
||||
|
||||
parse_doh() {
|
||||
local __doh=$1 __url_var=$2 __host_var=$3 __port_var=$4 __bootstrap_var=$5
|
||||
__doh=$(printf '%s' "$__doh" | tr -d ' \t\n')
|
||||
local __url=${__doh%%,*}
|
||||
local __bootstrap=${__doh#*,}
|
||||
[ "$__bootstrap" = "$__doh" ] && __bootstrap=""
|
||||
local __host_port=$(lua_api "get_domain_from_url(\"${__url}\")")
|
||||
local __host __port
|
||||
if echo "${__host_port}" | grep -q '^\[.*\]:[0-9]\+$'; then
|
||||
__host=${__host_port%%]:*}]
|
||||
__port=${__host_port##*:}
|
||||
elif echo "${__host_port}" | grep -q ':[0-9]\+$'; then
|
||||
__host=${__host_port%:*}
|
||||
__port=${__host_port##*:}
|
||||
else
|
||||
__host=${__host_port}
|
||||
__port=443
|
||||
fi
|
||||
__host=${__host#[}
|
||||
__host=${__host%]}
|
||||
if [ "$(lua_api "is_ip(\"${__host}\")")" = "true" ]; then
|
||||
__bootstrap=${__host}
|
||||
fi
|
||||
__bootstrap=${__bootstrap#[}
|
||||
__bootstrap=${__bootstrap%]}
|
||||
eval "${__url_var}='${__url}' ${__host_var}='${__host}' ${__port_var}='${__port}' ${__bootstrap_var}='${__bootstrap}'"
|
||||
}
|
||||
|
||||
host_from_url(){
|
||||
local f="${1}"
|
||||
f=${f#*://}
|
||||
f=${f%%[/?#]*}
|
||||
f=${f##*@}
|
||||
case "$f" in
|
||||
\[*\]*) f=${f#\[}; f=${f%%\]*} ;;
|
||||
*) f=${f%%:*} ;;
|
||||
esac
|
||||
printf '%s' "$f"
|
||||
}
|
||||
|
||||
hosts_foreach() {
|
||||
local __hosts
|
||||
eval "__hosts=\$${1}"; shift 1
|
||||
local __func=${1}; shift 1
|
||||
local __default_port=${1}; shift 1
|
||||
local __ret=1
|
||||
|
||||
[ -z "${__hosts}" ] && return 0
|
||||
local __ip __port
|
||||
for __host in $(echo $__hosts | sed 's/[ ,]/\n/g'); do
|
||||
get_ip_port_from "$__host" "__ip" "__port"
|
||||
eval "$__func \"${__host}\" \"\${__ip}\" \"\${__port:-${__default_port}}\" \"$@\""
|
||||
__ret=$?
|
||||
[ ${__ret} -ge ${ERROR_NO_CATCH:-1} ] && return ${__ret}
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
check_host() {
|
||||
local f=${1}
|
||||
a=$(echo $f | grep "\/")
|
||||
[ -n "$a" ] && return 1
|
||||
# 判断是否包含汉字~
|
||||
local tmp=$(echo -n $f | awk '{print gensub(/[!-~]/,"","g",$0)}')
|
||||
[ -n "$tmp" ] && return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
normalize_dns() {
|
||||
local s="$1"
|
||||
local addr port="${2-}"
|
||||
case "$s" in
|
||||
\[*\]:*)
|
||||
# [ip6]:port
|
||||
addr="${s%\]:*}"
|
||||
addr="${addr#\[}"
|
||||
port="${s##*:}"
|
||||
;;
|
||||
*\#*)
|
||||
# ip4#port or ip6#port
|
||||
addr="${s%\#*}"
|
||||
port="${s##*\#}"
|
||||
;;
|
||||
*.*:*)
|
||||
# ip4:port
|
||||
addr="${s%:*}"
|
||||
port="${s##*:}"
|
||||
;;
|
||||
\[*\])
|
||||
# [ip6]
|
||||
addr="${s#\[}"
|
||||
addr="${addr%\]}"
|
||||
;;
|
||||
*)
|
||||
addr="$s"
|
||||
;;
|
||||
esac
|
||||
[ -n "$port" ] && echo "${addr}#${port}" || echo "$addr"
|
||||
}
|
||||
|
||||
format_dns() {
|
||||
local dns="${1%%#*}"
|
||||
local port="${1#*#}"
|
||||
[ "$port" = "$1" ] && port="${2-53}"
|
||||
case "$dns" in
|
||||
*:*) echo "[$dns]:$port" ;;
|
||||
*) echo "$dns:$port" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
check_port_exists() {
|
||||
local port=$1
|
||||
local protocol=$2
|
||||
[ -n "$protocol" ] || protocol="tcp,udp"
|
||||
local result=
|
||||
if [ "$protocol" = "tcp" ]; then
|
||||
result=$(netstat -tln | grep -c ":$port ")
|
||||
elif [ "$protocol" = "udp" ]; then
|
||||
result=$(netstat -uln | grep -c ":$port ")
|
||||
elif [ "$protocol" = "tcp,udp" ]; then
|
||||
result=$(netstat -tuln | grep -c ":$port ")
|
||||
fi
|
||||
echo "${result}"
|
||||
}
|
||||
|
||||
get_new_port() {
|
||||
local default_start_port=2001
|
||||
local min_port=1025
|
||||
local max_port=49151
|
||||
local port="$1" # Required parameter; please pass "auto" if you want it to be automatic.
|
||||
local protocol=$(echo "$2" | tr 'A-Z' 'a-z')
|
||||
local is_auto
|
||||
if [ "$port" = "auto" ]; then
|
||||
is_auto=1
|
||||
local last_get_new_port_auto=$(get_cache_var "last_get_new_port_auto")
|
||||
if [ -n "$last_get_new_port_auto" ]; then
|
||||
port=$(expr "$last_get_new_port_auto" + 1)
|
||||
else
|
||||
port=$default_start_port
|
||||
fi
|
||||
fi
|
||||
([ "$port" -lt "$min_port" ] || [ "$port" -gt "$max_port" ]) && port=$default_start_port
|
||||
local start_port=$port
|
||||
while :; do
|
||||
local result=$(check_port_exists "$port" "$protocol")
|
||||
if [ "$is_auto" = "1" ] && [ -n "$(get_cache_var "get_port_${port}")" ]; then
|
||||
# The port has already been allocated, continue to the next port.
|
||||
result=1
|
||||
fi
|
||||
[ "$result" = "0" ] && break
|
||||
if [ "$port" -lt "$max_port" ]; then
|
||||
# If the port is smaller than the maximum port, increment by 1 and continue.
|
||||
port=$(expr "$port" + 1)
|
||||
else
|
||||
port=$min_port
|
||||
fi
|
||||
[ "$port" -eq "$start_port" ] && return 1
|
||||
done
|
||||
if [ "$is_auto" = "1" ]; then
|
||||
# Set cache to prevent the port from being allocated again.
|
||||
set_cache_var "get_port_${port}" "1"
|
||||
set_cache_var "last_get_new_port_auto" "$port"
|
||||
fi
|
||||
echo "$port"
|
||||
}
|
||||
|
||||
check_ver() {
|
||||
local version1="$1"
|
||||
local version2="$2"
|
||||
local i v1 v1_1 v1_2 v1_3 v2 v2_1 v2_2 v2_3
|
||||
local old_ifs="$IFS"
|
||||
IFS='.'
|
||||
set -- $version1; v1_1=${1:-0}; v1_2=${2:-0}; v1_3=${3:-0}
|
||||
set -- $version2; v2_1=${1:-0}; v2_2=${2:-0}; v2_3=${3:-0}
|
||||
IFS="$old_ifs"
|
||||
for i in 1 2 3; do
|
||||
eval v1=\$v1_$i
|
||||
eval v2=\$v2_$i
|
||||
if [ "$v1" -gt "$v2" ]; then
|
||||
# $1 大于 $2
|
||||
echo 0
|
||||
return
|
||||
elif [ "$v1" -lt "$v2" ]; then
|
||||
# $1 小于 $2
|
||||
echo 1
|
||||
return
|
||||
fi
|
||||
done
|
||||
# $1 等于 $2
|
||||
echo 255
|
||||
}
|
||||
|
||||
has_1_65535() {
|
||||
local val="$1"
|
||||
val=${val//:/-}
|
||||
case ",$val," in
|
||||
*,1-65535,*) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
add_ip2route() {
|
||||
local ip=$(get_host_ip "ipv4" $1)
|
||||
[ -z "$ip" ] && {
|
||||
echolog " - 无法解析[${1}],路由表添加失败!"
|
||||
return 1
|
||||
}
|
||||
local remarks="${1}"
|
||||
[ "$remarks" != "$ip" ] && remarks="${1}(${ip})"
|
||||
|
||||
local gateway device
|
||||
network_get_gateway gateway "$2"
|
||||
network_get_device device "$2"
|
||||
[ -z "${device}" ] && device=$(ubus call "network.interface.$2" status 2>/dev/null | jsonfilter -e '@.device' 2>/dev/null)
|
||||
[ -z "${device}" ] && [ -d "/sys/class/net/$2" ] && device="$2"
|
||||
[ -z "${device}" ] && device="$2"
|
||||
|
||||
if [ -n "${gateway}" ]; then
|
||||
if route add -host "$ip" gw "$gateway" dev "$device" >/dev/null 2>&1; then
|
||||
printf '%s\n' "$ip" >> "$TMP_ROUTE_PATH/$device"
|
||||
echolog " - [${remarks}]添加到接口[${device}]路由表成功!"
|
||||
else
|
||||
echolog " - [${remarks}]添加到接口[${device}]路由表失败!"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
echolog " - [${remarks}]添加到接口[${device}]路由表失败!原因是找不到[${device}]网关。"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
delete_ip2route() {
|
||||
[ -d "${TMP_ROUTE_PATH}" ] && {
|
||||
local interface
|
||||
for interface in $(ls ${TMP_ROUTE_PATH}); do
|
||||
for ip in $(cat ${TMP_ROUTE_PATH}/${interface}); do
|
||||
route del -host ${ip} dev ${interface} >/dev/null 2>&1
|
||||
done
|
||||
done
|
||||
}
|
||||
}
|
||||
|
||||
shell_quote() {
|
||||
printf "'"
|
||||
printf '%s' "$1" | sed "s/'/'\\\\''/g"
|
||||
printf "'"
|
||||
}
|
||||
|
||||
get_process_start_time() {
|
||||
sed 's/.*) //' "/proc/$1/stat" 2>/dev/null | awk '{print $20}'
|
||||
}
|
||||
|
||||
ln_run() {
|
||||
local file_func=${1}
|
||||
local ln_name=${2}
|
||||
local output=${3}
|
||||
|
||||
shift 3;
|
||||
case "$ln_name" in
|
||||
dnsmasq*) set -- -k "$@" ;;
|
||||
haproxy) set -- -db "$@" ;;
|
||||
esac
|
||||
if [ "${file_func%%/*}" != "${file_func}" ]; then
|
||||
[ ! -L "${file_func}" ] && {
|
||||
ln -s "${file_func}" "${TMP_BIN_PATH}/${ln_name}" >/dev/null 2>&1
|
||||
file_func="${TMP_BIN_PATH}/${ln_name}"
|
||||
}
|
||||
[ -x "${file_func}" ] || echolog " - $(readlink ${file_func}) 没有执行权限,无法启动:${file_func} $*"
|
||||
fi
|
||||
#echo "${file_func} $*" >&2
|
||||
[ -n "${file_func}" ] || {
|
||||
echolog " - 找不到 ${ln_name},无法启动..."
|
||||
return 1
|
||||
}
|
||||
|
||||
"$file_func" "$@" >"$output" 2>&1 &
|
||||
local pid=$!
|
||||
|
||||
[ "$NO_REC_PROCESS" = "1" ] && return
|
||||
local process_file=$(mktemp "$TMP_SCRIPT_FUNC_PATH/XXXXXX")
|
||||
local arg
|
||||
{
|
||||
printf '%s\n' "$pid" "$(get_process_start_time "$pid")"
|
||||
printf '%s' "$(shell_quote "$file_func")"
|
||||
for arg in "$@"; do
|
||||
printf ' %s' "$(shell_quote "$arg")"
|
||||
done
|
||||
printf ' >%s 2>&1\n' "$(shell_quote "$output")"
|
||||
} > "$process_file"
|
||||
}
|
||||
|
||||
kill_all() {
|
||||
kill -9 $(pidof "$@") >/dev/null 2>&1
|
||||
}
|
||||
|
||||
gen_lanlist() {
|
||||
cat $USER_RULES_PATH/lanlist_ipv4 | tr -s '\n' | grep -v "^#"
|
||||
}
|
||||
|
||||
gen_lanlist_6() {
|
||||
cat $USER_RULES_PATH/lanlist_ipv6 | tr -s '\n' | grep -v "^#"
|
||||
}
|
||||
|
||||
get_wan_ips() {
|
||||
local family="$1"
|
||||
local NET_ADDR
|
||||
local iface addr addrs
|
||||
local INTERFACES=$(ubus call network.interface dump | jsonfilter -e \
|
||||
'@.interface[!(@.interface ~ /lan/) && @.route[0]].interface')
|
||||
for iface in $INTERFACES; do
|
||||
if [ "$family" = "ip6" ]; then
|
||||
network_get_ipaddrs6 addrs "$iface"
|
||||
else
|
||||
network_get_ipaddrs addrs "$iface"
|
||||
fi
|
||||
for addr in $addrs; do
|
||||
case "$addr" in
|
||||
""|0.0.0.0|::|fe80:*) continue ;;
|
||||
esac
|
||||
case " $NET_ADDR " in
|
||||
*" $addr "*) ;;
|
||||
*) NET_ADDR="${NET_ADDR:+$NET_ADDR }$addr" ;;
|
||||
esac
|
||||
done
|
||||
done
|
||||
echo "$NET_ADDR"
|
||||
}
|
||||
|
||||
get_local_ips() {
|
||||
local family="$1"
|
||||
local ALL_IPS WAN_IPS ip NET_ADDR
|
||||
if [ "$family" = "ip6" ]; then
|
||||
ALL_IPS=$(ip -o -6 addr show scope global | awk '{print $4}' | cut -d/ -f1)
|
||||
WAN_IPS=$(get_wan_ips ip6)
|
||||
else
|
||||
ALL_IPS=$(ip -o -4 addr show scope global | awk '{print $4}' | cut -d/ -f1)
|
||||
WAN_IPS=$(get_wan_ips ip4)
|
||||
fi
|
||||
# 补充回环(scope global 不包含)
|
||||
[ "$family" = "ip6" ] && ALL_IPS="$ALL_IPS ::1"
|
||||
[ "$family" != "ip6" ] && ALL_IPS="$ALL_IPS 127.0.0.1"
|
||||
for ip in $ALL_IPS; do
|
||||
case "$ip" in
|
||||
""|0.0.0.0|::) continue ;;
|
||||
esac
|
||||
case " $WAN_IPS " in
|
||||
*" $ip "*) continue ;;
|
||||
esac
|
||||
case " $NET_ADDR " in
|
||||
*" $ip "*) ;;
|
||||
*) NET_ADDR="${NET_ADDR:+$NET_ADDR }$ip" ;;
|
||||
esac
|
||||
done
|
||||
for ip in $NET_ADDR; do
|
||||
echo "$ip"
|
||||
done
|
||||
}
|
||||
Reference in new issue
Block a user