commit 26e212c59a608d949d8fbd33aa41b0357b452221 from: Izzy Blacklock date: Tue Aug 08 16:02:08 2023 UTC copied BotNow::DNS.pm to IRCNOW::Acct::DNS.pm Initial import - no changes to imported file commit - d4d1086311a6998ac681e2e76aaab3aa4c0e3470 commit + 26e212c59a608d949d8fbd33aa41b0357b452221 blob - /dev/null blob + d7a45c1ec048a41f46881a14e853247f9b6f81c0 (mode 644) --- /dev/null +++ lib/IRCNOW/Acct/DNS.pm @@ -0,0 +1,269 @@ +package BotNow::DNS; + +use strict; +use warnings; +use OpenBSD::Pledge; +use OpenBSD::Unveil; +use lib qw(./lib); +use IRCNOW::IO qw(readarray writefile appendfile); +use IRCNOW::IO::IRC; +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 = $&; +} +IRCNOW::IO::IRC::cbind("msg", "-", "setrdns", \&msetrdns); +IRCNOW::IO::IRC::cbind("msg", "-", "delrdns", \&mdelrdns); +IRCNOW::IO::IRC::cbind("msg", "-", "setdns", \&msetdns); +IRCNOW::IO::IRC::cbind("msg", "-", "deldns", \&mdeldns); +IRCNOW::IO::IRC::cbind("msg", "-", "host", \&mhost); +IRCNOW::IO::IRC::cbind("msg", "-", "nextdns", \&mnextdns); +IRCNOW::IO::IRC::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 (! (IRCNOW::IO::IRC::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)) { + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $nick :$hostname set to $ip"); + } else { + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $nick :ERROR: failed to set rDNS"); + } + } +} + +# !delrdns 2001:bd8:: +sub mdelrdns { + my ($bot, $nick, $host, $hand, $text) = @_; + if (! (IRCNOW::IO::IRC::isstaff($bot, $nick))) { return; } + if ($text =~ /^([0-9A-Fa-f:\.]{3,})$/) { + my ($ip) = ($1); + if (delrdns($ip, $ip6subnet)) { + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $nick :$ip rDNS deleted"); + } else { + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $nick :ERROR: failed to set rDNS"); + } + } +} +# !setdns username 1.2.3.4 +sub msetdns { + my ($bot, $nick, $host, $hand, $text) = @_; + if (! (IRCNOW::IO::IRC::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)) { + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $nick :$name.$hostname AAAA set to $value"); + } elsif (setdns($name, $hostname, "A", $value)) { + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $nick :$name.$hostname A set to $value"); + } else { + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $nick :ERROR: failed to set DNS"); + } + } +} + +# !deldns username +sub mdeldns { + my ($bot, $nick, $host, $hand, $text) = @_; + if (! (IRCNOW::IO::IRC::isstaff($bot, $nick))) { return; } + if ($text =~ /^([-0-9A-Za-z\.]+)$/) { + my ($name) = ($1); + if (setdns($name, $hostname)) { + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $nick :$text deleted"); + } else { + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $nick :ERROR: failed to delete DNS records"); + } + } +} + +# !host username +sub mhost { + my ($bot, $nick, $host, $hand, $text) = @_; + if (! (IRCNOW::IO::IRC::isstaff($bot, $nick))) { return; } + if ($text =~ /^([-0-9A-Za-z:\.]{3,})/) { + my ($hostname) = ($1); + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $nick :".host($hostname)); + } +} + +# !nextdns username +sub mnextdns { + my ($bot, $nick, $host, $hand, $text) = @_; + if (! (IRCNOW::IO::IRC::isstaff($bot, $nick))) { return; } + if ($text =~ /^([-0-9a-zA-Z]+)/) { + IRCNOW::IO::IRC::putserv($bot, "PRIVMSG $nick :$text set to ".nextdns($text)); + } +} + +# !readip6s +sub mreadip6s { + my ($bot, $nick, $host, $hand, $text) = @_; + if (! (IRCNOW::IO::IRC::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 = 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 = readarray($filename); + foreach my $line (@lines) { + # increment the zone's serial number + if ($line =~ /(\d{8})(\d{2})((\s+\d+){4}\s*\))/) { + my $date = IRCNOW::IO::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 + 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()); + } + 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