#!/usr/bin/perl -w
# Converts en masse an /etc/ethers file to DNS records for IPv6.
# Author: James F. Carter <jimc@math.ucla.edu>, 2008-11-03

# Synopsis: ether2dns {-f|-r} -p 2012:3456:789a:bcde:: [/etc/ethers] > dns.zone
#   -p	IPv6 address prefix (required), no /64, this length is assumed.
#   -f	To produce a forward DNS map
#   -r	To produce a reverse DNS map (exactly one of -f or -r is required)
# The "ethers" map is read from the named file, or standard input if no file.
# The DNS map comes out on standard output.  You could do:
#   ypcat ethers | ether2dns...

# The format of the "ethers" map is 
#   aa:bb:cc:dd:ee:ff	fully.qualified.domain.name
# Lines starting with # are comments.  Comments are preserved but are converted
# to DNS style (starting with ;) in the output.  If you don't want them, use
#   grep -v '#' /etc/ethers | ether2dns...

# Origin statements are produced -- from the prefix for the reverse map, or
# from the first FQDN for the forward map.  However, there is no $TTL, no
# SOA, nor NS records.  Only AAAA and PTR records are produced.

# If your /etc/ethers file covers several domains, you will want to use grep
# to filter out one domain at a time.

use strict;
use Getopt::Std;

our $opt_f = 0;				# For forward map
our $opt_r = 0;				# For reverse map
our $opt_p = '';			# IPv6 prefix
our @prefix;
our @bad;
our $f;

OPTS: {
		# Basic command line arguments
    getopts("p:fr") or push(@bad, "Bad command line options\n");
    push(@bad, "Extra command line arguments, expecting 1 filename\n")
	unless @ARGV <= 1;
    push(@bad, "Can't read $ARGV[0]\n") unless -r $ARGV[0];
    push(@bad, "One of -f or -r is required\n")
	unless $opt_f + $opt_r == 1;
		# Expand the prefix into 8 hex fields and detect screwups.
    push(@bad, "-p prefix is required\n") and last unless $opt_p ne '';
    if ($opt_p =~ s/\/\d*//) {
	push(@bad, "Do not specify /length after the prefix; /64 is assumed\n");
    }
    push(@bad, "Prefix contains invalid characters (should be hex digits separated by colons)\n")
	if $opt_p =~ /[^[:xdigit:]:\/]/;
		# Substitute 0's for ::
    @prefix = split(/:/, $opt_p, 9);
    my $nmis = 8 - scalar(@prefix);	# Number of 0's to substitute for ::
    my $k;
    if ($opt_p eq '::') {
	@prefix = (0) x 8;
    } elsif (substr($opt_p,0,2) eq ':;') {
	splice(@prefix, 0, 2, (0) x ($nmis+2));
    } elsif (substr($opt_p,-2) eq '::') {
	splice(@prefix, -2, 2, (0) x ($nmis+2));
    } elsif (($k = index($opt_p, '::')) >= 0) {
	my @ipfx = split(/:/, substr($opt_p, 0, $k));
	my $null = scalar(@ipfx);
	splice(@prefix, $null, 1, (0) x ($nmis+1));
    }
    push(@bad, "The prefix must be 8 hex numbers separated by colons (:: OK)\n")
	unless @prefix == 8;
		# Supply leading 0's in each field.
    my $over4;
    foreach $f (@prefix) {
	my $l = length($f);
	$over4++ if $l > 4;
	substr($f, 0, 0) = '0' x (4-$l);
    }
    push(@bad, "Each field of the prefix must be 4 hex digits, leading 0 optional\n") if $over4;
}
if (@bad) {
    print @bad, "Usage: ether2dns {-f|-r} -p 2012:3456:789a:bcde:: [/etc/ethers] > dns.zone\n";
    exit 4;
}

                # Now presumably we have everything useable.  Convert to 32 hex
		# digits for the reverse map.
@prefix = split(//, join('', @prefix));
		# Already checked the input file; should never die.
$f = @ARGV ? $ARGV[0] : '<&STDIN';
open(ETHERS, $f) or die "Can't open $f: $!\n";

our $origin;				# Is 1 after the origin is put out
our @comments;
our %compl2 = qw(0 2  1 3  2 0  3 1  4 6  5 7  6 4  7 5  
		8 a  9 b  a 8  b 9  c e  d f  e c  f d); #Complement bit 2
while (<ETHERS>) {
    next if /^\s*$/;			# Skip blank lines
    if (/^\s*\#/) {			# A comment
	s/\#/;/;			# In DNS files, comments start with ;
	push(@comments, $_);		# Save comment with its newline
	next;
    }
    chomp;
    my @line = split(' ', $_);
    if (@line < 2) {
	print STDERR "Line with less than 2 fields:\n$_\n";
	undef @comments;
	next;
    }
    my @fqdn = split(/\./, $line[1]);
		# Put out the origin line.
    if ($origin++) {
	# Already did the origin line.
    } elsif ($opt_f) {
	print '$ORIGIN ', join('.', @fqdn[1..$#fqdn], "\n");
    } else {
	print '$ORIGIN ', join('.', reverse @prefix[0..15]), ".ip6.arpa.\n";
    }
		# Print saved comments.
    print @comments;
    undef @comments;
		# Normalize the MAC address: lower case with leading 0's.
    $line[0] =~ tr/A-F/a-f/;
    my @mac = split(/:/, $line[0]);
    foreach $f (@mac) {
	substr($f,0,0) = '0' if length($f) < 2;
    }
		# Squeeze fffe in the middle.
    splice(@mac, 3, 0, 'ff', 'fe');
		# Complement bit 2 in the first octet.
    substr($mac[0],1,1) = $compl2{substr($mac[0],1,1)};
		# Now the actual record
    if ($opt_f) {
	my $k = int((23 - length($fqdn[0]))/8);
	print $fqdn[0], (($k > 0) ? ("\t" x $k) : ' '), "IN\tAAAA\t";
	splice(@mac, 0, 0, @prefix[0..15]);
	my $addr = join('', @mac);
	foreach $k (qw(0 4 8 12 16 20 24 28)) {
	    $f = substr($addr, $k, 4);
	    $f =~ s/^0*//;
	    $f = '0' if $f eq '';
	    substr($f,0,0) = ':' if $k > 0;
	    print $f;
	}
	print "\n";
    } else {
	@mac = split(//, join('', @mac));
	print join('.', reverse @mac), "\tIN\tPTR\t", $line[1], "\n";
    }
}
close(ETHERS);
exit 0;
