Ignore:
Timestamp:
03/13/26 14:00:58 (27 hours ago)
Author:
Kris Deugau
Message:

/branches/stable

Merge CNAME collision

Location:
branches/stable
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • branches/stable

  • branches/stable/DNSDB.pm

    r1049 r1054  
    3333use Fcntl qw(:flock);
    3434use Time::TAI64 qw(:tai64);
     35use Date::Parse;
    3536
    3637use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
     
    213214                force_refresh   => 1,
    214215                lowercase       => 0,   # mangle as little as possible by default
     216                # tinker with timestamps if adding or updating a record would
     217                # cause overlapping CNAME and other in some way
     218                coerce_cname_timestamp => 'none',
    215219                # show IPs and CIDR blocks as-is for reverse zones.  valid values are
    216220                # 'none' (default, show natural IP or CIDR)
     
    275279    $self->{showrev_arpa} = 'none';
    276280  }
     281  if (!grep /$self->{coerce_cname_timestamp}/, ('none','adjust','full')) {
     282    warn "Bad coerce_cname_timestamp setting $self->{coerce_cname_timestamp}, using default\n";
     283    $self->{coerce_cname_timestamp} = 'none';
     284  }
    277285
    278286  # Try to connect to the DB, and initialize a number of handy globals.
     
    561569## Record validation subs.
    562570##
     571
     572# Check for name collisions relating to CNAMEs.  Needs to be called from all other validators.
     573sub _cname_collision {
     574  my $self = shift;
     575  my $dbh = $self->{dbh};
     576
     577  my %args = @_;
     578
     579  my $hcheck = ($args{revrec} eq 'y' ? ${$args{val}} : ${$args{host}});
     580  my $hfield = ($args{revrec} eq 'y' ? 'val' : 'host');
     581
     582  # $hcheck should be normalized by the time this sub is called.  Convert to the formal .arpa name for error reporting in reverse zones.
     583  my $arpaname = '';
     584  if ($args{revrec} eq 'y') {
     585    $arpaname = NetAddr::IP->new($hcheck);
     586##fixme:  more voodoo if global and/or per-user ARPA display mode flag set this way or that
     587    $arpaname = _ZONE($arpaname, 'ZONE', 'r', '.').($arpaname->{isv6} ? '.ip6.arpa' : '.in-addr.arpa');
     588  }
     589
     590  # The record type comparison is the only difference between two passes through this chunk of code.
     591  # CNAME records require both passes, where other records only need the second one.  Downside is
     592  # that returning error messages needs to check the loop variable on top of whatever else it references.
     593  foreach my $tcompare ('<>', '=') {
     594    next if $tcompare eq '<>' && ${$args{rectype}} != 5;
     595
     596    # Merging these sets of SQL statements is far too messy and doesn't reasonably
     597    # allow for more fine-grained error/warning messages to be returned
     598
     599    # First lookup fails out collisions with records without timestamps or default records (which can not have timestamps)
     600    my $sql = "SELECT count(*) FROM "._rectable($args{defrec}, $args{revrec}).
     601        " WHERE "._recparent($args{defrec}, $args{revrec})." = ? AND type $tcompare 5 AND $hfield = ?";
     602    my @lookupargs = ($args{id}, $hcheck);
     603    $sql .= " AND stampactive = 'f'" if $args{defrec} eq 'n';
     604    if ($args{update}) {
     605      $sql .= " AND record_id <> ?";
     606      push @lookupargs, $args{update};
     607    }
     608    my @t = $dbh->selectrow_array($sql, undef, @lookupargs);
     609    if ($t[0] > 0) {
     610      if ($tcompare eq '<>') {
     611        return ('FAIL', "One or more non-CNAME records already exist for ".($args{revrec} eq 'y' ? $arpaname : $hcheck).
     612                ".  CNAME records cannot use the same name as other records.");
     613      } else {
     614        return ('FAIL', "There is already a CNAME present for ".($args{revrec} eq 'y' ? $arpaname : $hcheck).
     615                ".  Only one CNAME may be present for a given name.");
     616      }
     617    }
     618
     619    # By this point, all failure cases for default records have been checked.
     620    # Default records cannot carry timestamps, so cannot have timestamp-based collisions
     621    return ('OK','OK') if $args{defrec} ne 'n';
     622
     623    # Second lookup fails out various timestamp-exists collision cases when adding/updating with a timestamp
     624    $sql = "SELECT count(*) FROM "._rectable($args{defrec}, $args{revrec}).
     625        " WHERE "._recparent($args{defrec}, $args{revrec})." = ? AND type $tcompare 5 AND $hfield = ?".
     626        " AND stampactive = 't'";
     627    @lookupargs = ($args{id}, $hcheck);
     628    if (${$args{stamp}} && ${$args{expires}}) {
     629      $sql .= " AND expires = ?";
     630      push @lookupargs, ${$args{expires}};
     631      if ($self->{coerce_cname_timestamp} eq 'none') {
     632        # no coercion means new valid-after < existing expires or new expires > existing valid-after will fail
     633        $sql .= " AND ". (${$args{expires}} eq 'f' ? "expires = 't' AND stamp <= ?)" : "expires = 'f' AND stamp >= ?");
     634        push @lookupargs, ${$args{stamp}};
     635      }
     636    }
     637    if ($args{update}) {
     638      $sql .= " AND record_id <> ?";
     639      push @lookupargs, $args{update};
     640    }
     641    @t = $dbh->selectrow_array($sql, undef, @lookupargs);
     642    if ($t[0] > 0) {
     643      if ($tcompare eq '<>') {
     644        return ('FAIL', "One or more non-CNAME records with timestamps already exist for ".($args{revrec} eq 'y' ? $arpaname : $hcheck).
     645                ".  CNAME records must expire before or become valid after any records with the same name.");
     646      } else {
     647        return ('FAIL', "There is already a CNAME with a timestamp present for ".($args{revrec} eq 'y' ? $arpaname : $hcheck).
     648                ".  Records with a matching name must expire before or become valid after this CNAME.");
     649      }
     650    }
     651
     652    # Third check starts retrieving actual timestamps to see if we need to,
     653    # and then if we can, coerce the new/updated record's timestamp to match
     654    $sql = "SELECT extract(epoch from stamp),expires,stamp < now() FROM "._rectable($args{defrec}, $args{revrec}).
     655        " WHERE "._recparent($args{defrec}, $args{revrec})." = ? AND type $tcompare 5 AND $hfield = ?".
     656        " AND stampactive = 't'";
     657    @lookupargs = ($args{id}, $hcheck);
     658    if ($args{update}) {
     659      $sql .= " AND record_id <> ?";
     660      push @lookupargs, $args{update};
     661    }
     662    if (${$args{stamp}}) {
     663      $sql .= " ORDER BY stamp ".(${$args{expires}} eq 'f' ? 'ASC' : 'DESC' )." LIMIT 1";
     664    } else {
     665      $sql .= " ORDER BY stamp LIMIT 1";
     666    }
     667    @t = $dbh->selectrow_array($sql, undef, @lookupargs);
     668    if (@t) {
     669      # caller requested an expiry time
     670      my $reqstamp = str2time(${$args{stamp}});
     671      if (${$args{expires}} eq 'f') {
     672        if ($reqstamp > $t[0]) {
     673          # do nothing, new record goes valid after the expiring record we found
     674        } else {
     675          if ($self->{coerce_cname_timestamp} eq 'adjust') {
     676            # coerce the valid-after timestamp
     677            ${$args{stamp}} = strftime('%Y-%m-%d %H:%M:%S', localtime($t[0]));
     678            return ('WARN', $typemap{${$args{rectype}}}." ".($args{update} ? 'updated' : 'added').
     679                " with modified valid-after time;  conflicting expiring record found");
     680          } else {
     681            # New valid-after overlaps existing expiry, and not configured to adjust it
     682            my $fill = ($tcompare eq '<>' ? ' CNAME, another record' : $typemap{${$args{rectype}}}.', a CNAME');
     683            return ('FAIL', "Cannot ".($args{update} ? 'update' : 'add').$fill.
     684                " with a valid-after time already exists for this name");
     685          }
     686        } # else ($reqstamp < $t[0])
     687      } else {
     688        if ($reqstamp < $t[0]) {
     689          # do nothing, new record will expire before the one we found
     690        } else {
     691          if ($self->{coerce_cname_timestamp} eq 'adjust') {
     692            if ($t[2] == 1) {
     693              # found a valid-after, but it's in the past, so adding an expiring record to match doesn't make
     694              # sense since it's effectively expired.
     695##fixme:  should probably remove this case once we get around to stripping valid-after timestamps once exported as active
     696              return ('FAIL', "Cannot ".($args{update} ? 'update' : 'add')." ".$typemap{${$args{rectype}}}.
     697                ", an existing valid-after record is already active for this name");
     698            } else {
     699              # coerce the expiry timestamp
     700              ${$args{stamp}} = strftime('%Y-%m-%d %H:%M:%S', localtime($t[0]));
     701              return ('WARN', $typemap{${$args{rectype}}}." ".($args{update} ? 'updated' : 'added').
     702                " with modified expiry time;  conflicting valid-after record found");
     703            }
     704          } else {
     705            return ('FAIL', "Cannot ".($args{update} ? 'update' : 'add')." ".$typemap{${$args{rectype}}}.
     706                ", another record with an overlapping valid-after timestamp already exists for this name");
     707          }
     708        }
     709      } # args{expires} ne 'f'
     710    } # if @t
     711
     712  } # each $tcompare
     713
     714  return ('OK', 'OK');
     715} # _cname_collision()
    563716
    564717## All of these subs take substantially the same arguments:
     
    758911      return ('FAIL', "The bare zone name may not be a CNAME") if ${$args{host}} eq $pname || ${$args{host}} =~ /^\@/;
    759912
    760 ##enhance:  Look up the passed value to see if it exists.  Ooo, fancy.
    761913      return ('FAIL', $errstr) if ! _check_hostname_form(${$args{val}}, ${$args{rectype}}, $args{defrec}, $args{revrec});
     914
    762915    } # $zname !~ .rpz
    763916  } # revzone eq 'n'
     
    16161769
    16171770  return ('WARN', join("\n", $errstr, $warnmsg) ) if $warnmsg;
    1618  
     1771
    16191772  return ('OK','OK');
    16201773} # done ALIAS record
     
    21562309      $cfg->{lowercase}         = $1 if /^lowercase\s*=\s*([a-z01]+)/i;
    21572310      $cfg->{showrev_arpa}      = $1 if /^showrev_arpa\s*=\s*([a-z]+)/i;
     2311      $cfg->{coerce_cname_timestamp}    = $1 if /^coerce_cname_timestamp\s*=\s*([a-z]+)/i;
    21582312      $cfg->{template_skip_0}   = $1 if /^template_skip_0\s*=\s*([a-z01]+)/i;
    21592313      $cfg->{template_skip_255} = $1 if /^template_skip_255\s*=\s*([a-z01]+)/i;
     
    46944848
    46954849  my $expires = shift || '';
    4696   $expires = 1 if $expires eq 'until';  # Turn some special values into the appropriate booleans.
    4697   $expires = 0 if $expires eq 'after';
     4850  $expires = 't' if $expires eq 'until';        # Turn some special values into the appropriate booleans.
     4851  $expires = 'f' if $expires eq 'after';
     4852  $expires = 't' if $expires eq '1';
     4853  $expires = 'f' if $expires eq '0';
    46984854  my $stamp = shift;
    46994855  $stamp = '' if !$stamp;        # Timestamp should be a string at this point.
     
    47054861  return ('FAIL', "expires must be 1, 't', or 'until',  or 0, 'f', or 'after'")
    47064862        if ($stamp && !defined($expires))
    4707         || ($stamp && $expires ne '0' && $expires ne '1' && $expires ne 't' && $expires ne 'f');
     4863        || ($stamp && $expires ne 't' && $expires ne 'f');
    47084864
    47094865  # Spaces are evil.
     
    47454901        host => $host, rectype => $rectype, val => $val, addr => $addr,
    47464902        dist => \$dist, port => \$port, weight => \$weight,
     4903        stamp => \$stamp, expires => \$expires,
    47474904        fields => \$fields, vallist => \@vallist);
    47484905
    47494906  return ($retcode,$retmsg) if $retcode eq 'FAIL';
     4907
     4908  # Check for CNAME collisions.
     4909##fixme:  should trim the list of arguments, not all of these should be required for CNAME collision checking
     4910  my ($ccode,$cmsg) = _cname_collision($self, defrec => $defrec, revrec => $revrec, id => $id,
     4911        host => $host, rectype => $rectype, val => $val, addr => $addr,
     4912        dist => \$dist, port => \$port, weight => \$weight,
     4913        stamp => \$stamp, expires => \$expires,
     4914        fields => \$fields, vallist => \@vallist);
     4915
     4916  return ($ccode,$cmsg) if $ccode eq 'FAIL';
     4917
     4918  # If both the validator and CNAME collision check return warnings, glue them together
     4919  if ($ccode eq 'WARN') {
     4920    if ($retcode eq 'WARN') {
     4921      $retmsg .= "<br>\n$cmsg";
     4922    } else {
     4923      $retmsg = $cmsg;
     4924    }
     4925    $retcode = 'WARN';
     4926  }
    47504927
    47514928  # Minor cleanup of invalid DNS labels
     
    48064983  $logdata{entry} .= "', TTL $ttl";
    48074984  $logdata{entry} .= ", location ".$self->getLoc($location)->{description} if $location;
    4808   $logdata{entry} .= ($expires ? ', expires at ' : ', valid after ').$stamp if $stamp;
     4985  $logdata{entry} .= ($expires eq 't' ? ', expires at ' : ', valid after ').$stamp if $stamp;
    48094986
    48104987  # Allow transactions, and raise an exception on errors so we can catch it later.
     
    48665043
    48675044  my $expires = shift || '';
    4868   $expires = 1 if $expires eq 'until';  # Turn some special values into the appropriate booleans.
    4869   $expires = 0 if $expires eq 'after';
     5045  $expires = 't' if $expires eq 'until';        # Turn some special values into the appropriate booleans.
     5046  $expires = 'f' if $expires eq 'after';
     5047  $expires = 't' if $expires eq '1';
     5048  $expires = 'f' if $expires eq '0';
    48705049  my $stamp = shift;
    48715050  $stamp = '' if !$stamp;        # Timestamp should be a string at this point.
     
    48765055  return ('FAIL', "expires must be 1, 't', or 'until',  or 0, 'f', or 'after'")
    48775056        if ($stamp && !defined($expires))
    4878         || ($stamp && $expires ne '0' && $expires ne '1' && $expires ne 't' && $expires ne 'f');
     5057        || ($stamp && $expires ne 't' && $expires ne 'f');
    48795058
    48805059  # Spaces are evil.
     
    49225101        host => $host, rectype => $rectype, val => $val, addr => $addr,
    49235102        dist => \$dist, port => \$port, weight => \$weight,
     5103        stamp => \$stamp, expires => \$expires,
    49245104        fields => \$fields, vallist => \@vallist,
    49255105        update => $id);
    49265106
    49275107  return ($retcode,$retmsg) if $retcode eq 'FAIL';
     5108
     5109  # Check for CNAME collisions.
     5110##fixme:  should trim the list of arguments, not all of these should be required for CNAME collision checking
     5111  my ($ccode,$cmsg) = _cname_collision($self, defrec => $defrec, revrec => $revrec,
     5112        id => ($defrec eq 'y' ? $oldrec->{group_id} : ($revrec eq 'n' ? $oldrec->{domain_id} : $oldrec->{rdns_id})),
     5113        host => $host, rectype => $rectype, val => $val, addr => $addr,
     5114        dist => \$dist, port => \$port, weight => \$weight,
     5115        stamp => \$stamp, expires => \$expires,
     5116        fields => \$fields, vallist => \@vallist,
     5117        update => $id);
     5118
     5119  return ($ccode,$cmsg) if $ccode eq 'FAIL';
     5120
     5121  # If both the validator and CNAME collision check return warnings, glue them together
     5122  if ($ccode eq 'WARN') {
     5123    if ($retcode eq 'WARN') {
     5124      $retmsg .= "<br>\n$cmsg";
     5125    } else {
     5126      $retmsg = $cmsg;
     5127    }
     5128    $retcode = 'WARN';
     5129  }
    49285130
    49295131  # Minor cleanup of invalid DNS labels
     
    50245226  $logdata{entry} .= "', TTL $ttl";
    50255227  $logdata{entry} .= ", location ".$self->getLoc($location)->{description} if $location;
    5026   $logdata{entry} .= ($expires ? ', expires at ' : ', valid after ').$stamp if $stamp;
     5228  $logdata{entry} .= ($expires eq 't' ? ', expires at ' : ', valid after ').$stamp if $stamp;
    50275229
    50285230  local $dbh->{AutoCommit} = 0;
Note: See TracChangeset for help on using the changeset viewer.