Ubuntu LoCo DNS nsset tool needs some Perl love

Page content

Recently I had to use the nsset commandline tool, which can be used to modify the DNS records of your Ubuntu LoCo (Local Community). The nsset tool can be found on the LoCo Teams DNS Admin wiki page in the attachments section. Just want to say that I really appreciate this tool and want to thank all contributors!

I’ve discovered a bug (deprecation actually), which I can not fix. To be honest, I am a SysAdmin and not a programmer. If you can code Perl or are Perl Monger, please feel free to have a look and help the Ubuntu LoCo’s out.

the bug / deprecation

The nsset tool relies on Net::DNS, which can be installed by installing the libnet-dns-perl package.

Checking the projects blog I found the release notes of version 1.36 where they deprecate the 2-argument TSIG->create() method. That method is being used in this script. So versions < 1.36 will still work with the current version of the script, while >= 1.36 will not work.

Personally I worked around the issue by using an older distribution with an older version of Net::DNS. These are the versions available in Ubuntu and Debian:

Debian version libnet-dns-perl version
buster 1.19
bullseye 1.29
bookworm 1.36
trixie 1.44
Ubuntu version libnet-dns-perl version
focal 1.22
jammy 1.33
lunar 1.36
mantic 1.39

nsset tool

So, just in case, I’ve added the code of the current version of the nsset tool here, as a copy.

#!/usr/bin/perl

use warnings;

# Source: kunde-1.2006.11.27

BEGIN { unshift(@INC,($ENV{'POPHOME'}||'/usr/pop').'/lib')
			unless $ENV{'KUNDE_NO_PERLPATH'};
      }

use strict;

my $NSI = "eshu.ubuntu-eu.org";

## not required on eshu
my $NSI_KEY; # = "xkz7ouvfVjSZkrzbvxsE0oETvGWOTI6947zpQbL0";
my $NSI_KEY_NAME; # = "foo.bar."; # update me! and note the trailing dot

sub Usage() {
    die <<END;
Usage: $0
      -a/-d/-u/-du what to do: add / delete / update / selective-delete
      -z ZONE      access this zone (default: auto)
      -t TTL       Default 86400, one day
      -c CLASS     default IN
      -r           update a reverse zone (CNAME or PTR only)
      -x WHAT,...  used with -d/-u: which entries to delete
                   (Default: -d: ANY (= all); -u: those mentioned on stdin)
      -s SERVER    if not $NSI
      -k key       Secret key
      -K keyname   Secret key's name
      -v           call 'dig' afterwards
      -D           Debug-Mode (Net::DNS)
      -U           Use UDP instead of TCP (Net::DNS::Resolver::usevc)
      NAME         the domain you want to update
Input for -a/-u:
A     Address              TXT   Comment etc.
MX    Pref Dest            PTR   Dest
CNAME destination

Normal use:
$0 -u bla.fasel.example
A 10.20.30.40
^D

"-u" and "-d" alone will try to delete all existing records of the given type.
(This does not work for NS records => the update fails.)
You can use "-du" to selectively delete the records you pass on stdin.
END
  exit 1;
}

use Net::DNS::Update;
use Net::DNS qw(rr_add rr_del);

use Getopt::Std;
Usage unless @ARGV;
use vars qw($opt_a $opt_d $opt_u $opt_z $opt_t $opt_c $opt_r $opt_x $opt_s $opt_r $opt_v $opt_D $opt_k $opt_K $opt_U);
getopts('aduz:t:c:rx:s:vDk:K:U') or Usage;

$opt_c ||= "IN";
$opt_t ||= 86400;
$NSI_KEY = $opt_k if defined $opt_k;
$NSI_KEY_NAME = $opt_K if defined $opt_K;

$opt_a=0 unless defined $opt_a;
$opt_d=0 unless defined $opt_d;
$opt_u=0 unless defined $opt_u;
Usage if $opt_a+($opt_u|$opt_d) != 1;
Usage if @ARGV != 1;

my ($was) = @ARGV;

if ($opt_r) {
	require Net::IP;
	my $ip = Net::IP->new($was)
	  or die "With -r you need to use an IP address.\n";
	$was = $ip->reverse_ip;
}

my $res = new Net::DNS::Resolver;
$res->{debug} = $opt_D;
$res->usevc(1) unless $opt_U;

$res->nameservers( my $nameserver = defined $opt_s ? $opt_s : $NSI );

if(defined $opt_z) {
	my $loc = substr($was,0,-length($opt_z));
	my $rmt = substr($was,length($loc));
} else {
	die "Domain names have dots!\n" unless $was =~ /\.(.*)/;
	$opt_z = $was;
	while(1) {
		my $query = $res->query($opt_z, "SOA");
		last if $query and $query->{'answer'}[0]{'type'} eq "SOA";
		die "No SOA!\n" unless $opt_z =~ s/^[^\.]+\.//;
	}
}

