diff --git a/dnsproxy/Makefile b/dnsproxy/Makefile
index 42692311..75eaadd1 100644
--- a/dnsproxy/Makefile
+++ b/dnsproxy/Makefile
@@ -5,8 +5,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=dnsproxy
-PKG_VERSION:=0.84.0
-PKG_RELEASE:=16
+PKG_VERSION:=0.84.1
+PKG_RELEASE:=17
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/AdguardTeam/dnsproxy/tar.gz/v$(PKG_VERSION)?
diff --git a/luci-app-passwall/Makefile b/luci-app-passwall/Makefile
index 3e16f047..0a6580b2 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.8.19
-PKG_RELEASE:=230
+PKG_RELEASE:=231
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \
diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/server/type/1_sing-box.lua b/luci-app-passwall/luasrc/model/cbi/passwall/server/type/1_sing-box.lua
index ac8b945b..50236d89 100644
--- a/luci-app-passwall/luasrc/model/cbi/passwall/server/type/1_sing-box.lua
+++ b/luci-app-passwall/luasrc/model/cbi/passwall/server/type/1_sing-box.lua
@@ -320,7 +320,7 @@ o:depends({ protocol = "tuic" })
o:depends({ protocol = "hysteria2" })
o.validate = function(self, value, t)
if value and value ~= "" then
- if not fs.access(value) then
+ if not api.fs.access(value) then
return nil, translate("Can't find this file!")
else
return value
@@ -339,7 +339,7 @@ o:depends({ protocol = "tuic" })
o:depends({ protocol = "hysteria2" })
o.validate = function(self, value, t)
if value and value ~= "" then
- if not fs.access(value) then
+ if not api.fs.access(value) then
return nil, translate("Can't find this file!")
else
return value
diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/server/type/2_xray.lua b/luci-app-passwall/luasrc/model/cbi/passwall/server/type/2_xray.lua
index 2e1b90bc..a27e518c 100644
--- a/luci-app-passwall/luasrc/model/cbi/passwall/server/type/2_xray.lua
+++ b/luci-app-passwall/luasrc/model/cbi/passwall/server/type/2_xray.lua
@@ -259,7 +259,7 @@ o:depends({ tls = true, reality = false })
o:depends({ protocol = "hysteria2"})
o.validate = function(self, value, t)
if value and value ~= "" then
- if not fs.access(value) then
+ if not api.fs.access(value) then
return nil, translate("Can't find this file!")
else
return value
@@ -275,7 +275,7 @@ o:depends({ tls = true, reality = false })
o:depends({ protocol = "hysteria2"})
o.validate = function(self, value, t)
if value and value ~= "" then
- if not fs.access(value) then
+ if not api.fs.access(value) then
return nil, translate("Can't find this file!")
else
return value
diff --git a/luci-app-passwall2/Makefile b/luci-app-passwall2/Makefile
index 16e3f412..adeb3c42 100644
--- a/luci-app-passwall2/Makefile
+++ b/luci-app-passwall2/Makefile
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall2
PKG_VERSION:=26.8.17
-PKG_RELEASE:=91
+PKG_RELEASE:=92
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \
diff --git a/luci-app-passwall2/luasrc/passwall2/api.lua b/luci-app-passwall2/luasrc/passwall2/api.lua
index 25e68be2..755bd2e6 100644
--- a/luci-app-passwall2/luasrc/passwall2/api.lua
+++ b/luci-app-passwall2/luasrc/passwall2/api.lua
@@ -1723,6 +1723,7 @@ function apply_redirect(m)
end
end
else
+ uci:revert(c_config .. "_redirect")
sys.call("/bin/rm -f " .. tmp_uci_file)
end
end
diff --git a/luci-app-ssr-plus/Makefile b/luci-app-ssr-plus/Makefile
index 0913ece2..23a8463b 100644
--- a/luci-app-ssr-plus/Makefile
+++ b/luci-app-ssr-plus/Makefile
@@ -4,7 +4,7 @@ LUCI_TITLE:=luci-app-ssr-plus
LUCI_PKGARCH:=all
PKG_NAME:=luci-app-ssr-plus
PKG_VERSION:=196
-PKG_RELEASE:=59
+PKG_RELEASE:=60
PKG_CONFIG_DEPENDS:= \
CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \
diff --git a/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client.lua b/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client.lua
index d830624b..1aea1be9 100644
--- a/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client.lua
+++ b/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client.lua
@@ -183,10 +183,10 @@ if is_finded("chinadns-ng") then
o:depends("pdnsd_enable", "6")
o.default = "8.8.4.4:53"
o.description = translate(
- "
" ..
- "- " .. translate("Custom DNS Server format as IP:PORT (default: 8.8.4.4:53)") .. "
" ..
- "- " .. translate("Muitiple DNS server can saperate with ','") .. "
" ..
- "
"
+ "" ..
+ "- " .. translate("Custom DNS Server format as IP:PORT (default: 8.8.4.4:53)") .. "
" ..
+ "- " .. translate("Muitiple DNS server can saperate with ','") .. "
" ..
+ "
"
)
o = s:option(ListValue, "chinadns_ng_proto", translate("ChinaDNS-NG query protocol"))
@@ -209,13 +209,33 @@ if is_finded("chinadns-ng") then
o:value("1.2.4.8:53", translate("CNNIC SDNS (1.2.4.8)"))
o:depends({pdnsd_enable = "6", run_mode = "router"})
o.default = "wan"
- o.description = translate("Custom DNS Server format as IP:PORT (default: disabled)")
+ o.description = translate(
+ "" ..
+ "- " .. translate("Custom DNS Server format as IP:PORT (default: disabled)") .. "
" ..
+ "- " .. translate("Muitiple DNS server can saperate with ','") .. "
" ..
+ "
"
+ )
o.validate = function(self, value, section)
if (section and value) then
if value == "none" or value == "wan" or value == "wan_114" then
return value
end
+ if string.find(value, ",") then
+ local parts = {}
+ for part in string.gmatch(value, "[^,]+") do
+ part = part:gsub("^%s*", ""):gsub("%s*$", "")
+ if not validation.ip4addrport(part) then
+ return nil, translate("Expecting: %s"):format(translate("valid address:port (comma separated)"))
+ end
+ table.insert(parts, part)
+ end
+ if #parts == 0 then
+ return nil, translate("Expecting: %s"):format(translate("valid address:port"))
+ end
+ return table.concat(parts, ",")
+ end
+
if validation.ip4addrport(value) then
return value
end
diff --git a/luci-app-ssr-plus/po/templates/ssr-plus.pot b/luci-app-ssr-plus/po/templates/ssr-plus.pot
index 6bb54013..6a1451bd 100644
--- a/luci-app-ssr-plus/po/templates/ssr-plus.pot
+++ b/luci-app-ssr-plus/po/templates/ssr-plus.pot
@@ -87,6 +87,7 @@ msgstr ""
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:1667
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:1680
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client.lua:186
+#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client.lua:213
msgid "- "
msgstr ""
@@ -588,7 +589,7 @@ msgstr ""
msgid "Custom DNS Server format as IP:PORT (default: 8.8.4.4:53)"
msgstr ""
-#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client.lua:212
+#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client.lua:214
msgid "Custom DNS Server format as IP:PORT (default: disabled)"
msgstr ""
@@ -981,7 +982,9 @@ msgstr ""
msgid "Example:"
msgstr ""
-#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client.lua:223
+#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client.lua:229
+#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client.lua:234
+#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client.lua:243
msgid "Expecting: %s"
msgstr ""
@@ -1650,6 +1653,7 @@ msgid "Move up"
msgstr ""
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client.lua:188
+#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client.lua:215
msgid "Muitiple DNS server can saperate with ','"
msgstr ""
@@ -3339,10 +3343,15 @@ msgstr ""
msgid "v2ray-plugin"
msgstr ""
-#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client.lua:223
+#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client.lua:234
+#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client.lua:243
msgid "valid address:port"
msgstr ""
+#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client.lua:229
+msgid "valid address:port (comma separated)"
+msgstr ""
+
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/server-config.lua:82
msgid "warning! Please do not reuse the port!"
msgstr ""
diff --git a/luci-app-ssr-plus/po/zh_Hans/ssr-plus.po b/luci-app-ssr-plus/po/zh_Hans/ssr-plus.po
index 74374ff1..27c64da9 100644
--- a/luci-app-ssr-plus/po/zh_Hans/ssr-plus.po
+++ b/luci-app-ssr-plus/po/zh_Hans/ssr-plus.po
@@ -91,6 +91,7 @@ msgstr ""
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:1667
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:1680
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client.lua:186
+#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client.lua:213
msgid "
- "
msgstr ""
@@ -592,7 +593,7 @@ msgstr "自定义客户端代理规则"
msgid "Custom DNS Server format as IP:PORT (default: 8.8.4.4:53)"
msgstr "格式为 IP:Port(默认:8.8.4.4:53)"
-#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client.lua:212
+#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client.lua:214
msgid "Custom DNS Server format as IP:PORT (default: disabled)"
msgstr "格式为 IP:PORT(默认:禁用)"
@@ -990,7 +991,9 @@ msgstr "每周三"
msgid "Example:"
msgstr "示例:"
-#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client.lua:223
+#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client.lua:229
+#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client.lua:234
+#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client.lua:243
msgid "Expecting: %s"
msgstr "应为:%s"
@@ -1664,6 +1667,7 @@ msgid "Move up"
msgstr "上移"
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client.lua:188
+#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client.lua:215
msgid "Muitiple DNS server can saperate with ','"
msgstr "多个上游 DNS 服务器请用 ',' 分隔(注意用英文逗号)"
@@ -3370,10 +3374,15 @@ msgstr ""
msgid "v2ray-plugin"
msgstr ""
-#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client.lua:223
+#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client.lua:234
+#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client.lua:243
msgid "valid address:port"
msgstr "有效的地址:端口"
+#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client.lua:229
+msgid "valid address:port (comma separated)"
+msgstr "有效地址:端口(用逗号分隔)"
+
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/server-config.lua:82
msgid "warning! Please do not reuse the port!"
msgstr "警告!请不要重复使用端口!"
diff --git a/luci-app-ssr-plus/root/etc/init.d/shadowsocksr b/luci-app-ssr-plus/root/etc/init.d/shadowsocksr
index 41df5c1c..64157fd0 100755
--- a/luci-app-ssr-plus/root/etc/init.d/shadowsocksr
+++ b/luci-app-ssr-plus/root/etc/init.d/shadowsocksr
@@ -1257,7 +1257,7 @@ start_dns() {
if [ "$builtin_dns_enabled" = "1" ]; then
pdnsd_enable_flag=7
elif [ "$ssrplus_dns" != "0" ]; then
- if [ "$HAS_IPSET" -eq 1 ]; then
+ if [ "$USE_TABLES" = "iptables" ]; then
if [ -n "$dnsserver" ]; then
add_dns_into_ipset "$run_mode" "$dnsserver"
fi
@@ -1273,7 +1273,7 @@ start_dns() {
output=$(for i in $(echo "$mosdns_dnsserver" | sed "s/,/ /g"); do
dnsserver="${i%:*}"
dnsserver="${i##*/}"
- if [ "$HAS_IPSET" -eq 1 ]; then
+ if [ "$USE_TABLES" = "iptables" ]; then
add_dns_into_ipset "$run_mode" "$dnsserver"
fi
echo " - addr: $i"
@@ -1295,29 +1295,39 @@ start_dns() {
# 遍历每个 DNS 服务器
IFS=',' # 设置分隔符为逗号
for chinadns_ng_server in $dnsserver; do
+ chinadns_ng_server="$(echo "$chinadns_ng_server" | sed 's/[[:space:]]//g')"
+ [ -z "$chinadns_ng_server" ] && continue
# 处理单个服务器地址
chinadns_ng_ip="${chinadns_ng_server%%:*}"
chinadns_ng_port="${chinadns_ng_server##*:}"
+
+ # 如果没有显式指定端口,根据协议填入默认端口
[ "$chinadns_ng_ip" = "$chinadns_ng_port" ] && chinadns_ng_port="53"
- chinadns_ng_tls_port="853"
+ # TLS 协议强制使用 853 端口(DoT 标准端口),忽略用户指定的端口
+ [ "$chinadns_ng_proto" = "tls" ] && chinadns_ng_port="853"
# 根据协议类型格式化服务器地址
case "$chinadns_ng_proto" in
"none")
- chinadns_ng_server="${chinadns_ng_ip}#${chinadns_ng_port}"
- ;;
+ chinadns_ng_server="${chinadns_ng_ip}#${chinadns_ng_port}"
+ ;;
"tls")
- chinadns_ng_server="${chinadns_ng_proto}://${chinadns_ng_ip}#${chinadns_ng_tls_port}"
- ;;
+ chinadns_ng_server="${chinadns_ng_proto}://${chinadns_ng_ip}#${chinadns_ng_port}"
+ ;;
*)
- chinadns_ng_server="${chinadns_ng_proto}://${chinadns_ng_ip}#${chinadns_ng_port}"
- ;;
+ chinadns_ng_server="${chinadns_ng_proto}://${chinadns_ng_ip}#${chinadns_ng_port}"
+ ;;
esac
# 添加到参数列表
chinadns_ng_dns="${chinadns_ng_dns} -t ${chinadns_ng_server}"
done
unset IFS # 恢复默认分隔符
+
dnsserver="$chinadns_ng_dns"
- ln_start_bin "$(first_type chinadns-ng)" chinadns-ng -b 127.0.0.1 -l "$tmp_dns_port" -l "$dns_port" -p 3 -d gfw "$dnsserver" -N --filter-qtype 64,65 -f -r --cache 4096 --cache-stale 86400 --cache-refresh 20
+ # $dnsserver holds a list of "-t " arguments built above, so it
+ # has to stay unquoted and word-split into separate arguments
+ # shellcheck disable=SC2086
+ ln_start_bin "$(first_type chinadns-ng)" chinadns-ng -b 127.0.0.1 -l "$dns_port" -p 3 -d gfw $dnsserver -N --filter-qtype 64,65 -f -r --cache 4096 --cache-stale 86400 --cache-refresh 20 --verdict-cache 4096
+ ln_start_bin "$(first_type chinadns-ng)" chinadns-ng -b 127.0.0.1 -l "$tmp_dns_port" -p 3 -d gfw $dnsserver -N --filter-qtype 64,65 -f -r --cache 4096 --cache-stale 86400 --cache-refresh 20 --verdict-cache 4096
echolog "ChinaDNS-NG query and cache Started!"
pdnsd_enable_flag=6
;;
@@ -1332,8 +1342,30 @@ start_dns() {
"wan_114") chinadns="$wandns,114.114.114.114" ;;
esac
- filtered_chinadns=$(echo "$chinadns" | sed 's/:/#/')
- ln_start_bin "$(first_type chinadns-ng)" chinadns-ng -l "$china_dns_port" -4 china -p 3 -c "$filtered_chinadns" -t "127.0.0.1#$dns_port" -N -f -r
+ filtered_chinadns=""
+ IFS=','
+ for chinadns_ip in $chinadns; do
+ # 过滤可能的前后空白字符
+ chinadns_ip="$(echo "$chinadns_ip" | sed 's/[[:space:]]//g')"
+ [ -z "$chinadns_ip" ] && continue
+
+ # 将 IP:PORT 格式转换为 IP#PORT(若无端口默认使用 53)
+ if echo "$chinadns_ip" | grep -q "#"; then
+ : # 已是 IP#PORT 格式,无需更改
+ elif echo "$chinadns_ip" | grep -qE ':[0-9]+$'; then
+ chinadns_ip="$(echo "$chinadns_ip" | sed -E 's/:([0-9]+)$/#\1/')"
+ else
+ chinadns_ip="${chinadns_ip}#53"
+ fi
+ filtered_chinadns="${filtered_chinadns} -c ${chinadns_ip}"
+ done
+ unset IFS
+
+ if [ "$USE_TABLES" = "iptables" ]; then
+ ln_start_bin "$(first_type chinadns-ng)" chinadns-ng -l "$china_dns_port" -4 china -p 3 $filtered_chinadns -t "127.0.0.1#$dns_port" -N -f -r --cache 4096 --cache-stale 86400 --cache-refresh 20 --verdict-cache 4096
+ elif [ "$USE_TABLES" = "nftables" ]; then
+ ln_start_bin "$(first_type chinadns-ng)" chinadns-ng -l "$china_dns_port" -4 inet@ss_spec@china -p 3 $filtered_chinadns -t "127.0.0.1#$dns_port" -N -f -r --cache 4096 --cache-stale 86400 --cache-refresh 20 --verdict-cache 4096
+ fi
cat <<-EOF >> "$TMP_DNSMASQ_PATH/chinadns_fixed_server.conf"
no-poll
@@ -1466,21 +1498,21 @@ generate_xray_config() {
fi
;;
esac
-
- cat > "$config_file" << EOF
-{
- "log": {
- "loglevel": "warning"
- },
- "inbounds": [$inbound_json],
- "outbounds": [
+
+ cat <<-EOF >"$config_file"
{
- "protocol": "freedom",
- "settings": {}
- }
- ]
-}
-EOF
+ "log": {
+ "loglevel": "warning"
+ },
+ "inbounds": [$inbound_json],
+ "outbounds": [
+ {
+ "protocol": "freedom",
+ "settings": {}
+ }
+ ]
+ }
+ EOF
return 0
}
@@ -2944,7 +2976,7 @@ start() {
start_rules
start_dns
# Restore ipsets after rules creation
- if [ "$HAS_IPSET" -eq 1 ]; then
+ if [ "$USE_TABLES" = "iptables" ]; then
for setname in gfwlist china blacklist whitelist; do
[ "$setname" = "gfwlist" ] && [ "$run_mode" != "gfw" ] && continue
if [ -f "/tmp/ssrplus_save/${setname}.save" ]; then
@@ -3037,7 +3069,7 @@ stop() {
set_lock
check_run_environment
# Save ipsets before stopping to persist transparent proxy state
- if [ "$HAS_IPSET" -eq 1 ]; then
+ if [ "$USE_TABLES" = "iptables" ]; then
mkdir -p /tmp/ssrplus_save
run_mode="$(uci_get_by_type global run_mode)"
if [ "$run_mode" = "gfw" ]; then
diff --git a/luci-app-ssr-plus/root/usr/share/shadowsocksr/gen_config.lua b/luci-app-ssr-plus/root/usr/share/shadowsocksr/gen_config.lua
index 5e85894a..75133640 100755
--- a/luci-app-ssr-plus/root/usr/share/shadowsocksr/gen_config.lua
+++ b/luci-app-ssr-plus/root/usr/share/shadowsocksr/gen_config.lua
@@ -4,12 +4,23 @@ require "luci.sys"
local ucursor = require "luci.model.uci".cursor()
local json = require "luci.jsonc"
-local server_section = arg[1]
-local proto = arg[2] or "tcp"
-local local_port = arg[3] or "0"
-local socks_port = arg[4] or "0"
+-- An omitted value reaches us either as a missing argument or as an empty
+-- string, depending on whether the caller quoted the expansion. Empty strings
+-- are truthy in Lua, so "arg[n] or default" alone does not cover both.
+local function argv(n, default)
+ local value = arg[n]
+ if value == nil or value == "" then
+ return default
+ end
+ return value
+end
-local chain = arg[5] or "0"
+local server_section = arg[1]
+local proto = argv(2, "tcp")
+local local_port = argv(3, "0")
+local socks_port = argv(4, "0")
+
+local chain = argv(5, "0")
-- trim
local function trim(text)
diff --git a/mosdns/Makefile b/mosdns/Makefile
index 5083c145..180903fa 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:=14
+PKG_RELEASE:=15
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/211-feat-add-query-log-support.patch b/mosdns/patches/211-feat-add-query-log-support.patch
new file mode 100644
index 00000000..3cd630ae
--- /dev/null
+++ b/mosdns/patches/211-feat-add-query-log-support.patch
@@ -0,0 +1,661 @@
+From ebf6a87a410d60785894f9c2c7693de5921591bf Mon Sep 17 00:00:00 2001
+From: sbwml
+Date: Wed, 19 Aug 2026 12:44:54 +0800
+Subject: [PATCH 1/2] feat: add query log support
+
+Signed-off-by: sbwml
+---
+ mlog/logger.go | 11 ++
+ pkg/query_context/context.go | 146 ++++++++++++++++++++++++-
+ pkg/server/doq.go | 1 +
+ pkg/server/http_handler.go | 1 +
+ pkg/server/iface.go | 1 +
+ pkg/server/tcp.go | 6 +-
+ pkg/server/udp.go | 2 +-
+ pkg/server_handler/entry_handler.go | 5 +
+ plugin/executable/cache/cache.go | 12 ++
+ plugin/executable/forward/forward.go | 34 +++++-
+ plugin/executable/sequence/built_in.go | 12 +-
+ plugin/executable/sequence/chain.go | 98 ++++++++++++++++-
+ plugin/executable/sequence/sequence.go | 8 +-
+ 13 files changed, 316 insertions(+), 21 deletions(-)
+
+--- a/mlog/logger.go
++++ b/mlog/logger.go
+@@ -55,13 +55,24 @@ var (
+ s = l.Sugar()
+
+ nop = zap.NewNop()
++
++ isDebug bool
+ )
+
++func IsDebug() bool {
++ return isDebug
++}
++
+ func NewLogger(lc LogConfig) (*zap.Logger, error) {
+ lvl, err := zapcore.ParseLevel(lc.Level)
+ if err != nil {
+ return nil, fmt.Errorf("invalid log level: %w", err)
+ }
++ if lvl <= zapcore.DebugLevel {
++ isDebug = true
++ } else {
++ isDebug = false
++ }
+
+ var out zapcore.WriteSyncer
+ if lf := lc.File; len(lf) > 0 {
+--- a/pkg/query_context/context.go
++++ b/pkg/query_context/context.go
+@@ -20,9 +20,11 @@
+ package query_context
+
+ import (
++ "fmt"
+ "sync/atomic"
+ "time"
+
++ "github.com/IrineSistiana/mosdns/v5/mlog"
+ "github.com/IrineSistiana/mosdns/v5/pkg/server"
+ "github.com/miekg/dns"
+ "go.uber.org/zap"
+@@ -33,6 +35,26 @@ const (
+ edns0Size = 1200
+ )
+
++type UpstreamLog struct {
++ Addr string
++ Protocol string
++ Tag string
++ Plugin string
++}
++
++type RuleHit struct {
++ Sequence string
++ Matches []string
++ Exec string
++}
++
++type CacheLog struct {
++ Hit bool
++ LazyHit bool
++ TTL int
++ RemainingTTL int
++}
++
+ // Context is a query context that pass through plugins.
+ // All Context funcs are not safe for concurrent use.
+ type Context struct {
+@@ -49,11 +71,42 @@ type Context struct {
+ respOpt *dns.OPT // nil if clientOpt == nil
+ upstreamOpt *dns.OPT // may be nil
+
++ // Log details
++ UpstreamSelected *UpstreamLog
++ RuleHits []RuleHit
++ CacheState CacheLog
++
+ // lazy init.
+ kv map[uint32]any
+ marks map[uint32]struct{}
+ }
+
++func (ctx *Context) AddRuleHit(seq string, matches []string, exec string) {
++ ctx.RuleHits = append(ctx.RuleHits, RuleHit{
++ Sequence: seq,
++ Matches: matches,
++ Exec: exec,
++ })
++}
++
++func (ctx *Context) SetUpstreamSelected(addr, protocol, tag, plugin string) {
++ ctx.UpstreamSelected = &UpstreamLog{
++ Addr: addr,
++ Protocol: protocol,
++ Tag: tag,
++ Plugin: plugin,
++ }
++}
++
++func (ctx *Context) SetCacheState(hit, lazyHit bool, ttl, remainingTTL int) {
++ ctx.CacheState = CacheLog{
++ Hit: hit,
++ LazyHit: lazyHit,
++ TTL: ttl,
++ RemainingTTL: remainingTTL,
++ }
++}
++
+ var contextUid atomic.Uint32
+
+ type ServerMeta = server.QueryMeta
+@@ -237,17 +290,106 @@ func (ctx *Context) MarshalLogObject(enc
+ encoder.AddUint32("uqid", ctx.id)
+
+ if clientAddr := ctx.ServerMeta.ClientAddr; clientAddr.IsValid() {
+- zap.Stringer("client", clientAddr).AddTo(encoder)
++ encoder.AddString("client", clientAddr.String())
+ }
+
+ question := ctx.query.Question[0]
+ encoder.AddString("qname", question.Name)
+- encoder.AddUint16("qtype", question.Qtype)
++
++ qTypeStr := dns.TypeToString[question.Qtype]
++ if qTypeStr == "" {
++ qTypeStr = fmt.Sprintf("TYPE%d", question.Qtype)
++ }
++ encoder.AddString("qtype", qTypeStr)
+ encoder.AddUint16("qclass", question.Qclass)
+
++ proto := ctx.ServerMeta.Protocol
++ if proto == "" {
++ if ctx.ServerMeta.FromUDP {
++ proto = "UDP"
++ } else {
++ proto = "TCP"
++ }
++ }
++ encoder.AddString("protocol", proto)
++
++ if mlog.IsDebug() && len(ctx.RuleHits) > 0 {
++ encoder.AddArray("rule_hits", zapcore.ArrayMarshalerFunc(func(arr zapcore.ArrayEncoder) error {
++ for _, hit := range ctx.RuleHits {
++ arr.AppendObject(zapcore.ObjectMarshalerFunc(func(enc zapcore.ObjectEncoder) error {
++ enc.AddString("sequence", hit.Sequence)
++ enc.AddArray("matches", zapcore.ArrayMarshalerFunc(func(ae zapcore.ArrayEncoder) error {
++ for _, m := range hit.Matches {
++ ae.AppendString(m)
++ }
++ return nil
++ }))
++ enc.AddString("exec", hit.Exec)
++ return nil
++ }))
++ }
++ return nil
++ }))
++ }
++
++ if ctx.UpstreamSelected != nil {
++ encoder.AddObject("upstream", zapcore.ObjectMarshalerFunc(func(enc zapcore.ObjectEncoder) error {
++ enc.AddString("addr", ctx.UpstreamSelected.Addr)
++ enc.AddString("protocol", ctx.UpstreamSelected.Protocol)
++ enc.AddString("tag", ctx.UpstreamSelected.Tag)
++ enc.AddString("plugin", ctx.UpstreamSelected.Plugin)
++ return nil
++ }))
++ }
++
++ encoder.AddObject("cache", zapcore.ObjectMarshalerFunc(func(enc zapcore.ObjectEncoder) error {
++ enc.AddBool("hit", ctx.CacheState.Hit)
++ enc.AddBool("lazy_hit", ctx.CacheState.LazyHit)
++ enc.AddInt("ttl", ctx.CacheState.TTL)
++ enc.AddInt("remaining_ttl", ctx.CacheState.RemainingTTL)
++ return nil
++ }))
++
+ if r := ctx.resp; r != nil {
+ encoder.AddInt("rcode", r.Rcode)
++ encoder.AddInt("resp_size", r.Len())
++
++ var ips []string
++ var cnames []string
++ var ttl uint32
++ for _, rr := range r.Answer {
++ ttl = rr.Header().Ttl
++ switch record := rr.(type) {
++ case *dns.A:
++ ips = append(ips, record.A.String())
++ case *dns.AAAA:
++ ips = append(ips, record.AAAA.String())
++ case *dns.CNAME:
++ cnames = append(cnames, record.Target)
++ }
++ }
++
++ if len(ips) > 0 {
++ encoder.AddArray("ips", zapcore.ArrayMarshalerFunc(func(ae zapcore.ArrayEncoder) error {
++ for _, ip := range ips {
++ ae.AppendString(ip)
++ }
++ return nil
++ }))
++ }
++ if len(cnames) > 0 {
++ encoder.AddArray("cnames", zapcore.ArrayMarshalerFunc(func(ae zapcore.ArrayEncoder) error {
++ for _, c := range cnames {
++ ae.AppendString(c)
++ }
++ return nil
++ }))
++ }
++ if len(r.Answer) > 0 {
++ encoder.AddUint32("original_ttl", ttl)
++ }
+ }
++
+ encoder.AddDuration("elapsed", time.Since(ctx.startTime))
+ return nil
+ }
+--- a/pkg/server/doq.go
++++ b/pkg/server/doq.go
+@@ -107,6 +107,7 @@ func ServeDoQ(l *quic.Listener, h Handle
+ queryMeta := QueryMeta{
+ ClientAddr: clientAddr,
+ ServerName: c.ConnectionState().TLS.ServerName,
++ Protocol: "DoQ",
+ }
+
+ resp := h.Handle(connCtx, req, queryMeta, pool.PackTCPBuffer)
+--- a/pkg/server/http_handler.go
++++ b/pkg/server/http_handler.go
+@@ -97,6 +97,7 @@ func (h *HttpHandler) ServeHTTP(w http.R
+
+ queryMeta := QueryMeta{
+ ClientAddr: clientAddr,
++ Protocol: "DoH",
+ }
+ if u := req.URL; u != nil {
+ queryMeta.UrlPath = u.Path
+--- a/pkg/server/iface.go
++++ b/pkg/server/iface.go
+@@ -26,4 +26,5 @@ type QueryMeta struct {
+ ClientAddr netip.Addr
+ ServerName string
+ UrlPath string
++ Protocol string
+ }
+--- a/pkg/server/tcp.go
++++ b/pkg/server/tcp.go
+@@ -101,7 +101,11 @@ func ServeTCP(l net.Listener, h Handler,
+ if ok {
+ clientAddr = ta.AddrPort().Addr()
+ }
+- r := h.Handle(tcpConnCtx, req, QueryMeta{ClientAddr: clientAddr, ServerName: serverName}, pool.PackTCPBuffer)
++ proto := "TCP"
++ if serverName != "" {
++ proto = "DoT"
++ }
++ r := h.Handle(tcpConnCtx, req, QueryMeta{ClientAddr: clientAddr, ServerName: serverName, Protocol: proto}, pool.PackTCPBuffer)
+ if r == nil {
+ c.Close() // abort the connection
+ return
+--- a/pkg/server/udp.go
++++ b/pkg/server/udp.go
+@@ -88,7 +88,7 @@ func ServeUDP(c *net.UDPConn, h Handler,
+
+ // handle query
+ go func() {
+- payload := h.Handle(listenerCtx, q, QueryMeta{ClientAddr: remoteAddr.Addr(), FromUDP: true}, pool.PackBuffer)
++ payload := h.Handle(listenerCtx, q, QueryMeta{ClientAddr: remoteAddr.Addr(), FromUDP: true, Protocol: "UDP"}, pool.PackBuffer)
+ if payload == nil {
+ return
+ }
+--- a/pkg/server_handler/entry_handler.go
++++ b/pkg/server_handler/entry_handler.go
+@@ -131,6 +131,11 @@ func (h *EntryHandler) Handle(ctx contex
+ h.opts.Logger.Error("internal err: failed to pack resp msg", qCtx.InfoField(), zap.Error(err))
+ return nil
+ }
++ if mlog.IsDebug() {
++ h.opts.Logger.Debug("query log", zap.Inline(qCtx))
++ } else {
++ h.opts.Logger.Info("query log", zap.Inline(qCtx))
++ }
+ return payload
+ }
+
+--- a/plugin/executable/cache/cache.go
++++ b/plugin/executable/cache/cache.go
+@@ -204,6 +204,18 @@ func (c *Cache) Exec(ctx context.Context
+ c.hitTotal.Inc()
+ cachedResp.Id = q.Id // change msg id
+ qCtx.SetResponse(cachedResp)
++ if v, _, ok := c.backend.Get(key(msgKey)); ok && v != nil {
++ ttl := int(v.expirationTime.Sub(v.storedTime).Seconds())
++ remainingTtl := int(v.expirationTime.Sub(time.Now()).Seconds())
++ if remainingTtl < 0 {
++ remainingTtl = 0
++ }
++ qCtx.SetCacheState(true, lazyHit, ttl, remainingTtl)
++ } else {
++ qCtx.SetCacheState(true, lazyHit, 0, 0)
++ }
++ } else {
++ qCtx.SetCacheState(false, false, 0, 0)
+ }
+
+ err := next.ExecNext(ctx, qCtx)
+--- a/plugin/executable/forward/forward.go
++++ b/plugin/executable/forward/forward.go
+@@ -104,6 +104,7 @@ type Forward struct {
+ logger *zap.Logger
+ us []*upstreamWrapper
+ tag2Upstream map[string]*upstreamWrapper // for fast tag lookup only.
++ pluginTag string
+ }
+
+ type Opts struct {
+@@ -125,6 +126,7 @@ func NewForward(args *Args, opt Opts) (*
+ args: args,
+ logger: opt.Logger,
+ tag2Upstream: make(map[string]*upstreamWrapper),
++ pluginTag: opt.MetricsTag,
+ }
+
+ applyGlobal := func(c *UpstreamConfig) {
+@@ -256,6 +258,7 @@ func (f *Forward) exchange(ctx context.C
+ type res struct {
+ r *dns.Msg
+ err error
++ u *upstreamWrapper
+ }
+
+ resChan := make(chan res)
+@@ -266,14 +269,14 @@ func (f *Forward) exchange(ctx context.C
+ for i := 0; i < concurrent; i++ {
+ u := us[(r+i)%len(us)]
+ qc := copyPayload(queryPayload)
+- go func(uqid uint32, question dns.Question) {
++ go func(uqid uint32, question dns.Question, chosenUpstream *upstreamWrapper) {
+ defer pool.ReleaseBuf(qc)
+ // Give each upstream a fixed timeout to finish the query.
+ upstreamCtx, cancel := context.WithTimeout(context.Background(), queryTimeout)
+ defer cancel()
+
+ var r *dns.Msg
+- respPayload, err := u.ExchangeContext(upstreamCtx, *qc)
++ respPayload, err := chosenUpstream.ExchangeContext(upstreamCtx, *qc)
+ if err != nil {
+ f.logger.Warn(
+ "upstream error",
+@@ -281,7 +284,7 @@ func (f *Forward) exchange(ctx context.C
+ zap.String("qname", question.Name),
+ zap.Uint16("qclass", question.Qclass),
+ zap.Uint16("qtype", question.Qtype),
+- zap.String("upstream", u.name()),
++ zap.String("upstream", chosenUpstream.name()),
+ zap.Error(err),
+ )
+ } else {
+@@ -293,16 +296,16 @@ func (f *Forward) exchange(ctx context.C
+ }
+ }
+ select {
+- case resChan <- res{r: r, err: err}:
++ case resChan <- res{r: r, err: err, u: chosenUpstream}:
+ case <-done:
+ }
+- }(qCtx.Id(), qCtx.QQuestion())
++ }(qCtx.Id(), qCtx.QQuestion(), u)
+ }
+
+ for i := 0; i < concurrent; i++ {
+ select {
+ case res := <-resChan:
+- r, err := res.r, res.err
++ r, err, chosenUpstream := res.r, res.err, res.u
+ if err != nil {
+ continue
+ }
+@@ -311,6 +314,25 @@ func (f *Forward) exchange(ctx context.C
+ if i < concurrent-1 && r.Rcode != dns.RcodeSuccess && r.Rcode != dns.RcodeNameError {
+ continue
+ }
++
++ if chosenUpstream != nil {
++ addr := chosenUpstream.cfg.Addr
++ proto := "UDP"
++ if strings.Contains(addr, "://") {
++ parts := strings.SplitN(addr, "://", 2)
++ proto = strings.ToUpper(parts[0])
++ addr = parts[1]
++ }
++ if proto == "TLS" {
++ proto = "DoT"
++ } else if proto == "HTTPS" {
++ proto = "DoH"
++ } else if proto == "QUIC" || proto == "DOQ" {
++ proto = "DoQ"
++ }
++ qCtx.SetUpstreamSelected(addr, proto, chosenUpstream.cfg.Tag, f.pluginTag)
++ }
++
+ return r, nil
+ case <-ctx.Done():
+ return nil, context.Cause(ctx)
+--- a/plugin/executable/sequence/built_in.go
++++ b/plugin/executable/sequence/built_in.go
+@@ -83,11 +83,13 @@ func setupReturn(_ BQ, _ string) (any, e
+ var _ RecursiveExecutable = (*ActionJump)(nil)
+
+ type ActionJump struct {
+- To []*ChainNode
++ To []*ChainNode
++ tag string
+ }
+
+ func (a *ActionJump) Exec(ctx context.Context, qCtx *query_context.Context, next ChainWalker) error {
+ w := NewChainWalker(a.To, &next)
++ w.sequenceTag = a.tag
+ return w.ExecNext(ctx, qCtx)
+ }
+
+@@ -96,17 +98,19 @@ func setupJump(bq BQ, s string) (any, er
+ if target == nil {
+ return nil, fmt.Errorf("can not find jump target %s", s)
+ }
+- return &ActionJump{To: target.chain}, nil
++ return &ActionJump{To: target.chain, tag: s}, nil
+ }
+
+ var _ RecursiveExecutable = (*ActionGoto)(nil)
+
+ type ActionGoto struct {
+- To []*ChainNode
++ To []*ChainNode
++ tag string
+ }
+
+ func (a ActionGoto) Exec(ctx context.Context, qCtx *query_context.Context, _ ChainWalker) error {
+ w := NewChainWalker(a.To, nil)
++ w.sequenceTag = a.tag
+ return w.ExecNext(ctx, qCtx)
+ }
+
+@@ -115,7 +119,7 @@ func setupGoto(bq BQ, s string) (any, er
+ if gt == nil {
+ return nil, fmt.Errorf("can not find goto target %s", s)
+ }
+- return &ActionGoto{To: gt.chain}, nil
++ return &ActionGoto{To: gt.chain, tag: s}, nil
+ }
+
+ var _ Matcher = (*MatchAlwaysTrue)(nil)
+--- a/plugin/executable/sequence/chain.go
++++ b/plugin/executable/sequence/chain.go
+@@ -25,6 +25,8 @@ import (
+ "fmt"
+ "github.com/IrineSistiana/mosdns/v5/pkg/query_context"
+ "io"
++ "reflect"
++ "strings"
+ )
+
+ type ChainNode struct {
+@@ -34,12 +36,17 @@ type ChainNode struct {
+ // In case both are set. E is preferred.
+ E Executable
+ RE RecursiveExecutable
++
++ RuleMatches []string
++ RuleExec string
++ IsControlOrSequence bool
+ }
+
+ type ChainWalker struct {
+- p int
+- chain []*ChainNode
+- jumpBack *ChainWalker
++ p int
++ chain []*ChainNode
++ jumpBack *ChainWalker
++ sequenceTag string
+ }
+
+ func NewChainWalker(chain []*ChainNode, jumpBack *ChainWalker) ChainWalker {
+@@ -49,6 +56,49 @@ func NewChainWalker(chain []*ChainNode,
+ }
+ }
+
++func formatMatchConfig(mc MatchConfig) string {
++ var s string
++ if mc.Tag != "" {
++ s = "$" + mc.Tag
++ } else {
++ s = mc.Type
++ if mc.Args != "" {
++ s += " " + mc.Args
++ }
++ }
++ if mc.Reverse {
++ s = "!" + s
++ }
++ return s
++}
++
++func formatRuleExec(rc RuleConfig) string {
++ if rc.Tag != "" {
++ return "$" + rc.Tag
++ }
++ s := rc.Type
++ if rc.Args != "" {
++ s += " " + rc.Args
++ }
++ return s
++}
++
++func shouldLogRule(n *ChainNode, hasRespBefore, hasRespAfter bool) bool {
++ for _, m := range n.RuleMatches {
++ if !strings.HasPrefix(m, "!") {
++ return true
++ }
++ }
++ if !hasRespBefore && hasRespAfter {
++ return true
++ }
++ exec := n.RuleExec
++ if exec == "accept" || exec == "reject" || strings.HasPrefix(exec, "reject ") || exec == "drop_resp" || exec == "black_hole" {
++ return true
++ }
++ return false
++}
++
+ func (w *ChainWalker) ExecNext(ctx context.Context, qCtx *query_context.Context) error {
+ p := w.p
+ // Evaluate rules' matchers in loop.
+@@ -71,16 +121,26 @@ checkMatchesLoop:
+ // Exec rules' executables in loop, or in stack if it is a recursive executable.
+ switch {
+ case n.E != nil:
++ hasRespBefore := qCtx.R() != nil
+ if err := n.E.Exec(ctx, qCtx); err != nil {
+ return err
+ }
++ hasRespAfter := qCtx.R() != nil
++ if shouldLogRule(n, hasRespBefore, hasRespAfter) {
++ qCtx.AddRuleHit(w.sequenceTag, n.RuleMatches, n.RuleExec)
++ }
+ p++
+ continue
+ case n.RE != nil:
++ hasRespBefore := qCtx.R() != nil
++ if shouldLogRule(n, hasRespBefore, false) {
++ qCtx.AddRuleHit(w.sequenceTag, n.RuleMatches, n.RuleExec)
++ }
+ next := ChainWalker{
+- p: p + 1,
+- chain: w.chain,
+- jumpBack: w.jumpBack,
++ p: p + 1,
++ chain: w.chain,
++ jumpBack: w.jumpBack,
++ sequenceTag: w.sequenceTag,
+ }
+ return n.RE.Exec(ctx, qCtx, next)
+ default:
+@@ -113,8 +173,33 @@ func (s *Sequence) buildChain(bq BQ, rs
+ return nil
+ }
+
++func isControlOrSequence(bq BQ, rc RuleConfig) bool {
++ exec := formatRuleExec(rc)
++ if exec == "accept" || exec == "reject" || exec == "return" {
++ return false
++ }
++ if strings.HasPrefix(exec, "jump") || strings.HasPrefix(exec, "goto") || strings.HasPrefix(exec, "return") {
++ return true
++ }
++ if rc.Tag != "" {
++ p := bq.M().GetPlugin(rc.Tag)
++ if p != nil {
++ t := reflect.TypeOf(p)
++ if t != nil {
++ name := t.String()
++ if strings.Contains(name, "Sequence") || strings.Contains(name, "Fallback") {
++ return true
++ }
++ }
++ }
++ }
++ return false
++}
++
+ func (s *Sequence) newNode(bq BQ, r RuleConfig, ri int) (*ChainNode, error) {
+ n := new(ChainNode)
++ n.RuleExec = formatRuleExec(r)
++ n.IsControlOrSequence = (len(r.Matches) == 0) && isControlOrSequence(bq, r)
+
+ // init matches
+ for mi, mc := range r.Matches {
+@@ -123,6 +208,7 @@ func (s *Sequence) newNode(bq BQ, r Rule
+ return nil, fmt.Errorf("failed to init matcher #%d, %w", mi, err)
+ }
+ n.Matches = append(n.Matches, m)
++ n.RuleMatches = append(n.RuleMatches, formatMatchConfig(mc))
+ }
+
+ // init exec
+--- a/plugin/executable/sequence/sequence.go
++++ b/plugin/executable/sequence/sequence.go
+@@ -40,6 +40,7 @@ func init() {
+ }
+
+ type Sequence struct {
++ tag string
+ chain []*ChainNode
+ anonymousPlugins []any
+ }
+@@ -58,7 +59,11 @@ func Init(bp *coremain.BP, args any) (an
+ }
+
+ func NewSequence(bq BQ, ra []RuleArgs) (*Sequence, error) {
+- s := &Sequence{}
++ var tag string
++ if t, ok := bq.(interface{ Tag() string }); ok {
++ tag = t.Tag()
++ }
++ s := &Sequence{tag: tag}
+
+ var rc []RuleConfig
+ for _, ra := range ra {
+@@ -73,5 +78,6 @@ func NewSequence(bq BQ, ra []RuleArgs) (
+
+ func (s *Sequence) Exec(ctx context.Context, qCtx *query_context.Context) error {
+ walker := NewChainWalker(s.chain, nil)
++ walker.sequenceTag = s.tag
+ return walker.ExecNext(ctx, qCtx)
+ }
diff --git a/mosdns/patches/212-fix-mlog-fix-log-file-padded-with-null-bytes-after-e.patch b/mosdns/patches/212-fix-mlog-fix-log-file-padded-with-null-bytes-after-e.patch
new file mode 100644
index 00000000..ef8c2dc4
--- /dev/null
+++ b/mosdns/patches/212-fix-mlog-fix-log-file-padded-with-null-bytes-after-e.patch
@@ -0,0 +1,38 @@
+From 0e78ad1d047043f9f3b4f47dea7cf9905ad09a7c Mon Sep 17 00:00:00 2001
+From: sbwml
+Date: Wed, 19 Aug 2026 12:32:48 +0800
+Subject: [PATCH 2/2] fix(mlog): fix log file padded with null bytes after
+ external truncation
+
+Signed-off-by: sbwml
+---
+ mlog/rotator.go | 11 ++++++++---
+ 1 file changed, 8 insertions(+), 3 deletions(-)
+
+--- a/mlog/rotator.go
++++ b/mlog/rotator.go
+@@ -52,8 +52,13 @@ func (r *Rotator) Write(p []byte) (n int
+
+ writeLen := int64(len(p))
+ if r.MaxSize > 0 && r.size+writeLen > r.MaxSize {
+- if err := r.rotate(); err != nil {
+- return 0, err
++ if info, err := r.f.Stat(); err == nil {
++ r.size = info.Size()
++ }
++ if r.size+writeLen > r.MaxSize {
++ if err := r.rotate(); err != nil {
++ return 0, err
++ }
+ }
+ }
+
+@@ -84,7 +89,7 @@ func (r *Rotator) openNew() error {
+ if err != nil {
+ return err
+ }
+- f, err := os.OpenFile(r.Filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
++ f, err := os.OpenFile(r.Filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC|os.O_APPEND, 0644)
+ if err != nil {
+ return err
+ }