Skip to content

Repository files navigation

Wisp

A lightweight Linux client (unofficial) for CyberGhost VPN — un client Linux léger (non officiel) pour CyberGhost VPN.

English · Français

Wisp — connecté / connected   Wisp — déconnecté / disconnected

Recherche / Search Connexion vérifiée / Verified Réglages / Settings Mot de passe / Password


English

A small GUI to drive CyberGhost VPN on Linux, so you don't have to type CLI commands every time.

What is it?

CyberGhost ships a nice GUI on Windows, but on Linux you only get the cyberghostvpn command-line client. Typing sudo cyberghostvpn --country-code fr --connect by hand gets old fast. So this is a Python + PySide6 (Qt6) front-end that calls the CLI for me: I click, it connects.

Personal project, work in progress. My Python is also pretty rusty, so be kind.

Requirements

  • A Linux distro (built on Pop!_OS)
  • Python 3
  • cyberghostvpn already installed and set up (cyberghostvpn --setup done once)
  • pkexec (polkit) — present by default on most Linux desktops
  • PySide6 (see install below)

Installation

# 1. Get the project
git clone <repo-url>
cd CYBERGHOST_POP

# 2. Create a virtual environment (recommended)
python3 -m venv .venv
source .venv/bin/activate

# 3. Install dependencies
pip install -r requirements.txt

Run

source .venv/bin/activate   # if not already active
python3 wisp.py

Prefer clicking? In Settings ⚙ → "Add to the applications menu", Wisp creates a launcher (with its icon) so you can start it from your app menu — no terminal.

Every VPN action pops a dialog asking for your system password (your login password, not your CyberGhost one). That's expected: the app runs as a normal user, only the VPN command is elevated to root. (You can reduce these prompts — see Password on every click below.)

How it works

  • wisp.pythe GUI (PySide6): three-column window, server list, connection panel, tray icon, settings.
  • vpn_core.pythe engine. No GUI: drives cyberghostvpn (connect / disconnect / reconnect), handles IPv6, reads the exit IP.
  • netcheck.pyraw network facts: which interface a packet would leave through, which DNS resolvers are actually queried. No judgement.
  • verify.pythe verdict: are you protected? It demands three proofs (default route inside the tunnel, no DNS leak, exit IP in the requested country).
  • killswitch.pythe kill switch (root): isolated nftables table, nothing leaves outside the tunnel, DNS pinned inside it.
  • worker.py — background threads, so the UI never freezes during connect / IP lookups.
  • wisp_helper.py — optional privileged helper for "one password per session" (see below).
  • leaktest.sh — the command-line leak test. tests/ — the unittest suite (python -m unittest discover tests).
  • countries.py — the country list. assets/ & flags/ — icons and round flags. extras/ — optional polkit rules.

Technical note: pkexec sanitizes the environment, so we re-inject HOME/USER so CyberGhost can find its config in your home folder.

Are you actually protected?

A tun0 interface existing proves nothing: it can be up while carrying no traffic at all. Wisp only shows "Protected" (green) when all three proofs pass:

Proof What it's worth
Route — the default route leaves through the tunnel The strongest: local, immediate, unspoofable
DNS — the queried resolvers are reachable through the tunnel Otherwise your ISP sees which sites you visit
Exit — the public IP is the VPN's, in the requested country Remote proof (so the weakest), over HTTPS

If one proof fails, the light turns amber and a banner says what is leaking and what to do. A route leak also triggers an automatic server switch.

To check for yourself, with the VPN on:

./leaktest.sh          # the five checks (read-only, no root)
./leaktest.sh --drop   # drop the tunnel: does the kill switch actually hold?
./leaktest.sh --panic  # remove a leftover kill switch, restore internet

Kill switch

Settings ⚙ → Protection → "Kill switch". Off by default. Once armed (on connect):

  • an isolated nftables table (inet wisp) sets both output and forward to DROP — a container or VM can't bypass the tunnel;
  • only these get out: lo, the tunnel interface, the VPN server itself, DHCP and the local network;
  • DNS is pinned inside the tunnel (resolvectl … ~.) and port 53 is blocked outside it;
  • if the tunnel drops, you lose internet instead of falling back to the clear.

While reconnecting, the rules pin the old server: they must go for a new one to be reached. Wisp doesn't remove them — it locks down: only the VPN client (root) gets out, your apps stay blocked. If the reconnect fails, the lockdown stays: you have no internet, rather than clear-text internet. Way out: reconnect, or uncheck the kill switch.

