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: debugvolumi:
penvoyage_volume:
esterno:
nome: penvoyage_volumereti:
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][client] [source/common/http/codec_client.cc:26] [C1] connecting
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][client] [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 /optESPORRE 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:
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 sysmentre 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
ottenere un elenco di opzioni della riga di comando per wscat:
wscat -help
Per controllare una connessione websocket sicura attraverso envoy con certificati autofirmati fare:
wscat -c ws://debug.local/mqtt_drinks_are_free -no-check
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à):
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.
Riferimenti:
- https://hub.docker.com/r/dataferret/websocket-echo non funziona per me! Ho fornito un codice alternativo in questo blogpost per un semplice contenitore Docker websocket echo
- https://github.com/vi/websocat un'alternativa al wscat
- https://github.com/websockets/wscat
- https://github.com/erebe/wstunnel permette di creare tunnel di connessioni su websockets, tra cui wireguard - roba interessante!
- ws://echo.websocket.org - servizio echo su un websocket
- http://websocketd.com/ si occupa di gestire le connessioni WebSocket e di lanciare le vostre applicazioni per gestire i WebSocket (isolate l'una dall'altra)
- https://github.com/envoyproxy/envoy/issues/3301 qui viene discusso l'aggiornamento della configurazione del nuovo stile
- https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/websocket.html - documentazione ufficiale dell'inviato
- https://www.envoyproxy.io/docs/envoy/latest/api-v2/api/v2/route/route.proto#envoy-api-field-route-routeaction-upgrade-configs
- https://www.envoyproxy.io/docs/envoy/latest/api-v2/api/v2/route/route.proto#envoy-api-msg-route-routeaction-upgradeconfig
- https://github.com/envoyproxy/envoy/issues/4740 - Ho visto il modo corretto di configurare qui, e lo sto elaborando
- https://www.envoyproxy.io/docs/envoy/latest/operations/cli informazioni sulla riga di comando di envoy