envoy, docker y websockets - depuración y configuración

Los Websockets son una tecnología apasionante, que permite convertir una conexión HTTP en una conexión binaria persistente de larga duración, que se puede utilizar para enviar mensajes bidireccionales.

Como nota, el protocolo MQTT puede ser transportado usando websockets - que es la única manera (?) para un cliente JavaScript entregado por el sitio web, por ejemplo.

En cualquier caso, como los websockets se ejecutan en los mismos puertos que el tráfico HTTP y HTTPS normal (80 / 443), es más probable que las redes corporativas los dejen pasar.

Envoy y websockets

Envoy soporta websockets. Hay algunos inconvenientes:

No se puede analizar JSON como proto (INVALID_ARGUMENT:(route_config.virtual_hosts[3].routes[0].route) use_websocket: No se puede encontrar el campo.):

Envoy solía soportar websockets con una antigua directiva, "use_websocket". En una instalación actual de Envoy (por ejemplo, actualmente utilizo Envoy 1.10.0-dev para hacer pruebas) esta directiva ha desaparecido y ha sido reemplazada.

La idea es tener más solicitudes/posibilidades de mejora genéricas.

La sintaxis correcta es ahora usar upgrade_configs. Echa un vistazo a mi envoy.yaml:

static_resources:
   oyentes:
   - dirección:
       dirección_de_socket:
         dirección: 0.0.0.0
         valor_puerto: 80
     cadenas_de_filtro:
     - filtros:
       - nombre: envoy.http_connection_manager
         configurar:
           upgrade_configs:
             - tipo_de_actualización: websocket

           codec_type: auto
           stat_prefix: ingress_http
           use_remote_address: true
           xff_num_trusted_hops: 0
           route_config:
             virtual_hosts:
             - nombre: depuración
               dominios: ["debug.local:80"]
               rutas:
                 - coincidencia: { prefijo: "/" }
                   ruta:
                     cluster: target_dwebsocket
             - nombre: backend
               dominios: ["morpheus.local"]
               rutas:
               - coincidencia: { prefijo: "/" }
                 redirigir:
                   path_redirect: "/"
                   https_redirect: true

(snip)

- dirección:
     dirección_de_socket:
       dirección: 0.0.0.0
       valor_de_puerto: 443
   cadenas_de_filtro:
   - tls_context:
       common_tls_context:
         tls_certificados:
         - cadena_certificada: { nombre de archivo: "/etc/envoy/ejemplo.crt" }
           private_key: { filename: "/etc/envoy/example.key" }
         alpn_protocols: [ "h2,http/1.1" ]
     filtros:
     - nombre: envoy.http_connection_manager
       configurar:
         upgrade_configs:
           - tipo_de_actualización: websocket
         stat_prefix: ingress_https
         use_remote_address: true
         xff_num_trusted_hops: 0
         route_config:
           virtual_hosts:
           - nombre: depuración
             dominios: ["debug.local:443"]
             rutas:
               - coincidencia: { prefijo: "/" }
                 ruta:
                   cluster: target_dwebsocket


(snip)

racimos:
- nombre: target_dwebsocket
   connect_timeout: 0.25s
   tipo: strict_dns
   lb_policy: round_robin
   anfitriones:
   - dirección_de_socket:
       dirección: dwse
       valor_puerto: 8080

Puede utilizar esta directiva upgrade_configs en dos lugares, según la documentación. en http_connection_manager (como en mi ejemplo anterior), y en rutas individuales.

          upgrade_configs:
            - tipo_de_actualización: websocket

Rutas no coincidentes

La otra cosa importante a tener en cuenta, si obtiene "404s" ( error: Unexpected server response: 404 ): la cabecera :authority será, por alguna extraña razón, establecida incluyendo el puerto - y por lo tanto no coincidirá si usted proporciona sólo el dominio.

Mira esta ruta:

["debug.local:80"]

El puerto se especifica después del dominio en esta instancia. En mis pruebas - sólo si el puerto fue especificado, fui capaz de conectarme a través de envoy al servidor websocket que he configurado. Esto es un obstáculo importante.

A continuación, voy a discutir, cómo depurar envoy en casos como este.

Depuración de envoy, websockets, Docker:

Hacer que la salida del envoy sea verbosa

Aquí está mi docker-compose.yaml:

versión: '3.7'

