Catalog

protonvpncli.sh

protonvpn-cli

Run after GNOME login when NetworkManager already owns wifi or ethernet — this script does not start NetworkManager. Linear one-liner: checksum Proton’s protonvpn-stable-release_1.0.8_all.deb from repo.protonvpn.com, install proton-vpn-cli from that extra repo (not Debian main), write a systemd user oneshot for connect-at-login (enable only; not started). It never execs /usr/bin/protonvpn. Nested sudo -u sign-in froze vendor getpass; after this installer, run protonvpn signin as the desktop user without sudo. Refuses if the Proton GNOME GUI or community protonvpn-cli is already installed, or if an apt lock is held. Uninstall is a copy-paste fence in the package SECURITY.md, not a flag.

v0.10.0requires rootidempotentDebian 12 / 13Updated 8 Sept 2026networksecurity

SHA-256 computing…

Run command

This pipes remote code to root. Pick a mode, read the impact lists, then copy. Extra flags go to bash, not curl.

curl -fsSL https://<host>/protonvpncli.sh | sudo bash

Pipes remote code to root. The lists below match this mode. Fetch only: curl -fsSL https://<host>/protonvpncli.sh

Open raw filehttps://<host>/protonvpncli.sh

Impact of this mode

Install (default)

These lists follow the install mode selected above. Switching the run command updates what this copy will do on a Debian host.

What it does

  • Refuse to continue if proton-vpn-gnome-desktop or community protonvpn-cli is already installed (prints a purge line)
  • Refuse if a dpkg/apt lock is held (do not wait forever)
  • Checksum Proton’s extra-repo .deb (protonvpn-stable-release_1.0.8_all.deb) then dpkg -i; skip when both extra-repo and proton-vpn-cli are already installed
  • apt-get install proton-vpn-cli from repo.protonvpn.com
  • Write a systemd user oneshot + wrapper, migrate a leftover debian-linux unit of the same name, systemctl --user enable (time-boxed; does not start)
  • Print desktop-user next steps: protonvpn signin, four config set knobs, protonvpn connect (no sudo)

What it does not do

  • Install or launch the Proton GNOME GUI (proton-vpn-gnome-desktop); CLI and GUI cannot run at the same time
  • Accept Ubuntu or any ID other than debian
  • Start or restart NetworkManager
  • Exec /usr/bin/protonvpn (no sign-in, config set, info, or connect from this script)
  • Pass the Proton password on argv or read -s it in this script
  • Accept path flags (--uninstall, --mode=, --no-install, --no-configure, --no-autostart, --no-receipt)
  • Uninstall as a script action (copy-paste fence lives in the package SECURITY.md)
  • pip / PyPI protonvpn-cli, Snap, or Flatpak
  • Fold this into setup.sh
  • loginctl enable-linger or a root/system protonvpn-connect.service
  • Start the user oneshot (enable only; connect is next GNOME login or a desktop-user protonvpn connect)

Prerequisites

  • Debian (ID=debian in /etc/os-release)
  • After GNOME login, with NetworkManager already owning wifi or ethernet
  • Root via curl | sudo bash — one-liner only (--help / --dry-run are developer-only)
  • Desktop user via SUDO_USER (root login is refused)
  • No proton-vpn-gnome-desktop or community protonvpn-cli package already installed

Side effects

  • Adds Proton’s extra apt repo — not Debian main
  • User unit protonvpn-connect.service is enabled for the next GNOME login (no linger, not started now)
  • Migrates a leftover debian-linux unit of the same name if present
  • Operator must run protonvpn signin without sudo afterward; vendor getpass/2FA and gnome-keyring belong to that session

Package files

protonvpncli.sh

499 lines

