envoy, docker et websockets - débogage et configuration

Les websockets sont une technologie passionnante, qui vous permet de transformer une connexion HTTP en une connexion binaire persistante de longue durée, que vous pouvez utiliser pour envoyer des messages bidirectionnels.

Soit dit en passant, le protocole MQTT peut être transporté à l'aide de websockets - ce qui est le seul ( ?) moyen pour un client JavaScript livré par le site web, par exemple.

Dans tous les cas, comme les websockets fonctionnent sur les mêmes ports que le trafic HTTP et HTTPS normal (80 / 443), les réseaux d'entreprise sont plus susceptibles de les laisser passer.

Envoy et websockets

Envoy supporte les websockets. Il y a quelques problèmes :

Impossible d'analyser JSON comme proto (INVALID_ARGUMENT :(route_config.virtual_hosts[3].routes[0].route) use_websocket : Impossible de trouver le champ.):

Envoy avait l'habitude de supporter les websockets avec une ancienne directive, "use_websocket". Sur une installation actuelle d'Envoy (par exemple, j'utilise actuellement Envoy 1.10.0-dev à des fins de test), cette directive a disparu et a été remplacée.

L'idée est d'avoir des demandes/possibilités de mise à niveau plus génériques.

La syntaxe correcte consiste maintenant à utiliser mise à niveau_configs. Jetez un coup d'oeil à mon envoy.yaml :

ressources_statiques :
   des auditeurs :
   - l'adresse :
       adresse_socle :
         adresse : 0.0.0.0
         valeur_du_port : 80
     chaînes de filtres :
     - filtres :
       - nom : envoy.http_connection_manager
         config :
           upgrade_configs :
             - upgrade_type : websocket

           codec_type : auto
           stat_prefix : ingress_http
           use_remote_address : true
           xff_num_trusted_hops : 0
           route_config :
             virtual_hosts :
             - nom : debug
               domaines : ["debug.local:80"]
               routes :
                 - match : { préfixe : "/" }
                   route :
                     cluster : target_dwebsocket
             - nom : backend
               domaines : ["morpheus.local"]
               routes :
               - match : { préfixe : "/" }
                 réorienter :
                   path_redirect : "/"
                   https_redirect : true

(snip)

- l'adresse :
     adresse_socle :
       adresse : 0.0.0.0
       port_value : 443
   chaînes de filtres :
   - tls_context :
       common_tls_context :
         tls_certificats :
         - certificate_chain : { filename : "/etc/envoy/example.crt" }
           private_key : { filename : "/etc/envoy/example.key" }
         alpn_protocols : [ "h2,http/1.1" ]
     filtres :
     - nom : envoy.http_connection_manager
       config :
         upgrade_configs :
           - upgrade_type : websocket
         stat_prefix : ingress_https
         use_remote_address : true
         xff_num_trusted_hops : 0
         route_config :
           virtual_hosts :
           - nom : debug
             domaines : ["debug.local:443"]
             routes :
               - match : { préfixe : "/" }
                 route :
                   cluster : target_dwebsocket


(snip)

clusters :
- nom : target_dwebsocket
   connect_timeout : 0.25s
   type : strict_dns
   lb_policy : round_robin
   hôtes :
   - adresse_socle :
       adresse : dwse
       valeur_du_port : 8080

Vous pouvez utiliser cette directive upgrade_configs à deux endroits, selon la documentation. sur http_connection_manager (comme dans mon exemple ci-dessus), et sur les routes individuelles.

          upgrade_configs :
            - upgrade_type : websocket

Routes ne correspondant pas

L'autre chose importante à noter, si vous obtenez des "404" ( error : Unexpected server response : 404 ) : l'en-tête :authority sera, pour une raison étrange, défini en incluant le port - et ne correspondra donc pas si vous fournissez uniquement le domaine.

Regardez cette route :

["debug.local:80"]

Dans ce cas, le port est spécifié après le domaine. Lors de mes tests, ce n'est que si le port était spécifié que je pouvais me connecter via envoy au serveur websocket que j'ai mis en place. C'est une pierre d'achoppement majeure.

La prochaine fois, je vais discuter, comment déboguer envoy dans des cas comme celui-ci.

Débogage d'Envoy, websockets, Docker :

Rendre la sortie d'envoy verbeuse

Voici mon docker-compose.yaml :

version : " 3.7 ".

services :
   envoyé :
     construire :
       contexte : ./
       dockerfile : Dockerfile
     nom_du_conteneur : penvoyage-morpheus-envoy
     ports :
       – “80:80”
       – “443:443”
     volumes :
       - type : volume
         source : penvoyage_volume
         cible : /etc/envoy
     réseaux :
       - envoy_net
     #user : "2000:2000"
     utilisateur : "root:root"
     redémarrage : à moins qu'il ne s'agisse d'un arrêt
     l'environnement :
       loglevel : debug

volumes :
   penvoyage_volume :
     externe :
       nom : penvoyage_volume

réseaux :
   envoy_net :
     externe :
       nom : my-bridge-network

Notez qu'une variable d'environnement "loglevel : debug" est définie.

loglevel peut être l'un des suivants :

  • trace
  • déboguer
  • info
  • avertir

Le démarrage du conteneur donne maintenant beaucoup plus de résultats :

docker-compose up

Notez que nous ne nous détachons pas du conteneur, pour voir directement sa sortie. (Une fois que vous êtes satisfait, vous pouvez l'exécuter en utilisant docker-compose up -d)

Votre résultat devrait maintenant ressembler à quelque chose comme ceci :

penvoyage-morpheus-envoy | [2019-05-18 14:20:32.093][1][debug][main] [source/server/server.cc:143] vidage des statistiques

sur une connexion d'un client websocket, vous obtiendrez le résultat suivant :

penvoyage-morpheus-envoy | [2019-05-18 14:21:14.915][23][debug][main] [source/server/connection_handler_impl.cc:257] [C0] nouvelle connexion
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.916][23][debug][http] [source/common/http/conn_manager_impl.cc:210] [C0] nouveau flux
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.918][23][debug][http] [source/common/http/conn_manager_impl.cc:548] [C0][S4222457489265630919] en-têtes de requête complets (end_stream=false) :
penvoyage-morpheus-envoy | ':authority', 'debug.local:80'.
penvoyage-morpheus-envoy | ':path", "/mqtt_drinks_are_free
penvoyage-morpheus-envoy | ':method', 'GET
penvoyage-morpheus-envoy | 'sec-websocket-version', '13'
penvoyage-morpheus-envoy | 'sec-websocket-key', 'HQFiCTWiFMktGDPFXwzrjQ=='
penvoyage-morpheus-envoy | 'connexion', 'Mise à jour'.
penvoyage-morpheus-envoy | mise à jour, websocket
penvoyage-morpheus-envoy | 'sec-websocket-extensions', 'permessage-deflate ; client_max_window_bits'.
penvoyage-morpheus-envoy |
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.918][23][debug][router] [source/common/router/router.cc:321] [C0][S4222457489265630919] correspondance du cluster 'target_dwebsocket' pour l'URL '/mqtt_drinks_are_free'.
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.918][23][debug][router] [source/common/router/router.cc:379] [C0][S4222457489265630919] routeur décodant les en-têtes :
penvoyage-morpheus-envoy | ':authority', 'debug.local:80'.
penvoyage-morpheus-envoy | ':path', '/mqtt_drinks_are_free
penvoyage-morpheus-envoy | ':method', 'GET
penvoyage-morpheus-envoy | ':scheme', 'http
penvoyage-morpheus-envoy | 'sec-websocket-version', '13'
penvoyage-morpheus-envoy | 'sec-websocket-key', 'HQFiCTWiFMktGDPFXwzrjQ=='
penvoyage-morpheus-envoy | 'connexion', 'Mise à jour'.
penvoyage-morpheus-envoy | 'upgrade', 'websocket' (mise à jour)
penvoyage-morpheus-envoy | 'sec-websocket-extensions', 'permessage-deflate ; client_max_window_bits'.
penvoyage-morpheus-envoy | 'content-length', '0'
penvoyage-morpheus-envoy | 'x-forwarded-for', '192.168.1.2'
penvoyage-morpheus-envoy | 'x-forwarded-proto', 'http
penvoyage-morpheus-envoy | 'x-envoy-internal', 'true
penvoyage-morpheus-envoy | 'x-request-id', 'ca8a765c-e549-4c45-988c-58b6c853df7b', 'x-request-id'.
penvoyage-morpheus-envoy | 'x-envoy-expected-rq-timeout-ms', '15000'
penvoyage-morpheus-envoy |
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.918][23][debug][pool] [source/common/http/http1/conn_pool.cc:82] création d'une nouvelle connexion
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.919][23][debug]

