From f994078db90e30126e28c6f3885ea98670abdfee Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 20 Apr 2026 08:30:53 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=8C=B4=20Sync=202026-04-20=2008:30:53?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- luci-app-openclawmgr/Makefile | 4 +- .../luci-static/resources/openclawmgr/app.css | 10 + .../luci-static/resources/openclawmgr/app.js | 577 +++++++++++---- .../luasrc/controller/openclawmgr.lua | 39 +- .../root/usr/libexec/istorec/openclawmgr.sh | 3 + luci-app-passwall/Makefile | 2 +- .../root/usr/share/passwall/subscribe.lua | 313 +++++---- luci-app-passwall2/Makefile | 4 +- .../model/cbi/passwall2/client/global.lua | 4 +- .../model/cbi/passwall2/client/haproxy.lua | 31 +- .../cbi/passwall2/client/socks_config.lua | 47 +- .../model/cbi/passwall2/client/type/ray.lua | 35 +- .../cbi/passwall2/client/type/sing-box.lua | 22 +- .../model/cbi/passwall2/server/type/ray.lua | 27 - .../luasrc/passwall2/util_xray.lua | 26 +- .../view/passwall2/cbi/nodes_dynamiclist.htm | 149 ++++ .../passwall2/cbi/nodes_dynamiclist_com.htm | 654 ++++++++++++++++++ .../root/etc/hotplug.d/iface/98-passwall2 | 28 +- .../root/usr/share/passwall2/app.sh | 4 +- .../root/usr/share/passwall2/haproxy.lua | 79 ++- .../root/usr/share/passwall2/haproxy_check.sh | 2 +- .../root/usr/share/passwall2/iptables.sh | 71 +- .../root/usr/share/passwall2/nftables.sh | 60 +- .../root/usr/share/passwall2/subscribe.lua | 329 +++++---- 24 files changed, 1811 insertions(+), 709 deletions(-) create mode 100644 luci-app-passwall2/luasrc/view/passwall2/cbi/nodes_dynamiclist.htm create mode 100644 luci-app-passwall2/luasrc/view/passwall2/cbi/nodes_dynamiclist_com.htm diff --git a/luci-app-openclawmgr/Makefile b/luci-app-openclawmgr/Makefile index 80fb9328b..81d26be8e 100644 --- a/luci-app-openclawmgr/Makefile +++ b/luci-app-openclawmgr/Makefile @@ -1,8 +1,8 @@ include $(TOPDIR)/rules.mk -PKG_VERSION:=0.1.10 -PKG_RELEASE:=12 +PKG_VERSION:=0.1.11 +PKG_RELEASE:=13 LUCI_TITLE:=LuCI support for OpenClaw Launcher LUCI_PKGARCH:=all diff --git a/luci-app-openclawmgr/htdocs/luci-static/resources/openclawmgr/app.css b/luci-app-openclawmgr/htdocs/luci-static/resources/openclawmgr/app.css index ebb8c6abf..32e44ff42 100644 --- a/luci-app-openclawmgr/htdocs/luci-static/resources/openclawmgr/app.css +++ b/luci-app-openclawmgr/htdocs/luci-static/resources/openclawmgr/app.css @@ -828,10 +828,15 @@ .oclm-dialog { position: fixed; inset: 0; + width: 100vw; + min-width: 100vw; + height: 100vh; + min-height: 100vh; z-index: 9998; display: flex; align-items: center; justify-content: center; + box-sizing: border-box; padding: 20px; background: rgba(15, 23, 42, 0.42); } @@ -932,10 +937,15 @@ .oclm-log-modal { position: fixed; inset: 0; + width: 100vw; + min-width: 100vw; + height: 100vh; + min-height: 100vh; z-index: 9999; display: flex; align-items: center; justify-content: center; + box-sizing: border-box; padding: 20px; background: rgba(15, 23, 42, 0.42); } diff --git a/luci-app-openclawmgr/htdocs/luci-static/resources/openclawmgr/app.js b/luci-app-openclawmgr/htdocs/luci-static/resources/openclawmgr/app.js index 01efa96ce..f89126616 100644 --- a/luci-app-openclawmgr/htdocs/luci-static/resources/openclawmgr/app.js +++ b/luci-app-openclawmgr/htdocs/luci-static/resources/openclawmgr/app.js @@ -65,6 +65,14 @@ } }; var styleText = ""; + var noticeOverlay = null; + var noticeCard = null; + var noticeTitle = null; + var noticeMessage = null; + var noticeCloseButton = null; + var actionDialogOverlay = null; + var actionDialogCard = null; + state.formErrors = state.formErrors || { base_dir: "" }; function request(url, options) { options = options || {}; @@ -197,6 +205,358 @@ return state.security; } + function ensureNoticeDialog() { + if (noticeOverlay && noticeOverlay.parentNode) { + return; + } + + noticeOverlay = document.createElement("div"); + noticeOverlay.style.position = "fixed"; + noticeOverlay.style.inset = "0"; + noticeOverlay.style.zIndex = "2147483647"; + noticeOverlay.style.display = "none"; + noticeOverlay.style.alignItems = "center"; + noticeOverlay.style.justifyContent = "center"; + noticeOverlay.style.padding = "20px"; + noticeOverlay.style.boxSizing = "border-box"; + noticeOverlay.style.background = "rgba(15, 23, 42, 0.42)"; + + noticeCard = document.createElement("div"); + noticeCard.style.width = "min(560px, calc(100vw - 40px))"; + noticeCard.style.borderRadius = "18px"; + noticeCard.style.border = "1px solid rgba(217, 225, 238, 0.95)"; + noticeCard.style.background = "#ffffff"; + noticeCard.style.boxShadow = "0 10px 28px rgba(15, 23, 42, 0.18)"; + noticeCard.style.padding = "20px"; + noticeCard.style.boxSizing = "border-box"; + noticeCard.style.color = "#1f2937"; + + noticeTitle = document.createElement("h3"); + noticeTitle.style.margin = "0 0 10px"; + noticeTitle.style.fontSize = "18px"; + noticeTitle.style.fontWeight = "700"; + + noticeMessage = document.createElement("div"); + noticeMessage.style.marginBottom = "16px"; + noticeMessage.style.color = "#6b7280"; + noticeMessage.style.lineHeight = "1.6"; + + var footer = document.createElement("div"); + footer.style.display = "flex"; + footer.style.justifyContent = "flex-end"; + footer.style.gap = "14px"; + + noticeCloseButton = document.createElement("button"); + noticeCloseButton.type = "button"; + noticeCloseButton.textContent = "知道了"; + noticeCloseButton.style.appearance = "none"; + noticeCloseButton.style.display = "inline-flex"; + noticeCloseButton.style.alignItems = "center"; + noticeCloseButton.style.justifyContent = "center"; + noticeCloseButton.style.border = "1px solid #d9e1ee"; + noticeCloseButton.style.background = "#2f67f6"; + noticeCloseButton.style.color = "#ffffff"; + noticeCloseButton.style.borderRadius = "10px"; + noticeCloseButton.style.height = "36px"; + noticeCloseButton.style.minWidth = "96px"; + noticeCloseButton.style.padding = "0 14px"; + noticeCloseButton.style.fontSize = "13px"; + noticeCloseButton.style.fontWeight = "600"; + noticeCloseButton.style.cursor = "pointer"; + noticeCloseButton.onclick = function() { + closeNoticeDialog(); + }; + + footer.appendChild(noticeCloseButton); + noticeCard.appendChild(noticeTitle); + noticeCard.appendChild(noticeMessage); + noticeCard.appendChild(footer); + noticeOverlay.appendChild(noticeCard); + document.body.appendChild(noticeOverlay); + } + + function openNoticeDialog(message, title) { + ensureNoticeDialog(); + noticeTitle.textContent = String(title || "提示"); + noticeMessage.textContent = String(message || ""); + noticeOverlay.style.display = "flex"; + } + + function closeNoticeDialog() { + if (!noticeOverlay) { + return; + } + noticeOverlay.style.display = "none"; + } + + function ensureActionDialog() { + if (actionDialogOverlay && actionDialogOverlay.parentNode) { + return; + } + + actionDialogOverlay = document.createElement("div"); + actionDialogOverlay.style.position = "fixed"; + actionDialogOverlay.style.inset = "0"; + actionDialogOverlay.style.zIndex = "2147483646"; + actionDialogOverlay.style.display = "none"; + actionDialogOverlay.style.alignItems = "center"; + actionDialogOverlay.style.justifyContent = "center"; + actionDialogOverlay.style.padding = "20px"; + actionDialogOverlay.style.boxSizing = "border-box"; + actionDialogOverlay.style.background = "rgba(15, 23, 42, 0.42)"; + + actionDialogCard = document.createElement("div"); + actionDialogCard.style.width = "min(560px, calc(100vw - 40px))"; + actionDialogCard.style.borderRadius = "18px"; + actionDialogCard.style.border = "1px solid rgba(217, 225, 238, 0.95)"; + actionDialogCard.style.background = "#ffffff"; + actionDialogCard.style.boxShadow = "0 10px 28px rgba(15, 23, 42, 0.18)"; + actionDialogCard.style.padding = "20px"; + actionDialogCard.style.boxSizing = "border-box"; + actionDialogCard.style.color = "#1f2937"; + + actionDialogOverlay.appendChild(actionDialogCard); + document.body.appendChild(actionDialogOverlay); + } + + function makeActionDialogText(text, muted) { + var el = document.createElement("div"); + el.textContent = String(text || ""); + el.style.marginBottom = "10px"; + el.style.lineHeight = "1.6"; + el.style.color = muted ? "#6b7280" : "#1f2937"; + return el; + } + + function makeActionDialogCode(text) { + var el = document.createElement("code"); + el.textContent = String(text || ""); + el.style.display = "block"; + el.style.marginBottom = "14px"; + el.style.padding = "10px 12px"; + el.style.borderRadius = "12px"; + el.style.background = "rgba(47, 103, 246, 0.08)"; + el.style.color = "#1f2937"; + el.style.overflowWrap = "anywhere"; + return el; + } + + function makeActionDialogFooter() { + var el = document.createElement("div"); + el.style.display = "flex"; + el.style.justifyContent = "flex-end"; + el.style.gap = "14px"; + el.style.marginTop = "12px"; + return el; + } + + function makeActionDialogButton(label, kind, onclick) { + var button = document.createElement("button"); + button.type = "button"; + button.textContent = String(label || ""); + button.style.appearance = "none"; + button.style.display = "inline-flex"; + button.style.alignItems = "center"; + button.style.justifyContent = "center"; + button.style.border = "1px solid " + (kind === "danger" ? "#ef4444" : kind === "primary" ? "#2f67f6" : "#d9e1ee"); + button.style.background = kind === "danger" ? "#ef4444" : kind === "primary" ? "#2f67f6" : "#ffffff"; + button.style.color = kind === "default" ? "#1f2937" : "#ffffff"; + button.style.borderRadius = "10px"; + button.style.minWidth = "96px"; + button.style.minHeight = "36px"; + button.style.padding = "0 14px"; + button.style.fontSize = "13px"; + button.style.fontWeight = "600"; + button.style.cursor = "pointer"; + button.onclick = onclick; + return button; + } + + function makeActionDialogOption(title, note, onclick) { + var button = document.createElement("button"); + var titleEl = document.createElement("span"); + var noteEl = document.createElement("span"); + + button.type = "button"; + button.style.appearance = "none"; + button.style.display = "flex"; + button.style.flexDirection = "column"; + button.style.alignItems = "flex-start"; + button.style.justifyContent = "flex-start"; + button.style.gap = "4px"; + button.style.width = "100%"; + button.style.padding = "12px 14px"; + button.style.border = "1px solid #d9e1ee"; + button.style.borderRadius = "12px"; + button.style.background = "#ffffff"; + button.style.cursor = "pointer"; + button.style.textAlign = "left"; + button.onclick = onclick; + + titleEl.textContent = String(title || ""); + titleEl.style.color = "#1f2937"; + titleEl.style.fontWeight = "700"; + + noteEl.textContent = String(note || ""); + noteEl.style.color = "#6b7280"; + noteEl.style.fontSize = "12px"; + noteEl.style.fontWeight = "500"; + + button.appendChild(titleEl); + button.appendChild(noteEl); + return button; + } + + function hideActionDialog() { + if (actionDialogOverlay) { + actionDialogOverlay.style.display = "none"; + } + } + + function syncActionDialog() { + ensureActionDialog(); + while (actionDialogCard.firstChild) { + actionDialogCard.removeChild(actionDialogCard.firstChild); + } + + var security = getSecurityState(); + var deleteDialogOpen = security.deleteTargetId != null; + var addDialogOpen = !!security.pendingAddPath; + + if (!deleteDialogOpen && !addDialogOpen) { + hideActionDialog(); + return; + } + + var title = document.createElement("h3"); + title.style.margin = "0 0 12px"; + title.style.fontSize = "18px"; + title.style.fontWeight = "700"; + actionDialogCard.appendChild(title); + + if (deleteDialogOpen) { + var actions = document.createElement("div"); + title.textContent = "删除禁止访问目录"; + actionDialogCard.appendChild(makeActionDialogText("确定要从保护列表中移除以下目录吗?", true)); + actionDialogCard.appendChild(makeActionDialogCode(security.deleteTargetPath || "")); + actionDialogCard.appendChild(makeActionDialogText("请选择删除方式:", true)); + + actions.style.display = "grid"; + actions.style.gap = "10px"; + actions.appendChild(makeActionDialogOption("直接删除", "仅从列表移除,不改变目录权限", function() { + handleSecurityDelete("direct"); + })); + actions.appendChild(makeActionDialogOption("恢复其他用户组可访问(drwxr-xr-x)", "移除并恢复权限", function() { + handleSecurityDelete("restore"); + })); + actionDialogCard.appendChild(actions); + + var deleteFooter = makeActionDialogFooter(); + deleteFooter.appendChild(makeActionDialogButton("取消", "default", function() { + var nextSecurity = getSecurityState(); + nextSecurity.deleteTargetId = null; + nextSecurity.deleteTargetPath = ""; + render(); + })); + actionDialogCard.appendChild(deleteFooter); + } else if (addDialogOpen) { + title.textContent = "确认添加禁止访问目录"; + actionDialogCard.appendChild(makeActionDialogText("添加后将尝试修改该目录权限,限制 OpenClaw 对其访问:", true)); + actionDialogCard.appendChild(makeActionDialogCode(security.pendingAddPath || "")); + + var warning = document.createElement("div"); + warning.style.marginTop = "4px"; + warning.style.marginBottom = "14px"; + warning.style.padding = "12px 14px"; + warning.style.border = "1px solid rgba(245, 158, 11, 0.24)"; + warning.style.borderRadius = "12px"; + warning.style.background = "rgba(245, 158, 11, 0.08)"; + + var warningTitle = document.createElement("div"); + warningTitle.textContent = "请确认你已知晓以下风险:"; + warningTitle.style.marginBottom = "8px"; + warningTitle.style.color = "#1f2937"; + warningTitle.style.fontSize = "13px"; + warningTitle.style.fontWeight = "700"; + + var warningList = document.createElement("ul"); + warningList.style.margin = "0"; + warningList.style.paddingLeft = "18px"; + warningList.style.color = "#6b7280"; + + [ + "该目录的 Linux 权限会被修改为仅允许 root 和 root 组访问。", + "这可能影响其他服务、用户或脚本对该目录的正常访问。", + "本功能不是容器或沙箱隔离,只是基于系统文件权限限制。", + "请勿将 OpenClaw 自身的数据目录、程序目录加入禁止访问列表。" + ].forEach(function(text) { + var item = document.createElement("li"); + item.textContent = text; + warningList.appendChild(item); + }); + + warning.appendChild(warningTitle); + warning.appendChild(warningList); + actionDialogCard.appendChild(warning); + + var addFooter = makeActionDialogFooter(); + addFooter.appendChild(makeActionDialogButton("取消", "default", function() { + var nextSecurity = getSecurityState(); + nextSecurity.pendingAddPath = ""; + render(); + })); + addFooter.appendChild(makeActionDialogButton("确认添加", "danger", function() { + handleSecurityAddConfirm(); + })); + actionDialogCard.appendChild(addFooter); + } + + actionDialogOverlay.style.display = "flex"; + } + + function handleSecurityAddConfirm() { + var security = getSecurityState(); + var path = security.pendingAddPath || ""; + if (!path) { + render(); + return; + } + postJson(config.securityAddUrl, { path: path }).then(function(rv) { + if (!rv || !rv.ok) { + security.pendingAddPath = ""; + security.directoryError = (rv && rv.error) || "添加目录失败"; + render(); + return; + } + security.pendingAddPath = ""; + security.newDirectoryPath = ""; + security.directoryError = ""; + loadSecurityData(); + }).catch(function() { + security.pendingAddPath = ""; + security.directoryError = "添加目录失败"; + render(); + }); + } + + function handleSecurityDelete(mode) { + var security = getSecurityState(); + if (security.deleteTargetId == null) { + return; + } + postJson(config.securityRemoveUrl, { id: security.deleteTargetId, mode: mode }).then(function(rv) { + if (!rv || !rv.ok) { + openNoticeDialog((rv && rv.error) || (mode === "restore" ? "恢复并删除失败" : "删除失败")); + return; + } + security.deleteTargetId = null; + security.deleteTargetPath = ""; + loadSecurityData(); + }).catch(function() { + openNoticeDialog(mode === "restore" ? "恢复并删除失败" : "删除失败"); + }); + } + function securityDataDirectory() { var formBaseDir = state.form && state.form.base_dir ? String(state.form.base_dir) : ""; var statusBaseDir = state.status && state.status.base_dir ? String(state.status.base_dir) : ""; @@ -204,6 +564,19 @@ return formBaseDir || statusBaseDir || suggestedBaseDir || ""; } + function getFormErrors() { + state.formErrors = state.formErrors || { base_dir: "" }; + if (state.formErrors.base_dir == null) { + state.formErrors.base_dir = ""; + } + return state.formErrors; + } + + function isBaseDirErrorMessage(message) { + var text = String(message || ""); + return text.indexOf("数据目录") >= 0 || text.indexOf("base_dir") >= 0; + } + function validateProtectedDirectory(path) { var security = getSecurityState(); var value = String(path || "").replace(/^\s+|\s+$/g, ""); @@ -443,7 +816,7 @@ function showTaskLog(taskId) { taskId = taskId || "openclawmgr"; if (!taskWindowAvailable()) { - window.alert("任务窗口不可用(未加载 taskd/xterm)。请安装 luci-lib-taskd 和 luci-lib-xterm,然后强制刷新页面。"); + openNoticeDialog("任务窗口不可用(未加载 taskd/xterm)。请安装 luci-lib-taskd 和 luci-lib-xterm,然后强制刷新页面。"); return; } var now = Date.now ? Date.now() : (+new Date()); @@ -548,6 +921,7 @@ function render() { var status = state.status || {}; var form = state.form || {}; + var formErrors = getFormErrors(); var options = state.options || { base_dir_choices: [] }; var security = getSecurityState(); var allowedOrigins = Array.isArray(form.allowed_origins) ? form.allowed_origins : []; @@ -590,9 +964,6 @@ '' + ''; }).join(""); - var addDialogOpen = !!security.pendingAddPath; - var deleteDialogOpen = security.deleteTargetId != null; - var canStartService = !status.running && !status.reachable; var canStopService = !!status.running; @@ -648,7 +1019,8 @@ ["loopback", "仅本机"], ["auto", "自动"] ])) + - fieldInput("数据目录", '' + baseDirOptions + '') + + fieldInput("数据目录", '' + baseDirOptions + '' + + '
' + escapeHtml(formErrors.base_dir || "") + '
') + '
' + '' + '' + @@ -745,45 +1117,10 @@ '' + '' + '
' + - '' + - '
' + - '
' + - '

删除禁止访问目录

' + - '
确定要从保护列表中移除以下目录吗?
' + - '' + escapeHtml(security.deleteTargetPath || "") + '' + - '
请选择删除方式:
' + - '
' + - '' + - '' + - '
' + - '' + - '
' + - '
' + - '
' + - '
' + - '

确认添加禁止访问目录

' + - '
添加后将尝试修改该目录权限,限制 OpenClaw 对其访问:
' + - '' + escapeHtml(security.pendingAddPath || "") + '' + - '
' + - '
请确认你已知晓以下风险:
' + - '
    ' + - '
  • 该目录的 Linux 权限会被修改为仅允许 root 和 root 组访问。
  • ' + - '
  • 这可能影响其他服务、用户或脚本对该目录的正常访问。
  • ' + - '
  • 本功能不是容器或沙箱隔离,只是基于系统文件权限限制。
  • ' + - '
  • 请勿将 OpenClaw 自身的数据目录、程序目录加入禁止访问列表。
  • ' + - '
' + - '
' + - '' + - '
' + - '
' + '' + ''; + syncActionDialog(); bindEvents(); } @@ -1036,7 +1373,7 @@ showTaskLog(rv.running_task_id); return; } - window.alert((rv && rv.error) || "操作失败"); + openNoticeDialog((rv && rv.error) || "操作失败"); return; } state.lastTaskRunning = true; @@ -1049,12 +1386,12 @@ Array.prototype.forEach.call(root.querySelectorAll("[data-open-console]"), function(el) { el.onclick = function() { if (!state.consoleReady) { - window.alert("控制台正在启动,请稍候再试。"); + openNoticeDialog("控制台正在启动,请稍候再试。"); return; } var url = (state.status && state.status.token_url) || ""; if (!url) { - window.alert("控制台地址不可用"); + openNoticeDialog("控制台地址不可用"); return; } window.open(url, "_blank", "noreferrer"); @@ -1068,7 +1405,7 @@ var baseDirEl = root.getElementById("oclm-base-dir"); var baseDir = baseDirEl && baseDirEl.value ? baseDirEl.value.trim() : ""; if (!baseDir) { - window.alert("请先选择数据目录并保存应用,再执行安装。"); + openNoticeDialog("请先选择数据目录并保存应用,再执行安装。"); if (baseDirEl) baseDirEl.focus(); return; } @@ -1083,7 +1420,7 @@ if (!cfgRv || !cfgRv.ok) { state.status.installing = false; render(); - window.alert((cfgRv && cfgRv.error) || "保存安装选项失败"); + openNoticeDialog((cfgRv && cfgRv.error) || "保存安装选项失败"); return; } state.form.install_accelerated = accelerated; @@ -1099,7 +1436,7 @@ } state.status.installing = false; render(); - window.alert((rv && rv.error) || "启动安装失败"); + openNoticeDialog((rv && rv.error) || "启动安装失败"); return; } state.lastTaskRunning = true; @@ -1108,7 +1445,7 @@ }).catch(function() { state.status.installing = false; render(); - window.alert("启动安装失败"); + openNoticeDialog("启动安装失败"); }); }; }); @@ -1121,7 +1458,7 @@ var baseDir = baseDirEl && baseDirEl.value ? baseDirEl.value.trim() : ""; var op = (state.status && state.status.installed) ? "upgrade" : "install"; if (!baseDir) { - window.alert("请先选择数据目录并保存应用,再执行安装。"); + openNoticeDialog("请先选择数据目录并保存应用,再执行安装。"); if (baseDirEl) baseDirEl.focus(); return; } @@ -1138,7 +1475,7 @@ state.status.task_running = false; state.status.task_op = ""; render(); - window.alert((cfgRv && cfgRv.error) || "保存安装选项失败"); + openNoticeDialog((cfgRv && cfgRv.error) || "保存安装选项失败"); return; } state.form.install_accelerated = accelerated; @@ -1156,7 +1493,7 @@ state.status.task_running = false; state.status.task_op = ""; render(); - window.alert((rv && rv.error) || "启动安装失败"); + openNoticeDialog((rv && rv.error) || "启动安装失败"); return; } state.lastTaskRunning = true; @@ -1167,7 +1504,7 @@ state.status.task_running = false; state.status.task_op = ""; render(); - window.alert("启动安装失败"); + openNoticeDialog("启动安装失败"); }); }; }); @@ -1191,7 +1528,7 @@ updateCheck.localVersion = String((rv && rv.local_version) || (state.status && state.status.openclaw_version) || ""); updateCheck.remoteVersion = String((rv && rv.remote_version) || ""); render(); - window.alert(updateCheck.error); + openNoticeDialog(updateCheck.error); return; } updateCheck.checked = true; @@ -1206,7 +1543,7 @@ updateCheck.hasUpdate = false; updateCheck.error = "检测更新失败"; render(); - window.alert("检测更新失败"); + openNoticeDialog("检测更新失败"); }); }; }); @@ -1349,55 +1686,18 @@ }; } - var securityAddCancel = root.getElementById("oclm-security-add-cancel"); - if (securityAddCancel) { - securityAddCancel.onclick = function() { - var security = getSecurityState(); - security.pendingAddPath = ""; - render(); - }; - } - - var securityAddConfirm = root.getElementById("oclm-security-add-confirm"); - if (securityAddConfirm) { - securityAddConfirm.onclick = function() { - var security = getSecurityState(); - var path = security.pendingAddPath || ""; - if (!path) { - render(); - return; - } - postJson(config.securityAddUrl, { path: path }).then(function(rv) { - if (!rv || !rv.ok) { - security.pendingAddPath = ""; - security.directoryError = (rv && rv.error) || "添加目录失败"; - render(); - return; - } - security.pendingAddPath = ""; - security.newDirectoryPath = ""; - security.directoryError = ""; - loadSecurityData(); - }).catch(function() { - security.pendingAddPath = ""; - security.directoryError = "添加目录失败"; - render(); - }); - }; - } - Array.prototype.forEach.call(root.querySelectorAll("[data-security-recheck]"), function(el) { el.onclick = function() { var id = el.getAttribute("data-security-recheck"); if (!id) return; postJson(config.securityRecheckUrl, { id: id }).then(function(rv) { if (!rv || !rv.ok) { - window.alert((rv && rv.error) || "重新检测失败"); + openNoticeDialog((rv && rv.error) || "重新检测失败"); return; } loadSecurityData(); }).catch(function() { - window.alert("重新检测失败"); + openNoticeDialog("重新检测失败"); }); }; }); @@ -1420,61 +1720,31 @@ }; }); - var securityDeleteDirect = root.getElementById("oclm-security-delete-direct"); - if (securityDeleteDirect) { - securityDeleteDirect.onclick = function() { - var security = getSecurityState(); - if (security.deleteTargetId == null) return; - postJson(config.securityRemoveUrl, { id: security.deleteTargetId, mode: "direct" }).then(function(rv) { - if (!rv || !rv.ok) { - window.alert((rv && rv.error) || "删除失败"); - return; - } - security.deleteTargetId = null; - security.deleteTargetPath = ""; - loadSecurityData(); - }).catch(function() { - window.alert("删除失败"); - }); - }; - } - - var securityDeleteRestore = root.getElementById("oclm-security-delete-restore"); - if (securityDeleteRestore) { - securityDeleteRestore.onclick = function() { - var security = getSecurityState(); - if (security.deleteTargetId == null) return; - postJson(config.securityRemoveUrl, { id: security.deleteTargetId, mode: "restore" }).then(function(rv) { - if (!rv || !rv.ok) { - window.alert((rv && rv.error) || "恢复并删除失败"); - return; - } - security.deleteTargetId = null; - security.deleteTargetPath = ""; - loadSecurityData(); - }).catch(function() { - window.alert("恢复并删除失败"); - }); - }; - } - - var securityDeleteCancel = root.getElementById("oclm-security-delete-cancel"); - if (securityDeleteCancel) { - securityDeleteCancel.onclick = function() { - var security = getSecurityState(); - security.deleteTargetId = null; - security.deleteTargetPath = ""; - render(); - }; - } - var saveBasic = root.getElementById("oclm-save-basic"); + var baseDirInput = root.getElementById("oclm-base-dir"); + if (baseDirInput) { + baseDirInput.oninput = function() { + var formErrors = getFormErrors(); + if (formErrors.base_dir) { + formErrors.base_dir = ""; + render(); + } + }; + } if (saveBasic) { saveBasic.onclick = function() { + var formErrors = getFormErrors(); + var baseDirValue = root.getElementById("oclm-base-dir").value; + if (!String(baseDirValue || "").replace(/^\s+|\s+$/g, "")) { + formErrors.base_dir = "数据目录不能为空"; + render(); + return; + } + formErrors.base_dir = ""; var payload = { port: root.getElementById("oclm-port").value, bind: root.getElementById("oclm-bind").value, - base_dir: root.getElementById("oclm-base-dir").value, + base_dir: baseDirValue, install_accelerated: !!(root.getElementById("oclm-install-accelerated") && root.getElementById("oclm-install-accelerated").checked), }; state.savingSection = "basic"; @@ -1482,7 +1752,7 @@ postJson(config.configUrl, payload).then(function(rv) { handleSaveResult(rv, "basic"); }).catch(function() { state.savingSection = ""; render(); - window.alert("保存失败"); + openNoticeDialog("保存失败"); }); }; } @@ -1494,7 +1764,7 @@ var modelValue = normalizeModelValue(root.getElementById("oclm-model").value, defaultModelForAgent(agentValue)); var baseUrlValue = root.getElementById("oclm-base-url").value; if (agentValue === "custom-provider" && !String(baseUrlValue || "").replace(/^\s+|\s+$/g, "")) { - window.alert("自定义供应商必须填写服务地址"); + openNoticeDialog("自定义供应商必须填写服务地址"); return; } var payload = { @@ -1510,7 +1780,7 @@ postJson(config.configUrl, payload).then(function(rv) { handleSaveResult(rv, "ai"); }).catch(function() { state.savingSection = ""; render(); - window.alert("保存失败"); + openNoticeDialog("保存失败"); }); }; } @@ -1529,7 +1799,7 @@ postJson(config.configUrl, payload).then(function(rv) { handleSaveResult(rv, "access"); }).catch(function() { state.savingSection = ""; render(); - window.alert("保存失败"); + openNoticeDialog("保存失败"); }); }; } @@ -1537,16 +1807,25 @@ function handleSaveResult(rv, section) { if (!rv || !rv.ok) { + var formErrors = getFormErrors(); state.savingSection = ""; + if (section === "basic" && isBaseDirErrorMessage(rv && rv.error)) { + formErrors.base_dir = (rv && rv.error) || "数据目录错误"; + render(); + return; + } render(); - window.alert((rv && rv.error) || "保存失败"); + openNoticeDialog((rv && rv.error) || "保存失败"); return; } + if (section === "basic") { + getFormErrors().base_dir = ""; + } postForm(config.applyUrl, {}).then(function(applyRv) { state.savingSection = ""; if (!applyRv || !applyRv.ok) { render(); - window.alert((applyRv && applyRv.error) || "应用配置失败"); + openNoticeDialog((applyRv && applyRv.error) || "应用配置失败"); loadConfig(); scheduleStatusRefresh(3, 600); return; @@ -1558,7 +1837,7 @@ }).catch(function() { state.savingSection = ""; render(); - window.alert("应用配置失败"); + openNoticeDialog("应用配置失败"); loadConfig(); scheduleStatusRefresh(3, 600); }); diff --git a/luci-app-openclawmgr/luasrc/controller/openclawmgr.lua b/luci-app-openclawmgr/luasrc/controller/openclawmgr.lua index fd7138022..2d0b82e47 100644 --- a/luci-app-openclawmgr/luasrc/controller/openclawmgr.lua +++ b/luci-app-openclawmgr/luasrc/controller/openclawmgr.lua @@ -211,6 +211,37 @@ local function trim(v) return tostring(v or ""):gsub("^%s+", ""):gsub("%s+$", "") end +local function normalize_dir_path(path) + path = trim(path) + if path == "" then + return "" + end + if path ~= "/" then + path = path:gsub("/+$", "") + if path == "" then + path = "/" + end + end + return path +end + +local function validate_base_dir_input(path) + path = normalize_dir_path(path) + if path == "" then + return false, "数据目录不能为空" + end + if not path:match("^/") then + return false, "数据目录必须是绝对路径" + end + if path == "/" then + return false, "数据目录不能使用 / 根目录" + end + if path == "/root" or path:match("^/root/") then + return false, "数据目录不能使用 /root 开头的目录" + end + return true, nil, path +end + local function split_dots(v) local parts = {} for part in tostring(v or ""):gmatch("[^%.]+") do @@ -1231,11 +1262,13 @@ function action_config_data() end if has("base_dir") then - local base_dir = tostring(body.base_dir or "") - if base_dir == "" then - write_json({ ok = false, error = "base_dir required" }) + local ok, err, normalized = validate_base_dir_input(body.base_dir or "") + if not ok then + write_json({ ok = false, error = err }) return end + body.base_dir = normalized + local base_dir = normalized uci:set("openclawmgr", section, "base_dir", base_dir) end diff --git a/luci-app-openclawmgr/root/usr/libexec/istorec/openclawmgr.sh b/luci-app-openclawmgr/root/usr/libexec/istorec/openclawmgr.sh index 432346d46..ba67e0594 100755 --- a/luci-app-openclawmgr/root/usr/libexec/istorec/openclawmgr.sh +++ b/luci-app-openclawmgr/root/usr/libexec/istorec/openclawmgr.sh @@ -24,6 +24,9 @@ validate_base_dir() { *) return 1 ;; esac [ "$dir" != "/" ] || return 1 + case "$dir" in + /root|/root/*) return 1 ;; + esac case "$dir" in *"/../"*|*"/.."|*"/./"*|*"/.") return 1 ;; esac diff --git a/luci-app-passwall/Makefile b/luci-app-passwall/Makefile index b38b7f22b..0b58d08bd 100644 --- a/luci-app-passwall/Makefile +++ b/luci-app-passwall/Makefile @@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-passwall PKG_VERSION:=26.4.15 -PKG_RELEASE:=83 +PKG_RELEASE:=84 PKG_PO_VERSION:=$(PKG_VERSION) PKG_CONFIG_DEPENDS:= \ diff --git a/luci-app-passwall/root/usr/share/passwall/subscribe.lua b/luci-app-passwall/root/usr/share/passwall/subscribe.lua index 478e07e5c..1ff26687b 100755 --- a/luci-app-passwall/root/usr/share/passwall/subscribe.lua +++ b/luci-app-passwall/root/usr/share/passwall/subscribe.lua @@ -30,60 +30,61 @@ local has_trojan_plus = api.is_finded("trojan-plus") local has_singbox = api.finded_com("sing-box") local has_xray = api.finded_com("xray") local has_hysteria2 = api.finded_com("hysteria") -local allowInsecure_default = nil +local DEFAULT_ALLOWINSECURE = true +local DEFAULT_FILTER_KEYWORD_MODE = uci:get(appname, "@global_subscribe[0]", "filter_keyword_mode") or "0" +local DEFAULT_FILTER_KEYWORD_DISCARD_LIST = uci:get(appname, "@global_subscribe[0]", "filter_discard_list") or {} +local DEFAULT_FILTER_KEYWORD_KEEP_LIST = uci:get(appname, "@global_subscribe[0]", "filter_keep_list") or {} -- 取节点使用core类型(节点订阅页面未设置时,自动取默认) -local ss_type_default = api.get_core("ss_type", {{has_ss,"shadowsocks-libev"},{has_ss_rust,"shadowsocks-rust"},{has_singbox,"sing-box"},{has_xray,"xray"}}) -local trojan_type_default = api.get_core("trojan_type", {{has_trojan_plus,"trojan-plus"},{has_singbox,"sing-box"},{has_xray,"xray"}}) -local vmess_type_default = api.get_core("vmess_type", {{has_xray,"xray"},{has_singbox,"sing-box"}}) -local vless_type_default = api.get_core("vless_type", {{has_xray,"xray"},{has_singbox,"sing-box"}}) -local hysteria2_type_default = api.get_core("hysteria2_type", {{has_hysteria2,"hysteria2"},{has_singbox,"sing-box"},{has_xray,"xray"}}) +local DEFAULT_SS_TYPE = api.get_core("ss_type", {{has_ss,"shadowsocks-libev"},{has_ss_rust,"shadowsocks-rust"},{has_singbox,"sing-box"},{has_xray,"xray"}}) +local DEFAULT_TROJAN_TYPE = api.get_core("trojan_type", {{has_trojan_plus,"trojan-plus"},{has_singbox,"sing-box"},{has_xray,"xray"}}) +local DEFAULT_VMESS_TYPE = api.get_core("vmess_type", {{has_xray,"xray"},{has_singbox,"sing-box"}}) +local DEFAULT_VLESS_TYPE = api.get_core("vless_type", {{has_xray,"xray"},{has_singbox,"sing-box"}}) +local DEFAULT_HYSTERIA2_TYPE = api.get_core("hysteria2_type", {{has_hysteria2,"hysteria2"},{has_singbox,"sing-box"},{has_xray,"xray"}}) local core_has = { - ["xray"] = has_xray, ["sing-box"] = has_singbox, ["shadowsocks-libev"] = has_ss,["shadowsocks-rust"] = has_ss_rust, - ["trojan-plus"] = has_trojan_plus, ["hysteria2"] = has_hysteria2 + ["xray"] = has_xray, + ["sing-box"] = has_singbox, + ["shadowsocks-libev"] = has_ss, + ["shadowsocks-rust"] = has_ss_rust, + ["trojan-plus"] = has_trojan_plus, + ["hysteria2"] = has_hysteria2 } ----- -local domain_resolver, domain_resolver_dns, domain_resolver_dns_https, domain_strategy -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 {} -local filter_keyword_keep_list_default = uci:get(appname, "@global_subscribe[0]", "filter_keep_list") or {} -local function is_filter_keyword(value) - if filter_keyword_mode_default == "1" then - for k,v in ipairs(filter_keyword_discard_list_default) do +local function is_filter_keyword(mode, discard_list, keep_list, value) + if mode == "1" then + for k,v in ipairs(discard_list) do if value:find(v, 1, true) then return true end end - elseif filter_keyword_mode_default == "2" then + elseif mode == "2" then local result = true - for k,v in ipairs(filter_keyword_keep_list_default) do + for k,v in ipairs(keep_list) do if value:find(v, 1, true) then result = false end end return result - elseif filter_keyword_mode_default == "3" then + elseif mode == "3" then local result = false - for k,v in ipairs(filter_keyword_discard_list_default) do + for k,v in ipairs(discard_list) do if value:find(v, 1, true) then result = true end end - for k,v in ipairs(filter_keyword_keep_list_default) do + for k,v in ipairs(keep_list) do if value:find(v, 1, true) then result = false end end return result - elseif filter_keyword_mode_default == "4" then + elseif mode == "4" then local result = true - for k,v in ipairs(filter_keyword_keep_list_default) do + for k,v in ipairs(keep_list) do if value:find(v, 1, true) then result = false end end - for k,v in ipairs(filter_keyword_discard_list_default) do + for k,v in ipairs(discard_list) do if value:find(v, 1, true) then result = true end @@ -448,16 +449,16 @@ local function get_subscribe_info(cfgid, value) end -- 设置 ss 协议实现类型 -local function set_ss_implementation(result) - if ss_type_default == "shadowsocks-libev" and has_ss then +local function set_ss_implementation(ss_type, result) + if ss_type == "shadowsocks-libev" and has_ss then result.type = "SS" - elseif ss_type_default == "shadowsocks-rust" and has_ss_rust then + elseif ss_type == "shadowsocks-rust" and has_ss_rust then result.type = 'SS-Rust' - elseif ss_type_default == "xray" and has_xray then + elseif ss_type == "xray" and has_xray then result.type = 'Xray' result.protocol = 'shadowsocks' result.transport = 'raw' - elseif ss_type_default == "sing-box" and has_singbox then + elseif ss_type == "sing-box" and has_singbox then result.type = 'sing-box' result.protocol = 'shadowsocks' else @@ -468,8 +469,60 @@ local function set_ss_implementation(result) end -- 处理数据 -local function processData(szType, content, add_mode, group) - --log(content, add_mode, group) +local function processData(szType, content, add_mode, group, sub_cfg) + --log(2, content, add_mode, group) + local default_allowinsecure = DEFAULT_ALLOWINSECURE + default_filter_keyword_mode = DEFAULT_FILTER_KEYWORD_MODE + default_filter_keyword_discard_list = DEFAULT_FILTER_KEYWORD_DISCARD_LIST + default_filter_keyword_keep_list = DEFAULT_FILTER_KEYWORD_KEEP_LIST + local default_ss_type = DEFAULT_SS_TYPE + local default_trojan_type = DEFAULT_TROJAN_TYPE + local default_vmess_type = DEFAULT_VMESS_TYPE + local default_vless_type = DEFAULT_VLESS_TYPE + local default_hysteria2_type = DEFAULT_HYSTERIA2_TYPE + if sub_cfg then + if sub_cfg.allowInsecure and sub_cfg.allowInsecure ~= "1" then + default_allowinsecure = nil + end + local filter_keyword_mode = sub_cfg.filter_keyword_mode or "5" -- 5 is global + if filter_keyword_mode == "0" then + default_filter_keyword_mode = "0" + elseif filter_keyword_mode == "1" then + default_filter_keyword_mode = "1" + default_filter_keyword_discard_list = sub_cfg.filter_discard_list or {} + elseif filter_keyword_mode == "2" then + default_filter_keyword_mode = "2" + default_filter_keyword_keep_list = sub_cfg.filter_keep_list or {} + elseif filter_keyword_mode == "3" then + default_filter_keyword_mode = "3" + default_filter_keyword_keep_list = sub_cfg.filter_keep_list or {} + default_filter_keyword_discard_list = sub_cfg.filter_discard_list or {} + elseif filter_keyword_mode == "4" then + default_filter_keyword_mode = "4" + default_filter_keyword_keep_list = sub_cfg.filter_keep_list or {} + default_filter_keyword_discard_list = sub_cfg.filter_discard_list or {} + end + local ss_type = sub_cfg.ss_type or "global" + if ss_type ~= "global" and core_has[ss_type] then + default_ss_type = ss_type + end + local trojan_type = sub_cfg.trojan_type or "global" + if trojan_type ~= "global" and core_has[trojan_type] then + default_trojan_type = trojan_type + end + local vmess_type = sub_cfg.vmess_type or "global" + if vmess_type ~= "global" and core_has[vmess_type] then + default_vmess_type = vmess_type + end + local vless_type = sub_cfg.vless_type or "global" + if vless_type ~= "global" and core_has[vless_type] then + default_vless_type = vless_type + end + local hysteria2_type = sub_cfg.hysteria2_type or "global" + if hysteria2_type ~= "global" and core_has[hysteria2_type] then + default_hysteria2_type = hysteria2_type + end + end local result = { timeout = 60, add_mode = add_mode, --0为手动配置,1为导入,2为订阅 @@ -509,9 +562,9 @@ local function processData(szType, content, add_mode, group) result.remarks = base64Decode(params.remarks) elseif szType == 'vmess' then local info = jsonParse(content) - if vmess_type_default == "sing-box" and has_singbox then + if default_vmess_type == "sing-box" and has_singbox then result.type = 'sing-box' - elseif vmess_type_default == "xray" and has_xray then + elseif default_vmess_type == "xray" and has_xray then result.type = "Xray" else log("跳过 VMess 节点,因未适配到 VMess 核心程序,或未正确设置节点使用类型。") @@ -606,7 +659,7 @@ local function processData(szType, content, add_mode, group) result.tls_CertSha = info.pcs result.tls_CertByName = info.vcn local insecure = info.allowinsecure or info.allowInsecure or info.insecure - result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0") + result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0") else result.tls = "0" end @@ -622,7 +675,7 @@ local function processData(szType, content, add_mode, group) return nil end elseif szType == "ss" then - result = set_ss_implementation(result) + result = set_ss_implementation(default_ss_type, result) if not result then return nil end --SS-URI = "ss://" userinfo "@" hostname ":" port [ "/" ] [ "?" plugin ] [ "#" tag ] @@ -886,7 +939,7 @@ local function processData(szType, content, add_mode, group) end end local insecure = params.allowinsecure or params.allowInsecure or params.insecure - result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0") + result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0") result.uot = params.udp elseif (params.type ~= "tcp" and params.type ~= "raw") and (params.headerType and params.headerType ~= "none") then result.error_msg = "请更换 Xray 或 Sing-Box 来支持 SS 更多的传输方式。" @@ -895,7 +948,7 @@ local function processData(szType, content, add_mode, group) if params["shadow-tls"] then if result.type ~= "sing-box" and result.type ~= "SS-Rust" then - result.error_msg = ss_type_default .. " 不支持 shadow-tls 插件。" + result.error_msg = default_ss_type .. " 不支持 shadow-tls 插件。" else -- 解析SS Shadow-TLS 插件参数 local function parseShadowTLSParams(b64str, out) @@ -937,12 +990,12 @@ local function processData(szType, content, add_mode, group) end end elseif szType == "trojan" then - if trojan_type_default == "trojan-plus" and has_trojan_plus then + if default_trojan_type == "trojan-plus" and has_trojan_plus then result.type = "Trojan-Plus" - elseif trojan_type_default == "sing-box" and has_singbox then + elseif default_trojan_type == "sing-box" and has_singbox then result.type = 'sing-box' result.protocol = 'trojan' - elseif trojan_type_default == "xray" and has_xray then + elseif default_trojan_type == "xray" and has_xray then result.type = 'Xray' result.protocol = 'trojan' else @@ -991,7 +1044,7 @@ local function processData(szType, content, add_mode, group) result.tls_CertSha = params.pcs result.tls_CertByName = params.vcn local insecure = params.allowinsecure or params.allowInsecure or params.insecure - result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0") + result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0") if not params.type then params.type = "tcp" end params.type = string.lower(params.type) @@ -1077,7 +1130,7 @@ local function processData(szType, content, add_mode, group) end elseif szType == "ssd" then - result = set_ss_implementation(result) + result = set_ss_implementation(default_ss_type, result) if not result then return nil end result.address = content.server result.port = content.port @@ -1088,9 +1141,9 @@ local function processData(szType, content, add_mode, group) result.group = content.airport result.remarks = content.remarks elseif szType == "vless" then - if vless_type_default == "sing-box" and has_singbox then + if default_vless_type == "sing-box" and has_singbox then result.type = 'sing-box' - elseif vless_type_default == "xray" and has_xray then + elseif default_vless_type == "xray" and has_xray then result.type = "Xray" else log("跳过 VLESS 节点,因未适配到 VLESS 核心程序,或未正确设置节点使用类型。") @@ -1243,7 +1296,7 @@ local function processData(szType, content, add_mode, group) result.reality_mldsa65Verify = params.pqv or nil end local insecure = params.allowinsecure or params.allowInsecure or params.insecure - result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0") + result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0") end result.port = port @@ -1300,7 +1353,7 @@ local function processData(szType, content, add_mode, group) result.hysteria_auth_password = params.auth result.tls_serverName = params.peer or params.sni or "" local insecure = params.allowinsecure or params.allowInsecure or params.insecure - result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0") + result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0") result.alpn = params.alpn result.hysteria_up_mbps = params.upmbps result.hysteria_down_mbps = params.downmbps @@ -1346,12 +1399,12 @@ local function processData(szType, content, add_mode, group) result.tls_CertSha = params.pcs result.tls_CertByName = params.vcn local insecure = params.allowinsecure or params.insecure - result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0") + result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0") result.hysteria2_tls_pinSHA256 = params.pinSHA256 result.hysteria2_hop = params.mport - if (hysteria2_type_default == "sing-box" and has_singbox) or (hysteria2_type_default == "xray" and has_xray) then - local is_singbox = hysteria2_type_default == "sing-box" and has_singbox + if (default_hysteria2_type == "sing-box" and has_singbox) or (default_hysteria2_type == "xray" and has_xray) then + local is_singbox = default_hysteria2_type == "sing-box" and has_singbox result.type = is_singbox and 'sing-box' or 'Xray' result.protocol = "hysteria2" if params["obfs-password"] or params["obfs_password"] then @@ -1426,7 +1479,7 @@ local function processData(szType, content, add_mode, group) result.tuic_congestion_control = params.congestion_control or "cubic" result.tuic_udp_relay_mode = params.udp_relay_mode or "native" local insecure = params.allowinsecure or params.insecure or params.allow_insecure - result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0") + result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0") elseif szType == "anytls" then if has_singbox then result.type = 'sing-box' @@ -1490,7 +1543,7 @@ local function processData(szType, content, add_mode, group) end result.port = port local insecure = params.allowinsecure or params.insecure - result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0") + result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0") end elseif szType == 'naive+https' or szType == 'naive+quic' then if has_singbox then @@ -1829,6 +1882,30 @@ local function update_node(manual) for _, v in ipairs(nodeResult) do local remark = v["remark"] local list = v["list"] + local sub_cfg = v["sub_cfg"] + local domain_resolver, domain_resolver_dns, domain_resolver_dns_https, domain_strategy + local preproxy_node_group, to_node_group, chain_node_type = "", "", "" + -- Subscription Group Chain Agent + 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 + if sub_cfg then + domain_resolver = sub_cfg.domain_resolver + domain_resolver_dns = sub_cfg.domain_resolver_dns + domain_resolver_dns_https = sub_cfg.domain_resolver_dns_https + local map = { UseIPv4v6 = 1, UseIPv6v4 = 1, UseIPv4 = 1, UseIPv6 = 1 } + domain_strategy = (sub_cfg.domain_strategy and map[sub_cfg.domain_strategy]) and sub_cfg.domain_strategy or nil + preproxy_node_group = (sub_cfg.chain_proxy == "1") and valid_chain_node(sub_cfg.preproxy_node) or "" + to_node_group = (sub_cfg.chain_proxy == "2") and valid_chain_node(sub_cfg.to_node) or "" + end for _, vv in ipairs(list) do local cfgid = uci:section(appname, "nodes", api.gen_short_uuid()) for kkk, vvv in pairs(vv) do @@ -1916,8 +1993,12 @@ local function update_node(manual) end end -local function parse_link(raw, add_mode, group, cfgid) +local function parse_link(raw, add_mode, group, sub_cfg) if raw and #raw > 0 then + local cfgid + if sub_cfg then + cfgid = sub_cfg[".name"] + end local nodes, szType local node_list = {} -- SSD 似乎是这种格式 ssd:// 开头的 @@ -1952,17 +2033,17 @@ local function parse_link(raw, add_mode, group, cfgid) xpcall(function () local result if szType == 'ssd' then - result = processData(szType, v, add_mode, group) + result = processData(szType, v, add_mode, group, sub_cfg) elseif not szType then local node = api.trim(v) local dat = split(node, "://") if dat and dat[1] and dat[2] then if dat[1] == 'vmess' or dat[1] == 'ssr' then local link = api.trim(dat[2]:gsub("#.*$", "")) - result = processData(dat[1], base64Decode(link), add_mode, group) + result = processData(dat[1], base64Decode(link), add_mode, group, sub_cfg) else local link = dat[2]:gsub("&", "&"):gsub("%s*#%s*", "#") -- 一些奇葩的链接用"&"当做"&","#"前后带空格 - result = processData(dat[1], link, add_mode, group) + result = processData(dat[1], link, add_mode, group, sub_cfg) end end else @@ -1974,7 +2055,7 @@ local function parse_link(raw, add_mode, group, cfgid) log('丢弃节点:' .. result.remarks .. " ,原因:" .. result.error_msg) elseif not result.type then log('丢弃节点:' .. result.remarks .. " ,找不到可使用二进制。") - elseif (add_mode == "2" and is_filter_keyword(result.remarks)) or not result.address or result.remarks == "NULL" or result.address == "127.0.0.1" or + elseif (add_mode == "2" and is_filter_keyword(default_filter_keyword_mode, default_filter_keyword_discard_list, default_filter_keyword_keep_list, result.remarks)) or not result.address or result.remarks == "NULL" or result.address == "127.0.0.1" or (not datatypes.hostname(result.address) and not (api.is_ip(result.address))) then log('丢弃过滤节点:' .. result.type .. ' 节点,' .. result.remarks) else @@ -1994,7 +2075,8 @@ local function parse_link(raw, add_mode, group, cfgid) if #node_list > 0 then nodeResult[#nodeResult + 1] = { remark = group, - list = node_list + list = node_list, + sub_cfg = sub_cfg } end log('成功解析【' .. group .. '】节点数量:' .. #node_list) @@ -2025,78 +2107,26 @@ local execute = function() local cfgid = value[".name"] local remark = value.remark or "" local url = value.url or "" - if value.allowInsecure and value.allowInsecure == "1" then - allowInsecure_default = true - end - local filter_keyword_mode = value.filter_keyword_mode or "5" - if filter_keyword_mode == "0" then - filter_keyword_mode_default = "0" - elseif filter_keyword_mode == "1" then - filter_keyword_mode_default = "1" - filter_keyword_discard_list_default = value.filter_discard_list or {} - elseif filter_keyword_mode == "2" then - filter_keyword_mode_default = "2" - filter_keyword_keep_list_default = value.filter_keep_list or {} - elseif filter_keyword_mode == "3" then - filter_keyword_mode_default = "3" - filter_keyword_keep_list_default = value.filter_keep_list or {} - filter_keyword_discard_list_default = value.filter_discard_list or {} - elseif filter_keyword_mode == "4" then - filter_keyword_mode_default = "4" - filter_keyword_keep_list_default = value.filter_keep_list or {} - filter_keyword_discard_list_default = value.filter_discard_list or {} - end - local ss_type = value.ss_type or "global" - if ss_type ~= "global" and core_has[ss_type] then - ss_type_default = ss_type - end - local trojan_type = value.trojan_type or "global" - if trojan_type ~= "global" and core_has[trojan_type] then - trojan_type_default = trojan_type - end - local vmess_type = value.vmess_type or "global" - if vmess_type ~= "global" and core_has[vmess_type] then - vmess_type_default = vmess_type - end - local vless_type = value.vless_type or "global" - if vless_type ~= "global" and core_has[vless_type] then - vless_type_default = vless_type - end - local hysteria2_type = value.hysteria2_type or "global" - if hysteria2_type ~= "global" and core_has[hysteria2_type] then - hysteria2_type_default = hysteria2_type - end - domain_resolver = value.domain_resolver - domain_resolver_dns = value.domain_resolver_dns - domain_resolver_dns_https = value.domain_resolver_dns_https - local map = { UseIPv4v6 = 1, UseIPv6v4 = 1, UseIPv4 = 1, UseIPv6 = 1 } - domain_strategy = (value.domain_strategy and map[value.domain_strategy]) and value.domain_strategy or nil - - -- 订阅组链式代理 - 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 "自动")) - log('正在订阅:【' .. remark .. '】' .. url .. ' [' .. result .. ']') - local tmp_file = "/tmp/" .. cfgid - value.http_code = curl(url, tmp_file, ua, access_mode) - if value.http_code ~= 200 then - fail_list[#fail_list + 1] = value + local url_is_local + if fs.access(url) then + -- debug, reads local files. + log('正在订阅:【' .. remark .. '】' .. url) + url_is_local = true + tmp_file = url else + 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 "自动")) + log('正在订阅:【' .. remark .. '】' .. url .. ' [' .. result .. ']') + tmp_file = "/tmp/" .. cfgid + value.http_code = curl(url, tmp_file, ua, access_mode) + if value.http_code ~= 200 then + fail_list[#fail_list + 1] = value + luci.sys.call("rm -f " .. tmp_file) + end + end + if fs.access(tmp_file) then if luci.sys.call("[ -f " .. tmp_file .. " ] && sed -i -e '/^[ \t]*$/d' -e '/^[ \t]*\r$/d' " .. tmp_file) == 0 then local f = io.open(tmp_file, "r") local stdout = f:read("*all") @@ -2107,33 +2137,18 @@ local execute = function() if not manual_sub and old_md5 == new_md5 then log('订阅:【' .. remark .. '】没有变化,无需更新。') else - parse_link(raw_data, "2", remark, cfgid) + parse_link(raw_data, "2", remark, value) uci:set(appname, cfgid, "md5", new_md5) end else fail_list[#fail_list + 1] = value end + if url_is_local then + value.http_code = 0 + else + luci.sys.call("rm -f " .. tmp_file) + end end - luci.sys.call("rm -f " .. tmp_file) - allowInsecure_default = nil - filter_keyword_mode_default = uci:get(appname, "@global_subscribe[0]", "filter_keyword_mode") or "0" - filter_keyword_discard_list_default = uci:get(appname, "@global_subscribe[0]", "filter_discard_list") or {} - filter_keyword_keep_list_default = uci:get(appname, "@global_subscribe[0]", "filter_keep_list") or {} - - ss_type = uci:get(appname, "@global_subscribe[0]", "ss_type") or "" - ss_type_default = core_has[ss_type] and ss_type or ss_type_default - - trojan_type = uci:get(appname, "@global_subscribe[0]", "trojan_type") or "" - trojan_type_default = core_has[trojan_type] and trojan_type or trojan_type_default - - vmess_type = uci:get(appname, "@global_subscribe[0]", "vmess_type") or "" - vmess_type_default = core_has[vmess_type] and vmess_type or vmess_type_default - - vless_type = uci:get(appname, "@global_subscribe[0]", "vless_type") or "" - vless_type_default = core_has[vless_type] and vless_type or vless_type_default - - hysteria2_type = uci:get(appname, "@global_subscribe[0]", "hysteria2_type") or "" - hysteria2_type_default = core_has[hysteria2_type] and hysteria2_type or hysteria2_type_default end if #fail_list > 0 then diff --git a/luci-app-passwall2/Makefile b/luci-app-passwall2/Makefile index a2c3610e6..f25d5d1b6 100644 --- a/luci-app-passwall2/Makefile +++ b/luci-app-passwall2/Makefile @@ -6,8 +6,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-passwall2 -PKG_VERSION:=26.4.10 -PKG_RELEASE:=37 +PKG_VERSION:=26.4.20 +PKG_RELEASE:=38 PKG_PO_VERSION:=$(PKG_VERSION) PKG_CONFIG_DEPENDS:= \ diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/global.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/global.lua index 84199dfc7..8ee881e32 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/global.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/global.lua @@ -268,8 +268,8 @@ o = s:taboption("DNS", Flag, "dns_redirect", translate("DNS Redirect"), translat o.default = "1" o.rmempty = false -local use_nft = m:get("@global_forwarding[0]", "use_nft") == "1" -local set_title = api.i18n.translate(use_nft and "Clear NFTSET" or "Clear IPSET") +local prefer_nft = m:get("@global_forwarding[0]", "prefer_nft") == "1" +local set_title = api.i18n.translate(prefer_nft and "Clear NFTSET" or "Clear IPSET") o = s:taboption("DNS", DummyValue, "clear_ipset", set_title, translate("Try this feature if the rule modification does not take effect.")) o.rawhtml = true function o.cfgvalue(self, section) diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/haproxy.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/haproxy.lua index 2304e7f13..0f2ea0fa4 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/haproxy.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/haproxy.lua @@ -48,15 +48,23 @@ o.default = "" o:depends("console_auth", true) ---- Console Port -o = s:option(Value, "console_port", translate("Console Port"), translate( - "In the browser input routing IP plus port access, such as:192.168.1.1:1188")) -o.default = "1188" +o = s:option(Value, "console_port", translate("Console Port"), translate("In the browser input routing IP plus port access, such as:192.168.1.1:1188")) +o.datatype = "port" +o.default = 1188 o:depends("balancing_enable", true) o = s:option(Flag, "bind_local", translate("Haproxy Port") .. " " .. translate("Bind Local"), translate("When selected, it can only be accessed localhost.")) o.default = "0" o:depends("balancing_enable", true) +o = s:option(ListValue, "balancingStrategy", translate("Balancing Strategy")) +o:value("roundrobin", "roundRobin") +o:value("random", "random") +o:value("leastconn", "leastConn") +o:value("source", "source") +o.default = "roundrobin" +o:depends("balancing_enable", true) + ---- Health Check Type o = s:option(ListValue, "health_check_type", translate("Health Check Type")) o.default = "passwall_logic" @@ -77,9 +85,11 @@ o.description = translate("The URL used to detect the connection status.") o:depends("health_check_type", "passwall_logic") ---- Health Check Inter -o = s:option(Value, "health_check_inter", translate("Health Check Inter"), translate("Units:seconds")) -o.default = "60" +o = s:option(Value, "health_check_inter", translate("Health Check Inter")) +o.datatype = "min(20)" +o.default = 60 o:depends("balancing_enable", true) +o.description = translate("Units:seconds") .. " ( ≥ 20s )" o = s:option(DummyValue, "health_check_tips", " ") o.rawhtml = true @@ -89,11 +99,10 @@ end o:depends("health_check_type", "passwall_logic") -- [[ Balancing Settings ]]-- -s = m:section(TypedSection, "haproxy_config", translate("Node List"), - "" .. - translate("Add a node, Export Of Multi WAN Only support Multi Wan. Load specific gravity range 1-256. Multiple primary servers can be load balanced, standby will only be enabled when the primary server is offline! Multiple groups can be set, Haproxy port same one for each group.") .. - "\n" .. translate("Note that the node configuration parameters for load balancing must be consistent when use TCP health check type, otherwise it cannot be used normally!") .. - "") +s = m:section(TypedSection, "haproxy_config", translate("Node List")) +s.description = "" .. + translate("Add a node, Export Of Multi WAN Only support Multi Wan. Load specific gravity range 1-256. Multiple primary servers can be load balanced, standby will only be enabled when the primary server is offline! Multiple groups can be set, Haproxy port same one for each group.") .. "
" .. + translate("Note that the node configuration parameters for load balancing must be consistent when use TCP health check type, otherwise it cannot be used normally!") .. "
" s.template = "cbi/tblsection" s.sortable = true s.anonymous = true @@ -148,7 +157,7 @@ o.rmempty = false ---- Node Weight o = s:option(Value, "lbweight", translate("Node Weight")) -o.datatype = "uinteger" +o.datatype = "min(1)" o.default = 5 o.rmempty = false diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/socks_config.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/socks_config.lua index 68f507ad0..f3930ed4f 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/socks_config.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/socks_config.lua @@ -8,7 +8,7 @@ if not arg[1] or not m:get(arg[1]) then luci.http.redirect(api.url()) end -m:append(Template(appname .. "/cbi/nodes_multivalue_com")) +m:append(Template(appname .. "/cbi/nodes_dynamiclist_com")) m:append(Template(appname .. "/cbi/nodes_listvalue_com")) local has_singbox = api.finded_com("sing-box") @@ -95,41 +95,28 @@ o.datatype = "min(1)" o.default = 1 o:depends("enable_autoswitch", true) -o = s:option(MultiValue, "autoswitch_backup_node", translate("List of backup nodes")) +o = s:option(DynamicList, "autoswitch_backup_node", translate("List of backup nodes")) o:depends("enable_autoswitch", true) -o.widget = "checkbox" -o.template = appname .. "/cbi/nodes_multivalue" +o.template = appname .. "/cbi/nodes_dynamiclist" o.group = {} +o.write = function(self, section, value) + local n = s.fields["node"]:formvalue(section) + local v = type(value) == "table" and value or { value } + local t = {} + for _, x in ipairs(v) do + if x and x ~= n then t[#t+1] = x end + end + return DynamicList.write(self, section, t) +end for i, v in pairs(nodes_table) do - o:value(v.id, v.remark) - o.group[#o.group+1] = v.group or "" + if v.protocol ~= "_shunt" then + o:value(v.id, v.remark) + o.group[#o.group+1] = (v.group and v.group ~= "") and v.group or translate("default") + s.fields["enable_autoswitch"]:depends({ node = v.id }) + end socks_node:value(v.id, v["remark"]) socks_node.group[#socks_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default") end --- Reading the old DynamicList -function o.cfgvalue(self, section) - return m.uci:get_list(appname, section, "autoswitch_backup_node") or {} -end --- Write-and-hold DynamicList -function o.write(self, section, value) - local old = m.uci:get_list(appname, section, "autoswitch_backup_node") or {} - local new, set = {}, {} - for v in value:gmatch("%S+") do - new[#new + 1] = v - set[v] = 1 - end - for _, v in ipairs(old) do - if not set[v] then - m.uci:set_list(appname, section, "autoswitch_backup_node", new) - return - end - set[v] = nil - end - for _ in pairs(set) do - m.uci:set_list(appname, section, "autoswitch_backup_node", new) - return - end -end o = s:option(Flag, "autoswitch_restore_switch", translate("Restore Switch"), translate("When detects main node is available, switch back to the main node.")) o:depends("enable_autoswitch", true) diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ray.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ray.lua index ac2a7b349..27b9f40a3 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ray.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ray.lua @@ -506,33 +506,6 @@ o:depends({ [_n("transport")] = "mkcp" }) o = s:option(Value, _n("mkcp_domain"), translate("Camouflage Domain"), translate("Use it together with the DNS disguised type. You can fill in any domain.")) o:depends({ [_n("mkcp_guise")] = "dns" }) -o = s:option(Value, _n("mkcp_mtu"), translate("KCP MTU")) -o.default = "1350" -o:depends({ [_n("transport")] = "mkcp" }) - -o = s:option(Value, _n("mkcp_tti"), translate("KCP TTI")) -o.default = "20" -o:depends({ [_n("transport")] = "mkcp" }) - -o = s:option(Value, _n("mkcp_uplinkCapacity"), translate("KCP uplinkCapacity")) -o.default = "5" -o:depends({ [_n("transport")] = "mkcp" }) - -o = s:option(Value, _n("mkcp_downlinkCapacity"), translate("KCP downlinkCapacity")) -o.default = "20" -o:depends({ [_n("transport")] = "mkcp" }) - -o = s:option(Flag, _n("mkcp_congestion"), translate("KCP Congestion")) -o:depends({ [_n("transport")] = "mkcp" }) - -o = s:option(Value, _n("mkcp_readBufferSize"), translate("KCP readBufferSize")) -o.default = "1" -o:depends({ [_n("transport")] = "mkcp" }) - -o = s:option(Value, _n("mkcp_writeBufferSize"), translate("KCP writeBufferSize")) -o.default = "1" -o:depends({ [_n("transport")] = "mkcp" }) - o = s:option(Value, _n("mkcp_seed"), translate("KCP Seed")) o:depends({ [_n("transport")] = "mkcp" }) @@ -645,6 +618,14 @@ end -- [[ User-Agent ]]-- o = s:option(Value, _n("user_agent"), translate("User-Agent")) +o.default = "" +o:value("", translate("default")) +o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36", "chrome") +o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0", "firefox") +o:value("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15", "safari") +o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 Edg/91.0.864.70", "edge") +o:value("Go-http-client/1.1", "golang") +o:value("curl/7.68.0", "curl") o:depends({ [_n("tcp_guise")] = "http" }) o:depends({ [_n("transport")] = "ws" }) o:depends({ [_n("transport")] = "httpupgrade" }) 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 42d39e8db..f45ee300c 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 @@ -416,8 +416,14 @@ if singbox_tags:find("with_quic") then end -- [[ SSH config start ]] -- -o = s:option(Value, _n("ssh_priv_key"), translate("Private Key")) +o = s:option(TextValue, _n("ssh_priv_key"), translate("Private Key")) +o.rows = 5 +o.wrap = "off" o:depends({ [_n("protocol")] = "ssh" }) +o.validate = function(self, value) + value = api.trim(value):gsub("\r\n", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n") + return value +end o = s:option(Value, _n("ssh_priv_key_pp"), translate("Private Key Passphrase")) o.password = true @@ -509,11 +515,7 @@ o.rows = 5 o.wrap = "off" o:depends({ [_n("ech")] = true }) o.validate = function(self, value) - value = value:gsub("^%s+", ""):gsub("%s+$","\n"):gsub("\r\n","\n"):gsub("[ \t]*\n[ \t]*", "\n") - value = value:gsub("^%s*\n", "") - if value:sub(-1) == "\n" then - value = value:sub(1, -2) - end + value = api.trim(value):gsub("\r\n", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n") return value end @@ -681,6 +683,14 @@ o:depends({ [_n("grpc_health_check")] = true }) -- [[ User-Agent ]]-- o = s:option(Value, _n("user_agent"), translate("User-Agent")) +o.default = "" +o:value("", translate("default")) +o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36", "chrome") +o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0", "firefox") +o:value("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15", "safari") +o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 Edg/91.0.864.70", "edge") +o:value("Go-http-client/1.1", "golang") +o:value("curl/7.68.0", "curl") o:depends({ [_n("tcp_guise")] = "http" }) o:depends({ [_n("transport")] = "http" }) o:depends({ [_n("transport")] = "ws" }) diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ray.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ray.lua index 17a441e31..cb2aba71c 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ray.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ray.lua @@ -356,33 +356,6 @@ o:depends({ [_n("transport")] = "mkcp" }) o = s:option(Value, _n("mkcp_domain"), translate("Camouflage Domain"), translate("Use it together with the DNS disguised type. You can fill in any domain.")) o:depends({ [_n("mkcp_guise")] = "dns" }) -o = s:option(Value, _n("mkcp_mtu"), translate("KCP MTU")) -o.default = "1350" -o:depends({ [_n("transport")] = "mkcp" }) - -o = s:option(Value, _n("mkcp_tti"), translate("KCP TTI")) -o.default = "20" -o:depends({ [_n("transport")] = "mkcp" }) - -o = s:option(Value, _n("mkcp_uplinkCapacity"), translate("KCP uplinkCapacity")) -o.default = "5" -o:depends({ [_n("transport")] = "mkcp" }) - -o = s:option(Value, _n("mkcp_downlinkCapacity"), translate("KCP downlinkCapacity")) -o.default = "20" -o:depends({ [_n("transport")] = "mkcp" }) - -o = s:option(Flag, _n("mkcp_congestion"), translate("KCP Congestion")) -o:depends({ [_n("transport")] = "mkcp" }) - -o = s:option(Value, _n("mkcp_readBufferSize"), translate("KCP readBufferSize")) -o.default = "1" -o:depends({ [_n("transport")] = "mkcp" }) - -o = s:option(Value, _n("mkcp_writeBufferSize"), translate("KCP writeBufferSize")) -o.default = "1" -o:depends({ [_n("transport")] = "mkcp" }) - o = s:option(Value, _n("mkcp_seed"), translate("KCP Seed")) o:depends({ [_n("transport")] = "mkcp" }) diff --git a/luci-app-passwall2/luasrc/passwall2/util_xray.lua b/luci-app-passwall2/luasrc/passwall2/util_xray.lua index 7cc389485..5ccfba884 100644 --- a/luci-app-passwall2/luasrc/passwall2/util_xray.lua +++ b/luci-app-passwall2/luasrc/passwall2/util_xray.lua @@ -217,13 +217,12 @@ function gen_outbound(flag, node, tag, proxy_table) } } or nil, kcpSettings = (node.transport == "mkcp") and { - mtu = tonumber(node.mkcp_mtu), - tti = tonumber(node.mkcp_tti), - uplinkCapacity = tonumber(node.mkcp_uplinkCapacity), - downlinkCapacity = tonumber(node.mkcp_downlinkCapacity), - congestion = (node.mkcp_congestion == "1") and true or false, - readBufferSize = tonumber(node.mkcp_readBufferSize), - writeBufferSize = tonumber(node.mkcp_writeBufferSize) + mtu = 1350, + tti = 50, + uplinkCapacity = 12, + downlinkCapacity = 100, + CwndMultiplier = 1, + MaxSendingWindow = 2 * 1024 * 1024 } or nil, wsSettings = (node.transport == "ws") and { path = node.ws_path or "/", @@ -675,13 +674,12 @@ function gen_config_server(node) } } or nil, kcpSettings = (node.transport == "mkcp") and { - mtu = tonumber(node.mkcp_mtu), - tti = tonumber(node.mkcp_tti), - uplinkCapacity = tonumber(node.mkcp_uplinkCapacity), - downlinkCapacity = tonumber(node.mkcp_downlinkCapacity), - congestion = (node.mkcp_congestion == "1") and true or false, - readBufferSize = tonumber(node.mkcp_readBufferSize), - writeBufferSize = tonumber(node.mkcp_writeBufferSize) + mtu = 1350, + tti = 50, + uplinkCapacity = 12, + downlinkCapacity = 100, + CwndMultiplier = 1, + MaxSendingWindow = 2 * 1024 * 1024 } or nil, wsSettings = (node.transport == "ws") and { host = node.ws_host or nil, diff --git a/luci-app-passwall2/luasrc/view/passwall2/cbi/nodes_dynamiclist.htm b/luci-app-passwall2/luasrc/view/passwall2/cbi/nodes_dynamiclist.htm new file mode 100644 index 000000000..f516acebe --- /dev/null +++ b/luci-app-passwall2/luasrc/view/passwall2/cbi/nodes_dynamiclist.htm @@ -0,0 +1,149 @@ +<%+cbi/valueheader%> +<% +-- Template Developers: +-- - lwb1978 +-- - snowie2000 +-- Copyright: copyright(c)2025–2027 +-- Description: Passwall(2) UI template + +local json = require "luci.jsonc" + +-- 读取值(keylist/vallist/group) +local values = {} +for i, key in pairs(self.keylist) do + values[#values + 1] = { + key = key, + label = self.vallist[i] or key, + group = self.group and self.group[i] or nil + } +end + +-- 获取当前配置值 +local current_key = {} +local cval = self:cfgvalue(section) +if type(cval) == "table" then + for _, v in ipairs(cval) do + for item in string.gmatch(v, "[^,]+") do + current_key[#current_key+1] = item + end + end +elseif type(cval) == "string" then + for item in string.gmatch(cval, "[^,]+") do + current_key[#current_key+1] = item + end +end + +-- 分离无组节点(ungrouped)与有组节点(grouped),并保持原有顺序 +local ungrouped = {} +local groups = {} +local group_order = {} +for _, item in ipairs(values) do + if not item.group or item.group == "" then + table.insert(ungrouped, item) + else + local g = item.group + if not groups[g] then + groups[g] = {} + table.insert(group_order, g) + end + table.insert(groups[g], item) + end +end + +-- 打包数据给 JS +local dropdown_data = { + current_key = current_key, + ungrouped = ungrouped, + groups = groups, + group_order = group_order, + cbid = cbid, +} +%> + + +
+ + + +
+ + +
+ + <%=translate("-- Please choose --")%> + + +
+ + +
+ + + +<%+cbi/valuefooter%> diff --git a/luci-app-passwall2/luasrc/view/passwall2/cbi/nodes_dynamiclist_com.htm b/luci-app-passwall2/luasrc/view/passwall2/cbi/nodes_dynamiclist_com.htm new file mode 100644 index 000000000..4f932a215 --- /dev/null +++ b/luci-app-passwall2/luasrc/view/passwall2/cbi/nodes_dynamiclist_com.htm @@ -0,0 +1,654 @@ +<% +-- Template Developers: +-- - lwb1978 +-- - snowie2000 +-- Copyright: copyright(c)2025–2027 +-- Description: Passwall(2) UI template +-- It is the common part of the template and cannot be used independently +%> + + + + diff --git a/luci-app-passwall2/root/etc/hotplug.d/iface/98-passwall2 b/luci-app-passwall2/root/etc/hotplug.d/iface/98-passwall2 index b47c6adb8..069212254 100644 --- a/luci-app-passwall2/root/etc/hotplug.d/iface/98-passwall2 +++ b/luci-app-passwall2/root/etc/hotplug.d/iface/98-passwall2 @@ -1,11 +1,23 @@ #!/bin/sh -[[ "$ACTION" == "ifup" && $(uci get "passwall2.@global[0].enabled") == "1" ]] && [ -f /var/lock/passwall2_ready.lock ] && { - default_device=$(ip route | grep default | awk -F 'dev ' '{print $2}' | awk '{print $1}') - [ "$default_device" == "$DEVICE" ] && { - LOCK_FILE_DIR=/var/lock - [ ! -d ${LOCK_FILE_DIR} ] && mkdir -p ${LOCK_FILE_DIR} - LOCK_FILE="${LOCK_FILE_DIR}/passwall2_ifup.lock" +[ "$ACTION" = "ifup" ] || [ "$ACTION" = "ifupdate" ] || exit 0 + +. /usr/share/passwall2/utils.sh + +([ "$(get_cache_var "ENABLED_DEFAULT_ACL")" = "1" ] || [ "$(get_cache_var "ENABLED_ACLS")" = "1" ]) && [ -f ${LOCK_PATH}/${CONFIG}_ready.lock ] && { + + default_device=$(ip route show default 2>/dev/null | awk -F 'dev ' '{print $2}' | awk '{print $1}' | head -n1) + default6_device=$(ip -6 route show default 2>/dev/null | awk -F 'dev ' '{print $2}' | awk '{print $1}' | head -n1) + ( [ "$default_device" = "$DEVICE" ] || [ "$default6_device" = "$DEVICE" ] ) && { + + if [ "$ACTION" = "ifupdate" ]; then + USE_TABLES=$(get_cache_var "USE_TABLES") + [ -n "$USE_TABLES" ] && ${APP_PATH}/${USE_TABLES}.sh update_wan_sets >/dev/null 2>&1 & + exit 0 + fi + + [ ! -d ${LOCK_PATH} ] && mkdir -p ${LOCK_PATH} + LOCK_FILE="${LOCK_PATH}/${CONFIG}_ifup.lock" if [ -s ${LOCK_FILE} ]; then SPID=$(cat ${LOCK_FILE}) if [ -e /proc/${SPID}/status ]; then @@ -15,8 +27,8 @@ fi echo $$ > ${LOCK_FILE} - /etc/init.d/passwall2 restart >/dev/null 2>&1 & - logger -p notice -t network -s "passwall2: restart when $INTERFACE ifup" + /etc/init.d/${CONFIG} restart >/dev/null 2>&1 & + logger -p notice -t network -s "${CONFIG}: restart when $INTERFACE ifup" rm -rf ${LOCK_FILE} } diff --git a/luci-app-passwall2/root/usr/share/passwall2/app.sh b/luci-app-passwall2/root/usr/share/passwall2/app.sh index bd1110eb8..dbe5234ac 100755 --- a/luci-app-passwall2/root/usr/share/passwall2/app.sh +++ b/luci-app-passwall2/root/usr/share/passwall2/app.sh @@ -933,8 +933,8 @@ start_haproxy() { [ "$(config_t_get global_haproxy balancing_enable 0)" != "1" ] && return haproxy_path=$TMP_PATH/haproxy haproxy_conf="config.cfg" - lua $APP_PATH/haproxy.lua -path ${haproxy_path} -conf ${haproxy_conf} -dns ${LOCAL_DNS:-${AUTO_DNS}} - ln_run ${QUEUE_RUN} "$(first_type haproxy)" haproxy "/dev/null" -f "${haproxy_path}/${haproxy_conf}" + lua $APP_PATH/haproxy.lua -path ${haproxy_path} -conf ${haproxy_conf} + ln_run 0 "$(first_type haproxy)" haproxy "/dev/null" -f "${haproxy_path}/${haproxy_conf}" } run_copy_dnsmasq() { diff --git a/luci-app-passwall2/root/usr/share/passwall2/haproxy.lua b/luci-app-passwall2/root/usr/share/passwall2/haproxy.lua index 8c7ee3c48..1021568fb 100644 --- a/luci-app-passwall2/root/usr/share/passwall2/haproxy.lua +++ b/luci-app-passwall2/root/usr/share/passwall2/haproxy.lua @@ -1,7 +1,7 @@ #!/usr/bin/lua local api = require ("luci.passwall2.api") -local appname = "passwall2" +local appname = api.appname local fs = api.fs local jsonc = api.jsonc local uci = api.uci @@ -18,11 +18,12 @@ end local var = api.get_args(arg) local haproxy_path = var["-path"] local haproxy_conf = var["-conf"] -local haproxy_dns = var["-dns"] or "119.29.29.29:53,223.5.5.5:53" +local haproxy_dns = "127.0.0.1" local cpu_thread = sys.exec('echo -n $(cat /proc/cpuinfo | grep "processor" | wc -l)') or "1" local health_check_type = uci:get(appname, "@global_haproxy[0]", "health_check_type") or "tcp" -local health_check_inter = uci:get(appname, "@global_haproxy[0]", "health_check_inter") or "10" +local health_check_inter = uci:get(appname, "@global_haproxy[0]", "health_check_inter") or "20" +local balancingStrategy = uci:get(appname, "@global_haproxy[0]", "balancingStrategy") or "roundrobin" local console_port = uci:get(appname, "@global_haproxy[0]", "console_port") local bind_local = uci:get(appname, "@global_haproxy[0]", "bind_local") or "0" local bind_address = "0.0.0.0" @@ -38,35 +39,37 @@ local f_out = io.open(haproxy_file, "a") local haproxy_config = [[ global daemon - log 127.0.0.1 local2 - maxconn 60000 - stats socket {{path}}/haproxy.sock + log 127.0.0.1 local2 + maxconn 60000 + stats socket {{path}}/haproxy.sock nbthread {{nbthread}} external-check insecure-fork-wanted + spread-checks 50 + tune.max-checks-per-thread 5 defaults - mode tcp - log global - option tcplog - option dontlognull + mode tcp + log global + option tcplog + option dontlognull option http-server-close - #option forwardfor except 127.0.0.0/8 - option redispatch - retries 2 - timeout http-request 10s - timeout queue 1m - timeout connect 10s - timeout client 1m - timeout server 1m + #option forwardfor except 127.0.0.0/8 + option redispatch + retries 2 + timeout http-request 10s + timeout queue 1m + timeout connect 10s + timeout client 1m + timeout server 1m timeout http-keep-alive 10s - timeout check 10s - maxconn 3000 - + timeout check 10s + maxconn 3000 + resolvers mydns - resolve_retries 1 - timeout resolve 5s - hold valid 600s + resolve_retries 1 + timeout resolve 5s + hold valid 600s {{dns}} ]] @@ -97,10 +100,12 @@ uci:foreach(appname, "haproxy_config", function(t) local lbss = t.lbss local listen_port = tonumber(t.haproxy_port) or 0 local server_node = uci:get_all(appname, lbss) - if server_node and server_node.address and server_node.port then - server_remark = server_node.address .. ":" .. server_node.port + local hop = (health_check_type == "passwall_logic") and (server_node.hysteria_hop or server_node.hysteria2_hop) or nil + hop = hop and hop:gsub(":", "-") or nil + if server_node and server_node.address and (server_node.port or hop) then + server_remark = server_node.address .. ":" .. (server_node.port or hop) server_address = server_node.address - server_port = server_node.port + server_port = server_node.port or hop t.origin_address = server_address t.origin_port = server_port if health_check_type == "passwall_logic" then @@ -143,6 +148,10 @@ uci:foreach(appname, "haproxy_config", function(t) end end) +local inter = tonumber((health_check_inter or "20"):match("^%d+")) +inter = (inter and inter >= 20) and inter or 20 +local inter_arg = "inter " .. inter .. "s fastinter " .. inter / 2 .. "s downinter " .. inter * 1.5 .. "s" + local sortTable = {} for i in pairs(listens) do if i ~= nil then @@ -158,14 +167,14 @@ for i, port in pairs(sortTable) do listen %s bind %s:%s mode tcp - balance roundrobin -]], port, bind_address, port)) + balance %s +]], port, bind_address, port, balancingStrategy)) if health_check_type == "passwall_logic" then f_out:write(string.format([[ option external-check - external-check command "/usr/share/passwall2/haproxy_check.sh" -]], port, port)) + external-check command "/usr/share/%s/haproxy_check.sh" +]], appname)) end local count_M, count_B = 1, 1 @@ -180,7 +189,7 @@ listen %s count_B = count_B + 1 end local server = o.server_address .. ":" .. o.server_port - local server_conf = "server {{remark}} {{server}} weight {{weight}} {{resolvers}} check inter {{inter}} rise 1 fall 3 {{backup}}" + local server_conf = "server {{remark}} {{server}} weight {{weight}} {{resolvers}} check {{inter_arg}} rise 2 fall 2 {{backup}}" server_conf = server_conf:gsub("{{remark}}", remark) server_conf = server_conf:gsub("{{server}}", server) server_conf = server_conf:gsub("{{weight}}", o.lbweight) @@ -189,13 +198,13 @@ listen %s resolvers = "" end server_conf = server_conf:gsub("{{resolvers}}", resolvers) - server_conf = server_conf:gsub("{{inter}}", tonumber(health_check_inter) .. "s") + server_conf = server_conf:gsub("{{inter_arg}}", inter_arg) server_conf = server_conf:gsub("{{backup}}", tostring(o.backup) == "1" and "backup" or "") - f_out:write(" " .. server_conf .. "\n") + f_out:write(" " .. api.trim(server_conf) .. "\n") if o.export ~= "0" then - sys.call(string.format(". /usr/share/passwall2/utils.sh ; add_ip2route %s %s", o.origin_address, o.export)) + sys.call(string.format(". /usr/share/%s/utils.sh ; add_ip2route %s %s", appname, o.origin_address, o.export)) end log(2, string.format(api.i18n.translatef("Node: %s:%s, Weight: %s", o.origin_address, o.origin_port, o.lbweight))) diff --git a/luci-app-passwall2/root/usr/share/passwall2/haproxy_check.sh b/luci-app-passwall2/root/usr/share/passwall2/haproxy_check.sh index 0bf0013eb..028484f90 100755 --- a/luci-app-passwall2/root/usr/share/passwall2/haproxy_check.sh +++ b/luci-app-passwall2/root/usr/share/passwall2/haproxy_check.sh @@ -24,7 +24,7 @@ if /usr/bin/curl --help all | grep -q "\-\-retry-all-errors"; then extra_params="${extra_params} --retry-all-errors" fi -status=$(/usr/bin/curl -I -o /dev/null -skL ${extra_params} --connect-timeout 3 --retry 2 --max-time 10 -w "%{http_code}" "${probeUrl}") +status=$(/usr/bin/curl -I -o /dev/null -skL ${extra_params} --connect-timeout 3 --retry 1 --max-time 6 -w "%{http_code}" "${probeUrl}") case "$status" in 200|204) diff --git a/luci-app-passwall2/root/usr/share/passwall2/iptables.sh b/luci-app-passwall2/root/usr/share/passwall2/iptables.sh index c66ea8f66..731dcc575 100755 --- a/luci-app-passwall2/root/usr/share/passwall2/iptables.sh +++ b/luci-app-passwall2/root/usr/share/passwall2/iptables.sh @@ -617,6 +617,24 @@ filter_direct_node_list() { done } +update_wan_sets() { + local WAN_IP=$(get_wan_ips ip4) + [ -n "$WAN_IP" ] && { + ipset -F "$IPSET_WAN" + for wan_ip in $WAN_IP; do + ipset -! add "$IPSET_WAN" "$wan_ip" + done + } + + local WAN6_IP=$(get_wan_ips ip6) + [ -n "$WAN6_IP" ] && { + ipset -F "$IPSET_WAN6" + for wan6_ip in $WAN6_IP; do + ipset -! add "$IPSET_WAN6" "$wan6_ip" + done + } +} + add_firewall_rule() { log_i18n 0 "Starting to load %s firewall rules..." "iptables" @@ -665,6 +683,8 @@ add_firewall_rule() { EOF } + update_wan_sets + [ -n "$ISP_DNS" ] && { for ispip in $ISP_DNS; do ipset -! add $IPSET_LAN $ispip @@ -716,6 +736,7 @@ add_firewall_rule() { $ipt_n -N PSW2 $ipt_n -A PSW2 $(dst $IPSET_LAN) -j RETURN $ipt_n -A PSW2 $(dst $IPSET_VPS) -j RETURN + $ipt_n -A PSW2 $(comment "WAN_IP_RETURN") $(dst $IPSET_WAN) -j RETURN [ "$accept_icmp" = "1" ] && insert_rule_after "$ipt_n" "PREROUTING" "prerouting_rule" "-p icmp -j PSW2" [ -z "${is_tproxy}" ] && insert_rule_after "$ipt_n" "PREROUTING" "prerouting_rule" "-p tcp -j PSW2" @@ -747,19 +768,9 @@ add_firewall_rule() { $ipt_m -N PSW2 $ipt_m -A PSW2 $(dst $IPSET_LAN) -j RETURN $ipt_m -A PSW2 $(dst $IPSET_VPS) -j RETURN + $ipt_m -A PSW2 $(comment "WAN_IP_RETURN") $(dst $IPSET_WAN) -j RETURN $ipt_m -A PSW2 -m conntrack --ctdir REPLY -j RETURN - WAN_IP=$(get_wan_ips ip4) - [ -n "${WAN_IP}" ] && { - ipset -F $IPSET_WAN - for wan_ip in $WAN_IP; do - ipset -! add $IPSET_WAN ${wan_ip} - done - $ipt_n -A PSW2 $(comment "WAN_IP_RETURN") $(dst $IPSET_WAN) -j RETURN - $ipt_m -A PSW2 $(comment "WAN_IP_RETURN") $(dst $IPSET_WAN) -j RETURN - } - unset WAN_IP wan_ip - insert_rule_before "$ipt_m" "PREROUTING" "mwan3" "-j PSW2" # Only TCP, UDP Invalid. insert_rule_before "$ipt_m" "PREROUTING" "PSW2" "-p tcp -m socket -j PSW2_DIVERT" @@ -807,17 +818,8 @@ add_firewall_rule() { $ip6t_m -N PSW2 $ip6t_m -A PSW2 $(dst $IPSET_LAN6) -j RETURN $ip6t_m -A PSW2 $(dst $IPSET_VPS6) -j RETURN + $ip6t_m -A PSW2 $(comment "WAN6_IP_RETURN") $(dst $IPSET_WAN6) -j RETURN $ip6t_m -A PSW2 -m conntrack --ctdir REPLY -j RETURN - - WAN6_IP=$(get_wan_ips ip6) - [ -n "${WAN6_IP}" ] && { - ipset -F $IPSET_WAN6 - for wan6_ip in $WAN6_IP; do - ipset -! add $IPSET_WAN6 ${wan6_ip} - done - $ip6t_m -A PSW2 $(comment "WAN6_IP_RETURN") $(dst $IPSET_WAN6) -j RETURN - } - unset WAN6_IP wan6_ip insert_rule_before "$ip6t_m" "PREROUTING" "mwan3" "-j PSW2" # Only TCP, UDP Invalid. @@ -835,7 +837,7 @@ add_firewall_rule() { local dns_port=$(echo $auto_dns | awk -F '#' '{print $2}') if [[ "$dns_address" == *::* ]]; then $ip6t_m -I PSW2_OUTPUT -p udp -d ${dns_address} --dport ${dns_port:-53} -j RETURN - log_i18n 1 "$(i18n "Add direct DNS to %s: %s" "ip6tables" "${dns_address}:${dns_port:-53}")" + log_i18n 1 "$(i18n "Add direct DNS to %s: %s" "ip6tables" "[${dns_address}]:${dns_port:-53}")" else $ipt_m -I PSW2_OUTPUT -p udp -d ${dns_address} --dport ${dns_port:-53} -j RETURN log_i18n 1 "$(i18n "Add direct DNS to %s: %s" "iptables" "${dns_address}:${dns_port:-53}")" @@ -1050,7 +1052,7 @@ gen_include() { local __ipt="" [ -n "${ipt}" ] && { __ipt=$(cat <<- EOF - . $UTILS_PATH + ${MY_PATH} update_wan_sets $ipt-save -c | grep -v "PSW2" | $ipt-restore -c $ipt-restore -n <<-EOT $(extract_rules 4 nat) @@ -1062,21 +1064,13 @@ gen_include() { \$(${MY_PATH} insert_rule_before "$ipt_m" "PREROUTING" "mwan3" "-j PSW2") \$(${MY_PATH} insert_rule_before "$ipt_m" "PREROUTING" "PSW2" "-p tcp -m socket -j PSW2_DIVERT") - - WAN_IP=\$(get_wan_ips ip4) - [ ! -z "\${WAN_IP}" ] && { - ipset -F $IPSET_WAN - for wan_ip in \$WAN_IP; do - ipset -! add $IPSET_WAN \${wan_ip} - done - } EOF ) } local __ip6t="" [ -n "${ip6t}" ] && { __ip6t=$(cat <<- EOF - . $UTILS_PATH + ${MY_PATH} update_wan_sets $ip6t-save -c | grep -v "PSW2" | $ip6t-restore -c $ip6t-restore -n <<-EOT $(extract_rules 6 nat) @@ -1087,20 +1081,12 @@ gen_include() { \$(${MY_PATH} insert_rule_before "$ip6t_m" "PREROUTING" "mwan3" "-j PSW2") \$(${MY_PATH} insert_rule_before "$ip6t_m" "PREROUTING" "PSW2" "-p tcp -m socket -j PSW2_DIVERT") - - WAN6_IP=\$(get_wan_ips ip6) - [ ! -z "\${WAN6_IP}" ] && { - ipset -F $IPSET_WAN6 - for wan6_ip in \$WAN6_IP; do - ipset -! add $IPSET_WAN6 \${wan6_ip} - done - } EOF ) } cat <<-EOF >> $FWI ${__ipt} - + ${__ip6t} return 0 @@ -1158,6 +1144,9 @@ get_ip6t_bin) filter_direct_node_list) filter_direct_node_list ;; +update_wan_sets) + update_wan_sets "$@" + ;; stop) stop ;; diff --git a/luci-app-passwall2/root/usr/share/passwall2/nftables.sh b/luci-app-passwall2/root/usr/share/passwall2/nftables.sh index 4f50bcb97..3411cea60 100755 --- a/luci-app-passwall2/root/usr/share/passwall2/nftables.sh +++ b/luci-app-passwall2/root/usr/share/passwall2/nftables.sh @@ -680,6 +680,20 @@ mwan3_start() { nft list chain ip mangle mwan3_hook >/dev/null 2>&1 && nft insert rule ip mangle mwan3_hook ct mark ${FWMARK} counter return >/dev/null 2>&1 } +update_wan_sets() { + local WAN_IP=$(get_wan_ips ip4) + [ -n "$WAN_IP" ] && { + nft flush set $NFTABLE_NAME $NFTSET_WAN + echo "$WAN_IP" | insert_nftset $NFTSET_WAN "-1" + } + + local WAN6_IP=$(get_wan_ips ip6) + [ -n "${WAN6_IP}" ] && { + nft flush set $NFTABLE_NAME $NFTSET_WAN6 + echo "$WAN6_IP" | insert_nftset $NFTSET_WAN6 "-1" + } +} + add_firewall_rule() { log_i18n 0 "Starting to load %s firewall rules..." "nftables" gen_nft_tables @@ -714,6 +728,8 @@ add_firewall_rule() { [ -n "$lan_ip6" ] && echo $lan_ip6 | insert_nftset $NFTSET_LAN6 "-1" } + update_wan_sets + [ -n "$ISP_DNS" ] && { echo "$ISP_DNS" | insert_nftset $NFTSET_LAN 0 for ispip in $ISP_DNS; do @@ -835,14 +851,9 @@ add_firewall_rule() { nft "add rule $NFTABLE_NAME nat_output meta l4proto {icmp,icmpv6} counter jump PSW2_ICMP_REDIRECT" fi - WAN_IP=$(get_wan_ips ip4) - [ -n "${WAN_IP}" ] && { - nft flush set $NFTABLE_NAME $NFTSET_WAN - echo $WAN_IP | insert_nftset $NFTSET_WAN "-1" - [ -z "${is_tproxy}" ] && nft "add rule $NFTABLE_NAME PSW2_NAT ip daddr @$NFTSET_WAN counter return comment \"WAN_IP_RETURN\"" - nft "add rule $NFTABLE_NAME PSW2_MANGLE ip daddr @$NFTSET_WAN counter return comment \"WAN_IP_RETURN\"" - } - unset WAN_IP + #ipv4 wan_ip + [ -z "${is_tproxy}" ] && nft "add rule $NFTABLE_NAME PSW2_NAT ip daddr @$NFTSET_WAN counter return comment \"WAN_IP_RETURN\"" + nft "add rule $NFTABLE_NAME PSW2_MANGLE ip daddr @$NFTSET_WAN counter return comment \"WAN_IP_RETURN\"" ip rule add fwmark ${FWMARK} table 999 priority 999 ip route add local 0.0.0.0/0 dev lo table 999 @@ -867,10 +878,11 @@ add_firewall_rule() { local dns_port=$(echo $auto_dns | awk -F '#' '{print $2}') if [[ "$dns_address" == *::* ]]; then nft "insert rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE_V6 meta l4proto udp ip6 daddr ${dns_address} $(factor ${dns_port:-53} "udp dport") counter return" + log_i18n 1 "$(i18n "Add direct DNS to %s: %s" "nftables" "[${dns_address}]:${dns_port:-53}")" else nft "insert rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE ip protocol udp ip daddr ${dns_address} $(factor ${dns_port:-53} "udp dport") counter return" + log_i18n 1 "$(i18n "Add direct DNS to %s: %s" "nftables" "${dns_address}:${dns_port:-53}")" fi - log_i18n 1 "$(i18n "Add direct DNS to %s: %s" "nftables" "${dns_address}:${dns_port:-53}")" done } @@ -879,13 +891,7 @@ add_firewall_rule() { nft "add rule $NFTABLE_NAME mangle_prerouting meta nfproto {ipv6} counter jump PSW2_MANGLE_V6" nft "add rule $NFTABLE_NAME mangle_output meta nfproto {ipv6} counter jump PSW2_OUTPUT_MANGLE_V6 comment \"PSW2_OUTPUT_MANGLE\"" - WAN6_IP=$(get_wan_ips ip6) - [ -n "${WAN6_IP}" ] && { - nft flush set $NFTABLE_NAME $NFTSET_WAN6 - echo $WAN6_IP | insert_nftset $NFTSET_WAN6 "-1" - nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 ip6 daddr @$NFTSET_WAN6 counter return comment \"WAN6_IP_RETURN\"" - } - unset WAN6_IP + nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 ip6 daddr @$NFTSET_WAN6 counter return comment \"WAN6_IP_RETURN\"" ip -6 rule add fwmark ${FWMARK} table 999 priority 999 ip -6 route add local ::/0 dev lo table 999 @@ -894,7 +900,7 @@ add_firewall_rule() { [ "$ENABLED_DEFAULT_ACL" == 1 ] && { TCP_LOCALHOST_PROXY=$LOCALHOST_PROXY UDP_LOCALHOST_PROXY=$LOCALHOST_PROXY - + msg="$(i18n "[Local],")" [ "$TCP_NO_REDIR_PORTS" != "disable" ] && { nft "add rule $NFTABLE_NAME $nft_output_chain ip protocol tcp $(factor $TCP_NO_REDIR_PORTS "tcp dport") counter return" @@ -906,7 +912,7 @@ add_firewall_rule() { log 1 "${msg}$(i18n "not proxy all %s" "TCP")" fi } - + [ "$UDP_NO_REDIR_PORTS" != "disable" ] && { nft "add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE ip protocol udp $(factor $UDP_NO_REDIR_PORTS "udp dport") counter return" nft "add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE_V6 meta l4proto udp $(factor $UDP_NO_REDIR_PORTS "udp dport") counter return" @@ -1088,20 +1094,9 @@ gen_include() { local __nft=" " __nft=$(cat <<- EOF - . $UTILS_PATH [ -z "\$(nft list chain $NFTABLE_NAME mangle_prerouting | grep PSW2)" ] && nft -f ${nft_chain_file} - WAN_IP=\$(get_wan_ips ip4) - [ ! -z "\${WAN_IP}" ] && { - nft flush set $NFTABLE_NAME $NFTSET_WAN - echo "\${WAN_IP}" | sh ${MY_PATH} insert_nftset $NFTSET_WAN "-1" - } - [ "$PROXY_IPV6" == "1" ] && { - WAN6_IP=\$(get_wan_ips ip6) - [ ! -z "\${WAN6_IP}" ] && { - nft flush set $NFTABLE_NAME $NFTSET_WAN6 - echo "\${WAN6_IP}" | sh ${MY_PATH} insert_nftset $NFTSET_WAN6 "-1" - } - } + + ${MY_PATH} update_wan_sets EOF ) @@ -1150,6 +1145,9 @@ mwan3_start) mwan3_stop) mwan3_stop ;; +update_wan_sets) + update_wan_sets "$@" + ;; stop) stop ;; diff --git a/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua b/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua index c1ada5881..90d15f0c4 100755 --- a/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua +++ b/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua @@ -31,59 +31,60 @@ local has_ssr = api.is_finded("ssr-local") and api.is_finded("ssr-redir") local has_singbox = api.finded_com("sing-box") local has_xray = api.finded_com("xray") local has_hysteria2 = api.finded_com("hysteria") -local allowInsecure_default = true +local DEFAULT_ALLOWINSECURE = true +local DEFAULT_FILTER_KEYWORD_MODE = uci:get(appname, "@global_subscribe[0]", "filter_keyword_mode") or "0" +local DEFAULT_FILTER_KEYWORD_DISCARD_LIST = uci:get(appname, "@global_subscribe[0]", "filter_discard_list") or {} +local DEFAULT_FILTER_KEYWORD_KEEP_LIST = uci:get(appname, "@global_subscribe[0]", "filter_keep_list") or {} -- Nodes should be retrieved using the core type (if not set on the node subscription page, the default type will be used automatically). -local ss_type_default = api.get_core("ss_type", {{has_ss,"shadowsocks-libev"},{has_ss_rust,"shadowsocks-rust"},{has_singbox,"sing-box"},{has_xray,"xray"}}) -local trojan_type_default = api.get_core("trojan_type", {{has_singbox,"sing-box"},{has_xray,"xray"}}) -local vmess_type_default = api.get_core("vmess_type", {{has_xray,"xray"},{has_singbox,"sing-box"}}) -local vless_type_default = api.get_core("vless_type", {{has_xray,"xray"},{has_singbox,"sing-box"}}) -local hysteria2_type_default = api.get_core("hysteria2_type", {{has_hysteria2,"hysteria2"},{has_singbox,"sing-box"},{has_xray,"xray"}}) +local DEFAULT_SS_TYPE = api.get_core("ss_type", {{has_ss,"shadowsocks-libev"},{has_ss_rust,"shadowsocks-rust"},{has_singbox,"sing-box"},{has_xray,"xray"}}) +local DEFAULT_TROJAN_TYPE = api.get_core("trojan_type", {{has_singbox,"sing-box"},{has_xray,"xray"}}) +local DEFAULT_VMESS_TYPE = api.get_core("vmess_type", {{has_xray,"xray"},{has_singbox,"sing-box"}}) +local DEFAULT_VLESS_TYPE = api.get_core("vless_type", {{has_xray,"xray"},{has_singbox,"sing-box"}}) +local DEFAULT_HYSTERIA2_TYPE = api.get_core("hysteria2_type", {{has_hysteria2,"hysteria2"},{has_singbox,"sing-box"},{has_xray,"xray"}}) local core_has = { - ["xray"] = has_xray, ["sing-box"] = has_singbox, ["shadowsocks-libev"] = has_ss,["shadowsocks-rust"] = has_ss_rust, + ["xray"] = has_xray, + ["sing-box"] = has_singbox, + ["shadowsocks-libev"] = has_ss, + ["shadowsocks-rust"] = has_ss_rust, ["hysteria2"] = has_hysteria2 } -local domain_resolver, domain_resolver_dns, domain_resolver_dns_https, domain_strategy -local preproxy_node_group, to_node_group, chain_node_type = "", "", "" -- Determine whether to filter node keywords -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 {} -local filter_keyword_keep_list_default = uci:get(appname, "@global_subscribe[0]", "filter_keep_list") or {} -local function is_filter_keyword(value) - if filter_keyword_mode_default == "1" then - for k,v in ipairs(filter_keyword_discard_list_default) do +local function is_filter_keyword(mode, discard_list, keep_list, value) + if mode == "1" then + for k,v in ipairs(discard_list) do if value:find(v, 1, true) then return true end end - elseif filter_keyword_mode_default == "2" then + elseif mode == "2" then local result = true - for k,v in ipairs(filter_keyword_keep_list_default) do + for k,v in ipairs(keep_list) do if value:find(v, 1, true) then result = false end end return result - elseif filter_keyword_mode_default == "3" then + elseif mode == "3" then local result = false - for k,v in ipairs(filter_keyword_discard_list_default) do + for k,v in ipairs(discard_list) do if value:find(v, 1, true) then result = true end end - for k,v in ipairs(filter_keyword_keep_list_default) do + for k,v in ipairs(keep_list) do if value:find(v, 1, true) then result = false end end return result - elseif filter_keyword_mode_default == "4" then + elseif mode == "4" then local result = true - for k,v in ipairs(filter_keyword_keep_list_default) do + for k,v in ipairs(keep_list) do if value:find(v, 1, true) then result = false end end - for k,v in ipairs(filter_keyword_discard_list_default) do + for k,v in ipairs(discard_list) do if value:find(v, 1, true) then result = true end @@ -425,16 +426,16 @@ local function get_subscribe_info(cfgid, value) end -- Configure the SS protocol implementation type -local function set_ss_implementation(result) - if ss_type_default == "shadowsocks-libev" and has_ss then +local function set_ss_implementation(ss_type, result) + if ss_type == "shadowsocks-libev" and has_ss then result.type = "SS" - elseif ss_type_default == "shadowsocks-rust" and has_ss_rust then + elseif ss_type == "shadowsocks-rust" and has_ss_rust then result.type = 'SS-Rust' - elseif ss_type_default == "xray" and has_xray then + elseif ss_type == "xray" and has_xray then result.type = 'Xray' result.protocol = 'shadowsocks' result.transport = 'raw' - elseif ss_type_default == "sing-box" and has_singbox then + elseif ss_type == "sing-box" and has_singbox then result.type = 'sing-box' result.protocol = 'shadowsocks' else @@ -445,8 +446,60 @@ local function set_ss_implementation(result) end -- Processing data -local function processData(szType, content, add_mode, group) +local function processData(szType, content, add_mode, group, sub_cfg) --log(2, content, add_mode, group) + local default_allowinsecure = DEFAULT_ALLOWINSECURE + local default_filter_keyword_mode = DEFAULT_FILTER_KEYWORD_MODE + local default_filter_keyword_discard_list = DEFAULT_FILTER_KEYWORD_DISCARD_LIST + local default_filter_keyword_keep_list = DEFAULT_FILTER_KEYWORD_KEEP_LIST + local default_ss_type = DEFAULT_SS_TYPE + local default_trojan_type = DEFAULT_TROJAN_TYPE + local default_vmess_type = DEFAULT_VMESS_TYPE + local default_vless_type = DEFAULT_VLESS_TYPE + local default_hysteria2_type = DEFAULT_HYSTERIA2_TYPE + if sub_cfg then + if sub_cfg.allowInsecure and sub_cfg.allowInsecure ~= "1" then + default_allowinsecure = nil + end + local filter_keyword_mode = sub_cfg.filter_keyword_mode or "5" -- 5 is global + if filter_keyword_mode == "0" then + default_filter_keyword_mode = "0" + elseif filter_keyword_mode == "1" then + default_filter_keyword_mode = "1" + default_filter_keyword_discard_list = sub_cfg.filter_discard_list or {} + elseif filter_keyword_mode == "2" then + default_filter_keyword_mode = "2" + default_filter_keyword_keep_list = sub_cfg.filter_keep_list or {} + elseif filter_keyword_mode == "3" then + default_filter_keyword_mode = "3" + default_filter_keyword_keep_list = sub_cfg.filter_keep_list or {} + default_filter_keyword_discard_list = sub_cfg.filter_discard_list or {} + elseif filter_keyword_mode == "4" then + default_filter_keyword_mode = "4" + default_filter_keyword_keep_list = sub_cfg.filter_keep_list or {} + default_filter_keyword_discard_list = sub_cfg.filter_discard_list or {} + end + local ss_type = sub_cfg.ss_type or "global" + if ss_type ~= "global" and core_has[ss_type] then + default_ss_type = ss_type + end + local trojan_type = sub_cfg.trojan_type or "global" + if trojan_type ~= "global" and core_has[trojan_type] then + default_trojan_type = trojan_type + end + local vmess_type = sub_cfg.vmess_type or "global" + if vmess_type ~= "global" and core_has[vmess_type] then + default_vmess_type = vmess_type + end + local vless_type = sub_cfg.vless_type or "global" + if vless_type ~= "global" and core_has[vless_type] then + default_vless_type = vless_type + end + local hysteria2_type = sub_cfg.hysteria2_type or "global" + if hysteria2_type ~= "global" and core_has[hysteria2_type] then + default_hysteria2_type = hysteria2_type + end + end local result = { timeout = 60, add_mode = add_mode, -- `0` for manual configuration, `1` for import, `2` for subscription @@ -486,9 +539,9 @@ local function processData(szType, content, add_mode, group) result.remarks = base64Decode(params.remarks) elseif szType == 'vmess' then local info = jsonParse(content) - if vmess_type_default == "sing-box" and has_singbox then + if default_vmess_type == "sing-box" and has_singbox then result.type = 'sing-box' - elseif vmess_type_default == "xray" and has_xray then + elseif default_vmess_type == "xray" and has_xray then result.type = "Xray" else log(2, i18n.translatef("Skipping the %s node is due to incompatibility with the %s core program or incorrect node usage type settings.", "VMess", "VMess")) @@ -559,12 +612,7 @@ local function processData(szType, content, add_mode, group) if info.net == 'kcp' or info.net == 'mkcp' then info.net = "mkcp" result.mkcp_guise = info.type - result.mkcp_mtu = 1350 - result.mkcp_tti = 50 - result.mkcp_uplinkCapacity = 5 - result.mkcp_downlinkCapacity = 20 - result.mkcp_readBufferSize = 2 - result.mkcp_writeBufferSize = 2 + result.mkcp_seed = info.seed end if info.net == 'quic' then result.quic_guise = info.type @@ -599,7 +647,7 @@ local function processData(szType, content, add_mode, group) result.tls_CertSha = info.pcs result.tls_CertByName = info.vcn local insecure = info.allowinsecure or info.allowInsecure or info.insecure - result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0") + result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0") else result.tls = "0" end @@ -615,7 +663,7 @@ local function processData(szType, content, add_mode, group) return nil end elseif szType == "ss" then - result = set_ss_implementation(result) + result = set_ss_implementation(default_ss_type, result) if not result then return nil end --SS-URI = "ss://" userinfo "@" hostname ":" port [ "/" ] [ "?" plugin ] [ "#" tag ] @@ -825,12 +873,6 @@ local function processData(szType, content, add_mode, group) if params.type == 'kcp' or params.type == 'mkcp' then result.transport = "mkcp" result.mkcp_guise = params.headerType or "none" - result.mkcp_mtu = 1350 - result.mkcp_tti = 50 - result.mkcp_uplinkCapacity = 5 - result.mkcp_downlinkCapacity = 20 - result.mkcp_readBufferSize = 2 - result.mkcp_writeBufferSize = 2 result.mkcp_seed = params.seed end if params.type == 'quic' then @@ -884,7 +926,7 @@ local function processData(szType, content, add_mode, group) end end local insecure = params.allowinsecure or params.allowInsecure or params.insecure - result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0") + result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0") result.uot = params.udp elseif (params.type ~= "tcp" and params.type ~= "raw") and (params.headerType and params.headerType ~= "none") then result.error_msg = i18n.translatef("Please replace Xray or Sing-Box to support more transmission methods in Shadowsocks.") @@ -893,7 +935,7 @@ local function processData(szType, content, add_mode, group) if params["shadow-tls"] then if result.type ~= "sing-box" and result.type ~= "SS-Rust" then - result.error_msg = ss_type_default .. " " .. i18n.translatef("unsupport %s plugin.", "shadow-tls") + result.error_msg = default_ss_type .. " " .. i18n.translatef("unsupport %s plugin.", "shadow-tls") else -- Parsing SS Shadow-TLS plugin parameters local function parseShadowTLSParams(b64str, out) @@ -935,10 +977,10 @@ local function processData(szType, content, add_mode, group) end end elseif szType == "trojan" then - if trojan_type_default == "sing-box" and has_singbox then + if default_trojan_type == "sing-box" and has_singbox then result.type = 'sing-box' result.protocol = 'trojan' - elseif trojan_type_default == "xray" and has_xray then + elseif default_trojan_type == "xray" and has_xray then result.type = 'Xray' result.protocol = 'trojan' else @@ -988,7 +1030,7 @@ local function processData(szType, content, add_mode, group) result.tls_CertSha = params.pcs result.tls_CertByName = params.vcn local insecure = params.allowinsecure or params.allowInsecure or params.insecure - result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0") + result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0") if not params.type then params.type = "tcp" end params.type = string.lower(params.type) @@ -1042,12 +1084,6 @@ local function processData(szType, content, add_mode, group) if params.type == 'kcp' or params.type == 'mkcp' then result.transport = "mkcp" result.mkcp_guise = params.headerType or "none" - result.mkcp_mtu = 1350 - result.mkcp_tti = 50 - result.mkcp_uplinkCapacity = 5 - result.mkcp_downlinkCapacity = 20 - result.mkcp_readBufferSize = 2 - result.mkcp_writeBufferSize = 2 result.mkcp_seed = params.seed end if params.type == 'quic' then @@ -1079,7 +1115,7 @@ local function processData(szType, content, add_mode, group) end end elseif szType == "ssd" then - result = set_ss_implementation(result) + result = set_ss_implementation(default_ss_type, result) if not result then return nil end result.address = content.server result.port = content.port @@ -1090,9 +1126,9 @@ local function processData(szType, content, add_mode, group) result.group = content.airport result.remarks = content.remarks elseif szType == "vless" then - if vless_type_default == "sing-box" and has_singbox then + if default_vless_type == "sing-box" and has_singbox then result.type = 'sing-box' - elseif vless_type_default == "xray" and has_xray then + elseif default_vless_type == "xray" and has_xray then result.type = "Xray" else log(2, i18n.translatef("Skipping the %s node is due to incompatibility with the %s core program or incorrect node usage type settings.", "VLESS", "VLESS")) @@ -1189,12 +1225,7 @@ local function processData(szType, content, add_mode, group) if params.type == 'kcp' or params.type == 'mkcp' then result.transport = "mkcp" result.mkcp_guise = params.headerType or "none" - result.mkcp_mtu = 1350 - result.mkcp_tti = 50 - result.mkcp_uplinkCapacity = 5 - result.mkcp_downlinkCapacity = 20 - result.mkcp_readBufferSize = 2 - result.mkcp_writeBufferSize = 2 + result.mkcp_seed = params.seed end if params.type == 'quic' then result.quic_guise = params.headerType or "none" @@ -1250,7 +1281,7 @@ local function processData(szType, content, add_mode, group) result.reality_mldsa65Verify = params.pqv or nil end local insecure = params.allowinsecure or params.allowInsecure or params.insecure - result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0") + result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0") end result.port = port @@ -1307,7 +1338,7 @@ local function processData(szType, content, add_mode, group) result.hysteria_auth_password = params.auth result.tls_serverName = params.peer or params.sni or "" local insecure = params.allowinsecure or params.allowInsecure or params.insecure - result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0") + result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0") result.alpn = params.alpn result.hysteria_up_mbps = params.upmbps result.hysteria_down_mbps = params.downmbps @@ -1353,12 +1384,12 @@ local function processData(szType, content, add_mode, group) result.tls_CertSha = params.pcs result.tls_CertByName = params.vcn local insecure = params.allowinsecure or params.insecure - result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0") + result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0") result.hysteria2_tls_pinSHA256 = params.pinSHA256 result.hysteria2_hop = params.mport - if (hysteria2_type_default == "sing-box" and has_singbox) or (hysteria2_type_default == "xray" and has_xray) then - local is_singbox = hysteria2_type_default == "sing-box" and has_singbox + if (default_hysteria2_type == "sing-box" and has_singbox) or (default_hysteria2_type == "xray" and has_xray) then + local is_singbox = default_hysteria2_type == "sing-box" and has_singbox result.type = is_singbox and 'sing-box' or 'Xray' result.protocol = "hysteria2" if params["obfs-password"] or params["obfs_password"] then @@ -1433,7 +1464,7 @@ local function processData(szType, content, add_mode, group) result.tuic_congestion_control = params.congestion_control or "cubic" result.tuic_udp_relay_mode = params.udp_relay_mode or "native" local insecure = params.allowinsecure or params.insecure or params.allow_insecure - result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0") + result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0") elseif szType == "anytls" then if has_singbox then result.type = 'sing-box' @@ -1497,7 +1528,7 @@ local function processData(szType, content, add_mode, group) end result.port = port local insecure = params.allowinsecure or params.insecure - result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0") + result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0") end elseif szType == 'naive+https' or szType == 'naive+quic' then if has_singbox then @@ -1840,6 +1871,29 @@ local function update_node(manual) for _, v in ipairs(nodeResult) do local remark = v["remark"] local list = v["list"] + local sub_cfg = v["sub_cfg"] + local domain_resolver, domain_resolver_dns, domain_resolver_dns_https, domain_strategy + local preproxy_node_group, to_node_group, chain_node_type = "", "", "" + -- Subscription Group Chain Agent + 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 + if sub_cfg then + domain_resolver = sub_cfg.domain_resolver + domain_resolver_dns = sub_cfg.domain_resolver_dns + domain_resolver_dns_https = sub_cfg.domain_resolver_dns_https + domain_strategy = (sub_cfg.domain_strategy == "UseIPv4" or sub_cfg.domain_strategy == "UseIPv6") and sub_cfg.domain_strategy or nil + preproxy_node_group = (sub_cfg.chain_proxy == "1") and valid_chain_node(sub_cfg.preproxy_node) or "" + to_node_group = (sub_cfg.chain_proxy == "2") and valid_chain_node(sub_cfg.to_node) or "" + end for _, vv in ipairs(list) do local cfgid = uci:section(appname, "nodes", api.gen_short_uuid()) for kkk, vvv in pairs(vv) do @@ -1929,8 +1983,12 @@ local function update_node(manual) end end -local function parse_link(raw, add_mode, group, cfgid) +local function parse_link(raw, add_mode, group, sub_cfg) if raw and #raw > 0 then + local cfgid + if sub_cfg then + cfgid = sub_cfg[".name"] + end local nodes, szType local node_list = {} -- ssd appear to be in this format, starting with ssd://. @@ -1965,17 +2023,17 @@ local function parse_link(raw, add_mode, group, cfgid) xpcall(function () local result if szType == 'ssd' then - result = processData(szType, v, add_mode, group) + result = processData(szType, v, add_mode, group, sub_cfg) elseif not szType then local node = api.trim(v) local dat = split(node, "://") if dat and dat[1] and dat[2] then if dat[1] == 'vmess' or dat[1] == 'ssr' then local link = api.trim(dat[2]:gsub("#.*$", "")) - result = processData(dat[1], base64Decode(link), add_mode, group) + result = processData(dat[1], base64Decode(link), add_mode, group, sub_cfg) else local link = dat[2]:gsub("&", "&"):gsub("%s*#%s*", "#") -- Some odd links use "&" as "&", and include spaces before and after "#". - result = processData(dat[1], link, add_mode, group) + result = processData(dat[1], link, add_mode, group, sub_cfg) end end else @@ -1987,7 +2045,7 @@ local function parse_link(raw, add_mode, group, cfgid) log(2, i18n.translatef("Discard node: %s, Reason:", result.remarks) .. " " .. result.error_msg) elseif not result.type then log(2, i18n.translatef("Discard node: %s, Reason:", result.remarks) .. " " .. i18n.translatef("No usable binary was found.")) - elseif (add_mode == "2" and is_filter_keyword(result.remarks)) or not result.address or result.remarks == "NULL" or result.address == "127.0.0.1" or + elseif (add_mode == "2" and is_filter_keyword(default_filter_keyword_mode, default_filter_keyword_discard_list, default_filter_keyword_keep_list, result.remarks)) or not result.address or result.remarks == "NULL" or result.address == "127.0.0.1" or (not datatypes.hostname(result.address) and not (api.is_ip(result.address))) then log(2, i18n.translatef("Discard filter nodes: %s type node %s", result.type, result.remarks)) else @@ -2007,7 +2065,8 @@ local function parse_link(raw, add_mode, group, cfgid) if #node_list > 0 then nodeResult[#nodeResult + 1] = { remark = group, - list = node_list + list = node_list, + sub_cfg = sub_cfg } end log(2, i18n.translatef("Successfully resolved the [%s] node, number: %s", group, #node_list)) @@ -2038,77 +2097,26 @@ local execute = function() local cfgid = value[".name"] local remark = value.remark or "" local url = value.url or "" - if value.allowInsecure and value.allowInsecure ~= "1" then - allowInsecure_default = nil - end - local filter_keyword_mode = value.filter_keyword_mode or "5" - if filter_keyword_mode == "0" then - filter_keyword_mode_default = "0" - elseif filter_keyword_mode == "1" then - filter_keyword_mode_default = "1" - filter_keyword_discard_list_default = value.filter_discard_list or {} - elseif filter_keyword_mode == "2" then - filter_keyword_mode_default = "2" - filter_keyword_keep_list_default = value.filter_keep_list or {} - elseif filter_keyword_mode == "3" then - filter_keyword_mode_default = "3" - filter_keyword_keep_list_default = value.filter_keep_list or {} - filter_keyword_discard_list_default = value.filter_discard_list or {} - elseif filter_keyword_mode == "4" then - filter_keyword_mode_default = "4" - filter_keyword_keep_list_default = value.filter_keep_list or {} - filter_keyword_discard_list_default = value.filter_discard_list or {} - end - local ss_type = value.ss_type or "global" - if ss_type ~= "global" and core_has[ss_type] then - ss_type_default = ss_type - end - local trojan_type = value.trojan_type or "global" - if trojan_type ~= "global" and core_has[trojan_type] then - trojan_type_default = trojan_type - end - local vmess_type = value.vmess_type or "global" - if vmess_type ~= "global" and core_has[vmess_type] then - vmess_type_default = vmess_type - end - local vless_type = value.vless_type or "global" - if vless_type ~= "global" and core_has[vless_type] then - vless_type_default = vless_type - end - local hysteria2_type = value.hysteria2_type or "global" - if hysteria2_type ~= "global" and core_has[hysteria2_type] then - hysteria2_type_default = hysteria2_type - end - domain_resolver = value.domain_resolver - domain_resolver_dns = value.domain_resolver_dns - domain_resolver_dns_https = value.domain_resolver_dns_https - domain_strategy = (value.domain_strategy == "UseIPv4" or value.domain_strategy == "UseIPv6") and value.domain_strategy or nil - - -- Subscription Group Chain Agent - 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 i18n.translatef("Auto") or (access_mode == "direct" and i18n.translatef("Direct") or (access_mode == "proxy" and i18n.translatef("Proxy") or i18n.translatef("Auto"))) - log(1, i18n.translatef("Start subscribing: %s", '【' .. remark .. '】' .. url .. ' [' .. result .. ']')) - local tmp_file = "/tmp/" .. cfgid - value.http_code = curl(url, tmp_file, ua, access_mode) - if value.http_code ~= 200 then - fail_list[#fail_list + 1] = value + local url_is_local + if fs.access(url) then + -- debug, reads local files. + log(1, i18n.translatef("Start subscribing: %s", '【' .. remark .. '】' .. url)) + url_is_local = true + tmp_file = url else + local ua = value.user_agent + local access_mode = value.access_mode + local result = (not access_mode) and i18n.translatef("Auto") or (access_mode == "direct" and i18n.translatef("Direct") or (access_mode == "proxy" and i18n.translatef("Proxy") or i18n.translatef("Auto"))) + log(1, i18n.translatef("Start subscribing: %s", '【' .. remark .. '】' .. url .. ' [' .. result .. ']')) + tmp_file = "/tmp/" .. cfgid + value.http_code = curl(url, tmp_file, ua, access_mode) + if value.http_code ~= 200 then + fail_list[#fail_list + 1] = value + luci.sys.call("rm -f " .. tmp_file) + end + end + if fs.access(tmp_file) then if luci.sys.call("[ -f " .. tmp_file .. " ] && sed -i -e '/^[ \t]*$/d' -e '/^[ \t]*\r$/d' " .. tmp_file) == 0 then local f = io.open(tmp_file, "r") local stdout = f:read("*all") @@ -2119,33 +2127,18 @@ local execute = function() if not manual_sub and old_md5 == new_md5 then log(1, i18n.translatef("Subscription: [%s] No changes, no update required.", remark)) else - parse_link(raw_data, "2", remark, cfgid) + parse_link(raw_data, "2", remark, value) uci:set(appname, cfgid, "md5", new_md5) end else fail_list[#fail_list + 1] = value end + if url_is_local then + value.http_code = 0 + else + luci.sys.call("rm -f " .. tmp_file) + end end - allowInsecure_default = true - luci.sys.call("rm -f " .. tmp_file) - filter_keyword_mode_default = uci:get(appname, "@global_subscribe[0]", "filter_keyword_mode") or "0" - filter_keyword_discard_list_default = uci:get(appname, "@global_subscribe[0]", "filter_discard_list") or {} - filter_keyword_keep_list_default = uci:get(appname, "@global_subscribe[0]", "filter_keep_list") or {} - - ss_type = uci:get(appname, "@global_subscribe[0]", "ss_type") or "" - ss_type_default = core_has[ss_type] and ss_type or ss_type_default - - trojan_type = uci:get(appname, "@global_subscribe[0]", "trojan_type") or "" - trojan_type_default = core_has[trojan_type] and trojan_type or trojan_type_default - - vmess_type = uci:get(appname, "@global_subscribe[0]", "vmess_type") or "" - vmess_type_default = core_has[vmess_type] and vmess_type or vmess_type_default - - vless_type = uci:get(appname, "@global_subscribe[0]", "vless_type") or "" - vless_type_default = core_has[vless_type] and vless_type or vless_type_default - - hysteria2_type = uci:get(appname, "@global_subscribe[0]", "hysteria2_type") or "" - hysteria2_type_default = core_has[hysteria2_type] and hysteria2_type or hysteria2_type_default end if #fail_list > 0 then