The Zones page's New reverse zone lists every network the records use without a reverse zone, in every view, also where nothing is suggested (suggestions off, dismissed, public views), marked. Picking one fills in the network and view; size buttons (/48 /56 /64 /128, /16 /24 /32) re-mask it around a record's address, so a /48 becomes the records' own /64. Fill stays on. jarl zone suggest-reverse --all [--view V]; GET /reverse-zones/suggestions?all=1&view=. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
913 lines
52 KiB
Bash
Executable File
913 lines
52 KiB
Bash
Executable File
#!/bin/bash
|
|
# End-to-end test: builds a legacy BIND setup, starts an unprivileged named,
|
|
# imports and adopts it with jarld, runs jarld, and drives every jarl CLI
|
|
# command, checking the answers with dig. Needs no root.
|
|
#
|
|
# BIND tools come from $JARL_BIND_ROOT (an extracted bind9 package tree, see
|
|
# docs/development.md) or from PATH. Run from the repository root:
|
|
#
|
|
# make e2e or JARL_BIND_ROOT=... scripts/e2e.sh
|
|
#
|
|
# Set KEEP=1 to leave named and jarld running afterwards (in $WORK).
|
|
set -u
|
|
ROOT=$(cd "$(dirname "$0")/.." && pwd)
|
|
WORK=${WORK:-$(mktemp -d /tmp/jarl-e2e.XXXXXX)}
|
|
E=$WORK
|
|
DNS=${DNS_PORT:-25353}; CTL=${CTL_PORT:-25953}; HTTP=${HTTP_PORT:-28053}; LISTS=${LISTS_PORT:-28054}; 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 <<EOF
|
|
include "$E/legacy/named.conf.options";
|
|
include "$E/legacy/named.conf.local";
|
|
include "$E/legacy/rndc.key";
|
|
controls { inet 127.0.0.1 port $CTL allow { 127.0.0.1; } keys { "rndc-key"; }; };
|
|
EOF
|
|
cat > $E/legacy/named.conf.options <<EOF
|
|
acl homelab { 127.0.0.0/8; 192.168.1.0/24; };
|
|
options {
|
|
directory "$E/cache";
|
|
pid-file "$E/cache/named.pid";
|
|
session-keyfile "$E/cache/session.key";
|
|
listen-on port $DNS { 127.0.0.1; };
|
|
listen-on-v6 { none; };
|
|
recursion yes;
|
|
allow-recursion { homelab; };
|
|
allow-query { any; };
|
|
};
|
|
EOF
|
|
cat > $E/legacy/named.conf.local <<EOF
|
|
zone "adbraun.test" { type master; file "$E/legacy/db.adbraun.test"; };
|
|
zone "1.168.192.in-addr.arpa" { type master; file "$E/legacy/db.192.168.1"; };
|
|
EOF
|
|
cat > $E/legacy/db.adbraun.test <<'EOF'
|
|
$TTL 3600
|
|
@ IN SOA ns1.adbraun.test. admin.adbraun.test. ( 2024051201 3600 900 1209600 300 )
|
|
IN NS ns1
|
|
ns1 IN A 127.0.0.1
|
|
nas IN A 192.168.1.20 ; synology
|
|
www 300 IN CNAME nas
|
|
EOF
|
|
cat > $E/legacy/db.192.168.1 <<'EOF'
|
|
$TTL 86400
|
|
@ IN SOA ns1.adbraun.test. admin.adbraun.test. ( 2024051201 3600 900 1209600 300 )
|
|
@ IN NS ns1.adbraun.test.
|
|
20 IN PTR nas.adbraun.test.
|
|
EOF
|
|
# -f like the Debian unit (-g would send all logging, including the
|
|
# statistics response log, to stderr).
|
|
$NAMED -c $E/legacy/named.conf -f -n 1 > $E/named.log 2>&1 &
|
|
NAMED_PID=$!
|
|
for i in $(seq 50); do $RNDC -k $E/legacy/rndc.key -s 127.0.0.1 -p $CTL status >/dev/null 2>&1 && break; sleep 0.2; done
|
|
|
|
# The local socket (a short path: unix socket paths are limited to 107 bytes).
|
|
SOCK=${XDG_RUNTIME_DIR:-/tmp}/jarl-e2e-$HTTP.sock
|
|
cat > $E/jarl.yaml <<EOF
|
|
listen: ["127.0.0.1:$HTTP"]
|
|
socket: $SOCK
|
|
database: $E/data/jarl.db
|
|
staging_dir: $E/data/staging
|
|
log: {level: info}
|
|
bind:
|
|
main_config: $E/legacy/named.conf
|
|
config_dir: $E/etc-jarl
|
|
zone_dir: $E/zones
|
|
cache_dir: $E/cache/jarl
|
|
file_group: ""
|
|
named_user: $(id -un)
|
|
named_checkconf: $CHECKCONF
|
|
named_checkzone: $CHECKZONE
|
|
rndc: $RNDC
|
|
rndc_args: ["-k", "$E/legacy/rndc.key", "-s", "127.0.0.1", "-p", "$CTL"]
|
|
backup:
|
|
dir: $E/backups
|
|
metrics:
|
|
enabled: true
|
|
listen: ["127.0.0.1:$METRICS"]
|
|
token: e2e-metrics-token-0123456789
|
|
EOF
|
|
if [ -n "$PEBBLE" ]; then cat >> $E/jarl.yaml <<EOF
|
|
acme:
|
|
account: {directory: "https://localhost:14000/dir", agree_tos: true}
|
|
resolvers: ["127.0.0.1:$DNS"]
|
|
ca_bundle: $E/pebble-minica.pem
|
|
check_propagation: false # lego would ask the name servers on port 53
|
|
EOF
|
|
fi
|
|
cd $E
|
|
echo "secret-pass-123" | $JARLD -c jarl.yaml init --password-stdin --token --nameservers ns1.adbraun.test. --contact admin@adbraun.test > init.out 2>&1 || { cat init.out; exit 1; }
|
|
grep -o 'jarl_[A-Za-z0-9_]*' init.out | head -1 > admin.token
|
|
$JARLD -c jarl.yaml import > import.out 2>&1 || { cat import.out; exit 1; }
|
|
$JARLD -c jarl.yaml adopt > adopt.out 2>&1 || { cat adopt.out; exit 1; }
|
|
$JARLD -c jarl.yaml serve > jarld.log 2>&1 &
|
|
JARLD_PID=$!
|
|
for i in $(seq 50); do curl -s -o /dev/null http://127.0.0.1:$HTTP/healthz && break; sleep 0.2; done
|
|
echo "setup done in $WORK"
|
|
|
|
# ---- CLI walkthrough
|
|
export JARL_CLI_CONFIG=$E/cli.yaml
|
|
unset JARL_SERVER JARL_TOKEN JARL_CHANGESET
|
|
PASS=0; FAIL=0
|
|
dig() { $DIG +short +norec -p $DNS @127.0.0.1 "$@" | sort | tr '\n' ' ' | sed 's/ $//'; }
|
|
ok() { echo "PASS: $1"; PASS=$((PASS+1)); }
|
|
bad() { echo "FAIL: $1"; FAIL=$((FAIL+1)); }
|
|
# run DESC EXPECTED_RC CMD...: runs a command and checks its exit code;
|
|
# the output is kept in $OUT.
|
|
run() {
|
|
local desc=$1 want=$2; shift 2
|
|
OUT=$("$@" 2>&1); local rc=$?
|
|
if [ "$rc" = "$want" ]; then ok "$desc (rc=$rc)"; else bad "$desc: rc=$rc want $want"; echo "$OUT" | sed 's/^/ /'; fi
|
|
}
|
|
expect_dig() { # DESC WANT DIG-ARGS...
|
|
local desc=$1 want=$2; shift 2
|
|
local got; got=$(dig "$@")
|
|
if [ "$got" = "$want" ]; then ok "dig $desc = '$want'"; else bad "dig $desc: got '$got' want '$want'"; fi
|
|
}
|
|
contains() { if echo "$OUT" | grep -q -- "$1"; then ok "output has '$1'"; else bad "output lacks '$1'"; echo "$OUT" | sed 's/^/ /'; fi; }
|
|
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 '<html>gone</html>\n' > $E/lists/list.txt
|
|
run "a broken download fails (exit 2)" 2 $J blocklist refresh test --force
|
|
contains "no domains found"
|
|
expect_rpz "and the previous version stays" x.new.adbraun.test test.blocklist.jarl.invalid.
|
|
# 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 ]
|