#!/bin/sh

# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
#
# SPDX-License-Identifier: MPL-2.0
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0.  If a copy of the MPL was not distributed with this
# file, you can obtain one at https://mozilla.org/MPL/2.0/.
#
# See the COPYRIGHT file distributed with this work for additional
# information regarding copyright ownership.

set -e

# test response policy zones (RPZ)

# touch dnsrps-off to not test with DNSRPS
# touch dnsrps-only to not test with classic RPZ

. ../conf.sh

ns=10.53.0
ns1=$ns.1   # root, defining the others
ns2=$ns.2   # authoritative server whose records are rewritten
ns3=$ns.3   # main rewriting resolver
ns4=$ns.4   # another authoritative server that is rewritten
ns5=$ns.5   # another rewriting resolver
ns6=$ns.6   # a forwarding server
ns7=$ns.7   # another rewriting resolver
ns8=$ns.8   # another rewriting resolver
ns9=$ns.9   # another rewriting resolver
ns10=$ns.10 # authoritative server

HAVE_CORE=

status=0
t=0

DEBUG=
SAVE_RESULTS=
ARGS=

USAGE="$0: [-xS]"
while getopts "xS:" c; do
  case $c in
    x)
      set -x
      DEBUG=-x
      ARGS="$ARGS -x"
      ;;
    S)
      SAVE_RESULTS=-S
      ARGS="$ARGS -S"
      ;;
    *)
      echo "$USAGE" 1>&2
      exit 1
      ;;
  esac
done
shift $((OPTIND - 1))
if test "$#" -ne 0; then
  echo "$USAGE" 1>&2
  exit 1
fi
# really quit on control-C
trap 'exit 1' 1 2 15

TS='%H:%M:%S '
TS=
comment() {
  if test -n "$TS"; then
    date "+${TS}$*" | cat_i
  fi
}

DNSRPSCMD=./dnsrps
RNDCCMD="$RNDC -c ../_common/rndc.conf -p ${CONTROLPORT} -s"

if test -x "$DNSRPSCMD"; then
  # speed up the many delays for dnsrpzd by waiting only 0.1 seconds
  WAIT_CMD="$DNSRPSCMD -w 0.1"
  TEN_SECS=100
else
  WAIT_CMD="sleep 1"
  TEN_SECS=10
fi

digcmd() {
  if test "$1" = TCP; then
    shift
  fi
  # Default to +noauth and @$ns3
  # Also default to -bX where X is the @value so that OS X will choose
  #	    the right IP source address.
  digcmd_args=$(echo "+nocookie +noadd +time=2 +tries=1 -p ${PORT} $*" \
    | sed -e "/@/!s/.*/& @$ns3/" \
      -e '/-b/!s/@\([^ ]*\)/@\1 -b\1/' \
      -e '/+n?o?auth/!s/.*/+noauth &/')
  #echo_i "dig $digcmd_args 1>&2
  $DIG $digcmd_args || return
}

# set DIGNM=file name for dig output
GROUP_NM=
TEST_NUM=0
make_dignm() {
  TEST_NUM=$((TEST_NUM + 1))
  DIGNM=dig.out$GROUP_NM-$TEST_NUM
  while test -f $DIGNM; do
    DIGNM="$DIGNM+"
  done
}

setret() {
  ret=1
  status=$((status + 1))
  echo_i "$*"
}

# set $SN to the SOA serial number of a zone
# $1=domain
# $2=DNS server and client IP address
get_sn() {
  SOA=$($DIG -p ${PORT} +short +norecurse soa "$1" "@$2" "-b$2")
  SN=$(expr "$SOA" : '[^ ]* [^ ]* \([^ ]*\) .*' || true)
  test "$SN" != "" && return
  echo_i "no serial number from \`dig -p ${PORT} soa $1 @$2\` in \"$SOA\""
  exit 1
}

get_sn_fast() {
  RSN=$($DNSRPSCMD -n "$1")
  #echo "dnsrps serial for $1 is $RSN"
  if test -z "$RSN"; then
    echo_i "dnsrps failed to get SOA serial number for $1"
    exit 1
  fi
}

# check that dnsrpzd has loaded its zones
# $1=domain
# $2=DNS server IP address
FZONES=$(sed -n -e 's/^zone "\(.*\)".*\(10.53.0..\).*/Z=\1;M=\2/p' dnsrpzd.conf)
dnsrps_loaded() {
  test "$mode" = dnsrps || return 0
  n=0
  for V in $FZONES; do
    eval "$V"
    get_sn $Z $M
    while true; do
      get_sn_fast "$Z"
      if test "$SN" -eq "0$RSN"; then
        #echo "$Z @$M serial=$SN"
        break
      fi
      n=$((n + 1))
      if test "$n" -gt $TEN_SECS; then
        echo_i "dnsrps serial for $Z is $RSN instead of $SN"
        exit 1
      fi
      $WAIT_CMD
    done
  done
}

# check the serial number in an SOA to ensure that a policy zone has
#   been (re)loaded
# $1=serial number
# $2=domain
# $3=DNS server
ck_soa() {
  n=0
  while true; do
    if test "$mode" = dnsrps; then
      get_sn_fast "$2"
      test "$RSN" -eq "$1" && return
    else
      get_sn "$2" "$3"
      test "$SN" -eq "$1" && return
    fi
    n=$((n + 1))
    if test "$n" -gt $TEN_SECS; then
      echo_i "got serial number \"$SN\" instead of \"$1\" from $2 @$3"
      return
    fi
    $WAIT_CMD
  done
}

# (re)load the response policy zones with the rules in the file $TEST_FILE
load_db() {
  if test -n "$TEST_FILE"; then
    copy_setports $TEST_FILE tmp
    if $NSUPDATE -v tmp; then
      :
      $RNDCCMD $ns3 sync
    else
      echo_i "failed to update policy zone with $TEST_FILE"
      $RNDCCMD $ns3 sync
      exit 1
    fi
    rm -f tmp
  fi
}

# restart name server
# $1 ns number
# $2 rebuild bl rpz zones if "rebuild-bl-rpz"
restart() {
  # try to ensure that the server really has stopped
  # and won't mess with ns$1/name.pid
  if test -z "$HAVE_CORE" -a -f ns$1/named.pid; then
    $RNDCCMD $ns$1 halt >/dev/null 2>&1
    if test -f ns$1/named.pid; then
      sleep 1
      PID=$(cat ns$1/named.pid 2>/dev/null)
      if test -n "$PID"; then
        echo_i "killing ns$1 server $PID"
        kill -9 $PID
      fi
    fi
  fi
  rm -f ns$1/*.jnl
  if [ "$2" = "rebuild-bl-rpz" ]; then
    if test -f ns$1/base.db; then
      for NM in ns$1/bl*.db; do
        cp -f ns$1/base.db $NM
      done
    fi
  fi
  start_server --noclean --restart --port ${PORT} ns$1
  load_db
  dnsrps_loaded
  sleep 1
}

# $1=server and irrelevant args
# $2=error message
ckalive() {
  CKALIVE_NS=$(expr "$1" : '.*@ns\([1-9]\).*' || true)
  if test -z "$CKALIVE_NS"; then
    CKALIVE_NS=3
  fi
  eval CKALIVE_IP=\$ns$CKALIVE_NS
  $RNDCCMD $CKALIVE_IP status >/dev/null 2>&1 && return 0
  HAVE_CORE=yes
  setret "$2"
  # restart the server to avoid stalling waiting for it to stop
  restart $CKALIVE_NS "rebuild-bl-rpz"
  return 0
}

resetstats() {
  NSDIR=$1
  eval "${NSDIR}_CNT=''"
}

ckstats() {
  HOST=$1
  LABEL="$2"
  NSDIR="$3"
  EXPECTED="$4"
  $RNDCCMD $HOST stats
  NEW_CNT=$(sed -n -e 's/[	 ]*\([0-9]*\).response policy.*/\1/p' \
    $NSDIR/named.stats | tail -1)
  eval "OLD_CNT=\$${NSDIR}_CNT"
  NEW_CNT=$((NEW_CNT))
  OLD_CNT=$((OLD_CNT))
  GOT=$((NEW_CNT - OLD_CNT))
  if test "$GOT" -ne "$EXPECTED"; then
    setret "wrong $LABEL $NSDIR statistics of $GOT instead of $EXPECTED"
  fi
  eval "${NSDIR}_CNT=$NEW_CNT"
}

ckstatsrange() {
  HOST=$1
  LABEL="$2"
  NSDIR="$3"
  MIN="$4"
  MAX="$5"
  $RNDCCMD $HOST stats
  NEW_CNT=$(sed -n -e 's/[	 ]*\([0-9]*\).response policy.*/\1/p' \
    $NSDIR/named.stats | tail -1)
  eval "OLD_CNT=\$${NSDIR}_CNT"
  NEW_CNT=$((NEW_CNT))
  OLD_CNT=$((OLD_CNT))
  GOT=$((NEW_CNT - OLD_CNT))
  if test "$GOT" -lt "$MIN" -o "$GOT" -gt "$MAX"; then
    setret "wrong $LABEL $NSDIR statistics of $GOT instead of ${MIN}..${MAX}"
  fi
  eval "${NSDIR}_CNT=$NEW_CNT"
}

# $1=message
# $2=optional test file name
start_group() {
  ret=0
  t=$((t + 1))
  test -n "$1" && date "+${TS}checking $1 (${t})" | cat_i
  TEST_FILE=$2
  if test -n "$TEST_FILE"; then
    GROUP_NM="-$TEST_FILE"
    load_db
  else
    GROUP_NM=
  fi
  dnsrps_loaded
  TEST_NUM=0
}

end_group() {
  if test -n "$TEST_FILE"; then
    # remove the previous set of test rules
    copy_setports $TEST_FILE tmp
    sed -e 's/[	 ]add[	 ]/ delete /' tmp | $NSUPDATE
    rm -f tmp
    TEST_FILE=
  fi
  ckalive $ns3 "failed; ns3 server crashed and restarted"
  dnsrps_loaded
  GROUP_NM=
}

clean_result() {
  if test -z "$SAVE_RESULTS"; then
    rm -f $*
  fi
}

