fix: detect zone name collisions with named.conf main config

创建 zone 时只检查 BIND_CONF_LOCAL 里的重复声明,遗漏了用户在
named.conf 主配置里手动声明的同名 zone —— 后果是 named reload
时直接报 'zone already exists' 错误,named 起不来。

新增 parse_all_managed_zones(),通过 named-checkconf -p 拿到 named
当前加载的所有 zone,区分 source=local(Web UI 管理的)和 source=main
(用户在 named.conf 主配置里写的)。

zone_create() 在写入新声明前检查这两个来源:
- 同名在 BIND_CONF_LOCAL:拒绝并提示
- 同名在 named.conf 主配置:拒绝并提示用户先去主配置里注释掉

回归测试在 dev 环境通过:识别 4 个 builtin zone (source=main) +
3 个 web UI zone (source=local)。
This commit is contained in:
Hermes
2026-07-22 17:56:41 +08:00
parent ab8d472b7b
commit f10ac5591e
+61 -2
View File
@@ -216,6 +216,53 @@ def parse_named_conf_local():
return zones return zones
def parse_all_managed_zones():
"""Return all zone names that BIND currently has loaded (master/slave/in-view),
including zones declared in the main named.conf outside BIND_CONF_LOCAL.
Uses `named-checkconf -p` which prints the fully-resolved configuration with
all included files expanded, so we can detect duplicates the user might have
added manually (e.g. a zone declared in /etc/named.conf that web UI doesn't
know about).
Returns a dict: zone_name -> {"type": str, "file": str, "source": "local"|"main"}.
"""
rc, out, err = run_cmd("named-checkconf -p 2>&1", timeout=15)
if rc != 0:
# If named-checkconf fails (e.g. broken config), fall back to local-only
# so the UI doesn't break entirely.
return {z["name"]: {"type": z["type"], "file": z["file"], "source": "local"}
for z in parse_named_conf_local()}
# Parse the dumped config: zone statements with a non-builtin type (i.e. not
# "hint" and not under automatic empty-zones). We deliberately include builtin
# zones too and let callers filter.
result = {}
pattern = re.compile(
r'zone\s+"([^"]+)"\s+(?:IN\s+)?\{\s*'
r'type\s+(\w+)\s*;\s*'
r'(?:file\s+"([^"]+)"\s*;\s*)?',
re.MULTILINE,
)
for m in pattern.finditer(out):
name, ztype, zfile = m.group(1), m.group(2), m.group(3) or ""
# Skip builtin/hint zones and the "empty" automatic zones (no file).
if ztype == "hint" or not zfile:
continue
result[name] = {"type": ztype, "file": zfile, "source": "unknown"}
# Mark which ones came from BIND_CONF_LOCAL for the UI to know whether to
# let the user edit/delete them.
local_names = {z["name"] for z in parse_named_conf_local()}
for name in result:
if name in local_names:
result[name]["source"] = "local"
else:
result[name]["source"] = "main"
return result
def write_named_conf_local(zones): def write_named_conf_local(zones):
"""Rewrite named.conf.local from zone list.""" """Rewrite named.conf.local from zone list."""
lines = ['// Local zone configurations - managed by DNS Web UI\n'] lines = ['// Local zone configurations - managed by DNS Web UI\n']
@@ -687,10 +734,22 @@ def zone_create():
zone_file = os.path.join(BIND_ZONES_DIR, f"db.{zone_name}") zone_file = os.path.join(BIND_ZONES_DIR, f"db.{zone_name}")
# Check duplicate # Check duplicate — both against zones we manage in BIND_CONF.local AND
# against zones declared elsewhere in named.conf (e.g. user manually
# added a zone block in /etc/named.conf). Without the second check,
# creating a zone with a name already declared in the main config would
# produce a duplicate zone error on BIND reload.
existing = parse_named_conf_local() existing = parse_named_conf_local()
all_managed = parse_all_managed_zones()
if any(z["name"] == zone_name for z in existing): if any(z["name"] == zone_name for z in existing):
flash(f"域名 {zone_name} 已存在", "error") flash(f"域名 {zone_name}在 Web UI 中存在", "error")
return render_template("zone_form.html")
if zone_name in all_managed:
flash(
f"域名 {zone_name} 已在 named.conf 中声明(不在 Web UI 管理范围内),"
f"请先到 named.conf 中注释/删除后再创建",
"error",
)
return render_template("zone_form.html") return render_template("zone_form.html")
# Generate initial zone file # Generate initial zone file