envoy, Docker und Websockets - Fehlersuche und Konfiguration

Websockets sind eine aufregende Technologie, die es Ihnen ermöglicht, eine HTTP-Verbindung zu einer dauerhaften binären Verbindung auszubauen, die Sie zum Senden bidirektionaler Nachrichten verwenden können.

Nebenbei bemerkt kann das MQTT-Protokoll über Websockets transportiert werden - die einzige (?) Möglichkeit für einen JavaScript-Client, der beispielsweise von der Website bereitgestellt wird.

Da Websockets auf denselben Ports laufen wie der normale HTTP- und HTTPS-Verkehr (80 / 443), werden sie in Unternehmensnetzen mit größerer Wahrscheinlichkeit durchgelassen.

Envoy und Websockets

Envoy unterstützt Websockets. Es gibt einige Probleme:

JSON kann nicht als Proto geparst werden (INVALID_ARGUMENT:(route_config.virtual_hosts[3].routes[0].route) use_websocket: Feld kann nicht gefunden werden.):

Envoy unterstützte früher Websockets mit einer alten Direktive, "use_websocket". Auf einer aktuellen Envoy-Installation (z.B. verwende ich derzeit envoy 1.10.0-dev für Testzwecke) ist diese Direktive verschwunden und wurde ersetzt.

Die Idee ist, allgemeinere Upgrade-Anfragen/Möglichkeiten zu haben.

Die korrekte Syntax ist nun die Verwendung von upgrade_configs. Werfen Sie einen Blick auf meine envoy.yaml:

statische_Ressourcen:
   Hörer:
   - Adresse:
       socket_address:
         Adresse: 0.0.0.0
         port_wert: 80
     filter_chains:
     - Filter:
       - Name: envoy.http_connection_manager
         Konfiguration:
           upgrade_configs:
             - upgrade_type: Websocket

           codec_type: auto
           stat_prefix: ingress_http
           use_remote_address: true
           xff_num_trusted_hops: 0
           route_config:
             virtuelle_hosts:
             - Name: Fehlersuche
               Domänen: ["debug.local:80"]
               Routen:
                 - übereinstimmen: { prefix: "/" }
                   Route:
                     cluster: ziel_websocket
             - Name: Backend
               Domains: ["morpheus.local"]
               Routen:
               - übereinstimmen: { prefix: "/" }
                 umleiten:
                   path_redirect: "/"
                   https_redirect: true

(Schnipsel)

- Adresse:
     socket_address:
       Adresse: 0.0.0.0
       port_wert: 443
   filter_chains:
   - tls_context:
       common_tls_context:
         tls_zertifikate:
         - certificate_chain: { filename: "/etc/envoy/example.crt" }
           private_key: { filename: "/etc/envoy/example.key" }
         alpn_protocols: [ "h2,http/1.1" ]
     Filter:
     - Name: envoy.http_connection_manager
       Konfiguration:
         upgrade_configs:
           - upgrade_type: Websocket
         stat_prefix: ingress_https
         use_remote_address: true
         xff_num_trusted_hops: 0
         route_config:
           virtuelle_hosts:
           - Name: Fehlersuche
             Domains: ["debug.local:443"]
             Routen:
               - übereinstimmen: { prefix: "/" }
                 Route:
                   cluster: ziel_websocket


(Schnipsel)

Clustern:
- Name: target_dwebsocket
   connect_timeout: 0.25s
   Typ: strict_dns
   lb_policy: round_robin
   Gastgeber:
   - socket_address:
       Anschrift: dwse
       port_wert: 8080

Sie können diese upgrade_configs-Direktive laut Dokumentation an zwei Stellen verwenden: im http_connection_manager (wie in meinem Beispiel oben) und in einzelnen Routen.

          upgrade_configs:
            - upgrade_type: Websocket

Nicht übereinstimmende Routen

Ein weiterer wichtiger Punkt, den Sie beachten sollten, wenn Sie "404s" erhalten (Fehler: Unerwartete Serverantwort: 404): der :authority-Header wird aus irgendeinem Grund einschließlich des Ports gesetzt - und stimmt daher nicht überein, wenn Sie nur die Domain angeben.

