diff --git a/gl-kit-connect/files/usr/sbin/connect-daemon.sh b/gl-kit-connect/files/usr/sbin/connect-daemon.sh index beb01b9..c694dcf 100644 --- a/gl-kit-connect/files/usr/sbin/connect-daemon.sh +++ b/gl-kit-connect/files/usr/sbin/connect-daemon.sh @@ -144,7 +144,7 @@ register_with_hub() { log "hub: registering device ${DEVICE_ID}..." - resp=$(curl -s --connect-timeout 10 \ + 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; } diff --git a/hub/register.sh b/hub/register.sh index a28ce0e..2182a2e 100644 --- a/hub/register.sh +++ b/hub/register.sh @@ -1,19 +1,69 @@ #!/bin/sh -# busfleet-hub register.sh — Device registration endpoint +# busfleet-hub register.sh — Device registration endpoint. # Called with DEVICE_ID as $1. Returns JSON on stdout. # Port pool: 2230-2299 +# +# Also supports --authorize-key to authorize a device's tunnel key +# with restrict,port-forwarding,permitlisten="". + +set -e + +ACTION="${1:-}" +DEVICE_ID="${2:-}" -DEVICE_ID="${1:-unknown}" REGISTRY="/opt/busfleet-hub/port-registry.json" +AUTHORIZED_KEYS="/home/node/.ssh/authorized_keys" PORT_POOL_START="${PORT_POOL_START:-2230}" PORT_POOL_END="${PORT_POOL_END:-2299}" TAILSCALE_AUTH_KEY="${TAILSCALE_AUTH_KEY:-tskey-auth-kJz8wqNVo211CNTRL-GNL5EFjp5aWQcaWPSVn2aW9TNworKUNBV}" # Init registry if missing -if [ ! -f "$REGISTRY" ]; then - echo '{}' > "$REGISTRY" +[ ! -f "$REGISTRY" ] && echo '{}' > "$REGISTRY" + +if [ "$ACTION" = "--authorize-key" ]; then + # ── Authorize a device's public key for tunnel access ────────── + PUBKEY="${3:-}" + DEVICE_ID="${2:-}" + if [ -z "$PUBKEY" ] || [ -z "$DEVICE_ID" ]; then + echo '{"error": "usage: register.sh --authorize-key "}' + exit 1 + fi + + # Look up the device's assigned port + ASSIGNED_PORT=$(python3 -c " +import json +with open('$REGISTRY') as f: + reg = json.load(f) +print(reg.get('$DEVICE_ID', {}).get('tunnel_port', '')) +" 2>/dev/null) + + if [ -z "$ASSIGNED_PORT" ]; then + echo "{\"error\": \"device ${DEVICE_ID} not registered — register first\"}" + exit 1 + fi + + # Check if key already authorized (update if so) + if grep -q "kit-connect-${DEVICE_ID}" "$AUTHORIZED_KEYS" 2>/dev/null; then + # Remove old entry + sed -i "/kit-connect-${DEVICE_ID}/d" "$AUTHORIZED_KEYS" 2>/dev/null + fi + + # Append with restrict options — only port-forwarding to the assigned port + RESTRICT="restrict,port-forwarding,permitlisten=\"${ASSIGNED_PORT}\"" + echo "${RESTRICT} ${PUBKEY} kit-connect-${DEVICE_ID}" >> "$AUTHORIZED_KEYS" + chown node:node "$AUTHORIZED_KEYS" 2>/dev/null || true + chmod 600 "$AUTHORIZED_KEYS" + + cat < — assign tunnel port, return JSON - GET /health — health check + GET /api/register/ — assign tunnel port, return JSON + POST /api/authorize-key — authorize device's tunnel key + GET /health — health check """ import json @@ -15,7 +16,7 @@ REGISTER_SH = "/opt/busfleet-hub/register.sh" class HubHandler(BaseHTTPRequestHandler): - """Handle hub registration requests.""" + """Handle hub registration and key authorization requests.""" def do_GET(self): # Route: /api/register/ @@ -45,6 +46,44 @@ class HubHandler(BaseHTTPRequestHandler): self.send_json_error(404, "Not found") + def do_POST(self): + # Route: /api/authorize-key + if self.path.startswith("/api/authorize-key"): + content_length = int(self.headers.get("Content-Length", 0)) + body = self.rfile.read(content_length).decode() if content_length else "{}" + + try: + data = json.loads(body) + except json.JSONDecodeError: + self.send_json_error(400, "Invalid JSON body") + return + + device_id = data.get("device_id", "").strip() + pubkey = data.get("pubkey", "").strip() + if not device_id or not pubkey: + self.send_json_error(400, "Missing device_id or pubkey") + return + + # Basic validation: key must look like an SSH public key + if not pubkey.startswith(("ssh-", "ecdsa-", "sk-")) or len(pubkey) < 80: + self.send_json_error(400, "Invalid SSH public key format") + return + + try: + result = subprocess.run( + [REGISTER_SH, "--authorize-key", device_id, pubkey], + capture_output=True, text=True, timeout=15, + ) + status = 200 if result.returncode == 0 else 500 + self.send_json(status, result.stdout.strip()) + except subprocess.TimeoutExpired: + self.send_json_error(504, "Key authorization timed out") + except Exception as exc: + self.send_json_error(500, str(exc)) + return + + self.send_json_error(404, "Not found") + def send_json(self, status, body): """Send a JSON response.""" self.send_response(status) diff --git a/kit-connect/bin/connect-daemon.sh b/kit-connect/bin/connect-daemon.sh old mode 100644 new mode 100755 index 1da994a..6aa4c65 --- a/kit-connect/bin/connect-daemon.sh +++ b/kit-connect/bin/connect-daemon.sh @@ -91,7 +91,7 @@ start_tailscale() { --accept-routes=false \ --accept-dns=false 2>&1 | while read line; do log "tailscale: $line"; done - local ts_ip=$("$TAILSCALE_BIN" --socket="$TAILSCALE_SOCKET" ip -4 2>/dev/null || echo "unknown") + ts_ip=$("$TAILSCALE_BIN" --socket="$TAILSCALE_SOCKET" ip -4 2>/dev/null || echo "unknown") log "tailscale: connected — IP=${ts_ip}" } @@ -152,33 +152,40 @@ start_tunnel() { # ── Hub registration — get assigned port ─────────────────────────── register_with_hub() { - [ "$TUNNEL_REMOTE_PORT" != "0" ] && { log "hub: already registered (port=${TUNNEL_REMOTE_PORT})"; return 0; } + # Already registered — skip + if [ "$TUNNEL_REMOTE_PORT" != "0" ] && [ -n "$TUNNEL_REMOTE_PORT" ]; then + return 0 + fi log "hub: registering device ${DEVICE_ID}..." - local resp="" - resp=$(curl -s --connect-timeout 10 \ + # --max-time 15: total timeout (connect + response). SRM busybox curl needs this + # or the daemon hangs forever if the hub accepts the TCP connection but + # never sends the HTTP response. + resp="" + resp=$(curl -s --connect-timeout 10 --max-time 15 \ "http://${HUB_HOST}:${HUB_PORT}/api/register/${DEVICE_ID}" 2>/dev/null) || true if [ -z "$resp" ]; then - log "hub: unreachable — using last-known config" + log "hub: unreachable — will retry next cycle" return 1 fi - # Parse JSON response (minimal — avoids jq dependency) - local port="" - port=$(echo "$resp" | grep -o '"tunnel_port"[[:space:]]*:[[:space:]]*[0-9]*' | grep -o '[0-9]*') - local tskey="" - tskey=$(echo "$resp" | grep -o '"tailscale_auth_key"[[:space:]]*:[[:space:]]*"[^"]*"' | cut -d'"' -f4) + # Parse JSON response — POSIX-safe, no jq dependency + port="" + tskey="" + port=$(printf '%s' "$resp" | sed -n 's/.*"tunnel_port"[[:space:]]*:[[:space:]]*\([0-9]*\).*/\1/p') + tskey=$(printf '%s' "$resp" | sed -n 's/.*"tailscale_auth_key"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p') if [ -n "$port" ] && [ "$port" != "0" ]; then - # Update config with assigned port sed -i "s/^TUNNEL_REMOTE_PORT=.*/TUNNEL_REMOTE_PORT=${port}/" "$CONF" 2>/dev/null + TUNNEL_REMOTE_PORT="$port" log "hub: assigned port ${port}" fi if [ -n "$tskey" ] && [ "$tskey" != "$TAILSCALE_AUTH_KEY" ]; then sed -i "s|^TAILSCALE_AUTH_KEY=.*|TAILSCALE_AUTH_KEY=${tskey}|" "$CONF" 2>/dev/null + TAILSCALE_AUTH_KEY="$tskey" log "hub: updated Tailscale key" fi } diff --git a/kit-connect/bin/x5925-boot.sh b/kit-connect/bin/x5925-boot.sh new file mode 100755 index 0000000..92e0e6b --- /dev/null +++ b/kit-connect/bin/x5925-boot.sh @@ -0,0 +1,86 @@ +#!/bin/sh +# x5925-boot.sh — Boot persistence stopgap for x5925 +# Brings up Tailscale + reverse SSH tunnel on reboot. +# Runs as a Synology scheduled trigger-on-boot task. +# Once the SPK daemon is fixed, this script becomes redundant. + +LOG_TAG="kit-connect-boot" +log() { logger -t "$LOG_TAG" -p local0.warn "$*"; } + +log "=== Boot persistence for x5925 ===" +log "device=x5925 hub=162.243.83.36 port=2230" + +# ── 1. Tailscale ──────────────────────────────────────────────── +TAILSCALE_BIN="/usr/local/bin/tailscale" +TAILSCALED_BIN="/usr/local/bin/tailscaled" +TAILSCALE_STATEDIR="/var/packages/Tailscale/var/state" +TAILSCALE_SOCKET="/var/packages/Tailscale/var/run/tailscaled.sock" +AUTH_KEY="tskey-auth-kJz8wqNVo211CNTRL-GNL5EFjp5aWQcaWPSVn2aW9TNworKUNBV" + +if [ -x "$TAILSCALED_BIN" ]; then + mkdir -p "$TAILSCALE_STATEDIR" "$(dirname "$TAILSCALE_SOCKET")" + + # Check if already running + if "$TAILSCALE_BIN" --socket="$TAILSCALE_SOCKET" status >/dev/null 2>&1; then + log "tailscale: already connected — $("$TAILSCALE_BIN" --socket="$TAILSCALE_SOCKET" ip -4 2>/dev/null || echo no-ip)" + else + log "tailscale: starting tailscaled..." + /usr/bin/setsid "$TAILSCALED_BIN" \ + --statedir="$TAILSCALE_STATEDIR" \ + --tun=userspace-networking \ + --socket="$TAILSCALE_SOCKET" \ + >/dev/null 2>&1 & + sleep 4 + + log "tailscale: authenticating..." + "$TAILSCALE_BIN" --socket="$TAILSCALE_SOCKET" up \ + --auth-key "$AUTH_KEY" \ + --hostname x5925 \ + --accept-routes=false \ + --accept-dns=false 2>&1 | while read -r line; do log "tailscale: $line"; done + + ts_ip=$("$TAILSCALE_BIN" --socket="$TAILSCALE_SOCKET" ip -4 2>/dev/null || echo "unknown") + log "tailscale: connected — IP=${ts_ip}" + fi +else + log "tailscale: binaries not found at $TAILSCALED_BIN" +fi + +# ── 2. Reverse SSH tunnel ─────────────────────────────────────── +TUNNEL_KEY="/var/packages/kit-connect/target/bin/connect_id_ed25519" +TUNNEL_HOST="162.243.83.36" +TUNNEL_USER="node" +TUNNEL_PORT="2230" +LOCAL_SSH_PORT="2223" + +if [ -f "$TUNNEL_KEY" ]; then + chmod 600 "$TUNNEL_KEY" + + # Check if tunnel already exists + if pgrep -f "ssh.*-R.*${TUNNEL_PORT}" >/dev/null 2>&1; then + log "tunnel: already active on port ${TUNNEL_PORT}" + else + log "tunnel: opening R:0.0.0.0:${TUNNEL_PORT} -> 127.0.0.1:${LOCAL_SSH_PORT}" + + ssh \ + -o StrictHostKeyChecking=no \ + -o UserKnownHostsFile=/dev/null \ + -o ServerAliveInterval=15 \ + -o ServerAliveCountMax=3 \ + -o ExitOnForwardFailure=yes \ + -o BatchMode=yes \ + -p 22 -N \ + -R "0.0.0.0:${TUNNEL_PORT}:127.0.0.1:${LOCAL_SSH_PORT}" \ + -i "$TUNNEL_KEY" \ + "${TUNNEL_USER}@${TUNNEL_HOST}" \ + 2>/var/packages/kit-connect/target/var/boot-tunnel.err & + + TUNNEL_PID=$! + log "tunnel: SSH PID=${TUNNEL_PID}" + echo "$TUNNEL_PID" > /var/packages/kit-connect/target/var/boot-tunnel.pid + fi +else + log "tunnel: key not found at $TUNNEL_KEY" +fi + +log "=== Boot persistence complete ===" diff --git a/kit-connect/scripts/postinst b/kit-connect/scripts/postinst index f75cfa2..9f21f0f 100755 --- a/kit-connect/scripts/postinst +++ b/kit-connect/scripts/postinst @@ -16,12 +16,16 @@ chmod +x "$PKG_DIR/wizard.sh" 2>/dev/null || true chmod 600 "$PKG_DIR/bin/connect_id_ed25519" 2>/dev/null || true # ── Install bundled Tailscale binaries if system doesn't have them ── +# SRM doesn't have /usr/local/bin by default — create it first. +mkdir -p /usr/local/bin if [ ! -f /usr/local/bin/tailscale ] && [ -f "$PKG_DIR/bin/tailscale" ]; then - log "Installing bundled Tailscale binaries..." + log "Installing bundled Tailscale binaries (1.98.9 ARM)..." cp "$PKG_DIR/bin/tailscale" /usr/local/bin/tailscale cp "$PKG_DIR/bin/tailscaled" /usr/local/bin/tailscaled chmod +x /usr/local/bin/tailscale /usr/local/bin/tailscaled log "Tailscale binaries installed to /usr/local/bin" +elif [ -f /usr/local/bin/tailscale ]; then + log "Tailscale already present at /usr/local/bin/tailscale" fi # Run the Keylink IT setup wizard (non-interactive)