Skip to content

Release: cPanel round, and the defects it uncovered - #92

Merged
0kaba0 merged 15 commits into
mainfrom
develop
Sep 6, 2026
Merged

0kaba0 merged 15 commits into
mainfrom
develop

Conversation

@0kaba0

@0kaba0 0kaba0 commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

Brings main up to develop after the cPanel integration was tested end to end on a live cPanel/WHM 136 server — the round that was blocked on a licence until today.

The cPanel integration did not work at all

Five defects, each sufficient on its own to make it non-functional. None of them would have been found without a real server.

AXFR never worked (#89). The installer wrote allow-axfr-ips into pdns.conf and ran systemctl reload pdns — a reload rereads the zones, not the server settings. Zones registered in SecondDNS and stayed permanently pending, empty on the secondary, under a cheerful [+] PowerDNS reloaded. Proven by the same dig AXFR refusing before a restart and answering after.

Hooks never fired (#90). The scripts read domain from the top level of the payload; cPanel nests the event under data. Finding nothing, they exited 0 without logging — account creation reached SecondDNS never, and said nothing about it.

Deleting an account removed nothing (#90). Accounts::Remove carries only the username. The hook now reads every domain of the account — main, addon and alias — from its userdata at stage pre, while it is still on disk.

Addon and alias domains were invisible (#90). The installer registered Api2::AddonDomain::addaddon, an event current cPanel does not have. They all go through Whostmgr::Domain::park / ::unpark, which carry target_domain (the account's main domain) and new_domain (the one being added) — reading domain would have registered the wrong name.

Renaming the main domain lost the zone (#91, closes #87). Same defect as DirectAdmin #78. WHM sends only the new name on both stages, so the hook runs at pre and takes the old one from userdata.

Also in this release

Verified live

Account create and remove, addon, alias, IDN (including an IDN TLD), main-domain rename, deletion of an account with three domains in one hook, and the queue under an unreachable API — operations waited with a growing backoff and drained without loss or duplicates when the block was lifted.

All four panels are now through the same round, and none carries a beta status.

0kaba0 and others added 15 commits September 6, 2026 00:23
The public guides tell customers to run `seconddns sync`, `seconddns list`,
`seconddns status <domain>` and `seconddns remove-all`. Only CyberPanel had a
`seconddns` command at all, with only add/remove/sync/list; on DirectAdmin,
cPanel and Plesk every one of those is a command not found, and `status
<domain>` and `remove-all` existed nowhere.

The shared CLI now ships with the other tools on all four panels. Every change
goes through seconddns-queue, so it carries retry, FIFO and the delete-side
ownership check, the same as the hooks.

Deleting is never implicit, which is where CyberPanel's version was dangerous:
its `sync` removed every remote zone missing from the local list, with no
guard at all — a panel that failed to answer, which we have seen happen, would
have wiped a customer's zones. Here plain `sync` only adds; `--prune` removes
and is refused when it would drop more than half of this server's zones;
`remove-all` prints what it would remove and asks for the master IP back.

CyberPanel keeps seconddns.py as the Django plugin only. Its signals service
now runs the plugin directly for ensure-signals, since that command is not
part of the shared CLI, and the plugin is installed executable for it.

tests/test_cli.sh: 20 checks, including both sides of the prune guard (half of
two allowed, two of three refused), a zone mastered elsewhere never touched,
remove-all refusing without confirmation, and list/status queuing nothing.
One seconddns command, on every panel
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.
On the live cPanel box the zone reached SecondDNS, the worker created it on
ns3, and then sat at pending forever:

    08:57:12  done  queue=pdns.zone-add  zone=my123domain.com
    08:57:12  notify empty, staying pending

AXFR was refused. The installer had written allow-axfr-ips into pdns.conf and
run `systemctl reload pdns` — and a reload rereads the zones, not the server
settings. The setting sat in the file with no effect until something restarted
the service. A customer would see zones registered in SecondDNS and empty on
the secondary, with nothing to explain it.

Proven in place: `dig AXFR` refused after the reload, the same query answered
after `systemctl restart pdns`, and my123domain.com moved pending -> synced
within seconds. cPanel was the only installer using reload; CyberPanel and
DirectAdmin already restart.

Writing the guard turned up a second one: DirectAdmin restarted PowerDNS and
reported success without checking it came back, so a config it could not parse
would leave DNS down under a "[+] configured and restarted". It now verifies
and restores the backup, as CyberPanel does.

tests/test_pdns_restart.sh asserts, for every installer that touches pdns.conf:
no reload, a restart, a message when it stays down, and a rollback to the
backup. Lower bound of 3 installers so a rename fails instead of matching
nothing. Mutations: restoring the reload fails 1, dropping the down-message
fails 2, dropping the rollback fails 1.
Drop cPanel's own sync, and keep the server's hostname out of it
Restart PowerDNS instead of reloading it, and notice when it stays down
Creating an account on the live box produced nothing: no queue entry, no log
line, not even an error. The zone existed in PowerDNS and the hook was
registered on Accounts::Create at stage post, pointing at a script that worked
when run by hand.

The script reads stdin and looks for "domain" at the top level. cPanel sends:

    {"context":{"category":"Whostmgr","event":"Accounts::Create","stage":"post"},
     "hook":{...},
     "data":{"user":"acct","domain":"second555domain.org", ...}}

The name is under "data". Finding nothing, the hook exits 0 without logging —
so account creation on cPanel has never reached SecondDNS, silently. The same
shape applies to the delete hook and to addon domains.

That also explains the installer's own account sync removed in #88: with the
hooks not firing, something had to register the zones.

Both hooks now read "data" when it is an object and fall back to the top level.

tests/test_cpanel_hook_payload.sh extracts the parser from each hook — so the
test cannot drift from the code — and feeds it the real WHM payload, the flat
shape, a payload with no domain, malformed input, and an addon domain arriving
as newdomain. Mutations: reading the top level fails 2, dropping newdomain
fails 1.
Two more, both found on the live box after the payload fix.

Accounts::Remove carries only the user:

    "data":{"killdns":1,"user":"second444mydom","keep_remote_databases":0}

There is no domain to read, so deleting an account removed nothing. The
installer already registers this at stage pre — the comment says "domain data
available before account is gone" — but the hook never used it. It now reads
userdata/<user>/main and queues every domain of the account: main, addon and
parked alike. Parsed by hand rather than through PyYAML, which is not a
dependency anywhere else.

And addon domains never fired at all: the installer registered
Api2::AddonDomain::addaddon, an event current cPanel does not have. Addon,
parked and alias domains all go through Whostmgr::Domain::park / ::unpark,
which carry target_domain (the account's main domain) and new_domain (the one
being added). Reading "domain" would have registered the wrong name. Both
events are now registered; the Api2 pair stays as a fallback for older cPanel.

Live, deleting an account with three domains:

    10:13:59  Zone deleted: addon4444domain.info   (addon)
    10:13:59  Zone deleted: second777domain.com    (main)
    10:13:59  Zone deleted: second1123domain.net   (alias)
    10:14:25  all delivered

Tests cover the park payload for both hooks, every domain of an account from
userdata, the DNS= fallback, an unknown user yielding nothing, and an explicit
domain still winning. Mutation: dropping new_domain fails 1 — the hook would
take target_domain, registering the account's main domain in place of the
addon.
The lookup failed twice in silence: except Exception: pass inside the parser
and 2>/dev/null around it. An account whose userdata cannot be read would
disappear from the panel with every one of its zones left on the secondary and
nothing in the log to say so.

With a user in the payload and no domains found, the hook now logs the account
name and the path to check, and queues nothing. Mutation: dropping that line
fails the test.
Read the cPanel hook payload where cPanel puts it
Changing an account's main domain in WHM removed the old zone from the panel
and created a new one, and SecondDNS heard about neither: the old zone stayed
on the secondary for a domain that no longer exists, the new one was never
registered. Same defect as DirectAdmin #78, filed as #87 and left until a
cPanel server existed to see it on.

WHM fires Whostmgr::Accounts::Modify for this, and a probe on both stages
showed the payload carries only the new name and the user:

    pre   domain=my999domain.com  user=my123domain
    post  domain=my999domain.com  user=my123domain

The old name is nowhere in it — which is why cPanel's own ChangeDomains
framework captures it at stage pre. The hook does the same: at pre the account
on disk is still the old one, so the old main domain comes from userdata and
the new one from the payload.

WHM fires this event for every account change — quota, package, contact — so
the hook queues nothing unless the two names differ.

Live: my999domain.com -> my777domain.com, old zone deleted, new one delivered
and answering on the secondary within seconds.

tests/test_cpanel_rename.sh: both operations and their order, an unchanged
domain queueing nothing, a quota-only change carrying no domain, an unknown
account, an IDN new name in punycode, and an unusable new name that still
removes the old zone and logs the refusal. Mutations: dropping the
old-vs-new comparison fails 1, skipping the removal of the old zone fails 5.
The README said two scripts and four events, two of which cPanel does not
fire. It is three scripts and five events now, and the beta banner goes: the
integration has been through the full round on a live cPanel 136 server —
account create and remove, addon, alias, IDN, rename, and the queue under an
unreachable API.

Also noted: WHM refuses Unicode in the domain field, so an IDN has to be
entered in punycode there — unlike DirectAdmin, which accepts either.
Handle a renamed main domain on cPanel
@0kaba0

0kaba0 commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator Author

Release check: develop contains main (fast-forward, 15 commits, nothing on main that develop lacks), MERGEABLE, CI green on develop, no Co-Authored-By: Claude trailer in any of the 15 commits, and the E2E gate is met on cPanel, DirectAdmin and CyberPanel. Ready for the owner's merge into main.

@0kaba0
0kaba0 merged commit 7a820f9 into main Sep 6, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cPanel: renaming an account's main domain loses the zone on the secondary

1 participant