Index: branches/stable/DNSDB.pm
===================================================================
--- branches/stable/DNSDB.pm	(revision 587)
+++ branches/stable/DNSDB.pm	(revision 649)
@@ -219,4 +219,7 @@
 		# 'all' (all IP values in any reverse zone view)
 		showrev_arpa	=> 'none',
+		# Two options for template record expansion:
+		template_skip_0	=> 0,	# publish .0 by default
+		template_skip_255	=> 0,	# publish .255 by default
 	);
 
@@ -248,5 +251,6 @@
 
   # Several settings are booleans.  Handle multiple possible ways of setting them.
-  for my $boolopt ('log_failures', 'force_refresh', 'lowercase', 'usecache') {
+  for my $boolopt ('log_failures', 'force_refresh', 'lowercase', 'usecache',
+	'template_skip_0', 'template_skip_255') {
     if ($self->{$boolopt} ne '1' && $self->{$boolopt} ne '0') {
       # true/false, on/off, yes/no all valid.
@@ -376,4 +380,51 @@
 } # end _ipparent()
 
+## DNSDB::_maybeip()
+# Wrapper for quick "does this look like an IP address?" regex, so we don't make dumb copy-paste mistakes
+sub _maybeip {
+  my $izzit = shift;  # reference
+  return 1 if $$izzit =~ m,^(?:[\d\./]+|[0-9a-fA-F:/]+)$,;
+}
+
+## DNSDB::_inrev()
+# Check if a given "hostname" is within a given reverse zone
+# Takes a reference to the "hostname" and the reverse zone CIDR as a NetAddr::IP
+# Returns true/false.  Sets $errstr on errors.
+sub _inrev {
+  my $self = shift;
+  my $dbh = $self->{dbh};
+  # References, since we might munge them
+  my $fq = shift;
+  my $zone = shift;
+
+  # set default error
+  $errstr = "$$fq not within $zone";
+
+  # Unlike forward zones, we will not coerce the data into the reverse zone - an A record
+  # in a reverse zone is already silly enough without appending a mess of 1.2.3.in-addr.arpa
+  # (or worse, 1.2.3.4.5.6.7.8.ip6.arpa) on the end of the nominal "hostname".
+  # We're also going to allow the "hostname" to be stored as .arpa or IP, because of
+  # non-IP FQDNs in .arpa
+  if ($$fq =~ /\.arpa$/) {
+    # "FQDN" could be any syntactically legitimate string, but it must be within the formal
+    # .arpa zone.  Note we're not validating these for correct reverse-IP values.
+    # yes, we really need the v6 branch on the end here.
+    $zone = _ZONE($zone, 'ZONE', 'r', '.').($zone->{isv6} ? '.ip6.arpa' : '.in-addr.arpa');
+    return unless $$fq =~ /$zone$/;
+  } else {
+    # in most cases we should be getting a real IP as the "FQDN" to test
+    my $addr = new NetAddr::IP $$fq if _maybeip($fq);
+
+    # "FQDN" should be a valid IP address.  Normalize formatting if so.
+    if (!$addr) {
+      $errstr = "$$fq is not a valid IP address";
+      return;
+    }
+    return if !$zone->contains($addr);
+    ($$fq = $addr) =~ s{/(?:32|128)$}{};
+  }
+  return 1;
+} # end _inrev()
+
 ## DNSDB::_hostparent()
 # A little different than _ipparent above;  this tries to *find* the parent zone of a hostname
@@ -449,19 +500,58 @@
   my %args = @_;
 
-  return ('FAIL', 'Reverse zones cannot contain A records') if $args{revrec} eq 'y';
-
-  # Coerce all hostnames to end in ".DOMAIN" for group/default records,
-  # or the intended parent domain for live records.
-  my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : $self->domainName($args{id}));
-  ${$args{host}} =~ s/\.*$/\.$pname/ if (${$args{host}} ne '@' && ${$args{host}} !~ /$pname$/);
-
-  # Check IP is well-formed, and that it's a v4 address
-  # Fail on "compact" IPv4 variants, because they are not consistent and predictable.
-  return ('FAIL',"$typemap{${$args{rectype}}} record must be a valid IPv4 address")
+# only for strict type restrictions
+#  return ('FAIL', 'Reverse zones cannot contain A records') if $args{revrec} eq 'y';
+
+  if ($args{revrec} eq 'y') {
+    # Get the revzone, so we can see if ${$args{val}} is in that zone
+    my $revzone = new NetAddr::IP $self->revName($args{id}, 'y');
+
+    return ('FAIL', $errstr) if !$self->_inrev($args{val}, $revzone);
+
+    # ${$args{val}} is either a valid IP or a string ending with the .arpa zone name;
+    # now check if it's a well-formed FQDN
+    return ('FAIL', $errstr) if ! _check_hostname_form(${$args{val}}, ${$args{rectype}}, $args{defrec}, $args{revrec}) &&
+	${$args{val}} =~ /\.arpa$/;
+
+    # Check IP is well-formed, and that it's a v4 address
+    # Fail on "compact" IPv4 variants, because they are not consistent and predictable.
+    return ('FAIL',"A record must be a valid IPv4 address")
+	unless ${$args{host}} =~ /^\d+\.\d+\.\d+\.\d+$/;
+    $args{addr} = new NetAddr::IP ${$args{host}};
+    return ('FAIL',"A record must be a valid IPv4 address")
+	unless $args{addr} && !$args{addr}->{isv6};
+    # coerce IP/value to normalized form for storage
+    ${$args{host}} = $args{addr}->addr;
+
+    # I'm just going to ignore the utterly barmy idea of an A record in the *default*
+    # records for a reverse zone;  it's bad enough to find one in funky legacy data.
+
+  } else {
+    # revrec ne 'y'
+
+    # Coerce all hostnames to end in ".DOMAIN" for group/default records,
+    # or the intended parent domain for live records.
+    my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : $self->domainName($args{id}));
+    ${$args{host}} =~ s/\.*$/\.$pname/ if (${$args{host}} ne '@' && ${$args{host}} !~ /$pname$/);
+
+    # Check if it's a proper formal .arpa name for an IP, and renormalize it to the IP
+    # value if so.  Done mainly for symmetry with PTR/A+PTR, and saves a conversion on export.
+    if (${$args{val}} =~ /\.arpa$/) {
+      my ($code,$tmp) = _zone2cidr(${$args{val}});
+      if ($code ne 'FAIL') {
+        ${$args{val}} = $tmp->addr;
+        $args{addr} = $tmp;
+      }
+    }
+    # Check IP is well-formed, and that it's a v4 address
+    # Fail on "compact" IPv4 variants, because they are not consistent and predictable.
+    return ('FAIL',"A record must be a valid IPv4 address")
 	unless ${$args{val}} =~ /^\d+\.\d+\.\d+\.\d+$/;
-  return ('FAIL',"$typemap{${$args{rectype}}} record must be a valid IPv4 address")
+    $args{addr} = new NetAddr::IP ${$args{val}};
+    return ('FAIL',"A record must be a valid IPv4 address")
 	unless $args{addr} && !$args{addr}->{isv6};
-  # coerce IP/value to normalized form for storage
-  ${$args{val}} = $args{addr}->addr;
+    # coerce IP/value to normalized form for storage
+    ${$args{val}} = $args{addr}->addr;
+  }
 
   return ('OK','OK');
@@ -475,31 +565,46 @@
   my %args = @_;
 
+  # NS target check - IP addresses not allowed.  Must be a more or less well-formed hostname.
+  if ($args{revrec} eq 'y') {
+    return ('FAIL', "NS records cannot point directly to an IP address")
+      if ${$args{host}} =~ /^(?:[\d.]+|[0-9a-fA-F:]+)$/;
+##enhance:  Look up the passed value to see if it exists.  Ooo, fancy.
+    return ('FAIL', $errstr) if ! _check_hostname_form(${$args{host}}, ${$args{rectype}}, $args{defrec}, $args{revrec});
+  } else {
+    return ('FAIL', "NS records cannot point directly to an IP address")
+      if ${$args{val}} =~ /^(?:[\d.]+|[0-9a-fA-F:]+)$/;
+##enhance:  Look up the passed value to see if it exists.  Ooo, fancy.
+    return ('FAIL', $errstr) if ! _check_hostname_form(${$args{val}}, ${$args{rectype}}, $args{defrec}, $args{revrec});
+  }
+
   # Check that the target of the record is within the parent.
-  # Yes, host<->val are mixed up here;  can't see a way to avoid it.  :(
   if ($args{defrec} eq 'n') {
     # Check if IP/address/zone/"subzone" is within the parent
     if ($args{revrec} eq 'y') {
-      my $tmpip = NetAddr::IP->new(${$args{val}});
-      my $pname = $self->revName($args{id});
-      return ('FAIL',"${$args{val}} not within $pname")
-	 unless $self->_ipparent($args{defrec}, $args{revrec}, $args{val}, $args{id}, \$tmpip);
-      # Sub the returned thing for ZONE?  This could get stupid if you have typos...
-      ${$args{val}} =~ s/ZONE/$tmpip->address/;
+      # Get the revzone, so we can see if ${$args{val}} is in that zone
+      my $revzone = new NetAddr::IP $self->revName($args{id}, 'y');
+
+      # Note the NS record may or may not be for the zone itself, it may be a pointer for a subzone
+      return ('FAIL', $errstr) if !$self->_inrev($args{val}, $revzone);
+
+      # ${$args{val}} is either a valid IP or a string ending with the .arpa zone name;
+      # now check if it's a well-formed FQDN
+##enhance or ##fixme
+# convert well-formed .arpa names to IP addresses to match old "strict" validation design
+      return ('FAIL', $errstr) if ! _check_hostname_form(${$args{val}}, ${$args{rectype}}, $args{defrec}, $args{revrec}) &&
+        ${$args{val}} =~ /\.arpa$/;
     } else {
+      # Forcibly append the domain name if the hostname being added does not end with the current domain name
       my $pname = $self->domainName($args{id});
-      ${$args{host}} = $pname if ${$args{host}} !~ /\.$pname$/;
+      ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/;
     }
   } else {
-    # Default reverse NS records should always refer to the implied parent
-    ${$args{host}} = 'DOMAIN' if $args{revrec} eq 'n';
-    ${$args{val}} = 'ZONE' if $args{revrec} eq 'y';
-  }
-
-# Let this lie for now.  Needs more magic.
-#  # Check IP is well-formed, and that it's a v4 address
-#  return ('FAIL',"A record must be a valid IPv4 address")
-#	unless $addr && !$addr->{isv6};
-#  # coerce IP/value to normalized form for storage
-#  $$val = $addr->addr;
+    # Default reverse NS records should always refer to the implied parent.  
+    if ($args{revrec} eq 'y') {
+      ${$args{val}} = 'ZONE';
+    } else {
+      ${$args{host}} = 'DOMAIN';
+    }    
+  }
 
   return ('OK','OK');
@@ -513,13 +618,50 @@
   my %args = @_;
 
-# Not really true, but these are only useful for delegating smaller-than-/24 IP blocks.
-# This is fundamentally a messy operation and should really just be taken care of by the
-# export process, not manual maintenance of the necessary records.
-  return ('FAIL', 'Reverse zones cannot contain CNAME records') if $args{revrec} eq 'y';
-
-  # Coerce all hostnames to end in ".DOMAIN" for group/default records,
-  # or the intended parent domain for live records.
-  my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : $self->domainName($args{id}));
-  ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/;
+  # CNAMEs in reverse zones shouldn't be handled manually, they should be generated on
+  # export by use of the "delegation" type.  For the masochistic, and those importing
+  # legacy data from $deity-knows-where, we'll support them.
+
+  if ($args{revrec} eq 'y') {
+    # CNAME target check - IP addresses not allowed.  Must be a more or less well-formed hostname.
+    return ('FAIL', "CNAME records cannot point directly to an IP address")
+      if ${$args{host}} =~ /^(?:[\d.]+|[0-9a-fA-F:]+)$/;
+
+    if ($args{defrec} eq 'n') {
+      # Get the revzone, so we can see if ${$args{val}} is in that zone
+      my $revzone = new NetAddr::IP $self->revName($args{id}, 'y');
+      return ('FAIL', $errstr) if !$self->_inrev($args{val}, $revzone);
+      # CNAMEs can not be used for parent nodes;  just leaf nodes with no other record types
+      # note that this WILL probably miss some edge cases.
+      if (${$args{val}} =~ /^[\d.\/]+$/) {
+        # convert IP "hostname" to .arpa
+        my $tmphn = _ZONE(NetAddr::IP->new(${$args{val}}), 'ZONE', 'r', '.');
+        my $tmpz = _ZONE($revzone, 'ZONE', 'r', '.');
+        return ('FAIL', "The bare zone may not be a CNAME") if $tmphn eq $tmpz;
+      }
+    }
+
+##enhance or ##fixme
+# convert well-formed .arpa names to IP addresses to match old "strict" validation design
+    return ('FAIL', $errstr) if ! _check_hostname_form(${$args{val}}, ${$args{rectype}}, $args{defrec}, $args{revrec}) &&
+      ${$args{val}} =~ /\.arpa$/;
+
+##enhance:  Look up the passed value to see if it exists.  Ooo, fancy.
+    return ('FAIL', $errstr) if ! _check_hostname_form(${$args{host}}, ${$args{rectype}}, $args{defrec}, $args{revrec});
+  } else {
+    # CNAME target check - IP addresses not allowed.  Must be a more or less well-formed hostname.
+    return ('FAIL', "CNAME records cannot point directly to an IP address")
+      if ${$args{val}} =~ /^(?:[\d.]+|[0-9a-fA-F:]+)$/;
+
+    # Forcibly append the domain name if the hostname being added does not end with the current domain name
+    my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : $self->domainName($args{id}));
+    ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/;
+
+    # CNAMEs can not be used for parent nodes;  just leaf nodes with no other record types
+    # Enforce this for the zone name
+    return ('FAIL', "The bare zone name may not be a CNAME") if ${$args{host}} eq $pname;
+
+##enhance:  Look up the passed value to see if it exists.  Ooo, fancy.
+    return ('FAIL', $errstr) if ! _check_hostname_form(${$args{val}}, ${$args{rectype}}, $args{defrec}, $args{revrec});
+  }
 
   return ('OK','OK');
