The setup takes several host names (one certificate for all; the health check says when the served certificate misses one), stored in the settings. Each listen address gets its own views for the host names' A/AAAA records (--address ADDR=VIEW,..., a view checkbox per address in the form), always local to the view, with a warning when a public view would get a private address; a view no longer given loses the address, other records of the names stay, and the form starts from where the records are now. Certificates (page and jarl cert show) say what named uses each for: DNS over TLS or HTTPS, on which addresses and ports. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
929 lines
53 KiB
Bash
Executable File
929 lines
53 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 "two more web host addresses" 0 $J record add revtest.test docs A 185.199.109.153 --view internal
|
|
run "and" 0 $J record add revtest.test pages A 185.199.110.153 --view internal
|
|
run "dismiss three at once" 0 $J zone suggest-reverse --dismiss 185.199.108.153/32,185.199.109.153/32 --dismiss 185.199.110.153/32 --view internal
|
|
run "gone from the suggestions" 0 $J zone suggest-reverse
|
|
not_contains "185.199."
|
|
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
|
|
# An address only the external view has a record for: a reverse zone in
|
|
# the internal view can still name it.
|
|
run "an AAAA only in external" 0 $J record add adbraun.test vps AAAA 2600:3c01::f03c:93ff:fe2b:1 --view external
|
|
run "offered for internal" 0 $J zone suggest-reverse --all --view internal
|
|
contains "2600:3c01::f03c:93ff:fe2b:1/128 .*records in external"
|
|
run "created in internal, filled from external's record" 0 $J zone create 2600:3c01::f03c:93ff:fe2b:1/128 --view internal --fill
|
|
expect_dig "internal answers its PTR" "vps.adbraun.test." -x 2600:3c01::f03c:93ff:fe2b:1
|
|
# 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 "cert show: what named uses it for" 0 $J cert show ns1
|
|
contains "^named: *DNS over TLS on 127.0.0.1 port $EDOT"
|
|
contains "^named: *DNS over HTTPS on 127.0.0.1 port $EDOH"
|
|
run "per address views: 127.0.0.1 only in internal" 0 $J encrypted-dns setup --hostname ns1.adbraun.test --address 127.0.0.1=internal --dot-port $EDOT --doh-port $EDOH
|
|
run "status shows where the records are" 0 $J encrypted-dns
|
|
contains "^Records: *127.0.0.1 in internal$"
|
|
contains "^Host names: *ns1.adbraun.test$"
|
|
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 ]
|