#!/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} 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= cleanup() { 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 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; } 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" # 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 "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" run "status at the end" 0 $J status contains "Drift:.*none" echo echo "RESULT: $PASS passed, $FAIL failed" [ $FAIL = 0 ]