#!/bin/sh
# =============================================================================
# bootstrap.sh  --  QualityNOC one-shot VPN bootstrap for Teltonika (RUTOS 7.x)
# -----------------------------------------------------------------------------
# Replaces the 5-step RMS task groups with a single Command. Installs the
# provisioner, sets the VPN tag, wires rc.local, runs the provisioning, and then
# VERIFIES that LAN traffic actually egresses through the tunnel.
#
# WHY THE VERIFICATION MATTERS
#   The old task group reported success as soon as the provisioner finished. But
#   "provisioned" is not "working": a router can end up with a healthy tunnel,
#   a correct-looking config, every step green -- and LAN clients still going out
#   the WAN. The failure modes we actually hit:
#     - routing table 100 empty  -> traffic falls through to `main` SILENTLY,
#       there is no error anywhere, and a plain ping still succeeds
#     - two tunnels configured   -> the last one to come up owns table 100, so a
#       dead interface steals the LAN's egress from a working one
#     - tunnel up but no data    -> WireGuard handshakes fine while the peer is
#       misconfigured server-side and discards every data packet
#   None of those are visible from the provisioner's exit code. So this script
#   compares the public IP seen through the tunnel against the one seen via WAN.
#   Different = the LAN is really going through the VPN. That check cannot be
#   fooled by any of the above.
#
# USAGE
#   ./bootstrap.sh ovpn      # OpenVPN
#   ./bootstrap.sh wgvpn     # WireGuard
#   ./bootstrap.sh ovpn -n   # skip the egress verification (offline testing)
#
# EXIT CODES
#   0  provisioned AND verified: LAN egress confirmed through the tunnel
#   1  provisioning failed (see /var/log/qualitynoc-vpn.log)
#   2  provisioned but verification failed -- tunnel is NOT carrying LAN traffic
#   3  bad usage / prerequisites missing
# =============================================================================

set -u

BASE_URL="https://vpn.qualitynoc.net/RMS-C3rT"
STATE_DIR="/etc/qualitynoc"
PROV="${STATE_DIR}/qualitynoc-vpn-provision.sh"
LOG="/var/log/qualitynoc-vpn.log"
TAG=""
VERIFY=1
PROV_WAIT=160      # seconds to wait for the provisioner to report success
IFACE_WAIT=60      # seconds to wait for the tunnel interface to appear

say() { echo "[bootstrap] $*"; }

while [ $# -gt 0 ]; do
    case "$1" in
        ovpn|wgvpn) TAG="$1"; shift ;;
        -n) VERIFY=0; shift ;;
        *) say "ERROR: unknown argument '$1' (expected: ovpn | wgvpn [-n])"; exit 3 ;;
    esac
done
[ -z "$TAG" ] && { say "ERROR: missing VPN type (ovpn | wgvpn)"; exit 3; }

command -v uci   >/dev/null 2>&1 || { say "ERROR: uci not found"; exit 3; }
command -v curl  >/dev/null 2>&1 || { say "ERROR: curl not found"; exit 3; }

mkdir -p "$STATE_DIR"

# ---- 1. install/update the provisioner (atomic: never leave a partial file) --
say "Downloading provisioner from ${BASE_URL}"
if ! curl -fsS --retry 2 --retry-delay 3 --max-time 90 \
        -H 'Cache-Control: no-cache' \
        -o "${PROV}.new" "${BASE_URL}/qualitynoc-vpn-provision.sh"; then
    say "ERROR: could not download the provisioner"
    exit 1
fi
# Sanity-check before replacing a working copy with garbage (a captive portal or
# an error page would otherwise be installed as the provisioner).
if ! head -n1 "${PROV}.new" | grep -q '^#!/bin/sh'; then
    say "ERROR: downloaded file is not a shell script -- refusing to install"
    rm -f "${PROV}.new"
    exit 1
fi
mv "${PROV}.new" "$PROV"
chmod +x "$PROV"

# ---- 2. tag + rc.local -------------------------------------------------------
echo "$TAG" > "${STATE_DIR}/vpn_tag"
say "VPN tag set to '${TAG}'"

printf '#!/bin/sh\n[ -x %s ] && (%s >> %s 2>&1) &\nexit 0\n' "$PROV" "$PROV" "$LOG" > /etc/rc.local
chmod +x /etc/rc.local