1#!/usr/bin/env bash2# protonvpn-cli 0.10.0 — extra-repo proton-vpn-cli + session autostart files.3# Does not run the vendor protonvpn CLI (signin/getpass/keyring must be the desktop user).4# Class: pipeable5# Brace group: bash slurps the whole file from a pipe before running (curl | sudo bash).6{7set -euo pipefail8export DEBIAN_FRONTEND=noninteractive9 10VERSION="0.10.0"11RELEASE_DEB_URL="https://repo.protonvpn.com/debian/dists/stable/main/binary-all/protonvpn-stable-release_1.0.8_all.deb"12RELEASE_DEB_SHA256="0b14e71586b22e498eb20926c48c7b434b751149b1f2af9902ef1cfe6b03e180"13RELEASE_DEB_NAME="protonvpn-stable-release_1.0.8_all.deb"14REPO_PKG="protonvpn-stable-release"15CLI_PKG="proton-vpn-cli"16GUI_PKG="proton-vpn-gnome-desktop"17OLD_CLI_PKG="protonvpn-cli"18AUTOSTART_UNIT_NAME="protonvpn-connect.service"19SYSTEMCTL_SEC=820 21DRY_RUN=022AUTOSTART_ENABLED=023DEB_TMPDIR=""24 25if [[ -n "${BASH_SOURCE[0]:-}" && -f "${BASH_SOURCE[0]}" ]]; then26  SCRIPT_PATH=$(readlink -f "${BASH_SOURCE[0]}")27else28  SCRIPT_PATH=""29fi30 31err() { printf '%s\n' "$*" >&2; }32log() { printf '%s\n' "$*"; }33 34die_usage() {35  err "error: $*"36  err "Try --help for invoke."37  exit 138}39 40die_run() {41  err "error: $*"42  exit 243}44 45cleanup_deb_tmp() {46  if [[ -n "${DEB_TMPDIR}" && -d "${DEB_TMPDIR}" ]]; then47    rm -rf "${DEB_TMPDIR}"48  fi49}50trap cleanup_deb_tmp EXIT51 52usage() {53  cat <<EOF54protonvpn-cli ${VERSION} — Proton extra repo + proton-vpn-cli + session autostart files.55 56Operator invoke:57  curl -fsSL https://debian.reporet.com/protonvpncli.sh | sudo bash58  sudo bash scripts/protonvpn/protonvpncli.sh59 60This script does not run protonvpn (no sign-in, no config set, no connect).61After it exits, run protonvpn signin as your desktop user (no sudo).62 63Developer-only:64  bash scripts/protonvpn/protonvpncli.sh --help65  bash scripts/protonvpn/protonvpncli.sh --dry-run66 67Do not use curl | bash without sudo (cannot re-exec a pipe).68Do not write curl …/protonvpncli.sh --dry-run | sudo bash (curl consumes the flag; use bash -s --).69 70Options:71  --dry-run   Print commands; no dpkg, apt, writes, or systemctl72  --help      Print this text and exit 073 74Exit codes: 0 success; 1 usage/preflight; 2 runtime.75EOF76}77 78parse_args() {79  while [[ $# -gt 0 ]]; do80    case "$1" in81      --help)82        usage83        exit 084        ;;85      --dry-run) DRY_RUN=1 ;;86      --)87        shift88        break89        ;;90      -*)91        die_usage "unknown option: $1"92        ;;93      *)94        die_usage "unexpected argument: $1"95        ;;96    esac97    shift98  done99  if [[ $# -gt 0 ]]; then100    die_usage "unexpected argument: $1"101  fi102}103 104login_user() {105  if [[ -n "${SUDO_USER:-}" && "${SUDO_USER}" != root ]]; then106    printf '%s\n' "$SUDO_USER"107    return 0108  fi109  local u110  u=$(logname 2>/dev/null || true)111  if [[ -n "$u" && "$u" != root ]]; then112    printf '%s\n' "$u"113    return 0114  fi115  if [[ -n "${USER:-}" && "${USER}" != root ]]; then116    printf '%s\n' "$USER"117    return 0118  fi119  printf 'root\n'120}121 122login_home() {123  local u home124  u=$(login_user)125  home=$(getent passwd "$u" | awk -F: '{print $6}')126  if [[ -n "$home" ]]; then127    printf '%s\n' "$home"128    return 0129  fi130  printf '/home/%s\n' "$u"131}132 133# GNU env treats -- after NAME=VALUE as the utility. sudo's -- stays.134# Never used to invoke protonvpn (vendor CLI is the desktop user, no sudo).135run_as_login() {136  local user uid runtime137  user=$(login_user)138  if [[ "$user" == root ]] || [[ "$(id -un)" == "$user" ]]; then139    "$@"140    return141  fi142  uid=$(id -u "$user")143  runtime="/run/user/${uid}"144  if command -v env >/dev/null 2>&1; then145    sudo -u "$user" -H -- env \146      "XDG_RUNTIME_DIR=${runtime}" \147      "DBUS_SESSION_BUS_ADDRESS=unix:path=${runtime}/bus" \148      "$@"149  else150    sudo -u "$user" -H \151      "XDG_RUNTIME_DIR=${runtime}" \152      "DBUS_SESSION_BUS_ADDRESS=unix:path=${runtime}/bus" \153      "$@"154  fi155}156 157timeout_bin() {158  if [[ -x /usr/bin/timeout ]]; then159    printf '%s\n' /usr/bin/timeout160  elif command -v timeout >/dev/null 2>&1; then161    command -v timeout162  fi163}164 165# Never pass a bare double-dash to GNU timeout (coreutils 9.7 treats it as the command).166run_as_login_timeout() {167  local sec=$1168  local tb169  shift170  tb=$(timeout_bin)171  if [[ -n "$tb" ]]; then172    run_as_login "$tb" -k 1 "$sec" "$@" </dev/null173  else174    run_as_login "$@" </dev/null175  fi176}177 178pkg_installed() {179  local st180  st=$(dpkg-query -W -f='${Status}' "$1" 2>/dev/null) || return 1181  [[ "$st" == "install ok installed" ]]182}183 184have_network() {185  if command -v ip >/dev/null 2>&1 && ip route show default 2>/dev/null | grep -q .; then186    return 0187  fi188  local d iface carrier189  for d in /sys/class/net/*; do190    [[ -e "$d" ]] || continue191    iface=$(basename "$d")192    [[ "$iface" != lo ]] || continue193    carrier=$(cat "$d/carrier" 2>/dev/null || printf '0')194    if [[ "$carrier" == 1 ]]; then195      return 0196    fi197  done198  return 1199}200 201# Fail immediately when another apt/dpkg holds the lock (retries otherwise wait forever).202apt_locks_free() {203  local f204  command -v flock >/dev/null 2>&1 || return 0205  for f in /var/lib/dpkg/lock-frontend /var/lib/apt/lists/lock; do206    [[ -e "$f" ]] || continue207    if ! flock --nonblock "$f" -c true 2>/dev/null; then208      return 1209    fi210  done211  return 0212}213 214wrapper_path() {215  printf '%s/.local/lib/debian-os-setup/protonvpn/protonvpn-connect-startup.sh\n' "$(login_home)"216}217 218linux_wrapper_path() {219  printf '%s/.local/lib/debian-linux/protonvpn/protonvpn-connect-startup.sh\n' "$(login_home)"220}221 222unit_path() {223  printf '%s/.config/systemd/user/%s\n' "$(login_home)" "$AUTOSTART_UNIT_NAME"224}225 226preflight() {227  if [[ ! -f /etc/os-release ]]; then228    die_usage "cannot read /etc/os-release; Debian is required"229  fi230  # ID=debian only; Ubuntu and derivatives set a different ID.231  # shellcheck source=/dev/null232  . /etc/os-release233  if [[ "${ID:-}" != debian ]]; then234    die_usage "this script requires Debian (ID=debian); refusing ID=${ID:-unknown} (Ubuntu is not supported)"235  fi236  if [[ "$(login_user)" == root ]]; then237    die_usage "run via sudo from the desktop user so SUDO_USER is set (do not run as root login)"238  fi239  if pkg_installed "$GUI_PKG"; then240    die_usage "${GUI_PKG} is installed. The official CLI and GUI must not run together. Purge it: sudo apt-get purge ${GUI_PKG}"241  fi242  if pkg_installed "$OLD_CLI_PKG"; then243    die_usage "community package ${OLD_CLI_PKG} is installed. Purge it: sudo apt-get purge ${OLD_CLI_PKG}"244  fi245  if ! have_network; then246    if ((DRY_RUN)); then247      err "warn: no default route or carrier; dry-run continues"248    else249      die_usage "no network (no default route). Run after GNOME Settings owns wifi or ethernet"250    fi251  fi252  if ((EUID == 0)) && ! apt_locks_free; then253    if ((DRY_RUN)); then254      err "warn: dpkg/apt lock is held; a live run would exit 2"255    else256      die_run "dpkg/apt is locked (another apt/dpkg is running, or a prior attempt left a lock). Wait for it to finish, then re-run"257    fi258  fi259  if command -v systemctl >/dev/null 2>&1; then260    local en261    en=$(systemctl is-enabled NetworkManager 2>/dev/null || true)262    if [[ "$en" != enabled ]]; then263      err "warn: NetworkManager is not enabled. Sign-in and connect need NM after GNOME login. This script does not start NM."264    fi265  fi266}267 268maybe_sudo_reexec() {269  ((EUID == 0)) && return 0270  command -v sudo >/dev/null 2>&1 || die_usage "root or sudo is required"271  if [[ -n "$SCRIPT_PATH" && -f "$SCRIPT_PATH" ]]; then272    exec sudo bash "$SCRIPT_PATH" "$@"273  fi274  die_usage "piped invoke requires root. Use: curl -fsSL https://debian.reporet.com/protonvpncli.sh | sudo bash"275}276 277install_cli() {278  log "Fence: install (${REPO_PKG} + ${CLI_PKG})"279  if pkg_installed "$REPO_PKG" && pkg_installed "$CLI_PKG"; then280    log "Already installed: ${REPO_PKG} ${CLI_PKG}"281    return 0282  fi283  if ((DRY_RUN)); then284    if ! pkg_installed "$REPO_PKG"; then285      printf 'dry-run: curl -fsSL %s -o %s\n' "$RELEASE_DEB_URL" "$RELEASE_DEB_NAME"286      printf 'dry-run: sha256sum --check  # expect %s\n' "$RELEASE_DEB_SHA256"287      printf 'dry-run: dpkg -i %s\n' "$RELEASE_DEB_NAME"288    fi289    printf 'dry-run: apt-get update\n'290    printf 'dry-run: apt-get install -y %s\n' "$CLI_PKG"291    return 0292  fi293  command -v curl >/dev/null 2>&1 || die_usage "curl is required"294  command -v sha256sum >/dev/null 2>&1 || die_usage "sha256sum is required"295  if ! pkg_installed "$REPO_PKG"; then296    DEB_TMPDIR=$(mktemp -d)297    local deb_path="${DEB_TMPDIR}/${RELEASE_DEB_NAME}"298    curl -fsSL "$RELEASE_DEB_URL" -o "$deb_path" || die_run "failed to download ${RELEASE_DEB_NAME}"299    if ! printf '%s  %s\n' "$RELEASE_DEB_SHA256" "$deb_path" | sha256sum --check -; then300      die_run "SHA-256 check failed for ${RELEASE_DEB_NAME}"301    fi302    dpkg -i "$deb_path" </dev/null || die_run "dpkg -i ${RELEASE_DEB_NAME} failed"303  fi304  apt-get update </dev/null || die_run "apt-get update failed"305  apt-get install -y "$CLI_PKG" </dev/null || die_run "apt-get install ${CLI_PKG} failed"306  pkg_installed "$CLI_PKG" || die_run "${CLI_PKG} missing after install"307}308 309write_autostart_wrapper() {310  cat <<'EOF'311#!/usr/bin/env bash312# Connect ProtonVPN at session start (debian-os-setup / scripts/protonvpn).313# Requires: official protonvpn CLI and a prior interactive login/config as this user.314 315set -euo pipefail316 317readonly SCRIPT_NAME="protonvpn-connect-startup.sh"318readonly PROTONVPN="/usr/bin/protonvpn"319readonly EXIT_OK=0320readonly EXIT_FAIL=1321readonly EXIT_NOT_FOUND=127322readonly STATUS_SEC=15323readonly CONNECT_SEC=45324 325log() {326  printf '%s %s: %s\n' "$(date -Iseconds 2>/dev/null || date)" "${SCRIPT_NAME}" "$*" >&2327}328 329die() {330  local code="$1"331  shift332  log "ERROR: $*"333  exit "${code}"334}335 336run_timeout() {337  local sec=$1338  shift339  if [[ -x /usr/bin/timeout ]]; then340    /usr/bin/timeout -k 1 "$sec" "$@"341  else342    "$@"343  fi344}345 346already_connected() {347  local status_out348  if ! status_out="$(run_timeout "${STATUS_SEC}" "${PROTONVPN}" status 2>/dev/null)"; then349    return 1350  fi351  if printf '%s\n' "${status_out}" | grep -Eiq \352    'status:[[:space:]]*connected|connection[[:space:]]+status:[[:space:]]*connected|[[:space:]]connected[[:space:]]*$'; then353    return 0354  fi355  return 1356}357 358main() {359  local rc360  if [[ ! -x "${PROTONVPN}" ]]; then361    die "${EXIT_NOT_FOUND}" "'${PROTONVPN}' not found. Install proton-vpn-cli."362  fi363  if already_connected; then364    log "Already connected; skipping protonvpn connect."365    exit "${EXIT_OK}"366  fi367  log "Running: protonvpn connect"368  if run_timeout "${CONNECT_SEC}" "${PROTONVPN}" connect; then369    log "protonvpn connect completed successfully."370    exit "${EXIT_OK}"371  else372    rc=$?373    die "${EXIT_FAIL}" "protonvpn connect failed (exit ${rc})."374  fi375}376 377main "$@"378EOF379}380 381write_autostart_unit() {382  cat <<'EOF'383[Unit]384Description=Connect ProtonVPN at session start (debian-os-setup)385After=network-online.target386Wants=network-online.target387 388[Service]389Type=oneshot390TimeoutStartSec=60391ExecStart=%h/.local/lib/debian-os-setup/protonvpn/protonvpn-connect-startup.sh392RemainAfterExit=yes393 394[Install]395WantedBy=default.target396EOF397}398 399install_autostart() {400  local wrapper unit linux_wrapper wrapper_dir unit_dir401  wrapper=$(wrapper_path)402  unit=$(unit_path)403  linux_wrapper=$(linux_wrapper_path)404  wrapper_dir=$(dirname "$wrapper")405  unit_dir=$(dirname "$unit")406  AUTOSTART_ENABLED=0407 408  log "Fence: autostart (user unit ${AUTOSTART_UNIT_NAME})"409  if ((DRY_RUN)); then410    printf 'dry-run: write %s (0755)\n' "$wrapper"411    printf 'dry-run: write %s (0644, TimeoutStartSec=60)\n' "$unit"412    if [[ -e "$linux_wrapper" ]] || grep -Fq debian-linux "$unit" 2>/dev/null; then413      printf 'dry-run: systemctl --user disable --now %s  # migrate debian-linux unit\n' "$AUTOSTART_UNIT_NAME"414      printf 'dry-run: rm -f %s\n' "$linux_wrapper"415    fi416    printf 'dry-run: systemctl --user daemon-reload\n'417    printf 'dry-run: systemctl --user enable %s\n' "$AUTOSTART_UNIT_NAME"418    printf 'dry-run: not starting the unit (protonvpn connect runs at next GNOME login)\n'419    return 0420  fi421 422  if [[ -e "$unit" ]] && grep -Fq debian-linux "$unit"; then423    log "Migrating debian-linux ${AUTOSTART_UNIT_NAME} to debian-os-setup"424    run_as_login_timeout "$SYSTEMCTL_SEC" systemctl --user disable --now "$AUTOSTART_UNIT_NAME" || \425      err "warn: systemctl --user disable --now ${AUTOSTART_UNIT_NAME} failed or timed out (continuing)"426  fi427 428  run_as_login install -d -m 0755 "$wrapper_dir" "$unit_dir" || \429    die_run "install -d autostart directories failed"430  write_autostart_wrapper | run_as_login tee "$wrapper" >/dev/null || \431    die_run "write wrapper failed"432  run_as_login chmod 0755 "$wrapper" || die_run "chmod 0755 wrapper failed"433  write_autostart_unit | run_as_login tee "$unit" >/dev/null || \434    die_run "write unit failed"435  run_as_login chmod 0644 "$unit" || die_run "chmod 0644 unit failed"436 437  if [[ -e "$linux_wrapper" ]]; then438    run_as_login rm -f "$linux_wrapper" || err "warn: could not remove debian-linux wrapper"439    run_as_login rmdir "$(dirname "$linux_wrapper")" 2>/dev/null || true440  fi441 442  if ! run_as_login_timeout "$SYSTEMCTL_SEC" systemctl --user daemon-reload; then443    err "warn: systemctl --user daemon-reload failed or timed out. Enable the unit after GNOME login (printed below)."444    return 0445  fi446  if run_as_login_timeout "$SYSTEMCTL_SEC" systemctl --user enable "$AUTOSTART_UNIT_NAME"; then447    AUTOSTART_ENABLED=1448  else449    err "warn: systemctl --user enable ${AUTOSTART_UNIT_NAME} failed or timed out. Enable it after GNOME login (printed below)."450  fi451}452 453print_next_steps() {454  log ""455  log "Done. This script did not run protonvpn (signin uses getpass + gnome-keyring in your session)."456  log "Next, as your desktop user, no sudo, in this GNOME terminal:"457  log ""458  log "  protonvpn signin YOUR_USERNAME_OR_EMAIL"459  log "  protonvpn config set netshield malware-ads-trackers"460  log "  protonvpn disconnect"461  log "  protonvpn config set kill-switch standard"462  log "  protonvpn config set ipv6 off"463  log "  protonvpn config set anonymous-crash-reports off"464  log "  protonvpn connect"465  log ""466  if ((DRY_RUN)); then467    log "Autostart (dry-run): would enable ${AUTOSTART_UNIT_NAME} for the next GNOME login."468  elif ((AUTOSTART_ENABLED)); then469    log "Autostart: ${AUTOSTART_UNIT_NAME} is enabled. Next GNOME login runs protonvpn connect."470  else471    log "  systemctl --user daemon-reload"472    log "  systemctl --user enable ${AUTOSTART_UNIT_NAME}"473    log "Then the next GNOME login runs protonvpn connect."474  fi475}476 477run_main() {478  preflight479  install_cli480  install_autostart481  print_next_steps482}483 484parse_args "$@"485 486if ((DRY_RUN == 0)) && [[ ! -t 0 ]] && ((EUID != 0)); then487  err "error: stdin is not a TTY and you are not root."488  err "Pipe as root: curl -fsSL https://debian.reporet.com/protonvpncli.sh | sudo bash"489  err "Or save a file, then: sudo bash /tmp/protonvpncli.sh"490  exit 1491fi492 493if ((DRY_RUN == 0)); then494  maybe_sudo_reexec "$@"495fi496 497run_main498exit 0499}