net-tools/bin/mesh-hosts-render
Natalie 03e47fc4df feat(@tools/net-tools): add mesh/lan tooling with host renderers
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-06-09 19:53:08 -07:00

142 lines
5 KiB
Bash
Executable file

#!/bin/sh
# mesh-hosts-render — render a static /etc/hosts block for the wg1 mesh from
# data/mesh-hosts.json, and optionally splice it into /etc/hosts.
#
# For ROAMING / DNS-less clients (plum off-LAN, or any host before dnsmasq is
# reachable). On-LAN hosts get the same names from dnsmasq; this static block is
# the fallback that always works while the tunnel is up.
#
# Emits a marked, idempotently-replaceable block:
# # >>> mesh-hosts (managed by smart-lan-router/bin/mesh-hosts-render)
# 10.9.0.2 apricot.wg apricot
# ...
# # <<< mesh-hosts
#
# .wg names map to mesh IPs (always reachable via tunnel). .lan names map to LAN
# IPs (home network only) for hosts that have a stable LAN IP. Roaming hosts
# (lan == null) get no .lan record. The bare host name aliases the .wg view.
#
# Usage:
# mesh-hosts-render # print the block to stdout (default; safe)
# mesh-hosts-render --install # splice/replace the block in /etc/hosts (needs root)
# mesh-hosts-render --diff # show what --install would change, no write
#
# Exit codes:
# 0 success (printed, or installed, or already up to date)
# 1 missing dependency / unlocatable or invalid JSON
# 2 --install needs root but it isn't available
set -eu
mode=print
case "${1:-}" in
""|--print) mode=print ;;
--install) mode=install ;;
--diff) mode=diff ;;
*) echo "mesh-hosts-render: unknown arg '$1' (use --print|--install|--diff)" >&2; exit 1 ;;
esac
BEGIN='# >>> mesh-hosts (managed by smart-lan-router/bin/mesh-hosts-render)'
END='# <<< mesh-hosts'
# Legacy block this tool replaces — stripped on install so its stale (drifted)
# host entries don't shadow ours in first-match resolution.
LEGACY_BEGIN='# >>> LAN hosts — managed by setup-lan-dns.sh'
LEGACY_END='# <<< LAN hosts'
HOSTS_FILE=/etc/hosts
# --- locate data file, surviving symlink invocation ----------------------------
self=$0
while [ -L "$self" ]; do
link=$(readlink "$self")
case $link in
/*) self=$link ;;
*) self=$(dirname "$self")/$link ;;
esac
done
root=$(cd "$(dirname "$self")" && pwd)
while [ "$root" != "/" ] && [ ! -f "$root/data/mesh-hosts.json" ]; do
root=$(dirname "$root")
done
data_file="$root/data/mesh-hosts.json"
[ -f "$data_file" ] || { echo "mesh-hosts-render: cannot locate data/mesh-hosts.json (from $self)" >&2; exit 1; }
command -v jq >/dev/null || { echo "mesh-hosts-render: jq not installed" >&2; exit 1; }
jq empty "$data_file" || { echo "mesh-hosts-render: invalid JSON in $data_file" >&2; exit 1; }
# Overlay: current LAN IPs discovered by the daemon (data/lan-state.json, a
# {name: ip} map) override the static `lan` seed, so records track DHCP drift.
overlay='{}'
state_file="$root/data/lan-state.json"
if [ -f "$state_file" ] && jq -e . "$state_file" >/dev/null 2>&1; then
overlay=$(cat "$state_file")
fi
render_block() {
printf '%s\n' "$BEGIN"
printf '# Auto-generated from smart-lan-router/data/mesh-hosts.json — re-run to update.\n'
printf '# .wg = mesh IP (anywhere via tunnel) · .lan = LAN IP (home network only)\n'
# Mesh (.wg) records + bare-name alias to the mesh view.
jq -r '
.hosts[]
| . as $h
| "\($h.wg)\t" + (([($h.name + ".wg"), $h.name] + (($h.aliases // []) | map(. + ".wg") + .)) | join(" "))
' "$data_file"
# LAN (.lan) records — current discovered IP (overlay) wins over the static seed.
jq -r --argjson ov "$overlay" '
.hosts[]
| . as $h
| (($ov[$h.name]) // $h.lan) as $lan
| select($lan != null)
| "\($lan)\t\($h.name).lan"
' "$data_file"
printf '%s\n' "$END"
}
block=$(render_block)
if [ "$mode" = "print" ]; then
printf '%s\n' "$block"
exit 0
fi
# Compute the new /etc/hosts: everything outside the markers, with our block
# appended (or replaced in place if markers already exist).
current=$(cat "$HOSTS_FILE" 2>/dev/null || true)
stripped=$(printf '%s\n' "$current" | awk -v b="$BEGIN" -v e="$END" '
$0 == b { skip = 1 }
skip != 1 { print }
$0 == e { skip = 0 }
')
# Trim trailing blank lines from the stripped body.
stripped=$(printf '%s\n' "$stripped" | awk 'NF {p=NR} {l[NR]=$0} END {for(i=1;i<=p;i++) print l[i]}')
# PREPEND our block so its records win /etc/hosts first-match resolution over any
# other (e.g. a stale setup-lan-dns block that still lists a drifted apricot.lan).
new=$(printf '%s\n\n%s\n' "$block" "$stripped")
if [ "$mode" = "diff" ]; then
if command -v diff >/dev/null 2>&1; then
printf '%s\n' "$new" | diff -u "$HOSTS_FILE" - || true
else
printf '%s\n' "$new"
fi
exit 0
fi
# --install
if printf '%s\n' "$new" | cmp -s - "$HOSTS_FILE"; then
echo "mesh-hosts-render: $HOSTS_FILE already up to date"
exit 0
fi
SUDO=
if [ "$(id -u)" -ne 0 ]; then
if command -v sudo >/dev/null 2>&1; then
SUDO="sudo"
else
echo "mesh-hosts-render: --install needs root" >&2
exit 2
fi
fi
printf '%s\n' "$new" | $SUDO tee "$HOSTS_FILE" >/dev/null
echo "mesh-hosts-render: updated $HOSTS_FILE"