# $1=dig args
# $2=other dig output file
ckresult() {
  #ckalive "$1" "server crashed by 'dig $1'" || return 1
  expr "$1" : 'TCP ' >/dev/null && tcp=1 || tcp=0
  digarg=${1#TCP }

  if grep "flags:.* aa .*ad;" $DIGNM; then
    setret "'dig $digarg' AA and AD set;"
  elif grep "flags:.* aa .*ad;" $DIGNM; then
    setret "'dig $digarg' AD set;"
  fi

  if $PERL ../digcomp.pl $DIGNM $2 >/dev/null; then
    grep -q 'Truncated, retrying in TCP' $DIGNM && trunc=1 || trunc=0
    if [ "$tcp" -ne "$trunc" ]; then
      setret "'dig $digarg' wrong; no or unexpected truncation in $DIGNM"
    else
      clean_result ${DIGNM}*
    fi
    return 0
  fi
  setret "'dig $digarg' wrong; diff $DIGNM $2"
  return 0
}

# check only that the server does not crash
# $1=target domain
# $2=optional query type
nocrash() {
  digcmd $* >/dev/null
  ckalive "$*" "server crashed by 'dig $*'"
}

# check rewrite to NXDOMAIN
# $1=target domain
# $2=optional query type
nxdomain() {
  make_dignm
  digcmd $* \
    | sed -e 's/^[a-z].*	IN	CNAME	/;xxx &/' \
      -e 's/^[a-z].*	IN	RRSIG	/;xxx &/' \
      >$DIGNM
  ckresult "$*" proto.nxdomain
}

# check rewrite to NODATA
# $1=target domain
# $2=optional query type
nodata() {
  make_dignm
  digcmd $* \
    | sed -e 's/^[a-z].*	IN	CNAME	/;xxx &/' >$DIGNM
  ckresult "$*" proto.nodata
}

# check rewrite to an address
#   modify the output so that it is easily compared, but save the original line
# $1=IPv4 address
# $2=digcmd args
# $3=optional TTL
addr() {
  ADDR=$1
  make_dignm
  digcmd $2 >$DIGNM
  #ckalive "$2" "server crashed by 'dig $2'" || return 1
  ADDR_ESC=$(echo "$ADDR" | sed -e 's/\./\\./g')
  ADDR_TTL=$(sed -n -e "s/^[-.a-z0-9]\{1,\}[	 ]*\([0-9]*\)	IN	AA*	${ADDR_ESC}\$/\1/p" $DIGNM)
  if test -z "$ADDR_TTL"; then
    setret "'dig $2' wrong; no address $ADDR record in $DIGNM"
    return 0
  fi
  if test -n "$3" && test "$ADDR_TTL" -ne "$3"; then
    setret "'dig $2' wrong; TTL=$ADDR_TTL instead of $3 in $DIGNM"
    return 0
  fi
  clean_result ${DIGNM}*
}

# Check that a response is not rewritten
#   Use $ns1 instead of the authority for most test domains, $ns2 to prevent
#   spurious differences for `dig +norecurse`
# $1=optional "TCP"
# remaining args for dig
nochange() {
  make_dignm
  digcmd $* >$DIGNM
  digcmd $* @$ns1 >${DIGNM}_OK
  ckresult "$*" ${DIGNM}_OK && clean_result ${DIGNM}_OK
}

nochange_ns10() {
  make_dignm
  digcmd $* >$DIGNM
  digcmd $* @$ns10 >${DIGNM}_OK
  ckresult "$*" ${DIGNM}_OK && clean_result ${DIGNM}_OK
}

# check against a 'here document'
here() {
  make_dignm
  sed -e 's/^[	 ]*//' >${DIGNM}_OK
  digcmd $* >$DIGNM
  ckresult "$*" ${DIGNM}_OK
}

# check dropped response
DROPPED='^;; no servers could be reached'
drop() {
  make_dignm
  digcmd $* >$DIGNM || true
  if grep "$DROPPED" $DIGNM >/dev/null; then
    clean_result ${DIGNM}*
    return 0
  fi
  setret "'dig $1' wrong; response in $DIGNM"
  return 0
}

nsd() {
  $NSUPDATE -p ${PORT} <<EOF
  server $1
  ttl 300
  update $2 $3 IN CNAME .
  update $2 $4 IN CNAME .
  send
EOF
  sleep 2
}

#
# generate prototype NXDOMAIN response to compare against.
#
make_proto_nxdomain() {
  digcmd nonexistent @$ns2 >proto.nxdomain || return 1
  grep "status: NXDOMAIN" proto.nxdomain >/dev/null || return 1
  return 0
}

#
# generate prototype NODATA response to compare against.
#
make_proto_nodata() {
  digcmd txt-only.tld2 @$ns2 >proto.nodata || return 1
  grep "status: NOERROR" proto.nodata >/dev/null || return 1
  return 0
}

# ensure that the fast-expire zone is populated before we begin testing
$RNDCCMD $ns3 retransfer fast-expire

native=0
dnsrps=0
for mode in native dnsrps; do
  status=0
  case ${mode} in
    native)
      if [ -e dnsrps-only ]; then
        echo_i "'dnsrps-only' found: skipping native RPZ sub-test"
        continue
      else
        echo_i "running native RPZ sub-test"
      fi
      ;;
    dnsrps)
      if [ -e dnsrps-off ]; then
        echo_i "'dnsrps-off' found: skipping DNSRPS sub-test"
        continue
      fi
      echo_i "attempting to configure servers with DNSRPS..."
      stop_server --use-rndc --port ${CONTROLPORT}
      $SHELL ./setup.sh -N -D $DEBUG
      for server in ns*; do
        resetstats $server
      done
      sed -n 's/^## //p' dnsrps.conf | cat_i
      if grep '^#fail' dnsrps.conf >/dev/null; then
        echo_i "exit status: 1"
        exit 1
      fi
      if grep '^#skip' dnsrps.conf >/dev/null; then
        echo_i "DNSRPS sub-test skipped"
        continue
      else
        echo_i "running DNSRPS sub-test"
        start_server --noclean --restart --port ${PORT}
        sleep 3
      fi
      ;;
  esac

  # make prototype files to check against rewritten results
  retry_quiet 10 make_proto_nxdomain
  retry_quiet 10 make_proto_nodata

  start_group "QNAME rewrites" test1
  nochange .                            # 1 do not crash or rewrite root
  nxdomain a0-1.tld2                    # 2
  nodata a3-1.tld2                      # 3
  nodata a3-2.tld2                      # 4 nodata at DNAME itself
  nochange sub.a3-2.tld2                # 5 miss where DNAME might work
  nxdomain a4-2.tld2                    # 6 rewrite based on CNAME target
  nxdomain a4-2-cname.tld2              # 7
  nodata a4-3-cname.tld2                # 8
  addr 12.12.12.12 a4-1.sub1.tld2       # 9 A replacement
  addr 12.12.12.12 a4-1.sub2.tld2       # 10 A replacement with wildcard
  addr 12.12.12.12 nxc1.sub1.tld2       # 11 replace NXDOMAIN with CNAME
  addr 12.12.12.12 nxc2.sub1.tld2       # 12 replace NXDOMAIN with CNAME chain
  addr 127.4.4.1 a4-4.tld2              # 13 prefer 1st conflicting QNAME zone
  nochange a6-1.tld2                    # 14
  addr 127.6.2.1 a6-2.tld2              # 15
  addr 56.56.56.56 a3-6.tld2            # 16 wildcard CNAME
  addr 57.57.57.57 a3-7.sub1.tld2       # 17 wildcard CNAME
  addr 127.0.0.16 a4-5-cname3.tld2      # 18 CNAME chain
  addr 127.0.0.17 a4-6-cname3.tld2      # 19 stop short in CNAME chain
  nochange a5-2.tld2 +norecurse         # 20 check that RD=1 is required
  nochange a5-3.tld2 +norecurse         # 21
  nochange a5-4.tld2 +norecurse         # 22
  nochange sub.a5-4.tld2 +norecurse     # 23
  nxdomain c1.crash2.tld3               # 24 assert in rbtdb.c
  nxdomain a0-1.tld2 +dnssec            # 25 simple DO=1 without signatures
  nxdomain a0-1.tld2s +nodnssec         # 26 simple DO=0 with signatures
  nochange a0-1.tld2s +dnssec           # 27 simple DO=1 with signatures
  nxdomain a0-1s-cname.tld2s +dnssec    # 28 DNSSEC too early in CNAME chain
  nochange a0-1-scname.tld2 +dnssec     # 29 DNSSEC on target in CNAME chain
  nochange a0-1.tld2s srv +auth +dnssec # 30 no write for DNSSEC and no record
  nxdomain a0-1.tld2s srv +nodnssec     # 31
  drop a3-8.tld2 any                    # 32 drop
  nochange TCP a3-9.tld2                # 33 tcp-only
  here x.servfail <<'EOF'               # 34 qname-wait-recurse yes
    ;; status: SERVFAIL, x
