Added CDS/CDNSKEY scanner. Needs live tests

This commit is contained in:
Pinga 2025-07-14 10:25:38 +03:00
parent 9d5c912e15
commit e413072ca4
3 changed files with 236 additions and 0 deletions

229
automation/cds_scanner.php Normal file
View file

@ -0,0 +1,229 @@
<?php
use Swoole\Coroutine;
use Swoole\Coroutine\Channel;
use Swoole\Coroutine\WaitGroup;
require __DIR__ . '/vendor/autoload.php';
$c = require_once 'config.php';
require_once 'helpers.php';
$logFilePath = '/var/log/namingo/cds_scanner.log';
$log = setupLogger($logFilePath, 'CDS_Scanner');
$log->info('job started.');
$pool = new Swoole\Database\PDOPool(
(new Swoole\Database\PDOConfig())
->withDriver($c['db_type'])
->withHost($c['db_host'])
->withPort($c['db_port'])
->withDbName($c['db_database'])
->withUsername($c['db_username'])
->withPassword($c['db_password'])
->withCharset('utf8mb4')
);
Co\run(function () use ($pool, $log) {
$concurrency = 20;
$chan = new Channel($concurrency);
$wg = new WaitGroup();
$failedDomains = [];
$pdo = $pool->get();
$stmt = $pdo->query("SELECT id, name FROM domain");
$domains = $stmt->fetchAll(PDO::FETCH_ASSOC);
$pool->put($pdo);
foreach ($domains as $domain) {
$chan->push(true);
$wg->add();
go(function () use ($domain, $pool, $chan, $wg, $log, &$failedDomains) {
defer(function () use ($chan, $wg) {
$chan->pop();
$wg->done();
});
$pdo = $pool->get();
// Get NS hosts
$stmt = $pdo->prepare("SELECT host_id FROM domain_host_map WHERE domain_id = ?");
$stmt->execute([$domain['id']]);
$host_ids = $stmt->fetchAll(PDO::FETCH_COLUMN);
if (empty($host_ids)) {
$pool->put($pdo);
return;
}
$host_stmt = $pdo->prepare("SELECT name FROM host WHERE id = ?");
foreach ($host_ids as $host_id) {
$host_stmt->execute([$host_id]);
$ns = $host_stmt->fetchColumn();
if (!$ns) continue;
// Prefer CDS
$cds = digRecords($domain['name'], $ns, 'CDS', $failedDomains);
if (!empty($cds) && validateCDS($cds)) {
try {
$log->info("Valid CDS for {$domain['name']} via $ns: " . json_encode($cds, JSON_THROW_ON_ERROR));
} catch (Throwable $e) {
$log->warning("CDS found for {$domain['name']} via $ns but failed to log JSON: " . $e->getMessage());
}
foreach ($cds as $rr) {
insertSecdnsDS($pdo, $domain['id'], $rr);
}
break;
}
// Fallback to CDNSKEY + generate DS
$cdnskey = digRecords($domain['name'], $ns, 'CDNSKEY', $failedDomains);
if (!empty($cdnskey)) {
$valid = array_filter($cdnskey, fn($k) => validateCDNSKEY($k));
foreach ($valid as $k) {
$ds = generateDSFromDNSKEY($domain['name'], $k);
if ($ds) insertSecdnsDS($pdo, $domain['id'], $ds);
}
break;
}
}
$pool->put($pdo);
});
}
$wg->wait();
if (!empty($failedDomains)) {
$log->info('--- DIG ERRORS SUMMARY ---');
foreach ($failedDomains as $line) {
$log->info($line);
}
}
$log->info('job finished successfully.');
});
// Run dig and parse
function digRecords(string $domain, string $ns, string $type, &$failedDomains): array {
static $logged = [];
$cmd = "dig @$ns +short $domain $type 2>&1";
$output = shell_exec($cmd);
if (!is_string($output)) return [];
$output = trim($output);
if ($output === '') return [];
$key = "$domain|$type";
$err = null;
if (stripos($output, "couldn't get address") !== false) {
$err = "NS unreachable";
} elseif (preg_match('/(connection timed out|SERVFAIL|refused|not found)/i', $output)) {
$err = "DNS error";
}
if ($err !== null && !isset($logged[$key])) {
$logged[$key] = true;
$failedDomains[] = "$domain ($type) via $ns - $err";
}
if ($err !== null) return [];
$results = [];
foreach (explode("\n", $output) as $line) {
if (empty($line)) continue;
$parts = preg_split('/\s+/', trim($line));
if ($type === 'CDS' && count($parts) >= 4) {
$results[] = [
'keytag' => (int)($parts[0] ?? 0),
'alg' => (int)($parts[1] ?? 0),
'digesttype' => (int)($parts[2] ?? 0),
'digest' => $parts[3] ?? '',
];
} elseif ($type === 'CDNSKEY' && count($parts) >= 4) {
$results[] = [
'flags' => (int)($parts[0] ?? 0),
'protocol' => (int)($parts[1] ?? 0),
'alg' => (int)($parts[2] ?? 0),
'pubkey' => implode('', array_slice($parts, 3)),
];
}
}
return $results;
}
function validateCDS(array $rr): bool {
return isset($rr['alg'], $rr['digesttype'], $rr['digest']) &&
$rr['alg'] >= 1 &&
$rr['digesttype'] >= 1 &&
strlen($rr['digest']) > 20;
}
function validateCDNSKEY(array $rr): bool {
return isset($rr['protocol'], $rr['alg'], $rr['pubkey']) &&
$rr['protocol'] === 3 &&
$rr['alg'] >= 1 &&
!empty($rr['pubkey']);
}
function generateDSFromDNSKEY(string $domain, array $rr): ?array {
if (!isset($rr['flags'], $rr['protocol'], $rr['alg'], $rr['pubkey'])) {
return null;
}
try {
$rdata = pack('nCC', $rr['flags'], $rr['protocol'], $rr['alg']) . base64_decode($rr['pubkey']);
$keytag = keyTag($rdata);
$digest_sha256 = strtolower(hash('sha256', canonicalDNSName($domain) . $rdata));
return [
'keytag' => $keytag,
'alg' => $rr['alg'],
'digesttype' => 2,
'digest' => $digest_sha256
];
} catch (Throwable $e) {
return null;
}
}
function canonicalDNSName(string $name): string {
$labels = explode('.', strtolower($name));
$out = '';
foreach ($labels as $label) {
$len = strlen($label);
$out .= chr($len) . $label;
}
return $out . chr(0);
}
function keyTag(string $rdata): int {
$ac = 0;
$len = strlen($rdata);
for ($i = 0; $i < $len; $i++) {
$ac += ($i & 1) ? ord($rdata[$i]) : ord($rdata[$i]) << 8;
}
$ac += ($ac >> 16) & 0xFFFF;
return $ac & 0xFFFF;
}
function insertSecdnsDS(Swoole\Database\PDOProxy $pdo, int $domain_id, array $r) {
$stmt = $pdo->prepare("
INSERT IGNORE INTO secdns
(domain_id, interface, keytag, alg, digesttype, digest)
VALUES (?, 'dsData', ?, ?, ?, ?)
");
$stmt->execute([
$domain_id,
$r['keytag'],
$r['alg'],
$r['digesttype'],
$r['digest'],
]);
}

View file

@ -17,6 +17,7 @@
// 'gtld_mode' => false, // Enable or disable gTLD mode
// 'spec11' => false, // Enable or disable Spec 11 checks
// 'exchange_rates' => false, // Enable or disable exchange rate download
// 'cds_scanner' => false, // Enable or disable CDS/CDNSKEY scanning and DS publishing to the zone
// ];
//
// Any keys omitted in cron_config.php will fall back to the defaults
@ -31,6 +32,7 @@ $defaultConfig = [
'gtld_mode' => false, // Set to true to enable
'spec11' => false, // Set to true to enable
'exchange_rates' => false, // Set to true to enable
'cds_scanner' => false, // Set to true to enable
];
// Load External Config if Exists
@ -90,5 +92,9 @@ if ($cronJobConfig['exchange_rates']) {
$scheduler->php('/opt/registry/automation/exchange-rates.php')->at('0 1 * * *');
}
if ($cronJobConfig['cds_scanner']) {
$scheduler->php('/opt/registry/automation/cds_scanner.php')->at('0 */6 * * *');
}
// Run Scheduled Tasks
$scheduler->run();

View file

@ -261,6 +261,7 @@ return [
'gtld_mode' => false, // Enable or disable gTLD mode
'spec11' => false, // Enable or disable Spec 11 checks
'exchange_rates' => false, // Enable or disable exchange rate download
'cds_scanner' => false, // Enable or disable CDS/CDNSKEY scanning and DS publishing to the zone
];
```