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>
339 lines
10 KiB
Bash
Executable File
339 lines
10 KiB
Bash
Executable File
#!/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
|