From 3e59e16836cb5abeb3f3feb3fb3e20adc1a45ffb Mon Sep 17 00:00:00 2001 From: Ihor Rusyn <0kaba0@gmail.com> Date: Sun, 6 Sep 2026 13:38:04 +0200 Subject: [PATCH 1/2] Edit only the options block of named.conf, and verify the result MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switching the cPanel box to BIND and rerunning the installer broke DNS. The insert was: sed -i "/^[[:space:]]*};/i\\ also-notify { IP; };" which writes before *every* closing brace in the file. cPanel ships a named.conf with three views and a logging block, so also-notify landed inside logging and inside a log channel: /etc/named.conf:30: unknown option 'also-notify' /etc/named.conf:34: unknown option 'also-notify' named stopped answering, and the installer printed "[+] BIND configured and reloaded". On a flat named.conf with one block the same code looked correct, which is why it survived. Also, cPanel's ACL is quoted — `allow-transfer { "none"; };` — and the code stripped only an unquoted `none;`, leaving `{ IP; "none"; }`. - the insert is bounded to the options block, as DirectAdmin already did - the quoted "none" is removed too, in all three installers - after editing, named-checkconf decides: a config it rejects is restored from the backup and reported, instead of reloading and claiming success Verified against the real cPanel named.conf, kept as a fixture: one also-notify in options, logging untouched, named-checkconf clean. tests/test_named_conf_edit.sh asserts, for every installer that edits named.conf: the insert is bounded to options, a quoted "none" is removed, the config is verified, a broken one is reported, and the backup is restored. Lower bound of 3 installers. --- .github/workflows/tests.yml | 3 + .../common/tests/fixtures/cpanel-named.conf | 188 ++++++++++++++++++ .../common/tests/test_named_conf_edit.sh | 45 +++++ hosting-panels/cpanel/install.sh | 30 ++- hosting-panels/directadmin/install.sh | 19 +- hosting-panels/plesk/install.sh | 29 ++- 6 files changed, 291 insertions(+), 23 deletions(-) create mode 100644 hosting-panels/common/tests/fixtures/cpanel-named.conf create mode 100755 hosting-panels/common/tests/test_named_conf_edit.sh diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c31044c..1ca0f89 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -68,6 +68,9 @@ jobs: - name: test_cpanel_rename.sh run: bash hosting-panels/common/tests/test_cpanel_rename.sh + - name: test_named_conf_edit.sh + run: bash hosting-panels/common/tests/test_named_conf_edit.sh + - name: test_cyberpanel_db.sh run: bash hosting-panels/common/tests/test_cyberpanel_db.sh diff --git a/hosting-panels/common/tests/fixtures/cpanel-named.conf b/hosting-panels/common/tests/fixtures/cpanel-named.conf new file mode 100644 index 0000000..9199289 --- /dev/null +++ b/hosting-panels/common/tests/fixtures/cpanel-named.conf @@ -0,0 +1,188 @@ +options { + /* make named use port 53 for the source of all queries, to allow + * firewalls to block all ports except 53: + */ + + // query-source port 53; + + recursion no; + + /* We no longer enable this by default as the dns posion exploit + has forced many providers to open up their firewalls a bit */ + + // Put files that named is allowed to write in the data/ directory: + directory "/var/named"; // the default + pid-file "/var/run/named/named.pid"; + dump-file "data/cache_dump.db"; + statistics-file "data/named_stats.txt"; + /* memstatistics-file "data/named_mem_stats.txt"; */ + allow-transfer { "none"; }; +}; + +logging { + channel default_log { + file "/var/log/named/named.log" versions 5 size 128M; + print-time yes; + print-severity yes; + print-category yes; + severity warning; + }; + category default { default_log; }; + category general { default_log; }; +}; + +// All BIND 9 zones are in a "view", which allow different zones to be served +// to different types of client addresses, and for options to be set for groups +// of zones. +// +// By default, if named.conf contains no "view" clauses, all zones are in the +// "default" view, which matches all clients. +// +// If named.conf contains any "view" clause, then all zones MUST be in a view; +// so it is recommended to start off using views to avoid having to restructure +// your configuration files in the future. + +view "localhost_resolver" { +/* This view sets up named to be a localhost resolver ( caching only nameserver ). + * If all you want is a caching-only nameserver, then you need only define this view: + */ + match-clients { 127.0.0.0/24; }; + match-destinations { localhost; }; + recursion yes; + + // The root hint zone has been removed from the default named.conf. + // You may add it again by uncommenting the following block: + /* + zone "." IN { + type hint; + file "/var/named/named.ca"; + }; + */ + + /* these are zones that contain definitions for all the localhost + * names and addresses, as recommended in RFC1912 - these names should + * ONLY be served to localhost clients: + */ + include "/var/named/named.rfc1912.zones"; +}; + +view "internal" { +/* This view will contain zones you want to serve only to "internal" clients + that connect via your directly attached LAN interfaces - "localnets" . + */ + match-clients { localnets; }; + match-destinations { localnets; }; + recursion yes; + + // The root hint zone has been removed from the default named.conf. + // You may add it again by uncommenting the following block: + /* + zone "." IN { + type hint; + file "/var/named/named.ca"; + }; + */ + + // include "/var/named/named.rfc1912.zones"; + // you should not serve your rfc1912 names to non-localhost clients. + + // These are your "authoritative" internal zones, and would probably + // also be included in the "localhost_resolver" view above : + +zone "server1.seconddns.com" { + type master; + file "/var/named/server1.seconddns.com.db"; +}; + + + + + + + + + + + + + + + + + + + + + + + + + + +zone "my888domain.com" { + type master; + file "/var/named/my888domain.com.db"; +}; + +}; + +view "external" { +/* This view will contain zones you want to serve only to "external" clients + * that have addresses that are not on your directly attached LAN interface subnets: + */ + recursion no; + // additional-from-cache no; // obsolete in BIND 9.16+ + // you'd probably want to deny recursion to external clients, so you don't + // end up providing free DNS service to all takers + + // The root hint zone has been removed from the default named.conf. + // You may add it again by uncommenting the following block: + /* + zone "." IN { + type hint; + file "/var/named/named.ca"; + }; + */ + + // These are your "authoritative" external zones, and would probably + // contain entries for just your web and mail servers: + + // BEGIN external zone entries + + +zone "server1.seconddns.com" { + type master; + file "/var/named/server1.seconddns.com.db"; +}; + + + + + + + + + + + + + + + + + + + + + + + + + + +zone "my888domain.com" { + type master; + file "/var/named/my888domain.com.db"; +}; + +}; diff --git a/hosting-panels/common/tests/test_named_conf_edit.sh b/hosting-panels/common/tests/test_named_conf_edit.sh new file mode 100755 index 0000000..2b7f911 --- /dev/null +++ b/hosting-panels/common/tests/test_named_conf_edit.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# cPanel ships a named.conf with views; inserting before every "};" lands inside +# the logging block and named stops parsing the file. +# Run: bash hosting-panels/common/tests/test_named_conf_edit.sh +set -u +HERE="$(cd "$(dirname "$0")" && pwd)" +PASS=0; FAIL=0 +ok() { PASS=$((PASS+1)); echo " ok: $1"; } +fail() { FAIL=$((FAIL+1)); echo " FAIL: $1"; } + +checked=0 +for panel in cpanel plesk directadmin cyberpanel; do + src="$HERE/../../$panel/install.sh" + [ -f "$src" ] || { echo "missing $src"; exit 1; } + grep -q "named.conf\|NAMED_CONF\|NAMED_OPTIONS" "$src" || continue + grep -q "allow-transfer" "$src" || continue + checked=$((checked+1)) + echo "== $panel" + + # an insert that is not bounded by the options block hits every closing brace + if grep -qE '^\s*sed -i "/\^\[\[:space:\]\]\*\};/i' "$src"; then + fail "inserts before every closing brace" + else + ok "insert is bounded" + fi + grep -qE 'sed -i "/\^options\[\[:space:\]\]\*\{/,/\^\};/' "$src" \ + && ok "bounded to the options block" || fail "no options-bounded insert" + + # the ACL cPanel ships is quoted + grep -q 'none"\\?;//g' "$src" \ + && ok 'removes a quoted "none"' || fail 'only removes an unquoted none' + + grep -q "named-checkconf" "$src" && ok "verifies the config" || fail "reloads without verifying" + grep -q "did not validate" "$src" \ + && ok "reports a config named cannot parse" || fail "would report success over a broken config" + grep -qE 'cp "\$NAMED_BAK"' "$src" \ + && ok "restores the backup on a bad config" || fail "no rollback" +done + +# a lower bound: a rename that stops matching the installers fails here +[ "$checked" -ge 3 ] && ok "checked $checked installers" || fail "only $checked matched" + +echo +echo "passed=$PASS failed=$FAIL" +[ "$FAIL" -eq 0 ] diff --git a/hosting-panels/cpanel/install.sh b/hosting-panels/cpanel/install.sh index 04aee4b..616b0ab 100755 --- a/hosting-panels/cpanel/install.sh +++ b/hosting-panels/cpanel/install.sh @@ -452,30 +452,42 @@ else if [ "$NEEDS_FIX" -eq 1 ]; then if confirm "Add allow-transfer and also-notify for $SECONDARY_IP to $NAMED_CONF?"; then - cp "$NAMED_CONF" "${NAMED_CONF}.bak.$(date +%s)" + NAMED_BAK="${NAMED_CONF}.bak.$(date +%s)" + cp "$NAMED_CONF" "$NAMED_BAK" if grep -q "allow-transfer" "$NAMED_CONF"; then - sed -i '/allow-transfer/s/none;//g' "$NAMED_CONF" + sed -i '/allow-transfer/s/"\?none"\?;//g' "$NAMED_CONF" if ! grep -q "allow-transfer.*$SECONDARY_IP" "$NAMED_CONF"; then sed -i "s|allow-transfer[[:space:]]*{|allow-transfer { $SECONDARY_IP; |" "$NAMED_CONF" fi else - sed -i "/^[[:space:]]*};/i\\ -\\tallow-transfer { $SECONDARY_IP; };" "$NAMED_CONF" + sed -i "/^options[[:space:]]*{/,/^};/ { /^};/ i\\ +\\tallow-transfer { $SECONDARY_IP; }; + }" "$NAMED_CONF" fi if grep -q "also-notify" "$NAMED_CONF"; then - sed -i '/also-notify/s/none;//g' "$NAMED_CONF" + sed -i '/also-notify/s/"\?none"\?;//g' "$NAMED_CONF" if ! grep -q "also-notify.*$SECONDARY_IP" "$NAMED_CONF"; then sed -i "s|also-notify[[:space:]]*{|also-notify { $SECONDARY_IP; |" "$NAMED_CONF" fi else - sed -i "/^[[:space:]]*};/i\\ -\\talso-notify { $SECONDARY_IP; };" "$NAMED_CONF" + sed -i "/^options[[:space:]]*{/,/^};/ { /^};/ i\\ +\\talso-notify { $SECONDARY_IP; }; + }" "$NAMED_CONF" fi - rndc reload 2>/dev/null || systemctl reload named 2>/dev/null || true - echo "[+] BIND configured and reloaded" + 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: + # put the file back and let a human look + cp "$NAMED_BAK" "$NAMED_CONF" + 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" diff --git a/hosting-panels/directadmin/install.sh b/hosting-panels/directadmin/install.sh index 94c3d27..4a70949 100644 --- a/hosting-panels/directadmin/install.sh +++ b/hosting-panels/directadmin/install.sh @@ -399,10 +399,11 @@ if [ -n "$DNS_IPS" ]; then else echo "[!] BIND allow-transfer does not include $SECONDARY_IP" if confirm "Add $SECONDARY_IP to allow-transfer in $NAMED_OPTIONS?"; then - cp "$NAMED_OPTIONS" "${NAMED_OPTIONS}.bak.$(date +%s)" + NAMED_BAK="${NAMED_OPTIONS}.bak.$(date +%s)" + cp "$NAMED_OPTIONS" "$NAMED_BAK" # Remove 'none;' if present, then add our IP sed -i "s|allow-transfer[[:space:]]*{|allow-transfer { $SECONDARY_IP; |" "$NAMED_OPTIONS" - sed -i "s|[[:space:]]*none[[:space:]]*;||g" "$NAMED_OPTIONS" + sed -i '/allow-transfer/s/"\?none"\?;//g' "$NAMED_OPTIONS" echo "[+] Added $SECONDARY_IP to allow-transfer" fi fi @@ -426,7 +427,7 @@ if [ -n "$DNS_IPS" ]; then echo "[!] also-notify does not include $SECONDARY_IP" if confirm "Add $SECONDARY_IP to also-notify?"; then sed -i "s|also-notify[[:space:]]*{|also-notify { $SECONDARY_IP; |" "$NAMED_OPTIONS" - sed -i "/also-notify/s|[[:space:]]*none[[:space:]]*;||g" "$NAMED_OPTIONS" + sed -i '/also-notify/s/"\?none"\?;//g' "$NAMED_OPTIONS" echo "[+] Added $SECONDARY_IP to also-notify" fi fi @@ -442,8 +443,16 @@ if [ -n "$DNS_IPS" ]; then # Reload named if confirm "Reload named to apply changes?"; then - rndc reload 2>/dev/null || systemctl reload named 2>/dev/null || service named reload 2>/dev/null - echo "[+] named reloaded" + if named-checkconf >/dev/null 2>&1; then + rndc reload >/dev/null 2>&1 || systemctl reload named >/dev/null 2>&1 || service named reload >/dev/null 2>&1 + echo "[+] named reloaded" + else + # a config named cannot parse is worse than no change + [ -n "${NAMED_BAK:-}" ] && 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 "[!] Could not find named options file" diff --git a/hosting-panels/plesk/install.sh b/hosting-panels/plesk/install.sh index a5507a2..c7d1764 100755 --- a/hosting-panels/plesk/install.sh +++ b/hosting-panels/plesk/install.sh @@ -391,34 +391,45 @@ else if [ "$NEEDS_FIX" -eq 1 ]; then if confirm "Add allow-transfer and also-notify to $NAMED_OPTIONS?"; then - cp "$NAMED_OPTIONS" "${NAMED_OPTIONS}.bak.$(date +%s)" + NAMED_BAK="${NAMED_OPTIONS}.bak.$(date +%s)" + cp "$NAMED_OPTIONS" "$NAMED_BAK" # allow-transfer if grep -q "allow-transfer" "$NAMED_OPTIONS"; then # Remove none; from allow-transfer line only - sed -i '/allow-transfer/s/none;//g' "$NAMED_OPTIONS" + sed -i '/allow-transfer/s/"\?none"\?;//g' "$NAMED_OPTIONS" # Add our IP if not already there if ! grep -q "allow-transfer.*$SECONDARY_IP" "$NAMED_OPTIONS"; then sed -i "s|allow-transfer[[:space:]]*{|allow-transfer { $SECONDARY_IP; |" "$NAMED_OPTIONS" fi else - sed -i "/^[[:space:]]*};/i\\ -\\tallow-transfer { $SECONDARY_IP; };" "$NAMED_OPTIONS" + sed -i "/^options[[:space:]]*{/,/^};/ { /^};/ i\\ +\\tallow-transfer { $SECONDARY_IP; }; + }" "$NAMED_OPTIONS" fi # also-notify if grep -q "also-notify" "$NAMED_OPTIONS"; then - sed -i '/also-notify/s/none;//g' "$NAMED_OPTIONS" + sed -i '/also-notify/s/"\?none"\?;//g' "$NAMED_OPTIONS" if ! grep -q "also-notify.*$SECONDARY_IP" "$NAMED_OPTIONS"; then sed -i "s|also-notify[[:space:]]*{|also-notify { $SECONDARY_IP; |" "$NAMED_OPTIONS" fi else - sed -i "/^[[:space:]]*};/i\\ -\\talso-notify { $SECONDARY_IP; };" "$NAMED_OPTIONS" + sed -i "/^options[[:space:]]*{/,/^};/ { /^};/ i\\ +\\talso-notify { $SECONDARY_IP; }; + }" "$NAMED_OPTIONS" fi - rndc reload 2>/dev/null || systemctl reload named 2>/dev/null || true - echo "[+] BIND configured and reloaded" + 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" From cd4f765e221f50de1c5dc85c8c06ba343e6711b9 Mon Sep 17 00:00:00 2001 From: Ihor Rusyn <0kaba0@gmail.com> Date: Sun, 6 Sep 2026 13:50:01 +0200 Subject: [PATCH 2/2] Run the named.conf edit against the fixture, not a grep of the source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The test asserted the shape of a sed command and left the fixture unused — checking the wording instead of the lock, and the fixture was the one thing that reproduced the failure. The edit is now a function, configure_named_axfr, that the test extracts from each installer and applies to a copy of the real cPanel named.conf: exactly one allow-transfer and one also-notify, both inside options, nothing in logging or any view, no leftover "none", braces balanced, and named-checkconf clean where it is available. The greps stay as cheap checks that the reload is gated. Making it callable also made it portable: sed -i and \? are GNU-only, and on BSD the old commands silently produced 'talso-notify' and left the quoted "none" in place. The insert is awk now, bounded to the options block. Mutations: the original unbounded sed writes also-notify 13 times, into logging and into a view — 3 red; not stripping "none" — 1 red; replacing 'if named-checkconf' with 'if true' — 1 red, after the criterion was narrowed from the word appearing anywhere to it gating the branch. --- .../common/tests/test_named_conf_edit.sh | 71 ++++++++++----- hosting-panels/cpanel/install.sh | 59 +++++++----- hosting-panels/directadmin/install.sh | 91 +++++++++---------- hosting-panels/plesk/install.sh | 61 ++++++++----- 4 files changed, 170 insertions(+), 112 deletions(-) diff --git a/hosting-panels/common/tests/test_named_conf_edit.sh b/hosting-panels/common/tests/test_named_conf_edit.sh index 2b7f911..e71ee26 100755 --- a/hosting-panels/common/tests/test_named_conf_edit.sh +++ b/hosting-panels/common/tests/test_named_conf_edit.sh @@ -4,40 +4,67 @@ # Run: bash hosting-panels/common/tests/test_named_conf_edit.sh set -u HERE="$(cd "$(dirname "$0")" && pwd)" +FIXTURE="$HERE/fixtures/cpanel-named.conf" +IP="203.0.113.9" PASS=0; FAIL=0 ok() { PASS=$((PASS+1)); echo " ok: $1"; } fail() { FAIL=$((FAIL+1)); echo " FAIL: $1"; } +[ -f "$FIXTURE" ] || { echo "missing fixture $FIXTURE"; exit 1; } +TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT + +# section boundaries of a block that starts at column 0 +block() { # $1 file, $2 block name -> prints the block + awk -v b="$2" '$0 ~ "^"b { inb=1 } inb { print } inb && /^};/ { exit }' "$1" +} + checked=0 -for panel in cpanel plesk directadmin cyberpanel; do +for panel in cpanel plesk directadmin; do src="$HERE/../../$panel/install.sh" [ -f "$src" ] || { echo "missing $src"; exit 1; } - grep -q "named.conf\|NAMED_CONF\|NAMED_OPTIONS" "$src" || continue - grep -q "allow-transfer" "$src" || continue + grep -q "configure_named_axfr" "$src" || { echo "$panel: no configure_named_axfr"; exit 1; } checked=$((checked+1)) - echo "== $panel" + echo "== $panel, applied to the real cPanel named.conf" + + # the function as the installer defines it, run against a copy of the fixture + eval "$(sed -n '/^configure_named_axfr() {/,/^}/p' "$src")" + f="$TMP/$panel.conf"; cp "$FIXTURE" "$f" + configure_named_axfr "$f" "$IP" + + n=$(grep -c "allow-transfer.*$IP" "$f") + [ "$n" -eq 1 ] && ok "one allow-transfer with the secondary" || fail "allow-transfer written $n times" + n=$(grep -c "also-notify.*$IP" "$f") + [ "$n" -eq 1 ] && ok "one also-notify with the secondary" || fail "also-notify written $n times" + + # both must be inside options, and nowhere else + opts=$(block "$f" "options") + echo "$opts" | grep -q "allow-transfer.*$IP" && ok "allow-transfer is inside options" || fail "allow-transfer outside options" + echo "$opts" | grep -q "also-notify.*$IP" && ok "also-notify is inside options" || fail "also-notify outside options" + block "$f" "logging" | grep -qE "allow-transfer|also-notify" \ + && fail "wrote into the logging block" || ok "logging untouched" + awk '/^view/ { inv=1 } inv && /also-notify|allow-transfer/ { found=1 } END { exit !found }' "$f" \ + && fail "wrote into a view" || ok "views untouched" - # an insert that is not bounded by the options block hits every closing brace - if grep -qE '^\s*sed -i "/\^\[\[:space:\]\]\*\};/i' "$src"; then - fail "inserts before every closing brace" - else - ok "insert is bounded" + grep -qE 'allow-transfer[^}]*"?none"?;' "$f" && fail '"none" survived beside the address' || ok '"none" replaced' + + opened=$(tr -cd '{' < "$f" | wc -c | tr -d ' ') + closed=$(tr -cd '}' < "$f" | wc -c | tr -d ' ') + [ "$opened" -eq "$closed" ] \ + && ok "braces balanced ($opened)" || fail "braces unbalanced: $opened open, $closed closed" + + if command -v named-checkconf >/dev/null 2>&1; then + named-checkconf "$f" >/dev/null 2>&1 && ok "named-checkconf accepts it" || fail "named-checkconf rejects it" fi - grep -qE 'sed -i "/\^options\[\[:space:\]\]\*\{/,/\^\};/' "$src" \ - && ok "bounded to the options block" || fail "no options-bounded insert" - - # the ACL cPanel ships is quoted - grep -q 'none"\\?;//g' "$src" \ - && ok 'removes a quoted "none"' || fail 'only removes an unquoted none' - - grep -q "named-checkconf" "$src" && ok "verifies the config" || fail "reloads without verifying" - grep -q "did not validate" "$src" \ - && ok "reports a config named cannot parse" || fail "would report success over a broken config" - grep -qE 'cp "\$NAMED_BAK"' "$src" \ - && ok "restores the backup on a bad config" || fail "no rollback" done -# a lower bound: a rename that stops matching the installers fails here +echo "== cheap greps: every installer verifies before reporting" +for panel in cpanel plesk 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" echo diff --git a/hosting-panels/cpanel/install.sh b/hosting-panels/cpanel/install.sh index 616b0ab..28682f7 100755 --- a/hosting-panels/cpanel/install.sh +++ b/hosting-panels/cpanel/install.sh @@ -75,6 +75,43 @@ 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 + 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 + fi + + rm -f "$tmp" + unset -f _rewrite +} + echo "=== SecondDNS cPanel/WHM Integration ===" echo "" @@ -455,27 +492,7 @@ else NAMED_BAK="${NAMED_CONF}.bak.$(date +%s)" cp "$NAMED_CONF" "$NAMED_BAK" - if grep -q "allow-transfer" "$NAMED_CONF"; then - sed -i '/allow-transfer/s/"\?none"\?;//g' "$NAMED_CONF" - if ! grep -q "allow-transfer.*$SECONDARY_IP" "$NAMED_CONF"; then - sed -i "s|allow-transfer[[:space:]]*{|allow-transfer { $SECONDARY_IP; |" "$NAMED_CONF" - fi - else - sed -i "/^options[[:space:]]*{/,/^};/ { /^};/ i\\ -\\tallow-transfer { $SECONDARY_IP; }; - }" "$NAMED_CONF" - fi - - if grep -q "also-notify" "$NAMED_CONF"; then - sed -i '/also-notify/s/"\?none"\?;//g' "$NAMED_CONF" - if ! grep -q "also-notify.*$SECONDARY_IP" "$NAMED_CONF"; then - sed -i "s|also-notify[[:space:]]*{|also-notify { $SECONDARY_IP; |" "$NAMED_CONF" - fi - else - sed -i "/^options[[:space:]]*{/,/^};/ { /^};/ i\\ -\\talso-notify { $SECONDARY_IP; }; - }" "$NAMED_CONF" - fi + configure_named_axfr "$NAMED_CONF" "$SECONDARY_IP" if named-checkconf >/dev/null 2>&1; then rndc reload >/dev/null 2>&1 || systemctl reload named >/dev/null 2>&1 || true diff --git a/hosting-panels/directadmin/install.sh b/hosting-panels/directadmin/install.sh index 4a70949..e95c9f4 100644 --- a/hosting-panels/directadmin/install.sh +++ b/hosting-panels/directadmin/install.sh @@ -74,6 +74,43 @@ 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 + 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 + fi + + rm -f "$tmp" + unset -f _rewrite +} + echo "=== SecondDNS DirectAdmin Integration ===" echo "" @@ -393,52 +430,14 @@ if [ -n "$DNS_IPS" ]; then done if [ -n "$NAMED_OPTIONS" ]; then - if grep -q "allow-transfer" "$NAMED_OPTIONS" 2>/dev/null; then - if grep -q "$SECONDARY_IP" "$NAMED_OPTIONS" 2>/dev/null; then - echo "[+] BIND allow-transfer already includes $SECONDARY_IP" - else - echo "[!] BIND allow-transfer does not include $SECONDARY_IP" - if confirm "Add $SECONDARY_IP to allow-transfer in $NAMED_OPTIONS?"; then - NAMED_BAK="${NAMED_OPTIONS}.bak.$(date +%s)" - cp "$NAMED_OPTIONS" "$NAMED_BAK" - # Remove 'none;' if present, then add our IP - sed -i "s|allow-transfer[[:space:]]*{|allow-transfer { $SECONDARY_IP; |" "$NAMED_OPTIONS" - sed -i '/allow-transfer/s/"\?none"\?;//g' "$NAMED_OPTIONS" - echo "[+] Added $SECONDARY_IP to allow-transfer" - fi - fi - else - echo "[!] No allow-transfer directive found" - if confirm "Add allow-transfer and also-notify to $NAMED_OPTIONS?"; then - cp "$NAMED_OPTIONS" "${NAMED_OPTIONS}.bak.$(date +%s)" - # Add before closing }; of options block - sed -i "/^options[[:space:]]*{/,/^};/ { - /^};/ i\\ -\\tallow-transfer { $SECONDARY_IP; };\\ -\\talso-notify { $SECONDARY_IP; }; - }" "$NAMED_OPTIONS" - echo "[+] Added allow-transfer and also-notify" - fi - fi - - # Check also-notify - if grep -q "also-notify" "$NAMED_OPTIONS" 2>/dev/null; then - if ! grep -q "also-notify.*$SECONDARY_IP" "$NAMED_OPTIONS" 2>/dev/null; then - echo "[!] also-notify does not include $SECONDARY_IP" - if confirm "Add $SECONDARY_IP to also-notify?"; then - sed -i "s|also-notify[[:space:]]*{|also-notify { $SECONDARY_IP; |" "$NAMED_OPTIONS" - sed -i '/also-notify/s/"\?none"\?;//g' "$NAMED_OPTIONS" - echo "[+] Added $SECONDARY_IP to also-notify" - fi - fi - else - if confirm "Add also-notify for $SECONDARY_IP?"; then - sed -i "/^options[[:space:]]*{/,/^};/ { - /^};/ i\\ -\\talso-notify { $SECONDARY_IP; }; - }" "$NAMED_OPTIONS" - echo "[+] Added also-notify" - fi + if grep -q "allow-transfer.*$SECONDARY_IP" "$NAMED_OPTIONS" 2>/dev/null \ + && grep -q "also-notify.*$SECONDARY_IP" "$NAMED_OPTIONS" 2>/dev/null; then + echo "[+] BIND AXFR config already includes $SECONDARY_IP" + elif confirm "Add allow-transfer and also-notify for $SECONDARY_IP to $NAMED_OPTIONS?"; then + NAMED_BAK="${NAMED_OPTIONS}.bak.$(date +%s)" + cp "$NAMED_OPTIONS" "$NAMED_BAK" + configure_named_axfr "$NAMED_OPTIONS" "$SECONDARY_IP" + echo "[+] Added allow-transfer and also-notify" fi # Reload named diff --git a/hosting-panels/plesk/install.sh b/hosting-panels/plesk/install.sh index c7d1764..e8b9626 100755 --- a/hosting-panels/plesk/install.sh +++ b/hosting-panels/plesk/install.sh @@ -74,6 +74,43 @@ 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 + 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 + fi + + rm -f "$tmp" + unset -f _rewrite +} + echo "=== SecondDNS Plesk Integration ===" echo "" @@ -395,30 +432,8 @@ else cp "$NAMED_OPTIONS" "$NAMED_BAK" # allow-transfer - if grep -q "allow-transfer" "$NAMED_OPTIONS"; then - # Remove none; from allow-transfer line only - sed -i '/allow-transfer/s/"\?none"\?;//g' "$NAMED_OPTIONS" - # Add our IP if not already there - if ! grep -q "allow-transfer.*$SECONDARY_IP" "$NAMED_OPTIONS"; then - sed -i "s|allow-transfer[[:space:]]*{|allow-transfer { $SECONDARY_IP; |" "$NAMED_OPTIONS" - fi - else - sed -i "/^options[[:space:]]*{/,/^};/ { /^};/ i\\ -\\tallow-transfer { $SECONDARY_IP; }; - }" "$NAMED_OPTIONS" - fi + configure_named_axfr "$NAMED_OPTIONS" "$SECONDARY_IP" - # also-notify - if grep -q "also-notify" "$NAMED_OPTIONS"; then - sed -i '/also-notify/s/"\?none"\?;//g' "$NAMED_OPTIONS" - if ! grep -q "also-notify.*$SECONDARY_IP" "$NAMED_OPTIONS"; then - sed -i "s|also-notify[[:space:]]*{|also-notify { $SECONDARY_IP; |" "$NAMED_OPTIONS" - fi - else - sed -i "/^options[[:space:]]*{/,/^};/ { /^};/ i\\ -\\talso-notify { $SECONDARY_IP; }; - }" "$NAMED_OPTIONS" - fi if named-checkconf >/dev/null 2>&1; then rndc reload >/dev/null 2>&1 || systemctl reload named >/dev/null 2>&1 || true