feat: add PowerDNS reverse DNS (PTR) integration
Introduces an opt-in reverse DNS management subsystem backed by a PowerDNS
Authoritative HTTP API. Runs via a companion WHMCS addon module
(modules/addons/VirtFusionDns) that holds settings and a Test Connection
page; the server module reads those settings from tbladdonmodules and
short-circuits when the addon is absent or disabled, so provisioning is
unaffected for operators who don't use the feature.
Lifecycle hooks:
- createAccount creates PTRs for every assigned IP (forward DNS must
already resolve to the IP — FCrDNS enforcement)
- renameServer updates only PTRs whose content matched the old hostname,
preserving client-custom records
- terminateAccount deletes all PTRs before the local state is purged
- TestConnection merges PowerDNS health check with the existing VirtFusion
check
- A DailyCronJob hook reconciles missing PTRs additive-only (never
overwrites)
Client UI: new "Reverse DNS" panel on the service overview with one
editable PTR input per assigned IP, per-row status badges, and
forward-DNS rejection on save. Admin services tab gets a parallel
widget with Reconcile (additive) and Reconcile (force reset) buttons.
New subsystem at lib/PowerDns/:
- Client.php PowerDNS API wrapper (X-API-Key, listZones/getZone/
patchRRset/notifyZone), auto-NOTIFY on successful PATCH
- Config.php Loads + decrypts addon settings from tbladdonmodules
- IpUtil.php PTR-name generation (IPv4 + IPv6), zone matching,
RFC 2317 classless parsing
- Resolver.php FCrDNS verification via dns_get_record with CNAME-chain
following and per-(hostname,ip) caching
- PtrManager.php Orchestrator: syncServer, deleteForServer, listPtrs,
setPtr, reconcile, reconcileAll
Security hardening helpers added to Module and applied to the rDNS
endpoints:
- requirePost() HTTP method gate (405 on non-POST mutations)
- requireSameOrigin() Origin/Referer check against WHMCS host (CSRF
defence against cross-site form POST)
- requireServiceStatus() tblhosting.domainstatus filter (Active for
writes, Active+Suspended for reads)
RFC 2317 classless delegations (e.g. 64/64.113.0.203.in-addr.arpa.)
supported with alignment validation: rejects misaligned start addresses
that don't correspond to any real delegation boundary.
PowerDNS zone IDs containing '/' are URL-encoded as '=2F' per the
PowerDNS API convention. PATCH success triggers PUT /zones/{id}/notify
so slaves pick up the SOA-bumped serial immediately.
Includes IPv4 + IPv6 support, per-IP write rate limit (10s), fresh
IP-ownership re-verification on every client write (defends against
stale-ownership after IP reassignment), and audit logging of every
successful edit to the WHMCS module log.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,27 @@ namespace WHMCS\Module\Server\VirtFusionDirect;
|
||||
|
||||
/**
|
||||
* Static methods that generate HTML fragments for the WHMCS admin services tab.
|
||||
*
|
||||
* WHY RAW HTML STRINGS INSTEAD OF TEMPLATES
|
||||
* -----------------------------------------
|
||||
* WHMCS's AdminServicesTabFields hook expects an associative array of
|
||||
* label => HTML-string pairs. It renders each entry as a table row with the
|
||||
* label on the left and the raw HTML inserted verbatim on the right. There's
|
||||
* no way to return a Smarty template reference from that hook — WHMCS doesn't
|
||||
* know how to render one in that context.
|
||||
*
|
||||
* So we concatenate HTML here. All variable interpolation uses htmlspecialchars()
|
||||
* at the PHP boundary — never trust that a value passed in is safe for HTML.
|
||||
*
|
||||
* ASSET INJECTION
|
||||
* ---------------
|
||||
* Some renderers (serverInfo, rdnsSection) embed <link> and <script> tags so
|
||||
* the admin services tab picks up our CSS and JS without a separate loader
|
||||
* hook. This is safe because WHMCS's admin CSP allows same-origin resources
|
||||
* and the admin page is already inside an authenticated admin session.
|
||||
*
|
||||
* Cache-busting uses time() as a query string — fine for an admin-only surface
|
||||
* where we'd rather pay for the extra fetch than let stale JS cause bugs.
|
||||
*/
|
||||
class AdminHTML
|
||||
{
|
||||
@@ -147,6 +168,38 @@ EOT;
|
||||
</div>
|
||||
</div>
|
||||
<script>vfServerDataAdmin("${serviceId}","${systemUrl}");</script>
|
||||
EOT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the admin Reverse DNS section for the services tab.
|
||||
*
|
||||
* Ships an empty container + a Reconcile button. Data is loaded client-side via
|
||||
* the admin rdnsStatus AJAX endpoint once the page opens. The JS function
|
||||
* vfAdminLoadRdns (defined in templates/js/module.js) populates #vf-rdns-list
|
||||
* and wires up the Reconcile button's onclick to admin.php?action=rdnsReconcile.
|
||||
*
|
||||
* @param string $systemUrl WHMCS system URL
|
||||
* @param int $serviceId WHMCS service ID
|
||||
* @return string HTML fragment for the admin services tab
|
||||
*/
|
||||
public static function rdnsSection($systemUrl, $serviceId)
|
||||
{
|
||||
$systemUrl = htmlspecialchars($systemUrl, ENT_QUOTES, 'UTF-8');
|
||||
$serviceId = (int) $serviceId;
|
||||
|
||||
return <<<EOT
|
||||
<div id="vf-rdns-admin-wrap">
|
||||
<div id="vf-rdns-list" class="vf-rdns-list">
|
||||
<em class="text-muted">Loading reverse DNS…</em>
|
||||
</div>
|
||||
<div class="vf-rdns-actions" style="margin-top:10px">
|
||||
<button type="button" class="btn btn-default btn-sm" onclick="vfAdminReconcileRdns(${serviceId}, '${systemUrl}', false)">Reconcile (additive)</button>
|
||||
<button type="button" class="btn btn-warning btn-sm" onclick="vfAdminReconcileRdns(${serviceId}, '${systemUrl}', true)">Reconcile (force reset)</button>
|
||||
<span id="vf-rdns-report" style="margin-left:10px"></span>
|
||||
</div>
|
||||
</div>
|
||||
<script>if(typeof vfAdminLoadRdns==='function'){vfAdminLoadRdns(${serviceId},"${systemUrl}");}</script>
|
||||
EOT;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user