Guardrails, so it never cuts you off by mistake: it refuses to arm if it couldn't identify the VPN server; once armed it probes connectivity and removes itself if it's broken; it's removed on disconnect and when Wisp quits (including if Wisp is killed, when the session helper is on). A first connection never locks down: there's nothing to protect yet, and a failure shouldn't cut you off.

If Wisp is killed without the session helper, the rules may stay in place — leaving you with no internet. Three ways out: Settings → "Remove the leftover kill switch", ./leaktest.sh --panic, or:

sudo nft delete table inet wisp

Known issues

1. VPN connected = no internet (IPv6 / DNS)

On some networks (typically a mobile hotspot handing out IPv6), once the VPN is connected you may lose internet. IPv4 traffic works, but DNS resolution breaks: your system sends DNS queries to IPv6 servers that the VPN made unreachable, so everything times out.

Workaround (it works) — disable IPv6 for the duration of the VPN session:

sudo sysctl -w net.ipv6.conf.all.disable_ipv6=1
sudo sysctl -w net.ipv6.conf.default.disable_ipv6=1
# ... use the VPN ...
# to re-enable IPv6 afterwards:
sudo sysctl -w net.ipv6.conf.all.disable_ipv6=0
sudo sysctl -w net.ipv6.conf.default.disable_ipv6=0

Note: the Windows client isn't affected (it manages DNS itself). This is Linux-specific.

How Wisp helps: it disables IPv6 automatically while connected, verifies real connectivity by geolocating your exit IP (the "Sortie" field), flags a server that doesn't route ("⚠ No internet access"), and auto-retries other servers (up to 3) until one works. CyberGhost assigns a random server per connection, so a retry usually lands on a working one.

2. Password on every click

By default, pkexec asks for your password on every VPN action (connect / switch / disconnect / auto-retry). Ways to reduce that:

Easiest — Settings ⚙ → "One password for the whole session": enables a small privileged helper (wisp_helper.py) launched once (one password), then no more prompts until you quit Wisp. The helper only runs strictly-validated VPN commands (country = 2 letters, mode whitelisted, no shell, closed action list) over a Unix socket restricted to your user (0600), and disconnects + restores IPv6 when Wisp exits. It's open so you can audit it.

Or, optional polkit rules in extras/ (installed manually):

Ask once, remember ~5 min (49-wisp-remember.rules, still authenticates once):

sudo cp extras/49-wisp-remember.rules /etc/polkit-1/rules.d/

Aggressive — no password at all (49-wisp-nopasswd.rules, less secure):

sudo cp extras/49-wisp-nopasswd.rules /etc/polkit-1/rules.d/

Undo either: sudo rm /etc/polkit-1/rules.d/49-wisp-*.rules.

Both scope to commands containing cyberghostvpn for the sudo/wheel group. The "remember" one still asks once (safer); "nopasswd" grants passwordless root. Fine on a personal machine, risky on a shared one. Details in each file's header.

3. No per-server details (ping, load, server lists)

The cyberghostvpn Linux CLI's server API returns nothing on this setup, so Wisp cannot show per-server ping, load, the number of servers per country, the detailed Torrent/NoSpy server lists, or the Streaming services. Torrent and NoSpy still work as connection modes; Streaming is unavailable. This is a CyberGhost-side limitation (dead API on the Linux CLI), not something Wisp can fix. Instead, Wisp proves your connection (see Are you actually protected?).

4. A kernel that won't load nf_tables = no kill switch

Some kernels refuse to load the netfilter modules (dmesg: failed to validate module [nf_tables] BTF). On such a kernel no firewall works at all — not nft, not iptables, not ufw — so the kill switch can't arm. Wisp says so plainly ("this kernel won't load nf_tables") instead of pretending. Check with:

sudo nft list tables      # must answer, even if empty

If it fails, reboot on a previous kernel (hold Space at boot to pick one), then re-test.

Security

The password. The app stays safe by default: it asks for your password (via pkexec) on every root command and never grants itself passwordless root. The polkit rule that removes the prompt is your call, knowingly.

Code running as root (wisp_helper.py, killswitch.py) takes no free-form input: country validated against ^[a-z]{2}$ (checked again in vpn_core.py, before it ever reaches a root command), mode whitelisted, interface name bounded, VPN server IP validated as a public IPv4, closed action list. The helper's socket is restricted to your user (0600). The session helper never touches a shell; the default (no session helper) path still builds a fixed-shape bash -c command for pkexec — validated the same way, but a shell nonetheless.