EOF
  addr 35.35.35.35 "x.servfail @$ns5" # 35 qname-wait-recurse no
  end_group
  ckstats $ns3 test1 ns3 22
  ckstats $ns5 test1 ns5 1
  ckstats $ns6 test1 ns6 0

  start_group "NXDOMAIN/NODATA action on QNAME trigger" test1
  nxdomain a0-1.tld2 @$ns6                 # 1
  nodata a3-1.tld2 @$ns6                   # 2
  nodata a3-2.tld2 @$ns6                   # 3 nodata at DNAME itself
  nxdomain a4-2.tld2 @$ns6                 # 4 rewrite based on CNAME target
  nxdomain a4-2-cname.tld2 @$ns6           # 5
  nodata a4-3-cname.tld2 @$ns6             # 6
  addr 12.12.12.12 "a4-1.sub1.tld2 @$ns6"  # 7 A replacement
  addr 12.12.12.12 "a4-1.sub2.tld2 @$ns6"  # 8 A replacement with wildcard
  addr 127.4.4.1 "a4-4.tld2 @$ns6"         # 9 prefer 1st conflicting QNAME zone
  addr 12.12.12.12 "nxc1.sub1.tld2 @$ns6"  # 10 replace NXDOMAIN w/ CNAME
  addr 12.12.12.12 "nxc2.sub1.tld2 @$ns6"  # 11 replace NXDOMAIN w/ CNAME chain
  addr 127.6.2.1 "a6-2.tld2 @$ns6"         # 12
  addr 56.56.56.56 "a3-6.tld2 @$ns6"       # 13 wildcard CNAME
  addr 57.57.57.57 "a3-7.sub1.tld2 @$ns6"  # 14 wildcard CNAME
  addr 127.0.0.16 "a4-5-cname3.tld2 @$ns6" # 15 CNAME chain
  addr 127.0.0.17 "a4-6-cname3.tld2 @$ns6" # 16 stop short in CNAME chain
  nxdomain c1.crash2.tld3 @$ns6            # 17 assert in rbtdb.c
  nxdomain a0-1.tld2 +dnssec @$ns6         # 18 simple DO=1 without sigs
  nxdomain a0-1s-cname.tld2s +dnssec @$ns6 # 19
  drop a3-8.tld2 any @$ns6                 # 20 drop
  end_group
  ckstatsrange $ns3 test1 ns3 22 30
  ckstats $ns5 test1 ns5 0
  ckstats $ns6 test1 ns6 0

  start_group "IP rewrites" test2
  nodata a3-1.tld2                    # 1 NODATA
  nochange a3-2.tld2                  # 2 no policy record so no change
  nochange a4-1.tld2                  # 3 obsolete PASSTHRU record style
  nxdomain a4-2.tld2                  # 4
  nochange a4-2.tld2 -taaaa           # 5 no A => no policy rewrite
  nochange a4-2.tld2 -ttxt            # 6 no A => no policy rewrite
  nxdomain a4-2.tld2 -tany            # 7 no A => no policy rewrite
  nodata a4-3.tld2                    # 8
  nxdomain a3-1.tld2 -taaaa           # 9 IPv6 policy
  nochange a4-1-aaaa.tld2 -taaaa      # 10
  addr 127.0.0.1 a5-1-2.tld2          # 11 prefer smallest policy address
  addr 127.0.0.1 a5-3.tld2            # 12 prefer first conflicting IP zone
  nochange a5-4.tld2 +norecurse       # 13 check that RD=1 is required for #14
  addr 14.14.14.14 a5-4.tld2          # 14 prefer QNAME to IP
  nochange a4-4.tld2                  # 15 PASSTHRU
  nxdomain c2.crash2.tld3             # 16 assert in rbtdb.c
  addr 127.0.0.17 "a4-4.tld2 -b $ns1" # 17 client-IP address trigger
  nxdomain a7-1.tld2                  # 18 secondary policy zone (RT34450)
  # updating an response zone policy
  cp ns2/blv2.tld2.db.in ns2/bl.tld2.db
  rndc_reload ns2 $ns2 bl.tld2
  ck_soa 2 bl.tld2 $ns3
  nochange a7-1.tld2 # 19 PASSTHRU
  # ensure that a clock tick has occurred so that named will do the reload
  sleep 1
  cp ns2/blv3.tld2.db.in ns2/bl.tld2.db
  rndc_reload ns2 $ns2 bl.tld2
  ck_soa 3 bl.tld2 $ns3
  nxdomain a7-1.tld2 # 20 secondary policy zone (RT34450)
  end_group
  ckstats $ns3 test2 ns3 12

  # check that IP addresses for previous group were deleted from the radix tree
  start_group "radix tree deletions"
  nochange a3-1.tld2
  nochange a3-2.tld2
  nochange a4-1.tld2
  nochange a4-2.tld2
  nochange a4-2.tld2 -taaaa
  nochange a4-2.tld2 -ttxt
  nochange a4-2.tld2 -tany
  nochange a4-3.tld2
  nochange a3-1.tld2 -tAAAA
  nochange a4-1-aaaa.tld2 -tAAAA
  nochange a5-1-2.tld2
  end_group
  ckstats $ns3 'radix tree deletions' ns3 0

  # these tests assume "min-ns-dots 0"
  start_group "NSDNAME rewrites" test3
  nextpart ns3/named.run >/dev/null
  nochange a3-1.tld2                     # 1
  nochange a3-1.tld2 +dnssec             # 2 this once caused problems
  nxdomain a3-1.sub1.tld2                # 3 NXDOMAIN *.sub1.tld2 by NSDNAME
  nxdomain a3-1.subsub.sub1.tld2         # 4
  nxdomain a3-1.subsub.sub1.tld2 -tany   # 5
  addr 12.12.12.12 a4-2.subsub.sub2.tld2 # 6 walled garden for *.sub2.tld2
  nochange a3-2.tld2.                    # 7 exempt rewrite by name
  nochange a0-1.tld2.                    # 8 exempt rewrite by address block
  addr 12.12.12.12 a4-1.tld2             # 9 prefer QNAME policy to NSDNAME
  addr 127.0.0.1 a3-1.sub3.tld2          # 10 prefer policy for largest NSDNAME
  addr 127.0.0.2 a3-1.subsub.sub3.tld2   # 11
  nxdomain xxx.crash1.tld2               # 12 dns_db_detachnode() crash

  nxdomain a3-1.stub                     # 13
  nxdomain a3-1.static-stub              # 14
  nochange_ns10 a3-1.stub-nomatch        # 15
  nochange_ns10 a3-1.static-stub-nomatch # 16
  if [ "$mode" = dnsrps ]; then
    addr 12.12.12.12 as-ns.tld5. # 17 qname-as-ns
  fi
  nextpart ns3/named.run | grep -q "unrecognized NS rpz_rrset_find() failed: glue" \
    && setret "seen: unrecognized NS rpz_rrset_find() failed: glue"
  end_group
  if [ "$mode" = dnsrps ]; then
    ckstats $ns3 test3 ns3 10
  else
    ckstats $ns3 test3 ns3 9
  fi

  # these tests assume "min-ns-dots 0"
  start_group "NSIP rewrites" test4
  nextpart ns3/named.run >/dev/null
  nxdomain a3-1.tld2                     # 1 NXDOMAIN for all of tld2
  nochange a3-2.tld2.                    # 2 exempt rewrite by name
  nochange a0-1.tld2.                    # 3 exempt rewrite by address block
  nochange a3-1.tld4                     # 4 different NS IP address
  nxdomain a4-1.stub                     # 5
  nxdomain a4-1.static-stub              # 6
  nochange_ns10 a4-1.stub-nomatch        # 7
  nochange_ns10 a4-1.static-stub-nomatch # 8
  if [ "$mode" = dnsrps ]; then
    addr 12.12.12.12 as-ns.tld5. # 9 ip-as-ns
  fi
  nextpart ns3/named.run | grep -q "unrecognized NS rpz_rrset_find() failed: glue" \
    && setret "seen: unrecognized NS rpz_rrset_find() failed: glue"
  end_group

  start_group "walled garden NSIP rewrites" test4a
  addr 41.41.41.41 a3-1.tld2     # 1 walled garden for all of tld2
  addr 2041::41 'a3-1.tld2 AAAA' # 2 walled garden for all of tld2
  here a3-1.tld2 TXT <<'EOF'     # 3 text message for all of tld2
    ;; status: NOERROR, x
    a3-1.tld2.	    x	IN	TXT   "NSIP walled garden"
