remove fallback hard-coded config

This commit is contained in:
2026-01-24 15:26:52 -05:00
parent b6af2cefe2
commit 9700ef19d8

64
DDNS.pm
View File

@@ -8,7 +8,7 @@ use JSON::PP qw/decode_json encode_json/;
memoize('_gethosts'); memoize('_gethosts');
our $VERSION = '0.7'; our $VERSION = '0.8';
# Control plane update keyfile (zone-management-key) # Control plane update keyfile (zone-management-key)
our $keyfile = '/etc/bind/zone-management.key'; our $keyfile = '/etc/bind/zone-management.key';
@@ -17,9 +17,6 @@ sub new {
my $me = shift; my $me = shift;
my %opts = @_; my %opts = @_;
# 'server' is where dig/axfr queries go for reading the control plane.
# On the hidden master, localhost is correct. On other hosts, you may
# pass 'server' => '<master-ip>'.
my $server = $opts{server} // 'localhost'; my $server = $opts{server} // 'localhost';
return bless { return bless {
@@ -58,23 +55,23 @@ sub _lookupOrDie {
_validateOrDie($dom); _validateOrDie($dom);
my $fqdn = _fqdn($dom, $type); my $fqdn = _fqdn($dom, $type);
my $answer;
my $all = ''; my $all = '';
my $fh; my $fh;
open($fh, "dig +short -t txt \@$this->{server} $fqdn |") open($fh, "dig +short -t txt \@$this->{server} $fqdn |")
|| die "Can't open dig: $!"; || die "Can't open dig: $!";
# dig +short returns one line per TXT RR, each possibly containing multiple chunks.
# We accept "exists" if we saw any quoted chunk at all.
while (<$fh>) { while (<$fh>) {
$all .= $_; $all .= $_;
if ($_ =~ /^\"(.+)\"$/) { if ($_ =~ /\"/) { # any TXT output
$answer = $1; close $fh;
}
}
if ($answer) {
return 1; return 1;
} else {
die "query failed: $all\n";
} }
} }
close $fh;
die "query failed: $all\n";
}
sub __docmd { sub __docmd {
my ($this, $cmd) = @_; my ($this, $cmd) = @_;
@@ -83,14 +80,11 @@ sub __docmd {
close $tmpfh; close $tmpfh;
my $fh; my $fh;
# Capture BOTH stdout and stderr into the temp file so errors are visible,
# and so we can fail the caller when nsupdate fails.
open($fh, "|nsupdate -k $this->{keyfile} > $filename 2>&1") open($fh, "|nsupdate -k $this->{keyfile} > $filename 2>&1")
|| die "Can't open nsupdate: $!"; || die "Can't open nsupdate: $!";
print $fh "server localhost\nzone private.invalid.\n$cmd\nshow\nsend\n"; print $fh "server localhost\nzone private.invalid.\n$cmd\nshow\nsend\n";
# If nsupdate fails, close() will return false and $? will be non-zero.
close($fh) or do { close($fh) or do {
open(my $rfh, $filename) || die "Can't re-open tmpfile $filename: $!"; open(my $rfh, $filename) || die "Can't re-open tmpfile $filename: $!";
my $out = do { local $/; <$rfh> }; my $out = do { local $/; <$rfh> };
@@ -99,7 +93,6 @@ sub __docmd {
die "nsupdate failed (exit=$?):\n$out\n"; die "nsupdate failed (exit=$?):\n$out\n";
}; };
# Echo nsupdate output (useful for operators/logs)
open(my $rfh, $filename) || die "Can't re-open tmpfile $filename: $!"; open(my $rfh, $filename) || die "Can't re-open tmpfile $filename: $!";
while (<$rfh>) { while (<$rfh>) {
print; print;
@@ -112,15 +105,12 @@ sub __docmd {
sub _txt_from_line { sub _txt_from_line {
my ($line) = @_; my ($line) = @_;
# TXT in dig AXFR output may be one or more quoted segments.
# Collect all segments and concatenate.
my @parts; my @parts;
while ($line =~ /\"((?:\\.|[^\"])*)\"/g) { while ($line =~ /\"((?:\\.|[^\"])*)\"/g) {
push @parts, $1; push @parts, $1;
} }
my $txt = join('', @parts); my $txt = join('', @parts);
# Unescape common presentation escapes (at minimum \" and \\).
$txt =~ s/\\\"/\"/g; $txt =~ s/\\\"/\"/g;
$txt =~ s/\\\\/\\/g; $txt =~ s/\\\\/\\/g;
@@ -142,7 +132,6 @@ sub _parse_txt_payload {
if (defined $obj) { if (defined $obj) {
$payload = $obj if ref($obj) eq 'HASH'; $payload = $obj if ref($obj) eq 'HASH';
# Normalize masters string from supported JSON shapes.
if (ref($obj) eq 'HASH') { if (ref($obj) eq 'HASH') {
if (defined $obj->{masters}) { if (defined $obj->{masters}) {
if (ref($obj->{masters}) eq 'ARRAY') { if (ref($obj->{masters}) eq 'ARRAY') {
@@ -169,20 +158,17 @@ sub _parse_txt_payload {
} }
# Produce BIND nsupdate TXT RDATA as one or more quoted strings. # Produce BIND nsupdate TXT RDATA as one or more quoted strings.
# BIND limits each TXT "character-string" to 255 bytes, so we chunk. # Each TXT character-string is limited to 255 bytes.
sub _quote_txt_rdata { sub _quote_txt_rdata {
my ($s) = @_; my ($s) = @_;
$s = '' unless defined $s; $s = '' unless defined $s;
# Escape for inclusion inside a quoted TXT character-string
$s =~ s/\\/\\\\/g; $s =~ s/\\/\\\\/g;
$s =~ s/\"/\\\"/g; $s =~ s/\"/\\\"/g;
# Conservative chunk size to stay well under 255 bytes after escaping
my $chunk_len = 200; my $chunk_len = 200;
my @chunks = ($s =~ /.{1,$chunk_len}/gs); my @chunks = ($s =~ /.{1,$chunk_len}/gs);
# Emit as: "chunk1" "chunk2" "chunk3"
return join(' ', map { "\"$_\"" } @chunks); return join(' ', map { "\"$_\"" } @chunks);
} }
@@ -213,6 +199,7 @@ sub _gethosts {
} }
} }
} }
close $fh;
return @vh; return @vh;
} }
@@ -248,7 +235,6 @@ sub set {
my ($this, $dom, $txt_payload, $type) = @_; my ($this, $dom, $txt_payload, $type) = @_;
_validateOrDie($dom); _validateOrDie($dom);
# Ensure it exists (for friendly errors)
_lookupOrDie($this, $dom, $type); _lookupOrDie($this, $dom, $type);
my $fqdn = _fqdn($dom, $type); my $fqdn = _fqdn($dom, $type);
@@ -296,54 +282,30 @@ sub is_dnssec {
return 0; return 0;
} }
sub _default_config {
return {
default_master => '5.78.68.64',
vhost_default_web_ip => '190.92.153.173',
soa_mname => 'mister-dns.martnet.com.',
soa_rname => 'root.martnet.com.',
ns => [ 'ns1.martnet.com.', 'ns2.martnet.com.' ],
vhost_mx => [
{ priority => 5, host => '@' },
{ priority => 10, host => 'mx2.martnet.com.' },
{ priority => 15, host => 'mx3.martnet.com.' },
],
master_ddns_keyname => 'master-ddns-tsig',
master_ddns_keyfile => '/etc/bind/master-ddns-tsig.key',
};
}
sub get_config { sub get_config {
my ($this) = @_; my ($this) = @_;
# Stored at: global._config.private.invalid.
my @cfg = $this->get('_config'); my @cfg = $this->get('_config');
foreach my $r (@cfg) { foreach my $r (@cfg) {
next unless lc($r->{zone}) eq 'global'; next unless lc($r->{zone}) eq 'global';
my $p = $r->{payload}; my $p = $r->{payload};
if (defined $p && ref($p) eq 'HASH') { if (defined $p && ref($p) eq 'HASH') {
# Merge defaults for any missing keys
my $d = _default_config();
for my $k (keys %$d) {
$p->{$k} = $d->{$k} unless exists $p->{$k};
}
return $p; return $p;
} }
} }
return _default_config();
die "_config is not present in the control plane (expected global._config.private.invalid TXT). Bootstrap it before running tools.\n";
} }
sub set_config { sub set_config {
my ($this, $cfg_hashref) = @_; my ($this, $cfg_hashref) = @_;
die "Config must be a hashref" unless (defined $cfg_hashref && ref($cfg_hashref) eq 'HASH'); die "Config must be a hashref" unless (defined $cfg_hashref && ref($cfg_hashref) eq 'HASH');
# Always store compact canonical JSON (encode_json is compact)
my $txt = encode_json($cfg_hashref); my $txt = encode_json($cfg_hashref);
my $dom = "global."; my $dom = "global.";
my $type = "_config"; my $type = "_config";
# Upsert behavior: if record exists, set, else add.
my $existing; my $existing;
eval { $existing = $this->type($dom); 1 }; eval { $existing = $this->type($dom); 1 };
if ($existing) { if ($existing) {