servicios:
   enviado:
     construir:
       contexto: ./
       archivo docker: Dockerfile
     nombre_contenedor: penvoyage-morpheus-envoy
     puertos:
       – “80:80”
       – “443:443”
     volúmenes:
       - tipo: volumen
         fuente: penvoyage_volume
         objetivo: /etc/envoy
     redes:
       - envoy_net
     #user: "2000:2000"
     usuario: "root:root"
     reinicio: a menos que se detenga
     ambiente:
       nivel de registro: depuración

volúmenes:
   penvoyage_volume:
     externo:
       nombre: penvoyage_volume

redes:
   envoy_net:
     externo:
       nombre: mi-red-puente

Tenga en cuenta que se establece una variable de entorno "loglevel: debug".

loglevel puede ser uno de:

  • rastrear
  • depurar
  • información
  • avisa a

Al iniciar el contenedor, ahora se obtendrá un resultado mucho mayor:

docker-compose up

Ten en cuenta que no nos desvinculamos del contenedor, para ver directamente su salida. (Una vez que estés satisfecho, puedes ejecutarlo usando docker-compose up -d)

El resultado debería ser algo parecido a esto:

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

en una conexión desde un cliente de websocket, obtendrá la siguiente salida:

penvoyage-morpheus-envoy | [2019-05-18 14:21:14.915][23][debug][main] [source/server/connection_handler_impl.cc:257] [C0] nueva conexión
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.916][23][debug][http] [source/common/http/conn_manager_impl.cc:210] [C0] new stream
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.918][23][debug][http] [source/common/http/conn_manager_impl.cc:548] [C0][S4222457489265630919] cabeceras de solicitud completas (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 | "actualización", "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 decodificando cabeceras:
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 nueva conexión
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.919][23][debug]

[source/common/http/codec_client.cc:26] [C1] conectando
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.919][23][debug][connection] [source/common/network/connection_impl.cc:638] [C1] conectando a 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] conexión en curso
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.919][23][debug][pool] [source/common/http/conn_pool_base.cc:20] solicitud en cola debido a que no hay conexiones disponibles
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] conectado
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.919][23][debug][pool] [source/common/http/http1/conn_pool.cc:236] [C1] adjuntando a la siguiente solicitud
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.919][23][debug][router] [source/common/router/router.cc:1122] [C0][S4222457489265630919] pool ready
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.920][23][debug][router] [source/common/router/router.cc:669] [C0][S4222457489265630919] cabeceras upstream completas: 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] codificando cabeceras a través de 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', '2W9caJQU0JKW3MhW6T8FHychjk='
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 |

Fíjese en la cabecera :authority que he mencionado anteriormente:

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

Esta es una cabecera de autoridad para una solicitud de sitio web normal:

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

Fíjate en la presencia del puerto en el primer caso (petición vía websocket), y en la ausencia del puerto en el segundo caso.

Contenedor de pruebas

Tenga en cuenta que https://hub.docker.com/r/dataferret/websocket-echo la imagen de dataferret/websocket-echo parece ser roto!

Parece que hay alguna incompatibilidad en twisted / Autobahn para la versión particular que se utilizó, y por lo tanto decidí simplemente rodar mi propio:

Vamos a construir un contenedor de prueba que se hará eco de la entrada que le enviemos como un websocket.

Aquí está el Dockerfile:

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

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

Tenga en cuenta que utilizando ENTRYPOINT, cuando el contenedor se inicia con CMD desde docker-compose.yml, el valor de CMD se pasará como parámetro al comando ENTRYPOINT.

websocketd es un demonio de websocket, puedes obtener el ejecutable independiente desde aquí:

http://websocketd.com/

He descargado https://github.com/joewalnes/websocketd/releases/download/v0.3.0/websocketd-0.3.0-linux_amd64.zip y lo descomprimí en la carpeta de activos que creé para mi contenedor docker.

La idea general de esta ingeniosa aplicación es proporcionar una envoltura alrededor de tu aplicación para poder servir websockets.

Todo lo que su aplicación tiene que hacer es escuchar en stdin, y escribir las respuestas / cualquier comunicación en stdout.

ejecutar.py - este es un simple script que se hará eco de todo lo que se escriba en él:

#!/usr/bin/python
importar sys