EOF
  end_group
  if [ "$mode" = dnsrps ]; then
    ckstats $ns3 test4 ns3 7
  else
    ckstats $ns3 test4 ns3 6
  fi

  # policies in ./test5 overridden by response-policy{} in ns3/named.conf
  # and in ns5/named.conf
  start_group "policy overrides" test5
  addr 127.0.0.1 a3-1.tld2            # 1 bl-given
  nochange a3-2.tld2                  # 2 bl-passthru
  nochange a3-3.tld2                  # 3 bl-no-op (obsolete for passthru)
  nochange a3-4.tld2                  # 4 bl-disabled
  nodata a3-5.tld2                    # 5 bl-nodata zone recursive-only no
  nodata a3-5.tld2 +norecurse         # 6 bl-nodata zone recursive-only no
  nodata a3-5.tld2                    # 7 bl-nodata not needed
  nxdomain a3-5.tld2 +norecurse @$ns5 # 8 bl-nodata global recursive-only no
  nxdomain a3-5.tld2s @$ns5           # 9 bl-nodata global break-dnssec
  nxdomain a3-5.tld2s +dnssec @$ns5   # 10 bl-nodata global break-dnssec
  nxdomain a3-6.tld2                  # 11 bl-nxdomain
  here a3-7.tld2 -tany <<'EOF'        # 12
    ;; status: NOERROR, x
    a3-7.tld2.	    x	IN	CNAME   txt-only.tld2.
    txt-only.tld2.  x	IN	TXT     "txt-only-tld2"
