Compare commits

...

21 Commits

Author SHA1 Message Date
03ec948359 doesn't really belong in this repo but I don't want to make a new repo for just this dnssec key validation tool 2025-09-09 20:37:11 +00:00
244df117fc swap ip for mister-dns 2025-04-08 11:57:30 -04:00
3b5a21d819 removed Net::DDNS and updated master 2023-11-12 17:29:33 +00:00
23b5a19166 update address/key for mister-dns 2023-11-12 11:40:06 -05:00
6ca3346a03 hard code mister-dns IP for chicken-and-egg problems 2023-10-28 23:41:51 -04:00
302c9ba09e missed a carat 2023-10-12 00:01:56 -04:00
aeeda96aca fix syntax 2023-10-11 23:57:43 -04:00
04076c0c35 pattern fix 2023-10-11 23:54:18 -04:00
e1068644f3 fixing tsig key use for notifies 2023-10-11 23:47:39 -04:00
0758403058 adding notify-key 2023-10-11 23:11:59 -04:00
c457fd1ed8 preserve original uid/gid of slave zone file 2023-10-09 21:38:09 -04:00
7b50aa74ab fixed regex 2022-11-01 13:46:17 +00:00
ca21337cbd another attempted fix at syntax 2022-11-01 09:08:16 -04:00
05eba37a2f fix master addys 2022-11-01 09:01:55 -04:00
8339247a1e add path to rndc 2022-11-01 08:59:48 -04:00
a69fb1dbd3 add missing lister to make 2022-11-01 08:44:01 -04:00
2733c6d02c add missing lister 2022-11-01 08:43:18 -04:00
1c0ae8137f fix multiple masters 2022-11-01 08:37:32 -04:00
9d58404b7d add v6 for all types 2022-11-01 08:34:42 -04:00
3f534cfe90 add v6 addy 2022-11-01 08:31:03 -04:00
d731312ebc logic fix? 2021-10-31 13:46:25 -04:00
9 changed files with 213 additions and 34 deletions

56
DDNS.pm
View File

@ -2,20 +2,24 @@ package Martnet::DDNS;
use strict; use strict;
use warnings; use warnings;
use Net::DNS;
use File::Temp qw/tempfile/; use File::Temp qw/tempfile/;
use Memoize; use Memoize;
memoize('_gethosts'); memoize('_gethosts');
our $VERSION = '0.3'; our $VERSION = '0.4';
# our $misterdns = '198.251.79.234';
our $misterdns = '5.78.68.64';
our $defaultmaster = '5.78.68.64'; # could be v4 and v6 like '198.251.79.234;2607:f1c0:86e:b66f:6b86:babb:c367:b0dc'
our $keyfile = '/etc/bind/zone-management.key';
sub new { sub new {
my $me = shift; my $me = shift;
my %opts = @_; my %opts = @_;
return bless { return bless {
'keyfile' => '/etc/bind/Kprivate.invalid.+157+14348.key', 'keyfile' => $keyfile,
%opts %opts
}, $me; }, $me;
} }
@ -51,20 +55,26 @@ sub _lookupOrDie {
_validateOrDie($dom); _validateOrDie($dom);
my $fqdn = _fqdn($dom, $type); my $fqdn = _fqdn($dom, $type);
my $res = Net::DNS::Resolver->new; my $answer;
my $query = $res->query($fqdn, "TXT"); my $all = '';
my $fh;
if ($query) { open($fh, "dig +short -t txt \@${misterdns} $fqdn |")
foreach my $rr (grep { $_->type eq 'TXT' } $query->answer) { || die "Can't open dig: $!";
# We found a record; that's all that I care about. while (<$fh>) {
#print $rr->nsdname, "\n"; $all .= $_;
return 1; if ($_ =~ /^\"(.+)\"$/) {
$answer = $1;
} }
} }
if ($answer) {
# We found a record; that's all I care about.
return 1;
}
else { else {
die "query failed: ", $res->errorstring, "\n"; die "query failed: $all\n";
} }
# Unreached
die "Failed to find existing DNS record for $fqdn"; die "Failed to find existing DNS record for $fqdn";
} }
@ -99,7 +109,7 @@ sub _gethosts {
} }
my $fh; my $fh;
open($fh, "dig -t AXFR \@mister-dns.martnet.com private.invalid. |") open($fh, "dig -t AXFR \@${misterdns} private.invalid. |")
|| die "Can't open dig: $!"; || die "Can't open dig: $!";
my @vh; my @vh;
@ -107,16 +117,20 @@ sub _gethosts {
while (<$fh>) { while (<$fh>) {
if ($type) { if ($type) {
if (/^(\S+)\.$type\.private\.invalid\.\s+\d+\s+IN\s+TXT\s+\"(.+)\"$/) { if (/^(\S+)\.$type\.private\.invalid\.\s+\d+\s+IN\s+TXT\s+\"(.+)\"$/) {
push (@vh, { zone => $1, my ($z, $m) = ($1, $2);
$m =~ s/\"//g;
push (@vh, { zone => $z,
type => $type, type => $type,
master => $2 }); master => $m });
} }
} else { } else {
# Querying everything # Querying everything
if (/^(\S+)\.(\S+)\.private\.invalid\.\s+\d+\s+IN\s+TXT\s+\"(.+)\"$/) { if (/^(\S+)\.(\S+)\.private\.invalid\.\s+\d+\s+IN\s+TXT\s+\"(.+)\"$/) {
push (@vh, { zone => $1, my ($z, $t, $m) = ($1, $2, $3);
type => $2, $m =~ s/\"//g;
master => $3 }); push (@vh, { zone => $z,
type => $t,
master => $m });
} }
} }
} }
@ -198,5 +212,11 @@ sub is_dnssec {
return 0; return 0;
} }
sub default_master {
my ($this, $type) = @_;
# This could return different masters for different types
return $defaultmaster;
}
1; 1;