@@ -539,75 +681,111 @@
 
   my %args = @_;
-
-  if ($args{revrec} eq 'y') {
-    if ($args{defrec} eq 'n') {
-      return ('FAIL', "IP or IP fragment ${$args{val}} is not within ".$self->revName($args{id}))
-	unless $self->_ipparent($args{defrec}, $args{revrec}, $args{val}, $args{id}, \$args{addr});
-      ${$args{val}} = $args{addr}->addr;
+  my $warnflag = '';
+
+  if ($args{defrec} eq 'y') {
+    if ($args{revrec} eq 'y') {
+      if (${$args{val}} =~ /^[\d.]+$/) {
+        # v4 or bare number
+        if (${$args{val}} =~ /^\d+\.\d+\.\d+\.\d+$/) {
+          # probable full IP.  pointless but harmless.  validate/normalize.
+          my $tmp = NetAddr::IP->new(${$args{val}})->addr
+            or return ('FAIL', "${$args{val}} is not a valid IP address");
+          ${$args{val}} = $tmp;
+          $warnflag = "${$args{val}} will only be added to a small number of zones\n";
+        } elsif (${$args{val}} =~ /^\d+$/) {
+          # bare number.  This can be expanded to either a v4 or v6 zone
+          ${$args{val}} =~ s/^\.*/ZONE./ unless ${$args{val}} =~ /^ZONE/;
+        } else {
+          # $deity-only-knows what kind of gibberish we've been given.  Only usable as a formal .arpa name.
+          # Append ARPAZONE to be replaced with the formal .arpa zone name when converted to a live record.
+          ${$args{val}} =~ s/\.*$/.ARPAZONE/ unless ${$args{val}} =~ /ARPAZONE$/;
+        }
+      } elsif (${$args{val}} =~ /^[a-fA-F0-9:]+$/) {
+        # v6 or fragment;  pray it's not complete gibberish
+        ${$args{val}} =~ s/^:*/ZONE::/ unless ${$args{val}} =~ /^ZONE/;
+      } else {
+        # $deity-only-knows what kind of gibberish we've been given.  Only usable as a formal .arpa name.
+        # Append ARPAZONE to be replaced with the formal .arpa zone name when converted to a live record.
+        ${$args{val}} .= ".ARPAZONE" unless ${$args{val}} =~ /ARPAZONE$/;
+      }
     } else {
-      if (${$args{val}} =~ /\./) {
-	# looks like a v4 or fragment
-	if (${$args{val}} =~ /^\d+\.\d+\.\d+\.\d+$/) {
-	  # woo!  a complete IP!  validate it and normalize, or fail.
-	  $args{addr} = NetAddr::IP->new(${$args{val}})
-		or return ('FAIL', "IP/value looks like IPv4 but isn't valid");
-	  ${$args{val}} = $args{addr}->addr;
-	} else {
-	  ${$args{val}} =~ s/^\.*/ZONE./ unless ${$args{val}} =~ /^ZONE/;
-	}
-      } elsif (${$args{val}} =~ /[a-f:]/) {
-	# looks like a v6 or fragment
-	${$args{val}} =~ s/^:*/ZONE::/ if !$args{addr} && ${$args{val}} !~ /^ZONE/;
-	if ($args{addr}) {
-	  if ($args{addr}->addr =~ /^0/) {
-	    ${$args{val}} =~ s/^:*/ZONE::/ unless ${$args{val}} =~ /^ZONE/;
-	  } else {
-	    ${$args{val}} = $args{addr}->addr;
-	  }
-	}
+      return ('FAIL', "PTR records are not supported in default record sets for forward zones (domains)");
+    }
+  } else {
+    if ($args{revrec} eq 'y') {
+      # Get the revzone, so we can see if ${$args{val}} is in that zone
+      my $revzone = new NetAddr::IP $self->revName($args{id}, 'y');
+
+      return ('FAIL', $errstr) if !$self->_inrev($args{val}, $revzone);
+
+      if (${$args{val}} =~ /\.arpa$/) {
+        # Check that it's well-formed
+        return ('FAIL', $errstr) if ! _check_hostname_form(${$args{val}}, ${$args{rectype}}, $args{defrec}, $args{revrec});
+
+        # Check if it's a proper formal .arpa name for an IP, and renormalize it to the IP
+        # value if so.  I can't see why someone would voluntarily work with those instead of
+        # the natural IP values but what the hey.
+        my ($code,$tmp) = _zone2cidr(${$args{val}});
+        ${$args{val}} = $tmp->addr if $code ne 'FAIL';
       } else {
-	# bare number (probably).  These could be v4 or v6, so we'll
-	# expand on these on creation of a reverse zone.
-	${$args{val}} = "ZONE,${$args{val}}" unless ${$args{val}} =~ /^ZONE/;
+        # not a formal .arpa name, so it should be an IP value.  Validate...
+        return ('FAIL', "${$args{val}} is not a valid IP value")
+            unless ${$args{val}} =~ /^(?:\d+\.\d+\.\d+\.\d+|[a-fA-F0-9:]+)$/;
+        $args{addr} = NetAddr::IP->new(${$args{val}})
+            or return ('FAIL', "IP/value looks like an IP address but isn't valid");
+        # ... and normalize.
+        ${$args{val}} = $args{addr}->addr;
       }
-      ${$args{host}} =~ s/\.*$/\.$self->{domain}/ if ${$args{host}} !~ /(?:$self->{domain}|ADMINDOMAIN)$/;
-    }
+      # Validate PTR target for form.
+      return ('FAIL', $errstr) if ! _check_hostname_form(${$args{host}}, ${$args{rectype}}, $args{defrec}, $args{revrec});
+    } else { # revrec ne 'y'
+      # Fetch the domain and append if the passed hostname isn't within it.
+      my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : $self->domainName($args{id}));
+      ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/;
+      # Validate hostname and target for form
+      return ('FAIL', $errstr) if ! _check_hostname_form(${$args{host}}, ${$args{rectype}}, $args{defrec}, $args{revrec});
+      return ('FAIL', $errstr) if ! _check_hostname_form(${$args{val}}, ${$args{rectype}}, $args{defrec}, $args{revrec});
+    }
+  }
 
 # Multiple PTR records do NOT generally do what most people believe they do,
 # and tend to fail in the most awkward way possible.  Check and warn.
-# We use $val instead of $addr->addr since we may be in a defrec, and may have eg "ZONE::42" or "ZONE.12"
-
-    my @checkvals = (${$args{val}});
-    if (${$args{val}} =~ /,/) {
-      # push . and :: variants into checkvals if val has ,
-      my $tmp;
-      ($tmp = ${$args{val}}) =~ s/,/./;
-      push @checkvals, $tmp;
-      ($tmp = ${$args{val}}) =~ s/,/::/;
-      push @checkvals, $tmp;
-    }
-    my $pcsth = $dbh->prepare("SELECT count(*) FROM "._rectable($args{defrec},$args{revrec})." WHERE val = ?");
-    foreach my $checkme (@checkvals) {
-      if ($args{update}) {
-	# Record update.  There should usually be an existing PTR (the record being updated)
-	my @ptrs = @{ $dbh->selectcol_arrayref("SELECT record_id FROM "._rectable($args{defrec},$args{revrec}).
-		" WHERE val = ?", undef, ($checkme)) };
-	return ('WARN', "PTR record for $checkme already exists;  adding another will probably not do what you want")
-		if @ptrs && (!grep /^$args{update}$/, @ptrs);
-      } else {
-	# New record.  Always warn if a PTR exists
-	my ($ptrcount) = $dbh->selectrow_array("SELECT count(*) FROM "._rectable($args{defrec},$args{revrec}).
-		" WHERE val = ?", undef, ($checkme));
-	return ('WARN', "PTR record for $checkme already exists;  adding another will probably not do what you want")
-		if $ptrcount;
-      }
-    }
-
-  } else {
-    # Not absolutely true but only useful if you hack things up for sub-/24 v4 reverse delegations
-    # Simpler to just create the reverse zone and grant access for the customer to edit it, and create direct
-    # PTR records on export
-    return ('FAIL',"Forward zones cannot contain PTR records");
-  }
+
+  my $chkbase = ${$args{val}};;
+  my $hostcol = 'val';	# Reverse zone hostnames are stored "backwards"
+  if ($args{revrec} eq 'n') {	# PTRs in forward zones should be rare.
+    $chkbase = ${$args{host}};
+    $hostcol = 'host';
+  }
+  my @checkvals = ($chkbase);
+  if ($chkbase =~ /,/) {
+    # push . and :: variants into checkvals if $chkbase has ,
+    my $tmp;
+    ($tmp = $chkbase) =~ s/,/./;
+    push @checkvals, $tmp;
+    ($tmp = $chkbase) =~ s/,/::/;
+    push @checkvals, $tmp;
+  }
+
+  my $pcsth = $dbh->prepare("SELECT count(*) FROM "._rectable($args{defrec},$args{revrec})." WHERE $hostcol = ?");
+  foreach my $checkme (@checkvals) {
+    if ($args{update}) {
+      # $args{update} contains the ID of the record being updated.  If the list of records that matches
+      # the new hostname specification doesn't include this, the change effectively adds a new PTR that's
+      # the same as one or more existing ones.
+      my @ptrs = @{ $dbh->selectcol_arrayref("SELECT record_id FROM "._rectable($args{defrec},$args{revrec}).
+	" WHERE val = ?", undef, ($checkme)) };
+      $warnflag .= "PTR record for $checkme already exists;  adding another will probably not do what you want"
+	if @ptrs && (!grep /^$args{update}$/, @ptrs);
+    } else {
+      # New record.  Always warn if a PTR exists
+      my ($ptrcount) = $dbh->selectrow_array("SELECT count(*) FROM "._rectable($args{defrec},$args{revrec}).
+	" WHERE $hostcol = ?", undef, ($checkme));
+      $warnflag .= "PTR record for $checkme already exists;  adding another will probably not do what you want"
+	if $ptrcount;
+    }
+  }
+
+  return ('WARN',$warnflag) if $warnflag;
 
   return ('OK','OK');
@@ -621,6 +799,6 @@
   my %args = @_;
 
-# Not absolutely true but WTF use is an MX record for a reverse zone?
-  return ('FAIL', 'Reverse zones cannot contain MX records') if $args{revrec} eq 'y';
+# only for strict type restrictions
+#  return ('FAIL', 'Reverse zones cannot contain MX records') if $args{revrec} eq 'y';
 
   return ('FAIL', "Distance is required for MX records") unless defined(${$args{dist}});
@@ -631,30 +809,65 @@
   push @{$args{vallist}}, ${$args{dist}};
 
