spf-dkim-dmarc-demo/dns/web-api/lib/Email/SpoofingDemo/DNS/ZoneFile.pm

240 lines
5.0 KiB
Perl
Raw Normal View History

2023-10-25 15:50:22 +02:00
package Email::SpoofingDemo::DNS::ZoneFile;
use strict;
use warnings;
use utf8;
use v5.10;
use Exporter qw(import);
use File::Slurp;
use Try::Tiny;
our @EXPORT_OK = qw(get_txt get_zone_file import_zone_file remove_txt set_txt
zone_file_path);
sub safe_system {
my @argv = @_;
my $retval = system @argv;
die $! if ($retval == -1);
if ($retval != 0) {
my $status = $retval >> 8;
die join(" ", @argv) . " exited with status $status\n";
};
return 1;
}
sub is_dynamic_zone {
my ($zone, $dynamic_zones) = @_;
return grep { $_ eq $zone } @$dynamic_zones;
}
sub rndc_freeze {
my ($zone) = @_;
safe_system('rndc', 'freeze', $zone);
}
sub rndc_thaw {
my ($zone) = @_;
safe_system('rndc', 'thaw', $zone);
}
sub rndc_reload {
my ($zone) = @_;
safe_system('rndc', 'reload', $zone);
}
sub encode_txt {
my ($text) = @_;
my $chunk_length = 255;
my $text_bytes = $text;
utf8::encode($text_bytes);
my @parts;
for (my $i = 0; $i < length($text_bytes); $i += $chunk_length) {
my $part = substr($text_bytes, $i, 255);
$part =~ s/[\x00-\x1f"\x7f-\xff]/sprintf('\\%03d', ord($0))/eg;
push @parts, qq{"$part"};
}
return join(" ", @parts);
}
sub relative_to_absolute {
my ($zone, $relative_owner) = @_;
if ($relative_owner eq '') {
return $zone;
}
else {
return "$relative_owner.$zone";
}
}
sub nsupdate {
my ($zone, @instructions) = @_;
open(my $pipe, '|-', 'nsupdate') or die "Cant fork: $!";
say $pipe qq{zone $zone};
say $pipe $_ foreach (@instructions);
close($pipe);
return (($? >> 8) == 0);
}
sub get_txt {
my ($zone, $relative_owner, $filter, $sublabels_ok) = @_;
$filter //= qr{.*};
$sublabels_ok //= 0;
my @dig_opts = qw(+nottlid +noclass +norecurse +noall +answer @127.0.0.1);
my $owner_suffix = relative_to_absolute($zone, $relative_owner);
my $qname = ($sublabels_ok ? $zone : $owner_suffix);
my $qtype = ($sublabels_ok ? 'AXFR' : 'TXT');
my $owner_match_re = ($sublabels_ok ? qr/^([^.]+\.)*$owner_suffix\.$/i : qr/^$qname\.$/i);
my @result;
open(my $pipe, '-|', 'dig', @dig_opts, $qtype, $qname) or die "Cant fork: $!";
while (<$pipe>) {
chomp;
next if /^;/;
next if /^\s*$/;
my ($owner, $type, $data) = split(" ", $_, 3);
unless ($type eq 'TXT') {
next;
}
unless ($owner =~ $owner_match_re) {
next;
}
my $concat_data = ($data =~ s/"((?:[^\\"]|\\"|\\\d{3})*)"\s*/$1/gr);
unless ($concat_data =~ $filter) {
next;
}
push @result, { owner => ($owner =~ s/\.$//r), txt => $concat_data };
}
close($pipe);
if (wantarray) {
return @result;
}
else {
return $result[0];
}
}
sub set_txt {
my ($zone, $relative_owner, $txt) = @_;
my $owner = relative_to_absolute($zone, $relative_owner);
my $txt_encoded = encode_txt($txt);
nsupdate(
$zone,
"update delete $owner 0 IN TXT",
"update add $owner IN TXT $txt_encoded",
"send");
}
sub remove_txt {
my ($zone, $relative_owner) = @_;
my $owner = relative_to_absolute($zone, $relative_owner);
nsupdate(
$zone,
"update delete $owner 0 IN TXT",
"send");
}
sub check_zone {
my ($zone_contents, $origin) = @_;
open(my $pipe, '|-', 'named-checkzone', $origin, '-') or die "Cant fork: $!";
print $pipe $zone_contents;
close($pipe);
return (($? >> 8) == 0);
}
sub zone_file_path {
my ($zone, $base_dir, $force) = @_;
my @components = reverse(split(/\./, $zone));
my $path = $base_dir . '/' . (join('/', @components)) . ".zone";
if (not -f $path and not $force) {
return undef;
}
return $path;
}
sub get_zone_file {
my ($zone, $base_dir, $dynamic_zones) = @_;
my $path = zone_file_path($zone, $base_dir);
return undef if not defined $path;
rndc_freeze($zone) if is_dynamic_zone($zone, $dynamic_zones);
my $contents;
try {
$contents = read_file($path);
}
finally {
rndc_thaw($zone) if is_dynamic_zone($zone, $dynamic_zones);
};
return $contents;
}
sub import_zone_file {
my ($zone, $base_dir, $contents, $dynamic_zones) = @_;
if ($contents =~ /^\s*$/) {
warn "Zone to import is empty";
return undef;
}
my $path = zone_file_path($zone, $base_dir);
if (not defined $path) {
warn "No zone file for $zone in $base_dir";
return undef;
}
if (not check_zone($contents, $zone)) {
warn "Zone file contents do not pass syntax check";
return undef;
}
rndc_freeze($zone) if is_dynamic_zone($zone, $dynamic_zones);
try {
write_file($path, $contents);
rndc_reload($zone) unless is_dynamic_zone($zone, $dynamic_zones);
}
catch {
die $_;
}
finally {
rndc_thaw($zone) if is_dynamic_zone($zone, $dynamic_zones);
};
return 1;
}
1;