mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-04 01:03:56 +08:00
🤞 Sync 2026-09-27 22:16:29
This commit is contained in:
1 parent
8ede4a8320
commit
618f51e9dc
459 files changed
+44817
-2191
No files matched your search
+388
@@ -0,0 +1,388 @@
|
||||
#!/bin/sh
|
||||
# dnsguard-apply —— 把 UCI(/etc/config/dnsguard) 渲染成 /etc/nftables.d/20-dns-guard.nft 并应用
|
||||
#
|
||||
# 设计原则(与邮件日报应用一致):本脚本是唯一"写防火墙"的地方,WebUI 只调用它。
|
||||
#
|
||||
# 用法:
|
||||
# dnsguard-apply 渲染 + 校验 + 落盘 + fw4 reload,输出一行 JSON
|
||||
# dnsguard-apply --quiet 同上,不输出
|
||||
# dnsguard-apply --render-only 只渲染到 --out 指定文件(不动 /etc、不 reload),用于界面预览
|
||||
# dnsguard-apply --no-reload 渲染 + 落盘,但不 reload(文件变更下次 fw4 reload 生效)
|
||||
#
|
||||
# 安全闸门:
|
||||
# ① 片段先包进临时表名做 nft -c 语法校验,不通过绝不落盘
|
||||
# ② 落盘后用 fw4 check 全量校验真实规则集,不通过立即还原旧文件
|
||||
# ③ reload 后核对 dns_guard_redirect 链是否真的存在,不存在立即还原
|
||||
# ④ redirect53=1 而 AdGuardHome 未运行 -> 直接拒绝(不然全屋解析全挂)
|
||||
#
|
||||
# 生成:WorkBuddy 2026-09-26
|
||||
|
||||
set -u
|
||||
|
||||
CFG=dnsguard
|
||||
OUT=/etc/nftables.d/20-dns-guard.nft
|
||||
BAKDIR=/mnt/data/backup-dnsguard
|
||||
GENMARK='由 LuCI 应用「DNS 防绕过」自动生成'
|
||||
|
||||
QUIET=0
|
||||
RENDER_ONLY=0
|
||||
NO_RELOAD=0
|
||||
OUTFILE=""
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--quiet) QUIET=1 ;;
|
||||
--render-only) RENDER_ONLY=1 ;;
|
||||
--no-reload) NO_RELOAD=1 ;;
|
||||
--out) shift; OUTFILE="${1:-}" ;;
|
||||
*) : ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
# ---------------- 工具 ----------------
|
||||
|
||||
uget() { uci -q get "$CFG.$1" 2>/dev/null; }
|
||||
|
||||
bool() {
|
||||
case "${1:-}" in
|
||||
1|on|true|yes|enabled) echo 1 ;;
|
||||
*) echo 0 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
digits_only() { printf '%s' "${1:-}" | tr -dc '0-9'; }
|
||||
|
||||
# 单行转义成 JSON 字符串内容(换行 -> \n,反斜杠与双引号转义)
|
||||
jesc() {
|
||||
printf '%s' "${1:-}" | tr '\n' '\001' | sed 's/\\/\\\\/g; s/"/\\"/g' | tr '\001' '\n' | awk '{printf "%s\\n", $0}' | sed 's/\\n$//'
|
||||
}
|
||||
|
||||
# 统一的行输出:整行作为单个参数传入,避免 ${VAR} 里的双引号被 shell 吃掉
|
||||
p() { printf '%s\n' "$1"; }
|
||||
|
||||
out_json() {
|
||||
[ "$QUIET" -eq 1 ] && return 0
|
||||
printf '%s\n' "$1"
|
||||
}
|
||||
|
||||
EMIT_JSON() {
|
||||
out_json "{\"status\":\"${STATUS:-error}\",\"message\":\"$(jesc "${MSG:-}")\",\"enabled\":\"${ENABLED:-0}\",\"redirect\":\"${REDIRECT:-0}\",\"watch\":\"${WATCH:-0}\",\"block853\":\"${B853:-0}\",\"blockdoh\":\"${BDOH:-0}\",\"file\":\"${FILE_STATE:-none}\",\"reload\":\"${RELOAD_STATE:-skipped}\",\"redirect_active\":\"${REDIRECT_ACTIVE:-0}\",\"target\":\"${TARGET:-53}\",\"ifaces\":\"$(jesc "${IFACES:-}")\",\"backup\":\"$(jesc "${BAK_TS:-}")\"}"
|
||||
}
|
||||
|
||||
fail() {
|
||||
STATUS=error
|
||||
MSG="$1"
|
||||
EMIT_JSON
|
||||
exit "${2:-1}"
|
||||
}
|
||||
|
||||
# 只保留合法 IP/CIDR,其余丢弃 —— 防止把垃圾写进 nft 导致整个防火墙加载失败
|
||||
# ⚠️ 必须用 printf '%s\n' 补结尾换行:否则 while read 会把「最后一个元素」丢掉(踩过)
|
||||
set_elems() {
|
||||
printf '%s\n' "${1:-}" | sed 's/[[:space:]]\{1,\}/\n/g' | while read -r e; do
|
||||
[ -n "$e" ] || continue
|
||||
case "$e" in
|
||||
*:*)
|
||||
printf '%s' "$e" | grep -qE '^[0-9a-fA-F:]+(/[0-9]{1,3})?$' && printf '%s ' "$e" ;;
|
||||
*)
|
||||
printf '%s' "$e" | grep -qE '^([0-9]{1,3}\.){3}[0-9]{1,3}(/[0-9]{1,2})?$' && printf '%s ' "$e" ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# 允许的入接口(字母数字点下划线连字符)
|
||||
set_ifaces() {
|
||||
printf '%s\n' "${1:-}" | sed 's/[[:space:]]\{1,\}/\n/g' | while read -r e; do
|
||||
[ -n "$e" ] || continue
|
||||
printf '%s' "$e" | grep -qE '^[0-9A-Za-z._-]+$' && printf '%s ' "$e"
|
||||
done
|
||||
}
|
||||
|
||||
split4() { for e in ${1:-}; do case "$e" in *:*) : ;; *) printf '%s ' "$e" ;; esac; done; }
|
||||
split6() { for e in ${1:-}; do case "$e" in *:*) printf '%s ' "$e" ;; *) : ;; esac; done; }
|
||||
|
||||
# 空格分隔 -> nft 集合元素 "a, b, c"
|
||||
join_elems() {
|
||||
printf '%s' "${1:-}" | sed 's/^ *//; s/ *$//; s/ */, /g'
|
||||
}
|
||||
|
||||
# ---------------- 1. 读配置 ----------------
|
||||
|
||||
ENABLED=$(bool "$(uget main.enabled)")
|
||||
REDIRECT=$(bool "$(uget main.redirect53)")
|
||||
WATCH=$(bool "$(uget main.watch)")
|
||||
B853=$(bool "$(uget main.block853)")
|
||||
BDOH=$(bool "$(uget main.blockdoh)")
|
||||
|
||||
TARGET=$(digits_only "$(uget main.target_port)"); [ -n "$TARGET" ] || TARGET=53
|
||||
LOGLIMIT=$(digits_only "$(uget main.log_limit)"); [ -n "$LOGLIMIT" ] || LOGLIMIT=5
|
||||
IFACES=$(set_ifaces "$(uget main.ifaces)")
|
||||
[ -n "$IFACES" ] || IFACES="br-lan"
|
||||
IFACES=$(printf '%s' "$IFACES" | sed 's/ *$//')
|
||||
|
||||
SKIP_RAW=$(set_elems "$(uget main.skip_devices)")
|
||||
SKIP4=$(split4 "$SKIP_RAW")
|
||||
SKIP6=$(split6 "$SKIP_RAW")
|
||||
|
||||
NH4=$(set_elems "$(uget limits.never_hijack4)")
|
||||
NH6=$(set_elems "$(uget limits.never_hijack6)")
|
||||
DOH=$(set_elems "$(uget limits.doh_servers)")
|
||||
|
||||
# 接口集合:单接口写 "br-lan",多接口写 { "a", "b" }
|
||||
IIF_RENDER=$(printf '%s' "$IFACES" | sed 's/^ *//; s/ *$//' | awk '{
|
||||
n = split($0, a, " ")
|
||||
if (n <= 1) { print "\"" a[1] "\"" }
|
||||
else { s = "{ "; for (i = 1; i <= n; i++) { s = s "\"" a[i] "\"" (i < n ? ", " : " ") } s = s "}"; print s }
|
||||
}')
|
||||
|
||||
STATUS=ok
|
||||
MSG=""
|
||||
FILE_STATE=none
|
||||
RELOAD_STATE=skipped
|
||||
REDIRECT_ACTIVE=0
|
||||
BAK_TS=""
|
||||
|
||||
# 安全闸门 ④:重定向开着但 AGH 没跑 —— 无论如何都不能生成这条规则
|
||||
AGH_PID=$(pidof AdGuardHome 2>/dev/null)
|
||||
if [ "$ENABLED" = "1" ] && [ "$REDIRECT" = "1" ] && [ -z "$AGH_PID" ]; then
|
||||
fail "AdGuard Home 未运行(pidof 为空)—— 已拒绝应用 53 透明重定向:此时重定向会让全屋解析失败。请先启动 AdGuard Home。" 3
|
||||
fi
|
||||
|
||||
# ---------------- 2. 渲染 ----------------
|
||||
|
||||
TMPD="/tmp/dg-apply.$$"
|
||||
mkdir -p "$TMPD" 2>/dev/null
|
||||
TMP="$TMPD/20-dns-guard.nft"
|
||||
|
||||
gen() {
|
||||
p "## ============================================================"
|
||||
p "## DNS 防绕过 —— ${GENMARK},请勿手工编辑"
|
||||
p "## 生成时间:$(date '+%Y-%m-%d %H:%M:%S') (= 规则上一次实际变化的时间;幂等比较时会忽略本行)"
|
||||
p "## 入口:LuCI -> 服务 -> DNS 防绕过 回滚:/usr/bin/dnsguard-rollback"
|
||||
p "## 备份目录:$BAKDIR"
|
||||
p "## 本文件由 fw4 在 inet/fw4 表上下文内 include(见 /etc/nftables.d/README)"
|
||||
p "## ============================================================"
|
||||
p ""
|
||||
|
||||
if [ "$ENABLED" != "1" ]; then
|
||||
p "## 总开关已关闭:不生成任何链(等于恢复出厂状态)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# ---- 集合 ----
|
||||
if [ -n "$DOH" ]; then
|
||||
p "set doh_servers {"
|
||||
p " type ipv4_addr"
|
||||
p " flags interval"
|
||||
p " elements = { $(join_elems "$DOH") }"
|
||||
p "}"
|
||||
p ""
|
||||
fi
|
||||
|
||||
p "## 永不劫持的目标(内网 / 保留段)—— 避免把指向内网 DNS 的查询也改道"
|
||||
p "set dns_never_hijack4 {"
|
||||
p " type ipv4_addr"
|
||||
p " flags interval"
|
||||
[ -n "$NH4" ] && p " elements = { $(join_elems "$NH4") }"
|
||||
p "}"
|
||||
p ""
|
||||
p "set dns_never_hijack6 {"
|
||||
p " type ipv6_addr"
|
||||
p " flags interval"
|
||||
[ -n "$NH6" ] && p " elements = { $(join_elems "$NH6") }"
|
||||
p "}"
|
||||
p ""
|
||||
|
||||
if [ "$REDIRECT" = "1" ] && [ -n "$SKIP_RAW" ]; then
|
||||
p "## 设备级例外:这些源地址不做透明重定向(界面「例外设备」)"
|
||||
p "set dns_hijack_skip4 {"
|
||||
p " type ipv4_addr"
|
||||
p " flags interval"
|
||||
[ -n "$SKIP4" ] && p " elements = { $(join_elems "$SKIP4") }"
|
||||
p "}"
|
||||
p ""
|
||||
p "set dns_hijack_skip6 {"
|
||||
p " type ipv6_addr"
|
||||
p " flags interval"
|
||||
[ -n "$SKIP6" ] && p " elements = { $(join_elems "$SKIP6") }"
|
||||
p "}"
|
||||
p ""
|
||||
fi
|
||||
|
||||
# ---- 层 ①:53 透明重定向 ----
|
||||
if [ "$REDIRECT" = "1" ]; then
|
||||
if [ -n "$SKIP_RAW" ]; then
|
||||
S4="ip saddr != @dns_hijack_skip4"
|
||||
S6="ip6 saddr != @dns_hijack_skip6"
|
||||
else
|
||||
S4=""
|
||||
S6=""
|
||||
fi
|
||||
p "## 层 ① 53 透明重定向 -> AdGuard Home(nat prerouting priority -105,早于 fw4 dstnat -100)"
|
||||
p "## 客户端无感:以为还在跟 8.8.8.8 说话,实际被改道到本机 :${TARGET}"
|
||||
p "chain dns_guard_redirect {"
|
||||
p " type nat hook prerouting priority -105; policy accept;"
|
||||
p ""
|
||||
p " iifname ${IIF_RENDER} meta l4proto { tcp, udp } th dport 53 ${S4} ip daddr != @dns_never_hijack4 counter redirect to :${TARGET}"
|
||||
p " iifname ${IIF_RENDER} meta l4proto { tcp, udp } th dport 53 ${S6} ip6 daddr != @dns_never_hijack6 counter redirect to :${TARGET}"
|
||||
p "}"
|
||||
p ""
|
||||
fi
|
||||
|
||||
# ---- 层 ②③:853 / DoH 阻断(默认关闭)----
|
||||
if [ "$B853" = "1" ] || [ "$BDOH" = "1" ]; then
|
||||
p "## 层 ②③ 853(DoT) 与 DoH 专用地址阻断(filter forward priority -2,早于 fw4 默认链)"
|
||||
p "chain dns_guard_block {"
|
||||
p " type filter hook forward priority -2; policy accept;"
|
||||
p ""
|
||||
if [ "$B853" = "1" ]; then
|
||||
if [ -n "$SKIP_RAW" ]; then SB="ip saddr != @dns_hijack_skip4"; else SB=""; fi
|
||||
p " ## DoT 阻断:客户端会回落到明文 53,从而被上面的重定向收编"
|
||||
p " iifname ${IIF_RENDER} meta l4proto tcp th dport 853 ${SB} counter reject with tcp reset"
|
||||
p " iifname ${IIF_RENDER} meta l4proto udp th dport 853 ${SB} counter reject"
|
||||
p ""
|
||||
fi
|
||||
if [ "$BDOH" = "1" ] && [ -n "$DOH" ]; then
|
||||
p " ## DoH 专用地址阻断(按 IP 命中,可能误伤「恰好也在这个 IP 上」的其它服务,慎用)"
|
||||
p " iifname ${IIF_RENDER} tcp dport 443 ip daddr @doh_servers counter reject with tcp reset"
|
||||
p ""
|
||||
fi
|
||||
p "}"
|
||||
p ""
|
||||
fi
|
||||
|
||||
# ---- 观察计数(阶段 1 沿用)----
|
||||
if [ "$WATCH" = "1" ]; then
|
||||
p "## 观察计数:只计数 + 抽样记日志,不拦截"
|
||||
p "## 注意:53 被上面重定向后不再进 forward 链,此链的 53 计数会趋近于 0(只剩 853/DoH)"
|
||||
p "chain dns_guard_pre_forward {"
|
||||
p " type filter hook forward priority -1; policy accept;"
|
||||
p ""
|
||||
p " iifname ${IIF_RENDER} oifname \"pppoe-wan\" meta l4proto { tcp, udp } th dport { 53, 853 } counter log prefix \"DNS-BYPASS-53853: \" limit rate ${LOGLIMIT}/minute"
|
||||
[ -n "$DOH" ] && p " iifname ${IIF_RENDER} oifname \"pppoe-wan\" meta l4proto tcp th dport 443 ip daddr @doh_servers counter log prefix \"DNS-BYPASS-DOH: \" limit rate ${LOGLIMIT}/minute"
|
||||
p "}"
|
||||
p ""
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
gen > "$TMP" 2>/dev/null
|
||||
|
||||
if [ ! -s "$TMP" ]; then
|
||||
rm -rf "$TMPD"
|
||||
fail "渲染失败:生成的规则文件为空" 4
|
||||
fi
|
||||
|
||||
# ---------------- 3. 语法校验(临时表包裹)----------------
|
||||
|
||||
validate() {
|
||||
CK="$TMPD/check.nft"
|
||||
{ echo "table inet dgcheck {"; cat "$1"; echo "}"; } > "$CK" 2>/dev/null
|
||||
ERR=$(nft -c -f "$CK" 2>&1)
|
||||
RC=$?
|
||||
rm -f "$CK"
|
||||
if [ "$RC" -ne 0 ]; then
|
||||
printf '%s' "$ERR"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
ERRMSG=$(validate "$TMP")
|
||||
if [ -n "$ERRMSG" ]; then
|
||||
rm -rf "$TMPD"
|
||||
fail "nft 语法校验未通过,已中止(未改动任何文件):$ERRMSG" 5
|
||||
fi
|
||||
|
||||
# ---------------- 4. 仅渲染(界面预览)----------------
|
||||
|
||||
if [ "$RENDER_ONLY" -eq 1 ]; then
|
||||
DEST="${OUTFILE:-/tmp/dg-preview.nft}"
|
||||
cp "$TMP" "$DEST" 2>/dev/null
|
||||
rm -rf "$TMPD"
|
||||
STATUS=ok
|
||||
MSG="已渲染到 $DEST(未应用到防火墙)"
|
||||
FILE_STATE=preview
|
||||
EMIT_JSON
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ---------------- 5. 落盘 + 应用 ----------------
|
||||
|
||||
# 幂等比较:必须忽略会变的「生成时间」行,否则每次都比出差异、白白重载一次防火墙
|
||||
strip_volatile() { sed '/^## 生成时间:/d' "$1"; }
|
||||
|
||||
if [ -f "$OUT" ]; then
|
||||
strip_volatile "$TMP" > "$TMPD/norm.new" 2>/dev/null
|
||||
strip_volatile "$OUT" > "$TMPD/norm.old" 2>/dev/null
|
||||
if cmp -s "$TMPD/norm.new" "$TMPD/norm.old"; then
|
||||
FILE_STATE=unchanged
|
||||
rm -rf "$TMPD"
|
||||
if [ "$REDIRECT" = "1" ]; then
|
||||
nft list chain inet fw4 dns_guard_redirect >/dev/null 2>&1 && REDIRECT_ACTIVE=1
|
||||
fi
|
||||
STATUS=ok
|
||||
MSG="规则无变化(未重新加载防火墙)"
|
||||
EMIT_JSON
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# 备份旧文件
|
||||
OLD="/tmp/dg-old.$$.nft"
|
||||
cp "$OUT" "$OLD" 2>/dev/null
|
||||
if [ -f "$OUT" ]; then
|
||||
BAK_TS=$(date +%Y%m%d-%H%M%S)
|
||||
mkdir -p "$BAKDIR/$BAK_TS" 2>/dev/null
|
||||
cp "$OUT" "$BAKDIR/$BAK_TS/20-dns-guard.nft" 2>/dev/null
|
||||
fi
|
||||
|
||||
cp "$TMP" "$OUT" 2>/dev/null
|
||||
chmod 644 "$OUT" 2>/dev/null
|
||||
FILE_STATE=written
|
||||
|
||||
# fw4 全量校验:不通过就立刻还原
|
||||
if ! fw4 check >/dev/null 2>&1; then
|
||||
CKERR=$(fw4 check 2>&1 | grep -v 'Automatically including' | grep -v 'docker_nat' | head -5)
|
||||
if [ -f "$OLD" ]; then
|
||||
cp "$OLD" "$OUT" 2>/dev/null
|
||||
else
|
||||
rm -f "$OUT"
|
||||
fi
|
||||
rm -f "$OLD"; rm -rf "$TMPD"
|
||||
fail "fw4 全量校验未通过,已还原旧规则:$CKERR" 6
|
||||
fi
|
||||
|
||||
if [ "$NO_RELOAD" -eq 1 ]; then
|
||||
RELOAD_STATE="deferred"
|
||||
rm -f "$OLD"; rm -rf "$TMPD"
|
||||
STATUS=ok
|
||||
MSG="规则已写入 $OUT(未重载,下次防火墙重载生效)"
|
||||
EMIT_JSON
|
||||
exit 0
|
||||
fi
|
||||
|
||||
fw4 reload >/dev/null 2>&1
|
||||
RELOAD_STATE=done
|
||||
|
||||
# 应用后核对:开了重定向就必须真有这条链
|
||||
if [ "$REDIRECT" = "1" ]; then
|
||||
if nft list chain inet fw4 dns_guard_redirect >/dev/null 2>&1; then
|
||||
REDIRECT_ACTIVE=1
|
||||
else
|
||||
if [ -f "$OLD" ]; then
|
||||
cp "$OLD" "$OUT" 2>/dev/null
|
||||
fw4 reload >/dev/null 2>&1
|
||||
fi
|
||||
rm -f "$OLD"; rm -rf "$TMPD"
|
||||
fail "规则已写入但重定向链未生效(已还原旧规则)—— 请检查 /etc/nftables.d 与 fw4 日志" 7
|
||||
fi
|
||||
fi
|
||||
|
||||
rm -f "$OLD"; rm -rf "$TMPD"
|
||||
STATUS=ok
|
||||
MSG="规则已应用并重载防火墙"
|
||||
EMIT_JSON
|
||||
exit 0
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
#!/bin/sh
|
||||
# dnsguard-rollback —— 回滚点管理与自动回滚
|
||||
#
|
||||
# dnsguard-rollback 列出可用回滚点
|
||||
# dnsguard-rollback --arm [分钟] 把"当前 UCI + 当前规则文件"做成回滚点,并挂倒计时自动回滚
|
||||
# dnsguard-rollback --disarm 取消挂起的自动回滚(= 确认保留;不影响已有回滚点)
|
||||
# dnsguard-rollback --status 输出一行 JSON:挂起状态 + 可用回滚点
|
||||
# dnsguard-rollback <时间戳|latest> 还原到指定回滚点(UCI + 规则文件一起还原)并 reload
|
||||
# dnsguard-rollback --auto <ts> <秒> 内部使用:倒计时结束后自动还原
|
||||
#
|
||||
# 为什么要有「自动回滚」:DNS 是全家网的命门,改错了人可能连路由器都访问不到(域名解析不了)。
|
||||
# 挂一个 N 分钟倒计时,人不在旁边确认就自动还原 —— 与"改 LAN IP 前挂回滚定时器"同一套纪律。
|
||||
#
|
||||
# 生成:WorkBuddy 2026-09-26
|
||||
|
||||
set -u
|
||||
|
||||
BAKDIR=/mnt/data/backup-dnsguard
|
||||
OUT=/etc/nftables.d/20-dns-guard.nft
|
||||
REC=/tmp/.dnsguard-rollback
|
||||
LOG_TAG=dnsguard
|
||||
|
||||
snapshot() {
|
||||
# snapshot <时间戳> 把 UCI + 当前规则文件存进回滚点目录
|
||||
_t="$1"
|
||||
mkdir -p "$BAKDIR/$_t" 2>/dev/null
|
||||
[ -f "$OUT" ] && cp "$OUT" "$BAKDIR/$_t/20-dns-guard.nft" 2>/dev/null
|
||||
uci export dnsguard > "$BAKDIR/$_t/dnsguard.uci" 2>/dev/null
|
||||
[ -f "$BAKDIR/$_t/dnsguard.uci" ] && chmod 600 "$BAKDIR/$_t/dnsguard.uci" 2>/dev/null
|
||||
}
|
||||
|
||||
do_restore() {
|
||||
_t="$1"
|
||||
if [ ! -d "$BAKDIR/$_t" ]; then
|
||||
echo "回滚点不存在:$BAKDIR/$_t" >&2
|
||||
return 1
|
||||
fi
|
||||
# 还原 UCI(让界面显示与实际一致)
|
||||
if [ -f "$BAKDIR/$_t/dnsguard.uci" ]; then
|
||||
uci import dnsguard < "$BAKDIR/$_t/dnsguard.uci" 2>/dev/null
|
||||
uci commit dnsguard 2>/dev/null
|
||||
fi
|
||||
# 还原规则文件
|
||||
if [ -f "$BAKDIR/$_t/20-dns-guard.nft" ]; then
|
||||
cp "$BAKDIR/$_t/20-dns-guard.nft" "$OUT" 2>/dev/null
|
||||
else
|
||||
rm -f "$OUT" 2>/dev/null
|
||||
fi
|
||||
fw4 reload >/dev/null 2>&1
|
||||
logger -t "$LOG_TAG" "已回滚到回滚点 $_t(UCI + 规则文件),防火墙已重载"
|
||||
echo "已回滚到 $_t"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---------------- 参数分发 ----------------
|
||||
|
||||
case "${1:-}" in
|
||||
"")
|
||||
echo "可用回滚点(新 → 旧):"
|
||||
ls -1t "$BAKDIR" 2>/dev/null | head -20
|
||||
;;
|
||||
|
||||
--status)
|
||||
PEND=""
|
||||
if [ -f "$REC" ]; then
|
||||
read -r RTS RSEC RDL RSRC < "$REC" 2>/dev/null
|
||||
NOW=$(date +%s)
|
||||
LEFT=$(( ${RDL:-0} - NOW ))
|
||||
[ "$LEFT" -lt 0 ] && LEFT=0
|
||||
PEND="\"pending\":true,\"pending_ts\":\"${RTS:-}\",\"pending_left\":${LEFT},\"pending_total\":${RSEC:-0}"
|
||||
else
|
||||
PEND='"pending":false,"pending_ts":"","pending_left":0,"pending_total":0'
|
||||
fi
|
||||
LIST=""
|
||||
for d in $(ls -1t "$BAKDIR" 2>/dev/null | head -10); do
|
||||
[ -n "$LIST" ] && LIST="$LIST,"
|
||||
LIST="$LIST\"$d\""
|
||||
done
|
||||
printf '{"status":"ok",%s,"backups":[%s]}\n' "$PEND" "$LIST"
|
||||
;;
|
||||
|
||||
--arm)
|
||||
SEC=$(printf '%s' "${2:-10}" | tr -dc '0-9'); [ -n "$SEC" ] || SEC=10
|
||||
# 秒数上限保护:1 分钟 ~ 120 分钟
|
||||
[ "$SEC" -lt 1 ] && SEC=1
|
||||
[ "$SEC" -gt 120 ] && SEC=120
|
||||
TS=$(date +%Y%m%d-%H%M%S)
|
||||
snapshot "$TS"
|
||||
DL=$(( $(date +%s) + SEC * 60 ))
|
||||
printf '%s %s %s %s\n' "$TS" "$((SEC * 60))" "$DL" "pending" > "$REC"
|
||||
setsid /bin/sh -c "/usr/bin/dnsguard-rollback --auto '$TS' '$((SEC * 60))'" >/dev/null 2>&1 &
|
||||
logger -t "$LOG_TAG" "已挂自动回滚:$SEC 分钟后(若无确认)还原到回滚点 $TS"
|
||||
printf '{"status":"ok","ts":"%s","seconds":%s}\n' "$TS" "$((SEC * 60))"
|
||||
;;
|
||||
|
||||
--disarm)
|
||||
if [ -f "$REC" ]; then
|
||||
read -r RTS RSEC RDL RSRC < "$REC" 2>/dev/null
|
||||
rm -f "$REC"
|
||||
# 尽力杀掉倒计时进程;即使没杀干净,--auto 也会因为记录文件不存在而放弃
|
||||
for pid in $(ps w 2>/dev/null | grep '[d]nsguard-rollback --auto' | awk '{print $1}'); do
|
||||
kill "$pid" 2>/dev/null
|
||||
done
|
||||
logger -t "$LOG_TAG" "已取消自动回滚(确认保留当前设置),回滚点 $RTS 仍保留"
|
||||
printf '{"status":"ok","message":"已取消自动回滚,当前设置被保留","ts":"%s"}\n' "$RTS"
|
||||
else
|
||||
printf '{"status":"ok","message":"没有挂起的自动回滚"}\n'
|
||||
fi
|
||||
;;
|
||||
|
||||
--auto)
|
||||
TS="${2:-}"
|
||||
SEC="${3:-600}"
|
||||
sleep "$SEC" 2>/dev/null
|
||||
# 记录文件是唯一真相:被 --disarm 删掉、或被后来的 --arm 覆盖,就什么都不做
|
||||
[ -f "$REC" ] || exit 0
|
||||
read -r RTS RSEC RDL RSRC < "$REC" 2>/dev/null
|
||||
[ "${RTS:-}" = "$TS" ] || exit 0
|
||||
rm -f "$REC"
|
||||
logger -t "$LOG_TAG" "自动回滚触发:$((SEC / 60)) 分钟内未确认,正在还原到 $TS"
|
||||
do_restore "$TS" >/dev/null 2>&1
|
||||
exit 0
|
||||
;;
|
||||
|
||||
*)
|
||||
TS="$1"
|
||||
if [ "$TS" = "latest" ]; then
|
||||
TS=$(ls -1t "$BAKDIR" 2>/dev/null | head -1)
|
||||
fi
|
||||
[ -n "$TS" ] || { echo "找不到回滚点" >&2; exit 1; }
|
||||
do_restore "$TS"
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 0
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
#!/bin/sh
|
||||
# dnsguard-selftest —— DNS 防绕过健康自检,输出一行 JSON(供 WebUI 渲染)
|
||||
#
|
||||
# 用法:dnsguard-selftest
|
||||
# 只读:不修改任何配置、不重启任何服务
|
||||
#
|
||||
# 生成:WorkBuddy 2026-09-26
|
||||
|
||||
set -u
|
||||
|
||||
jesc() {
|
||||
printf '%s' "${1:-}" | tr '\n' '\001' | sed 's/\\/\\\\/g; s/"/\\"/g' | tr '\001' '\n' | awk '{printf "%s\\n", $0}' | sed 's/\\n$//'
|
||||
}
|
||||
|
||||
ITEMS=""
|
||||
add() {
|
||||
# add <名称> <ok|warn|fail> <说明>
|
||||
_n="$1"; _s="$2"; _d="$3"
|
||||
[ -n "$ITEMS" ] && ITEMS="$ITEMS,"
|
||||
ITEMS="$ITEMS{\"name\":\"$(jesc "$_n")\",\"state\":\"$_s\",\"detail\":\"$(jesc "$_d")\"}"
|
||||
}
|
||||
|
||||
MODE="none"
|
||||
|
||||
# ---- 1. AdGuard Home 进程 ----
|
||||
AGH_PID=$(pidof AdGuardHome 2>/dev/null)
|
||||
if [ -n "$AGH_PID" ]; then
|
||||
add "AdGuard Home 进程" ok "运行中,PID $AGH_PID"
|
||||
AGH_OK=1
|
||||
else
|
||||
add "AdGuard Home 进程" fail "未运行 —— 此时若已启用 53 重定向,全屋解析会失败"
|
||||
AGH_OK=0
|
||||
fi
|
||||
|
||||
# ---- 2. :53 监听 ----
|
||||
LISTEN=$(netstat -lntup 2>/dev/null | grep ':53 ' | tr -s ' ' ' ')
|
||||
if [ -n "$LISTEN" ]; then
|
||||
add ":53 监听(TCP/UDP)" ok "$(printf '%s' "$LISTEN" | sed 's/ *$//' | tr '\n' ';')"
|
||||
LISTEN_OK=1
|
||||
else
|
||||
add ":53 监听(TCP/UDP)" fail "没有任何进程监听 :53"
|
||||
LISTEN_OK=0
|
||||
fi
|
||||
|
||||
# ---- 3. 本机解析(走 AGH)----
|
||||
if [ "$AGH_OK" = 1 ]; then
|
||||
OUT1=$(nslookup baidu.com 127.0.0.1 2>&1)
|
||||
N1=$(printf '%s' "$OUT1" | grep -c 'Address')
|
||||
if [ "${N1:-0}" -ge 2 ]; then
|
||||
IP1=$(printf '%s' "$OUT1" | grep 'Address' | tail -1 | awk '{print $NF}')
|
||||
add "公网域名解析(baidu.com → 127.0.0.1)" ok "成功,返回 $IP1"
|
||||
else
|
||||
add "公网域名解析(baidu.com → 127.0.0.1)" fail "$(printf '%s' "$OUT1" | tr '\n' ' ' | sed 's/ */ /g' | cut -c1-160)"
|
||||
fi
|
||||
|
||||
OUT2=$(nslookup iStoreOS.lan 127.0.0.1 2>&1)
|
||||
N2=$(printf '%s' "$OUT2" | grep -c 'Address')
|
||||
if [ "${N2:-0}" -ge 2 ]; then
|
||||
IP2=$(printf '%s' "$OUT2" | grep 'Address' | tail -1 | awk '{print $NF}')
|
||||
add "本地域名解析(iStoreOS.lan → 127.0.0.1)" ok "成功,返回 $IP2"
|
||||
else
|
||||
add "本地域名解析(iStoreOS.lan → 127.0.0.1)" warn "未返回地址(dnsmasq 5353 或 .lan 转发可能异常)"
|
||||
fi
|
||||
else
|
||||
add "本机解析自检" warn "AdGuard Home 未运行,跳过解析测试"
|
||||
fi
|
||||
|
||||
# ---- 4. 重定向链是否生效 ----
|
||||
RED=$(nft list chain inet fw4 dns_guard_redirect 2>/dev/null)
|
||||
if printf '%s' "$RED" | grep -q 'chain dns_guard_redirect'; then
|
||||
MODE=redirect
|
||||
RP=$(printf '%s' "$RED" | grep -o 'counter packets [0-9]*' | head -1 | awk '{print $3}')
|
||||
RB=$(printf '%s' "$RED" | grep -o 'bytes [0-9]*' | awk '{s+=$2} END {print s+0}')
|
||||
add "53 透明重定向链" ok "已生效 · 已收编 ${RP:-0} 个包 / ${RB:-0} 字节(自上次重载起)"
|
||||
if [ "${RP:-0}" -gt 0 ]; then
|
||||
add "端到端收编验证" ok "有 LAN 客户端的 53 查询被改道到本机(计数 > 0)"
|
||||
else
|
||||
add "端到端收编验证" warn "计数为 0:可能是刚重载,或当前没有设备使用非本机 DNS"
|
||||
fi
|
||||
else
|
||||
add "53 透明重定向链" warn "不存在(未启用,或总开关关闭)"
|
||||
fi
|
||||
|
||||
# ---- 5. 观察计数链 ----
|
||||
OBS=$(nft list chain inet fw4 dns_guard_pre_forward 2>/dev/null)
|
||||
if printf '%s' "$OBS" | grep -q 'chain dns_guard_pre_forward'; then
|
||||
CNT=$(printf '%s' "$OBS" | grep -o 'packets [0-9]*' | awk '{print $2}' | tr '\n' ' ')
|
||||
CNT=$(printf '%s' "$CNT" | sed 's/ *$//; s/ */ \/ /g')
|
||||
add "观察计数链" ok "存在 · 各规则包数 ${CNT:-0}"
|
||||
else
|
||||
add "观察计数链" warn "不存在(观察模式未启用)"
|
||||
fi
|
||||
|
||||
# ---- 6. 规则文件归属 ----
|
||||
FN=/etc/nftables.d/20-dns-guard.nft
|
||||
if [ -f "$FN" ]; then
|
||||
MT=$(stat -c '%y' "$FN" 2>/dev/null | cut -c1-19)
|
||||
if grep -q 'DNS 防绕过' "$FN" 2>/dev/null; then
|
||||
add "规则文件" ok "$FN · 由本应用托管 · 最后生成 $MT"
|
||||
else
|
||||
add "规则文件" warn "$FN 存在但不是本应用生成的(手工文件)—— 点「保存并应用」会被接管"
|
||||
fi
|
||||
else
|
||||
add "规则文件" warn "$FN 不存在(总开关关闭或从未应用过)"
|
||||
fi
|
||||
|
||||
# ---- 7. 例外设备 ----
|
||||
SKIP=$(uci -q get dnsguard.main.skip_devices)
|
||||
TMODE=$(uci -q get dnsguard.main.redirect53)
|
||||
if [ "${TMODE:-0}" = "1" ]; then
|
||||
if [ -n "$SKIP" ]; then
|
||||
add "重定向例外设备" ok "已配置:$SKIP"
|
||||
else
|
||||
add "重定向例外设备" ok "无(所有 LAN 客户端的明文 DNS 都会被收编)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---- 8. 挂起的自动回滚 ----
|
||||
RF=/tmp/.dnsguard-rollback
|
||||
if [ -f "$RF" ]; then
|
||||
read -r RTS RSEC RDL RSRC < "$RF" 2>/dev/null
|
||||
NOW=$(date +%s)
|
||||
LEFT=$(( ${RDL:-0} - NOW ))
|
||||
[ "$LEFT" -lt 0 ] && LEFT=0
|
||||
add "自动回滚倒计时" warn "挂起中:剩余约 ${LEFT} 秒后自动还原到快照 $RTS(点「确认保留」可取消)"
|
||||
else
|
||||
add "自动回滚倒计时" ok "无挂起"
|
||||
fi
|
||||
|
||||
printf '{"status":"ok","mode":"%s","items":[%s]}\n' "$MODE" "$ITEMS"
|
||||
exit 0
|
||||
Reference in new issue
Block a user