#!/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}; METRICS=${METRICS_PORT:-28055} 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 < $E/legacy/named.conf.options < $E/legacy/named.conf.local < $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 < 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; } not_contains() { if echo "$OUT" | grep -q -- "$1"; then bad "output has '$1'"; echo "$OUT" | sed 's/^/ /'; else ok "output lacks '$1'"; 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" # View keys: a query signed with a view's key lands in that view, from # any address (this host is in homelab, so unsigned it gets internal). run "view key" 0 sh -c "$J view key external --named-conf > $E/external.key" expect_dig "unsigned from here: the internal view" "127.0.0.1" ns1.adbraun.test A expect_dig "signed with the external view's key" "203.0.113.1" -k $E/external.key ns1.adbraun.test A run "view key shows a dig command" 0 $J view key external contains "dig -y hmac-sha256:jarl-view-external:" # named-checkconf's own warnings stay on the dashboard after the apply. run "option set recursion no (external)" 0 $J option set recursion no --view external run "allow-recursion without its braces" 0 $J option set allow-recursion 192.0.2.1 --view external contains "named-checkconf" run "status keeps named's warning" 0 $J status contains "named-checkconf: .*recursion" run "option list: braces added" 0 $J option list --view external contains "{ 192.0.2.1; }" run "unset allow-recursion" 0 $J option unset allow-recursion --view external run "unset recursion" 0 $J option unset recursion --view external run "status: the warning is gone" 0 $J status not_contains "named-checkconf" 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 "record add --view all" 0 $J record add adbraun.test both A 192.0.2.77 --view all contains "applied" expect_dig "both views: internal has it" "192.0.2.77" both.adbraun.test A run "record list external has it too" 0 $J record list adbraun.test --view external contains "192.0.2.77" run "several views are all-or-nothing (exit 4)" 4 $J record add adbraun.test half A 192.0.2.78 --view internal,nosuch contains "nothing was changed in any view" expect_dig "so internal didn't get it" "" half.adbraun.test A run "record delete --view internal,external" 0 $J record delete adbraun.test both A --view internal,external -y expect_dig "deleted in both" "" both.adbraun.test A run "and gone from external" 0 $J record list adbraun.test --view external not_contains "192.0.2.77" 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 # Disabled zones: kept in Jarl, not loaded by named. run "zone for disabling" 0 $J zone create parked.test --view internal --ns ns1.adbraun.test. run "record in it" 0 $J record add parked.test www A 192.0.2.70 --view internal expect_dig "parked www served" "192.0.2.70" www.parked.test A run "zone disable" 0 $J zone disable parked.test --view internal contains "recursion" expect_dig "disabled zone not served" "" www.parked.test A run "named.conf says it's disabled" 0 grep -q 'zone "parked.test" is disabled in Jarl' $E/etc-jarl/named.conf run "zone list marks it" 0 $J zone list contains "parked.test.*primary (disabled)" run "editing a disabled zone works (no reload of a zone named lacks)" 0 $J record add parked.test api A 192.0.2.71 --view internal run "zone enable" 0 $J zone enable parked.test --view internal expect_dig "enabled zone served again" "192.0.2.70" www.parked.test A expect_dig "with the change made while disabled" "192.0.2.71" api.parked.test A run "delete it" 0 $J zone delete parked.test --view internal --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 *normal *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" run "block pause" 0 $J block pause 5m expect_rpz "paused: a listed name is answered" x.tracker.adbraun.test none expect_rpz "paused: so is a blocked rule's name" blocked.adbraun.test none run "block list says it's paused" 0 $J block list contains "Blocking is paused until" run "block resume" 0 $J block resume run "block pause indefinitely" 0 $J block pause indefinitely expect_rpz "paused until resumed: a listed name is answered" x.tracker.adbraun.test none run "block list says until resumed" 0 $J block list contains "paused until you resume it" run "block resume after an indefinite pause" 0 $J block resume expect_rpz "resumed: the list blocks again" x.tracker.adbraun.test test.blocklist.jarl.invalid. expect_rpz "resumed: the rule blocks again" blocked.adbraun.test rules.jarl.invalid. # 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 'gone\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. # Client groups and steps: per-device blocking in one view, by client # address. 127.0.0.2 and .3 are in homelab too, so they get view internal. rpz_from() { # SOURCE NAME $DIG +tries=1 +time=3 -b "$1" -p $DNS @127.0.0.1 "$2" A | awk '$4 == "SOA" && $1 ~ /jarl\.invalid\.$/ {print $1; f = 1} END {if (!f) print "none"}' } expect_rpz_from() { # DESC SOURCE NAME WANT local got for i in $(seq 30); do got=$(rpz_from "$2" "$3"); [ "$got" = "$4" ] && break; sleep 0.2; done if [ "$got" = "$4" ]; then ok "$1: $3 from $2 -> $4"; else bad "$1: $3 from $2 -> $got, want $4"; fi } printf '||social.adbraun.test^\n' > $E/lists/strict.txt run "block step add" 0 $J block step add strict run "blocklist add at a step" 0 $J blocklist add social http://127.0.0.1:$LISTS/strict.txt --step strict contains "applied" run "a step no group reaches: warned" 0 $J status contains "blocks nothing" run "block group add (off)" 0 $J block group add work 127.0.0.2 --step off run "block group add (strict, with an IPv6 network)" 0 $J block group add kids 127.0.0.3 2001:db8:77::/64 --step strict run "block group list" 0 $J block group list contains "kids *strict *127.0.0.3, 2001:db8:77::/64" expect_rpz_from "normal: the normal list" 127.0.0.1 x.new.adbraun.test test.blocklist.jarl.invalid. expect_rpz_from "normal: not the strict list" 127.0.0.1 x.social.adbraun.test none expect_rpz_from "off: no lists" 127.0.0.2 x.new.adbraun.test none expect_rpz_from "off: but your rules" 127.0.0.2 blocked.adbraun.test rules.jarl.invalid. expect_rpz_from "strict: the strict list" 127.0.0.3 x.social.adbraun.test social.blocklist.jarl.invalid. expect_rpz_from "strict: and the normal list" 127.0.0.3 x.new.adbraun.test test.blocklist.jarl.invalid. run "block check names the step" 0 $J block check x.social.adbraun.test contains "for clients at step strict and above" run "a step in use can't be deleted (exit 5)" 5 $J block step delete strict run "move the work laptop to normal" 0 $J block group update work --step normal expect_rpz_from "now normal" 127.0.0.2 x.new.adbraun.test test.blocklist.jarl.invalid. run "block group delete" 0 $J block group delete work run "block group delete kids" 0 $J block group delete kids run "blocklist delete social" 0 $J blocklist delete social --yes run "block step delete" 0 $J block step delete strict run "no gates left" 1 grep -q 'gate.jarl.invalid' $E/etc-jarl/named.conf 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= # Prometheus metrics (docs/metrics.md) run "metrics on the main listener" 0 curl -sf http://127.0.0.1:$HTTP/metrics contains "^jarl_named_up 1$" contains "^jarl_adopted 1$" contains '^jarl_problems{severity="error"} 0$' contains '^jarl_objects{kind="zones"} ' run "metrics on metrics.listen" 0 curl -sf http://127.0.0.1:$METRICS/metrics contains "^jarl_build_info{version=" run "metrics.listen serves nothing else" 0 curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:$METRICS/api/v1/zones contains "^404$" run "metrics with the token" 0 curl -sf -H "Authorization: Bearer e2e-metrics-token-0123456789" http://127.0.0.1:$METRICS/metrics run "metrics with a wrong token" 0 curl -s -o /dev/null -w '%{http_code}' -H "Authorization: Bearer wrong-token-0123456789" http://127.0.0.1:$METRICS/metrics contains "^401$" run "metrics on the socket" 0 curl -sf --unix-socket $SOCK http://jarl/metrics contains "^jarl_open_changesets " run "status at the end" 0 $J status contains "Drift:.*none" echo echo "RESULT: $PASS passed, $FAIL failed" [ $FAIL = 0 ]