- 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".
165 lines
6.7 KiB
Bash
Executable file
165 lines
6.7 KiB
Bash
Executable file
#!/bin/sh
|
|
# host-apply — render THIS device's view of the fleet from data/mesh-hosts.json.
|
|
#
|
|
# Unlike the other renderers (which emit a uniform artifact), host-apply detects
|
|
# which host it runs on and computes addresses from THAT device's vantage point:
|
|
#
|
|
# ssh HostName for self→target =
|
|
# target.public if the target has a public IP (robust, always up)
|
|
# target.lan elif target has a LAN IP AND self can reach the LAN
|
|
# (self has a LAN IP, or self is the roaming laptop —
|
|
# the wg tunnel routes the LAN /24, and the
|
|
# smart-lan-router daemon makes it direct when home)
|
|
# target.wg else (mesh-only)
|
|
#
|
|
# It writes a single managed block (Host <name> <aliases> → HostName/User) to the
|
|
# invoking user's ~/.ssh/config, placed at the TOP so it wins first-match over
|
|
# any hand-maintained stanzas. Old names are kept as Host aliases (alias-first).
|
|
# Each stanza sets `CheckHostIP no` (host keys are stable, IPs drift — trust is
|
|
# keyed on the name, so a DHCP move doesn't trip verification) and
|
|
# `StrictHostKeyChecking accept-new` (TOFU within the private fleet, so
|
|
# non-interactive/BatchMode hops to a freshly-moved host still work).
|
|
#
|
|
# Self is identified by matching the box's hostname/short-name or any local IPv4
|
|
# (incl. the wg IP) against hosts[].{name,aliases,lan,wg}.
|
|
#
|
|
# Usage:
|
|
# host-apply # --ssh-print : print this device's ssh block (default)
|
|
# host-apply --ssh-diff # diff against current ~/.ssh/config
|
|
# host-apply --ssh-apply # splice/replace the managed block (backs up first)
|
|
# host-apply --whoami # just print which host this device resolves to
|
|
#
|
|
# Companion (run separately, needs root): `mesh-hosts-render --install` writes
|
|
# this device's /etc/hosts view (the .wg/.lan names). Together they cover a
|
|
# device's ssh + hosts views from the one source of truth.
|
|
|
|
set -eu
|
|
|
|
mode=ssh-print
|
|
case "${1:-}" in
|
|
""|--ssh-print) mode=ssh-print ;;
|
|
--ssh-diff) mode=ssh-diff ;;
|
|
--ssh-apply) mode=ssh-apply ;;
|
|
--whoami) mode=whoami ;;
|
|
*) echo "host-apply: unknown arg '$1'" >&2; exit 1 ;;
|
|
esac
|
|
|
|
BEGIN='# >>> net-tools fleet (managed by host-apply) — do not edit by hand'
|
|
END='# <<< net-tools fleet'
|
|
SSH_CONFIG="$HOME/.ssh/config"
|
|
|
|
# --- locate data file (symlink-resolving walk) ---------------------------------
|
|
self_path=$0
|
|
while [ -L "$self_path" ]; do
|
|
link=$(readlink "$self_path")
|
|
case $link in /*) self_path=$link ;; *) self_path=$(dirname "$self_path")/$link ;; esac
|
|
done
|
|
root=$(cd "$(dirname "$self_path")" && 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 "host-apply: cannot locate data/mesh-hosts.json" >&2; exit 1; }
|
|
command -v jq >/dev/null || { echo "host-apply: jq not installed" >&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 ssh tracks 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
|
|
|
|
# --- identify self -------------------------------------------------------------
|
|
short=$(hostname 2>/dev/null | cut -d. -f1)
|
|
[ -n "$short" ] || short=$(uname -n | 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 .)
|
|
|
|
self=$(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.name ] | first // empty
|
|
' "$data_file")
|
|
|
|
[ -n "$self" ] || { echo "host-apply: could not identify this host (short=$short, ips=$local_ips) in mesh-hosts.json" >&2; exit 1; }
|
|
|
|
if [ "$mode" = "whoami" ]; then
|
|
echo "$self"
|
|
exit 0
|
|
fi
|
|
|
|
# self_reaches_lan: a host with its own LAN IP, or the roaming laptop (tunnel
|
|
# routes 10.0.0.0/24; the daemon makes it direct when home).
|
|
reachlan=$(jq -r --arg s "$self" '
|
|
.hosts[] | select(.name == $s)
|
|
| ((.lan != null) or (.class == "laptop"))
|
|
' "$data_file")
|
|
|
|
# --- render this device's ssh block --------------------------------------------
|
|
render_block() {
|
|
printf '%s\n' "$BEGIN"
|
|
if [ "$hide_homelan" = "true" ]; then
|
|
printf '# rendered for: %s (dx/cloud-only; homelan hidden — set dx.hide_homelan=false to recover)\n' "$self"
|
|
else
|
|
printf '# rendered for: %s (vantage: %s)\n' "$self" \
|
|
"$( [ "$reachlan" = "true" ] && echo 'LAN-capable → prefer .lan' || echo 'mesh-only → prefer .wg' )"
|
|
fi
|
|
jq -r --arg s "$self" --argjson reachlan "$reachlan" --argjson ov "$overlay" --argjson hide "$hide_homelan" '
|
|
.hosts[]
|
|
| select(.name != $s)
|
|
| select(.ssh_user != null)
|
|
| select( if $hide then (.class == "cloud") else true end )
|
|
| . as $h
|
|
| (($ov[$h.name]) // $h.lan) as $lan
|
|
| ( $h.public
|
|
// (if $reachlan and $lan != null then $lan else null end)
|
|
// $h.wg ) as $addr
|
|
| "\nHost \(([$h.name] + $h.aliases) | join(" "))\n HostName \($addr)\n User \($h.ssh_user // "lilith")"
|
|
+ (if $h.ssh_identity then "\n IdentityFile \($h.ssh_identity)" else "" end)
|
|
+ "\n CheckHostIP no\n StrictHostKeyChecking accept-new"
|
|
' "$data_file"
|
|
printf '\n%s\n' "$END"
|
|
}
|
|
|
|
block=$(render_block)
|
|
|
|
if [ "$mode" = "ssh-print" ]; then
|
|
printf '%s\n' "$block"
|
|
exit 0
|
|
fi
|
|
|
|
# Strip any existing managed block, then prepend the fresh one (top = wins).
|
|
current=""
|
|
[ -f "$SSH_CONFIG" ] && current=$(cat "$SSH_CONFIG")
|
|
stripped=$(printf '%s\n' "$current" | awk -v b="$BEGIN" -v e="$END" '
|
|
$0 == b { skip = 1 } skip != 1 { print } $0 == e { skip = 0 }')
|
|
new=$(printf '%s\n\n%s\n' "$block" "$stripped")
|
|
|
|
if [ "$mode" = "ssh-diff" ]; then
|
|
if command -v diff >/dev/null 2>&1; then
|
|
printf '%s\n' "$new" | diff -u "${SSH_CONFIG:-/dev/null}" - || true
|
|
else
|
|
printf '%s\n' "$new"
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
# --ssh-apply
|
|
if [ -f "$SSH_CONFIG" ] && printf '%s\n' "$new" | cmp -s - "$SSH_CONFIG"; then
|
|
echo "host-apply: $SSH_CONFIG already up to date for $self"
|
|
exit 0
|
|
fi
|
|
mkdir -p "$HOME/.ssh"; chmod 700 "$HOME/.ssh"
|
|
[ -f "$SSH_CONFIG" ] && cp "$SSH_CONFIG" "$SSH_CONFIG.netbak"
|
|
printf '%s\n' "$new" > "$SSH_CONFIG"
|
|
chmod 600 "$SSH_CONFIG"
|
|
echo "host-apply: wrote $self's fleet block to $SSH_CONFIG (backup: $SSH_CONFIG.netbak)"
|