Dynamic DNS using OpenBSD slowcgi

Dynamic DNS server using NSD DNS server on OpenBSD with httpd and slowcgi

Setup NSD

Create new zone file

vi /var/nsd/etc/nsd.conf

zone:
    name: "dynamic.example.com"
    # under /var/nsd/zones/
    zonefile: "master/dynamic.example.com.zone"

Create zone file(example)
/var/nsd/zones/master/dynamic.example.com.zone

$ORIGIN example.com.  ; default zone domain
$TTL 86400            ; default time to live
@ IN SOA dynamic.example.com. admin.example.com. (
           0;serial
           28800       ; Refresh
           7200        ; Retry
           864000      ; Expire
           86400       ; Min TTL
           )

           NS          ns1.example.com.
;

Enable remote control

Deactivate control-interface on nsd.conf
/var/run/nsd.sock is not see in www chroot
vi /var/nsd/etc/nsd.conf

remote-control:
       control-enable: yes
#       control-interface: /var/run/nsd.sock

Setup remote-control

nsd-control-setup

Enable NSD

rcctl enable nsd
rcctl start nsd

Set up Slowcgi

Copy perl to www chroot

Detect library for perl and nsd-control

nsd# ldd /usr/bin/perl
/usr/bin/perl:
        Start            End              Type  Open Ref GrpRef Name
        00000fbcf6721000 00000fbcf6726000 exe   1    0   0      /usr/bin/perl
        00000fbfe3210000 00000fbfe359a000 rlib  0    1   0      /usr/lib/libperl.so.22.0
        00000fbfb3b94000 00000fbfb3bc5000 rlib  0    2   0      /usr/lib/libm.so.10.1
        00000fbf63961000 00000fbf63a56000 rlib  0    2   0      /usr/lib/libc.so.96.2
        00000fbf0115e000 00000fbf0115e000 ld.so 0    1   0      /usr/libexec/ld.so
nsd# ldd /usr/sbin/nsd-control
/usr/sbin/nsd-control:
        Start            End              Type  Open Ref GrpRef Name
        00000bcd4e8c6000 00000bcd4e95d000 exe   1    0   0      /usr/sbin/nsd-control
        00000bd01d97f000 00000bd01d9f0000 rlib  0    1   0      /usr/lib/libssl.so.53.0
        00000bcf73a7f000 00000bcf73cd4000 rlib  0    2   0      /usr/lib/libcrypto.so.50.0
        00000bcfa0d7d000 00000bcfa0d8e000 rlib  0    1   0      /usr/lib/libevent.so.4.1
        00000bcfd4c9a000 00000bcfd4d8f000 rlib  0    1   0      /usr/lib/libc.so.96.2
        00000bd024370000 00000bd024370000 ld.so 0    1   0      /usr/libexec/ld.so
nsd#

Copy binary and library to www chroot

mkdir -p /var/www/usr/lib/
mkdir -p /var/www/usr/libexec/
cp /usr/bin/perl /var/www/bin/perl
cp /usr/lib/libperl.so.22.0 /var/www/usr/lib/
cp /usr/lib/libc.so.96.2 /var/www/usr/lib/
cp /usr/libexec/ld.so /var/www/usr/libexec/

mkdir -p /var/www/usr/libdata/perl5
cp /usr/libdata/perl5/utf8.pm /var/www/usr/libdata/perl5/
cp /usr/libdata/perl5/strict.pm /var/www/usr/libdata/perl5/

cp /usr/sbin/nsd-control /var/www/bin/nsd-control
cp /usr/lib/libssl.so.53.0 /var/www/usr/lib/
cp /usr/lib/libcrypto.so.50.0  /var/www/usr/lib/
cp /usr/lib/libevent.so.4.1  /var/www/usr/lib/
cp /usr/lib/libc.so.96.2  /var/www/usr/lib/
cp /usr/libexec/ld.so /var/www/usr/libexec/

Enable slowcgi

rcctl enable slowcgi
rcctl start slowcgi

CGI script to edit zone file

NSD not support dynamic update zone file yet Need CGI to edit zone file and reload

mkdir /var/www/zones
ln /var/nsd/zones/master/dynamic.example.com.zone /var/www/zones/dynamic.example.com.zone
chown www:www /var/www/zones/dynamic.example.com.zone

Copy nsd control key to www chroot

Need copy control key to use nsd-control

mkdir -p /var/www/var/nsd/etc/
cp /var/nsd/etc/nsd.conf /var/www/var/nsd/etc/
chown www:www /var/www/var/nsd/etc/nsd.conf
cp /var/nsd/etc/nsd_control.pem /var/www/var/nsd/etc/
chown www:www /var/www/var/nsd/etc/nsd_control.pem
cp /var/nsd/etc/nsd_control.key /var/www/var/nsd/etc/
chown www:www /var/www/var/nsd/etc/nsd_control.key
cp /var/nsd/etc/nsd_server.pem  /var/www/var/nsd/etc/
chown www:www /var/www/var/nsd/etc/nsd_server.pem