-  # Coerce all hostnames to end in ".DOMAIN" for group/default records,
-  # or the intended parent domain for live records.
-  my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : $self->domainName($args{id}));
-  ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/;
-
-# hmm..  this might work.  except possibly for something pointing to "deadbeef.ca".  <g>
-#  if ($type == $reverse_typemap{NS} || $type == $reverse_typemap{MX} || $type == $reverse_typemap{SRV}) {
-#    if ($val =~ /^\s*[\da-f:.]+\s*$/) {
-#      return ('FAIL',"$val is not a valid IP address") if !$addr;
-#    }
-#  }
-
-  return ('OK','OK');
-} # done MX record
-
-# TXT record
-sub _validate_16 {
-  my $self = shift;
-
-  my %args = @_;
-
-  if ($args{revrec} eq 'y') {
+  if ($args{revrec} eq 'n') {
+    # MX target check - IP addresses not allowed.  Must be a more or less well-formed hostname.
+    return ('FAIL', "MX records cannot point directly to an IP address")
+      if ${$args{val}} =~ /^(?:[\d.]+|[0-9a-fA-F:]+)$/;
+
     # Coerce all hostnames to end in ".DOMAIN" for group/default records,
     # or the intended parent domain for live records.
     my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : $self->domainName($args{id}));
     ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/;
+  } else {
+    # MX target check - IP addresses not allowed.  Must be a more or less well-formed hostname.
+    return ('FAIL', "MX records cannot point directly to an IP address")
+      if ${$args{host}} =~ /^(?:[\d.]+|[0-9a-fA-F:]+)$/;
+
+    # MX records in reverse zones get stricter treatment.  The UI bars adding them in
+    # reverse record sets, but we "need" to allow editing existing ones.  And we'll allow
+    # editing them if some loon manually munges one into a default reverse record set.
+    if ($args{defrec} eq 'n') {
+      # Get the revzone, so we can see if ${$args{val}} is in that zone
+      my $revzone = new NetAddr::IP $self->revName($args{id}, 'y');
+      return ('FAIL', $errstr) if !$self->_inrev($args{val}, $revzone);
+    }
+
+##enhance or ##fixme
+# convert well-formed .arpa names to IP addresses to match old "strict" validation design
+    return ('FAIL', $errstr) if ! _check_hostname_form(${$args{val}}, ${$args{rectype}}, $args{defrec}, $args{revrec}) &&
+      ${$args{val}} =~ /\.arpa$/;
+
+##enhance:  Look up the passed value to see if it exists.  Ooo, fancy.
+    return ('FAIL', $errstr) if ! _check_hostname_form(${$args{host}}, ${$args{rectype}}, $args{defrec}, $args{revrec});
+
+  }
+
+  return ('OK','OK');
+} # done MX record
+
+# TXT record
+sub _validate_16 {
+  my $self = shift;
+
+  my %args = @_;
+
+  if ($args{revrec} eq 'n') {
+    # Coerce all hostnames to end in ".DOMAIN" for group/default records,
+    # or the intended parent domain for live records.
+    my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : $self->domainName($args{id}));
+    ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/;
+    return ('FAIL', $errstr) if ! _check_hostname_form(${$args{host}}, ${$args{rectype}}, $args{defrec}, $args{revrec});
+  } else {
+    # We don't coerce reverse "hostnames" into the zone, mainly because we store most sane
+    # records as IP values, not .arpa names.
+    if ($args{defrec} eq 'n') {
+      # Get the revzone, so we can see if ${$args{val}} is in that zone
+      my $revzone = new NetAddr::IP $self->revName($args{id}, 'y');
+      return ('FAIL', $errstr) if !$self->_inrev($args{val}, $revzone);
+    }
+
+##enhance or ##fixme
+# convert well-formed .arpa names to IP addresses to match old "strict" validation design
+    return ('FAIL', $errstr) if ! _check_hostname_form(${$args{val}}, ${$args{rectype}}, $args{defrec}, $args{revrec}) &&
+      ${$args{val}} =~ /\.arpa$/;
   }
 
@@ -665,9 +878,11 @@
 # RP record
 sub _validate_17 {
-  # Probably have to validate these some day
-  return ('OK','OK');
+  # Probably have to validate these separately some day.  Call _validate_16() above since
+  # they're otherwise very similar
+  return _validate_16(@_);
 } # done RP record
 
 # AAAA record
+# Almost but not quite an exact duplicate of A record
 sub _validate_28 {
   my $self = shift;
@@ -676,16 +891,57 @@
   my %args = @_;
 
-  return ('FAIL', 'Reverse zones cannot contain AAAA records') if $args{revrec} eq 'y';
-
-  # Coerce all hostnames to end in ".DOMAIN" for group/default records,
-  # or the intended parent domain for live records.
-  my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : $self->domainName($args{id}));
-  ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/;
-
-  # Check IP is well-formed, and that it's a v6 address
-  return ('FAIL',"$typemap{${$args{rectype}}} record must be a valid IPv6 address")
+# only for strict type restrictions
+#  return ('FAIL', 'Reverse zones cannot contain AAAA records') if $args{revrec} eq 'y';
+
+  if ($args{revrec} eq 'y') {
+    # Get the revzone, so we can see if ${$args{val}} is in that zone
+    my $revzone = new NetAddr::IP $self->revName($args{id}, 'y');
+
+    return ('FAIL', $errstr) if !$self->_inrev($args{val}, $revzone);
+
+    # ${$args{val}} is either a valid IP or a string ending with the .arpa zone name;
+    # now check if it's a well-formed FQDN
+    return ('FAIL', $errstr) if ! _check_hostname_form(${$args{val}}, ${$args{rectype}}, $args{defrec}, $args{revrec}) &&
+	${$args{val}} =~ /\.arpa$/;
+
+    # Check IP is well-formed, and that it's a v4 address
+    # Fail on "compact" IPv4 variants, because they are not consistent and predictable.
+    return ('FAIL',"AAAA record must be a valid IPv6 address")
+	unless ${$args{host}} =~ /^[a-fA-F0-9:]+$/;
+    $args{addr} = new NetAddr::IP ${$args{host}};
+    return ('FAIL',"AAAA record must be a valid IPv6 address")
 	unless $args{addr} && $args{addr}->{isv6};
-  # coerce IP/value to normalized form for storage
-  ${$args{val}} = $args{addr}->addr;
+    # coerce IP/value to normalized form for storage
+    ${$args{host}} = $args{addr}->addr;
+
+    # I'm just going to ignore the utterly barmy idea of an AAAA record in the *default*
+    # records for a reverse zone;  it's bad enough to find one in funky legacy data.
+
+  } else {
+    # revrec ne 'y'
+
+    # Coerce all hostnames to end in ".DOMAIN" for group/default records,
+    # or the intended parent domain for live records.
+    my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : $self->domainName($args{id}));
+    ${$args{host}} =~ s/\.*$/\.$pname/ if (${$args{host}} ne '@' && ${$args{host}} !~ /$pname$/);
+
+    # Check if it's a proper formal .arpa name for an IP, and renormalize it to the IP
+    # value if so.  Done mainly for symmetry with PTR/AAAA+PTR, and saves a conversion on export.
+    if (${$args{val}} =~ /\.arpa$/) {
+      my ($code,$tmp) = _zone2cidr(${$args{val}});
+      if ($code ne 'FAIL') {
+        ${$args{val}} = $tmp->addr;
+        $args{addr} = $tmp;
+      }
+    }
+    # Check IP is well-formed, and that it's a v6 address
+    return ('FAIL',"AAAA record must be a valid IPv6 address")
+	unless ${$args{val}} =~ /^[a-fA-F0-9:]+$/;
+    $args{addr} = new NetAddr::IP ${$args{val}};
+    return ('FAIL',"AAAA record must be a valid IPv6 address")
+	unless $args{addr} && $args{addr}->{isv6};
+    # coerce IP/value to normalized form for storage
+    ${$args{val}} = $args{addr}->addr;
+  }
 
   return ('OK','OK');
@@ -702,21 +958,11 @@
   return ('FAIL', 'Reverse zones cannot contain SRV records') if $args{revrec} eq 'y';
 
-  return ('FAIL', "Distance is required for SRV records") unless defined(${$args{dist}});
+  # Key additional record parts.  Always required.
+  return ('FAIL',"Distance, port and weight are required for SRV records")
+	unless defined(${$args{weight}}) && defined(${$args{port}}) && defined(${$args{dist}});
   ${$args{dist}} =~ s/\s*//g;
-  return ('FAIL',"Distance is required, and must be numeric") unless ${$args{dist}} =~ /^\d+$/;
-
-  # Coerce all hostnames to end in ".DOMAIN" for group/default records,
-  # or the intended parent domain for live records.
-  my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : $self->domainName($args{id}));
-  ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/;
-
-  return ('FAIL',"SRV records must begin with _service._protocol [${$args{host}}]")
-	unless ${$args{host}} =~ /^_[A-Za-z]+\._[A-Za-z]+\.[a-zA-Z0-9-]+/;
-  return ('FAIL',"Port and weight are required for SRV records")
-	unless defined(${$args{weight}}) && defined(${$args{port}});
   ${$args{weight}} =~ s/\s*//g;
   ${$args{port}} =~ s/\s*//g;
-
-  return ('FAIL',"Port and weight are required, and must be numeric")
+  return ('FAIL',"Distance, port and weight are required, and must be numeric")
 	unless ${$args{weight}} =~ /^\d+$/ && ${$args{port}} =~ /^\d+$/;
 
@@ -724,4 +970,55 @@
   push @{$args{vallist}}, (${$args{dist}}, ${$args{weight}}, ${$args{port}});
 
+  if ($args{revrec} eq 'n') {
+    # Coerce all hostnames to end in ".DOMAIN" for group/default records,
+    # or the intended parent domain for live records.
+    my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : $self->domainName($args{id}));
+    ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/;
+
+##enhance:  Rejig so that we can pass back a WARN red flag, instead of
+# hard-failing, since it seems that purely from the DNS record perspective,
+# SRV records without underscores are syntactically valid
+    # Not strictly true, but SRV records not following this convention won't be found.
+    return ('FAIL',"SRV records must begin with _service._protocol [${$args{host}}]")
+	unless ${$args{host}} =~ /^_[A-Za-z]+\._[A-Za-z]+\.[a-zA-Z0-9-]+/;
+
+    # SRV target check - IP addresses not allowed.  Must be a more or less well-formed hostname.
+    return ('FAIL', "SRV records cannot point directly to an IP address")
+      if ${$args{val}} =~ /^(?:[\d.]+|[0-9a-fA-F:]+)$/;
+  } else {
+    # hm.  we can't do anything sane with IP values here;  part of the record data is in
+    # fact encoded in the "hostname".  enforce .arpa names?  OTOH, SRV records in a reverse
+    # zone are pretty silly.
+
+##enhance:  Rejig so that we can pass back a WARN red flag, instead of
+# hard-failing, since it seems that purely from the DNS record perspective,
+# SRV records without underscores are syntactically valid
+    # Not strictly true, but SRV records not following this convention won't be found.
+    return ('FAIL',"SRV records must begin with _service._protocol [${$args{host}}]")
+	unless ${$args{val}} =~ /^_[A-Za-z]+\._[A-Za-z]+\.[a-zA-Z0-9-]+/;
+
+    # SRV target check - IP addresses not allowed.  Must be a more or less well-formed hostname.
+    return ('FAIL', "SRV records cannot point directly to an IP address")
+      if ${$args{host}} =~ /^(?:[\d.]+|[0-9a-fA-F:]+)$/;
+
+    # SRV records in reverse zones get stricter treatment.  The UI bars adding them in
+    # reverse record sets, but we "need" to allow editing existing ones.  And we'll allow
+    # editing them if some loon manually munges one into a default reverse record set.
+    if ($args{defrec} eq 'n') {
+      # Get the revzone, so we can see if ${$args{val}} is in that zone
+      my $revzone = new NetAddr::IP $self->revName($args{id}, 'y');
+      return ('FAIL', $errstr) if !$self->_inrev($args{val}, $revzone);
+    }
+
+##enhance or ##fixme
+# convert well-formed .arpa names to IP addresses to match old "strict" validation design
+    return ('FAIL', $errstr) if ! _check_hostname_form(${$args{val}}, ${$args{rectype}}, $args{defrec}, $args{revrec}) &&
+      ${$args{val}} =~ /\.arpa$/;
+
+##enhance:  Look up the passed value to see if it exists.  Ooo, fancy.
+    return ('FAIL', $errstr) if ! _check_hostname_form(${$args{host}}, ${$args{rectype}}, $args{defrec}, $args{revrec});
+
+  }
+
   return ('OK','OK');
 } # done SRV record
@@ -741,4 +1038,16 @@
   if ($args{defrec} eq 'n') {
     # live record;  revrec determines whether we validate the PTR or A component first.
+
+    # Fail early on non-IP gibberish in ${$args{val}}.  Arguably .arpa names might be acceptable
+    # but that gets stupid in forward zones, since these records are shared.
+    return ('FAIL', "$typemap{${$args{rectype}}} record must be a valid IPv4 address")
+      if ${$args{rectype}} == 65280 && ${$args{val}} !~ /^\d+\.\d+\.\d+\.\d+$/;
+    return ('FAIL', "$typemap{${$args{rectype}}} record must be a valid IPv6 address")
+      if ${$args{rectype}} == 65281 && ${$args{val}} !~ /^[a-fA-F0-9:]+$/;
+    # If things are not OK, this should prevent Stupid in the error log.
+    $args{addr} = new NetAddr::IP ${$args{val}}
+      or return ('FAIL', "$typemap{${$args{rectype}}} record must be a valid IPv".
+			(${$args{rectype}} == 65280 ? '4' : '6')." address");
+    ${$args{val}} = $args{addr}->addr;
 
     if ($args{revrec} eq 'y') {
@@ -803,13 +1112,5 @@
       }
 
-#      my ($ptrcount) = $dbh->selectrow_array("SELECT count(*) FROM "._rectable($args{defrec},$args{revrec}).
-#	" WHERE val = ?", undef, ${$args{val}});
-#      if ($ptrcount) {
-#        my $curid = $dbh->selectrow_array("SELECT record_id FROM "._rectable($args{defrec},$args{revrec}).
-#		" WHERE val = ?
-#	$msg = "PTR record for ${$args{val}} already exists;  adding another will probably not do what you want";
-#	$code = 'WARN';
-#      }
-
+      # Add the reverse zone ID to the fieldlist
       ${$args{fields}} .= "rdns_id,";
       push @{$args{vallist}}, $revid;
@@ -817,4 +1118,5 @@
 
   } else {	# defrec eq 'y'
+
     if ($args{revrec} eq 'y') {
       ($code,$msg) = $self->_validate_12(%args);
@@ -989,7 +1291,8 @@
 
   return ('OK','OK');
-} # done AAAA+PTR template record
+} # done A+PTR template record
 
 # AAAA+PTR template record
