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..e71ee26 --- /dev/null +++ b/hosting-panels/common/tests/test_named_conf_edit.sh @@ -0,0 +1,72 @@ +#!/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)" +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; 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; } + checked=$((checked+1)) + 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" + + 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 +done + +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 +echo "passed=$PASS failed=$FAIL" +[ "$FAIL" -eq 0 ] diff --git a/hosting-panels/cpanel/install.sh b/hosting-panels/cpanel/install.sh index 04aee4b..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 "" @@ -452,30 +489,22 @@ 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" - 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" - fi + configure_named_axfr "$NAMED_CONF" "$SECONDARY_IP" - 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 + 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 - sed -i "/^[[:space:]]*};/i\\ -\\talso-notify { $SECONDARY_IP; };" "$NAMED_CONF" + # 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 - - rndc reload 2>/dev/null || systemctl reload named 2>/dev/null || true - echo "[+] BIND configured and reloaded" 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..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,57 +430,28 @@ 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 - cp "$NAMED_OPTIONS" "${NAMED_OPTIONS}.bak.$(date +%s)" - # 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" - 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|[[:space:]]*none[[:space:]]*;||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 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..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 "" @@ -391,34 +428,23 @@ 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" - # 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" - 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 + 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 - sed -i "/^[[:space:]]*};/i\\ -\\talso-notify { $SECONDARY_IP; };" "$NAMED_OPTIONS" + # 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 - - rndc reload 2>/dev/null || systemctl reload named 2>/dev/null || true - echo "[+] BIND configured and reloaded" fi else echo "[+] BIND AXFR config already includes $SECONDARY_IP"