SSH into one stable Tailscale node and reach your whole tailnet from it via short per-host functions + sshfs mounts. A riced Arch dev container with sshd, a stateful home, and declarative .env auth. See README.md and ROAMING.md.
154 lines
6.6 KiB
Bash
154 lines
6.6 KiB
Bash
# ╔══════════════════════════════════════════════════════════════════╗
|
|
# ║ arch-dev roaming :: engine ║
|
|
# ║ Roaming ~ over the tailnet. Source this from .zshrc. ║
|
|
# ║ ║
|
|
# ║ Reads ~/.config/roaming/hosts and generates a function per ║
|
|
# ║ host so you can run: ║
|
|
# ║ mail → interactive shell on mail ║
|
|
# ║ mail tail -f /var/log/x → run with TTY (works for -f, sudo) ║
|
|
# ║ mount_host mail → sshfs the host's mount at ║
|
|
# ║ /workspace/mail ║
|
|
# ╚══════════════════════════════════════════════════════════════════╝
|
|
|
|
export ROAMING_DIR="${ROAMING_DIR:-$HOME/.config/roaming}"
|
|
export ROAMING_HOSTS="${ROAMING_HOSTS:-$ROAMING_DIR/hosts}"
|
|
export ROAMING_MOUNT_BASE="${ROAMING_MOUNT_BASE:-/workspace}"
|
|
export ROAMING_DEFAULT_USER="${ROAMING_DEFAULT_USER:-$USER}"
|
|
|
|
# sshfs options that prevent the dreaded indefinite-freeze on network blips.
|
|
# ServerAliveInterval=15 + CountMax=3 → I/O errors out after ~45s instead of
|
|
# hanging forever. reconnect re-establishes when the host comes back.
|
|
export ROAMING_SSHFS_OPTS="reconnect,ServerAliveInterval=15,ServerAliveCountMax=3,follow_symlinks"
|
|
|
|
# ── Internal: resolve a host name to its registry fields ──────────────────────
|
|
# Sets globals: _RH_NAME _RH_FQDN _RH_USER _RH_MOUNT
|
|
# Returns 1 if not found.
|
|
_roaming_lookup() {
|
|
local want="$1"
|
|
[[ -f "$ROAMING_HOSTS" ]] || return 1
|
|
local name fqdn user mount
|
|
while read -r name fqdn user mount; do
|
|
[[ -z "$name" || "$name" == \#* ]] && continue
|
|
if [[ "$name" == "$want" ]]; then
|
|
_RH_NAME="$name"
|
|
_RH_FQDN="$fqdn"
|
|
_RH_USER="${user:-$ROAMING_DEFAULT_USER}"
|
|
_RH_MOUNT="$mount"
|
|
return 0
|
|
fi
|
|
done < "$ROAMING_HOSTS"
|
|
return 1
|
|
}
|
|
|
|
# ── Internal: the actual remote-exec logic ────────────────────────────────────
|
|
# No args → interactive shell. Args → run with a TTY (so tail -f / sudo work).
|
|
# "Dumb" by design: pipes/redirects run LOCALLY, exactly like plain ssh.
|
|
_roaming_exec() {
|
|
local name="$1"; shift
|
|
if ! _roaming_lookup "$name"; then
|
|
echo "roaming: unknown host '$name' (check $ROAMING_HOSTS)" >&2
|
|
return 1
|
|
fi
|
|
local target="${_RH_USER}@${_RH_FQDN}"
|
|
if [[ $# -eq 0 ]]; then
|
|
ssh -t "$target"
|
|
else
|
|
ssh -t "$target" "$@"
|
|
fi
|
|
}
|
|
|
|
# ── mount_host <name> ─────────────────────────────────────────────────────────
|
|
mount_host() {
|
|
local name="$1"
|
|
if [[ -z "$name" ]]; then
|
|
echo "usage: mount_host <name>" >&2
|
|
return 1
|
|
fi
|
|
if ! _roaming_lookup "$name"; then
|
|
echo "roaming: unknown host '$name'" >&2
|
|
return 1
|
|
fi
|
|
if [[ -z "$_RH_MOUNT" ]]; then
|
|
echo "roaming: '$name' has no mount path in registry" >&2
|
|
return 1
|
|
fi
|
|
local mp="$ROAMING_MOUNT_BASE/$name"
|
|
# Already mounted?
|
|
if mountpoint -q "$mp" 2>/dev/null; then
|
|
echo "roaming: $name already mounted at $mp"
|
|
return 0
|
|
fi
|
|
mkdir -p "$mp"
|
|
echo "roaming: mounting ${_RH_USER}@${_RH_FQDN}:${_RH_MOUNT} → $mp"
|
|
sshfs "${_RH_USER}@${_RH_FQDN}:${_RH_MOUNT}" "$mp" \
|
|
-o "$ROAMING_SSHFS_OPTS" \
|
|
&& echo "roaming: ✓ mounted" \
|
|
|| echo "roaming: ✗ mount failed" >&2
|
|
}
|
|
|
|
# ── unmount_host <name> ───────────────────────────────────────────────────────
|
|
unmount_host() {
|
|
local name="$1"
|
|
if [[ -z "$name" ]]; then
|
|
echo "usage: unmount_host <name>" >&2
|
|
return 1
|
|
fi
|
|
local mp="$ROAMING_MOUNT_BASE/$name"
|
|
if ! mountpoint -q "$mp" 2>/dev/null; then
|
|
echo "roaming: $name not mounted"
|
|
return 0
|
|
fi
|
|
# fusermount3 first; -z (lazy) as fallback for stale mounts
|
|
fusermount3 -u "$mp" 2>/dev/null \
|
|
|| fusermount3 -uz "$mp" 2>/dev/null \
|
|
|| umount "$mp" 2>/dev/null
|
|
if mountpoint -q "$mp" 2>/dev/null; then
|
|
echo "roaming: ✗ failed to unmount $name (stale? try: fusermount3 -uz $mp)" >&2
|
|
return 1
|
|
fi
|
|
rmdir "$mp" 2>/dev/null
|
|
echo "roaming: ✓ unmounted $name"
|
|
}
|
|
|
|
# ── roaming-status ────────────────────────────────────────────────────────────
|
|
roaming-status() {
|
|
echo "── registered hosts ──"
|
|
if [[ -f "$ROAMING_HOSTS" ]]; then
|
|
local name fqdn user mount
|
|
while read -r name fqdn user mount; do
|
|
[[ -z "$name" || "$name" == \#* ]] && continue
|
|
local mp="$ROAMING_MOUNT_BASE/$name"
|
|
local mflag=""
|
|
mountpoint -q "$mp" 2>/dev/null && mflag=" [mounted: $mp]"
|
|
printf " %-10s %s@%s%s\n" "$name" "${user:-$ROAMING_DEFAULT_USER}" "$fqdn" "$mflag"
|
|
done < "$ROAMING_HOSTS"
|
|
else
|
|
echo " (no registry at $ROAMING_HOSTS)"
|
|
fi
|
|
}
|
|
|
|
# ── Generate a function per host ──────────────────────────────────────────────
|
|
# After this loop, each registry entry `mail` becomes a `mail` command.
|
|
_roaming_generate() {
|
|
[[ -f "$ROAMING_HOSTS" ]] || return 0
|
|
local name fqdn user mount
|
|
while read -r name fqdn user mount; do
|
|
[[ -z "$name" || "$name" == \#* ]] && continue
|
|
# Skip if it would clobber an existing command (safety)
|
|
if command -v "$name" >/dev/null 2>&1 && ! typeset -f "$name" >/dev/null 2>&1; then
|
|
echo "roaming: skipping '$name' — conflicts with existing command" >&2
|
|
continue
|
|
fi
|
|
eval "$name() { _roaming_exec '$name' \"\$@\"; }"
|
|
done < "$ROAMING_HOSTS"
|
|
}
|
|
|
|
# Reload registry + regenerate functions (call after editing hosts)
|
|
roaming-reload() {
|
|
_roaming_generate
|
|
echo "roaming: reloaded $(grep -cvE '^\s*#|^\s*$' "$ROAMING_HOSTS" 2>/dev/null || echo 0) host(s)"
|
|
}
|
|
|
|
# Generate on source
|
|
_roaming_generate
|