14d46d96a4
SPK daemon fixes (x5925 testing feedback): - register_with_hub: added --max-time 15 to prevent indefinite hang - Removed bash 'local' keyword for busybox ash compatibility - postinst: mkdir -p /usr/local/bin before copying Tailscale binaries - postinst: chmod +x all bin/*.sh (fixes 644 execute bit bug) - Added x5925-boot.sh for reboot persistence (stopgap until daemon fixed) Hub security hardening: - Added POST /api/authorize-key endpoint with device_id + pubkey - Keys auto-authorized with restrict,port-forwarding,permitlisten="<port>" - No shell access allowed — only tunnel forwarding to assigned port - Server.py updated with input validation on key format - register.sh --authorize-key subcommand for secure key management GL daemon: same --max-time fix applied for curl timeout Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
189 lines
6.6 KiB
Bash
189 lines
6.6 KiB
Bash
#!/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
|