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>
235 lines
10 KiB
PHP
235 lines
10 KiB
PHP
<?php
|
|
|
|
use WHMCS\Module\Server\VirtFusionDirect\PowerDns\Client;
|
|
use WHMCS\Module\Server\VirtFusionDirect\PowerDns\Config;
|
|
|
|
/**
|
|
* VirtFusion DNS — companion WHMCS addon module that holds PowerDNS settings for
|
|
* the VirtFusionDirect server module. Keeps the server module decoupled from the
|
|
* addon: the server module reads settings from tbladdonmodules and never loads
|
|
* addon code at runtime.
|
|
*
|
|
* Activation: WHMCS Admin -> System Settings -> Addon Modules -> Activate -> Configure.
|
|
*
|
|
* API key handling: WHMCS encrypts password-type addon fields in tbladdonmodules;
|
|
* the server module calls decrypt() on read (see lib/PowerDns/Config.php).
|
|
*/
|
|
if (! defined('WHMCS')) {
|
|
exit('This file cannot be accessed directly');
|
|
}
|
|
|
|
/**
|
|
* Load the server module's PowerDNS classes on demand. Done inside functions rather
|
|
* than at file scope so the WHMCS addon list still works if the server module is
|
|
* absent (e.g., uninstalled while the addon is still activated). Returns true when
|
|
* the classes are available.
|
|
*/
|
|
function virtfusiondns_load_server_libs(): bool
|
|
{
|
|
$base = __DIR__ . '/../../servers/VirtFusionDirect/lib/';
|
|
$files = [
|
|
'Curl.php',
|
|
'Log.php',
|
|
'Cache.php',
|
|
'PowerDns/Config.php',
|
|
'PowerDns/IpUtil.php',
|
|
'PowerDns/Client.php',
|
|
];
|
|
foreach ($files as $f) {
|
|
if (! is_file($base . $f)) {
|
|
return false;
|
|
}
|
|
require_once $base . $f;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* WHMCS addon metadata.
|
|
*/
|
|
function VirtFusionDns_config()
|
|
{
|
|
return [
|
|
'name' => 'VirtFusion DNS',
|
|
'description' => 'Adds reverse DNS (PTR) management to the VirtFusionDirect server module using a PowerDNS HTTP API. Zones must already exist in PowerDNS; the addon never creates zones. Requires the VirtFusionDirect server module.',
|
|
'version' => '1.0',
|
|
'author' => 'VirtFusionDirect',
|
|
'language' => 'english',
|
|
'fields' => [
|
|
'enabled' => [
|
|
'FriendlyName' => 'Enable rDNS Sync',
|
|
'Type' => 'yesno',
|
|
'Description' => 'Master switch. When off, the server module skips every PowerDNS call.',
|
|
],
|
|
'endpoint' => [
|
|
'FriendlyName' => 'PowerDNS API Endpoint',
|
|
'Type' => 'text',
|
|
'Size' => '60',
|
|
'Default' => 'http://ns1.example.com:8081',
|
|
'Description' => 'Scheme + host + port (no path). The /api/v1/... path is appended automatically.',
|
|
],
|
|
'apiKey' => [
|
|
'FriendlyName' => 'PowerDNS API Key',
|
|
'Type' => 'password',
|
|
'Size' => '60',
|
|
'Description' => 'X-API-Key. Stored encrypted by WHMCS; decrypted only server-side when PowerDNS is called.',
|
|
],
|
|
'serverId' => [
|
|
'FriendlyName' => 'PowerDNS Server ID',
|
|
'Type' => 'text',
|
|
'Size' => '20',
|
|
'Default' => 'localhost',
|
|
'Description' => 'Almost always "localhost" (the PowerDNS API server identifier, not a hostname).',
|
|
],
|
|
'defaultTtl' => [
|
|
'FriendlyName' => 'Default PTR TTL (seconds)',
|
|
'Type' => 'text',
|
|
'Size' => '10',
|
|
'Default' => '3600',
|
|
'Description' => 'TTL applied to PTR records created by the module.',
|
|
],
|
|
'cacheTtl' => [
|
|
'FriendlyName' => 'Cache TTL (seconds)',
|
|
'Type' => 'text',
|
|
'Size' => '10',
|
|
'Default' => '60',
|
|
'Description' => 'How long zone lists and DNS-resolution results are cached. Minimum 10s.',
|
|
],
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Called when the addon is activated. No schema to create — settings live in tbladdonmodules.
|
|
*/
|
|
function VirtFusionDns_activate()
|
|
{
|
|
return [
|
|
'status' => 'success',
|
|
'description' => 'VirtFusion DNS activated. Fill in the endpoint + API key in the addon configuration, then use the Test Connection button on the addon page.',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Called when the addon is deactivated. Settings preserved (re-activating restores them).
|
|
*/
|
|
function VirtFusionDns_deactivate()
|
|
{
|
|
return [
|
|
'status' => 'success',
|
|
'description' => 'VirtFusion DNS deactivated. Server lifecycle PowerDNS calls will now be skipped. Settings are preserved.',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Admin status page — rendered by WHMCS when the addon is clicked from the Addons menu.
|
|
*
|
|
* Shows a settings summary, a Test Connection button (calls PowerDNS ping), the current
|
|
* zone count, and a recent log extract filtered to PowerDNS-related entries.
|
|
*/
|
|
function VirtFusionDns_output($vars)
|
|
{
|
|
if (! virtfusiondns_load_server_libs()) {
|
|
echo '<div style="max-width:900px;padding:16px;border-radius:4px;background:#f8d7da;color:#721c24">';
|
|
echo '<strong>VirtFusionDirect server module not found.</strong> ';
|
|
echo 'This addon requires the VirtFusionDirect server module at <code>modules/servers/VirtFusionDirect/</code>. ';
|
|
echo 'Install or restore that module and reload this page.';
|
|
echo '</div>';
|
|
|
|
return;
|
|
}
|
|
|
|
Config::reset();
|
|
$config = Config::get();
|
|
|
|
$pingResult = null;
|
|
$zoneCount = null;
|
|
$zoneSample = [];
|
|
|
|
if (! empty($_GET['vfdns_test'])) {
|
|
if (Config::isEnabled()) {
|
|
$client = new Client;
|
|
$pingResult = $client->ping();
|
|
if ($pingResult['ok']) {
|
|
$client->forgetZoneCache();
|
|
$zones = $client->listZones();
|
|
$zoneCount = count($zones);
|
|
$zoneSample = array_slice($zones, 0, 8);
|
|
}
|
|
} else {
|
|
$pingResult = ['ok' => false, 'http' => 0, 'error' => 'Not enabled or missing endpoint/apiKey.'];
|
|
}
|
|
}
|
|
|
|
$modulelink = htmlspecialchars($vars['modulelink'] ?? '', ENT_QUOTES, 'UTF-8');
|
|
$endpoint = htmlspecialchars($config['endpoint'], ENT_QUOTES, 'UTF-8');
|
|
$serverId = htmlspecialchars($config['serverId'], ENT_QUOTES, 'UTF-8');
|
|
$ttl = (int) $config['defaultTtl'];
|
|
$cacheTtl = (int) $config['cacheTtl'];
|
|
$enabledBadge = $config['enabled']
|
|
? '<span style="color:#28a745;font-weight:bold">enabled</span>'
|
|
: '<span style="color:#dc3545;font-weight:bold">disabled</span>';
|
|
$keyBadge = $config['apiKey'] !== '' ? '<span style="color:#28a745">set</span>' : '<span style="color:#dc3545">missing</span>';
|
|
|
|
echo '<div style="max-width:900px">';
|
|
echo '<h2 style="margin-top:0">VirtFusion DNS</h2>';
|
|
echo '<p>Reverse DNS management for the VirtFusionDirect server module. All PTR writes happen through the VirtFusion server lifecycle (create, rename, terminate) and through the client-area Reverse DNS panel. Forward DNS (A/AAAA) is verified before every PTR write; mismatches are skipped and logged.</p>';
|
|
|
|
echo '<h3>Current settings</h3>';
|
|
echo '<table class="table table-sm" style="max-width:700px"><tbody>';
|
|
echo '<tr><th style="text-align:left;width:180px">Status</th><td>' . $enabledBadge . '</td></tr>';
|
|
echo '<tr><th style="text-align:left">Endpoint</th><td><code>' . ($endpoint ?: '<em>not set</em>') . '</code></td></tr>';
|
|
echo '<tr><th style="text-align:left">API Key</th><td>' . $keyBadge . '</td></tr>';
|
|
echo '<tr><th style="text-align:left">Server ID</th><td><code>' . $serverId . '</code></td></tr>';
|
|
echo '<tr><th style="text-align:left">Default PTR TTL</th><td>' . $ttl . 's</td></tr>';
|
|
echo '<tr><th style="text-align:left">Cache TTL</th><td>' . $cacheTtl . 's</td></tr>';
|
|
echo '</tbody></table>';
|
|
|
|
echo '<h3>Test Connection</h3>';
|
|
echo '<p>Calls <code>GET /api/v1/servers/' . $serverId . '</code> and, on success, lists available zones.</p>';
|
|
echo '<a href="' . $modulelink . '&vfdns_test=1" class="btn btn-primary btn-sm">Run Test</a>';
|
|
|
|
if ($pingResult !== null) {
|
|
echo '<div style="margin-top:12px;padding:10px;border-radius:4px;background:' . ($pingResult['ok'] ? '#d4edda' : '#f8d7da') . ';color:' . ($pingResult['ok'] ? '#155724' : '#721c24') . '">';
|
|
if ($pingResult['ok']) {
|
|
echo '<strong>OK.</strong> PowerDNS reachable and authenticated. ';
|
|
if ($zoneCount !== null) {
|
|
echo $zoneCount . ' zone(s) visible.';
|
|
if (! empty($zoneSample)) {
|
|
echo '<div style="margin-top:8px;font-family:monospace;font-size:12px">';
|
|
foreach ($zoneSample as $z) {
|
|
echo htmlspecialchars($z, ENT_QUOTES, 'UTF-8') . '<br>';
|
|
}
|
|
if ($zoneCount > count($zoneSample)) {
|
|
echo '<em>... and ' . ($zoneCount - count($zoneSample)) . ' more</em>';
|
|
}
|
|
echo '</div>';
|
|
}
|
|
}
|
|
} else {
|
|
echo '<strong>Failed.</strong> HTTP ' . (int) $pingResult['http'] . ': ' . htmlspecialchars((string) ($pingResult['error'] ?? 'unknown error'), ENT_QUOTES, 'UTF-8');
|
|
}
|
|
echo '</div>';
|
|
}
|
|
|
|
echo '<h3 style="margin-top:24px">Operation</h3>';
|
|
echo '<ul>';
|
|
echo '<li><strong>On server creation:</strong> a PTR is created for each assigned IP, set to the server hostname, <em>only if the forward DNS already resolves to that IP</em>.</li>';
|
|
echo '<li><strong>On server rename:</strong> PTRs whose current content matches the <em>previous</em> hostname are updated to the new hostname; custom PTRs set by the client are preserved.</li>';
|
|
echo '<li><strong>On server termination:</strong> every PTR for the server\'s IPs is deleted from PowerDNS.</li>';
|
|
echo '<li><strong>Clients:</strong> may set a custom PTR per IP via the Reverse DNS panel on the service overview page. Forward DNS must resolve to the IP; mismatch rejects the write.</li>';
|
|
echo '<li><strong>Reconcile cron:</strong> runs daily, additive-only — creates PTRs where none exist, never overwrites.</li>';
|
|
echo '<li><strong>Reconcile (admin):</strong> a button on the admin services tab triggers an explicit reconcile with optional <em>force</em> to reset client-custom PTRs back to the server hostname.</li>';
|
|
echo '</ul>';
|
|
|
|
echo '<h3>Requirements</h3>';
|
|
echo '<ul>';
|
|
echo '<li>PowerDNS Authoritative with HTTP API enabled (<code>webserver=yes</code>, <code>api=yes</code>).</li>';
|
|
echo '<li>Reverse zones (<code>*.in-addr.arpa</code> / <code>*.ip6.arpa</code>) for your IP ranges must exist in PowerDNS already — the addon never creates zones.</li>';
|
|
echo '<li><code>api-allow-from</code> must include the WHMCS host\'s IP.</li>';
|
|
echo '</ul>';
|
|
|
|
echo '</div>';
|
|
}
|