remove fallback hard-coded config
This commit is contained in:
64
DDNS.pm
64
DDNS.pm
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user