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 bashPipes remote code to root. The lists below match this mode. Fetch only: curl -fsSL https://<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}