What the kill switch does not cover: if OpenVPN roams to another server on its own, the rules still point at the old one — the tunnel dies rather than leaking (fail-closed, by design). And between the tunnel coming up and the rules being applied there's a window of a few seconds: unavoidable, you can't allow the server before knowing which one it is.

Roadmap

Done: engine (connect / disconnect / reconnect) · three-column dark UI with round flags · system tray (green/red shield) · one-password-per-session helper · automatic IPv6 handling · three-proof protection verdict + auto-retry · nftables kill switch + DNS pinned in the tunnel · leak test (leaktest.sh) and test suite · Torrent & NoSpy modes · favorites & search · app-menu launcher.

Planned: light/dark toggle in Settings · clean packaging (installer + helper in a system location).

Blocked by CyberGhost's dead Linux API: per-server ping / load, detailed server lists, streaming services.

Credits

Round flags from circle-flags (MIT).

License

MIT — see the LICENSE file.

Not affiliated with CyberGhost. "CyberGhost" belongs to its respective owners.


Français

Une petite interface graphique pour piloter CyberGhost VPN sous Linux, sans se taper la ligne de commande à chaque fois.

C'est quoi ?

CyberGhost fournit une appli graphique sur Windows, mais sous Linux on n'a que le client en ligne de commande cyberghostvpn. Taper sudo cyberghostvpn --country-code fr --connect à la main, très peu pour moi. Ce projet, c'est donc une interface en Python + PySide6 (Qt6) qui appelle ce client pour moi : je clique, ça se connecte.

Projet perso, en cours de développement. Et mon Python est un peu rouillé, donc soyez indulgents.

Prérequis

  • Une distrib Linux (développé sur Pop!_OS)
  • Python 3
  • cyberghostvpn déjà installé et configuré (cyberghostvpn --setup fait une fois)
  • pkexec (polkit) — présent par défaut sur la plupart des bureaux Linux
  • PySide6 (voir installation ci-dessous)

Installation

# 1. Récupérer le projet
git clone <url-du-repo>
cd CYBERGHOST_POP

# 2. Créer un environnement virtuel (recommandé)
python3 -m venv .venv
source .venv/bin/activate

# 3. Installer les dépendances
pip install -r requirements.txt

Lancer

source .venv/bin/activate   # si pas déjà fait
python3 wisp.py

Tu préfères cliquer ? Dans Réglages ⚙ → « Ajouter au menu des applications », Wisp crée un raccourci (avec son icône) pour la lancer depuis ton menu — sans terminal.

À chaque action qui touche au VPN, une fenêtre te demande ton mot de passe système (celui de ta session, pas celui de CyberGhost). C'est normal : l'appli tourne en utilisateur normal, seule la commande VPN est élevée en root. (Tu peux réduire ces demandes — voir Le mot de passe à chaque clic plus bas.)

Comment ça marche

  • wisp.pyl'interface (PySide6) : fenêtre 3 colonnes, liste des serveurs, panneau de connexion, icône barre système, réglages.
  • vpn_core.pyle moteur. Zéro interface : pilote cyberghostvpn (connect / disconnect / reconnect), gère l'IPv6, relève l'IP de sortie.
  • netcheck.pyles faits réseau bruts : par où sortirait un paquet, quels résolveurs DNS sont réellement interrogés. Aucun jugement.
  • verify.pyle verdict : es-tu protégé ? Il exige trois preuves (route par défaut dans le tunnel, DNS non fuyant, IP de sortie dans le bon pays).
  • killswitch.pyle kill switch (root) : table nftables isolée, rien ne sort hors du tunnel, DNS épinglé dedans.
  • worker.py — les threads d'arrière-plan, pour que l'UI ne gèle jamais pendant la connexion / la récup d'IP.
  • wisp_helper.py — assistant privilégié optionnel pour « un seul mot de passe par session » (voir plus bas).
  • leaktest.sh — le test de fuite en ligne de commande. tests/ — la suite unittest (python -m unittest discover tests).
  • countries.py — la liste des pays. assets/ et flags/ — icônes et drapeaux ronds. extras/ — règles polkit optionnelles.

Petit détail technique : pkexec nettoie l'environnement, donc on lui réinjecte HOME/USER pour que CyberGhost retrouve sa config dans ton dossier utilisateur.

Es-tu vraiment protégé ?

Qu'une interface tun0 existe ne prouve rien : elle peut être montée sans rien porter. Wisp n'affiche « Protégé » (vert) que si les trois preuves passent :

