🎉 Sync 2026-04-20 23:57:06

This commit is contained in:
github-actions[bot] committed 2026-04-20 23:57:06 +08:00
1 parent ede68af29e
commit c8457a2742
27 files changed
+1000 -151

No files matched your search

+2 -2
View File
@@ -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)?
+2 -2
View File
@@ -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
@@ -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');
});
};
@@ -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
});
+1 -4
View File
@@ -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() {
@@ -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();
@@ -18,8 +18,7 @@
"clean_log",
"flush_cache",
"get_version",
"print_log",
"restart"
"print_log"
]
},
"uci": [ "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." };
}
}
};
+1 -1
View File
@@ -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:= \
@@ -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
@@ -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("<a href='#' onclick='fetchCertSha256(); return false;'>%s</a>", "[ " .. 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 })
+28
View File
@@ -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 2>/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
@@ -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;
});
});
}
//]]>
</script>
+6
View File
@@ -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 配置"
@@ -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 = '<span style="color:%s"><strong>%s: %s %s</strong></span>';
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.') + '<br />' +
_('— 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();
}
});
@@ -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
@@ -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.") .. "<br />" ..
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('<span style="color:%s"><strong>%s: %s %s</strong></span>', "red", translate("Status"), ver, translate(sts))
else
o.value = string.format('<span style="color:%s"><strong>%s: %s %s</strong></span>', "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 = "<p><img src='/qiyougamebooster/Tutorial.png' height='350'/></p>"
return m
@@ -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 ""
+1
View File
@@ -0,0 +1 @@
zh_Hans
@@ -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 "未运行"
-1
View File
@@ -1 +0,0 @@
zh-cn
@@ -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 "未运行"
@@ -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
@@ -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 }
}
}
}
@@ -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": {
+1 -1
View File
@@ -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)?
@@ -0,0 +1,697 @@
From 9554c5b29aff4107504bdf45c35582e6d9743a93 Mon Sep 17 00:00:00 2001
From: sbwml <admin@cooluc.com>
Date: Mon, 20 Apr 2026 23:06:17 +0800
Subject: [PATCH] add auto-reload rule files on modification
Signed-off-by: sbwml <admin@cooluc.com>
---
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
}