[ADD] menu vpn : créer un profil, déposer les secrets, monter le tunnel
`vpn.py` savait tout faire en ligne de commande, mais il fallait écrire le
profil JSON à la main et déposer les secrets dans KeePassXC soi-même. Le menu
pose les questions que le pilote choisi déclare, et lui seul : ajouter une
technologie n'ajoute pas une ligne ici.
Le coffre est manipulé EN PROCESSUS — le mot de passe maître est déjà en
mémoire, le redemander à un sous-processus serait une saisie de plus à chaque
geste. Montage, démontage et diagnostic passent au contraire par `vpn.py` en
sous-processus : ils durent, ils parlent, et la sortie en direct est ce qui
rend une montée de tunnel suivable.
Vérifié : 37 tests sur le menu, aucune saisie réelle.
--- EN ---
`vpn.py` could already do everything from the command line, but the JSON
profile had to be written by hand and the secrets filed into KeePassXC by
hand. The menu asks the questions the chosen driver declares, and only those:
adding a technology adds no line here.
The vault is handled IN PROCESS — the master password is already in memory,
and asking a subprocess for it again would be one more entry at every step.
Raising, tearing down and diagnosing go through `vpn.py` as a subprocess
instead: they last, they talk, and live output is what makes a tunnel coming
up followable.
Checked: 37 tests on the menu, no real input.
Assisted-by: Claude Opus 5
2026-09-03 23:43:02 -04:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
# © 2026 TechnoLibre (http://www.technolibre.ca)
|
|
|
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
|
|
|
"""Le menu VPN : profils, secrets, montée, diagnostic.
|
|
|
|
|
|
|
|
|
|
La frontière avec `script/vpn/` est nette : ici on DEMANDE (quel profil,
|
|
|
|
|
quelle adresse, quel PSK) et on affiche ; là-bas on décide et on exécute. Ce
|
|
|
|
|
fichier ne connaît ni ipsec.conf, ni xl2tpd, ni aucun chemin système.
|
|
|
|
|
|
|
|
|
|
Deux chemins d'exécution, pour une raison :
|
|
|
|
|
|
|
|
|
|
· les profils et les secrets sont manipulés EN PROCESSUS, par les modules
|
|
|
|
|
`script.vpn.profiles` et `script.vpn.vault` — le mot de passe maître du
|
|
|
|
|
coffre est déjà en mémoire ici, le redemander à un sous-processus serait
|
|
|
|
|
une saisie de plus à chaque geste ;
|
|
|
|
|
· le montage, la descente et le diagnostic passent par `script/vpn/vpn.py`
|
|
|
|
|
en sous-processus — ils durent, ils parlent, et ils appellent sudo. La
|
|
|
|
|
sortie en direct est ce qui rend un « ipsec up » suivable.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import getpass
|
[ADD] vpn openconnect : groupe d'URL, SSO délégué, mot de passe borné
Deux mécanismes désignent un service sur un concentrateur : un chemin
d'URL, une valeur de menu déroulant. Les confondre rend le formulaire d'un
AUTRE service — identifiants justes refusés, rien ne désignant le groupe.
Et certains ne comparent que les N premiers caractères du mot de passe,
que le profil déclare désormais sans jamais rien tronquer.
Une passerelle qui exige un navigateur intégré arrête openconnect sur
« No SSO handler », les distributions le bâtissant sans webview. Un greffon
fait l'étape web et rend un cookie ; le pilote monte lui-même, et le profil
garde son interface, son état et son diagnostic.
Vérifié : 249 tests unitaires, un tunnel monté contre une passerelle SAML.
--- EN ---
Two mechanisms designate a service on one concentrator: a URL path, and a
dropdown value. Confusing them hands over ANOTHER service's login form —
correct credentials refused, with nothing pointing at the group. And some
compare only the first N characters of the password, which the profile now
declares without ever truncating anything.
A gateway demanding an embedded browser stops openconnect on "No SSO
handler", distributions building it without a webview. A helper does the
web step and returns a cookie; the driver mounts the tunnel itself, so the
profile keeps its interface, its state and its diagnosis.
Checked: 249 unit tests, and one tunnel mounted against a SAML gateway.
Assisted-by: Claude Opus 5
2026-09-04 05:52:09 -04:00
|
|
|
import os
|
[ADD] menu vpn : créer un profil, déposer les secrets, monter le tunnel
`vpn.py` savait tout faire en ligne de commande, mais il fallait écrire le
profil JSON à la main et déposer les secrets dans KeePassXC soi-même. Le menu
pose les questions que le pilote choisi déclare, et lui seul : ajouter une
technologie n'ajoute pas une ligne ici.
Le coffre est manipulé EN PROCESSUS — le mot de passe maître est déjà en
mémoire, le redemander à un sous-processus serait une saisie de plus à chaque
geste. Montage, démontage et diagnostic passent au contraire par `vpn.py` en
sous-processus : ils durent, ils parlent, et la sortie en direct est ce qui
rend une montée de tunnel suivable.
Vérifié : 37 tests sur le menu, aucune saisie réelle.
--- EN ---
`vpn.py` could already do everything from the command line, but the JSON
profile had to be written by hand and the secrets filed into KeePassXC by
hand. The menu asks the questions the chosen driver declares, and only those:
adding a technology adds no line here.
The vault is handled IN PROCESS — the master password is already in memory,
and asking a subprocess for it again would be one more entry at every step.
Raising, tearing down and diagnosing go through `vpn.py` as a subprocess
instead: they last, they talk, and live output is what makes a tunnel coming
up followable.
Checked: 37 tests on the menu, no real input.
Assisted-by: Claude Opus 5
2026-09-03 23:43:02 -04:00
|
|
|
|
|
|
|
|
import click
|
|
|
|
|
|
[ADD] vpn : parcourir les profils AnyConnect au lieu de taper le chemin
Personne ne retient « /opt/cisco/secureclient/vpn/profile ». Le chemin
tapé oblige à le connaître ; le parcours seul n'aide pas quand le fichier
vient d'ailleurs, d'un courriel ou du portail d'un site. Les deux, dans
cet ordre, et la saisie reprend la main dès qu'on sort du parcours sans
rien choisir ou qu'on en rapporte un répertoire.
Le parcours n'est proposé que si l'un des répertoires connus existe :
l'ouvrir sur un chemin absent afficherait une liste vide, ce qui ressemble
à une panne. Vérifié : 7 tests unitaires, dont chacun des replis.
--- EN ---
Nobody remembers "/opt/cisco/secureclient/vpn/profile". A typed path
requires knowing it; browsing alone does not help when the file comes
from elsewhere, from an e-mail or a site's portal. Both, in that order,
and typing takes over as soon as one leaves the browser without choosing
anything, or brings back a directory from it.
Browsing is offered only if one of the known directories exists: opening
it on an absent path would show an empty list, which looks like a fault.
Checked: 7 unit tests, one for each fallback.
Assisted-by: Claude Opus 5
2026-09-04 08:48:28 -04:00
|
|
|
try:
|
|
|
|
|
from script.todo import todo_file_browser
|
|
|
|
|
except Exception:
|
|
|
|
|
# urwid peut manquer : le parcours devient indisponible, la saisie
|
|
|
|
|
# directe reste. Un menu qui ne s'ouvre plus serait pire.
|
|
|
|
|
todo_file_browser = None
|
|
|
|
|
|
[ADD] menu vpn : créer un profil, déposer les secrets, monter le tunnel
`vpn.py` savait tout faire en ligne de commande, mais il fallait écrire le
profil JSON à la main et déposer les secrets dans KeePassXC soi-même. Le menu
pose les questions que le pilote choisi déclare, et lui seul : ajouter une
technologie n'ajoute pas une ligne ici.
Le coffre est manipulé EN PROCESSUS — le mot de passe maître est déjà en
mémoire, le redemander à un sous-processus serait une saisie de plus à chaque
geste. Montage, démontage et diagnostic passent au contraire par `vpn.py` en
sous-processus : ils durent, ils parlent, et la sortie en direct est ce qui
rend une montée de tunnel suivable.
Vérifié : 37 tests sur le menu, aucune saisie réelle.
--- EN ---
`vpn.py` could already do everything from the command line, but the JSON
profile had to be written by hand and the secrets filed into KeePassXC by
hand. The menu asks the questions the chosen driver declares, and only those:
adding a technology adds no line here.
The vault is handled IN PROCESS — the master password is already in memory,
and asking a subprocess for it again would be one more entry at every step.
Raising, tearing down and diagnosing go through `vpn.py` as a subprocess
instead: they last, they talk, and live output is what makes a tunnel coming
up followable.
Checked: 37 tests on the menu, no real input.
Assisted-by: Claude Opus 5
2026-09-03 23:43:02 -04:00
|
|
|
from script.todo.todo_i18n import t
|
[ADD] vpn openconnect : groupe d'URL, SSO délégué, mot de passe borné
Deux mécanismes désignent un service sur un concentrateur : un chemin
d'URL, une valeur de menu déroulant. Les confondre rend le formulaire d'un
AUTRE service — identifiants justes refusés, rien ne désignant le groupe.
Et certains ne comparent que les N premiers caractères du mot de passe,
que le profil déclare désormais sans jamais rien tronquer.
Une passerelle qui exige un navigateur intégré arrête openconnect sur
« No SSO handler », les distributions le bâtissant sans webview. Un greffon
fait l'étape web et rend un cookie ; le pilote monte lui-même, et le profil
garde son interface, son état et son diagnostic.
Vérifié : 249 tests unitaires, un tunnel monté contre une passerelle SAML.
--- EN ---
Two mechanisms designate a service on one concentrator: a URL path, and a
dropdown value. Confusing them hands over ANOTHER service's login form —
correct credentials refused, with nothing pointing at the group. And some
compare only the first N characters of the password, which the profile now
declares without ever truncating anything.
A gateway demanding an embedded browser stops openconnect on "No SSO
handler", distributions building it without a webview. A helper does the
web step and returns a cookie; the driver mounts the tunnel itself, so the
profile keeps its interface, its state and its diagnosis.
Checked: 249 unit tests, and one tunnel mounted against a SAML gateway.
Assisted-by: Claude Opus 5
2026-09-04 05:52:09 -04:00
|
|
|
from script.vpn import anyconnect_xml, presets, profiles
|
[ADD] menu vpn : créer un profil, déposer les secrets, monter le tunnel
`vpn.py` savait tout faire en ligne de commande, mais il fallait écrire le
profil JSON à la main et déposer les secrets dans KeePassXC soi-même. Le menu
pose les questions que le pilote choisi déclare, et lui seul : ajouter une
technologie n'ajoute pas une ligne ici.
Le coffre est manipulé EN PROCESSUS — le mot de passe maître est déjà en
mémoire, le redemander à un sous-processus serait une saisie de plus à chaque
geste. Montage, démontage et diagnostic passent au contraire par `vpn.py` en
sous-processus : ils durent, ils parlent, et la sortie en direct est ce qui
rend une montée de tunnel suivable.
Vérifié : 37 tests sur le menu, aucune saisie réelle.
--- EN ---
`vpn.py` could already do everything from the command line, but the JSON
profile had to be written by hand and the secrets filed into KeePassXC by
hand. The menu asks the questions the chosen driver declares, and only those:
adding a technology adds no line here.
The vault is handled IN PROCESS — the master password is already in memory,
and asking a subprocess for it again would be one more entry at every step.
Raising, tearing down and diagnosing go through `vpn.py` as a subprocess
instead: they last, they talk, and live output is what makes a tunnel coming
up followable.
Checked: 37 tests on the menu, no real input.
Assisted-by: Claude Opus 5
2026-09-03 23:43:02 -04:00
|
|
|
from script.vpn.drivers import DRIVERS, get_driver
|
|
|
|
|
from script.vpn.vault import VaultError, VpnVault, secrets_to_env
|
|
|
|
|
|
|
|
|
|
# `-u` : sans lui, la sortie du script est mise en tampon par blocs dès
|
|
|
|
|
# qu'elle est redirigée, et une montée de tunnel de trente secondes
|
|
|
|
|
# n'afficherait rien avant la fin.
|
|
|
|
|
VPN_CLI = "./.venv.erplibre/bin/python -u ./script/vpn/vpn.py"
|
|
|
|
|
|
|
|
|
|
# Nommé pour que la clé i18n tienne sur une ligne lisible — c'est la même
|
|
|
|
|
# chaîne que celle du CLI, et elle est longue parce qu'elle doit dire quoi
|
|
|
|
|
# faire, pas seulement que quelque chose ne va pas.
|
|
|
|
|
# Ce qu'on dit à qui n'a reçu du site qu'une passerelle et des
|
|
|
|
|
# identifiants : le profil est utilisable, et le premier montage dira
|
|
|
|
|
# lui-même quel réseau ajouter.
|
|
|
|
|
NO_ROUTE_NOTE = (
|
|
|
|
|
"No network routed yet: this tunnel will only reach the remote host."
|
|
|
|
|
" Connect once — the address you get tells you which network to add."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# La légende de l'étoile posée sur les technologies non éprouvées. Elle dit
|
|
|
|
|
# ce qui manque — la confrontation au terrain — et non que le code serait
|
|
|
|
|
# douteux : les tests unitaires, eux, sont là.
|
|
|
|
|
UNPROVEN_NOTE = "never mounted against a real server: only unit tests cover it"
|
|
|
|
|
|
[ADD] vpn openconnect : groupe d'URL, SSO délégué, mot de passe borné
Deux mécanismes désignent un service sur un concentrateur : un chemin
d'URL, une valeur de menu déroulant. Les confondre rend le formulaire d'un
AUTRE service — identifiants justes refusés, rien ne désignant le groupe.
Et certains ne comparent que les N premiers caractères du mot de passe,
que le profil déclare désormais sans jamais rien tronquer.
Une passerelle qui exige un navigateur intégré arrête openconnect sur
« No SSO handler », les distributions le bâtissant sans webview. Un greffon
fait l'étape web et rend un cookie ; le pilote monte lui-même, et le profil
garde son interface, son état et son diagnostic.
Vérifié : 249 tests unitaires, un tunnel monté contre une passerelle SAML.
--- EN ---
Two mechanisms designate a service on one concentrator: a URL path, and a
dropdown value. Confusing them hands over ANOTHER service's login form —
correct credentials refused, with nothing pointing at the group. And some
compare only the first N characters of the password, which the profile now
declares without ever truncating anything.
A gateway demanding an embedded browser stops openconnect on "No SSO
handler", distributions building it without a webview. A helper does the
web step and returns a cookie; the driver mounts the tunnel itself, so the
profile keeps its interface, its state and its diagnosis.
Checked: 249 unit tests, and one tunnel mounted against a SAML gateway.
Assisted-by: Claude Opus 5
2026-09-04 05:52:09 -04:00
|
|
|
# Où poser un préréglage quand il n'y en a aucun. Dit les DEUX répertoires,
|
|
|
|
|
# parce qu'ils ne servent pas au même usage : l'un est suivi par git et ne
|
|
|
|
|
# doit rien porter d'identifiant, l'autre est ignoré et existe pour ça.
|
|
|
|
|
PRESET_LOCATION_NOTE = (
|
|
|
|
|
"Drop a .json file in conf/vpn_presets/ (shared, nothing identifying)"
|
|
|
|
|
" or in private/vpn/presets/ (git-ignored, where a site preset goes)."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Ce qu'on dit quand le nom tapé désigne un profil déjà là. Dit LEQUEL des
|
|
|
|
|
# deux gagne, champ par champ : sans cela, on ne sait pas si rejouer un
|
|
|
|
|
# préréglage remet la passerelle à jour ou efface les routes ajoutées.
|
|
|
|
|
PRESET_REPLAYED_NOTE = (
|
|
|
|
|
"This profile already exists: the preset refreshes what it declares,"
|
|
|
|
|
" everything personal is kept."
|
|
|
|
|
)
|
|
|
|
|
|
[ADD] vpn : parcourir les profils AnyConnect au lieu de taper le chemin
Personne ne retient « /opt/cisco/secureclient/vpn/profile ». Le chemin
tapé oblige à le connaître ; le parcours seul n'aide pas quand le fichier
vient d'ailleurs, d'un courriel ou du portail d'un site. Les deux, dans
cet ordre, et la saisie reprend la main dès qu'on sort du parcours sans
rien choisir ou qu'on en rapporte un répertoire.
Le parcours n'est proposé que si l'un des répertoires connus existe :
l'ouvrir sur un chemin absent afficherait une liste vide, ce qui ressemble
à une panne. Vérifié : 7 tests unitaires, dont chacun des replis.
--- EN ---
Nobody remembers "/opt/cisco/secureclient/vpn/profile". A typed path
requires knowing it; browsing alone does not help when the file comes
from elsewhere, from an e-mail or a site's portal. Both, in that order,
and typing takes over as soon as one leaves the browser without choosing
anything, or brings back a directory from it.
Browsing is offered only if one of the known directories exists: opening
it on an absent path would show an empty list, which looks like a fault.
Checked: 7 unit tests, one for each fallback.
Assisted-by: Claude Opus 5
2026-09-04 08:48:28 -04:00
|
|
|
# Répertoires où chercher un profil AnyConnect, dans l'ordre. Les deux
|
|
|
|
|
# premiers sont ceux du client de Cisco — le nom a changé entre AnyConnect
|
|
|
|
|
# et Secure Client. Les deux derniers parce qu'un site le distribue aussi
|
|
|
|
|
# par courriel ou par son portail, et le fichier atterrit alors là.
|
|
|
|
|
ANYCONNECT_DIRS = (
|
|
|
|
|
"/opt/cisco/secureclient/vpn/profile",
|
|
|
|
|
"/opt/cisco/anyconnect/profile",
|
|
|
|
|
"~/Downloads",
|
|
|
|
|
"~/Téléchargements",
|
|
|
|
|
)
|
|
|
|
|
|
[ADD] vpn openconnect : groupe d'URL, SSO délégué, mot de passe borné
Deux mécanismes désignent un service sur un concentrateur : un chemin
d'URL, une valeur de menu déroulant. Les confondre rend le formulaire d'un
AUTRE service — identifiants justes refusés, rien ne désignant le groupe.
Et certains ne comparent que les N premiers caractères du mot de passe,
que le profil déclare désormais sans jamais rien tronquer.
Une passerelle qui exige un navigateur intégré arrête openconnect sur
« No SSO handler », les distributions le bâtissant sans webview. Un greffon
fait l'étape web et rend un cookie ; le pilote monte lui-même, et le profil
garde son interface, son état et son diagnostic.
Vérifié : 249 tests unitaires, un tunnel monté contre une passerelle SAML.
--- EN ---
Two mechanisms designate a service on one concentrator: a URL path, and a
dropdown value. Confusing them hands over ANOTHER service's login form —
correct credentials refused, with nothing pointing at the group. And some
compare only the first N characters of the password, which the profile now
declares without ever truncating anything.
A gateway demanding an embedded browser stops openconnect on "No SSO
handler", distributions building it without a webview. A helper does the
web step and returns a cookie; the driver mounts the tunnel itself, so the
profile keeps its interface, its state and its diagnosis.
Checked: 249 unit tests, and one tunnel mounted against a SAML gateway.
Assisted-by: Claude Opus 5
2026-09-04 05:52:09 -04:00
|
|
|
# Où le client de Cisco dépose les profils qu'un site distribue. Le dire
|
|
|
|
|
# évite d'avoir à le chercher, et c'est le seul endroit où il se trouve
|
|
|
|
|
# quand le client graphique a déjà servi sur la machine.
|
|
|
|
|
ANYCONNECT_LOCATION_NOTE = (
|
|
|
|
|
"An AnyConnect profile usually sits in"
|
|
|
|
|
" /opt/cisco/secureclient/vpn/profile/ (or .../anyconnect/profile/)."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Ce que le fichier ne dit PAS, et qu'il reste donc à régler. Le profil
|
|
|
|
|
# AnyConnect ne déclare pas la méthode d'authentification : c'est le
|
|
|
|
|
# concentrateur qui l'annonce à la connexion.
|
|
|
|
|
ANYCONNECT_NEXT_STEP = (
|
|
|
|
|
"Next step: create a profile from one of them. The .xml carries no"
|
|
|
|
|
" username and does not say whether the service authenticates by"
|
|
|
|
|
" password or by web form."
|
|
|
|
|
)
|
|
|
|
|
|
[ADD] vpn install : proposer et poser le greffon SSO
openconnect refuse les passerelles qui exigent un navigateur intégré, sur
« No SSO handler » : les distributions le bâtissent sans webview. Rien
n'installait le greffon qui fait cette étape, que le pilote attendait
pourtant — une machine paraissait équipée sans l'être.
La question n'est posée que pour le pilote qui peut s'en servir, et
seulement quand le greffon manque. Son amont est arrêté depuis 2023 :
épingles intenables sur un Python récent, Qt et lxml pris de la
distribution, correctif rejoué à chaque installation. Le venv appartient
à l'utilisateur, non à root, qui n'a ni affichage ni trousseau. Vérifié :
installation depuis rien en 6,5 s, paquets éprouvés sur debian et ubuntu.
--- EN ---
openconnect refuses gateways demanding an embedded browser, on "No SSO
handler": distributions build it without a webview. Nothing installed the
helper that performs that step, which the driver expected all the same —
a machine looked equipped without being so.
The question is asked only for the driver that can use it, and only when
the helper is missing. Its upstream has been unmaintained since 2023:
pins unsatisfiable on a recent Python, Qt and lxml taken from the
distribution, a patch replayed on every install. The venv belongs to the
user, not root, which has neither display nor keyring. Checked: install
from nothing in 6.5 s, package names proven on debian and ubuntu only.
Assisted-by: Claude Opus 5
2026-09-04 07:37:49 -04:00
|
|
|
# Ce qu'on dit avant de proposer le greffon SSO. Il faut que la réponse
|
|
|
|
|
# soit éclairée : le greffon n'est utile que pour une passerelle qui exige
|
|
|
|
|
# un navigateur intégré, et son amont n'est plus entretenu.
|
|
|
|
|
SSO_HELPER_NOTE = (
|
|
|
|
|
"Some gateways demand an embedded browser (SAML): openconnect stops on"
|
|
|
|
|
" « No SSO handler » and a helper is needed for the web step. It is"
|
|
|
|
|
" optional, and its upstream is no longer maintained."
|
|
|
|
|
)
|
|
|
|
|
|
[FIX] vpn : juger un profil monté sur son interface, et le marquer
Le verdict se fondait sur le seul fichier d'état, que RIEN n'efface quand
un tunnel meurt sans passer par « down » — machine redémarrée, processus
tué, session expirée. Un état laissé derrière était annoncé monté sur
l'écran même qui déclarait le processus mort et l'interface absente.
L'interface arbitre désormais : son nom vient de l'état s'il y en a un,
sinon du pilote pour ceux qui la nomment d'avance, si bien qu'un tunnel
monté hors de l'outil est vu aussi. Sshuttle n'en crée aucune, là le
processus reste seul juge. La liste des profils le marque, connecter un
profil déjà monté demande confirmation, et déconnecter un profil déjà
tombé le dit sans l'empêcher : « down » nettoie l'état laissé.
--- EN ---
The verdict rested on the state file alone, which NOTHING clears when a
tunnel dies without going through "down" — machine rebooted, process
killed, session expired. A leftover state was reported as mounted on the
very screen declaring the process gone and the interface absent.
The interface now arbitrates: its name comes from the state when there is
one, otherwise from drivers that name it in advance, so a tunnel brought
up outside the tool is seen too. Sshuttle creates none, and there the
process stays the only judge. The profile list marks it, connecting an
already-mounted profile asks first, and disconnecting an already-down one
says so without refusing: "down" clears the leftover state.
Assisted-by: Claude Opus 5
2026-09-07 23:27:35 -04:00
|
|
|
# Ce qu'on dit avant de descendre un profil qui n'est pas monté. Le geste
|
|
|
|
|
# garde un sens — il nettoie ce qu'un tunnel mort a laissé dans /run — et
|
|
|
|
|
# le taire ferait croire à une erreur de choix.
|
|
|
|
|
NOT_CONNECTED_NOTE = (
|
|
|
|
|
"This profile is not connected. Disconnecting still clears the state"
|
|
|
|
|
" a dead tunnel left behind."
|
|
|
|
|
)
|
|
|
|
|
|
[FIX] vpn : ne plus demander la route par défaut à qui ne la pose pas
Le formulaire demandait « tout le trafic ? » à un pilote dont le SERVEUR
décide du routage, ne faisait rien de la réponse, et `status` la jugeait
quand même : un ✗ permanent sur un tunnel sain, et un profil annonçant
« tout le trafic » sans l'obtenir.
Un drapeau, sur le modèle de celui du MTU, dit quels pilotes posent cette
route. Les autres ne sont ni interrogés ni jugés, et un drapeau laissé à
vrai n'est plus conservé. L'honorer serait pire qu'inutile : forcer une
route par défaut contre une passerelle en tunnel scindé donne un trou
noir, une passerelle ne routant pas ce qu'elle n'a pas annoncé. Les routes
déclarées, elles, restent honorées — le formulaire le dit.
--- EN ---
The form asked "all traffic?" of a driver whose SERVER decides the
routing, did nothing with the answer, and `status` judged it anyway: a
permanent ✗ on a healthy tunnel, and a profile announcing "all traffic"
without getting it.
A flag, modelled on the MTU one, says which drivers lay that route. The
others are neither asked nor judged, and a flag left true is no longer
kept. Honouring it would be worse than useless: forcing a default route
against a split-tunnel gateway gives a black hole, a gateway not routing
what it never advertised. Declared routes are still honoured — the form
says so.
Assisted-by: Claude Opus 5
2026-09-08 09:41:17 -04:00
|
|
|
# Ce qu'on dit à la place de la question « tout le trafic ? », pour un
|
|
|
|
|
# pilote dont le SERVEUR décide du routage. Nomme l'échappatoire, sans quoi
|
|
|
|
|
# le retrait de la question se lit comme une capacité perdue.
|
|
|
|
|
SERVER_ROUTES_NOTE = (
|
|
|
|
|
"The gateway decides what enters this tunnel. To force a network"
|
|
|
|
|
" through it anyway, add it to the routes above — 0.0.0.0/0 for all."
|
|
|
|
|
)
|
|
|
|
|
|
[ADD] menu vpn : créer un profil, déposer les secrets, monter le tunnel
`vpn.py` savait tout faire en ligne de commande, mais il fallait écrire le
profil JSON à la main et déposer les secrets dans KeePassXC soi-même. Le menu
pose les questions que le pilote choisi déclare, et lui seul : ajouter une
technologie n'ajoute pas une ligne ici.
Le coffre est manipulé EN PROCESSUS — le mot de passe maître est déjà en
mémoire, le redemander à un sous-processus serait une saisie de plus à chaque
geste. Montage, démontage et diagnostic passent au contraire par `vpn.py` en
sous-processus : ils durent, ils parlent, et la sortie en direct est ce qui
rend une montée de tunnel suivable.
Vérifié : 37 tests sur le menu, aucune saisie réelle.
--- EN ---
`vpn.py` could already do everything from the command line, but the JSON
profile had to be written by hand and the secrets filed into KeePassXC by
hand. The menu asks the questions the chosen driver declares, and only those:
adding a technology adds no line here.
The vault is handled IN PROCESS — the master password is already in memory,
and asking a subprocess for it again would be one more entry at every step.
Raising, tearing down and diagnosing go through `vpn.py` as a subprocess
instead: they last, they talk, and live output is what makes a tunnel coming
up followable.
Checked: 37 tests on the menu, no real input.
Assisted-by: Claude Opus 5
2026-09-03 23:43:02 -04:00
|
|
|
MASTER_PASSWORD_WARNING = (
|
|
|
|
|
"The vault MASTER password is stored in the configuration in clear"
|
|
|
|
|
" text. Remove it and type it on demand."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Les technologies se choisissent par LETTRE. Le menu qui précède numérote
|
|
|
|
|
# ses entrées ; une seconde liste numérotée juste après invite à retaper un
|
|
|
|
|
# numéro de menu — et c'est exactement ce qui s'est produit. La lettre dit
|
|
|
|
|
# « autre question ».
|
|
|
|
|
DRIVER_LETTERS = "abcdefghijklmnopqrstuvwxyz"
|
|
|
|
|
|
|
|
|
|
|
[ADD] vpn install : proposer et poser le greffon SSO
openconnect refuse les passerelles qui exigent un navigateur intégré, sur
« No SSO handler » : les distributions le bâtissent sans webview. Rien
n'installait le greffon qui fait cette étape, que le pilote attendait
pourtant — une machine paraissait équipée sans l'être.
La question n'est posée que pour le pilote qui peut s'en servir, et
seulement quand le greffon manque. Son amont est arrêté depuis 2023 :
épingles intenables sur un Python récent, Qt et lxml pris de la
distribution, correctif rejoué à chaque installation. Le venv appartient
à l'utilisateur, non à root, qui n'a ni affichage ni trousseau. Vérifié :
installation depuis rien en 6,5 s, paquets éprouvés sur debian et ubuntu.
--- EN ---
openconnect refuses gateways demanding an embedded browser, on "No SSO
handler": distributions build it without a webview. Nothing installed the
helper that performs that step, which the driver expected all the same —
a machine looked equipped without being so.
The question is asked only for the driver that can use it, and only when
the helper is missing. Its upstream has been unmaintained since 2023:
pins unsatisfiable on a recent Python, Qt and lxml taken from the
distribution, a patch replayed on every install. The venv belongs to the
user, not root, which has neither display nor keyring. Checked: install
from nothing in 6.5 s, package names proven on debian and ubuntu only.
Assisted-by: Claude Opus 5
2026-09-04 07:37:49 -04:00
|
|
|
def _sso_helper_seen():
|
|
|
|
|
"""Le greffon SSO est-il déjà joignable sur cette machine ?
|
|
|
|
|
|
|
|
|
|
Interrogé au PILOTE, pour que le menu et le montage cherchent au même
|
|
|
|
|
endroit : deux recherches distinctes finiraient par diverger, et le
|
|
|
|
|
menu proposerait d'installer ce que le montage trouve déjà.
|
|
|
|
|
"""
|
|
|
|
|
driver_cls = DRIVERS.get("openconnect")
|
|
|
|
|
if driver_cls is None:
|
|
|
|
|
return True
|
|
|
|
|
return bool(driver_cls({"name": "check"}).sso_helper)
|
|
|
|
|
|
|
|
|
|
|
[ADD] menu vpn : créer un profil, déposer les secrets, monter le tunnel
`vpn.py` savait tout faire en ligne de commande, mais il fallait écrire le
profil JSON à la main et déposer les secrets dans KeePassXC soi-même. Le menu
pose les questions que le pilote choisi déclare, et lui seul : ajouter une
technologie n'ajoute pas une ligne ici.
Le coffre est manipulé EN PROCESSUS — le mot de passe maître est déjà en
mémoire, le redemander à un sous-processus serait une saisie de plus à chaque
geste. Montage, démontage et diagnostic passent au contraire par `vpn.py` en
sous-processus : ils durent, ils parlent, et la sortie en direct est ce qui
rend une montée de tunnel suivable.
Vérifié : 37 tests sur le menu, aucune saisie réelle.
--- EN ---
`vpn.py` could already do everything from the command line, but the JSON
profile had to be written by hand and the secrets filed into KeePassXC by
hand. The menu asks the questions the chosen driver declares, and only those:
adding a technology adds no line here.
The vault is handled IN PROCESS — the master password is already in memory,
and asking a subprocess for it again would be one more entry at every step.
Raising, tearing down and diagnosing go through `vpn.py` as a subprocess
instead: they last, they talk, and live output is what makes a tunnel coming
up followable.
Checked: 37 tests on the menu, no real input.
Assisted-by: Claude Opus 5
2026-09-03 23:43:02 -04:00
|
|
|
def match_driver(answer, names):
|
|
|
|
|
"""Le pilote désigné par `answer`.
|
|
|
|
|
|
|
|
|
|
Rend le nom du pilote, "" si rien ne correspond, ou la LISTE des
|
|
|
|
|
candidats quand c'est ambigu — le dire vaut mieux qu'en choisir un.
|
|
|
|
|
|
|
|
|
|
Trois formes, dans cet ordre : la lettre affichée ; le rang, parce que
|
|
|
|
|
quelqu'un tapera un chiffre et qu'il a raison de le faire vu le menu qui
|
|
|
|
|
précède ; et un début de libellé, parce que devant « L2TP/IPsec PSK » on
|
|
|
|
|
tape « L ». « open » désigne deux pilotes : celui-là est refusé en le
|
|
|
|
|
nommant.
|
|
|
|
|
"""
|
|
|
|
|
answer = (answer or "").strip().lower()
|
|
|
|
|
if not answer:
|
|
|
|
|
return ""
|
|
|
|
|
if len(answer) == 1 and answer in DRIVER_LETTERS:
|
|
|
|
|
index = DRIVER_LETTERS.index(answer)
|
|
|
|
|
if index < len(names):
|
|
|
|
|
return names[index]
|
|
|
|
|
if answer.isdigit():
|
|
|
|
|
index = int(answer) - 1
|
|
|
|
|
return names[index] if 0 <= index < len(names) else ""
|
|
|
|
|
matches = [
|
|
|
|
|
name
|
|
|
|
|
for name in names
|
|
|
|
|
if DRIVERS[name].label.lower().startswith(answer)
|
|
|
|
|
or name.startswith(answer)
|
|
|
|
|
]
|
|
|
|
|
if len(matches) == 1:
|
|
|
|
|
return matches[0]
|
|
|
|
|
return matches or ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class VpnMenuMixin:
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
# Menu
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
def prompt_execute_vpn(self):
|
|
|
|
|
print(f"🔐 {t('VPN tunnels: connect, profiles, vault secrets')}")
|
|
|
|
|
choices = [
|
|
|
|
|
{"section": t("Connection")},
|
|
|
|
|
{"prompt_description": t("VPN - Connect a profile")},
|
|
|
|
|
{"prompt_description": t("VPN - Disconnect a profile")},
|
|
|
|
|
{"prompt_description": t("VPN - Status and diagnosis")},
|
|
|
|
|
{"section": t("Profiles & secrets")},
|
[ADD] vpn openconnect : groupe d'URL, SSO délégué, mot de passe borné
Deux mécanismes désignent un service sur un concentrateur : un chemin
d'URL, une valeur de menu déroulant. Les confondre rend le formulaire d'un
AUTRE service — identifiants justes refusés, rien ne désignant le groupe.
Et certains ne comparent que les N premiers caractères du mot de passe,
que le profil déclare désormais sans jamais rien tronquer.
Une passerelle qui exige un navigateur intégré arrête openconnect sur
« No SSO handler », les distributions le bâtissant sans webview. Un greffon
fait l'étape web et rend un cookie ; le pilote monte lui-même, et le profil
garde son interface, son état et son diagnostic.
Vérifié : 249 tests unitaires, un tunnel monté contre une passerelle SAML.
--- EN ---
Two mechanisms designate a service on one concentrator: a URL path, and a
dropdown value. Confusing them hands over ANOTHER service's login form —
correct credentials refused, with nothing pointing at the group. And some
compare only the first N characters of the password, which the profile now
declares without ever truncating anything.
A gateway demanding an embedded browser stops openconnect on "No SSO
handler", distributions building it without a webview. A helper does the
web step and returns a cookie; the driver mounts the tunnel itself, so the
profile keeps its interface, its state and its diagnosis.
Checked: 249 unit tests, and one tunnel mounted against a SAML gateway.
Assisted-by: Claude Opus 5
2026-09-04 05:52:09 -04:00
|
|
|
{
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
"VPN - Create a profile from a site preset"
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
"VPN - Import an AnyConnect profile (.xml)"
|
|
|
|
|
)
|
|
|
|
|
},
|
[ADD] menu vpn : créer un profil, déposer les secrets, monter le tunnel
`vpn.py` savait tout faire en ligne de commande, mais il fallait écrire le
profil JSON à la main et déposer les secrets dans KeePassXC soi-même. Le menu
pose les questions que le pilote choisi déclare, et lui seul : ajouter une
technologie n'ajoute pas une ligne ici.
Le coffre est manipulé EN PROCESSUS — le mot de passe maître est déjà en
mémoire, le redemander à un sous-processus serait une saisie de plus à chaque
geste. Montage, démontage et diagnostic passent au contraire par `vpn.py` en
sous-processus : ils durent, ils parlent, et la sortie en direct est ce qui
rend une montée de tunnel suivable.
Vérifié : 37 tests sur le menu, aucune saisie réelle.
--- EN ---
`vpn.py` could already do everything from the command line, but the JSON
profile had to be written by hand and the secrets filed into KeePassXC by
hand. The menu asks the questions the chosen driver declares, and only those:
adding a technology adds no line here.
The vault is handled IN PROCESS — the master password is already in memory,
and asking a subprocess for it again would be one more entry at every step.
Raising, tearing down and diagnosing go through `vpn.py` as a subprocess
instead: they last, they talk, and live output is what makes a tunnel coming
up followable.
Checked: 37 tests on the menu, no real input.
Assisted-by: Claude Opus 5
2026-09-03 23:43:02 -04:00
|
|
|
{"prompt_description": t("VPN - Add or edit a profile")},
|
|
|
|
|
{"prompt_description": t("VPN - Store secrets in the vault")},
|
|
|
|
|
{
|
|
|
|
|
"prompt_description": t(
|
|
|
|
|
"VPN - Show the rendered configuration (dry-run)"
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
{"prompt_description": t("VPN - Delete a profile")},
|
|
|
|
|
{"section": t("Host")},
|
|
|
|
|
{"prompt_description": t("VPN - Install the client packages")},
|
|
|
|
|
{"prompt_description": t("VPN - What can this machine do?")},
|
|
|
|
|
]
|
|
|
|
|
help_info = self.fill_help_info(choices)
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
status = click.prompt(help_info)
|
|
|
|
|
print()
|
|
|
|
|
if status == "0":
|
|
|
|
|
return False
|
|
|
|
|
elif status == "1":
|
|
|
|
|
self._vpn_connect()
|
|
|
|
|
elif status == "2":
|
|
|
|
|
self._vpn_disconnect()
|
|
|
|
|
elif status == "3":
|
|
|
|
|
self._vpn_diagnose()
|
|
|
|
|
elif status == "4":
|
[ADD] vpn openconnect : groupe d'URL, SSO délégué, mot de passe borné
Deux mécanismes désignent un service sur un concentrateur : un chemin
d'URL, une valeur de menu déroulant. Les confondre rend le formulaire d'un
AUTRE service — identifiants justes refusés, rien ne désignant le groupe.
Et certains ne comparent que les N premiers caractères du mot de passe,
que le profil déclare désormais sans jamais rien tronquer.
Une passerelle qui exige un navigateur intégré arrête openconnect sur
« No SSO handler », les distributions le bâtissant sans webview. Un greffon
fait l'étape web et rend un cookie ; le pilote monte lui-même, et le profil
garde son interface, son état et son diagnostic.
Vérifié : 249 tests unitaires, un tunnel monté contre une passerelle SAML.
--- EN ---
Two mechanisms designate a service on one concentrator: a URL path, and a
dropdown value. Confusing them hands over ANOTHER service's login form —
correct credentials refused, with nothing pointing at the group. And some
compare only the first N characters of the password, which the profile now
declares without ever truncating anything.
A gateway demanding an embedded browser stops openconnect on "No SSO
handler", distributions building it without a webview. A helper does the
web step and returns a cookie; the driver mounts the tunnel itself, so the
profile keeps its interface, its state and its diagnosis.
Checked: 249 unit tests, and one tunnel mounted against a SAML gateway.
Assisted-by: Claude Opus 5
2026-09-04 05:52:09 -04:00
|
|
|
self._vpn_from_preset()
|
[ADD] menu vpn : créer un profil, déposer les secrets, monter le tunnel
`vpn.py` savait tout faire en ligne de commande, mais il fallait écrire le
profil JSON à la main et déposer les secrets dans KeePassXC soi-même. Le menu
pose les questions que le pilote choisi déclare, et lui seul : ajouter une
technologie n'ajoute pas une ligne ici.
Le coffre est manipulé EN PROCESSUS — le mot de passe maître est déjà en
mémoire, le redemander à un sous-processus serait une saisie de plus à chaque
geste. Montage, démontage et diagnostic passent au contraire par `vpn.py` en
sous-processus : ils durent, ils parlent, et la sortie en direct est ce qui
rend une montée de tunnel suivable.
Vérifié : 37 tests sur le menu, aucune saisie réelle.
--- EN ---
`vpn.py` could already do everything from the command line, but the JSON
profile had to be written by hand and the secrets filed into KeePassXC by
hand. The menu asks the questions the chosen driver declares, and only those:
adding a technology adds no line here.
The vault is handled IN PROCESS — the master password is already in memory,
and asking a subprocess for it again would be one more entry at every step.
Raising, tearing down and diagnosing go through `vpn.py` as a subprocess
instead: they last, they talk, and live output is what makes a tunnel coming
up followable.
Checked: 37 tests on the menu, no real input.
Assisted-by: Claude Opus 5
2026-09-03 23:43:02 -04:00
|
|
|
elif status == "5":
|
[ADD] vpn openconnect : groupe d'URL, SSO délégué, mot de passe borné
Deux mécanismes désignent un service sur un concentrateur : un chemin
d'URL, une valeur de menu déroulant. Les confondre rend le formulaire d'un
AUTRE service — identifiants justes refusés, rien ne désignant le groupe.
Et certains ne comparent que les N premiers caractères du mot de passe,
que le profil déclare désormais sans jamais rien tronquer.
Une passerelle qui exige un navigateur intégré arrête openconnect sur
« No SSO handler », les distributions le bâtissant sans webview. Un greffon
fait l'étape web et rend un cookie ; le pilote monte lui-même, et le profil
garde son interface, son état et son diagnostic.
Vérifié : 249 tests unitaires, un tunnel monté contre une passerelle SAML.
--- EN ---
Two mechanisms designate a service on one concentrator: a URL path, and a
dropdown value. Confusing them hands over ANOTHER service's login form —
correct credentials refused, with nothing pointing at the group. And some
compare only the first N characters of the password, which the profile now
declares without ever truncating anything.
A gateway demanding an embedded browser stops openconnect on "No SSO
handler", distributions building it without a webview. A helper does the
web step and returns a cookie; the driver mounts the tunnel itself, so the
profile keeps its interface, its state and its diagnosis.
Checked: 249 unit tests, and one tunnel mounted against a SAML gateway.
Assisted-by: Claude Opus 5
2026-09-04 05:52:09 -04:00
|
|
|
self._vpn_import_anyconnect()
|
[ADD] menu vpn : créer un profil, déposer les secrets, monter le tunnel
`vpn.py` savait tout faire en ligne de commande, mais il fallait écrire le
profil JSON à la main et déposer les secrets dans KeePassXC soi-même. Le menu
pose les questions que le pilote choisi déclare, et lui seul : ajouter une
technologie n'ajoute pas une ligne ici.
Le coffre est manipulé EN PROCESSUS — le mot de passe maître est déjà en
mémoire, le redemander à un sous-processus serait une saisie de plus à chaque
geste. Montage, démontage et diagnostic passent au contraire par `vpn.py` en
sous-processus : ils durent, ils parlent, et la sortie en direct est ce qui
rend une montée de tunnel suivable.
Vérifié : 37 tests sur le menu, aucune saisie réelle.
--- EN ---
`vpn.py` could already do everything from the command line, but the JSON
profile had to be written by hand and the secrets filed into KeePassXC by
hand. The menu asks the questions the chosen driver declares, and only those:
adding a technology adds no line here.
The vault is handled IN PROCESS — the master password is already in memory,
and asking a subprocess for it again would be one more entry at every step.
Raising, tearing down and diagnosing go through `vpn.py` as a subprocess
instead: they last, they talk, and live output is what makes a tunnel coming
up followable.
Checked: 37 tests on the menu, no real input.
Assisted-by: Claude Opus 5
2026-09-03 23:43:02 -04:00
|
|
|
elif status == "6":
|
[ADD] vpn openconnect : groupe d'URL, SSO délégué, mot de passe borné
Deux mécanismes désignent un service sur un concentrateur : un chemin
d'URL, une valeur de menu déroulant. Les confondre rend le formulaire d'un
AUTRE service — identifiants justes refusés, rien ne désignant le groupe.
Et certains ne comparent que les N premiers caractères du mot de passe,
que le profil déclare désormais sans jamais rien tronquer.
Une passerelle qui exige un navigateur intégré arrête openconnect sur
« No SSO handler », les distributions le bâtissant sans webview. Un greffon
fait l'étape web et rend un cookie ; le pilote monte lui-même, et le profil
garde son interface, son état et son diagnostic.
Vérifié : 249 tests unitaires, un tunnel monté contre une passerelle SAML.
--- EN ---
Two mechanisms designate a service on one concentrator: a URL path, and a
dropdown value. Confusing them hands over ANOTHER service's login form —
correct credentials refused, with nothing pointing at the group. And some
compare only the first N characters of the password, which the profile now
declares without ever truncating anything.
A gateway demanding an embedded browser stops openconnect on "No SSO
handler", distributions building it without a webview. A helper does the
web step and returns a cookie; the driver mounts the tunnel itself, so the
profile keeps its interface, its state and its diagnosis.
Checked: 249 unit tests, and one tunnel mounted against a SAML gateway.
Assisted-by: Claude Opus 5
2026-09-04 05:52:09 -04:00
|
|
|
self._vpn_edit_profile()
|
[ADD] menu vpn : créer un profil, déposer les secrets, monter le tunnel
`vpn.py` savait tout faire en ligne de commande, mais il fallait écrire le
profil JSON à la main et déposer les secrets dans KeePassXC soi-même. Le menu
pose les questions que le pilote choisi déclare, et lui seul : ajouter une
technologie n'ajoute pas une ligne ici.
Le coffre est manipulé EN PROCESSUS — le mot de passe maître est déjà en
mémoire, le redemander à un sous-processus serait une saisie de plus à chaque
geste. Montage, démontage et diagnostic passent au contraire par `vpn.py` en
sous-processus : ils durent, ils parlent, et la sortie en direct est ce qui
rend une montée de tunnel suivable.
Vérifié : 37 tests sur le menu, aucune saisie réelle.
--- EN ---
`vpn.py` could already do everything from the command line, but the JSON
profile had to be written by hand and the secrets filed into KeePassXC by
hand. The menu asks the questions the chosen driver declares, and only those:
adding a technology adds no line here.
The vault is handled IN PROCESS — the master password is already in memory,
and asking a subprocess for it again would be one more entry at every step.
Raising, tearing down and diagnosing go through `vpn.py` as a subprocess
instead: they last, they talk, and live output is what makes a tunnel coming
up followable.
Checked: 37 tests on the menu, no real input.
Assisted-by: Claude Opus 5
2026-09-03 23:43:02 -04:00
|
|
|
elif status == "7":
|
[ADD] vpn openconnect : groupe d'URL, SSO délégué, mot de passe borné
Deux mécanismes désignent un service sur un concentrateur : un chemin
d'URL, une valeur de menu déroulant. Les confondre rend le formulaire d'un
AUTRE service — identifiants justes refusés, rien ne désignant le groupe.
Et certains ne comparent que les N premiers caractères du mot de passe,
que le profil déclare désormais sans jamais rien tronquer.
Une passerelle qui exige un navigateur intégré arrête openconnect sur
« No SSO handler », les distributions le bâtissant sans webview. Un greffon
fait l'étape web et rend un cookie ; le pilote monte lui-même, et le profil
garde son interface, son état et son diagnostic.
Vérifié : 249 tests unitaires, un tunnel monté contre une passerelle SAML.
--- EN ---
Two mechanisms designate a service on one concentrator: a URL path, and a
dropdown value. Confusing them hands over ANOTHER service's login form —
correct credentials refused, with nothing pointing at the group. And some
compare only the first N characters of the password, which the profile now
declares without ever truncating anything.
A gateway demanding an embedded browser stops openconnect on "No SSO
handler", distributions building it without a webview. A helper does the
web step and returns a cookie; the driver mounts the tunnel itself, so the
profile keeps its interface, its state and its diagnosis.
Checked: 249 unit tests, and one tunnel mounted against a SAML gateway.
Assisted-by: Claude Opus 5
2026-09-04 05:52:09 -04:00
|
|
|
self._vpn_store_secrets()
|
[ADD] menu vpn : créer un profil, déposer les secrets, monter le tunnel
`vpn.py` savait tout faire en ligne de commande, mais il fallait écrire le
profil JSON à la main et déposer les secrets dans KeePassXC soi-même. Le menu
pose les questions que le pilote choisi déclare, et lui seul : ajouter une
technologie n'ajoute pas une ligne ici.
Le coffre est manipulé EN PROCESSUS — le mot de passe maître est déjà en
mémoire, le redemander à un sous-processus serait une saisie de plus à chaque
geste. Montage, démontage et diagnostic passent au contraire par `vpn.py` en
sous-processus : ils durent, ils parlent, et la sortie en direct est ce qui
rend une montée de tunnel suivable.
Vérifié : 37 tests sur le menu, aucune saisie réelle.
--- EN ---
`vpn.py` could already do everything from the command line, but the JSON
profile had to be written by hand and the secrets filed into KeePassXC by
hand. The menu asks the questions the chosen driver declares, and only those:
adding a technology adds no line here.
The vault is handled IN PROCESS — the master password is already in memory,
and asking a subprocess for it again would be one more entry at every step.
Raising, tearing down and diagnosing go through `vpn.py` as a subprocess
instead: they last, they talk, and live output is what makes a tunnel coming
up followable.
Checked: 37 tests on the menu, no real input.
Assisted-by: Claude Opus 5
2026-09-03 23:43:02 -04:00
|
|
|
elif status == "8":
|
[ADD] vpn openconnect : groupe d'URL, SSO délégué, mot de passe borné
Deux mécanismes désignent un service sur un concentrateur : un chemin
d'URL, une valeur de menu déroulant. Les confondre rend le formulaire d'un
AUTRE service — identifiants justes refusés, rien ne désignant le groupe.
Et certains ne comparent que les N premiers caractères du mot de passe,
que le profil déclare désormais sans jamais rien tronquer.
Une passerelle qui exige un navigateur intégré arrête openconnect sur
« No SSO handler », les distributions le bâtissant sans webview. Un greffon
fait l'étape web et rend un cookie ; le pilote monte lui-même, et le profil
garde son interface, son état et son diagnostic.
Vérifié : 249 tests unitaires, un tunnel monté contre une passerelle SAML.
--- EN ---
Two mechanisms designate a service on one concentrator: a URL path, and a
dropdown value. Confusing them hands over ANOTHER service's login form —
correct credentials refused, with nothing pointing at the group. And some
compare only the first N characters of the password, which the profile now
declares without ever truncating anything.
A gateway demanding an embedded browser stops openconnect on "No SSO
handler", distributions building it without a webview. A helper does the
web step and returns a cookie; the driver mounts the tunnel itself, so the
profile keeps its interface, its state and its diagnosis.
Checked: 249 unit tests, and one tunnel mounted against a SAML gateway.
Assisted-by: Claude Opus 5
2026-09-04 05:52:09 -04:00
|
|
|
self._vpn_show_config()
|
[ADD] menu vpn : créer un profil, déposer les secrets, monter le tunnel
`vpn.py` savait tout faire en ligne de commande, mais il fallait écrire le
profil JSON à la main et déposer les secrets dans KeePassXC soi-même. Le menu
pose les questions que le pilote choisi déclare, et lui seul : ajouter une
technologie n'ajoute pas une ligne ici.
Le coffre est manipulé EN PROCESSUS — le mot de passe maître est déjà en
mémoire, le redemander à un sous-processus serait une saisie de plus à chaque
geste. Montage, démontage et diagnostic passent au contraire par `vpn.py` en
sous-processus : ils durent, ils parlent, et la sortie en direct est ce qui
rend une montée de tunnel suivable.
Vérifié : 37 tests sur le menu, aucune saisie réelle.
--- EN ---
`vpn.py` could already do everything from the command line, but the JSON
profile had to be written by hand and the secrets filed into KeePassXC by
hand. The menu asks the questions the chosen driver declares, and only those:
adding a technology adds no line here.
The vault is handled IN PROCESS — the master password is already in memory,
and asking a subprocess for it again would be one more entry at every step.
Raising, tearing down and diagnosing go through `vpn.py` as a subprocess
instead: they last, they talk, and live output is what makes a tunnel coming
up followable.
Checked: 37 tests on the menu, no real input.
Assisted-by: Claude Opus 5
2026-09-03 23:43:02 -04:00
|
|
|
elif status == "9":
|
[ADD] vpn openconnect : groupe d'URL, SSO délégué, mot de passe borné
Deux mécanismes désignent un service sur un concentrateur : un chemin
d'URL, une valeur de menu déroulant. Les confondre rend le formulaire d'un
AUTRE service — identifiants justes refusés, rien ne désignant le groupe.
Et certains ne comparent que les N premiers caractères du mot de passe,
que le profil déclare désormais sans jamais rien tronquer.
Une passerelle qui exige un navigateur intégré arrête openconnect sur
« No SSO handler », les distributions le bâtissant sans webview. Un greffon
fait l'étape web et rend un cookie ; le pilote monte lui-même, et le profil
garde son interface, son état et son diagnostic.
Vérifié : 249 tests unitaires, un tunnel monté contre une passerelle SAML.
--- EN ---
Two mechanisms designate a service on one concentrator: a URL path, and a
dropdown value. Confusing them hands over ANOTHER service's login form —
correct credentials refused, with nothing pointing at the group. And some
compare only the first N characters of the password, which the profile now
declares without ever truncating anything.
A gateway demanding an embedded browser stops openconnect on "No SSO
handler", distributions building it without a webview. A helper does the
web step and returns a cookie; the driver mounts the tunnel itself, so the
profile keeps its interface, its state and its diagnosis.
Checked: 249 unit tests, and one tunnel mounted against a SAML gateway.
Assisted-by: Claude Opus 5
2026-09-04 05:52:09 -04:00
|
|
|
self._vpn_delete_profile()
|
|
|
|
|
elif status == "10":
|
|
|
|
|
self._vpn_install()
|
|
|
|
|
elif status == "11":
|
[ADD] menu vpn : créer un profil, déposer les secrets, monter le tunnel
`vpn.py` savait tout faire en ligne de commande, mais il fallait écrire le
profil JSON à la main et déposer les secrets dans KeePassXC soi-même. Le menu
pose les questions que le pilote choisi déclare, et lui seul : ajouter une
technologie n'ajoute pas une ligne ici.
Le coffre est manipulé EN PROCESSUS — le mot de passe maître est déjà en
mémoire, le redemander à un sous-processus serait une saisie de plus à chaque
geste. Montage, démontage et diagnostic passent au contraire par `vpn.py` en
sous-processus : ils durent, ils parlent, et la sortie en direct est ce qui
rend une montée de tunnel suivable.
Vérifié : 37 tests sur le menu, aucune saisie réelle.
--- EN ---
`vpn.py` could already do everything from the command line, but the JSON
profile had to be written by hand and the secrets filed into KeePassXC by
hand. The menu asks the questions the chosen driver declares, and only those:
adding a technology adds no line here.
The vault is handled IN PROCESS — the master password is already in memory,
and asking a subprocess for it again would be one more entry at every step.
Raising, tearing down and diagnosing go through `vpn.py` as a subprocess
instead: they last, they talk, and live output is what makes a tunnel coming
up followable.
Checked: 37 tests on the menu, no real input.
Assisted-by: Claude Opus 5
2026-09-03 23:43:02 -04:00
|
|
|
self._vpn_check()
|
|
|
|
|
else:
|
|
|
|
|
print(t("Command not found !"))
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
# Actions déléguées au CLI
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
def _vpn_cli(self, arguments, secrets_env=None):
|
|
|
|
|
self.execute.exec_command_live(
|
|
|
|
|
f"{VPN_CLI} {arguments}",
|
|
|
|
|
source_erplibre=False,
|
|
|
|
|
new_env=secrets_env or None,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def _vpn_secrets_env(self, name):
|
|
|
|
|
"""Secrets du profil, prêts pour l'environnement du sous-processus.
|
|
|
|
|
|
|
|
|
|
Le coffre est déjà ouvert ici : le faire rouvrir par `vpn.py` ferait
|
|
|
|
|
retaper le mot de passe maître deux fois par connexion, puisqu'un
|
|
|
|
|
essai à blanc précède le montage. Rend {} quand le coffre n'est pas
|
|
|
|
|
joignable — `vpn.py` demandera alors lui-même, et le dira.
|
|
|
|
|
"""
|
|
|
|
|
profile = profiles.load(name)
|
|
|
|
|
driver_cls = get_driver(profile["driver"]) if profile else None
|
|
|
|
|
if driver_cls is None or not driver_cls.secret_fields:
|
|
|
|
|
return {}
|
|
|
|
|
fields = tuple(key for key, _, _ in driver_cls.secret_fields)
|
|
|
|
|
vault = VpnVault(self.config_file, self.kdbx_manager)
|
|
|
|
|
if not vault.vault_path():
|
|
|
|
|
return {}
|
|
|
|
|
try:
|
|
|
|
|
values = vault.read(profiles.secret_title(name), fields=fields)
|
|
|
|
|
except VaultError as error:
|
|
|
|
|
print(f"! {error}")
|
|
|
|
|
return {}
|
|
|
|
|
return secrets_to_env({key: values.get(key, "") for key in fields})
|
|
|
|
|
|
|
|
|
|
def _vpn_connect(self):
|
|
|
|
|
name = self._vpn_select_profile()
|
|
|
|
|
if not name:
|
|
|
|
|
return
|
[FIX] vpn : juger un profil monté sur son interface, et le marquer
Le verdict se fondait sur le seul fichier d'état, que RIEN n'efface quand
un tunnel meurt sans passer par « down » — machine redémarrée, processus
tué, session expirée. Un état laissé derrière était annoncé monté sur
l'écran même qui déclarait le processus mort et l'interface absente.
L'interface arbitre désormais : son nom vient de l'état s'il y en a un,
sinon du pilote pour ceux qui la nomment d'avance, si bien qu'un tunnel
monté hors de l'outil est vu aussi. Sshuttle n'en crée aucune, là le
processus reste seul juge. La liste des profils le marque, connecter un
profil déjà monté demande confirmation, et déconnecter un profil déjà
tombé le dit sans l'empêcher : « down » nettoie l'état laissé.
--- EN ---
The verdict rested on the state file alone, which NOTHING clears when a
tunnel dies without going through "down" — machine rebooted, process
killed, session expired. A leftover state was reported as mounted on the
very screen declaring the process gone and the interface absent.
The interface now arbitrates: its name comes from the state when there is
one, otherwise from drivers that name it in advance, so a tunnel brought
up outside the tool is seen too. Sshuttle creates none, and there the
process stays the only judge. The profile list marks it, connecting an
already-mounted profile asks first, and disconnecting an already-down one
says so without refusing: "down" clears the leftover state.
Assisted-by: Claude Opus 5
2026-09-07 23:27:35 -04:00
|
|
|
# Remonter un tunnel qui tient rejoue toute l'authentification —
|
|
|
|
|
# jusqu'à un formulaire web et un second facteur — pour aboutir à
|
|
|
|
|
# une interface qui existait déjà.
|
|
|
|
|
if self._vpn_is_up(profiles.load(name)):
|
|
|
|
|
print(f"\n! {t('This profile is already connected.')}")
|
|
|
|
|
if not self._is_yes(input(f"{t('Connect it again? (y/N)')} : ")):
|
|
|
|
|
return
|
[ADD] menu vpn : créer un profil, déposer les secrets, monter le tunnel
`vpn.py` savait tout faire en ligne de commande, mais il fallait écrire le
profil JSON à la main et déposer les secrets dans KeePassXC soi-même. Le menu
pose les questions que le pilote choisi déclare, et lui seul : ajouter une
technologie n'ajoute pas une ligne ici.
Le coffre est manipulé EN PROCESSUS — le mot de passe maître est déjà en
mémoire, le redemander à un sous-processus serait une saisie de plus à chaque
geste. Montage, démontage et diagnostic passent au contraire par `vpn.py` en
sous-processus : ils durent, ils parlent, et la sortie en direct est ce qui
rend une montée de tunnel suivable.
Vérifié : 37 tests sur le menu, aucune saisie réelle.
--- EN ---
`vpn.py` could already do everything from the command line, but the JSON
profile had to be written by hand and the secrets filed into KeePassXC by
hand. The menu asks the questions the chosen driver declares, and only those:
adding a technology adds no line here.
The vault is handled IN PROCESS — the master password is already in memory,
and asking a subprocess for it again would be one more entry at every step.
Raising, tearing down and diagnosing go through `vpn.py` as a subprocess
instead: they last, they talk, and live output is what makes a tunnel coming
up followable.
Checked: 37 tests on the menu, no real input.
Assisted-by: Claude Opus 5
2026-09-03 23:43:02 -04:00
|
|
|
# Lu UNE fois pour les deux exécutions qui suivent.
|
|
|
|
|
secrets_env = self._vpn_secrets_env(name)
|
|
|
|
|
# Le plan d'abord, l'exécution ensuite : monter un tunnel réécrit
|
|
|
|
|
# /etc/ipsec.conf et la table de routage. Le voir avant coûte une
|
|
|
|
|
# touche et évite de découvrir une faute de frappe dans un journal.
|
|
|
|
|
self._vpn_cli(f"up --profile {name} --dry-run", secrets_env)
|
|
|
|
|
if not self._is_yes(input(f"\n{t('Run this plan? (y/N): ')}")):
|
|
|
|
|
return
|
|
|
|
|
self._vpn_cli(f"up --profile {name}", secrets_env)
|
|
|
|
|
|
|
|
|
|
def _vpn_disconnect(self):
|
|
|
|
|
name = self._vpn_select_profile()
|
[FIX] vpn : juger un profil monté sur son interface, et le marquer
Le verdict se fondait sur le seul fichier d'état, que RIEN n'efface quand
un tunnel meurt sans passer par « down » — machine redémarrée, processus
tué, session expirée. Un état laissé derrière était annoncé monté sur
l'écran même qui déclarait le processus mort et l'interface absente.
L'interface arbitre désormais : son nom vient de l'état s'il y en a un,
sinon du pilote pour ceux qui la nomment d'avance, si bien qu'un tunnel
monté hors de l'outil est vu aussi. Sshuttle n'en crée aucune, là le
processus reste seul juge. La liste des profils le marque, connecter un
profil déjà monté demande confirmation, et déconnecter un profil déjà
tombé le dit sans l'empêcher : « down » nettoie l'état laissé.
--- EN ---
The verdict rested on the state file alone, which NOTHING clears when a
tunnel dies without going through "down" — machine rebooted, process
killed, session expired. A leftover state was reported as mounted on the
very screen declaring the process gone and the interface absent.
The interface now arbitrates: its name comes from the state when there is
one, otherwise from drivers that name it in advance, so a tunnel brought
up outside the tool is seen too. Sshuttle creates none, and there the
process stays the only judge. The profile list marks it, connecting an
already-mounted profile asks first, and disconnecting an already-down one
says so without refusing: "down" clears the leftover state.
Assisted-by: Claude Opus 5
2026-09-07 23:27:35 -04:00
|
|
|
if not name:
|
|
|
|
|
return
|
|
|
|
|
# « down » reste utile sur un profil déjà tombé : c'est lui qui
|
|
|
|
|
# efface l'état laissé dans /run par un tunnel mort sans lui. On le
|
|
|
|
|
# dit, on ne l'empêche pas.
|
|
|
|
|
if not self._vpn_is_up(profiles.load(name)):
|
|
|
|
|
print(f"\n! {t(NOT_CONNECTED_NOTE)}")
|
|
|
|
|
self._vpn_cli(f"down --profile {name}")
|
[ADD] menu vpn : créer un profil, déposer les secrets, monter le tunnel
`vpn.py` savait tout faire en ligne de commande, mais il fallait écrire le
profil JSON à la main et déposer les secrets dans KeePassXC soi-même. Le menu
pose les questions que le pilote choisi déclare, et lui seul : ajouter une
technologie n'ajoute pas une ligne ici.
Le coffre est manipulé EN PROCESSUS — le mot de passe maître est déjà en
mémoire, le redemander à un sous-processus serait une saisie de plus à chaque
geste. Montage, démontage et diagnostic passent au contraire par `vpn.py` en
sous-processus : ils durent, ils parlent, et la sortie en direct est ce qui
rend une montée de tunnel suivable.
Vérifié : 37 tests sur le menu, aucune saisie réelle.
--- EN ---
`vpn.py` could already do everything from the command line, but the JSON
profile had to be written by hand and the secrets filed into KeePassXC by
hand. The menu asks the questions the chosen driver declares, and only those:
adding a technology adds no line here.
The vault is handled IN PROCESS — the master password is already in memory,
and asking a subprocess for it again would be one more entry at every step.
Raising, tearing down and diagnosing go through `vpn.py` as a subprocess
instead: they last, they talk, and live output is what makes a tunnel coming
up followable.
Checked: 37 tests on the menu, no real input.
Assisted-by: Claude Opus 5
2026-09-03 23:43:02 -04:00
|
|
|
|
|
|
|
|
def _vpn_diagnose(self):
|
|
|
|
|
name = self._vpn_select_profile()
|
|
|
|
|
if name:
|
|
|
|
|
self._vpn_cli(f"diagnose --profile {name}")
|
|
|
|
|
|
|
|
|
|
def _vpn_show_config(self):
|
|
|
|
|
name = self._vpn_select_profile()
|
|
|
|
|
if name:
|
|
|
|
|
self._vpn_cli(
|
|
|
|
|
f"up --profile {name} --dry-run",
|
|
|
|
|
self._vpn_secrets_env(name),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def _vpn_check(self):
|
|
|
|
|
self._vpn_cli("check")
|
|
|
|
|
|
|
|
|
|
def _vpn_install(self):
|
|
|
|
|
driver_cls = self._vpn_pick_driver(None)
|
|
|
|
|
if driver_cls is None:
|
|
|
|
|
return
|
[ADD] vpn install : proposer et poser le greffon SSO
openconnect refuse les passerelles qui exigent un navigateur intégré, sur
« No SSO handler » : les distributions le bâtissent sans webview. Rien
n'installait le greffon qui fait cette étape, que le pilote attendait
pourtant — une machine paraissait équipée sans l'être.
La question n'est posée que pour le pilote qui peut s'en servir, et
seulement quand le greffon manque. Son amont est arrêté depuis 2023 :
épingles intenables sur un Python récent, Qt et lxml pris de la
distribution, correctif rejoué à chaque installation. Le venv appartient
à l'utilisateur, non à root, qui n'a ni affichage ni trousseau. Vérifié :
installation depuis rien en 6,5 s, paquets éprouvés sur debian et ubuntu.
--- EN ---
openconnect refuses gateways demanding an embedded browser, on "No SSO
handler": distributions build it without a webview. Nothing installed the
helper that performs that step, which the driver expected all the same —
a machine looked equipped without being so.
The question is asked only for the driver that can use it, and only when
the helper is missing. Its upstream has been unmaintained since 2023:
pins unsatisfiable on a recent Python, Qt and lxml taken from the
distribution, a patch replayed on every install. The venv belongs to the
user, not root, which has neither display nor keyring. Checked: install
from nothing in 6.5 s, package names proven on debian and ubuntu only.
Assisted-by: Claude Opus 5
2026-09-04 07:37:49 -04:00
|
|
|
# La question n'est posée que pour le pilote qui peut s'en servir,
|
|
|
|
|
# et seulement si le greffon n'est pas déjà là : proposer
|
|
|
|
|
# d'installer ce qui est installé fait douter de ce qu'on lit.
|
|
|
|
|
options = ""
|
|
|
|
|
if driver_cls is DRIVERS.get("openconnect") and not _sso_helper_seen():
|
|
|
|
|
print(f"\n{t(SSO_HELPER_NOTE)}")
|
|
|
|
|
if self._is_yes(input(f"{t('Install it as well? (y/N)')} : ")):
|
|
|
|
|
options = " --with-sso"
|
[ADD] menu vpn : créer un profil, déposer les secrets, monter le tunnel
`vpn.py` savait tout faire en ligne de commande, mais il fallait écrire le
profil JSON à la main et déposer les secrets dans KeePassXC soi-même. Le menu
pose les questions que le pilote choisi déclare, et lui seul : ajouter une
technologie n'ajoute pas une ligne ici.
Le coffre est manipulé EN PROCESSUS — le mot de passe maître est déjà en
mémoire, le redemander à un sous-processus serait une saisie de plus à chaque
geste. Montage, démontage et diagnostic passent au contraire par `vpn.py` en
sous-processus : ils durent, ils parlent, et la sortie en direct est ce qui
rend une montée de tunnel suivable.
Vérifié : 37 tests sur le menu, aucune saisie réelle.
--- EN ---
`vpn.py` could already do everything from the command line, but the JSON
profile had to be written by hand and the secrets filed into KeePassXC by
hand. The menu asks the questions the chosen driver declares, and only those:
adding a technology adds no line here.
The vault is handled IN PROCESS — the master password is already in memory,
and asking a subprocess for it again would be one more entry at every step.
Raising, tearing down and diagnosing go through `vpn.py` as a subprocess
instead: they last, they talk, and live output is what makes a tunnel coming
up followable.
Checked: 37 tests on the menu, no real input.
Assisted-by: Claude Opus 5
2026-09-03 23:43:02 -04:00
|
|
|
print(f"\n{t('The installation requires sudo.')}")
|
[ADD] vpn install : proposer et poser le greffon SSO
openconnect refuse les passerelles qui exigent un navigateur intégré, sur
« No SSO handler » : les distributions le bâtissent sans webview. Rien
n'installait le greffon qui fait cette étape, que le pilote attendait
pourtant — une machine paraissait équipée sans l'être.
La question n'est posée que pour le pilote qui peut s'en servir, et
seulement quand le greffon manque. Son amont est arrêté depuis 2023 :
épingles intenables sur un Python récent, Qt et lxml pris de la
distribution, correctif rejoué à chaque installation. Le venv appartient
à l'utilisateur, non à root, qui n'a ni affichage ni trousseau. Vérifié :
installation depuis rien en 6,5 s, paquets éprouvés sur debian et ubuntu.
--- EN ---
openconnect refuses gateways demanding an embedded browser, on "No SSO
handler": distributions build it without a webview. Nothing installed the
helper that performs that step, which the driver expected all the same —
a machine looked equipped without being so.
The question is asked only for the driver that can use it, and only when
the helper is missing. Its upstream has been unmaintained since 2023:
pins unsatisfiable on a recent Python, Qt and lxml taken from the
distribution, a patch replayed on every install. The venv belongs to the
user, not root, which has neither display nor keyring. Checked: install
from nothing in 6.5 s, package names proven on debian and ubuntu only.
Assisted-by: Claude Opus 5
2026-09-04 07:37:49 -04:00
|
|
|
self._vpn_cli(f"install --driver {driver_cls.name}{options}")
|
[ADD] menu vpn : créer un profil, déposer les secrets, monter le tunnel
`vpn.py` savait tout faire en ligne de commande, mais il fallait écrire le
profil JSON à la main et déposer les secrets dans KeePassXC soi-même. Le menu
pose les questions que le pilote choisi déclare, et lui seul : ajouter une
technologie n'ajoute pas une ligne ici.
Le coffre est manipulé EN PROCESSUS — le mot de passe maître est déjà en
mémoire, le redemander à un sous-processus serait une saisie de plus à chaque
geste. Montage, démontage et diagnostic passent au contraire par `vpn.py` en
sous-processus : ils durent, ils parlent, et la sortie en direct est ce qui
rend une montée de tunnel suivable.
Vérifié : 37 tests sur le menu, aucune saisie réelle.
--- EN ---
`vpn.py` could already do everything from the command line, but the JSON
profile had to be written by hand and the secrets filed into KeePassXC by
hand. The menu asks the questions the chosen driver declares, and only those:
adding a technology adds no line here.
The vault is handled IN PROCESS — the master password is already in memory,
and asking a subprocess for it again would be one more entry at every step.
Raising, tearing down and diagnosing go through `vpn.py` as a subprocess
instead: they last, they talk, and live output is what makes a tunnel coming
up followable.
Checked: 37 tests on the menu, no real input.
Assisted-by: Claude Opus 5
2026-09-03 23:43:02 -04:00
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
# Profils
|
|
|
|
|
# ------------------------------------------------------------------
|
[FIX] vpn : juger un profil monté sur son interface, et le marquer
Le verdict se fondait sur le seul fichier d'état, que RIEN n'efface quand
un tunnel meurt sans passer par « down » — machine redémarrée, processus
tué, session expirée. Un état laissé derrière était annoncé monté sur
l'écran même qui déclarait le processus mort et l'interface absente.
L'interface arbitre désormais : son nom vient de l'état s'il y en a un,
sinon du pilote pour ceux qui la nomment d'avance, si bien qu'un tunnel
monté hors de l'outil est vu aussi. Sshuttle n'en crée aucune, là le
processus reste seul juge. La liste des profils le marque, connecter un
profil déjà monté demande confirmation, et déconnecter un profil déjà
tombé le dit sans l'empêcher : « down » nettoie l'état laissé.
--- EN ---
The verdict rested on the state file alone, which NOTHING clears when a
tunnel dies without going through "down" — machine rebooted, process
killed, session expired. A leftover state was reported as mounted on the
very screen declaring the process gone and the interface absent.
The interface now arbitrates: its name comes from the state when there is
one, otherwise from drivers that name it in advance, so a tunnel brought
up outside the tool is seen too. Sshuttle creates none, and there the
process stays the only judge. The profile list marks it, connecting an
already-mounted profile asks first, and disconnecting an already-down one
says so without refusing: "down" clears the leftover state.
Assisted-by: Claude Opus 5
2026-09-07 23:27:35 -04:00
|
|
|
@staticmethod
|
|
|
|
|
def _vpn_is_up(profile):
|
|
|
|
|
"""Ce profil porte-t-il un tunnel vivant ? Faux si on ne peut pas
|
|
|
|
|
savoir — un pilote retiré de la configuration ne doit pas empêcher
|
|
|
|
|
de lister les profils."""
|
|
|
|
|
driver_cls = get_driver(profile.get("driver"))
|
|
|
|
|
return bool(driver_cls) and driver_cls(profile).is_up()
|
|
|
|
|
|
[ADD] menu vpn : créer un profil, déposer les secrets, monter le tunnel
`vpn.py` savait tout faire en ligne de commande, mais il fallait écrire le
profil JSON à la main et déposer les secrets dans KeePassXC soi-même. Le menu
pose les questions que le pilote choisi déclare, et lui seul : ajouter une
technologie n'ajoute pas une ligne ici.
Le coffre est manipulé EN PROCESSUS — le mot de passe maître est déjà en
mémoire, le redemander à un sous-processus serait une saisie de plus à chaque
geste. Montage, démontage et diagnostic passent au contraire par `vpn.py` en
sous-processus : ils durent, ils parlent, et la sortie en direct est ce qui
rend une montée de tunnel suivable.
Vérifié : 37 tests sur le menu, aucune saisie réelle.
--- EN ---
`vpn.py` could already do everything from the command line, but the JSON
profile had to be written by hand and the secrets filed into KeePassXC by
hand. The menu asks the questions the chosen driver declares, and only those:
adding a technology adds no line here.
The vault is handled IN PROCESS — the master password is already in memory,
and asking a subprocess for it again would be one more entry at every step.
Raising, tearing down and diagnosing go through `vpn.py` as a subprocess
instead: they last, they talk, and live output is what makes a tunnel coming
up followable.
Checked: 37 tests on the menu, no real input.
Assisted-by: Claude Opus 5
2026-09-03 23:43:02 -04:00
|
|
|
def _vpn_select_profile(self):
|
[FIX] vpn : juger un profil monté sur son interface, et le marquer
Le verdict se fondait sur le seul fichier d'état, que RIEN n'efface quand
un tunnel meurt sans passer par « down » — machine redémarrée, processus
tué, session expirée. Un état laissé derrière était annoncé monté sur
l'écran même qui déclarait le processus mort et l'interface absente.
L'interface arbitre désormais : son nom vient de l'état s'il y en a un,
sinon du pilote pour ceux qui la nomment d'avance, si bien qu'un tunnel
monté hors de l'outil est vu aussi. Sshuttle n'en crée aucune, là le
processus reste seul juge. La liste des profils le marque, connecter un
profil déjà monté demande confirmation, et déconnecter un profil déjà
tombé le dit sans l'empêcher : « down » nettoie l'état laissé.
--- EN ---
The verdict rested on the state file alone, which NOTHING clears when a
tunnel dies without going through "down" — machine rebooted, process
killed, session expired. A leftover state was reported as mounted on the
very screen declaring the process gone and the interface absent.
The interface now arbitrates: its name comes from the state when there is
one, otherwise from drivers that name it in advance, so a tunnel brought
up outside the tool is seen too. Sshuttle creates none, and there the
process stays the only judge. The profile list marks it, connecting an
already-mounted profile asks first, and disconnecting an already-down one
says so without refusing: "down" clears the leftover state.
Assisted-by: Claude Opus 5
2026-09-07 23:27:35 -04:00
|
|
|
"""Nom du profil choisi, "" si l'utilisateur renonce.
|
|
|
|
|
|
|
|
|
|
L'état de chaque profil est affiché, parce que la liste sert autant
|
|
|
|
|
à connecter qu'à déconnecter : sans lui, on descend un tunnel déjà
|
|
|
|
|
mort ou on remonte celui qui tient, et la sortie du CLI est la
|
|
|
|
|
première chose qui le dit — trop tard.
|
|
|
|
|
"""
|
[ADD] menu vpn : créer un profil, déposer les secrets, monter le tunnel
`vpn.py` savait tout faire en ligne de commande, mais il fallait écrire le
profil JSON à la main et déposer les secrets dans KeePassXC soi-même. Le menu
pose les questions que le pilote choisi déclare, et lui seul : ajouter une
technologie n'ajoute pas une ligne ici.
Le coffre est manipulé EN PROCESSUS — le mot de passe maître est déjà en
mémoire, le redemander à un sous-processus serait une saisie de plus à chaque
geste. Montage, démontage et diagnostic passent au contraire par `vpn.py` en
sous-processus : ils durent, ils parlent, et la sortie en direct est ce qui
rend une montée de tunnel suivable.
Vérifié : 37 tests sur le menu, aucune saisie réelle.
--- EN ---
`vpn.py` could already do everything from the command line, but the JSON
profile had to be written by hand and the secrets filed into KeePassXC by
hand. The menu asks the questions the chosen driver declares, and only those:
adding a technology adds no line here.
The vault is handled IN PROCESS — the master password is already in memory,
and asking a subprocess for it again would be one more entry at every step.
Raising, tearing down and diagnosing go through `vpn.py` as a subprocess
instead: they last, they talk, and live output is what makes a tunnel coming
up followable.
Checked: 37 tests on the menu, no real input.
Assisted-by: Claude Opus 5
2026-09-03 23:43:02 -04:00
|
|
|
all_profiles = [profiles.with_defaults(p) for p in profiles.load_all()]
|
|
|
|
|
if not all_profiles:
|
|
|
|
|
print(t("No VPN profile yet: create one first."))
|
|
|
|
|
return ""
|
|
|
|
|
for index, profile in enumerate(all_profiles, start=1):
|
|
|
|
|
target = (
|
|
|
|
|
t("all traffic")
|
|
|
|
|
if profile["default_route"]
|
|
|
|
|
else ", ".join(profile["routes"])
|
|
|
|
|
)
|
[FIX] vpn : juger un profil monté sur son interface, et le marquer
Le verdict se fondait sur le seul fichier d'état, que RIEN n'efface quand
un tunnel meurt sans passer par « down » — machine redémarrée, processus
tué, session expirée. Un état laissé derrière était annoncé monté sur
l'écran même qui déclarait le processus mort et l'interface absente.
L'interface arbitre désormais : son nom vient de l'état s'il y en a un,
sinon du pilote pour ceux qui la nomment d'avance, si bien qu'un tunnel
monté hors de l'outil est vu aussi. Sshuttle n'en crée aucune, là le
processus reste seul juge. La liste des profils le marque, connecter un
profil déjà monté demande confirmation, et déconnecter un profil déjà
tombé le dit sans l'empêcher : « down » nettoie l'état laissé.
--- EN ---
The verdict rested on the state file alone, which NOTHING clears when a
tunnel dies without going through "down" — machine rebooted, process
killed, session expired. A leftover state was reported as mounted on the
very screen declaring the process gone and the interface absent.
The interface now arbitrates: its name comes from the state when there is
one, otherwise from drivers that name it in advance, so a tunnel brought
up outside the tool is seen too. Sshuttle creates none, and there the
process stays the only judge. The profile list marks it, connecting an
already-mounted profile asks first, and disconnecting an already-down one
says so without refusing: "down" clears the leftover state.
Assisted-by: Claude Opus 5
2026-09-07 23:27:35 -04:00
|
|
|
# Deux colonnes de large dans les deux cas : un emoji en occupe
|
|
|
|
|
# deux, et sans cela les lignes non connectées décaleraient tout
|
|
|
|
|
# ce qui suit.
|
|
|
|
|
marque = "🟢" if self._vpn_is_up(profile) else " "
|
[ADD] menu vpn : créer un profil, déposer les secrets, monter le tunnel
`vpn.py` savait tout faire en ligne de commande, mais il fallait écrire le
profil JSON à la main et déposer les secrets dans KeePassXC soi-même. Le menu
pose les questions que le pilote choisi déclare, et lui seul : ajouter une
technologie n'ajoute pas une ligne ici.
Le coffre est manipulé EN PROCESSUS — le mot de passe maître est déjà en
mémoire, le redemander à un sous-processus serait une saisie de plus à chaque
geste. Montage, démontage et diagnostic passent au contraire par `vpn.py` en
sous-processus : ils durent, ils parlent, et la sortie en direct est ce qui
rend une montée de tunnel suivable.
Vérifié : 37 tests sur le menu, aucune saisie réelle.
--- EN ---
`vpn.py` could already do everything from the command line, but the JSON
profile had to be written by hand and the secrets filed into KeePassXC by
hand. The menu asks the questions the chosen driver declares, and only those:
adding a technology adds no line here.
The vault is handled IN PROCESS — the master password is already in memory,
and asking a subprocess for it again would be one more entry at every step.
Raising, tearing down and diagnosing go through `vpn.py` as a subprocess
instead: they last, they talk, and live output is what makes a tunnel coming
up followable.
Checked: 37 tests on the menu, no real input.
Assisted-by: Claude Opus 5
2026-09-03 23:43:02 -04:00
|
|
|
print(
|
[FIX] vpn : juger un profil monté sur son interface, et le marquer
Le verdict se fondait sur le seul fichier d'état, que RIEN n'efface quand
un tunnel meurt sans passer par « down » — machine redémarrée, processus
tué, session expirée. Un état laissé derrière était annoncé monté sur
l'écran même qui déclarait le processus mort et l'interface absente.
L'interface arbitre désormais : son nom vient de l'état s'il y en a un,
sinon du pilote pour ceux qui la nomment d'avance, si bien qu'un tunnel
monté hors de l'outil est vu aussi. Sshuttle n'en crée aucune, là le
processus reste seul juge. La liste des profils le marque, connecter un
profil déjà monté demande confirmation, et déconnecter un profil déjà
tombé le dit sans l'empêcher : « down » nettoie l'état laissé.
--- EN ---
The verdict rested on the state file alone, which NOTHING clears when a
tunnel dies without going through "down" — machine rebooted, process
killed, session expired. A leftover state was reported as mounted on the
very screen declaring the process gone and the interface absent.
The interface now arbitrates: its name comes from the state when there is
one, otherwise from drivers that name it in advance, so a tunnel brought
up outside the tool is seen too. Sshuttle creates none, and there the
process stays the only judge. The profile list marks it, connecting an
already-mounted profile asks first, and disconnecting an already-down one
says so without refusing: "down" clears the leftover state.
Assisted-by: Claude Opus 5
2026-09-07 23:27:35 -04:00
|
|
|
f"[{index}] {marque} {profile['name']:<20}"
|
[ADD] menu vpn : créer un profil, déposer les secrets, monter le tunnel
`vpn.py` savait tout faire en ligne de commande, mais il fallait écrire le
profil JSON à la main et déposer les secrets dans KeePassXC soi-même. Le menu
pose les questions que le pilote choisi déclare, et lui seul : ajouter une
technologie n'ajoute pas une ligne ici.
Le coffre est manipulé EN PROCESSUS — le mot de passe maître est déjà en
mémoire, le redemander à un sous-processus serait une saisie de plus à chaque
geste. Montage, démontage et diagnostic passent au contraire par `vpn.py` en
sous-processus : ils durent, ils parlent, et la sortie en direct est ce qui
rend une montée de tunnel suivable.
Vérifié : 37 tests sur le menu, aucune saisie réelle.
--- EN ---
`vpn.py` could already do everything from the command line, but the JSON
profile had to be written by hand and the secrets filed into KeePassXC by
hand. The menu asks the questions the chosen driver declares, and only those:
adding a technology adds no line here.
The vault is handled IN PROCESS — the master password is already in memory,
and asking a subprocess for it again would be one more entry at every step.
Raising, tearing down and diagnosing go through `vpn.py` as a subprocess
instead: they last, they talk, and live output is what makes a tunnel coming
up followable.
Checked: 37 tests on the menu, no real input.
Assisted-by: Claude Opus 5
2026-09-03 23:43:02 -04:00
|
|
|
f" {profile['server']:<26} {target}"
|
|
|
|
|
)
|
|
|
|
|
answer = input(f"{t('Profile number (0 to go back)')} : ").strip()
|
|
|
|
|
if not answer.isdigit() or not 1 <= int(answer) <= len(all_profiles):
|
|
|
|
|
if answer not in ("0", ""):
|
|
|
|
|
print(t("Unknown choice."))
|
|
|
|
|
return ""
|
|
|
|
|
return all_profiles[int(answer) - 1]["name"]
|
|
|
|
|
|
[ADD] vpn openconnect : groupe d'URL, SSO délégué, mot de passe borné
Deux mécanismes désignent un service sur un concentrateur : un chemin
d'URL, une valeur de menu déroulant. Les confondre rend le formulaire d'un
AUTRE service — identifiants justes refusés, rien ne désignant le groupe.
Et certains ne comparent que les N premiers caractères du mot de passe,
que le profil déclare désormais sans jamais rien tronquer.
Une passerelle qui exige un navigateur intégré arrête openconnect sur
« No SSO handler », les distributions le bâtissant sans webview. Un greffon
fait l'étape web et rend un cookie ; le pilote monte lui-même, et le profil
garde son interface, son état et son diagnostic.
Vérifié : 249 tests unitaires, un tunnel monté contre une passerelle SAML.
--- EN ---
Two mechanisms designate a service on one concentrator: a URL path, and a
dropdown value. Confusing them hands over ANOTHER service's login form —
correct credentials refused, with nothing pointing at the group. And some
compare only the first N characters of the password, which the profile now
declares without ever truncating anything.
A gateway demanding an embedded browser stops openconnect on "No SSO
handler", distributions building it without a webview. A helper does the
web step and returns a cookie; the driver mounts the tunnel itself, so the
profile keeps its interface, its state and its diagnosis.
Checked: 249 unit tests, and one tunnel mounted against a SAML gateway.
Assisted-by: Claude Opus 5
2026-09-04 05:52:09 -04:00
|
|
|
def _vpn_from_preset(self):
|
|
|
|
|
"""Crée un profil à partir d'un préréglage de site.
|
|
|
|
|
|
|
|
|
|
Le préréglage porte ce que l'établissement publie et qui est le même
|
|
|
|
|
pour tout le monde ; il ne reste à taper que l'identifiant. Le
|
|
|
|
|
formulaire est celui de `_vpn_edit_profile`, amorcé : dupliquer les
|
|
|
|
|
questions ici ferait vivre deux formulaires qui divergeraient au
|
|
|
|
|
prochain champ ajouté à un pilote.
|
|
|
|
|
"""
|
|
|
|
|
found, errors = presets.load_all()
|
|
|
|
|
for error in errors:
|
|
|
|
|
print(f"! {t('Unreadable preset: ')}{error}")
|
|
|
|
|
if not found:
|
|
|
|
|
print(t("No site preset available."))
|
|
|
|
|
print(f" {t(PRESET_LOCATION_NOTE)}")
|
|
|
|
|
return
|
|
|
|
|
for index, preset in enumerate(found, start=1):
|
|
|
|
|
print(
|
|
|
|
|
f"[{index}] {presets.label(preset):<34}"
|
|
|
|
|
f" {preset.get('server', ''):<28}"
|
|
|
|
|
f" {t(preset.get('hint', '') or '')}"
|
|
|
|
|
)
|
|
|
|
|
answer = input(f"{t('Preset number (0 to go back)')} : ").strip()
|
|
|
|
|
if not answer.isdigit() or not 1 <= int(answer) <= len(found):
|
|
|
|
|
if answer not in ("0", ""):
|
|
|
|
|
print(t("Unknown choice."))
|
|
|
|
|
return
|
|
|
|
|
preset = found[int(answer) - 1]
|
|
|
|
|
print(f"\n{t('An empty answer keeps the preset value.')}")
|
|
|
|
|
name = input(
|
|
|
|
|
f"{t('Profile name (lowercase, digits, - or _)')} : "
|
|
|
|
|
).strip()
|
|
|
|
|
if not name:
|
|
|
|
|
return
|
|
|
|
|
existing = profiles.load(name)
|
|
|
|
|
seed = presets.apply(preset, name)
|
|
|
|
|
if existing:
|
|
|
|
|
print(f"! {t(PRESET_REPLAYED_NOTE)}")
|
|
|
|
|
# Le PRÉRÉGLAGE gagne sur les champs qu'il déclare : rejouer un
|
|
|
|
|
# préréglage sur un profil existant sert à le remettre à jour
|
|
|
|
|
# après un déménagement de passerelle ou un groupe renommé, et
|
|
|
|
|
# garder l'ancienne valeur ne ferait rien de ce qu'on demande.
|
|
|
|
|
#
|
|
|
|
|
# Le reste vient du profil, parce que c'est ce qui est PERSONNEL
|
|
|
|
|
# et qu'aucun préréglage ne porte : l'identifiant, les routes
|
|
|
|
|
# ajoutées à la main, le certificat épinglé, l'adresse témoin.
|
|
|
|
|
#
|
|
|
|
|
# `k in seed` borne la reprise aux champs que le pilote du
|
|
|
|
|
# préréglage connaît : sur un profil qui change de technologie,
|
|
|
|
|
# recopier tout ferait suivre une clé WireGuard dans un profil
|
|
|
|
|
# OpenConnect, où rien ne la lirait jamais.
|
|
|
|
|
declared = set(preset) - set(presets.META_KEYS)
|
|
|
|
|
seed.update(
|
|
|
|
|
{
|
|
|
|
|
key: value
|
|
|
|
|
for key, value in existing.items()
|
|
|
|
|
if key in seed and key not in declared
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
self._vpn_edit_profile(seed=seed)
|
|
|
|
|
|
[ADD] vpn : parcourir les profils AnyConnect au lieu de taper le chemin
Personne ne retient « /opt/cisco/secureclient/vpn/profile ». Le chemin
tapé oblige à le connaître ; le parcours seul n'aide pas quand le fichier
vient d'ailleurs, d'un courriel ou du portail d'un site. Les deux, dans
cet ordre, et la saisie reprend la main dès qu'on sort du parcours sans
rien choisir ou qu'on en rapporte un répertoire.
Le parcours n'est proposé que si l'un des répertoires connus existe :
l'ouvrir sur un chemin absent afficherait une liste vide, ce qui ressemble
à une panne. Vérifié : 7 tests unitaires, dont chacun des replis.
--- EN ---
Nobody remembers "/opt/cisco/secureclient/vpn/profile". A typed path
requires knowing it; browsing alone does not help when the file comes
from elsewhere, from an e-mail or a site's portal. Both, in that order,
and typing takes over as soon as one leaves the browser without choosing
anything, or brings back a directory from it.
Browsing is offered only if one of the known directories exists: opening
it on an absent path would show an empty list, which looks like a fault.
Checked: 7 unit tests, one for each fallback.
Assisted-by: Claude Opus 5
2026-09-04 08:48:28 -04:00
|
|
|
def _vpn_select_xml(self):
|
|
|
|
|
"""Chemin du profil `.xml`, "" si l'utilisateur renonce.
|
|
|
|
|
|
|
|
|
|
Le parcours d'abord, la saisie ensuite, parce que ni l'un ni l'autre
|
|
|
|
|
ne suffit : le parcours part des répertoires du client de Cisco et
|
|
|
|
|
n'aide pas si le fichier vient d'ailleurs ; le chemin tapé oblige à
|
|
|
|
|
le connaître, or personne ne retient
|
|
|
|
|
« /opt/cisco/secureclient/vpn/profile ».
|
|
|
|
|
|
|
|
|
|
Le parcours n'est PROPOSÉ que si un de ces répertoires existe :
|
|
|
|
|
l'ouvrir sur un chemin absent afficherait une liste vide, ce qui
|
|
|
|
|
ressemble à une panne.
|
|
|
|
|
"""
|
|
|
|
|
start = next(
|
|
|
|
|
(
|
|
|
|
|
path
|
|
|
|
|
for path in (os.path.expanduser(d) for d in ANYCONNECT_DIRS)
|
|
|
|
|
if os.path.isdir(path)
|
|
|
|
|
),
|
|
|
|
|
"",
|
|
|
|
|
)
|
|
|
|
|
if todo_file_browser is not None and start:
|
|
|
|
|
print(f" {t('Found')} : {start}")
|
|
|
|
|
if self._is_yes(input(f"{t('Browse it? (Y/n)')} : ") or "o"):
|
|
|
|
|
self._xml_path = ""
|
|
|
|
|
browser = todo_file_browser.FileBrowser(
|
|
|
|
|
start, self._on_xml_selected
|
|
|
|
|
)
|
|
|
|
|
browser.run_main_frame()
|
|
|
|
|
if self._xml_path and os.path.isfile(self._xml_path):
|
|
|
|
|
return self._xml_path
|
|
|
|
|
answer = input(f"{t('Path to the .xml profile')} : ").strip()
|
|
|
|
|
return os.path.expanduser(answer) if answer else ""
|
|
|
|
|
|
|
|
|
|
def _on_xml_selected(self, path):
|
|
|
|
|
self._xml_path = path
|
|
|
|
|
|
[ADD] vpn openconnect : groupe d'URL, SSO délégué, mot de passe borné
Deux mécanismes désignent un service sur un concentrateur : un chemin
d'URL, une valeur de menu déroulant. Les confondre rend le formulaire d'un
AUTRE service — identifiants justes refusés, rien ne désignant le groupe.
Et certains ne comparent que les N premiers caractères du mot de passe,
que le profil déclare désormais sans jamais rien tronquer.
Une passerelle qui exige un navigateur intégré arrête openconnect sur
« No SSO handler », les distributions le bâtissant sans webview. Un greffon
fait l'étape web et rend un cookie ; le pilote monte lui-même, et le profil
garde son interface, son état et son diagnostic.
Vérifié : 249 tests unitaires, un tunnel monté contre une passerelle SAML.
--- EN ---
Two mechanisms designate a service on one concentrator: a URL path, and a
dropdown value. Confusing them hands over ANOTHER service's login form —
correct credentials refused, with nothing pointing at the group. And some
compare only the first N characters of the password, which the profile now
declares without ever truncating anything.
A gateway demanding an embedded browser stops openconnect on "No SSO
handler", distributions building it without a webview. A helper does the
web step and returns a cookie; the driver mounts the tunnel itself, so the
profile keeps its interface, its state and its diagnosis.
Checked: 249 unit tests, and one tunnel mounted against a SAML gateway.
Assisted-by: Claude Opus 5
2026-09-04 05:52:09 -04:00
|
|
|
def _vpn_import_anyconnect(self):
|
|
|
|
|
"""Transforme un profil AnyConnect (`.xml`) en préréglages.
|
|
|
|
|
|
|
|
|
|
Le fichier qu'un site distribue porte déjà le nom d'hôte et le
|
|
|
|
|
groupe de connexion, et c'est ce dernier qui décide quel service du
|
|
|
|
|
concentrateur on joint. Le retaper à la main est l'occasion de se
|
|
|
|
|
tromper sur le seul champ qui compte.
|
|
|
|
|
|
|
|
|
|
Écrit dans `private/vpn/presets/`, jamais dans `conf/` : le fichier
|
|
|
|
|
nomme un établissement.
|
|
|
|
|
"""
|
|
|
|
|
print(t(ANYCONNECT_LOCATION_NOTE))
|
[ADD] vpn : parcourir les profils AnyConnect au lieu de taper le chemin
Personne ne retient « /opt/cisco/secureclient/vpn/profile ». Le chemin
tapé oblige à le connaître ; le parcours seul n'aide pas quand le fichier
vient d'ailleurs, d'un courriel ou du portail d'un site. Les deux, dans
cet ordre, et la saisie reprend la main dès qu'on sort du parcours sans
rien choisir ou qu'on en rapporte un répertoire.
Le parcours n'est proposé que si l'un des répertoires connus existe :
l'ouvrir sur un chemin absent afficherait une liste vide, ce qui ressemble
à une panne. Vérifié : 7 tests unitaires, dont chacun des replis.
--- EN ---
Nobody remembers "/opt/cisco/secureclient/vpn/profile". A typed path
requires knowing it; browsing alone does not help when the file comes
from elsewhere, from an e-mail or a site's portal. Both, in that order,
and typing takes over as soon as one leaves the browser without choosing
anything, or brings back a directory from it.
Browsing is offered only if one of the known directories exists: opening
it on an absent path would show an empty list, which looks like a fault.
Checked: 7 unit tests, one for each fallback.
Assisted-by: Claude Opus 5
2026-09-04 08:48:28 -04:00
|
|
|
path = self._vpn_select_xml()
|
[ADD] vpn openconnect : groupe d'URL, SSO délégué, mot de passe borné
Deux mécanismes désignent un service sur un concentrateur : un chemin
d'URL, une valeur de menu déroulant. Les confondre rend le formulaire d'un
AUTRE service — identifiants justes refusés, rien ne désignant le groupe.
Et certains ne comparent que les N premiers caractères du mot de passe,
que le profil déclare désormais sans jamais rien tronquer.
Une passerelle qui exige un navigateur intégré arrête openconnect sur
« No SSO handler », les distributions le bâtissant sans webview. Un greffon
fait l'étape web et rend un cookie ; le pilote monte lui-même, et le profil
garde son interface, son état et son diagnostic.
Vérifié : 249 tests unitaires, un tunnel monté contre une passerelle SAML.
--- EN ---
Two mechanisms designate a service on one concentrator: a URL path, and a
dropdown value. Confusing them hands over ANOTHER service's login form —
correct credentials refused, with nothing pointing at the group. And some
compare only the first N characters of the password, which the profile now
declares without ever truncating anything.
A gateway demanding an embedded browser stops openconnect on "No SSO
handler", distributions building it without a webview. A helper does the
web step and returns a cookie; the driver mounts the tunnel itself, so the
profile keeps its interface, its state and its diagnosis.
Checked: 249 unit tests, and one tunnel mounted against a SAML gateway.
Assisted-by: Claude Opus 5
2026-09-04 05:52:09 -04:00
|
|
|
if not path:
|
|
|
|
|
return
|
|
|
|
|
try:
|
[ADD] vpn : parcourir les profils AnyConnect au lieu de taper le chemin
Personne ne retient « /opt/cisco/secureclient/vpn/profile ». Le chemin
tapé oblige à le connaître ; le parcours seul n'aide pas quand le fichier
vient d'ailleurs, d'un courriel ou du portail d'un site. Les deux, dans
cet ordre, et la saisie reprend la main dès qu'on sort du parcours sans
rien choisir ou qu'on en rapporte un répertoire.
Le parcours n'est proposé que si l'un des répertoires connus existe :
l'ouvrir sur un chemin absent afficherait une liste vide, ce qui ressemble
à une panne. Vérifié : 7 tests unitaires, dont chacun des replis.
--- EN ---
Nobody remembers "/opt/cisco/secureclient/vpn/profile". A typed path
requires knowing it; browsing alone does not help when the file comes
from elsewhere, from an e-mail or a site's portal. Both, in that order,
and typing takes over as soon as one leaves the browser without choosing
anything, or brings back a directory from it.
Browsing is offered only if one of the known directories exists: opening
it on an absent path would show an empty list, which looks like a fault.
Checked: 7 unit tests, one for each fallback.
Assisted-by: Claude Opus 5
2026-09-04 08:48:28 -04:00
|
|
|
found = anyconnect_xml.parse_file(path)
|
[ADD] vpn openconnect : groupe d'URL, SSO délégué, mot de passe borné
Deux mécanismes désignent un service sur un concentrateur : un chemin
d'URL, une valeur de menu déroulant. Les confondre rend le formulaire d'un
AUTRE service — identifiants justes refusés, rien ne désignant le groupe.
Et certains ne comparent que les N premiers caractères du mot de passe,
que le profil déclare désormais sans jamais rien tronquer.
Une passerelle qui exige un navigateur intégré arrête openconnect sur
« No SSO handler », les distributions le bâtissant sans webview. Un greffon
fait l'étape web et rend un cookie ; le pilote monte lui-même, et le profil
garde son interface, son état et son diagnostic.
Vérifié : 249 tests unitaires, un tunnel monté contre une passerelle SAML.
--- EN ---
Two mechanisms designate a service on one concentrator: a URL path, and a
dropdown value. Confusing them hands over ANOTHER service's login form —
correct credentials refused, with nothing pointing at the group. And some
compare only the first N characters of the password, which the profile now
declares without ever truncating anything.
A gateway demanding an embedded browser stops openconnect on "No SSO
handler", distributions building it without a webview. A helper does the
web step and returns a cookie; the driver mounts the tunnel itself, so the
profile keeps its interface, its state and its diagnosis.
Checked: 249 unit tests, and one tunnel mounted against a SAML gateway.
Assisted-by: Claude Opus 5
2026-09-04 05:52:09 -04:00
|
|
|
except anyconnect_xml.ProfileXmlError as error:
|
|
|
|
|
print(f"\n✗ {error}")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
print()
|
|
|
|
|
for preset in found:
|
|
|
|
|
print(f" {presets.label(preset)}")
|
|
|
|
|
print(f" {t('Gateway')} : {preset['server']}")
|
|
|
|
|
print(
|
|
|
|
|
f" {t('Connection group')} :"
|
|
|
|
|
f" {preset['oc_usergroup'] or t('none')}"
|
|
|
|
|
)
|
|
|
|
|
stem = os.path.splitext(os.path.basename(path))[0]
|
|
|
|
|
stem = presets.slug_stem(stem)
|
|
|
|
|
written = presets.save(found, stem)
|
|
|
|
|
print(f"\n✓ {t('Presets written: ')}{written}")
|
|
|
|
|
print(f" {t(ANYCONNECT_NEXT_STEP)}")
|
|
|
|
|
|
|
|
|
|
def _vpn_edit_profile(self, seed=None):
|
[ADD] menu vpn : créer un profil, déposer les secrets, monter le tunnel
`vpn.py` savait tout faire en ligne de commande, mais il fallait écrire le
profil JSON à la main et déposer les secrets dans KeePassXC soi-même. Le menu
pose les questions que le pilote choisi déclare, et lui seul : ajouter une
technologie n'ajoute pas une ligne ici.
Le coffre est manipulé EN PROCESSUS — le mot de passe maître est déjà en
mémoire, le redemander à un sous-processus serait une saisie de plus à chaque
geste. Montage, démontage et diagnostic passent au contraire par `vpn.py` en
sous-processus : ils durent, ils parlent, et la sortie en direct est ce qui
rend une montée de tunnel suivable.
Vérifié : 37 tests sur le menu, aucune saisie réelle.
--- EN ---
`vpn.py` could already do everything from the command line, but the JSON
profile had to be written by hand and the secrets filed into KeePassXC by
hand. The menu asks the questions the chosen driver declares, and only those:
adding a technology adds no line here.
The vault is handled IN PROCESS — the master password is already in memory,
and asking a subprocess for it again would be one more entry at every step.
Raising, tearing down and diagnosing go through `vpn.py` as a subprocess
instead: they last, they talk, and live output is what makes a tunnel coming
up followable.
Checked: 37 tests on the menu, no real input.
Assisted-by: Claude Opus 5
2026-09-03 23:43:02 -04:00
|
|
|
"""Crée ou modifie un profil, quelle que soit la technologie.
|
|
|
|
|
|
|
|
|
|
Les questions viennent du PILOTE (`form_fields`) : ce menu ne sait
|
|
|
|
|
pas qu'un profil L2TP a un utilisateur PPP ni qu'un profil WireGuard
|
|
|
|
|
a une clé de pair. Ajouter une technologie n'ajoute donc pas une
|
|
|
|
|
ligne ici.
|
|
|
|
|
|
|
|
|
|
Une réponse vide garde la valeur actuelle : modifier une seule route
|
|
|
|
|
ne doit pas obliger à ressaisir tout le reste.
|
[ADD] vpn openconnect : groupe d'URL, SSO délégué, mot de passe borné
Deux mécanismes désignent un service sur un concentrateur : un chemin
d'URL, une valeur de menu déroulant. Les confondre rend le formulaire d'un
AUTRE service — identifiants justes refusés, rien ne désignant le groupe.
Et certains ne comparent que les N premiers caractères du mot de passe,
que le profil déclare désormais sans jamais rien tronquer.
Une passerelle qui exige un navigateur intégré arrête openconnect sur
« No SSO handler », les distributions le bâtissant sans webview. Un greffon
fait l'étape web et rend un cookie ; le pilote monte lui-même, et le profil
garde son interface, son état et son diagnostic.
Vérifié : 249 tests unitaires, un tunnel monté contre une passerelle SAML.
--- EN ---
Two mechanisms designate a service on one concentrator: a URL path, and a
dropdown value. Confusing them hands over ANOTHER service's login form —
correct credentials refused, with nothing pointing at the group. And some
compare only the first N characters of the password, which the profile now
declares without ever truncating anything.
A gateway demanding an embedded browser stops openconnect on "No SSO
handler", distributions building it without a webview. A helper does the
web step and returns a cookie; the driver mounts the tunnel itself, so the
profile keeps its interface, its state and its diagnosis.
Checked: 249 unit tests, and one tunnel mounted against a SAML gateway.
Assisted-by: Claude Opus 5
2026-09-04 05:52:09 -04:00
|
|
|
|
|
|
|
|
`seed` amorce le formulaire avec un profil déjà rempli — un
|
|
|
|
|
préréglage de site. Il porte alors le nom ET la technologie, donc les
|
|
|
|
|
deux questions correspondantes ne sont pas posées : le préréglage y a
|
|
|
|
|
déjà répondu, et redemander « quelle technologie ? » invite à
|
|
|
|
|
contredire le seul champ qu'on ne doit pas changer.
|
[ADD] menu vpn : créer un profil, déposer les secrets, monter le tunnel
`vpn.py` savait tout faire en ligne de commande, mais il fallait écrire le
profil JSON à la main et déposer les secrets dans KeePassXC soi-même. Le menu
pose les questions que le pilote choisi déclare, et lui seul : ajouter une
technologie n'ajoute pas une ligne ici.
Le coffre est manipulé EN PROCESSUS — le mot de passe maître est déjà en
mémoire, le redemander à un sous-processus serait une saisie de plus à chaque
geste. Montage, démontage et diagnostic passent au contraire par `vpn.py` en
sous-processus : ils durent, ils parlent, et la sortie en direct est ce qui
rend une montée de tunnel suivable.
Vérifié : 37 tests sur le menu, aucune saisie réelle.
--- EN ---
`vpn.py` could already do everything from the command line, but the JSON
profile had to be written by hand and the secrets filed into KeePassXC by
hand. The menu asks the questions the chosen driver declares, and only those:
adding a technology adds no line here.
The vault is handled IN PROCESS — the master password is already in memory,
and asking a subprocess for it again would be one more entry at every step.
Raising, tearing down and diagnosing go through `vpn.py` as a subprocess
instead: they last, they talk, and live output is what makes a tunnel coming
up followable.
Checked: 37 tests on the menu, no real input.
Assisted-by: Claude Opus 5
2026-09-03 23:43:02 -04:00
|
|
|
"""
|
[ADD] vpn openconnect : groupe d'URL, SSO délégué, mot de passe borné
Deux mécanismes désignent un service sur un concentrateur : un chemin
d'URL, une valeur de menu déroulant. Les confondre rend le formulaire d'un
AUTRE service — identifiants justes refusés, rien ne désignant le groupe.
Et certains ne comparent que les N premiers caractères du mot de passe,
que le profil déclare désormais sans jamais rien tronquer.
Une passerelle qui exige un navigateur intégré arrête openconnect sur
« No SSO handler », les distributions le bâtissant sans webview. Un greffon
fait l'étape web et rend un cookie ; le pilote monte lui-même, et le profil
garde son interface, son état et son diagnostic.
Vérifié : 249 tests unitaires, un tunnel monté contre une passerelle SAML.
--- EN ---
Two mechanisms designate a service on one concentrator: a URL path, and a
dropdown value. Confusing them hands over ANOTHER service's login form —
correct credentials refused, with nothing pointing at the group. And some
compare only the first N characters of the password, which the profile now
declares without ever truncating anything.
A gateway demanding an embedded browser stops openconnect on "No SSO
handler", distributions building it without a webview. A helper does the
web step and returns a cookie; the driver mounts the tunnel itself, so the
profile keeps its interface, its state and its diagnosis.
Checked: 249 unit tests, and one tunnel mounted against a SAML gateway.
Assisted-by: Claude Opus 5
2026-09-04 05:52:09 -04:00
|
|
|
if seed is not None:
|
|
|
|
|
current = dict(seed)
|
|
|
|
|
name = current["name"]
|
|
|
|
|
driver_cls = get_driver(current.get("driver"))
|
|
|
|
|
if driver_cls is None:
|
|
|
|
|
print(f"✗ {t('Unknown driver: ')}{current.get('driver')}")
|
|
|
|
|
return
|
|
|
|
|
else:
|
|
|
|
|
name = input(
|
|
|
|
|
f"{t('Profile name (lowercase, digits, - or _)')} : "
|
|
|
|
|
).strip()
|
|
|
|
|
if not name:
|
|
|
|
|
return
|
|
|
|
|
current = profiles.load(name) or {"name": name}
|
|
|
|
|
driver_cls = self._vpn_pick_driver(current.get("driver"))
|
|
|
|
|
if driver_cls is None:
|
|
|
|
|
return
|
[ADD] menu vpn : créer un profil, déposer les secrets, monter le tunnel
`vpn.py` savait tout faire en ligne de commande, mais il fallait écrire le
profil JSON à la main et déposer les secrets dans KeePassXC soi-même. Le menu
pose les questions que le pilote choisi déclare, et lui seul : ajouter une
technologie n'ajoute pas une ligne ici.
Le coffre est manipulé EN PROCESSUS — le mot de passe maître est déjà en
mémoire, le redemander à un sous-processus serait une saisie de plus à chaque
geste. Montage, démontage et diagnostic passent au contraire par `vpn.py` en
sous-processus : ils durent, ils parlent, et la sortie en direct est ce qui
rend une montée de tunnel suivable.
Vérifié : 37 tests sur le menu, aucune saisie réelle.
--- EN ---
`vpn.py` could already do everything from the command line, but the JSON
profile had to be written by hand and the secrets filed into KeePassXC by
hand. The menu asks the questions the chosen driver declares, and only those:
adding a technology adds no line here.
The vault is handled IN PROCESS — the master password is already in memory,
and asking a subprocess for it again would be one more entry at every step.
Raising, tearing down and diagnosing go through `vpn.py` as a subprocess
instead: they last, they talk, and live output is what makes a tunnel coming
up followable.
Checked: 37 tests on the menu, no real input.
Assisted-by: Claude Opus 5
2026-09-03 23:43:02 -04:00
|
|
|
|
|
|
|
|
# Les défauts DU PILOTE CHOISI, pour que chaque question ait un
|
|
|
|
|
# défaut sensé même sur un profil qui change de technologie.
|
|
|
|
|
draft = profiles.with_defaults(dict(current, driver=driver_cls.name))
|
|
|
|
|
|
|
|
|
|
draft["server"] = self._vpn_ask(
|
|
|
|
|
t(driver_cls.server_label), draft.get("server", "")
|
|
|
|
|
)
|
|
|
|
|
# L'identité d'abord, le routage ensuite : c'est l'ordre du document
|
|
|
|
|
# que le site remet — passerelle, utilisateur, mot de passe, clé —
|
|
|
|
|
# et le routage est une question à part, à laquelle ce document ne
|
|
|
|
|
# répond souvent pas.
|
|
|
|
|
self._vpn_ask_fields(draft, driver_cls, advanced=False)
|
|
|
|
|
draft["routes"] = self._vpn_ask(
|
|
|
|
|
t("Networks to reach, comma-separated"),
|
|
|
|
|
", ".join(draft.get("routes", [])),
|
|
|
|
|
)
|
[FIX] vpn : ne plus demander la route par défaut à qui ne la pose pas
Le formulaire demandait « tout le trafic ? » à un pilote dont le SERVEUR
décide du routage, ne faisait rien de la réponse, et `status` la jugeait
quand même : un ✗ permanent sur un tunnel sain, et un profil annonçant
« tout le trafic » sans l'obtenir.
Un drapeau, sur le modèle de celui du MTU, dit quels pilotes posent cette
route. Les autres ne sont ni interrogés ni jugés, et un drapeau laissé à
vrai n'est plus conservé. L'honorer serait pire qu'inutile : forcer une
route par défaut contre une passerelle en tunnel scindé donne un trou
noir, une passerelle ne routant pas ce qu'elle n'a pas annoncé. Les routes
déclarées, elles, restent honorées — le formulaire le dit.
--- EN ---
The form asked "all traffic?" of a driver whose SERVER decides the
routing, did nothing with the answer, and `status` judged it anyway: a
permanent ✗ on a healthy tunnel, and a profile announcing "all traffic"
without getting it.
A flag, modelled on the MTU one, says which drivers lay that route. The
others are neither asked nor judged, and a flag left true is no longer
kept. Honouring it would be worse than useless: forcing a default route
against a split-tunnel gateway gives a black hole, a gateway not routing
what it never advertised. Declared routes are still honoured — the form
says so.
Assisted-by: Claude Opus 5
2026-09-08 09:41:17 -04:00
|
|
|
if driver_cls.uses_default_route:
|
|
|
|
|
draft["default_route"] = self._vpn_ask_flag(
|
|
|
|
|
t("Send ALL traffic through the tunnel?"),
|
|
|
|
|
draft.get("default_route", False),
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
# Ni demandée, ni conservée : un drapeau laissé à vrai sur un
|
|
|
|
|
# pilote qui l'ignore reste un champ sans effet, et le profil
|
|
|
|
|
# continuerait d'annoncer « tout le trafic » à la liste.
|
|
|
|
|
draft["default_route"] = False
|
|
|
|
|
print(f" {t(SERVER_ROUTES_NOTE)}")
|
[ADD] menu vpn : créer un profil, déposer les secrets, monter le tunnel
`vpn.py` savait tout faire en ligne de commande, mais il fallait écrire le
profil JSON à la main et déposer les secrets dans KeePassXC soi-même. Le menu
pose les questions que le pilote choisi déclare, et lui seul : ajouter une
technologie n'ajoute pas une ligne ici.
Le coffre est manipulé EN PROCESSUS — le mot de passe maître est déjà en
mémoire, le redemander à un sous-processus serait une saisie de plus à chaque
geste. Montage, démontage et diagnostic passent au contraire par `vpn.py` en
sous-processus : ils durent, ils parlent, et la sortie en direct est ce qui
rend une montée de tunnel suivable.
Vérifié : 37 tests sur le menu, aucune saisie réelle.
--- EN ---
`vpn.py` could already do everything from the command line, but the JSON
profile had to be written by hand and the secrets filed into KeePassXC by
hand. The menu asks the questions the chosen driver declares, and only those:
adding a technology adds no line here.
The vault is handled IN PROCESS — the master password is already in memory,
and asking a subprocess for it again would be one more entry at every step.
Raising, tearing down and diagnosing go through `vpn.py` as a subprocess
instead: they last, they talk, and live output is what makes a tunnel coming
up followable.
Checked: 37 tests on the menu, no real input.
Assisted-by: Claude Opus 5
2026-09-03 23:43:02 -04:00
|
|
|
draft["probe"] = self._vpn_ask(
|
|
|
|
|
t("Witness address reachable only through the tunnel (optional)"),
|
|
|
|
|
draft.get("probe", ""),
|
|
|
|
|
)
|
|
|
|
|
if self._is_yes(input(f"{t('Advanced settings? (y/N)')} : ")):
|
|
|
|
|
if driver_cls.uses_mtu:
|
|
|
|
|
draft["mtu"] = self._vpn_ask(
|
|
|
|
|
t("MTU"), str(draft.get("mtu", 1280))
|
|
|
|
|
)
|
|
|
|
|
self._vpn_ask_fields(draft, driver_cls, advanced=True)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
saved = profiles.save(draft)
|
|
|
|
|
except profiles.ProfileError as error:
|
|
|
|
|
print(f"\n✗ {t('Profile refused: ')}{error}")
|
|
|
|
|
return
|
|
|
|
|
print(f"\n✓ {t('Profile saved: ')}{saved['name']}")
|
|
|
|
|
if not saved["routes"] and not saved["default_route"]:
|
|
|
|
|
print(f" {t(NO_ROUTE_NOTE)}")
|
|
|
|
|
if driver_cls.secret_fields:
|
|
|
|
|
print(f" {t('Next step: store its secrets in the vault.')}")
|
|
|
|
|
else:
|
|
|
|
|
print(
|
|
|
|
|
f" {t('No secret to store: this one authenticates over SSH.')}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def _vpn_ask_fields(self, draft, driver_cls, advanced):
|
|
|
|
|
"""Déroule les champs déclarés par le pilote."""
|
|
|
|
|
for key, label, kind, is_advanced in driver_cls.form_fields:
|
|
|
|
|
if bool(is_advanced) != advanced:
|
|
|
|
|
continue
|
|
|
|
|
if kind == "flag":
|
|
|
|
|
draft[key] = self._vpn_ask_flag(
|
|
|
|
|
t(label), draft.get(key, False)
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
draft[key] = self._vpn_ask(
|
|
|
|
|
t(label), str(draft.get(key, "") or "")
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _vpn_pick_driver(current):
|
|
|
|
|
"""La technologie, par lettre, avec un conseil par ligne.
|
|
|
|
|
|
|
|
|
|
C'est la seule décision du formulaire où l'utilisateur a besoin
|
|
|
|
|
d'aide : le reste se déduit de ce que le site lui a donné.
|
|
|
|
|
|
|
|
|
|
Une étoile marque les technologies qu'aucun serveur réel n'a
|
|
|
|
|
encore validées, et une légende dit ce qu'elle signifie : la liste
|
|
|
|
|
montre autrement cinq choix d'apparence égale.
|
|
|
|
|
|
|
|
|
|
`[0] Retour` est là comme dans tous les menus de ce CLI : sans lui,
|
|
|
|
|
on est coincé dans le formulaire dès qu'on a tapé un nom de profil.
|
|
|
|
|
"""
|
|
|
|
|
names = list(DRIVERS)
|
|
|
|
|
if len(names) == 1:
|
|
|
|
|
return DRIVERS[names[0]]
|
|
|
|
|
default = current if current in DRIVERS else names[0]
|
|
|
|
|
print(f"\n{t('Which technology?')}")
|
|
|
|
|
unproven = False
|
|
|
|
|
for letter, name in zip(DRIVER_LETTERS, names):
|
|
|
|
|
driver_cls = DRIVERS[name]
|
|
|
|
|
mark = " ←" if name == default else ""
|
|
|
|
|
# L'étoile occupe une colonne à elle : sans cela, les lignes
|
|
|
|
|
# marquées décaleraient leur conseil et la liste se lirait mal.
|
|
|
|
|
star = " " if driver_cls.proven else "*"
|
|
|
|
|
unproven = unproven or not driver_cls.proven
|
|
|
|
|
print(
|
|
|
|
|
f"[{letter}] {driver_cls.label:<16}{star}"
|
|
|
|
|
f" {t(driver_cls.hint)}{mark}"
|
|
|
|
|
)
|
|
|
|
|
if unproven:
|
|
|
|
|
print(f" * {t(UNPROVEN_NOTE)}")
|
|
|
|
|
print(f"[0] {t('Back')}")
|
|
|
|
|
default_letter = DRIVER_LETTERS[names.index(default)]
|
|
|
|
|
answer = input(
|
|
|
|
|
f"{t('Choice')} [{default_letter} = {DRIVERS[default].label}] : "
|
|
|
|
|
).strip()
|
|
|
|
|
if not answer:
|
|
|
|
|
return DRIVERS[default]
|
|
|
|
|
if answer == "0":
|
|
|
|
|
return None
|
|
|
|
|
chosen = match_driver(answer, names)
|
|
|
|
|
if isinstance(chosen, list):
|
|
|
|
|
labels = ", ".join(DRIVERS[name].label for name in chosen)
|
|
|
|
|
print(f"{t('Several technologies match: ')}{labels}")
|
|
|
|
|
return None
|
|
|
|
|
if not chosen:
|
|
|
|
|
print(t("Unknown choice."))
|
|
|
|
|
return None
|
|
|
|
|
return DRIVERS[chosen]
|
|
|
|
|
|
|
|
|
|
def _vpn_delete_profile(self):
|
|
|
|
|
name = self._vpn_select_profile()
|
|
|
|
|
if not name:
|
|
|
|
|
return
|
|
|
|
|
if not self._is_yes(
|
|
|
|
|
input(f"{t('Delete profile')} « {name} » ? (y/N) : ")
|
|
|
|
|
):
|
|
|
|
|
return
|
|
|
|
|
if profiles.delete(name):
|
|
|
|
|
print(f"✓ {t('Profile deleted.')}")
|
|
|
|
|
print(f" {t('Its vault entry is kept: delete it in KeePassXC.')}")
|
|
|
|
|
else:
|
|
|
|
|
message = t(
|
|
|
|
|
"Not deletable here: this profile comes from a shared"
|
|
|
|
|
" configuration file."
|
|
|
|
|
)
|
|
|
|
|
print(f"✗ {message}")
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
# Secrets
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
def _vpn_store_secrets(self):
|
|
|
|
|
name = self._vpn_select_profile()
|
|
|
|
|
if not name:
|
|
|
|
|
return
|
|
|
|
|
profile = profiles.load(name)
|
|
|
|
|
driver_cls = get_driver(profile["driver"])
|
|
|
|
|
if driver_cls is None:
|
|
|
|
|
print(f"✗ {t('Unknown driver: ')}{profile['driver']}")
|
|
|
|
|
return
|
|
|
|
|
if not driver_cls.secret_fields:
|
|
|
|
|
print(
|
|
|
|
|
f"{t('No secret to store: this one authenticates over SSH.')}"
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
vault = VpnVault(self.config_file, self.kdbx_manager)
|
|
|
|
|
try:
|
|
|
|
|
path = vault.ensure_vault(ask=input)
|
|
|
|
|
except VaultError as error:
|
|
|
|
|
print(f"✗ {error}")
|
|
|
|
|
return
|
|
|
|
|
if not path:
|
|
|
|
|
print(t("No vault: nothing stored."))
|
|
|
|
|
return
|
|
|
|
|
if vault.master_password_is_stored():
|
|
|
|
|
print(f"\n! {t(MASTER_PASSWORD_WARNING)}")
|
|
|
|
|
|
|
|
|
|
title = profiles.secret_title(name)
|
|
|
|
|
fields = tuple(key for key, _, _ in driver_cls.secret_fields)
|
|
|
|
|
# Lu AVANT les invites, pour deux raisons : le mot de passe maître
|
|
|
|
|
# est alors demandé avant qu'on tape des secrets, et non après ; et
|
|
|
|
|
# chaque invite peut dire s'il y a déjà quelque chose derrière.
|
|
|
|
|
# « Une réponse vide garde la valeur en place » est un piège quand
|
|
|
|
|
# il n'y a rien en place.
|
|
|
|
|
try:
|
|
|
|
|
existing = vault.read(title, fields=fields)
|
|
|
|
|
except VaultError as error:
|
|
|
|
|
print(f"✗ {error}")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
print(f"\n{t('Vault entry')} : {title}")
|
[ADD] vpn openconnect : groupe d'URL, SSO délégué, mot de passe borné
Deux mécanismes désignent un service sur un concentrateur : un chemin
d'URL, une valeur de menu déroulant. Les confondre rend le formulaire d'un
AUTRE service — identifiants justes refusés, rien ne désignant le groupe.
Et certains ne comparent que les N premiers caractères du mot de passe,
que le profil déclare désormais sans jamais rien tronquer.
Une passerelle qui exige un navigateur intégré arrête openconnect sur
« No SSO handler », les distributions le bâtissant sans webview. Un greffon
fait l'étape web et rend un cookie ; le pilote monte lui-même, et le profil
garde son interface, son état et son diagnostic.
Vérifié : 249 tests unitaires, un tunnel monté contre une passerelle SAML.
--- EN ---
Two mechanisms designate a service on one concentrator: a URL path, and a
dropdown value. Confusing them hands over ANOTHER service's login form —
correct credentials refused, with nothing pointing at the group. And some
compare only the first N characters of the password, which the profile now
declares without ever truncating anything.
A gateway demanding an embedded browser stops openconnect on "No SSO
handler", distributions building it without a webview. A helper does the
web step and returns a cookie; the driver mounts the tunnel itself, so the
profile keeps its interface, its state and its diagnosis.
Checked: 249 unit tests, and one tunnel mounted against a SAML gateway.
Assisted-by: Claude Opus 5
2026-09-04 05:52:09 -04:00
|
|
|
print(f"{t('An empty answer keeps the stored value.')}")
|
|
|
|
|
# Les contraintes du pilote AVANT la première invite : une borne de
|
|
|
|
|
# longueur annoncée après coup coûte une deuxième saisie.
|
|
|
|
|
for note in driver_cls(profile).secret_notes():
|
|
|
|
|
print(f"! {note}")
|
|
|
|
|
print()
|
[ADD] menu vpn : créer un profil, déposer les secrets, monter le tunnel
`vpn.py` savait tout faire en ligne de commande, mais il fallait écrire le
profil JSON à la main et déposer les secrets dans KeePassXC soi-même. Le menu
pose les questions que le pilote choisi déclare, et lui seul : ajouter une
technologie n'ajoute pas une ligne ici.
Le coffre est manipulé EN PROCESSUS — le mot de passe maître est déjà en
mémoire, le redemander à un sous-processus serait une saisie de plus à chaque
geste. Montage, démontage et diagnostic passent au contraire par `vpn.py` en
sous-processus : ils durent, ils parlent, et la sortie en direct est ce qui
rend une montée de tunnel suivable.
Vérifié : 37 tests sur le menu, aucune saisie réelle.
--- EN ---
`vpn.py` could already do everything from the command line, but the JSON
profile had to be written by hand and the secrets filed into KeePassXC by
hand. The menu asks the questions the chosen driver declares, and only those:
adding a technology adds no line here.
The vault is handled IN PROCESS — the master password is already in memory,
and asking a subprocess for it again would be one more entry at every step.
Raising, tearing down and diagnosing go through `vpn.py` as a subprocess
instead: they last, they talk, and live output is what makes a tunnel coming
up followable.
Checked: 37 tests on the menu, no real input.
Assisted-by: Claude Opus 5
2026-09-03 23:43:02 -04:00
|
|
|
values = {}
|
|
|
|
|
if driver_cls.user_field:
|
|
|
|
|
# Recopié pour que le coffre reste LISIBLE dans KeePassXC ; le
|
|
|
|
|
# profil reste la source de vérité de l'identifiant.
|
|
|
|
|
values["username"] = profile.get(driver_cls.user_field, "")
|
|
|
|
|
for key, label, _required in driver_cls.secret_fields:
|
|
|
|
|
state = t("already set") if existing.get(key) else t("empty")
|
|
|
|
|
secret = self._vpn_ask_secret(f"{t(label)} [{state}]")
|
|
|
|
|
if secret is None:
|
|
|
|
|
return
|
|
|
|
|
if secret:
|
|
|
|
|
values[key] = secret
|
|
|
|
|
try:
|
|
|
|
|
vault.write(title, values)
|
|
|
|
|
except VaultError as error:
|
|
|
|
|
print(f"✗ {error}")
|
|
|
|
|
return
|
|
|
|
|
print(f"\n✓ {t('Secrets stored in the vault.')}")
|
|
|
|
|
|
|
|
|
|
# Ce qui reste vide et qui est OBLIGATOIRE : le dire ici, pas au
|
|
|
|
|
# premier montage raté.
|
|
|
|
|
absents = [
|
|
|
|
|
t(label)
|
|
|
|
|
for key, label, required in driver_cls.secret_fields
|
|
|
|
|
if required and not (values.get(key) or existing.get(key))
|
|
|
|
|
]
|
|
|
|
|
if absents:
|
|
|
|
|
print(
|
|
|
|
|
f"✗ {t('Still missing, the tunnel will not come up: ')}"
|
|
|
|
|
f"{', '.join(absents)}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _vpn_ask_secret(label):
|
|
|
|
|
"""Un secret, saisi deux fois, jamais affiché.
|
|
|
|
|
|
|
|
|
|
Deux fois parce qu'une faute de frappe dans un PSK ne se voit pas :
|
|
|
|
|
elle ressort en « no matching proposal » côté IKE, trois étages plus
|
|
|
|
|
loin, et fait chercher au mauvais endroit pendant une heure.
|
|
|
|
|
|
|
|
|
|
Rend "" pour « garder la valeur en place », None pour renoncer.
|
|
|
|
|
"""
|
|
|
|
|
first = getpass.getpass(f"{label} : ")
|
|
|
|
|
if not first:
|
|
|
|
|
return ""
|
|
|
|
|
if first != getpass.getpass(f"{t('Confirm')} : "):
|
|
|
|
|
print(f"✗ {t('The two entries differ, nothing stored.')}")
|
|
|
|
|
return None
|
|
|
|
|
return first
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _vpn_ask_flag(label, current):
|
|
|
|
|
"""Question oui/non dont le défaut est la valeur ACTUELLE.
|
|
|
|
|
|
|
|
|
|
Une réponse vide garde ce qui est en place : rééditer un profil pour
|
|
|
|
|
changer une route ne doit pas remettre le mode de routage à zéro.
|
|
|
|
|
"""
|
|
|
|
|
answer = input(f"{label} [{'O/n' if current else 'o/N'}] : ")
|
|
|
|
|
answer = answer.strip().lower()
|
|
|
|
|
if not answer:
|
|
|
|
|
return bool(current)
|
|
|
|
|
return answer in ("y", "yes", "o", "oui")
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _vpn_ask(label, default):
|
|
|
|
|
"""Question à réponse par défaut. Vide = on garde `default`."""
|
|
|
|
|
shown = f" [{default}]" if default else ""
|
|
|
|
|
answer = input(f"{label}{shown} : ").strip()
|
|
|
|
|
return answer or default
|