#!/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 $ipv4 = $conf{ipv4}; my $zonedir = $conf{zonedir}; my $ipv6path = $conf{ipv6path}; my $hostnameif = $conf{hostnameif}; # Validate ipv6s if it exists, otherwise load addresses from /etc/hostname.if my @ipv6s; if (!(-s "$ipv6path")) { print "No IPv6 addresses in $ipv6path, loading from $hostnameif...\n"; @ipv6s = readipv6s($hostnameif); } else { @ipv6s = readipv6s($ipv6path); } if (!@ipv6s) { die "No IPv6 addresses in $ipv6path or $hostnameif!"; } if (host($hostname) =~ /(\d+\.){3,}\d+/) { $ipv4 = $&; } 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); sub init { unveil("$ipv6path", "rwc") or die "Unable to unveil $!"; unveil("$zonedir", "rwc") or die "Unable to unveil $!"; #dependencies for doas unveil("/usr/bin/doas", "rx") or die "Unable to unveil $!"; #dependencies for host unveil("/usr/bin/host", "rx") or die "Unable to unveil $!"; } 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, $hostname)) { main::putserv($bot, "PRIVMSG $nick :$hostname set to $ip"); } else { main::putserv($bot, "PRIVMSG $nick :ERROR: failed to set rDNS"); } } } sub mdelrdns { my ($bot, $nick, $host, $hand, $text) = @_; if (! (main::isstaff($bot, $nick))) { return; } if ($text =~ /^([0-9A-Fa-f:\.]{3,})$/) { my $ip = $1; my $hostname = "notset"; if (setrdns($ip, $hostname)) { main::putserv($bot, "PRIVMSG $nick :$ip rDNS deleted"); } else { main::putserv($bot, "PRIVMSG $nick :ERROR: failed to set rDNS"); } } } 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 ($hostname, $ip) = ($1, $2); if (setdns($hostname, $ip)) { main::putserv($bot, "PRIVMSG $nick :$hostname set to $ip"); } else { main::putserv($bot, "PRIVMSG $nick :ERROR: failed to set DNS"); } } } sub mdeldns { my ($bot, $nick, $host, $hand, $text) = @_; if (! (main::isstaff($bot, $nick))) { return; } if ($text =~ /^([-0-9A-Za-z\.]+)/) { if (setdns($text)) { main::putserv($bot, "PRIVMSG $nick :$text deleted"); } else { main::putserv($bot, "PRIVMSG $nick :ERROR: failed to delete DNS records"); } } } sub mhost { my ($bot, $nick, $host, $hand, $text) = @_; if (! (main::isstaff($bot, $nick))) { return; } if ($text =~ /^([-0-9A-Za-z:\.]{3,})/) { my ($hostname, $version) = ($1, $2); main::putserv($bot, "PRIVMSG $nick :".host($hostname)); } } 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)); } } # Given filename, return a list of ipv6 addresses sub readipv6s { my ($filename) = @_; my @lines = main::readarray($filename); my @ipv6s; foreach my $line (@lines) { if ($line =~ /^\s*inet6 (alias )?([0-9a-f:]{4,}) [0-9]+\s*$/i) { push(@ipv6s, $2); } elsif ($line =~ /^\s*([0-9a-f:]{4,})\s*$/i) { push(@ipv6s, $1); } } return @ipv6s; } # TODO: fix rdns request with buyvm's api, the ips must not skip 0s # returns true upon success, false upon failure sub setrdns { my ($ip, $hostname) = @_; my $stdout = `curl -d \"key=$key&hash=$hash&action=rdns&ip=$ip&rdns=$hostname\" https://manage.buyvm.net/api/client/command.php`; if ($stdout !~ /success/) { return 0; } return 1; } # set $domain to $ip if provided; otherwise, delete $domain # returns true upon success, false upon failure sub setdns { my ($domain, $ip) = @_; my $filename = "$zonedir/$hostname"; my $subdomain; if ($domain =~ /^([a-zA-Z][-\.a-zA-Z0-9]+)\.$hostname$/) { $subdomain = $1; } else { return 0; } 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 ($ip =~ /^([0-9\.]+)$/) { # if IPv4 push(@lines, "$subdomain 3600 IN A $ip"); } elsif ($ip =~ /:/) { # if IPv6 push(@lines, "$subdomain 3600 IN AAAA $ip"); } elsif (!defined($ip)) { # delete records @lines = grep !/\b$subdomain\s*3600\s*IN/, @lines; } # 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); } # create A and AAAA records for subdomain, set the rDNS, # and return the new ipv6 address sub nextdns { my ($subdomain) = @_; my $ipv6 = shift(@ipv6s); my $fqdn = "$subdomain.$hostname"; main::writefile($ipv6path, join("\n", @ipv6s)); if (setdns($fqdn, $ipv4) && setdns($fqdn, $ipv6) && setrdns($ipv6, $fqdn)) { return "$ipv6"; } return "false"; } 1; # MUST BE LAST STATEMENT IN FILE