envoy, docker e websockets - debug e configurazione

I websockets sono una tecnologia eccitante, che permette di aggiornare una connessione HTTP a una connessione binaria persistente di lunga durata, che si può usare per inviare messaggi bidirezionali.

Per inciso, il protocollo MQTT può essere trasportato usando websockets - che è l'unico (?) modo per un client JavaScript consegnato dal sito web, per esempio.

In ogni caso, poiché i websockets girano sulle stesse porte del normale traffico HTTP e HTTPS (80 / 443), le reti aziendali sono più propense a lasciarli passare.

Envoy e websockets

Envoy supporta i websockets. Ci sono alcuni inconvenienti:

Impossibile analizzare JSON come proto (INVALID_ARGUMENT:(route_config.virtual_hosts[3].routes[0].route) use_websocket: Impossibile trovare il campo.):

Envoy supportava i websockets con una vecchia direttiva, "use_websocket". Su un'installazione attuale di envoy (ad esempio io attualmente uso envoy 1.10.0-dev per scopi di test) questa direttiva non c'è più ed è stata sostituita.

L'idea è quella di avere richieste/possibilità di aggiornamento più generiche.

La sintassi corretta ora è usare aggiornamento_configurazioni. Date un'occhiata al mio envoy.yaml:

risorse statiche:
   ascoltatori:
   - indirizzo:
       indirizzo_presa:
         indirizzo: 0.0.0.0
         valore della porta: 80
     catene_di_filtri:
     - filtri:
       - nome: envoy.http_connection_manager
         config:
           upgrade_configs:
             - tipo di aggiornamento: websocket

           tipo di codec: auto
           prefisso statico: ingress_http
           indirizzo_remoto: vero
           xff_num_trusted_hops: 0
           route_config:
             virtual_hosts:
             - nome: debug
               domini: ["debug.local:80"]
               percorsi:
                 - match: { prefix: "/" }
                   percorso:
                     cluster: target_dwebsocket
             - nome: backend
               domini: ["morpheus.local"]
               percorsi:
               - match: { prefix: "/" }
                 reindirizzare:
                   path_redirect: "/"
                   https_redirect: vero

(snip)

- indirizzo:
     indirizzo_presa:
       indirizzo: 0.0.0.0
       valore_porta: 443
   catene_di_filtri:
   - tls_context:
       common_tls_context:
         tls_certificati:
         - certificate_chain: { filename: "/etc/envoy/example.crt" }
           private_key: { nome file: "/etc/envoy/example.key" }
         alpn_protocols: [ "h2,http/1.1" ]
     filtri:
     - nome: envoy.http_connection_manager
       config:
         upgrade_configs:
           - tipo di aggiornamento: websocket
         prefisso statico: ingress_https
         indirizzo_remoto: vero
         xff_num_trusted_hops: 0
         route_config:
           virtual_hosts:
           - nome: debug
             domini: ["debug.local:443"]
             percorsi:
               - match: { prefix: "/" }
                 percorso:
                   cluster: target_dwebsocket


(snip)

gruppi:
- nome: target_dwebsocket
   connect_timeout: 0.25s
   tipo: strict_dns
   lb_policy: round_robin
   ospiti:
   - indirizzo_presa:
       indirizzo: dwse
       valore della porta: 8080

Puoi usare questa direttiva upgrade_configs in due posti, secondo la documentazione. su http_connection_manager (come nel mio esempio sopra), e sulle singole rotte.

          upgrade_configs:
            - tipo di aggiornamento: websocket

Percorsi che non corrispondono

L'altra cosa importante da notare, se si ottengono "404" (errore: Unexpected server response: 404): l'intestazione :authority sarà, per qualche strana ragione, impostata includendo la porta - e quindi non corrisponderà se si fornisce solo il dominio.

Guardate questo percorso:

["debug.local:80"]

La porta è specificata dopo il dominio in questo caso. Nei miei test - solo se la porta è stata specificata, sono stato in grado di connettermi attraverso envoy al server websocket che ho impostato. Questo è un grande ostacolo.

Prossimamente, discuterò come eseguire il debug di envoy in casi come questo.

Debug di envoy, websockets, Docker:

Rendere l'output di envoy verboso

Ecco il mio docker-compose.yaml:

versione: '3.7'

