#!/bin/bash
#
# vpnmine-tunnel — the privileged half of the VPNmine Linux client.
#
# Invoked through pkexec; see com.spagreen.vpnmine.policy. The unprivileged app
# stages a wg-quick profile in its own runtime directory, then asks this script
# to sanitise it, install it where AppArmor lets wg-quick read it, and raise or
# drop the interface.
#
# Two deliberate constraints make this safe to expose to an unprivileged
# caller, and both must survive any edit:
#
#  1. It never accepts a path. The staging location is derived from PKEXEC_UID,
#     so a caller cannot aim it at a file it does not own.
#  2. It refuses PreUp/PostUp/PreDown/PostDown. wg-quick runs those as root, so
#     a profile the caller controls would otherwise be a root shell — exactly
#     the hole that made `NOPASSWD: wg-quick` unshippable.

set -euo pipefail

# pkexec provides a sanitised environment; be explicit anyway, since wg-quick
# lives in /usr/bin on Debian/Ubuntu and /usr/sbin on some other distributions.
export PATH=/usr/sbin:/usr/bin:/sbin:/bin

readonly INTERFACE="vpnmine"
readonly CONFIG="/etc/wireguard/${INTERFACE}.conf"

TMPFILE=""
cleanup() { [[ -n $TMPFILE && -e $TMPFILE ]] && rm -f "$TMPFILE"; return 0; }
trap cleanup EXIT

die() { printf 'vpnmine-tunnel: %s\n' "$*" >&2; exit 1; }

[[ ${PKEXEC_UID-} =~ ^[0-9]+$ ]] || die "must be invoked through pkexec"
readonly STAGED="/run/user/${PKEXEC_UID}/vpnmine-staged.conf"

# Every key that describes a tunnel, and nothing that runs a command. An
# unknown key is an error rather than a silent drop, so a profile that genuinely
# needs something new fails loudly here instead of coming up half-configured.
is_allowed_key() {
  case "$1:$2" in
    Interface:PrivateKey|Interface:Address|Interface:DNS|Interface:MTU) return 0 ;;
    Interface:ListenPort|Interface:FwMark|Interface:Table) return 0 ;;
    Peer:PublicKey|Peer:PresharedKey|Peer:AllowedIPs) return 0 ;;
    Peer:Endpoint|Peer:PersistentKeepalive) return 0 ;;
    *) return 1 ;;
  esac
}

sanitize() {
  local section="" line key value residue
  while IFS= read -r line || [[ -n $line ]]; do
    line="${line%%#*}"
    line="${line#"${line%%[![:space:]]*}"}"
    line="${line%"${line##*[![:space:]]}"}"
    [[ -z $line ]] && continue

    if [[ $line =~ ^\[(Interface|Peer)\]$ ]]; then
      section="${BASH_REMATCH[1]}"
      printf '[%s]\n' "$section"
      continue
    fi

    [[ -n $section ]] || die "profile does not open with a section header"
    [[ $line =~ ^([A-Za-z]+)[[:space:]]*=[[:space:]]*(.*)$ ]] \
      || die "malformed line in profile"

    key="${BASH_REMATCH[1]}"
    value="${BASH_REMATCH[2]}"

    is_allowed_key "$section" "$key" \
      || die "refusing profile: '$key' is not permitted in [$section]"

    # Whatever is left after deleting the permitted characters must be empty.
    # tr treats '[', ']' and a trailing '-' literally, which avoids the bracket
    # quoting traps of a regex here.
    residue="$(printf '%s' "$value" | LC_ALL=C tr -d 'A-Za-z0-9.:/,=+_ []-')"
    [[ -z $residue ]] || die "refusing profile: unsafe characters in '$key'"

    printf '%s = %s\n' "$key" "$value"
  done
}

cmd_up() {
  [[ -e $STAGED ]] || die "no staged profile at $STAGED"
  [[ -L $STAGED ]] && die "staged profile is a symlink"
  [[ -f $STAGED ]] || die "staged profile is not a regular file"
  [[ "$(stat -c '%u' "$STAGED")" == "$PKEXEC_UID" ]] \
    || die "staged profile is not owned by the calling user"

  install -d -m 700 /etc/wireguard

  # Sanitise into a temp file inside the (root-only) target directory, then
  # rename, so a rejected profile can never half-overwrite a good one.
  TMPFILE="$(mktemp "/etc/wireguard/.${INTERFACE}.XXXXXX")"
  sanitize < "$STAGED" > "$TMPFILE"
  chmod 600 "$TMPFILE"
  mv -f "$TMPFILE" "$CONFIG"
  TMPFILE=""

  # A tunnel left behind by a killed session would make `up` fail outright.
  if ip link show dev "$INTERFACE" >/dev/null 2>&1; then
    wg-quick down "$INTERFACE" >/dev/null 2>&1 || true
  fi
  wg-quick up "$INTERFACE"
}

cmd_down() {
  if ip link show dev "$INTERFACE" >/dev/null 2>&1; then
    wg-quick down "$INTERFACE"
  fi
  # The profile holds the tunnel's private key; it has no reason to outlive
  # the connection.
  rm -f "$CONFIG"
}

case "${1-}" in
  up)   cmd_up ;;
  down) cmd_down ;;
  *)    die "usage: vpnmine-tunnel up|down" ;;
esac
