Changeset 1051 for trunk/DNSDB.pm


Ignore:
Timestamp:
03/06/26 11:33:19 (3 days ago)
Author:
Kris Deugau
Message:

/trunk

Reintegrate /branches/cname-collision
See #72

Location:
trunk
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • trunk

  • trunk/DNSDB.pm

    r1036 r1051  
    3333use Fcntl qw(:flock);
    3434use Time::TAI64 qw(:tai64);
     35use Date::Parse;
    3536
    3637use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
     
    222223                force_refresh   => 1,
    223224                lowercase       => 0,   # mangle as little as possible by default
     225                # tinker with timestamps if adding or updating a record would
     226                # cause overlapping CNAME and other in some way
     227                coerce_cname_timestamp => 'none',
    224228                # show IPs and CIDR blocks as-is for reverse zones.  valid values are
    225229                # 'none' (default, show natural IP or CIDR)
     
    283287    warn "Bad showrev_arpa setting $self->{showrev_arpa}, using default\n";
    284288    $self->{showrev_arpa} = 'none';
     289  }
     290  if (!grep /$self->{coerce_cname_timestamp}/, ('none','adjust','full')) {
     291    warn "Bad coerce_cname_timestamp setting $self->{coerce_cname_timestamp}, using default\n";
     292    $self->{coerce_cname_timestamp} = 'none';
    285293  }
    286294
     
    612620##
    613621
     622# Check for name collisions relating to CNAMEs.  Needs to be called from all other validators.
     623sub _cname_collision {
     624  my $self = shift;
     625  my $dbh = $self->{dbh};
     626
     627  my %args = @_;
     628
     629  my $hcheck = ($args{revrec} eq 'y' ? ${$args{val}} : ${$args{host}});
     630  my $hfield = ($args{revrec} eq 'y' ? 'val' : 'host');
     631
     632  # $hcheck should be normalized by the time this sub is called.  Convert to the formal .arpa name for error reporting in reverse zones.
     633  my $arpaname = '';
     634  if ($args{revrec} eq 'y') {
     635    $arpaname = NetAddr::IP->new($hcheck);
     636##fixme:  more voodoo if global and/or per-user ARPA display mode flag set this way or that
     637    $arpaname = _ZONE($arpaname, 'ZONE', 'r', '.').($arpaname->{isv6} ? '.ip6.arpa' : '.in-addr.arpa');
     638  }
     639
     640  # The record type comparison is the only difference between two passes through this chunk of code.
     641  # CNAME records require both passes, where other records only need the second one.  Downside is
     642  # that returning error messages needs to check the loop variable on top of whatever else it references.
     643  foreach my $tcompare ('<>', '=') {
     644    next if $tcompare eq '<>' && ${$args{rectype}} != 5;
     645
     646    # Merging these sets of SQL statements is far too messy and doesn't reasonably
     647    # allow for more fine-grained error/warning messages to be returned
     648
     649    # First lookup fails out collisions with records without timestamps or default records (which can not have timestamps)
     650    my $sql = "SELECT count(*) FROM "._rectable($args{defrec}, $args{revrec}).
     651        " WHERE "._recparent($args{defrec}, $args{revrec})." = ? AND type $tcompare 5 AND $hfield = ?";
     652    my @lookupargs = ($args{id}, $hcheck);
     653    $sql .= " AND stampactive = 'f'" if $args{defrec} eq 'n';
     654    if ($args{update}) {
     655      $sql .= " AND record_id <> ?";
     656      push @lookupargs, $args{update};
     657    }
     658    my @t = $dbh->selectrow_array($sql, undef, @lookupargs);
     659    if ($t[0] > 0) {
     660      if ($tcompare eq '<>') {
     661        return ('FAIL', "One or more non-CNAME records already exist for ".($args{revrec} eq 'y' ? $arpaname : $hcheck).
     662                ".  CNAME records cannot use the same name as other records.");
     663      } else {
     664        return ('FAIL', "There is already a CNAME present for ".($args{revrec} eq 'y' ? $arpaname : $hcheck).
     665                ".  Only one CNAME may be present for a given name.");
     666      }
     667    }
     668
     669    # By this point, all failure cases for default records have been checked.
     670    # Default records cannot carry timestamps, so cannot have timestamp-based collisions
     671    return ('OK','OK') if $args{defrec} ne 'n';
     672
     673    # Second lookup fails out various timestamp-exists collision cases when adding/updating with a timestamp
     674    $sql = "SELECT count(*) FROM "._rectable($args{defrec}, $args{revrec}).
     675        " WHERE "._recparent($args{defrec}, $args{revrec})." = ? AND type $tcompare 5 AND $hfield = ?".
     676        " AND stampactive = 't'";
     677    @lookupargs = ($args{id}, $hcheck);
     678    if (${$args{stamp}} && ${$args{expires}}) {
     679      $sql .= " AND expires = ?";
     680      push @lookupargs, ${$args{expires}};
     681      if ($self->{coerce_cname_timestamp} eq 'none') {
     682        # no coercion means new valid-after < existing expires or new expires > existing valid-after will fail
     683        $sql .= " AND ". (${$args{expires}} eq 'f' ? "expires = 't' AND stamp <= ?)" : "expires = 'f' AND stamp >= ?");
     684        push @lookupargs, ${$args{stamp}};
     685      }
     686    }
     687    if ($args{update}) {
     688      $sql .= " AND record_id <> ?";
     689      push @lookupargs, $args{update};
     690    }
     691    @t = $dbh->selectrow_array($sql, undef, @lookupargs);
     692    if ($t[0] > 0) {
     693      if ($tcompare eq '<>') {
     694        return ('FAIL', "One or more non-CNAME records with timestamps already exist for ".($args{revrec} eq 'y' ? $arpaname : $hcheck).
     695                ".  CNAME records must expire before or become valid after any records with the same name.");
     696      } else {
     697        return ('FAIL', "There is already a CNAME with a timestamp present for ".($args{revrec} eq 'y' ? $arpaname : $hcheck).
     698                ".  Records with a matching name must expire before or become valid after this CNAME.");
     699      }
     700    }
     701
     702    # Third check starts retrieving actual timestamps to see if we need to,
     703    # and then if we can, coerce the new/updated record's timestamp to match
     704    $sql = "SELECT extract(epoch from stamp),expires,stamp < now() FROM "._rectable($args{defrec}, $args{revrec}).
     705        " WHERE "._recparent($args{defrec}, $args{revrec})." = ? AND type $tcompare 5 AND $hfield = ?".
     706        " AND stampactive = 't'";
     707    @lookupargs = ($args{id}, $hcheck);
     708    if ($args{update}) {
     709      $sql .= " AND record_id <> ?";
     710      push @lookupargs, $args{update};
     711    }
     712    if (${$args{stamp}}) {
     713      $sql .= " ORDER BY stamp ".(${$args{expires}} eq 'f' ? 'ASC' : 'DESC' )." LIMIT 1";
     714    } else {
     715      $sql .= " ORDER BY stamp LIMIT 1";
     716    }
     717    @t = $dbh->selectrow_array($sql, undef, @lookupargs);
     718    if (@t) {
     719      # caller requested an expiry time
     720      my $reqstamp = str2time(${$args{stamp}});
     721      if (${$args{expires}} eq 'f') {
     722        if ($reqstamp > $t[0]) {
     723          # do nothing, new record goes valid after the expiring record we found
     724        } else {
     725          if ($self->{coerce_cname_timestamp} eq 'adjust') {
     726            # coerce the valid-after timestamp
     727            ${$args{stamp}} = strftime('%Y-%m-%d %H:%M:%S', localtime($t[0]));
     728            return ('WARN', $typemap{${$args{rectype}}}." ".($args{update} ? 'updated' : 'added').
     729                " with modified valid-after time;  conflicting expiring record found");
     730          } else {
     731            # New valid-after overlaps existing expiry, and not configured to adjust it
     732            my $fill = ($tcompare eq '<>' ? ' CNAME, another record' : $typemap{${$args{rectype}}}.', a CNAME');
     733            return ('FAIL', "Cannot ".($args{update} ? 'update' : 'add').$fill.
     734                " with a valid-after time already exists for this name");
     735          }
     736        } # else ($reqstamp < $t[0])
     737      } else {
     738        if ($reqstamp < $t[0]) {
     739          # do nothing, new record will expire before the one we found
     740        } else {
     741          if ($self->{coerce_cname_timestamp} eq 'adjust') {
     742            if ($t[2] == 1) {
     743              # found a valid-after, but it's in the past, so adding an expiring record to match doesn't make
     744              # sense since it's effectively expired.
     745##fixme:  should probably remove this case once we get around to stripping valid-after timestamps once exported as active
     746              return ('FAIL', "Cannot ".($args{update} ? 'update' : 'add')." ".$typemap{${$args{rectype}}}.
     747                ", an existing valid-after record is already active for this name");
     748            } else {
     749              # coerce the expiry timestamp
     750              ${$args{stamp}} = strftime('%Y-%m-%d %H:%M:%S', localtime($t[0]));
     751              return ('WARN', $typemap{${$args{rectype}}}." ".($args{update} ? 'updated' : 'added').
     752                " with modified expiry time;  conflicting valid-after record found");
     753            }
     754          } else {
     755            return ('FAIL', "Cannot ".($args{update} ? 'update' : 'add')." ".$typemap{${$args{rectype}}}.
     756                ", another record with an overlapping valid-after timestamp already exists for this name");
     757          }
     758        }
     759      } # args{expires} ne 'f'
     760    } # if @t
     761
     762  } # each $tcompare
     763
     764  return ('OK', 'OK');
     765} # _cname_collision()
     766
    614767## All of these subs take substantially the same arguments:
    615768# a hash containing at least the following keys:
     
    808961      return ('FAIL', "The bare zone name may not be a CNAME") if ${$args{host}} eq $pname || ${$args{host}} =~ /^\@/;
    809962
    810 ##enhance:  Look up the passed value to see if it exists.  Ooo, fancy.
    811963      return ('FAIL', $errstr) if ! _check_hostname_form(${$args{val}}, ${$args{rectype}}, $args{defrec}, $args{revrec});
     964
    812965    } # $zname !~ .rpz
    813966  } # revzone eq 'n'
     
    16661819
    16671820  return ('WARN', join("\n", $errstr, $warnmsg) ) if $warnmsg;
    1668  
     1821
    16691822  return ('OK','OK');
    16701823} # done ALIAS record
     
    22222375      $cfg->{lowercase}         = $1 if /^lowercase\s*=\s*([a-z01]+)/i;
    22232376      $cfg->{showrev_arpa}      = $1 if /^showrev_arpa\s*=\s*([a-z]+)/i;
     2377      $cfg->{coerce_cname_timestamp}    = $1 if /^coerce_cname_timestamp\s*=\s*([a-z]+)/i;
    22242378      $cfg->{template_skip_0}   = $1 if /^template_skip_0\s*=\s*([a-z01]+)/i;
    22252379      $cfg->{template_skip_255} = $1 if /^template_skip_255\s*=\s*([a-z01]+)/i;
     
    47614915
    47624916  my $expires = shift || '';
    4763   $expires = 1 if $expires eq 'until';  # Turn some special values into the appropriate booleans.
    4764   $expires = 0 if $expires eq 'after';
     4917  $expires = 't' if $expires eq 'until';        # Turn some special values into the appropriate booleans.
     4918  $expires = 'f' if $expires eq 'after';
     4919  $expires = 't' if $expires eq '1';
     4920  $expires = 'f' if $expires eq '0';
    47654921  my $stamp = shift;
    47664922  $stamp = '' if !$stamp;        # Timestamp should be a string at this point.
     
    47724928  return ('FAIL', "expires must be 1, 't', or 'until',  or 0, 'f', or 'after'")
    47734929        if ($stamp && !defined($expires))
    4774         || ($stamp && $expires ne '0' && $expires ne '1' && $expires ne 't' && $expires ne 'f');
     4930        || ($stamp && $expires ne 't' && $expires ne 'f');
    47754931
    47764932  # Spaces are evil.
     
    48124968        host => $host, rectype => $rectype, val => $val, addr => $addr,
    48134969        dist => \$dist, port => \$port, weight => \$weight,
     4970        stamp => \$stamp, expires => \$expires,
    48144971        fields => \$fields, vallist => \@vallist);
    48154972
    48164973  return ($retcode,$retmsg) if $retcode eq 'FAIL';
     4974
     4975  # Check for CNAME collisions.
     4976##fixme:  should trim the list of arguments, not all of these should be required for CNAME collision checking
     4977  my ($ccode,$cmsg) = _cname_collision($self, defrec => $defrec, revrec => $revrec, id => $id,
     4978        host => $host, rectype => $rectype, val => $val, addr => $addr,
     4979        dist => \$dist, port => \$port, weight => \$weight,
     4980        stamp => \$stamp, expires => \$expires,
     4981        fields => \$fields, vallist => \@vallist);
     4982
     4983  return ($ccode,$cmsg) if $ccode eq 'FAIL';
     4984
     4985  # If both the validator and CNAME collision check return warnings, glue them together
     4986  if ($ccode eq 'WARN') {
     4987    if ($retcode eq 'WARN') {
     4988      $retmsg .= "<br>\n$cmsg";
     4989    } else {
     4990      $retmsg = $cmsg;
     4991    }
     4992    $retcode = 'WARN';
     4993  }
    48174994
    48184995  # Minor cleanup of invalid DNS labels
     
    48735050  $logdata{entry} .= "', TTL $ttl";
    48745051  $logdata{entry} .= ", location ".$self->getLoc($location)->{description} if $location;
    4875   $logdata{entry} .= ($expires ? ', expires at ' : ', valid after ').$stamp if $stamp;
     5052  $logdata{entry} .= ($expires eq 't' ? ', expires at ' : ', valid after ').$stamp if $stamp;
    48765053
    48775054  # Allow transactions, and raise an exception on errors so we can catch it later.
     
    49335110
    49345111  my $expires = shift || '';
    4935   $expires = 1 if $expires eq 'until';  # Turn some special values into the appropriate booleans.
    4936   $expires = 0 if $expires eq 'after';
     5112  $expires = 't' if $expires eq 'until';        # Turn some special values into the appropriate booleans.
     5113  $expires = 'f' if $expires eq 'after';
     5114  $expires = 't' if $expires eq '1';
     5115  $expires = 'f' if $expires eq '0';
    49375116  my $stamp = shift;
    49385117  $stamp = '' if !$stamp;        # Timestamp should be a string at this point.
     
    49435122  return ('FAIL', "expires must be 1, 't', or 'until',  or 0, 'f', or 'after'")
    49445123        if ($stamp && !defined($expires))
    4945         || ($stamp && $expires ne '0' && $expires ne '1' && $expires ne 't' && $expires ne 'f');
     5124        || ($stamp && $expires ne 't' && $expires ne 'f');
    49465125
    49475126  # Spaces are evil.
     
    49895168        host => $host, rectype => $rectype, val => $val, addr => $addr,
    49905169        dist => \$dist, port => \$port, weight => \$weight,
     5170        stamp => \$stamp, expires => \$expires,
    49915171        fields => \$fields, vallist => \@vallist,
    49925172        update => $id);
    49935173
    49945174  return ($retcode,$retmsg) if $retcode eq 'FAIL';
     5175
     5176  # Check for CNAME collisions.
     5177##fixme:  should trim the list of arguments, not all of these should be required for CNAME collision checking
     5178  my ($ccode,$cmsg) = _cname_collision($self, defrec => $defrec, revrec => $revrec,
     5179        id => ($defrec eq 'y' ? $oldrec->{group_id} : ($revrec eq 'n' ? $oldrec->{domain_id} : $oldrec->{rdns_id})),
     5180        host => $host, rectype => $rectype, val => $val, addr => $addr,
     5181        dist => \$dist, port => \$port, weight => \$weight,
     5182        stamp => \$stamp, expires => \$expires,
     5183        fields => \$fields, vallist => \@vallist,
     5184        update => $id);
     5185
     5186  return ($ccode,$cmsg) if $ccode eq 'FAIL';
     5187
     5188  # If both the validator and CNAME collision check return warnings, glue them together
     5189  if ($ccode eq 'WARN') {
     5190    if ($retcode eq 'WARN') {
     5191      $retmsg .= "<br>\n$cmsg";
     5192    } else {
     5193      $retmsg = $cmsg;
     5194    }
     5195    $retcode = 'WARN';
     5196  }
    49955197
    49965198  # Minor cleanup of invalid DNS labels
     
    50915293  $logdata{entry} .= "', TTL $ttl";
    50925294  $logdata{entry} .= ", location ".$self->getLoc($location)->{description} if $location;
    5093   $logdata{entry} .= ($expires ? ', expires at ' : ', valid after ').$stamp if $stamp;
     5295  $logdata{entry} .= ($expires eq 't' ? ', expires at ' : ', valid after ').$stamp if $stamp;
    50945296
    50955297  local $dbh->{AutoCommit} = 0;
Note: See TracChangeset for help on using the changeset viewer.