update 2026-07-28 01:38:11

This commit is contained in:
action committed 2026-07-28 01:38:11 +08:00
1 parent 20b786ab77
commit 25d570431d
6243 files changed
+2850542

No files matched your search

@@ -0,0 +1,4 @@
config global 'global'
option enable '0'
option start_delay '90'
@@ -0,0 +1,35 @@
#!/bin/sh
. /usr/share/passwall/utils.sh
[ "$ACTION" = "ifup" ] || [ "$ACTION" = "ifupdate" ] || exit 0
([ "$(get_cache_var "ENABLED_DEFAULT_ACL")" = "1" ] || [ "$(get_cache_var "ENABLED_ACLS")" = "1" ]) && [ -f ${LOCK_PATH}/${CONFIG}_ready.lock ] && {
default_device=$(ip route show default 2>/dev/null | awk -F 'dev ' '{print $2}' | awk '{print $1}' | head -n1)
default6_device=$(ip -6 route show default 2>/dev/null | awk -F 'dev ' '{print $2}' | awk '{print $1}' | head -n1)
( [ "$default_device" = "$DEVICE" ] || [ "$default6_device" = "$DEVICE" ] ) && {
if [ "$ACTION" = "ifupdate" ]; then
USE_TABLES=$(get_cache_var "USE_TABLES")
[ -n "$USE_TABLES" ] && { ${APP_PATH}/${USE_TABLES}.sh update_wan_sets >/dev/null 2>&1 & }
exit 0
fi
[ ! -d ${LOCK_PATH} ] && mkdir -p ${LOCK_PATH}
LOCK_FILE="${LOCK_PATH}/${CONFIG}_ifup.lock"
if [ -s ${LOCK_FILE} ]; then
SPID=$(cat ${LOCK_FILE})
if [ -e /proc/${SPID}/status ]; then
exit 1
fi
cat /dev/null > ${LOCK_FILE}
fi
echo $$ > ${LOCK_FILE}
/etc/init.d/${CONFIG} restart >/dev/null 2>&1 &
logger -p notice -t network -s "${CONFIG}: restart when $INTERFACE ifup"
rm -rf ${LOCK_FILE}
}
}
+86
View File
@@ -0,0 +1,86 @@
#!/bin/sh /etc/rc.common
START=99
STOP=15
ROOT="${IPKG_INSTROOT:-}"
. "$ROOT/usr/share/passwall/utils.sh"
APP_FILE=${APP_PATH}/app.sh
LOCK_FILE_DIR=/var/lock
LOCK_FILE=${LOCK_FILE_DIR}/${CONFIG}.lock
set_lock() {
[ ! -d "$LOCK_FILE_DIR" ] && mkdir -p $LOCK_FILE_DIR
exec 999>"$LOCK_FILE"
flock -xn 999
}
unset_lock() {
flock -u 999
rm -rf "$LOCK_FILE"
}
unlock() {
failcount=1
while [ "$failcount" -le 10 ]; do
if [ -f "$LOCK_FILE" ]; then
let "failcount++"
sleep 1s
[ "$failcount" -ge 10 ] && unset_lock
else
break
fi
done
}
boot_func() {
local delay=$(uci -q get ${CONFIG}.@global_delay[0].start_delay || echo 1)
if [ "$delay" -gt 0 ]; then
echolog "执行启动延时 $delay 秒后再启动!"
sleep $delay
fi
restart boot
touch ${LOCK_FILE_DIR}/${CONFIG}_ready.lock
}
boot() {
boot_func >/dev/null 2>&1 &
}
start() {
set_lock
[ $? == 1 ] && echolog "脚本已经在运行,不重复运行,退出." && exit 0
$APP_FILE start
unset_lock
}
stop() {
unlock
set_lock
[ $? == 1 ] && echolog "停止脚本等待超时,不重复运行,退出." && exit 0
$APP_FILE stop
unset_lock
}
restart() {
set_lock
[ $? == 1 ] && echolog "脚本已经在运行,不重复运行,退出." && exit 0
rule_lock=${LOCK_FILE_DIR}/${CONFIG}_rule_update.lock
sub_lock=${LOCK_FILE_DIR}/${CONFIG}_subscribe.lock
for i in $(seq 1 300); do
if [ ! -f "$rule_lock" ] && [ ! -f "$sub_lock" ]; then
break
fi
sleep 2
done
rm -f "$rule_lock" "$sub_lock"
$APP_FILE stop
$APP_FILE start $1
unset_lock
}
reload() {
echo "Warning: This service does not support configuration reloading"
echo "Performing full restart instead..."
restart
}
+37
View File
@@ -0,0 +1,37 @@
#!/bin/sh /etc/rc.common
START=99
boot_func() {
config_load passwall_server
config_get start_delay global start_delay 90
case "$start_delay" in
''|*[!0-9]*) start_delay=90 ;;
*) [ "$start_delay" -le 600 ] 2>/dev/null || start_delay=90 ;;
esac
[ "$start_delay" -gt 0 ] && sleep "$start_delay"
restart
}
boot() {
boot_func >/dev/null 2>&1 &
}
start() {
lua /usr/lib/lua/luci/passwall/server_app.lua start
}
stop() {
lua /usr/lib/lua/luci/passwall/server_app.lua stop
}
restart() {
stop
start
}
reload() {
echo "Warning: This service does not support configuration reloading"
echo "Performing full restart instead..."
restart
}
+58
View File
@@ -0,0 +1,58 @@
#!/bin/sh
if [ -e "/etc/config/ucitrack" ]; then
uci -q batch <<-EOF
delete ucitrack.@passwall[-1]
add ucitrack passwall
set ucitrack.@passwall[-1].init=passwall
delete ucitrack.@passwall_server[-1]
add ucitrack passwall_server
set ucitrack.@passwall_server[-1].init=passwall_server
commit ucitrack
EOF
fi
uci -q batch <<-EOF
delete firewall.passwall
set firewall.passwall=include
set firewall.passwall.type='script'
set firewall.passwall.path='/var/etc/passwall.include'
delete firewall.passwall_server
set firewall.passwall_server=include
set firewall.passwall_server.type='script'
set firewall.passwall_server.path='/var/etc/passwall_server.include'
set dhcp.@dnsmasq[0].localuse=1
commit dhcp
set uhttpd.main.max_requests=50
commit uhttpd
EOF
if [ -x "/sbin/fw3" ]; then
uci -q set firewall.passwall.reload='1'
uci -q set firewall.passwall_server.reload='1'
else
uci -q delete firewall.passwall.reload
uci -q delete firewall.passwall.fw4_compatible
uci -q delete firewall.passwall_server.reload
uci -q delete firewall.passwall_server.fw4_compatible
fi
uci commit firewall
[ ! -s "/etc/config/passwall" ] && cp -f /usr/share/passwall/0_default_config /etc/config/passwall
chmod +x /usr/share/passwall/*.sh
[ "$(uci -q get passwall.@global_xray[0].sniffing)" == "1" ] && [ "$(uci -q get passwall.@global_xray[0].route_only)" != "1" ] && uci -q set passwall.@global_xray[0].sniffing_override_dest=1
uci -q delete passwall.@global_xray[0].sniffing
uci -q delete passwall.@global_xray[0].route_only
uci -q commit passwall
sed -i "s#add_from#group#g" /etc/config/passwall 2>/dev/null
rm -f /tmp/luci-indexcache /tmp/luci-indexcache.*
rm -rf /tmp/luci-modulecache/
killall -HUP rpcd 2>/dev/null
exit 0
@@ -0,0 +1,254 @@
config global
option enabled '0'
option socks_enabled '0'
option tcp_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_tcp '0'
option log_udp '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 '# steam直连域名获取国内CDN走国内线路下载
cm.steampowered.com
steamserver.net
# steam国内CDN华为云
steampipe.steamcontent.tnkjmec.com
# steam国内CDN白山云
st.dl.eccdnx.com
st.dl.bscstorage.net
st.dl.pinyuncloud.com
# steam国内CDN新流云(原金山云)(支持ipv6)
dl.steam.clngaa.com
# steam国内CDN网宿
cdn.mileweb.cs.steampowered.com.8686c.com
cdn-ws.content.steamchina.com
# steam国内CDN腾讯云 (蒸汽中国独占)
cdn-qc.content.steamchina.com
# steam国内CDN阿里云(支持ipv6)
cdn-ali.content.steamchina.com
xz.pphimalayanrt.com
lv.queniujq.cn
alibaba.cdn.steampipe.steamcontent.com
# 国内游戏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
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'
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,590 @@
#!/usr/bin/lua
-- Copyright(c): lwb1978 2025–2027
local api = require("luci.passwall.api")
local urlencode = api.UrlEncode
local base64 = api.base64Encode
local json = api.jsonc
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.insecure = node["skip-cert-verify"] == true
o.tls.alpn = build_alpn(node.alpn)
local ech_opts = node["ech-opts"]
if ech_opts and ech_opts.enable == true then
if ech_opts["query-server-name"] then
o.tls.ech = ech_opts["query-server-name"] .. "+https://223.5.5.5/dns-query"
elseif ech_opts.config then
o.tls.ech = ech_opts.config
end
end
-- ===== 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
o.transport.path = opts.path
o.transport.host = opts.headers and opts.headers.Host
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
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))
o.transport.path = get_first(opts.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 = {}
-- headers
if opts.headers then
extra.headers = opts.headers
end
if opts["x-padding-bytes"] then
extra.xPaddingBytes = opts["x-padding-bytes"]
end
if opts["no-grpc-header"] ~= nil then
extra.noGRPCHeader = opts["no-grpc-header"]
end
if opts["sc-max-each-post-bytes"] then
extra.scMaxEachPostBytes = opts["sc-max-each-post-bytes"]
end
if opts["sc-min-posts-interval-ms"] then
extra.scMinPostsIntervalMs = opts["sc-min-posts-interval-ms"]
end
-- xmux
if opts["reuse-settings"] then
local r = opts["reuse-settings"]
local xmux = {}
if r["max-concurrency"] then xmux.maxConcurrency = r["max-concurrency"] end
if r["max-connections"] then xmux.maxConnections = tonumber(r["max-connections"]) end
if r["c-max-reuse-times"] then xmux.cMaxReuseTimes = tonumber(r["c-max-reuse-times"]) end
if r["h-max-request-times"] then xmux.hMaxRequestTimes = r["h-max-request-times"] end
if r["h-max-reusable-secs"] then xmux.hMaxReusableSecs = r["h-max-reusable-secs"] end
if r["h-keep-alive-period"] then xmux.hKeepAlivePeriod = tonumber(r["h-keep-alive-period"]) end
if next(xmux) then extra.xmux = xmux end
end
-- download-settings
if opts["download-settings"] then
local d = opts["download-settings"]
local ds = {}
if d.server then ds.address = d.server end
if d.port then ds.port = d.port end
ds.network = "xhttp"
-- TLS
if d.tls then
ds.security = "tls"
ds.tlsSettings = {}
if d.servername then
ds.tlsSettings.serverName = d.servername
end
if d["skip-cert-verify"] == true then
ds.tlsSettings.allowInsecure = true
end
if d["client-fingerprint"] then
ds.tlsSettings.fingerprint = d["client-fingerprint"]
end
if d.fingerprint then
ds.tlsSettings.pinnedPeerCertSha256 = d.fingerprint
end
if d.alpn then
ds.tlsSettings.alpn = d.alpn
end
end
-- xhttpSettings
local xs = {}
if d.path then xs.path = d.path end
if d.host then xs.host = d.host end
if next(xs) then
ds.xhttpSettings = xs
end
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 node.flow then table.insert(p, "flow=" .. urlencode(node.flow)) end
if node.encryption then table.insert(p, "encryption=" .. urlencode(node.encryption)) end
-- TLS
if o.tls.security then table.insert(p, "security=" .. o.tls.security) end
if o.tls.pbk then table.insert(p, "pbk=" .. urlencode(o.tls.pbk)) end
if o.tls.sid then table.insert(p, "sid=" .. urlencode(o.tls.sid)) end
if o.tls.sni then table.insert(p, "sni=" .. urlencode(o.tls.sni)) end
if o.tls.fp then table.insert(p, "fp=" .. urlencode(o.tls.fp)) end
if o.tls.alpn then table.insert(p, "alpn=" .. urlencode(o.tls.alpn)) end
if o.tls.ech then table.insert(p, "ech=" .. urlencode(o.tls.ech)) end
if o.tls.pcs then table.insert(p, "pcs=" .. urlencode(o.tls.pcs)) end
table.insert(p, "allowInsecure=" .. (o.tls.insecure and "1" or "0"))
-- transport
table.insert(p, "type=" .. o.transport.type)
if o.transport.host then table.insert(p, "host=" .. urlencode(o.transport.host)) end
if o.transport.path then table.insert(p, "path=" .. urlencode(o.transport.path)) end
if o.transport.serviceName then table.insert(p, "serviceName=" .. urlencode(o.transport.serviceName)) end
if o.transport.mode then table.insert(p, "mode=" .. urlencode(o.transport.mode)) end
if o.transport.extra then table.insert(p, "extra=" .. urlencode(o.transport.extra)) end
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://" .. node.password .. "@" .. o.server .. ":" .. o.port
local p = {}
if o.tls.security then table.insert(p, "security=" .. o.tls.security) end
if o.tls.pbk then table.insert(p, "pbk=" .. urlencode(o.tls.pbk)) end
if o.tls.sid then table.insert(p, "sid=" .. urlencode(o.tls.sid)) end
if o.tls.sni then table.insert(p, "sni=" .. urlencode(o.tls.sni)) end
if o.tls.fp then table.insert(p, "fp=" .. urlencode(o.tls.fp)) end
if o.tls.alpn then table.insert(p, "alpn=" .. urlencode(o.tls.alpn)) end
if o.tls.ech then table.insert(p, "ech=" .. urlencode(o.tls.ech)) end
if o.tls.pcs then table.insert(p, "pcs=" .. urlencode(o.tls.pcs)) end
table.insert(p, "allowInsecure=" .. (o.tls.insecure and "1" or "0"))
table.insert(p, "type=" .. o.transport.type)
if o.transport.host then table.insert(p, "host=" .. urlencode(o.transport.host)) end
if o.transport.path then table.insert(p, "path=" .. urlencode(o.transport.path)) end
if o.transport.serviceName then table.insert(p, "serviceName=" .. urlencode(o.transport.serviceName)) end
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 = "none",
host = o.transport.host or "",
path = 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,
insecure = o.tls.insecure and "1" or "0",
tfo = node.tfo and "1" or "0"
}
if o.transport.type == "grpc" then
obj.path = 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 table.insert(p, "udp=1") end
if node["udp-over-tcp"] then table.insert(p, "uot=1") 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 {}))
table.insert(p, "shadow-tls=" .. urlencode(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
table.insert(p, "plugin=" .. urlencode(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_hysteria2(node)
local link = "hysteria://" .. host_format(node.server) .. ":" .. (node.port or "")
local p = {}
if node["auth-str"] then table.insert(p, "auth=" .. node["auth-str"]) end
if node["ports"] then table.insert(p, "mport=" .. node["ports"]) end
if node.obfs then table.insert(p, "obfsParam=" .. node.obfs) end
if node.sni then table.insert(p, "sni=" .. node.sni) end
if node.up then table.insert(p, "upmbps=" .. node.up) end
if node.down then table.insert(p, "downmbps=" .. node.down) end
if node["skip-cert-verify"] then table.insert(p, "insecure=1") end
if node["fingerprint"] then table.insert(p, "pinSHA256=" .. urlencode(node["fingerprint"])) end
if node.alpn then
table.insert(p, "alpn=" .. urlencode(build_alpn(node.alpn)))
end
if #p > 0 then
link = link .. "?" .. table.concat(p, "&")
end
return 0, link .. "#" .. urlencode(node.name or "")
end
-- Hysteria2
local function encode_hysteria2(node)
local link = "hysteria2://" .. (node.password or "") .. "@" .. host_format(node.server) .. ":" .. (node.port or "")
local p = {}
if node["ports"] then table.insert(p, "mport=" .. urlencode(node["ports"])) end
if node.obfs then table.insert(p, "obfs=" .. node.obfs) end
if node["obfs-password"] then table.insert(p, "obfs-password=" .. node["obfs-password"]) end
if node["obfs-min-packet-size"] then table.insert(p, "minPacketSize=" .. node["obfs-min-packet-size"]) end
if node["obfs-max-packet-size"] then table.insert(p, "maxPacketSize=" .. node["obfs-max-packet-size"]) end
if node.up then table.insert(p, "upmbps=" .. node.up) end
if node.down then table.insert(p, "downmbps=" .. node.down) end
if node.sni then table.insert(p, "sni=" .. urlencode(node.sni)) end
if node["skip-cert-verify"] then table.insert(p, "insecure=1") end
if node["fingerprint"] then table.insert(p, "pinSHA256=" .. urlencode(node["fingerprint"])) end
if #p > 0 then
link = link .. "?" .. table.concat(p, "&")
end
return 0, link .. "#" .. urlencode(node.name or "")
end
-- TUIC
local function encode_tuic(node)
local link = "tuic://" .. node.uuid .. ":" .. node.password .. "@" .. host_format(node.server) .. ":" .. node.port
local p = {}
if node["congestion-controller"] then
table.insert(p, "congestion_control=" .. node["congestion-controller"])
end
if node.alpn then
table.insert(p, "alpn=" .. urlencode(build_alpn(node.alpn)))
end
if node.sni then table.insert(p, "sni=" .. urlencode(node.sni)) end
if node["disable-sni"] then table.insert(p, "disable_sni=1") end
if node["skip-cert-verify"] then table.insert(p, "allowInsecure=1") end
if node["udp-relay-mode"] then table.insert(p, "udp_relay_mode=" .. node["udp-relay-mode"]) end
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://" .. (node.password or "") .. "@" .. host_format(node.server) .. ":" .. node.port
local p = {}
if o.tls.security then table.insert(p, "security=" .. o.tls.security) end
if o.tls.pbk then table.insert(p, "pbk=" .. urlencode(o.tls.pbk)) end
if o.tls.sid then table.insert(p, "sid=" .. urlencode(o.tls.sid)) end
if o.tls.sni then table.insert(p, "sni=" .. urlencode(o.tls.sni)) end
if o.tls.alpn then table.insert(p, "alpn=" .. urlencode(o.tls.alpn)) end
if o.tls.fp then table.insert(p, "fp=" .. urlencode(o.tls.fp)) end
if o.tls.ech then table.insert(p, "ech=" .. urlencode(o.tls.ech)) end
if o.tls.pcs then
table.insert(p, "insecure=1")
else
table.insert(p, "insecure=" .. (o.tls.insecure and "1" or "0"))
end
if node["disable-reuse"] then table.insert(p, "disable_reuse=1") end
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
local function encode_node(node)
if (not node.type) or (not node.name) then return nil end
local t = node.type
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)
else api.log("订阅转换 → 丢弃不支持的节点:" .. node.name .. ",节点类型:" .. t)
end
end
function parseClashNode(raw, remark)
if not raw then return "" end
local ok, lyaml = pcall(require, "lyaml")
if not ok then return raw end
local data = lyaml.load(raw)
if not data or type(data) ~= "table" then return raw end
if not data.proxies then return "" end
api.log('检测到 Clash 订阅,正在进行转换 ...')
local links = {}
for _, node in ipairs(data.proxies) do
local err, link = encode_node(node)
if 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,238 @@
#!/usr/bin/lua
local api = require ("luci.passwall.api")
local appname = api.appname
local fs = api.fs
local jsonc = api.jsonc
local uci = api.uci
local sys = api.sys
local log = function(...)
api.log(...)
end
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 = var["-dns"] or "119.29.29.29:53,223.5.5.5:53"
local cpu_thread = sys.exec('echo -n $(cat /proc/cpuinfo | grep "processor" | wc -l)') or "1"
local health_check_type = uci:get(appname, "@global_haproxy[0]", "health_check_type") or "tcp"
local health_check_inter = uci:get(appname, "@global_haproxy[0]", "health_check_inter") or "20"
local balancingStrategy = uci:get(appname, "@global_haproxy[0]", "balancingStrategy") or "roundrobin"
local console_port = uci:get(appname, "@global_haproxy[0]", "console_port")
local bind_local = uci:get(appname, "@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 50
tune.max-checks-per-thread 5
defaults
mode tcp
log global
option tcplog
option dontlognull
option http-server-close
#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 = {}
uci:foreach(appname, "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 = uci:get_all(appname, lbss)
local hop = (health_check_type == "passwall_logic") 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 == "passwall_logic" then
if server_node.type ~= "Socks" then
local relay_port = server_node.port
local new_port = api.get_new_port()
local config_file = string.format("haproxy_%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
)
)
)
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 or "20"):match("^%d+"))
inter = (inter and inter >= 20) and inter or 20
local inter_arg = "inter " .. inter .. "s fastinter " .. inter / 2 .. "s downinter " .. inter * 1.5 .. "s"
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)
for i, port in pairs(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 == "passwall_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 ""
-- 防止重名导致无法运行
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 = o.server_address .. ":" .. o.server_port
local server_conf = "server {{remark}} {{server}} weight {{weight}} {{resolvers}} check {{inter_arg}} rise 2 fall 2 {{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("{{inter_arg}}", inter_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
--控制台配置
local console_user = uci:get(appname, "@global_haproxy[0]", "console_user")
local console_password = uci:get(appname, "@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 == "passwall_logic" then
local probeUrl = uci:get(appname, "@global_haproxy[0]", "health_probe_url") or "https://www.google.com/generate_204"
local f_url = io.open(haproxy_path .. "/Probe_URL", "w")
f_url:write(probeUrl)
f_url:close()
end
+37
View File
@@ -0,0 +1,37 @@
#!/bin/sh
export PATH=/usr/sbin:/usr/bin:/sbin:/bin:/root/bin
CONFIG=passwall
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
}
probe_file="/tmp/etc/passwall/haproxy/Probe_URL"
probeUrl="https://www.google.com/generate_204"
if [ -f "$probe_file" ]; then
firstLine=$(head -n 1 "$probe_file" | tr -d ' \t\n')
[ -n "$firstLine" ] && probeUrl="$firstLine"
fi
extra_params="-x socks5h://${server_address}:${server_port}"
if /usr/bin/curl --help all | grep -q "\-\-retry-all-errors"; then
extra_params="${extra_params} --retry-all-errors"
fi
status=$(/usr/bin/curl -I -o /dev/null -skL ${extra_params} --connect-timeout 3 --retry 1 --max-time 6 -w "%{http_code}" "${probeUrl}")
case "$status" in
200|204)
exit 0
;;
*)
exit 1
;;
esac
@@ -0,0 +1,533 @@
local api = require "luci.passwall.api"
local appname = api.appname
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 TCP_NODE = var["-TCP_NODE"]
local NFTFLAG = var["-NFTFLAG"]
local FILTER_HTTPS = var["-FILTER_HTTPS"]
local LOG_FILE = var["-LOG_FILE"]
local uci = api.uci
local sys = api.sys
local fs = api.fs
local datatypes = api.datatypes
local TMP_PATH = "/tmp/etc/" .. appname
local TMP_ACL_PATH = TMP_PATH .. "/acl"
local RULES_PATH = "/usr/share/" .. appname .. "/rules"
local FLAG_PATH = TMP_ACL_PATH .. "/" .. FLAG
local config_lines = {}
local tmp_lines = {}
local USE_GEOVIEW = uci:get(appname, "@global_rules[0]", "enable_geoview")
local IS_SHUNT_NODE = uci:get(appname, TCP_NODE, "protocol") == "_shunt"
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 then
if sys.exec('[ -s "%s" ] && echo -n 1' % path) == "1" then
return true
end
end
return nil
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 = uci:get(appname, "@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 end
local bin = api.finded_com("geoview")
if bin and list_arg and out_path then
local cmd = string.format("%q -type geosite -append=true -input %q -list %q -output %q -lowmem=true",
bin, geosite_path, list_arg, out_path)
sys.call(cmd)
return 0
end
return 1
end
if not fs.access(FLAG_PATH) then
fs.mkdir(FLAG_PATH)
end
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 = TMP_ACL_PATH .. "/block_host"
if USE_BLOCK_LIST == "1" and not fs.access(file_block_host) then
local block_domain, lookup_block_domain = {}, {}
local geosite_arg = ""
for line in io.lines(RULES_PATH .. "/block_host") 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
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
if USE_GEOVIEW == "1" and geosite_arg ~= "" and api.is_finded("geoview") then
if get_geosite(geosite_arg, file_block_host) == 0 then
log(" * 解析[屏蔽列表] Geosite 到屏蔽域名表(blocklist)完成")
else
log(" * 解析[屏蔽列表] Geosite 到屏蔽域名表(blocklist)失败!")
end
end
end
if USE_BLOCK_LIST == "1" and is_file_nonzero(file_block_host) then
tmp_lines = {
"group null",
"group-dnl " .. file_block_host
}
insert_array_after(config_lines, tmp_lines, "#--5")
end
--始终用国内DNS解析节点域名
local file_vpslist = TMP_ACL_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
uci:foreach(appname, "nodes", function(t)
process_address(t.address)
process_address(t.download_address)
end)
uci:foreach(appname, "subscribe_list", function(t) --订阅链接
local url, _ = api.get_domain_port_from_url(t.url or "")
if url and url ~= "" then
process_address(url)
end
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 = TMP_ACL_PATH .. "/direct_host"
if USE_DIRECT_LIST == "1" and not fs.access(file_direct_host) then
local direct_domain, lookup_direct_domain = {}, {}
local geosite_arg = ""
for line in io.lines(RULES_PATH .. "/direct_host") 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(direct_domain, line, lookup_direct_domain)
end
end
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
if USE_GEOVIEW == "1" and geosite_arg ~= "" and api.is_finded("geoview") then
if get_geosite(geosite_arg, file_direct_host) == 0 then
log(" * 解析[直连列表] Geosite 到域名白名单(whitelist)完成")
else
log(" * 解析[直连列表] Geosite 到域名白名单(whitelist)失败!")
end
end
end
if USE_DIRECT_LIST == "1" and is_file_nonzero(file_direct_host) then
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 = TMP_ACL_PATH .. "/proxy_host"
if USE_PROXY_LIST == "1" and not fs.access(file_proxy_host) then
local proxy_domain, lookup_proxy_domain = {}, {}
local geosite_arg = ""
for line in io.lines(RULES_PATH .. "/proxy_host") 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(proxy_domain, line, lookup_proxy_domain)
end
end
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
if USE_GEOVIEW == "1" and geosite_arg ~= "" and api.is_finded("geoview") then
if get_geosite(geosite_arg, file_proxy_host) == 0 then
log(" * 解析[代理列表] Geosite 到代理域名表(blacklist)完成")
else
log(" * 解析[代理列表] Geosite 到代理域名表(blacklist)失败!")
end
end
end
if USE_PROXY_LIST == "1" and is_file_nonzero(file_proxy_host) then
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
tmp_lines = {
"chnlist-file " .. RULES_PATH .. "/chnlist",
"ipset-name4 " .. setflag .. "psw_chn",
"ipset-name6 " .. setflag .. "psw_chn6",
"add-tagchn-ip",
"chnlist-first"
}
merge_array(config_lines, tmp_lines)
log(string.format(" - 中国域名表(chnroute):%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(" - 中国域名表(chnroute):%s", DNS_TRUST or "默认"))
end
end
--分流规则
if IS_SHUNT_NODE then
local white_domain, lookup_white_domain = {}, {}
local shunt_domain, lookup_shunt_domain = {}, {}
local file_white_host = FLAG_PATH .. "/shunt_direct_host"
local file_shunt_host = FLAG_PATH .. "/shunt_proxy_host"
local geosite_white_arg, geosite_shunt_arg = "", ""
local t = uci:get_all(appname, TCP_NODE)
local default_node_id = t["default_node"] or "_direct"
uci:foreach(appname, "shunt_rules", function(s)
local _node_id = t[s[".name"]]
if _node_id and _node_id ~= "_blackhole" 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:") then
if line:find("geosite:") then
line = string.match(line, ":([^:]+)$")
if _node_id == "_direct" then
geosite_white_arg = geosite_white_arg .. (geosite_white_arg ~= "" and "," or "") .. line
else
geosite_shunt_arg = geosite_shunt_arg .. (geosite_shunt_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(white_domain, line, lookup_white_domain)
else
insert_unique(shunt_domain, line, lookup_shunt_domain)
end
end
end
end
end
if _node_id ~= "_direct" then
log(string.format(" - Sing-Box/Xray分流规则(%s):%s", s.remarks, DNS_TRUST or "默认"))
end
end
end)
if is_file_nonzero(file_white_host) == nil then
if #white_domain > 0 then
local f_out = io.open(file_white_host, "w")
for i = 1, #white_domain do
f_out:write(white_domain[i] .. "\n")
end
f_out:close()
end
end
if is_file_nonzero(file_shunt_host) == nil then
if #shunt_domain > 0 then
local f_out = io.open(file_shunt_host, "w")
for i = 1, #shunt_domain do
f_out:write(shunt_domain[i] .. "\n")
end
f_out:close()
end
end
if GFWLIST == "1" and CHNLIST == "0" and USE_GEOVIEW == "1" and api.is_finded("geoview") then --仅GFW模式解析geosite
local return_white, return_shunt
if geosite_white_arg ~= "" then
return_white = get_geosite(geosite_white_arg, file_white_host)
end
if geosite_shunt_arg ~= "" then
return_shunt = get_geosite(geosite_shunt_arg, file_shunt_host)
end
if (return_white == nil or return_white == 0) and (return_shunt == nil or return_shunt == 0) then
log(" * 解析[分流节点] Geosite 完成")
else
log(" * 解析[分流节点] Geosite 失败!")
end
end
local sets = {
setflag .. "psw_shunt",
setflag .. "psw_shunt6"
}
if FLAG ~= "default" then
sets = {
setflag .. "psw_" .. FLAG .. "_shunt",
setflag .. "psw_" .. FLAG .. "_shunt6"
}
end
if is_file_nonzero(file_white_host) then
if USE_DIRECT_LIST == "1" then
--当白名单启用时,添加到白名单组一同处理
for i, v in ipairs(config_lines) do
if v == "group-dnl " .. file_direct_host then
config_lines[i] = "group-dnl " .. file_direct_host .. "," .. file_white_host
break
end
end
else
--当白名单不启用时,创建新组,ipset到shuntlist
tmp_lines = {
"group whitelist",
"group-dnl " .. file_white_host,
"group-upstream " .. DNS_LOCAL,
"group-ipset " .. table.concat(sets, ",")
}
insert_array_after(config_lines, tmp_lines, "#--4")
end
end
if is_file_nonzero(file_shunt_host) then
tmp_lines = {
"group shuntlist",
"group-dnl " .. file_shunt_host,
"group-upstream " .. DNS_TRUST,
"group-ipset " .. table.concat(sets, ",")
}
if NO_IPV6_TRUST == "1" then table.insert(tmp_lines, "no-ipv6 tag:shuntlist") end
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 DEFAULT_TAG == nil 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,718 @@
local api = require "luci.passwall.api"
local appname = "passwall"
local uci = api.uci
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 = uci:get("dhcp", "@dnsmasq[0]", "server")
if DNSMASQ_DNS and #DNSMASQ_DNS > 0 then
uci:set(appname, "@global[0]", "dnsmasq_servers", DNSMASQ_DNS)
api.uci_save(uci, appname, true)
end
end
local function restore_servers()
local dns_table = {}
local DNSMASQ_DNS = 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 = uci:get(appname, "@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
uci:delete(appname, "@global[0]", "dnsmasq_servers")
api.uci_save(uci, appname, true)
end
if dns_table and #dns_table > 0 then
uci:set_list("dhcp", "@dnsmasq[0]", "server", dns_table)
api.uci_save(uci, "dhcp", true)
end
end
function stretch()
local dnsmasq_server = uci:get("dhcp", "@dnsmasq[0]", "server")
local dnsmasq_noresolv = 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
uci:delete("dhcp", "@dnsmasq[0]", "noresolv")
local RESOLVFILE = "/tmp/resolv.conf.d/resolv.conf.auto"
local file = io.open(RESOLVFILE, "r")
if not file then
RESOLVFILE = "/tmp/resolv.conf.auto"
else
local size = file:seek("end")
file:close()
if size == 0 then
RESOLVFILE = "/tmp/resolv.conf.auto"
end
end
uci:set("dhcp", "@dnsmasq[0]", "resolvfile", RESOLVFILE)
api.uci_save(uci, "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 = 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
uci:set_list("dhcp", "@dnsmasq[0]", "server", dns_table)
api.uci_save(uci, "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]}')")
for line in io.lines("/tmp/etc/dnsmasq.conf." .. DEFAULT_DNSMASQ_CFGID) 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("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
tinsert(conf_lines, "port=" .. LISTEN_PORT)
if TMP_DNSMASQ_PATH then
sys.call("rm -rf " .. 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 TCP_NODE = var["-TCP_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 = uci:get(appname, TCP_NODE, "protocol") == "_shunt"
local list1 = {}
local excluded_domain = {}
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 cache_text = ""
local nodes_address_md5 = sys.exec("echo -n $(uci show passwall | grep '\\.address') | md5sum")
local new_rules = sys.exec("echo -n $(find /usr/share/passwall/rules -type f | xargs md5sum)")
local new_text = 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 .. nodes_address_md5 .. new_rules .. NFTFLAG
if fs.access(CACHE_TEXT_FILE) then
for line in io.lines(CACHE_TEXT_FILE) do
cache_text = line
end
end
if cache_text ~= new_text then
api.remove(CACHE_DNS_PATH .. "*")
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 ""
if not fs.access(CACHE_DNS_PATH) then
fs.mkdir(CACHE_DNS_PATH)
--屏蔽列表
if USE_CHINADNS_NG == "0" then
if USE_BLOCK_LIST == "1" then
for line in io.lines("/usr/share/passwall/rules/block_host") do
line = api.get_std_domain(line)
if line ~= "" and not line:find("#") and not line:find(":") then
set_domain_address(line, "")
end
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
uci:foreach(appname, "nodes", function(t)
process_address(t.address)
process_address(t.download_address)
end)
uci:foreach(appname, "subscribe_list", function(t) --订阅链接
local url, _ = api.get_domain_port_from_url(t.url or "")
if url and url ~= "" then
process_address(url)
end
end)
log(string.format(" - 节点列表中的域名(vpslist):%s", fwd_dns or "默认"))
end
end
--直连(白名单)列表
if USE_DIRECT_LIST == "1" then
if fs.access("/usr/share/passwall/rules/direct_host") 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解析直连(白名单)列表
for line in io.lines("/usr/share/passwall/rules/direct_host") do
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
log(string.format(" - 域名白名单(whitelist):%s", fwd_dns or "默认"))
end
end
end
--代理(黑名单)列表
if USE_PROXY_LIST == "1" then
if fs.access("/usr/share/passwall/rules/proxy_host") 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解析代理(黑名单)列表
for line in io.lines("/usr/share/passwall/rules/proxy_host") do
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
log(string.format(" - 代理域名表(blacklist):%s", fwd_dns or "默认"))
end
end
end
--GFW列表
if USE_GFW_LIST == "1" then
if fs.access("/usr/share/passwall/rules/gfwlist") 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
for line in io.lines("/usr/share/passwall/rules/gfwlist") 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
log(string.format(" - 防火墙域名表(gfwlist):%s", fwd_dns or "默认"))
end
end
end
--中国列表
if CHN_LIST ~= "0" then
if fs.access("/usr/share/passwall/rules/chnlist") 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
for line in io.lines("/usr/share/passwall/rules/chnlist") 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
log(string.format(" - 中国域名表(chnroute):%s", fwd_dns or "默认"))
end
end
end
--分流规则
if IS_SHUNT_NODE and USE_CHINADNS_NG == "0" then
local t = uci:get_all(appname, TCP_NODE)
local default_node_id = t["default_node"] or "_direct"
uci:foreach(appname, "shunt_rules", function(s)
local _node_id = t[s[".name"]]
if _node_id and _node_id ~= "_blackhole" then
if _node_id == "_default" then
_node_id = default_node_id
end
fwd_dns = nil
no_ipv6 = nil
local sets = {}
if _node_id == "_direct" then
fwd_dns = LOCAL_DNS
if USE_DIRECT_LIST == "1" then
table.insert(sets, setflag_4 .. "psw_white")
table.insert(sets, setflag_6 .. "psw_white6")
else
local set_name = "psw_shunt"
local set6_name = "psw_shunt6"
if FLAG ~= "default" then
set_name = "psw_" .. FLAG .. "_shunt"
set6_name = "psw_" .. FLAG .. "_shunt6"
end
table.insert(sets, setflag_4 .. set_name)
table.insert(sets, setflag_6 .. set6_name)
end
else
local set_name = "psw_shunt"
local set6_name = "psw_shunt6"
if FLAG ~= "default" then
set_name = "psw_" .. FLAG .. "_shunt"
set6_name = "psw_" .. FLAG .. "_shunt6"
end
fwd_dns = TUN_DNS
table.insert(sets, setflag_4 .. set_name)
if NO_PROXY_IPV6 ~= "1" then
table.insert(sets, setflag_6 .. set6_name)
else
no_ipv6 = true
end
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("geosite:") and not line:find("ext:") then
if line:find("domain:") or line:find("full:") then
line = string.match(line, ":([^:]+)$")
end
line = api.get_std_domain(line)
add_excluded_domain(line)
if no_ipv6 then
set_domain_address(line, "::")
end
set_domain_dns(line, fwd_dns)
set_domain_ipset(line, table.concat(sets, ","))
end
end
if _node_id ~= "_direct" 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
local f_out = io.open(CACHE_TEXT_FILE, "a")
f_out:write(new_text)
f_out:close()
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 = "/tmp/etc/" .. appname .. "_tmp/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")
conf_out:write(table.concat(conf_lines, "\n"))
conf_out:write("\n")
conf_out:close()
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,30 @@
#!/bin/sh
restart() {
local no_log
eval_set_val $@
_LOG_FILE=$LOG_FILE
[ -n "$no_log" ] && LOG_FILE="/dev/null"
rm -rf /tmp/smartdns.cache
/etc/init.d/smartdns reload >/dev/null 2>&1
LOG_FILE=${_LOG_FILE}
}
del() {
rm -rf /tmp/etc/smartdns/passwall.conf
sed -i "/passwall/d" /etc/smartdns/custom.conf >/dev/null 2>&1
rm -rf /tmp/smartdns.cache
/etc/init.d/smartdns reload >/dev/null 2>&1
}
arg1=$1
shift
case $arg1 in
del)
del $@
;;
restart)
restart $@
;;
*) ;;
esac
@@ -0,0 +1,678 @@
local api = require "luci.passwall.api"
local appname = api.appname
local var = api.get_args(arg)
local FLAG = var["-FLAG"]
local SMARTDNS_CONF = var["-SMARTDNS_CONF"]
local LOCAL_GROUP = var["-LOCAL_GROUP"]
local REMOTE_GROUP = var["-REMOTE_GROUP"]
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 TCP_NODE = var["-TCP_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 LOCAL_PORT = var["-LOCAL_PORT"]
local uci = api.uci
local sys = api.sys
local fs = api.fs
local datatypes = api.datatypes
local TMP_PATH = "/tmp/etc/" .. appname
local TMP_ACL_PATH = TMP_PATH .. "/acl"
local RULES_PATH = "/usr/share/" .. appname .. "/rules"
local FLAG_PATH = TMP_ACL_PATH .. "/" .. FLAG
local TMP_CONF_FILE = FLAG_PATH .. "/smartdns.conf"
local config_lines = {}
local tmp_lines = {}
local USE_GEOVIEW = uci:get(appname, "@global_rules[0]", "enable_geoview")
local IS_SHUNT_NODE = uci:get(appname, TCP_NODE, "protocol") == "_shunt"
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 then
if sys.exec('[ -s "%s" ] && echo -n 1' % path) == "1" then
return true
end
end
return nil
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 = uci:get(appname, "@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 end
local bin = api.finded_com("geoview")
if bin and list_arg and out_path then
local cmd = string.format("%q -type geosite -append=true -input %q -list %q -output %q -lowmem=true",
bin, geosite_path, list_arg, out_path)
sys.call(cmd)
return 0
end
return 1
end
if not fs.access(FLAG_PATH) then
fs.mkdir(FLAG_PATH)
end
local LOCAL_EXTEND_ARG = ""
if LOCAL_GROUP == "null" or LOCAL_GROUP == "" then
log(" * 注意:SmartDNS 国内分组名未设置,DNS 将无法正常工作!!!")
os.exit(1)
else
--从smartdns配置中读取参数
local custom_conf_path = "/etc/smartdns/custom.conf"
local options = {
{key = "dualstack_ip_selection", config_key = "dualstack-ip-selection", yes_no = true, arg_yes = "-d yes", arg_no = "-d no", default = "yes"},
{key = "speed_check_mode", config_key = "speed-check-mode", prefix = "-c ", default = "ping,tcp:80,tcp:443"},
{key = "serve_expired", config_key = "serve-expired", yes_no = true, arg_yes = "", arg_no = "-no-serve-expired", default = "yes"},
{key = "response_mode", config_key = "response-mode", prefix = "-r ", default = "first-ping"},
{key = "rr_ttl", config_key = "rr-ttl", prefix = "-rr-ttl "},
{key = "rr_ttl_min", config_key = "rr-ttl-min", prefix = "-rr-ttl-min "},
{key = "rr_ttl_max", config_key = "rr-ttl-max", prefix = "-rr-ttl-max "},
{key = "rr_ttl_reply_max", config_key = "rr-ttl-reply-max", prefix = "-rr-ttl-reply-max "},
{
key = "force_aaaa_soa",
config_key = "force-qtype-SOA",
prefix = "-address ",
get_value = function(custom_config)
local soa = custom_config["force-qtype-SOA"]
return ((soa and soa:match("(^|%s)28(%s|$)"))
or custom_config["force-AAAA-SOA"] == "yes"
or uci:get("smartdns", "@smartdns[0]", "force_aaaa_soa") == "1")
and "#6" or "-6"
end
}
}
-- 从 custom.conf 中读取值,以最后出现的值为准
local custom_config = {}
local f_in = io.open(custom_conf_path, "r")
if f_in then
for line in f_in:lines() do
line = api.trim(line)
if line ~= "" and not line:match("^#") then
local param, value = line:match("^(%S+)%s+(.+)$")
if param and value then custom_config[param] = value end
end
end
f_in:close()
end
-- 从 smartdns 配置中读取值,优先级以 custom.conf 为准
for _, opt in ipairs(options) do
local val
if opt.get_value then
val = opt.get_value(custom_config)
else
val = custom_config[opt.config_key] or uci:get("smartdns", "@smartdns[0]", opt.key) or opt.default
end
if val == "yes" then val = "1" elseif val == "no" then val = "0" end
if opt.yes_no then
local arg = (val == "1" and opt.arg_yes or opt.arg_no)
if arg and arg ~= "" then
LOCAL_EXTEND_ARG = LOCAL_EXTEND_ARG .. (LOCAL_EXTEND_ARG ~= "" and " " or "") .. arg
end
else
if val and (not opt.value or (opt.invert and val ~= opt.value) or (not opt.invert and val == opt.value)) then
LOCAL_EXTEND_ARG = LOCAL_EXTEND_ARG .. (LOCAL_EXTEND_ARG ~= "" and " " or "") .. (opt.prefix or "") .. (opt.arg or val)
end
end
end
end
if not REMOTE_GROUP or REMOTE_GROUP == "nil" then
REMOTE_GROUP = "passwall_proxy"
if REMOTE_DNS then
REMOTE_DNS = REMOTE_DNS:gsub("#", ":")
end
sys.call('sed -i "/passwall/d" /etc/smartdns/custom.conf >/dev/null 2>&1')
end
local force_https_soa = uci:get(appname, "@global[0]", "force_https_soa") or 0
local proxy_server_name = "passwall-proxy-server"
config_lines = {
tonumber(LISTEN_PORT) ~= 0 and "bind [::]:" .. LISTEN_PORT .. "@lo" or "",
(tonumber(LOCAL_PORT) ~= 0 and LOCAL_GROUP) and "bind [::]:" .. LOCAL_PORT .. "@lo -group " .. LOCAL_GROUP or "",
tonumber(force_https_soa) == 1 and "force-qtype-SOA 65" or "force-qtype-SOA -,65",
"server 223.5.5.5 -bootstrap-dns",
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 DNS_MODE == "socks" then
for w in string.gmatch(REMOTE_DNS, '[^|]+') do
local server_dns = api.trim(w)
local server_param
local dnsType = string.match(server_dns, "^(.-)://")
dnsType = dnsType and string.lower(dnsType) or nil
local dnsServer = string.match(server_dns, "://(.+)") or server_dns
if dnsType and dnsType ~= "" and dnsType ~= "udp" then
if dnsType == "tcp" then
server_param = "server-tcp " .. dnsServer
elseif dnsType == "tls" then
server_param = "server-tls " .. dnsServer
elseif dnsType == "quic" then
server_param = "server-quic " .. dnsServer
elseif dnsType == "https" or dnsType == "h3" then
local http_host = nil
local url = w
local port = 443
local s = api.split(w, ",")
if s and #s > 1 then
url = s[1]
local dns_ip = s[2]
local host_port = api.get_domain_from_url(s[1])
if host_port and #host_port > 0 then
http_host = host_port
local s2 = api.split(host_port, ":")
if s2 and #s2 > 1 then
http_host = s2[1]
port = s2[2]
end
url = url:gsub(http_host, dns_ip)
end
end
server_dns = url
if http_host then
server_dns = server_dns .. " -http-host " .. http_host
end
server_param = (dnsType == "https" and "server-https " or "server-h3 ") .. server_dns
end
else
server_param = "server " .. dnsServer
end
if not api.is_local_ip(w) then
server_param = server_param .. " -proxy " .. proxy_server_name
end
server_param = server_param .. " -group " .. REMOTE_GROUP .. " -exclude-default-group"
if SUBNET and SUBNET ~= "" and SUBNET ~= "0" then
server_param = server_param .. " -subnet " .. SUBNET
end
table.insert(config_lines, server_param)
end
else
local server_param = string.format("server %s -group %s -exclude-default-group", TUN_DNS:gsub("#", ":"), REMOTE_GROUP)
table.insert(config_lines, server_param)
log(" - " .. DNS_MODE:gsub("^%l",string.upper) .. " " .. TUN_DNS .. " -> " .. REMOTE_GROUP)
end
--设置默认 DNS 分组(托底组)
local DEFAULT_DNS_GROUP = (USE_DEFAULT_DNS == "direct" and LOCAL_GROUP) or
(USE_DEFAULT_DNS == "remote" and REMOTE_GROUP)
local only_global = (DEFAULT_PROXY_MODE == "proxy" and CHN_LIST == "0" and USE_GFW_LIST == "0") and 1 --没有启用中国列表和GFW列表时(全局)
if only_global == 1 then
DEFAULT_DNS_GROUP = REMOTE_GROUP
end
if DEFAULT_DNS_GROUP then
local domain_rules_str = "domain-rules /./ -nameserver " .. DEFAULT_DNS_GROUP
if DEFAULT_DNS_GROUP == REMOTE_GROUP then
domain_rules_str = domain_rules_str .. " -speed-check-mode none -d no -no-serve-expired"
domain_rules_str = domain_rules_str .. " -address " .. (NO_PROXY_IPV6 == "1" and "#6" or "-6")
elseif DEFAULT_DNS_GROUP == LOCAL_GROUP then
domain_rules_str = domain_rules_str .. (LOCAL_EXTEND_ARG ~= "" and " " .. LOCAL_EXTEND_ARG or "")
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, 8 do
table.insert(config_lines, "#--" .. i)
end
--屏蔽列表
local file_block_host = TMP_ACL_PATH .. "/block_host"
if USE_BLOCK_LIST == "1" and not fs.access(file_block_host) then
local block_domain, lookup_block_domain = {}, {}
local geosite_arg = ""
for line in io.lines(RULES_PATH .. "/block_host") 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
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
if USE_GEOVIEW == "1" and geosite_arg ~= "" and api.is_finded("geoview") then
if get_geosite(geosite_arg, file_block_host) == 0 then
log(" * 解析[屏蔽列表] Geosite 到屏蔽域名表(blocklist)完成")
else
log(" * 解析[屏蔽列表] Geosite 到屏蔽域名表(blocklist)失败!")
end
end
end
if USE_BLOCK_LIST == "1" and is_file_nonzero(file_block_host) then
local domain_set_name = "passwall-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, "#--7")
end
--始终用国内DNS解析节点域名
local file_vpslist = TMP_ACL_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
uci:foreach(appname, "nodes", function(t)
process_address(t.address)
process_address(t.download_address)
end)
uci:foreach(appname, "subscribe_list", function(t) --订阅链接
local url, _ = api.get_domain_port_from_url(t.url or "")
if url and url ~= "" then
process_address(url)
end
end)
f_out:close()
end
if is_file_nonzero(file_vpslist) then
local domain_set_name = "passwall-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/ %s', domain_set_name, LOCAL_GROUP and "-nameserver " .. LOCAL_GROUP or "")
domain_rules_str = domain_rules_str .. " " .. set_type .. " " .. table.concat(sets, ",")
domain_rules_str = domain_rules_str .. (LOCAL_EXTEND_ARG ~= "" and " " .. LOCAL_EXTEND_ARG or "")
table.insert(tmp_lines, domain_rules_str)
insert_array_after(config_lines, tmp_lines, "#--8")
log(string.format(" - 节点列表中的域名(vpslist)使用分组:%s", LOCAL_GROUP or "默认"))
end
--直连(白名单)列表
local file_direct_host = TMP_ACL_PATH .. "/direct_host"
if USE_DIRECT_LIST == "1" and not fs.access(file_direct_host) then
local direct_domain, lookup_direct_domain = {}, {}
local geosite_arg = ""
for line in io.lines(RULES_PATH .. "/direct_host") 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(direct_domain, line, lookup_direct_domain)
end
end
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
if USE_GEOVIEW == "1" and geosite_arg ~= "" and api.is_finded("geoview") then
if get_geosite(geosite_arg, file_direct_host) == 0 then
log(" * 解析[直连列表] Geosite 到域名白名单(whitelist)完成")
else
log(" * 解析[直连列表] Geosite 到域名白名单(whitelist)失败!")
end
end
end
if USE_DIRECT_LIST == "1" and is_file_nonzero(file_direct_host) then
local domain_set_name = "passwall-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/ %s', domain_set_name, LOCAL_GROUP and "-nameserver " .. LOCAL_GROUP or "")
domain_rules_str = domain_rules_str .. " " .. set_type .. " " .. table.concat(sets, ",")
domain_rules_str = domain_rules_str .. (LOCAL_EXTEND_ARG ~= "" and " " .. LOCAL_EXTEND_ARG or "")
table.insert(tmp_lines, domain_rules_str)
insert_array_after(config_lines, tmp_lines, "#--6")
log(string.format(" - 域名白名单(whitelist)使用分组:%s", LOCAL_GROUP or "默认"))
end
--代理(黑名单)列表
local file_proxy_host = TMP_ACL_PATH .. "/proxy_host"
if USE_PROXY_LIST == "1" and not fs.access(file_proxy_host) then
local proxy_domain, lookup_proxy_domain = {}, {}
local geosite_arg = ""
for line in io.lines(RULES_PATH .. "/proxy_host") 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(proxy_domain, line, lookup_proxy_domain)
end
end
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
if USE_GEOVIEW == "1" and geosite_arg ~= "" and api.is_finded("geoview") then
if get_geosite(geosite_arg, file_proxy_host) == 0 then
log(" * 解析[代理列表] Geosite 到代理域名表(blacklist)完成")
else
log(" * 解析[代理列表] Geosite 到代理域名表(blacklist)失败!")
end
end
end
if USE_PROXY_LIST == "1" and is_file_nonzero(file_proxy_host) then
local domain_set_name = "passwall-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 %s', domain_set_name, REMOTE_GROUP)
domain_rules_str = domain_rules_str .. " -speed-check-mode none"
domain_rules_str = domain_rules_str .. " -no-serve-expired"
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 -d no " .. set_type .. " " .. table.concat(sets, ",")
end
table.insert(tmp_lines, domain_rules_str)
insert_array_after(config_lines, tmp_lines, "#--5")
log(string.format(" - 代理域名表(blacklist)使用分组:%s", REMOTE_GROUP or "默认"))
end
--GFW列表
if USE_GFW_LIST == "1" and is_file_nonzero(RULES_PATH .. "/gfwlist") then
local domain_set_name = "passwall-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 %s', domain_set_name, REMOTE_GROUP)
domain_rules_str = domain_rules_str .. " -speed-check-mode none"
domain_rules_str = domain_rules_str .. " -no-serve-expired"
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 -d no " .. set_type .. " " .. table.concat(sets, ",")
end
table.insert(tmp_lines, domain_rules_str)
insert_array_after(config_lines, tmp_lines, "#--1")
log(string.format(" - 防火墙域名表(gfwlist)使用分组:%s", REMOTE_GROUP or "默认"))
end
--中国列表
if CHN_LIST ~= "0" and is_file_nonzero(RULES_PATH .. "/chnlist") then
local domain_set_name = "passwall-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/ %s', domain_set_name, LOCAL_GROUP and "-nameserver " .. LOCAL_GROUP or "")
domain_rules_str = domain_rules_str .. " " .. set_type .. " " .. table.concat(sets, ",")
domain_rules_str = domain_rules_str .. (LOCAL_EXTEND_ARG ~= "" and " " .. LOCAL_EXTEND_ARG or "")
table.insert(tmp_lines, domain_rules_str)
insert_array_after(config_lines, tmp_lines, "#--2")
log(string.format(" - 中国域名表(chnroute)使用分组:%s", LOCAL_GROUP or "默认"))
end
--回中国模式
if CHN_LIST == "proxy" then
local domain_rules_str = string.format('domain-rules /domain-set:%s/ -nameserver %s', domain_set_name, REMOTE_GROUP)
domain_rules_str = domain_rules_str .. " -speed-check-mode none"
domain_rules_str = domain_rules_str .. " -no-serve-expired"
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 -d no " .. set_type .. " " .. table.concat(sets, ",")
end
table.insert(tmp_lines, domain_rules_str)
insert_array_after(config_lines, tmp_lines, "#--2")
log(string.format(" - 中国域名表(chnroute)使用分组:%s", REMOTE_GROUP or "默认"))
end
end
--分流规则
if IS_SHUNT_NODE then
local white_domain, lookup_white_domain = {}, {}
local shunt_domain, lookup_shunt_domain = {}, {}
local file_white_host = FLAG_PATH .. "/shunt_direct_host"
local file_shunt_host = FLAG_PATH .. "/shunt_proxy_host"
local geosite_white_arg, geosite_shunt_arg = "", ""
local t = uci:get_all(appname, TCP_NODE)
local default_node_id = t["default_node"] or "_direct"
uci:foreach(appname, "shunt_rules", function(s)
local _node_id = t[s[".name"]]
if _node_id and _node_id ~= "_blackhole" 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:") then
if line:find("geosite:") then
line = string.match(line, ":([^:]+)$")
if _node_id == "_direct" then
geosite_white_arg = geosite_white_arg .. (geosite_white_arg ~= "" and "," or "") .. line
else
geosite_shunt_arg = geosite_shunt_arg .. (geosite_shunt_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(white_domain, line, lookup_white_domain)
else
insert_unique(shunt_domain, line, lookup_shunt_domain)
end
end
end
end
end
if _node_id ~= "_direct" then
log(string.format(" - Sing-Box/Xray分流规则(%s)使用分组:%s", s.remarks, REMOTE_GROUP or "默认"))
end
end
end)
if is_file_nonzero(file_white_host) == nil then
if #white_domain > 0 then
local f_out = io.open(file_white_host, "w")
for i = 1, #white_domain do
f_out:write(white_domain[i] .. "\n")
end
f_out:close()
end
end
if is_file_nonzero(file_shunt_host) == nil then
if #shunt_domain > 0 then
local f_out = io.open(file_shunt_host, "w")
for i = 1, #shunt_domain do
f_out:write(shunt_domain[i] .. "\n")
end
f_out:close()
end
end
if USE_GFW_LIST == "1" and CHN_LIST == "0" and USE_GEOVIEW == "1" and api.is_finded("geoview") then --仅GFW模式解析geosite
local return_white, return_shunt
if geosite_white_arg ~= "" then
return_white = get_geosite(geosite_white_arg, file_white_host)
end
if geosite_shunt_arg ~= "" then
return_shunt = get_geosite(geosite_shunt_arg, file_shunt_host)
end
if (return_white == nil or return_white == 0) and (return_shunt == nil or return_shunt == 0) then
log(" * 解析[分流节点] Geosite 完成")
else
log(" * 解析[分流节点] Geosite 失败!")
end
end
if is_file_nonzero(file_white_host) then
local domain_set_name = "passwall-whitehost"
tmp_lines = {
string.format("domain-set -name %s -file %s", domain_set_name, file_white_host)
}
local domain_rules_str = string.format('domain-rules /domain-set:%s/ %s', domain_set_name, LOCAL_GROUP and "-nameserver " .. LOCAL_GROUP or "")
if USE_DIRECT_LIST == "1" then
local sets = {
"#4:" .. setflag .. "psw_white",
"#6:" .. setflag .. "psw_white6"
}
domain_rules_str = domain_rules_str .. " " .. set_type .. " " .. table.concat(sets, ",")
else
local sets = {
"#4:" .. setflag .. "psw_shunt",
"#6:" .. setflag .. "psw_shunt6"
}
domain_rules_str = domain_rules_str .. " " .. set_type .. " " .. table.concat(sets, ",")
end
domain_rules_str = domain_rules_str .. (LOCAL_EXTEND_ARG ~= "" and " " .. LOCAL_EXTEND_ARG or "")
table.insert(tmp_lines, domain_rules_str)
insert_array_after(config_lines, tmp_lines, "#--4")
end
if is_file_nonzero(file_shunt_host) then
local domain_set_name = "passwall-shuntlist"
tmp_lines = {
string.format("domain-set -name %s -file %s", domain_set_name, file_shunt_host)
}
local domain_rules_str = string.format('domain-rules /domain-set:%s/ -nameserver %s', domain_set_name, REMOTE_GROUP)
domain_rules_str = domain_rules_str .. " -speed-check-mode none"
domain_rules_str = domain_rules_str .. " -no-serve-expired"
local sets = {
"#4:" .. setflag .. "psw_shunt"
}
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_shunt6")
domain_rules_str = domain_rules_str .. " -address -6 -d no " .. set_type .. " " .. table.concat(sets, ",")
end
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))
end
fs.symlink(TMP_CONF_FILE, SMARTDNS_CONF)
sys.call(string.format('echo "conf-file %s" >> /etc/smartdns/custom.conf', string.gsub(SMARTDNS_CONF, appname, appname .. "*")))
log(" - SmartDNS已作为Dnsmasq上游,如果你自行配置了错误的DNS流程,将会导致域名(直连/代理域名)分流失效!!!")
File diff suppressed because it is too large. Load diff
+51
View File
@@ -0,0 +1,51 @@
#!/bin/sh
# dhcp.leases to hosts
. /usr/share/passwall/utils.sh
LOCK_FILE=${LOCK_PATH}/${CONFIG}_lease2hosts.lock
LEASE_FILE="/tmp/dhcp.leases"
HOSTS_FILE="$TMP_PATH2/dhcp-hosts"
TMP_FILE="/tmp/dhcp-hosts.tmp"
exec 99>"$LOCK_FILE"
flock -n 99
if [ "$?" != 0 ]; then
exit 0
fi
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
if [ -f "$LEASE_FILE" ]; then
awk 'NF >= 4 && $4 != "*" {print $3" "$4}' "$LEASE_FILE" | sort > "$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
fi
sleep 60
done 2>/dev/null
+59
View File
@@ -0,0 +1,59 @@
#!/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"
sleep 58s
last_cleanup_date=$(date +%Y%m%d)
while [ 1 -eq 1 ]; do
[ -f "$LOCK_FILE" ] && {
sleep 6s
continue
}
touch $LOCK_FILE
for file in "$TMP_SCRIPT_FUNC_PATH"/*; do
[ -f "$file" ] || continue
IFS= read -r cmd < "$file"
[ -z "$cmd" ] && continue
cmd_check=$(printf '%s' "$cmd" | sed 's/>.*$//;s/[[:space:]]*$//')
case "$cmd_check" in
*dns2socks*) cmd_check=${cmd_check//:/ } ;;
esac
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
if ! busybox pgrep -f "$cmd_check" >/dev/null; then
restart_count=$((restart_count + 1))
echo "$restart_count" > "$stats_file"
#echo "${cmd} 进程挂掉,重启" >> /tmp/log/passwall.log
sh -c "nohup $cmd 2>&1 &"
sleep 1
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
rm -f $LOCK_FILE
sleep 58s
done
File diff suppressed because it is too large. Load diff
+802
View File
@@ -0,0 +1,802 @@
#!/usr/bin/lua
local api = require ("luci.passwall.api")
local name = api.appname
local uci = api.uci
local sys = api.sys
local jsonc = api.jsonc
local fs = api.fs
local arg1 = arg[1]
local arg2 = arg[2]
local arg3 = arg[3]
local nftable_name = "inet passwall"
local rule_path = "/usr/share/" .. name .. "/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(name, "@global_rules[0]", "gfwlist_url") or {"https://fastly.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/gfw.txt"}
local chnroute_url = uci:get(name, "@global_rules[0]", "chnroute_url") or {"https://ispip.clang.cn/all_cn.txt"}
local chnroute6_url = uci:get(name, "@global_rules[0]", "chnroute6_url") or {"https://ispip.clang.cn/all_cn_ipv6.txt"}
local chnlist_url = uci:get(name, "@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(name, "@global_rules[0]", "geoip_url") or "https://github.com/Loyalsoldier/geoip/releases/latest/download/geoip.dat"
local geosite_url = uci:get(name, "@global_rules[0]", "geosite_url") or "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat"
local asset_location = uci:get(name, "@global_rules[0]", "v2ray_location_asset") or "/usr/share/v2ray/"
local geo2rule = uci:get(name, "@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",
'-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
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 geosite_path = asset_location .. "geosite.dat"
local geoip_path = asset_location .. "geoip.dat"
local file_path = (rule_type == "domain") and geosite_path or geoip_path
local geo_arg
if rule_type == "domain" then
if rule_name == "gfwlist" then
geo_arg = "-type geosite -list gfw"
else
geo_arg = "-type geosite -list cn"
end
elseif rule_type == "ip4" then
geo_arg = "-type geoip -list cn -ipv6=false"
elseif rule_type == "ip6" then
geo_arg = "-type geoip -list cn -ipv4=false"
end
local cmd = string.format(bin .. " -input '%s' %s -lowmem=true -output '%s'", file_path, geo_arg, out_path)
sys.exec(cmd)
local local_file_size = tonumber(fs.stat(out_path, "size") or 0)
if local_file_size == 0 then
os.remove(out_path)
log(rule_name .. " 生成失败,请确保 Geo 文件正确且包含目标规则。")
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
os.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
os.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 = sys.exec(string.format("md5sum %s 2>/dev/null | awk '{print $1}'", rule_final_path)):gsub("\n", "")
local new_md5 = sys.exec(string.format("md5sum %s 2>/dev/null | awk '{print $1}'", file_tmp)):gsub("\n", "")
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 .. " 版本一致,无需更新。")
os.remove(file_tmp)
end
else
log(rule_name .. " 更新失败(部分或全部资源无法下载)。")
os.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, down_filename .. ".sha256sum")
local sha_path = tmp_path .. ".sha256sum"
local function verify_sha256(sha_file)
return sys.call("sha256sum -c " .. sha_file .. " > /dev/null 2>&1") == 0
end
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()
if content then
content = content:gsub("(%x+)%s+.+", "%1 " .. tmp_path)
f = io.open(sha_path, "w")
if f then
f:write(content)
f:close()
end
end
end
if fs.access(asset_path) then
sys.call(string.format("cp -f %s %s", asset_path, tmp_path))
if verify_sha256(sha_path) then
log(geo_type .. " 版本一致,无需更新。")
return 0
end
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 .. " 下载文件过程出错,尝试重新下载。")
os.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 verify_sha256(sha_path) 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 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(name, "@global_rules[0]", "gfwlist_update") or "1"
chnroute_update = uci:get(name, "@global_rules[0]", "chnroute_update") or "1"
chnroute6_update = uci:get(name, "@global_rules[0]", "chnroute6_update") or "1"
chnlist_update = uci:get(name, "@global_rules[0]", "chnlist_update") or "1"
geoip_update = uci:get(name, "@global_rules[0]", "geoip_update") or "1"
geosite_update = uci:get(name, "@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
os.exit(0)
end
local function check_instance(action)
local rule_lock = "/var/lock/" .. name .. "_rule_update.lock"
local sub_lock = "/var/lock/" .. name .. "_subscribe.lock"
if action == "start" then
math.randomseed(os.time() + math.floor(os.clock() * 1000))
api.nixio.nanosleep(0, math.random(100, 1000) * 1000000)
if fs.access(rule_lock) then
log("有[规则更新]实例正在运行,请稍后再试...\n")
os.exit(0)
else
luci.sys.call("touch " .. rule_lock)
end
elseif action == "end" then
luci.sys.call("rm -f " .. rule_lock)
return
end
if fs.access(sub_lock) then
log("[订阅]实例正在运行,[规则更新]进入队列等待...\n")
end
while fs.access(sub_lock) do
api.nixio.nanosleep(2, 0)
end
end
check_instance("start")
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)
os.remove("/tmp/" .. name .. ".dat")
os.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
chnroute_update, chnroute6_update, gfwlist_update, chnlist_update = "1", "1", "1", "1"
if geoip_update_ok then
if fs.access(asset_location .. "geoip.dat") then
safe_call(fetch_chnroute, "生成chnroute发生错误...")
safe_call(fetch_chnroute6, "生成chnroute6发生错误...")
else
log("geoip.dat 文件不存在,跳过规则生成。")
end
end
if geosite_update_ok then
if fs.access(asset_location .. "geosite.dat") then
safe_call(fetch_gfwlist, "生成gfwlist发生错误...")
safe_call(fetch_chnlist, "生成chnlist发生错误...")
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(name, "@global_rules[0]", "gfwlist_update", gfwlist_update)
uci:set(name, "@global_rules[0]", "chnroute_update", chnroute_update)
uci:set(name, "@global_rules[0]", "chnroute6_update", chnroute6_update)
uci:set(name, "@global_rules[0]", "chnlist_update", chnlist_update)
uci:set(name, "@global_rules[0]", "geoip_update", geoip_update)
uci:set(name, "@global_rules[0]", "geosite_update", geosite_update)
api.uci_save(uci, name, true)
end
if reboot == 1 then
if arg3 == "cron" then
if not fs.access("/var/lock/" .. name .. ".lock") then
sys.call("touch /tmp/lock/" .. name .. "_cron.lock")
end
end
log("重启服务,应用新的规则。")
uci:set(name, "@global[0]", "flush_set", "1")
api.uci_save(uci, name, true, true)
end
log("规则更新完毕...\n")
check_instance("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,30 @@
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
@@ -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
marscdn.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,13 @@
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
@@ -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,194 @@
#!/bin/sh
. /usr/share/passwall/utils.sh
APP_FILE=${APP_PATH}/app.sh
flag=0
check_process() {
while busybox pgrep -af "${CONFIG}/" | grep -E 'app\.sh.*(start|stop)|nftables\.sh|iptables\.sh|subscribe\.lua' >/dev/null; do
sleep 6s
done
}
test_url() {
local url=$1
local try=1
[ -n "$2" ] && try=$2
local timeout=2
[ -n "$3" ] && timeout=$3
local extra_params=$4
if /usr/bin/curl --help all | grep -q "\-\-retry-all-errors"; then
extra_params="--retry-all-errors ${extra_params}"
fi
local status=$(/usr/bin/curl -I -o /dev/null -skL ${extra_params} --connect-timeout ${timeout} --retry ${try} -w %{http_code} "$url")
case "$status" in
204)
status=200
;;
esac
echo $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
[ $? -eq 0 ] && {
result=1
}
fi
fi
echo $result
}
test_node() {
local node_id=$1
local _type=$(echo $(config_n_get ${node_id} type) | tr 'A-Z' 'a-z')
[ -n "${_type}" ] && {
check_process
local _tmp_port=$(get_new_port 48800 tcp,udp)
NO_REC_PROCESS=1 $APP_FILE run_socks flag="test_node_${node_id}" node=${node_id} bind=127.0.0.1 socks_port=${_tmp_port} config_file=test_node_${node_id}.json
sleep 2s
local curlx="socks5h://127.0.0.1:${_tmp_port}"
local _proxy_status=$(test_url "${probe_url}" ${retry_num} ${connect_timeout} "-x $curlx")
# 结束 SS 插件进程
local pid_file="/tmp/etc/${CONFIG}/test_node_${node_id}_plugin.pid"
[ -s "$pid_file" ] && kill -9 "$(head -n 1 "$pid_file")" >/dev/null 2>&1
busybox pgrep -af "test_node_${node_id}" | awk '! /socks_auto_switch\.sh/{print $1}' | xargs kill -9 >/dev/null 2>&1
rm -rf /tmp/etc/${CONFIG}/test_node_${node_id}*.*
if [ "${_proxy_status}" -eq 200 ]; then
return 0
fi
}
return 1
}
test_auto_switch() {
flag=$((flag + 1))
local b_nodes=$1
local now_node=$2
[ -z "$now_node" ] && {
if [ -n "$(get_cache_var "socks_${id}")" ]; then
now_node=$(get_cache_var "socks_${id}")
else
#echolog "Socks切换检测:未知错误"
return 1
fi
}
[ $flag -le 1 ] && {
main_node=$now_node
}
local status=$(test_proxy)
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)]】正常,切换到主节点!"
NO_REC_PROCESS=1 $APP_FILE socks_node_switch flag=${id} new_node=${main_node}
[ $? -eq 0 ] && {
echolog "Socks切换检测:端口[${socks_port}] 节点切换完毕!"
}
return 0
}
fi
if [ "$status" = "0" ]; then
#echolog "Socks切换检测:${id}【$(config_n_get $now_node type):[$(config_n_get $now_node remarks)]】正常。"
return 0
elif [ "$status" = "1" ]; then
local new_node msg
if [ "$backup_node_num" -gt 1 ]; then
# 有多个后备节点时
local first_node found node
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"
if [ "$new_node" = "$main_node" ]; then
msg="切换到主节点检测!"
else
msg="切换到$([ "$now_node" = "$main_node" ] && echo 备用节点 || echo 下一个备用节点)检测!"
fi
else
# 只有一个后备节点时,与主节点轮询
new_node=$([ "$now_node" = "$main_node" ] && echo "$b_nodes" || echo "$main_node")
msg="切换到$([ "$now_node" = "$main_node" ] && echo 备用节点 || echo 主节点)检测!"
fi
echolog "Socks切换检测:端口[${socks_port}]【$(config_n_get $now_node type):[$(config_n_get $now_node remarks)]】异常,$msg"
test_node ${new_node}
if [ $? -eq 0 ]; then
check_process
echolog "Socks切换检测:端口[${socks_port}]【$(config_n_get $new_node type):[$(config_n_get $new_node remarks)]】正常,切换到此节点!"
NO_REC_PROCESS=1 $APP_FILE socks_node_switch flag=${id} new_node=${new_node}
[ $? -eq 0 ] && {
echolog "Socks切换检测:端口[${socks_port}] 节点切换完毕!"
}
return 0
else
test_auto_switch "${b_nodes}" ${new_node}
fi
fi
}
start() {
id=$1
LOCK_FILE=${LOCK_PATH}/${CONFIG}_socks_auto_switch_${id}.lock
LOG_EVENT_FILTER=$(uci -q get "${CONFIG}.global[0].log_event_filter" 2>/dev/null)
LOG_EVENT_CMD=$(uci -q get "${CONFIG}.global[0].log_event_cmd" 2>/dev/null)
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" ] && {
[ -z "$(echo $backup_node | grep -F "$main_node")" ] && backup_node="${backup_node} ${main_node}"
}
fi
else
echolog " - Socks切换检测:端口[${socks_port}] 后备节点数量:0"
return
fi
while [ -n "$backup_node" ]; do
[ -f "$LOCK_FILE" ] && {
sleep 6s
continue
}
check_process
touch $LOCK_FILE
test_auto_switch "$backup_node"
rm -f $LOCK_FILE
sleep ${delay}
done
}
start $@
File diff suppressed because it is too large. Load diff
+71
View File
@@ -0,0 +1,71 @@
#!/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
if [ "$?" != 0 ]; then
exit 0
fi
while true
do
if [ "$CFG_UPDATE_INT" -ne 0 ]; then
restart_week_mode=$(config_t_get global_delay restart_week_mode)
restart_interval_mode=$(config_t_get global_delay 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 > /dev/null 2>&1 & }
}
fi
rules_update_week_mode=$(config_t_get global_rules update_week_mode)
rules_update_interval_mode=$(config_t_get global_rules 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 & }
}
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" ]; then
cfgid=$(uci show ${CONFIG}.$item | head -n 1 | cut -d '.' -sf 2 | cut -d '=' -sf 1)
remark=$(config_n_get $item remark)
sub_update_interval_mode=$(config_n_get $item update_interval_mode)
echo "$cfgid" >> $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=$(echo -n $(cat ${TMP_SUB_PATH}/${name}) | sed 's# #,#g')
[ "$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 & }
}
done
rm -rf $TMP_SUB_PATH
}
fi
CFG_UPDATE_INT=$(expr "$CFG_UPDATE_INT" + 10)
sleep 600
done 2>/dev/null
+84
View File
@@ -0,0 +1,84 @@
#!/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
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} --retry ${try} -w %{http_code} "$url")
case "$status" in
204|\
200)
status=200
;;
esac
echo $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() {
result=0
local node_id=$1
local _type=$(echo $(config_n_get ${node_id} type) | tr 'A-Z' 'a-z')
[ -n "${_type}" ] && {
if [ "${_type}" == "socks" ]; then
local _address=$(config_n_get ${node_id} address)
local _port=$(config_n_get ${node_id} port)
[ -n "${_address}" ] && [ -n "${_port}" ] && {
local curlx="socks5h://${_address}:${_port}"
local _username=$(config_n_get ${node_id} username)
local _password=$(config_n_get ${node_id} password)
[ -n "${_username}" ] && [ -n "${_password}" ] && curlx="socks5h://${_username}:${_password}@${_address}:${_port}"
}
else
local _tmp_port=$(get_new_port 48900 tcp,udp)
NO_REC_PROCESS=1 /usr/share/${CONFIG}/app.sh run_socks flag="url_test_${node_id}" node=${node_id} bind=127.0.0.1 socks_port=${_tmp_port} config_file=url_test_${node_id}.json
local curlx="socks5h://127.0.0.1:${_tmp_port}"
fi
sleep 2s
local probeUrl=$(config_t_get global_other url_test_url https://www.google.com/generate_204)
result=$(curl --connect-timeout 3 --max-time 5 -o /dev/null -I -skL -w "%{http_code}:%{time_pretransfer}" -x ${curlx} "${probeUrl}")
# 结束 SS 插件进程
local pid_file="/tmp/etc/${CONFIG}/url_test_${node_id}_plugin.pid"
[ -s "$pid_file" ] && kill -9 "$(head -n 1 "$pid_file")" >/dev/null 2>&1
busybox pgrep -af "url_test_${node_id}" | awk '! /test\.sh/{print $1}' | xargs kill -9 >/dev/null 2>&1
rm -rf /tmp/etc/${CONFIG}/*url_test_${node_id}*.*
}
echo $result
}
arg1=$1
shift
case $arg1 in
test_url)
test_url $@
;;
url_test_node)
url_test_node $@
;;
esac
+593
View File
@@ -0,0 +1,593 @@
#!/bin/sh
# Copyright (C) 2022-2025 xiaorouji
# Copyright (C) 2026 Openwrt-Passwall Organization
CONFIG=passwall
APP_PATH=/usr/share/${CONFIG}
TMP_PATH=/tmp/etc/${CONFIG}
TMP_PATH2=${TMP_PATH}_tmp
LOCK_PATH=/tmp/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
. /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)
echo "${ret:=$2}"
}
config_n_get() {
local ret=$(uci -q get "${CONFIG}.${1}.${2}" 2>/dev/null)
echo "${ret:=$3}"
}
config_t_get() {
local index=${4:-0}
local ret=$(uci -q get "${CONFIG}.@${1}[${index}].${2}" 2>/dev/null)
echo "${ret:=${3}}"
}
config_t_set() {
local index=${4:-0}
local ret=$(uci -q set "${CONFIG}.@${1}[${index}].${2}=${3}" 2>/dev/null)
}
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_t_get global_rules v2ray_location_asset)"
geoip_path="${geoip_path%*/}/geoip.dat"
local bin="$(first_type $(config_t_get global_app 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
isip=$(echo $host | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}")
if [ -n "$isip" ]; then
ip=$(echo "$host" | tr -d '[]')
fi
else
isip=$(echo $host | grep -E "([0-9]{1,3}[\.]){3}[0-9]{1,3}")
[ -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 val2
val2=$(echo "$__host" | sed -n '
s/^[^#]*[#]\([0-9]*\)$/\1/p; t;
s/^\(\[[^]]*\]\)[:]\([0-9]*\)$/\2/p; t;
s/^.*[:#]\([0-9]*\)$/\1/p
')
if [ -n "${__ucipriority}" ]; then
val2=$(config_n_get ${__host} port "${val2}")
val1=$(config_n_get ${__host} address "${__host%%${val2:+[:#]${val2}*}}")
else
val1="${__host%%${val2:+[:#]${val2}*}}"
fi
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}"
## Remove protocol part of url ##
f="${f##http://}"
f="${f##https://}"
f="${f##ftp://}"
f="${f##sftp://}"
## Remove username and/or username:password part of URL ##
f="${f##*:*@}"
f="${f##*@}"
## Remove rest of urls ##
f="${f%%/*}"
echo "${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
}
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
}
get_first_dns() {
local __hosts_val=${1}; shift 1
__first() {
[ -z "${2}" ] && return 0
echo "${2}#${3}"
return 1
}
hosts_foreach "${__hosts_val}" __first "$@"
}
get_last_dns() {
local __hosts_val=${1}; shift 1
local __first __last
__every() {
[ -z "${2}" ] && return 0
__last="${2}#${3}"
__first=${__first:-${__last}}
}
hosts_foreach "${__hosts_val}" __first "$@"
[ "${__first}" = "${__last}" ] || echo "${__last}"
}
normalize_dns() {
local s="$1"
local addr port
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%\]}"
port=""
;;
*)
addr="$s"
port=""
;;
esac
if [ -n "$port" ]; then
echo "${addr}#${port}"
else
echo "$addr"
fi
}
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"
local protocol=$(echo "$2" | tr 'A-Z' 'a-z')
local LOCK_FILE="${LOCK_PATH}/${CONFIG}_get_prot.lock"
while ! mkdir "$LOCK_FILE" 2>/dev/null; do
sleep 0.05
done
if [ "$port" = "auto" ]; then
local now last_time diff last_port
now=$(date +%s 2>/dev/null)
last_time=$(get_cache_var "last_get_new_port_time")
if [ -n "$now" ] && [ -n "$last_time" ]; then
diff=$(expr "$now" - "$last_time")
[ "$diff" -lt 0 ] && diff=$(expr 0 - "$diff")
else
diff=999
fi
if [ "$diff" -gt 10 ]; then
port=$default_start_port
else
last_port=$(get_cache_var "last_get_new_port_auto")
if [ -n "$last_port" ]; then
port=$(expr "$last_port" + 1)
else
port=$default_start_port
fi
fi
fi
[ "$port" -lt $min_port ] || [ "$port" -gt $max_port ] && port=$default_start_port
local start_port="$port"
while :; do
if [ "$(check_port_exists "$port" "$protocol")" = 0 ]; then
break
fi
port=$(expr "$port" + 1)
if [ "$port" -gt $max_port ]; then
port=$min_port
fi
[ "$port" = "$start_port" ] && {
rmdir "$LOCK_FILE" 2>/dev/null
return 1
}
done
if [ "$1" = "auto" ]; then
set_cache_var "last_get_new_port_auto" "$port"
[ -n "$now" ] && set_cache_var "last_get_new_port_time" "$now"
fi
rmdir "$LOCK_FILE" 2>/dev/null
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
}
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})")
}
set_cache_var() {
local key="${1}"
shift 1
local val="$@"
[ -n "${key}" ] && [ -n "${val}" ] && {
[ ! -d $TMP_PATH ] && mkdir -p $TMP_PATH
sed -i "/${key}=/d" $TMP_PATH/var >/dev/null 2>&1
echo "${key}=\"${val}\"" >> $TMP_PATH/var
eval ${key}=\"${val}\"
}
}
get_cache_var() {
local key="${1}"
[ -n "${key}" ] && [ -s "$TMP_PATH/var" ] && {
echo $(cat $TMP_PATH/var | grep "^${key}=" | awk -F '=' '{print $2}' | tail -n 1 | awk -F'"' '{print $2}')
}
}
eval_cache_var() {
[ -s "$TMP_PATH/var" ] && eval $(cat "$TMP_PATH/var")
}
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
route add -host ${ip} gw ${gateway} dev ${device} >/dev/null 2>&1
echo "$ip" >> $TMP_ROUTE_PATH/${device}
echolog " - [${remarks}]添加到接口[${device}]路由表成功!"
else
echolog " - [${remarks}]添加到接口[${device}]路由表失功!原因是找不到[${device}]网关。"
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
}
}
ln_run() {
local file_func=${1}
local ln_name=${2}
local output=${3}
shift 3;
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},无法启动..."
[ "${output}" != "/dev/null" ] && [ -n "$(echo "${output}" | grep -E "default|SOCKS_")" ] && [ "${ln_name}" != "chinadns-ng" ] && {
local persist_log_path=$(config_t_get global persist_log_path)
local sys_log=$(config_t_get global sys_log "0")
}
if [ -z "$persist_log_path" ] && [ "$sys_log" != "1" ]; then
${file_func:-echolog " - ${ln_name}"} "$@" >${output} 2>&1 &
else
case "$output" in
*TCP.log) local protocol="TCP" ;;
*UDP.log) local protocol="UDP" ;;
esac
if [ -n "${persist_log_path}" ]; then
mkdir -p ${persist_log_path}
local log_file=${persist_log_path}/passwall_${protocol}_${ln_name}_$(date '+%F').log
echolog "记录到持久性日志文件:${log_file}"
${file_func:-echolog " - ${ln_name}"} "$@" >> ${log_file} 2>&1 &
sys_log=0
fi
if [ "${sys_log}" = "1" ]; then
echolog "记录 ${ln_name}_${protocol} 到系统日志"
${file_func:-echolog " - ${ln_name}"} "$@" 2>&1 | logger -t PASSWALL_${protocol}_${ln_name} &
fi
fi
[ "$NO_REC_PROCESS" = "1" ] && return
process_count=$(ls $TMP_SCRIPT_FUNC_PATH | wc -l)
process_count=$((process_count + 1))
echo "${file_func:-echolog " - ${ln_name}"} $@ >${output}" > $TMP_SCRIPT_FUNC_PATH/$process_count
}
is_socks_wrap() {
case "$1" in
Socks_*) return 0 ;;
*) return 1 ;;
esac
}
kill_all() {
kill -9 $(pidof "$@") >/dev/null 2>&1
}
get_subscribe_host(){
local line
uci show "${CONFIG}" | grep "=subscribe_list" | while read -r line; do
local section="$(echo "$line" | cut -d '.' -sf 2 | cut -d '=' -sf 1)"
local url="$(config_n_get $section url)"
[ -n "$url" ] || continue
url="$(host_from_url "$url")"
echo "$url"
done
}
gen_lanlist() {
cat $RULES_PATH/lanlist_ipv4 | tr -s '\n' | grep -v "^#"
}
gen_lanlist_6() {
cat $RULES_PATH/lanlist_ipv6 | tr -s '\n' | grep -v "^#"
}
get_wan_ips() {
local family="$1"
local NET_ADDR
local iface
local INTERFACES=$(ubus call network.interface dump | jsonfilter -e \
'@.interface[!(@.interface ~ /lan/) && !(@.l3_device ~ /\./) && @.route[0]].interface')
for iface in $INTERFACES; do
local addr
if [ "$family" = "ip6" ]; then
network_get_ipaddr6 addr "$iface"
case "$addr" in
""|fe80*) continue ;;
esac
else
network_get_ipaddr addr "$iface"
case "$addr" in
""|"0.0.0.0") continue ;;
esac
fi
case " $NET_ADDR " in
*" $addr "*) ;;
*) NET_ADDR="${NET_ADDR:+$NET_ADDR }$addr" ;;
esac
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
}
@@ -0,0 +1,11 @@
{
"luci-app-passwall": {
"description": "Grant UCI access for luci-app-passwall",
"read": {
"uci": [ "passwall", "passwall_server" ]
},
"write": {
"uci": [ "passwall", "passwall_server" ]
}
}
}
@@ -0,0 +1,4 @@
{
"config": "passwall_server",
"init": "passwall_server"
}
@@ -0,0 +1,4 @@
{
"config": "passwall",
"init": "passwall"
}