#!/bin/sh
#
# ssh-reverse 6.0.2
# Copyright (c)2021-2026 John Lawson & Sons
# All Rights Reserved
#
# Persistent reverse SSH tunnel with API-assigned remote port.
#

set -eu
umask 077

# ---------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------

API_BASE_URL="https://api.isignage.app/devices"
SSH_HOST="ssh.isignage.uk"
SSH_USER="root"

LOCAL_PORT=65535

CONNECT_TIMEOUT=10
SERVER_ALIVE_INTERVAL=15
SERVER_ALIVE_COUNT_MAX=2

STABLE_CONNECTION_SECS=120

NET_FAIL_INITIAL_DELAY=5
NET_FAIL_MAX_DELAY=60

FORWARD_FAIL_INITIAL_DELAY=5
FORWARD_FAIL_MAX_DELAY=30

RUNDIR="/run/ssh-reverse"
PORTFILE="$RUNDIR/port"
ERRFILE="$RUNDIR/ssh.err"

KNOWN_HOSTS_FILE="/root/.ssh/known_hosts"

# ---------------------------------------------------------------------
# Runtime directory
# ---------------------------------------------------------------------

mkdir -p "$RUNDIR"

# ---------------------------------------------------------------------
# Get the unique device ID
# ---------------------------------------------------------------------

ID="$(
	tr -d '\0' \
		< /sys/firmware/devicetree/base/serial-number \
		2>/dev/null |
		tr '[:lower:]' '[:upper:]' ||
		true
)"

if [ -z "${ID:-}" ]; then
	ID="$(
		cat /etc/machine-id 2>/dev/null |
			tr '[:lower:]' '[:upper:]' ||
			true
	)"
fi

[ -n "${ID:-}" ] || exit 0

API_URL="${API_BASE_URL}/${ID}/port"

# ---------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------

increase_delay()
{
	current="$1"
	maximum="$2"

	next=$((current * 2))

	if [ "$next" -gt "$maximum" ]; then
		next="$maximum"
	fi

	printf '%s\n' "$next"
}

fetch_port()
{
	PORT="$(
		curl \
			-fsS \
			--user-agent "iSIGNAGE" \
			--connect-timeout 5 \
			--max-time 10 \
			--retry 3 \
			--retry-delay 1 \
			--retry-all-errors \
			"$API_URL" |
			tr -d '\r\n'
	)"

	case "$PORT" in
		''|*[!0-9]*)
			return 1
			;;
	esac

	if [ "$PORT" -lt 49152 ] 2>/dev/null ||
		[ "$PORT" -gt 65535 ] 2>/dev/null; then
		return 1
	fi

	printf '%s\n' "$PORT" >"$PORTFILE"
	printf '%s\n' "$PORT"

	return 0
}

run_tunnel()
{
	port="$1"

	ssh \
		-o BatchMode=yes \
		-o ConnectTimeout="$CONNECT_TIMEOUT" \
		-o ExitOnForwardFailure=yes \
		-o ServerAliveInterval="$SERVER_ALIVE_INTERVAL" \
		-o ServerAliveCountMax="$SERVER_ALIVE_COUNT_MAX" \
		-o StrictHostKeyChecking=yes \
		-o UserKnownHostsFile="$KNOWN_HOSTS_FILE" \
		-N \
		-R "${port}:localhost:${LOCAL_PORT}" \
		"${SSH_USER}@${SSH_HOST}" \
		2>"$ERRFILE"
}

# ---------------------------------------------------------------------
# Main reconnect loop
# ---------------------------------------------------------------------

net_fail_delay="$NET_FAIL_INITIAL_DELAY"
forward_fail_delay="$FORWARD_FAIL_INITIAL_DELAY"

while :; do
	if ! PORT="$(fetch_port 2>/dev/null)"; then
		sleep "$net_fail_delay"

		net_fail_delay="$(
			increase_delay \
				"$net_fail_delay" \
				"$NET_FAIL_MAX_DELAY"
		)"

		continue
	fi

	# DNS, HTTPS and the API are reachable again.
	net_fail_delay="$NET_FAIL_INITIAL_DELAY"

	: >"$ERRFILE"

	start_ts="$(date +%s)"

	run_tunnel "$PORT" || true

	end_ts="$(date +%s)"
	runtime=$((end_ts - start_ts))

	# A tunnel that remained active for this long is considered stable.
	# Reset both retry delays after a stable connection.
	if [ "$runtime" -ge "$STABLE_CONNECTION_SECS" ]; then
		net_fail_delay="$NET_FAIL_INITIAL_DELAY"
		forward_fail_delay="$FORWARD_FAIL_INITIAL_DELAY"
	fi

	# A stale tunnel on the server may temporarily retain the assigned
	# remote port. Back off progressively to give the old connection time
	# to expire.
	if grep -q \
		"remote port forwarding failed for listen port" \
		"$ERRFILE" \
		2>/dev/null; then

		sleep "$forward_fail_delay"

		forward_fail_delay="$(
			increase_delay \
				"$forward_fail_delay" \
				"$FORWARD_FAIL_MAX_DELAY"
		)"

		continue
	fi

	# For DNS, routing, network or general SSH failures, use the shorter
	# network backoff.
	sleep "$net_fail_delay"

	net_fail_delay="$(
		increase_delay \
			"$net_fail_delay" \
			"$NET_FAIL_MAX_DELAY"
	)"
done
