- data/mesh-hosts.json: "dx": {"hide_homelan": true} (with note). Data for apricot/pear/fennel/lan/services fully preserved for recovery.
- bin/mesh-hosts-render + bin/host-apply: respect the flag — filter to .class=="cloud" hosts only (yuzu, lime), emit dx mode note in headers, services filtered too.
- When true: generated /etc/hosts mesh-block and ~/.ssh/config net-tools fleet block only contain DO/cloud (homelan names like apricot.lan, bare fennel etc. hidden). dx-forges (ctforge/mcforge) unaffected at bottom.
- `net sync` (and direct renderers) now produce clean DO-only configs.
- README updated. To recover: set false + net sync.
Fulfills "hide the homelan config... now only use DO... may try to recover homelan so dont delete it".
215 lines
9 KiB
Bash
Executable file
215 lines
9 KiB
Bash
Executable file
#!/bin/sh
|
|
# mesh-hosts-render — render the fleet's /etc/hosts block from
|
|
# data/mesh-hosts.json (+ the daemon's discovered-IP overlay) and splice it in.
|
|
#
|
|
# Emits a marked, idempotently-replaceable block at the TOP of /etc/hosts (so it
|
|
# wins first-match resolution):
|
|
# # >>> mesh-hosts (managed by smart-lan-router/bin/mesh-hosts-render)
|
|
# 10.0.0.118 apricot.lan apricot
|
|
# 10.9.0.2 apricot.wg
|
|
# 10.0.0.118 quinn.apricot.lan www.quinn.apricot.lan ...
|
|
# ...
|
|
# # <<< mesh-hosts
|
|
#
|
|
# Name semantics (see mesh-hosts.json _schema.naming):
|
|
# <host>.lan + BARE <host> -> current LAN IP (overlay over static seed).
|
|
# Direct at home; via the tunnel when away (the daemon routes the LAN /24
|
|
# through wg then) — so this is the right default everywhere.
|
|
# <host>.wg -> mesh IP (explicit tunnel path).
|
|
# bare name of a LAN-less host (fennel/yuzu) -> its wg IP.
|
|
# services (mesh-hosts.json .services) -> the hosting host's current LAN IP.
|
|
#
|
|
# Adoption: on --install this also REMOVES (a) the legacy setup-lan-dns block and
|
|
# (b) any loose non-comment line that names a host/service this block manages —
|
|
# those are hand-maintained duplicates that go stale on DHCP drift. The program
|
|
# owns these names now; never hand-edit them in /etc/hosts.
|
|
#
|
|
# Usage:
|
|
# mesh-hosts-render # print the block to stdout (default; safe)
|
|
# mesh-hosts-render --install # splice/replace 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; }
|
|
|
|
hide_homelan=$(jq -r '.dx.hide_homelan // false' "$data_file")
|
|
|
|
# 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
|
|
|
|
# Vantage: can THIS node reach LAN IPs at all? A node with its own LAN leg (or
|
|
# the roaming laptop, whose tunnel carries the LAN /24) renders bare names and
|
|
# services at LAN IPs; a mesh-only node (yuzu) must use wg IPs instead.
|
|
short=$(hostname 2>/dev/null | cut -d. -f1)
|
|
if command -v ip >/dev/null 2>&1; then
|
|
local_ips=$(ip -o -4 addr show 2>/dev/null | awk '{print $4}' | cut -d/ -f1)
|
|
else
|
|
local_ips=$(ifconfig 2>/dev/null | awk '/inet /{print $2}')
|
|
fi
|
|
ips_json=$(printf '%s\n' $local_ips | jq -R . | jq -s .)
|
|
reachlan=$(jq -r --arg h "$short" --argjson ips "$ips_json" '
|
|
[ .hosts[] | . as $x
|
|
| select( ($x.name == $h) or ($x.aliases | index($h))
|
|
or ($x.lan != null and ($ips | index($x.lan)))
|
|
or ($ips | index($x.wg)) )
|
|
| (($x.lan != null) or ($x.class == "laptop")) ] | first // false
|
|
' "$data_file")
|
|
|
|
render_block() {
|
|
printf '%s\n' "$BEGIN"
|
|
printf '# Auto-generated from net-tools/data/mesh-hosts.json + lan-state.json — re-run to update.\n'
|
|
printf '# bare/<host>.lan = current LAN IP (direct at home, tunnel when away) · <host>.wg = mesh IP\n'
|
|
if [ "$hide_homelan" = "true" ]; then
|
|
printf '# dx mode: homelan hidden (only cloud/DO hosts rendered). Data preserved for recovery.\n'
|
|
fi
|
|
# LAN records — current discovered IP (overlay) over the static seed. Bare
|
|
# names live here only when THIS node can reach LAN IPs (vantage).
|
|
jq -r --argjson ov "$overlay" --argjson reachlan "$reachlan" --argjson hide "$hide_homelan" '
|
|
.hosts[]
|
|
| select( if $hide then (.class == "cloud") else true end )
|
|
| . as $h
|
|
| (($ov[$h.name]) // $h.lan) as $lan
|
|
| select($lan != null)
|
|
| "\($lan)\t"
|
|
+ ((([$h.name] + ($h.aliases // [])) | map(. + ".lan")) | join(" "))
|
|
+ (if $reachlan then " " + (([$h.name] + ($h.aliases // [])) | join(" ")) else "" end)
|
|
' "$data_file"
|
|
# Mesh (.wg) records — explicit tunnel path. Bare names land here for
|
|
# LAN-less hosts always, and for ALL hosts when this node is mesh-only.
|
|
jq -r --argjson reachlan "$reachlan" --argjson hide "$hide_homelan" '
|
|
.hosts[]
|
|
| select( if $hide then (.class == "cloud") else true end )
|
|
| . as $h
|
|
| "\($h.wg)\t"
|
|
+ ((([$h.name] + ($h.aliases // [])) | map(. + ".wg")) | join(" "))
|
|
+ (if ($h.lan == null) or ($reachlan | not)
|
|
then " " + (([$h.name] + ($h.aliases // [])) | join(" ")) else "" end)
|
|
' "$data_file"
|
|
# Service vhosts — the hosting host'\''s current LAN IP, or its wg IP from a
|
|
# mesh-only vantage.
|
|
jq -r --argjson ov "$overlay" --argjson reachlan "$reachlan" --argjson hide "$hide_homelan" '
|
|
. as $d
|
|
| ($d.services // {}) | to_entries[]
|
|
| select(.key != "_note")
|
|
| .key as $hname
|
|
| ($d.hosts[] | select(.name == $hname) | select( if $hide then (.class == "cloud") else true end )) as $h
|
|
| (if $reachlan then (($ov[$hname]) // $h.lan) else $h.wg end) as $addr
|
|
| select($addr != null)
|
|
| "\($addr)\t\(.value | join(" "))"
|
|
' "$data_file"
|
|
printf '%s\n' "$END"
|
|
}
|
|
|
|
block=$(render_block)
|
|
|
|
if [ "$mode" = "print" ]; then
|
|
printf '%s\n' "$block"
|
|
exit 0
|
|
fi
|
|
|
|
# Every hostname our block manages (host names, .wg/.lan forms, aliases, services)
|
|
# — loose lines carrying any of these are stale hand-maintained duplicates and
|
|
# get adopted (removed; our block supersedes them).
|
|
managed=$(printf '%s\n' "$block" | awk '!/^#/ && NF >= 2 { for (i = 2; i <= NF; i++) print $i }' | sort -u | tr '\n' ' ')
|
|
|
|
# Compute the new /etc/hosts: drop our old block + the legacy setup-lan-dns
|
|
# block, then scrub managed names out of the loose remainder.
|
|
current=$(cat "$HOSTS_FILE" 2>/dev/null || true)
|
|
stripped=$(printf '%s\n' "$current" | awk -v b="$BEGIN" -v e="$END" -v lb="$LEGACY_BEGIN" -v le="$LEGACY_END" '
|
|
$0 == b || $0 == lb { skip = 1 }
|
|
skip != 1 { print }
|
|
$0 == e || $0 == le { skip = 0 }
|
|
')
|
|
stripped=$(printf '%s\n' "$stripped" | awk -v names="$managed" '
|
|
BEGIN { n = split(names, a, /[[:space:]]+/); for (i = 1; i <= n; i++) if (a[i] != "") set[a[i]] = 1 }
|
|
/^[[:space:]]*#/ || NF < 2 { print; next }
|
|
{
|
|
kept = ""; removed = 0
|
|
for (i = 2; i <= NF; i++) {
|
|
if ($i in set) removed++
|
|
else kept = kept " " $i
|
|
}
|
|
if (removed == 0) { print; next } # untouched
|
|
if (kept == "") next # every name managed → fully adopted
|
|
print $1 kept # partially adopted → keep the rest
|
|
}
|
|
')
|
|
# Trim leading + trailing blank lines from the stripped body (the separators our
|
|
# previous splice left behind — without this, blanks accumulate and the install
|
|
# is never a no-op).
|
|
stripped=$(printf '%s\n' "$stripped" | awk 'NF {f=1; p=NR} f {l[NR]=$0} END {for(i=1;i<=p;i++) if (i in l) 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"
|