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:
1 parent
4d00be53c3
commit
a3a1da54df
3 files changed
+377
No files matched your search
@@ -6,6 +6,15 @@ All notable changes to Jarl are documented here. The format follows
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- `scripts/testenv.sh`: turns a fresh Debian/Ubuntu machine into a test bed.
|
||||
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
|
||||
(docs/development.md).
|
||||
|
||||
## [0.1.0] - 2026-10-09
|
||||
|
||||
The first version.
|
||||
|
||||
@@ -114,6 +114,36 @@ ACME hooks, audit) and checks the answers with `dig`. `KEEP=1` leaves named
|
||||
and jarld running afterwards. The output says where; set `JARL_CLI_CONFIG`
|
||||
to that directory's `cli.yaml` to keep using the CLI against it.
|
||||
|
||||
### A test machine
|
||||
|
||||
`scripts/testenv.sh` turns a fresh Debian/Ubuntu VM or container into a
|
||||
server to try Jarl on, the way a user would. Run it as root from the
|
||||
repository:
|
||||
|
||||
```sh
|
||||
sudo scripts/testenv.sh # zone example.com
|
||||
sudo JARL_TEST_ZONE=lab.test scripts/testenv.sh # another zone
|
||||
```
|
||||
|
||||
It installs BIND 9, make, git, curl and Go (from go.dev, at the version in
|
||||
`go.mod`, unless a new enough one is installed). It then writes a typical
|
||||
hand-made configuration for Jarl to import: an ACL, options with logging,
|
||||
an `internal` view (this machine's networks and the private ranges, with
|
||||
recursion) and an `external` view (everyone else, authoritative only).
|
||||
The zone is in both views with different answers. There is also a reverse
|
||||
zone for the machine's /24. The zone files in `/etc/bind/zones` use
|
||||
comments, `$TTL`, `$ORIGIN`, `$GENERATE`, legacy `type master` and most
|
||||
record types. The script checks the result with `named-checkconf -z`,
|
||||
(re)starts named (in the background where there is no systemd), and asks
|
||||
named a few questions with `dig`. At the end it prints the steps to build
|
||||
and install the packages, then run `init`, `import` and `adopt`.
|
||||
|
||||
`JARL_TEST_VIEWS=no` leaves out the views, and `JARL_TEST_IP` sets the
|
||||
machine's address (default: the address of the default route). The old
|
||||
`/etc/bind` is saved in `/root/bind-before-testenv-<time>.tgz`. Once Jarl
|
||||
has adopted the configuration, running the script again needs
|
||||
`JARL_TEST_FORCE=1`. Debian 13 has BIND 9.20; Ubuntu has it from 25.04.
|
||||
|
||||
### The web UI
|
||||
|
||||
The UI is plain JavaScript modules under `internal/web/static/`, with no
|
||||
|
||||
Executable
+338
@@ -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
|
||||
Reference in new issue
Block a user