diff --git a/app.py b/app.py index 5768d64..b118509 100644 --- a/app.py +++ b/app.py @@ -1045,23 +1045,54 @@ def _parse_options_lists(content): return forwarders, recursion -def _replace_or_append_option(content, directive, new_block): - """Replace an existing 'directive { ... };' in an options block with new_block, - or append it after the opening 'options {' if not present. +_VALIDATION_VALUES = ("auto", "yes", "no") - `new_block` is the full 'directive { ... };' text (without leading/trailing newline). - Handles nested braces in directives by counting brace depth (so multi-line - directives like `forwarders { 1.1.1.1; }` work; nested ones like `key ...` - are not expected in options block). +def _parse_options_dnssec(content): + """Return current dnssec-validation value as one of 'auto'/'yes'/'no'. + Returns 'auto' if the directive is absent (BIND default). + """ + m = re.search(r'dnssec-validation\s+(\w+)\s*;', content) + if not m: + return "auto" + val = m.group(1).lower() + if val in _VALIDATION_VALUES: + return val + return "auto" + + +def _listen_on_v4_set(content): + """True if the options block contains any IPv4 'listen-on' directive.""" + # Match listen-on (port N)? { ... } but NOT listen-on-v6 + return bool(re.search(r'(?;' in an + options block with new_block, or append it after the opening 'options {' if + not present. + + `new_block` is the full directive text (without leading/trailing newline). + Handles: + - brace-delimited: 'forwarders { ... };' / 'allow-recursion { ... };' + - value-terminated: 'dnssec-validation auto;' / 'recursion yes;' + Nested braces are matched by counting depth. """ - # Try to find an existing directive with balanced-brace body pattern = re.compile( - r'(' + re.escape(directive) + r')\s*\{', + r'(' + re.escape(directive) + r')\b(?!\-)', re.MULTILINE, ) - m = pattern.search(content) - if m: + + for m in pattern.finditer(content): + # Skip lines where the directive is part of a longer hyphenated name, + # e.g. don't match 'listen-on' when looking for 'listen-on-v6'. + # The negative lookbehind `(?= len(content): + continue + + if content[i] == '{': + # Brace-delimited: walk forward to matching `};` + depth = 0 + for j in range(i, len(content)): + c = content[j] + if c == '{': + depth += 1 + elif c == '}': + depth -= 1 + if depth == 0: + end = j + 1 + while end < len(content) and content[end] in ' \t\n': + end += 1 + if end < len(content) and content[end] == ';': + end += 1 + indented = '\n'.join( + (indent + line) if line else line + for line in new_block.split('\n') + ) + return content[:start] + indented + content[end:] + continue # malformed, try next match + else: + # Value-terminated: read tokens up to `;` + end = i + while end < len(content) and content[end] != ';': + end += 1 + if end < len(content): + end += 1 # consume the `;` + indented = '\n'.join( + (indent + line) if line else line + for line in new_block.split('\n') + ) + return content[:start] + indented + content[end:] + # else: malformed, try next match # Not found — append after the opening "options {" line opt_match = re.search(r'options\s*\{', content) if opt_match: insert_at = opt_match.end() - # Insert after the newline that follows nl = content.find('\n', insert_at) if nl >= 0: insert_at = nl + 1 - # Indent new_block 4 spaces (typical options-body indent) indented = '\n'.join(' ' + line if line else line for line in new_block.split('\n')) indented = '\n' + indented + '\n' return content[:insert_at] + indented + content[insert_at:] @@ -1176,6 +1225,7 @@ def config_view(): # Parse structured fields for the upstream-DNS form forwarders, recursion = _parse_options_lists(options_content) + dnssec_value = _parse_options_dnssec(options_content) return render_template("config.html", options_content=options_content, @@ -1184,7 +1234,9 @@ def config_view(): config_ok=config_ok, config_msg=config_msg, forwarders=forwarders, - recursion=recursion) + recursion=recursion, + dnssec_value=dnssec_value, + dnssec_values=_VALIDATION_VALUES) @app.route("/config/options", methods=["POST"]) @@ -1267,6 +1319,9 @@ def config_upstream_save(): raw_fwd = request.form.get("forwarders", "") raw_rc = request.form.get("recursion", "") + dnssec_in = (request.form.get("dnssec") or "auto").strip().lower() + if dnssec_in not in _VALIDATION_VALUES: + dnssec_in = "auto" new_fwd = _parse_list(raw_fwd) new_rc = _parse_list(raw_rc) @@ -1287,6 +1342,10 @@ def config_upstream_save(): new_content, "allow-recursion", _format_list_block("allow-recursion", new_rc), ) + new_content = _replace_or_append_option( + new_content, "dnssec-validation", + f"dnssec-validation {dnssec_in};", + ) # Validate BEFORE writing to the live file tmp = "/tmp/named_check.tmp" @@ -1309,8 +1368,8 @@ def config_upstream_save(): u = current_user() AuditLog.log(u.username, "修改上游DNS转发配置", - f"forwarders={new_fwd}; allow-recursion={new_rc}") - flash(f"已保存:forwarders {len(new_fwd)} 条,allow-recursion {len(new_rc)} 条", "success") + f"forwarders={new_fwd}; allow-recursion={new_rc}; dnssec={dnssec_in}") + flash(f"已保存:forwarders {len(new_fwd)} 条,allow-recursion {len(new_rc)} 条,dnssec-validation {dnssec_in}", "success") return redirect(url_for("config_view")) diff --git a/templates/config.html b/templates/config.html index a3f22ea..073f62e 100644 --- a/templates/config.html +++ b/templates/config.html @@ -48,10 +48,21 @@ placeholder="127.0.0.0/8 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16">{% for net in recursion %}{{ net }} {% endfor %} +