[source/common/http/codec_client.cc:26] [C1] connexion
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.919][23][debug][connexion] [source/common/network/connection_impl.cc:638] [C1] connexion à 172.18.0.8:8080
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.919][23][debug][connexion] [source/common/network/connection_impl.cc:647] [C1] connexion en cours
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.919][23][debug][pool] [source/common/http/conn_pool_base.cc:20] mise en file d'attente de la demande en raison d'aucune connexion disponible
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.919][23][debug][connexion] [source/common/network/connection_impl.cc:516] [C1] connecté
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.919][23][debug] [source/common/http/codec_client.cc:64] [C1] connecté
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.919][23][debug][pool] [source/common/http/http1/conn_pool.cc:236] [C1] attachement à la prochaine requête
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.919][23][debug][router] [source/common/router/router.cc:1122] [C0][S4222457489265630919] pool prêt
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.920][23][debug][router] [source/common/router/router.cc:669] [C0][S4222457489265630919] en-têtes amont complets : end_stream=false
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.920][23][debug][http] [source/common/http/conn_manager_impl.cc:1234] [C0][S4222457489265630919] encodage des en-têtes via le codec (end_stream=false) :
penvoyage-morpheus-envoy | ':status', '101'
penvoyage-morpheus-envoy | 'upgrade', 'websocket' (mise à jour)
penvoyage-morpheus-envoy | 'connexion', 'Mise à jour'.
penvoyage-morpheus-envoy | 'sec-websocket-accept', '2W9caJQU0JKW3MhWV6T8FHychjk='
penvoyage-morpheus-envoy | "date", "Sat, 18 May 2019 14:21:14 GMT
penvoyage-morpheus-envoy | 'serveur', 'envoy'.
penvoyage-morpheus-envoy | 'content-length', '0'
penvoyage-morpheus-envoy |

Notez l'en-tête :authority que j'ai mentionné plus haut :

':authority', 'debug.local:80'.

Voici un en-tête d'autorité pour une demande normale de site web :

penvoyage-morpheus-envoy | ':method', 'GET
penvoyage-morpheus-envoy | ':authority', 'morpheus.local
penvoyage-morpheus-envoy | ':scheme', 'https

Notez la présence du port dans le premier cas (demande via websocket), et l'absence du port dans le second cas.

Testcontainer

Veuillez noter que https://hub.docker.com/r/dataferret/websocket-echo l'image dataferret/websocket-echo semble être brisé!

Il semble y avoir une certaine incompatibilité dans twisted / Autobahn pour la version particulière qui a été utilisée, et j'ai donc décidé de simplement rouler mon propre :

Nous allons construire un conteneur de test qui fera écho à l'entrée que nous lui envoyons par websocket.

Voici le Dockerfile:

FROM python:stretch
COPY assets/websocketd /usr/bin/websocketd
RUN chmod +x /usr/bin/websocketd
COPY assets/run.py /opt/run.py
répertoire de travail /opt

EXPOSE 8080
ENTRYPOINT ["websocketd", "-port=8080", "python3", "/opt/run.py"]

Notez qu'en utilisant ENTRYPOINT, lorsque le conteneur est démarré avec CMD à partir de docker-compose.yml, la valeur CMD sera passée comme paramètre à la commande ENTRYPOINT.

websocketd est un démon websocket, vous pouvez obtenir l'exécutable autonome ici :

http://websocketd.com/

J'ai téléchargé https://github.com/joewalnes/websocketd/releases/download/v0.3.0/websocketd-0.3.0-linux_amd64.zip et l'a décompressé dans le dossier des actifs que j'ai créé pour mon conteneur docker.

L'idée générale de cette application astucieuse est de fournir une enveloppe autour de votre application pour pouvoir servir des websockets.

Tout ce que votre application doit faire, c'est écouter sur stdin, et écrire les réponses / toute communication sur stdout.

run.py - Il s'agit d'un script simple qui fera écho à tout ce qui lui est écrit :

#!/usr/bin/python
importer sys

pendant que Vrai :
     read_co = sys.stdin.readline()
     print(read_co, end="")
     sys.stdout.flush()

Remarquez l'utilisation de readline(), qui retournera la chaîne immédiatement après une nouvelle ligne. Sinon, vous devez gérer les tampons et attendre que l'entrée s'accumule.

Remarquez aussi que sys.stdout.flush()