mientras sea cierto:
     read_co = sys.stdin.readline()
     print(read_co, end="")
     sys.stdout.flush()

Observe el uso de readline(), que devolverá la cadena inmediatamente después de una nueva línea. De lo contrario, tiene que lidiar con los búferes / esperar a que la entrada se acumule.

También observa el sys.stdout.flush()

Por último, aquí está el docker-compose.yml:

versión: '3.6'

servicios:
   dwse:
     construir:
       contexto: ./
       archivo docker: Dockerfile
     nombre_contenedor: dwse
     nombre de host: dwse
     puertos:
       – “8078:8080”
     redes:
       - envoy_net
     reinicio: siempre
     comando: "80"
     #user: "root:root"

redes:
   envoy_net:
     externo:
       nombre: mi-red-puente

Fíjate que lo he puesto en la misma red "mi-red-puente" que el envoy de arriba.

el comando aquí es un "80" aleatorio que no es interpretado por la aplicación, pero podrías crear líneas de comando más elaboradas, etc. usando esto.

depuración

Por último, queremos que un cliente se conecte y sea capaz de ver los mensajes que enviamos a nuestro servicio de websocket de prueba, el cual es proxy a través de envoy.

Sugiero wscat por esto.

wscat es un paquete npm (node.js). Por lo tanto, tienes que instalar npm primero:

apt install npm

Entonces puedes instalar wscat:

npm install -g wscat

Probar wscat con un servicio público de websocket echo:

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

imagen

obtener una lista de opciones de línea de comandos para wscat:

wscat -help

imagen

Para comprobar una conexión segura de websocket a través de envoy con certificados autofirmados hacer:

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

imagen

Consejos

Respuesta inesperada del servidor

error: Respuesta inesperada del servidor: 404

Echa un vistazo a tu ruta: ¿pones el puerto en el dominio para que coincida, como se ha comentado anteriormente? Parece que es necesario para los clientes de websocket.

En caso de duda, incluya una coincidencia de todos los dominios al final de sus hosts virtuales, para facilitar la depuración. Sugiero establecer una ruta fija que coincida con esto, en lugar de prefijo - cambiando la ruta que se utiliza en wscat, de esta manera, se puede verificar qué regla se emparejó:

- nombre: matcheverything
   dominios: ["*"]
   rutas:
     - coincidencia: { ruta: "/mqtt" }
       ruta: { cluster: target_dwebsocket }

No se puede analizar JSON / no se puede encontrar el campo:

No se puede analizar JSON como proto (INVALID_ARGUMENT:(route_config.virtual_hosts[3].routes[0].route) use_websocket: No se puede encontrar el campo.):

Como ya se ha dicho, se trata de un estilo de configuración heredado que ha sido sustituido por la nueva configuración:

          upgrade_configs:
            - tipo_de_actualización: websocket

no hay coincidencia de cluster para la URL

[source/common/router/router.cc:278] [C0][S5067510418337690596] no hay coincidencia de cluster para la URL '/'

Ver arriba - la URL es emparejada incluyendo el puerto para Websockets aparentemente ( en mis pruebas), por lo tanto incluya una regla para emparejar todos los dominios, y depure desde allí / use la sintaxis que he proporcionado arriba:

          route_config:
            virtual_hosts:
            - nombre: depuración
              dominios: ["debug.local:80"]
              rutas:
                - coincidencia: { prefijo: "/" }
                  ruta:
                    cluster: target_dwebsocket

certificado autofirmado

error: certificado autofirmado

Ejecute wscat con el parámetro de línea de comandos -no-check (se trata de un doble guión que WordPress estropeará):

imagen

La conexión de Websocket ha fallado:

pcp-code.js:15 Falló la conexión WebSocket con 'wss://key:[email protected]/mqtt': Error durante el handshake de WebSocket: Código de respuesta inesperado: 403

Véase la información anterior: lo más probable es que este mensaje de error de javascript se deba a una mala configuración de la coincidencia de dominios (véase más arriba).

error de autopista:

" Para utilizar txaio, debe seleccionar primero un framework " exceptions.RuntimeError: Para utilizar txaio, primero debe seleccionar un framework con .use_twisted() o .use_txaio()

Este es el error que obtuve al ejecutar la imagen dataferret/websocket-echo. Le sugiero que utilice mi código que he proporcionado anteriormente como una alternativa.

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

Referencias: