Add scripts/testenv.sh to set up a test machine

On a fresh Debian/Ubuntu machine it installs BIND and the build tools
(Go from go.dev), writes a hand-made style configuration with internal
and external views, a split-horizon zone (example.com or
JARL_TEST_ZONE) and a reverse zone, starts named and prints the steps
to install Jarl and adopt the configuration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
abrown38andClaude Opus 5.5 committed 2026-10-09 09:05:58 +00:00
1 parent 4d00be53c3
commit a3a1da54df
3 files changed
+377

No files matched your search

+338
View File
@@ -0,0 +1,338 @@
#!/bin/bash
# Turns a fresh Debian/Ubuntu machine (a VM or container you can throw
# away) into a test bed for Jarl:
#
# * installs BIND 9 (bind9, bind9-utils, bind9-dnsutils) and what building
# Jarl needs (make, git, curl, and Go from go.dev at the version go.mod
# asks for, unless a new enough go is already installed);
# * writes a typical hand-made BIND configuration for Jarl to import:
# ACLs, options, an "internal" view (this machine's networks, with
# recursion) and an "external" view (everyone else, authoritative only),
# the zone in both views with different answers (split horizon), a
# reverse zone for this machine's /24, and the zones in
# /etc/bind/zones with comments, $TTL, $ORIGIN, $GENERATE and most
# record types;
# * checks it with named-checkconf -z, (re)starts named and asks it a few
# questions with dig.
#
# Run as root from the repository root:
#
# sudo scripts/testenv.sh
#
# then build and install Jarl and adopt the configuration (the script
# prints the steps at the end).
#
# Settings (environment):
# JARL_TEST_ZONE the zone (default: example.com)
# JARL_TEST_IP this machine's address in the zone (default: the
# address of the default route)
# JARL_TEST_VIEWS no: no views, just the zones (default: yes)
# JARL_TEST_FORCE 1: also overwrite a configuration Jarl already adopted
#
# The current /etc/bind is saved in /root/bind-before-testenv-<time>.tgz
# first. Don't run this on a server you care about.
set -euo pipefail
ZONE=${JARL_TEST_ZONE:-example.com}
ZONE=${ZONE%.}
VIEWS=${JARL_TEST_VIEWS:-yes}
ROOT=$(cd "$(dirname "$0")/.." && pwd)
say() { printf '\n== %s\n' "$*"; }
die() { printf 'testenv: %s\n' "$*" >&2; exit 1; }
[ "$(id -u)" = 0 ] || die "run as root (sudo $0)"
command -v apt-get >/dev/null || die "needs Debian or Ubuntu (apt-get)"
case $ZONE in
*[!a-zA-Z0-9.-]* | .* | *..* | "") die "JARL_TEST_ZONE=$ZONE is not a domain name" ;;
esac
case $VIEWS in yes | no) ;; *) die "JARL_TEST_VIEWS must be yes or no" ;; esac
if [ -z "${JARL_TEST_FORCE:-}" ] && grep -qs 'jarl' /etc/bind/named.conf; then
die "/etc/bind/named.conf already includes Jarl's configuration (adopted); set JARL_TEST_FORCE=1 to replace it anyway"
fi
# ---- packages
say "Installing BIND and build tools"
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq --no-install-recommends \
bind9 bind9-utils bind9-dnsutils make git curl ca-certificates iproute2 tar gzip >/dev/null
named_version=$(named -v | sed -n 's/^BIND \([0-9]*\.[0-9]*\).*/\1/p')
echo "named: $(named -v)"
case $named_version in
9.2[0-9] | 9.[3-9][0-9]) ;;
*) echo "WARNING: Jarl is made for BIND 9.20 or newer; this is $named_version (on Ubuntu, 25.04 or newer has 9.20)" ;;
esac
# Go: the version go.mod names, from go.dev, unless one at least as new is
# already in PATH or /usr/local/go.
want=$(sed -n 's/^go \([0-9.]*\).*/\1/p' "$ROOT/go.mod" 2>/dev/null || true)
want=${want:-1.27.1}
export PATH=$PATH:/usr/local/go/bin
have=$(go env GOVERSION 2>/dev/null | sed 's/^go//' || true)
if [ -n "$have" ] && [ "$(printf '%s\n%s\n' "$want" "$have" | sort -V | head -1)" = "$want" ]; then
echo "go: $have (new enough)"
else
case $(dpkg --print-architecture) in
amd64) goarch=amd64 ;; arm64) goarch=arm64 ;; armhf) goarch=armv6l ;; i386) goarch=386 ;;
*) die "no Go download for $(dpkg --print-architecture); install Go $want yourself" ;;
esac
say "Installing Go $want in /usr/local/go"
tmp=$(mktemp -d)
curl -fsSL "https://go.dev/dl/go$want.linux-$goarch.tar.gz" -o "$tmp/go.tgz"
rm -rf /usr/local/go
tar -C /usr/local -xzf "$tmp/go.tgz"
rm -rf "$tmp"
echo 'export PATH=$PATH:/usr/local/go/bin' > /etc/profile.d/go.sh
echo "go: $(/usr/local/go/bin/go version) (log in again, or: . /etc/profile.d/go.sh)"
fi
# ---- addresses
IP=${JARL_TEST_IP:-$(ip -4 route get 1.1.1.1 2>/dev/null | sed -n 's/.* src \([0-9.]*\).*/\1/p')}
IP=${IP:-127.0.0.1}
case $IP in
*.*.*.*) ;;
*) die "JARL_TEST_IP=$IP is not an IPv4 address" ;;
esac
IFS=. read -r o1 o2 o3 o4 <<<"$IP"
NET="$o1.$o2.$o3"
REV="$o3.$o2.$o1.in-addr.arpa"
SERIAL=$(date +%Y%m%d)01
echo "zone $ZONE, this machine $IP, reverse zone $REV, views: $VIEWS"
# ---- configuration
say "Writing /etc/bind"
backup=/root/bind-before-testenv-$(date +%Y%m%d-%H%M%S).tgz
tar -czf "$backup" -C / etc/bind
echo "saved the old /etc/bind in $backup"
rm -rf /etc/bind/zones
install -d -o root -g bind -m 2755 /etc/bind/zones
# Debian's stock zones: named.conf.default-zones (localhost and friends)
# before BIND 9.20, named.conf.root-hints (just the root hints) since.
DEFAULTS=
for f in /etc/bind/named.conf.default-zones /etc/bind/named.conf.root-hints; do
if [ -f "$f" ]; then DEFAULTS="include \"$f\";"; break; fi
done
cat >/etc/bind/named.conf <<EOF
// Test configuration written by Jarl's scripts/testenv.sh.
include "/etc/bind/named.conf.options";
include "/etc/bind/named.conf.local";
EOF
if [ "$VIEWS" = no ] && [ -n "$DEFAULTS" ]; then
echo "$DEFAULTS" >>/etc/bind/named.conf
fi
# With views, each view says whether it recurses.
GLOBAL_RECURSION=$'\trecursion yes;\n\tallow-recursion { trusted; };'
[ "$VIEWS" = no ] || GLOBAL_RECURSION=$'\t// recursion: per view, in named.conf.local'
cat >/etc/bind/named.conf.options <<EOF
// Who may use this server as a resolver.
acl trusted {
127.0.0.0/8;
::1;
$NET.0/24;
10.0.0.0/8;
172.16.0.0/12;
192.168.0.0/16;
};
options {
directory "/var/cache/bind";
listen-on { any; };
listen-on-v6 { any; };
$GLOBAL_RECURSION
allow-query { any; };
allow-transfer { none; };
dnssec-validation auto;
minimal-responses yes;
version "not disclosed";
};
logging {
channel default_log {
file "/var/log/named/default.log" versions 3 size 5m;
severity info;
print-time yes;
};
category default { default_log; };
category lame-servers { null; };
};
EOF
install -d -o bind -g bind -m 0750 /var/log/named
if [ "$VIEWS" = yes ]; then
cat >/etc/bind/named.conf.local <<EOF
// Split horizon: the internal view answers this machine's networks with
// private addresses (and resolves for them); the external view answers
// everyone else with public ones.
view "internal" {
match-clients { trusted; };
recursion yes;
allow-recursion { trusted; };
$DEFAULTS
zone "$ZONE" {
type master;
file "/etc/bind/zones/db.$ZONE.internal";
allow-update { none; };
};
zone "$REV" {
type master;
file "/etc/bind/zones/db.$NET";
};
};
view "external" {
match-clients { any; };
recursion no;
zone "$ZONE" {
type master;
file "/etc/bind/zones/db.$ZONE.external";
};
};
EOF
else
cat >/etc/bind/named.conf.local <<EOF
zone "$ZONE" {
type master;
file "/etc/bind/zones/db.$ZONE.internal";
};
zone "$REV" {
type master;
file "/etc/bind/zones/db.$NET";
};
EOF
fi
# The internal zone: what machines on the LAN see.
cat >"/etc/bind/zones/db.$ZONE.internal" <<EOF
; $ZONE as the LAN sees it.
\$TTL 3600
@ IN SOA ns1.$ZONE. hostmaster.$ZONE. (
$SERIAL ; serial
3600 ; refresh
900 ; retry
1209600 ; expire
300 ) ; negative caching
IN NS ns1
IN MX 10 mail
IN TXT "v=spf1 mx -all"
IN CAA 0 issue "letsencrypt.org"
@ IN A $IP
ns1 IN A $IP ; this server
mail IN A $NET.25
nas IN A $NET.20 ; storage
nas IN AAAA fd00:1::20
printer 300 IN A $NET.30
www IN CNAME @
git IN CNAME nas
_dmarc IN TXT "v=DMARC1; p=reject; rua=mailto:postmaster@$ZONE"
_sip._tcp IN SRV 10 60 5060 voip
voip IN A $NET.40
; Lab machines: lab-100 .. lab-109 at .100 .. .109
\$GENERATE 100-109 lab-\$ IN A $NET.\$
\$ORIGIN k8s.$ZONE.
api IN A $NET.50
* IN A $NET.51 ; ingress
EOF
# The external zone: what the internet sees (documentation addresses).
cat >"/etc/bind/zones/db.$ZONE.external" <<EOF
; $ZONE as the internet sees it.
\$TTL 3600
@ IN SOA ns1.$ZONE. hostmaster.$ZONE. ( $SERIAL 3600 900 1209600 300 )
IN NS ns1
IN NS ns2
IN MX 10 mail
IN TXT "v=spf1 mx -all"
IN CAA 0 issue "letsencrypt.org"
@ IN A 203.0.113.10
@ IN AAAA 2001:db8::10
ns1 IN A 203.0.113.53
ns2 IN A 198.51.100.53
mail IN A 203.0.113.25
www IN CNAME @
EOF
cat >"/etc/bind/zones/db.$NET" <<EOF
; Reverse zone for $NET.0/24.
\$TTL 86400
@ IN SOA ns1.$ZONE. hostmaster.$ZONE. ( $SERIAL 3600 900 1209600 300 )
IN NS ns1.$ZONE.
$o4 IN PTR ns1.$ZONE.
20 IN PTR nas.$ZONE.
25 IN PTR mail.$ZONE.
30 IN PTR printer.$ZONE.
40 IN PTR voip.$ZONE.
\$GENERATE 100-109 \$ IN PTR lab-\$.$ZONE.
EOF
chown root:bind /etc/bind/named.conf /etc/bind/named.conf.options /etc/bind/named.conf.local /etc/bind/zones/*
chmod 0644 /etc/bind/named.conf /etc/bind/named.conf.options /etc/bind/named.conf.local /etc/bind/zones/*
[ -f /etc/bind/rndc.key ] || rndc-confgen -a -A hmac-sha256 >/dev/null
say "Checking with named-checkconf -z"
named-checkconf -z /etc/bind/named.conf | grep -v '^zone .* loaded serial' || true
named-checkconf -z /etc/bind/named.conf >/dev/null
# ---- start named
say "Starting named"
if [ -d /run/systemd/system ]; then
systemctl enable -q named 2>/dev/null || systemctl enable -q bind9 2>/dev/null || true
systemctl restart named 2>/dev/null || systemctl restart bind9
else
# No systemd (a container): run named in the background.
pkill -x named 2>/dev/null && sleep 1 || true
install -d -o bind -g bind /run/named
named -u bind -c /etc/bind/named.conf
fi
for _ in $(seq 20); do rndc status >/dev/null 2>&1 && break; sleep 0.5; done
rndc status | sed -n 's/^\(version\|number of zones\|server is up and running\).*/ &/p'
say "Asking named"
q() { printf ' %-45s %s\n' "$*" "$(dig +short +time=2 +tries=1 "$@" | tr '\n' ' ')"; }
q @127.0.0.1 "$ZONE" SOA
q @127.0.0.1 "nas.$ZONE" A
q @127.0.0.1 "lab-105.$ZONE" A
q @127.0.0.1 "foo.k8s.$ZONE" A
q @127.0.0.1 -x "$NET.20"
if [ "$VIEWS" = yes ]; then
echo " (the answers above come from the internal view; from outside the"
echo " trusted networks, $ZONE is 203.0.113.10)"
fi
cat <<EOF
Done. BIND serves $ZONE as a hand-written configuration would. Now install
Jarl and let it take over (docs/install.md has the details):
. /etc/profile.d/go.sh 2>/dev/null; make deb
apt install ./dist/jarl_*_\$(dpkg --print-architecture).deb ./dist/jarl-cli_*_\$(dpkg --print-architecture).deb
sudo -u jarl jarld init --token --nameservers ns1.$ZONE. --contact hostmaster@$ZONE
sudo -u jarl jarld import --dry-run && sudo -u jarl jarld import
jarld adopt
systemctl enable --now jarl
jarl status
The web UI listens on http://127.0.0.1:8053 (listen: in /etc/jarl/jarl.yaml).
To start again, run this script again (with JARL_TEST_FORCE=1 once adopted);
'apt purge jarl' and 'rm -rf /var/lib/jarl /etc/bind/jarl /var/lib/bind/jarl'
also clear Jarl's state.
EOF