Commit Diff


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