#!/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= # Certificates are tested against Pebble, Let's Encrypt's test CA, in # Docker (PEBBLE=0 skips them). It uses this test named to check # challenges, and the host's network. PEBBLE=${PEBBLE:-} if [ "$PEBBLE" != 0 ] && command -v docker >/dev/null && docker info >/dev/null 2>&1; then PEBBLE=jarl-e2e-pebble-$HTTP docker rm -f $PEBBLE >/dev/null 2>&1 docker run -d --name $PEBBLE --network host -e PEBBLE_VA_NOSLEEP=1 ghcr.io/letsencrypt/pebble:latest \ -config test/config/pebble-config.json -dnsserver 127.0.0.1:$DNS >/dev/null || PEBBLE= [ -n "$PEBBLE" ] && docker cp $PEBBLE:/test/certs/pebble.minica.pem $E/pebble-minica.pem >/dev/null else PEBBLE= fi cleanup() { [ -n "$LISTS_PID" ] && kill $LISTS_PID 2>/dev/null [ -n "$PEBBLE" ] && docker rm -f $PEBBLE >/dev/null 2>&1 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 <> $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 # JARL_CHANGESET: staging and the changeset commands default to it, and # it is checked before it is used. export JARL_CHANGESET=$($J changeset create "e2e env" -q) ENVCS=$JARL_CHANGESET run "stage via JARL_CHANGESET" 0 $J record add adbraun.test envstaged A 192.168.1.81 contains "changeset $ENVCS (e2e env, 0 operations) from JARL_CHANGESET" expect_dig "env-staged not live yet" "" envstaged.adbraun.test A run "changeset show defaults to it" 0 $J changeset show contains "envstaged" run "changeset diff defaults to it" 0 $J changeset diff contains "apply with 'jarl changeset apply'" OTHER=$($J changeset create "e2e other" -q) run "-C wins over JARL_CHANGESET" 0 $J changeset show -C $OTHER contains "e2e other" run "an ID argument wins too" 0 $J changeset discard $OTHER run "changeset apply defaults to it" 0 $J changeset apply contains "unset JARL_CHANGESET" expect_dig "env-staged live" "192.168.1.81" envstaged.adbraun.test A run "staging into the applied changeset is refused" 1 $J record add adbraun.test envlate A 192.168.1.82 contains "changeset $ENVCS is applied" run "applying it again is refused" 1 $J changeset apply contains "is applied" export JARL_CHANGESET=999999 run "a changeset that doesn't exist is explained" 1 $J record add adbraun.test envlate A 192.168.1.82 contains "there is no changeset 999999" run "reads don't care about a stale JARL_CHANGESET" 0 $J zone list unset JARL_CHANGESET expect_dig "nothing was staged or applied" "" envlate.adbraun.test A run "changeset apply without an ID or JARL_CHANGESET" 1 $J changeset apply contains "which changeset?" run "clean up envstaged" 0 $J record delete adbraun.test envstaged A --yes 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 # Address ranges: kept as written, written to named.conf as prefixes. run "acl with ranges" 0 $J acl create ranged "127.0.0.2 - 127.0.0.3" 2001:db8::100-2001:db8::1ff run "acl list keeps the ranges" 0 $J acl list contains "127.0.0.2-127.0.0.3" run "named.conf has the prefixes" 0 grep -A4 'acl "ranged"' $E/etc-jarl/named.conf contains "127.0.0.2/31;" contains "2001:db8::100/120;" contains "// 127.0.0.2-127.0.0.3" run "a reversed range is refused (exit 6)" 6 $J acl create badrange 127.0.0.9-127.0.0.1 contains "start is after the end" run "zone allow-query by the range" 0 $J option set allow-query '{ ranged; }' --zone adbraun.test range_dig() { $DIG +tries=1 +time=3 -b "$1" -p $DNS @127.0.0.1 ns1.adbraun.test A | awk '/status:/ {sub(",", "", $6); print $6}'; } for src in 127.0.0.2 127.0.0.3; do if [ "$(range_dig $src)" = NOERROR ]; then ok "range: $src may query"; else bad "range: $src got $(range_dig $src)"; fi done for src in 127.0.0.1 127.0.0.4; do if [ "$(range_dig $src)" = REFUSED ]; then ok "range: $src is refused"; else bad "range: $src got $(range_dig $src)"; fi done run "unset the zone's allow-query" 0 $J option unset allow-query --zone adbraun.test run "acl delete ranged" 0 $J acl delete ranged 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 # Someone else's changeset in JARL_CHANGESET isn't used, even by an admin. THEIRS=$($J $ACME changeset create "certbot's" -q) run "another user's changeset from JARL_CHANGESET is refused" 1 env JARL_CHANGESET=$THEIRS $J record add adbraun.test notmine A 192.168.1.83 contains "is certbot's" run "applying it from JARL_CHANGESET is refused" 1 env JARL_CHANGESET=$THEIRS $J changeset apply run "-C uses it anyway (admin)" 0 $J changeset show -C $THEIRS run "discard certbot's changeset" 0 $J changeset discard $THEIRS 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" # A reverse zone created after the record: "update PTR" alone adds it. run "an address record" 0 $J record add adbraun.test ldn A 192.0.2.154 run "its /32 reverse zone, afterwards" 0 $J zone create 192.0.2.154/32 LDN=$($J record list adbraun.test --name ldn -o id) run "record update --ptr adds the missing PTR" 0 $J record update $LDN --ptr expect_dig "the PTR is served" "ldn.adbraun.test." -x 192.0.2.154 run "remove the reverse zone again" 0 $J zone delete 154.2.0.192.in-addr.arpa -y run "and the record" 0 $J record delete adbraun.test ldn A -y # 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:" # Linked zones (docs/views.md#linked-zones): the same zone in both views, # shared records kept in step, local ones per view. run "zone link: what is the same is shared" 0 $J zone link adbraun.test --view internal --with external contains "differ between the views and stay local" run "zone show names the link" 0 $J zone show adbraun.test --view external contains "Linked with: *internal" run "record list shows shared and local" 0 $J record list adbraun.test --view external contains "ns1 *[0-9]*\\* *A *local *203.0.113.1" contains "@ *[0-9]*\\* *NS *shared" run "a shared add in internal" 0 $J record add adbraun.test linked A 198.51.100.7 --view internal expect_dig "reaches the external view" "198.51.100.7" -k $E/external.key linked.adbraun.test A expect_dig "and is in the internal view" "198.51.100.7" linked.adbraun.test A run "a local override in internal" 0 $J record add adbraun.test linked A 10.1.1.1 --view internal --local expect_dig "internal answers its own" "10.1.1.1" linked.adbraun.test A expect_dig "external keeps the shared one" "198.51.100.7" -k $E/external.key linked.adbraun.test A LID=$($J record list adbraun.test --view internal --name linked -o id) run "share internal's version" 0 $J record update $LID --shared expect_dig "now every view's" "10.1.1.1" -k $E/external.key linked.adbraun.test A run "--view all changes a link once" 0 $J record set adbraun.test linked A 10.1.1.2 --view all contains "linked with internal" expect_dig "both views" "10.1.1.2" -k $E/external.key linked.adbraun.test A run "zone diff: internal compared with external" 0 $J zone diff adbraun.test contains "^adbraun.test in internal, compared with external$" contains "^ linked: a difference is a view's own records" contains "^ external: [1-9][0-9]* answered differently, [0-9]* only in internal, [0-9]* only in external, [0-9]* the same" contains "^ANSWERED DIFFERENTLY" contains "^ ns1 A$" contains "^ internal 127.0.0.1 \\[local, id [0-9]*\\]" contains "^ external 203.0.113.1 \\[local, id [0-9]*\\]" contains "the same (--all lists them)" run "zone diff from external" 0 $J zone diff adbraun.test --view external contains "^adbraun.test in external, compared with internal" run "zone diff for one name" 0 $J zone diff adbraun.test ns1 A contains "^ ns1 A$" not_contains "the same (--all" run "a long TXT record" 0 $J record add adbraun.test longtxt TXT "$(printf 'x%.0s' $(seq 300))" --view internal --local run "zone diff --wide shows it whole" 0 $J zone diff adbraun.test longtxt --wide contains "x\\{250\\}" run "the compare API" 0 curl -sf -H "Authorization: Bearer $(cat $E/admin.token)" "http://127.0.0.1:$HTTP/api/v1/zones/adbraun.test/compare?view=internal&with=external&all=1" contains '"linked":true' contains '"base":"internal"' contains '"same_rows":\[' run "remove the long TXT" 0 $J record delete adbraun.test longtxt TXT --view internal -y run "a shared delete" 0 $J record delete adbraun.test linked A --view external -y expect_dig "gone from internal too" "" linked.adbraun.test A run "zone unlink" 0 $J zone unlink adbraun.test --view external run "unlinked: no link column" 0 $J record list adbraun.test --view external not_contains "shared" run "--local on a zone that isn't linked is fine" 0 $J record add adbraun.test solo A 10.9.9.9 --view external --local run "--shared there is refused (exit 6)" 6 $J record add adbraun.test solo2 A 10.9.9.8 --view external --shared contains "nothing to share" run "remove it" 0 $J record delete adbraun.test solo A --view external -y run "zone create --link-with: a linked copy" 0 $J zone create linkcopy.test --view internal --ns ns1.adbraun.test. run "a record in it" 0 $J record add linkcopy.test www A 192.0.2.80 --view internal run "copy it into external, linked" 0 $J zone create linkcopy.test --view external --link-with internal expect_dig "the copy answers" "192.0.2.80" -k $E/external.key www.linkcopy.test A run "delete the copy" 0 $J zone delete linkcopy.test --view external -y run "the other is unlinked again" 0 $J zone show linkcopy.test --view internal not_contains "Linked with" run "delete the other" 0 $J zone delete linkcopy.test --view internal -y # Internet exposure (docs/exposure.md): external lets in anyone, internal # only homelab. run "exposure" 0 $J exposure contains "^internal *private" contains "^external *public *off" contains "exposure_rate_limit " contains "fix: jarl exposure fix external exposure_rate_limit" run "fix: rate limiting, log-only first" 0 $J exposure fix external exposure_rate_limit run "named.conf has it" 0 grep -A2 'view "external"' $E/etc-jarl/named.conf contains "rate-limit { responses-per-second 10; log-only yes; };" run "exposure: log-only now" 0 $J exposure contains "^external *public *log-only" contains "exposure_rate_limit_log_only" # Statistics are off here; render/stats_test.go covers their statements. run "what Jarl adds by itself" 0 $J option generated contains "^include .*keys.conf" contains 'match-clients { !key "jarl-view-external"; key "jarl-view-internal"; ... };' not_contains "statistics-channels" run "generated, as JSON" 0 $J option generated -o json contains '"setting": "always, when there are views"' run "fix: start enforcing" 0 $J exposure fix external exposure_rate_limit_log_only contains "responses-per-second 10; }" # A burst of identical queries in the external view (signed with its key): # some are dropped. The internal view has no rate limit. DROPS=0 for i in $(seq 60); do $DIG +tries=1 +time=1 +ignore -k $E/external.key -p $DNS @127.0.0.1 ns1.adbraun.test A | grep -q "status:" || DROPS=$((DROPS+1)) done if [ $DROPS -gt 0 ]; then ok "rate limiting dropped $DROPS of 60"; else bad "rate limiting dropped nothing"; fi INTERNAL=$(for i in $(seq 30); do $DIG +tries=1 +time=1 +short -p $DNS @127.0.0.1 ns1.adbraun.test A; done | grep -c 127.0.0.1) if [ "$INTERNAL" = 30 ]; then ok "the internal view isn't limited"; else bad "the internal view answered $INTERNAL of 30"; fi run "remove the rate limit again" 0 $J option unset rate-limit --view external run "an open resolver on a public view needs confirming (exit 5)" 5 $J option set allow-recursion any --view external contains "accept-risk" run "with --accept-risk" 0 $J option set allow-recursion any --view external --accept-risk run "acknowledge it" 0 $J exposure ack external security_open_resolver --reason "e2e: on purpose" run "acknowledged: info, with the reason" 0 $J exposure contains "info *external *security_open_resolver .*acknowledged by .*e2e: on purpose" run "take it back" 0 $J exposure unack external security_open_resolver run "close the resolver again" 0 $J option unset allow-recursion --view external run "fix: hide the version" 0 $J exposure fix server exposure_version expect_dig "version.bind is hidden" "" -c CH version.bind TXT run "mark external private" 0 $J view update external --exposure private run "exposure: private by hand" 0 $J exposure contains "^external *private (set)" run "back to auto" 0 $J view update external --exposure auto # 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 # Reverse zones made easy (docs/reverse.md): suggested from the records' # addresses, created in the right view and filled, PTRs kept in step. run "zone for reverse" 0 $J zone create revtest.test --view internal --ns ns1.adbraun.test. run "its hosts" 0 $J record add revtest.test nas A 10.99.1.10 --view internal run "a second name for nas" 0 $J record add revtest.test files A 10.99.1.10 --view internal run "another host" 0 $J record add revtest.test pi A 10.99.1.11 --view internal run "and IPv6" 0 $J record add revtest.test nas AAAA fdab:99:0:10::10 --view internal run "suggest-reverse" 0 $J zone suggest-reverse contains "^yours *internal *10.99.1.0/24 *1.99.10.in-addr.arpa *2 *10.99.1.10 files.revtest.test, 10.99.1.11 pi.revtest.test" contains "^yours *internal *fdab:99::/48 *0.0.0.0.9.9.0.0.b.a.d.f.ip6.arpa *1" run "create it without a view, filled" 0 $J zone create 10.99.1.0/24 --fill contains "10.99.1.10 has several names" run "it went to the internal view" 0 $J zone show 1.99.10.in-addr.arpa contains "^View: *internal" contains "^Network: *10.99.1.0/24" not_contains "Missing PTRs" expect_dig "filled: PTR for pi" "pi.revtest.test." -x 10.99.1.11 expect_dig "filled: one name for nas" "files.revtest.test." -x 10.99.1.10 run "an empty /48 zone" 0 $J zone create fdab:99::/48 run "shows what it lacks" 0 $J zone show fdab:99::/48 contains "^Missing PTRs: *1 " run "fill-ptr" 0 $J zone fill-ptr fdab:99::/48 expect_dig "filled later: IPv6 PTR" "nas.revtest.test." -x fdab:99:0:10::10 run "fill-ptr again: nothing to add (exit 6)" 6 $J zone fill-ptr fdab:99::/48 contains "nothing to add" run "keep PTRs in step" 0 $J zone update revtest.test --view internal --auto-ptr run "zone show says so" 0 $J zone show revtest.test --view internal contains "^PTR records: *kept in step" run "a new host" 0 $J record add revtest.test cam A 10.99.1.12 --view internal expect_dig "its PTR came along" "cam.revtest.test." -x 10.99.1.12 CAM=$($J record list revtest.test --view internal -o json | python3 -c 'import json,sys; print([r["id"] for r in json.load(sys.stdin) if r["name"].startswith("cam.")][0])') run "the host moves" 0 $J record update $CAM --data 10.99.1.13 expect_dig "PTR followed: new address" "cam.revtest.test." -x 10.99.1.13 expect_dig "PTR followed: old address gone" "" -x 10.99.1.12 run "the host goes" 0 $J record delete --id $CAM expect_dig "PTR gone with it" "" -x 10.99.1.13 run "a web host's address" 0 $J record add revtest.test blog A 185.199.108.153 --view internal run "suggested as public" 0 $J zone suggest-reverse contains "^public *internal *185.199.108.153/32" run "dismiss it" 0 $J zone suggest-reverse --dismiss 185.199.108.153/32 --view internal run "gone from the suggestions" 0 $J zone suggest-reverse not_contains "185.199.108.153" run "suggest-reverse --create: yours only" 0 $J zone suggest-reverse --create run "none of yours missing now" 0 $J zone suggest-reverse not_contains "^yours" run "a view can turn suggestions off" 0 $J view update internal --reverse-suggestions=false run "then none for it" 0 $J zone suggest-reverse not_contains "internal" run "but --all still lists its networks" 0 $J zone suggest-reverse --all --view internal contains "^public *internal *185.199.108.153/32 .* suggestions off" run "and back on" 0 $J view update internal --reverse-suggestions # A zone created inside another takes the parent's records under it. run "a /16 reverse zone" 0 $J zone create 10.88.0.0/16 --view internal run "with a PTR made by hand" 0 $J record add 10.88.0.0/16 10.88.5.5 PTR printer.revtest.test --view internal expect_dig "the /16 answers" "printer.revtest.test." -x 10.88.5.5 run "a /24 inside it" 0 $J zone create 10.88.5.0/24 --view internal run "the PTR moved" 0 $J record list 10.88.5.0/24 --view internal contains "printer.revtest.test." expect_dig "and still answers, from the /24" "printer.revtest.test." -x 10.88.5.5 # 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 " # TLS certificates (docs/certificates.md), from Pebble. if [ -n "$PEBBLE" ]; then DOT=${DOT_PORT:-25854} curl -sf --cacert $E/pebble-minica.pem https://localhost:15000/roots/0 > $E/pebble-root.pem run "cert check: the challenge goes to Jarl's zone" 0 $J cert check --domain ns1.adbraun.test --domain '*.adbraun.test' contains "ns1.adbraun.test *jarl *_acme-challenge.ns1.adbraun.test" run "cert check: a name nobody serves (exit 1)" 1 $J cert check --domain www.nowhere.invalid contains "www.nowhere.invalid *none" run "cert create: obtained from the CA" 0 $J cert create ns1 --domain ns1.adbraun.test --domain '*.adbraun.test' --use web --use dns contains "Status: *ok" contains "Challenges: .*ns1.adbraun.test: jarl (zone adbraun.test)" run "the key for named is group-readable only" 0 stat -c %a $E/etc-jarl/tls/ns1/privkey.pem contains "^640$" expect_dig "the challenge records are gone" "" _acme-challenge.ns1.adbraun.test TXT run "jarld serves it over HTTPS" 0 curl -sf --cacert $E/pebble-root.pem --resolve ns1.adbraun.test:$HTTP:127.0.0.1 https://ns1.adbraun.test:$HTTP/healthz run "plain HTTP from this host still works" 0 curl -sf http://127.0.0.1:$HTTP/healthz run "named: tls block with the certificate" 0 $J option set tls "e2e { cert-file \"$E/etc-jarl/tls/ns1/fullchain.pem\"; key-file \"$E/etc-jarl/tls/ns1/privkey.pem\"; }" --toplevel run "named: a DoT listener" 0 $J option set listen-on "port $DOT tls e2e { 127.0.0.1; }" --add run "DNS over TLS answers, with Jarl's certificate" 0 $DIG +tls +tls-ca=$E/pebble-root.pem +tls-hostname=ns1.adbraun.test +short -p $DOT @127.0.0.1 nas.adbraun.test A contains "^192.168.1.20$" serial() { echo | openssl s_client -connect 127.0.0.1:$1 -servername ns1.adbraun.test 2>/dev/null | openssl x509 -noout -serial; } before=$(serial $DOT) run "cert issue: renewed" 0 $J cert issue ns1 after=$(serial $DOT) if [ -n "$before" ] && [ "$before" != "$after" ] && [ "$after" = "$(serial $HTTP)" ]; then ok "named and jarld serve the renewed certificate"; else bad "renewed certificate not served: named $before -> $after, jarld $(serial $HTTP)"; fi run "cert list" 0 $J cert list contains "ns1 *acme *dns,web *ns1.adbraun.test \\*.adbraun.test .* ok" run "a second certificate for the web UI is refused (exit 6)" 6 $J cert create other --domain nas.adbraun.test --use web run "certificate metrics" 0 curl -sf http://127.0.0.1:$HTTP/metrics contains '^jarl_certificate_failing{certificate="ns1",source="acme"} 0$' # Encrypted DNS set up by Jarl (docs/encrypted-dns.md), next to the # hand-written listener above. EDOT=${EDOT_PORT:-25855}; EDOH=${EDOH_PORT:-25856} run "encrypted-dns: off" 0 $J encrypted-dns contains "Encrypted DNS: *off" run "encrypted-dns check: the ports are free" 0 $J encrypted-dns check --address 127.0.0.1 --doh-port $EDOH contains "The ports are free" run "encrypted-dns check: jarld's port is taken" 0 $J encrypted-dns check --address 127.0.0.1 --doh-port $HTTP contains "jarld (Jarl's web UI and API) listens there" run "encrypted-dns setup" 0 $J encrypted-dns setup --hostname ns1.adbraun.test --address 127.0.0.1 --dot-port $EDOT --doh-port $EDOH run "status: on, with what to give clients" 0 $J encrypted-dns contains "DNS over TLS: *127.0.0.1 port $EDOT" contains "For DoH clients: *https://ns1.adbraun.test:$EDOH/dns-query" run "DoT through the setup" 0 $DIG +tls +tls-ca=$E/pebble-root.pem +tls-hostname=ns1.adbraun.test +short -p $EDOT @127.0.0.1 nas.adbraun.test A contains "^192.168.1.20$" run "DoH through the setup" 0 $DIG +https +tls-ca=$E/pebble-root.pem +tls-hostname=ns1.adbraun.test +short -p $EDOH @127.0.0.1 nas.adbraun.test A contains "^192.168.1.20$" run "plain DNS still answers on port 53's listener" 0 $DIG +short -p $DNS @127.0.0.1 nas.adbraun.test A contains "^192.168.1.20$" run "encrypted-dns health: both listeners work" 0 $J encrypted-dns health contains "DoT 127.0.0.1 port $EDOT: *working (NOERROR" contains "DoH 127.0.0.1 port $EDOH: *working" contains "isn't from a CA this server trusts" sleep 16 # /metrics caches Jarl's state for 15 seconds run "health metric" 0 curl -sf http://127.0.0.1:$HTTP/metrics contains "^jarl_encrypted_dns_up{kind=\"dot\",address=\"127.0.0.1\",port=\"$EDOT\"} 1$" run "a certificate named uses can't be deleted (exit 6)" 6 $J cert delete ns1 --yes contains "take it out first" run "encrypted-dns disable" 0 $J encrypted-dns disable run "encrypted-dns: off again" 0 $J encrypted-dns contains "Encrypted DNS: *off" dot_id=$($J option list -o field=id,name,value | awk -F'\t' '$2 == "listen-on" && /tls e2e/ {print $1}') run "named: remove the DoT listener" 0 $J option unset --id "$dot_id" run "named: remove the tls block" 0 $J option unset tls --toplevel run "cert delete" 0 $J cert delete ns1 --yes run "its files are gone" 1 test -e $E/etc-jarl/tls/ns1 fi run "status at the end" 0 $J status contains "Drift:.*none" echo echo "RESULT: $PASS passed, $FAIL failed" [ $FAIL = 0 ]