+# Not sure this can be handled sanely due to the size of IPv6 address space
 sub _validate_65284 {
   return ('OK','OK');
@@ -1027,4 +1330,28 @@
   return ('OK','OK');
 }
+
+# Subs not specific to a particular record type
+
+# Convert $$host and/or $$val to lowercase as appropriate.
+# Should only be called if $self->{lowercase} is true.
+# $rectype is also a reference for caller convenience
+sub _caseclean {
+  my ($rectype, $host, $val, $defrec, $revrec) = @_;
+
+  # Can't case-squash default records, due to DOMAIN, ZONE, and ADMINDOMAIN templating
+  return if $defrec eq 'y';
+
+  if ($typemap{$$rectype} eq 'TXT' || $typemap{$$rectype} eq 'SPF') {
+    # TXT records should preserve user entry in the string.
+    # SPF records are a duplicate of TXT with a new record type value (99)
+    $$host = lc($$host) if $revrec eq 'n';	# only lowercase $$host on live forward TXT;  preserve TXT content
+    $$val = lc($$val) if $revrec eq 'y';	# only lowercase $$val on live reverse TXT;  preserve TXT content
+  } else {
+    # Non-TXT, live records, are fully case-insensitive
+    $$host = lc($$host);
+    $$val = lc($$val);
+  } # $typemap{$$rectype} else
+
+} # _caseclean()
 
 
@@ -1309,13 +1636,20 @@
       return;
     }
