#!/bin/sh
# incy-helper-linux — privileged helper for INCY VPN client on Linux.
# Installed by the .deb / .rpm as /usr/lib/incy/incy-helper-linux.sh,
# owned root:root mode 0755 (NOT setuid — it's always invoked via
# pkexec, which checks the polkit policy at /usr/share/polkit-1/actions/
# cc.incy.vpn.policy and prompts the user once).
#
# Whitelist of subcommands kept narrow on purpose: each shell-out below
# is what the GUI would otherwise call directly, but routed through one
# policy action so polkit's `auth_admin_keep` flag can cache the prompt
# across start → stop in the same session (~5 min window).
#
# CALLERS:
#   start-xray-tun  <xray_bin> <config> <pid_file> <log_file> [asset_dir] [tun_name]
#   stop-xray-tun   <pid_file>
#   setup-routes    <tun_iface> <fwmark> <table>
#   cleanup-routes  <tun_iface> <fwmark> <table>

set -eu

CMD="${1:-}"
shift 2>/dev/null || true

case "$CMD" in

  start-xray-tun)
    # xray.exe equivalent on Linux: opens /dev/net/tun, surfaces
    # `tun_incy`. setsid so the child gets its own session and
    # survives a Tauri force-quit; nohup-ish redirection so the
    # parent shell (this script) can exit without SIGHUP'ing xray.
    XRAY_BIN="${1:-}"; XRAY_CFG="${2:-}"
    PID_FILE="${3:-}"; LOG_FILE="${4:-/tmp/incy-xray.log}"
    ASSET_DIR="${5:-}"
    # 6th arg is the expected TUN device name. xray writes it into
    # /sys/class/net/<name> when it opens /dev/net/tun; we poll for
    # the directory to appear. Defaults to `tun_incy` for callers
    # built against the pre-v3.1.x naming, but current callers pass
    # `wwan99` (modem-looking alias to dodge naive VPN scanners).
    TUN_NAME="${6:-tun_incy}"
    [ -n "$XRAY_BIN" ] || { echo "ERROR: xray binary path required" >&2; exit 1; }
    [ -f "$XRAY_BIN" ] || { echo "ERROR: xray binary not found at $XRAY_BIN" >&2; exit 1; }
    [ -f "$XRAY_CFG" ] || { echo "ERROR: xray config not found at $XRAY_CFG" >&2; exit 1; }
    [ -n "$PID_FILE" ] || { echo "ERROR: pid file path required" >&2; exit 1; }

    # Reap any leftover xray from a previous crash — same idea as
    # the macOS helper, but cleaner: just match the absolute path.
    if [ -f "$PID_FILE" ]; then
      OLD_PID=$(cat "$PID_FILE" 2>/dev/null || echo "")
      if [ -n "$OLD_PID" ] && kill -0 "$OLD_PID" 2>/dev/null; then
        kill -TERM "$OLD_PID" 2>/dev/null || true
        i=0
        while [ $i -lt 10 ]; do
          kill -0 "$OLD_PID" 2>/dev/null || break
          sleep 0.1
          i=$((i+1))
        done
        kill -9 "$OLD_PID" 2>/dev/null || true
      fi
    fi
    pkill -9 -f "${XRAY_BIN} run" 2>/dev/null || true
    sleep 0.3

    : > "$LOG_FILE"
    chmod 0644 "$LOG_FILE" 2>/dev/null || true

    # Export asset dir if provided, then spawn xray detached. Capture
    # PID after `setsid` so the recorded pid is the xray process,
    # not the helper script.
    if [ -n "$ASSET_DIR" ]; then
      XRAY_LOCATION_ASSET="$ASSET_DIR" \
        setsid "$XRAY_BIN" run -config "$XRAY_CFG" </dev/null >>"$LOG_FILE" 2>&1 &
    else
      setsid "$XRAY_BIN" run -config "$XRAY_CFG" </dev/null >>"$LOG_FILE" 2>&1 &
    fi
    XRAY_PID=$!
    echo "$XRAY_PID" > "$PID_FILE"
    chmod 0644 "$PID_FILE" 2>/dev/null || true
    sleep 0.5

    if ! kill -0 "$XRAY_PID" 2>/dev/null; then
      echo "ERROR: xray exited immediately. log:" >&2
      tail -50 "$LOG_FILE" >&2 2>/dev/null || true
      rm -f "$PID_FILE"
      exit 1
    fi

    # Wait up to 30 s for /sys/class/net/$TUN_NAME. xray creates the
    # device synchronously after opening /dev/net/tun, so this
    # usually finishes in <1 s; the long timeout is for cold-start
    # corner cases (kmod load, geoip.dat slurp).
    NEW_TUN="$TUN_NAME"
    i=0
    while [ $i -lt 150 ]; do
      if [ -d "/sys/class/net/$NEW_TUN" ]; then
        break
      fi
      if ! kill -0 "$XRAY_PID" 2>/dev/null; then
        echo "ERROR: xray died waiting for $NEW_TUN. log:" >&2
        tail -50 "$LOG_FILE" >&2 2>/dev/null || true
        rm -f "$PID_FILE"
        exit 1
      fi
      sleep 0.2
      i=$((i+1))
    done

    if [ ! -d "/sys/class/net/$NEW_TUN" ]; then
      echo "ERROR: $NEW_TUN did not appear within 30s. xray log:" >&2
      tail -50 "$LOG_FILE" >&2 2>/dev/null || true
      kill -9 "$XRAY_PID" 2>/dev/null || true
      rm -f "$PID_FILE"
      exit 1
    fi

    echo "$NEW_TUN"
    echo "$XRAY_PID"
    ;;

  stop-xray-tun)
    PID_FILE="${1:-}"
    [ -n "$PID_FILE" ] || { echo "ERROR: pid file path required" >&2; exit 1; }
    if [ -f "$PID_FILE" ]; then
      XRAY_PID=$(cat "$PID_FILE" 2>/dev/null || echo "")
      if [ -n "$XRAY_PID" ] && kill -0 "$XRAY_PID" 2>/dev/null; then
        kill -TERM "$XRAY_PID" 2>/dev/null || true
        i=0
        while [ $i -lt 10 ]; do
          kill -0 "$XRAY_PID" 2>/dev/null || break
          sleep 0.1
          i=$((i+1))
        done
        kill -9 "$XRAY_PID" 2>/dev/null || true
      fi
      rm -f "$PID_FILE"
    fi
    # Belt-and-suspenders: any remaining xray with our typical
    # invocation pattern. Skipped if pid file gave us a clean kill.
    pkill -9 -f "xray run -config" 2>/dev/null || true
    ;;

  setup-routes)
    # `ip route` + `ip rule` install for our split-table layout.
    # Same TABLE/FWMARK constants the Rust side uses; passing them
    # in keeps the helper agnostic of caller changes.
    TUN="${1:-}"; FWMARK="${2:-}"; TABLE="${3:-}"
    [ -n "$TUN" ] && [ -n "$FWMARK" ] && [ -n "$TABLE" ] || \
      { echo "ERROR: setup-routes <tun> <fwmark> <table>" >&2; exit 1; }
    # Drop any leftover entries from a prior session before adding
    # ours — avoids "RTNETLINK answers: File exists" on reconnect.
    ip rule del not fwmark "$FWMARK" table "$TABLE" 2>/dev/null || true
    ip route del default dev "$TUN" table "$TABLE" 2>/dev/null || true
    ip -6 rule del not fwmark "$FWMARK" table "$TABLE" 2>/dev/null || true
    ip -6 route del default dev "$TUN" table "$TABLE" 2>/dev/null || true
    ip route replace default dev "$TUN" table "$TABLE"
    ip rule add not fwmark "$FWMARK" table "$TABLE"
    ip -6 route replace default dev "$TUN" table "$TABLE" 2>/dev/null || true
    ip -6 rule add not fwmark "$FWMARK" table "$TABLE" 2>/dev/null || true
    ;;

  cleanup-routes)
    TUN="${1:-}"; FWMARK="${2:-}"; TABLE="${3:-}"
    [ -n "$TUN" ] && [ -n "$FWMARK" ] && [ -n "$TABLE" ] || \
      { echo "ERROR: cleanup-routes <tun> <fwmark> <table>" >&2; exit 1; }
    ip rule del not fwmark "$FWMARK" table "$TABLE" 2>/dev/null || true
    ip route del default dev "$TUN" table "$TABLE" 2>/dev/null || true
    ip -6 rule del not fwmark "$FWMARK" table "$TABLE" 2>/dev/null || true
    ip -6 route del default dev "$TUN" table "$TABLE" 2>/dev/null || true
    ;;

  dns-set)
    # Args: <tun_device> <dns_ip>
    # Point the OS at the tunnel resolver while connected.
    #
    # Routing alone can't fix system DNS: the resolver handed out by DHCP is
    # on-link, so its directly-connected route always beats our default route
    # through the tun device, and every lookup leaks to the ISP.
    #
    # Two backends, picked by what the system actually runs:
    #   * systemd-resolved  -> `resolvectl dns <tun> <ip>` + `resolvectl domain
    #     <tun> ~.` The `~.` routing domain makes this link the default route
    #     for ALL names, which is what stops queries going to the per-link ISP
    #     resolver. Nothing to back up: state dies with the link.
    #   * plain /etc/resolv.conf -> rewrite it, keeping a backup alongside.
    TUN="${1:-}"; DNS_IP="${2:-}"
    [ -n "$TUN" ] && [ -n "$DNS_IP" ] || { echo "ERROR: dns-set <tun> <dns_ip>" >&2; exit 1; }
    case "$DNS_IP" in
      *[!0-9a-fA-F:.]*) echo "ERROR: invalid dns ip" >&2; exit 1 ;;
    esac

    if command -v resolvectl >/dev/null 2>&1 && \
       systemctl is-active --quiet systemd-resolved 2>/dev/null; then
      resolvectl dns "$TUN" "$DNS_IP" 2>/dev/null || true
      resolvectl domain "$TUN" '~.' 2>/dev/null || true
      resolvectl flush-caches 2>/dev/null || true
      echo "resolved"
    else
      # Only take a backup on the FIRST capture — a reconnect while already
      # tunnelled must not overwrite it with our own resolver, which would
      # make dns-restore a no-op and strand the user on a dead nameserver.
      if [ ! -f /etc/resolv.conf.incy-backup ]; then
        cp -a /etc/resolv.conf /etc/resolv.conf.incy-backup 2>/dev/null || true
      fi
      printf 'nameserver %s\n' "$DNS_IP" > /etc/resolv.conf
      echo "resolvconf"
    fi
    ;;

  dns-restore)
    # Args: [tun_device]
    # Idempotent: safe to call on every disconnect and from crash cleanup.
    TUN="${1:-}"
    if command -v resolvectl >/dev/null 2>&1 && \
       systemctl is-active --quiet systemd-resolved 2>/dev/null; then
      # `revert` drops every per-link override we set (dns + routing domain).
      # The link is usually gone already, in which case this is a harmless
      # no-op and the settings died with it.
      [ -n "$TUN" ] && resolvectl revert "$TUN" 2>/dev/null || true
      resolvectl flush-caches 2>/dev/null || true
    fi
    if [ -f /etc/resolv.conf.incy-backup ]; then
      cp -a /etc/resolv.conf.incy-backup /etc/resolv.conf 2>/dev/null || true
      rm -f /etc/resolv.conf.incy-backup
    fi
    echo "OK"
    ;;

  *)
    echo "Usage: incy-helper-linux {start-xray-tun|stop-xray-tun|setup-routes|cleanup-routes|dns-set|dns-restore}" >&2
    exit 2
    ;;
esac