Enfin, voici le docker-compose.yml:

version : "3.6

services :
   dwse :
     construire :
       contexte : ./
       dockerfile : Dockerfile
     nom_du_conteneur : dwse
     nom d'hôte : dwse
     ports :
       – “8078:8080”
     réseaux :
       - envoy_net
     redémarrage : toujours
     commande : "80"
     #user : "root:root"

réseaux :
   envoy_net :
     externe :
       nom : my-bridge-network

Remarquez que je l'ai mis dans le même réseau "my-bridge-network" que envoy ci-dessus.

la commande ici est un "80" aléatoire qui n'est pas interprété par l'application, mais vous pourriez créer des lignes de commande plus élaborées, etc. en utilisant ceci.

Débogage

Enfin, nous voulons qu'un client se connecte et soit capable de voir les messages que nous envoyons à notre service websocket de test, qui est proxié par envoy.

Je propose wscat pour ça.

wscat est un paquet npm (node.js). Par conséquent, vous devez d'abord installer npm :

apt install npm

Ensuite, vous pouvez installer wscat :

npm install -g wscat

Testez wscat avec un service public d'écho websocket :

wscat -c ws://echo.websocket.org

image

obtenir une liste d'options de ligne de commande pour wscat :

wscat -help

image

Pour vérifier une connexion websocket sécurisée via envoy avec des certificats auto-signés, faites :

wscat -c ws://debug.local/mqtt_drinks_are_free -no-check

image

Conseils

Réponse inattendue du serveur

erreur : Réponse inattendue du serveur : 404

Jetez un coup d'oeil à votre route : mettez-vous le port dans le domaine pour correspondre, comme discuté ci-dessus ? Apparemment, cela semble être nécessaire pour les clients websocket.

En cas de doute, incluez un domaine match all à la fin de vos hôtes virtuels, pour faciliter le débogage. Je suggère de définir un chemin d'accès fixe pour cette correspondance, au lieu d'un préfixe - en changeant le chemin que vous utilisez dans wscat, de cette façon, vous pouvez vérifier quelle règle a été satisfaite :

- nom : matcheverything
   domaines : ["*"]
   routes :
     - match : { path : "/mqtt" }
       route : { cluster : target_dwebsocket }

Impossible d'analyser le JSON / Impossible de trouver le champ :

Impossible d'analyser JSON comme proto (INVALID_ARGUMENT :(route_config.virtual_hosts[3].routes[0].route) use_websocket : Impossible de trouver le champ.):

Comme indiqué plus haut, il s'agit d'un ancien style de configuration qui a été remplacé par la nouvelle configuration :

          upgrade_configs :
            - upgrade_type : websocket

aucun cluster ne correspond à l'URL

[source/common/router/router.cc:278] [C0][S5067510418337690596] Aucun cluster ne correspond à l'URL '/'.

Voir ci-dessus - l'URL correspond y compris le port pour les Websockets apparemment (dans mes tests), donc inclure une règle pour correspondre à tous les domaines, et déboguer à partir de là / utiliser la syntaxe que j'ai fourni ci-dessus :

          route_config :
            virtual_hosts :
            - nom : debug
              domaines : ["debug.local:80"]
              routes :
                - match : { préfixe : "/" }
                  route :
                    cluster : target_dwebsocket

certificat auto-signé

erreur : certificat auto-signé

Exécutez wscat avec le paramètre de ligne de commande -no-check (il s'agit d'un double tiret que WordPress va gâcher) :

image

La connexion Websocket a échoué :

pcp-code.js:15 La connexion WebSocket à 'wss://key:[email protected]/mqtt' a échoué : Erreur pendant l'établissement de la connexion WebSocket : Unxpected response code : 403

Voir les informations ci-dessus : il est fort probable que ce message d'erreur javascript soit dû à une mauvaise configuration de la correspondance de domaine (voir ci-dessus).

erreur d'autobahn :

" Pour utiliser txaio, vous devez d'abord sélectionner un framework " exceptions.RuntimeError : Pour utiliser txaio, vous devez d'abord sélectionner un framework avec .use_twisted() ou .use_txaio().

Voici l'erreur que j'ai obtenue en exécutant l'image dataferret/websocket-echo. Je vous suggère d'utiliser le code que j'ai fourni ci-dessus comme alternative.

Réf : https://stackoverflow.com/questions/34157314/autobahn-websocket-issue-while-running-with-twistd-using-tac-file

Références :