diff --git a/luci-app-passwall2/Makefile b/luci-app-passwall2/Makefile index 0a5035be0..5e900d425 100644 --- a/luci-app-passwall2/Makefile +++ b/luci-app-passwall2/Makefile @@ -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:= \ diff --git a/luci-app-passwall2/luasrc/controller/passwall2.lua b/luci-app-passwall2/luasrc/controller/passwall2.lua index 89ea0b541..099ceee8e 100644 --- a/luci-app-passwall2/luasrc/controller/passwall2.lua +++ b/luci-app-passwall2/luasrc/controller/passwall2.lua @@ -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 diff --git a/luci-app-passwall2/luasrc/passwall2/api.lua b/luci-app-passwall2/luasrc/passwall2/api.lua index e7a64a600..ffbefc47e 100644 --- a/luci-app-passwall2/luasrc/passwall2/api.lua +++ b/luci-app-passwall2/luasrc/passwall2/api.lua @@ -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 diff --git a/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua b/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua index 91cb5bae4..9b44bb683 100644 --- a/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua +++ b/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua @@ -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 diff --git a/luci-app-passwall2/luasrc/passwall2/util_xray.lua b/luci-app-passwall2/luasrc/passwall2/util_xray.lua index 01fa69b68..d2aee8389 100644 --- a/luci-app-passwall2/luasrc/passwall2/util_xray.lua +++ b/luci-app-passwall2/luasrc/passwall2/util_xray.lua @@ -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 diff --git a/luci-app-passwall2/root/etc/hotplug.d/ntp/30-passwall2-resync b/luci-app-passwall2/root/etc/hotplug.d/ntp/30-passwall2-resync index ebe31a102..8d7c6424b 100644 --- a/luci-app-passwall2/root/etc/hotplug.d/ntp/30-passwall2-resync +++ b/luci-app-passwall2/root/etc/hotplug.d/ntp/30-passwall2-resync @@ -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)" } diff --git a/luci-app-passwall2/root/usr/share/passwall2/app.sh b/luci-app-passwall2/root/usr/share/passwall2/app.sh index a8ee0d4ea..c738c10fc 100755 --- a/luci-app-passwall2/root/usr/share/passwall2/app.sh +++ b/luci-app-passwall2/root/usr/share/passwall2/app.sh @@ -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 } diff --git a/luci-app-passwall2/root/usr/share/passwall2/iptables.sh b/luci-app-passwall2/root/usr/share/passwall2/iptables.sh index 4176e3839..b6c3572e9 100755 --- a/luci-app-passwall2/root/usr/share/passwall2/iptables.sh +++ b/luci-app-passwall2/root/usr/share/passwall2/iptables.sh @@ -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 diff --git a/luci-app-passwall2/root/usr/share/passwall2/nftables.sh b/luci-app-passwall2/root/usr/share/passwall2/nftables.sh index 7cb1960c4..72e4f8a4c 100755 --- a/luci-app-passwall2/root/usr/share/passwall2/nftables.sh +++ b/luci-app-passwall2/root/usr/share/passwall2/nftables.sh @@ -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" ] && { diff --git a/sing-box/Makefile b/sing-box/Makefile index d1dd0b837..5de609eb1 100644 --- a/sing-box/Makefile +++ b/sing-box/Makefile @@ -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)? diff --git a/sing-box/patches/001-support-remote-rule-set-path.patch b/sing-box/patches/001-support-remote-rule-set-path.patch new file mode 100644 index 000000000..9f6068919 --- /dev/null +++ b/sing-box/patches/001-support-remote-rule-set-path.patch @@ -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 + } diff --git a/sing-box/patches/002-support-clash-proxy-providers.patch b/sing-box/patches/002-support-clash-proxy-providers.patch new file mode 100644 index 000000000..9de3cf99a --- /dev/null +++ b/sing-box/patches/002-support-clash-proxy-providers.patch @@ -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"` diff --git a/wwand/Makefile b/wwand/Makefile index b22e9c7d2..2397b8d3b 100644 --- a/wwand/Makefile +++ b/wwand/Makefile @@ -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