diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f193586..dbee3f6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -57,6 +57,17 @@ jobs: echo "$out" | grep -q '^skipped' && { echo "::error::BIND missing on the runner"; exit 1; } exit $rc + - name: test_plesk_acl.sh + run: bash hosting-panels/common/tests/test_plesk_acl.sh + + # needs the bind9 installed by the step above + - name: test_plesk_acl_named.sh + run: | + out=$(bash hosting-panels/common/tests/test_plesk_acl_named.sh); rc=$? + echo "$out" + echo "$out" | grep -q '^skipped' && { echo "::error::BIND missing on the runner"; exit 1; } + exit $rc + - name: test_valid_ip.sh run: bash hosting-panels/common/tests/test_valid_ip.sh diff --git a/README.md b/README.md index a349fac..4a9b41f 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ Both integrations use the SecondDNS API key. See the README in each directory fo - SecondDNS account and API key — [get one here](https://seconddns.com/dashboard/api-key) - TCP port 53 open from your server to the SecondDNS secondary nameserver IP -- BIND or PowerDNS with `allow-transfer` and `also-notify` for the secondary IP — the installer configures this after asking, and keeps a backup of the file it edits +- BIND or PowerDNS with `allow-transfer` and `also-notify` for the secondary IP — the installer configures this after asking, and keeps a backup of the file it edits (on Plesk, transfers are opened through the Transfer Restrictions Template instead, see [Plesk](hosting-panels/plesk/)) --- diff --git a/hosting-panels/common/tests/test_named_conf_edit.sh b/hosting-panels/common/tests/test_named_conf_edit.sh index 7d9639a..a725631 100755 --- a/hosting-panels/common/tests/test_named_conf_edit.sh +++ b/hosting-panels/common/tests/test_named_conf_edit.sh @@ -19,7 +19,7 @@ block() { # $1 file, $2 block name -> prints the block } checked=0 -for panel in cpanel plesk directadmin; do +for panel in cpanel directadmin; do src="$HERE/../../$panel/install.sh" [ -f "$src" ] || { echo "missing $src"; exit 1; } grep -q "configure_named_axfr" "$src" || { echo "$panel: no configure_named_axfr"; exit 1; } @@ -63,14 +63,20 @@ for panel in cpanel plesk directadmin; do done echo "== cheap greps: every installer verifies before reporting" -for panel in cpanel plesk directadmin; do +for panel in cpanel directadmin; do src="$HERE/../../$panel/install.sh" grep -qE 'if named-checkconf' "$src" && ok "$panel gates the reload on named-checkconf" || fail "$panel reloads without verifying" grep -q "did not validate" "$src" && ok "$panel reports a broken config" || fail "$panel would claim success" grep -qE 'cp "\$NAMED_BAK"' "$src" && ok "$panel restores the backup" || fail "$panel has no rollback" done -[ "$checked" -ge 3 ] && ok "checked $checked installers" || fail "only $checked matched" +[ "$checked" -ge 2 ] && ok "checked $checked installers" || fail "only $checked matched" + +echo "== plesk: zones carry allow-transfer { common-allow-transfer; }, so options must not be edited" +src="$HERE/../../plesk/install.sh" +grep -q "configure_named_axfr" "$src" && fail "plesk still edits the options block" || ok "plesk no longer edits options" +grep -q "allow-transfer { \$" "$src" && fail "plesk still tells the customer to put allow-transfer in server-wide settings" || ok "no allow-transfer advice for server-wide settings" +grep -q "server_dns -u -add-acl" "$src" && ok "plesk uses the transfer ACL" || fail "plesk does not use server_dns -add-acl" echo echo "passed=$PASS failed=$FAIL" diff --git a/hosting-panels/common/tests/test_plesk_acl.sh b/hosting-panels/common/tests/test_plesk_acl.sh new file mode 100755 index 0000000..6344678 --- /dev/null +++ b/hosting-panels/common/tests/test_plesk_acl.sh @@ -0,0 +1,96 @@ +#!/bin/bash +# Copyright © 2025-2026 SecondDNS +# Licensed under GNU General Public License v3.0 or SecondDNS Commercial License +# See LICENSE (GPLv3) or LICENSE.COMMERCIAL (commercial) for details +# configure_plesk_acl, as the Plesk installer defines it, against a stand-in for the plesk +# CLI that behaves like Plesk 18.0.81 (checked on a live server): the transfer ACL lives in +# misc.DNS_Allow_Transfer*, and server_dns -u -add-acl adds to it and rewrites +# acl common-allow-transfer in named.conf at once. +# Requires: bash, awk. No network. +# +# Usage: ./test_plesk_acl.sh + +set -u + +HERE="$(cd "$(dirname "$0")" && pwd)" +SRC="$HERE/../../plesk/install.sh" +TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT + +PASS=0; FAIL=0 +ok() { PASS=$((PASS+1)); echo " ok: $1"; } +fail() { FAIL=$((FAIL+1)); echo " FAIL: $1"; } +assert_eq() { [ "$1" = "$2" ] && ok "$3" || fail "$3 (expected '$2', got '$1')"; } + +# stand-in plesk: $TMP/acl is misc.DNS_Allow_Transfer*, one IP per line +cat > "$TMP/plesk" <<'EOF' +#!/bin/bash +T="$(dirname "$0")" +case "$1 $2" in + "db -Ne") cat "$T/acl" 2>/dev/null ;; + "bin server_dns") + echo "$*" >> "$T/calls" + [ -e "$T/add-fails" ] && exit 1 + tr ',' '\n' <<< "${5}" >> "$T/acl" + [ -e "$T/no-regen" ] && exit 0 + { echo 'options { directory "/var"; };' + printf 'acl common-allow-transfer {\n'; sed 's/^/\t/; s/$/;/' "$T/acl"; printf '};\n' + echo 'zone "a.example" { type master; file "a.example"; allow-transfer { common-allow-transfer; }; };' + } > "$T/named.conf" ;; +esac +EOF +chmod +x "$TMP/plesk" +export SECONDDNS_PLESK="$TMP/plesk" SECONDDNS_PLESK_NAMED_CONF="$TMP/named.conf" + +reset() { + rm -f "$TMP/acl" "$TMP/calls" "$TMP/add-fails" "$TMP/no-regen" + printf 'options { directory "/var"; };\nacl common-allow-transfer {\n\tnone;\n};\n' > "$TMP/named.conf" +} +acl() { awk '/^acl common-allow-transfer/,/^};/' "$TMP/named.conf" | grep -v -e acl -e '};' | tr -d '\t;' | tr '\n' ' '; } + +grep -q '^configure_plesk_acl() {' "$SRC" || { echo "configure_plesk_acl not found in $SRC"; exit 1; } +eval "$(sed -n '/^configure_plesk_acl() {/,/^}/p' "$SRC")" + +echo "== 1. empty ACL: both addresses added in one call, named.conf has them" +reset +out=$(configure_plesk_acl "203.0.113.9,2001:db8::9"); rc=$? +assert_eq "$rc" 0 "returns 0" +assert_eq "$(cat "$TMP/calls")" "bin server_dns -u -add-acl 203.0.113.9,2001:db8::9" "one add-acl call with both IPs" +assert_eq "$(acl)" "203.0.113.9 2001:db8::9 " "acl common-allow-transfer lists both" + +echo "== 2. second run: nothing added" +: > "$TMP/calls" +out=$(configure_plesk_acl "203.0.113.9,2001:db8::9"); rc=$? +assert_eq "$rc" 0 "returns 0" +assert_eq "$(wc -l < "$TMP/calls" | tr -d ' ')" 0 "no add-acl call" +echo "$out" | grep -q "already allows" && ok "says it is already there" || fail "output: $out" + +echo "== 3. one address present: only the other is added" +reset +echo "203.0.113.9" > "$TMP/acl" +configure_plesk_acl "203.0.113.9,2001:db8::9" > /dev/null +assert_eq "$(cat "$TMP/calls")" "bin server_dns -u -add-acl 2001:db8::9" "only the missing IP" + +echo "== 4. a similar address is not taken for ours" +reset +echo "203.0.113.90" > "$TMP/acl" +configure_plesk_acl "203.0.113.9" > /dev/null +assert_eq "$(cat "$TMP/calls")" "bin server_dns -u -add-acl 203.0.113.9" "203.0.113.90 does not count as 203.0.113.9" + +echo "== 5. named.conf not rewritten: reported, not claimed" +reset +touch "$TMP/no-regen" +out=$(configure_plesk_acl "203.0.113.9"); rc=$? +[ "$rc" -ne 0 ] && ok "returns non-zero" || fail "claimed success with the ACL still none" +echo "$out" | grep -q "not in acl common-allow-transfer" && ok "names the problem" || fail "output: $out" +echo "$out" | grep -q "Transfer Restrictions Template" && ok "says where to add it" || fail "no hint: $out" + +echo "== 6. add-acl fails: reported" +reset +touch "$TMP/add-fails" +out=$(configure_plesk_acl "203.0.113.9"); rc=$? +[ "$rc" -ne 0 ] && ok "returns non-zero" || fail "claimed success after a failed add-acl" +assert_eq "$(acl)" "none " "ACL untouched" + +echo +echo "passed: $PASS, failed: $FAIL" +[ "$FAIL" -eq 0 ] diff --git a/hosting-panels/common/tests/test_plesk_acl_named.sh b/hosting-panels/common/tests/test_plesk_acl_named.sh new file mode 100755 index 0000000..037dd5d --- /dev/null +++ b/hosting-panels/common/tests/test_plesk_acl_named.sh @@ -0,0 +1,97 @@ +#!/bin/bash +# Copyright © 2025-2026 SecondDNS +# Licensed under GNU General Public License v3.0 or SecondDNS Commercial License +# See LICENSE (GPLv3) or LICENSE.COMMERCIAL (commercial) for details +# A real named with a Plesk-style named.conf: every zone has +# allow-transfer { common-allow-transfer; }. Our IP in the options block (what the old +# installer wrote) opens nothing; the same IP in the ACL, put there by +# configure_plesk_acl, does. Requires: named, dig. Skips itself when BIND is not installed. +# +# Usage: ./test_plesk_acl_named.sh + +set -u + +for t in named dig; do + command -v "$t" > /dev/null || { echo "skipped: $t not installed"; exit 0; } +done + +HERE="$(cd "$(dirname "$0")" && pwd)" +SRC="$HERE/../../plesk/install.sh" +TMP="$(mktemp -d)" +PORT=5301 +CONF="$TMP/named.conf" + +PASS=0; FAIL=0 +ok() { PASS=$((PASS+1)); echo " ok: $1"; } +fail() { FAIL=$((FAIL+1)); echo " FAIL: $1"; } + +cat > "$TMP/a.example" <<'EOF' +$TTL 300 +@ IN SOA ns1.a.example. hostmaster.a.example. 1 300 60 86400 60 +@ IN NS ns1.a.example. +ns1 IN A 192.0.2.53 +www IN A 192.0.2.10 +EOF + +# written the way Plesk writes it; $TMP/acl holds misc.DNS_Allow_Transfer* +write_conf() { + { cat < "$CONF" +} + +# the stand-in rewrites the config like Plesk does, then named is reloaded +cat > "$TMP/plesk" </dev/null ;; + "bin server_dns") tr ',' '\n' <<< "\${5}" >> "\$TMP/acl"; write_conf; kill -HUP \$(cat "\$TMP/named.pid"); sleep 1 ;; +esac +EOF +chmod +x "$TMP/plesk" +export SECONDDNS_PLESK="$TMP/plesk" SECONDDNS_PLESK_NAMED_CONF="$CONF" + +write_conf +named -c "$CONF" -g > "$TMP/named.log" 2>&1 & +NAMED_PID=$! +disown $NAMED_PID 2>/dev/null || true +trap 'kill $NAMED_PID 2>/dev/null; rm -rf "$TMP"' EXIT +for i in $(seq 1 20); do dig +short -p $PORT @127.0.0.1 a.example SOA > /dev/null 2>&1 && break; sleep 0.5; done + +transfers() { dig +tries=1 +time=3 -p $PORT @127.0.0.1 -b "$1" a.example AXFR 2>&1 | grep -q 'www.a.example.*192.0.2.10'; } + +echo "== 1. our IP only in options (the old installer): no transfer" +transfers 127.0.0.1 && fail "options allow-transfer opened a Plesk zone" || ok "zone-level ACL overrides options: refused" + +echo "== 2. configure_plesk_acl puts our IP in the ACL" +eval "$(sed -n '/^configure_plesk_acl() {/,/^}/p' "$SRC")" +out=$(configure_plesk_acl "127.0.0.1"); rc=$? +[ "$rc" -eq 0 ] && ok "configure_plesk_acl succeeds" || fail "configure_plesk_acl: $out" +transfers 127.0.0.1 && ok "our IP transfers" || fail "still refused after the ACL change" +transfers 127.0.0.2 && fail "an address outside the ACL transfers" || ok "other addresses stay refused" + +echo +echo "passed: $PASS, failed: $FAIL" +[ "$FAIL" -eq 0 ] diff --git a/hosting-panels/plesk/README.md b/hosting-panels/plesk/README.md index 0196ae4..7bf27e8 100644 --- a/hosting-panels/plesk/README.md +++ b/hosting-panels/plesk/README.md @@ -68,19 +68,23 @@ The installer: ## AXFR Configuration -The installer configures `allow-transfer` and `also-notify` in `named.conf.options` directly. However, Plesk may overwrite these settings during config regeneration. +Plesk writes `allow-transfer { common-allow-transfer; };` into every zone, so `allow-transfer` in the BIND `options` block (or in **Server-wide Settings → Additional DNS settings**, which lands there) does not open transfers for Plesk zones. The ACL `common-allow-transfer` is what counts. Plesk builds it from **Tools & Settings → DNS Settings → Transfer Restrictions Template** and keeps it across config regeneration. -To make AXFR settings permanent, also add them via Plesk UI: +The installer adds the SecondDNS addresses to that template, after asking: -1. Go to **Tools & Settings** → **DNS Settings** → **Server-wide Settings** -2. In the **Additional DNS settings** field, add: +```bash +plesk bin server_dns -u -add-acl SECONDARY_IP[,SECONDARY_IPV6] +``` + +It skips addresses already listed and checks that they appear in `acl common-allow-transfer` in `named.conf`. On Plesk 18.0.81 the call rewrites `named.conf` and reloads named immediately. If an address does not show up, the installer says so; add it in **Transfer Restrictions Template** by hand. + +NOTIFY is not set per zone, so the server-wide settings do reach it. For instant updates, add this to **Server-wide Settings → Additional DNS settings**: ``` -allow-transfer { SECONDARY_IP; }; also-notify { SECONDARY_IP; }; ``` -Replace `SECONDARY_IP` with the IP shown by the installer. Click **Apply**. +The uninstaller does not remove the addresses from the Transfer Restrictions Template (`server_dns` has no command for it): remove them there by hand. ## DNS Template diff --git a/hosting-panels/plesk/install.sh b/hosting-panels/plesk/install.sh index 6ec6e25..8e0adeb 100755 --- a/hosting-panels/plesk/install.sh +++ b/hosting-panels/plesk/install.sh @@ -74,41 +74,35 @@ command -v python3 >/dev/null 2>&1 || { exit 1 } -# Add the secondary to allow-transfer and also-notify inside the options block. -# Bounded to options: a config with views has a closing brace per block, and an -# unbounded insert lands in logging. -configure_named_axfr() { - local conf="$1" ip="$2" tmp - tmp="$(mktemp)" - - _rewrite() { cat > "$tmp" && cat "$tmp" > "$conf"; } - - # cPanel writes the ACL quoted, other panels bare - sed -e '/allow-transfer/s/"none";//g' -e '/allow-transfer/s/[[:space:]]none;//g' \ - -e '/also-notify/s/"none";//g' -e '/also-notify/s/[[:space:]]none;//g' "$conf" | _rewrite - - if grep -q "allow-transfer" "$conf"; then - grep -q "allow-transfer.*$ip" "$conf" || \ - sed "s|allow-transfer[[:space:]]*{|allow-transfer { $ip; |" "$conf" | _rewrite - else - awk -v ip="$ip" ' - /^options[[:space:]]*{/ { inopt = 1 } - inopt && /^};/ { print " allow-transfer { " ip "; };"; inopt = 0 } - { print }' "$conf" | _rewrite +# Plesk writes allow-transfer { common-allow-transfer; } into every zone, so an IP in +# the options block opens nothing. The ACL comes from Tools & Settings > DNS Settings > +# Transfer Restrictions Template (misc.DNS_Allow_Transfer*), which server_dns -add-acl +# fills and Plesk keeps across config regeneration. On Plesk 18.0.81 the call rewrites +# named.conf and reloads named at once; the result is still checked, not assumed. +configure_plesk_acl() { # $1 = comma-separated IPs + local plesk_bin="${SECONDDNS_PLESK:-plesk}" + local conf="${SECONDDNS_PLESK_NAMED_CONF:-$(readlink -f /etc/named.conf 2>/dev/null || echo /etc/named.conf)}" + local have missing="" ip + have=$("$plesk_bin" db -Ne "select val from misc where param like 'DNS_Allow_Transfer%'" 2>/dev/null) + for ip in ${1//,/ }; do + printf '%s\n' "$have" | grep -qxF "$ip" || missing="${missing:+$missing,}$ip" + done + if [ -z "$missing" ]; then + echo "[+] Transfer Restrictions Template already allows $1" + return 0 fi - - if grep -q "also-notify" "$conf"; then - grep -q "also-notify.*$ip" "$conf" || \ - sed "s|also-notify[[:space:]]*{|also-notify { $ip; |" "$conf" | _rewrite - else - awk -v ip="$ip" ' - /^options[[:space:]]*{/ { inopt = 1 } - inopt && /^};/ { print " also-notify { " ip "; };"; inopt = 0 } - { print }' "$conf" | _rewrite + if ! "$plesk_bin" bin server_dns -u -add-acl "$missing" >/dev/null 2>&1; then + echo "[!] plesk bin server_dns -add-acl $missing failed" + return 1 fi - - rm -f "$tmp" - unset -f _rewrite + for ip in ${missing//,/ }; do + if ! awk '/^acl common-allow-transfer/,/^};/' "$conf" | grep -qF "$ip;"; then + echo "[!] $ip is not in acl common-allow-transfer of $conf" + echo " Add it in Tools & Settings > DNS Settings > Transfer Restrictions Template" + return 1 + fi + done + echo "[+] Transfer Restrictions Template: added $missing (acl common-allow-transfer)" } echo "=== SecondDNS Plesk Integration ===" @@ -408,60 +402,23 @@ echo "--- AXFR configuration ---" if [ -z "$DNS_IPS" ]; then echo "[!] No secondary DNS IP — configure AXFR manually" else - SECONDARY_IP="${DNS_IPS%%,*}" - echo "[+] Secondary DNS IP: $SECONDARY_IP" - - # Find BIND config - NAMED_OPTIONS="" - for f in /etc/bind/named.conf.options /etc/named.conf.options /etc/named.conf; do - [ -f "$f" ] && NAMED_OPTIONS="$f" && break - done + echo "[+] Secondary DNS IPs: $DNS_IPS" - if [ -n "$NAMED_OPTIONS" ]; then - echo "[=] Detected BIND config: $NAMED_OPTIONS" - - NEEDS_FIX=0 - if ! grep -q "allow-transfer.*$SECONDARY_IP" "$NAMED_OPTIONS" 2>/dev/null; then - NEEDS_FIX=1 - fi - if ! grep -q "also-notify.*$SECONDARY_IP" "$NAMED_OPTIONS" 2>/dev/null; then - NEEDS_FIX=1 - fi - - if [ "$NEEDS_FIX" -eq 1 ]; then - if confirm "Add allow-transfer and also-notify to $NAMED_OPTIONS?"; then - NAMED_BAK="${NAMED_OPTIONS}.bak.$(date +%s)" - cp "$NAMED_OPTIONS" "$NAMED_BAK" - - # allow-transfer - configure_named_axfr "$NAMED_OPTIONS" "$SECONDARY_IP" - - - if named-checkconf >/dev/null 2>&1; then - rndc reload >/dev/null 2>&1 || systemctl reload named >/dev/null 2>&1 || true - echo "[+] BIND configured and reloaded" - else - # a config named cannot parse is worse than no change - cp "$NAMED_BAK" "$NAMED_OPTIONS" - rndc reload >/dev/null 2>&1 || systemctl reload named >/dev/null 2>&1 || true - echo "[!] named.conf did not validate — restored from the backup" - echo "[!] Check: named-checkconf" - fi - fi - else - echo "[+] BIND AXFR config already includes $SECONDARY_IP" + if command -v plesk >/dev/null 2>&1; then + if confirm "Allow zone transfers to $DNS_IPS in Plesk's Transfer Restrictions Template?"; then + configure_plesk_acl "$DNS_IPS" || true fi + else + echo "[!] plesk CLI not found — add $DNS_IPS in Tools & Settings > DNS Settings > Transfer Restrictions Template" fi + # also-notify is not set per zone by Plesk, so the server-wide settings do reach it echo "" - echo -e "\033[1;33m[!] IMPORTANT: Plesk may overwrite direct config changes.\033[0m" - echo -e "\033[1;33m To make AXFR settings permanent, also add them in:\033[0m" - echo "" + echo -e "\033[1;33m[!] For instant updates, add NOTIFY to the server-wide settings:\033[0m" echo -e " \033[1mTools & Settings > DNS Settings > Server-wide Settings\033[0m" echo -e " \033[1m> Additional DNS settings:\033[0m" echo "" - echo -e " \033[1;32mallow-transfer { $SECONDARY_IP; };\033[0m" - echo -e " \033[1;32malso-notify { $SECONDARY_IP; };\033[0m" + echo -e " \033[1;32malso-notify { ${DNS_IPS//,/; }; };\033[0m" echo "" echo -e " \033[1;33mThen click Apply.\033[0m" fi @@ -493,11 +450,10 @@ echo " automatically synced to your secondary DNS." echo "" echo " Verify handlers: plesk bin event_handler --list" if [ -n "$DNS_IPS" ]; then - SECONDARY_IP="${DNS_IPS%%,*}" echo "" - echo -e "\033[1;33m[!] Don't forget to configure AXFR in Plesk UI:\033[0m" - echo -e " \033[1mTools & Settings > DNS Settings > Server-wide Settings\033[0m" - echo -e " Add to 'Additional DNS settings':" - echo -e " \033[1;32mallow-transfer { $SECONDARY_IP; };\033[0m" - echo -e " \033[1;32malso-notify { $SECONDARY_IP; };\033[0m" + echo -e "\033[1;33m[!] AXFR in Plesk:\033[0m" + echo -e " Transfers: Tools & Settings > DNS Settings > Transfer Restrictions Template" + echo -e " must list ${DNS_IPS//,/, } (the installer adds them when allowed)" + echo -e " NOTIFY: Server-wide Settings > Additional DNS settings:" + echo -e " \033[1;32malso-notify { ${DNS_IPS//,/; }; };\033[0m" fi diff --git a/hosting-panels/plesk/uninstall.sh b/hosting-panels/plesk/uninstall.sh index 8add921..07b45b4 100755 --- a/hosting-panels/plesk/uninstall.sh +++ b/hosting-panels/plesk/uninstall.sh @@ -105,6 +105,12 @@ for f in /etc/bind/named.conf.options /etc/named.conf.options; do break done +# the installer added our IPs to Plesk's transfer ACL; server_dns has no command to take +# them out, and editing Plesk's database directly is not ours to do +echo "" +echo "[!] SecondDNS IPs stay in Plesk's zone-transfer ACL. Remove them in:" +echo " Tools & Settings > DNS Settings > Transfer Restrictions Template" + # Remove config if [ -f "$CONFIG_FILE" ]; then rm -f "$CONFIG_FILE"