my $packet = new Net::DNS::Update($opt_z);

my $in = "";
# Step 0: Read
if($opt_a or $opt_u) {
	my $lines = 0;
	while(<STDIN>) {
		$lines++;
		$in .= $_;
	}
	die "Empty input!\n" unless $lines;
	my %ty;
	unless(defined $opt_x) {
		foreach my $lin(split(/\n/,$in)) {
			my($ty,$ex) = split(/\s+/,$lin,2);
			$ty{$ty}++;
		}
		$opt_x = join(",",keys %ty);
	}

}
# Step 1: Delete
if($opt_u+$opt_d == 1){
	my $type;
	foreach $type(split(/,/, ($opt_x or "ANY"))) {
		$packet->push("update", new Net::DNS::RR(
        	Name => $was, Class => "ANY", Type  => $type, ttl => 0));
	}
}
# Step 2: Update
if($opt_a or $opt_u) {
	foreach my $lin(split(/\n/,$in)) {
		my ( $ty, $ex ) = split( /\s+/, $lin, 2 );

		warn "Unknown record type '$ty'!\n"
		  if $ty !~ /^(?:A|AAAA|CNAME|MX|NS|PTR|TXT)$/i;

		$ex = qq("$ex") if uc $ty eq 'TXT' && $ex !~ /"/;

		my $rr;
		if($opt_d) {
			$rr = rr_del("$was $opt_t $opt_c \U$ty\E $ex");
		} else { 
			$rr = rr_add("$was $opt_t $opt_c \U$ty\E $ex");
		}

		$packet->push(update => $rr);
	}
}

$packet->sign_tsig($NSI_KEY_NAME, $NSI_KEY) if $NSI_KEY_NAME && $NSI_KEY;
my $ans = $res->send($packet);

if (defined $ans) {
	print my $rcode = $ans->header->rcode, "\n";
	system dig => '@'.$nameserver, $was, 'any' if $opt_v;
} else {
	print $res->errorstring, "\n";
}

99 Bottles of beer

My Perl knowledge is limited to showing off a certain obfuscated Perl script from the 99 Bottles of Beer website, which is just amazing to see:

    ''=~(        '(?{'        .('`'        |'%')        .('['        ^'-')
    .('`'        |'!')        .('`'        |',')        .'"'.        '\\$'
    .'=='        .('['        ^'+')        .('`'        |'/')        .('['
    ^'+')        .'||'        .(';'        &'=')        .(';'        &'=')
    .';-'        .'-'.        '\\$'        .'=;'        .('['        ^'(')
    .('['        ^'.')        .('`'        |'"')        .('!'        ^'+')
   .'_\\{'      .'(\\$'      .';=('.      '\\$=|'      ."\|".(      '`'^'.'
  ).(('`')|    '/').').'    .'\\"'.+(    '{'^'[').    ('`'|'"')    .('`'|'/'
 ).('['^'/')  .('['^'/').  ('`'|',').(  '`'|('%')).  '\\".\\"'.(  '['^('(')).
 '\\"'.('['^  '#').'!!--'  .'\\$=.\\"'  .('{'^'[').  ('`'|'/').(  '`'|"\&").(
 '{'^"\[").(  '`'|"\"").(  '`'|"\%").(  '`'|"\%").(  '['^(')')).  '\\").\\"'.
 ('{'^'[').(  '`'|"\/").(  '`'|"\.").(  '{'^"\[").(  '['^"\/").(  '`'|"\(").(
 '`'|"\%").(  '{'^"\[").(  '['^"\,").(  '`'|"\!").(  '`'|"\,").(  '`'|(',')).
 '\\"\\}'.+(  '['^"\+").(  '['^"\)").(  '`'|"\)").(  '`'|"\.").(  '['^('/')).
 '+_,\\",'.(  '{'^('[')).  ('\\$;!').(  '!'^"\+").(  '{'^"\/").(  '`'|"\!").(
 '`'|"\+").(  '`'|"\%").(  '{'^"\[").(  '`'|"\/").(  '`'|"\.").(  '`'|"\%").(
 '{'^"\[").(  '`'|"\$").(  '`'|"\/").(  '['^"\,").(  '`'|('.')).  ','.(('{')^
 '[').("\["^  '+').("\`"|  '!').("\["^  '(').("\["^  '(').("\{"^  '[').("\`"|
 ')').("\["^  '/').("\{"^  '[').("\`"|  '!').("\["^  ')').("\`"|  '/').("\["^
 '.').("\`"|  '.').("\`"|  '$')."\,".(  '!'^('+')).  '\\",_,\\"'  .'!'.("\!"^
 '+').("\!"^  '+').'\\"'.  ('['^',').(  '`'|"\(").(  '`'|"\)").(  '`'|"\,").(
 '`'|('%')).  '++\\$="})'  );$:=('.')^  '~';$~='@'|  '(';$^=')'^  '[';$/='`';