Retour au blog
Ingénierie9 min de lecture

Déboguer les connexions WebSocket OCPP : 1006, sous-protocole et TLS

Une borne qui refuse de se connecter ne vous laisse presque rien : un code de fermeture, peut-être une ligne de log. Voici comment lire les échecs de connexion OCPP-J — 1006, négociation de sous-protocole, TLS et les tempêtes de reconnexion qu’ils provoquent.

I
Infinite Service Team

Toute intégration OCPP se heurte au même mur au moins une fois. La borne est alimentée, le réseau fonctionne, et le CSMS n’affiche rien. Pas de BootNotification, pas d’erreur, juste le silence — ou une ligne de log qui dit 1006 et s’arrête là.

OCPP-J s’appuie sur WebSocket, et presque tout ce qui casse avant votre premier message casse dans la poignée de main WebSocket. Voici un guide de terrain des défaillances que nous rencontrons le plus souvent.

Le code 1006 veut dire « je n’ai rien à vous dire »

1006 est le code de fermeture qui apparaît le plus souvent dans les logs de bornes, et c’est le moins informatif de la spécification. Il est défini comme une *fermeture anormale* : la connexion est morte sans qu’aucune trame de fermeture WebSocket n’ait été échangée.

L’essentiel à comprendre, c’est que 1006 ne circule jamais sur le réseau. C’est une invention locale — la bibliothèque WebSocket de la borne le fabrique pour décrire le fait que la connexion TCP s’est évaporée sous elle. Il ne vous dit donc rien de l’avis du CSMS, puisque le CSMS n’a jamais eu l’occasion d’en exprimer un.

Cela réduit la cause aux couches situées sous OCPP :

  • la connexion TCP a été réinitialisée ou a expiré
  • la négociation TLS a échoué et la socket a été démolie
  • un intermédiaire — répartiteur de charge, proxy inverse, NAT d’opérateur mobile — a coupé une connexion inactive
  • la requête d’upgrade HTTP a été refusée et le client a fermé avant de lire la réponse

Si vous obtenez 1006 immédiatement à la connexion, c’est la poignée de main. Si vous l’obtenez après des minutes ou des heures de trafic sain, c’est presque toujours un délai d’inactivité, et le correctif est l’intervalle de heartbeat, pas quelque chose dans votre code.

La négociation de sous-protocole est le tueur silencieux

OCPP-J annonce sa version via l’en-tête de sous-protocole WebSocket. La borne envoie :

GET /ocpp/CP001 HTTP/1.1
Host: csms.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Protocol: ocpp1.6

Le serveur doit renvoyer exactement l’un des protocoles proposés :

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: ocpp1.6

Trois choses se passent mal ici, et toutes les trois produisent le même symptôme — une connexion qui s’ouvre et se referme aussitôt.

Le serveur omet complètement l’en-tête. Beaucoup de bibliothèques WebSocket acceptent l’upgrade de bon cœur et ne renvoient tout simplement pas Sec-WebSocket-Protocol. Un client OCPP strict interprète cette absence comme une négociation échouée et ferme. Votre CSMS journalise une connexion réussie ; la borne journalise un échec. Les deux disent vrai.

Les valeurs ne correspondent pas exactement. Le jeton de sous-protocole est ocpp1.6, pas ocpp1.6j, OCPP1.6 ni ocpp-1.6. La comparaison de chaînes distingue la casse et il n’y a aucune étape de normalisation. Une borne qui propose ocpp1.6 et un serveur qui répond OCPP1.6 ne se sont mis d’accord sur rien.

Le serveur choisit un protocole qui n’a pas été proposé. Si une borne ne propose que ocpp1.6 et que le CSMS répond ocpp2.0.1 parce que c’est sa version préférée, le client est tenu de faire échouer la connexion.

Quand une borne propose plusieurs versions, elles arrivent par ordre de préférence :

Sec-WebSocket-Protocol: ocpp2.0.1, ocpp1.6

Le serveur devrait retenir la première qu’il prend en charge, pas la dernière qu’il a analysée.

Des problèmes TLS qui ressemblent à des problèmes réseau

