From 893377a19f006b382ac22e2ea00faad367e804e8 Mon Sep 17 00:00:00 2001 From: actions-user Date: Tue, 26 Aug 2025 09:20:45 +0800 Subject: [PATCH] update 2025-08-26 09:20:45 --- .../luci-static/resources/tools/nikki.js | 28 ++++--------- .../root/usr/share/rpcd/ucode/luci.nikki | 31 +++++++++++++- .../luasrc/passwall/util_xray.lua | 4 ++ luci-app-passwall2/Makefile | 2 +- .../luasrc/controller/passwall2.lua | 42 +++++++++++++++---- .../model/cbi/passwall2/client/node_list.lua | 12 ++++++ .../client/node_subscribe_config.lua | 37 ++++++++++++++++ .../cbi/passwall2/client/type/sing-box.lua | 21 ++++++++++ luci-app-passwall2/luasrc/passwall2/api.lua | 4 ++ .../luasrc/passwall2/util_sing-box.lua | 12 ++++++ .../luasrc/passwall2/util_xray.lua | 4 ++ .../luasrc/view/passwall2/global/backup.htm | 13 +++--- luci-app-passwall2/po/zh-cn/passwall2.po | 27 ++++++++++++ .../root/usr/share/passwall2/subscribe.lua | 27 ++++++++++++ 14 files changed, 227 insertions(+), 37 deletions(-) diff --git a/luci-app-nikki/htdocs/luci-static/resources/tools/nikki.js b/luci-app-nikki/htdocs/luci-static/resources/tools/nikki.js index 9e5ea4d..926ce2e 100644 --- a/luci-app-nikki/htdocs/luci-static/resources/tools/nikki.js +++ b/luci-app-nikki/htdocs/luci-static/resources/tools/nikki.js @@ -39,6 +39,13 @@ const callNikkiUpdateSubscription = rpc.declare({ expect: { '': {} } }); +const callNikkiAPI = rpc.declare({ + object: 'luci.nikki', + method: 'api', + params: ['method', 'path', 'query', 'body'], + expect: { '': {} } +}); + const callNikkiGetIdentifiers = rpc.declare({ object: 'luci.nikki', method: 'get_identifiers', @@ -103,21 +110,8 @@ return baseclass.extend({ return callNikkiUpdateSubscription(section_id); }, - api: async function (method, path, query, body) { - const profile = await callNikkiProfile({ 'external-controller': null, 'secret': null }); - const apiListen = profile['external-controller']; - const apiSecret = profile['secret'] ?? ''; - if (!apiListen) { - return Promise.reject('API has not been configured'); - } - const apiPort = apiListen.substring(apiListen.lastIndexOf(':') + 1); - const url = `http://${window.location.hostname}:${apiPort}${path}`; - return request.request(url, { - method: method, - headers: { 'Authorization': `Bearer ${apiSecret}` }, - query: query, - content: body - }); + updateDashboard: function () { + return callNikkiAPI('POST', '/upgrade/ui'); }, openDashboard: async function () { @@ -146,10 +140,6 @@ return baseclass.extend({ return Promise.resolve(); }, - updateDashboard: function () { - return this.api('POST', '/upgrade/ui'); - }, - getIdentifiers: function () { return callNikkiGetIdentifiers(); }, diff --git a/luci-app-nikki/root/usr/share/rpcd/ucode/luci.nikki b/luci-app-nikki/root/usr/share/rpcd/ucode/luci.nikki index 7c9013f..9bd3069 100644 --- a/luci-app-nikki/root/usr/share/rpcd/ucode/luci.nikki +++ b/luci-app-nikki/root/usr/share/rpcd/ucode/luci.nikki @@ -3,7 +3,7 @@ 'use strict'; import { access, popen, writefile } from 'fs'; -import { get_users, get_groups, get_cgroups } from '/etc/nikki/ucode/include.uc'; +import { load_profile, get_users, get_groups, get_cgroups } from '/etc/nikki/ucode/include.uc'; const methods = { version: { @@ -62,6 +62,35 @@ const methods = { return { success: success }; } }, + api: { + args: { method: 'method', path: 'path', query: 'query', body: 'body' }, + call: function(req) { + let result = {}; + + const method = req.args?.method; + const path = req.args?.path; + const query = req.args?.query; + const body = req.args?.body; + + const profile = load_profile(); + const api_listen = profile['external-controller']; + const api_secret = profile['secret']; + + if (!api_listen) { + return result; + } + + const url = api_listen + path; + + const process = popen(`curl --request '${method}' --oauth2-bearer '${api_secret}' --url-query '${query}' --data '${body}' '${url}'`); + if (process) { + result = json(process); + process.close(); + } + + return result; + } + }, get_identifiers: { call: function() { const users = filter(get_users(), (x) => x != ''); diff --git a/luci-app-passwall/luasrc/passwall/util_xray.lua b/luci-app-passwall/luasrc/passwall/util_xray.lua index 7152ad0..0ede4a9 100644 --- a/luci-app-passwall/luasrc/passwall/util_xray.lua +++ b/luci-app-passwall/luasrc/passwall/util_xray.lua @@ -779,6 +779,10 @@ function gen_config(var) table.insert(outbounds, outbound) fallback_node_tag = outbound.tag end + else + if gen_balancer(fallback_node) then + fallback_node_tag = fallback_node_id + end end end end diff --git a/luci-app-passwall2/Makefile b/luci-app-passwall2/Makefile index 7589c39..1a295a0 100644 --- a/luci-app-passwall2/Makefile +++ b/luci-app-passwall2/Makefile @@ -5,7 +5,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-passwall2 -PKG_VERSION:=25.8.22 +PKG_VERSION:=25.8.25 PKG_RELEASE:=1 PKG_CONFIG_DEPENDS:= \ diff --git a/luci-app-passwall2/luasrc/controller/passwall2.lua b/luci-app-passwall2/luasrc/controller/passwall2.lua index 5a6eba4..3705051 100644 --- a/luci-app-passwall2/luasrc/controller/passwall2.lua +++ b/luci-app-passwall2/luasrc/controller/passwall2.lua @@ -376,6 +376,9 @@ function clear_all_nodes() end) uci:foreach(appname, "subscribe_list", function(t) uci:delete(appname, t[".name"], "md5") + uci:delete(appname, t[".name"], "chain_proxy") + uci:delete(appname, t[".name"], "preproxy_node") + uci:delete(appname, t[".name"], "to_node") end) api.uci_save(uci, appname, true, true) @@ -440,6 +443,16 @@ function delete_select_nodes() uci:delete(appname, t[".name"], "fallback_node") end end) + uci:foreach(appname, "subscribe_list", function(t) + if t["preproxy_node"] == w then + uci:delete(appname, t[".name"], "preproxy_node") + uci:delete(appname, t[".name"], "chain_proxy") + end + if t["to_node"] == w then + uci:delete(appname, t[".name"], "to_node") + uci:delete(appname, t[".name"], "chain_proxy") + end + end) if (uci:get(appname, w, "add_mode") or "0") == "2" then local add_from = uci:get(appname, w, "add_from") or "" if add_from ~= "" then @@ -531,20 +544,29 @@ function create_backup() end function restore_backup() + local result = { status = "error", message = "unknown error" } local ok, err = pcall(function() local filename = http.formvalue("filename") local chunk = http.formvalue("chunk") local chunk_index = tonumber(http.formvalue("chunk_index") or "-1") local total_chunks = tonumber(http.formvalue("total_chunks") or "-1") - if not filename or not chunk then - http_write_json({ status = "error", message = "Missing filename or chunk" }) + if not filename then + result = { status = "error", message = "Missing filename" } + return + end + if not chunk then + result = { status = "error", message = "Missing chunk data" } return end local file_path = "/tmp/" .. filename local decoded = nixio.bin.b64decode(chunk) + if not decoded then + result = { status = "error", message = "Base64 decode failed" } + return + end local fp = io.open(file_path, "a+") if not fp then - http_write_json({ status = "error", message = "Failed to open file for writing: " .. file_path }) + result = { status = "error", message = "Failed to open file: " .. file_path } return end fp:write(decoded) @@ -552,7 +574,7 @@ function restore_backup() if chunk_index + 1 == total_chunks then api.sys.call("echo '' > /tmp/log/passwall2.log") api.log(" * PassWall2 配置文件上传成功…") - local temp_dir = '/tmp/passwall_bak' + local temp_dir = '/tmp/passwall2_bak' api.sys.call("mkdir -p " .. temp_dir) if api.sys.call("tar -xzf " .. file_path .. " -C " .. temp_dir) == 0 then for _, backup_file in ipairs(backup_files) do @@ -563,21 +585,23 @@ function restore_backup() end api.log(" * PassWall2 配置还原成功…") api.log(" * 重启 PassWall2 服务中…\n") - api.sys.call('/etc/init.d/passwall2 restart > /dev/null 2>&1 &') - api.sys.call('/etc/init.d/passwall2_server restart > /dev/null 2>&1 &') + luci.sys.call('/etc/init.d/passwall2 restart > /dev/null 2>&1 &') + luci.sys.call('/etc/init.d/passwall2_server restart > /dev/null 2>&1 &') + result = { status = "success", message = "Upload completed", path = file_path } else api.log(" * PassWall2 配置文件解压失败,请重试!") + result = { status = "error", message = "Decompression failed" } end api.sys.call("rm -rf " .. temp_dir) fs.remove(file_path) - http_write_json({ status = "success", message = "Upload completed", path = file_path }) else - http_write_json({ status = "success", message = "Chunk received" }) + result = { status = "success", message = "Chunk received" } end end) if not ok then - http_write_json({ status = "error", message = tostring(err) }) + result = { status = "error", message = tostring(err) } end + http_write_json(result) end function geo_view() diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_list.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_list.lua index c6ab4c3..b243f11 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_list.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_list.lua @@ -84,6 +84,16 @@ function s.remove(e, t) m:del(s[".name"], "fallback_node") end end) + m.uci:foreach(appname, "subscribe_list", function(s) + if s["preproxy_node"] == t then + m:del(s[".name"], "preproxy_node") + m:del(s[".name"], "chain_proxy") + end + if s["to_node"] == t then + m:del(s[".name"], "to_node") + m:del(s[".name"], "chain_proxy") + end + end) if (m:get(t, "add_mode") or "0") == "2" then local add_from = m:get(t, "add_from") or "" if add_from ~= "" then @@ -157,6 +167,8 @@ o.cfgvalue = function(t, n) protocol = "HY2" elseif protocol == "anytls" then protocol = "AnyTLS" + elseif protocol == "ssh" then + protocol = "SSH" else protocol = protocol:gsub("^%l",string.upper) end diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_subscribe_config.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_subscribe_config.lua index db0c137..df392e3 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_subscribe_config.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_subscribe_config.lua @@ -51,6 +51,18 @@ if has_hysteria2 then local s = "hysteria2" table.insert(hysteria2_type, s) end +local nodes_table = {} +for k, e in ipairs(api.get_valid_nodes()) do + if e.node_type == "normal" then + nodes_table[#nodes_table + 1] = { + id = e[".name"], + remark = e["remark"], + type = e["type"], + add_mode = e["add_mode"], + chain_proxy = e["chain_proxy"] + } + end +end s = m:section(NamedSection, arg[1]) s.addremove = false @@ -205,4 +217,29 @@ o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, li o:value("Passwall2/OpenWrt", "PassWall2") o:value("v2rayN/9.99", "v2rayN") +o = s:option(ListValue, "chain_proxy", translate("Chain Proxy")) +o:value("", translate("Close(Not use)")) +o:value("1", translate("Preproxy Node")) +o:value("2", translate("Landing Node")) + +local descrStr = "Chained proxy works only with Xray or Sing-box nodes.
" +descrStr = descrStr .. "The chained node must be the same type as your subscription node (Xray with Xray, Sing-box with Sing-box).
" +descrStr = descrStr .. "You can only use manual or imported nodes as chained nodes." +descrStr = translate(descrStr) .. "
" .. translate("Only support a layer of proxy.") + +o = s:option(ListValue, "preproxy_node", translate("Preproxy Node")) +o:depends({ ["chain_proxy"] = "1" }) +o.description = descrStr + +o = s:option(ListValue, "to_node", translate("Landing Node")) +o:depends({ ["chain_proxy"] = "2" }) +o.description = descrStr + +for k, v in pairs(nodes_table) do + if (v.type == "Xray" or v.type == "sing-box") and (not v.chain_proxy or v.chain_proxy == "") and v.add_mode ~= "2" then + s.fields["preproxy_node"]:value(v.id, v.remark) + s.fields["to_node"]:value(v.id, v.remark) + end +end + return m diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/sing-box.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/sing-box.lua index 13f2f7c..c66028a 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/sing-box.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/sing-box.lua @@ -65,6 +65,7 @@ end if version_ge_1_12_0 then o:value("anytls", "AnyTLS") end +o:value("ssh", "SSH") o:value("_urltest", translate("URLTest")) o:value("_shunt", translate("Shunt")) o:value("_iface", translate("Custom Interface")) @@ -258,6 +259,7 @@ end o = s:option(Value, _n("username"), translate("Username")) o:depends({ [_n("protocol")] = "http" }) o:depends({ [_n("protocol")] = "socks" }) +o:depends({ [_n("protocol")] = "ssh" }) o = s:option(Value, _n("password"), translate("Password")) o.password = true @@ -268,6 +270,7 @@ o:depends({ [_n("protocol")] = "shadowsocksr" }) o:depends({ [_n("protocol")] = "trojan" }) o:depends({ [_n("protocol")] = "tuic" }) o:depends({ [_n("protocol")] = "anytls" }) +o:depends({ [_n("protocol")] = "ssh" }) o = s:option(ListValue, _n("security"), translate("Encrypt Method")) for a, t in ipairs(security_list) do o:value(t) end @@ -456,6 +459,24 @@ if singbox_tags:find("with_quic") then o:depends({ [_n("protocol")] = "hysteria2"}) end +-- [[ SSH config start ]] -- +o = s:option(Value, _n("ssh_priv_key"), translate("Private Key")) +o:depends({ [_n("protocol")] = "ssh" }) + +o = s:option(Value, _n("ssh_priv_key_pp"), translate("Private Key Passphrase")) +o.password = true +o:depends({ [_n("protocol")] = "ssh" }) + +o = s:option(DynamicList, _n("ssh_host_key"), translate("Host Key"), translate("Accept any if empty.")) +o:depends({ [_n("protocol")] = "ssh" }) + +o = s:option(DynamicList, _n("ssh_host_key_algo"), translate("Host Key Algorithms")) +o:depends({ [_n("protocol")] = "ssh" }) + +o = s:option(Value, _n("ssh_client_version"), translate("Client Version"), translate("Random version will be used if empty.")) +o:depends({ [_n("protocol")] = "ssh" }) +-- [[ SSH config end ]] -- + o = s:option(Flag, _n("tls"), translate("TLS")) o.default = 0 o:depends({ [_n("protocol")] = "vmess" }) diff --git a/luci-app-passwall2/luasrc/passwall2/api.lua b/luci-app-passwall2/luasrc/passwall2/api.lua index 58af053..9b059df 100644 --- a/luci-app-passwall2/luasrc/passwall2/api.lua +++ b/luci-app-passwall2/luasrc/passwall2/api.lua @@ -485,6 +485,8 @@ function get_valid_nodes() protocol = "HY2" elseif protocol == "anytls" then protocol = "AnyTLS" + elseif protocol == "ssh" then + protocol = "SSH" else protocol = protocol:gsub("^%l",string.upper) end @@ -530,6 +532,8 @@ function get_node_remarks(n) protocol = "HY2" elseif protocol == "anytls" then protocol = "AnyTLS" + elseif protocol == "ssh" then + protocol = "SSH" else protocol = protocol:gsub("^%l",string.upper) end diff --git a/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua b/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua index c4eee19..79b83fb 100644 --- a/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua +++ b/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua @@ -439,6 +439,18 @@ function gen_outbound(flag, node, tag, proxy_table) } end + if node.protocol == "ssh" then + protocol_table = { + user = (node.username and node.username ~= "") and node.username or "root", + password = (node.password and node.password ~= "") and node.password or "", + private_key = node.ssh_priv_key, + private_key_passphrase = node.ssh_priv_key_pp, + host_key = node.ssh_host_key, + host_key_algorithms = node.ssh_host_key_algo, + client_version = node.ssh_client_version + } + end + if protocol_table then for key, value in pairs(protocol_table) do result[key] = value diff --git a/luci-app-passwall2/luasrc/passwall2/util_xray.lua b/luci-app-passwall2/luasrc/passwall2/util_xray.lua index 7fbd0b4..b819dc8 100644 --- a/luci-app-passwall2/luasrc/passwall2/util_xray.lua +++ b/luci-app-passwall2/luasrc/passwall2/util_xray.lua @@ -772,6 +772,10 @@ function gen_config(var) table.insert(outbounds, outbound) fallback_node_tag = outbound.tag end + else + if gen_balancer(fallback_node) then + fallback_node_tag = fallback_node_id + end end end end diff --git a/luci-app-passwall2/luasrc/view/passwall2/global/backup.htm b/luci-app-passwall2/luasrc/view/passwall2/global/backup.htm index 67aca2b..c96acda 100644 --- a/luci-app-passwall2/luasrc/view/passwall2/global/backup.htm +++ b/luci-app-passwall2/luasrc/view/passwall2/global/backup.htm @@ -184,7 +184,6 @@ local api = require "luci.passwall2.api" const chunk = base64Data.substring(currentChunk * chunkSize, (currentChunk + 1) * chunkSize); const xhr = new XMLHttpRequest(); xhr.open("POST", '<%= api.url("restore_backup") %>', true); - xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); xhr.onreadystatechange = function () { if (xhr.readyState === 4) { if (xhr.status === 200) { @@ -203,12 +202,12 @@ local api = require "luci.passwall2.api" } } }; - xhr.send( - "filename=" + encodeURIComponent(file.name) + - "&chunk=" + encodeURIComponent(chunk) + - "&chunk_index=" + currentChunk + - "&total_chunks=" + totalChunks - ); + const formData = new FormData(); + formData.append("filename", file.name); + formData.append("chunk", chunk); + formData.append("chunk_index", currentChunk); + formData.append("total_chunks", totalChunks); + xhr.send(formData); } else { //alert("Upload completed."); document.getElementById("upload-btn").value = "<%:UL Restore%>"; diff --git a/luci-app-passwall2/po/zh-cn/passwall2.po b/luci-app-passwall2/po/zh-cn/passwall2.po index c3ac43c..819935b 100644 --- a/luci-app-passwall2/po/zh-cn/passwall2.po +++ b/luci-app-passwall2/po/zh-cn/passwall2.po @@ -1645,6 +1645,15 @@ msgstr "落地节点" msgid "Only support a layer of proxy." msgstr "仅支持一层代理。" +msgid "" +"Chained proxy works only with Xray or Sing-box nodes.
" +"The chained node must be the same type as your subscription node (Xray with Xray, Sing-box with Sing-box).
" +"You can only use manual or imported nodes as chained nodes." +msgstr "" +"链式代理仅支持 Xray 与 Sing-box 节点。
" +"链式节点需与订阅节点类型一致(Xray 对应 Xray,Sing-box 对应 Sing-box)。
" +"仅支持手动添加或导入的节点用作链式节点。" + msgid "Set the default domain resolution strategy for the sing-box node." msgstr "为 sing-box 节点设置默认的域名解析策略。" @@ -1797,3 +1806,21 @@ msgstr "可以通过输入 GeoIP/Geosite,提取它们所包含的域名/IP。" msgid "Use the GeoIP/Geosite query function to verify if the entered Geo rules are correct." msgstr "利用 GeoIP/Geosite 查询功能,可以验证输入的 Geo 规则是否正确。" + +msgid "Private Key Passphrase" +msgstr "私钥指纹" + +msgid "Host Key" +msgstr "主机密钥" + +msgid "Accept any if empty." +msgstr "留空则不校验。" + +msgid "Host Key Algorithms" +msgstr "主机密钥算法" + +msgid "Client Version" +msgstr "客户端版本" + +msgid "Random version will be used if empty." +msgstr "如留空,则使用随机版本。" diff --git a/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua b/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua index 2d37277..b794d7e 100755 --- a/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua +++ b/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua @@ -35,6 +35,7 @@ local vless_type_default = uci:get(appname, "@global_subscribe[0]", "vless_type" local hysteria2_type_default = uci:get(appname, "@global_subscribe[0]", "hysteria2_type") or "hysteria2" local domain_strategy_default = uci:get(appname, "@global_subscribe[0]", "domain_strategy") or "" local domain_strategy_node = "" +local preproxy_node_group, to_node_group, chain_node_type = "", "", "" -- 判断是否过滤节点关键字 local filter_keyword_mode_default = uci:get(appname, "@global_subscribe[0]", "filter_keyword_mode") or "0" local filter_keyword_discard_list_default = uci:get(appname, "@global_subscribe[0]", "filter_discard_list") or {} @@ -1717,6 +1718,16 @@ local function update_node(manual) if kkk == "type" and vvv == "sing-box" then uci:set(appname, cfgid, "domain_strategy", domain_strategy_node) end + -- 订阅组链式代理 + if chain_node_type ~= "" and kkk == "type" and vvv == chain_node_type then + if preproxy_node_group ~="" then + uci:set(appname, cfgid, "chain_proxy", "1") + uci:set(appname, cfgid, "preproxy_node", preproxy_node_group) + elseif to_node_group ~= "" then + uci:set(appname, cfgid, "chain_proxy", "2") + uci:set(appname, cfgid, "to_node", to_node_group) + end + end end end end @@ -1919,6 +1930,22 @@ local execute = function() else domain_strategy_node = domain_strategy_default end + + -- 订阅组链式代理 + local function valid_chain_node(node) + if not node then return "" end + local cp = uci:get(appname, node, "chain_proxy") or "" + local am = uci:get(appname, node, "add_mode") or "0" + chain_node_type = (cp == "" and am ~= "2") and (uci:get(appname, node, "type") or "") or "" + if chain_node_type ~= "Xray" and chain_node_type ~= "sing-box" then + chain_node_type = "" + return "" + end + return node + end + preproxy_node_group = (value.chain_proxy == "1") and valid_chain_node(value.preproxy_node) or "" + to_node_group = (value.chain_proxy == "2") and valid_chain_node(value.to_node) or "" + local ua = value.user_agent local access_mode = value.access_mode local result = (not access_mode) and "自动" or (access_mode == "direct" and "直连访问" or (access_mode == "proxy" and "通过代理" or "自动"))