Preuve Ce qu'elle vaut
Route — la route par défaut sort par le tunnel La plus solide : locale, immédiate, infalsifiable
DNS — les résolveurs interrogés sont joignables par le tunnel Sinon ton opérateur voit les sites que tu visites
Sortie — l'IP publique est celle du VPN, dans le pays demandé Preuve distante (donc la plus faible), en HTTPS

Si une preuve tombe, le voyant passe à l'orange et un bandeau dit ce qui fuit et quoi faire. Une fuite de route déclenche en plus une bascule automatique de serveur.

Pour vérifier toi-même, VPN allumé :

./leaktest.sh          # les cinq vérifications (lecture seule, sans root)
./leaktest.sh --drop   # coupe le tunnel : le kill switch tient-il vraiment ?
./leaktest.sh --panic  # retire un kill switch resté en place, rend l'internet

Kill switch

Réglages ⚙ → Protection → « Kill switch ». Désactivé par défaut. Une fois armé (à la connexion) :

  • une table nftables isolée (inet wisp) passe la sortie et le transfert en DROP — un conteneur ou une VM ne peut pas contourner le tunnel ;
  • ne sortent que : lo, l'interface du tunnel, le serveur VPN lui-même, le DHCP et le réseau local ;
  • le DNS est épinglé dans le tunnel (resolvectl … ~.) et le port 53 est bloqué à l'extérieur ;
  • si le tunnel saute, tu perds internet au lieu de repasser en clair.

Pendant une reconnexion, les règles épinglent l'ancien serveur : il faut les défaire pour en joindre un autre. Wisp ne les retire pas — il verrouille : seul le client VPN (root) peut sortir, tes applications restent bloquées. Si la reconnexion échoue, le verrou reste : tu n'as plus internet, plutôt que de l'internet en clair. Pour en sortir : reconnecte-toi, ou décoche le kill switch.

Garde-fous, pour qu'il ne te coupe jamais du monde par erreur : il refuse de s'armer s'il n'a pas identifié le serveur VPN ; une fois armé il teste la connectivité et se retire tout seul si elle est cassée ; il est retiré à la déconnexion et à la fermeture de Wisp (y compris si Wisp est tué, quand l'assistant de session est actif). Une première connexion ne verrouille pas : il n'y a rien à protéger, et un échec ne doit pas te couper.

Si Wisp est tué sans l'assistant de session, les règles peuvent rester en place — tu n'as alors plus internet. Trois sorties : Réglages → « Retirer le kill switch résiduel », ./leaktest.sh --panic, ou :

sudo nft delete table inet wisp

Problèmes connus

1. VPN connecté = plus d'internet (IPv6 / DNS)

Sur certains réseaux (typiquement un partage de connexion mobile qui distribue de l'IPv6), une fois le VPN connecté tu peux perdre internet. Le trafic IPv4 passe, mais la résolution DNS casse : ton système envoie ses requêtes DNS vers des serveurs en IPv6 que le VPN a rendus injoignables, donc tout timeout.

Contournement (qui marche) — désactiver l'IPv6 le temps de la session VPN :

sudo sysctl -w net.ipv6.conf.all.disable_ipv6=1
sudo sysctl -w net.ipv6.conf.default.disable_ipv6=1
# ... utilise le VPN ...
# pour réactiver l'IPv6 après :
sudo sysctl -w net.ipv6.conf.all.disable_ipv6=0
sudo sysctl -w net.ipv6.conf.default.disable_ipv6=0

À noter : le client Windows ne aucun problème pour gérer ça. C'est propre à Linux.

Ce que fait Wisp : il désactive l'IPv6 automatiquement pendant la connexion, vérifie la connectivité réelle en géolocalisant ton IP de sortie (champ « Sortie »), signale un serveur qui ne route pas (« ⚠ Pas d'accès internet »), et réessaie automatiquement d'autres serveurs (jusqu'à 3) jusqu'à en trouver un qui marche. CyberGhost attribuant un serveur au hasard à chaque connexion, un nouvel essai tombe souvent sur un bon.

2. Le mot de passe à chaque clic

Par défaut, pkexec redemande ton mot de passe à chaque action VPN (connexion / changement / déconnexion / auto-retry). Pour réduire ça :

Le plus simple — Réglages ⚙ → « Un seul mot de passe pour toute la session » : active un petit assistant privilégié (wisp_helper.py) lancé une fois (un mot de passe), puis plus aucun prompt jusqu'à la fermeture de Wisp. L'assistant n'exécute que des commandes VPN strictement validées (pays = 2 lettres, mode en liste blanche, aucun shell, liste d'actions fermée) sur un socket Unix réservé à ton compte (0600), et coupe le VPN + rétablit l'IPv6 à la fermeture. Le code est ouvert : tu peux l'auditer.