# ---- 3. run it ---------------------------------------------------------------
# Always background + poll, even for OpenVPN which does not restart the network:
# the WireGuard path does, and that briefly drops RMS Remote Access. Run in the
# foreground and RMS loses the exit code and marks a successful run as failed.
rm -f "${STATE_DIR}/last_provision_ok"
say "Running provisioner in background"
nohup "$PROV" >> "$LOG" 2>&1 &
sleep 3

i=0
while [ "$i" -lt "$PROV_WAIT" ]; do
    [ -f "${STATE_DIR}/last_provision_ok" ] && break
    sleep 5
    i=$((i + 5))
done

if [ ! -f "${STATE_DIR}/last_provision_ok" ]; then
    say "FAIL: provisioner did not complete within ${PROV_WAIT}s"
    say "--- last 20 log lines ---"
    tail -n 20 "$LOG" 2>/dev/null
    exit 1
fi
say "Provisioned OK at $(cat "${STATE_DIR}/last_provision_ok")"

[ "$VERIFY" = "0" ] && { say "Verification skipped (-n)"; exit 0; }

# ---- 4. wait for the tunnel interface ---------------------------------------
TUN=""
i=0
while [ "$i" -lt "$IFACE_WAIT" ]; do
    if [ "$TAG" = "wgvpn" ]; then
        [ -d /sys/class/net/wg_qualitynoc ] && { TUN="wg_qualitynoc"; break; }
    else
        for d in /sys/class/net/tun*; do
            [ -d "$d" ] || continue
            TUN=$(basename "$d"); break
        done
        [ -n "$TUN" ] && break
    fi
    sleep 3
    i=$((i + 3))
done

if [ -z "$TUN" ]; then
    say "FAIL: tunnel interface never appeared after ${IFACE_WAIT}s"
    tail -n 20 "$LOG" 2>/dev/null
    exit 2
fi
say "Tunnel interface: ${TUN}"

# ---- 5. the check that cannot be fooled -------------------------------------
# Compare the public IP reached through the tunnel against the one via WAN.
# We source from the LAN address so the packet matches the same policy rule a
# real client would hit. A temporary `from <LAN_IP>` rule is added because the
# provisioner's rule is `iif br-lan`, and locally-generated traffic has no input
# interface -- without this, the test would produce a false negative.
LANIP=$(uci -q get network.lan.ipaddr 2>/dev/null || echo "")

VPNIP=$(curl -s --interface "$TUN" --max-time 20 https://api.ipify.org 2>/dev/null)
WANIP=$(curl -s --max-time 15 https://api.ipify.org 2>/dev/null)

RULE_ADDED=0
if [ -n "$LANIP" ]; then
    ip rule add from "$LANIP" table 100 priority 29999 2>/dev/null && RULE_ADDED=1
fi
LANOK=0
if [ -n "$LANIP" ] && ping -c2 -W3 -I "$LANIP" 1.1.1.1 >/dev/null 2>&1; then
    LANOK=1
fi
[ "$RULE_ADDED" = "1" ] && ip rule del from "$LANIP" table 100 priority 29999 2>/dev/null

T100=$(ip route show table 100 2>/dev/null | head -n1)

echo ""
echo "==================== VERIFICATION ===================="
echo "  tunnel iface : ${TUN}"
echo "  table 100    : ${T100:-<EMPTY>}"
echo "  public via VPN : ${VPNIP:-<none>}"
echo "  public via WAN : ${WANIP:-<none>}"
echo "  LAN-sourced ping : $([ "$LANOK" = "1" ] && echo OK || echo FAIL)"
echo "  default route  : $(ip route show default 2>/dev/null | head -n1)"
echo "======================================================"
echo ""

# An empty table 100 is the silent killer: traffic falls through to `main` and
# leaves via WAN with no error at all, so treat it as a hard failure.
if [ -z "$T100" ]; then
    say "FAIL: routing table 100 is EMPTY -- LAN traffic is falling through to WAN"
    exit 2
fi
if [ -z "$VPNIP" ]; then
    say "FAIL: no egress through ${TUN} -- tunnel is up but carries no traffic"
    exit 2
fi
if [ "$VPNIP" = "$WANIP" ]; then
    say "FAIL: same public IP via tunnel and WAN (${VPNIP}) -- traffic is NOT using the VPN"
    exit 2
fi
if [ "$LANOK" != "1" ]; then
    say "FAIL: a LAN-sourced packet cannot reach the internet through the tunnel"
    exit 2
fi

say "VERIFIED: LAN egresses via ${TUN} (vpn=${VPNIP}, wan=${WANIP})"
exit 0