Sehen Sie sich diese Route an:

["debug.local:80"]

Der Port wird in diesem Fall nach der Domäne angegeben. In meinen Tests konnte ich mich nur dann über envoy mit dem von mir eingerichteten Websocket-Server verbinden, wenn der Port angegeben war. Dies ist ein großer Stolperstein.

Als nächstes werde ich besprechen, wie man envoy in solchen Fällen debuggen kann.

Debugging envoy, Websockets, Docker:

Ausführliche Ausgabe von envoy

Hier ist meine docker-compose.yaml:

Version: '3.7'

Dienstleistungen:
   Gesandter:
     bauen:
       Kontext: ./
       Dockerfile: Dockerfile
     container_name: penvoyage-morpheus-envoy
     Häfen:
       – “80:80”
       – “443:443”
     Volumen:
       - Typ: Volumen
         Quelle: penvoyage_volume
         Ziel: /etc/envoy
     Netzwerke:
       - envoy_net
     1TP3Benutzer: "2000:2000"
     Benutzer: "root:root"
     Neustart: wenn nicht gestoppt
     Umwelt:
       Loglevel: Debug

Volumen:
   penvoyage_volume:
     extern:
       name: penvoyage_volume

Netzwerke:
   envoy_net:
     extern:
       Name: Mein-Brücken-Netz

Beachten Sie, dass eine Umgebungsvariable "loglevel: debug" gesetzt ist.

loglevel kann einer der folgenden Werte sein:

  • Spur
  • Fehlersuche
  • Infos
  • warnen

Das Starten des Containers wird nun viel mehr Output liefern:

docker-compose aufwärts

Beachten Sie, dass wir den Container nicht abkoppeln, um seine Ausgabe direkt zu sehen. (Wenn Sie zufrieden sind, können Sie es mit docker-compose up -d)

Ihre Ausgabe sollte nun in etwa so aussehen:

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

auf eine Verbindung von einem Websocket-Client, erhalten Sie die folgende Ausgabe:

penvoyage-morpheus-envoy | [2019-05-18 14:21:14.915][23][debug][main] [source/server/connection_handler_impl.cc:257] [C0] neue Verbindung
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.916][23][debug][http] [source/common/http/conn_manager_impl.cc:210] [C0] neuer Stream
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.918][23][debug][http] [source/common/http/conn_manager_impl.cc:548] [C0][S4222457489265630919] request headers complete (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 | 'Verbindung', 'Upgrade'
penvoyage-morpheus-envoy | Upgrade', '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] cluster 'target_dwebsocket' match for 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] Router decodiert Header:
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 | 'Verbindung', 'Upgrade'
penvoyage-morpheus-envoy | 'upgrade', 'websocket'
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-intern', 'wahr'
penvoyage-morpheus-envoy | 'x-request-id', 'ca8a765c-e549-4c45-988c-58b6c853df7b'
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] Erstellen einer neuen Verbindung
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.919][23][debug]

[source/common/http/codec_client.cc:26] [C1] Verbindungsaufbau
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.919][23][debug][connection] [source/common/network/connection_impl.cc:638] [C1] Verbindung zu 172.18.0.8:8080
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.919][23][debug][connection] [source/common/network/connection_impl.cc:647] [C1] Verbindung in Arbeit
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.919][23][debug][pool] [source/common/http/conn_pool_base.cc:20] Anfrage in der Warteschlange, da keine Verbindungen verfügbar sind
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.919][23][debug][connection] [source/common/network/connection_impl.cc:516] [C1] connected
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.919][23][debug] [source/common/http/codec_client.cc:64] [C1] verbunden
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.919][23][debug][pool] [source/common/http/http1/conn_pool.cc:236] [C1] Anhängen an nächste Anfrage
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.919][23][debug][router] [source/common/router/router.cc:1122] [C0][S4222457489265630919] Pool bereit
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.920][23][debug][router] [source/common/router/router.cc:669] [C0][S4222457489265630919] upstream headers complete: 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] Kodierung von Headern über Codec (end_stream=false):
penvoyage-morpheus-envoy | ':status', '101'
penvoyage-morpheus-envoy | 'upgrade', 'websocket'
penvoyage-morpheus-envoy | 'Verbindung', 'Upgrade'
penvoyage-morpheus-envoy | 'sec-websocket-accept', '2W9caJQU0JKW3MhWV6T8FHychjk='
penvoyage-morpheus-envoy | 'date', 'Sat, 18 May 2019 14:21:14 GMT'
penvoyage-morpheus-envoy | 'server', 'envoy'
penvoyage-morpheus-envoy | 'content-length', '0'
penvoyage-morpheus-envoy |