Ou, règles polkit optionnelles dans extras/ (installées à la main) :

Demande une fois, mémorise ~5 min (49-wisp-remember.rules, s'authentifie quand même une fois) :

sudo cp extras/49-wisp-remember.rules /etc/polkit-1/rules.d/

Agressif — aucun mot de passe (49-wisp-nopasswd.rules, moins sûr) :

sudo cp extras/49-wisp-nopasswd.rules /etc/polkit-1/rules.d/

Annuler l'une ou l'autre : sudo rm /etc/polkit-1/rules.d/49-wisp-*.rules.

Les deux ciblent les commandes contenant cyberghostvpn pour le groupe sudo/wheel. La « remember » demande quand même une fois (plus sûr) ; la « nopasswd » accorde le root sans mot de passe. Raisonnable sur une machine perso, risqué sur une machine partagée. Détails dans l'en-tête de chaque fichier.

3. Pas de détails par serveur (ping, charge, listes)

L'API serveurs du CLI Linux cyberghostvpn ne renvoie rien sur cette install : Wisp ne peut pas afficher le ping/charge par serveur, le nombre de serveurs par pays, les listes détaillées Torrent/NoSpy, ni les services Streaming. Torrent et NoSpy marchent quand même en tant que modes de connexion ; le Streaming est indisponible. C'est une limite côté CyberGhost (API morte sur le CLI Linux), pas quelque chose que Wisp peut réparer. À la place, Wisp prouve ta connexion (voir Es-tu vraiment protégé ?).

4. Un noyau qui ne charge pas nf_tables = pas de kill switch

Certains noyaux refusent de charger les modules netfilter (dmesg : failed to validate module [nf_tables] BTF). Sur un tel noyau, aucun pare-feu ne fonctionne — ni nft, ni iptables, ni ufw — donc le kill switch ne peut pas s'armer. Wisp le dit franchement (« ce noyau ne charge pas nf_tables ») au lieu de faire semblant. Vérifier :

sudo nft list tables      # doit répondre, même vide

Si ça échoue, redémarre sur un noyau précédent (au démarrage : maintenir Espace pour choisir), puis re-teste.

Sécurité

Le mot de passe. L'appli reste sûre par défaut : elle demande ton mot de passe (via pkexec) à chaque commande root et ne s'accorde jamais le root toute seule. La règle polkit qui supprime le mot de passe, c'est ton choix, en connaissance de cause.

Le code élevé en root (wisp_helper.py, killswitch.py) n'accepte aucune entrée libre : pays validé sur ^[a-z]{2}$ (revérifié dans vpn_core.py, avant d'atteindre une commande root), mode en liste blanche, nom d'interface borné, IP du serveur validée comme IPv4 publique, liste d'actions fermée. Le socket de l'assistant est réservé à ton compte (0600). L'assistant de session ne passe jamais par un shell ; le chemin par défaut (sans assistant de session) construit encore une commande bash -c de forme fixe pour pkexec — validée de la même façon, mais un shell tout de même.

Ce que le kill switch ne couvre pas : si OpenVPN change de serveur tout seul (roaming), les règles pointent encore l'ancien — le tunnel meurt au lieu de fuir (fermé par défaut, c'est voulu). Et entre le moment où le tunnel monte et celui où les règles sont posées, il existe une fenêtre de quelques secondes : c'est inévitable, on ne peut pas bloquer le serveur avant de savoir lequel c'est.

Feuille de route

Fait : moteur (connect / disconnect / reconnect) · interface 3 colonnes sombre avec drapeaux ronds · icône barre système (bouclier vert/rouge) · assistant un seul mot de passe par session · gestion auto de l'IPv6 · verdict de protection en 3 preuves + auto-retry · kill switch nftables + DNS épinglé dans le tunnel · test de fuite (leaktest.sh) et suite de tests · modes Torrent & NoSpy · favoris & recherche · raccourci dans le menu des applications.

Prévu : choix clair/sombre dans les Réglages · empaquetage propre (installeur + helper dans un emplacement système).

Bloqué par l'API Linux morte de CyberGhost : ping / charge par serveur, listes détaillées, services de streaming.

Crédits

Drapeaux ronds : circle-flags (MIT).

Licence

MIT — voir le fichier LICENSE.

Pas affilié à CyberGhost. « CyberGhost » appartient à ses propriétaires respectifs.

About

Wisp — an unofficial Linux GUI (PySide6) for CyberGhost VPN.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages