diff --git a/ddns-go/Makefile b/ddns-go/Makefile
index 9b9bfe607..9499dbc70 100644
--- a/ddns-go/Makefile
+++ b/ddns-go/Makefile
@@ -5,8 +5,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=ddns-go
-PKG_VERSION:=6.16.9
-PKG_RELEASE:=8
+PKG_VERSION:=6.16.10
+PKG_RELEASE:=9
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/jeessy2/ddns-go/tar.gz/v$(PKG_VERSION)?
diff --git a/luci-app-mosdns/Makefile b/luci-app-mosdns/Makefile
index 8d33cea09..8f78bfb74 100644
--- a/luci-app-mosdns/Makefile
+++ b/luci-app-mosdns/Makefile
@@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-mosdns
-PKG_VERSION:=1.7.0
-PKG_RELEASE:=4
+PKG_VERSION:=1.7.1
+PKG_RELEASE:=5
LUCI_TITLE:=LuCI Support for mosdns
LUCI_PKGARCH:=all
diff --git a/luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js b/luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js
index d8f047c88..1c9c0bd09 100644
--- a/luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js
+++ b/luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js
@@ -391,10 +391,7 @@ return view.extend({
if (value == formvalue) {
return;
}
- return fs.write('/etc/mosdns/rule/cloudflare-cidr.txt', formvalue.trim().replace(/\r\n/g, '\n') + '\n')
- .then(function (i) {
- return fs.exec('/etc/init.d/mosdns', ['restart']);
- });
+ return fs.write('/etc/mosdns/rule/cloudflare-cidr.txt', formvalue.trim().replace(/\r\n/g, '\n') + '\n');
});
};
diff --git a/luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js b/luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js
index 4f49aaa36..e43bb3579 100644
--- a/luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js
+++ b/luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js
@@ -3,12 +3,6 @@
'require fs';
'require ui';
'require view';
-'require rpc';
-
-var callRestart = rpc.declare({
- object: 'luci.mosdns',
- method: 'restart'
-});
return view.extend({
render: function () {
@@ -274,19 +268,9 @@ return view.extend({
},
handleSaveApply: function (ev) {
- var m = this.map;
- onclick = L.bind(this.handleSave, this, m);
- return callRestart()
- .then(function (res) {
- if (res && res.code === 0) {
- window.location.reload();
- } else {
- ui.addNotification(null, E('p', _('Failed to restart mosdns: %s').format(res.output || 'Unknown error')));
- }
- })
- .catch(function (e) {
- ui.addNotification(null, E('p', _('Failed to restart mosdns: %s').format(e.message)));
- });
+ return this.handleSave(ev).then(function() {
+ window.location.reload();
+ });
},
handleReset: null
});
diff --git a/luci-app-mosdns/root/etc/init.d/mosdns b/luci-app-mosdns/root/etc/init.d/mosdns
index 4d2861480..59d8073dd 100755
--- a/luci-app-mosdns/root/etc/init.d/mosdns
+++ b/luci-app-mosdns/root/etc/init.d/mosdns
@@ -822,10 +822,7 @@ start_service() {
fi
fi
- [ "$update_list" -eq 1 ] && {
- sleep 3
- $MOSDNS_SCRIPT update_adlist
- } &
+ [ "$update_list" -eq 1 ] && $MOSDNS_SCRIPT update_adlist &
}
stop_service() {
diff --git a/luci-app-mosdns/root/usr/share/mosdns/mosdns.uc b/luci-app-mosdns/root/usr/share/mosdns/mosdns.uc
index 943a1ffd7..cf0a537a4 100755
--- a/luci-app-mosdns/root/usr/share/mosdns/mosdns.uc
+++ b/luci-app-mosdns/root/usr/share/mosdns/mosdns.uc
@@ -315,12 +315,10 @@ switch (action) {
case "update":
update_geodat();
update_adlist();
- exec_sys('/etc/init.d/mosdns restart');
+ v2dat_dump();
break;
case "update_adlist":
- if (update_adlist()) {
- exec_sys('/etc/init.d/mosdns restart');
- }
+ update_adlist();
break;
case "v2dat_dump":
v2dat_dump();
diff --git a/luci-app-mosdns/root/usr/share/rpcd/acl.d/luci-app-mosdns.json b/luci-app-mosdns/root/usr/share/rpcd/acl.d/luci-app-mosdns.json
index 35a86d07f..438fefcc3 100644
--- a/luci-app-mosdns/root/usr/share/rpcd/acl.d/luci-app-mosdns.json
+++ b/luci-app-mosdns/root/usr/share/rpcd/acl.d/luci-app-mosdns.json
@@ -18,8 +18,7 @@
"clean_log",
"flush_cache",
"get_version",
- "print_log",
- "restart"
+ "print_log"
]
},
"uci": [ "mosdns" ]
diff --git a/luci-app-mosdns/root/usr/share/rpcd/ucode/luci.mosdns b/luci-app-mosdns/root/usr/share/rpcd/ucode/luci.mosdns
index d04e367b0..25247d21b 100644
--- a/luci-app-mosdns/root/usr/share/rpcd/ucode/luci.mosdns
+++ b/luci-app-mosdns/root/usr/share/rpcd/ucode/luci.mosdns
@@ -34,11 +34,6 @@ function exec_sys(cmd) {
return { code: code, stdout: stdout || "" };
}
-function restart_mosdns() {
- let p = popen('(sleep 1; /etc/init.d/mosdns restart) >/dev/null 2>&1 &', 'r');
- if (p) p.close();
-}
-
function get_logfile_path_internal() {
let uci_cursor = cursor();
uci_cursor.load('mosdns');
@@ -124,13 +119,6 @@ const methods = {
return { error: res.stdout };
}
}
- },
-
- restart: {
- call: function() {
- restart_mosdns();
- return { code: 0, output: "mosdns is restarting in background." };
- }
}
};
diff --git a/luci-app-passwall/Makefile b/luci-app-passwall/Makefile
index f992e5492..8fa03a358 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:=85
+PKG_RELEASE:=86
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \
diff --git a/luci-app-passwall/luasrc/controller/passwall.lua b/luci-app-passwall/luasrc/controller/passwall.lua
index 070a7ea28..8f0f6609d 100644
--- a/luci-app-passwall/luasrc/controller/passwall.lua
+++ b/luci-app-passwall/luasrc/controller/passwall.lua
@@ -117,6 +117,8 @@ function index()
--[[geoview]]
entry({"admin", "services", appname, "geo_view"}, call("geo_view")).leaf = true
+
+ entry({"admin", "services", appname, "fetch_certsha256"}, call("fetch_certsha256")).leaf = true
end
local function http_write_json(content)
@@ -1103,3 +1105,15 @@ function flush_set()
http.redirect(api.url("log"))
end
end
+
+function fetch_certsha256()
+ local id = http.formvalue("id") or ""
+ local address = (id ~= "") and uci:get(appname, id, "address") or ""
+ local port = (id ~= "") and uci:get(appname, id, "port") or 0
+ if id == "" or address == "" or not api.datatypes.hostname(address) or port == 0 then
+ http_write_json({ code = 1 })
+ return
+ end
+ local data = api.fetch_cert_sha256(address, port, 5)
+ http_write_json(data ~= "" and { code = 0, data = data } or { code = 1 })
+end
diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/client/type/ray.lua b/luci-app-passwall/luasrc/model/cbi/passwall/client/type/ray.lua
index 9957ac930..cbab09588 100644
--- a/luci-app-passwall/luasrc/model/cbi/passwall/client/type/ray.lua
+++ b/luci-app-passwall/luasrc/model/cbi/passwall/client/type/ray.lua
@@ -376,9 +376,11 @@ if api.compare_versions(os.date("%Y.%m.%d"), "<", "2026.6.1") then
end
if api.compare_versions(xray_version, ">=", "26.1.31") then
- o = s:option(Value, _n("tls_CertSha"), translate("TLS Chain Fingerprint (SHA256)"), translate("Once set, connects only when the server’s chain fingerprint matches."))
+ o = s:option(Value, _n("tls_CertSha"), translate("TLS Chain Fingerprint (SHA256)"))
o:depends({ [_n("tls")] = true, [_n("reality")] = false })
o:depends({ [_n("protocol")] = "hysteria2" })
+ o.description = translate("Once set, connects only when the server’s chain fingerprint matches.") ..
+ string.format("%s", "[ " .. translate("Fetch Manually") .. " ]")
o = s:option(Value, _n("tls_CertByName"), translate("TLS Certificate Name (CertName)"), translate("TLS is used to verify the leaf certificate name."))
o:depends({ [_n("tls")] = true, [_n("reality")] = false })
diff --git a/luci-app-passwall/luasrc/passwall/api.lua b/luci-app-passwall/luasrc/passwall/api.lua
index ddd5d7d59..65e839046 100644
--- a/luci-app-passwall/luasrc/passwall/api.lua
+++ b/luci-app-passwall/luasrc/passwall/api.lua
@@ -1531,3 +1531,31 @@ function cleanEmptyTables(t)
end
return next(t) and t or nil
end
+
+function fetch_cert_sha256(host, port, timeout)
+ if not host or not datatypes.hostname(host) then return "" end
+ port = tonumber(port) or 443
+ timeout = tonumber(timeout) or 5
+ local xray = finded_com("xray")
+ local cmd
+ if xray then
+ cmd = string.format(
+ "timeout %d %q tls ping %s:%d 2>/dev/null " ..
+ "| awk 'tolower($0) ~ /without sni/ {f=1} tolower($0) ~ /with sni/ {f=0} " ..
+ "f && tolower($0) ~ /cert.*leaf.*sha256/ {sub(/.*:/,\"\"); gsub(/[[:space:]]/,\"\"); print; exit}'",
+ timeout, xray, host, port
+ )
+ else
+ cmd = string.format(
+ "timeout %d openssl s_client -connect %s:%d -servername %s -showcerts /dev/null " ..
+ "| awk 'BEGIN{c=0}/BEGIN CERT/{c++} c==1{print} /END CERT/{if(c==1)exit}' " ..
+ "| openssl x509 -outform der 2>/dev/null " ..
+ "| sha256sum 2>/dev/null",
+ timeout, host, port, host
+ )
+ end
+ local out = trim(sys.exec(cmd))
+ local fp = out:match("^([0-9a-fA-F]+)")
+ if not fp then return "" end
+ return fp:upper()
+end
diff --git a/luci-app-passwall/luasrc/view/passwall/node_config/header.htm b/luci-app-passwall/luasrc/view/passwall/node_config/header.htm
index 06bf0eea8..108ad838d 100644
--- a/luci-app-passwall/luasrc/view/passwall/node_config/header.htm
+++ b/luci-app-passwall/luasrc/view/passwall/node_config/header.htm
@@ -52,5 +52,22 @@ local api = self.api
}
});
}
+
+ function fetchCertSha256() {
+ XHR.get('<%=api.url("fetch_certsha256")%>', { id: node_id }, function(x, res) {
+ if (!x || x.status !== 200) {
+ alert("<%:Error%>");
+ return;
+ }
+ if (!res || res.code !== 0 || !res.data) {
+ alert(res && res.msg ? res.msg : "<%:Fetch Failed%> !");
+ return;
+ }
+ const inputs = document.querySelectorAll('input[id$=".xray_tls_CertSha"]');
+ inputs.forEach(function(input) {
+ input.value = res.data;
+ });
+ });
+ }
//]]>
diff --git a/luci-app-passwall/po/zh-cn/passwall.po b/luci-app-passwall/po/zh-cn/passwall.po
index 6c4dd7ee1..105d0c4ea 100644
--- a/luci-app-passwall/po/zh-cn/passwall.po
+++ b/luci-app-passwall/po/zh-cn/passwall.po
@@ -1897,6 +1897,12 @@ msgstr "TLS 证书名称(CertName)"
msgid "TLS is used to verify the leaf certificate name."
msgstr "TLS 用于验证 leaf 证书的 name。"
+msgid "Fetch Manually"
+msgstr "手动获取"
+
+msgid "Fetch Failed"
+msgstr "获取失败"
+
msgid "ECH Config"
msgstr "ECH 配置"
diff --git a/luci-app-qiyougamebooster/htdocs/luci-static/resources/view/qiyougamebooster.js b/luci-app-qiyougamebooster/htdocs/luci-static/resources/view/qiyougamebooster.js
new file mode 100644
index 000000000..be1e631d9
--- /dev/null
+++ b/luci-app-qiyougamebooster/htdocs/luci-static/resources/view/qiyougamebooster.js
@@ -0,0 +1,96 @@
+// SPDX-License-Identifier: Apache-2.0
+
+'use strict';
+'require form';
+'require poll';
+'require uci';
+'require view';
+'require fs';
+
+function getStatus() {
+ var result
+ try {
+ result = fs.exec('/usr/bin/qiyougamebooster.sh', ['status']);
+ return result.then(function(status) {
+ return status.stdout.trim()
+ })
+ } catch (e) { }
+ return result;
+}
+
+function getVersion() {
+ var result
+ try {
+ result = fs.exec('/usr/bin/qiyougamebooster.sh', ['version'])
+ return result.then(function(version) {
+ return version.stdout.trim() || _('QiYou Game Booster');
+ }).catch(function() {
+ return _('QiYou Game Booster');
+ });
+ } catch (e) { }
+ return Promise.resolve(_('QiYou Game Booster'));
+}
+
+function renderStatus(status, version) {
+ var spanTemp = '%s: %s %s';
+ var renderHTML;
+ if (status == 'NOT ENABLED' || status == 'NOT RUNNING' || status == 'NOT SUPPORTED') {
+ renderHTML = spanTemp.format('red', _('Status'), _(version), _(status));
+ } else {
+ renderHTML = spanTemp.format('green', _('Status'), _(version), _(status));
+ }
+ return renderHTML;
+}
+
+return view.extend({
+ load: function() {
+ return Promise.all([
+ uci.load('qiyougamebooster')
+ ]);
+ },
+
+ render: function() {
+ let m, s, o;
+
+ m = new form.Map('qiyougamebooster', _('QiYou Game Booster'),
+ _('Play console games online with less lag and more stability.') + '
' +
+ _('— now supporting PS, Switch, Xbox, PC, and mobile.'));
+
+ s = m.section(form.TypedSection);
+ s.anonymous = true;
+ s.render = function () {
+ poll.add(function () {
+ return L.resolveDefault(getStatus()).then(function (status) {
+ return L.resolveDefault(getVersion()).then(function (version) {
+ var view = document.getElementById('service_status');
+ view.innerHTML = renderStatus(status, version);
+ });
+ });
+ });
+
+ return E('div', { class: 'cbi-section', id: 'status_bar' }, [
+ E('p', { id: 'service_status' }, _('Collecting data...'))
+ ]);
+ }
+
+ s = m.section(form.NamedSection, 'config', 'qiyougamebooster');
+
+ o = s.option(form.Flag, 'enabled', _('Enable'));
+ o.default = o.disabled;
+ o.rmempty = false;
+
+ s = m.section(form.TypedSection);
+ s.anonymous = true;
+ s.render = function () {
+ return E('div', {class: 'cbi-section'}, [
+ E('p', [
+ E('img', {src: '/qiyougamebooster/Tutorial.png', height: '350'})
+ ])
+ ])
+ }
+
+
+ return m.render();
+ }
+
+});
diff --git a/luci-app-qiyougamebooster/luasrc/controller/qiyougamebooster.lua b/luci-app-qiyougamebooster/luasrc/controller/qiyougamebooster.lua
deleted file mode 100644
index 7a28ee80e..000000000
--- a/luci-app-qiyougamebooster/luasrc/controller/qiyougamebooster.lua
+++ /dev/null
@@ -1,12 +0,0 @@
-module("luci.controller.qiyougamebooster", package.seeall)
-
-function index()
- if not nixio.fs.access("/etc/config/qiyougamebooster") then
- return
- end
-
- local page
- page = entry({"admin", "services", "qiyougamebooster"}, cbi("qiyougamebooster"), ("QiYou Game Booster"), 99)
- page.dependent = false
- page.acl_depends = {"luci-app-qiyougamebooster"}
-end
diff --git a/luci-app-qiyougamebooster/luasrc/model/cbi/qiyougamebooster.lua b/luci-app-qiyougamebooster/luasrc/model/cbi/qiyougamebooster.lua
deleted file mode 100644
index 4fcde4a06..000000000
--- a/luci-app-qiyougamebooster/luasrc/model/cbi/qiyougamebooster.lua
+++ /dev/null
@@ -1,28 +0,0 @@
-require("luci.sys")
-
-m = Map("qiyougamebooster", translate("QiYou Game Booster"),
- translate("Play console games online with less lag and more stability.") .. "
" ..
- translate("— now supporting PS, Switch, Xbox, PC, and mobile."))
-
-s = m:section(TypedSection, "qiyougamebooster")
-s.anonymous = true
-s.addremove = false
-
-sts = luci.sys.exec("qiyougamebooster.sh status 2> /dev/null")
-ver = luci.sys.exec("qiyougamebooster.sh version 2> /dev/null")
-o = s:option(DummyValue, "status")
-o.rawhtml = true
-if sts == "NOT ENABLED" or sts == "NOT SUPPORTED" or sts == "NOT RUNNING" then
- o.value = string.format('%s: %s %s', "red", translate("Status"), ver, translate(sts))
-else
- o.value = string.format('%s: %s %s', "green", translate("Status"), ver, translate(sts))
-end
-
-o = s:option(Flag, "enabled", translate("Enable"))
-o.default = 0
-
-o = s:option(DummyValue, "instructions")
-o.rawhtml = true
-o.value = "

