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 "自动"))