CGI script

touch /var/www/cgi-bin/dns-dyn chmod +x /var/www/cgi-bin/dns-dyn vi /var/www/cgi-bin/dns-dyn

#!/bin/perl

use utf8;
use strict;

my $file   = '/zones/dynamic.example.com.zone';
my $serial = 'serial';

sub update {
	my $rec = shift;
	my $ip  = shift;
	my $ip6 = shift;

	open( my $fh, "<$file" ) or die "File not found";
	my @lines = <$fh>;
	close($fh);
	my $found  = 0;
	my $change = 0;
	foreach (@lines) {
		if (/\d+;$serial/) {
			my $t = time;
			$_ =~ s/\d+;\Q$serial\E/$t;$serial/;
		}
		elsif ( $ip && /^\Q$rec\E.*(\d+\.\d+\.\d+\.\d+)/ ) {
			$found = 1;
			next if ( $ip eq $1 );
			$_ =~ s/\Q$1\E/$ip/;
			$change = 1;
		}
		elsif ( $ip6 && /^\Q$rec\E.*([0-9a-fA-F]+\:[\:0-9a-fA-F]+)/ ) {
			$found = 1;
			next if ( $ip6 eq $1 );
			$_ =~ s/\Q$1\E/$ip6/;
			$change = 1;
		}
	}
	unless ($found) {
		push( @lines, sprintf( '%-35s A     %s', $rec, $ip ), "\n" )
		  if ($ip);
		push( @lines, sprintf( '%-35s AAAA  %s', $rec, $ip6 ), "\n" )
		  if ($ip6);
		$change = 1;
	}

	if ($change) {
		open( my $fh, ">$file" ) or die "File not found";
		print $fh @lines;
		close($fh);
	}
	return $change;
}

my $query_string = $ENV{'QUERY_STRING'};
my %query        = map { split '=' } split( '&', $query_string );

my $ipv4 = $query{'ipv4'};
my $ipv6 = $query{'ipv6'};
my $ip   = $query{'ip'}   || $ENV{'REMOTE_ADDR'};
my $host = $query{'host'} || $ENV{'REMOTE_USER'};

if ( $ENV{'REMOTE_USER'} && ( $host ne $ENV{'REMOTE_USER'} ) ) {
	print "Status: 403\n";
	print "Content-type: text/plain\n\n";
	print "Not authorization. Username not match record.\n";
	exit;
}
if ( $host !~ /^([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/ ) {
	print "Status: 400\n";
	print "Content-type: text/plain\n\n";
	print "Hostname invalid: $host\n";
	exit;
}

if ( !$ipv4 && !$ipv6 ) {
	if    ( $ip =~ /(\d+\.\d+\.\d+\.\d+)/ ) { $ipv4 = $1; }
	elsif ( $ip =~ /([\:0-9a-fA-F]+)/ )     { $ipv6 = $1; }
}

if ( $ipv4 && $ipv4 !~ /(\d+\.\d+\.\d+\.\d+)/ ) {
	print "Status: 400\n";
	print "Content-type: text/plain\n\n";
	print "IPv4 invalid: $ipv4\n";
	exit;
}

if ( $ipv6 && $ipv6 !~ /([0-9a-fA-F]+\:[\:0-9a-fA-F]+)/ ) {
	print "Status: 400\n";
	print "Content-type: text/plain\n\n";
	print "IPv6 invalid: $ipv6\n";
	exit;
}

print "Content-type: text/plain\n\n";

print "Record $host: $ip\n";
print "nsd-control: ", qx(/bin/nsd-control reload 2>&1)
  if ( update( $host, $ipv4, $ipv6 ) );

Set up httpd

httpd config

vi /etc/httpd.conf

server "default" {
  listen on * port 80
  location "/.well-known/acme-challenge/*" {
    root "/acme"
    request strip 2
  }
  #block return 301 "https://$HTTP_HOST$REQUEST_URI"
}
server "dns.example.com" {
  listen on * port 80
  root "/htdocs/"

  location "/dyn/update" {
    fastcgi
    root "/cgi-bin/dns-dyn"
  }
}

Optional password protect

Make .htpasswd file, repeat command to add multiple user

htpasswd /var/www/conf/.htpasswd USERNAME
nsd# htpasswd /var/www/conf/.htpasswd test1
Password:
Retype Password:
nsd# htpasswd /var/www/conf/.htpasswd test2
Password:
Retype Password:
nsd#

Add authenticate to httpd.conf
vi /etc/httpd.conf

  location "/dyn/update" {
    authenticate with "/conf/.htpasswd"
    fastcgi
    root "/cgi-bin/dns-dyn"
  }

Enable httpd

rcctl enable httpd
rcctl start httpd

Access url to update IP

Example url

http://dns.example.com/dyn/update?host=test
http://dns.example.com/dyn/update?host=test&ip=1.2.3.4

With password protect

http://test1:test@dns.example.com/dyn/update
http://test1:test@dns.example.com/dyn/update&ip=1.2.3.4