Beachten Sie die oben erwähnte Überschrift :authority:

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

Hier ist ein Autoritäts-Header für eine normale Website-Anfrage:

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

Beachten Sie das Vorhandensein des Ports im ersten Fall (Anfrage über Websocket) und das Fehlen des Ports im zweiten Fall.

Testcontainer

Bitte beachten Sie, dass https://hub.docker.com/r/dataferret/websocket-echo das dataferret/websocket-echo-Bild scheint zu sein gebrochen!

Es scheint eine Inkompatibilität zwischen Twisted / Autobahn und der verwendeten Version zu geben, so dass ich mich entschlossen habe, meine eigene Version zu entwickeln:

Wir werden einen Testcontainer bauen, der die Eingaben, die wir als Websocket an ihn senden, als Echo wiedergibt.

Hier ist die Dockerdatei:

FROM python:stretch
COPY assets/websocketd /usr/bin/websocketd
RUN chmod +x /usr/bin/websocketd
COPY assets/run.py /opt/run.py
WORKDIR /opt

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

Beachten Sie, dass bei der Verwendung von ENTRYPOINT beim Starten des Containers mit CMD aus docker-compose.yml der CMD-Wert als Parameter an den ENTRYPOINT-Befehl übergeben wird.

websocketd ist ein Websocket-Daemon. Sie können die eigenständige ausführbare Datei von hier beziehen:

http://websocketd.com/

Ich habe heruntergeladen https://github.com/joewalnes/websocketd/releases/download/v0.3.0/websocketd-0.3.0-linux_amd64.zip und entpackte es in den Ordner assets, den ich für meinen Docker-Container erstellt habe.

Die allgemeine Idee dieser raffinierten Anwendung ist es, einen Wrapper um Ihre Anwendung herum bereitzustellen, um Websockets bereitstellen zu können.

Alles, was Ihre Anwendung tun muss, ist, auf stdin zu hören und die Antworten / jegliche Kommunikation auf stdout zu schreiben.

run.py - Dies ist ein einfaches Skript, das alles ausgibt, was in es geschrieben wird:

#!/usr/bin/python
System einführen

while True:
     read_co = sys.stdin.readline()
     print(read_co, end="")
     sys.stdout.flush()

Beachten Sie die Verwendung von readline(), das die Zeichenkette sofort nach einem Zeilenumbruch zurückgibt. Andernfalls müssen Sie mit Puffern arbeiten bzw. darauf warten, dass die Eingabe akkumuliert wird.

Beachten Sie auch die Funktion sys.stdout.flush()

Zum Schluss noch die docker-compose.yml:

Version: '3.6'

Dienstleistungen:
   dwse:
     bauen:
       Kontext: ./
       Dockerfile: Dockerfile
     container_name: dwse
     Hostname: dwse
     Häfen:
       – “8078:8080”
     Netzwerke:
       - envoy_net
     Neustart: immer
     Befehl: "80"
     1TP3Benutzer: "root:root"

Netzwerke:
   envoy_net:
     extern:
       Name: Mein-Brücken-Netz

Beachten Sie, dass ich es in dasselbe Netzwerk "my-bridge-network" wie envoy oben eingefügt habe.

der Befehl hier ist ein zufälliges "80", das von der Anwendung nicht interpretiert wird, aber Sie können damit auch komplexere Befehlszeilen usw. erstellen.

