[ADD] vpn : cinq pilotes, secrets en coffre, diagnostic étagé
Le dépôt n'avait aucun moyen de monter un tunnel VPN ni de dire pourquoi il
refuse de monter. Cinq technologies libres, un pilote chacune, derrière un
`vpn.py` qui monte, démonte et diagnostique.
Ce qui n'est pas secret — hôte, utilisateur, routes, MTU — vit dans une
configuration JSON lisible ; clés pré-partagées et mots de passe vivent dans
un coffre KeePassXC. Un profil se montre et se partage sans donner de quoi
monter le tunnel. Les secrets s'écrivent en tmpfs sous 0700, jamais sur un
disque persistant. Le diagnostic part du noyau et remonte, pour que la
première ligne fausse soit la cause et non une conséquence.
Vérifié : 138 tests, dont le rendu de chaque fichier généré.
--- EN ---
The repository had no way to raise a VPN tunnel, nor to say why one refuses
to come up. Five free technologies, one driver each, behind a `vpn.py` that
raises, tears down and diagnoses.
What is not secret — host, user, routes, MTU — lives in readable JSON
configuration; pre-shared keys and passwords live in a KeePassXC vault. A
profile can be shown and shared without handing over the means to raise the
tunnel. Secrets are written to tmpfs at 0700, never to a persistent disk.
Diagnosis starts at the kernel and climbs, so the first false line is the
cause and not a consequence.
Checked: 138 tests, including the rendering of every generated file.
Assisted-by: Claude Opus 5
2026-09-03 23:42:49 -04:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
# © 2026 TechnoLibre (http://www.technolibre.ca)
|
|
|
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
|
|
|
"""OpenConnect : le seul pilote où aucun secret ne touche un fichier.
|
|
|
|
|
|
|
|
|
|
`--passwd-on-stdin` fait lire le mot de passe sur l'entrée standard. Il ne
|
|
|
|
|
passe donc ni par un argument (`/proc/<pid>/cmdline`, lisible par tous), ni
|
|
|
|
|
par un fichier, même en tmpfs. C'est le cas idéal, et il vaut la peine d'être
|
|
|
|
|
nommé : les autres pilotes composent avec des technologies qui EXIGENT un
|
|
|
|
|
fichier, celui-ci n'en a pas besoin.
|
|
|
|
|
|
|
|
|
|
Un client, plusieurs protocoles : AnyConnect (Cisco), Pulse/Juniper,
|
|
|
|
|
GlobalProtect (Palo Alto), Fortinet, F5, Array. `--protocol` décide.
|
|
|
|
|
|
|
|
|
|
`--non-inter` est passé volontairement. Sans lui, un certificat serveur
|
|
|
|
|
inconnu déclenche une question — et openconnect la lirait sur l'entrée
|
|
|
|
|
standard, celle par laquelle arrive justement le mot de passe. Le tunnel
|
|
|
|
|
échouerait sur un « certificate verify failed » incompréhensible. Avec
|
|
|
|
|
`--non-inter`, openconnect refuse tout de suite ET imprime la ligne
|
|
|
|
|
`--servercert sha256:…` à recopier dans le champ `oc_servercert` du profil.
|
|
|
|
|
|
|
|
|
|
Les routes appartiennent au serveur : c'est `vpnc-script` qui les pose, à
|
|
|
|
|
partir de ce que le concentrateur pousse. Le profil peut en AJOUTER, il ne
|
|
|
|
|
les remplace pas — d'où `needs_routes = False`.
|
|
|
|
|
|
[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
|
|
|
Deux « groupes » que rien ne distingue au premier regard
|
|
|
|
|
--------------------------------------------------------
|
|
|
|
|
Un même concentrateur héberge plusieurs services, et deux mécanismes tout
|
|
|
|
|
à fait différents servent à en désigner un. Les confondre ne donne pas une
|
|
|
|
|
erreur de syntaxe : cela donne le formulaire d'authentification d'un AUTRE
|
|
|
|
|
service, donc un refus d'identifiants sur des identifiants justes.
|
|
|
|
|
|
|
|
|
|
`oc_usergroup` → `--usergroup` : le CHEMIN D'URL de la connexion initiale.
|
|
|
|
|
`--usergroup=X` et `https://hôte/X` sont la même chose. C'est ce que porte
|
|
|
|
|
la balise `<UserGroup>` d'un profil AnyConnect (`.xml`), à ne pas confondre
|
|
|
|
|
avec son `<HostName>`, qui n'est qu'un libellé d'affichage.
|
|
|
|
|
|
|
|
|
|
`oc_authgroup` → `--authgroup` : une valeur à choisir dans un MENU
|
|
|
|
|
DÉROULANT du formulaire, quand le serveur en présente un. Cisco l'appelle
|
|
|
|
|
authgroup, Juniper et Fortinet realm, F5 domain, GlobalProtect gateway.
|
|
|
|
|
|
|
|
|
|
Un site qui remet un profil `.xml` désigne son service par le chemin ; un
|
|
|
|
|
site qui décrit une liste à choisir dans une capture d'écran désigne le
|
|
|
|
|
sien par le menu déroulant. Les deux champs coexistent parce que les deux
|
|
|
|
|
cas existent.
|
|
|
|
|
|
[ADD] vpn : cinq pilotes, secrets en coffre, diagnostic étagé
Le dépôt n'avait aucun moyen de monter un tunnel VPN ni de dire pourquoi il
refuse de monter. Cinq technologies libres, un pilote chacune, derrière un
`vpn.py` qui monte, démonte et diagnostique.
Ce qui n'est pas secret — hôte, utilisateur, routes, MTU — vit dans une
configuration JSON lisible ; clés pré-partagées et mots de passe vivent dans
un coffre KeePassXC. Un profil se montre et se partage sans donner de quoi
monter le tunnel. Les secrets s'écrivent en tmpfs sous 0700, jamais sur un
disque persistant. Le diagnostic part du noyau et remonte, pour que la
première ligne fausse soit la cause et non une conséquence.
Vérifié : 138 tests, dont le rendu de chaque fichier généré.
--- EN ---
The repository had no way to raise a VPN tunnel, nor to say why one refuses
to come up. Five free technologies, one driver each, behind a `vpn.py` that
raises, tears down and diagnoses.
What is not secret — host, user, routes, MTU — lives in readable JSON
configuration; pre-shared keys and passwords live in a KeePassXC vault. A
profile can be shown and shared without handing over the means to raise the
tunnel. Secrets are written to tmpfs at 0700, never to a persistent disk.
Diagnosis starts at the kernel and climbs, so the first false line is the
cause and not a consequence.
Checked: 138 tests, including the rendering of every generated file.
Assisted-by: Claude Opus 5
2026-09-03 23:42:49 -04:00
|
|
|
SSO / SAML — le cas du « formulaire web »
|
|
|
|
|
-----------------------------------------
|
|
|
|
|
Quand le concentrateur authentifie par un fournisseur d'identité (Azure AD,
|
[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
|
|
|
Okta, Duo…), il n'y a pas de mot de passe à envoyer : il faut compléter une
|
|
|
|
|
page web. Cisco annonce ce besoin de DEUX façons, et openconnect n'en sait
|
|
|
|
|
traiter qu'une seule depuis un CLI :
|
|
|
|
|
|
|
|
|
|
· `single-sign-on-external-browser` — openconnect lance le programme donné à
|
|
|
|
|
`--external-browser`, écoute sur son port local 29786 et attend la
|
|
|
|
|
redirection. Fonctionne avec un paquet de distribution ;
|
|
|
|
|
· `sso-v2` — le concentrateur exige un navigateur INTÉGRÉ au client. Il faut
|
|
|
|
|
alors une webview compilée dans openconnect (libwebkit2gtk), et Debian,
|
|
|
|
|
Ubuntu, Fedora et Arch la compilent tous sans. openconnect s'arrête sur
|
|
|
|
|
« No SSO handler », et `--external-browser` n'y change rien : il ne prend
|
|
|
|
|
ce chemin que si le SERVEUR a annoncé la méthode « navigateur externe ».
|
|
|
|
|
|
|
|
|
|
C'est la passerelle qui choisit, groupe de connexion par groupe de connexion.
|
|
|
|
|
Pour le second cas, `oc_sso_helper` délègue la seule étape que ce pilote ne
|
|
|
|
|
sait pas faire.
|
|
|
|
|
|
|
|
|
|
Déléguer l'authentification, garder le tunnel
|
|
|
|
|
---------------------------------------------
|
|
|
|
|
`openconnect-sso --authenticate json` pilote un vrai navigateur, laisse
|
|
|
|
|
l'humain s'authentifier, et rend `{host, cookie, fingerprint}` sans monter
|
|
|
|
|
aucun tunnel. Le pilote reprend alors la main et monte lui-même, avec
|
|
|
|
|
`--cookie-on-stdin`.
|
|
|
|
|
|
|
|
|
|
La frontière est là, et elle est le tout de ce dispositif : le greffon ne
|
|
|
|
|
fait que la danse SAML ; le PROFIL reste la source de vérité pour le nom
|
|
|
|
|
d'interface, les routes ajoutées, les fichiers d'état et le diagnostic. Un
|
|
|
|
|
tunnel monté par le greffon lui-même s'appellerait `tun0`, ne laisserait
|
|
|
|
|
rien dans /run, et `status`, `diagnose` et `down` ne le verraient pas.
|
|
|
|
|
|
|
|
|
|
Deux détails qui font échouer le cookie si on les néglige :
|
|
|
|
|
|
|
|
|
|
· l'IDENTITÉ annoncée doit être la même aux deux étapes. Le concentrateur
|
|
|
|
|
délivre le cookie à un client qui s'est présenté sous une version donnée ;
|
|
|
|
|
monter ensuite sous une autre le fait refuser. D'où `oc_ac_version`, passé
|
|
|
|
|
au greffon ET à openconnect ;
|
|
|
|
|
· l'EMPREINTE que rend le greffon est celle contre laquelle il a authentifié.
|
|
|
|
|
Elle est préférée à `oc_servercert` du profil, qui peut dater. Beaucoup de
|
|
|
|
|
ces passerelles présentent une chaîne que le magasin du système ne valide
|
|
|
|
|
pas (« signer not found »), et `--non-inter` la refuserait sans elle.
|
|
|
|
|
|
|
|
|
|
Le greffon lance un navigateur : il tourne donc SANS sudo, sous
|
|
|
|
|
l'utilisateur, avec son affichage. Le cookie qu'il rend ne touche aucun
|
|
|
|
|
fichier — il vit dans une variable, part par l'entrée standard, et est
|
|
|
|
|
masqué de tout affichage dès qu'il existe.
|
|
|
|
|
|
|
|
|
|
Les concentrateurs qui tronquent le mot de passe
|
|
|
|
|
------------------------------------------------
|
|
|
|
|
Certains ne comparent que les N premiers caractères — un reste d'annuaire
|
|
|
|
|
qui borne la longueur, dont le site documente la valeur. Un mot de passe
|
|
|
|
|
plus long est alors refusé, et le refus se lit « identifiants invalides » :
|
|
|
|
|
rien n'y met la longueur en cause, et on cherche du côté du groupe
|
|
|
|
|
d'authentification ou du certificat.
|
|
|
|
|
|
|
|
|
|
`oc_password_len` déclare cette borne. Le champ ne TRONQUE rien : le coffre
|
|
|
|
|
reste la source de vérité de ce qu'on envoie, et un outil qui couperait un
|
|
|
|
|
mot de passe en silence rendrait un jour un « ça marchait pourtant »
|
|
|
|
|
indébrouillable — le jour où le site lève la limite. Il fait deux choses,
|
|
|
|
|
toutes deux à un moment où l'humain peut agir : le menu l'annonce avant la
|
|
|
|
|
saisie du secret, et le montage compare les longueurs si ce qui est déposé
|
|
|
|
|
la dépasse. Zéro = aucune limite.
|
[ADD] vpn : cinq pilotes, secrets en coffre, diagnostic étagé
Le dépôt n'avait aucun moyen de monter un tunnel VPN ni de dire pourquoi il
refuse de monter. Cinq technologies libres, un pilote chacune, derrière un
`vpn.py` qui monte, démonte et diagnostique.
Ce qui n'est pas secret — hôte, utilisateur, routes, MTU — vit dans une
configuration JSON lisible ; clés pré-partagées et mots de passe vivent dans
un coffre KeePassXC. Un profil se montre et se partage sans donner de quoi
monter le tunnel. Les secrets s'écrivent en tmpfs sous 0700, jamais sur un
disque persistant. Le diagnostic part du noyau et remonte, pour que la
première ligne fausse soit la cause et non une conséquence.
Vérifié : 138 tests, dont le rendu de chaque fichier généré.
--- EN ---
The repository had no way to raise a VPN tunnel, nor to say why one refuses
to come up. Five free technologies, one driver each, behind a `vpn.py` that
raises, tears down and diagnoses.
What is not secret — host, user, routes, MTU — lives in readable JSON
configuration; pre-shared keys and passwords live in a KeePassXC vault. A
profile can be shown and shared without handing over the means to raise the
tunnel. Secrets are written to tmpfs at 0700, never to a persistent disk.
Diagnosis starts at the kernel and climbs, so the first false line is the
cause and not a consequence.
Checked: 138 tests, including the rendering of every generated file.
Assisted-by: Claude Opus 5
2026-09-03 23:42:49 -04:00
|
|
|
"""
|
[REF] format : passer l'outillage et les tests sous ruff
Le formateur de ce dépôt est ruff depuis qu'il remplace black, qui ne connaît
aucune cible au-delà de py313 ; ce passage applique sa norme à l'arbre entier,
d'un coup, pour qu'aucun commit de fond n'ait à porter du style. L'écart tient
presque entièrement aux chaînes coupées à la main que ruff recolle quand elles
tiennent sur une ligne, et aux « with » multiples qu'il regroupe : aucune
valeur ne change, et les clés de traduction non plus.
Vérifié : la suite unitaire reste verte après le passage, et le contrôle de
syntaxe ne signale rien.
--- EN ---
This repository's formatter is ruff since it replaced black, which knows no
target beyond py313; this pass applies its standard to the whole tree at once,
so that no substantive commit has to carry style. The difference is almost
entirely the hand-split strings ruff joins back when they fit on one line, and
the multiple "with" it merges: no value changes, nor do the translation keys.
Checked: the unit suite stays green after the pass, and the syntax check
reports nothing.
Assisted-by: Claude Opus 5
2026-09-24 13:30:31 -04:00
|
|
|
|
[ADD] vpn : cinq pilotes, secrets en coffre, diagnostic étagé
Le dépôt n'avait aucun moyen de monter un tunnel VPN ni de dire pourquoi il
refuse de monter. Cinq technologies libres, un pilote chacune, derrière un
`vpn.py` qui monte, démonte et diagnostique.
Ce qui n'est pas secret — hôte, utilisateur, routes, MTU — vit dans une
configuration JSON lisible ; clés pré-partagées et mots de passe vivent dans
un coffre KeePassXC. Un profil se montre et se partage sans donner de quoi
monter le tunnel. Les secrets s'écrivent en tmpfs sous 0700, jamais sur un
disque persistant. Le diagnostic part du noyau et remonte, pour que la
première ligne fausse soit la cause et non une conséquence.
Vérifié : 138 tests, dont le rendu de chaque fichier généré.
--- EN ---
The repository had no way to raise a VPN tunnel, nor to say why one refuses
to come up. Five free technologies, one driver each, behind a `vpn.py` that
raises, tears down and diagnoses.
What is not secret — host, user, routes, MTU — lives in readable JSON
configuration; pre-shared keys and passwords live in a KeePassXC vault. A
profile can be shown and shared without handing over the means to raise the
tunnel. Secrets are written to tmpfs at 0700, never to a persistent disk.
Diagnosis starts at the kernel and climbs, so the first false line is the
cause and not a consequence.
Checked: 138 tests, including the rendering of every generated file.
Assisted-by: Claude Opus 5
2026-09-03 23:42:49 -04:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
[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 json
|
|
|
|
|
import os
|
[ADD] vpn : cinq pilotes, secrets en coffre, diagnostic étagé
Le dépôt n'avait aucun moyen de monter un tunnel VPN ni de dire pourquoi il
refuse de monter. Cinq technologies libres, un pilote chacune, derrière un
`vpn.py` qui monte, démonte et diagnostique.
Ce qui n'est pas secret — hôte, utilisateur, routes, MTU — vit dans une
configuration JSON lisible ; clés pré-partagées et mots de passe vivent dans
un coffre KeePassXC. Un profil se montre et se partage sans donner de quoi
monter le tunnel. Les secrets s'écrivent en tmpfs sous 0700, jamais sur un
disque persistant. Le diagnostic part du noyau et remonte, pour que la
première ligne fausse soit la cause et non une conséquence.
Vérifié : 138 tests, dont le rendu de chaque fichier généré.
--- EN ---
The repository had no way to raise a VPN tunnel, nor to say why one refuses
to come up. Five free technologies, one driver each, behind a `vpn.py` that
raises, tears down and diagnoses.
What is not secret — host, user, routes, MTU — lives in readable JSON
configuration; pre-shared keys and passwords live in a KeePassXC vault. A
profile can be shown and shared without handing over the means to raise the
tunnel. Secrets are written to tmpfs at 0700, never to a persistent disk.
Diagnosis starts at the kernel and climbs, so the first false line is the
cause and not a consequence.
Checked: 138 tests, including the rendering of every generated file.
Assisted-by: Claude Opus 5
2026-09-03 23:42:49 -04:00
|
|
|
import shlex
|
[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 shutil
|
[ADD] vpn : cinq pilotes, secrets en coffre, diagnostic étagé
Le dépôt n'avait aucun moyen de monter un tunnel VPN ni de dire pourquoi il
refuse de monter. Cinq technologies libres, un pilote chacune, derrière un
`vpn.py` qui monte, démonte et diagnostique.
Ce qui n'est pas secret — hôte, utilisateur, routes, MTU — vit dans une
configuration JSON lisible ; clés pré-partagées et mots de passe vivent dans
un coffre KeePassXC. Un profil se montre et se partage sans donner de quoi
monter le tunnel. Les secrets s'écrivent en tmpfs sous 0700, jamais sur un
disque persistant. Le diagnostic part du noyau et remonte, pour que la
première ligne fausse soit la cause et non une conséquence.
Vérifié : 138 tests, dont le rendu de chaque fichier généré.
--- EN ---
The repository had no way to raise a VPN tunnel, nor to say why one refuses
to come up. Five free technologies, one driver each, behind a `vpn.py` that
raises, tears down and diagnoses.
What is not secret — host, user, routes, MTU — lives in readable JSON
configuration; pre-shared keys and passwords live in a KeePassXC vault. A
profile can be shown and shared without handing over the means to raise the
tunnel. Secrets are written to tmpfs at 0700, never to a persistent disk.
Diagnosis starts at the kernel and climbs, so the first false line is the
cause and not a consequence.
Checked: 138 tests, including the rendering of every generated file.
Assisted-by: Claude Opus 5
2026-09-03 23:42:49 -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
|
|
|
from script.todo.todo_i18n import t
|
[ADD] vpn : cinq pilotes, secrets en coffre, diagnostic étagé
Le dépôt n'avait aucun moyen de monter un tunnel VPN ni de dire pourquoi il
refuse de monter. Cinq technologies libres, un pilote chacune, derrière un
`vpn.py` qui monte, démonte et diagnostique.
Ce qui n'est pas secret — hôte, utilisateur, routes, MTU — vit dans une
configuration JSON lisible ; clés pré-partagées et mots de passe vivent dans
un coffre KeePassXC. Un profil se montre et se partage sans donner de quoi
monter le tunnel. Les secrets s'écrivent en tmpfs sous 0700, jamais sur un
disque persistant. Le diagnostic part du noyau et remonte, pour que la
première ligne fausse soit la cause et non une conséquence.
Vérifié : 138 tests, dont le rendu de chaque fichier généré.
--- EN ---
The repository had no way to raise a VPN tunnel, nor to say why one refuses
to come up. Five free technologies, one driver each, behind a `vpn.py` that
raises, tears down and diagnoses.
What is not secret — host, user, routes, MTU — lives in readable JSON
configuration; pre-shared keys and passwords live in a KeePassXC vault. A
profile can be shown and shared without handing over the means to raise the
tunnel. Secrets are written to tmpfs at 0700, never to a persistent disk.
Diagnosis starts at the kernel and climbs, so the first false line is the
cause and not a consequence.
Checked: 138 tests, including the rendering of every generated file.
Assisted-by: Claude Opus 5
2026-09-03 23:42:49 -04:00
|
|
|
from script.vpn import valid
|
|
|
|
|
from script.vpn.drivers.base import (
|
|
|
|
|
VpnDriver,
|
|
|
|
|
interface_exists,
|
[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
|
|
|
wait_for_interface_address,
|
|
|
|
|
)
|
|
|
|
|
from script.vpn.vault import PLACEHOLDER
|
|
|
|
|
|
|
|
|
|
# Emplacements conventionnels du greffon SSO, essayés dans l'ordre après
|
|
|
|
|
# le PATH. `pipx install openconnect-sso` pose le premier ; une install en
|
|
|
|
|
# environnement virtuel dédié n'est trouvable que par `oc_sso_helper`.
|
|
|
|
|
SSO_HELPER_NAME = "openconnect-sso"
|
|
|
|
|
SSO_HELPER_PATHS = (
|
|
|
|
|
"~/.local/bin/openconnect-sso",
|
|
|
|
|
"~/.local/share/openconnect-sso-venv/bin/openconnect-sso",
|
[ADD] vpn : cinq pilotes, secrets en coffre, diagnostic étagé
Le dépôt n'avait aucun moyen de monter un tunnel VPN ni de dire pourquoi il
refuse de monter. Cinq technologies libres, un pilote chacune, derrière un
`vpn.py` qui monte, démonte et diagnostique.
Ce qui n'est pas secret — hôte, utilisateur, routes, MTU — vit dans une
configuration JSON lisible ; clés pré-partagées et mots de passe vivent dans
un coffre KeePassXC. Un profil se montre et se partage sans donner de quoi
monter le tunnel. Les secrets s'écrivent en tmpfs sous 0700, jamais sur un
disque persistant. Le diagnostic part du noyau et remonte, pour que la
première ligne fausse soit la cause et non une conséquence.
Vérifié : 138 tests, dont le rendu de chaque fichier généré.
--- EN ---
The repository had no way to raise a VPN tunnel, nor to say why one refuses
to come up. Five free technologies, one driver each, behind a `vpn.py` that
raises, tears down and diagnoses.
What is not secret — host, user, routes, MTU — lives in readable JSON
configuration; pre-shared keys and passwords live in a KeePassXC vault. A
profile can be shown and shared without handing over the means to raise the
tunnel. Secrets are written to tmpfs at 0700, never to a persistent disk.
Diagnosis starts at the kernel and climbs, so the first false line is the
cause and not a consequence.
Checked: 138 tests, including the rendering of every generated file.
Assisted-by: Claude Opus 5
2026-09-03 23:42:49 -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
|
|
|
# Réglages de rendu que le greffon hérite, SAUF s'ils sont déjà dans
|
|
|
|
|
# l'environnement — qui les a posés sait mieux.
|
|
|
|
|
#
|
|
|
|
|
# Le navigateur du greffon est un Chromium embarqué. Sur une machine
|
|
|
|
|
# virtuelle, il n'y a pas d'accélération exploitable : il se rabat sur
|
|
|
|
|
# Vulkan, échoue à importer sa mémoire graphique et la fenêtre MEURT au
|
|
|
|
|
# milieu de l'authentification. Le rendu logiciel est plus lent et il
|
|
|
|
|
# aboutit, ce qui est le seul critère ici.
|
|
|
|
|
SSO_RENDER_ENV = {
|
|
|
|
|
"QTWEBENGINE_CHROMIUM_FLAGS": (
|
|
|
|
|
"--disable-gpu --disable-gpu-compositing"
|
|
|
|
|
" --disable-features=Vulkan --disable-dev-shm-usage"
|
|
|
|
|
),
|
|
|
|
|
"LIBGL_ALWAYS_SOFTWARE": "1",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
SSO_RENDER_ENV_HINT = " ".join(f"{k}={v!r}" for k, v in SSO_RENDER_ENV.items())
|
|
|
|
|
|
[ADD] vpn : cinq pilotes, secrets en coffre, diagnostic étagé
Le dépôt n'avait aucun moyen de monter un tunnel VPN ni de dire pourquoi il
refuse de monter. Cinq technologies libres, un pilote chacune, derrière un
`vpn.py` qui monte, démonte et diagnostique.
Ce qui n'est pas secret — hôte, utilisateur, routes, MTU — vit dans une
configuration JSON lisible ; clés pré-partagées et mots de passe vivent dans
un coffre KeePassXC. Un profil se montre et se partage sans donner de quoi
monter le tunnel. Les secrets s'écrivent en tmpfs sous 0700, jamais sur un
disque persistant. Le diagnostic part du noyau et remonte, pour que la
première ligne fausse soit la cause et non une conséquence.
Vérifié : 138 tests, dont le rendu de chaque fichier généré.
--- EN ---
The repository had no way to raise a VPN tunnel, nor to say why one refuses
to come up. Five free technologies, one driver each, behind a `vpn.py` that
raises, tears down and diagnoses.
What is not secret — host, user, routes, MTU — lives in readable JSON
configuration; pre-shared keys and passwords live in a KeePassXC vault. A
profile can be shown and shared without handing over the means to raise the
tunnel. Secrets are written to tmpfs at 0700, never to a persistent disk.
Diagnosis starts at the kernel and climbs, so the first false line is the
cause and not a consequence.
Checked: 138 tests, including the rendering of every generated file.
Assisted-by: Claude Opus 5
2026-09-03 23:42:49 -04:00
|
|
|
# Ce que ce client sait parler. La liste vient de `openconnect --protocol`.
|
|
|
|
|
PROTOCOLS = ("anyconnect", "nc", "pulse", "gp", "f5", "fortinet", "array")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OpenconnectDriver(VpnDriver):
|
|
|
|
|
name = "openconnect"
|
|
|
|
|
label = "OpenConnect"
|
|
|
|
|
binaries = ("openconnect", "ip")
|
|
|
|
|
# Non obligatoire : en SSO il n'y a AUCUN mot de passe à déposer, et le
|
|
|
|
|
# menu ne doit pas réclamer un secret que la méthode n'utilise pas. La
|
|
|
|
|
# vraie exigence dépend du mode, elle est donc dans `up`.
|
|
|
|
|
secret_fields = (("password", "VPN password", False),)
|
|
|
|
|
iface_kind = "tun"
|
|
|
|
|
hint = "Cisco AnyConnect, Pulse, GlobalProtect, Fortinet appliances"
|
|
|
|
|
user_field = "oc_user"
|
|
|
|
|
# Le MTU vient du serveur (ou du .ovpn), pas du profil.
|
|
|
|
|
uses_mtu = False
|
|
|
|
|
# Le serveur pousse les routes : exiger une route déclarée serait une
|
|
|
|
|
# fausse exigence.
|
|
|
|
|
needs_routes = False
|
[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
|
|
|
# Et pour la même raison, la route par défaut ne se demande pas ici :
|
|
|
|
|
# c'est le concentrateur qui décide de ce qui entre dans le tunnel, et
|
|
|
|
|
# `vpnc-script` pose ce qu'il pousse. La forcer côté client contre une
|
|
|
|
|
# passerelle en tunnel scindé ne donnerait pas « tout le trafic » mais
|
|
|
|
|
# un trou noir — la passerelle ne route pas ce qu'elle n'a pas annoncé.
|
|
|
|
|
uses_default_route = False
|
[ADD] vpn : cinq pilotes, secrets en coffre, diagnostic étagé
Le dépôt n'avait aucun moyen de monter un tunnel VPN ni de dire pourquoi il
refuse de monter. Cinq technologies libres, un pilote chacune, derrière un
`vpn.py` qui monte, démonte et diagnostique.
Ce qui n'est pas secret — hôte, utilisateur, routes, MTU — vit dans une
configuration JSON lisible ; clés pré-partagées et mots de passe vivent dans
un coffre KeePassXC. Un profil se montre et se partage sans donner de quoi
monter le tunnel. Les secrets s'écrivent en tmpfs sous 0700, jamais sur un
disque persistant. Le diagnostic part du noyau et remonte, pour que la
première ligne fausse soit la cause et non une conséquence.
Vérifié : 138 tests, dont le rendu de chaque fichier généré.
--- EN ---
The repository had no way to raise a VPN tunnel, nor to say why one refuses
to come up. Five free technologies, one driver each, behind a `vpn.py` that
raises, tears down and diagnoses.
What is not secret — host, user, routes, MTU — lives in readable JSON
configuration; pre-shared keys and passwords live in a KeePassXC vault. A
profile can be shown and shared without handing over the means to raise the
tunnel. Secrets are written to tmpfs at 0700, never to a persistent disk.
Diagnosis starts at the kernel and climbs, so the first false line is the
cause and not a consequence.
Checked: 138 tests, including the rendering of every generated file.
Assisted-by: Claude Opus 5
2026-09-03 23:42:49 -04:00
|
|
|
defaults = {
|
|
|
|
|
"port": 443,
|
|
|
|
|
"oc_user": "",
|
|
|
|
|
"oc_protocol": "anyconnect",
|
|
|
|
|
"oc_authgroup": "",
|
|
|
|
|
"oc_servercert": "",
|
|
|
|
|
# SSO : le concentrateur authentifie par un fournisseur d'identité,
|
|
|
|
|
# dans un navigateur. Voir l'en-tête du fichier.
|
|
|
|
|
"oc_sso": False,
|
|
|
|
|
# Programme lancé avec l'URL de connexion. Vide = `echo`, qui
|
|
|
|
|
# l'affiche : c'est ce qu'on veut sur une machine sans écran.
|
|
|
|
|
"oc_external_browser": "",
|
[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
|
|
|
# Nombre de caractères du mot de passe que le concentrateur compare.
|
|
|
|
|
# 0 = aucune limite. Voir l'en-tête du fichier.
|
|
|
|
|
"oc_password_len": 0,
|
|
|
|
|
# Chemin d'URL de la connexion initiale. Voir l'en-tête : ce n'est
|
|
|
|
|
# PAS `oc_authgroup`, et les confondre mène à un formulaire
|
|
|
|
|
# d'authentification qui n'est pas celui du service visé.
|
|
|
|
|
"oc_usergroup": "",
|
|
|
|
|
# Greffon qui pilote un navigateur pour l'étape SAML. Vide : cherché
|
|
|
|
|
# dans le PATH puis aux emplacements conventionnels. Voir l'en-tête.
|
|
|
|
|
"oc_sso_helper": "",
|
|
|
|
|
# Version de client AnyConnect annoncée. Elle doit être la MÊME à
|
|
|
|
|
# l'authentification et au montage : le concentrateur délivre le
|
|
|
|
|
# cookie à un client qui s'est présenté ainsi.
|
|
|
|
|
"oc_ac_version": "4.7.00136",
|
[ADD] vpn : cinq pilotes, secrets en coffre, diagnostic étagé
Le dépôt n'avait aucun moyen de monter un tunnel VPN ni de dire pourquoi il
refuse de monter. Cinq technologies libres, un pilote chacune, derrière un
`vpn.py` qui monte, démonte et diagnostique.
Ce qui n'est pas secret — hôte, utilisateur, routes, MTU — vit dans une
configuration JSON lisible ; clés pré-partagées et mots de passe vivent dans
un coffre KeePassXC. Un profil se montre et se partage sans donner de quoi
monter le tunnel. Les secrets s'écrivent en tmpfs sous 0700, jamais sur un
disque persistant. Le diagnostic part du noyau et remonte, pour que la
première ligne fausse soit la cause et non une conséquence.
Vérifié : 138 tests, dont le rendu de chaque fichier généré.
--- EN ---
The repository had no way to raise a VPN tunnel, nor to say why one refuses
to come up. Five free technologies, one driver each, behind a `vpn.py` that
raises, tears down and diagnoses.
What is not secret — host, user, routes, MTU — lives in readable JSON
configuration; pre-shared keys and passwords live in a KeePassXC vault. A
profile can be shown and shared without handing over the means to raise the
tunnel. Secrets are written to tmpfs at 0700, never to a persistent disk.
Diagnosis starts at the kernel and climbs, so the first false line is the
cause and not a consequence.
Checked: 138 tests, including the rendering of every generated file.
Assisted-by: Claude Opus 5
2026-09-03 23:42:49 -04:00
|
|
|
}
|
|
|
|
|
form_fields = (
|
|
|
|
|
("oc_user", "VPN user", "text", False),
|
|
|
|
|
(
|
|
|
|
|
"oc_protocol",
|
|
|
|
|
"Protocol (anyconnect, nc, pulse, gp, f5, fortinet, array)",
|
|
|
|
|
"text",
|
|
|
|
|
False,
|
|
|
|
|
),
|
[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
|
|
|
(
|
|
|
|
|
"oc_usergroup",
|
|
|
|
|
"Connection group in the URL — <UserGroup> of an AnyConnect"
|
|
|
|
|
" profile (optional)",
|
|
|
|
|
"text",
|
|
|
|
|
False,
|
|
|
|
|
),
|
[ADD] vpn : cinq pilotes, secrets en coffre, diagnostic étagé
Le dépôt n'avait aucun moyen de monter un tunnel VPN ni de dire pourquoi il
refuse de monter. Cinq technologies libres, un pilote chacune, derrière un
`vpn.py` qui monte, démonte et diagnostique.
Ce qui n'est pas secret — hôte, utilisateur, routes, MTU — vit dans une
configuration JSON lisible ; clés pré-partagées et mots de passe vivent dans
un coffre KeePassXC. Un profil se montre et se partage sans donner de quoi
monter le tunnel. Les secrets s'écrivent en tmpfs sous 0700, jamais sur un
disque persistant. Le diagnostic part du noyau et remonte, pour que la
première ligne fausse soit la cause et non une conséquence.
Vérifié : 138 tests, dont le rendu de chaque fichier généré.
--- EN ---
The repository had no way to raise a VPN tunnel, nor to say why one refuses
to come up. Five free technologies, one driver each, behind a `vpn.py` that
raises, tears down and diagnoses.
What is not secret — host, user, routes, MTU — lives in readable JSON
configuration; pre-shared keys and passwords live in a KeePassXC vault. A
profile can be shown and shared without handing over the means to raise the
tunnel. Secrets are written to tmpfs at 0700, never to a persistent disk.
Diagnosis starts at the kernel and climbs, so the first false line is the
cause and not a consequence.
Checked: 138 tests, including the rendering of every generated file.
Assisted-by: Claude Opus 5
2026-09-03 23:42:49 -04:00
|
|
|
(
|
|
|
|
|
"oc_authgroup",
|
|
|
|
|
"Authentication group / realm (optional)",
|
|
|
|
|
"text",
|
|
|
|
|
True,
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
"oc_servercert",
|
|
|
|
|
"Pinned server certificate (sha256:... , printed on first refusal)",
|
|
|
|
|
"text",
|
|
|
|
|
True,
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
"oc_sso",
|
|
|
|
|
"Authentication through a web form (SAML / SSO)?",
|
|
|
|
|
"flag",
|
|
|
|
|
False,
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
"oc_external_browser",
|
|
|
|
|
"Browser command for SSO (empty: show the URL to open yourself)",
|
|
|
|
|
"path",
|
|
|
|
|
True,
|
|
|
|
|
),
|
[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
|
|
|
(
|
|
|
|
|
"oc_sso_helper",
|
|
|
|
|
"SSO helper driving a real browser (empty: openconnect-sso from"
|
|
|
|
|
" the PATH)",
|
|
|
|
|
"path",
|
|
|
|
|
True,
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
"oc_ac_version",
|
|
|
|
|
"AnyConnect version announced to the gateway",
|
|
|
|
|
"text",
|
|
|
|
|
True,
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
"oc_password_len",
|
|
|
|
|
"Password characters the server compares (0: no limit)",
|
|
|
|
|
"int",
|
|
|
|
|
True,
|
|
|
|
|
),
|
[ADD] vpn : cinq pilotes, secrets en coffre, diagnostic étagé
Le dépôt n'avait aucun moyen de monter un tunnel VPN ni de dire pourquoi il
refuse de monter. Cinq technologies libres, un pilote chacune, derrière un
`vpn.py` qui monte, démonte et diagnostique.
Ce qui n'est pas secret — hôte, utilisateur, routes, MTU — vit dans une
configuration JSON lisible ; clés pré-partagées et mots de passe vivent dans
un coffre KeePassXC. Un profil se montre et se partage sans donner de quoi
monter le tunnel. Les secrets s'écrivent en tmpfs sous 0700, jamais sur un
disque persistant. Le diagnostic part du noyau et remonte, pour que la
première ligne fausse soit la cause et non une conséquence.
Vérifié : 138 tests, dont le rendu de chaque fichier généré.
--- EN ---
The repository had no way to raise a VPN tunnel, nor to say why one refuses
to come up. Five free technologies, one driver each, behind a `vpn.py` that
raises, tears down and diagnoses.
What is not secret — host, user, routes, MTU — lives in readable JSON
configuration; pre-shared keys and passwords live in a KeePassXC vault. A
profile can be shown and shared without handing over the means to raise the
tunnel. Secrets are written to tmpfs at 0700, never to a persistent disk.
Diagnosis starts at the kernel and climbs, so the first false line is the
cause and not a consequence.
Checked: 138 tests, including the rendering of every generated file.
Assisted-by: Claude Opus 5
2026-09-03 23:42:49 -04:00
|
|
|
("port", "HTTPS port", "int", True),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
@property
|
|
|
|
|
def iface(self):
|
|
|
|
|
"""Interface NOMMÉE, et non découverte : openconnect sait le faire
|
|
|
|
|
(`--interface`), et un nom connu d'avance rend `status` fiable même
|
|
|
|
|
après un redémarrage du CLI. Tronqué à 15 caractères."""
|
|
|
|
|
return f"vpn-{self.name_tag}"[:15]
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def validate_profile(cls, profile):
|
|
|
|
|
# Le mode d'abord : en SSO, c'est le fournisseur d'identité qui
|
|
|
|
|
# décide de qui on est, et exiger un utilisateur ici refuserait un
|
|
|
|
|
# profil parfaitement valide.
|
|
|
|
|
valid.flag(profile, "oc_sso")
|
|
|
|
|
valid.text(
|
|
|
|
|
profile,
|
|
|
|
|
"oc_user",
|
|
|
|
|
"Utilisateur VPN",
|
|
|
|
|
required=not profile["oc_sso"],
|
|
|
|
|
)
|
|
|
|
|
protocol = valid.text(profile, "oc_protocol", "Protocole")
|
|
|
|
|
if protocol not in PROTOCOLS:
|
|
|
|
|
raise valid.ProfileError(
|
|
|
|
|
f"Protocole inconnu : « {protocol} »."
|
|
|
|
|
f" Connus : {', '.join(PROTOCOLS)}."
|
|
|
|
|
)
|
[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
|
|
|
valid.text(
|
|
|
|
|
profile,
|
|
|
|
|
"oc_usergroup",
|
|
|
|
|
"Groupe de connexion (chemin d'URL)",
|
|
|
|
|
required=False,
|
|
|
|
|
pattern=valid.URL_PATH_RE,
|
|
|
|
|
)
|
[ADD] vpn : cinq pilotes, secrets en coffre, diagnostic étagé
Le dépôt n'avait aucun moyen de monter un tunnel VPN ni de dire pourquoi il
refuse de monter. Cinq technologies libres, un pilote chacune, derrière un
`vpn.py` qui monte, démonte et diagnostique.
Ce qui n'est pas secret — hôte, utilisateur, routes, MTU — vit dans une
configuration JSON lisible ; clés pré-partagées et mots de passe vivent dans
un coffre KeePassXC. Un profil se montre et se partage sans donner de quoi
monter le tunnel. Les secrets s'écrivent en tmpfs sous 0700, jamais sur un
disque persistant. Le diagnostic part du noyau et remonte, pour que la
première ligne fausse soit la cause et non une conséquence.
Vérifié : 138 tests, dont le rendu de chaque fichier généré.
--- EN ---
The repository had no way to raise a VPN tunnel, nor to say why one refuses
to come up. Five free technologies, one driver each, behind a `vpn.py` that
raises, tears down and diagnoses.
What is not secret — host, user, routes, MTU — lives in readable JSON
configuration; pre-shared keys and passwords live in a KeePassXC vault. A
profile can be shown and shared without handing over the means to raise the
tunnel. Secrets are written to tmpfs at 0700, never to a persistent disk.
Diagnosis starts at the kernel and climbs, so the first false line is the
cause and not a consequence.
Checked: 138 tests, including the rendering of every generated file.
Assisted-by: Claude Opus 5
2026-09-03 23:42:49 -04:00
|
|
|
valid.text(
|
|
|
|
|
profile,
|
|
|
|
|
"oc_authgroup",
|
|
|
|
|
"Groupe d'authentification",
|
|
|
|
|
required=False,
|
|
|
|
|
)
|
|
|
|
|
valid.text(
|
|
|
|
|
profile,
|
|
|
|
|
"oc_servercert",
|
|
|
|
|
"Empreinte du certificat serveur",
|
|
|
|
|
required=False,
|
|
|
|
|
)
|
|
|
|
|
valid.port(profile, "port", "Port HTTPS")
|
[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
|
|
|
# 128 comme plafond : au-delà, ce n'est plus une borne d'annuaire
|
|
|
|
|
# mais une valeur tapée de travers, et l'accepter ferait taire
|
|
|
|
|
# l'avertissement pour tous les mots de passe.
|
|
|
|
|
valid.integer(
|
|
|
|
|
profile,
|
|
|
|
|
"oc_password_len",
|
|
|
|
|
"Longueur de mot de passe comparée",
|
|
|
|
|
0,
|
|
|
|
|
128,
|
|
|
|
|
)
|
[ADD] vpn : cinq pilotes, secrets en coffre, diagnostic étagé
Le dépôt n'avait aucun moyen de monter un tunnel VPN ni de dire pourquoi il
refuse de monter. Cinq technologies libres, un pilote chacune, derrière un
`vpn.py` qui monte, démonte et diagnostique.
Ce qui n'est pas secret — hôte, utilisateur, routes, MTU — vit dans une
configuration JSON lisible ; clés pré-partagées et mots de passe vivent dans
un coffre KeePassXC. Un profil se montre et se partage sans donner de quoi
monter le tunnel. Les secrets s'écrivent en tmpfs sous 0700, jamais sur un
disque persistant. Le diagnostic part du noyau et remonte, pour que la
première ligne fausse soit la cause et non une conséquence.
Vérifié : 138 tests, dont le rendu de chaque fichier généré.
--- EN ---
The repository had no way to raise a VPN tunnel, nor to say why one refuses
to come up. Five free technologies, one driver each, behind a `vpn.py` that
raises, tears down and diagnoses.
What is not secret — host, user, routes, MTU — lives in readable JSON
configuration; pre-shared keys and passwords live in a KeePassXC vault. A
profile can be shown and shared without handing over the means to raise the
tunnel. Secrets are written to tmpfs at 0700, never to a persistent disk.
Diagnosis starts at the kernel and climbs, so the first false line is the
cause and not a consequence.
Checked: 138 tests, including the rendering of every generated file.
Assisted-by: Claude Opus 5
2026-09-03 23:42:49 -04:00
|
|
|
valid.path(
|
|
|
|
|
profile,
|
|
|
|
|
"oc_external_browser",
|
|
|
|
|
"Programme navigateur",
|
|
|
|
|
required=False,
|
|
|
|
|
)
|
[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
|
|
|
valid.path(profile, "oc_sso_helper", "Greffon SSO", required=False)
|
|
|
|
|
valid.text(
|
|
|
|
|
profile,
|
|
|
|
|
"oc_ac_version",
|
|
|
|
|
"Version AnyConnect annoncée",
|
|
|
|
|
required=False,
|
|
|
|
|
)
|
[ADD] vpn : cinq pilotes, secrets en coffre, diagnostic étagé
Le dépôt n'avait aucun moyen de monter un tunnel VPN ni de dire pourquoi il
refuse de monter. Cinq technologies libres, un pilote chacune, derrière un
`vpn.py` qui monte, démonte et diagnostique.
Ce qui n'est pas secret — hôte, utilisateur, routes, MTU — vit dans une
configuration JSON lisible ; clés pré-partagées et mots de passe vivent dans
un coffre KeePassXC. Un profil se montre et se partage sans donner de quoi
monter le tunnel. Les secrets s'écrivent en tmpfs sous 0700, jamais sur un
disque persistant. Le diagnostic part du noyau et remonte, pour que la
première ligne fausse soit la cause et non une conséquence.
Vérifié : 138 tests, dont le rendu de chaque fichier généré.
--- EN ---
The repository had no way to raise a VPN tunnel, nor to say why one refuses
to come up. Five free technologies, one driver each, behind a `vpn.py` that
raises, tears down and diagnoses.
What is not secret — host, user, routes, MTU — lives in readable JSON
configuration; pre-shared keys and passwords live in a KeePassXC vault. A
profile can be shown and shared without handing over the means to raise the
tunnel. Secrets are written to tmpfs at 0700, never to a persistent disk.
Diagnosis starts at the kernel and climbs, so the first false line is the
cause and not a consequence.
Checked: 138 tests, including the rendering of every generated file.
Assisted-by: Claude Opus 5
2026-09-03 23:42:49 -04:00
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def browser(self):
|
|
|
|
|
"""Programme lancé avec l'URL de connexion SSO.
|
|
|
|
|
|
|
|
|
|
`echo` par défaut : sur une machine sans écran, afficher l'URL est
|
|
|
|
|
exactement ce qu'on veut — openconnect attend ensuite la redirection
|
|
|
|
|
sur son port 29786."""
|
|
|
|
|
return self.profile.get("oc_external_browser") or "echo"
|
|
|
|
|
|
[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
|
|
|
@property
|
|
|
|
|
def password_len(self):
|
|
|
|
|
"""Longueur comparée par le concentrateur, 0 si aucune limite."""
|
|
|
|
|
try:
|
|
|
|
|
return int(self.profile.get("oc_password_len") or 0)
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def wants_secrets(cls, profile):
|
|
|
|
|
"""Rien à lire au coffre en SSO : c'est le fournisseur d'identité
|
|
|
|
|
qui authentifie, dans un navigateur, et aucun mot de passe stocké
|
|
|
|
|
n'entre dans l'échange."""
|
|
|
|
|
return not profile.get("oc_sso")
|
|
|
|
|
|
|
|
|
|
def secret_notes(self):
|
|
|
|
|
"""La borne de longueur, dite avant la saisie du mot de passe.
|
|
|
|
|
|
|
|
|
|
En SSO il n'y a pas de mot de passe à déposer : annoncer une borne
|
|
|
|
|
de longueur y serait une consigne sans objet.
|
|
|
|
|
"""
|
|
|
|
|
limit = self.password_len
|
|
|
|
|
if not limit or self.profile.get("oc_sso"):
|
|
|
|
|
return []
|
|
|
|
|
return [
|
|
|
|
|
t(
|
|
|
|
|
"This gateway compares only the first {limit} characters of"
|
|
|
|
|
" the password: store just those, a longer one is refused."
|
|
|
|
|
).format(limit=limit)
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
# SSO délégué : le greffon authentifie, ce pilote monte
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
@property
|
|
|
|
|
def sso_helper(self):
|
|
|
|
|
"""Chemin du greffon SSO, "" s'il est introuvable.
|
|
|
|
|
|
|
|
|
|
Le champ du profil d'abord — c'est le seul moyen de désigner une
|
|
|
|
|
installation en environnement virtuel dédié. Puis le PATH, puis les
|
|
|
|
|
emplacements conventionnels.
|
|
|
|
|
|
|
|
|
|
Un chemin déclaré mais inexécutable rend "" ; c'est `up` qui le DIT,
|
|
|
|
|
plutôt que de se replier sans bruit sur un autre chemin
|
|
|
|
|
d'authentification.
|
|
|
|
|
"""
|
|
|
|
|
declared = self.profile.get("oc_sso_helper") or ""
|
|
|
|
|
if declared:
|
|
|
|
|
path = os.path.expanduser(declared)
|
|
|
|
|
return path if os.access(path, os.X_OK) else ""
|
|
|
|
|
found = shutil.which(SSO_HELPER_NAME)
|
|
|
|
|
if found:
|
|
|
|
|
return found
|
|
|
|
|
for candidate in SSO_HELPER_PATHS:
|
|
|
|
|
path = os.path.expanduser(candidate)
|
|
|
|
|
if os.access(path, os.X_OK):
|
|
|
|
|
return path
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def ac_version(self):
|
|
|
|
|
return self.profile.get("oc_ac_version") or "4.7.00136"
|
|
|
|
|
|
|
|
|
|
def helper_command(self):
|
|
|
|
|
"""La ligne du greffon : elle ne porte AUCUN secret.
|
|
|
|
|
|
|
|
|
|
Les variables de rendu sont préfixées plutôt que posées dans
|
|
|
|
|
l'environnement de ce processus : la commande affichée est alors
|
|
|
|
|
exactement celle qui s'exécute, ce que `--dry-run` promet.
|
|
|
|
|
"""
|
|
|
|
|
p = self.profile
|
|
|
|
|
target = p["server"]
|
|
|
|
|
if p.get("oc_usergroup"):
|
|
|
|
|
target = f"{target}/{p['oc_usergroup']}"
|
|
|
|
|
parts = []
|
|
|
|
|
for key, value in SSO_RENDER_ENV.items():
|
|
|
|
|
if key not in os.environ:
|
|
|
|
|
parts.append(f"{key}={shlex.quote(value)}")
|
|
|
|
|
parts += [
|
|
|
|
|
shlex.quote(self.sso_helper),
|
|
|
|
|
"--authenticate",
|
|
|
|
|
"json",
|
|
|
|
|
f"--server={shlex.quote(target)}",
|
|
|
|
|
f"--ac-version={shlex.quote(self.ac_version)}",
|
|
|
|
|
]
|
|
|
|
|
if p.get("oc_authgroup"):
|
|
|
|
|
parts.append(f"--authgroup={shlex.quote(p['oc_authgroup'])}")
|
|
|
|
|
# `| tee /dev/stderr` : la sortie standard du greffon est à la fois
|
|
|
|
|
# LUE par nous et VUE par l'utilisateur.
|
|
|
|
|
#
|
|
|
|
|
# Le processus navigateur du greffon journalise sur sa SORTIE
|
|
|
|
|
# STANDARD — il n'a pas de configuration propre et hérite du
|
|
|
|
|
# journaliseur par défaut de structlog, qui imprime là. Le parent,
|
|
|
|
|
# lui, journalise sur l'erreur standard. Capturer la sortie sans la
|
|
|
|
|
# dupliquer laisse donc l'utilisateur devant un terminal muet
|
|
|
|
|
# pendant qu'une fenêtre attend son geste, et mélange ces lignes au
|
|
|
|
|
# JSON qu'on doit lire.
|
|
|
|
|
return " ".join(parts) + " | tee /dev/stderr"
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def extract_json(text):
|
|
|
|
|
"""Le dernier objet JSON de `text`, ou None.
|
|
|
|
|
|
|
|
|
|
La sortie du greffon MÊLE ses lignes de journal au JSON final :
|
|
|
|
|
`json.loads` sur le tout échoue même quand l'authentification a
|
|
|
|
|
réussi. On isole donc les accolades équilibrées, et on retient le
|
|
|
|
|
dernier objet qui parse et qui porte un cookie — le dernier, parce
|
|
|
|
|
qu'une ligne de journal peut elle aussi contenir des accolades.
|
|
|
|
|
"""
|
|
|
|
|
best = None
|
|
|
|
|
depth = 0
|
|
|
|
|
start = -1
|
|
|
|
|
for index, char in enumerate(text):
|
|
|
|
|
if char == "{":
|
|
|
|
|
if depth == 0:
|
|
|
|
|
start = index
|
|
|
|
|
depth += 1
|
|
|
|
|
elif char == "}" and depth:
|
|
|
|
|
depth -= 1
|
|
|
|
|
if depth == 0 and start >= 0:
|
|
|
|
|
try:
|
|
|
|
|
candidate = json.loads(text[start : index + 1])
|
|
|
|
|
except ValueError:
|
|
|
|
|
continue
|
|
|
|
|
if isinstance(candidate, dict) and candidate.get("cookie"):
|
|
|
|
|
best = candidate
|
|
|
|
|
return best
|
|
|
|
|
|
|
|
|
|
def _kill_helper_strays(self, runner):
|
|
|
|
|
"""Les navigateurs que le greffon laisse derrière lui.
|
|
|
|
|
|
|
|
|
|
Le greffon lance son navigateur dans des processus séparés. Quand
|
|
|
|
|
il est tué — délai dépassé — eux survivent, et le suivant repart
|
|
|
|
|
sur une machine déjà encombrée. Reconnaissables sans ambiguïté :
|
|
|
|
|
QtWebEngine porte `--application-name=openconnect-sso`.
|
|
|
|
|
"""
|
|
|
|
|
# `[Q]t…` et non `Qt…` : `pkill -f` compare le motif à TOUTES les
|
|
|
|
|
# lignes de commande, y compris celle du shell qui le porte. Le
|
|
|
|
|
# motif écrit en clair s'y trouve donc lui-même, et pkill tuerait
|
|
|
|
|
# son propre parent avant d'avoir servi. Entre crochets, le motif
|
|
|
|
|
# désigne toujours « Qt… » mais ne se reconnaît plus dans le texte
|
|
|
|
|
# qui le contient.
|
|
|
|
|
runner.cmd(
|
|
|
|
|
"fermer les navigateurs laissés par le greffon",
|
|
|
|
|
"pkill -f '[Q]tWebEngineProcess.*application-name=openconnect-sso'",
|
|
|
|
|
sudo=False,
|
|
|
|
|
check=False,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def authenticate_with_helper(self, runner):
|
|
|
|
|
"""(cookie, empreinte) rendus par le greffon, ou (None, None).
|
|
|
|
|
|
|
|
|
|
Sans sudo, et c'est essentiel : le greffon ouvre un navigateur, donc
|
|
|
|
|
il lui faut l'affichage et le trousseau de l'UTILISATEUR. Sous sudo
|
|
|
|
|
il perdrait les deux.
|
|
|
|
|
|
|
|
|
|
`capture="stdout"` : le JSON est LU, les messages de progression du
|
|
|
|
|
greffon restent VUS. L'authentification réclame un geste humain —
|
|
|
|
|
taper un mot de passe, approuver une notification — et une attente
|
|
|
|
|
muette de plusieurs minutes ressemble à un blocage.
|
|
|
|
|
"""
|
|
|
|
|
helper = self.sso_helper
|
|
|
|
|
if not helper:
|
|
|
|
|
report = runner.warn if runner.dry_run else runner.fail
|
|
|
|
|
report(
|
|
|
|
|
"Greffon SSO introuvable. Ce concentrateur exige un"
|
|
|
|
|
" navigateur intégré, que l'openconnect des distributions"
|
|
|
|
|
" n'a pas. Installer openconnect-sso, ou renseigner"
|
|
|
|
|
" oc_sso_helper avec son chemin."
|
|
|
|
|
)
|
|
|
|
|
return None, None
|
|
|
|
|
self._explain_the_web_form(runner)
|
|
|
|
|
code, out = runner.cmd(
|
|
|
|
|
"authentifier par formulaire web (navigateur du greffon)",
|
|
|
|
|
self.helper_command(),
|
|
|
|
|
sudo=False,
|
|
|
|
|
check=False,
|
|
|
|
|
capture="stdout",
|
|
|
|
|
timeout=600,
|
|
|
|
|
)
|
|
|
|
|
if runner.dry_run:
|
|
|
|
|
runner.info(
|
|
|
|
|
" (à blanc : le greffon rendrait un cookie de session"
|
|
|
|
|
" et l'empreinte du certificat qu'il a vu)"
|
|
|
|
|
)
|
|
|
|
|
# L'empreinte du profil, faute de mieux : à blanc on ne peut pas
|
|
|
|
|
# savoir celle que le greffon verrait, et une empreinte n'est
|
|
|
|
|
# pas un secret — lui donner le marqueur des secrets ferait
|
|
|
|
|
# lire au plan une nature qu'elle n'a pas.
|
|
|
|
|
return PLACEHOLDER, self.profile.get("oc_servercert") or ""
|
|
|
|
|
# Le VERDICT est le cookie, pas le code de retour : la commande est
|
|
|
|
|
# un tube, et son code est celui de `tee`. Un cookie obtenu vaut
|
|
|
|
|
# succès, quoi qu'ait rendu le tube.
|
|
|
|
|
answer = self.extract_json(out)
|
|
|
|
|
if answer is None:
|
|
|
|
|
if code == 124:
|
|
|
|
|
runner.fail(
|
|
|
|
|
"Le formulaire web n'a pas abouti dans le délai"
|
|
|
|
|
" imparti (10 min) : personne ne l'a complété, ou la"
|
|
|
|
|
" fenêtre ne s'est jamais affichée."
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
runner.fail(
|
|
|
|
|
f"Le greffon SSO n'a rendu aucun cookie (code {code})."
|
|
|
|
|
)
|
|
|
|
|
self._explain_helper_failure(runner)
|
|
|
|
|
self._kill_helper_strays(runner)
|
|
|
|
|
return None, None
|
|
|
|
|
cookie = answer["cookie"]
|
|
|
|
|
fingerprint = answer.get("fingerprint") or ""
|
|
|
|
|
# Masqué DÈS qu'il existe : ce cookie ouvre le tunnel à lui seul, et
|
|
|
|
|
# il va traverser des affichages et un enregistrement d'opérations.
|
|
|
|
|
runner.add_secret(cookie)
|
|
|
|
|
runner.ok("Authentification web réussie, cookie de session obtenu.")
|
|
|
|
|
return cookie, fingerprint
|
|
|
|
|
|
|
|
|
|
def cookie_command(self, fingerprint):
|
|
|
|
|
"""Le montage à partir d'un cookie. Le cookie N'Y EST PAS : il
|
|
|
|
|
arrive par l'entrée standard, via `--cookie-on-stdin`.
|
|
|
|
|
|
|
|
|
|
L'identité annoncée est celle sous laquelle le cookie a été délivré,
|
|
|
|
|
sinon le concentrateur le refuse. L'empreinte du greffon prime sur
|
|
|
|
|
celle du profil : c'est celle contre laquelle il a authentifié.
|
|
|
|
|
"""
|
|
|
|
|
p = self.profile
|
|
|
|
|
pinned = fingerprint or p.get("oc_servercert") or ""
|
|
|
|
|
parts = [
|
|
|
|
|
"openconnect",
|
|
|
|
|
f"--protocol={shlex.quote(p['oc_protocol'])}",
|
|
|
|
|
f"--useragent={shlex.quote(f'AnyConnect Linux_64 {self.ac_version}')}",
|
|
|
|
|
f"--version-string={shlex.quote(self.ac_version)}",
|
|
|
|
|
"--cookie-on-stdin",
|
|
|
|
|
"--non-inter",
|
|
|
|
|
"--background",
|
|
|
|
|
f"--pid-file={shlex.quote(self.pid_file)}",
|
|
|
|
|
f"--interface={shlex.quote(self.iface)}",
|
|
|
|
|
]
|
|
|
|
|
if p.get("oc_usergroup"):
|
|
|
|
|
parts.append(f"--usergroup={shlex.quote(p['oc_usergroup'])}")
|
|
|
|
|
if pinned:
|
|
|
|
|
parts.append(f"--servercert={shlex.quote(pinned)}")
|
|
|
|
|
parts.append(shlex.quote(f"{p['server']}:{p['port']}"))
|
|
|
|
|
return " ".join(parts)
|
|
|
|
|
|
|
|
|
|
def _explain_the_web_form(self, runner):
|
|
|
|
|
"""Dit ce qui va s'ouvrir, AVANT que ça s'ouvre.
|
|
|
|
|
|
|
|
|
|
Le greffon lance un navigateur et attend, silencieusement du point
|
|
|
|
|
de vue du terminal. Sans cette annonce, la fenêtre surgit sans
|
|
|
|
|
raison apparente et l'attente ressemble à un blocage.
|
|
|
|
|
"""
|
|
|
|
|
runner.info(
|
|
|
|
|
" Une fenêtre de navigateur va s'ouvrir pour"
|
|
|
|
|
" l'authentification. La compléter à l'écran ; le tunnel monte"
|
|
|
|
|
" ensuite tout seul."
|
|
|
|
|
)
|
|
|
|
|
runner.info(
|
|
|
|
|
" Le mot de passe et le second facteur ne passent que par"
|
|
|
|
|
" cette fenêtre : ni ce terminal ni le coffre ne les voient."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def _explain_helper_failure(self, runner):
|
|
|
|
|
runner.info(
|
|
|
|
|
" → Fenêtre morte en cours de route ? Le navigateur"
|
|
|
|
|
" embarqué échoue au rendu sur une machine sans accélération"
|
|
|
|
|
f" exploitable. Relancer avec {SSO_RENDER_ENV_HINT}."
|
|
|
|
|
)
|
|
|
|
|
runner.info(
|
|
|
|
|
" → Identifiants refusés ? Ce service authentifie par le"
|
|
|
|
|
" fournisseur d'identité, qui prend le mot de passe ENTIER."
|
|
|
|
|
" Une limite oc_password_len ne s'applique PAS ici."
|
|
|
|
|
)
|
|
|
|
|
|
[ADD] vpn : cinq pilotes, secrets en coffre, diagnostic étagé
Le dépôt n'avait aucun moyen de monter un tunnel VPN ni de dire pourquoi il
refuse de monter. Cinq technologies libres, un pilote chacune, derrière un
`vpn.py` qui monte, démonte et diagnostique.
Ce qui n'est pas secret — hôte, utilisateur, routes, MTU — vit dans une
configuration JSON lisible ; clés pré-partagées et mots de passe vivent dans
un coffre KeePassXC. Un profil se montre et se partage sans donner de quoi
monter le tunnel. Les secrets s'écrivent en tmpfs sous 0700, jamais sur un
disque persistant. Le diagnostic part du noyau et remonte, pour que la
première ligne fausse soit la cause et non une conséquence.
Vérifié : 138 tests, dont le rendu de chaque fichier généré.
--- EN ---
The repository had no way to raise a VPN tunnel, nor to say why one refuses
to come up. Five free technologies, one driver each, behind a `vpn.py` that
raises, tears down and diagnoses.
What is not secret — host, user, routes, MTU — lives in readable JSON
configuration; pre-shared keys and passwords live in a KeePassXC vault. A
profile can be shown and shared without handing over the means to raise the
tunnel. Secrets are written to tmpfs at 0700, never to a persistent disk.
Diagnosis starts at the kernel and climbs, so the first false line is the
cause and not a consequence.
Checked: 138 tests, including the rendering of every generated file.
Assisted-by: Claude Opus 5
2026-09-03 23:42:49 -04:00
|
|
|
def command(self):
|
|
|
|
|
"""La ligne de commande, dans l'une de ses deux formes.
|
|
|
|
|
|
|
|
|
|
Classique : le mot de passe arrive par l'entrée standard, grâce à
|
|
|
|
|
`--passwd-on-stdin` — il n'est jamais dans la ligne de commande.
|
|
|
|
|
|
|
|
|
|
SSO : il n'y a pas de mot de passe. Pas de `--non-inter` non plus,
|
|
|
|
|
car l'échange avec le navigateur EST l'interaction ; l'interdire
|
|
|
|
|
ferait échouer la seule étape qui compte.
|
|
|
|
|
"""
|
|
|
|
|
p = self.profile
|
|
|
|
|
parts = [
|
|
|
|
|
"openconnect",
|
|
|
|
|
f"--protocol={shlex.quote(p['oc_protocol'])}",
|
|
|
|
|
]
|
|
|
|
|
if p["oc_user"]:
|
|
|
|
|
parts.append(f"--user={shlex.quote(p['oc_user'])}")
|
|
|
|
|
if p["oc_sso"]:
|
|
|
|
|
parts.append(f"--external-browser={shlex.quote(self.browser)}")
|
|
|
|
|
else:
|
|
|
|
|
parts += ["--passwd-on-stdin", "--non-inter"]
|
|
|
|
|
parts += [
|
|
|
|
|
"--background",
|
|
|
|
|
f"--pid-file={shlex.quote(self.pid_file)}",
|
|
|
|
|
f"--interface={shlex.quote(self.iface)}",
|
|
|
|
|
]
|
[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 p.get("oc_usergroup"):
|
|
|
|
|
parts.append(f"--usergroup={shlex.quote(p['oc_usergroup'])}")
|
[ADD] vpn : cinq pilotes, secrets en coffre, diagnostic étagé
Le dépôt n'avait aucun moyen de monter un tunnel VPN ni de dire pourquoi il
refuse de monter. Cinq technologies libres, un pilote chacune, derrière un
`vpn.py` qui monte, démonte et diagnostique.
Ce qui n'est pas secret — hôte, utilisateur, routes, MTU — vit dans une
configuration JSON lisible ; clés pré-partagées et mots de passe vivent dans
un coffre KeePassXC. Un profil se montre et se partage sans donner de quoi
monter le tunnel. Les secrets s'écrivent en tmpfs sous 0700, jamais sur un
disque persistant. Le diagnostic part du noyau et remonte, pour que la
première ligne fausse soit la cause et non une conséquence.
Vérifié : 138 tests, dont le rendu de chaque fichier généré.
--- EN ---
The repository had no way to raise a VPN tunnel, nor to say why one refuses
to come up. Five free technologies, one driver each, behind a `vpn.py` that
raises, tears down and diagnoses.
What is not secret — host, user, routes, MTU — lives in readable JSON
configuration; pre-shared keys and passwords live in a KeePassXC vault. A
profile can be shown and shared without handing over the means to raise the
tunnel. Secrets are written to tmpfs at 0700, never to a persistent disk.
Diagnosis starts at the kernel and climbs, so the first false line is the
cause and not a consequence.
Checked: 138 tests, including the rendering of every generated file.
Assisted-by: Claude Opus 5
2026-09-03 23:42:49 -04:00
|
|
|
if p.get("oc_authgroup"):
|
|
|
|
|
parts.append(f"--authgroup={shlex.quote(p['oc_authgroup'])}")
|
|
|
|
|
if p.get("oc_servercert"):
|
|
|
|
|
parts.append(f"--servercert={shlex.quote(p['oc_servercert'])}")
|
|
|
|
|
parts.append(shlex.quote(f"{p['server']}:{p['port']}"))
|
|
|
|
|
return " ".join(parts)
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
def up(self, runner):
|
[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
|
|
|
"""Trois chemins, un seul aboutissement.
|
|
|
|
|
|
|
|
|
|
Le choix est explicite et se lit ici : greffon si le profil est en
|
|
|
|
|
SSO et qu'un greffon existe, `--external-browser` en SSO sans
|
|
|
|
|
greffon, mot de passe sinon. Un greffon sait faire les DEUX formes
|
|
|
|
|
de SSO ; `--external-browser` n'en fait qu'une. D'où l'ordre.
|
|
|
|
|
"""
|
[ADD] vpn : cinq pilotes, secrets en coffre, diagnostic étagé
Le dépôt n'avait aucun moyen de monter un tunnel VPN ni de dire pourquoi il
refuse de monter. Cinq technologies libres, un pilote chacune, derrière un
`vpn.py` qui monte, démonte et diagnostique.
Ce qui n'est pas secret — hôte, utilisateur, routes, MTU — vit dans une
configuration JSON lisible ; clés pré-partagées et mots de passe vivent dans
un coffre KeePassXC. Un profil se montre et se partage sans donner de quoi
monter le tunnel. Les secrets s'écrivent en tmpfs sous 0700, jamais sur un
disque persistant. Le diagnostic part du noyau et remonte, pour que la
première ligne fausse soit la cause et non une conséquence.
Vérifié : 138 tests, dont le rendu de chaque fichier généré.
--- EN ---
The repository had no way to raise a VPN tunnel, nor to say why one refuses
to come up. Five free technologies, one driver each, behind a `vpn.py` that
raises, tears down and diagnoses.
What is not secret — host, user, routes, MTU — lives in readable JSON
configuration; pre-shared keys and passwords live in a KeePassXC vault. A
profile can be shown and shared without handing over the means to raise the
tunnel. Secrets are written to tmpfs at 0700, never to a persistent disk.
Diagnosis starts at the kernel and climbs, so the first false line is the
cause and not a consequence.
Checked: 138 tests, including the rendering of every generated file.
Assisted-by: Claude Opus 5
2026-09-03 23:42:49 -04:00
|
|
|
p = self.profile
|
|
|
|
|
if not self.ensure_ready(runner):
|
|
|
|
|
return False
|
|
|
|
|
# `secrets=False` : ce pilote n'écrit AUCUN fichier de secret, et
|
|
|
|
|
# créer un répertoire pour rien serait laisser croire qu'il en a un.
|
|
|
|
|
self.prepare_dirs(runner, secrets=False)
|
|
|
|
|
|
|
|
|
|
if p["oc_sso"]:
|
[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
|
|
|
# Un greffon DÉCLARÉ mais inexécutable est une erreur, pas une
|
|
|
|
|
# invitation à prendre l'autre chemin : basculer en silence
|
|
|
|
|
# ferait échouer le montage sur « No SSO handler », trois
|
|
|
|
|
# étages au-dessus de la vraie cause — un chemin fautif.
|
|
|
|
|
declared = p.get("oc_sso_helper") or ""
|
|
|
|
|
if declared and not self.sso_helper:
|
[ADD] vpn : cinq pilotes, secrets en coffre, diagnostic étagé
Le dépôt n'avait aucun moyen de monter un tunnel VPN ni de dire pourquoi il
refuse de monter. Cinq technologies libres, un pilote chacune, derrière un
`vpn.py` qui monte, démonte et diagnostique.
Ce qui n'est pas secret — hôte, utilisateur, routes, MTU — vit dans une
configuration JSON lisible ; clés pré-partagées et mots de passe vivent dans
un coffre KeePassXC. Un profil se montre et se partage sans donner de quoi
monter le tunnel. Les secrets s'écrivent en tmpfs sous 0700, jamais sur un
disque persistant. Le diagnostic part du noyau et remonte, pour que la
première ligne fausse soit la cause et non une conséquence.
Vérifié : 138 tests, dont le rendu de chaque fichier généré.
--- EN ---
The repository had no way to raise a VPN tunnel, nor to say why one refuses
to come up. Five free technologies, one driver each, behind a `vpn.py` that
raises, tears down and diagnoses.
What is not secret — host, user, routes, MTU — lives in readable JSON
configuration; pre-shared keys and passwords live in a KeePassXC vault. A
profile can be shown and shared without handing over the means to raise the
tunnel. Secrets are written to tmpfs at 0700, never to a persistent disk.
Diagnosis starts at the kernel and climbs, so the first false line is the
cause and not a consequence.
Checked: 138 tests, including the rendering of every generated file.
Assisted-by: Claude Opus 5
2026-09-03 23:42:49 -04:00
|
|
|
report = runner.warn if runner.dry_run else runner.fail
|
|
|
|
|
report(
|
[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
|
|
|
f"Greffon SSO déclaré mais inexécutable : {declared}."
|
|
|
|
|
" Corriger oc_sso_helper, ou le vider pour chercher"
|
|
|
|
|
" openconnect-sso dans le PATH."
|
[ADD] vpn : cinq pilotes, secrets en coffre, diagnostic étagé
Le dépôt n'avait aucun moyen de monter un tunnel VPN ni de dire pourquoi il
refuse de monter. Cinq technologies libres, un pilote chacune, derrière un
`vpn.py` qui monte, démonte et diagnostique.
Ce qui n'est pas secret — hôte, utilisateur, routes, MTU — vit dans une
configuration JSON lisible ; clés pré-partagées et mots de passe vivent dans
un coffre KeePassXC. Un profil se montre et se partage sans donner de quoi
monter le tunnel. Les secrets s'écrivent en tmpfs sous 0700, jamais sur un
disque persistant. Le diagnostic part du noyau et remonte, pour que la
première ligne fausse soit la cause et non une conséquence.
Vérifié : 138 tests, dont le rendu de chaque fichier généré.
--- EN ---
The repository had no way to raise a VPN tunnel, nor to say why one refuses
to come up. Five free technologies, one driver each, behind a `vpn.py` that
raises, tears down and diagnoses.
What is not secret — host, user, routes, MTU — lives in readable JSON
configuration; pre-shared keys and passwords live in a KeePassXC vault. A
profile can be shown and shared without handing over the means to raise the
tunnel. Secrets are written to tmpfs at 0700, never to a persistent disk.
Diagnosis starts at the kernel and climbs, so the first false line is the
cause and not a consequence.
Checked: 138 tests, including the rendering of every generated file.
Assisted-by: Claude Opus 5
2026-09-03 23:42:49 -04:00
|
|
|
)
|
|
|
|
|
if not runner.dry_run:
|
|
|
|
|
return False
|
[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
|
|
|
monte = (
|
|
|
|
|
self._up_delegated(runner)
|
|
|
|
|
if self.sso_helper
|
|
|
|
|
else self._up_external_browser(runner)
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
monte = self._up_password(runner)
|
|
|
|
|
if not monte:
|
|
|
|
|
return False
|
|
|
|
|
if runner.dry_run:
|
|
|
|
|
runner.info(f" (à blanc : l'interface serait {self.iface})")
|
|
|
|
|
return True
|
|
|
|
|
return self._settle(runner)
|
[ADD] vpn : cinq pilotes, secrets en coffre, diagnostic étagé
Le dépôt n'avait aucun moyen de monter un tunnel VPN ni de dire pourquoi il
refuse de monter. Cinq technologies libres, un pilote chacune, derrière un
`vpn.py` qui monte, démonte et diagnostique.
Ce qui n'est pas secret — hôte, utilisateur, routes, MTU — vit dans une
configuration JSON lisible ; clés pré-partagées et mots de passe vivent dans
un coffre KeePassXC. Un profil se montre et se partage sans donner de quoi
monter le tunnel. Les secrets s'écrivent en tmpfs sous 0700, jamais sur un
disque persistant. Le diagnostic part du noyau et remonte, pour que la
première ligne fausse soit la cause et non une conséquence.
Vérifié : 138 tests, dont le rendu de chaque fichier généré.
--- EN ---
The repository had no way to raise a VPN tunnel, nor to say why one refuses
to come up. Five free technologies, one driver each, behind a `vpn.py` that
raises, tears down and diagnoses.
What is not secret — host, user, routes, MTU — lives in readable JSON
configuration; pre-shared keys and passwords live in a KeePassXC vault. A
profile can be shown and shared without handing over the means to raise the
tunnel. Secrets are written to tmpfs at 0700, never to a persistent disk.
Diagnosis starts at the kernel and climbs, so the first false line is the
cause and not a consequence.
Checked: 138 tests, including the rendering of every generated file.
Assisted-by: Claude Opus 5
2026-09-03 23:42:49 -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
|
|
|
def _up_delegated(self, runner):
|
|
|
|
|
"""Le greffon authentifie, ce pilote monte. Voir l'en-tête."""
|
|
|
|
|
cookie, fingerprint = self.authenticate_with_helper(runner)
|
|
|
|
|
if cookie is None:
|
|
|
|
|
return False
|
[ADD] vpn : cinq pilotes, secrets en coffre, diagnostic étagé
Le dépôt n'avait aucun moyen de monter un tunnel VPN ni de dire pourquoi il
refuse de monter. Cinq technologies libres, un pilote chacune, derrière un
`vpn.py` qui monte, démonte et diagnostique.
Ce qui n'est pas secret — hôte, utilisateur, routes, MTU — vit dans une
configuration JSON lisible ; clés pré-partagées et mots de passe vivent dans
un coffre KeePassXC. Un profil se montre et se partage sans donner de quoi
monter le tunnel. Les secrets s'écrivent en tmpfs sous 0700, jamais sur un
disque persistant. Le diagnostic part du noyau et remonte, pour que la
première ligne fausse soit la cause et non une conséquence.
Vérifié : 138 tests, dont le rendu de chaque fichier généré.
--- EN ---
The repository had no way to raise a VPN tunnel, nor to say why one refuses
to come up. Five free technologies, one driver each, behind a `vpn.py` that
raises, tears down and diagnoses.
What is not secret — host, user, routes, MTU — lives in readable JSON
configuration; pre-shared keys and passwords live in a KeePassXC vault. A
profile can be shown and shared without handing over the means to raise the
tunnel. Secrets are written to tmpfs at 0700, never to a persistent disk.
Diagnosis starts at the kernel and climbs, so the first false line is the
cause and not a consequence.
Checked: 138 tests, including the rendering of every generated file.
Assisted-by: Claude Opus 5
2026-09-03 23:42:49 -04:00
|
|
|
code, _ = runner.cmd(
|
[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
|
|
|
f"monter le tunnel sur {self.profile['server']} avec le cookie",
|
|
|
|
|
self.cookie_command(fingerprint),
|
|
|
|
|
stdin=f"{cookie}\n",
|
|
|
|
|
secret_stdin=True,
|
|
|
|
|
check=False,
|
|
|
|
|
timeout=120,
|
|
|
|
|
)
|
|
|
|
|
if code != 0 and not runner.dry_run:
|
|
|
|
|
runner.fail("openconnect a refusé le cookie.")
|
|
|
|
|
runner.info(
|
|
|
|
|
" → Un cookie de session est à usage unique et de"
|
|
|
|
|
" courte durée. S'il a été délivré à un client annonçant"
|
|
|
|
|
" une autre version que oc_ac_version, ou si le montage"
|
|
|
|
|
" arrive trop tard, il est refusé : relancer."
|
|
|
|
|
)
|
|
|
|
|
return False
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
def _up_external_browser(self, runner):
|
|
|
|
|
"""SSO sans greffon : openconnect s'en charge, s'il peut."""
|
|
|
|
|
self._explain_the_sso_round_trip(runner)
|
|
|
|
|
# Entrée standard VIDE, et non héritée du terminal. En SSO
|
|
|
|
|
# openconnect ne lit jamais l'entrée standard : il attend la
|
|
|
|
|
# redirection sur son port. Mais un concentrateur qui ne fait PAS de
|
|
|
|
|
# SSO répond par un formulaire mot de passe, et openconnect se met
|
|
|
|
|
# alors à le demander — sur le terminal, puisqu'il en a un, et sans
|
|
|
|
|
# que `--non-inter` soit là pour l'en empêcher (en SSO la
|
|
|
|
|
# redirection EST l'interaction).
|
|
|
|
|
#
|
|
|
|
|
# L'attente ressemble alors à l'attente normale du SSO, et chaque
|
|
|
|
|
# essai revient en « Login failed » jusqu'au délai. Une entrée
|
|
|
|
|
# standard fermée transforme cette boucle en un échec immédiat, que
|
|
|
|
|
# le message d'aide ci-dessous explique.
|
|
|
|
|
code, _ = runner.cmd(
|
|
|
|
|
f"ouvrir la session {self.profile['oc_protocol']} sur"
|
|
|
|
|
f" {self.profile['server']}",
|
[ADD] vpn : cinq pilotes, secrets en coffre, diagnostic étagé
Le dépôt n'avait aucun moyen de monter un tunnel VPN ni de dire pourquoi il
refuse de monter. Cinq technologies libres, un pilote chacune, derrière un
`vpn.py` qui monte, démonte et diagnostique.
Ce qui n'est pas secret — hôte, utilisateur, routes, MTU — vit dans une
configuration JSON lisible ; clés pré-partagées et mots de passe vivent dans
un coffre KeePassXC. Un profil se montre et se partage sans donner de quoi
monter le tunnel. Les secrets s'écrivent en tmpfs sous 0700, jamais sur un
disque persistant. Le diagnostic part du noyau et remonte, pour que la
première ligne fausse soit la cause et non une conséquence.
Vérifié : 138 tests, dont le rendu de chaque fichier généré.
--- EN ---
The repository had no way to raise a VPN tunnel, nor to say why one refuses
to come up. Five free technologies, one driver each, behind a `vpn.py` that
raises, tears down and diagnoses.
What is not secret — host, user, routes, MTU — lives in readable JSON
configuration; pre-shared keys and passwords live in a KeePassXC vault. A
profile can be shown and shared without handing over the means to raise the
tunnel. Secrets are written to tmpfs at 0700, never to a persistent disk.
Diagnosis starts at the kernel and climbs, so the first false line is the
cause and not a consequence.
Checked: 138 tests, including the rendering of every generated file.
Assisted-by: Claude Opus 5
2026-09-03 23:42:49 -04:00
|
|
|
self.command(),
|
[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
|
|
|
stdin="",
|
[ADD] vpn : cinq pilotes, secrets en coffre, diagnostic étagé
Le dépôt n'avait aucun moyen de monter un tunnel VPN ni de dire pourquoi il
refuse de monter. Cinq technologies libres, un pilote chacune, derrière un
`vpn.py` qui monte, démonte et diagnostique.
Ce qui n'est pas secret — hôte, utilisateur, routes, MTU — vit dans une
configuration JSON lisible ; clés pré-partagées et mots de passe vivent dans
un coffre KeePassXC. Un profil se montre et se partage sans donner de quoi
monter le tunnel. Les secrets s'écrivent en tmpfs sous 0700, jamais sur un
disque persistant. Le diagnostic part du noyau et remonte, pour que la
première ligne fausse soit la cause et non une conséquence.
Vérifié : 138 tests, dont le rendu de chaque fichier généré.
--- EN ---
The repository had no way to raise a VPN tunnel, nor to say why one refuses
to come up. Five free technologies, one driver each, behind a `vpn.py` that
raises, tears down and diagnoses.
What is not secret — host, user, routes, MTU — lives in readable JSON
configuration; pre-shared keys and passwords live in a KeePassXC vault. A
profile can be shown and shared without handing over the means to raise the
tunnel. Secrets are written to tmpfs at 0700, never to a persistent disk.
Diagnosis starts at the kernel and climbs, so the first false line is the
cause and not a consequence.
Checked: 138 tests, including the rendering of every generated file.
Assisted-by: Claude Opus 5
2026-09-03 23:42:49 -04:00
|
|
|
check=False,
|
[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
|
|
|
timeout=300,
|
[ADD] vpn : cinq pilotes, secrets en coffre, diagnostic étagé
Le dépôt n'avait aucun moyen de monter un tunnel VPN ni de dire pourquoi il
refuse de monter. Cinq technologies libres, un pilote chacune, derrière un
`vpn.py` qui monte, démonte et diagnostique.
Ce qui n'est pas secret — hôte, utilisateur, routes, MTU — vit dans une
configuration JSON lisible ; clés pré-partagées et mots de passe vivent dans
un coffre KeePassXC. Un profil se montre et se partage sans donner de quoi
monter le tunnel. Les secrets s'écrivent en tmpfs sous 0700, jamais sur un
disque persistant. Le diagnostic part du noyau et remonte, pour que la
première ligne fausse soit la cause et non une conséquence.
Vérifié : 138 tests, dont le rendu de chaque fichier généré.
--- EN ---
The repository had no way to raise a VPN tunnel, nor to say why one refuses
to come up. Five free technologies, one driver each, behind a `vpn.py` that
raises, tears down and diagnoses.
What is not secret — host, user, routes, MTU — lives in readable JSON
configuration; pre-shared keys and passwords live in a KeePassXC vault. A
profile can be shown and shared without handing over the means to raise the
tunnel. Secrets are written to tmpfs at 0700, never to a persistent disk.
Diagnosis starts at the kernel and climbs, so the first false line is the
cause and not a consequence.
Checked: 138 tests, including the rendering of every generated file.
Assisted-by: Claude Opus 5
2026-09-03 23:42:49 -04:00
|
|
|
)
|
|
|
|
|
if code != 0 and not runner.dry_run:
|
|
|
|
|
runner.fail("openconnect a refusé.")
|
[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
|
|
|
# Les causes sont classées par ce que le message d'openconnect
|
|
|
|
|
# permet de reconnaître, et non par fréquence : chacune se lit
|
|
|
|
|
# sur une ligne précise de la sortie ci-dessus.
|
|
|
|
|
runner.info(
|
|
|
|
|
" → « No SSO handler » : ce concentrateur exige le"
|
|
|
|
|
" navigateur INTÉGRÉ (sso-v2), et l'openconnect des"
|
|
|
|
|
" distributions est bâti sans. `--external-browser` ne"
|
|
|
|
|
" s'applique que si le serveur annonce le mode navigateur"
|
|
|
|
|
" externe. Installer openconnect-sso, ou renseigner"
|
|
|
|
|
" oc_sso_helper : ce pilote délèguera l'étape web."
|
|
|
|
|
)
|
|
|
|
|
runner.info(
|
|
|
|
|
" → « username and password » demandé : alors le"
|
|
|
|
|
" service ne fait PAS de SSO. Décocher « formulaire web »"
|
|
|
|
|
" (oc_sso) et déposer le mot de passe."
|
|
|
|
|
)
|
|
|
|
|
runner.info(
|
|
|
|
|
" → Sinon : la redirection n'est jamais revenue sur le"
|
|
|
|
|
" port 29786 (redirection ssh en place ?), ou le délai de"
|
|
|
|
|
" 5 minutes a expiré avant la fin de l'authentification."
|
|
|
|
|
)
|
[ADD] vpn : cinq pilotes, secrets en coffre, diagnostic étagé
Le dépôt n'avait aucun moyen de monter un tunnel VPN ni de dire pourquoi il
refuse de monter. Cinq technologies libres, un pilote chacune, derrière un
`vpn.py` qui monte, démonte et diagnostique.
Ce qui n'est pas secret — hôte, utilisateur, routes, MTU — vit dans une
configuration JSON lisible ; clés pré-partagées et mots de passe vivent dans
un coffre KeePassXC. Un profil se montre et se partage sans donner de quoi
monter le tunnel. Les secrets s'écrivent en tmpfs sous 0700, jamais sur un
disque persistant. Le diagnostic part du noyau et remonte, pour que la
première ligne fausse soit la cause et non une conséquence.
Vérifié : 138 tests, dont le rendu de chaque fichier généré.
--- EN ---
The repository had no way to raise a VPN tunnel, nor to say why one refuses
to come up. Five free technologies, one driver each, behind a `vpn.py` that
raises, tears down and diagnoses.
What is not secret — host, user, routes, MTU — lives in readable JSON
configuration; pre-shared keys and passwords live in a KeePassXC vault. A
profile can be shown and shared without handing over the means to raise the
tunnel. Secrets are written to tmpfs at 0700, never to a persistent disk.
Diagnosis starts at the kernel and climbs, so the first false line is the
cause and not a consequence.
Checked: 138 tests, including the rendering of every generated file.
Assisted-by: Claude Opus 5
2026-09-03 23:42:49 -04:00
|
|
|
return False
|
[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
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
def _up_password(self, runner):
|
|
|
|
|
"""Mot de passe du coffre, par l'entrée standard."""
|
|
|
|
|
if not self.secrets.get("password"):
|
|
|
|
|
report = runner.warn if runner.dry_run else runner.fail
|
|
|
|
|
report(
|
|
|
|
|
"Aucun mot de passe dans le coffre, et le profil n'est pas"
|
|
|
|
|
" en SSO : les déposer, ou cocher « formulaire web »."
|
|
|
|
|
)
|
|
|
|
|
if not runner.dry_run:
|
|
|
|
|
return False
|
|
|
|
|
secret = self.secrets.get("password", "")
|
|
|
|
|
# Comparé, jamais tronqué : le coffre décide de ce qui part.
|
|
|
|
|
# Comparer des LONGUEURS ne divulgue rien, et c'est le seul endroit
|
|
|
|
|
# où le mot de passe déposé et la borne déclarée sont tous deux
|
|
|
|
|
# connus.
|
|
|
|
|
limite = self.password_len
|
|
|
|
|
if limite and secret != PLACEHOLDER and len(secret) > limite:
|
|
|
|
|
runner.warn(
|
|
|
|
|
f"Le mot de passe du coffre fait {len(secret)} caractères"
|
|
|
|
|
f" et ce concentrateur n'en compare que {limite} : n'en"
|
|
|
|
|
f" déposer que {limite}."
|
|
|
|
|
)
|
|
|
|
|
code, _ = runner.cmd(
|
|
|
|
|
f"ouvrir la session {self.profile['oc_protocol']} sur"
|
|
|
|
|
f" {self.profile['server']}",
|
|
|
|
|
self.command(),
|
|
|
|
|
stdin=f"{secret}\n",
|
|
|
|
|
secret_stdin=True,
|
|
|
|
|
check=False,
|
|
|
|
|
timeout=120,
|
|
|
|
|
)
|
|
|
|
|
if code != 0 and not runner.dry_run:
|
|
|
|
|
runner.fail("openconnect a refusé.")
|
|
|
|
|
runner.info(
|
|
|
|
|
" → Causes usuelles : identifiants, certificat serveur"
|
|
|
|
|
" non épinglé (recopier la ligne « --servercert sha256:… »"
|
|
|
|
|
" ci-dessus dans le champ oc_servercert), groupe"
|
|
|
|
|
" d'authentification absent."
|
|
|
|
|
)
|
|
|
|
|
return False
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
def _settle(self, runner):
|
|
|
|
|
"""Ce qui suit un montage réussi, quel que soit le chemin pris.
|
[ADD] vpn : cinq pilotes, secrets en coffre, diagnostic étagé
Le dépôt n'avait aucun moyen de monter un tunnel VPN ni de dire pourquoi il
refuse de monter. Cinq technologies libres, un pilote chacune, derrière un
`vpn.py` qui monte, démonte et diagnostique.
Ce qui n'est pas secret — hôte, utilisateur, routes, MTU — vit dans une
configuration JSON lisible ; clés pré-partagées et mots de passe vivent dans
un coffre KeePassXC. Un profil se montre et se partage sans donner de quoi
monter le tunnel. Les secrets s'écrivent en tmpfs sous 0700, jamais sur un
disque persistant. Le diagnostic part du noyau et remonte, pour que la
première ligne fausse soit la cause et non une conséquence.
Vérifié : 138 tests, dont le rendu de chaque fichier généré.
--- EN ---
The repository had no way to raise a VPN tunnel, nor to say why one refuses
to come up. Five free technologies, one driver each, behind a `vpn.py` that
raises, tears down and diagnoses.
What is not secret — host, user, routes, MTU — lives in readable JSON
configuration; pre-shared keys and passwords live in a KeePassXC vault. A
profile can be shown and shared without handing over the means to raise the
tunnel. Secrets are written to tmpfs at 0700, never to a persistent disk.
Diagnosis starts at the kernel and climbs, so the first false line is the
cause and not a consequence.
Checked: 138 tests, including the rendering of every generated file.
Assisted-by: Claude Opus 5
2026-09-03 23:42:49 -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
|
|
|
On ATTEND l'interface au lieu de la constater. `--background` fait
|
|
|
|
|
sortir openconnect dès que la session est ouverte, et c'est
|
|
|
|
|
`vpnc-script` qui crée l'interface et lui pose son adresse, un
|
|
|
|
|
instant plus tard. Regarder tout de suite déclare absent un tunnel
|
|
|
|
|
qui monte — et accuse `vpnc-script` d'être absent alors qu'il est
|
|
|
|
|
précisément en train de travailler.
|
|
|
|
|
"""
|
|
|
|
|
addresses = wait_for_interface_address(self.iface, timeout=25)
|
|
|
|
|
if not addresses and not interface_exists(self.iface):
|
[ADD] vpn : cinq pilotes, secrets en coffre, diagnostic étagé
Le dépôt n'avait aucun moyen de monter un tunnel VPN ni de dire pourquoi il
refuse de monter. Cinq technologies libres, un pilote chacune, derrière un
`vpn.py` qui monte, démonte et diagnostique.
Ce qui n'est pas secret — hôte, utilisateur, routes, MTU — vit dans une
configuration JSON lisible ; clés pré-partagées et mots de passe vivent dans
un coffre KeePassXC. Un profil se montre et se partage sans donner de quoi
monter le tunnel. Les secrets s'écrivent en tmpfs sous 0700, jamais sur un
disque persistant. Le diagnostic part du noyau et remonte, pour que la
première ligne fausse soit la cause et non une conséquence.
Vérifié : 138 tests, dont le rendu de chaque fichier généré.
--- EN ---
The repository had no way to raise a VPN tunnel, nor to say why one refuses
to come up. Five free technologies, one driver each, behind a `vpn.py` that
raises, tears down and diagnoses.
What is not secret — host, user, routes, MTU — lives in readable JSON
configuration; pre-shared keys and passwords live in a KeePassXC vault. A
profile can be shown and shared without handing over the means to raise the
tunnel. Secrets are written to tmpfs at 0700, never to a persistent disk.
Diagnosis starts at the kernel and climbs, so the first false line is the
cause and not a consequence.
Checked: 138 tests, including the rendering of every generated file.
Assisted-by: Claude Opus 5
2026-09-03 23:42:49 -04:00
|
|
|
runner.fail(
|
|
|
|
|
f"openconnect s'est lancé mais {self.iface} n'existe pas."
|
|
|
|
|
" vpnc-script est-il installé ? (paquet vpnc-scripts)"
|
|
|
|
|
)
|
|
|
|
|
return False
|
[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
|
|
|
runner.ok(
|
|
|
|
|
f"interface {self.iface} :"
|
|
|
|
|
f" {', '.join(addresses) or 'sans adresse'}"
|
[ADD] vpn : cinq pilotes, secrets en coffre, diagnostic étagé
Le dépôt n'avait aucun moyen de monter un tunnel VPN ni de dire pourquoi il
refuse de monter. Cinq technologies libres, un pilote chacune, derrière un
`vpn.py` qui monte, démonte et diagnostique.
Ce qui n'est pas secret — hôte, utilisateur, routes, MTU — vit dans une
configuration JSON lisible ; clés pré-partagées et mots de passe vivent dans
un coffre KeePassXC. Un profil se montre et se partage sans donner de quoi
monter le tunnel. Les secrets s'écrivent en tmpfs sous 0700, jamais sur un
disque persistant. Le diagnostic part du noyau et remonte, pour que la
première ligne fausse soit la cause et non une conséquence.
Vérifié : 138 tests, dont le rendu de chaque fichier généré.
--- EN ---
The repository had no way to raise a VPN tunnel, nor to say why one refuses
to come up. Five free technologies, one driver each, behind a `vpn.py` that
raises, tears down and diagnoses.
What is not secret — host, user, routes, MTU — lives in readable JSON
configuration; pre-shared keys and passwords live in a KeePassXC vault. A
profile can be shown and shared without handing over the means to raise the
tunnel. Secrets are written to tmpfs at 0700, never to a persistent disk.
Diagnosis starts at the kernel and climbs, so the first false line is the
cause and not a consequence.
Checked: 138 tests, including the rendering of every generated file.
Assisted-by: Claude Opus 5
2026-09-03 23:42:49 -04:00
|
|
|
)
|
|
|
|
|
self.write_state(runner, "iface", self.iface)
|
|
|
|
|
# Les routes du serveur sont déjà posées par vpnc-script. Celles du
|
|
|
|
|
# profil s'AJOUTENT : un réseau que le concentrateur ne pousse pas
|
|
|
|
|
# mais qu'on sait joignable.
|
|
|
|
|
self.add_routes(runner, self.iface)
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
def _explain_the_sso_round_trip(self, runner):
|
|
|
|
|
"""Dit à l'humain ce qu'il va devoir faire, AVANT de le bloquer.
|
|
|
|
|
|
|
|
|
|
openconnect va afficher une URL puis attendre, silencieusement, sur
|
|
|
|
|
son port 29786. Sans cette explication, l'attente ressemble à un
|
|
|
|
|
blocage — et la redirection ne revient jamais si personne n'a monté
|
|
|
|
|
le tunnel ssh."""
|
|
|
|
|
runner.info(
|
|
|
|
|
" Authentification par formulaire web. openconnect va"
|
|
|
|
|
" afficher une URL, puis attendre la redirection sur son port"
|
|
|
|
|
" local 29786."
|
|
|
|
|
)
|
|
|
|
|
runner.info(
|
|
|
|
|
" Depuis VOTRE poste, avant d'ouvrir l'URL :"
|
|
|
|
|
" ssh -L 29786:localhost:29786 <cette machine>"
|
|
|
|
|
)
|
|
|
|
|
if self.browser == "echo":
|
|
|
|
|
runner.info(
|
|
|
|
|
" L'URL s'affichera ici : l'ouvrir dans votre propre"
|
|
|
|
|
" navigateur. Le mot de passe ne quitte pas votre poste."
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
runner.info(f" Navigateur lancé sur place : {self.browser}")
|
|
|
|
|
|
|
|
|
|
def down(self, runner):
|
|
|
|
|
# SIGTERM : openconnect rappelle vpnc-script, qui défait les routes
|
|
|
|
|
# et le DNS qu'il avait posés. Un « kill -9 » les laisserait en
|
|
|
|
|
# place, et la machine resterait à moitié dans le tunnel.
|
|
|
|
|
self.kill_pidfile(runner, "arrêter openconnect (SIGTERM)")
|
|
|
|
|
self.clear_state(runner, "iface", "pid")
|
|
|
|
|
runner.ok(
|
|
|
|
|
"Session fermée. Aucun secret à effacer : il n'a jamais"
|
|
|
|
|
" touché le disque."
|
|
|
|
|
)
|
|
|
|
|
return True
|
|
|
|
|
|
[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 check_sso_helper(self):
|
|
|
|
|
"""Verdict sur le greffon, quand le profil en a besoin.
|
|
|
|
|
|
|
|
|
|
`None` en verdict quand le profil n'est pas en SSO : ce n'est pas
|
|
|
|
|
« bon », ce n'est pas « en défaut », c'est hors sujet — et un ✓ sur
|
|
|
|
|
une ligne hors sujet fait croire qu'elle a été vérifiée.
|
|
|
|
|
"""
|
|
|
|
|
if not self.profile.get("oc_sso"):
|
|
|
|
|
return (
|
|
|
|
|
t("SSO helper"),
|
|
|
|
|
None,
|
|
|
|
|
"sans objet : ce profil authentifie par mot de passe",
|
|
|
|
|
)
|
|
|
|
|
helper = self.sso_helper
|
|
|
|
|
if helper:
|
|
|
|
|
return (
|
|
|
|
|
t("SSO helper"),
|
|
|
|
|
True,
|
|
|
|
|
helper,
|
|
|
|
|
)
|
|
|
|
|
declared = self.profile.get("oc_sso_helper") or ""
|
|
|
|
|
return (
|
|
|
|
|
t("SSO helper"),
|
|
|
|
|
False,
|
|
|
|
|
(
|
|
|
|
|
f"déclaré mais inexécutable : {declared}"
|
|
|
|
|
if declared
|
|
|
|
|
else "absent : openconnect-sso introuvable dans le PATH"
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
[ADD] vpn : cinq pilotes, secrets en coffre, diagnostic étagé
Le dépôt n'avait aucun moyen de monter un tunnel VPN ni de dire pourquoi il
refuse de monter. Cinq technologies libres, un pilote chacune, derrière un
`vpn.py` qui monte, démonte et diagnostique.
Ce qui n'est pas secret — hôte, utilisateur, routes, MTU — vit dans une
configuration JSON lisible ; clés pré-partagées et mots de passe vivent dans
un coffre KeePassXC. Un profil se montre et se partage sans donner de quoi
monter le tunnel. Les secrets s'écrivent en tmpfs sous 0700, jamais sur un
disque persistant. Le diagnostic part du noyau et remonte, pour que la
première ligne fausse soit la cause et non une conséquence.
Vérifié : 138 tests, dont le rendu de chaque fichier généré.
--- EN ---
The repository had no way to raise a VPN tunnel, nor to say why one refuses
to come up. Five free technologies, one driver each, behind a `vpn.py` that
raises, tears down and diagnoses.
What is not secret — host, user, routes, MTU — lives in readable JSON
configuration; pre-shared keys and passwords live in a KeePassXC vault. A
profile can be shown and shared without handing over the means to raise the
tunnel. Secrets are written to tmpfs at 0700, never to a persistent disk.
Diagnosis starts at the kernel and climbs, so the first false line is the
cause and not a consequence.
Checked: 138 tests, including the rendering of every generated file.
Assisted-by: Claude Opus 5
2026-09-03 23:42:49 -04:00
|
|
|
def status(self, runner):
|
|
|
|
|
return self.standard_status(
|
[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
|
|
|
runner,
|
|
|
|
|
extra=[
|
|
|
|
|
self.check_daemon("processus openconnect"),
|
|
|
|
|
self.check_sso_helper(),
|
|
|
|
|
],
|
[ADD] vpn : cinq pilotes, secrets en coffre, diagnostic étagé
Le dépôt n'avait aucun moyen de monter un tunnel VPN ni de dire pourquoi il
refuse de monter. Cinq technologies libres, un pilote chacune, derrière un
`vpn.py` qui monte, démonte et diagnostique.
Ce qui n'est pas secret — hôte, utilisateur, routes, MTU — vit dans une
configuration JSON lisible ; clés pré-partagées et mots de passe vivent dans
un coffre KeePassXC. Un profil se montre et se partage sans donner de quoi
monter le tunnel. Les secrets s'écrivent en tmpfs sous 0700, jamais sur un
disque persistant. Le diagnostic part du noyau et remonte, pour que la
première ligne fausse soit la cause et non une conséquence.
Vérifié : 138 tests, dont le rendu de chaque fichier généré.
--- EN ---
The repository had no way to raise a VPN tunnel, nor to say why one refuses
to come up. Five free technologies, one driver each, behind a `vpn.py` that
raises, tears down and diagnoses.
What is not secret — host, user, routes, MTU — lives in readable JSON
configuration; pre-shared keys and passwords live in a KeePassXC vault. A
profile can be shown and shared without handing over the means to raise the
tunnel. Secrets are written to tmpfs at 0700, never to a persistent disk.
Diagnosis starts at the kernel and climbs, so the first false line is the
cause and not a consequence.
Checked: 138 tests, including the rendering of every generated file.
Assisted-by: Claude Opus 5
2026-09-03 23:42:49 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def log_commands(self):
|
|
|
|
|
return [
|
|
|
|
|
(
|
|
|
|
|
"journal openconnect",
|
|
|
|
|
"journalctl -n 40 --no-pager -t openconnect",
|
|
|
|
|
)
|
|
|
|
|
]
|