From 8379c462011fffc580821297c0dceaa8d5095931 Mon Sep 17 00:00:00 2001 From: Ihor Rusyn <0kaba0@gmail.com> Date: Sun, 6 Sep 2026 11:03:59 +0200 Subject: [PATCH 1/2] Drop cPanel's own sync, and keep the server's hostname out of it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Installing on the live cPanel box ran the initial fill twice: #80 added the reconcile-based one, and cPanel already had its own under a different name — "Sync existing cPanel accounts", which my grep for "Queue existing domains" did not match. I reported that cPanel had no initial fill at all. It had one, and a worse one: - it POSTs to the API directly, so an outage during install loses the zones instead of queueing them - no canonicalisation, so an IDN goes as whatever the panel stored - it reads DNS= from /var/cpanel/users, which is the account's main domain only — addon and parked domains are not there Removed; the reconcile path covers all of it. Second, on the same install: whmapi1 listzones includes the server's own hostname zone, which the API refuses (it is under the service's domain). Every cPanel install therefore ended with a failed op in the queue and an alarming line in the log, on a zone that was never the customer's. panel_zones() now drops the zone whose name is the machine's FQDN, for every panel. tests/test_hostname_filter.sh: the hostname zone goes, case and trailing dot included; the parent zone and a subdomain of it stay; a hostname without a dot drops nothing; an empty panel stays empty. Mutations: removing the filter fails 3, comparing without normalising fails 2. Live on the cPanel box: panel_zones() now returns only the customer zone and reconcile reports ok: 1, missing: 0. --- .github/workflows/tests.yml | 3 ++ hosting-panels/common/seconddns_common.py | 19 ++++++++ .../common/tests/test_hostname_filter.sh | 44 +++++++++++++++++++ hosting-panels/cpanel/install.sh | 33 -------------- 4 files changed, 66 insertions(+), 33 deletions(-) create mode 100755 hosting-panels/common/tests/test_hostname_filter.sh diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 64c8eed..7f1b219 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -56,6 +56,9 @@ jobs: - name: test_cli.sh run: bash hosting-panels/common/tests/test_cli.sh + - name: test_hostname_filter.sh + run: bash hosting-panels/common/tests/test_hostname_filter.sh + - name: test_cyberpanel_db.sh run: bash hosting-panels/common/tests/test_cyberpanel_db.sh diff --git a/hosting-panels/common/seconddns_common.py b/hosting-panels/common/seconddns_common.py index 3974751..7121f57 100644 --- a/hosting-panels/common/seconddns_common.py +++ b/hosting-panels/common/seconddns_common.py @@ -5,6 +5,7 @@ import configparser import json import os +import socket import subprocess import sys import urllib.error @@ -163,11 +164,29 @@ def panel_zones(): zones = [l.strip() for l in (out or "").splitlines() if l.strip()] if out is None: raise PanelError(f"could not read the zone list ({source}); refusing to continue") + # The server's own hostname zone belongs to the host, not to the customer: + # cPanel keeps one and lists it, and every install then queues a create the + # API refuses. Add it by hand with `seconddns add` if it is really wanted. + host = _server_hostname() + if host: + zones = [z for z in zones if z.lower().rstrip(".") != host] # A panel with no zones is a legal state — a fresh server, or one that was # emptied. Only a failed read is an error, and that is the branch above. return zones +def _server_hostname(): + """This machine's fully qualified name, lowercased, or '' if unknown.""" + name = os.environ.get("SECONDDNS_HOSTNAME") # tests + if name is None: + try: + name = socket.getfqdn() + except OSError: + return "" + name = (name or "").strip().lower().rstrip(".") + return name if "." in name else "" + + def zones_from_file(path): zones = [l.strip() for l in open(path) if l.strip() and not l.startswith("#")] if not zones: diff --git a/hosting-panels/common/tests/test_hostname_filter.sh b/hosting-panels/common/tests/test_hostname_filter.sh new file mode 100755 index 0000000..91e3e3b --- /dev/null +++ b/hosting-panels/common/tests/test_hostname_filter.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# The server's own hostname zone is not a customer zone: cPanel lists it and the +# API refuses it, so every install left a failed op behind. +# Run: bash hosting-panels/common/tests/test_hostname_filter.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"; } + +zones() { # $1 = hostname, $2 = panel zone list + env SECONDDNS_HOSTNAME="$1" SECONDDNS_PANEL_ZONES_CMD="printf '%s\n' $2" \ + PYTHONPATH="$HERE/.." python3 -c " +from seconddns_common import panel_zones +print(' '.join(panel_zones())) +" 2>&1 +} + +echo "== the hostname zone is dropped" +out=$(zones "server1.seconddns.com" "a.example.com server1.seconddns.com b.example.com") +[ "$out" = "a.example.com b.example.com" ] && ok "hostname removed, order kept" || fail "got '$out'" + +echo "== matching is case- and dot-insensitive" +out=$(zones "server1.seconddns.com" "SERVER1.SecondDNS.com a.example.com") +[ "$out" = "a.example.com" ] && ok "uppercase form removed" || fail "got '$out'" +out=$(zones "server1.seconddns.com" "server1.seconddns.com. a.example.com") +[ "$out" = "a.example.com" ] && ok "trailing dot removed" || fail "got '$out'" + +echo "== only the exact name goes" +out=$(zones "server1.seconddns.com" "seconddns.com sub.server1.seconddns.com a.example.com") +[ "$out" = "seconddns.com sub.server1.seconddns.com a.example.com" ] \ + && ok "parent and child zones kept" || fail "got '$out'" + +echo "== a bare hostname is not a zone name" +out=$(zones "localhost" "localhost a.example.com") +[ "$out" = "localhost a.example.com" ] && ok "no dot, nothing dropped" || fail "got '$out'" + +echo "== an empty panel stays empty, not an error" +out=$(zones "server1.seconddns.com" "") +[ -z "$out" ] && ok "empty list survives the filter" || fail "got '$out'" + +echo +echo "passed=$PASS failed=$FAIL" +[ "$FAIL" -eq 0 ] diff --git a/hosting-panels/cpanel/install.sh b/hosting-panels/cpanel/install.sh index a01c8ac..0d5ca92 100755 --- a/hosting-panels/cpanel/install.sh +++ b/hosting-panels/cpanel/install.sh @@ -503,39 +503,6 @@ if [ -n "$API_NS" ]; then fi fi -# Initial sync -echo "" -if confirm "Sync existing cPanel accounts to secondary DNS now?"; then - echo "[*] Syncing accounts..." - added=0 - failed=0 - - if [ -d /var/cpanel/users ]; then - for user_file in /var/cpanel/users/*; do - [ -f "$user_file" ] || continue - sdomain=$(grep "^DNS=" "$user_file" 2>/dev/null | cut -d= -f2) - [ -z "$sdomain" ] && continue - - response=$(curl -sf --max-time 15 \ - -X POST \ - -H "X-API-Key: $API_KEY" \ - -H "Content-Type: application/json" \ - -H "User-Agent: SecondDNS-cPanel/1.0" \ - -d "{\"name\":\"$sdomain\",\"masterIp\":\"$MASTER_IP\"}" \ - "$API_URL/api/zones" 2>/dev/null) - if [ $? -eq 0 ]; then - echo " [+] $sdomain" - added=$((added+1)) - else - failed=$((failed+1)) - fi - done - else - echo "[!] /var/cpanel/users not found — skipping sync" - fi - - echo "[+] Synced: $added domains, failed: $failed" -fi echo "" # reconcile already knows where each panel keeps its zone list; --add-missing From f23e1422058fc4145996511fbbb4754c969f0b29 Mon Sep 17 00:00:00 2001 From: Ihor Rusyn <0kaba0@gmail.com> Date: Sun, 6 Sep 2026 11:07:47 +0200 Subject: [PATCH 2/2] Two-line comments over the hostname filter and its test --- hosting-panels/common/seconddns_common.py | 5 ++--- hosting-panels/common/tests/test_hostname_filter.sh | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/hosting-panels/common/seconddns_common.py b/hosting-panels/common/seconddns_common.py index 7121f57..868c3c9 100644 --- a/hosting-panels/common/seconddns_common.py +++ b/hosting-panels/common/seconddns_common.py @@ -164,9 +164,8 @@ def panel_zones(): zones = [l.strip() for l in (out or "").splitlines() if l.strip()] if out is None: raise PanelError(f"could not read the zone list ({source}); refusing to continue") - # The server's own hostname zone belongs to the host, not to the customer: - # cPanel keeps one and lists it, and every install then queues a create the - # API refuses. Add it by hand with `seconddns add` if it is really wanted. + # The server's own hostname zone belongs to the host, not the customer; the + # API refuses it, so every cPanel install left a failed op behind. host = _server_hostname() if host: zones = [z for z in zones if z.lower().rstrip(".") != host] diff --git a/hosting-panels/common/tests/test_hostname_filter.sh b/hosting-panels/common/tests/test_hostname_filter.sh index 91e3e3b..7668da5 100755 --- a/hosting-panels/common/tests/test_hostname_filter.sh +++ b/hosting-panels/common/tests/test_hostname_filter.sh @@ -1,6 +1,6 @@ #!/bin/bash -# The server's own hostname zone is not a customer zone: cPanel lists it and the -# API refuses it, so every install left a failed op behind. +# The server's own hostname is not a customer zone: cPanel lists it, the API +# refuses it, and every install left a failed op behind. # Run: bash hosting-panels/common/tests/test_hostname_filter.sh set -u HERE="$(cd "$(dirname "$0")" && pwd)"