+  } elsif ($rectype == $reverse_typemap{CNAME}) {
+    # Allow / in reverse CNAME hostnames for sub-/24 delegation
+    if (lc($hname) !~ m|^[0-9a-z_./-]+$|) {
+      # error message is deliberately restrictive;  special cases are SPECIAL and not for general use
+      $errstr = "Hostnames may not contain anything other than (0-9 a-z . _)";
+      return;
+    }
   } elsif ($revrec eq 'y') {
     # Reverse zones don't support @ in hostnames
-    # Also skip failure on revzone TXT records;  the hostname contains the TXT content in that case.
-    if ($rectype != $reverse_typemap{TXT} && lc($hname) !~ /^[0-9a-z_.-]+$/) {
+    if (lc($hname) !~ /^(?:\*\.)?[0-9a-z_.-]+$/) {
+      # error message is deliberately restrictive;  special cases are SPECIAL and not for general use
       $errstr = "Hostnames may not contain anything other than (0-9 a-z . _)";
       return;
     }
   } else {
-    if (lc($hname) !~ /^(?:[0-9a-z_.-]+|@)$/) {
+    if (lc($hname) !~ /^(?:\*\.)?(?:[0-9a-z_.-]+|@)$/) {
       # Don't mention @, because it would be far too wordy to explain the nuance of @
       $errstr = "Hostnames may not contain anything other than (0-9 a-z . _)";
@@ -1370,4 +1704,6 @@
       $cfg->{lowercase}		= $1 if /^lowercase\s*=\s*([a-z01]+)/i;
       $cfg->{showrev_arpa}	= $1 if /^showrev_arpa\s*=\s*([a-z]+)/i;
+      $cfg->{template_skip_0}	= $1 if /^template_skip_0\s*=\s*([a-z01]+)/i;
+      $cfg->{template_skip_255}	= $1 if /^template_skip_255\s*=\s*([a-z01]+)/i;
 # not supported in dns.cgi yet
 #      $cfg->{templatedir}	= $1 if m{^templatedir\s*=\s*([a-z0-9/_.-]+)}i;
@@ -1735,4 +2071,5 @@
 # Returns '>', '<', '=', '!'
 sub comparePermissions {
+  my $self = shift;
   my $p1 = shift;
   my $p2 = shift;
@@ -1893,4 +2230,5 @@
       $host =~ s/DOMAIN/$domain/g;
       $val =~ s/DOMAIN/$domain/g;
+      _caseclean(\$type, \$host, \$val, 'n', 'n') if $self->{lowercase};
       $sth_in->execute($host, $type, $val, $dist, $weight, $port, $ttl, $defloc);
       if ($typemap{$type} eq 'SOA') {
@@ -2197,4 +2535,6 @@
 	}
       }
+
+      _caseclean(\$type, \$host, \$val, 'n', 'y') if $self->{lowercase};
 
       $sth_in->execute($domid,$host,$type,$val,$ttl,$defloc);
@@ -2816,4 +3156,5 @@
     # insert the user...  note we set inherited perms by default since
     # it's simple and cleans up some other bits of state
+##fixme:  need better handling of case of inherited or missing (!!) permissions entries
     my $sth = $dbh->prepare("INSERT INTO users ".
 	"(group_id,username,password,firstname,lastname,phone,type,status,permission_id,inherit_perm) ".
@@ -3665,5 +4006,5 @@
   foreach my $sf (split /,/, $args{sortby}) {
     $sf = "r.$sf";
-    $sf =~ s/r\.val/CAST (r.val AS inet)/
+    $sf =~ s/r\.val/inetlazy(r.val)/
 	if $args{revrec} eq 'y' && $args{defrec} eq 'n';
     $sf =~ s/r\.type/t.alphaorder/;
@@ -3685,13 +4026,9 @@
   $sql .= "WHERE "._recparent($args{defrec},$args{revrec})." = ?";
   $sql .= " AND NOT r.type=$reverse_typemap{SOA}";
-#    if ($self->{showrev_arpa} eq 'zone' || $self->{showrev_arpa} eq 'all') {
-      # Just In Case the UI is using formal .arpa notation, and someone enters something reversed,
-      # we want to match both the formal and natural zone name
-      $sql .= " AND (r.host ~* ? OR r.val ~* ? OR r.host ~* ? OR r.val ~* ?)" if $args{filter};
-      my $tmp = join('.',reverse(split(/\./,$args{filter})));
-      push @bindvars, ($tmp, $tmp) if $args{filter};
-#    } else {
-#      $sql .= " AND (r.host ~* ? OR r.val ~* ?)" if $args{filter};
-#    }
+  if ($args{filter}) {
+    $sql .= " AND (r.host ~* ? OR r.val ~* ? OR r.host ~* ? OR r.val ~* ?)";
+    my $tmp = join('.',reverse(split(/\./,$args{filter})));
+    push @bindvars, ($tmp, $tmp);
+  }
   $sql .= " ORDER BY $newsort $args{sortorder}";
   # ensure consistent ordering by sorting on record_id too
@@ -3703,8 +4040,11 @@
   $recsth->execute(@bindvars);
   while (my $rec = $recsth->fetchrow_hashref) {
-    if ($args{revrec} eq 'y' && ($self->{showrev_arpa} eq 'record' || $self->{showrev_arpa} eq 'all')) {
+    if ($args{revrec} eq 'y' && $args{defrec} eq 'n' &&
+	($self->{showrev_arpa} eq 'record' || $self->{showrev_arpa} eq 'all') &&
+	$rec->{val} !~ /\.arpa$/ ) {
+      # skip all reverse zone .arpa "hostnames" since they're already .arpa names.
 ##enhance:  extend {showrev_arpa} eq 'record' to specify record types
-      my $tmp = new NetAddr::IP $rec->{val};
-      $rec->{val} = DNSDB::_ZONE($tmp, 'ZONE', 'r', '.').($tmp->{isv6} ? '.ip6.arpa' : '.in-addr.arpa');
+      my $tmpip = new NetAddr::IP $rec->{val} if $rec->{val} =~ /^(?:[\d.\/]+|[a-fA-F0-9:\/]+)$/;
+      $rec->{val} = DNSDB::_ZONE($tmpip, 'ZONE', 'r', '.').($tmpip->{isv6} ? '.ip6.arpa' : '.in-addr.arpa') if $tmpip;
     }
     push @working, $rec;
@@ -3737,13 +4077,12 @@
 	" WHERE "._recparent($defrec,$revrec)."=? ".
 	"AND NOT type=$reverse_typemap{SOA}";
-#    if ($self->{showrev_arpa} eq 'zone' || $self->{showrev_arpa} eq 'all') {
-      # Just In Case the UI is using formal .arpa notation, and someone enters something reversed,
-      # we want to match both the formal and natural zone name
-      $sql .= " AND (host ~* ? OR val ~* ? OR host ~* ? OR val ~* ?)" if $filter;
-      my $tmp = join('.',reverse(split(/\./,$filter)));
-      push @bindvars, ($tmp, $tmp) if $filter;
-#    } else {
-#      $sql .= " AND (host ~* ? OR val ~* ?)" if $filter;
-#    }
+  if ($filter) {
+    $sql .= " AND (r.host ~* ? OR r.val ~* ? OR r.host ~* ? OR r.val ~* ?)";
+    my $tmp = join('.',reverse(split(/\./,$filter)));
+    push @bindvars, ($tmp, $tmp);
+  }
+  $sql .= " AND (host ~* ? OR val ~* ? OR host ~* ? OR val ~* ?)" if $filter;
+  my $tmp = join('.',reverse(split(/\./,$filter)));
+  push @bindvars, ($tmp, $tmp) if $filter;
 
   my ($count) = $dbh->selectrow_array($sql, undef, (@bindvars) );
@@ -3793,26 +4132,8 @@
   }
 
-  if ($self->{lowercase}) {
-    if ($typemap{$$rectype} ne 'TXT') {
-      $$host = lc($$host);
-      $$val = lc($$val);
-    } else {
-      # TXT records should preserve user entry in the string.
-      if ($revrec eq 'n') {
-        $$host = lc($$host);
-      } else {
-        $$val = lc($$val);
-      }
-    }
-  }
+  _caseclean($rectype, $host, $val, $defrec, $revrec) if $self->{lowercase};
 
   # prep for validation
-  # Autodetect formal .arpa names
-  if ($$val =~ /\.arpa\.?$/) {
-    my ($code,$tmpval) = _zone2cidr($$val);
-    return ('FAIL', $tmpval) if $code eq 'FAIL';
-    $$val = $tmpval;
-  }
-  my $addr = NetAddr::IP->new($$val);
+  my $addr = NetAddr::IP->new($$val) if _maybeip($val);
   $$host =~ s/\.+$//;	# FQDNs ending in . are an internal detail, and really shouldn't be exposed in the UI.
 
@@ -3825,7 +4146,4 @@
   # do simple validation first
   return ('FAIL', "TTL must be numeric") unless $ttl =~ /^-?\d+$/;
-
-  # Quick check on hostname parts.  There are enough variations to justify a sub now.
-  return ('FAIL', $errstr) if ! _check_hostname_form($$host, $$rectype, $defrec, $revrec);
 
   # Collect these even if we're only doing a simple A record so we can call *any* validation sub
@@ -3879,6 +4197,6 @@
 	if $defrec eq 'n';
   $logdata{entry} = "Added ".($defrec eq 'y' ? 'default record' : 'record');
-  # NS records for revzones get special treatment
-  if ($revrec eq 'y' && $$rectype == 2) {
+  # Log reverse records to match the formal .arpa tree
+  if ($revrec eq 'y') {
     $logdata{entry} .= " '$$val $typemap{$$rectype} $$host";
   } else {
@@ -3891,5 +4209,5 @@
   $logdata{entry} .= "', TTL $ttl";
   $logdata{entry} .= ", location ".$self->getLoc($location)->{description} if $location;
-  $logdata{entry} .= ($expires eq 'after' ? ', valid after ' : ', expires at ').$stamp if $stamp;
+  $logdata{entry} .= ($expires ? ', expires at ' : ', valid after ').$stamp if $stamp;
 
   # Allow transactions, and raise an exception on errors so we can catch it later.
@@ -3963,26 +4281,8 @@
   }
 
-  if ($self->{lowercase}) {
-    if ($typemap{$$rectype} ne 'TXT') {
-      $$host = lc($$host);
-      $$val = lc($$val);
-    } else {
-      # TXT records should preserve user entry in the string.
-      if ($revrec eq 'n') {
-        $$host = lc($$host);
-      } else {
-        $$val = lc($$val);
-      }
-    }
-  }
+  _caseclean($rectype, $host, $val, $defrec, $revrec) if $self->{lowercase};
 
   # prep for validation
-  # Autodetect formal .arpa names
-  if ($$val =~ /\.arpa\.?$/) {
-    my ($code,$tmpval) = _zone2cidr($$val);
-    return ('FAIL', $tmpval) if $code eq 'FAIL';
-    $$val = $tmpval;
-  }
-  my $addr = NetAddr::IP->new($$val);
+  my $addr = NetAddr::IP->new($$val) if _maybeip($val);
   $$host =~ s/\.+$//;	# FQDNs ending in . are an internal detail, and really shouldn't be exposed in the UI.
 
@@ -3995,7 +4295,4 @@
   # do simple validation first
   return ('FAIL', "TTL must be numeric") unless $ttl =~ /^-?\d+$/;
-
-  # Quick check on hostname parts.  There are enough variations to justify a sub now.
-  return ('FAIL', $errstr) if ! _check_hostname_form($$host, $$rectype, $defrec, $revrec);
 
   # only MX and SRV will use these
@@ -4045,6 +4342,7 @@
   # need to forcibly make sure we disassociate a record with a parent it's no longer related to.
   # eg, PTR records may not have a domain parent, or A/AAAA records may not have a revzone parent.
-  # mainly needed for crossover types that got coerced down to "standard" types
-  if ($defrec eq 'n') {
+  # needed for crossover types that got coerced down to "standard" types due to data changes
+  # need to *avoid* funky records being updated like A/AAAA records in revzones, or PTRs in forward zones.
+  if ($defrec eq 'n' && $oldrec->{type} > 65000) {
     if ($$rectype == $reverse_typemap{PTR}) {
       $fields .= ",domain_id";
@@ -4057,9 +4355,10 @@
   }
   # fix fat-finger-originated record type changes
-  if ($$rectype == 65285) {
+  if ($$rectype == 65285) {  # delegation
     $fields .= ",rdns_id" if $revrec eq 'n';
     $fields .= ",domain_id" if $revrec eq 'y';
     push @vallist, 0;
   }
+  # ... and now make sure we *do* associate a record with the "calling" parent
   if ($defrec eq 'n') {
     $domid = $parid if $revrec eq 'n';
@@ -4082,6 +4381,6 @@
 	if $defrec eq 'n';
   $logdata{entry} = "Updated ".($defrec eq 'y' ? 'default record' : 'record')." from\n";
-  # NS records for revzones get special treatment
-  if ($revrec eq 'y' && $$rectype == 2) {
+  # Log reverse records "naturally", since they're stored, um, unnaturally.
+  if ($revrec eq 'y') {
     $logdata{entry} .= " '$oldrec->{val} $typemap{$oldrec->{type}} $oldrec->{host}";
   } else {
@@ -4096,6 +4395,6 @@
 	if $oldrec->{stampactive};
   $logdata{entry} .= "\nto\n";
-  # More NS special
-  if ($revrec eq 'y' && $$rectype == 2) {
+  # Log reverse records "naturally", since they're stored, um, unnaturally.
+  if ($revrec eq 'y') {
     $logdata{entry} .= "'$$val $typemap{$$rectype} $$host";
   } else {
@@ -4106,5 +4405,5 @@
   $logdata{entry} .= "', TTL $ttl";
   $logdata{entry} .= ", location ".$self->getLoc($location)->{description} if $location;
-  $logdata{entry} .= ($expires eq 'after' ? ', valid after ' : ', expires at ').$stamp if $stamp;
+  $logdata{entry} .= ($expires ? ', expires at ' : ', valid after ').$stamp if $stamp;
 
   local $dbh->{AutoCommit} = 0;
@@ -4338,6 +4637,6 @@
 ##fixme?  may need to narrow things down more by octet-chopping and doing text comparisons before casting.
   my ($revpatt) = $dbh->selectrow_array("SELECT host FROM records ".
-	"WHERE (type in (12,65280,65281,65282,65283,65284)) AND rdns_id = ? AND CAST (val AS inet) >>= ? ".
-	"ORDER BY CAST (val AS inet) DESC LIMIT 1", undef, ($revid, $cidr) );
+	"WHERE (type in (12,65280,65281,65282,65283,65284)) AND rdns_id = ? AND inetlazy(val) >>= ? ".
+	"ORDER BY inetlazy(val) DESC LIMIT 1", undef, ($revid, $cidr) );
   return $revpatt;
 } # end getRevPattern()
@@ -4375,7 +4674,20 @@
   $sth->execute;
   my @typelist;
+  # track whether the passed type is in the list at all.  allows you to edit a record
+  # that wouldn't otherwise be generally available in that zone (typically, reverse zones)
+  # without changing its type (accidentally or otherwise)
+  my $selflag = 0;
   while (my ($rval,$rname) = $sth->fetchrow_array()) {
     my %row = ( recval => $rval, recname => $rname );
-    $row{tselect} = 1 if $rval == $type;
+    if ($rval == $type) {
+      $row{tselect} = 1;
+      $selflag = 1;
+    }
+    push @typelist, \%row;
+  }
+
+  # add the passed type if it wasn't in the list
+  if (!$selflag) {
+    my %row = ( recval => $type, recname => $typemap{$type}, tselect => 1 );
     push @typelist, \%row;
   }
@@ -4644,5 +4956,5 @@
 # Handles sub-octet v4 zones in the format specified in the Cricket Book, 2nd Ed, p217-218
 
-  if ($zone =~ m{(?:\.arpa\.?|/\d+)$}) {
+  if ($zone =~ m{(?:\.arpa\.?|/\d+|^[\d.]+|^[a-fA-F0-9:]+)$}) {
     # we seem to have a reverse zone
     $rev = 'y';
@@ -4657,4 +4969,14 @@
       $cidr = NetAddr::IP->new($zone) or return ('FAIL',"$zone is not a valid CIDR block");
       $zone = _ZONE($cidr, 'ZONE.in-addr.arpa', 'r', '.');
+    } elsif ($zone =~ /^[\d.]+$/) {
+      # v4 revzone, leading-octet format
+      my $mask = 32;
+      while ($zone !~ /^\d+\.\d+\.\d+\.\d+$/) {
+        $zone .= '.0';
+        $mask -= 8;
+      }
+      $zone .= "/$mask";
+      $cidr = NetAddr::IP->new($zone) or return ('FAIL',"$zone is not a valid CIDR block");
+      $zone = _ZONE($cidr, 'ZONE.in-addr.arpa', 'r', '.');
     } elsif ($zone =~ m|^[a-fA-F\d:]+/\d+$|) {
       # v6 revzone, CIDR netblock
@@ -4662,8 +4984,22 @@
       return ('FAIL', "$zone is not a nibble-aligned block") if $cidr->masklen % 4 != 0;
       $zone = _ZONE($cidr, 'ZONE.ip6.arpa', 'r', '.');
+    } elsif ($zone =~ /^[a-fA-F\d:]+$/) {
+      # v6 revzone, leading-group format
+      $zone =~ s/::$//;
+      my $mask = 128;
+      while ($zone !~ /^(?:[a-fA-F\d]{1,4}:){7}[a-fA-F\d]$/) {
+        $zone .= ":0";
+        $mask -= 16;
+      }
+      $zone .= "/$mask";
+      $cidr = NetAddr::IP->new($zone) or return ('FAIL',"$zone is not a valid CIDR block");
+      $zone = _ZONE($cidr, 'ZONE.ip6.arpa', 'r', '.');
     } else {
       # there is. no. else!
-      return ('FAIL', "Unknown zone name format");
-    }
+      return ('FAIL', "Unknown zone name format '$zone'");
+    }
+
+    # several places this can be triggered from;  better to do it once.
+    $warnmsg .= "Apparent sub-/64 IPv6 reverse zone\n" if $cidr->masklen > 64;
 
     # quick check to start to see if we've already got one
@@ -4695,4 +5031,7 @@
       my $nc;
       foreach (@nibs) {
+#        # fail on multicharacter nibbles;  it's syntactically valid but no standard lookup
+#        # will ever reach it, because it doesn't directly represent a real IP address.
+#        return ('FAIL', "Invalid reverse v6 entry") if $_ !~ /^.$/;
         $rechost.= $_;
         $rechost .= ":" if ++$nc % 4 == 0 && $nc < 32;
@@ -4761,4 +5100,12 @@
     while (my $rr = $res->axfr_next()) {
 
+      # Discard out-of-zone records.  After trying for a while to replicate this with
+      # *nix-based DNS servers, it appears that only MS DNS is prone to including these
+      # in the AXFR data in the first place, and possibly only older versions at that...
+      # so it can't be reasonably tested.  Yay Microsoft.
+      if ($rr->name !~ /$zone$/i) {
+        $warnmsg .= "Discarding out-of-zone record ".$rr->string."\n";
+      }
+
       my $val;
       my $distance = 0;
@@ -4767,8 +5114,10 @@
       my $logfrag = '';
 
+      # Collect some record parts
       my $type = $rr->type;
       my $host = $rr->name;
       my $ttl = ($args{newttl} ? $args{newttl} : $rr->ttl);	# allow force-override TTLs
 
+      # Info flags for SOA and NS records
       $soaflag = 1 if $type eq 'SOA';
       $nsflag = 1 if $type eq 'NS';
@@ -4793,4 +5142,8 @@
 # processing depending on the record.  le sigh.
 
+# do the initial processing as if the record was in a forward zone.  If we're
+# doing a revzone, we can flip $host and $val as needed, once, after this
+# monster if-elsif-...-elsif-else.  This actually simplifies things a lot.
+
 ##fixme:  what record types other than TXT can/will have >255-byte payloads?
 
@@ -4798,41 +5151,14 @@
 	$val = $rr->address;
       } elsif ($type eq 'NS') {
-# hmm.  should we warn here if subdomain NS'es are left alone?
-	if ($rev eq 'y') {
-	  # revzones have records more or less reversed from forward zones.
-	  my ($tmpcode,$tmpmsg) = _zone2cidr($host);
-	  die "Error converting NS record: $tmpmsg\n" if $tmpcode eq 'FAIL';	# hmm.  may not make sense...
-	  next if ($args{rwns} && ($tmpmsg eq "$cidr"));
-	  $val = "$tmpmsg";
-	  $host = $rr->nsdname;
-	  $logfrag = "Added record '$val $type $host', TTL $ttl";
-# Tag and preserve.  For now this is commented for a no-op, but we have Ideas for
-# another custom storage type ("DELEGATE") that will use these subzone-delegation records
-#if ($val ne "$cidr") {
-#  push @{$suboct{$val}{ns}}, $host;
-#}
-	} else {
-	  next if ($args{rwns} && ($rr->name eq $zone));
-	  $val = $rr->nsdname;
-	}
+# hmm.  should we warn here if subdomain NS'es are left alone?  OTOH, those should rarely be rewritten anyway.
+        next if ($args{rwns} && ($host eq $zone));
+	$val = $rr->nsdname;
+        $warnmsg .= "Suspect record '".$rr->string."' may not be imported correctly: NS records may not be bare IP addresses\n"
+          if $val =~ /^(?:(?:\d+\.){3}\d+|[a-fA-F0-9:]+)$/;
 	$nsflag = 1;
       } elsif ($type eq 'CNAME') {
-	if ($rev eq 'y') {
-	  # hmm.  do we even want to bother with storing these at this level?  Sub-octet delegation
-	  # by CNAME is essentially a record-publication hack, and we want to just represent the
-	  # "true" logical intentions as far down the stack as we can from the UI.
-	  ($host,$val) = _revswap($host,$rr->cname);
-	  $logfrag = "Added record '$val $type $host', TTL $ttl";
-# Tag and preserve in case we want to commit them as-is later, but mostly we don't care.
-# Commented pending actually doing something with possibly new type DELEGATE
-#my $tmprev = $host;
-#$tmprev =~ s/^\d+\.//;
-#($code,$tmprev) = _zone2cidr($tmprev);
-#push @{$suboct{"$tmprev"}{cname}}, $val;
-	  # Silently skip CNAMEs in revzones.
-	  next;
-	} else {
-	  $val = $rr->cname;
-	}
+	$val = $rr->cname;
+        $warnmsg .= "Suspect record '".$rr->string."' may not be imported correctly: CNAME records may not be bare IP addresses\n"
+          if $val =~ /^(?:(?:\d+\.){3}\d+|[a-fA-F0-9:]+)$/;
       } elsif ($type eq 'SOA') {
 	next if $args{rwsoa};
@@ -4841,7 +5167,5 @@
 	$soaflag = 1;
       } elsif ($type eq 'PTR') {
-	($host,$val) = _revswap($host,$rr->ptrdname);
-	$logfrag = "Added record '$val $type $host', TTL $ttl";
-	# hmm.  PTR records should not be in forward zones.
+	$val = $rr->ptrdname;
       } elsif ($type eq 'MX') {
 	$val = $rr->exchange;
@@ -4856,10 +5180,5 @@
 # details as far down the stack as we can)
 # NB:  this may turn out to be more troublesome if we ever have need of >512-byte TXT records.
-	if ($rev eq 'y') {
-	  ($host,$val) = _revswap($host,$rr->txtdata);
-          $logfrag = "Added record '$val $type $host', TTL $ttl";
-	} else {
-	  $val = $rr->txtdata;
-	}
+	$val = $rr->txtdata;
       } elsif ($type eq 'SPF') {
 ##fixme: and the same caveat here, since it is apparently a clone of ::TXT
@@ -4872,4 +5191,6 @@
 	$weight = $rr->weight;
 	$port = $rr->port;
+        $warnmsg .= "Suspect record '".$rr->string."' may not be imported correctly: SRV records may not be bare IP addresses\n"
+          if $val =~ /^(?:(?:\d+\.){3}\d+|[a-fA-F0-9:]+)$/;
       } elsif ($type eq 'KEY') {
 	# we don't actually know what to do with these...
@@ -4881,4 +5202,55 @@
 	$warnmsg .= "Unusual record ".$rr->name." ($type) found\n";
       }
+
+      if ($rev eq 'y' && $type ne 'SOA') {
+        # up to this point we haven't meddled with the record's hostname part or rdata part.
+        # for reverse records, (except SOA) we must swap the two.
+        $host = $val;
+        $val = $rr->name;
+        my ($tmpcode,$tmpmsg) = _zone2cidr($val);
+        if ($tmpcode eq 'FAIL') {
+          # $val did not have a valid IP value.  It's syntactically valid but WTF?
+          $warnmsg .= "Suspect record '".$rr->string."' may not be imported correctly: $tmpmsg\n";
+        } else {
+          # $val has a valid IP value.  See if we can store it as that IP value.
+          # Note we're enumerating do-nothing cases for clarity.
+##enhance:  this is where we will implement the more subtle variations on #53
+          if ($type ne 'PTR' && $type ne 'NS' && $type ne 'CNAME' && $type ne 'TXT') {
+            # case: the record is "weird" - ie, not a PTR, NS, CNAME, or TXT
+            # $warnmsg .= "Discarding suspect record '".$rr->string."'\n" if $self->{strict} eq 'full';
+          } elsif ($type eq 'PTR' && $tmpmsg->masklen != 32 && $tmpmsg->masklen != 128) {
+            # case: PTR with netblock value, not IP value
+            # eg, "@ PTR foo" in zone f.e.e.b.d.a.e.d.ip6.arpa should not be
+            # stored/displayed as dead:beef::/32 PTR foo
+
+## hrm.  WTF is this case for, anyway?  Needs testing to check the logic.
+#          } elsif ( ($type eq 'PTR' || $type eq 'NS' || $type eq 'CNAME' || $type eq 'TXT') &&
+#                    ($tmpmsg->masklen != $cidr->masklen) 
+#                  ) {
+#            # leave $val as-is if the record is "normal" (a PTR, NS, CNAME, or TXT),
+#            # and the mask does not match the zone
+#$warnmsg .= "WTF case: $host $type $val\n";
+#            # $warnmsg .= "Discarding suspect record '".$rr->string."'\n" if $self->{strict} eq 'full';
+
+          } else {
+            $val = $tmpmsg;
+            $val =~ s/\/(?:32|128)$//;  # automagically converts $val back to a string before s///
+            #$val =~ s/:0$//g;
+          }
+        }
+        # magic?  convert * records to PTR template (not sure this actually makes sense)
+        #if ($val =~ /^\*/) {
+        #  $val =~ s/\*\.//;
+        #  ($tmpcode,$tmpmsg) = _zone2cidr($val);
+        #  if ($tmpcode eq 'FAIL') {
+        #    $val = "*.$val";
+        #    $warnmsg .= "Suspect record '".$rr->string."' may not be converted to PTR template correctly: $tmpmsg\n";
+        #  } else {
+        #    $type = 'PTR template';
+        #    $val = $tmpmsg; if $tmp
+        #    $val =~ s/\/(?:32|128)$//;  # automagically converts $val back to a string before s///
+        #  }
+        #}
+      } # non-SOA revrec $host/$val inversion and munging
 
       my $logentry = "[AXFR ".($rev eq 'n' ? $zone : $cidr)."] ";
@@ -4948,8 +5320,8 @@
 	$logentry .= $logfrag;
       } else {
-	$logentry .= "Added record '$host $type";
+	$logentry .= "Added record '".($rev eq 'y' ? $val : $host)." $type";
 	$logentry .= " [distance $distance]" if $type eq 'MX';
 	$logentry .= " [priority $distance] [weight $weight] [port $port]" if $type eq 'SRV';
-	$logentry .= " $val', TTL $ttl";
+	$logentry .= " ".($rev eq 'y' ? $host : $val)."', TTL $ttl";
       }
       $self->_log(group_id => $group, domain_id => $domain_id, rdns_id => $rdns_id, entry => $logentry);
@@ -5153,5 +5525,5 @@
   my $recsth = $dbh->prepare("SELECT host,type,val,distance,weight,port,ttl,record_id,location,extract(epoch from stamp),expires,stampactive ".
 	"FROM records WHERE rdns_id=? AND NOT type=6 ".
-	"ORDER BY masklen(CAST(val AS inet)) DESC, CAST(val AS inet)");
+	"ORDER BY masklen(inetlazy(val)) DESC, inetlazy(val)");
   my $revsth = $dbh->prepare("SELECT rdns_id,revnet,status,changed FROM revzones WHERE status=1 ".
 	"ORDER BY masklen(revnet) DESC, rdns_id");
@@ -5188,23 +5560,39 @@
         $soasth->execute($revid);
         my (@zsoa) = $soasth->fetchrow_array();
-        _printrec_tiny($zonefilehandle, $zsoa[7], 'y',\%recflags,$revzone,
+        $self->_printrec_tiny($zonefilehandle, $zsoa[7], 'y',\%recflags,$revzone,
           $zsoa[0],$zsoa[1],$zsoa[2],$zsoa[3],$zsoa[4],$zsoa[5],$zsoa[6],$zsoa[8],'');
 
         $recsth->execute($revid);
+        my $fullzone = _ZONE($tmpzone, 'ZONE', 'r', '.').($tmpzone->{isv6} ? '.ip6.arpa' : '.in-addr.arpa');
+
         while (my ($host, $type, $val, $dist, $weight, $port, $ttl, $recid, $loc, $stamp, $expires, $stampactive)
 		= $recsth->fetchrow_array) {
           next if $recflags{$recid};
 
-# not sure this is necessary for revzones.
-#	  # Spaces are evil.
-#	  $val =~ s/^\s+//;
-#	  $val =~ s/\s+$//;
-#	  if ($typemap{$type} ne 'TXT') {
-#	    # Leading or trailng spaces could be legit in TXT records.
-#	    $host =~ s/^\s+//;
-#	    $host =~ s/\s+$//;
-#	  }
-
-          _printrec_tiny($zonefilehandle, $recid, 'y', \%recflags, $revzone,
+          # Check for out-of-zone data
+          if ($val =~ /\.arpa$/) {
+            # val is non-IP
+            if ($val !~ /$fullzone$/) {
+              warn "Not exporting out-of-zone record $val $typemap{$type} $host, $ttl (zone $tmpzone)\n";
+              next;
+            }
+          } else {
+            my $ipval = new NetAddr::IP $val;
+            if (!$tmpzone->contains($ipval)) {
+              warn "Not exporting out-of-zone record $val $typemap{$type} $host, $ttl (zone $tmpzone)\n";
+              next;
+            }
+          } # is $val a raw .arpa name?
+
+	  # Spaces are evil.
+	  $val =~ s/^\s+//;
+	  $val =~ s/\s+$//;
+	  if ($typemap{$type} ne 'TXT') {
+	    # Leading or trailng spaces could be legit in TXT records.
+	    $host =~ s/^\s+//;
+	    $host =~ s/\s+$//;
+	  }
+
+          $self->_printrec_tiny($zonefilehandle, $recid, 'y', \%recflags, $revzone,
             $host, $type, $val, $dist, $weight, $port, $ttl, $loc, $stamp, $expires, $stampactive);
 
@@ -5283,5 +5671,5 @@
         $soasth->execute($domid);
         my (@zsoa) = $soasth->fetchrow_array();
-        _printrec_tiny($zonefilehandle, $zsoa[7], 'n',\%recflags,$dom,
+        $self->_printrec_tiny($zonefilehandle, $zsoa[7], 'n',\%recflags,$dom,
           $zsoa[0],$zsoa[1],$zsoa[2],$zsoa[3],$zsoa[4],$zsoa[5],$zsoa[6],$zsoa[8],'');
 
@@ -5289,4 +5677,10 @@
         while (my ($host,$type,$val,$dist,$weight,$port,$ttl,$recid,$loc,$stamp,$expires,$stampactive) = $recsth->fetchrow_array) {
 	  next if $recflags{$recid};
+
+          # Check for out-of-zone data
+          if ($host !~ /$dom$/) {
+            warn "Not exporting out-of-zone record $host $type $val, $ttl (zone $dom)\n";
+            next;
+          }
 
 	  # Spaces are evil.
@@ -5299,5 +5693,5 @@
 	  }
 
-	  _printrec_tiny($zonefilehandle, $recid, 'n', \%recflags,
+	  $self->_printrec_tiny($zonefilehandle, $recid, 'n', \%recflags,
 		$dom, $host, $type, $val, $dist, $weight, $port, $ttl, $loc, $stamp, $expires, $stampactive);
 
@@ -5348,4 +5742,5 @@
 # Utility sub for __export_tiny above
 sub _printrec_tiny {
+  my $self = shift;
   my ($datafile, $recid, $revrec, $recflags, $zone, $host, $type, $val, $dist, $weight, $port, $ttl,
 	$loc, $stamp, $expires, $stampactive) = @_;
@@ -5433,4 +5828,15 @@
   }
 
+  # Utility sub-sub for reverse records;  with "any-record-in-any-zone"
+  # we may need to do extra processing on $val to make it publishable.
+  sub __revswap {
+    my $host = shift;
+    my $val = shift;
+    return ($val, $host) if $val =~ /\.arpa/;
+    $val = new NetAddr::IP $val;
+    my $newval = _ZONE($val, 'ZONE', 'r', '.').($val->{isv6} ? '.ip6.arpa' : '.in-addr.arpa');
+    return ($newval, $host);
+  }
+
 ## WARNING:  This works to export even the whole Internet's worth of IP space...
 ##  if you have the disk/RAM to handle the dataset, and you call this sub based on /16-sized chunks
@@ -5441,4 +5847,5 @@
 ##  forked process
   sub __publish_subnet {
+    my $obj = shift;	# *sigh*  need to pass in the DNSDB object so we can read a couple of options
     my $sub = shift;
     my $recflags = shift;
@@ -5448,5 +5855,9 @@
     my $stamp = shift;
     my $loc = shift;
+    my $zone = new NetAddr::IP shift;
     my $ptronly = shift || 0;
+
+    # do this conversion once, not (number-of-ips-in-subnet) times
+    my $arpabase = _ZONE($zone, 'ZONE.in-addr.arpa', 'r', '.');
 
     my $iplist = $sub->splitref(32);
@@ -5454,116 +5865,126 @@
       my $ip = $_->addr;
       # make as if we split the non-octet-aligned block into octet-aligned blocks as with SOA
-      next if $ip =~ /\.(0|255)$/;
-      next if $$recflags{$ip};
+      my $lastoct = (split /\./, $ip)[3];
+      next if $ip =~ /\.0$/ && $obj->{template_skip_0};
+      next if $ip =~ /\.255$/ && $obj->{template_skip_255};
+      next if $$recflags{$ip}; # && $self->{skip_bcast_255}
       $$recflags{$ip}++;
       next if $hpat eq '%blank%';	# Allows blanking a subnet so no records are published.
       my $rec = $hpat;	# start fresh with the template for each IP
+##fixme:  there really isn't a good way to handle sub-/24 zones here.  This way at least
+# seems less bad than some alternatives.  
       _template4_expand(\$rec, $ip);
-      print $fh ($ptronly ? "^"._ZONE($_, 'ZONE.in-addr.arpa', 'r', '.').":$rec" : "=$rec:$ip").
-	":$ttl:$stamp:$loc\n" or die $!;
-    }
-  }
+      if ($ptronly || $zone->masklen > 24) {
+        print $fh "^$lastoct.$arpabase:$rec:$ttl:$stamp:$loc\n" or die $!;
+        if (!$ptronly) {
+          # print a separate A record.  Arguably we could use an = record here instead.
+          print $fh "+$rec:$ip:$ttl:$stamp:$loc\n" or die $!;
+        }
+      } else {
+        print $fh "=$rec:$ip:$ttl:$stamp:$loc\n" or die $!;
+      }
+    }
+  }
+
+## And now the meat.
 
 ##fixme?  append . to all host/val hostnames
-      if ($typemap{$type} eq 'SOA') {
-
-	# host contains pri-ns:responsible
-	# val is abused to contain refresh:retry:expire:minttl
+#print "debug: rawdata: $host $typemap{$type} $val\n";
+
+  if ($typemap{$type} eq 'SOA') {
+    # host contains pri-ns:responsible
+    # val is abused to contain refresh:retry:expire:minttl
 ##fixme:  "manual" serial vs tinydns-autoserial
-	# let's be explicit about abusing $host and $val
-	my ($email, $primary) = (split /:/, $host)[0,1];
-	my ($refresh, $retry, $expire, $min_ttl) = (split /:/, $val)[0,1,2,3];
-	if ($revrec eq 'y') {
+    # let's be explicit about abusing $host and $val
+    my ($email, $primary) = (split /:/, $host)[0,1];
+    my ($refresh, $retry, $expire, $min_ttl) = (split /:/, $val)[0,1,2,3];
+    if ($revrec eq 'y') {
 ##fixme:  have to publish SOA records for each v4 /24 in sub-/16, and each /16 in sub-/8
 # what about v6?
 # -> only need SOA for local chunks offset from reverse delegation boundaries, so v6 is fine
-	  $zone = NetAddr::IP->new($zone);
-	  # handle split-n-multiply SOA for off-octet (8 < mask < 16) or (16 < mask < 24) v4 zones
-	  if (!$zone->{isv6} && ($zone->masklen < 24) && ($zone->masklen % 8 != 0)) {
-	    foreach my $szone ($zone->split($zone->masklen + (8 - $zone->masklen % 8))) {
-	      $szone = _ZONE($szone, 'ZONE.in-addr.arpa', 'r', '.');
-	      print $datafile "Z$szone:$primary:$email"."::$refresh:$retry:$expire:$min_ttl:$ttl:$stamp:$loc\n"
-		or die $!;
-	    }
-	    return; # skips "default" bits just below
-	  }
-	  $zone = _ZONE($zone, 'ZONE', 'r', '.').($zone->{isv6} ? '.ip6.arpa' : '.in-addr.arpa');
-	}
-	print $datafile "Z$zone:$primary:$email"."::$refresh:$retry:$expire:$min_ttl:$ttl:$stamp:$loc\n"
-		or die $!;
-
-      } elsif ($typemap{$type} eq 'A') {
-
-	print $datafile "+$host:$val:$ttl:$stamp:$loc\n" or die $!;
-
-      } elsif ($typemap{$type} eq 'NS') {
-
-	if ($revrec eq 'y') {
-	  $val = NetAddr::IP->new($val);
-	  # handle split-n-multiply SOA for off-octet (8 < mask < 16) or (16 < mask < 24) v4 zones
-	  if (!$val->{isv6} && ($val->masklen < 24) && ($val->masklen % 8 != 0)) {
-	    foreach my $szone ($val->split($val->masklen + (8 - $val->masklen % 8))) {
-	      my $szone2 = _ZONE($szone, 'ZONE.in-addr.arpa', 'r', '.');
-	      next if $$recflags{$szone2} && $$recflags{$szone2} > $val->masklen;
-	      print $datafile "\&$szone2"."::$host:$ttl:$stamp:$loc\n" or die $!;
-	      $$recflags{$szone2} = $val->masklen;
-	    }
-	  } elsif ($val->{isv6} && ($val->masklen < 64) && ($val->masklen % 4 !=0)) {
-	    foreach my $szone ($val->split($val->masklen + (4 - $val->masklen % 4))) {
-	      my $szone2 = _ZONE($szone, 'ZONE.ip6.arpa', 'r', '.');
-	      next if $$recflags{$szone2} && $$recflags{$szone2} > $val->masklen;
-	      print $datafile "\&$szone2"."::$host:$ttl:$stamp:$loc\n" or die $!;
-	      $$recflags{$szone2} = $val->masklen;
-	    }
-	  } else {
-	    my $val2 = _ZONE($val, 'ZONE', 'r', '.').($val->{isv6} ? '.ip6.arpa' : '.in-addr.arpa');
-	    print $datafile "\&$val2"."::$host:$ttl:$stamp:$loc\n" or die $!;
-	    $$recflags{$val2} = $val->masklen;
-	  }
-	} else {
-	  print $datafile "\&$host"."::$val:$ttl:$stamp:$loc\n" or die $!;
-	}
-
-      } elsif ($typemap{$type} eq 'AAAA') {
-
-#	print $datafile ":$host:28:";
-	my $altgrp = 0;
-	my @altconv;
-	# Split in to up to 8 groups of hex digits (allows for IPv6 :: 0-collapsing)
-	foreach (split /:/, $val) {
-	  if (/^$/) {
-	    # flag blank entry;  this is a series of 0's of (currently) unknown length
-	    $altconv[$altgrp++] = 's';
-	  } else {
-	    # call sub to convert 1-4 hex digits to 2 string-rep octal bytes
-	    $altconv[$altgrp++] = octalize($_)
-	  }
-	}
-        my $prefix = ":$host:28:";
-	foreach my $octet (@altconv) {
-	  # if not 's', output
-	  $prefix .= $octet unless $octet =~ /^s$/;
-	  # if 's', output (9-array length)x literal '\000\000'
-	  $prefix .= '\000\000'x(9-$altgrp) if $octet =~ /^s$/;
-	}
-	print $datafile "$prefix:$ttl:$stamp:$loc\n" or die $!;
-
-      } elsif ($typemap{$type} eq 'MX') {
-
-##fixme:  what if we get an MX AXFRed into a reverse zone?
-	print $datafile "\@$host"."::$val:$dist:$ttl:$stamp:$loc\n" or die $!;
-
-      } elsif ($typemap{$type} eq 'TXT') {
-
+# anyone who says they need sub-nibble v6 delegations, at this time, needs their head examined.
+      $zone = NetAddr::IP->new($zone);
+      # handle split-n-multiply SOA for off-octet (8 < mask < 16) or (16 < mask < 24) v4 zones
+      if (!$zone->{isv6} && ($zone->masklen < 24) && ($zone->masklen % 8 != 0)) {
+        foreach my $szone ($zone->split($zone->masklen + (8 - $zone->masklen % 8))) {
+          $szone = _ZONE($szone, 'ZONE.in-addr.arpa', 'r', '.');
+          print $datafile "Z$szone:$primary:$email"."::$refresh:$retry:$expire:$min_ttl:$ttl:$stamp:$loc\n"
+            or die $!;
+        }
+        return; # skips "default" bits just below
+      }
+      $zone = _ZONE($zone, 'ZONE', 'r', '.').($zone->{isv6} ? '.ip6.arpa' : '.in-addr.arpa');
+    }
+    print $datafile "Z$zone:$primary:$email"."::$refresh:$retry:$expire:$min_ttl:$ttl:$stamp:$loc\n"
+      or die $!;
+
+  } elsif ($typemap{$type} eq 'A') {
+
+    ($host,$val) = __revswap($host,$val) if $revrec eq 'y';
+    print $datafile "+$host:$val:$ttl:$stamp:$loc\n" or die $!;
+
+  } elsif ($typemap{$type} eq 'NS') {
+
+    if ($revrec eq 'y') {
+      $val = NetAddr::IP->new($val);
+      # handle split-n-multiply SOA for off-octet (8 < mask < 16) or (16 < mask < 24) v4 zones
+      if (!$val->{isv6} && ($val->masklen < 24) && ($val->masklen % 8 != 0)) {
+        foreach my $szone ($val->split($val->masklen + (8 - $val->masklen % 8))) {
+          my $szone2 = _ZONE($szone, 'ZONE.in-addr.arpa', 'r', '.');
+          next if $$recflags{$szone2} && $$recflags{$szone2} > $val->masklen;
+          print $datafile "\&$szone2"."::$host:$ttl:$stamp:$loc\n" or die $!;
+          $$recflags{$szone2} = $val->masklen;
+        }
+      } elsif ($val->{isv6} && ($val->masklen < 64) && ($val->masklen % 4 !=0)) {
+        foreach my $szone ($val->split($val->masklen + (4 - $val->masklen % 4))) {
+          my $szone2 = _ZONE($szone, 'ZONE.ip6.arpa', 'r', '.');
+          next if $$recflags{$szone2} && $$recflags{$szone2} > $val->masklen;
+          print $datafile "\&$szone2"."::$host:$ttl:$stamp:$loc\n" or die $!;
+          $$recflags{$szone2} = $val->masklen;
+        }
+      } else {
+        my $val2 = _ZONE($val, 'ZONE', 'r', '.').($val->{isv6} ? '.ip6.arpa' : '.in-addr.arpa');
+        print $datafile "\&$val2"."::$host:$ttl:$stamp:$loc\n" or die $!;
+        $$recflags{$val2} = $val->masklen;
+      }
+    } else {
+      print $datafile "\&$host"."::$val:$ttl:$stamp:$loc\n" or die $!;
+    }
+
+  } elsif ($typemap{$type} eq 'AAAA') {
+
+    ($host,$val) = __revswap($host,$val) if $revrec eq 'y';
+    my $altgrp = 0;
+    my @altconv;
+    # Split in to up to 8 groups of hex digits (allows for IPv6 :: 0-collapsing)
+    foreach (split /:/, $val) {
+      if (/^$/) {
+        # flag blank entry;  this is a series of 0's of (currently) unknown length
+        $altconv[$altgrp++] = 's';
+      } else {
+        # call sub to convert 1-4 hex digits to 2 string-rep octal bytes
+        $altconv[$altgrp++] = octalize($_)
+      }
+    }
+    my $prefix = ":$host:28:";
+    foreach my $octet (@altconv) {
+      # if not 's', output
+      $prefix .= $octet unless $octet =~ /^s$/;
+      # if 's', output (9-array length)x literal '\000\000'
+      $prefix .= '\000\000'x(9-$altgrp) if $octet =~ /^s$/;
+    }
+    print $datafile "$prefix:$ttl:$stamp:$loc\n" or die $!;
+
+  } elsif ($typemap{$type} eq 'MX') {
+
+    ($host,$val) = __revswap($host,$val) if $revrec eq 'y';
+    print $datafile "\@$host"."::$val:$dist:$ttl:$stamp:$loc\n" or die $!;
+
+  } elsif ($typemap{$type} eq 'TXT') {
+
+    ($host,$val) = __revswap($host,$val) if $revrec eq 'y';
 ##fixme:  split v-e-r-y long TXT strings?  will need to do so for BIND export, at least
-        if ($revrec eq 'n') {
-	  $val =~ s/:/\\072/g;	# may need to replace other symbols
-	  print $datafile "'$host:$val:$ttl:$stamp:$loc\n" or die $!;
-	} else {
-	  $host =~ s/:/\\072/g;	# may need to replace other symbols
-	  my $val2 = NetAddr::IP->new($val);
-	  print $datafile "'"._ZONE($val2, 'ZONE', 'r', '.').($val2->{isv6} ? '.ip6.arpa' : '.in-addr.arpa').
-		":$host:$ttl:$stamp:$loc\n" or die $!;
-	}
+    $val =~ s/:/\\072/g;	# may need to replace other symbols
+    print $datafile "'$host:$val:$ttl:$stamp:$loc\n" or die $!;
 
 # by-hand TXT
@@ -5584,132 +6005,148 @@
 #:3600
 
-      } elsif ($typemap{$type} eq 'CNAME') {
-
-	if ($revrec eq 'n') {
-	  print $datafile "C$host:$val:$ttl:$stamp:$loc\n" or die $!;
-	} else {
-	  my $val2 = NetAddr::IP->new($val);
-	  print $datafile "C"._ZONE($val2, 'ZONE', 'r', '.').($val2->{isv6} ? '.ip6.arpa' : '.in-addr.arpa').
-		":$host:$ttl:$stamp:$loc\n" or die $!;
-	}
-
-      } elsif ($typemap{$type} eq 'SRV') {
-
-	# data is two-byte values for priority, weight, port, in that order,
-	# followed by length/string data
-
-	print $datafile ":$host:33:".octalize($dist,'d').octalize($weight,'d').octalize($port,'d') or die $!;
-
-	$val .= '.' if $val !~ /\.$/;
-	foreach (split /\./, $val) {
-	  printf $datafile "\\%0.3o%s", length($_), $_ or die $!;
-	}
-	print $datafile "\\000:$ttl:$stamp:$loc\n" or die $!;
-
-      } elsif ($typemap{$type} eq 'RP') {
-
-	# RP consists of two mostly free-form strings.
-	# The first is supposed to be an email address with @ replaced by . (as with the SOA contact)
-	# The second is the "hostname" of a TXT record with more info.
-	my $prefix = ":$host:17:";
-	my ($who,$what) = split /\s/, $val;
-	foreach (split /\./, $who) {
-	  $prefix .= sprintf "\\%0.3o%s", length($_), $_;
-	}
-	$prefix .= '\000';
-	foreach (split /\./, $what) {
-	  $prefix .= sprintf "\\%0.3o%s", length($_), $_;
-	}
-	print $datafile "$prefix\\000:$ttl:$stamp:$loc\n" or die $!;
-
-      } elsif ($typemap{$type} eq 'PTR') {
-
-	$zone = NetAddr::IP->new($zone);
-	$$recflags{$val}++;
-	if (!$zone->{isv6} && $zone->masklen > 24) {
-	  ($val) = ($val =~ /\.(\d+)$/);
-	  print $datafile "^$val."._ZONE($zone, 'ZONE', 'r', '.').'.in-addr.arpa'.
-		":$host:ttl:$stamp:$loc\n" or die $!;
-	} else {
-	  $val = NetAddr::IP->new($val);
-	  print $datafile "^".
-		_ZONE($val, 'ZONE', 'r', '.').($val->{isv6} ? '.ip6.arpa' : '.in-addr.arpa').
-		":$host:$ttl:$stamp:$loc\n" or die $!;
-	}
-
-      } elsif ($type == 65280) { # A+PTR
-
-	$$recflags{$val}++;
-	print $datafile "=$host:$val:$ttl:$stamp:$loc\n" or die $!;
-
-      } elsif ($type == 65281) { # AAAA+PTR
-
-	$$recflags{$val}++;
-	# treat these as two separate records.  since tinydns doesn't have
-	# a native combined type, we have to create them separately anyway.
-	# print both;  a dangling record is harmless, and impossible via web
-	# UI anyway
-	_printrec_tiny($datafile,$revrec,$recflags,$zone,$host,28,$val,$dist,$weight,$port,$ttl,$loc,$stamp);
-	_printrec_tiny($datafile,$revrec,$recflags,$zone,$host,12,$val,$dist,$weight,$port,$ttl,$loc,$stamp);
+  } elsif ($typemap{$type} eq 'CNAME') {
+
+    ($host,$val) = __revswap($host,$val) if $revrec eq 'y';
+    print $datafile "C$host:$val:$ttl:$stamp:$loc\n" or die $!;
+
+  } elsif ($typemap{$type} eq 'SRV') {
+
+    ($host,$val) = __revswap($host,$val) if $revrec eq 'y';
+
+    # data is two-byte values for priority, weight, port, in that order,
+    # followed by length/string data
+
+    my $prefix = ":$host:33:".octalize($dist,'d').octalize($weight,'d').octalize($port,'d');
+
+    $val .= '.' if $val !~ /\.$/;
+    foreach (split /\./, $val) {
+      $prefix .= sprintf "\\%0.3o%s", length($_), $_ or die $!;
+    }
+    print $datafile "$prefix\\000:$ttl:$stamp:$loc\n" or die $!;
+
+  } elsif ($typemap{$type} eq 'RP') {
+
+    ($host,$val) = __revswap($host,$val) if $revrec eq 'y';
+    # RP consists of two mostly free-form strings.
+    # The first is supposed to be an email address with @ replaced by . (as with the SOA contact)
+    # The second is the "hostname" of a TXT record with more info.
+    my $prefix = ":$host:17:";
+    my ($who,$what) = split /\s/, $val;
+    foreach (split /\./, $who) {
+      $prefix .= sprintf "\\%0.3o%s", length($_), $_;
+    }
+    $prefix .= '\000';
+    foreach (split /\./, $what) {
+      $prefix .= sprintf "\\%0.3o%s", length($_), $_;
+    }
+    print $datafile "$prefix\\000:$ttl:$stamp:$loc\n" or die $!;
+
+  } elsif ($typemap{$type} eq 'PTR') {
+
+    $$recflags{$val}++;
+    if ($revrec eq 'y') {
+
+      if ($val =~ /\.arpa$/) {
+        # someone put in the formal .arpa name.  humor them.
+        print $datafile "^$val:$host:$ttl:$stamp:$loc\n" or die $!;
+      } else {
+        $zone = NetAddr::IP->new($zone);
+        if (!$zone->{isv6} && $zone->masklen > 24) {
+          # sub-octet v4 zone
+          ($val) = ($val =~ /\.(\d+)$/);
+          print $datafile "^$val."._ZONE($zone, 'ZONE', 'r', '.').'.in-addr.arpa'.
+            ":$host:$ttl:$stamp:$loc\n" or die $!;
+        } else {
+          # not going to care about strange results if $val is not an IP value and is resolveable in DNS
+          $val = NetAddr::IP->new($val);
+          print $datafile "^".
+            _ZONE($val, 'ZONE', 'r', '.').($val->{isv6} ? '.ip6.arpa' : '.in-addr.arpa').
+            ":$host:$ttl:$stamp:$loc\n" or die $!;
+        }
+      } # non-".arpa" $val
+
+    } else {
+      # PTRs in forward zones are less bizarre and insane than some other record types
+      # in reverse zones...  OTOH we can't validate them any which way, so we cross our
+      # fingers and close our eyes and make it Someone Else's Problem.
+      print $datafile "^$host:$val:$ttl:$stamp:$loc\n" or die $!;
+    }
+
+  } elsif ($type == 65280) { # A+PTR
+
+    $$recflags{$val}++;
+    print $datafile "=$host:$val:$ttl:$stamp:$loc\n" or die $!;
+
+  } elsif ($type == 65281) { # AAAA+PTR
+
+    $$recflags{$val}++;
+    # treat these as two separate records.  since tinydns doesn't have
+    # a native combined type, we have to create them separately anyway.
+    # print both;  a dangling record is harmless, and impossible via web
+    # UI anyway
+    $self->_printrec_tiny($datafile,$revrec,$recflags,$zone,$host,28,$val,$dist,$weight,$port,$ttl,$loc,$stamp);
+    $self->_printrec_tiny($datafile,$revrec,$recflags,$zone,$host,12,$val,$dist,$weight,$port,$ttl,$loc,$stamp);
 ##fixme: add a config flag to indicate use of the patch from http://www.fefe.de/dns/
 # type 6 is for AAAA+PTR, type 3 is for AAAA
 
-      } elsif ($type == 65282) { # PTR template
-
-	# only useful for v4 with standard DNS software, since this expands all
-	# IPs in $zone (or possibly $val?) with autogenerated records
-	$val = NetAddr::IP->new($val);
-	return if $val->{isv6};
-
-	if ($val->masklen <= 16) {
-	  foreach my $sub ($val->split(16)) {
-	    __publish_subnet($sub, $recflags, $host, $datafile, $ttl, $stamp, $loc, 1);
-	  }
-	} else {
-	  __publish_subnet($val, $recflags, $host, $datafile, $ttl, $stamp, $loc, 1);
-	}
-
-      } elsif ($type == 65283) { # A+PTR template
-
-	$val = NetAddr::IP->new($val);
-	# Just In Case.  An A+PTR should be impossible to add to a v6 revzone via API.
-	return if $val->{isv6};
-
-	if ($val->masklen <= 16) {
-	  foreach my $sub ($val->split(16)) {
-	    __publish_subnet($sub, $recflags, $host, $datafile, $ttl, $stamp, $loc, 0);
-	  }
-	} else {
-	  __publish_subnet($val, $recflags, $host, $datafile, $ttl, $stamp, $loc, 0);
-	}
-
-      } elsif ($type == 65284) { # AAAA+PTR template
-	# Stub for completeness.  Could be exported to DNS software that supports
-	# some degree of internal automagic in generic-record-creation
-	# (eg http://search.cpan.org/dist/AllKnowingDNS/ )
-
-      } elsif ($type == 65285) { # Delegation
-	# This is intended for reverse zones, but may prove useful in forward zones.
-
-	# All delegations need to create one or more NS records.  The NS record handler knows what to do.
-	_printrec_tiny($datafile,$revrec,$recflags,$zone,$host,$reverse_typemap{'NS'},
-		$val,$dist,$weight,$port,$ttl,$loc,$stamp);
-	if ($revrec eq 'y') {
-	  # In the case of a sub-/24 v4 reverse delegation, we need to generate CNAMEs
-	  # to redirect all of the individual IP lookups as well.
-	  # Not sure how this would actually resolve if a /24 or larger was delegated
-	  # one way, and a sub-/24 in that >=/24 was delegated elsewhere...
-	  my $dblock = NetAddr::IP->new($val);
-	  if (!$dblock->{isv6} && $dblock->masklen > 24) {
-	    my @subs = $dblock->split;
-	    foreach (@subs) {
-	      next if $$recflags{"$_"};
-	      my ($oct) = ($_->addr =~ /(\d+)$/);
-	      print $datafile "C"._ZONE($_, 'ZONE.in-addr.arpa', 'r', '.').":$oct.".
-		_ZONE($dblock, 'ZONE.in-addr.arpa', 'r', '.').":$ttl:$stamp:$loc\n" or die $!;
-	      $$recflags{"$_"}++;
-	    }
-	  }
-	}
+  } elsif ($type == 65282) { # PTR template
+
+    # only useful for v4 with standard DNS software, since this expands all
+    # IPs in $zone (or possibly $val?) with autogenerated records
+    $val = NetAddr::IP->new($val);
+    return if $val->{isv6};
+
+    if ($val->masklen <= 16) {
+      foreach my $sub ($val->split(16)) {
+        $self->__publish_subnet($sub, $recflags, $host, $datafile, $ttl, $stamp, $loc, $zone, 1);
+      }
+    } else {
+      $self->__publish_subnet($val, $recflags, $host, $datafile, $ttl, $stamp, $loc, $zone, 1);
+    }
+
+  } elsif ($type == 65283) { # A+PTR template
+
+    $val = NetAddr::IP->new($val);
+    # Just In Case.  An A+PTR should be impossible to add to a v6 revzone via API.
+    return if $val->{isv6};
+
+    if ($val->masklen <= 16) {
+      foreach my $sub ($val->split(16)) {
+        $self->__publish_subnet($sub, $recflags, $host, $datafile, $ttl, $stamp, $loc, $zone, 0);
+      }
+    } else {
+      $self->__publish_subnet($val, $recflags, $host, $datafile, $ttl, $stamp, $loc, $zone, 0);
+    }
+
+  } elsif ($type == 65284) { # AAAA+PTR template
+    # Stub for completeness.  Could be exported to DNS software that supports
+    # some degree of internal automagic in generic-record-creation
+    # (eg http://search.cpan.org/dist/AllKnowingDNS/ )
+
+  } elsif ($type == 65285) { # Delegation
+    # This is intended for reverse zones, but may prove useful in forward zones.
+
+    # All delegations need to create one or more NS records.  The NS record handler knows what to do.
+    $self->_printrec_tiny($datafile,$recid,$revrec,$recflags,$zone,$host,$reverse_typemap{'NS'},
+      $val,$dist,$weight,$port,$ttl,$loc,$stamp);
+    if ($revrec eq 'y') {
+      # In the case of a sub-/24 v4 reverse delegation, we need to generate CNAMEs
+      # to redirect all of the individual IP lookups as well.
+      # OR
+      # create NS records for each IP
+      # Not sure how this would actually resolve if a /24 or larger was delegated
+      # one way, and a sub-/24 in that >=/24 was delegated elsewhere...
+      my $dblock = NetAddr::IP->new($val);
+      if (!$dblock->{isv6} && $dblock->masklen > 24) {
+        my @subs = $dblock->split;
+        foreach (@subs) {
+          next if $$recflags{"$_"};
+          my ($oct) = ($_->addr =~ /(\d+)$/);
+          print $datafile "C"._ZONE($_, 'ZONE.in-addr.arpa', 'r', '.').":$oct.".
+            _ZONE($dblock, 'ZONE.in-addr.arpa', 'r', '.').":$ttl:$stamp:$loc\n" or die $!;
+            $$recflags{"$_"}++;
+        }
+      }
+    }
 
 ##
@@ -5717,26 +6154,29 @@
 ##
 
-      } elsif ($type == 44) { # SSHFP
-	my ($algo,$fpt,$fp) = split /\s+/, $val;
-
-	my $rec = sprintf ":$host:44:\\%0.3o\\%0.3o", $algo, $fpt;
-	while (my ($byte) = ($fp =~ /^(..)/) ) {
-	  $rec .= sprintf "\\%0.3o", hex($byte);
-	  $fp =~ s/^..//;
-	}
-	print $datafile "$rec:$ttl:$stamp:$loc\n" or die $!;
-
-      } else {
-	# raw record.  we don't know what's in here, so we ASS-U-ME the user has
-	# put it in correctly, since either the user is messing directly with the
-	# database, or the record was imported via AXFR
-	# <split by char>
-	# convert anything not a-zA-Z0-9.- to octal coding
+  } elsif ($type == 44) { # SSHFP
+
+    ($host,$val) = __revswap($host,$val) if $revrec eq 'y';
+
+    my ($algo,$fpt,$fp) = split /\s+/, $val;
+
+    my $rec = sprintf ":$host:44:\\%0.3o\\%0.3o", $algo, $fpt;
+    while (my ($byte) = ($fp =~ /^(..)/) ) {
+      $rec .= sprintf "\\%0.3o", hex($byte);
+      $fp =~ s/^..//;
+    }
+    print $datafile "$rec:$ttl:$stamp:$loc\n" or die $!;
+
+  } else {
+    # raw record.  we don't know what's in here, so we ASS-U-ME the user has
+    # put it in correctly, since either the user is messing directly with the
+    # database, or the record was imported via AXFR
+    # <split by char>
+    # convert anything not a-zA-Z0-9.- to octal coding
 
 ##fixme: add flag to export "unknown" record types - note we'll probably end up
 # mangling them since they were written to the DB from Net::DNS::RR::<type>->rdatastr.
-	#print $datafile ":$host:$type:$val:$ttl:$stamp:$loc\n";
-
-      } # record type if-else
+    #print $datafile ":$host:$type:$val:$ttl:$stamp:$loc\n";
+
+  } # record type if-else
 
 } # end _printrec_tiny()
