#!/usr/bin/perl
#
-# $Id: valtz,v 0.7 2003/07/10 16:39:30 magnus Exp $
-#
# <BSD-license>
#
-# Copyright (c) 2003, Magnus Bodin, <magnus@bodin.org>, http://x42.com
+# Copyright (c) 2020, the valtz authors
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
-# Neither the name of Magnus Bodin, x42.com nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
+# Neither the name of valtz nor the names of its contributors may be
+# used to endorse or promote products derived from this software
+# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
use File::Copy qw/ move /;
-my $VERSION = $1 if '$Revision: 0.7 $' =~ /(\d+\.\d+)/;
-my $COPYRIGHT = '; (C) 2003 Magnus Bodin, http://x42.com/software/';
+my $VERSION = '0.8';
+
$| = 1;
my %opt;
-getopts('?fFhHiIqrRstT:x', \%opt);
+getopts('?fFhHiIqrRtT:', \%opt);
-my $FILESUFFIXREGEXP = '('.join('|', qw/
- ,v ~ .bak .log .old .swp .tmp
- /).')$';
-
+# Validation errors
my $verrs_total = 0;
+
+# "Permission" errors with respect to what record types are allowed
my $perrs_total = 0;
##
# global location registry
# (reset for every zone file)
-my %loreg;
+my %loreg;
# NOTE : DO NOT CHANGE the id numbers
my %validation_msg = (
1008 => 'integer out of bounds',
1009 => 'must have at least three labels to be valid as mail address',
1010 => 'must not be 2(NS), 5(CNAME), 6(SOA), 12(PTR), 15(MX) or 252(AXFR)',
+ 1011 => 'IP address found where hostname expected'
);
# NOTE : ONLY translate the right-hand part
{
my $i = $1;
- $result = 1008 if $boundary && ($i >= $boundary);
+ $result = 1008 if $boundary && ($i >= $boundary);
}
else
{
if ($s =~ /^(\d+)(\.(\d+)(\.(\d+)(\.(\d+))?)?)?$/)
{
my ($a, $b, $c, $d) = ($1, $3, $5, $7);
- $a ||= 0;
- $b ||= 0;
- $c ||= 0;
- $d ||= 0;
- if (($a > 255) || ($b > 255) || ($c > 255) || ($d > 255))
+ $a ||= 0;
+ $b ||= 0;
+ $c ||= 0;
+ $d ||= 0;
+ if (($a > 255) || ($b > 255) || ($c > 255) || ($d > 255))
{
$result = 1003;
}
if ($s =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)\.?$/)
{
my ($a, $b, $c, $d) = ($1, $3, $5, $7);
- $a ||= 0;
- $b ||= 0;
- $c ||= 0;
- $d ||= 0;
- if (($a > 255) || ($b > 255) || ($c > 255) || ($d > 255))
+ $a ||= 0;
+ $b ||= 0;
+ $c ||= 0;
+ $d ||= 0;
+ if (($a > 255) || ($b > 255) || ($c > 255) || ($d > 255))
{
$result = 1003
}
'x' => [ 5, sub {
my ($type, $s) = @_;
my $result = 0;
+
+ # Check to see if someone put an IP address in a hostname
+ # field. The motivation for this was MX records where many
+ # people expect an IP address to be a valid response, but I
+ # see no harm in enforcing it elsewhere.
+ return 1011 if $s =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\.?$/;
+
# check all parts
for (split /\./, $s)
{
's' => [ 10, sub {
my ($type, $s) = @_;
my $result = 0;
- # TODO : Validation needed?
+ # TODO : Validation needed?
return $result;
}],
'p' => [ 11, sub {
}
return $result;
}],
- 'mname' => [ 12, sub {
+ 'mname' => [ 12, sub {
my ($type, $s) = @_;
my $result = 0;
# check all parts
'rname' => [ 13, sub {
my ($type, $s) = @_;
my $result = 0;
-
+
# check all parts
my @parts = split /\./, $s;
return 1009 if @parts < 3;
'n' => [ 19, sub {
my ($type, $s) = @_;
my $result = validate_integer($s, 65535);
-
+
return 1010 if ($s==2)||($s==5)||($s==6)||($s==12)||($s==15)||($s==252);
return $result;
}],
'rdata' => [ 20, sub {
my ($type, $s) = @_;
- # TODO : Validation needed?
+ # TODO : Validation needed?
my $result = 0;
return $result;
}],
{
$ip = $token;
}
-
+
#
if (length($ip) && ($mask[$c] eq 'x'))
{
$tmp =~ s/\.$//;
push @{$$result[3]}, $tmp;
}
-
+
# perform validation
-
+
my $tv = &{$$validator[1]}($type, $token);
if ($tv)
{
$$result[0] ^= (2 ** $$validator[0]);
- $$result[1] .=
+ $$result[1] .=
"\npos $c; $mask[$c]; $validation_msg{$tv}";
}
}
if ($mand)
{
- $$result[0] ^= (2 ** $$validator[0]);
+ $$result[0] ^= (2 ** $$validator[0]);
$$result[1] .= "\npos $c; $mask[$c]; ".
$token_name{$mask[$c]}.' is mandatory';
}
$c++;
}
}
-
+
if ($$result[0])
{
$$result[1] = "expected: ".$line_type{$type}->[1]."\n".
}
}
- else
+ else
{
$result = [ 1, sprintf("unknown record type: #%02x",
ord($type)) ];
$$result[1] =~ s/^\n+//;
$$result[1] =~ s/\n+/\n/g;
- # result is now [ iErrno, sErrtxt, sRecordType, [ sFQDN ] ]
+ # result is now [ iErrno, sErrtxt, sRecordType, [ sFQDN ] ]
return $result;
}
chomp;
s/^\s+//;
s/\s+$//;
-
+
if (/^(\w+)\s+(.+)$/)
{
my ($key, $value) = ($1, $2);
$$f{allowtype} = (keys %{$$f{allowtype}})[0];
$$f{allowtype} .= $opt{T};
-
+
my $allowtyperegex = make_char_regexp($$f{allowtype});
if ($$f{extralog})
}
for my $zonefile (@zonefiles)
{
- unless ($opt{s})
- {
- next if $zonefile =~ /$FILESUFFIXREGEXP/i;
- }
-
my $info = 0;
my $filehandle = \*STDIN;
my $fopen = 1;
{
my $temp = ($zonefile eq '-') ? '<STDIN>' : $zonefile;
p $output, "File $temp";
-
+
%loreg = ();
my $errs = 0;
my $lno = 0;
# Check $$v[3] against allowed fqdn:s:wq!
if (keys %{$$f{deny}})
{
- #
my $patterns = regexped_patterns($$f{deny});
# Default ALLOW ALL
$ok = $fqdnok = 1;
$reason = 'default allow ^.*$';
-
+
for my $pat (@{$patterns})
{
for (@{$$v[3]})
}
}
} # if deny/allow
- } # if fqdn
+ } # if fqdn
} # if recordtype ok
- } #
+ }
if ($ok && length($line))
{
{
$errs++;
$perrs_total++;
- p $output, " line $lno; err -2; $line";
+ p $output, " line $lno; err -2; $line";
p $output, " use of fqdn denied; $reason";
if ($opt{I})
{
- print STDOUT "# line $lno; err -2; $line\n";
+ print STDOUT "# line $lno; err -2; $line\n";
print STDOUT "# use of fqdn denied; $reason\n";
- }
+ }
}
}
}
}
}
-
- # Close all extra logfiles
+
+ # Close all extra logfiles
for my $el (@extralogs)
{
if (close($$el[0]))
my $files = funiq(@ARGV);
+sub usage {
+ print <<"--EOT";
+valtz $VERSION - validate tinydns-data files
-if ($opt{h} || $opt{H} || $opt{'?'})
-{
- print <<"--EOT";
-valtz $VERSION, $COPYRIGHT
-validates tinydns-data zone files
Usage:
- $0 [-hfFqrRiItTx] <file(s)>
-
- -h shows this help.
-
-
- -f filter (don't just validate) file and output accepted lines to STDOUT.
-
-
- -F treat files as filter configuration files for more advanced filtering.
- These filterfiles one or several of the following filter directives:
-
- zonefile <zonefilepath>
- zonefile file:<path to textfile including zonefilepaths>
- Defines the file(s) to be filtered. Can be a globbed value, like
- /var/zones/external/*
-
- extralog <logfile>
- Defines an extra logfile that the STDERR output will be copied for
- this specific filterfile. Useful if you have a lot of filterfiles
- and want to separate the logs.
+ $0 [-r] [-R] [-i] tinydns-file1 [tinydns-file2...]
- deny <zonepattern>
- deny file:<path to <zonepatternfile>
- Defines a zonepattern to explicitly DENY after implicitly allowing all.
- (cannot be combined with allow)
+ $0 [-HiIqrRt] [-T types] -f tinydns-file1 [tinydns-file2 ...]
- allow <zonepattern>
- allow file:<path to <zonepatternfile>
- Defines a zonepattern to explicitly ALLOW after implicitly denying all.
+ $0 valtz [-fHiIqrRt] [-T types] -F filter-file1 [filter-file2 ...]
- allowtype <recordtype character(s)>
- Explicitly sets the allowed recordtypes. Note that even comments
- has to be allowed (but these will not result in errors unless -t)
- to be copied to the output.
+Flags:
+ -h print usage information
+ -f filter invalid lines (filter mode)
+ -F filter using configuration files (advanced filter mode)
+ -r allow "fqdn" fields to be empty
+ -R allow "mname" and "p" fields to be empty
+ -i allow "ip" fields to be empty
+ -I include rejected lines as comments (filtering only)
+ -q don't print valid lines to standard out (filtering only)
+ -t don't ignore comment lines (filtering only)
+ -T <types> allow additional record types (advanced filtering only)
- Multiple zonefile, allow- and deny-lines are allowed, but also the
- alternative file:-line that points to a textfile containing one
- value per line.
-
-
- -r allows fqdn to be empty thus denoting the root.
- This is also allowed per default when doing implict allow - see deny,
- or when specifying 'allow .', i.e. explictly allowing root as such.
- (cannot be combined with deny)
-
-
- -R relaxes the validation and allows empty mname and p-fields.xi
- This is probably not very useful.
-
-
- -i allows the ip-fields to be empty as well. These will then not generate any
- records.
-
-
- -I Include rejected lines as comments in output (valid when filtering).
-
-
- -q Do not echo valid lines to STDOUT.
-
- -s DO NOT ignore files ending with ,v ~ .bak .log .old .swp .tmp
- which is done per default.
-
-
- -t Give error even on #comment-lines when they are not allowed.
- (These errors are silently ignored per default)
-
-
- -T<types>
- A commandline way to explicitly set the allowed recordtypes.
- This is _concatenated_ to the allowtype-allowed recordtypes.
-
- -x Exit with non-null exit code on errors; i.e. make errors detectable by
- e.g. shell scripts; 1 = validation error, 2 = permission error,
- 3 = combination of 1 and 2.
-
-
-
-All errors in the zonefiles are sent to STDERR.
-
- Example; simple use:
- valtz zone-bodin-org
-
- Example; simple filter-use;
- valtz -f /etc/zones/zone-* \
- >/etc/tinydns/data.filtered \
- 2>/var/log/tinydns/valtz.log
-
- Example; filterfile use;
- valtz -F /etc/zones/filter/zones-otto \
- >/etc/tinydns/data.otto \
- 2>/var/log/tinydns/valtz.log
-
-
- Example filterfile for using as import from primary (as above):
- zonefile /var/zones/external/otto/zone-*
- deny bodin.org
- deny x42.com
- extralog /var/log/tinydns/external-otto.log
-
- Example #2, strict filter for a certain user editing just A-records
-
- zonefile /home/felix/zones/zone-fl3x-net
- allow fl3x.net
- allowtype +
- extralog /var/log/tinydns/fl3x-net.log
-
- Example #3, export filter to secondary
-
- zonefile /var/zones/primary/zone-*
- # just allow OUR zones to be exported, not to annoy secondary partner
- allow file:/var/zones/primary-zones.txt
- # don't allow any other types than this; e.g. comments won't be exported
- allowtype Z + @ . C
- extralog /var/log/tinydns/primary-export.log
+Errors are generally printed to standard error, and the exit code
+shall reflect the presense of both usage and validation errors. See
+the man page for details.
--EOT
- exit 0;
}
-elsif (@{$files} == 0)
-{
- print <<"--EOT";
-valtz $VERSION, $COPYRIGHT
-validates tinydns-data zone files
-Usage:
- Simple validation:
- $0 [-qrRix] <zonefiles>
- Simple filtering:
- $0 -f[qrRiItTx] <zonefiles>
- Extensive filtering:
- $0 -F[qrRiItTx] <zonefiles>
- More help and information about options:
- $0 -h
+if ($opt{h} || $opt{H} || $opt{'?'}) {
+ usage();
---EOT
+ # If they asked for help, ignore whatever else they may have done
+ # wrong.
exit 0;
}
+if (@{$files} == 0) {
+ usage();
+ exit 4;
+}
if ($opt{F})
{
for my $zonefile (sort @{$files})
{
- unless ($opt{s})
- {
- next if $zonefile =~ /$FILESUFFIXREGEXP/i;
- }
-
my $filehandle = \*STDIN;
my $fopen = 1;
if ($zonefile ne '-')
{
print STDOUT "# line $lno; err $$v[0] $line
print STDOUT "# $$v[1]; \n";
- }
+ }
}
else
{
}
}
-if ($opt{x} && ($verrs_total + $perrs_total))
+if ($verrs_total + $perrs_total)
{
my $exitcode = $verrs_total > 0 ? 1 : 0;
$exitcode += $perrs_total > 0 ? 2 : 0;