Ignore:
Timestamp:
02/11/26 14:51:02 (9 hours ago)
Author:
Kris Deugau
Message:

/branches/stable

Completed triaging existing trunk commits from production

Location:
branches/stable
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • branches/stable

  • branches/stable/DNSDB.pm

    r1033 r1034  
    689689    return ('FAIL', $errstr) if ! _check_hostname_form(${$args{host}}, ${$args{rectype}}, $args{defrec}, $args{revrec});
    690690  } else {
    691     # CNAME target check - IP addresses not allowed.  Must be a more or less well-formed hostname.
    692     return ('FAIL', "CNAME records cannot point directly to an IP address")
    693       if ${$args{val}} =~ /^(?:[\d.]+|[0-9a-fA-F:]+)$/;
    694 
    695     # Make sure target is a well-formed hostname
    696     return ('FAIL', $errstr) if ! _check_hostname_form(${$args{val}}, ${$args{rectype}}, $args{defrec}, $args{revrec});
    697 
    698     # Forcibly append the domain name if the hostname being added does not end with the current domain name
    699     my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : $self->domainName($args{id}));
    700     ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/i;
    701 
    702     # CNAMEs can not be used for parent nodes;  just leaf nodes with no other record types
    703     # Enforce this for the zone name
    704     return ('FAIL', "The bare zone name may not be a CNAME") if ${$args{host}} eq $pname || ${$args{host}} =~ /^\@/;
     691    # a bit expensive to put this here, but we need some kind of cheap flag for an RPZ zone with different rules
     692    my $zname = $self->domainName($args{id});
     693    if ($zname =~ /\.rpz$/) {
     694      # RPZ domains consist almost entirely of CNAME records, and have special rules for their syntax
     695      # From the Unbound doc:  https://unbound.docs.nlnetlabs.nl/en/latest/topics/filtering/rpz.html
     696      # Supposedly other overrides are also valid
     697      return ('FAIL', "Unsupported RPZ override ${$args{val}}")
     698        unless ${$args{val}} =~ /^(?:\.|\*\.|rpz-passthru\.|rpz-drop\.|rpz-tcp-only\.)$/;
     699      # Append the RPZ name
     700      my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : $self->domainName($args{id}));
     701      ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/i;
     702    } else {
     703      # CNAME target check - IP addresses not allowed.  Must be a more or less well-formed hostname.
     704      return ('FAIL', "CNAME records cannot point directly to an IP address")
     705        if ${$args{val}} =~ /^(?:[\d.]+|[0-9a-fA-F:]+)$/;
     706
     707      # Make sure target is a well-formed hostname
     708      return ('FAIL', $errstr) if ! _check_hostname_form(${$args{val}}, ${$args{rectype}}, $args{defrec}, $args{revrec});
     709
     710      # Forcibly append the domain name if the hostname being added does not end with the current domain name
     711      my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : $zname);
     712      ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/i;
     713
     714      # CNAMEs can not be used for parent nodes;  just leaf nodes with no other record types
     715      # Enforce this for the zone name
     716      return ('FAIL', "The bare zone name may not be a CNAME") if ${$args{host}} eq $pname || ${$args{host}} =~ /^\@/;
    705717
    706718##enhance:  Look up the passed value to see if it exists.  Ooo, fancy.
    707     return ('FAIL', $errstr) if ! _check_hostname_form(${$args{val}}, ${$args{rectype}}, $args{defrec}, $args{revrec});
    708   }
     719      return ('FAIL', $errstr) if ! _check_hostname_form(${$args{val}}, ${$args{rectype}}, $args{defrec}, $args{revrec});
     720    } # $zname !~ .rpz
     721  } # revzone eq 'n'
    709722
    710723  return ('OK','OK');
     
    10331046    # Not strictly true, but SRV records not following this convention won't be found.
    10341047    return ('FAIL',"SRV records must begin with _service._protocol [${$args{host}}]")
    1035         unless ${$args{host}} =~ /^_[A-Za-z-]+\._[A-Za-z]+\.[a-zA-Z0-9-]+/;
     1048        unless ${$args{host}} =~ /^_[A-Za-z\d-]+\._[A-Za-z]+\.[a-zA-Z0-9-]+/;
    10361049
    10371050    # SRV target check - IP addresses not allowed.  Must be a more or less well-formed hostname.
     
    10481061    # Not strictly true, but SRV records not following this convention won't be found.
    10491062    return ('FAIL',"SRV records must begin with _service._protocol [${$args{host}}]")
    1050         unless ${$args{val}} =~ /^_[A-Za-z]+\._[A-Za-z]+\.[a-zA-Z0-9-]+/;
     1063        unless ${$args{val}} =~ /^_[A-Za-z\d-]+\._[A-Za-z]+\.[a-zA-Z0-9-]+/;
    10511064
    10521065    # SRV target check - IP addresses not allowed.  Must be a more or less well-formed hostname.
     
    10751088  return ('OK','OK');
    10761089} # done SRV record
     1090
     1091# CAA record
     1092sub _validate_257 {
     1093  my $self = shift;
     1094  my $dbh = $self->{dbh};
     1095
     1096  my %args = @_;
     1097
     1098  my $code = 'OK';
     1099  my $msg = '';    # Default to no message, because there are a lot of handwavy warning cases.
     1100
     1101  if ($args{revrec} eq 'n') {
     1102    # Coerce all hostnames to end in ".DOMAIN" for group/default records,
     1103    # or the intended parent domain for live records.
     1104    my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : $self->domainName($args{id}));
     1105    ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/i;
     1106
     1107    my ($caaflag, $caatag, $caadetail) = (${$args{val}} =~ /(\d+)\s+(\w+)\s+(.+)/);
     1108
     1109    return ('FAIL', "Poorly formed CAA record missing one or more of flag, tag, or detail")
     1110      if (!defined($caaflag) || !defined($caatag) || !defined($caadetail));
     1111
     1112    # flag is a bitfield, only bit 0 currently has meaning as "Issuer Critical"
     1113    # Not 100% clear if this flag is permitted on known tags, or if it's
     1114    # semantically null since the known tags are already defined.  We'll allow it.
     1115    return ('FAIL', "CAA flags other than 0 or 128 not currently supported in DNS") if $caaflag ne '0' && $caaflag ne '128';
     1116
     1117    # known tags:
     1118    #    issue
     1119    #    issuewild
     1120    #    iodef  (RFC5070)
     1121    #    auth (reserved, do not use)
     1122    #    path (reserved, do not use)
     1123    #    policy (reserved, do not use)
     1124    return ('FAIL', "CAA tags may only use a-z, A-Z, and 0-9") if $caatag !~ /^[a-zA-Z0-9]+$/;
     1125    return ('FAIL', "Can't use reserved CAA tag '$caatag'") if $caatag =~ /^(?:auth|path|policy)$/;
     1126    if ($caatag !~ /^(?:issue|issuewild|iodef)$/) {
     1127      $code = 'WARN';
     1128      $msg = ($msg ? $msg."  " : '')."Unknown CAA tag '$caatag' will be published as-is.";
     1129    }
     1130    if (length($caatag) > 15) {
     1131      $code = 'WARN';
     1132      $msg = ($msg ? $msg."  " : '').'Custom CAA tag > 15 characters may not behave as intended.';
     1133    }
     1134
     1135    if ($caatag eq 'issue' || $caatag eq 'issuewild') {
     1136      # Detail format is possibly as complex as:
     1137      # [;|cert-authority [; key=value[ key=value ...]]
     1138      # but arguably a strict reading says there can only be one key, and the
     1139      # rest of the string after first = is the value, even if it appears to
     1140      # contain extra key=value pairs.
     1141      # See https://datatracker.ietf.org/doc/html/rfc6844 for full ABNF definition.
     1142      # Either way all we need to validate is that it's within the specified characters.
     1143      if ($caadetail eq ';') {
     1144        # No certs permitted
     1145      } else {
     1146        my ($certauth,$remainder) = ($caadetail =~ /^\s*([a-zA-Z0-9.-]+)\s*((?:;|\s|$).*)?$/);
     1147        if (!$certauth) {
     1148          # We can't reasonably validate individual domains, just that it's well-formed
     1149          return ('FAIL', "CAA authority domain must be a valid domain name");
     1150        }
     1151        if ($remainder) {
     1152          return ('FAIL', "CAA authority domain and optional key=value entry or entries must be separated by a ';'")
     1153            if $remainder !~ /^\s*;/;
     1154          $remainder =~ s/\s*;\s*//;
     1155          # Just validate the characters in the remainder.  Any details are CA-specific and not sanely validateable.
     1156          return ('FAIL', "Invalid characters in optional key=value entry or entries")
     1157            if $remainder !~ /^[a-zA-Z0-9]+\s*=[\x21-\x7e\s]+$/;
     1158        }
     1159      }
     1160    } # issue/isseuewild
     1161
     1162    elsif ($caatag eq 'iodef') {
     1163      # Two valid forms:
     1164      # mailto:address@example.com
     1165      # http://iodef.example.com/
     1166      # RFC seems a little handwavy whether https:// is valid or not, but the chained
     1167      # RFC for the HTTP-based reporting protocol says that this should be assumed to
     1168      # be a dedicated port (4590) and service, requiring TLS.  Allowing https:// per
     1169      # the detail description in https://datatracker.ietf.org/doc/html/rfc6844#section-5.4.
     1170      return ('FAIL', "iodef tag data must reference a mailto: or http: URI") if $caadetail !~ /^(mailto|https?):/;
     1171      if ($1 eq 'mailto') {
     1172        # not going full RFC on validating form, just "reasonably sane"
     1173        return ('FAIL', "Poorly formed email for iodef tag") if $caadetail !~ /^mailto:[^\s]+\@[a-zA-z0-9._-]+$/
     1174      } else {
     1175        return ('FAIL', "Poorly formed URI for iodef tag") if $caadetail !~ m,^https?://[a-zA-z0-9._-]+/?$,
     1176      }
     1177    } # iodef
     1178
     1179  } else {
     1180    # CAA records don't make much sense in reverse zones
     1181    return ('FAIL', "CAA records not supported in reverse zones");
     1182  }
     1183
     1184  # Allow CAA records in default record sets for now, but it's a bit iffy
     1185  # whether this makes any sense.  Not nice to publish a default "issue ;"
     1186  # record, then go through a support mess trying to figure out why a
     1187  # customer can't register a cert somewhere.
     1188#  if ($args{defrec} eq 'n') {
     1189#  } else {
     1190#  }
     1191
     1192  return ($code, $msg);
     1193} # done CAA record
     1194
    10771195
    10781196# Now the custom types
     
    43934511  # Filtering on host/val (mainly normal record list)
    43944512  if ($args{filter}) {
    4395     $sql .= " AND (r.host ~* ? OR r.val ~* ? OR r.host ~* ? OR r.val ~* ?)";
    4396     my $tmp = join('.',reverse(split(/\./,$args{filter})));
    4397     push @bindvars, ($args{filter},$args{filter});
    4398     push @bindvars, ($tmp, $tmp);
     4513    # not much use to end users, but internal callers may want more fine-grained restriction on CIDR ranges
     4514    # we'll only support the value-comparison operators;  bitwise/add/subtract don't make much sense in this context
     4515    my $ipfilt = 0;
     4516    if ($args{filter} =~ /^\s*(<|<=|=|>=|>|<>|<<|<<=|>>|>>=)\s*([\da-fA-F].+)\s*$/) {
     4517      my $filt_op = $1;
     4518      my $filt_val = $2;
     4519      # do we have an IP-ish value?
     4520      if ($filt_val =~ m,^(?:[\d.]+|[0-9a-f]+)(?:/\d+)?$,) {
     4521        # now make sure
     4522        my $tmp = new NetAddr::IP $filt_val;
     4523        if ($tmp) {
     4524          $sql .= " AND inetlazy(r.val) $filt_op ?";
     4525          push @bindvars, $filt_val;
     4526          $ipfilt = 1;
     4527        } # really looks like a valid IP/CIDR
     4528      } # looks IPish
     4529    } # has CIDR operator
     4530    if (!$ipfilt) {
     4531      # simple text matching, with a bit of mix-n-match to account for .arpa names
     4532      $sql .= " AND (r.host ~* ? OR r.val ~* ? OR r.host ~* ? OR r.val ~* ?)";
     4533      my $tmp = join('.',reverse(split(/\./,$args{filter})));
     4534      push @bindvars, ($args{filter},$args{filter});
     4535      push @bindvars, ($tmp, $tmp);
     4536    }
    43994537  }
    44004538
     
    44704608  # Filtering on host/val (mainly normal record list)
    44714609  if ($args{filter}) {
    4472     $sql .= " AND (r.host ~* ? OR r.val ~* ? OR r.host ~* ? OR r.val ~* ?)";
    4473     my $tmp = join('.',reverse(split(/\./,$args{filter})));
    4474     push @bindvars, ($args{filter},$args{filter});
    4475     push @bindvars, ($tmp, $tmp);
     4610    # not much use to end users, but internal callers may want more fine-grained restriction on CIDR ranges
     4611    # we'll only support the value-comparison operators;  bitwise/add/subtract don't make much sense in this context
     4612    my $ipfilt = 0;
     4613    if ($args{filter} =~ /^\s*(<|<=|=|>=|>|<>|<<|<<=|>>|>>=)\s*([\da-fA-F].+)\s*$/) {
     4614      my $filt_op = $1;
     4615      my $filt_val = $2;
     4616      # do we have an IP-ish value?
     4617      if ($filt_val =~ m,^(?:[\d.]+|[0-9a-f]+)(?:/\d+)?$,) {
     4618        # now make sure
     4619        my $tmp = new NetAddr::IP $filt_val;
     4620        if ($tmp) {
     4621          $sql .= " AND inetlazy(r.val) $filt_op ?";
     4622          push @bindvars, $filt_val;
     4623          $ipfilt = 1;
     4624        } # really looks like a valid IP/CIDR
     4625      } # looks IPish
     4626    } # has CIDR operator
     4627    if (!$ipfilt) {
     4628      # simple text matching, with a bit of mix-n-match to account for .arpa names
     4629      $sql .= " AND (r.host ~* ? OR r.val ~* ? OR r.host ~* ? OR r.val ~* ?)";
     4630      my $tmp = join('.',reverse(split(/\./,$args{filter})));
     4631      push @bindvars, ($args{filter},$args{filter});
     4632      push @bindvars, ($tmp, $tmp);
     4633    }
    44764634  }
    44774635
     
    58295987        $warnmsg .= "Suspect record '".$rr->string."' may not be imported correctly: SRV records may not be bare IP addresses\n"
    58305988          if $val =~ /^(?:(?:\d+\.){3}\d+|[a-fA-F0-9:]+)$/;
    5831       } elsif ($type eq 'KEY') {
     5989      }
     5990
     5991      elsif ($type eq 'CAA') {
     5992        # Store CAA records without the syntactic quotes
     5993        $val = join(" ", $rr->flags, $rr->tag, $rr->value);
     5994      }
     5995
     5996      elsif ($type eq 'KEY') {
    58325997        # we don't actually know what to do with these...
    58335998        $val = $rr->flags.":".$rr->protocol.":".$rr->algorithm.":".$rr->key.":".$rr->keytag.":".$rr->privatekeyname;
     
    66916856  elsif ($typemap{$type} eq 'CNAME') {
    66926857    ($host,$val) = __revswap($host,$val) if $revrec eq 'y';
     6858    if ($zone =~ /\.rpz$/) {
     6859      $val = '..' if $val eq '.';
     6860    }
    66936861    print $datafile "C$host:$val:$ttl:$stamp:$loc\n" or die $!;
    66946862  } # CNAME
     
    67086876    print $datafile "$prefix\\000:$ttl:$stamp:$loc\n" or die $!;
    67096877  } # SRV
     6878
     6879  elsif ($typemap{$type} eq 'CAA') {
     6880    # CAA records really don't make much sense in reverse zones
     6881    #($host,$val) = __revswap($host,$val) if $revrec eq 'y';
     6882    return if $revrec eq 'y';
     6883
     6884    # data is a bitfield byte, length byte+string, then "everything else"
     6885
     6886    my $prefix = ":$host:257:";
     6887
     6888    my ($caaflags, $caatag, $caadetail) = ($val =~ /(\d+)\s+(\w+)\s+(.+)/);
     6889    $prefix .= sprintf "\\%0.3o", $caaflags;
     6890    $prefix .= sprintf "\\%0.3o%s", length($caatag), $caatag;
     6891    $caadetail =~ s/:/\\072/g;
     6892    $caadetail =~ s/;/\\073/g;  # Not strictly necessary but may be helpful validating records by eye
     6893    $caadetail =~ s/^\s*"\s*//; # AXFR imports may produce strings with embedded quotes;  these
     6894    $caadetail =~ s/\s*"\s*$//; # are purely a syntactic crutch for BIND-style zone files
     6895    print $datafile "$prefix$caadetail:$ttl:$stamp:$loc\n" or die $!;
     6896  } # CAA
    67106897
    67116898  elsif ($typemap{$type} eq 'RP') {
Note: See TracChangeset for help on using the changeset viewer.