servizi:
   inviato:
     costruire:
       contesto: ./
       dockerfile: Dockerfile
     nome_contenitore: penvoyage-morpheus-envoy
     porti:
       – “80:80”
       – “443:443”
     volumi:
       - tipo: volume
         fonte: penvoyage_volume
         obiettivo: /etc/envoy
     reti:
       - envoy_net
     #user: "2000:2000"
     utente: "root:root"
     riavvio: a meno che non si sia fermato
     ambiente:
       livello di log: debug

volumi:
   penvoyage_volume:
     esterno:
       nome: penvoyage_volume

reti:
   envoy_net:
     esterno:
       nome: my-bridge-network

Notate che è impostata una variabile d'ambiente "loglevel: debug".

loglevel può essere uno di:

  • traccia
  • debug
  • info
  • avvertire

L'avvio del contenitore produrrà ora molto più output:

docker-comporre su

Notate che non ci stacchiamo dal contenitore, per vedere direttamente il suo output. (Una volta che siete soddisfatti, potete eseguirlo usando docker-compose up -d)

Il vostro output dovrebbe ora assomigliare a questo:

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

su una connessione da un client websocket, otterrete il seguente output:

penvoyage-morpheus-envoy | [2019-05-18 14:21:14.915][23][debug][main] [source/server/connection_handler_impl.cc:257] [C0] nuova connessione
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.916][23][debug][http] [source/common/http/conn_manager_impl.cc:210] [C0] nuovo flusso
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.918][23][debug][http] [source/common/http/conn_manager_impl.cc:548] [C0][S4222457489265630919] intestazioni di richiesta 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 | 'connection', 'Upgrade'
penvoyage-morpheus-envoy | 'aggiornamento', '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 decodifica intestazioni:
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 | 'connection', '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-internal', 'true
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] creando una nuova connessione
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.919][23][debug]

[source/common/http/codec_client.cc:26] [C1] in connessione
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.919][23][debug][connessione] [source/common/network/connection_impl.cc:638] [C1] connessione a 172.18.0.8:8080
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.919][23][debug][connessione] [source/common/network/connection_impl.cc:647] [C1] connessione in corso
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.919][23][debug][pool] [source/common/http/conn_pool_base.cc:20] richiesta in coda a causa di nessuna connessione disponibile
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.919][23][debug][connessione] [source/common/network/connection_impl.cc:516] [C1] connesso
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.919][23][debug] [source/common/http/codec_client.cc:64] [C1] connesso
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.919][23][debug][pool] [source/common/http/http1/conn_pool.cc:236] [C1] allegando alla prossima richiesta
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.919][23][debug][router] [source/common/router/router.cc:1122] [C0][S4222457489265630919] pool pronto
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.920][23][debug][router] [source/common/router/router.cc:669] [C0][S4222457489265630919] intestazioni upstream 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] codifica intestazioni tramite codec (end_stream=false):
penvoyage-morpheus-envoy | ':status', '101'
penvoyage-morpheus-envoy | 'upgrade', 'websocket'
penvoyage-morpheus-envoy | 'connection', '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 |

Notate l'intestazione :authority come ho detto sopra:

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

Ecco un'intestazione di autorità per una normale richiesta di un sito web:

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

Notate la presenza della porta nel primo caso (richiesta via websocket), e l'assenza della porta nel secondo caso.

Testcontainer

Si prega di notare che https://hub.docker.com/r/dataferret/websocket-echo l'immagine dataferret/websocket-echo sembra essere rotto!

Sembra che ci sia qualche incompatibilità in twisted / Autobahn per la particolare versione che è stata usata, e quindi ho deciso di rollare solo il mio:

Costruiremo un contenitore di prova che farà l'eco dell'input che gli mandiamo come websocket.

Ecco il Dockerfile:

Da python:stretch
COPIA assets/websocketd /usr/bin/websocketd
Eseguire chmod +x /usr/bin/websocketd
COPIA assets/run.py /opt/run.py
DESTINAZIONE LAVORO /opt

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

Si noti che usando ENTRYPOINT, quando il contenitore viene avviato con CMD da docker-compose.yml, il valore CMD sarà passato come parametro al comando ENTRYPOINT.

websocketd è un demone websocket, potete ottenere l'eseguibile standalone da qui:

http://websocketd.com/

Ho scaricato https://github.com/joewalnes/websocketd/releases/download/v0.3.0/websocketd-0.3.0-linux_amd64.zip e l'ho decompresso nella cartella delle risorse che ho creato per il mio contenitore docker.

L'idea generale di questa ingegnosa applicazione è di fornire un wrapper intorno alla vostra applicazione per essere in grado di servire i websockets.

