🎉 Sync 2026-09-15 12:29:06
Merge-upstream / merge (push) Canceled after 0s

This commit is contained in:
github-actions[bot] committed 2026-09-15 12:29:06 +08:00
1 parent 25adce2488
commit d34867614a
13 files changed
+638 -90

No files matched your search

+1 -1
View File
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall2
PKG_VERSION:=26.9.12
PKG_RELEASE:=121
PKG_RELEASE:=122
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \
@@ -294,7 +294,7 @@ end
function get_now_use_node()
local e = {}
local node = api.get_cache_var("ACL_GLOBAL_node")
local node = api.get_cache_var(("ACL_${flag}_node"):gsub("${flag}", "acl_default"))
if node then
e["global"] = node
end
+9 -6
View File
@@ -292,12 +292,15 @@ end
function curl_proxy(url, file, args)
-- Use the proxy
local socks_server = get_cache_var("GLOBAL_SOCKS_server")
if socks_server and socks_server ~= "" then
if not args then args = {} end
local tmp_args = clone(args)
tmp_args[#tmp_args + 1] = "-x socks5h://" .. socks_server
return curl_base(url, file, tmp_args)
local socks_port = get_cache_var(("ACL_${flag}_node_socks_port"):gsub("${flag}", "acl_default"))
if socks_port then
local socks_server = "127.0.0.1:%s" % socks_port
if socks_server and socks_server ~= "" then
if not args then args = {} end
local tmp_args = clone(args)
tmp_args[#tmp_args + 1] = "-x socks5h://" .. socks_server
return curl_base(url, file, tmp_args)
end
end
return nil, nil
end
@@ -111,20 +111,21 @@ function gen_outbound(flag, node, tag, proxy_table)
end
local remarks = node.remarks
local proxy_tag = nil
local fragment = nil
local record_fragment = nil
local run_socks_instance = true
local proxy_tag, fragment, record_fragment
if proxy_table ~= nil and type(proxy_table) == "table" then
proxy_tag = proxy_table.tag or nil
fragment = (proxy_table.fragment and node.protocol ~= "naive" and not node.hysteria2_realms) and true or nil
record_fragment = (proxy_table.record_fragment and node.protocol ~= "naive" and not node.hysteria2_realms) and true or nil
run_socks_instance = proxy_table.run_socks_instance
end
if node.type ~= "sing-box" then
local new_port
if run_socks_instance then
local run_socks_instance = true
if NO_RUN then
TMP_PORT = TMP_PORT and TMP_PORT + 1 or 3001
new_port = TMP_PORT
run_socks_instance = nil
else
local relay_port = (proxy_tag and node.port) and tostring(node.port) or ""
if relay_port == "" then
local cache = api.get_socks_port_by_cache(node_id)
@@ -1181,8 +1182,7 @@ function gen_config(var)
local remote_dns_client_ip = var["remote_dns_client_ip"]
local remote_rewrite_ttl = var["remote_rewrite_ttl"] or "30"
local dns_cache = var["dns_cache"]
local tags = var["tags"]
local no_run = var["no_run"]
NO_RUN = var["no_run"]
local dns_domain_rules = {}
local dns = {}
@@ -1405,7 +1405,7 @@ function gen_config(var)
end
end
if is_new_ut_node then
local outboundTag = gen_outbound_get_tag(flag, ut_node_id, ut_node_tag, { fragment = singbox_settings.fragment == "1" or nil, record_fragment = singbox_settings.record_fragment == "1" or nil, run_socks_instance = not no_run })
local outboundTag = gen_outbound_get_tag(flag, ut_node_id, ut_node_tag, { fragment = singbox_settings.fragment == "1" or nil, record_fragment = singbox_settings.record_fragment == "1" or nil })
if outboundTag then
valid_nodes[#valid_nodes + 1] = outboundTag
end
@@ -1508,7 +1508,6 @@ function gen_config(var)
to_node.port = new_port
to_outbound = gen_outbound(node[".name"], to_node, tag, {
tag = tag,
run_socks_instance = not no_run
})
else
to_outbound = gen_outbound(node[".name"], to_node)
@@ -1607,7 +1606,6 @@ function gen_config(var)
local proxy_table = {
fragment = singbox_settings.fragment == "1",
record_fragment = singbox_settings.record_fragment == "1",
run_socks_instance = not no_run,
}
local preproxy_node_id = node[rule_name .. "_proxy_tag"]
if preproxy_node_id == _node_id then preproxy_node_id = nil end
@@ -2284,7 +2282,7 @@ function gen_config(var)
routing_mark = 255,
})
for index, value in ipairs(config.outbounds) do
if not value["_flag_proxy_tag"] and not value.detour and value["_id"] and value.server and value.server_port and not no_run then
if not value["_flag_proxy_tag"] and not value.detour and value["_id"] and value.server and value.server_port and not NO_RUN then
sys.call(string.format("echo '%s' >> %s", value["_id"], api.TMP_PATH .. "/direct_node_list"))
end
if not value.detour and not value.bind_interface and value.server then
@@ -2427,7 +2425,7 @@ if arg[1] then
var = jsonc.parse(arg[2])
end
print(func(var))
if (next(GEO_VAR.SITE_TAGS) or next(GEO_VAR.IP_TAGS)) and not no_run then
if (next(GEO_VAR.SITE_TAGS) or next(GEO_VAR.IP_TAGS)) and not NO_RUN then
convert_geofile()
end
end
@@ -12,7 +12,7 @@ local GLOBAL = {
local xray_version = api.get_app_version("xray")
local xray_min_version = "26.3.27"
local xray_min_version = "26.7.11"
local function get_domain_excluded()
local path = "/usr/share/passwall2/domains_excluded"
@@ -58,21 +58,21 @@ function gen_outbound(flag, node, tag, proxy_table)
end
local remarks = node.remarks
local proxy_tag = nil
local dialer_proxy_tag = nil
local fragment = nil
local noise = nil
local run_socks_instance = true
local proxy_tag, dialer_proxy_tag, fragment, noise
if proxy_table ~= nil and type(proxy_table) == "table" then
proxy_tag = proxy_table.tag or nil
fragment = (proxy_table.fragment and not node.hysteria2_realms) and true or nil
noise = (proxy_table.noise and not node.hysteria2_realms) and true or nil
run_socks_instance = proxy_table.run_socks_instance
end
if node.type ~= "Xray" then
local new_port
if run_socks_instance then
local run_socks_instance = true
if NO_RUN then
TMP_PORT = TMP_PORT and TMP_PORT + 1 or 3001
new_port = TMP_PORT
run_socks_instance = nil
else
local relay_port = (proxy_tag and node.port) and tostring(node.port) or ""
if relay_port == "" then
local cache = api.get_socks_port_by_cache(node_id)
@@ -106,7 +106,7 @@ function gen_outbound(flag, node, tag, proxy_table)
if new_port then
node = {}
node.protocol = "socks"
node.transport = "tcp"
node.transport = "raw"
node.address = "127.0.0.1"
node.port = new_port
node.stream_security = "none"
@@ -181,7 +181,7 @@ function gen_outbound(flag, node, tag, proxy_table)
} or nil,
dialerProxy = dialer_proxy_tag,
},
[(api.compare_versions(xray_version, "<", "26.7.11")) and "network" or "method"] = node.transport, -- Todo: Remove version check and "network"
method = node.transport,
security = node.stream_security,
tlsSettings = (node.stream_security == "tls") and {
serverName = node.tls_serverName,
@@ -665,7 +665,7 @@ function gen_config_server(node)
outbound_node_t = {
type = node.type,
protocol = node.outbound_node:gsub("_", ""),
transport = "tcp",
transport = "raw",
address = node.outbound_node_address,
port = node.outbound_node_port,
username = (node.outbound_node_username and node.outbound_node_username ~= "") and node.outbound_node_username or nil,
@@ -696,7 +696,7 @@ function gen_config_server(node)
protocol = node.protocol,
settings = settings,
streamSettings = {
[(api.compare_versions(xray_version, "<", "26.7.11")) and "network" or "method"] = node.transport, -- Todo: Remove version check and "network"
method = node.transport,
security = "none",
tlsSettings = ("1" == node.tls) and {
disableSystemRoot = false,
@@ -877,9 +877,6 @@ function gen_config_server(node)
config.outbounds[index][k] = nil
end
end
if value.protocol == "freedom" and api.compare_versions(xray_version, "<", "26.5.3") then -- Todo is to remove it
value.settings = nil
end
end
return config
@@ -921,7 +918,7 @@ function gen_config(var)
local remote_dns_query_strategy = var["remote_dns_query_strategy"]
local remote_dns_detour = var["remote_dns_detour"]
local dns_cache = var["dns_cache"]
local no_run = var["no_run"]
NO_RUN = var["no_run"]
local dns_domain_rules = {}
local dns = {}
@@ -1046,7 +1043,7 @@ function gen_config(var)
protocol = "socks",
address = "127.0.0.1",
port = section.port,
transport = "tcp",
transport = "raw",
stream_security = "none"
}
end
@@ -1104,7 +1101,7 @@ function gen_config(var)
end
end
if is_new_blc_node then
local outboundTag = gen_outbound_get_tag(flag, blc_node_id, blc_node_tag, { fragment = xray_settings.fragment == "1" or nil, noise = xray_settings.noise == "1" or nil, run_socks_instance = not no_run })
local outboundTag = gen_outbound_get_tag(flag, blc_node_id, blc_node_tag, { fragment = xray_settings.fragment == "1" or nil, noise = xray_settings.noise == "1" or nil })
if outboundTag then
valid_nodes[#valid_nodes + 1] = outboundTag
end
@@ -1133,7 +1130,7 @@ function gen_config(var)
local fallback_node = get_node_by_id(fallback_node_id)
if fallback_node then
if fallback_node.protocol ~= "_balancing" then
local outboundTag = gen_outbound_get_tag(flag, fallback_node, fallback_node_id, { fragment = xray_settings.fragment == "1" or nil, noise = xray_settings.noise == "1" or nil, run_socks_instance = not no_run })
local outboundTag = gen_outbound_get_tag(flag, fallback_node, fallback_node_id, { fragment = xray_settings.fragment == "1" or nil, noise = xray_settings.noise == "1" or nil })
if outboundTag then
fallback_node_tag = outboundTag
end
@@ -1265,7 +1262,6 @@ function gen_config(var)
})
to_outbound = gen_outbound(node[".name"], to_node, to_node[".name"], {
tag = to_node[".name"],
run_socks_instance = not no_run
})
else
to_outbound = gen_outbound(node[".name"], to_node)
@@ -1336,9 +1332,9 @@ function gen_config(var)
interface = node.iface
}
},
settings = (api.compare_versions(xray_version, ">", "26.4.25")) and { -- Todo: Remove version check
settings = {
finalRules = {{ action = "allow" }}
} or nil
}
}
sys.call(string.format("mkdir -p %s && touch %s/%s", api.TMP_IFACE_PATH, api.TMP_IFACE_PATH, node.iface))
end
@@ -1398,7 +1394,6 @@ function gen_config(var)
local proxy_table = {
fragment = xray_settings.fragment == "1",
noise = xray_settings.noise == "1",
run_socks_instance = not no_run,
}
local preproxy_node_id = node[rule_name .. "_proxy_tag"]
if preproxy_node_id == _node_id then preproxy_node_id = nil end
@@ -1750,12 +1745,10 @@ function gen_config(var)
})
local direct_type_dns = {
settings = {
address = direct_dns_udp_server,
port = tonumber(direct_dns_udp_port) or 53,
network = "udp",
nonIPQuery = (api.compare_versions(xray_version, "<", "26.4.25")) and "skip" or nil, -- Todo is to remove it
blockTypes = (api.compare_versions(xray_version, "<", "26.4.25")) and { 65 } or nil, -- Todo is to remove it
rules = (api.compare_versions(xray_version, ">", "26.4.17")) and {
rewriteAddress = direct_dns_udp_server,
rewritePort = tonumber(direct_dns_udp_port) or 53,
rewriteNetwork = "udp",
rules = {
{
qType = "1,28",
action = "hijack"
@@ -1776,11 +1769,10 @@ function gen_config(var)
}
local remote_type_dns = {
settings = {
address = remote_dns_udp_server,
port = tonumber(remote_dns_udp_port) or 53,
network = _remote_dns_proto or "tcp",
nonIPQuery = (api.compare_versions(xray_version, "<", "26.4.25")) and "reject" or nil, -- Todo is to remove it
rules = (api.compare_versions(xray_version, ">", "26.4.17")) and {
rewriteAddress = remote_dns_udp_server,
rewritePort = tonumber(remote_dns_udp_port) or 53,
rewriteNetwork = _remote_dns_proto or "tcp",
rules = {
{
qType = "1,28",
action = "hijack"
@@ -2058,7 +2050,7 @@ function gen_config(var)
local direct_outbound = {
protocol = "freedom",
tag = "direct",
settings = (api.compare_versions(xray_version, ">", "26.4.25")) and { -- Todo: Remove version check
settings = {
finalRules = {{ action = "allow" }}
} or nil,
streamSettings = {
@@ -2086,7 +2078,7 @@ function gen_config(var)
for index, value in ipairs(config.outbounds) do
local s = value.settings
if not value["_flag_proxy_tag"] and value["_id"] and s and not no_run and
if not value["_flag_proxy_tag"] and value["_id"] and s and not NO_RUN and
((s.vnext and s.vnext[1] and s.vnext[1].address and s.vnext[1].port) or
(s.servers and s.servers[1] and s.servers[1].address and s.servers[1].port) or
(s.peers and s.peers[1] and s.peers[1].endpoint) or
@@ -2168,7 +2160,7 @@ function gen_proto_config(var)
local outbound = {
protocol = server_proto,
streamSettings = {
network = "tcp",
method = "raw",
security = "none"
},
settings = {
@@ -2192,10 +2184,12 @@ function gen_proto_config(var)
table.insert(outbounds, {
protocol = "freedom",
tag = "direct",
settings = (api.compare_versions(xray_version, ">", "26.4.25")) and { -- Todo: Remove version check
settings = {
finalRules = {{ action = "allow" }}
} or nil,
sockopt = {mark = 255}
streamSettings = {
sockopt = {mark = 255}
}
})
local config = {
@@ -2204,7 +2198,10 @@ function gen_proto_config(var)
},
inbounds = inbounds,
outbounds = outbounds,
routing = routing
routing = routing,
version = {
min = xray_min_version
}
}
return jsonc.stringify(config, 1)
end
@@ -1,17 +1,12 @@
#!/bin/sh
# Devices without a hardware RTC boot with a wrong system clock: sysfixtime can
# only restore the mtime of the newest file under /etc, so the clock is usually
# hours behind after a cold boot. passwall2 is then started by
# /etc/hotplug.d/iface/98-passwall2 on ifup, which typically happens before NTP
# has corrected the time, and time-sensitive handshakes (VMess AEAD, TLS) fail.
#
# Nothing restarts passwall2 once the clock is corrected, so it stays broken
# until the user restarts it manually. Restart once when ntpd reports that the
# time is valid -- the same approach dnsmasq uses for DNSSEC in
# /etc/hotplug.d/ntp/25-dnsmasqsec.
[ "$ACTION" = "step" ] || exit 0
[ "$ACTION" = "stratum" ] || exit 0
offset=${offset#-}
offset=${offset%.*}
# Skip service restart if the time adjustment is less than 120 seconds.
[ "$offset" -lt 120 ] && exit 0
. /usr/share/passwall2/utils.sh
@@ -23,5 +18,5 @@ NTP_LOCK_FILE="${LOCK_PATH}/${CONFIG}_ntp.lock"
echo $$ > ${NTP_LOCK_FILE}
/etc/init.d/${CONFIG} restart >/dev/null 2>&1 &
logger -p notice -t network -s "${CONFIG}: restart after NTP time became valid"
logger -p notice -t network -s "${CONFIG}: restart after NTP time step (${offset}s)"
}
@@ -856,8 +856,8 @@ acl_node() {
local DNSMASQ_LOCAL_DNS="${LOCAL_DNS:-${AUTO_DNS}}"
[ -n "${DIRECT_DNS_DNSMASQ_SERVER}" ] && DNSMASQ_LOCAL_DNS="${DIRECT_DNS_DNSMASQ_SERVER}"
if [ "${flag}" = "acl_default" ]; then
set_cache_var "GLOBAL_SOCKS_server" "127.0.0.1:$socks_port"
set_cache_var "ACL_GLOBAL_node" "$node"
set_cache_var "ACL_${flag}_node" "$node"
set_cache_var "ACL_${flag}_node_socks_port" "$socks_port"
run_new_dnsmasq=$(config_n_get @global[0] dns_redirect 1)
if [ "${run_new_dnsmasq}" != "1" ]; then
#Rewrite the default DNS service configuration
@@ -865,15 +865,15 @@ acl_node() {
lua $APP_PATH/helper_dnsmasq.lua stretch
json_init
json_add_string "FLAG" "${flag}"
json_add_string "TMP_DNSMASQ_PATH" "${GLOBAL_DNSMASQ_CONF_PATH}"
json_add_string "DNSMASQ_CONF_FILE" "${GLOBAL_DNSMASQ_CONF}"
json_add_string "TMP_DNSMASQ_PATH" "${DEFAULT_DNSMASQ_CONF_PATH}"
json_add_string "DNSMASQ_CONF_FILE" "${DEFAULT_DNSMASQ_CONF}"
json_add_string "DEFAULT_DNS" "${DNSMASQ_DEFAULT_DNS}"
json_add_string "LOCAL_DNS" "${DNSMASQ_LOCAL_DNS}"
json_add_string "TUN_DNS" "${DNSMASQ_TUN_DNS}"
json_add_string "NFTFLAG" "${nftflag:-0}"
json_add_string "NO_LOGIC_LOG" "${NO_LOGIC_LOG:-0}"
lua $APP_PATH/helper_dnsmasq.lua add_rule "$(json_dump)"
uci -q add_list dhcp.@dnsmasq[0].addnmount=${GLOBAL_DNSMASQ_CONF_PATH}
uci -q add_list dhcp.@dnsmasq[0].addnmount=${DEFAULT_DNSMASQ_CONF_PATH}
uci -q commit dhcp
lua $APP_PATH/helper_dnsmasq.lua logic_restart
@@ -973,8 +973,8 @@ stop() {
unset XRAY_LOCATION_ASSET
unset SS_SYSTEM_DNS_RESOLVER_FORCE_BUILTIN
stop_crontab
rm -rf $GLOBAL_DNSMASQ_CONF
rm -rf $GLOBAL_DNSMASQ_CONF_PATH
rm -rf $DEFAULT_DNSMASQ_CONF
rm -rf $DEFAULT_DNSMASQ_CONF_PATH
[ "1" = "1" ] && {
#restore logic
bak_dnsmasq_dns_redirect=$(config_n_get @global[0] dnsmasq_dns_redirect)
@@ -985,7 +985,7 @@ stop() {
uci -q commit ${CONFIG}
}
if [ -z "${ACL_default_dns_port}" ] || [ -n "${bak_dnsmasq_dns_redirect}" ]; then
uci -q del_list dhcp.@dnsmasq[0].addnmount="${GLOBAL_DNSMASQ_CONF_PATH}"
uci -q del_list dhcp.@dnsmasq[0].addnmount="${DEFAULT_DNSMASQ_CONF_PATH}"
uci -q commit dhcp
json_init
@@ -1061,8 +1061,8 @@ get_config() {
DNSMASQ_CONF_DIR=${DEFAULT_DNSMASQ_CONF_DIR}
fi
fi
set_cache_var GLOBAL_DNSMASQ_CONF ${DNSMASQ_CONF_DIR}/dnsmasq-${CONFIG}.conf
set_cache_var GLOBAL_DNSMASQ_CONF_PATH ${TMP_ACL_PATH}/acl_default_dnsmasq.d
set_cache_var DEFAULT_DNSMASQ_CONF ${DNSMASQ_CONF_DIR}/dnsmasq-${CONFIG}.conf
set_cache_var DEFAULT_DNSMASQ_CONF_PATH ${TMP_ACL_PATH}/acl_default_dnsmasq.d
QUEUE_RUN=1
}
@@ -230,7 +230,7 @@ gen_shunt_list() {
}
[ -n "${_SHUNT_LIST4}" ] && eval ${shunt_list4_var_name}=\"${_SHUNT_LIST4}\"
[ -n "${_SHUNT_LIST6}" ] && eval ${shunt_list6_var_name}=\"${_SHUNT_LIST6}\"
set_cache_var "gen_shunt_list_${node}" "1"
set_cache_var "node_${node}_gen_shunt_list" "1"
}
add_shunt_t_rule() {
@@ -261,7 +261,7 @@ load_acl() {
for sid in $(jsonfilter -s "${ACL_JSON}" -e '$.acl[*].flag'); do
eval local $(cat "${TMP_ACL_PATH}/${sid}/var")
[ -z "$(get_cache_var "gen_shunt_list_${node}")" ] && [ -n "${node}" ] && gen_shunt_list "${node}" shunt_list4 shunt_list6
[ -z "$(get_cache_var "node_${node}_gen_shunt_list")" ] && [ -n "${node}" ] && gen_shunt_list "${node}" shunt_list4 shunt_list6
[ -n "${use}" ] && local dns_redirect_port=$(get_cache_var "ACL_${use}_dns_port")
local ipt_tmp=$ipt_n
@@ -271,7 +271,7 @@ gen_shunt_list() {
}
[ -n "${_SHUNT_LIST4}" ] && eval ${shunt_list4_var_name}=\"${_SHUNT_LIST4}\"
[ -n "${_SHUNT_LIST6}" ] && eval ${shunt_list6_var_name}=\"${_SHUNT_LIST6}\"
set_cache_var "gen_shunt_list_${node}" "1"
set_cache_var "node_${node}_gen_shunt_list" "1"
}
add_shunt_t_rule() {
@@ -299,7 +299,7 @@ load_acl() {
for sid in $(jsonfilter -s "${ACL_JSON}" -e '$.acl[*].flag'); do
eval $(cat "${TMP_ACL_PATH}/${sid}/var")
[ -z "$(get_cache_var "gen_shunt_list_${node}")" ] && [ -n "${node}" ] && gen_shunt_list "${node}" shunt_list4 shunt_list6
[ -z "$(get_cache_var "node_${node}_gen_shunt_list")" ] && [ -n "${node}" ] && gen_shunt_list "${node}" shunt_list4 shunt_list6
[ -n "${use}" ] && local dns_redirect_port=$(get_cache_var "ACL_${use}_dns_port")
[ "${local_proxy}" = "1" ] && {
+1 -1
View File
@@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=sing-box
PKG_VERSION:=1.14.1
PKG_RELEASE:=31
PKG_RELEASE:=32
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/SagerNet/sing-box/tar.gz/v$(PKG_VERSION)?
@@ -0,0 +1,134 @@
diff --git a/option/rule_set.go b/option/rule_set.go
index b063422..1ce631a 100644
--- a/option/rule_set.go
+++ b/option/rule_set.go
@@ -122,6 +122,7 @@ type LocalRuleSet struct {
type RemoteRuleSet struct {
URL string `json:"url"`
+ Path string `json:"path,omitempty"`
DownloadDetour string `json:"download_detour,omitempty"`
UpdateInterval badoption.Duration `json:"update_interval,omitempty"`
}
diff --git a/route/rule/rule_set_remote.go b/route/rule/rule_set_remote.go
index bda6e23..8f84796 100644
--- a/route/rule/rule_set_remote.go
+++ b/route/rule/rule_set_remote.go
@@ -7,6 +7,8 @@ import (
"io"
"net"
"net/http"
+ "os"
+ "path/filepath"
"runtime"
"strings"
"sync"
@@ -27,6 +29,7 @@ import (
"github.com/sagernet/sing/common/ntp"
"github.com/sagernet/sing/common/x/list"
"github.com/sagernet/sing/service"
+ "github.com/sagernet/sing/service/filemanager"
"github.com/sagernet/sing/service/pause"
"go4.org/netipx"
@@ -94,14 +97,36 @@ func (s *RemoteRuleSet) StartContext(ctx context.Context, startContext *adapter.
dialer = s.outbound.Default()
}
s.dialer = dialer
+ loadedFromPath := false
+ pathMissing := false
+ if s.options.RemoteOptions.Path != "" {
+ err := s.loadFromPath()
+ if err == nil {
+ loadedFromPath = true
+ } else if os.IsNotExist(err) {
+ pathMissing = true
+ s.logger.Warn("rule-set file not found: ", s.options.RemoteOptions.Path)
+ } else {
+ s.logger.Warn("restore rule-set file ", s.options.RemoteOptions.Path, ": ", err)
+ }
+ }
if s.cacheFile != nil {
if savedSet := s.cacheFile.LoadRuleSet(s.options.Tag); savedSet != nil {
- err := s.loadBytes(savedSet.Content)
- if err != nil {
- return E.Cause(err, "restore cached rule-set")
+ if !loadedFromPath && !pathMissing {
+ err := s.loadBytes(savedSet.Content)
+ if err != nil {
+ return E.Cause(err, "restore cached rule-set")
+ }
+ s.lastUpdated = savedSet.LastUpdated
+ }
+ if !pathMissing {
+ if s.lastUpdated.IsZero() {
+ s.lastUpdated = savedSet.LastUpdated
+ }
+ if s.lastEtag == "" {
+ s.lastEtag = savedSet.LastEtag
+ }
}
- s.lastUpdated = savedSet.LastUpdated
- s.lastEtag = savedSet.LastEtag
}
}
if s.lastUpdated.IsZero() {
@@ -159,6 +184,46 @@ func (s *RemoteRuleSet) UnregisterCallback(element *list.Element[adapter.RuleSet
s.callbacks.Remove(element)
}
+func (s *RemoteRuleSet) localPath() string {
+ if s.options.RemoteOptions.Path == "" {
+ return ""
+ }
+ return filemanager.BasePath(s.ctx, s.options.RemoteOptions.Path)
+}
+
+func (s *RemoteRuleSet) loadFromPath() error {
+ path := s.localPath()
+ if path == "" {
+ return os.ErrNotExist
+ }
+ content, err := os.ReadFile(path)
+ if err != nil {
+ return err
+ }
+ err = s.loadBytes(content)
+ if err != nil {
+ return err
+ }
+ if info, err := os.Stat(path); err == nil {
+ s.lastUpdated = info.ModTime()
+ }
+ return nil
+}
+
+func (s *RemoteRuleSet) saveToPath(content []byte) error {
+ path := s.localPath()
+ if path == "" {
+ return nil
+ }
+ if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
+ return err
+ }
+ if err := os.WriteFile(path, content, 0o644); err != nil {
+ return err
+ }
+ return filemanager.Chown(s.ctx, path)
+}
+
func (s *RemoteRuleSet) loadBytes(content []byte) error {
var (
ruleSet option.PlainRuleSetCompat
@@ -308,6 +373,11 @@ func (s *RemoteRuleSet) fetch(ctx context.Context, startContext *adapter.HTTPSta
s.logger.Error("save rule-set cache: ", err)
}
}
+ if s.options.RemoteOptions.Path != "" {
+ if err := s.saveToPath(content); err != nil {
+ s.logger.Error("save rule-set file: ", err)
+ }
+ }
s.logger.Info("updated rule-set ", s.options.Tag)
return nil
}
@@ -0,0 +1,421 @@
diff --git a/experimental/clashapi/provider.go b/experimental/clashapi/provider.go
index 352b289..ef07e6d 100644
--- a/experimental/clashapi/provider.go
+++ b/experimental/clashapi/provider.go
@@ -3,43 +3,65 @@ package clashapi
import (
"context"
"net/http"
+ "os/exec"
+ "regexp"
+
+ "github.com/sagernet/sing-box/option"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
)
-func proxyProviderRouter() http.Handler {
+func proxyProviderRouter(server *Server) http.Handler {
r := chi.NewRouter()
- r.Get("/", getProviders)
+ r.Get("/", getProviders(server))
r.Route("/{name}", func(r chi.Router) {
- r.Use(parseProviderName, findProviderByName)
- r.Get("/", getProvider)
+ r.Use(parseProviderName, findProviderByName(server))
+ r.Get("/", getProvider(server))
r.Put("/", updateProvider)
r.Get("/healthcheck", healthCheckProvider)
})
return r
}
-func getProviders(w http.ResponseWriter, r *http.Request) {
- render.JSON(w, r, render.M{
- "providers": render.M{},
- })
+func getProviders(server *Server) func(w http.ResponseWriter, r *http.Request) {
+ return func(w http.ResponseWriter, r *http.Request) {
+ providers := render.M{}
+ for _, provider := range server.proxyProviders {
+ if provider.Name == "" {
+ continue
+ }
+ providers[provider.Name] = proxyProviderInfo(server, provider)
+ }
+ render.JSON(w, r, render.M{
+ "providers": providers,
+ })
+ }
}
-func getProvider(w http.ResponseWriter, r *http.Request) {
- /*provider := r.Context().Value(CtxKeyProvider).(provider.ProxyProvider)
- render.JSON(w, r, provider)*/
- render.NoContent(w, r)
+func getProvider(server *Server) func(w http.ResponseWriter, r *http.Request) {
+ return func(w http.ResponseWriter, r *http.Request) {
+ provider := r.Context().Value(CtxKeyProvider).(option.ClashProxyProviderOptions)
+ render.JSON(w, r, proxyProviderInfo(server, provider))
+ }
}
func updateProvider(w http.ResponseWriter, r *http.Request) {
- /*provider := r.Context().Value(CtxKeyProvider).(provider.ProxyProvider)
- if err := provider.Update(); err != nil {
+ provider := r.Context().Value(CtxKeyProvider).(option.ClashProxyProviderOptions)
+ if !regexp.MustCompile(`^[0-9a-f]{32}$`).MatchString(provider.UpdateID) {
+ render.Status(r, http.StatusServiceUnavailable)
+ render.JSON(w, r, newError("Provider update is unavailable"))
+ return
+ }
+
+ command := exec.Command("/bin/sh", "/etc/homeproxy/scripts/update_provider.sh", provider.UpdateID)
+ if err := command.Start(); err != nil {
render.Status(r, http.StatusServiceUnavailable)
render.JSON(w, r, newError(err.Error()))
return
- }*/
+ }
+ _ = command.Process.Release()
render.NoContent(w, r)
}
@@ -57,18 +79,58 @@ func parseProviderName(next http.Handler) http.Handler {
})
}
-func findProviderByName(next http.Handler) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- /*name := r.Context().Value(CtxKeyProviderName).(string)
- providers := tunnel.ProxyProviders()
- provider, exist := providers[name]
- if !exist {*/
- render.Status(r, http.StatusNotFound)
- render.JSON(w, r, ErrNotFound)
- //return
- //}
+func findProviderByName(server *Server) func(next http.Handler) http.Handler {
+ return func(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ name := r.Context().Value(CtxKeyProviderName).(string)
+ for _, provider := range server.proxyProviders {
+ if provider.Name == name {
+ ctx := context.WithValue(r.Context(), CtxKeyProvider, provider)
+ next.ServeHTTP(w, r.WithContext(ctx))
+ return
+ }
+ }
+ render.Status(r, http.StatusNotFound)
+ render.JSON(w, r, ErrNotFound)
+ })
+ }
+}
- // ctx := context.WithValue(r.Context(), CtxKeyProvider, provider)
- // next.ServeHTTP(w, r.WithContext(ctx))
- })
+func proxyProviderInfo(server *Server, provider option.ClashProxyProviderOptions) render.M {
+ providerType := provider.Type
+ if providerType == "" {
+ providerType = "Proxy"
+ }
+ vehicleType := provider.VehicleType
+ if vehicleType == "" {
+ vehicleType = "HTTP"
+ }
+
+ proxies := make([]any, 0, len(provider.Proxies))
+ for _, proxyName := range provider.Proxies {
+ proxy, exists := lookupProxyByName(server, proxyName)
+ if !exists {
+ continue
+ }
+ proxies = append(proxies, proxyInfo(server, proxy))
+ }
+
+ info := render.M{
+ "name": provider.Name,
+ "type": providerType,
+ "vehicleType": vehicleType,
+ "proxies": proxies,
+ }
+ if provider.UpdatedAt != "" {
+ info["updatedAt"] = provider.UpdatedAt
+ }
+ if provider.SubscriptionInfo != nil {
+ info["subscriptionInfo"] = render.M{
+ "Upload": provider.SubscriptionInfo.Upload,
+ "Download": provider.SubscriptionInfo.Download,
+ "Total": provider.SubscriptionInfo.Total,
+ "Expire": provider.SubscriptionInfo.Expire,
+ }
+ }
+ return info
}
diff --git a/experimental/clashapi/proxies.go b/experimental/clashapi/proxies.go
index ef88ff3..1f7d14d 100644
--- a/experimental/clashapi/proxies.go
+++ b/experimental/clashapi/proxies.go
@@ -21,6 +21,50 @@ import (
"github.com/go-chi/render"
)
+func proxyDisplayName(tag string) string {
+ switch tag {
+ case "GLOBAL":
+ return "GLOBAL"
+ case "main-out":
+ return "主代理"
+ case "main-udp-out":
+ return "UDP 主代理"
+ case "direct-out":
+ return "直连"
+ case "block-out":
+ return "拦截"
+ default:
+ return tag
+ }
+}
+
+func lookupProxyByName(server *Server, name string) (adapter.Outbound, bool) {
+ proxy, exist := server.outbound.Outbound(name)
+ if exist {
+ return proxy, true
+ }
+
+ for _, detour := range server.outbound.Outbounds() {
+ if proxyDisplayName(detour.Tag()) == name {
+ return detour, true
+ }
+ }
+
+ for _, endpoint := range server.endpoint.Endpoints() {
+ if proxyDisplayName(endpoint.Tag()) == name {
+ return endpoint, true
+ }
+ }
+
+ return nil, false
+}
+
+func proxyDisplayNames(tags []string) []string {
+ return common.Map(tags, func(tag string) string {
+ return proxyDisplayName(tag)
+ })
+}
+
func proxyRouter(server *Server, router adapter.Router) http.Handler {
r := chi.NewRouter()
r.Get("/", getProxies(server))
@@ -29,7 +73,7 @@ func proxyRouter(server *Server, router adapter.Router) http.Handler {
r.Use(parseProxyName, findProxyByName(server))
r.Get("/", getProxy(server))
r.Get("/delay", getProxyDelay(server))
- r.Put("/", updateProxy)
+ r.Put("/", updateProxy(server))
})
return r
}
@@ -46,7 +90,7 @@ func findProxyByName(server *Server) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
name := r.Context().Value(CtxKeyProxyName).(string)
- proxy, exist := server.outbound.Outbound(name)
+ proxy, exist := lookupProxyByName(server, name)
if !exist {
render.Status(r, http.StatusNotFound)
render.JSON(w, r, ErrNotFound)
@@ -68,7 +112,7 @@ func proxyInfo(server *Server, detour adapter.Outbound) *badjson.JSONObject {
clashType = C.ProxyDisplayName(detour.Type())
}
info.Put("type", clashType)
- info.Put("name", detour.Tag())
+ info.Put("name", proxyDisplayName(detour.Tag()))
info.Put("udp", common.Contains(detour.Network(), N.NetworkUDP))
delayHistory := server.urlTestHistory.LoadURLTestHistory(adapter.OutboundTag(detour))
if delayHistory != nil {
@@ -77,8 +121,8 @@ func proxyInfo(server *Server, detour adapter.Outbound) *badjson.JSONObject {
info.Put("history", []*adapter.URLTestHistory{})
}
if group, isGroup := detour.(adapter.OutboundGroup); isGroup {
- info.Put("now", group.Now())
- info.Put("all", group.All())
+ info.Put("now", proxyDisplayName(group.Now()))
+ info.Put("all", proxyDisplayNames(group.All()))
}
return &info
}
@@ -102,10 +146,10 @@ func getProxies(server *Server) func(w http.ResponseWriter, r *http.Request) {
case C.TypeDirect, C.TypeBlock, C.TypeDNS:
continue
}
- allProxies = append(allProxies, detour.Tag())
+ allProxies = append(allProxies, proxyDisplayName(detour.Tag()))
}
- defaultTag := server.outbound.Default().Tag()
+ defaultTag := proxyDisplayName(server.outbound.Default().Tag())
sort.SliceStable(allProxies, func(i, j int) bool {
return allProxies[i] == defaultTag
@@ -114,7 +158,7 @@ func getProxies(server *Server) func(w http.ResponseWriter, r *http.Request) {
// fix clash dashboard
proxyMap.Put("GLOBAL", map[string]any{
"type": "Fallback",
- "name": "GLOBAL",
+ "name": proxyDisplayName("GLOBAL"),
"udp": true,
"history": []*adapter.URLTestHistory{},
"all": allProxies,
@@ -126,7 +170,7 @@ func getProxies(server *Server) func(w http.ResponseWriter, r *http.Request) {
if detour.Tag() == "" {
tag = F.ToString(i)
} else {
- tag = detour.Tag()
+ tag = proxyDisplayName(detour.Tag())
}
proxyMap.Put(tag, proxyInfo(server, detour))
}
@@ -159,29 +203,36 @@ type UpdateProxyRequest struct {
Name string `json:"name"`
}
-func updateProxy(w http.ResponseWriter, r *http.Request) {
- req := UpdateProxyRequest{}
- if err := render.DecodeJSON(r.Body, &req); err != nil {
- render.Status(r, http.StatusBadRequest)
- render.JSON(w, r, ErrBadRequest)
- return
- }
+func updateProxy(server *Server) func(w http.ResponseWriter, r *http.Request) {
+ return func(w http.ResponseWriter, r *http.Request) {
+ req := UpdateProxyRequest{}
+ if err := render.DecodeJSON(r.Body, &req); err != nil {
+ render.Status(r, http.StatusBadRequest)
+ render.JSON(w, r, ErrBadRequest)
+ return
+ }
- proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound)
- selector, ok := proxy.(*group.Selector)
- if !ok {
- render.Status(r, http.StatusBadRequest)
- render.JSON(w, r, newError("Must be a Selector"))
- return
- }
+ proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound)
+ selector, ok := proxy.(*group.Selector)
+ if !ok {
+ render.Status(r, http.StatusBadRequest)
+ render.JSON(w, r, newError("Must be a Selector"))
+ return
+ }
- if !selector.SelectOutbound(req.Name) {
- render.Status(r, http.StatusBadRequest)
- render.JSON(w, r, newError("Selector update error: not found"))
- return
- }
+ selectedName := req.Name
+ if targetProxy, exist := lookupProxyByName(server, req.Name); exist {
+ selectedName = targetProxy.Tag()
+ }
+
+ if !selector.SelectOutbound(selectedName) {
+ render.Status(r, http.StatusBadRequest)
+ render.JSON(w, r, newError("Selector update error: not found"))
+ return
+ }
- render.NoContent(w, r)
+ render.NoContent(w, r)
+ }
}
func getProxyDelay(server *Server) func(w http.ResponseWriter, r *http.Request) {
diff --git a/experimental/clashapi/server.go b/experimental/clashapi/server.go
index 950dca0..e4c1210 100644
--- a/experimental/clashapi/server.go
+++ b/experimental/clashapi/server.go
@@ -60,6 +60,7 @@ type Server struct {
externalUI string
externalUIDownloadURL string
externalUIDownloadDetour string
+ proxyProviders []option.ClashProxyProviderOptions
}
func NewServer(ctx context.Context, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) {
@@ -82,6 +83,7 @@ func NewServer(ctx context.Context, logFactory log.ObservableFactory, options op
externalController: options.ExternalController != "",
externalUIDownloadURL: options.ExternalUIDownloadURL,
externalUIDownloadDetour: options.ExternalUIDownloadDetour,
+ proxyProviders: options.ProxyProviders,
}
s.urlTestHistory = service.FromContext[adapter.URLTestHistoryStorage](ctx)
if s.urlTestHistory == nil {
@@ -122,7 +124,7 @@ func NewServer(ctx context.Context, logFactory log.ObservableFactory, options op
r.Mount("/proxies", proxyRouter(s, s.router))
r.Mount("/rules", ruleRouter(s.router))
r.Mount("/connections", connectionRouter(s.ctx, s.router, trafficManager))
- r.Mount("/providers/proxies", proxyProviderRouter())
+ r.Mount("/providers/proxies", proxyProviderRouter(s))
r.Mount("/providers/rules", ruleProviderRouter())
r.Mount("/script", scriptRouter())
r.Mount("/profile", profileRouter())
diff --git a/option/experimental.go b/option/experimental.go
index bf0df9e..50de31d 100644
--- a/option/experimental.go
+++ b/option/experimental.go
@@ -19,15 +19,16 @@ type CacheFileOptions struct {
}
type ClashAPIOptions struct {
- ExternalController string `json:"external_controller,omitempty"`
- ExternalUI string `json:"external_ui,omitempty"`
- ExternalUIDownloadURL string `json:"external_ui_download_url,omitempty"`
- ExternalUIDownloadDetour string `json:"external_ui_download_detour,omitempty"`
- Secret string `json:"secret,omitempty"`
- DefaultMode string `json:"default_mode,omitempty"`
- ModeList []string `json:"-"`
- AccessControlAllowOrigin badoption.Listable[string] `json:"access_control_allow_origin,omitempty"`
- AccessControlAllowPrivateNetwork bool `json:"access_control_allow_private_network,omitempty"`
+ ExternalController string `json:"external_controller,omitempty"`
+ ExternalUI string `json:"external_ui,omitempty"`
+ ExternalUIDownloadURL string `json:"external_ui_download_url,omitempty"`
+ ExternalUIDownloadDetour string `json:"external_ui_download_detour,omitempty"`
+ Secret string `json:"secret,omitempty"`
+ DefaultMode string `json:"default_mode,omitempty"`
+ ProxyProviders []ClashProxyProviderOptions `json:"proxy_providers,omitempty"`
+ ModeList []string `json:"-"`
+ AccessControlAllowOrigin badoption.Listable[string] `json:"access_control_allow_origin,omitempty"`
+ AccessControlAllowPrivateNetwork bool `json:"access_control_allow_private_network,omitempty"`
// Deprecated: migrated to global cache file
CacheFile string `json:"cache_file,omitempty"`
@@ -41,6 +42,23 @@ type ClashAPIOptions struct {
StoreFakeIP bool `json:"store_fakeip,omitempty"`
}
+type ClashProxyProviderOptions struct {
+ Name string `json:"name,omitempty"`
+ Type string `json:"type,omitempty"`
+ VehicleType string `json:"vehicle_type,omitempty"`
+ Proxies []string `json:"proxies,omitempty"`
+ UpdatedAt string `json:"updated_at,omitempty"`
+ UpdateID string `json:"update_id,omitempty"`
+ SubscriptionInfo *ClashProxyProviderSubscriptionInfo `json:"subscription_info,omitempty"`
+}
+
+type ClashProxyProviderSubscriptionInfo struct {
+ Upload uint64 `json:"upload,omitempty"`
+ Download uint64 `json:"download,omitempty"`
+ Total uint64 `json:"total,omitempty"`
+ Expire int64 `json:"expire,omitempty"`
+}
+
type V2RayAPIOptions struct {
Listen string `json:"listen,omitempty"`
Stats *V2RayStatsServiceOptions `json:"stats,omitempty"`
+2 -2
View File
@@ -50,11 +50,11 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=wwand
PKG_VERSION:=1.6.7
PKG_RELEASE:=40
PKG_RELEASE:=41
PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/ddimension/wwand.git
PKG_SOURCE_VERSION:=1ecf9382898979cfbc2bb31fc2b42b805f9dda60
PKG_SOURCE_VERSION:=b5ec86779fb145d23d0fe5085241c8b64f05b930
PKG_MIRROR_HASH:=skip
PKG_LICENSE:=GPL-2.0-only