#!/usr/bin/perl package DNS; use strict; use warnings; use OpenBSD::Pledge; use OpenBSD::Unveil; use Data::Dumper; use File::Copy qw(copy); my %conf = %main::conf; my $chans = $conf{chans}; my $staff = $conf{staff}; my $key = $conf{key}; my $hash = $conf{hash}; my $hostname = $conf{hostname}; my $verbose = $conf{verbose}; my $ip4 = $conf{ip4}; my $ip6 = $conf{ip6}; my $ip6subnet = $conf{ip6subnet}; my $zonedir = $conf{zonedir}; my $hostnameif = $conf{hostnameif}; if (host($hostname) =~ /(\d+\.){3,}\d+/) { $ip4 = $&; } main::cbind("msg", "-", "setrdns", \&msetrdns); main::cbind("msg", "-", "delrdns", \&mdelrdns); main::cbind("msg", "-", "setdns", \&msetdns); main::cbind("msg", "-", "deldns", \&mdeldns); main::cbind("msg", "-", "host", \&mhost); main::cbind("msg", "-", "nextdns", \&mnextdns); main::cbind("msg", "-", "readip6s", \&mreadip6s); sub init { unveil("$zonedir", "rwc") or die "Unable to unveil $!"; unveil("/usr/bin/doas", "rx") or die "Unable to unveil $!"; unveil("/usr/bin/host", "rx") or die "Unable to unveil $!"; unveil("$hostnameif", "rwc") or die "Unable to unveil $!"; } # !setrdns 2001:bd8:: username.example.com sub msetrdns { my ($bot, $nick, $host, $hand, $text) = @_; if (! (main::isstaff($bot, $nick))) { return; } if ($text =~ /^([0-9A-Fa-f:\.]{3,})\s+([-0-9A-Za-z\.]+)$/) { my ($ip, $hostname) = ($1, $2); if (setrdns($ip, $ip6subnet, $hostname)) { main::putserv($bot, "PRIVMSG $nick :$hostname set to $ip"); } else { main::putserv($bot, "PRIVMSG $nick :ERROR: failed to set rDNS"); } } } # !delrdns 2001:bd8:: sub mdelrdns { my ($bot, $nick, $host, $hand, $text) = @_; if (! (main::isstaff($bot, $nick))) { return; } if ($text =~ /^([0-9A-Fa-f:\.]{3,})$/) { my ($ip) = ($1); if (delrdns($ip, $ip6subnet)) { main::putserv($bot, "PRIVMSG $nick :$ip rDNS deleted"); } else { main::putserv($bot, "PRIVMSG $nick :ERROR: failed to set rDNS"); } } } # !setdns username 1.2.3.4 sub msetdns { my ($bot, $nick, $host, $hand, $text) = @_; if (! (main::isstaff($bot, $nick))) { return; } if ($text =~ /^([-0-9A-Za-z\.]+)\s+([0-9A-Fa-f:\.]+)/) { my ($name, $value) = ($1, $2); if ($value =~ /:/ and setdns($name, $hostname, "AAAA", $value)) { main::putserv($bot, "PRIVMSG $nick :$name.$hostname AAAA set to $value"); } elsif (setdns($name, $hostname, "A", $value)) { main::putserv($bot, "PRIVMSG $nick :$name.$hostname A set to $value"); } else { main::putserv($bot, "PRIVMSG $nick :ERROR: failed to set DNS"); } } } # !deldns username sub mdeldns { my ($bot, $nick, $host, $hand, $text) = @_; if (! (main::isstaff($bot, $nick))) { return; } if ($text =~ /^([-0-9A-Za-z\.]+)$/) { my ($name) = ($1); if (setdns($name, $hostname)) { main::putserv($bot, "PRIVMSG $nick :$text deleted"); } else { main::putserv($bot, "PRIVMSG $nick :ERROR: failed to delete DNS records"); } } } # !host username sub mhost { my ($bot, $nick, $host, $hand, $text) = @_; if (! (main::isstaff($bot, $nick))) { return; } if ($text =~ /^([-0-9A-Za-z:\.]{3,})/) { my ($hostname) = ($1); main::putserv($bot, "PRIVMSG $nick :".host($hostname)); } } # !nextdns username sub mnextdns { my ($bot, $nick, $host, $hand, $text) = @_; if (! (main::isstaff($bot, $nick))) { return; } if ($text =~ /^([-0-9a-zA-Z]+)/) { main::putserv($bot, "PRIVMSG $nick :$text set to ".nextdns($text)); } } # !readip6s sub mreadip6s { my ($bot, $nick, $host, $hand, $text) = @_; if (! (main::isstaff($bot, $nick))) { return; } foreach my $line (readip6s($hostnameif)) { print "$line\n" } } # Return list of ipv6 addresses from filename sub readip6s { my ($filename) = @_; my @lines = main::readarray($filename); my @ipv6s; foreach my $line (@lines) { if ($line =~ /^\s*inet6\s+(alias\s+)?([0-9a-f:]{4,})\s+[0-9]+\s*$/i) { push(@ipv6s, $2); } elsif ($line =~ /^\s*([0-9a-f:]{4,})\s*$/i) { push(@ipv6s, $1); } } return @ipv6s; } # set rdns of $ip6 to $hostname given $subnet # return true on success; false on failure sub setrdns { my ($ip6, $subnet, $hostname) = @_; my $digits = ip6full($ip6); $digits =~ tr/://d; my $reversed = reverse($digits); my $origin = substr($reversed, 32-$subnet/4); $origin = join('.', split(//, $origin)).".ip6.arpa"; my $name = substr($reversed, 0, 32-$subnet/4); $name = join('.', split(//, $name)); # delete old PTR records, then set new one return setdns($name, $origin) && setdns($name, $origin, "PTR", $hostname); } # delete rdns of $ip6 given $subnet # return true on success; false on failure sub delrdns { my ($ip6, $subnet) = @_; return setrdns($ip6, $subnet); } # given $origin. create $name RR of $type and set to $value if provided; # if $value is missing, delete $domain # returns true upon success, false upon failure sub setdns { my ($name, $origin, $type, $value) = @_; my $filename = "$zonedir/$origin"; my @lines = main::readarray($filename); foreach my $line (@lines) { # increment the zone's serial number if ($line =~ /(\d{8})(\d{2})((\s+\d+){4}\s*\))/) { my $date = main::date(); my $serial = 0; if ($date <= $1) { $serial = $2+1; } $line = $`.$date.sprintf("%02d",$serial).$3.$'; } } if (!defined($value)) { # delete records @lines = grep !/\b$name\s*3600\s*IN/, @lines; } else { push(@lines, "$name 3600 IN $type $value"); } # trailing newline necessary main::writefile("$filename.bak", join("\n", @lines)."\n"); copy "$filename.bak", $filename; if (system("doas -u _nsd nsd-control reload")) { return 0; } else { return 1; } } # given hostname, return IP addresses; or given IP address, return hostname sub host { my ($name) = @_; my @matches; my @lines = split /\n/m, `host $name`; if ($name =~ /^[0-9\.]+$/ or $name =~ /:/) { # IP address foreach my $line (@lines) { if ($line =~ /([\d\.]+).(in-addr|ip6).arpa domain name pointer (.*)/) { push(@matches, $3); } } } else { # hostname foreach my $line (@lines) { if ($line =~ /$name has (IPv6 )?address ([0-9a-fA-F\.:]+)/) { push(@matches, $2); } } } return join(' ', @matches); } # Return an ipv6 address with all zeroes filled in sub ip6full { my ($ip6) = @_; my $left = substr($ip6, 0, index($ip6, "::")); my $leftcolons = ($left =~ tr/://); $ip6 =~ s{::}{:}; my @quartets = split(':', $ip6); my $length = scalar(@quartets); for (my $n = 1; $n <= 8 - $length; $n++) { splice(@quartets, $leftcolons+1, 0, "0000"); } my @newquartets = map(sprintf('%04s', $_), @quartets); my $full = join(':',@newquartets); return $full; } # Returns the network part of the first IPv6 address (indicated by subnet) # with the host part of the second IPv6 address sub ip6mask { my ($ip6net, $subnet, $ip6host) = @_; my $netdigits = ip6full($ip6net); $netdigits =~ tr/://d; my $hostdigits = ip6full($ip6host); $hostdigits =~ tr/://d; my $digits = substr($netdigits,0,($subnet/4)).substr($hostdigits,($subnet/4)); my $ip6; for (my $n = 0; $n < 32; $n++) { if ($n > 0 && $n % 4 == 0) { $ip6 .= ":"; } $ip6 .= substr($digits,$n,1); } return $ip6; } sub randip6 { return join ':', map { sprintf '%04x', rand 0x10000 } (1 .. 8); } # create A and AAAA records for subdomain, set the rDNS, # and return the new ipv6 address sub nextdns { my ($subdomain) = @_; my $newip6 = $ip6; my @allip6s = readip6s($hostnameif); while (grep(/$newip6/, @allip6s)) { $newip6 = ip6mask($ip6, $ip6subnet,randip6()); } main::appendfile($hostnameif, "inet6 alias $newip6 48\n"); `doas ifconfig vio0 inet6 $newip6/48`; if (setdns($subdomain, $hostname, "A", $ip4) && setdns($subdomain, $hostname, "AAAA", $newip6) && setrdns($newip6, $ip6subnet, "$subdomain.$hostname")) { return "$newip6"; } return "false"; } 1; # MUST BE LAST STATEMENT IN FILE