Revision 146391aa
Added by Christian McDonald 24 days ago
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
kea2unbound: use the new unbound fast-reload feature in v1.23