fix(auth): le serveur reconnaît le poste à son adresse, plus à un identifiant

La fenêtre de saisie réclamait l'Identifiant VPN à chaque ouverture sur un
poste neuf, et l'authentification échouait. Les deux symptômes n'en faisaient
qu'un : l'identifiant n'était enregistré qu'en cas d'authentification réussie,
or aucune n'aboutissait, donc il était redemandé sans fin.

Il n'est plus transmis du tout. `POST /api/session` désigne désormais le compte
par l'adresse source de la requête, comme `GET /api/session` le fait depuis la
v0.8.6. Le serveur l'a toujours su ; le poste, lui, ne faisait que répéter un
renseignement qu'il pouvait taper de travers. C'est aussi plus sûr : sur
WireGuard l'adresse source est garantie par le routage par clé, alors qu'un
identifiant en clair permettait d'ouvrir l'accès d'un compte depuis n'importe
quelle adresse. Exige la v0.3.2 du serveur, qui accepte les deux formes.

Le champ disparaît de la fenêtre de saisie. Celui du panneau Administrateur
reste, purement indicatif désormais, avec une infobulle qui le dit.

Trois décisions d'interface se fondaient sur « un identifiant est-il
configuré ? », une question que le poste ne pouvait pas trancher : proposer ou
non la saisie automatique, le libellé de la ligne Accès distant, et l'aspect du
bouton. Elles suivent maintenant la réponse du serveur, qui distingue « aucun
pair à cette adresse » (401, nouveau) de « accès fermé, code attendu » (403).
Un poste non enrôlé n'est donc plus harcelé pour un code qui n'ouvrirait rien,
et la ligne affiche « Non géré par le serveur » au lieu de le supposer.

Corrige aussi une régression de la v0.9.0 : `tunnel_gateway` retenait la route
par défaut des IPs autorisées comme réseau de tunnel, et rendait donc 0.0.0.1
comme passerelle en tunnel intégral — sondage et validation partaient vers une
adresse inexistante. Elle ne retient plus que les réseaux contenant l'adresse
du client, préfère le plus spécifique, et se replie sur /24 (ou /64) faute de
candidat. Le diagnostic « Pair joignable dans le tunnel » en bénéficie aussi.

Enfin, `authenticate` distingue le silence du réseau d'un refus du serveur,
comme `session_status` le faisait déjà.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tuxgyver
2026-09-08 11:10:42 +02:00
co-authored by Claude Opus 5
parent b3c0515244
commit 023ec81e03
5 changed files with 120 additions and 66 deletions
+33 -5
View File
@@ -615,12 +615,34 @@ def _networks(value: str) -> list[ipaddress.IPv4Network | ipaddress.IPv6Network]
return nets
# Réseau supposé autour de l'adresse du client quand rien d'autre ne permet
# de le déterminer : les conventions les plus répandues pour un tunnel.
_TUNNEL_FALLBACK_PREFIX = {4: 24, 6: 64}
def tunnel_gateway(client_address: str, allowed_ips: str = "") -> str:
"""Première IP utilisable du réseau du tunnel (typiquement le serveur).
Une adresse client en /32 — la forme la plus courante — ne décrit aucun
réseau : on se replie alors sur le premier réseau des « IPs autorisées »,
qui contient le pair distant.
réseau : on se replie alors sur les « IPs autorisées ».
Trois pièges, tous rencontrés :
- Une route par défaut (`0.0.0.0/0`, `::/0`) est une IP autorisée
parfaitement banale — c'est le tunnel intégral — mais ne désigne aucun
réseau de tunnel. La retenir donnait pour passerelle son premier hôte,
`0.0.0.1`, injoignable par construction.
- Les IPs autorisées listent aussi les réseaux *distants*, joints à
travers le tunnel. Le serveur n'y est pas : il est dans le réseau du
tunnel, celui qui contient l'adresse du client. D'où le filtre sur
l'appartenance.
- Plusieurs réseaux peuvent contenir le client. Le plus spécifique est le
bon : un /16 englobant décrirait un plan d'adressage, pas ce tunnel-ci.
Faute de tout candidat, on suppose le réseau usuel autour de l'adresse du
client — /24 en IPv4, /64 en IPv6. C'est ce que faisait l'ancienne
déduction, et elle avait raison sur ce point : mieux vaut une convention
répandue qu'aucune adresse du tout.
"""
try:
iface = ipaddress.ip_interface(client_address.split(",")[0].strip())
@@ -630,9 +652,15 @@ def tunnel_gateway(client_address: str, allowed_ips: str = "") -> str:
candidates = []
if iface.network.prefixlen < iface.network.max_prefixlen - 1:
candidates.append(iface.network)
candidates += [n for n in _networks(allowed_ips)
if n.prefixlen < n.max_prefixlen - 1
and n.version == iface.ip.version]
usable = [n for n in _networks(allowed_ips)
if n.version == iface.ip.version
and 0 < n.prefixlen < n.max_prefixlen - 1
and iface.ip in n]
candidates += sorted(usable, key=lambda n: n.prefixlen, reverse=True)
if not candidates:
candidates.append(ipaddress.ip_network(
f"{iface.ip}/{_TUNNEL_FALLBACK_PREFIX[iface.ip.version]}",
strict=False))
for net in candidates:
for host in net.hosts():