#!/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 → 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)"