feat: listen-on and recursion toggles in upstream DNS form
Web 端 '上游 DNS 转发' 卡片新增两个字段:
1. Listen-on (IPv4) 53 端口 — textarea,每行一个 CIDR/IP/ACL 名。
留空 = any(任意来源)。对应 BIND 指令 listen-on port 53 { ... };。
2. Recursion — yes/no 下拉,控制本 DNS 是否代客户端去外网递归。
内网递归服务器选 yes;纯权威服务器选 no。
修了一个 regex bug:之前 _replace_or_append_option 用 \\b(?!\\-) 做
lookahead 但没用 lookbehind,导致在内容含 allow-recursion 时匹配
recursion 时也会匹配到 allow- 后面的 recursion,把 allow-recursion
破坏。改为 (?<!\\w)(?<!\\-)keyword\\b(?!\\w)(?!\\-) 双向拒绝,
确保 keyword 必须是独立单词,不能是其它带连字符 directive 的子串。
测试覆盖:
- parse listen-on (IPv4 list)
- parse recursion (yes/no/default)
- 完整 round-trip:5 个字段同时更新,allow-recursion / listen-on-v6
等未触及的 directives 正确保留
This commit is contained in:
@@ -1067,6 +1067,31 @@ def _listen_on_v4_set(content):
|
||||
return bool(re.search(r'(?<!-)listen-on\b', content))
|
||||
|
||||
|
||||
def _parse_listen_on_v4(content):
|
||||
"""Return list of CIDR/IP/ACL names from `listen-on [port 53] { ... };` block.
|
||||
Returns [] if directive is absent.
|
||||
"""
|
||||
m = re.search(r'(?<!-)listen-on(?:\s+port\s+\d+)?\s*\{([^}]*)\}', content, re.DOTALL)
|
||||
if not m:
|
||||
return []
|
||||
body = m.group(1)
|
||||
items = []
|
||||
for tm in re.finditer(r'"([^"]+)"|([\w./-]+)', body):
|
||||
token = tm.group(1) or tm.group(2)
|
||||
if token and token not in ('any', 'none'):
|
||||
items.append(token)
|
||||
return items
|
||||
|
||||
|
||||
def _parse_recursion(content):
|
||||
"""Return 'yes' (default), 'no', or specific value from 'recursion <v>;'."""
|
||||
m = re.search(r'recursion\s+(\w+)\s*;', content)
|
||||
if not m:
|
||||
return "yes"
|
||||
val = m.group(1).lower()
|
||||
return val if val in ("yes", "no") else "yes"
|
||||
|
||||
|
||||
def _replace_or_append_option(content, directive, new_block):
|
||||
"""Replace an existing 'directive { ... };' or 'directive <value>;' in an
|
||||
options block with new_block, or append it after the opening 'options {' if
|
||||
@@ -1079,16 +1104,15 @@ def _replace_or_append_option(content, directive, new_block):
|
||||
Nested braces are matched by counting depth.
|
||||
"""
|
||||
pattern = re.compile(
|
||||
r'(' + re.escape(directive) + r')\b(?!\-)',
|
||||
r'(?<!\w)(?<!\-)(' + re.escape(directive) + r')\b(?!\w)(?!\-)',
|
||||
re.MULTILINE,
|
||||
)
|
||||
|
||||
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 `(?<!\-)` in the regex above handles most
|
||||
# cases; also check that the next char isn't '-' (handles 'foo-bar'
|
||||
# matching 'foo').
|
||||
# e.g. don't match 'recursion' when looking for it inside 'allow-recursion'.
|
||||
# The negative lookbehind `(?<!\-)` and lookahead `(?!\-)` together cover
|
||||
# both directions. Belt-and-suspenders: also check next char isn't '-'.
|
||||
end_kw = m.end()
|
||||
if end_kw < len(content) and content[end_kw] == '-':
|
||||
continue
|
||||
@@ -1226,6 +1250,8 @@ 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)
|
||||
listen_on_v4 = _parse_listen_on_v4(options_content)
|
||||
recursion_value = _parse_recursion(options_content)
|
||||
|
||||
return render_template("config.html",
|
||||
options_content=options_content,
|
||||
@@ -1236,7 +1262,9 @@ def config_view():
|
||||
forwarders=forwarders,
|
||||
recursion=recursion,
|
||||
dnssec_value=dnssec_value,
|
||||
dnssec_values=_VALIDATION_VALUES)
|
||||
dnssec_values=_VALIDATION_VALUES,
|
||||
listen_on_v4=listen_on_v4,
|
||||
recursion_value=recursion_value)
|
||||
|
||||
|
||||
@app.route("/config/options", methods=["POST"])
|
||||
@@ -1319,11 +1347,16 @@ def config_upstream_save():
|
||||
|
||||
raw_fwd = request.form.get("forwarders", "")
|
||||
raw_rc = request.form.get("recursion", "")
|
||||
raw_listen = request.form.get("listen_on_v4", "")
|
||||
dnssec_in = (request.form.get("dnssec") or "auto").strip().lower()
|
||||
if dnssec_in not in _VALIDATION_VALUES:
|
||||
dnssec_in = "auto"
|
||||
recursion_in = (request.form.get("recursion_toggle") or "yes").strip().lower()
|
||||
if recursion_in not in ("yes", "no"):
|
||||
recursion_in = "yes"
|
||||
new_fwd = _parse_list(raw_fwd)
|
||||
new_rc = _parse_list(raw_rc)
|
||||
new_listen = _parse_list(raw_listen)
|
||||
|
||||
_ensure_options_file()
|
||||
try:
|
||||
@@ -1346,6 +1379,17 @@ def config_upstream_save():
|
||||
new_content, "dnssec-validation",
|
||||
f"dnssec-validation {dnssec_in};",
|
||||
)
|
||||
# listen-on: empty list -> listen-on port 53 { any; }; (default safe)
|
||||
if not new_listen:
|
||||
new_listen = ["any"]
|
||||
new_content = _replace_or_append_option(
|
||||
new_content, "listen-on",
|
||||
_format_list_block("listen-on port 53", new_listen),
|
||||
)
|
||||
new_content = _replace_or_append_option(
|
||||
new_content, "recursion",
|
||||
f"recursion {recursion_in};",
|
||||
)
|
||||
|
||||
# Validate BEFORE writing to the live file
|
||||
tmp = "/tmp/named_check.tmp"
|
||||
@@ -1368,8 +1412,14 @@ def config_upstream_save():
|
||||
|
||||
u = current_user()
|
||||
AuditLog.log(u.username, "修改上游DNS转发配置",
|
||||
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")
|
||||
f"forwarders={new_fwd}; allow-recursion={new_rc}; "
|
||||
f"dnssec={dnssec_in}; listen-on={new_listen}; recursion={recursion_in}")
|
||||
flash(
|
||||
f"已保存:forwarders {len(new_fwd)} 条,allow-recursion {len(new_rc)} 条,"
|
||||
f"dnssec-validation {dnssec_in},listen-on {len(new_listen)} 项,"
|
||||
f"recursion {recursion_in}",
|
||||
"success",
|
||||
)
|
||||
return redirect(url_for("config_view"))
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user