File: [local] / botnow / DNS.pm (download)
Revision 1.2, Tue Jul 20 17:40:08 2021 UTC (2 years, 9 months ago) by bountyht
Branch: MAIN
CVS Tags: HEAD Changes since 1.1: +113 -57 lines
Fixes from jrmu regarding DNS handling. DNS now relies on NSD. IPV6 addresses are added dynamically. A number of non-specified bugs was fixed.
|
#!/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