From f10ac5591e4f99cb1826a2c2e83b398de3417b0b Mon Sep 17 00:00:00 2001 From: Hermes Date: Wed, 22 Jul 2026 17:56:41 +0800 Subject: [PATCH] fix: detect zone name collisions with named.conf main config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 创建 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)。 --- app.py | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/app.py b/app.py index 71fc733..afa2e57 100644 --- a/app.py +++ b/app.py @@ -216,6 +216,53 @@ def parse_named_conf_local(): 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): """Rewrite named.conf.local from zone list.""" 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}") - # 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() + all_managed = parse_all_managed_zones() 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") # Generate initial zone file