En production, OCPP passe par wss://, et les piles TLS des bornes ont fréquemment des années de retard sur les serveurs auxquels elles parlent. Les échecs sont d’une généralité désespérante.

Le magasin de confiance est périmé ou absent. Les bornes embarquées sont livrées avec un paquet d’autorités de certification gravé dans le firmware. Si le certificat de votre CSMS remonte à une racine ajoutée aux magasins publics après la compilation du firmware, la borne ne peut pas le valider. Rien ne le montrera côté serveur — du point de vue du CSMS, le client a simplement raccroché pendant la poignée de main.

La chaîne intermédiaire est incomplète. Les navigateurs masquent un certificat intermédiaire manquant en allant le chercher eux-mêmes. Les clients TLS embarqués, généralement pas. Un site qui s’ouvre parfaitement dans Chrome peut être injoignable pour toutes les bornes de votre parc. Servez la chaîne complète, pas seulement la feuille.

SNI est absent. Un firmware de borne ancien peut ne pas envoyer d’indication de nom de serveur (SNI). Si votre CSMS est derrière un hôte qui a besoin de SNI pour choisir un certificat, la connexion reçoit le certificat par défaut — qui ne correspondra pas, et la borne le rejettera.

La version du protocole est trop ancienne. Une borne qui ne parle que TLS 1.0 ou 1.1 ne peut pas se connecter à un serveur qui exige 1.2 ou plus. C’est le seul cas où la bonne réponse est généralement de mettre la borne à jour plutôt que d’affaiblir le serveur.

Les tempêtes de reconnexion

Dès qu’un parc commence à échouer, la défaillance tend à s’amplifier. Une borne qui n’arrive pas à se connecter réessaie ; si toutes les bornes réessaient au même intervalle fixe, elles se synchronisent, et le CSMS voit le parc entier arriver dans la même seconde, encore et encore.

Deux choses l’évitent, et OCPP n’en impose aucune : c’est donc à vous de les construire.

  • Un repli exponentiel plafonné. Réessayer après 1 s, 2 s, 4 s, 8 s, jusqu’à un plafond de quelques minutes.
  • De la gigue. Randomisez chaque délai de ±20 %. Sans gigue, le repli synchronise encore le parc — il le synchronise simplement à intervalles plus longs.

Le même problème surgit après un déploiement du CSMS. Toutes les bornes tombent en même temps, et toutes se reconnectent en même temps. Le repli et la gigue sont ce qui transforme une ruée en une montée douce.

Lire la poignée de main directement

Quand les logs se contredisent, regardez le réseau. Un seul curl vous dit si le serveur négocie correctement le sous-protocole :

curl -i -N \
  -H "Connection: Upgrade" \
  -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Version: 13" \
  -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
  -H "Sec-WebSocket-Protocol: ocpp1.6" \
  https://csms.example.com/ocpp/CP001

Vous voulez 101 Switching Protocols et un en-tête Sec-WebSocket-Protocol: ocpp1.6 dans la réponse. Si vous obtenez 101 sans l’en-tête, vous avez trouvé votre bug — et il est dans le CSMS, pas dans la borne.

Pour la couche TLS, openssl s_client -connect csms.example.com:443 -showcerts affiche la chaîne complète que le serveur présente réellement, ce qui est le moyen le plus rapide de repérer un intermédiaire manquant.

Là où un simulateur aide

Le plus dur dans tout cela, c’est que vous déboguez deux implémentations à la fois et que vous n’en voyez qu’une. Un simulateur de borne vous donne un client dont vous savez qu’il est bon : si SimPilot se connecte à votre CSMS et que votre borne non, le bug est dans la borne. Si aucun des deux ne se connecte, c’est le serveur.

Dans l’autre sens, TestPilot joue le rôle d’un CSMS dont vous savez qu’il est bon : vous pouvez pointer une borne physique vers lui et vérifier que la poignée de main et le premier BootNotification sont propres, bien avant que votre propre serveur entre dans l’histoire.

Dans les deux cas la valeur est la même : remplacez l’une des deux inconnues par quelque chose en quoi vous avez confiance, et il ne reste plus qu’un seul endroit où la défaillance puisse se cacher.

Articles liés