#!/bin/sh # kit-connect daemon — manages Tailscale + reverse SSH tunnel. # Runs under procd supervision (auto-respawn on crash). # Config: /etc/config/kitconnect (UCI) + hub-assigned settings. # One package, all GL routers. Hub assigns ports and keys. PKG="kit-connect" STATE_DIR="/var/run/kitconnect" CONF="/etc/config/kitconnect" KEY="/etc/kitconnect/connect_id_ed25519" PID_FILE="$STATE_DIR/daemon.pid" TUNNEL_PID="$STATE_DIR/tunnel.pid" LOG_TAG="kit-connect" log() { logger -t "$LOG_TAG" -p daemon.info "$*" } warn() { logger -t "$LOG_TAG" -p daemon.warn "WARN: $*" } mkdir -p "$STATE_DIR" chmod 600 "$KEY" 2>/dev/null echo "$$" > "$PID_FILE" # ── Config loader (UCI → shell vars) ────────────────────────── load_config() { _uci() { uci -q get "kitconnect.main.${1}" 2>/dev/null || echo "${2}"; } DEVICE_ID=$(_uci device_id "") [ -z "$DEVICE_ID" ] && DEVICE_ID=$(cat /etc/busrouter/device-id 2>/dev/null || hostname | sed 's/[^a-zA-Z0-9_-]//g') HUB_HOST=$(_uci hub_host "162.243.83.36") HUB_PORT=$(_uci hub_port "8080") TUNNEL_ENABLE=$(_uci tunnel_enable "1") TUNNEL_REMOTE_HOST=$(_uci tunnel_remote_host "162.243.83.36") TUNNEL_REMOTE_USER=$(_uci tunnel_remote_user "node") TUNNEL_REMOTE_SSH_PORT=$(_uci tunnel_remote_ssh_port "22") TUNNEL_REMOTE_PORT=$(_uci tunnel_remote_port "0") TUNNEL_LOCAL_PORT=$(_uci tunnel_local_port "2223") TUNNEL_RETRY_DELAY=$(_uci tunnel_retry_delay "30") TAILSCALE_ENABLE=$(_uci tailscale_enable "1") TAILSCALE_AUTH_KEY=$(_uci tailscale_auth_key "") TAILSCALE_HOSTNAME=$(_uci tailscale_hostname "") [ -z "$TAILSCALE_HOSTNAME" ] && TAILSCALE_HOSTNAME="$DEVICE_ID" WATCHDOG_INTERVAL=$(_uci watchdog_interval "60") FORWARD_FAIL_LIMIT=$(_uci forward_fail_limit "2") # Binary detection TAILSCALE_BIN=$(command -v tailscale 2>/dev/null || echo "/usr/sbin/tailscale") TAILSCALED_BIN=$(command -v tailscaled 2>/dev/null || echo "/usr/sbin/tailscaled") } # ── Start Tailscale ──────────────────────────────────────────── start_tailscale() { [ "$TAILSCALE_ENABLE" != "1" ] && { log "tailscale: disabled in config"; return 0; } [ -z "$TAILSCALE_AUTH_KEY" ] && { log "tailscale: no auth key — skipping"; return 0; } ! command -v "$TAILSCALE_BIN" >/dev/null 2>&1 && { log "tailscale: binary not found"; return 1; } # Already connected? if "$TAILSCALE_BIN" status >/dev/null 2>&1; then log "tailscale: already connected ($($TAILSCALE_BIN ip -4 2>/dev/null || echo no-ip))" return 0 fi log "tailscale: starting daemon..." "$TAILSCALED_BIN" --tun=userspace-networking >/dev/null 2>&1 & sleep 3 log "tailscale: authenticating..." "$TAILSCALE_BIN" up \ --auth-key "$TAILSCALE_AUTH_KEY" \ --hostname "${TAILSCALE_HOSTNAME}" \ --accept-routes=false \ --accept-dns=false 2>&1 | while read -r line; do log "tailscale: $line"; done local ts_ip ts_ip=$("$TAILSCALE_BIN" ip -4 2>/dev/null || echo "unknown") log "tailscale: connected — IP=${ts_ip}" } # ── Start reverse SSH tunnel ─────────────────────────────────── start_tunnel() { [ "$TUNNEL_ENABLE" != "1" ] && { log "tunnel: disabled"; return 0; } [ "$TUNNEL_REMOTE_PORT" = "0" ] && { register_with_hub; load_config; } [ "$TUNNEL_REMOTE_PORT" = "0" ] && { log "tunnel: no port assigned — skipping"; return 0; } log "tunnel: opening R:0.0.0.0:${TUNNEL_REMOTE_PORT} → 127.0.0.1:${TUNNEL_LOCAL_PORT}" ssh \ -o StrictHostKeyChecking=no \ -o UserKnownHostsFile=/dev/null \ -o ServerAliveInterval=15 \ -o ServerAliveCountMax=3 \ -o ExitOnForwardFailure=yes \ -o BatchMode=yes \ -p "${TUNNEL_REMOTE_SSH_PORT}" \ -N \ -R "0.0.0.0:${TUNNEL_REMOTE_PORT}:127.0.0.1:${TUNNEL_LOCAL_PORT}" \ -i "$KEY" \ "${TUNNEL_REMOTE_USER}@${TUNNEL_REMOTE_HOST}" \ 2>"$STATE_DIR/ssh_err" & SSH_PID=$! echo "$SSH_PID" > "$TUNNEL_PID" log "tunnel: SSH PID=${SSH_PID} port=${TUNNEL_REMOTE_PORT}" # Watchdog loop — monitors tunnel health _fail_count=0 while kill -0 "$SSH_PID" 2>/dev/null; do sleep "$WATCHDOG_INTERVAL" kill -0 "$SSH_PID" 2>/dev/null || break if timeout 10 ssh \ -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ -o ConnectTimeout=5 -o BatchMode=yes \ -p "${TUNNEL_REMOTE_SSH_PORT}" -i "$KEY" \ "${TUNNEL_REMOTE_USER}@${TUNNEL_REMOTE_HOST}" \ "timeout 3 bash -c 'echo >/dev/tcp/127.0.0.1/${TUNNEL_REMOTE_PORT}' 2>/dev/null" \ 2>/dev/null; then [ "$_fail_count" -gt 0 ] && log "tunnel: forward recovered after ${_fail_count} checks" _fail_count=0 else _fail_count=$((_fail_count + 1)) warn "tunnel: forward check failed (${_fail_count}/${FORWARD_FAIL_LIMIT})" [ "$_fail_count" -ge "$FORWARD_FAIL_LIMIT" ] && break fi done kill "$SSH_PID" 2>/dev/null rm -f "$TUNNEL_PID" log "tunnel: exited — will retry in ${TUNNEL_RETRY_DELAY}s" sleep "$TUNNEL_RETRY_DELAY" return 1 } # ── Hub registration — get assigned port ─────────────────────── register_with_hub() { [ "$TUNNEL_REMOTE_PORT" != "0" ] && { return 0; } log "hub: registering device ${DEVICE_ID}..." resp=$(curl -s --connect-timeout 10 --max-time 15 \ "http://${HUB_HOST}:${HUB_PORT}/api/register/${DEVICE_ID}" 2>/dev/null) || true [ -z "$resp" ] && { warn "hub: unreachable"; return 1; } # Parse JSON response (no jq dependency — grep + cut) port=$(echo "$resp" | grep -o '"tunnel_port"[[:space:]]*:[[:space:]]*[0-9]*' | grep -o '[0-9]*') tskey=$(echo "$resp" | grep -o '"tailscale_auth_key"[[:space:]]*:[[:space:]]*"[^"]*"' | cut -d'"' -f4) if [ -n "$port" ] && [ "$port" != "0" ]; then uci set kitconnect.main.tunnel_remote_port="$port" uci commit kitconnect log "hub: assigned port ${port}" echo "$port" > "$STATE_DIR/hub_port" fi if [ -n "$tskey" ] && [ "$tskey" != "$TAILSCALE_AUTH_KEY" ]; then uci set kitconnect.main.tailscale_auth_key="$tskey" uci commit kitconnect log "hub: updated Tailscale key" fi } # ── Main loop (one cycle — procd respawns on exit) ──────────── log "EVENT=DAEMON_START device=${DEVICE_ID}" load_config # Ensure device-id file exists mkdir -p /etc/busrouter [ ! -f /etc/busrouter/device-id ] && echo "$DEVICE_ID" > /etc/busrouter/device-id # Register with hub (non-fatal — retries next cycle) register_with_hub 2>/dev/null || true # Start Tailscale (runs in background, stays up) start_tailscale 2>/dev/null || true # Start tunnel (blocks until failure, then exits — procd restarts) start_tunnel 2>/dev/null || true log "EVENT=DAEMON_EXIT device=${DEVICE_ID}" exit 0