4 Commits

Author SHA1 Message Date
Your Name a025d627b7 增强setup-bind.sh: 添加配置验证和错误路径警告 2026-08-07 11:54:20 +08:00
Your Name 450ffb89ae 修复 _replace_or_append_option 函数的复杂匹配逻辑bug,简化实现确保配置替换正确 2026-08-07 11:36:45 +08:00
Your Name 948d4cff9a 修复3个问题: 1) setup-bind.sh确保服务启动 2) DNSSEC配置验证options块包裹 3) 添加IPv6开关功能 2026-08-07 11:31:23 +08:00
Your Name e20e235a1e feat: one-click deploy.sh (system deps + venv + DB + systemd)
scripts/deploy.sh covers the full path from fresh OS to running service:
  1. install system packages (apt/yum/dnf auto-detect, includes bind/python3/venv/git)
  2. create Python venv + pip install -r requirements.txt (avoids PEP 668)
  3. init SQLite DB (default admin/admin)
  4. delegate BIND config to setup-bind.sh (or --skip-bind)
  5. install + enable systemd unit dns-web (gunicorn, port 5300)
  6. restart + verify (port listen + HTTP 302)

Idempotent; supports --dry-run, --skip-bind, --port, DNS_WEB_PORT/USER/WORKERS.
Type=exec (not Type=notify) because gunicorn 21.x has no sd_notify.
README updated with deploy.sh as recommended path; .gitignore adds venv/.
2026-08-07 11:19:01 +08:00
6 changed files with 475 additions and 131 deletions
+1
View File
@@ -5,3 +5,4 @@ instance/
*.bak
.env
.venv/
venv/
+45 -1
View File
@@ -81,7 +81,51 @@ dns-service/
## 安装部署
### 1. 安装 BIND9
### 方式一:一键部署(推荐)
`scripts/deploy.sh` 从干净系统一路到运行中的服务,自动完成:
1. 安装系统包(bind9 / bind-utils / python3 / pip / venv / git,自动识别 apt/yum/dnf
2. 创建 Python 虚拟环境 `venv/`,安装 `requirements.txt`(用 venv 的 pip,避开 Debian 12+ PEP 668
3. 初始化 SQLite 数据库(默认 `admin/admin`
4. 调用 `scripts/setup-bind.sh` 配置 BINDzone 目录、named.conf.local include、权限)
5. 安装并启用 systemd 服务 `dns-web`gunicorn,端口 53002 workers
6. 启动服务并验证端口 + HTTP 响应
```bash
git clone ssh://git@git.cnbugs.com:10022/AI-Agent/dns-service.git
cd dns-service
# 先预览会做什么(推荐):
sudo bash scripts/deploy.sh --dry-run
# 确认后真跑:
sudo bash scripts/deploy.sh
# 如果 BIND 已经配好了只想更新应用:
sudo bash scripts/deploy.sh --skip-bind
# 换端口:
sudo bash scripts/deploy.sh --port 5301
# 或用环境变量:
sudo DNS_WEB_PORT=5301 DNS_WEB_WORKERS=4 bash scripts/deploy.sh
```
脚本幂等,可重复执行。升级代码后重跑一次即可(venv 和 DB 保留,只补缺失部分)。
**可调环境变量:**
| 变量 | 默认 | 说明 |
|------|------|------|
| `DNS_WEB_PORT` | `5300` | 监听端口 |
| `DNS_WEB_USER` | `root` | 服务运行用户(需能操作 BIND 配置 + systemctl |
| `DNS_WEB_WORKERS` | `2` | gunicorn worker 数 |
> **关于 root 用户**Web 应用需要 root 权限来操作 `/etc/bind/*` 配置文件和执行 `systemctl`/`rndc` 命令。如果你的环境不允许 root 跑服务,用 `DNS_WEB_USER=<user>` 覆盖,并给该用户配 sudo 规则。systemd unit 用 `Type=exec`gunicorn 21.x 不带 sd_notify,用 `Type=notify` 会卡 90 秒超时然后失败)。
### 方式二:手动部署
适合需要完全控制每一步的场景。
```bash
apt-get update
+93 -100
View File
@@ -1083,6 +1083,24 @@ def _parse_listen_on_v4(content):
return items
def _parse_listen_on_v6(content):
"""Return 'any'/'none'/'localhost' — the IPv6 listen mode.
Returns 'none' if directive is absent (disabled by default).
"""
m = re.search(r'listen-on-v6(?:\s+port\s+\d+)?\s*\{([^}]*)\}', content, re.DOTALL)
if not m:
return "none"
body = m.group(1)
# Look for the main token: { any; } or { none; } or { localhost; }
for tm in re.finditer(r'"([^"]+)"|([\w./:-]+)', body):
token = tm.group(1) or tm.group(2)
if token in ("any", "none", "localhost"):
return token
# Fallback: if the block is not empty, assume "custom" mode
body_stripped = body.strip().strip(';').strip()
return "any" if body_stripped else "none"
def _parse_recursion(content):
"""Return 'yes' (default), 'no', or specific value from 'recursion <v>;'."""
m = re.search(r'recursion\s+(\w+)\s*;', content)
@@ -1093,78 +1111,44 @@ def _parse_recursion(content):
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
not present.
"""Replace an existing directive or append it inside the options block.
`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. Comments ('//', '#', '/*...*/')
are skipped so the directive name doesn't match inside comment text.
Simple and reliable version:
1. First try to match brace-delimited directives: 'directive { ... };'
2. Then try to match value-terminated directives: 'directive <value>;'
3. If not found, append after 'options {' opening line
"""
# Build a mask of which character ranges are inside comments. We treat the
# whole file linearly:
# - '//' to end-of-line: line comment (BIND and shell style)
# - '#' to end-of-line: line comment (BIND 9.18+ accepts # too)
# - '/* ... */': block comment
n = len(content)
in_comment = [False] * n # True at position i = i is inside a comment
i = 0
while i < n:
c = content[i]
# End of block comment
if in_comment[i] is False and i + 1 < n and c == '/' and content[i + 1] == '*':
j = i + 2
depth = 1
while j < n and depth > 0:
if j + 1 < n and content[j] == '*' and content[j + 1] == '/':
depth -= 1
j += 2
else:
j += 1
for k in range(i, j):
in_comment[k] = True
i = j
continue
# Line comment // ... \n
if in_comment[i] is False and i + 1 < n and c == '/' and content[i + 1] == '/':
j = i
while j < n and content[j] != '\n':
in_comment[j] = True
j += 1
i = j
continue
# Line comment # ... \n (skip only if at start of token; be conservative
# and treat any '#' preceded by whitespace or start-of-line as a comment)
if in_comment[i] is False and c == '#':
# Only treat as comment if preceded by whitespace or start-of-line
prev_ok = (i == 0) or content[i - 1] in ' \t'
if prev_ok:
j = i
while j < n and content[j] != '\n':
in_comment[j] = True
j += 1
i = j
continue
i += 1
pattern = re.compile(
r'(?<!\w)(?<!-)(' + re.escape(directive) + r')\b(?!-)(?!\w)',
# Pattern 1: brace-delimited (handles nested braces, ignores 'port X' prefixes)
# Match: directive [optional port N] { ... };
# The [\w\s.-]* matches things like " port 53"
pattern_brace = re.compile(
r'(?<!\w)(?<!-)' + re.escape(directive) + r'(?!-)(?!\w)[\w\s.-]*\{',
re.DOTALL
)
for m in pattern.finditer(content):
for m in pattern_brace.finditer(content):
start = m.start()
# Skip if this match is inside a comment
if in_comment[start]:
continue
end_kw = m.end()
if end_kw < len(content) and content[end_kw] == '-':
continue
brace_start = m.end() - 1 # position of '{'
# Detect indentation
# Find matching closing brace
depth = 1
i = brace_start + 1
while i < len(content) and depth > 0:
if content[i] == '{':
depth += 1
elif content[i] == '}':
depth -= 1
i += 1
if depth == 0:
# Found matching }, now find the trailing ;
end = i
while end < len(content) and content[end] in ' \t\n':
end += 1
if end < len(content) and content[end] == ';':
end += 1
# Find indentation from original line
line_start = content.rfind('\n', 0, start) + 1
indent = ''
for ch in content[line_start:start]:
@@ -1173,42 +1157,31 @@ def _replace_or_append_option(content, directive, new_block):
else:
break
# Look ahead: brace-delimited or value-terminated?
i = end_kw
while i < len(content) and content[i] in ' \t':
i += 1
if i >= len(content):
continue
if content[i] == '{':
depth = 0
for j in range(i, len(content)):
# Skip over comments inside the brace body
if in_comment[j]:
continue
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
# Pattern 2: value-terminated (simple values without braces)
pattern_value = re.compile(
r'(?<!\w)(?<!-)' + re.escape(directive) + r'(?!-)(?!\w)[^;]*;',
re.MULTILINE
)
m = pattern_value.search(content)
if m:
start = m.start()
end = m.end()
line_start = content.rfind('\n', 0, start) + 1
indent = ''
for ch in content[line_start:start]:
if ch in ' \t':
indent += ch
else:
end = i
while end < len(content) and content[end] != ';':
end += 1
if end < len(content):
end += 1
break
indented = '\n'.join(
(indent + line) if line else line
for line in new_block.split('\n')
@@ -1225,6 +1198,8 @@ def _replace_or_append_option(content, directive, new_block):
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:]
# No options block at all — create one
return 'options {\n' + new_block + '\n};\n'
@@ -1296,6 +1271,7 @@ def config_view():
dnssec_value = _parse_options_dnssec(options_content)
listen_on_v4 = _parse_listen_on_v4(options_content)
recursion_value = _parse_recursion(options_content)
listen_on_v6 = _parse_listen_on_v6(options_content)
return render_template("config.html",
options_content=options_content,
@@ -1308,6 +1284,7 @@ def config_view():
dnssec_value=dnssec_value,
dnssec_values=_VALIDATION_VALUES,
listen_on_v4=listen_on_v4,
listen_on_v6=listen_on_v6,
recursion_value=recursion_value)
@@ -1319,6 +1296,11 @@ def config_options_save():
run_cmd(f"cp {BIND_CONF_OPTIONS} {backup}")
# Write to temp and validate
# NOTE: BIND_CONF_OPTIONS content must have 'options { ... }' wrapper (it's a complete file)
# that gets included from named.conf, so named-checkconf can validate it directly
if not re.search(r'^\s*options\s*\{', content, re.MULTILINE):
# User may have accidentally removed the options wrapper in text editor
content = f"options {{\n{content}\n}};\n"
with open("/tmp/named_check.tmp", 'w') as f:
f.write(content)
rc, out, err = run_cmd("named-checkconf /tmp/named_check.tmp")
@@ -1330,7 +1312,7 @@ def config_options_save():
with open(BIND_CONF_OPTIONS, 'w') as f:
f.write(content)
run_cmd(f"chown root:bind {BIND_CONF_OPTIONS}")
run_cmd(f"chown root:bind {BIND_CONF_OPTIONS} 2>/dev/null || chown root:named {BIND_CONF_OPTIONS} 2>/dev/null")
bind_reload()
@@ -1392,6 +1374,9 @@ def config_upstream_save():
raw_fwd = request.form.get("forwarders", "")
raw_rc = request.form.get("recursion", "")
raw_listen = request.form.get("listen_on_v4", "")
listen_on_v6_in = (request.form.get("listen_on_v6") or "none").strip().lower()
if listen_on_v6_in not in ("any", "none", "localhost"):
listen_on_v6_in = "none"
dnssec_in = (request.form.get("dnssec") or "auto").strip().lower()
if dnssec_in not in _VALIDATION_VALUES:
dnssec_in = "auto"
@@ -1430,12 +1415,20 @@ def config_upstream_save():
new_content, "listen-on",
_format_list_block("listen-on port 53", new_listen),
)
new_content = _replace_or_append_option(
new_content, "listen-on-v6",
_format_list_block("listen-on-v6 port 53", [listen_on_v6_in]),
)
new_content = _replace_or_append_option(
new_content, "recursion",
f"recursion {recursion_in};",
f"recursion {recursion_in};\n",
)
# Validate BEFORE writing to the live file
# Ensure content has options block wrapper (required for named-checkconf)
if not re.search(r'^\s*options\s*\{', new_content, re.MULTILINE):
# No options block found — wrap the entire content
new_content = f"options {{\n{new_content}\n}};\n"
tmp = "/tmp/named_check.tmp"
with open(tmp, 'w') as f:
f.write(new_content)
@@ -1457,11 +1450,11 @@ def config_upstream_save():
u = current_user()
AuditLog.log(u.username, "修改上游DNS转发配置",
f"forwarders={new_fwd}; allow-recursion={new_rc}; "
f"dnssec={dnssec_in}; listen-on={new_listen}; recursion={recursion_in}")
f"dnssec={dnssec_in}; listen-on={new_listen}; listen-on-v6={listen_on_v6_in}; 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}",
f"listen-on-v6 {listen_on_v6_in}recursion {recursion_in}",
"success",
)
return redirect(url_for("config_view"))
+274
View File
@@ -0,0 +1,274 @@
#!/usr/bin/env bash
# deploy.sh - one-click deployment for DNS Web Manager
#
# From a fresh OS to a running service in one shot:
# 1. Install system packages (BIND, Python, pip, venv, git)
# 2. Create Python venv and install requirements.txt
# 3. Initialize SQLite database (default admin/admin)
# 4. Configure BIND (delegates to scripts/setup-bind.sh)
# 5. Install + enable systemd service (gunicorn, port 5300)
# 6. (Re)start the service and verify
#
# Usage:
# sudo bash scripts/deploy.sh # full deploy
# sudo bash scripts/deploy.sh --dry-run # preview, no changes
# sudo bash scripts/deploy.sh --skip-bind # skip BIND setup if already done
# sudo bash scripts/deploy.sh --port 5300 # override listen port
#
# Idempotent: safe to run multiple times. Existing venv/DB/config are
# preserved; only missing pieces are created.
#
# Tunables (env vars):
# DNS_WEB_PORT - listen port (default 5300)
# DNS_WEB_USER - service user (default root, needed for BIND config)
# DNS_WEB_WORKERS - gunicorn worker count (default 2)
set -euo pipefail
# ─── parse args ────────────────────────────────────────────────────────────
DRY_RUN=0
SKIP_BIND=0
SERVICE_PORT="${DNS_WEB_PORT:-5300}"
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run) DRY_RUN=1; shift ;;
--skip-bind) SKIP_BIND=1; shift ;;
--port) SERVICE_PORT="$2"; shift 2 ;;
-h|--help) sed -n '2,22p' "$0"; exit 0 ;;
*)
echo "Unknown argument: $1" >&2
exit 1
;;
esac
done
# ─── resolve project root from script location ─────────────────────────────
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$PROJECT_DIR"
SERVICE_NAME="dns-web"
SERVICE_USER="${DNS_WEB_USER:-root}"
WORKERS="${DNS_WEB_WORKERS:-2}"
VENV_DIR="$PROJECT_DIR/venv"
# ─── helpers ───────────────────────────────────────────────────────────────
log() { printf '\033[1;34m[deploy]\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m[deploy]\033[0m %s\n' "$*" >&2; }
err() { printf '\033[1;31m[deploy]\033[0m %s\n' "$*" >&2; }
run() {
if [[ $DRY_RUN -eq 1 ]]; then
printf ' [DRY-RUN] %s\n' "$*"
else
"$@"
fi
}
must_be_root() {
if [[ $EUID -ne 0 ]]; then
err "Must run as root (use sudo)"
exit 1
fi
}
# ─── detect distro via package-manager presence (not os-release) ──────────
# File-based detection for BIND layout is in setup-bind.sh; here we only need
# the package manager. Check command -v first, fall back to os-release.
detect_distro() {
if command -v apt-get >/dev/null 2>&1; then
DISTRO="debian"
PKG_INSTALL=(apt-get install -y)
SYS_PKGS=(bind9 bind9utils dnsutils python3 python3-pip python3-venv git)
elif command -v dnf >/dev/null 2>&1; then
DISTRO="rhel"
PKG_INSTALL=(dnf install -y)
SYS_PKGS=(bind bind-utils python3 python3-pip python3-virtualenv git)
elif command -v yum >/dev/null 2>&1; then
DISTRO="rhel"
PKG_INSTALL=(yum install -y)
SYS_PKGS=(bind bind-utils python3 python3-pip python3-virtualenv git)
else
err "Unsupported distro: no apt-get/yum/dnf found"
exit 1
fi
log "Detected distro: $DISTRO (pkg mgr: ${PKG_INSTALL[0]})"
}
# ─── steps ────────────────────────────────────────────────────────────────
install_system_deps() {
log "Step 1: install system packages"
log " packages: ${SYS_PKGS[*]}"
if [[ "$DISTRO" == "debian" ]]; then
# apt-get needs a refresh of the package index first; yum/dnf don't
run apt-get update -qq
fi
run "${PKG_INSTALL[@]}" "${SYS_PKGS[@]}"
}
setup_venv() {
log "Step 2: set up Python venv"
if [[ ! -d "$VENV_DIR" ]]; then
log " creating venv at $VENV_DIR"
run python3 -m venv "$VENV_DIR"
else
log " venv already exists, reusing"
fi
log " upgrading pip"
run "$VENV_DIR/bin/pip" install --upgrade pip --quiet
log " installing requirements.txt"
run "$VENV_DIR/bin/pip" install -r "$PROJECT_DIR/requirements.txt" --quiet
}
init_database() {
log "Step 3: initialize SQLite database"
run mkdir -p "$PROJECT_DIR/instance"
run "$VENV_DIR/bin/python" "$PROJECT_DIR/init_db.py"
}
configure_bind() {
log "Step 4: configure BIND (delegating to setup-bind.sh)"
if [[ ! -f "$SCRIPT_DIR/setup-bind.sh" ]]; then
warn " $SCRIPT_DIR/setup-bind.sh not found; skipping BIND setup"
return
fi
if [[ $DRY_RUN -eq 1 ]]; then
printf ' [DRY-RUN] bash %s/setup-bind.sh\n' "$SCRIPT_DIR"
else
bash "$SCRIPT_DIR/setup-bind.sh"
fi
}
install_systemd_unit() {
log "Step 5: install systemd service ($SERVICE_NAME on port $SERVICE_PORT)"
local unit_file="/etc/systemd/system/${SERVICE_NAME}.service"
local gunicorn_bin="$VENV_DIR/bin/gunicorn"
# Note on Type=exec vs Type=notify: gunicorn 21.x does NOT ship sd_notify
# support out of the box. Type=notify would hang for DefaultTimeoutStartSec
# (90s) then mark the unit failed. Type=exec considers the unit "running"
# as soon as the exec() succeeds, which is what we want.
local unit_content
unit_content="[Unit]
Description=DNS Web Manager (BIND9 web UI)
Documentation=https://git.cnbugs.com/AI-Agent/dns-service
After=network.target named.service
Wants=named.service
[Service]
Type=exec
User=$SERVICE_USER
WorkingDirectory=$PROJECT_DIR
ExecStart=$gunicorn_bin --workers $WORKERS --bind 0.0.0.0:$SERVICE_PORT wsgi:app
Restart=always
RestartSec=5
# Optional: override BIND paths via env vars (see README \"路径优先级\")
# Environment=BIND_ZONES_DIR=/opt/bind/zones
[Install]
WantedBy=multi-user.target
"
if [[ $DRY_RUN -eq 1 ]]; then
printf ' [DRY-RUN] write %s\n' "$unit_file"
printf '%s\n' "$unit_content" | sed 's/^/ | /'
else
printf '%s\n' "$unit_content" > "$unit_file"
chmod 644 "$unit_file"
log " wrote $unit_file"
systemctl daemon-reload
systemctl enable "$SERVICE_NAME" 2>&1 | grep -v 'Created symlink' || true
log " enabled $SERVICE_NAME"
fi
}
restart_service() {
log "Step 6: (re)start $SERVICE_NAME"
if [[ $DRY_RUN -eq 1 ]]; then
printf ' [DRY-RUN] systemctl restart %s\n' "$SERVICE_NAME"
return
fi
systemctl restart "$SERVICE_NAME"
sleep 2
if systemctl is-active --quiet "$SERVICE_NAME"; then
log " $SERVICE_NAME is active"
else
err " $SERVICE_NAME failed to start"
err " check: journalctl -xe -u $SERVICE_NAME -n 30"
return 1
fi
}
verify() {
log "Step 7: verify deployment"
if [[ $DRY_RUN -eq 1 ]]; then
return
fi
# Gunicorn needs a moment to bind the port after exec(); retry a few
# times so we don't false-alarm "not listening" on a slow box.
local port_ok=0 http_code
for _ in 1 2 3 4 5; do
if ss -lnt 2>/dev/null | grep -qE ":$SERVICE_PORT\b"; then
port_ok=1
break
fi
sleep 1
done
if [[ $port_ok -eq 1 ]]; then
log " port $SERVICE_PORT is listening"
else
warn " port $SERVICE_PORT not listening after 5s (may still be warming up)"
fi
# HTTP responding? 302 = login redirect (expected), 200 = some page.
http_code=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:$SERVICE_PORT/" --max-time 5 || echo "000")
if [[ "$http_code" == "302" || "$http_code" == "200" ]]; then
log " HTTP check: $http_code (302=login redirect, 200=page)"
else
warn " HTTP check returned $http_code (expected 302/200)"
warn " check: journalctl -u $SERVICE_NAME -n 30"
fi
}
print_summary() {
log "════════════════════════════════════════════════════════"
log " Deploy complete"
log "════════════════════════════════════════════════════════"
log " Service : systemctl status $SERVICE_NAME"
log " Logs : journalctl -u $SERVICE_NAME -f"
log " URL : http://$(hostname -I 2>/dev/null | awk '{print $1}'):${SERVICE_PORT}"
log " Login : admin / admin (change after first login!)"
log " Reload : bash $SCRIPT_DIR/deploy.sh"
if [[ "$SERVICE_USER" == "root" ]]; then
warn " Runs as root (required to manage BIND config + systemctl)."
warn " If your env blocks root services, override with DNS_WEB_USER=<user>"
warn " and grant that user sudo on systemctl/named/chown instead."
fi
log "════════════════════════════════════════════════════════"
}
# ─── main ─────────────────────────────────────────────────────────────────
main() {
must_be_root
detect_distro
if [[ $DRY_RUN -eq 1 ]]; then
log "DRY-RUN mode: no changes will be made"
log " project dir : $PROJECT_DIR"
log " venv : $VENV_DIR"
log " service : $SERVICE_NAME ($SERVICE_USER@$SERVICE_PORT, $WORKERS workers)"
fi
install_system_deps
setup_venv
init_database
if [[ $SKIP_BIND -eq 0 ]]; then
configure_bind
else
warn "Skipping BIND setup (--skip-bind)"
fi
install_systemd_unit
restart_service
verify
print_summary
}
main "$@"
+32 -11
View File
@@ -120,6 +120,11 @@ ensure_named_conf_local() {
run chmod 640 "$NAMED_CONF_LOCAL"
else
log " $NAMED_CONF_LOCAL already exists; preserving"
# Check for common path misconfigurations
if grep -q '/opt/bind' "$NAMED_CONF_LOCAL"; then
warn " ⚠️ WARNING: Found '/opt/bind' path in $NAMED_CONF_LOCAL"
warn " This is likely incorrect! Expected zones directory: $ZONES_DIR"
fi
# Don't change ownership if file already has content — it may be managed
# by someone else. Just make sure web process can write to it.
run chmod 644 "$NAMED_CONF_LOCAL"
@@ -145,21 +150,37 @@ ensure_include_in_named_conf() {
}
restart_named() {
log "Step 5: restart named so include takes effect"
if ! systemctl is-active --quiet "$SERVICE_NAME"; then
warn " $SERVICE_NAME not running; skipping restart"
log "Step 5: verify config before starting"
# Always validate config first
if ! named-checkconf >/dev/null 2>&1; then
err " BIND configuration has errors!"
err " Running 'named-checkconf' to show details:"
named-checkconf
exit 1
fi
log " Configuration syntax OK"
log "Step 6: start/enable named service"
# Always enable and start/restart the service
if [[ $DRY_RUN -eq 1 ]]; then
printf ' [DRY-RUN] systemctl enable named\n'
printf ' [DRY-RUN] systemctl restart named (or start if not running)\n'
printf ' [DRY-RUN] sleep 1\n'
printf ' [DRY-RUN] systemctl is-active --quiet named && echo "named is active"\n'
return
fi
if [[ $DRY_RUN -eq 1 ]]; then
printf ' [DRY-RUN] systemctl restart %s\n' "$SERVICE_NAME"
systemctl enable named 2>/dev/null || true
if systemctl is-active --quiet named; then
systemctl restart named
else
run systemctl restart "$SERVICE_NAME"
sleep 1
if systemctl is-active --quiet "$SERVICE_NAME"; then
log " $SERVICE_NAME is active"
else
err " $SERVICE_NAME failed to start; check 'journalctl -xe -u $SERVICE_NAME'"
systemctl start named
fi
sleep 1
if systemctl is-active --quiet named; then
log " named is active and running"
else
err " named failed to start; check 'journalctl -xe -u named'"
exit 1
fi
}
+13 -2
View File
@@ -68,14 +68,25 @@
placeholder="any&#10;或具体网段,如:&#10;192.168.1.0/24&#10;10.0.0.0/8">{% for net in listen_on_v4 %}{{ net }}
{% endfor %}</textarea>
</div>
<div class="form-group">
<label for="listen_on_v6">
<strong>Listen-on-v6 (IPv6) 53 端口</strong>
<span class="text-muted text-sm">启用或禁用 IPv6 DNS 监听。内网无 IPv6 时选 <code>none</code>(关闭)。</span>
</label>
<select name="listen_on_v6" id="listen_on_v6" class="form-control">
<option value="none" {% if listen_on_v6 == 'none' %}selected{% endif %}>none — 关闭 IPv6 监听</option>
<option value="any" {% if listen_on_v6 == 'any' %}selected{% endif %}>any — 开启 IPv6 监听(所有接口)</option>
<option value="localhost" {% if listen_on_v6 == 'localhost' %}selected{% endif %}>localhost — 仅本机 IPv6</option>
</select>
</div>
<div class="form-group">
<label for="recursion_toggle">
<strong>Recursion (递归查询)</strong>
<span class="text-muted text-sm">是否允许本 DNS 代客户端去外网查询。内网递归服务器选 <code>yes</code>;纯权威服务器选 <code>no</code></span>
</label>
<select name="recursion_toggle" id="recursion_toggle" class="form-control">
<option value="yes" {% if recursion_value == 'yes' %}selected{% endif %}>yes允许递归</option>
<option value="no" {% if recursion_value == 'no' %}selected{% endif %}>no仅权威应答,不递归</option>
<option value="yes" {% if recursion_value == 'yes' %}selected{% endif %}>yes允许递归查询</option>
<option value="no" {% if recursion_value == 'no' %}selected{% endif %}>no仅权威应答,不递归</option>
</select>
</div>
<div class="form-actions">