EOF
  addr 58.58.58.58 a3-8.tld2             # 13 bl_wildcname
  addr 59.59.59.59 a3-9.sub9.tld2        # 14 bl_wildcname
  addr 12.12.12.12 a3-15.tld2            # 15 bl-garden via CNAME to a12.tld2
  addr 127.0.0.16 a3-16.tld2 100         # 16 bl	max-policy-ttl 100
  addr 17.17.17.17 "a3-17.tld2 @$ns5" 90 # 17 ns5 bl max-policy-ttl 90
  drop a3-18.tld2 any                    # 18 bl-drop
  nxdomain TCP a3-19.tld2                # 19 bl-tcp-only
  end_group
  ckstats $ns3 test5 ns3 12
  ckstats $ns5 test5 ns5 4

  # check that miscellaneous bugs are still absent
  start_group "crashes" test6
  for Q in RRSIG SIG ANY 'ANY +dnssec'; do
    nocrash a3-1.tld2 -t$Q
    nocrash a3-2.tld2 -t$Q
    nocrash a3-5.tld2 -t$Q
    nocrash www.redirect -t$Q
    nocrash www.credirect -t$Q
  done

  # This is not a bug, because any data leaked by writing 24.4.3.2.10.rpz-ip
  # (or whatever) is available by publishing "foo A 10.2.3.4" and then
  # resolving foo.
  # nxdomain 32.3.2.1.127.rpz-ip
  end_group
  ckstats $ns3 bugs ns3 8

  # superficial test for major performance bugs
  QPERF=$(sh qperf.sh)
  if test -n "$QPERF"; then
    perf() {
      date "+${TS}checking performance $1" | cat_i
      # Dry run to prime everything
      comment "before dry run $1"
      $RNDCCMD $ns5 notrace
      $QPERF -c -1 -l30 -d ns5/requests -s $ns5 -p ${PORT} >/dev/null
      comment "before real test $1"
      PFILE="ns5/$2.perf"
      $QPERF -c -1 -l30 -d ns5/requests -s $ns5 -p ${PORT} >$PFILE
      comment "after test $1"
      X=$(sed -n -e 's/.*Returned *\([^ ]*:\) *\([0-9]*\) .*/\1\2/p' $PFILE \
        | tr '\n' ' ')
      if test "$X" != "$3"; then
        setret "wrong results '$X' in $PFILE"
      fi
      ckalive $ns5 "failed; server #5 crashed"
    }
    trim() {
      sed -n -e 's/.*Queries per second: *\([0-9]*\).*/\1/p' ns5/$1.perf
    }

    # get qps with rpz
    perf 'with RPZ' rpz 'NOERROR:2900 NXDOMAIN:100 '
    RPZ=$(trim rpz)
    # turn off rpz and measure qps again
    echo "# RPZ off" >ns5/rpz-switch
    RNDCCMD_OUT=$($RNDCCMD $ns5 reload)
    perf 'without RPZ' norpz 'NOERROR:3000 '
    NORPZ=$(trim norpz)

    PERCENT=$(((RPZ * 100 + (NORPZ / 2)) / NORPZ))
    echo_i "$RPZ qps with RPZ is $PERCENT% of $NORPZ qps without RPZ"

    MIN_PERCENT=30
    if test "$PERCENT" -lt $MIN_PERCENT; then
      echo_i "$RPZ qps with rpz or $PERCENT% is below $MIN_PERCENT% of $NORPZ qps"
    fi

    if test "$PERCENT" -ge 100; then
      echo_i "$RPZ qps with RPZ or $PERCENT% of $NORPZ qps without RPZ is too high"
    fi

    ckstats $ns5 performance ns5 200

  else
    echo_i "performance not checked; queryperf not available"
  fi

  if [ "$mode" = dnsrps ]; then
    echo_i "checking that dnsrpzd is automatically restarted"
    OLD_PID=$(cat dnsrpzd.pid)
    kill "$OLD_PID"
    n=0
    while true; do
      NEW_PID=$(cat dnsrpzd.pid 2>/dev/null)
      if test -n "$NEW_PID" -a "0$OLD_PID" -ne "0$NEW_PID"; then
        #echo "OLD_PID=$OLD_PID  NEW_PID=$NEW_PID"
        break
      fi
      $DIG -p ${PORT} +short +norecurse a0-1.tld2 @$ns3 >/dev/null
      n=$((n + 1))
      if test "$n" -gt $TEN_SECS; then
        setret "dnsrpzd did not restart"
        break
      fi
      $WAIT_CMD
    done
  fi

  # Ensure ns3 manages to transfer the fast-expire zone before shutdown.
  nextpartreset ns3/named.run
  wait_for_log 20 "zone fast-expire/IN: transferred serial 1" ns3/named.run

  # reconfigure the ns5 primary server without the fast-expire zone, so
  # it can't be refreshed on ns3, and will expire in 5 seconds.
  cat /dev/null >ns5/expire.conf
  rndc_reconfig ns5 10.53.0.5

  # restart the main test RPZ server to see if that creates a core file
  if test -z "$HAVE_CORE"; then
    stop_server --use-rndc --port ${CONTROLPORT} ns3
    restart 3 "rebuild-bl-rpz"
    HAVE_CORE=$(find ns* -name '*core*' -print)
    test -z "$HAVE_CORE" || setret "found $HAVE_CORE; memory leak?"
  fi

  # look for complaints from lib/dns/rpz.c and bin/name/query.c
  for runfile in ns*/named.run; do
    EMSGS=$(nextpart $runfile | grep -E -l 'invalid rpz|rpz.*failed' || true)
    if test -n "$EMSGS"; then
      setret "error messages in $runfile starting with:"
      grep -E 'invalid rpz|rpz.*failed' ns*/named.run \
        | sed -e '10,$d' -e 's/^//' | cat_i
    fi
  done

  if [ native = "$mode" ]; then
    # restart the main test RPZ server with a bad zone.
    t=$((t + 1))
    echo_i "checking that ns3 with broken rpz does not crash (${t})"
    stop_server --use-rndc --port ${CONTROLPORT} ns3
    cp ns3/broken.db.in ns3/bl.db
    restart 3 # do not rebuild rpz zones
    nocrash a3-1.tld2 -tA
    stop_server --use-rndc --port ${CONTROLPORT} ns3
    restart 3 "rebuild-bl-rpz"

    t=$((t + 1))
    echo_i "checking if rpz survives a certain class of failed reconfiguration attempts (${t})"
    sed -e "s/^#BAD//" <ns3/named.conf.in >ns3/named.conf.tmp
    copy_setports ns3/named.conf.tmp ns3/named.conf
    rm ns3/named.conf.tmp
    $RNDCCMD $ns3 reconfig >/dev/null 2>&1 && setret "failed"
    sleep 1
    copy_setports ns3/named.conf.in ns3/named.conf
    $RNDCCMD $ns3 reconfig || setret "failed"

    # reload a RPZ zone that is now deliberately broken.
    t=$((t + 1))
    echo_i "checking rpz failed update will keep previous rpz rules (${t})"
    $DIG -p ${PORT} @$ns3 walled.tld2 >dig.out.$t.before
    grep "walled\.tld2\..*IN.*A.*10\.0\.0\.1" dig.out.$t.before >/dev/null || setret "failed"
    cp ns3/broken.db.in ns3/manual-update-rpz.db
    rndc_reload ns3 $ns3 manual-update-rpz
    sleep 1
    # ensure previous RPZ rules still apply.
    $DIG -p ${PORT} @$ns3 walled.tld2 >dig.out.$t.after
    grep "walled\.tld2\..*IN.*A.*10\.0\.0\.1" dig.out.$t.after >/dev/null || setret "failed"

    t=$((t + 1))
    echo_i "checking reload of a mixed-case RPZ zone (${t})"
    # First, a sanity check: the A6-2.TLD2.mixed-case-rpz RPZ record should
    # cause a6-2.tld2 NOERROR answers to be rewritten to NXDOMAIN answers.
    $DIG -p ${PORT} @$ns3 a6-2.tld2. A >dig.out.$t.before
    grep "status: NXDOMAIN" dig.out.$t.before >/dev/null || setret "failed"
    # Add a sibling name (a6-1.tld2.mixed-case-rpz, with "tld2" in lowercase
    # rather than uppercase) before A6-2.TLD.mixed-case-rpz.
    nextpart ns3/named.run >/dev/null
    cp ns3/mixed-case-rpz-2.db.in ns3/mixed-case-rpz.db
    rndc_reload ns3 $ns3 mixed-case-rpz
    wait_for_log 20 "rpz: mixed-case-rpz: reload done" ns3/named.run
    # a6-2.tld2 NOERROR answers should still be rewritten to NXDOMAIN answers.
    # (The bug we try to trigger here caused a6-2.tld2.mixed-case-rpz to be
    # erroneously removed from the summary RPZ database after reload.)
    $DIG -p ${PORT} @$ns3 a6-2.tld2. A >dig.out.$t.after
    grep "status: NXDOMAIN" dig.out.$t.after >/dev/null || setret "failed"
  fi

  t=$((t + 1))
  echo_i "checking that ttl values are not zeroed when qtype is '*' (${t})"
  $DIG +noall +answer -p ${PORT} @$ns3 any a3-2.tld2 >dig.out.$t
  ttl=$(awk '/a3-2 tld2 text/ {print $2}' dig.out.$t)
  if test ${ttl:=0} -eq 0; then setret "failed"; fi

  t=$((t + 1))
  echo_i "checking rpz updates/transfers with parent nodes added after children (${t})"
  # regression test for RT #36272: the success condition
  # is the secondary server not crashing.
  for i in 1 2 3 4 5; do
    nsd $ns5 add example.com.policy1. '*.example.com.policy1.'
    nsd $ns5 delete example.com.policy1. '*.example.com.policy1.'
  done
  for i in 1 2 3 4 5; do
    nsd $ns5 add '*.example.com.policy1.' example.com.policy1.
    nsd $ns5 delete '*.example.com.policy1.' example.com.policy1.
  done

  t=$((t + 1))
  echo_i "checking that going from an empty policy zone works (${t})"
  nsd $ns5 add '*.x.servfail.policy2.' x.servfail.policy2.
  sleep 1
  rndc_reload ns7 $ns7 policy2
  $DIG z.x.servfail -p ${PORT} @$ns7 >dig.out.${t}
  grep NXDOMAIN dig.out.${t} >/dev/null || setret "failed"

  t=$((t + 1))
  echo_i "checking that "add-soa no" at rpz zone level works (${t})"
  $DIG z.x.servfail -p ${PORT} @$ns7 >dig.out.${t}
  grep SOA dig.out.${t} >/dev/null && setret "failed"

  if [ native = "$mode" ]; then
    t=$((t + 1))
    echo_i "checking that "add-soa yes" at response-policy level works (${t})"
    $DIG walled.tld2 -p ${PORT} +noall +add @$ns3 >dig.out.${t}
    grep "^manual-update-rpz\..*SOA" dig.out.${t} >/dev/null || setret "failed"
  fi

  if [ native = "$mode" ]; then
    t=$((t + 1))
    echo_i "reconfiguring server with 'add-soa no' (${t})"
    cp ns3/named.conf ns3/named.conf.tmp
    sed -e "s/add-soa yes/add-soa no/g" <ns3/named.conf.tmp >ns3/named.conf
    rndc_reconfig ns3 $ns3
    echo_i "checking that 'add-soa no' at response-policy level works (${t})"
    $DIG walled.tld2 -p ${PORT} +noall +add @$ns3 >dig.out.${t}
    grep "^manual-update-rpz\..*SOA" dig.out.${t} >/dev/null && setret "failed"
  fi

  if [ native = "$mode" ]; then
    t=$((t + 1))
    echo_i "checking that 'add-soa unset' works (${t})"
    $DIG walled.tld2 -p ${PORT} +noall +add @$ns8 >dig.out.${t}
    grep "^manual-update-rpz\..*SOA" dig.out.${t} >/dev/null || setret "failed"
  fi

  # dnsrps does not allow NS RRs in policy zones, so this check
  # with dnsrps results in no rewriting.
  if [ native = "$mode" ]; then
    t=$((t + 1))
    echo_i "checking rpz with delegation fails correctly (${t})"
    $DIG -p ${PORT} @$ns3 ns example.com >dig.out.$t
    grep "status: SERVFAIL" dig.out.$t >/dev/null || setret "failed"

    t=$((t + 1))
    echo_i "checking policies from expired zone are no longer in effect ($t)"
    $DIG -p ${PORT} @$ns3 a expired >dig.out.$t
    grep "expired.*10.0.0.10" dig.out.$t >/dev/null && setret "failed"
    grep "fast-expire/IN: response-policy zone expired" ns3/named.run >/dev/null || setret "failed"
  fi

  # RPZ 'CNAME *.' (NODATA) trumps DNS64.  Test against various DNS64 scenarios.
  for label in a-only no-a-no-aaaa a-plus-aaaa; do
    for type in AAAA A; do
      t=$((t + 1))
      case $label in
        a-only)
          echo_i "checking rpz 'CNAME *.' (NODATA) with dns64, $type lookup with A-only (${t})"
          ;;
        no-a-no-aaaa)
          echo_i "checking rpz 'CNAME *.' (NODATA) with dns64, $type lookup with no A or AAAA (${t})"
          ;;
        a-plus-aaaa)
          echo_i "checking rpz 'CNAME *.' (NODATA) with dns64, $type lookup with A and AAAA (${t})"
          ;;
      esac
      ret=0
      $DIG ${label}.example -p ${PORT} $type @10.53.0.9 >dig.out.${t}
      grep "status: NOERROR" dig.out.$t >/dev/null || ret=1
      grep "ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 2$" dig.out.$t >/dev/null || ret=1
      grep "^rpz" dig.out.$t >/dev/null || ret=1
      [ $ret -eq 0 ] || echo_i "failed"
      status=$((status + ret))
    done
  done

  if [ native = "$mode" ]; then
    t=$((t + 1))
    echo_i "checking that rewriting CD=1 queries handles pending data correctly (${t})"
    $RNDCCMD $ns3 flush
    $RNDCCMD $ns6 flush
    $DIG a7-2.tld2s -p ${PORT} @$ns6 +cd >dig.out.${t}
    grep -w "1.1.1.1" dig.out.${t} >/dev/null || setret "failed"
  fi

  [ $status -ne 0 ] && pf=fail || pf=pass
  case $mode in
    native)
      native=$status
      echo_i "status (native RPZ sub-test): $status ($pf)"
      ;;

    dnsrps)
      dnsrps=$status
      echo_i "status (DNSRPS sub-test): $status ($pf)"
      ;;
    *) echo_i "invalid test mode" ;;
  esac
done
status=$((native + dnsrps))

[ $status -eq 0 ] || exit 1
