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("数据目录", '') +
+ fieldInput("数据目录", '' +
+ '
' + 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