Files
jarl/scripts/e2e.sh
T
adamandClaude Opus 5.5 9fc23310bd Add blocking: blocklists and block/allow rules with BIND response policy zones
jarld downloads blocklists (Adblock, hosts or plain domains), checks them
daily and reloads named only when a list changed; your own block and
allow rules take precedence, and the most specific rule wins. Lists and
rules apply per view (default: every view that offers recursion). Web UI
Blocking page with name checks, jarl blocklist and jarl block, the
/blocking API, blocked counts in the statistics. Database schema 6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 09:24:19 +00:00

473 lines
24 KiB
Bash
Executable File

#!/bin/bash
# End-to-end test: builds a legacy BIND setup, starts an unprivileged named,
# imports and adopts it with jarld, runs jarld, and drives every jarl CLI
# command, checking the answers with dig. Needs no root.
#
# BIND tools come from $JARL_BIND_ROOT (an extracted bind9 package tree, see
# docs/development.md) or from PATH. Run from the repository root:
#
# make e2e or JARL_BIND_ROOT=... scripts/e2e.sh
#
# Set KEEP=1 to leave named and jarld running afterwards (in $WORK).
set -u
ROOT=$(cd "$(dirname "$0")/.." && pwd)
WORK=${WORK:-$(mktemp -d /tmp/jarl-e2e.XXXXXX)}
E=$WORK
DNS=${DNS_PORT:-25353}; CTL=${CTL_PORT:-25953}; HTTP=${HTTP_PORT:-28053}; LISTS=${LISTS_PORT:-28054}
if [ -n "${JARL_BIND_ROOT:-}" ]; then
B=$JARL_BIND_ROOT
export LD_LIBRARY_PATH=$(echo $B/usr/lib/*-linux-gnu | cut -d' ' -f1)
NAMED=$B/usr/sbin/named RNDC=$B/usr/sbin/rndc TSIGKEYGEN=$B/usr/sbin/tsig-keygen
CHECKCONF=$B/usr/bin/named-checkconf CHECKZONE=$B/usr/bin/named-checkzone DIG=$B/usr/bin/dig
else
NAMED=$(command -v named || echo /usr/sbin/named) RNDC=$(command -v rndc || echo /usr/sbin/rndc)
TSIGKEYGEN=$(command -v tsig-keygen || echo /usr/sbin/tsig-keygen)
CHECKCONF=$(command -v named-checkconf) CHECKZONE=$(command -v named-checkzone) DIG=$(command -v dig)
fi
for t in "$NAMED" "$RNDC" "$TSIGKEYGEN" "$CHECKCONF" "$CHECKZONE" "$DIG"; do
[ -x "$t" ] || { echo "missing BIND tool: $t (set JARL_BIND_ROOT)"; exit 2; }
done
mkdir -p $E/legacy $E/cache $E/data
JARLD=$E/jarld J=$E/jarl
(cd "$ROOT" && go build -o $JARLD ./cmd/jarld && go build -o $J ./cmd/jarl) || exit 1
NAMED_PID= JARLD_PID= LISTS_PID=
cleanup() {
[ -n "$LISTS_PID" ] && kill $LISTS_PID 2>/dev/null
if [ "${KEEP:-}" = 1 ]; then echo "left running in $WORK (named $NAMED_PID, jarld $JARLD_PID)"; return; fi
[ -n "$JARLD_PID" ] && kill $JARLD_PID 2>/dev/null
[ -n "$NAMED_PID" ] && kill $NAMED_PID 2>/dev/null
wait 2>/dev/null
rm -rf "$WORK"
}
trap cleanup EXIT
# ---- legacy setup, import, adopt, serve
$TSIGKEYGEN -a hmac-sha256 rndc-key > $E/legacy/rndc.key
cat > $E/legacy/named.conf <<EOF
include "$E/legacy/named.conf.options";
include "$E/legacy/named.conf.local";
include "$E/legacy/rndc.key";
controls { inet 127.0.0.1 port $CTL allow { 127.0.0.1; } keys { "rndc-key"; }; };
EOF
cat > $E/legacy/named.conf.options <<EOF
acl homelab { 127.0.0.0/8; 192.168.1.0/24; };
options {
directory "$E/cache";
pid-file "$E/cache/named.pid";
session-keyfile "$E/cache/session.key";
listen-on port $DNS { 127.0.0.1; };
listen-on-v6 { none; };
recursion yes;
allow-recursion { homelab; };
allow-query { any; };
};
EOF
cat > $E/legacy/named.conf.local <<EOF
zone "adbraun.test" { type master; file "$E/legacy/db.adbraun.test"; };
zone "1.168.192.in-addr.arpa" { type master; file "$E/legacy/db.192.168.1"; };
EOF
cat > $E/legacy/db.adbraun.test <<'EOF'
$TTL 3600
@ IN SOA ns1.adbraun.test. admin.adbraun.test. ( 2024051201 3600 900 1209600 300 )
IN NS ns1
ns1 IN A 127.0.0.1
nas IN A 192.168.1.20 ; synology
www 300 IN CNAME nas
EOF
cat > $E/legacy/db.192.168.1 <<'EOF'
$TTL 86400
@ IN SOA ns1.adbraun.test. admin.adbraun.test. ( 2024051201 3600 900 1209600 300 )
@ IN NS ns1.adbraun.test.
20 IN PTR nas.adbraun.test.
EOF
# -f like the Debian unit (-g would send all logging, including the
# statistics response log, to stderr).
$NAMED -c $E/legacy/named.conf -f -n 1 > $E/named.log 2>&1 &
NAMED_PID=$!
for i in $(seq 50); do $RNDC -k $E/legacy/rndc.key -s 127.0.0.1 -p $CTL status >/dev/null 2>&1 && break; sleep 0.2; done
# The local socket (a short path: unix socket paths are limited to 107 bytes).
SOCK=${XDG_RUNTIME_DIR:-/tmp}/jarl-e2e-$HTTP.sock
cat > $E/jarl.yaml <<EOF
listen: ["127.0.0.1:$HTTP"]
socket: $SOCK
database: $E/data/jarl.db
staging_dir: $E/data/staging
log: {level: info}
bind:
main_config: $E/legacy/named.conf
config_dir: $E/etc-jarl
zone_dir: $E/zones
cache_dir: $E/cache/jarl
file_group: ""
named_user: $(id -un)
named_checkconf: $CHECKCONF
named_checkzone: $CHECKZONE
rndc: $RNDC
rndc_args: ["-k", "$E/legacy/rndc.key", "-s", "127.0.0.1", "-p", "$CTL"]
backup:
dir: $E/backups
EOF
cd $E
echo "secret-pass-123" | $JARLD -c jarl.yaml init --password-stdin --token --nameservers ns1.adbraun.test. --contact admin@adbraun.test > init.out 2>&1 || { cat init.out; exit 1; }
grep -o 'jarl_[A-Za-z0-9_]*' init.out | head -1 > admin.token
$JARLD -c jarl.yaml import > import.out 2>&1 || { cat import.out; exit 1; }
$JARLD -c jarl.yaml adopt > adopt.out 2>&1 || { cat adopt.out; exit 1; }
$JARLD -c jarl.yaml serve > jarld.log 2>&1 &
JARLD_PID=$!
for i in $(seq 50); do curl -s -o /dev/null http://127.0.0.1:$HTTP/healthz && break; sleep 0.2; done
echo "setup done in $WORK"
# ---- CLI walkthrough
export JARL_CLI_CONFIG=$E/cli.yaml
unset JARL_SERVER JARL_TOKEN JARL_CHANGESET
PASS=0; FAIL=0
dig() { $DIG +short +norec -p $DNS @127.0.0.1 "$@" | sort | tr '\n' ' ' | sed 's/ $//'; }
ok() { echo "PASS: $1"; PASS=$((PASS+1)); }
bad() { echo "FAIL: $1"; FAIL=$((FAIL+1)); }
# run DESC EXPECTED_RC CMD...: runs a command and checks its exit code;
# the output is kept in $OUT.
run() {
local desc=$1 want=$2; shift 2
OUT=$("$@" 2>&1); local rc=$?
if [ "$rc" = "$want" ]; then ok "$desc (rc=$rc)"; else bad "$desc: rc=$rc want $want"; echo "$OUT" | sed 's/^/ /'; fi
}
expect_dig() { # DESC WANT DIG-ARGS...
local desc=$1 want=$2; shift 2
local got; got=$(dig "$@")
if [ "$got" = "$want" ]; then ok "dig $desc = '$want'"; else bad "dig $desc: got '$got' want '$want'"; fi
}
contains() { if echo "$OUT" | grep -q -- "$1"; then ok "output has '$1'"; else bad "output lacks '$1'"; echo "$OUT" | sed 's/^/ /'; fi; }
# With nothing configured, the CLI uses jarld's local socket, where
# jarld's own user (this script's) is an administrator without a token.
run "socket: nothing configured uses local jarld" 0 env JARL_SOCKET=$SOCK $J whoami
contains "admin (everything)"
run "socket: missing socket is explained" 1 env JARL_SOCKET=${XDG_RUNTIME_DIR:-/tmp}/jarl-e2e-none.sock $J status
contains "no local jarld"
run "config set server" 0 $J config set server http://127.0.0.1:$HTTP
run "config set token (from stdin)" 0 sh -c "$J config set token < $E/admin.token"
run "config show masks token" 0 $J config show
contains "jarl_"
run "status" 0 $J status
contains "Adopted:.*yes"
run "whoami" 0 $J whoami
contains "admin (everything)"
run "zone list" 0 $J zone list
contains "adbraun.test"
run "socket: -s local with a server configured" 0 env JARL_SOCKET=$SOCK $J -s local zone list
contains "adbraun.test"
# Records
run "record add with PTR" 0 $J record add adbraun.test web A 192.168.1.30 --ptr --ttl 300
expect_dig "web A" "192.168.1.30" web.adbraun.test A
expect_dig "PTR 30" "web.adbraun.test." 30.1.168.192.in-addr.arpa PTR
run "record disable with PTR" 0 $J record disable adbraun.test web A --ptr
expect_dig "disabled web A gone" "" web.adbraun.test A
expect_dig "disabled PTR 30 gone" "" 30.1.168.192.in-addr.arpa PTR
run "record list marks it" 0 $J record list adbraun.test --disabled
contains "\[disabled\]"
run "disabling twice says so (exit 5)" 5 $J record disable adbraun.test web A
contains "already disabled"
run "record enable with PTR" 0 $J record enable adbraun.test web A --ptr
run "record list -o id" 0 $J record list adbraun.test --name web -t A -o id
if echo "$OUT" | grep -qx '[0-9][0-9]*'; then ok "-o id is just the ID"; else bad "-o id gave '$OUT'"; fi
run "disable by the listed ID" 0 $J record disable --id "$OUT"
run "zone show -o field" 0 $J zone show adbraun.test -o field=name,record_count,disabled_count
contains "^adbraun.test. [0-9]* 1$"
run "enable by ID" 0 $J record enable --id "$($J record list adbraun.test --name web -t A -o id)"
expect_dig "enabled web A back" "192.168.1.30" web.adbraun.test A
expect_dig "enabled PTR 30 back" "web.adbraun.test." 30.1.168.192.in-addr.arpa PTR
run "CNAME conflict rejected (exit 6)" 6 $J record add adbraun.test www A 192.168.1.99
contains "CNAME"
run "record add TXT with spaces" 0 $J record add adbraun.test @ TXT "v=spf1 mx -all"
expect_dig "apex TXT" '"v=spf1 mx -all"' adbraun.test TXT
run "record set RRset" 0 $J record set adbraun.test api A 192.168.1.40 192.168.1.41 --ttl 60
expect_dig "api A" "192.168.1.40 192.168.1.41" api.adbraun.test A
run "dry run changes nothing" 0 $J record add adbraun.test dry A 192.168.1.50 --dry-run
contains "dry run"
expect_dig "dry A" "" dry.adbraun.test A
run "invalid IPv4 rejected" 6 $J record add adbraun.test bad A 192.168.1.300
run "record list json" 0 $J record list adbraun.test -o json
contains '"relative_name": "api"'
# Batch file
cat > $E/batch.yaml <<'EOF'
description: e2e batch
changes:
- add: {zone: adbraun.test, name: printer, type: A, data: 192.168.1.60, ptr: true}
- delete: {zone: adbraun.test, name: api, type: A, data: 192.168.1.41}
- set: {zone: adbraun.test, name: mail, type: MX, data: ["10 nas"]}
EOF
run "apply -f batch" 0 $J apply -f $E/batch.yaml
expect_dig "printer A" "192.168.1.60" printer.adbraun.test A
expect_dig "api A after batch" "192.168.1.40" api.adbraun.test A
expect_dig "mail MX" "10 nas.adbraun.test." mail.adbraun.test MX
cat > $E/badbatch.yaml <<'EOF'
changes:
- add: {zone: adbraun.test, name: ok1, type: A, data: 192.168.1.70}
- add: {zone: adbraun.test, name: www, type: TXT, data: "conflicts with CNAME"}
EOF
run "bad batch is all-or-nothing" 6 $J apply -f $E/badbatch.yaml
expect_dig "ok1 not created" "" ok1.adbraun.test A
# Changesets
CS=$($J changeset create "e2e staged" -q)
run "stage into changeset" 0 $J record add adbraun.test staged A 192.168.1.80 --changeset $CS
expect_dig "staged not live yet" "" staged.adbraun.test A
run "changeset diff" 0 $J changeset diff $CS
contains "staged"
run "changeset apply" 0 $J changeset apply $CS
expect_dig "staged now live" "192.168.1.80" staged.adbraun.test A
REV=$($J changeset revert $CS -q)
run "apply revert changeset" 0 $J changeset apply $REV
expect_dig "staged reverted" "" staged.adbraun.test A
run "history" 0 $J history
contains "e2e batch"
# ACLs, views, options, TSIG
run "acl create" 0 $J acl create trusted 10.0.0.0/8 192.168.1.0/24
run "acl update add" 0 $J acl update trusted --add 172.16.0.0/12
run "acl list" 0 $J acl list
contains "172.16.0.0/12"
run "acl delete in-use refused" 5 sh -c "$J option set allow-transfer '{ trusted; }' --zone adbraun.test && $J acl delete trusted"
run "option unset" 0 $J option unset allow-transfer --zone adbraun.test
run "acl delete" 0 $J acl delete trusted
run "tsig create" 0 $J tsig create xfer-test
contains "secret:"
run "tsig rotate" 0 $J tsig rotate xfer-test
run "tsig show named-conf" 0 $J tsig show xfer-test --named-conf
contains 'algorithm hmac-sha256'
run "tsig delete" 0 $J tsig delete xfer-test
run "option describe" 0 $J option describe allow-recursion
run "option list" 0 $J option list
contains "allow-recursion"
run "check" 0 $J check
contains "valid"
run "search" 0 $J search printer
contains "printer"
# Users, tokens, ACME
run "user create acme service account" 0 $J user create certbot --no-password --role acme --comment "e2e"
run "token create for certbot" 0 sh -c "$J token create certbot-hook --user certbot --role acme --expires 30d -q > $E/acme.token"
ACME="--token @$E/acme.token"
run "acme whoami" 0 $J $ACME whoami
contains "_acme-challenge"
run "acme set" 0 $J $ACME acme set adbraun.test value-one
expect_dig "acme TXT" '"value-one"' _acme-challenge.adbraun.test TXT
run "acme set again (idempotent)" 0 $J $ACME acme set adbraun.test value-one
run "acme set wildcard second value" 0 $J $ACME acme set '*.adbraun.test' value-two
expect_dig "acme TXT both" '"value-one" "value-two"' _acme-challenge.adbraun.test TXT
run "acme token cannot touch A records (exit 3)" 3 $J $ACME record add adbraun.test evil A 192.168.1.66
run "certbot hooks" 0 env CERTBOT_DOMAIN=nas.adbraun.test CERTBOT_VALIDATION=cb-value $J $ACME acme certbot-auth
expect_dig "certbot TXT" '"cb-value"' _acme-challenge.nas.adbraun.test TXT
run "certbot cleanup" 0 env CERTBOT_DOMAIN=nas.adbraun.test CERTBOT_VALIDATION=cb-value $J $ACME acme certbot-cleanup
expect_dig "certbot TXT gone" "" _acme-challenge.nas.adbraun.test TXT
run "acme unset one value" 0 $J $ACME acme unset adbraun.test value-one
expect_dig "acme TXT one left" '"value-two"' _acme-challenge.adbraun.test TXT
run "acme unset all" 0 $J $ACME acme unset adbraun.test
expect_dig "acme TXT none" "" _acme-challenge.adbraun.test TXT
run "acme unset when nothing there" 0 $J $ACME acme unset adbraun.test
run "acme in unknown zone" 1 $J $ACME acme set example.org v
run "token list --all" 0 $J token list --all
contains "certbot-hook"
cp $E/acme.token $E/acme.token.old
run "acme token rotates itself (grace)" 0 $J $ACME token rotate --current --save --grace 1h
run "rotated token works" 0 $J $ACME whoami
run "old secret works during grace" 0 $J --token @$E/acme.token.old whoami
run "rotate again without grace" 0 $J $ACME token rotate --current --save
run "old secret refused after rotation (exit 3)" 3 $J --token @$E/acme.token.old whoami
run "rotated token keeps its grants" 0 $J $ACME acme set adbraun.test rotated
run "acme unset after rotation" 0 $J $ACME acme unset adbraun.test rotated
run "acme token cannot rotate the admin's token (exit 4)" 4 $J $ACME token rotate 1
run "token list shows rotation" 0 $J token list --user certbot
contains "certbot-hook"
TID=$($J token list --user certbot -o json | grep -o '"id": [0-9]*' | head -1 | grep -o '[0-9]*')
run "token revoke" 0 $J token revoke $TID
run "revoked token refused (exit 3)" 3 $J $ACME whoami
run "user update disable" 0 $J user update certbot --disabled
run "user list" 0 $J user list
run "user delete" 0 $J user delete certbot -y
run "audit" 0 $J audit --limit 10
contains "token.revoke"
# Views
run "view create (adopts zones)" 0 $J view create internal --match-clients homelab --adopt-zones
run "view create external" 0 $J view create external --match-clients any
run "zone create in external" 0 $J zone create adbraun.test --view external --ns ns1.adbraun.test. --record "ns1 A 203.0.113.1"
run "zone list shows both views" 0 $J zone list
contains "external"
expect_dig "internal view nas" "192.168.1.20" nas.adbraun.test A
run "record add needs a view now" 6 $J record add adbraun.test x A 192.168.1.90
run "acme set goes into both views" 0 $J acme set adbraun.test both-views
expect_dig "acme TXT (internal, from 127.0.0.1)" '"both-views"' _acme-challenge.adbraun.test TXT
run "acme set --view external only" 0 $J acme unset adbraun.test --view internal
expect_dig "acme TXT removed from internal" "" _acme-challenge.adbraun.test TXT
run "zone file" 0 $J zone file adbraun.test --view external
contains "_acme-challenge"
# Name server checks: the external zone's NS (203.0.113.1) doesn't answer;
# the internal one lists only this server.
run "zone serials: internal, only this server" 0 $J zone serials adbraun.test --view internal
contains "this server"
run "zone serials: unreachable secondary (exit 2)" 2 $J zone serials adbraun.test --view external
contains "NO ANSWER"
# Server header and token expiry warnings
if curl -sI http://127.0.0.1:$HTTP/healthz | grep -qi "^Server: Jarl/"; then ok "Server header"; else bad "no Server: Jarl header"; fi
SOON=$($J token create soon --expires 3d -q)
run "expiring token works" 0 $J --token "$SOON" whoami
contains "warning: the API token expires"
run "token list --expiring" 0 $J token list --expiring
contains "soon"
run "status lists the expiring token" 0 $J status
contains "Token:.*soon"
# DNSSEC: sign the external view's zone (the parent can't be asked here).
api() { curl -s -H "Authorization: Bearer $(cat $E/admin.token)" -H 'Content-Type: application/json' "$@"; }
run "dnssec status: off" 0 $J zone dnssec status adbraun.test --view external
contains "Policy:.*none"
run "dnssec enable" 0 $J zone dnssec enable adbraun.test --view external
contains "named is signing"
for i in $(seq 50); do $J zone dnssec status adbraun.test --view external 2>/dev/null | grep -q "signed serial" && break; sleep 0.2; done
run "dnssec status: signing" 0 $J zone dnssec status adbraun.test --view external
contains "CSK.*ECDSAP256SHA256"
contains "named:.*signed serial"
contains "DS record for the parent zone"
contains "not yet: wait until"
run "dnssec: switching off at once is refused" 0 api -X PATCH -d '{"dnssec_policy":""}' "http://127.0.0.1:$HTTP/api/v1/zones/adbraun.test?view=external"
contains "switching DNSSEC off at once"
run "dnssec: a TTL over a day is refused" 6 $J record add adbraun.test longttl A 203.0.113.9 --view external --ttl 172800
contains "max-zone-ttl"
run "record in the signed zone" 0 $J record add adbraun.test signed A 203.0.113.5 --view external
run "check-ds without a reachable parent (exit 2)" 2 $J zone dnssec check-ds adbraun.test --view external --resolver 127.0.0.1
contains "named wasn't told anything"
KEYS=$(ls $E/zones/dnssec-keys/*/Kadbraun.test.+013+*.private 2>/dev/null | head -1)
if [ -n "$KEYS" ]; then ok "named's key is in Jarl's key directory"; else bad "no key in $E/zones/dnssec-keys"; fi
# Backups and restore
run "jarld backup" 0 $JARLD -c jarl.yaml backup
contains "Database backed up to $E/backups/jarl-"
contains "with 3 DNSSEC key files"
run "status shows the backup" 0 $J status
contains "Backup:.*$E/backups/jarl-"
run "restore refused while jarld runs" 1 $JARLD -c jarl.yaml restore latest --yes
contains "jarld is running"
run "a second jarld is refused" 1 $JARLD -c jarl.yaml serve
contains "in use by a running jarld"
run "record after the backup" 0 $J record add adbraun.test afterbackup A 192.168.1.99 --view internal
expect_dig "afterbackup A" "192.168.1.99" afterbackup.adbraun.test A
kill $JARLD_PID; wait $JARLD_PID 2>/dev/null
KEYSUM=$(sha256sum "$KEYS" | cut -d' ' -f1)
rm -f "$KEYS"
run "jarld restore lists backups" 0 $JARLD -c jarl.yaml restore
contains "jarl-.*\.db"
run "jarld restore latest" 0 $JARLD -c jarl.yaml restore latest --yes
contains "previous database is in"
contains "Put back 3 DNSSEC key files"
if [ "$(sha256sum "$KEYS" 2>/dev/null | cut -d' ' -f1)" = "$KEYSUM" ]; then ok "the private key is back"; else bad "the private key wasn't restored"; fi
$JARLD -c jarl.yaml serve >> jarld.log 2>&1 &
JARLD_PID=$!
for i in $(seq 50); do curl -s -o /dev/null http://127.0.0.1:$HTTP/healthz && break; sleep 0.2; done
run "restored database lacks the later record" 0 $J record list adbraun.test --view internal
if echo "$OUT" | grep -q afterbackup; then bad "afterbackup still there after restore"; else ok "afterbackup gone after restore"; fi
run "resync after restore" 0 $J resync --force
contains "serial"
expect_dig "afterbackup gone from named" "" afterbackup.adbraun.test A
run "audit shows the restore" 0 $J audit --action system.restore
contains "system.restore"
# Turning DNSSEC off: insecure first; the DS never reached a parent, so
# named can finish at once. Then wipe the old keys.
run "dnssec disable starts turning off" 0 $J zone dnssec disable adbraun.test --view external --yes
contains "remove the DS record"
for i in $(seq 75); do $J zone dnssec status adbraun.test --view external 2>/dev/null | grep -q "Turning DNSSEC off: the parent has no DS\|has finished" && break; sleep 0.2; done
run "dnssec disable finishes" 0 $J zone dnssec disable adbraun.test --view external --yes
contains "DNSSEC is off"
run "dnssec status: old keys left" 0 $J zone dnssec status adbraun.test --view external
contains "keys from when the zone was signed"
run "dnssec wipe" 0 $J zone dnssec wipe adbraun.test --view external --yes --force --resolver 127.0.0.1
contains "removed .*Kadbraun.test"
run "dnssec status: clean" 0 $J zone dnssec status adbraun.test --view external
contains "Status:.*DNSSEC is off.$"
# DNSSEC for a zone whose parent is in Jarl: the DS goes in as a record.
run "child zone" 0 $J zone create sub.adbraun.test --view external --ns ns1.adbraun.test.
run "delegate the child" 0 $J record add adbraun.test sub NS ns1.adbraun.test. --view external
run "dnssec enable the child" 0 $J zone dnssec enable sub.adbraun.test --view external
for i in $(seq 50); do $J zone dnssec status sub.adbraun.test --view external 2>/dev/null | grep -q "signed serial" && break; sleep 0.2; done
run "child status gives the jarl command" 0 $J zone dnssec status sub.adbraun.test --view external
contains "in the parent zone adbraun.test, which is in Jarl"
contains "jarl record add adbraun.test sub DS [0-9]* 13 2 [0-9A-F]* --view external"
DSCMD=$(echo "$OUT" | grep -o "jarl record add adbraun.test sub DS .*")
run "the printed command adds the DS" 0 $J ${DSCMD#jarl }
run "the parent's zone file has the DS" 0 $J zone file adbraun.test --view external
contains "^sub.*IN.DS.$(echo "$DSCMD" | sed 's/.* DS \(.*\) --view.*/\1/')"
run "child status: parent has it" 0 $J zone dnssec status sub.adbraun.test --view external
contains "adbraun.test has it."
run "remove the child's DS" 0 $J record delete adbraun.test sub DS --view external --yes
run "wipe the child" 0 $J zone dnssec wipe sub.adbraun.test --view external --yes --force --resolver 127.0.0.1
run "delete the child" 0 $J zone delete sub.adbraun.test --view external --yes
run "remove the delegation" 0 $J record delete adbraun.test sub NS --view external --yes
# Blocking (docs/blocking.md). It only changes answers to recursive
# queries, so these ask with recursion and look at which policy zone (if
# any) the answer came from.
rpz() {
$DIG +tries=1 +time=3 -p $DNS @127.0.0.1 "$1" A | awk '$4 == "SOA" && $1 ~ /jarl\.invalid\.$/ {print $1; f = 1} END {if (!f) print "none"}'
}
expect_rpz() { # DESC NAME WANT: named reloads in the background, so wait a little
local got
for i in $(seq 30); do got=$(rpz "$2"); [ "$got" = "$3" ] && break; sleep 0.2; done
if [ "$got" = "$3" ]; then ok "$1: $2 -> $3"; else bad "$1: $2 -> $got, want $3"; fi
}
mkdir -p $E/lists
printf '# test list\n||tracker.adbraun.test^\n0.0.0.0 ads.adbraun.test\nnot a domain\n' > $E/lists/list.txt
(cd $E/lists && exec python3 -m http.server --bind 127.0.0.1 $LISTS) > /dev/null 2>&1 &
LISTS_PID=$!
run "block add" 0 $J block add blocked.adbraun.test
expect_rpz "a rule blocks the name" blocked.adbraun.test rules.jarl.invalid.
expect_rpz "and the names below it" a.blocked.adbraun.test rules.jarl.invalid.
expect_rpz "other names are answered" www.adbraun.test none
run "named.conf has the policy" 0 grep -q 'response-policy' $E/etc-jarl/named.conf
for i in $(seq 25); do curl -s -o /dev/null http://127.0.0.1:$LISTS/list.txt && break; sleep 0.2; done
run "blocklist add" 0 $J blocklist add test http://127.0.0.1:$LISTS/list.txt
run "blocklist refresh" 0 $J blocklist refresh test
contains "test: 2 domains (1 lines skipped)"
expect_rpz "an Adblock entry blocks the names below it" x.tracker.adbraun.test test.blocklist.jarl.invalid.
expect_rpz "a hosts entry blocks the name" ads.adbraun.test test.blocklist.jarl.invalid.
expect_rpz "but not the names below it" sub.ads.adbraun.test none
run "blocklist list" 0 $J blocklist list
contains "^test *ok *2 "
run "block allow" 0 $J block allow ok.tracker.adbraun.test
expect_rpz "an allow rule beats the list" deep.ok.tracker.adbraun.test none
expect_rpz "the rest of the list still blocks" x.tracker.adbraun.test test.blocklist.jarl.invalid.
run "block check" 0 $J block check deep.ok.tracker.adbraun.test
contains "allowed by your rule"
# The test server dates the file to the second, and the download above
# remembers that date: change it a second later, as a real list would.
sleep 1.1
printf '||new.adbraun.test^\n' > $E/lists/list.txt
run "blocklist refresh after the list changed" 0 $J blocklist refresh test
expect_rpz "the new version applies" x.new.adbraun.test test.blocklist.jarl.invalid.
expect_rpz "and replaces the old one" x.tracker.adbraun.test none
printf '<html>gone</html>\n' > $E/lists/list.txt
run "a broken download fails (exit 2)" 2 $J blocklist refresh test --force
contains "no domains found"
expect_rpz "and the previous version stays" x.new.adbraun.test test.blocklist.jarl.invalid.
run "blocklist disable" 0 $J blocklist disable test
expect_rpz "a disabled list doesn't block" x.new.adbraun.test none
run "blocklist delete" 0 $J blocklist delete test --yes
run "block delete" 0 $J block delete blocked.adbraun.test
expect_rpz "a deleted rule doesn't block" blocked.adbraun.test none
run "block delete (allow rule)" 0 $J block delete ok.tracker.adbraun.test
run "no blocking left in named.conf" 1 grep -q 'response-policy' $E/etc-jarl/named.conf
kill $LISTS_PID 2>/dev/null; LISTS_PID=
run "status at the end" 0 $J status
contains "Drift:.*none"
echo
echo "RESULT: $PASS passed, $FAIL failed"
[ $FAIL = 0 ]