View File

@ -3,8 +3,7 @@ use ExtUtils::MakeMaker;
WriteMakefile( WriteMakefile(
'NAME' => 'Martnet::DDNS', 'NAME' => 'Martnet::DDNS',
'VERSION_FROM' => 'DDNS.pm', 'VERSION_FROM' => 'DDNS.pm',
'PREREQ_PM' => { Net::DNS => 0.66, 'PREREQ_PM' => { File::Temp => 0.22,
File::Temp => 0.22,
Regexp::Common => 2013031301, Regexp::Common => 2013031301,
}, },
'EXE_FILES' => [ 'bin/add-vhost', 'EXE_FILES' => [ 'bin/add-vhost',
@ -23,6 +22,7 @@ WriteMakefile(
'bin/validate-master', 'bin/validate-master',
'bin/add-dnssec', 'bin/add-dnssec',
'bin/del-dnssec', 'bin/del-dnssec',
'bin/list-dnssec',
], ],
'AUTHOR' => 'Jorj Bauer <jorj@jorj.org>', 'AUTHOR' => 'Jorj Bauer <jorj@jorj.org>',
); );

View File

@ -7,7 +7,8 @@ use Regexp::Common qw/net/;
my $host = shift || die "No zonename provided"; my $host = shift || die "No zonename provided";
my $master = shift; my $master = shift;
$master ||= '198.251.79.234'; my $ddns = Martnet::DDNS->new();
$master ||= $ddns->default_master();
die "Zonename must end in a dot" die "Zonename must end in a dot"
unless ($host =~ /^[a-zA-Z0-9\.\-\_]+\.$/); unless ($host =~ /^[a-zA-Z0-9\.\-\_]+\.$/);
@ -16,5 +17,4 @@ my $regex = $RE{net}{IPv4} . '|' . $RE{net}{IPv6};
die "Master must be an IPv4 or IPv6 address" die "Master must be an IPv4 or IPv6 address"
unless ($master =~ /^$regex$/); unless ($master =~ /^$regex$/);
my $ddns = Martnet::DDNS->new();
$ddns->add($host, $master, '_custom'); $ddns->add($host, $master, '_custom');

View File

@ -7,7 +7,9 @@ use Regexp::Common qw/net/;
my $host = shift || die "No zonename provided"; my $host = shift || die "No zonename provided";
my $master = shift; my $master = shift;
$master ||= '198.251.79.234'; my $ddns = Martnet::DDNS->new();
$master ||= $ddns->default_master();
die "Zonename must end in a dot" die "Zonename must end in a dot"
unless ($host =~ /^[a-zA-Z0-9\.\-\_]+\.$/); unless ($host =~ /^[a-zA-Z0-9\.\-\_]+\.$/);
@ -16,5 +18,4 @@ my $regex = $RE{net}{IPv4} . '|' . $RE{net}{IPv6};
die "Master must be an IPv4 or IPv6 address" die "Master must be an IPv4 or IPv6 address"
unless ($master =~ /^$regex$/); unless ($master =~ /^$regex$/);
my $ddns = Martnet::DDNS->new();
$ddns->add($host, $master, '_dnssec'); $ddns->add($host, $master, '_dnssec');

View File

@ -6,8 +6,8 @@ use Martnet::DDNS;
use Regexp::Common qw/net/; use Regexp::Common qw/net/;
my $host = shift || die "No vhost provided"; my $host = shift || die "No vhost provided";
#my $master = shift || die "No master DNS IP provided"; my $ddns = Martnet::DDNS->new();
my $master = '198.251.79.234'; my $master ||= $ddns->default_master();
die "Hostname must end in a dot" die "Hostname must end in a dot"
unless ($host =~ /^[a-zA-Z0-9\.\-\_]+\.$/); unless ($host =~ /^[a-zA-Z0-9\.\-\_]+\.$/);
@ -16,5 +16,4 @@ my $regex = $RE{net}{IPv4} . '|' . $RE{net}{IPv6};
die "Master must be an IPv4 or IPv6 address" die "Master must be an IPv4 or IPv6 address"
unless ($master =~ /^$regex$/); unless ($master =~ /^$regex$/);
my $ddns = Martnet::DDNS->new();
$ddns->add($host, $master, '_vhosts'); $ddns->add($host, $master, '_vhosts');

12
bin/list-dnssec Executable file
View File

@ -0,0 +1,12 @@
#!/usr/bin/perl
use strict;
use warnings;
use Martnet::DDNS;
my $ddns = Martnet::DDNS->new();
my @vh = $ddns->get('_dnssec');
foreach my $i (sort {$a->{zone} cmp $b->{zone}} @vh) {
print $i->{zone}, ". master: ", $i->{master},"\n";
}

View File

@ -20,7 +20,7 @@ my @vh = $ddns->get('_vhosts');
my $changecount = 0; my $changecount = 0;
foreach my $i (@vh) { foreach my $i (@vh) {
my $zf = "/var/lib/bind/vhost/db.$i->{zone}"; my $zf = "/var/lib/bind/vhost/db.$i->{zone}";
unless ( $force_replace || -f $zf ) { if ( $force_replace || (! -f $zf) ) {
print "Generating new zone file for $i->{zone}\n"; print "Generating new zone file for $i->{zone}\n";
open(my $fh, ">", $zf) open(my $fh, ">", $zf)
|| die "Can't create output file $zf: $!"; || die "Can't create output file $zf: $!";
@ -54,7 +54,7 @@ if ($changecount) {
print "Installing new vhost list\n"; print "Installing new vhost list\n";
system("install -o bind -g bind $path /var/lib/bind/vhost.zones.9"); system("install -o bind -g bind $path /var/lib/bind/vhost.zones.9");
print "Reloading DNS files\n"; print "Reloading DNS files\n";
system("rndc reload"); system("/usr/sbin/rndc reload");
} }
exit 0; exit 0;

View File

@ -4,6 +4,7 @@ use strict;
use warnings; use warnings;
use Martnet::DDNS; use Martnet::DDNS;
use File::Temp qw/tempfile/; use File::Temp qw/tempfile/;
use Data::Dumper;
my $force = shift; # a "force" flag, if the update is big my $force = shift; # a "force" flag, if the update is big
@ -14,12 +15,12 @@ my %vhh = map { $_->{zone} => 1 } @vh;
my @all = parse_slavefile("/etc/bind/martnet.slave.zones.9"); my @all = parse_slavefile("/etc/bind/martnet.slave.zones.9");
my %allh = all_zones_hash(@all); my %allh = all_zones_hash(@all);
use Data::Dumper;
# For each virtual host, see if we've got it already # For each virtual host, see if we've got it already
my $changecount = 0; my $changecount = 0;
foreach my $i (@vh) { foreach my $i (@vh) {
unless (contains_zone($i, @all)) { unless (contains_zone($i, @all)) {
print "don't have $i->{zone}\n";
$changecount++; $changecount++;
} }
} }
@ -45,7 +46,7 @@ sub parse_slavefile {
open(my $fh, $f) || die "Can't open $f: $!"; open(my $fh, $f) || die "Can't open $f: $!";
while (<$fh>) { while (<$fh>) {
if (/^zone\s+\"([^\"]+)\"\s+\{.+masters\s?\{\s?([^;]+);/) { if (/^zone\s+\"([^\"]+)\"\s+\{.+masters\s?\{\s?([^\}]+);\s?\};/) {
push ( @ret, { zone => $1, push ( @ret, { zone => $1,
master => $2 master => $2
} ); } );
@ -62,11 +63,13 @@ sub do_rewrite {
foreach my $i (sort {$a->{zone} cmp $b->{zone}} @vh) { foreach my $i (sort {$a->{zone} cmp $b->{zone}} @vh) {
next if ($i->{type} eq '_dnssec'); next if ($i->{type} eq '_dnssec');
print $fh "zone \"$i->{zone}\" { type slave; file \"/var/cache/bind/db.$i->{zone}\"; masters { $i->{master}; }; };\n"; print $fh "zone \"$i->{zone}\" { type slave; file \"/var/cache/bind/db.$i->{zone}\"; masters { $i->{master}; }; allow-notify {key \"notify-key\";}; };\n";
} }
close $fh; close $fh;
print "Installing new slave host list\n"; print "Installing new slave host list\n";
system("install -o bind -g bind $path /etc/bind/martnet.slave.zones.9"); my $old_uid = (lstat "/etc/bind/martnet.slave.zones.9")[4];
my $old_gid = (lstat "/etc/bind/martnet.slave.zones.9")[5];
system("install -o $old_uid -g $old_gid $path /etc/bind/martnet.slave.zones.9");
print "Reloading DNS files\n"; print "Reloading DNS files\n";
system("/usr/sbin/rndc reload"); system("/usr/sbin/rndc reload");
} }
@ -85,6 +88,10 @@ sub contains_zone {
my ($zone, @zl) = @_; my ($zone, @zl) = @_;
foreach my $i (@zl) { foreach my $i (@zl) {
if ($i->{zone} eq $zone->{zone}) {
print "m: '$i->{master}' ne '$zone->{master}'\n"
unless ($i->{master} eq $zone->{master});
}
return 1 return 1
if ($i->{zone} eq $zone->{zone} && if ($i->{zone} eq $zone->{zone} &&
$i->{master} eq $zone->{master} $i->{master} eq $zone->{master}

140
dnssec-verify.sh Executable file
View File

@ -0,0 +1,140 @@
#!/usr/bin/env bash
# dnssec-verify.sh — emit exact DS lines to ADD/REMOVE in the *right place*.
# If parent is a TLD (like com/org), suggest "registrar".
# If parent is another zone (delegated child), suggest "parent zone '<parent>'".
#
# Compares parent DS vs child-derived DS (SHA-256) robustly and prints only real diffs.
set -Eeuo pipefail
if [[ $# -ne 1 ]]; then
echo "Usage: $0 <domain>" >&2; exit 2
fi
domain="${1%.}"
need() { command -v "$1" >/dev/null 2>&1 || { echo "Missing $1" >&2; exit 3; }; }
for t in dig dnssec-dsfromkey awk sed tr sort uniq grep; do need "$t"; done
parent_of() {
local d="$1"
[[ "$d" == *.* ]] || { echo ""; return; }
echo "${d#*.}"
}
pick_ns_all() {
# Return up to 6 NS for a zone (one per line)
dig +short NS "$1" | awk 'NF' | head -n 6
}
normalize_parent_ds() {
# Normalize DS from full +answer or +short: "keytag alg 2 DIGEST"
awk -v dom="$domain." '
BEGIN { IGNORECASE=1 }
{
line = $0
gsub(/\(/,"",line); gsub(/\)/,"",line)
n = split(line, f, /[ \t]+/)
dsidx = 0
for (i=1; i<=n; i++) if (toupper(f[i])=="DS") { dsidx=i; break }
if (dsidx>0) {
kt=f[dsidx+1]; alg=f[dsidx+2]; dt=f[dsidx+3]
if (kt ~ /^[0-9]+$/ && alg ~ /^[0-9]+$/ && dt ~ /^[0-9]+$/) {
digest=""
for (j=dsidx+4; j<=n; j++) if (f[j] ~ /^[0-9A-Fa-f]+$/) digest=digest toupper(f[j])
if (dt=="2" && digest!="") printf "%s %s %s\n", kt, alg, digest
}
} else {
if (n>=4 && f[1] ~ /^[0-9]+$/ && f[2] ~ /^[0-9]+$/ && f[3] ~ /^[0-9]+$/) {
if (f[3]=="2") {
digest=""
for (k=4; k<=n; k++) if (f[k] ~ /^[0-9A-Fa-f]+$/) digest=digest toupper(f[k])
if (digest!="") printf "%s %s %s\n", f[1], f[2], digest
}
}
}
}'
}
normalize_child_ds() {
# Normalize dnssec-dsfromkey output: "keytag alg 2 DIGEST"
awk -v dom="$domain." '
BEGIN { IGNORECASE=1 }
toupper($3)=="DS" && $4 ~ /^[0-9]+$/ && $5 ~ /^[0-9]+$/ && $6 ~ /^[0-9]+$/ {
if ($6=="2") {
digest=""
for (i=7; i<=NF; i++) if ($i ~ /^[0-9A-Fa-f]+$/) digest=digest toupper($i)
if (digest!="") printf "%s %s %s\n", $4, $5, digest
}
}'
}
parent_zone="$(parent_of "$domain")"
[[ -z "$parent_zone" || "$parent_zone" == "$domain" ]] && { echo "Cannot determine parent for $domain" >&2; exit 4; }
# Where to publish DS?
# - If parent has no additional dot (e.g., "org", "com"): registrar.
# - Else: publish in the parent authoritative zone you control.
if [[ "$parent_zone" == *.* ]]; then
where="parent zone '$parent_zone'"
else
where="registrar (parent '$parent_zone')"
fi
# Parent NS set (authoritative for the parent zone)
mapfile -t parent_ns_list < <(pick_ns_all "$parent_zone")
((${#parent_ns_list[@]})) || { echo "No NS for parent zone $parent_zone" >&2; exit 5; }
# Child NS (authoritative for the child)
child_ns="$(dig +short NS "$domain" | awk 'NR==1{print}')"
[[ -z "${child_ns:-}" ]] && child_ns="${parent_ns_list[0]}"
# ----- Fetch and normalize parent DS (union across parent NS) -----
parent_ds_tmp="$(mktemp)"; trap 'rm -f "$parent_ds_tmp"' EXIT; : >"$parent_ds_tmp"
for ns in "${parent_ns_list[@]}"; do
dig @"$ns" +norec +dnssec +noall +answer "$domain" DS 2>/dev/null \
| normalize_parent_ds >> "$parent_ds_tmp" || true
done
mapfile -t parent_ds < <(sort -u "$parent_ds_tmp")
# ----- Fetch child DNSKEY and derive DS (SHA-256) -----
dnskey_ans="$(dig @"$child_ns" +tcp +dnssec +noall +answer "$domain" DNSKEY 2>/dev/null || true)"
if [[ -z "$dnskey_ans" ]]; then
echo "Could not fetch DNSKEY from $child_ns for $domain" >&2
exit 6
fi
mapfile -t child_ds < <(
printf '%s\n' "$dnskey_ans" \
| dnssec-dsfromkey -f - -a SHA-256 "$domain" 2>/dev/null \
| normalize_child_ds \
| sort -u
)
# Build sets
declare -A P C
for r in "${parent_ds[@]:-}"; do [[ -n "${r// }" ]] && P["$r"]=1; done
for r in "${child_ds[@]:-}"; do [[ -n "${r// }" ]] && C["$r"]=1; done
adds=() removes=()
# ADD = child-derived not present at parent
for r in "${child_ds[@]:-}"; do [[ -z "${P[$r]+x}" ]] && adds+=("$r"); done
# REMOVE = present at parent but not derivable from current child keys
for r in "${parent_ds[@]:-}"; do [[ -z "${C[$r]+x}" ]] && removes+=("$r"); done
emit_lines() {
local hdr="$1"; shift
local -a arr=( "$@" )
((${#arr[@]})) || return 0
echo "## $hdr in $where:"
for r in "${arr[@]}"; do
set -- $r
echo "$domain. IN DS $1 $2 2 $3"
done
}
if ((${#adds[@]}==0 && ${#removes[@]}==0)); then
echo "## Parent DS already matches child KSKs (no changes) — in $where."
else
emit_lines "ADD these DS" "${adds[@]}"
emit_lines "REMOVE these DS" "${removes[@]}"
fi