feat(@scripts): ✨ add dnsmasq sync script for wg1 mesh
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
96edee4b11
commit
8a222d547d
2 changed files with 136 additions and 0 deletions
113
bin/wg-dns-sync
Executable file
113
bin/wg-dns-sync
Executable file
|
|
@ -0,0 +1,113 @@
|
|||
#!/bin/sh
|
||||
# wg-dns-sync — render dnsmasq records for the wg1 mesh from wg-mesh-hosts.json
|
||||
# and (re-)install them to /etc/dnsmasq.d/wg-mesh.conf on the local host.
|
||||
#
|
||||
# Source of truth: ../data/wg-mesh-hosts.json (relative to this script).
|
||||
# Output file: /etc/dnsmasq.d/wg-mesh.conf
|
||||
# Daemon: dnsmasq.service (restarted only if conf changed)
|
||||
#
|
||||
# Why a separate file (not editing lilith-local.conf):
|
||||
# lilith-local.conf returns 127.0.0.1 for *.lilith.apricot.local on purpose
|
||||
# — that's split-horizon DNS for apricot's own loopback Traefik. Other clients
|
||||
# (e.g. phone over wg1) need the LAN IP instead. This script writes a SECOND
|
||||
# conf file that targets only the wg1 listen address, so both views coexist.
|
||||
#
|
||||
# Idempotent: re-run is a no-op if rendered conf hashes match what's installed.
|
||||
#
|
||||
# Usage:
|
||||
# wg-dns-sync # render + install + restart dnsmasq if changed
|
||||
# wg-dns-sync --dry-run # print rendered conf, no install
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 success (or unchanged no-op)
|
||||
# 1 missing dependency (jq, sudo, dnsmasq) or invalid JSON
|
||||
# 2 sudo required but not available non-interactively
|
||||
# 3 dnsmasq failed to start after install (rolled back)
|
||||
|
||||
set -eu
|
||||
|
||||
dry_run=0
|
||||
[ "${1:-}" = "--dry-run" ] && dry_run=1
|
||||
|
||||
script_dir=$(cd "$(dirname "$0")" && pwd)
|
||||
data_file="$script_dir/../data/wg-mesh-hosts.json"
|
||||
target=/etc/dnsmasq.d/wg-mesh.conf
|
||||
|
||||
[ -r "$data_file" ] || { echo "missing $data_file" >&2; exit 1; }
|
||||
command -v jq >/dev/null || { echo "jq not installed" >&2; exit 1; }
|
||||
|
||||
# Validate JSON early so render errors don't half-write.
|
||||
jq empty "$data_file" || { echo "invalid JSON in $data_file" >&2; exit 1; }
|
||||
|
||||
listen=$(jq -r '.listen_address' "$data_file")
|
||||
[ -n "$listen" ] && [ "$listen" != "null" ] || {
|
||||
echo "wg-mesh-hosts.json missing .listen_address" >&2; exit 1
|
||||
}
|
||||
|
||||
# Render the conf to a temp file. Header is structured so future audits can see
|
||||
# it was machine-generated and from where (path, host, time, data sha).
|
||||
tmp=$(mktemp /tmp/wg-mesh.conf.XXXXXX)
|
||||
trap 'rm -f "$tmp"' EXIT
|
||||
|
||||
data_sha=$(shasum -a 256 "$data_file" 2>/dev/null | awk '{print $1}' \
|
||||
|| sha256sum "$data_file" | awk '{print $1}')
|
||||
when=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
host=$(hostname -s)
|
||||
|
||||
{
|
||||
printf '# Generated by session-tools/bin/wg-dns-sync — DO NOT EDIT MANUALLY\n'
|
||||
printf '# To change records: edit session-tools/data/wg-mesh-hosts.json and re-run this script.\n'
|
||||
printf '# rendered_at: %s\n' "$when"
|
||||
printf '# rendered_on: %s\n' "$host"
|
||||
printf '# source_sha256: %s\n' "$data_sha"
|
||||
printf '\n'
|
||||
printf '# Bind only to the wg1 IP so this view is invisible to LAN/loopback clients\n'
|
||||
printf '# (which are served by lilith-local.conf with split-horizon 127.0.0.1 records).\n'
|
||||
printf 'listen-address=%s\n' "$listen"
|
||||
printf 'bind-interfaces\n'
|
||||
printf '\n'
|
||||
printf '# DNS records (one per record entry in wg-mesh-hosts.json)\n'
|
||||
jq -r '.records[] | "address=/\(.name|sub("^\\.";""))/\(.ip) # \(.comment // "")"' "$data_file"
|
||||
} > "$tmp"
|
||||
|
||||
if [ "$dry_run" -eq 1 ]; then
|
||||
cat "$tmp"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Compare to existing target. If identical, no-op (avoid unneeded restart).
|
||||
if [ -r "$target" ] && cmp -s "$tmp" "$target"; then
|
||||
echo "wg-dns-sync: $target unchanged (no-op)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Install requires sudo. Refuse to prompt mid-script — fail loudly if non-interactive.
|
||||
if ! sudo -n true 2>/dev/null; then
|
||||
echo "wg-dns-sync: sudo required (run interactively or pre-auth with 'sudo -v')" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Back up current target (if any) before replacing — undo handled by re-running
|
||||
# wg-dns-sync after editing the JSON, NOT by restoring this backup. Backup is
|
||||
# audit-only; safe to delete.
|
||||
if [ -r "$target" ]; then
|
||||
sudo cp "$target" "${target}.prev"
|
||||
fi
|
||||
|
||||
sudo install -m 0644 -o root -g root "$tmp" "$target"
|
||||
echo "wg-dns-sync: installed $target"
|
||||
|
||||
# Restart dnsmasq and verify it came back up. If it failed (e.g. listen-address
|
||||
# unreachable because wg1 is down), restore the previous conf and exit 3.
|
||||
if ! sudo systemctl restart dnsmasq; then
|
||||
echo "wg-dns-sync: dnsmasq failed to restart, rolling back" >&2
|
||||
if [ -r "${target}.prev" ]; then
|
||||
sudo install -m 0644 -o root -g root "${target}.prev" "$target"
|
||||
else
|
||||
sudo rm -f "$target"
|
||||
fi
|
||||
sudo systemctl restart dnsmasq || true
|
||||
exit 3
|
||||
fi
|
||||
|
||||
echo "wg-dns-sync: dnsmasq restarted, listening on $listen"
|
||||
23
data/wg-mesh-hosts.json
Normal file
23
data/wg-mesh-hosts.json
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"_purpose": "Source of truth for DNS records served to the wg1 mesh by apricot's dnsmasq (instance bound to 10.9.0.2:53). Consumed by bin/wg-dns-sync.",
|
||||
"_consumers": ["bin/wg-dns-sync"],
|
||||
"_dnsmasq_address_syntax": "Leading dot in 'name' = wildcard match for the domain and any subdomain. No leading dot = exact match only.",
|
||||
"listen_address": "10.9.0.2",
|
||||
"records": [
|
||||
{
|
||||
"name": ".apricot.local",
|
||||
"ip": "10.0.0.116",
|
||||
"comment": "apricot's primary LAN IP (eno2). Covers apricot.local plus all *.apricot.local subdomains used by lilith-platform nginx vhosts."
|
||||
},
|
||||
{
|
||||
"name": ".atlilith.local",
|
||||
"ip": "10.0.0.116",
|
||||
"comment": "atlilith.local TLD also served by apricot's lilith-dev-nginx (api, imajin, minio, search, stream-api)."
|
||||
},
|
||||
{
|
||||
"name": ".black.local",
|
||||
"ip": "10.0.0.11",
|
||||
"comment": "black's LAN IP (forge.black.local). Covers black.local and all subdomains."
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue