mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-05 01:44:16 +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
@@ -0,0 +1,358 @@
|
||||
-- dnsguard.lua —— LuCI 控制器:DNS 防绕过(53 透明重定向 / 观察计数 / 853·DoH 阻断 / 健康自检 / 回滚)
|
||||
--
|
||||
-- 实现方式与同机第三方应用 luci-app-lmclient、以及本机 luci-app-routerreport 一致:
|
||||
-- 控制器只做「校验参数 -> uci set/commit -> 调本机脚本」,真正写防火墙的动作全在 dnsguard-apply 里。
|
||||
-- 生成:WorkBuddy 2026-09-26
|
||||
|
||||
module("luci.controller.dnsguard", package.seeall)
|
||||
|
||||
local sys = require "luci.sys"
|
||||
local http = require "luci.http"
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
|
||||
local APPLY = "/usr/bin/dnsguard-apply"
|
||||
local SELFTEST = "/usr/bin/dnsguard-selftest"
|
||||
local ROLLBACK = "/usr/bin/dnsguard-rollback"
|
||||
local NFTPREV = "/tmp/dg-preview.nft"
|
||||
local LOGFILE = "/mnt/data/logs/messages"
|
||||
local NFTOUT = "/etc/nftables.d/20-dns-guard.nft"
|
||||
|
||||
-- ---------- 小工具 ----------
|
||||
|
||||
local function jesc(s)
|
||||
s = tostring(s or "")
|
||||
s = s:gsub("\\", "\\\\"):gsub('"', '\\"'):gsub("\r", "")
|
||||
s = s:gsub("\n", "\\n"):gsub("\t", " ")
|
||||
return s
|
||||
end
|
||||
|
||||
local function jstr(s) return '"' .. jesc(s) .. '"' end
|
||||
|
||||
local function num(s) return tonumber(s or "") or 0 end
|
||||
|
||||
local function exec(cmd)
|
||||
local out = sys.exec(cmd .. " 2>/dev/null")
|
||||
return out or ""
|
||||
end
|
||||
|
||||
local function trim(s) return (tostring(s or ""):gsub("^%s+", ""):gsub("%s+$", "")) end
|
||||
|
||||
local function json_out(s)
|
||||
http.prepare_content("application/json")
|
||||
http.write(s)
|
||||
return
|
||||
end
|
||||
|
||||
local function fv(name, maxlen)
|
||||
local v = http.formvalue(name)
|
||||
if v == nil then return nil end
|
||||
v = v:gsub("[\r\n\t]", " ")
|
||||
if maxlen and #v > maxlen then v = v:sub(1, maxlen) end
|
||||
return v
|
||||
end
|
||||
|
||||
-- 只保留 IP/CIDR 里合法出现的字符(写入 nft 前的第一道清洗;dnsguard-apply 里还有第二道)
|
||||
-- ⚠️ 白名单必须含 `/`,否则 CIDR 会被洗成 192.168.1.024 这种怪物(联调时真踩到过)
|
||||
local function clean_set(s)
|
||||
s = tostring(s or "")
|
||||
return trim(s:gsub("[^%w%.%:%-/%s%,]", ""))
|
||||
end
|
||||
|
||||
-- 接口名:字母数字点下划线连字符 + 空格
|
||||
local function clean_ifaces(s)
|
||||
s = tostring(s or "")
|
||||
return trim(s:gsub("[^%w%._%-%s]", ""))
|
||||
end
|
||||
|
||||
local function digits(s)
|
||||
s = tostring(s or "")
|
||||
return s:gsub("[^%d]", "")
|
||||
end
|
||||
|
||||
-- ---------- 菜单 ----------
|
||||
|
||||
function index()
|
||||
entry({ "admin", "services", "dnsguard" },
|
||||
template("dnsguard/main"), _("DNS 防绕过"), 82).acl_depends = { "luci-app-dnsguard" }
|
||||
entry({ "admin", "services", "dnsguard", "api" },
|
||||
call("action_api"), nil).acl_depends = { "luci-app-dnsguard" }
|
||||
end
|
||||
|
||||
-- ---------- 动作分发 ----------
|
||||
|
||||
function action_api()
|
||||
local action = fv("action") or ""
|
||||
if not action:match("^[%w_]+$") then
|
||||
return json_out('{"status":"error","message":"invalid action"}')
|
||||
end
|
||||
if action == "save" then
|
||||
return action_save()
|
||||
elseif action == "apply" then
|
||||
return action_apply()
|
||||
elseif action == "status" then
|
||||
return action_status()
|
||||
elseif action == "selftest" then
|
||||
return action_selftest()
|
||||
elseif action == "rules" then
|
||||
return action_rules()
|
||||
elseif action == "disarm" then
|
||||
return action_rollback_cmd("--disarm", "")
|
||||
elseif action == "rollback" then
|
||||
return action_restore()
|
||||
end
|
||||
return json_out('{"status":"error","message":"unknown action"}')
|
||||
end
|
||||
|
||||
-- ---------- 保存配置 ----------
|
||||
|
||||
function action_save()
|
||||
local function flag(n) return (fv(n) == "1") and "1" or "0" end
|
||||
|
||||
local new_enabled = flag("enabled")
|
||||
local new_redirect = flag("redirect53")
|
||||
local watch = flag("watch")
|
||||
local block853 = flag("block853")
|
||||
local blockdoh = flag("blockdoh")
|
||||
|
||||
local target_port = digits(fv("target_port", 6))
|
||||
local log_limit = digits(fv("log_limit", 4))
|
||||
local roll_min = digits(fv("rollback_minutes", 4))
|
||||
local ifaces = clean_ifaces(fv("ifaces", 200))
|
||||
local skip = clean_set(fv("skip_devices", 400))
|
||||
local nh4 = clean_set(fv("never_hijack4", 600))
|
||||
local nh6 = clean_set(fv("never_hijack6", 600))
|
||||
local doh = clean_set(fv("doh_servers", 900))
|
||||
|
||||
-- 校验
|
||||
if target_port == "" or num(target_port) < 1 or num(target_port) > 65535 then
|
||||
return json_out('{"status":"error","message":"重定向目标端口必须是 1-65535 的数字"}')
|
||||
end
|
||||
if ifaces == "" then
|
||||
return json_out('{"status":"error","message":"入接口不能为空(默认 br-lan)"}')
|
||||
end
|
||||
if log_limit == "" then log_limit = "5" end
|
||||
if num(log_limit) < 1 or num(log_limit) > 999 then
|
||||
return json_out('{"status":"error","message":"日志限速必须是 1-999(每分钟条数)"}')
|
||||
end
|
||||
if roll_min == "" then roll_min = "10" end
|
||||
if num(roll_min) < 1 or num(roll_min) > 120 then
|
||||
return json_out('{"status":"error","message":"自动回滚倒计时必须是 1-120 分钟"}')
|
||||
end
|
||||
|
||||
local c = uci
|
||||
local OLD_REDIRECT = c:get("dnsguard", "main", "redirect53") or "0"
|
||||
|
||||
c:set("dnsguard", "main", "enabled", new_enabled)
|
||||
c:set("dnsguard", "main", "redirect53", new_redirect)
|
||||
c:set("dnsguard", "main", "watch", watch)
|
||||
c:set("dnsguard", "main", "block853", block853)
|
||||
c:set("dnsguard", "main", "blockdoh", blockdoh)
|
||||
c:set("dnsguard", "main", "target_port", target_port)
|
||||
c:set("dnsguard", "main", "ifaces", ifaces)
|
||||
c:set("dnsguard", "main", "skip_devices", skip)
|
||||
c:set("dnsguard", "main", "log_limit", log_limit)
|
||||
c:set("dnsguard", "main", "rollback_minutes", roll_min)
|
||||
|
||||
c:set("dnsguard", "limits", "never_hijack4", nh4)
|
||||
c:set("dnsguard", "limits", "never_hijack6", nh6)
|
||||
c:set("dnsguard", "limits", "doh_servers", doh)
|
||||
c:commit("dnsguard")
|
||||
|
||||
-- 关 -> 开 的那一刻,自动挂一个回滚倒计时(DNS 改错会让全家上不了网,留条后路)
|
||||
local armed = ""
|
||||
if new_enabled == "1" and new_redirect == "1" and OLD_REDIRECT ~= "1" then
|
||||
local r = exec(ROLLBACK .. " --arm " .. roll_min)
|
||||
if r:match('"ts":"([%d%-]+)"') then
|
||||
armed = r:match('"ts":"([%d%-]+)"')
|
||||
end
|
||||
end
|
||||
|
||||
-- 渲染并应用
|
||||
local out = trim(exec(APPLY))
|
||||
local j = out:match("({.*})")
|
||||
if not j then
|
||||
return json_out('{"status":"error","message":"配置已保存,但应用失败:' .. jesc(out) .. '"}')
|
||||
end
|
||||
|
||||
local st = j:match('"status":"([%w]+)"') or "error"
|
||||
local msg = j:match('"message":"(.-)","enabled"') or ""
|
||||
|
||||
if st ~= "ok" then
|
||||
return json_out('{"status":"error","message":' .. jstr(msg) .. ',"apply":' .. jstr(j) .. '}')
|
||||
end
|
||||
|
||||
local red_ok = (j:match('"redirect_active":"(%d)"') == "1")
|
||||
local file_st = j:match('"file":"([%w]+)"') or ""
|
||||
|
||||
local head = "配置已保存"
|
||||
if file_st == "unchanged" then
|
||||
head = head .. "(规则无变化,未重载防火墙)"
|
||||
else
|
||||
head = head .. "并生效"
|
||||
end
|
||||
head = head .. " · 53 重定向:" .. ((new_enabled == "1" and new_redirect == "1") and (red_ok and "已生效" or "未生效") or "关闭")
|
||||
|
||||
if armed ~= "" then
|
||||
head = head .. " · 已挂 " .. roll_min .. " 分钟自动回滚(快照 " .. armed .. "),未确认将自动还原"
|
||||
end
|
||||
|
||||
return json_out('{"status":"ok","message":' .. jstr(head) ..
|
||||
',"at":' .. jstr(os.date("%H:%M:%S")) ..
|
||||
',"armed":' .. jstr(armed) ..
|
||||
',"redirect_active":' .. (red_ok and "true" or "false") ..
|
||||
',"apply":' .. jstr(j) .. '}')
|
||||
end
|
||||
|
||||
-- ---------- 只重新应用(不改配置)----------
|
||||
|
||||
function action_apply()
|
||||
local out = trim(exec(APPLY .. " --quiet"))
|
||||
local j = out:match("({.*})")
|
||||
if not j then
|
||||
return json_out('{"status":"error","message":"应用失败:' .. jesc(out) .. '"}')
|
||||
end
|
||||
local st = j:match('"status":"([%w]+)"') or "error"
|
||||
local msg = j:match('"message":"(([^"\\]|\\.)*)"') or ""
|
||||
if st ~= "ok" then
|
||||
return json_out('{"status":"error","message":' .. jstr(msg) .. ',"apply":' .. jstr(j) .. '}')
|
||||
end
|
||||
return json_out('{"status":"ok","message":' .. jstr(msg) ..
|
||||
',"at":' .. jstr(os.date("%H:%M:%S")) .. ',"apply":' .. jstr(j) .. '}')
|
||||
end
|
||||
|
||||
-- ---------- 状态 ----------
|
||||
|
||||
local function nft_chain_summary(name)
|
||||
local t = exec("nft list chain inet fw4 " .. name)
|
||||
local active = t:find("chain " .. name) ~= nil
|
||||
local pk, by, list = 0, 0, {}
|
||||
for a, b in t:gmatch("counter packets (%d+) bytes (%d+)") do
|
||||
pk = pk + num(a); by = by + num(b)
|
||||
list[#list + 1] = '{"packets":' .. num(a) .. ',"bytes":' .. num(b) .. '}'
|
||||
end
|
||||
return active, pk, by, "[" .. table.concat(list, ",") .. "]"
|
||||
end
|
||||
|
||||
function action_status()
|
||||
local c = uci
|
||||
local function g(sec, opt, def)
|
||||
local v = c:get("dnsguard", sec, opt)
|
||||
if v == nil or v == "" then return def or "" end
|
||||
return v
|
||||
end
|
||||
|
||||
local agh = trim(exec("pidof AdGuardHome"))
|
||||
local listen = trim(exec("netstat -lntup 2>/dev/null | grep ':53 ' | head -2 | tr -s ' ' ' '"))
|
||||
|
||||
local r_active, r_pk, r_by, r_list = nft_chain_summary("dns_guard_redirect")
|
||||
local o_active, o_pk, o_by, o_list = nft_chain_summary("dns_guard_pre_forward")
|
||||
local b_active = nft_chain_summary("dns_guard_block")
|
||||
|
||||
-- 规则文件是否由本应用托管
|
||||
local managed = "0"
|
||||
local mtime = ""
|
||||
local fh = io.open(NFTOUT, "r")
|
||||
if fh then
|
||||
local body = fh:read("*a") or ""
|
||||
fh:close()
|
||||
if body:find("DNS 防绕过", 1, true) then managed = "1" end
|
||||
end
|
||||
mtime = trim(exec("stat -c '%y' " .. NFTOUT .. " 2>/dev/null | cut -c1-19"))
|
||||
|
||||
-- 观察日志条数
|
||||
local logs = trim(exec("grep -c 'DNS-BYPASS' " .. LOGFILE))
|
||||
|
||||
-- 回滚状态
|
||||
local rb = exec(ROLLBACK .. " --status")
|
||||
local pending = (rb:find('"pending":true', 1, true) ~= nil)
|
||||
local pts = rb:match('"pending_ts":"([%d%-]*)"') or ""
|
||||
local pleft = num(rb:match('"pending_left":(%d+)'))
|
||||
local backups = rb:match('"backups":%[([^%]]*)%]') or ""
|
||||
|
||||
local resp = {
|
||||
'{"status":"ok"',
|
||||
'"enabled":' .. jstr(g("main", "enabled", "0")),
|
||||
'"redirect53":' .. jstr(g("main", "redirect53", "0")),
|
||||
'"watch":' .. jstr(g("main", "watch", "0")),
|
||||
'"block853":' .. jstr(g("main", "block853", "0")),
|
||||
'"blockdoh":' .. jstr(g("main", "blockdoh", "0")),
|
||||
'"target_port":' .. jstr(g("main", "target_port", "53")),
|
||||
'"ifaces":' .. jstr(g("main", "ifaces", "br-lan")),
|
||||
'"skip_devices":' .. jstr(g("main", "skip_devices", "")),
|
||||
'"log_limit":' .. jstr(g("main", "log_limit", "5")),
|
||||
'"rollback_minutes":' .. jstr(g("main", "rollback_minutes", "10")),
|
||||
'"never_hijack4":' .. jstr(g("limits", "never_hijack4", "")),
|
||||
'"never_hijack6":' .. jstr(g("limits", "never_hijack6", "")),
|
||||
'"doh_servers":' .. jstr(g("limits", "doh_servers", "")),
|
||||
'"agh_pid":' .. jstr(agh),
|
||||
'"listen53":' .. jstr(listen),
|
||||
'"redirect_active":' .. (r_active and "true" or "false"),
|
||||
'"redirect_packets":' .. r_pk,
|
||||
'"redirect_bytes":' .. r_by,
|
||||
'"redirect_rules":' .. r_list,
|
||||
'"watch_active":' .. (o_active and "true" or "false"),
|
||||
'"watch_packets":' .. o_pk,
|
||||
'"watch_bytes":' .. o_by,
|
||||
'"watch_rules":' .. o_list,
|
||||
'"block_active":' .. (b_active and "true" or "false"),
|
||||
'"managed":' .. jstr(managed),
|
||||
'"file_mtime":' .. jstr(mtime),
|
||||
'"bypass_lines":' .. jstr(logs),
|
||||
'"pending":' .. (pending and "true" or "false"),
|
||||
'"pending_ts":' .. jstr(pts),
|
||||
'"pending_left":' .. pleft,
|
||||
'"backups":[' .. backups .. ']'
|
||||
}
|
||||
return json_out(table.concat(resp, ",") .. "}")
|
||||
end
|
||||
|
||||
-- ---------- 健康自检 ----------
|
||||
|
||||
function action_selftest()
|
||||
local out = trim(exec(SELFTEST))
|
||||
local j = out:match("({.*})")
|
||||
if not j then
|
||||
return json_out('{"status":"error","message":"自检脚本执行失败","raw":' .. jstr(out) .. '}')
|
||||
end
|
||||
return json_out(j)
|
||||
end
|
||||
|
||||
-- ---------- 预览将要生成的规则 ----------
|
||||
|
||||
function action_rules()
|
||||
exec(APPLY .. " --render-only --out " .. NFTPREV .. " --quiet")
|
||||
local fh = io.open(NFTPREV, "r")
|
||||
local text = ""
|
||||
if fh then
|
||||
text = fh:read("*a") or ""
|
||||
fh:close()
|
||||
end
|
||||
if text == "" then
|
||||
return json_out('{"status":"error","message":"渲染预览失败:未取到内容(请查看 dnsguard-apply 是否可执行)"}')
|
||||
end
|
||||
local n = select(2, text:gsub("\n", "\n"))
|
||||
return json_out('{"status":"ok","lines":' .. n .. ',"text":' .. jstr(text) .. '}')
|
||||
end
|
||||
|
||||
-- ---------- 取消自动回滚(确认保留)----------
|
||||
|
||||
function action_rollback_cmd(opt, extra)
|
||||
local out = trim(exec(ROLLBACK .. " " .. opt .. " " .. extra))
|
||||
local j = out:match("({.*})")
|
||||
if not j then
|
||||
return json_out('{"status":"error","message":' .. jstr(out) .. '}')
|
||||
end
|
||||
return json_out(j)
|
||||
end
|
||||
|
||||
-- ---------- 还原到指定回滚点 ----------
|
||||
|
||||
function action_restore()
|
||||
local ts = fv("ts", 40) or ""
|
||||
if ts ~= "latest" and not ts:match("^[%d%-]+$") then
|
||||
return json_out('{"status":"error","message":"时间戳格式不正确"}')
|
||||
end
|
||||
local out = trim(exec(ROLLBACK .. " " .. ts))
|
||||
return json_out('{"status":"ok","message":' .. jstr(out ~= "" and out or ("已回滚到 " .. ts)) ..
|
||||
',"at":' .. jstr(os.date("%H:%M:%S")) .. '}')
|
||||
end
|
||||
@@ -0,0 +1,524 @@
|
||||
<%+header%>
|
||||
<%
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
|
||||
local function g(sec, opt, def)
|
||||
local v = uci:get("dnsguard", sec, opt)
|
||||
if v == nil or v == "" then return def or "" end
|
||||
return v
|
||||
end
|
||||
|
||||
local function e(s)
|
||||
s = tostring(s or "")
|
||||
return (s:gsub("&", "&"):gsub("<", "<"):gsub(">", ">"):gsub('"', """))
|
||||
end
|
||||
|
||||
local function on(sec, opt)
|
||||
return g(sec, opt, "0") == "1"
|
||||
end
|
||||
|
||||
-- 取 API 地址:优先 build_url,拿不到就退回固定路径(保证页面不白屏)
|
||||
local api_url = "/cgi-bin/luci/admin/services/dnsguard/api"
|
||||
local ok_disp, disp = pcall(require, "luci.dispatcher")
|
||||
if ok_disp and disp and disp.build_url then
|
||||
local ok1, u1 = pcall(disp.build_url, "admin", "services", "dnsguard", "api")
|
||||
if ok1 and u1 then api_url = u1 end
|
||||
end
|
||||
|
||||
local cfg_enabled = on("main", "enabled")
|
||||
local cfg_redirect = on("main", "redirect53")
|
||||
local cfg_watch = on("main", "watch")
|
||||
local cfg_853 = on("main", "block853")
|
||||
local cfg_doh = on("main", "blockdoh")
|
||||
local cfg_port = g("main", "target_port", "53")
|
||||
local cfg_ifaces = g("main", "ifaces", "br-lan")
|
||||
local cfg_skip = g("main", "skip_devices", "")
|
||||
local cfg_loglimit = g("main", "log_limit", "5")
|
||||
local cfg_rollmin = g("main", "rollback_minutes", "10")
|
||||
local cfg_nh4 = g("limits", "never_hijack4", "")
|
||||
local cfg_nh6 = g("limits", "never_hijack6", "")
|
||||
local cfg_dohsrv = g("limits", "doh_servers", "")
|
||||
%>
|
||||
|
||||
<style>
|
||||
/* 主题里 .cbi-section-node 横向 padding 为 0,自定义块必须自己补,否则会顶到左边 */
|
||||
.dg-pad { padding: 0 1rem .5rem 1rem; }
|
||||
.dg-table { width: 100%; max-width: 720px; border-collapse: collapse; font-size: 13px; }
|
||||
.dg-table td { padding: .45rem 0; border-bottom: 1px solid #eceef1; vertical-align: top; }
|
||||
.dg-table tr:last-child td { border-bottom: 0; }
|
||||
.dg-table .dg-k { width: 11rem; color: #57606a; }
|
||||
.dg-mono { font-family: ui-monospace, Consolas, monospace; font-size: 12px; }
|
||||
.dg-ok { color: #0f6e56; font-weight: 600; }
|
||||
.dg-bad { color: #a32d2d; font-weight: 600; }
|
||||
.dg-warn{ color: #8a5a00; font-weight: 600; }
|
||||
.dg-banner { display:none; padding:.6rem .75rem; border-radius:.375rem; margin-bottom:.75rem; font-size:13px; line-height:1.7; white-space:pre-wrap; }
|
||||
.dg-pre { max-height:300px; overflow:auto; background:#f6f8fa; border-radius:.375rem; padding:.625rem; font-size:12px; line-height:1.55; color:#333; margin:.5rem 0 0; }
|
||||
</style>
|
||||
|
||||
<div class="cbi-map">
|
||||
<h2 name="content">DNS 防绕过</h2>
|
||||
<div class="cbi-map-descr">
|
||||
把局域网内「绕过 AdGuard Home 的 DNS」拉回本机解析:<strong>53 明文</strong>用透明重定向(客户端无感)、
|
||||
<strong>853 DoT</strong> 用阻断(逼其回落 53)、<strong>DoH 专用 IP</strong> 可选阻断。
|
||||
配置存于 <code>/etc/config/dnsguard</code>,保存后渲染到 <code>/etc/nftables.d/20-dns-guard.nft</code> 并重载 fw4。
|
||||
</div>
|
||||
|
||||
<div class="cbi-section">
|
||||
<h3>当前状态</h3>
|
||||
<div class="cbi-section-node">
|
||||
<div class="dg-pad">
|
||||
<div id="dg-banner" class="dg-banner"></div>
|
||||
<table class="dg-table">
|
||||
<tr><td class="dg-k">总开关</td><td id="st-enabled">读取中…</td></tr>
|
||||
<tr><td class="dg-k">AdGuard Home</td><td id="st-agh">—</td></tr>
|
||||
<tr><td class="dg-k">:53 监听</td><td id="st-listen" class="dg-mono">—</td></tr>
|
||||
<tr><td class="dg-k">53 透明重定向</td><td id="st-redirect">—</td></tr>
|
||||
<tr><td class="dg-k">已收编包数</td><td id="st-redpk">—</td></tr>
|
||||
<tr><td class="dg-k">观察计数(forward)</td><td id="st-watch">—</td></tr>
|
||||
<tr><td class="dg-k">规则文件</td><td id="st-file">—</td></tr>
|
||||
<tr><td class="dg-k">观察期累计日志</td><td id="st-logs">—</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="dg-config" onsubmit="return false">
|
||||
<div class="cbi-section">
|
||||
<h3>开关</h3>
|
||||
<div class="cbi-section-node">
|
||||
<div class="cbi-value">
|
||||
<label class="cbi-value-title" for="dg-enabled">启用 DNS 防绕过</label>
|
||||
<div class="cbi-value-field">
|
||||
<input type="checkbox" id="dg-enabled"<% if cfg_enabled then %> checked="checked"<% end %> />
|
||||
<div class="cbi-value-description">总开关。关闭后不生成任何规则(等于恢复出厂状态)。</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cbi-value">
|
||||
<label class="cbi-value-title" for="dg-redirect">53 透明重定向</label>
|
||||
<div class="cbi-value-field">
|
||||
<input type="checkbox" id="dg-redirect"<% if cfg_redirect then %> checked="checked"<% end %> />
|
||||
<div class="cbi-value-description">
|
||||
把 LAN 内发往外部 <code>:53</code> 的 TCP/UDP 查询改道到本机 AdGuard Home(nat prerouting,优先级 -105)。
|
||||
客户端<strong>无感</strong>,硬编码 DNS 的设备不用改设置。<br />
|
||||
⚠️ 只在 AdGuard Home 正常运行时才有意义:<strong>AGH 挂了就会被安全闸门拒绝应用</strong>。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cbi-value">
|
||||
<label class="cbi-value-title" for="dg-watch">观察计数</label>
|
||||
<div class="cbi-value-field">
|
||||
<input type="checkbox" id="dg-watch"<% if cfg_watch then %> checked="checked"<% end %> />
|
||||
<div class="cbi-value-description">
|
||||
只计数 + 抽样记日志到 <code>/mnt/data/logs/messages</code>,不拦截。<br />
|
||||
注意:53 被重定向后不再进入 forward 链,<strong>此计数会趋近于 0(只剩 853 / DoH)</strong>,这是正常的。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cbi-value">
|
||||
<label class="cbi-value-title" for="dg-853">阻断 853(DoT)</label>
|
||||
<div class="cbi-value-field">
|
||||
<input type="checkbox" id="dg-853"<% if cfg_853 then %> checked="checked"<% end %> />
|
||||
<div class="cbi-value-description">DoT 无法透明重定向,只能阻断;客户端会回落到 853 之外的明文 53,从而被上面的重定向收编。建议重定向稳定 1 天后再开。</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cbi-value">
|
||||
<label class="cbi-value-title" for="dg-doh">阻断 DoH 专用 IP</label>
|
||||
<div class="cbi-value-field">
|
||||
<input type="checkbox" id="dg-doh"<% if cfg_doh then %> checked="checked"<% end %> />
|
||||
<div class="cbi-value-description">
|
||||
⚠️ 风险最高:按 IP 命中,可能误伤「恰好也部署在这些 IP 上」的其它服务(例如 <code>223.5.5.5:443</code>)。
|
||||
更精准的做法是在 AdGuard Home 里加域名规则。默认关闭。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cbi-section">
|
||||
<h3>参数</h3>
|
||||
<div class="cbi-section-node">
|
||||
<div class="cbi-value">
|
||||
<label class="cbi-value-title" for="dg-port">重定向目标端口</label>
|
||||
<div class="cbi-value-field">
|
||||
<input type="text" class="cbi-input-text" id="dg-port" style="width:80px" value="<%=e(cfg_port)%>" />
|
||||
<div class="cbi-value-description">本机 AdGuard Home 的监听端口,默认 53。<strong>不要</strong>填 dnsmasq 的 5353。</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cbi-value">
|
||||
<label class="cbi-value-title" for="dg-ifaces">入接口</label>
|
||||
<div class="cbi-value-field">
|
||||
<input type="text" class="cbi-input-text" id="dg-ifaces" style="width:260px" value="<%=e(cfg_ifaces)%>" />
|
||||
<div class="cbi-value-description">监听哪个接口进来的流量,默认 <code>br-lan</code>。多个用空格分隔。</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cbi-value">
|
||||
<label class="cbi-value-title" for="dg-skip">例外设备</label>
|
||||
<div class="cbi-value-field">
|
||||
<input type="text" class="cbi-input-text" id="dg-skip" style="width:100%;max-width:420px" value="<%=e(cfg_skip)%>" placeholder="留空 = 全屋收编" />
|
||||
<div class="cbi-value-description">
|
||||
这些源地址<strong>不做</strong>重定向(IPv4 / IPv6 混填,空格分隔)。只在确实需要保留某个设备走外部 DNS 时使用。<br />
|
||||
⚠️ 不要把整个 <code>192.168.0.0/16</code> 填进「永不劫持」:那样红米 K80 这类静态指向旧私网地址的设备会继续解析失败。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cbi-value">
|
||||
<label class="cbi-value-title" for="dg-loglimit">日志限速</label>
|
||||
<div class="cbi-value-field">
|
||||
<input type="text" class="cbi-input-text" id="dg-loglimit" style="width:80px" value="<%=e(cfg_loglimit)%>" />
|
||||
<div class="cbi-value-description">观察日志每分钟最多写几条(1-999),防止刷爆日志。</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cbi-value">
|
||||
<label class="cbi-value-title" for="dg-rollmin">自动回滚倒计时</label>
|
||||
<div class="cbi-value-field">
|
||||
<input type="text" class="cbi-input-text" id="dg-rollmin" style="width:80px" value="<%=e(cfg_rollmin)%>" />
|
||||
<div class="cbi-value-description">
|
||||
把「53 重定向」从<strong>关改到开</strong>时,自动拍一个回滚快照并倒计时;到点未点「确认保留」就自动还原。<br />
|
||||
DNS 是全家网的命门,改错了人可能连这台路由器都访问不到(域名解析不了)——这是最后的保险绳。单位:分钟(1-120)。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cbi-section">
|
||||
<h3>高级:地址集合</h3>
|
||||
<div class="cbi-section-node">
|
||||
<div class="cbi-value">
|
||||
<label class="cbi-value-title" for="dg-nh4">永不劫持(IPv4)</label>
|
||||
<div class="cbi-value-field">
|
||||
<textarea class="cbi-input-textarea" id="dg-nh4" rows="3" style="width:100%;max-width:560px"><%=e(cfg_nh4)%></textarea>
|
||||
<div class="cbi-value-description">指向这些网段的 DNS 查询不会被改道(内网 / 保留段)。空格分隔。</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cbi-value">
|
||||
<label class="cbi-value-title" for="dg-nh6">永不劫持(IPv6)</label>
|
||||
<div class="cbi-value-field">
|
||||
<textarea class="cbi-input-textarea" id="dg-nh6" rows="2" style="width:100%;max-width:560px"><%=e(cfg_nh6)%></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cbi-value">
|
||||
<label class="cbi-value-title" for="dg-dohsrv">DoH 专用地址集合</label>
|
||||
<div class="cbi-value-field">
|
||||
<textarea class="cbi-input-textarea" id="dg-dohsrv" rows="4" style="width:100%;max-width:560px"><%=e(cfg_dohsrv)%></textarea>
|
||||
<div class="cbi-value-description">用于「DoH 专用 IP 阻断」与观察计数,也用于反查哪些设备在走这些地址。空格分隔。</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="cbi-section">
|
||||
<h3>操作</h3>
|
||||
<div class="cbi-section-node">
|
||||
<div class="dg-pad">
|
||||
<div id="dg-msg" class="dg-banner"></div>
|
||||
|
||||
<div id="dg-pendbox" style="display:none;padding:.6rem .75rem;border-radius:.375rem;margin-bottom:.75rem;font-size:13px;background:#fff6e5;border:1px solid #d9a400;color:#5a3c00"></div>
|
||||
|
||||
<div style="margin-bottom:.75rem">
|
||||
<button type="button" id="btn-save" class="cbi-button cbi-button-apply" style="margin-right:.5rem" onclick="dgSave()">保存并应用</button>
|
||||
<button type="button" id="btn-apply" class="cbi-button" style="margin-right:.5rem" onclick="dgApply()">按当前配置重新应用</button>
|
||||
<button type="button" id="btn-test" class="cbi-button cbi-button-action" style="margin-right:.5rem" onclick="dgSelfTest()">健康自检</button>
|
||||
<button type="button" id="btn-rules" class="cbi-button" onclick="dgRules()">预览将要生成的规则</button>
|
||||
</div>
|
||||
|
||||
<div id="dg-pendbtn" style="display:none;margin-bottom:.75rem">
|
||||
<button type="button" id="btn-disarm" class="cbi-button cbi-button-negative" onclick="dgDisarm()">确认保留(取消自动回滚)</button>
|
||||
</div>
|
||||
|
||||
<details id="dg-details">
|
||||
<summary style="cursor:pointer;font-size:12.5px;color:#57606a">自检结果 / 规则预览 / 命令回显(点开查看)</summary>
|
||||
<div id="dg-checks" style="margin:.5rem 0 0"></div>
|
||||
<pre id="dg-out" class="dg-pre">(等待操作)</pre>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cbi-section">
|
||||
<h3>回滚点</h3>
|
||||
<div class="cbi-section-node">
|
||||
<div class="cbi-value">
|
||||
<label class="cbi-value-title" for="dg-rb">选择回滚点</label>
|
||||
<div class="cbi-value-field">
|
||||
<select class="cbi-input-select" id="dg-rb" style="min-width:200px;margin-right:8px"></select>
|
||||
<button type="button" id="btn-restore" class="cbi-button cbi-button-negative" onclick="dgRestore()">还原到该回滚点</button>
|
||||
<div class="cbi-value-description">
|
||||
还原会同时恢复 <strong>UCI 配置 + 规则文件</strong>并重载防火墙。回滚点目录:<code>/mnt/data/backup-dnsguard/</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
var DG_API = '<%=e(api_url)%>';
|
||||
|
||||
function dgEsc(s) {
|
||||
s = (s === null || s === undefined) ? '' : String(s);
|
||||
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
function dgBox(el, kind, text) {
|
||||
var map = {
|
||||
ok: ['#e1f5ee', '#0f6e56', '#04342c'],
|
||||
err: ['#fcebeb', '#a32d2d', '#501313'],
|
||||
busy: ['#e6f1fb', '#185fa5', '#042c53']
|
||||
};
|
||||
var c = map[kind] || map.busy;
|
||||
el.style.display = 'block';
|
||||
el.style.background = c[0];
|
||||
el.style.border = '1px solid ' + c[1];
|
||||
el.style.color = c[2];
|
||||
el.textContent = text;
|
||||
if (el.scrollIntoView) { el.scrollIntoView({ block: 'nearest' }); }
|
||||
}
|
||||
|
||||
function dgMsg(kind, text) { dgBox(document.getElementById('dg-msg'), kind, text); }
|
||||
|
||||
function dgOut(t) { document.getElementById('dg-out').textContent = t; }
|
||||
|
||||
function dgDetails(openIt) {
|
||||
var d = document.getElementById('dg-details');
|
||||
if (openIt) { d.open = true; }
|
||||
}
|
||||
|
||||
function dgBusy(id, on, label) {
|
||||
var b = document.getElementById(id);
|
||||
if (!b) { return; }
|
||||
if (on) {
|
||||
if (!b.getAttribute('data-orig')) { b.setAttribute('data-orig', b.textContent); }
|
||||
b.textContent = label || '处理中…';
|
||||
b.disabled = true;
|
||||
} else {
|
||||
var o = b.getAttribute('data-orig');
|
||||
if (o) { b.textContent = o; }
|
||||
b.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function dgVal(id) { var e = document.getElementById(id); return e ? e.value : ''; }
|
||||
function dgChk(id) { var e = document.getElementById(id); return e && e.checked ? '1' : '0'; }
|
||||
|
||||
function dgEnc(o) {
|
||||
var out = [];
|
||||
for (var k in o) { out.push(encodeURIComponent(k) + '=' + encodeURIComponent(o[k])); }
|
||||
return out.join('&');
|
||||
}
|
||||
|
||||
function dgPost(params, cb) {
|
||||
var x = new XMLHttpRequest();
|
||||
x.open('POST', DG_API, true);
|
||||
x.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
|
||||
x.timeout = 90000;
|
||||
x.onreadystatechange = function () {
|
||||
if (x.readyState !== 4) { return; }
|
||||
var d = null;
|
||||
try { d = JSON.parse(x.responseText); } catch (err) { d = null; }
|
||||
cb(x.status, d, x.responseText);
|
||||
};
|
||||
x.ontimeout = function () { cb(0, null, '请求超时(90 秒)—— 防火墙重载可能较慢,请刷新页面查看当前状态'); };
|
||||
x.send(params);
|
||||
}
|
||||
|
||||
function dgFmtBytes(n) {
|
||||
n = Number(n) || 0;
|
||||
if (n < 1024) { return n + ' B'; }
|
||||
if (n < 1048576) { return (n / 1024).toFixed(1) + ' KB'; }
|
||||
return (n / 1048576).toFixed(1) + ' MB';
|
||||
}
|
||||
|
||||
function dgSave() {
|
||||
dgMsg('busy', '正在保存并应用(会重载防火墙,几秒钟)…');
|
||||
dgBusy('btn-save', true, '保存中…');
|
||||
dgPost(dgEnc({
|
||||
action: 'save',
|
||||
enabled: dgChk('dg-enabled'),
|
||||
redirect53: dgChk('dg-redirect'),
|
||||
watch: dgChk('dg-watch'),
|
||||
block853: dgChk('dg-853'),
|
||||
blockdoh: dgChk('dg-doh'),
|
||||
target_port: dgVal('dg-port'),
|
||||
ifaces: dgVal('dg-ifaces'),
|
||||
skip_devices: dgVal('dg-skip'),
|
||||
log_limit: dgVal('dg-loglimit'),
|
||||
rollback_minutes: dgVal('dg-rollmin'),
|
||||
never_hijack4: dgVal('dg-nh4'),
|
||||
never_hijack6: dgVal('dg-nh6'),
|
||||
doh_servers: dgVal('dg-dohsrv')
|
||||
}), function (code, d, raw) {
|
||||
dgBusy('btn-save', false);
|
||||
if (!d) {
|
||||
dgMsg('err', '保存请求失败(HTTP ' + code + ')\n' + raw);
|
||||
dgOut(raw); dgDetails(true);
|
||||
return;
|
||||
}
|
||||
dgOut(d.message + '\n\n--- dnsguard-apply 回显 ---\n' + (d.apply || ''));
|
||||
if (d.status === 'ok') {
|
||||
var extra = '';
|
||||
if (d.armed) { extra = '\n🛟 已挂自动回滚,未点「确认保留」将自动还原(快照 ' + d.armed + ')'; }
|
||||
dgMsg('ok', '✅ ' + d.message + '(' + (d.at || '') + ')' + extra);
|
||||
dgStatus();
|
||||
} else {
|
||||
dgMsg('err', '❌ ' + d.message);
|
||||
dgDetails(true);
|
||||
dgStatus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function dgApply() {
|
||||
dgMsg('busy', '正在按当前配置重新渲染并应用…');
|
||||
dgBusy('btn-apply', true, '应用中…');
|
||||
dgPost(dgEnc({ action: 'apply' }), function (code, d, raw) {
|
||||
dgBusy('btn-apply', false);
|
||||
if (!d) { dgMsg('err', '请求失败(HTTP ' + code + ')\n' + raw); dgOut(raw); dgDetails(true); return; }
|
||||
dgOut(d.message + '\n\n' + (d.apply || ''));
|
||||
if (d.status === 'ok') { dgMsg('ok', '✅ ' + d.message + '(' + (d.at || '') + ')'); }
|
||||
else { dgMsg('err', '❌ ' + d.message); dgDetails(true); }
|
||||
dgStatus();
|
||||
});
|
||||
}
|
||||
|
||||
function dgSelfTest() {
|
||||
dgMsg('busy', '正在执行健康自检…');
|
||||
dgBusy('btn-test', true, '自检中…');
|
||||
dgDetails(true);
|
||||
dgPost(dgEnc({ action: 'selftest' }), function (code, d, raw) {
|
||||
dgBusy('btn-test', false);
|
||||
if (!d || !d.items) {
|
||||
dgMsg('err', '自检失败(HTTP ' + code + ')\n' + raw);
|
||||
dgOut(raw);
|
||||
return;
|
||||
}
|
||||
var html = '<table class="dg-table">';
|
||||
var bad = 0, warn = 0;
|
||||
for (var i = 0; i < d.items.length; i++) {
|
||||
var it = d.items[i];
|
||||
var cls = it.state === 'ok' ? 'dg-ok' : (it.state === 'warn' ? 'dg-warn' : 'dg-bad');
|
||||
var tag = it.state === 'ok' ? '正常' : (it.state === 'warn' ? '注意' : '异常');
|
||||
if (it.state === 'fail') { bad++; }
|
||||
if (it.state === 'warn') { warn++; }
|
||||
html += '<tr><td class="dg-k">' + dgEsc(it.name) + '</td><td><span class="' + cls + '">' + tag + '</span> · ' + dgEsc(it.detail) + '</td></tr>';
|
||||
}
|
||||
html += '</table>';
|
||||
document.getElementById('dg-checks').innerHTML = html;
|
||||
dgOut('(自检明细见上方表格)');
|
||||
if (bad > 0) { dgMsg('err', '❌ 自检发现 ' + bad + ' 项异常、' + warn + ' 项注意 —— 详见下方明细'); }
|
||||
else if (warn > 0) { dgMsg('busy', '⚠️ 自检通过,但有 ' + warn + ' 项注意 —— 详见下方明细'); }
|
||||
else { dgMsg('ok', '✅ 自检全部正常'); }
|
||||
dgStatus();
|
||||
});
|
||||
}
|
||||
|
||||
function dgRules() {
|
||||
dgMsg('busy', '正在渲染规则预览…');
|
||||
dgBusy('btn-rules', true, '渲染中…');
|
||||
dgDetails(true);
|
||||
dgPost(dgEnc({ action: 'rules' }), function (code, d, raw) {
|
||||
dgBusy('btn-rules', false);
|
||||
if (!d || d.status !== 'ok') {
|
||||
dgMsg('err', '❌ 预览失败:' + ((d && d.message) || ('HTTP ' + code)));
|
||||
dgOut(raw);
|
||||
return;
|
||||
}
|
||||
dgOut(d.text || '');
|
||||
dgMsg('ok', '✅ 已生成 ' + d.lines + ' 行规则(未应用到防火墙,只是预览)');
|
||||
});
|
||||
}
|
||||
|
||||
function dgDisarm() {
|
||||
dgBusy('btn-disarm', true, '处理中…');
|
||||
dgPost(dgEnc({ action: 'disarm' }), function (code, d, raw) {
|
||||
dgBusy('btn-disarm', false);
|
||||
if (!d) { dgMsg('err', '请求失败(HTTP ' + code + ')\n' + raw); return; }
|
||||
dgMsg(d.status === 'ok' ? 'ok' : 'err', (d.status === 'ok' ? '✅ ' : '❌ ') + (d.message || raw));
|
||||
dgStatus();
|
||||
});
|
||||
}
|
||||
|
||||
function dgRestore() {
|
||||
var sel = document.getElementById('dg-rb');
|
||||
var ts = sel ? sel.value : '';
|
||||
if (!ts) { dgMsg('err', '请先选择一个回滚点'); return; }
|
||||
if (!confirm('确认还原到回滚点 ' + ts + ' ?\n\n会同时恢复 UCI 配置与规则文件,并重载防火墙。')) { return; }
|
||||
dgMsg('busy', '正在还原到 ' + ts + ' …');
|
||||
dgBusy('btn-restore', true, '还原中…');
|
||||
dgPost(dgEnc({ action: 'rollback', ts: ts }), function (code, d, raw) {
|
||||
dgBusy('btn-restore', false);
|
||||
if (!d) { dgMsg('err', '请求失败(HTTP ' + code + ')\n' + raw); return; }
|
||||
dgMsg(d.status === 'ok' ? 'ok' : 'err', (d.status === 'ok' ? '✅ ' : '❌ ') + (d.message || raw));
|
||||
setTimeout(function () { location.reload(); }, 1500);
|
||||
});
|
||||
}
|
||||
|
||||
function dgStatus() {
|
||||
dgPost(dgEnc({ action: 'status' }), function (code, d, raw) {
|
||||
if (!d) {
|
||||
document.getElementById('st-enabled').textContent = '状态读取失败(HTTP ' + code + ')';
|
||||
return;
|
||||
}
|
||||
function set(id, html) { document.getElementById(id).innerHTML = html; }
|
||||
|
||||
set('st-enabled', d.enabled === '1' ? '<span class="dg-ok">已启用</span>' : '<span class="dg-bad">已关闭</span>');
|
||||
set('st-agh', d.agh_pid ? '<span class="dg-ok">运行中</span> · PID ' + dgEsc(d.agh_pid) : '<span class="dg-bad">未运行</span>');
|
||||
set('st-listen', d.listen53 ? dgEsc(d.listen53) : '<span class="dg-bad">无进程监听 :53</span>');
|
||||
|
||||
if (d.redirect_active) {
|
||||
set('st-redirect', '<span class="dg-ok">已生效</span> · 目标 :' + dgEsc(d.target_port) + ' · 接口 ' + dgEsc(d.ifaces) + (d.skip_devices ? ' · 例外 ' + dgEsc(d.skip_devices) : ' · 无例外'));
|
||||
} else if (d.redirect53 === '1') {
|
||||
set('st-redirect', '<span class="dg-bad">配置为开启,但链不存在!</span> 请点「健康自检」');
|
||||
} else {
|
||||
set('st-redirect', '<span class="dg-warn">未启用</span>(仅观察)');
|
||||
}
|
||||
|
||||
set('st-redpk', d.redirect_packets ? (d.redirect_packets + ' 个包 / ' + dgFmtBytes(d.redirect_bytes) + '(自上次重载)') : '0(未有流量被收编)');
|
||||
|
||||
if (d.watch_active) {
|
||||
set('st-watch', d.watch_packets + ' 个包 / ' + dgFmtBytes(d.watch_bytes) + (d.redirect_active && Number(d.watch_packets) === 0 ? ' · 53 已改道,计数归零属正常' : ''));
|
||||
} else {
|
||||
set('st-watch', '<span class="dg-warn">未启用</span>');
|
||||
}
|
||||
|
||||
if (d.managed === '1') {
|
||||
set('st-file', '<span class="dg-ok">由本应用托管</span> · 最后生成 ' + dgEsc(d.file_mtime));
|
||||
} else if (d.file_mtime) {
|
||||
set('st-file', '<span class="dg-warn">存在但不是本应用生成</span> · ' + dgEsc(d.file_mtime) + '(点「保存并应用」会被接管)');
|
||||
} else {
|
||||
set('st-file', '不存在(总开关关闭或从未应用过)');
|
||||
}
|
||||
|
||||
set('st-logs', dgEsc(d.bypass_lines || '0') + ' 行(DNS-BYPASS)');
|
||||
|
||||
// 自动回滚横幅
|
||||
var pb = document.getElementById('dg-pendbox');
|
||||
var pbtn = document.getElementById('dg-pendbtn');
|
||||
if (d.pending) {
|
||||
pb.style.display = 'block';
|
||||
pb.textContent = '🛟 自动回滚倒计时进行中:约 ' + d.pending_left + ' 秒后(快照 ' + d.pending_ts + ')若无确认将自动还原,并重载防火墙。确认当前设置没问题就点下面的「确认保留」。';
|
||||
pbtn.style.display = 'block';
|
||||
} else {
|
||||
pb.style.display = 'none';
|
||||
pbtn.style.display = 'none';
|
||||
}
|
||||
|
||||
// 回滚点下拉
|
||||
var sel = document.getElementById('dg-rb');
|
||||
if (sel && d.backups) {
|
||||
var cur = sel.value;
|
||||
var html = '';
|
||||
for (var i = 0; i < d.backups.length; i++) {
|
||||
html += '<option value="' + dgEsc(d.backups[i]) + '">' + dgEsc(d.backups[i]) + '</option>';
|
||||
}
|
||||
sel.innerHTML = html || '<option value="">(暂无回滚点)</option>';
|
||||
if (cur) { sel.value = cur; }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('load', function () { dgStatus(); });
|
||||
</script>
|
||||
|
||||
<%+footer%>
|
||||
Reference in new issue
Block a user