"
-
-return m
diff --git a/luci-app-qiyougamebooster/po/templates/qiyougamebooster.pot b/luci-app-qiyougamebooster/po/templates/qiyougamebooster.pot
new file mode 100644
index 000000000..d01d1f585
--- /dev/null
+++ b/luci-app-qiyougamebooster/po/templates/qiyougamebooster.pot
@@ -0,0 +1,52 @@
+msgid ""
+msgstr "Content-Type: text/plain; charset=UTF-8"
+
+#: applications/luci-app-qiyougamebooster/htdocs/luci-static/resources/view/qiyougamebooster.js:26
+#: applications/luci-app-qiyougamebooster/htdocs/luci-static/resources/view/qiyougamebooster.js:28
+#: applications/luci-app-qiyougamebooster/htdocs/luci-static/resources/view/qiyougamebooster.js:53
+msgid "QiYou Game Booster"
+msgstr ""
+
+#: applications/luci-app-qiyougamebooster/htdocs/luci-static/resources/view/qiyougamebooster.js:54
+msgid "Play console games online with less lag and more stability."
+msgstr ""
+
+#: applications/luci-app-qiyougamebooster/htdocs/luci-static/resources/view/qiyougamebooster.js:55
+msgid "— now supporting PS, Switch, Xbox, PC, and mobile."
+msgstr ""
+
+#: applications/luci-app-qiyougamebooster/htdocs/luci-static/resources/view/qiyougamebooster.js:70
+msgid "Collecting data..."
+msgstr ""
+
+#: applications/luci-app-qiyougamebooster/htdocs/luci-static/resources/view/qiyougamebooster.js:76
+msgid "Enable"
+msgstr ""
+
+#: applications/luci-app-qiyougamebooster/htdocs/luci-static/resources/view/qiyougamebooster.js:38
+msgid "Status"
+msgstr ""
+
+msgid "NOT ENABLED"
+msgstr ""
+
+msgid "CLOSED"
+msgstr ""
+
+msgid "BOOSTING"
+msgstr ""
+
+msgid "RUNNING"
+msgstr ""
+
+msgid "NOT SUPPORTED"
+msgstr ""
+
+msgid "DOWNLOADING"
+msgstr ""
+
+msgid "INSTALLING"
+msgstr ""
+
+msgid "NOT RUNNING"
+msgstr ""
diff --git a/luci-app-qiyougamebooster/po/zh-cn b/luci-app-qiyougamebooster/po/zh-cn
new file mode 120000
index 000000000..8d69574dd
--- /dev/null
+++ b/luci-app-qiyougamebooster/po/zh-cn
@@ -0,0 +1 @@
+zh_Hans
\ No newline at end of file
diff --git a/luci-app-qiyougamebooster/po/zh-cn/qiyougamebooster.po b/luci-app-qiyougamebooster/po/zh-cn/qiyougamebooster.po
deleted file mode 100644
index 9740f1354..000000000
--- a/luci-app-qiyougamebooster/po/zh-cn/qiyougamebooster.po
+++ /dev/null
@@ -1,38 +0,0 @@
-msgid "QiYou Game Booster"
-msgstr "奇游联机宝"
-
-msgid "Play console games online with less lag and more stability."
-msgstr "加速主机游戏,实现满速下载、稳定联机、飞快匹配体验。"
-
-msgid "— now supporting PS, Switch, Xbox, PC, and mobile."
-msgstr "—— 现已支持 PS、Switch、Xbox、PC 与移动端。"
-
-msgid "Enable"
-msgstr "启用"
-
-msgid "Status"
-msgstr "状态"
-
-msgid "NOT ENABLED"
-msgstr "未启用"
-
-msgid "CLOSED"
-msgstr "已关闭"
-
-msgid "BOOSTING"
-msgstr "加速中"
-
-msgid "RUNNING"
-msgstr "运行中"
-
-msgid "NOT SUPPORTED"
-msgstr "不支持"
-
-msgid "DOWNLOADING"
-msgstr "下载中"
-
-msgid "INSTALLING"
-msgstr "安装中"
-
-msgid "NOT RUNNING"
-msgstr "未运行"
diff --git a/luci-app-qiyougamebooster/po/zh_Hans b/luci-app-qiyougamebooster/po/zh_Hans
deleted file mode 120000
index 41451e4a1..000000000
--- a/luci-app-qiyougamebooster/po/zh_Hans
+++ /dev/null
@@ -1 +0,0 @@
-zh-cn
\ No newline at end of file
diff --git a/luci-app-qiyougamebooster/po/zh_Hans/qiyougamebooster.po b/luci-app-qiyougamebooster/po/zh_Hans/qiyougamebooster.po
new file mode 100644
index 000000000..1e5856f32
--- /dev/null
+++ b/luci-app-qiyougamebooster/po/zh_Hans/qiyougamebooster.po
@@ -0,0 +1,52 @@
+msgid ""
+msgstr "Content-Type: text/plain; charset=UTF-8"
+
+#: applications/luci-app-qiyougamebooster/htdocs/luci-static/resources/view/qiyougamebooster.js:26
+#: applications/luci-app-qiyougamebooster/htdocs/luci-static/resources/view/qiyougamebooster.js:28
+#: applications/luci-app-qiyougamebooster/htdocs/luci-static/resources/view/qiyougamebooster.js:53
+msgid "QiYou Game Booster"
+msgstr "奇游联机宝"
+
+#: applications/luci-app-qiyougamebooster/htdocs/luci-static/resources/view/qiyougamebooster.js:54
+msgid "Play console games online with less lag and more stability."
+msgstr "加速主机游戏,实现满速下载、稳定联机、飞快匹配体验。"
+
+#: applications/luci-app-qiyougamebooster/htdocs/luci-static/resources/view/qiyougamebooster.js:55
+msgid "— now supporting PS, Switch, Xbox, PC, and mobile."
+msgstr "—— 现已支持 PS、Switch、Xbox、PC 与移动端。"
+
+#: applications/luci-app-qiyougamebooster/htdocs/luci-static/resources/view/qiyougamebooster.js:70
+msgid "Collecting data..."
+msgstr "正在收集数据中..."
+
+#: applications/luci-app-qiyougamebooster/htdocs/luci-static/resources/view/qiyougamebooster.js:76
+msgid "Enable"
+msgstr "启用"
+
+#: applications/luci-app-qiyougamebooster/htdocs/luci-static/resources/view/qiyougamebooster.js:38
+msgid "Status"
+msgstr "状态"
+
+msgid "NOT ENABLED"
+msgstr "未启用"
+
+msgid "CLOSED"
+msgstr "已关闭"
+
+msgid "BOOSTING"
+msgstr "加速中"
+
+msgid "RUNNING"
+msgstr "运行中"
+
+msgid "NOT SUPPORTED"
+msgstr "不支持"
+
+msgid "DOWNLOADING"
+msgstr "下载中"
+
+msgid "INSTALLING"
+msgstr "安装中"
+
+msgid "NOT RUNNING"
+msgstr "未运行"
diff --git a/luci-app-qiyougamebooster/root/etc/uci-defaults/qiyougamebooster b/luci-app-qiyougamebooster/root/etc/uci-defaults/qiyougamebooster
deleted file mode 100644
index fbd81c30a..000000000
--- a/luci-app-qiyougamebooster/root/etc/uci-defaults/qiyougamebooster
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/bin/sh
-[ ! -f "/usr/share/ucitrack/luci-app-qiyougamebooster.json" ] && {
- cat > /usr/share/ucitrack/luci-app-qiyougamebooster.json << EEOF
-{
- "config": "qiyougamebooster",
- "init": "qiyougamebooster"
-}
-EEOF
-}
-
-uci -q batch <<-EOF >/dev/null
- delete ucitrack.@qiyougamebooster[-1]
- add ucitrack qiyougamebooster
- set ucitrack.@qiyougamebooster[-1].init=qiyougamebooster
- commit ucitrack
-EOF
-
-rm -f /tmp/luci-indexcache
-exit 0
diff --git a/luci-app-qiyougamebooster/root/usr/share/luci/menu.d/luci-app-qiyougamebooster.json b/luci-app-qiyougamebooster/root/usr/share/luci/menu.d/luci-app-qiyougamebooster.json
new file mode 100644
index 000000000..0c161061a
--- /dev/null
+++ b/luci-app-qiyougamebooster/root/usr/share/luci/menu.d/luci-app-qiyougamebooster.json
@@ -0,0 +1,16 @@
+{
+ "admin/services/qiyougamebooster": {
+ "title": "QiYou Game Booster",
+ "action": {
+ "type": "view",
+ "path": "qiyougamebooster"
+ },
+ "depends": {
+ "acl": [ "luci-app-qiyougamebooster" ],
+ "fs": {
+ "/usr/bin/qiyougamebooster.sh": "executable"
+ },
+ "uci": { "qiyougamebooster": true }
+ }
+ }
+}
diff --git a/luci-app-qiyougamebooster/root/usr/share/rpcd/acl.d/luci-app-qiyougamebooster.json b/luci-app-qiyougamebooster/root/usr/share/rpcd/acl.d/luci-app-qiyougamebooster.json
index ed85a9ce5..205ce66e3 100644
--- a/luci-app-qiyougamebooster/root/usr/share/rpcd/acl.d/luci-app-qiyougamebooster.json
+++ b/luci-app-qiyougamebooster/root/usr/share/rpcd/acl.d/luci-app-qiyougamebooster.json
@@ -1,7 +1,10 @@
{
- "luci-app-qiyougamebooster": {
+ "luci-app-qiyougamebooster": {
"description": "Grant UCI access for luci-app-qiyougamebooster",
"read": {
+ "file": {
+ "/usr/bin/qiyougamebooster.sh": [ "exec" ]
+ },
"uci": [ "qiyougamebooster" ]
},
"write": {
diff --git a/mosdns/Makefile b/mosdns/Makefile
index c6e2048c9..6d21b80cd 100644
--- a/mosdns/Makefile
+++ b/mosdns/Makefile
@@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=mosdns
PKG_VERSION:=5.3.4
-PKG_RELEASE:=6
+PKG_RELEASE:=7
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/IrineSistiana/mosdns/tar.gz/v$(PKG_VERSION)?
diff --git a/mosdns/patches/206-add-auto-reload-rule-files-on-modification.patch b/mosdns/patches/206-add-auto-reload-rule-files-on-modification.patch
new file mode 100644
index 000000000..403e714cf
--- /dev/null
+++ b/mosdns/patches/206-add-auto-reload-rule-files-on-modification.patch
@@ -0,0 +1,697 @@
+From 9554c5b29aff4107504bdf45c35582e6d9743a93 Mon Sep 17 00:00:00 2001
+From: sbwml
+Date: Mon, 20 Apr 2026 23:06:17 +0800
+Subject: [PATCH] add auto-reload rule files on modification
+
+Signed-off-by: sbwml
+---
+ pkg/utils/file_watcher.go | 73 +++++++++++++++
+ plugin/data_provider/domain_set/domain_set.go | 71 +++++++++++---
+ plugin/data_provider/ip_set/ip_set.go | 75 +++++++++++----
+ plugin/executable/arbitrary/arbitrary.go | 81 +++++++++++-----
+ plugin/executable/hosts/hosts.go | 87 ++++++++++++-----
+ plugin/executable/redirect/redirect.go | 93 +++++++++++++------
+ 6 files changed, 374 insertions(+), 106 deletions(-)
+ create mode 100644 pkg/utils/file_watcher.go
+
+--- /dev/null
++++ b/pkg/utils/file_watcher.go
+@@ -0,0 +1,73 @@
++package utils
++
++import (
++ "os"
++ "time"
++)
++
++// FileWatcher periodically checks the modification time of the specified files.
++type FileWatcher struct {
++ done chan struct{}
++}
++
++// StartFileWatcher starts a new FileWatcher that checks files every 'interval'.
++// If any file modification time changes or a file is missing, onChange is triggered.
++// Note: onChange must be safe to run concurrently with other components.
++func StartFileWatcher(files []string, interval time.Duration, onChange func(changedFiles []string)) *FileWatcher {
++ if len(files) == 0 {
++ return nil
++ }
++ fw := &FileWatcher{
++ done: make(chan struct{}),
++ }
++ go func() {
++ modTimes := make(map[string]time.Time)
++ for _, f := range files {
++ stat, err := os.Stat(f)
++ if err == nil {
++ modTimes[f] = stat.ModTime()
++ }
++ }
++ ticker := time.NewTicker(interval)
++ defer ticker.Stop()
++
++ for {
++ select {
++ case <-fw.done:
++ return
++ case <-ticker.C:
++ var changedFiles []string
++ for _, f := range files {
++ stat, err := os.Stat(f)
++ if err != nil {
++ if os.IsNotExist(err) {
++ // File is missing or deleted.
++ if mt, ok := modTimes[f]; ok && !mt.IsZero() {
++ modTimes[f] = time.Time{} // mark as missing
++ changedFiles = append(changedFiles, f)
++ }
++ }
++ continue
++ }
++
++ if mt, ok := modTimes[f]; !ok || mt != stat.ModTime() {
++ modTimes[f] = stat.ModTime()
++ changedFiles = append(changedFiles, f)
++ }
++ }
++ if len(changedFiles) > 0 {
++ onChange(changedFiles)
++ }
++ }
++ }
++ }()
++ return fw
++}
++
++// Close stops the FileWatcher.
++func (fw *FileWatcher) Close() error {
++ if fw != nil {
++ close(fw.done)
++ }
++ return nil
++}
+--- a/plugin/data_provider/domain_set/domain_set.go
++++ b/plugin/data_provider/domain_set/domain_set.go
+@@ -22,10 +22,15 @@ package domain_set
+ import (
+ "bytes"
+ "fmt"
++ "os"
++ "sync/atomic"
++ "time"
++
+ "github.com/IrineSistiana/mosdns/v5/coremain"
+ "github.com/IrineSistiana/mosdns/v5/pkg/matcher/domain"
++ "github.com/IrineSistiana/mosdns/v5/pkg/utils"
+ "github.com/IrineSistiana/mosdns/v5/plugin/data_provider"
+- "os"
++ "go.uber.org/zap"
+ )
+
+ const PluginType = "domain_set"
+@@ -51,33 +56,69 @@ type Args struct {
+ var _ data_provider.DomainMatcherProvider = (*DomainSet)(nil)
+
+ type DomainSet struct {
+- mg []domain.Matcher[struct{}]
++ mg atomic.Pointer[MatcherGroup]
++ fw *utils.FileWatcher
+ }
+
+ func (d *DomainSet) GetDomainMatcher() domain.Matcher[struct{}] {
+- return MatcherGroup(d.mg)
++ return d
++}
++
++func (d *DomainSet) Match(s string) (struct{}, bool) {
++ if mg := d.mg.Load(); mg != nil {
++ return mg.Match(s)
++ }
++ return struct{}{}, false
++}
++
++func (d *DomainSet) Close() error {
++ if d.fw != nil {
++ return d.fw.Close()
++ }
++ return nil
+ }
+
+ // NewDomainSet inits a DomainSet from given args.
+ func NewDomainSet(bp *coremain.BP, args *Args) (*DomainSet, error) {
+ ds := &DomainSet{}
+
+- m := domain.NewDomainMixMatcher()
+- if err := LoadExpsAndFiles(args.Exps, args.Files, m); err != nil {
+- return nil, err
++ loadInner := func() (*MatcherGroup, error) {
++ var mg MatcherGroup
++ m := domain.NewDomainMixMatcher()
++ if err := LoadExpsAndFiles(args.Exps, args.Files, m); err != nil {
++ return nil, err
++ }
++ if m.Len() > 0 {
++ mg = append(mg, m)
++ }
++
++ for _, tag := range args.Sets {
++ provider, _ := bp.M().GetPlugin(tag).(data_provider.DomainMatcherProvider)
++ if provider == nil {
++ return nil, fmt.Errorf("%s is not a DomainMatcherProvider", tag)
++ }
++ m := provider.GetDomainMatcher()
++ mg = append(mg, m)
++ }
++ return &mg, nil
+ }
+- if m.Len() > 0 {
+- ds.mg = append(ds.mg, m)
++
++ mg, err := loadInner()
++ if err != nil {
++ return nil, err
+ }
++ ds.mg.Store(mg)
+
+- for _, tag := range args.Sets {
+- provider, _ := bp.M().GetPlugin(tag).(data_provider.DomainMatcherProvider)
+- if provider == nil {
+- return nil, fmt.Errorf("%s is not a DomainMatcherProvider", tag)
+- }
+- m := provider.GetDomainMatcher()
+- ds.mg = append(ds.mg, m)
++ if len(args.Files) > 0 {
++ ds.fw = utils.StartFileWatcher(args.Files, time.Second*3, func(changedFiles []string) {
++ newMg, err := loadInner()
++ if err == nil {
++ ds.mg.Store(newMg)
++ bp.L().Info("reloaded files", zap.Strings("files", changedFiles))
++ }
++ })
+ }
++
+ return ds, nil
+ }
+
+--- a/plugin/data_provider/ip_set/ip_set.go
++++ b/plugin/data_provider/ip_set/ip_set.go
+@@ -22,12 +22,17 @@ package ip_set
+ import (
+ "bytes"
+ "fmt"
+- "github.com/IrineSistiana/mosdns/v5/coremain"
+- "github.com/IrineSistiana/mosdns/v5/pkg/matcher/netlist"
+- "github.com/IrineSistiana/mosdns/v5/plugin/data_provider"
+ "net/netip"
+ "os"
+ "strings"
++ "sync/atomic"
++ "time"
++
++ "github.com/IrineSistiana/mosdns/v5/coremain"
++ "github.com/IrineSistiana/mosdns/v5/pkg/matcher/netlist"
++ "github.com/IrineSistiana/mosdns/v5/pkg/utils"
++ "github.com/IrineSistiana/mosdns/v5/plugin/data_provider"
++ "go.uber.org/zap"
+ )
+
+ const PluginType = "ip_set"
+@@ -49,31 +54,67 @@ type Args struct {
+ var _ data_provider.IPMatcherProvider = (*IPSet)(nil)
+
+ type IPSet struct {
+- mg []netlist.Matcher
++ mg atomic.Pointer[MatcherGroup]
++ fw *utils.FileWatcher
+ }
+
+ func (d *IPSet) GetIPMatcher() netlist.Matcher {
+- return MatcherGroup(d.mg)
++ return d
++}
++
++func (d *IPSet) Match(addr netip.Addr) bool {
++ if mg := d.mg.Load(); mg != nil {
++ return mg.Match(addr)
++ }
++ return false
++}
++
++func (d *IPSet) Close() error {
++ if d.fw != nil {
++ return d.fw.Close()
++ }
++ return nil
+ }
+
+ func NewIPSet(bp *coremain.BP, args *Args) (*IPSet, error) {
+ p := &IPSet{}
+
+- l := netlist.NewList()
+- if err := LoadFromIPsAndFiles(args.IPs, args.Files, l); err != nil {
+- return nil, err
++ loadInner := func() (*MatcherGroup, error) {
++ var mg MatcherGroup
++ l := netlist.NewList()
++ if err := LoadFromIPsAndFiles(args.IPs, args.Files, l); err != nil {
++ return nil, err
++ }
++ l.Sort()
++ if l.Len() > 0 {
++ mg = append(mg, l)
++ }
++ for _, tag := range args.Sets {
++ provider, _ := bp.M().GetPlugin(tag).(data_provider.IPMatcherProvider)
++ if provider == nil {
++ return nil, fmt.Errorf("%s is not an IPMatcherProvider", tag)
++ }
++ mg = append(mg, provider.GetIPMatcher())
++ }
++ return &mg, nil
+ }
+- l.Sort()
+- if l.Len() > 0 {
+- p.mg = append(p.mg, l)
++
++ mg, err := loadInner()
++ if err != nil {
++ return nil, err
+ }
+- for _, tag := range args.Sets {
+- provider, _ := bp.M().GetPlugin(tag).(data_provider.IPMatcherProvider)
+- if provider == nil {
+- return nil, fmt.Errorf("%s is not an IPMatcherProvider", tag)
+- }
+- p.mg = append(p.mg, provider.GetIPMatcher())
++ p.mg.Store(mg)
++
++ if len(args.Files) > 0 {
++ p.fw = utils.StartFileWatcher(args.Files, time.Second*3, func(changedFiles []string) {
++ newMg, err := loadInner()
++ if err == nil {
++ p.mg.Store(newMg)
++ bp.L().Info("reloaded files", zap.Strings("files", changedFiles))
++ }
++ })
+ }
++
+ return p, nil
+ }
+
+--- a/plugin/executable/arbitrary/arbitrary.go
++++ b/plugin/executable/arbitrary/arbitrary.go
+@@ -23,12 +23,17 @@ import (
+ "bytes"
+ "context"
+ "fmt"
++ "os"
++ "strings"
++ "sync/atomic"
++ "time"
++
+ "github.com/IrineSistiana/mosdns/v5/coremain"
+ "github.com/IrineSistiana/mosdns/v5/pkg/query_context"
++ "github.com/IrineSistiana/mosdns/v5/pkg/utils"
+ "github.com/IrineSistiana/mosdns/v5/pkg/zone_file"
+ "github.com/IrineSistiana/mosdns/v5/plugin/executable/sequence"
+- "os"
+- "strings"
++ "go.uber.org/zap"
+ )
+
+ const PluginType = "arbitrary"
+@@ -45,38 +50,68 @@ type Args struct {
+ var _ sequence.Executable = (*Arbitrary)(nil)
+
+ type Arbitrary struct {
+- m *zone_file.Matcher
++ m atomic.Pointer[zone_file.Matcher]
++ fw *utils.FileWatcher
+ }
+
+-func NewArbitrary(args *Args) (*Arbitrary, error) {
+- m := new(zone_file.Matcher)
+- for i, s := range args.Rules {
+- if err := m.Load(strings.NewReader(s)); err != nil {
+- return nil, fmt.Errorf("failed to load rr #%d [%s], %w", i, s, err)
+- }
+- }
+- for i, file := range args.Files {
+- b, err := os.ReadFile(file)
+- if err != nil {
+- return nil, fmt.Errorf("failed to read file #%d [%s], %w", i, file, err)
++func NewArbitrary(bp *coremain.BP, args *Args) (*Arbitrary, error) {
++ a := &Arbitrary{}
++
++ loadInner := func() (*zone_file.Matcher, error) {
++ m := new(zone_file.Matcher)
++ for i, s := range args.Rules {
++ if err := m.Load(strings.NewReader(s)); err != nil {
++ return nil, fmt.Errorf("failed to load rr #%d [%s], %w", i, s, err)
++ }
+ }
+- if err := m.Load(bytes.NewReader(b)); err != nil {
+- return nil, fmt.Errorf("failed to load rr file #%d [%s], %w", i, file, err)
++ for i, file := range args.Files {
++ b, err := os.ReadFile(file)
++ if err != nil {
++ return nil, fmt.Errorf("failed to read file #%d [%s], %w", i, file, err)
++ }
++ if err := m.Load(bytes.NewReader(b)); err != nil {
++ return nil, fmt.Errorf("failed to load rr file #%d [%s], %w", i, file, err)
++ }
+ }
++ return m, nil
++ }
++
++ inner, err := loadInner()
++ if err != nil {
++ return nil, err
++ }
++ a.m.Store(inner)
++
++ if len(args.Files) > 0 {
++ a.fw = utils.StartFileWatcher(args.Files, time.Second*3, func(changedFiles []string) {
++ newInner, err := loadInner()
++ if err == nil {
++ a.m.Store(newInner)
++ bp.L().Info("reloaded files", zap.Strings("files", changedFiles))
++ }
++ })
+ }
+- return &Arbitrary{
+- m: m,
+- }, nil
++
++ return a, nil
+ }
+
+ func (a *Arbitrary) Exec(_ context.Context, qCtx *query_context.Context) error {
+- if r := a.m.Reply(qCtx.Q()); r != nil {
+- qCtx.SetResponse(r)
++ if inner := a.m.Load(); inner != nil {
++ if r := inner.Reply(qCtx.Q()); r != nil {
++ qCtx.SetResponse(r)
++ }
++ }
++ return nil
++}
++
++func (a *Arbitrary) Close() error {
++ if a.fw != nil {
++ return a.fw.Close()
+ }
+ return nil
+ }
+
+-func Init(_ *coremain.BP, v any) (any, error) {
++func Init(bp *coremain.BP, v any) (any, error) {
+ args := v.(*Args)
+- return NewArbitrary(args)
++ return NewArbitrary(bp, args)
+ }
+--- a/plugin/executable/hosts/hosts.go
++++ b/plugin/executable/hosts/hosts.go
+@@ -23,13 +23,18 @@ import (
+ "bytes"
+ "context"
+ "fmt"
++ "os"
++ "sync/atomic"
++ "time"
++
+ "github.com/IrineSistiana/mosdns/v5/coremain"
+ "github.com/IrineSistiana/mosdns/v5/pkg/hosts"
+ "github.com/IrineSistiana/mosdns/v5/pkg/matcher/domain"
+ "github.com/IrineSistiana/mosdns/v5/pkg/query_context"
++ "github.com/IrineSistiana/mosdns/v5/pkg/utils"
+ "github.com/IrineSistiana/mosdns/v5/plugin/executable/sequence"
+ "github.com/miekg/dns"
+- "os"
++ "go.uber.org/zap"
+ )
+
+ const PluginType = "hosts"
+@@ -46,44 +51,76 @@ type Args struct {
+ }
+
+ type Hosts struct {
+- h *hosts.Hosts
++ h atomic.Pointer[hosts.Hosts]
++ fw *utils.FileWatcher
+ }
+
+-func Init(_ *coremain.BP, args any) (any, error) {
+- return NewHosts(args.(*Args))
++func Init(bp *coremain.BP, args any) (any, error) {
++ return NewHosts(bp, args.(*Args))
+ }
+
+-func NewHosts(args *Args) (*Hosts, error) {
+- m := domain.NewMixMatcher[*hosts.IPs]()
+- m.SetDefaultMatcher(domain.MatcherFull)
+- for i, entry := range args.Entries {
+- if err := domain.Load[*hosts.IPs](m, entry, hosts.ParseIPs); err != nil {
+- return nil, fmt.Errorf("failed to load entry #%d %s, %w", i, entry, err)
+- }
+- }
+- for i, file := range args.Files {
+- b, err := os.ReadFile(file)
+- if err != nil {
+- return nil, fmt.Errorf("failed to read file #%d %s, %w", i, file, err)
++func NewHosts(bp *coremain.BP, args *Args) (*Hosts, error) {
++ h := &Hosts{}
++
++ loadInner := func() (*hosts.Hosts, error) {
++ m := domain.NewMixMatcher[*hosts.IPs]()
++ m.SetDefaultMatcher(domain.MatcherFull)
++ for i, entry := range args.Entries {
++ if err := domain.Load[*hosts.IPs](m, entry, hosts.ParseIPs); err != nil {
++ return nil, fmt.Errorf("failed to load entry #%d %s, %w", i, entry, err)
++ }
+ }
+- if err := domain.LoadFromTextReader[*hosts.IPs](m, bytes.NewReader(b), hosts.ParseIPs); err != nil {
+- return nil, fmt.Errorf("failed to load file #%d %s, %w", i, file, err)
++ for i, file := range args.Files {
++ b, err := os.ReadFile(file)
++ if err != nil {
++ return nil, fmt.Errorf("failed to read file #%d %s, %w", i, file, err)
++ }
++ if err := domain.LoadFromTextReader[*hosts.IPs](m, bytes.NewReader(b), hosts.ParseIPs); err != nil {
++ return nil, fmt.Errorf("failed to load file #%d %s, %w", i, file, err)
++ }
+ }
++ return hosts.NewHosts(m), nil
++ }
++
++ inner, err := loadInner()
++ if err != nil {
++ return nil, err
+ }
++ h.h.Store(inner)
+
+- return &Hosts{
+- h: hosts.NewHosts(m),
+- }, nil
++ if len(args.Files) > 0 {
++ h.fw = utils.StartFileWatcher(args.Files, time.Second*3, func(changedFiles []string) {
++ newInner, err := loadInner()
++ if err == nil {
++ h.h.Store(newInner)
++ bp.L().Info("reloaded files", zap.Strings("files", changedFiles))
++ }
++ })
++ }
++
++ return h, nil
+ }
+
+ func (h *Hosts) Response(q *dns.Msg) *dns.Msg {
+- return h.h.LookupMsg(q)
++ if inner := h.h.Load(); inner != nil {
++ return inner.LookupMsg(q)
++ }
++ return nil
+ }
+
+ func (h *Hosts) Exec(_ context.Context, qCtx *query_context.Context) error {
+- r := h.h.LookupMsg(qCtx.Q())
+- if r != nil {
+- qCtx.SetResponse(r)
++ if inner := h.h.Load(); inner != nil {
++ r := inner.LookupMsg(qCtx.Q())
++ if r != nil {
++ qCtx.SetResponse(r)
++ }
++ }
++ return nil
++}
++
++func (h *Hosts) Close() error {
++ if h.fw != nil {
++ return h.fw.Close()
+ }
+ return nil
+ }
+--- a/plugin/executable/redirect/redirect.go
++++ b/plugin/executable/redirect/redirect.go
+@@ -25,10 +25,13 @@ import (
+ "fmt"
+ "os"
+ "strings"
++ "sync/atomic"
++ "time"
+
+ "github.com/IrineSistiana/mosdns/v5/coremain"
+ "github.com/IrineSistiana/mosdns/v5/pkg/matcher/domain"
+ "github.com/IrineSistiana/mosdns/v5/pkg/query_context"
++ "github.com/IrineSistiana/mosdns/v5/pkg/utils"
+ "github.com/IrineSistiana/mosdns/v5/plugin/executable/sequence"
+ "github.com/miekg/dns"
+ "go.uber.org/zap"
+@@ -48,11 +51,12 @@ type Args struct {
+ }
+
+ type Redirect struct {
+- m *domain.MixMatcher[string]
++ m atomic.Pointer[domain.MixMatcher[string]]
++ fw *utils.FileWatcher
+ }
+
+ func Init(bp *coremain.BP, args any) (any, error) {
+- r, err := NewRedirect(args.(*Args))
++ r, err := NewRedirect(bp, args.(*Args))
+ if err != nil {
+ return nil, err
+ }
+@@ -60,7 +64,8 @@ func Init(bp *coremain.BP, args any) (an
+ return r, nil
+ }
+
+-func NewRedirect(args *Args) (*Redirect, error) {
++func NewRedirect(bp *coremain.BP, args *Args) (*Redirect, error) {
++ r := &Redirect{}
+ parseFunc := func(s string) (p, v string, err error) {
+ f := strings.Fields(s)
+ if len(f) != 2 {
+@@ -68,23 +73,44 @@ func NewRedirect(args *Args) (*Redirect,
+ }
+ return f[0], dns.Fqdn(f[1]), nil
+ }
+- m := domain.NewMixMatcher[string]()
+- m.SetDefaultMatcher(domain.MatcherFull)
+- for i, rule := range args.Rules {
+- if err := domain.Load[string](m, rule, parseFunc); err != nil {
+- return nil, fmt.Errorf("failed to load rule #%d %s, %w", i, rule, err)
+- }
+- }
+- for i, file := range args.Files {
+- b, err := os.ReadFile(file)
+- if err != nil {
+- return nil, fmt.Errorf("failed to read file #%d %s, %w", i, file, err)
++
++ loadInner := func() (*domain.MixMatcher[string], error) {
++ m := domain.NewMixMatcher[string]()
++ m.SetDefaultMatcher(domain.MatcherFull)
++ for i, rule := range args.Rules {
++ if err := domain.Load[string](m, rule, parseFunc); err != nil {
++ return nil, fmt.Errorf("failed to load rule #%d %s, %w", i, rule, err)
++ }
+ }
+- if err := domain.LoadFromTextReader[string](m, bytes.NewReader(b), parseFunc); err != nil {
+- return nil, fmt.Errorf("failed to load file #%d %s, %w", i, file, err)
++ for i, file := range args.Files {
++ b, err := os.ReadFile(file)
++ if err != nil {
++ return nil, fmt.Errorf("failed to read file #%d %s, %w", i, file, err)
++ }
++ if err := domain.LoadFromTextReader[string](m, bytes.NewReader(b), parseFunc); err != nil {
++ return nil, fmt.Errorf("failed to load file #%d %s, %w", i, file, err)
++ }
+ }
++ return m, nil
++ }
++
++ inner, err := loadInner()
++ if err != nil {
++ return nil, err
++ }
++ r.m.Store(inner)
++
++ if len(args.Files) > 0 {
++ r.fw = utils.StartFileWatcher(args.Files, time.Second*3, func(changedFiles []string) {
++ newInner, err := loadInner()
++ if err == nil {
++ r.m.Store(newInner)
++ bp.L().Info("reloaded files", zap.Strings("files", changedFiles), zap.Int("length", newInner.Len()))
++ }
++ })
+ }
+- return &Redirect{m: m}, nil
++
++ return r, nil
+ }
+
+ func (r *Redirect) Exec(ctx context.Context, qCtx *query_context.Context, next sequence.ChainWalker) error {
+@@ -94,7 +120,12 @@ func (r *Redirect) Exec(ctx context.Cont
+ }
+
+ orgQName := q.Question[0].Name
+- redirectTarget, ok := r.m.Match(orgQName)
++ inner := r.m.Load()
++ if inner == nil {
++ return next.ExecNext(ctx, qCtx)
++ }
++
++ redirectTarget, ok := inner.Match(orgQName)
+ if !ok {
+ return next.ExecNext(ctx, qCtx)
+ }
+@@ -104,16 +135,16 @@ func (r *Redirect) Exec(ctx context.Cont
+ q.Question[0].Name = orgQName
+ }()
+ err := next.ExecNext(ctx, qCtx)
+- if r := qCtx.R(); r != nil {
++ if rResp := qCtx.R(); rResp != nil {
+ // Restore original query name.
+- for i := range r.Question {
+- if r.Question[i].Name == redirectTarget {
+- r.Question[i].Name = orgQName
++ for i := range rResp.Question {
++ if rResp.Question[i].Name == redirectTarget {
++ rResp.Question[i].Name = orgQName
+ }
+ }
+
+ // Insert a CNAME record.
+- newAns := make([]dns.RR, 1, len(r.Answer)+1)
++ newAns := make([]dns.RR, 1, len(rResp.Answer)+1)
+ newAns[0] = &dns.CNAME{
+ Hdr: dns.RR_Header{
+ Name: orgQName,
+@@ -123,12 +154,22 @@ func (r *Redirect) Exec(ctx context.Cont
+ },
+ Target: redirectTarget,
+ }
+- newAns = append(newAns, r.Answer...)
+- r.Answer = newAns
++ newAns = append(newAns, rResp.Answer...)
++ rResp.Answer = newAns
+ }
+ return err
+ }
+
+ func (r *Redirect) Len() int {
+- return r.m.Len()
++ if inner := r.m.Load(); inner != nil {
++ return inner.Len()
++ }
++ return 0
++}
++
++func (r *Redirect) Close() error {
++ if r.fw != nil {
++ return r.fw.Close()
++ }
++ return nil
+ }