Fehlersuche

Schließlich möchten wir, dass ein Client eine Verbindung herstellt und in der Lage ist, die Nachrichten zu sehen, die wir an unseren Test-Websocket-Dienst senden, der durch envoy vermittelt wird.

Ich schlage vor wscat für diese.

wscat ist ein npm (node.js) Paket. Daher müssen Sie zuerst npm installieren:

apt install npm

Dann können Sie wscat installieren:

npm install -g wscat

Testen Sie wscat mit einem öffentlichen Websocket-Echodienst:

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

Bild

erhalten Sie eine Liste der Befehlszeilenoptionen für wscat:

wscat -help

Bild

Um eine sichere Websocket-Verbindung über envoy mit selbstsignierten Zertifikaten zu prüfen, gehen Sie wie folgt vor

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

Bild

Hinweise

Unerwartete Serverantwort

Fehler: Unerwartete Serverantwort: 404

Schauen Sie sich Ihre Route an: Haben Sie den Port in der Domain angepasst, wie oben beschrieben? Offenbar scheint dies für Websocket-Clients erforderlich zu sein.

Im Zweifelsfall sollten Sie am Ende Ihrer virtuellen Hosts eine Match-All-Domain einfügen, um die Fehlersuche zu erleichtern. Ich schlage vor, einen festen Pfad anstelle eines Präfixes zu verwenden - durch Ändern des Pfades, den Sie in wscat verwenden, können Sie auf diese Weise überprüfen, welche Regel erfüllt wurde:

- Name: matcheverything
   Domänen: ["*"]
   Routen:
     - übereinstimmen: { path: "/mqtt" }
       Route: { cluster: target_dwebsocket }

JSON kann nicht geparst werden / Feld kann nicht gefunden werden:

JSON kann nicht als Proto geparst werden (INVALID_ARGUMENT:(route_config.virtual_hosts[3].routes[0].route) use_websocket: Feld kann nicht gefunden werden.):

Wie bereits erwähnt, handelt es sich hierbei um eine alte Konfiguration, die durch die neue Konfiguration abgelöst wurde:

          upgrade_configs:
            - upgrade_type: Websocket

keine Clusterübereinstimmung für URL

[source/common/router/router.cc:278] [C0][S5067510418337690596] kein Cluster-Match für URL '/'

Siehe oben - die URL wird anscheinend (in meinen Tests) einschließlich des Ports für Websockets abgeglichen, also fügen Sie eine Regel ein, um alle Domänen abzugleichen, und debuggen Sie von dort aus / verwenden Sie die Syntax, die ich oben angegeben habe:

          route_config:
            virtuelle_hosts:
            - Name: Fehlersuche
              Domänen: ["debug.local:80"]
              Routen:
                - übereinstimmen: { prefix: "/" }
                  Route:
                    cluster: ziel_websocket

selbstsigniertes Zertifikat

Fehler: selbst signiertes Zertifikat

Führen Sie wscat mit dem Befehlszeilenparameter -no-check aus (dies ist ein doppelter Bindestrich, den WordPress durcheinanderbringt):

Bild

Websocket-Verbindung fehlgeschlagen:

pcp-code.js:15 WebSocket-Verbindung zu 'wss://key:[email protected]/mqtt' fehlgeschlagen: Fehler beim WebSocket-Handshake: Unerwarteter Antwortcode: 403

Siehe obige Informationen: Höchstwahrscheinlich ist diese Javascript-Fehlermeldung auf eine Fehlkonfiguration der Domainübereinstimmung zurückzuführen (siehe oben).

Fehler auf der Autobahn:

"Um txaio zu verwenden, müssen Sie zuerst ein Framework auswählen " exceptions.RuntimeError: Um txaio zu verwenden, müssen Sie zunächst ein Framework mit .use_twisted() oder .use_txaio() auswählen

Dies ist der Fehler, den ich beim Ausführen des dataferret/websocket-echo-Images erhielt. Ich schlage vor, meinen Code zu verwenden, den ich oben als Alternative angegeben habe.

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

Referenzen: