Files
kit-busrouter/gl-kit-connect/files/usr/sbin/connect-daemon.sh
T
kitadmin 14d46d96a4 fix: daemon hang, postinst mkdir, hub key authorization, boot persistence
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>
2026-07-22 04:09:49 +00:00

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