Tutto quello che la vostra applicazione deve fare è ascoltare su stdin, e scrivere le risposte / qualsiasi comunicazione su stdout.

eseguire.py - questo è un semplice script che farà l'eco di tutto ciò che gli viene scritto:

#!/usr/bin/python
importazione sys

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

Notate l'uso di readline(), che restituirà la stringa immediatamente dopo una newline. Altrimenti dovrete avere a che fare con i buffer / aspettare che l'input si accumuli.

Notate anche il sys.stdout.flush()

Infine, ecco il docker-compose.yml:

versione: '3.6'

servizi:
   dwse:
     costruire:
       contesto: ./
       dockerfile: Dockerfile
     nome_contenitore: dwse
     nome host: dwse
     porti:
       – “8078:8080”
     reti:
       - envoy_net
     riavvio: sempre
     comando: "80"
     #user: "root:root"

reti:
   envoy_net:
     esterno:
       nome: my-bridge-network

Notate che l'ho messo nella stessa rete "my-bridge-network" come envoy sopra.

il comando qui è un "80" casuale che non viene interpretato dall'applicazione, ma si potrebbero creare linee di comando più elaborate, ecc. usando questo.

debug

Infine, vogliamo che un client si connetta e sia in grado di vedere i messaggi che inviamo al nostro servizio websocket di prova, che è proxy attraverso envoy.

Suggerisco wscat per questo.

wscat è un pacchetto npm (node.js). Perciò dovete prima installare npm:

apt installare npm

Poi potete installare wscat:

npm install -g wscat

Prova wscat con un servizio websocket echo pubblico:

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

immagine

ottenere un elenco di opzioni della riga di comando per wscat:

wscat -help

immagine

Per controllare una connessione websocket sicura attraverso envoy con certificati autofirmati fare:

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

immagine

Suggerimenti

Risposta inattesa del server

errore: Risposta inattesa del server: 404

Dai un'occhiata al tuo percorso: metti la porta nel dominio per corrispondere, come discusso sopra? Apparentemente sembra essere necessario per i client websocket.

In caso di dubbio, includete un dominio match all alla fine dei vostri host virtuali, per rendere più facile il debugging. Suggerisco di impostare un percorso fisso di corrispondenza su questo, invece del prefisso - cambiando il percorso utilizzato in wscat, in questo modo, è possibile verificare quale regola è stata abbinata:

- nome: matcheverything
   domini: ["*"]
   percorsi:
     - match: { path: "/mqtt" }
       route: { cluster: target_dwebsocket }

Impossibile analizzare JSON / impossibile trovare il campo:

Impossibile analizzare JSON come proto (INVALID_ARGUMENT:(route_config.virtual_hosts[3].routes[0].route) use_websocket: Impossibile trovare il campo.):

Come discusso sopra, questo è uno stile di configurazione legacy che è stato sostituito dalla nuova configurazione:

          upgrade_configs:
            - tipo di aggiornamento: websocket

nessuna corrispondenza di cluster per l'URL

[source/common/router/router.cc:278] [C0][S5067510418337690596] no cluster match for URL '/'

Vedi sopra - l'URL è abbinato compresa la porta per i Websockets apparentemente (nei miei test), quindi includi una regola per abbinare tutti i domini, e fai il debug da lì / usa la sintassi che ho fornito sopra:

          route_config:
            virtual_hosts:
            - nome: debug
              domini: ["debug.local:80"]
              percorsi:
                - match: { prefix: "/" }
                  percorso:
                    cluster: target_dwebsocket

certificato autofirmato

errore: certificato autofirmato

Esegui wscat con il parametro -no-check della linea di comando (questo è un doppio trattino che WordPress incasinerà):

immagine

Connessione websocket fallita:

pcp-code.js:15 Connessione WebSocket a 'wss://key:[email protected]/mqtt' fallita: Errore durante l'handshake WebSocket: Codice di risposta inaspettato: 403

Vedi informazioni sopra: molto probabilmente, questo messaggio di errore javascript è dovuto a una configurazione errata della corrispondenza del dominio (vedi sopra).

errore autobahn:

"Per usare txaio, devi prima selezionare un framework " exceptions.RuntimeError: Per usare txaio, devi prima selezionare un framework con .use_twisted() o .use_txaio()

Questo è l'errore che ho ottenuto quando ho eseguito l'immagine dataferret/websocket-echo. Suggerisco di usare il mio codice che ho fornito sopra come alternativa.

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

Riferimenti: