Project

General

Profile

« Previous | Next » 

Revision 146391aa

Added by Christian McDonald 24 days ago

kea2unbound: use the new unbound fast-reload feature in v1.23

View differences:

src/usr/local/bin/kea2unbound
9 9
 *
10 10
 */
11 11

  
12
declare(strict_types=1);
13

  
12 14
namespace kea2unbound;
13 15

  
14 16
require_once 'vendor/autoload.php';
......
28 30
final class FileLockException extends \Exception {}
29 31
final class KeaConfigException extends \Exception {}
30 32
final class KeaException extends \Exception {}
31
final class UnboundException extends \Exception {}
32 33

  
33
enum DnsRecordType {
34
enum DnsRecordType
35
{
34 36
    case A;
35 37
    case AAAA;
36 38
    case PTR;
......
56 58
    }
57 59
}
58 60

  
61
class DnsRecordSet
62
{
63
    private $hash = null;
64
    private $records = [];
65

  
66
    public function __construct(
67
        private string $hashAlgo = 'xxh3'
68
    ) {}
69

  
70
    public function add(string $record): self
71
    {
72
        if (!isset($this->records[$record])) {
73
            $this->records[$record] = true;
74
            $this->hash = null; /* trigger rehash */
75
        }
76

  
77
        return ($this);
78
    }
79

  
80
    public function toArray(): array
81
    {
82
        return (array_keys($this->records));
83
    }
84

  
85
    public function getHash(): string
86
    {
87
        if ($this->hash === null) {
88
            $sortedRecords = $this->records;
89
            ksort($sortedRecords);
90
            $this->hash = hash($this->hashAlgo, serialize($sortedRecords));
91
        }
92
        return ($this->hash);
93
    }
94
}
95

  
59 96
abstract class Singleton
60 97
{
61 98
    private static array $instances = [];
......
88 125
    private array $config;
89 126
    private string $socketPath;
90 127

  
91
    protected function __construct(private string $confPath, AddressFamily $family = AddressFamily::ANY)
128
    protected function __construct(private string $confPath, AddressFamily $familyHint = AddressFamily::ANY)
92 129
    {
93 130
        $configJson = \file_get_contents($confPath);
94 131
        if ($configJson === false) {
......
126 163
        }
127 164

  
128 165
        /* Apply family constaint if provided */
129
        if (!$family->is(AddressFamily::ANY) &&
130
            !$this->addressFamily->is($family)) {
166
        if (!$familyHint->is(AddressFamily::ANY) &&
167
            !$this->addressFamily->is($familyHint)) {
131 168
                throw new KeaConfigException(
132 169
                    \sprintf(
133 170
                        \gettext("Address family mismatch: expected '%s', found '%s' in '%s'."),
134
                        $family,
171
                        $familyHint,
135 172
                        $this->addressFamily,
136 173
                        $this->confPath
137 174
                    )
......
168 205
    public function __construct(private string $lockFile = __FILE__)
169 206
    {
170 207
        if (!\file_exists($lockFile) && @\touch($lockFile)) {
208
            /* We created it, so mark for cleanup */
171 209
            $this->removeFile = true;
172 210
        }
173 211
    }
......
177 215
        $this->release();
178 216

  
179 217
        if ($this->removeFile && \file_exists($this->lockFile)) {
218
            /* We created it, so clean it up */
180 219
            @\unlink($this->lockFile);
181 220
        }
182 221
    }
......
241 280
    }
242 281
}
243 282

  
244
function unlink_safe(string $filename): bool
283
function syslogf(int $priority, string $format, mixed ...$values): true
245 284
{
246
    if (\file_exists($filename)) {
247
        try {
248
            return (\unlink($filename));
249
        } catch (\Exception $e) {
250
            syslogf(LOG_NOTICE, \gettext('Unable to unlink file: %s'), $filename);
251
        }
252
    }
253

  
254
    return (false);
285
    return (\syslog($priority, \sprintf($format, ...$values)));
255 286
}
256 287

  
257 288
function mkdir_safe(string $directory, int $permissions = 0777, bool $recursive = false): bool
......
267 298
    return (false);
268 299
}
269 300

  
270
function normalize_whitespace(string &$input): void
271
{
272
    $input = \preg_replace('/\s+/', ' ', \trim($input));
273
}
274

  
275
function unset_if(callable $testfn, mixed &...$inputs): void
276
{
277
    foreach ($inputs as &$input) {
278
        if ($testfn($input)) {
279
            unset($input);
280
        }
281
    }
282
}
283

  
284
function unset_if_empty(mixed &...$inputs): void
285
{
286
    unset_if(fn($x) => empty($x), ...$inputs);
287
}
288

  
289
function syslogf(int $priority, string $format, mixed ...$values): true
290
{
291
    return (\syslog($priority, \sprintf($format, ...$values)));
292
}
293

  
294 301
function first_of(mixed ...$inputs): mixed
295 302
{
296 303
    foreach ($inputs as $input) {
......
302 309
    return ($input);
303 310
}
304 311

  
305
function ensure_arrays(mixed &...$inputs): void
306
{
307
    foreach ($inputs as &$input) {
308
        if (!\is_array($input)) {
309
            $input = [];
310
        }
311
    }
312
}
313

  
314
function all_empty(mixed ...$inputs): bool
315
{
316
    foreach ($inputs as $input) {
317
        if (!empty($input)) {
318
            return (false);
319
        }
320
    }
321

  
322
    return (true);
323
}
324

  
325
function array_diff_key_recursive(array $array, array ...$arrays): array
326
{
327
    $diff = \array_diff_key($array, ...$arrays);
328
    foreach ($array as $key => $value) {
329
        if (!\is_array($value)) {
330
            continue;
331
        }
332
        $nested_diff = [];
333
        foreach ($arrays as $compare) {
334
            if (isset($compare[$key]) && \is_array($compare[$key])) {
335
                $nested_diff = array_diff_key_recursive($value, $compare[$key]);
336
                break;
337
            }
338
        }
339
        if (!empty($nested_diff)) {
340
            $diff[$key] = $nested_diff;
341
        }
342
    }
343

  
344
    return ($diff);
345
}
346

  
347 312
function ipv6_to_ptr(string $ip): string|false
348 313
{
349 314
    if (\filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
......
368 333
    return (ipv4_to_ptr($ip) ?: ipv6_to_ptr($ip));
369 334
}
370 335

  
371
function kea_leases_records(KeaConfig $config, ?string $fallbackDomain = null, array $subnetIds = [], bool $exclude = false): array
336
function kea_leases_records(KeaConfig $config, ?string $fallbackDomain = null, array $subnetIds = [], bool $exclude = false): DnsRecordSet
372 337
{
373
    $subnetIds = \array_flip($subnetIds); /* For O(1) lookups */
374 338
    $family = $config->getAddressFamily();
375 339

  
376
    /* Initialize and connect to socket */
340
    /* Initialize and connect to Kea control socket */
377 341
    $socket = \stream_socket_client($config->getSocketAddress(), $errno, $errstr);
378 342
    if (!$socket) {
379 343
        throw new KeaException(
......
387 351
    }
388 352

  
389 353
    /* Craft the request payload */
390
    $command = \sprintf('lease%s-get-all', $config->getAddressFamily()->value);
391
    $reqJson = \json_encode(['command' => $command]);
392

  
393
    $length = \strlen($reqJson);
394
    $written = 0;
354
    $reqCommand = \sprintf('lease%s-get-all', $family->value);
355
    $reqJson = \json_encode(['command' => $reqCommand]);
356
    $reqLength = \strlen($reqJson);
395 357

  
396 358
    /* Send request payload, handling partial writes as needed */
397
    while ($written < $length) {
398
        $bytes = \fwrite($socket, \substr($reqJson, $written), $length - $written);
399
        if ($bytes === false) {
359
    $reqWritten = 0;
360
    while ($reqWritten < $reqLength) {
361
        $wrote = \fwrite($socket, \substr($reqJson, $reqWritten), $reqLength - $reqWritten);
362
        if ($wrote === false) {
400 363
            \fclose($socket);
401 364
            throw new KeaException(
402 365
                \sprintf(
403 366
                    \gettext('Failed to send \'%s\' command to Kea control socket: %s'),
404
                    $command,
367
                    $reqCommand,
405 368
                    $config->getSocketPath()
406 369
                )
407 370
            );
408 371
        }
409
        $written += $bytes;
372
        $reqWritten += $wrote;
410 373
    }
411 374

  
412 375
    /* Receive the response payload */
413 376
    $resJson = '';
414 377
    while (!\feof($socket)) {
415
        $chunk = \fread($socket, BUFSIZ);
416
        if ($chunk === false) {
378
        $read = \fread($socket, BUFSIZ);
379
        if ($read === false) {
417 380
            \fclose($socket);
418 381
            throw new KeaException(
419 382
                \sprintf(
......
422 385
                )
423 386
            );
424 387
        }
425
        $resJson .= $chunk;
388
        $resJson .= $read;
426 389
    }
427 390

  
428 391
    /* All done with the socket */
429 392
    \fclose($socket);
430 393

  
431 394
    /* Decode and parse response payload */
432
    $res = \json_decode($resJson, true);
395
    $response = \json_decode($resJson, true);
433 396
    if (\json_last_error() !== JSON_ERROR_NONE) {
434 397
        throw new KeaException(
435 398
            \sprintf(
......
439 402
        );
440 403
    }
441 404

  
442
    $result = [];
443
    foreach ($res['arguments']['leases'] as $lease) {
405
    /* Transform Kea leases into a DNSRecordSet */
406
    $recordSet = new DnsRecordSet(HASHALGO);
407
    $subnetIds = \array_flip($subnetIds); /* For O(1) lookups */
408
    foreach ($response['arguments']['leases'] as $lease) {
444 409
        /* Apply the filtering logic */
445 410
        $inList = isset($subnetIds[$lease['subnet-id']]);
446 411
        if (($exclude && $inList) || (!$exclude && !$inList)) {
......
459 424
            }
460 425
        }
461 426

  
462
        /* Determine the domain name to use */
427
        /* Determine the domain name to use for the record */
463 428
        $option_data = &$lease['user-context']['Netgate']['option-data'];
464 429
        $domain = \rtrim(
465 430
            first_of(
466
                $option_data['domain-name'],
467
                $option_data['domain-search'][0],
431
                $option_data['domain-name'] ?? null,
432
                $option_data['domain-search'][0] ?? null,
468 433
                $fallbackDomain,
469
                'unknown.home.arpa' /* Should never get this far */
434
                'unknown.home.arpa'
470 435
            ),
471
            '.' /* Remove trailing dot */
436
            '.' /* Remove trailing dot if present */
472 437
        );
473 438

  
474 439
        /* Ensure hostname is not already a fqdn */
......
479 444

  
480 445
        /* Add address record */
481 446
        $fqdn = "{$hostname}.{$domain}.";
482
        $result[$fqdn][\implode(' ', [
447
        $recordSet->add(\implode(' ', [
483 448
            $fqdn,
484
            '%d',
449
            $ttl,
485 450
            'IN',
486 451
            $family->getDNSRecordType()->name,
487 452
            $lease['ip-address']
488
        ])] = [$ttl];
453
        ]));
489 454

  
490 455
        /* Add pointer record */
491 456
        $ptr_fqdn = ip_to_ptr($lease['ip-address']);
492
        $result[$ptr_fqdn][\implode(' ', [
457
        $recordSet->add(\implode(' ', [
493 458
            $ptr_fqdn,
494
            '%d',
459
            $ttl,
495 460
            'IN',
496 461
            'PTR',
497 462
            $fqdn
498
        ])] = [$ttl];
463
        ]));
499 464
    }
500 465

  
501
    return ($result);
466
    return ($recordSet);
502 467
}
503 468

  
504
function unbound_all_records(string $unboundConfFile): array
469
function unbound_read_include_hash(string $unboundIncludeFile): string|false
505 470
{
506
    $argv = [
507
        UBCTRLBIN,
508
        '-c', $unboundConfFile, /* proc_open will escape */
509
        'list_local_data'
510
    ];
511
    $desc = [
512
        0 => ['file', '/dev/null', 'r'],
513
        1 => ['pipe', 'w'],
514
        2 => ['file', '/dev/null', 'w']
515
    ];
516
    $result = false;
517
    $tries = 0;
518
    while ($tries < RETRIES) {
519
        $proc = \proc_open($argv, $desc, $pipes);
520
        if ($proc) {
521
            $result = [];
522
            while ($line = \fgets($pipes[1])) {
523
                normalize_whitespace($line);
524
                if (\preg_match('/^(?<name>\S+)\s+(?<ttl>\d+)\s+(?<data>.+)$/', $line, $matches)) {
525
                    $result[$matches['name']][\implode(' ', [
526
                        $matches['name'],
527
                        '%d',
528
                        $matches['data']
529
                    ])] = [(int)$matches['ttl']];
530
                }
531
            }
532
            if (\proc_close($proc) === 0) {
533
                /* All is good! */
534
                break;
535
            }
536
        }
537
        /* Something has gone wrong, try again! */
538
        $result = false;
539
        $tries++;
540
    }
541

  
542
    if ($result === false) {
543
        throw new UnboundException(
544
            \sprintf(
545
                \gettext('Unable to query Unbound Control: %s'),
546
                $unboundConfFile
547
            )
548
        );
549
    }
550

  
551
    return ($result);
552
}
553

  
554
function unbound_leases_records(string $unboundConfFile, string $unboundIncludeFile, bool $skipValidation = false): array|false
555
{
556
    $result = false;
471
    $hash = false;
557 472
    $fd = \fopen($unboundIncludeFile, 'r');
558 473
    if ($fd) {
559
        $result = [];
560
        $hasher = \hash_init(HASHALGO);
561
        \hash_update($hasher, (string) unbound_get_pid($unboundConfFile));
562
        $expectedHash = \trim(\substr(\fgets($fd), 1));
563
        while ($line = \fgets($fd)) {
564
            normalize_whitespace($line);
565
            if (\preg_match('/^local-data:\s*\"(?<record>(?<name>\S+)\s+(?<ttl>\d+)\s+(?<data>.+))\"/', $line, $matches)) {
566
                \hash_update($hasher, $matches['record']);
567
                $result[$matches['name']][\implode(' ', [
568
                    $matches['name'],
569
                    '%d',
570
                    $matches['data']
571
                ])] = [(int)$matches['ttl']];
572
            }
573
        }
574
        \fclose($fd);
575
        if (!($skipValidation || \hash_equals($expectedHash, \hash_final($hasher)))) {
576
            return (false);
577
        }
474
        /* First line is assumed to *always* be a commented hash */
475
        $hash = \trim(\substr(\fgets($fd), 1));
476
        fclose($fd);
578 477
    }
579 478

  
580
    return ($result);
479
    return ($hash);
581 480
}
582 481

  
583
function unbound_write_include(string $unboundConfFile, string $unboundIncludeFile, array $recordsByName): void
482
function unbound_write_include(string $unboundConfFile, string $unboundIncludeFile, DnsRecordSet $recordSet, bool $force = false): bool
584 483
{
585
    mkdir_safe(\dirname($unboundIncludeFile), recursive: true);
586

  
587
    $fd = \fopen($unboundIncludeFile, 'w');
588
    if ($fd) {
589
        $entries = [];
590
        $hasher = \hash_init(HASHALGO);
591
        \hash_update($hasher, (string) unbound_get_pid($unboundConfFile));
592
        foreach ($recordsByName as $name => $records) {
593
            foreach ($records as $recordf => $values) {
594
                $record = \sprintf($recordf, ...$values);
595
                \hash_update($hasher, $record);
596
                $entries[] = \sprintf('local-data: "%s"', $record);
484
    /* Gather the hashes */
485
    $oldHash = unbound_read_include_hash($unboundIncludeFile);
486
    $newHash = \hash(HASHALGO, (string) unbound_get_pid($unboundConfFile) . $recordSet->getHash());
487

  
488
    /* Determine if there is something to update on disk */
489
    if ($force || ($oldHash !== $newHash)) {
490
        mkdir_safe(\dirname($unboundIncludeFile), recursive: true);
491
        $fd = \fopen($unboundIncludeFile, 'w');
492
        if ($fd) {
493
            \fprintf($fd, "# %s\n", $newHash);
494
            \fprintf($fd, "# Automatically generated! DO NOT EDIT!\n");
495
            \fprintf($fd, "# Last updated: %s\n", \date('Y-m-d H:i:s'));
496
            foreach ($recordSet->toArray() as $record) {
497
                \fprintf($fd, "local-data: \"%s\"\n", $record);
597 498
            }
499
            \fclose($fd);
500
            syslogf(LOG_NOTICE, \gettext('Include updated: %s (%s)'), $unboundIncludeFile, $newHash);
501
            return (true);
598 502
        }
599
        $hash = \hash_final($hasher);
600
        \fprintf($fd, "# %s\n", $hash);
601
        \fprintf($fd, "# Automatically generated! DO NOT EDIT!\n");
602
        \fprintf($fd, "# Last updated: %s\n", \date('Y-m-d H:i:s'));
603
        if (!empty($entries)) {
604
            \fwrite($fd, implode("\n", $entries));
605
            \fwrite($fd, "\n");
606
        }
607
        \fclose($fd);
608
        syslogf(LOG_NOTICE, \gettext('Include updated: %s (%s)'), $unboundIncludeFile, $hash);
609 503
    }
504

  
505
    /* Nothing updated on disk */
506
    return (false);
610 507
}
611 508

  
612
function unbound_reload(string $unboundConfFile): void
509
function unbound_slow_reload(string $unboundConfFile): bool
613 510
{
614
    /* Only reloads if already running, fails otherwise */
615 511
    \exec(\implode(' ', [
616 512
        UBCTRLBIN,
617 513
        '-c', \escapeshellarg($unboundConfFile),
618
        'reload',
514
        'reload'
619 515
    ]), $_lines, $rc);
620 516
    if ($rc === 0) {
621
        syslogf(LOG_NOTICE, \gettext('Unbound reloaded: %s'), $unboundConfFile);
622
    }
623
}
624

  
625
function validate_pid_file(string $pidFilePath): int|false
626
{
627
    if (\is_readable($pidFilePath)) {
628
        $pid = \trim(\file_get_contents($pidFilePath));
629
        if ($pid !== false && \is_numeric($pid)) {
630
            $pid = (int) $pid;
631
            if (\posix_kill($pid, 0) && \posix_get_last_error() === 0) {
632
                return ($pid);
633
            }
634
        }
517
        syslogf(LOG_NOTICE, \gettext('Unbound slow reloaded: %s'), $unboundConfFile);
518
        return (true);
635 519
    }
636 520

  
637 521
    return (false);
638 522
}
639 523

  
640
function unbound_get_pid(string $unboundConfFile, bool $flush = false): int|false
524
function unbound_fast_reload(string $unboundConfFile, bool $dropQueries = false, bool $noPause = false): bool
641 525
{
642
    static $pids = [];
526
    $args = []; 
643 527

  
644
    $pid = &$pids[$unboundConfFile];
645
    if (!$flush && isset($pid)) {
646
        return ($pid);
528
    /* Drop queries that Unbound worker threads are already working on */
529
    if ($dropQueries) {
530
        $args[] = '+d';
647 531
    }
648 532

  
533
    /* Keep Unbound worker threads running during the fast(er) reload */
534
    if ($noPause) {
535
        $args[] = '+p';
536
    }
537
    
649 538
    \exec(\implode(' ', [
650 539
        UBCTRLBIN,
651 540
        '-c', \escapeshellarg($unboundConfFile),
652
        'get_option', 'pidfile'
653
    ]), $lines, $rc);
654
    if (($rc === 0)) {
655
        $tpid = validate_pid_file($lines[0]);
656
        if ($tpid !== false) {
657
            $pid = $tpid;
658
            return ($pid);
659
        }
541
        'fast_reload',
542
        ...$args
543
    ]), $_lines, $rc);
544
    if ($rc === 0) {
545
        syslogf(LOG_NOTICE, \gettext('Unbound fast reloaded: %s'), $unboundConfFile);
546
        return (true);
660 547
    }
661 548

  
662
    return (false);
549
    syslogf(LOG_ERR, \gettext('Unbound fast reload not available: %s'), $unboundConfFile);
550

  
551
    /* try again using the old (slow) reload command */
552
    return (unbound_slow_reload($unboundConfFile));
663 553
}
664 554

  
665
function unbound_local_datas(string $unboundConfFile, array $recordsByName): bool
555
function pid_is_running(int $pid): bool
666 556
{
667
    $argv = [
668
        UBCTRLBIN,
669
        '-c', $unboundConfFile, /* proc_open will escape */
670
        'local_datas'
671
    ];
672
    $desc = [
673
        0 => ['pipe', 'r'],
674
        1 => ['file', '/dev/null', 'w'],
675
        2 => ['file', '/dev/null', 'w']
676
    ];
677
    $result = false;
678
    $tries = 0;
679
    while ($tries < RETRIES) {
680
        $proc = \proc_open($argv, $desc, $pipes);
681
        if ($proc) {
682
            foreach ($recordsByName as $name => $records) {
683
                foreach ($records as $recordf => $values) {
684
                    $record = \sprintf($recordf, ...$values);
685
                    syslogf(LOG_NOTICE, \gettext('Record installed: "%s"'), $record);
686
                    \fwrite($pipes[0], "{$record}\n");
687
                }
688
            }
689
            if (\proc_close($proc) === 0) {
690
                /* All is good! */
691
                $result = true;
692
                break;
693
            }
694
        }
695
        /* Something has gone wrong, try again! */
696
        $tries++;
697
    }
698

  
699
    return ($result);
557
    return (\posix_kill($pid, 0) && (\posix_get_last_error() === 0));
700 558
}
701 559

  
702
function unbound_local_datas_remove(string $unboundConfFile, array $recordsByName): bool
560
function pid_file_read(string $pidFilePath): int|false
703 561
{
704
    $argv = [
705
        UBCTRLBIN,
706
        '-c', $unboundConfFile, /* proc_open will escape */
707
        'local_datas_remove'
708
    ];
709
    $desc = [
710
        0 => ['pipe', 'r'],
711
        1 => ['file', '/dev/null', 'w'],
712
        2 => ['file', '/dev/null', 'w']
713
    ];
714
    $tries = 0;
715
    while ($tries < RETRIES) {
716
        $proc = \proc_open($argv, $desc, $pipes);
717
        if ($proc) {
718
            foreach ($recordsByName as $name => $records) {
719
                foreach ($records as $recordf => $values) {
720
                    $record = \sprintf($recordf, ...$values);
721
                    syslogf(LOG_NOTICE, \gettext('Record removed: "%s"'), $record);
722
                }
723
                /* One name per line */
724
                \fwrite($pipes[0], $name);
725
                \fwrite($pipes[0], "\n");
726
            }
727
            if (\proc_close($proc) === 0) {
728
                return (true);
729
            }
562
    $ret = false;
563

  
564
    if (\is_readable($pidFilePath)) {
565
        $pid = \trim(\file_get_contents($pidFilePath));
566
        if ($pid !== false && \is_numeric($pid)) {
567
            $ret = (int) $pid;
730 568
        }
731
        /* Something has gone wrong, try again! */
732
        $tries++;
733 569
    }
734 570

  
735
    return (false);
736
}
737

  
738
function unbound_flush(string $unboundConfFile, $unboundIncludeFile): void
739
{
740
    unbound_write_include($unboundConfFile, $unboundIncludeFile, []);
741
    unbound_reload($unboundConfFile);
571
    return ($ret);
742 572
}
743 573

  
744
function unbound_sync(string $unboundConfFile, array $newAllRecordsByName, array $oldAllRecordsByName): bool
574
function unbound_get_pid(string $unboundConfFile, bool $flush = false): int|false
745 575
{
746
    $toRemove = array_diff_key_recursive($oldAllRecordsByName, $newAllRecordsByName);
747
    $toAdd = array_diff_key_recursive($newAllRecordsByName, $oldAllRecordsByName);
576
    static $pidCache = [];
748 577

  
749
    foreach ($toRemove as $name => $records) {
750
        if (isset($newAllRecordsByName[$name])) {
751
            $toAdd[$name] = array_diff_key_recursive($newAllRecordsByName[$name], $records);
752
        }
578
    if (!$flush && isset($pidCache[$unboundConfFile]) && pid_is_running($pidCache[$unboundConfFile])) {
579
        return ($pidCache[$unboundConfFile]);
753 580
    }
754 581

  
755
    /* Nothing to do */
756
    if (all_empty($toRemove, $toAdd)) {
757
        return (false);
758
    }
759

  
760
    /* Apply removals */
761
    if (!empty($toRemove) && !unbound_local_datas_remove($unboundConfFile, $toRemove)) {
762
        syslogf(LOG_ERR, \gettext('Error processing records to remove'));
763
        return (false);
764
    }
582
    unset($pidCache[$unboundConfFile]);
765 583

  
766
    /* Apply additions */
767
    if (!empty($toAdd) && !unbound_local_datas($unboundConfFile, $toAdd)) {
768
        syslogf(LOG_ERR, \gettext('Error processing records to install'));
769
        unbound_reload($unboundConfFile);
770
        return (false);
584
    \exec(\implode(' ', [
585
        UBCTRLBIN,
586
        '-c', \escapeshellarg($unboundConfFile),
587
        'get_option', 'pidfile'
588
    ]), $lines, $rc);
589
    if (($rc === 0)) {
590
        $pid = pid_file_read($lines[0]);
591
        if (($pid !== false) && pid_is_running($pid)) {
592
            $pidCache[$unboundConfFile] = $pid;
593
            return ($pid);
594
        }
771 595
    }
772 596

  
773
    /* Things happened! */
774
    return (true);
597
    return (false);
775 598
}
776 599

  
777
function records_sync(array $leaseRecords, array &$installedRecords, array &$allRecords): void
600
function unbound_is_running(string $unboundConfFile): bool
778 601
{
779
    /* Process records to add */
780
    foreach ($leaseRecords as $name => $records) {
781
        ensure_arrays($records, $installedRecords[$name], $allRecords[$name]);
782
        $to_add = \array_diff_key($records, $allRecords[$name]);
783
        $installedRecords[$name] = \array_merge($installedRecords[$name], $to_add);
784
        $allRecords[$name] = \array_merge($allRecords[$name], $to_add);
785
        unset_if_empty($installedRecords[$name], $allRecords[$name]);
786
    }
787

  
788
    /* Process records to remove */
789
    foreach ($installedRecords as $name => $records) {
790
        ensure_arrays($records, $leaseRecords[$name], $allRecords[$name]);
791
        $to_remove = \array_diff_key($records, $leaseRecords[$name]);
792
        $installedRecords[$name] = \array_diff_key($records, $to_remove);
793
        $allRecords[$name] = \array_diff_key($allRecords[$name], $to_remove);
794
        unset_if_empty($installedRecords[$name], $allRecords[$name]);
795
    }
602
    return (unbound_get_pid($unboundConfFile) !== false);
796 603
}
797 604

  
798 605
class FlushCommand extends Command
......
823 630
        try {
824 631
            $lock->aquire();
825 632

  
633
            /* Parse Kea configuration */
826 634
            $keaConfFile = $input->getOption('kea-conf');
827 635
            $keaConfig = KeaConfig::getInstance($keaConfFile);
828 636
            $family = $keaConfig->getAddressFamily();
829 637

  
830
            $includeFile = \sprintf($input->getOption('include-file'), $family->value);
638
            /* Parse Unbound configuration */
831 639
            $unboundConfFile = $input->getOption('unbound-conf');
640
            $unboundIncludeFile = $input->getOption('include-file');
641

  
642
            /* Writing an empty record set is a flush */
643
            $leaseRecordSet = new DnsRecordSet(HASHALGO);
832 644

  
833
            unbound_flush($unboundConfFile, $includeFile);
834
            $flushHappened = true;
645
            /* Write out include as necessary and reload Unbound accordingly */
646
            if (unbound_write_include($unboundConfFile, $unboundIncludeFile, $leaseRecordSet, true)) {
647
                $flushHappened = true;
648
                if (unbound_is_running($unboundConfFile)) {
649
                    unbound_fast_reload($unboundConfFile);
650
                }
651
            }
835 652
        } catch (Exception $e) {
836 653
            syslogf(LOG_ERR, $e->getMessage());
837 654
            $ret = Command::FAILURE;
......
855 672
    {
856 673
        $this
857 674
            ->setName('sync')
858
            ->setDescription(\gettext('Sync Kea lease records with Unbound'))
675
            ->setDescription(\gettext('Sync Kea lease records with Unbound (fast)'))
859 676
            ->addOption(
860 677
                'exclude',
861 678
                'x',
......
902 719
            $family = $keaConfig->getAddressFamily();
903 720

  
904 721
            /* Grab lease records from Kea */
905
            $leaseRecords = kea_leases_records(
722
            $leaseRecordSet = kea_leases_records(
906 723
                $keaConfig,
907 724
                $input->getOption('fallback-domain'),
908 725
                $input->getOption('subnet-id'),
......
911 728

  
912 729
            /* Parse Unbound configuration */
913 730
            $unboundConfFile = $input->getOption('unbound-conf');
914
            $includeFile = \sprintf($input->getOption('include-file'), $family->value);
915

  
916
            /* Grab installed lease records from Unbound */
917
            $installedRecords = unbound_leases_records($unboundConfFile, $includeFile);
918
            if ($installedRecords === false) {
919
                syslogf(LOG_NOTICE, \gettext('Unbound lease include is missing or inconsistent: %s'), $includeFile);
920
                unbound_flush($unboundConfFile, $includeFile);
921
                $installedRecords = []; /* No need to reparse */
922
            }
923

  
924
            /* Finaly, grab *two* copies of the Unbound cache, one to mutate and one to compare against */
925
            $desiredRecords = $currentRecords = unbound_all_records($unboundConfFile);
731
            $unboundIncludeFile = $input->getOption('include-file');
926 732

  
927
            /* Mutate the first copy in-place (this becomces the "desired" state) */
928
            records_sync($leaseRecords, $installedRecords, $desiredRecords);
929

  
930
            /* Reconcile the desired state with the original copy (the "current" state) */
931
            if (unbound_sync($unboundConfFile, $desiredRecords, $currentRecords)) {
932
                /* Update the include file *only* if something changed */
933
                unbound_write_include($unboundConfFile, $includeFile, $installedRecords);
733
            /* Write out include as necessary and reload Unbound accordingly */
734
            if (unbound_write_include($unboundConfFile, $unboundIncludeFile, $leaseRecordSet)) {
934 735
                $syncHappened = true;
736
                if (unbound_is_running($unboundConfFile)) {
737
                    unbound_fast_reload($unboundConfFile);
738
                }
935 739
            }
936 740
        } catch (Exception $e) {
937 741
            syslogf(LOG_ERR, $e->getMessage());
......
941 745
        }
942 746

  
943 747
        if ($syncHappened) {
944
            syslogf(LOG_NOTICE, \gettext('Syncronization completed: %.4fms'), (\microtime(true) - $startTime) * 1000);
748
            syslogf(LOG_NOTICE, \gettext('Synchronization completed: %.4fms'), (\microtime(true) - $startTime) * 1000);
945 749
        }
946 750

  
947 751
        \closelog();
......
958 762
        'i',
959 763
        InputOption::VALUE_REQUIRED,
960 764
        \gettext('Unbound include file'),
961
        '/var/unbound/leases/leases%s.conf'
765
        '/var/unbound/leases/leases4.conf'
962 766
    ),
963 767
    new InputOption(
964 768
        'kea-conf',
......
985 789
$app->add(new FlushCommand());
986 790
$app->add(new SyncCommand());
987 791

  
988
$app->run();
792
$app->run();

Also available in: Unified diff