Compare commits
11 Commits
7b50aa74ab
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 03ec948359 | |||
| 244df117fc | |||
| 3b5a21d819 | |||
| 23b5a19166 | |||
| 6ca3346a03 | |||
| 302c9ba09e | |||
| aeeda96aca | |||
| 04076c0c35 | |||
| e1068644f3 | |||
| 0758403058 | |||
| c457fd1ed8 |
42
DDNS.pm
42
DDNS.pm
@ -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;
|
||||||
@ -202,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;
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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;2607:f1c0:86e:b66f:6b86:babb:c367:b0dc';
|
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');
|
||||||
|
|||||||
@ -7,8 +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 ||= '198.251.79.234;2607:f1c0:86e:b66f:6b86:babb:c367:b0dc';
|
$master ||= $ddns->default_master();
|
||||||
|
|
||||||
|
|
||||||
die "Zonename must end in a dot"
|
die "Zonename must end in a dot"
|
||||||
@ -18,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');
|
||||||
|
|||||||
@ -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;2607:f1c0:86e:b66f:6b86:babb:c367:b0dc';
|
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');
|
||||||
|
|||||||
@ -46,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?(.+);\s?\};\s?\};/) {
|
if (/^zone\s+\"([^\"]+)\"\s+\{.+masters\s?\{\s?([^\}]+);\s?\};/) {
|
||||||
push ( @ret, { zone => $1,
|
push ( @ret, { zone => $1,
|
||||||
master => $2
|
master => $2
|
||||||
} );
|
} );
|
||||||
@ -63,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");
|
||||||
}
|
}
|
||||||
|
|||||||
140
dnssec-verify.sh
Executable file
140
dnssec-verify.sh
Executable 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
|
||||||
Reference in New Issue
Block a user