envoy, docker e websockets - depuração e configuração

Os Websockets são uma tecnologia excitante, permitindo-lhe actualizar uma ligação HTTP para uma ligação binária persistente de longa duração, que pode utilizar para enviar mensagens bidireccionais.

Como um aparte, o protocolo MQTT pode ser transportado usando websockets - que é a única forma (?) para um cliente JavaScript entregue pelo site, por exemplo.

Em qualquer caso, como os websockets funcionam nas mesmas portas que o tráfego normal HTTP & HTTPS (80 / 443), é mais provável que as redes corporativas as deixem passar.

Enviado e websockets

O Envoy suporta websockets. Há algumas gotchas:

Incapaz de analisar JSON como protótipo (INVALID_ARGUMENT:(route_config.virtual_hosts[3].routes[0].route) use_websocket: Não é possível encontrar o campo.):

Enviado usado para suportar websockets com uma diretiva antiga, "use_websocket". Em uma instalação atual do envoy (por exemplo, eu atualmente uso envoy 1.10.0-dev para fins de teste) esta diretiva desapareceu e foi substituída.

A idéia é ter pedidos / possibilidades mais genéricos de atualização.

A sintaxe correcta está agora a usar atualizar_configs. Dê uma olhada no meu enviado.yaml:

estática_recursos:
   Ouvintes:
   - morada:
       endereço_da_cova:
         morada: 0.0.0.0
         port_value: 80
     filter_chains:
     - filtros:
       - nome: envoy.http_connection_manager
         configurar:
           upgrade_configs:
             - upgrade_type: websocket

           codec_type: auto
           stat_prefix: ingresso_http
           use_remote_address: true
           xff_num_trusted_hops: 0
           route_config:
             virtual_hosts:
             - nome: debug
               domínios: ["debug.local:80"]
               rotas:
                 - ...partida: Prefixo: “/” }
                   rota:
                     cluster: target_dwebsocket
             - nome: backend
               domínios: ["morpheus.local"]
               rotas:
               - ...partida: Prefixo: “/” }
                 redireccionar:
                   path_redirect: "/"
                   https_redirect: true

(corte)

- morada:
     endereço_da_cova:
       morada: 0.0.0.0
       port_value: 443
   filter_chains:
   - tls_context:
       texto_comum_tls_context:
         tls_certificates:
         - cadeia_cade certificado: { nome do ficheiro: "/etc/envoy/example.crt" }
           private_key: { filename: "/etc/envoy/example.key" }
         alpn_protocols: ["h2,http/1.1" ]
     filtros:
     - nome: envoy.http_connection_manager
       configurar:
         upgrade_configs:
           - upgrade_type: websocket
         stat_prefix: ingresso_https
         use_remote_address: true
         xff_num_trusted_hops: 0
         route_config:
           virtual_hosts:
           - nome: debug
             domínios: ["debug.local:443"]
             rotas:
               - ...partida: Prefixo: “/” }
                 rota:
                   cluster: target_dwebsocket


(corte)

grupos:
- nome: target_dwebsocket
   connect_timeout: 0.25s
   tipo: strict_dns
   lb_policy: round_robin
   anfitriões:
   - endereço_da_cova:
       endereço: dwse
       port_value: 8080

Você pode usar esta diretiva upgrade_configs em dois lugares, de acordo com a documentação. no http_connection_manager (como no meu exemplo acima), e em rotas individuais.

          upgrade_configs:
            - upgrade_type: websocket

Itinerários não correspondentes

A outra coisa importante a notar, se você receber "404s" ( erro: resposta inesperada do servidor: 404 ): o cabeçalho :authority será, por alguma razão estranha, definido incluindo a porta - e, portanto, não será compatível se você fornecer apenas o domínio.

Olha para esta rota:

["debug.local:80"]

A porta é especificada depois do domínio neste caso. Nos meus testes - somente se a porta foi especificada, fui capaz de me conectar através do envoy ao servidor websocket que eu configurei. Este é um grande tropeço.

A seguir, vou discutir, como depurar o enviado em casos como este.

Enviado para a depuração, websockets, Docker:

Fazendo verbose de saída do enviado

Aqui está o meu docker-compose.yaml:

versão: '3.7'.

serviços:
   Enviado:
     construir:
       contexto: ./
       Arquivo de docas: Arquivo de doca
     container_name: penvoyage-morpheus-envoy
     portos:
       – “80:80”
       – “443:443”
     volumes:
       - tipo: volume
         fonte: penvoyage_volume
         alvo: /etc/envoy
     redes:
       - envoy_net
     #user: “2000:2000”
     usuário: "raiz:raiz"
     reinício: a não ser que seja parado
     ambiente:
       nível de registo: debug

volumes:
   penvoyage_volume:
     externo:
       nome: penvoyage_volume

redes:
   envoy_net:
     externo:
       nome: my-bridge-network

Note que uma variável de ambiente "loglevel: debug" é definida.

O nível de registo pode ser um de:

  • vestígio
  • depurar
  • info
  • avise

A partir de agora, o contentor irá produzir muito mais resultados:

construir uma doca

Note que não nos separamos do contentor, para ver directamente a sua saída. (Quando estiver satisfeito, você pode executá-lo usando Componente -d)

A tua produção deve agora ser algo parecido com isto:

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

em uma conexão de um cliente websocket, você terá a seguinte saída:

penvoyage-morpheus-envoy | [2019-05-18 14:21:14.915][23][debug][main] [source/server/connection_handler_implpl.cc:257] [C0] nova conexão
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.916][23][debug][http] [source/common/http/conn_manager_implpl.cc:210] [C0] novo fluxo
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.918][23][debug][http] [source/common/http/conn_manager_implpl.cc:548] [C0][S4222457489265630919] request headers complete (end_stream=false):
penvoyage-morpheus-envoy | ':autoridade', 'debug.local:80'.
penvoyage-morpheus-envoy | ':path', '/mqtt_drinks_are_free', '/mqtt_drinks_are_free'.
penvoyage-morpheus-envoy | ':método', 'GET'.
penvoyage-morpheus-envoy | 'sec-websocket-version', '13
penvoyage-morpheus-envoy | 'sec-websocket-key', 'HQFiCTWiFMktGDPFXwzrjQ=='
penvoyage-morpheus-envoy | 'conexão', '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][roteador] [source/common/router/router.cc:321] [C0][S4222457489265630919] cluster 'target_dwebsocket' correspondente à 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] cabeçalhos de descodificação do router:
penvoyage-morpheus-envoy | ':autoridade', 'debug.local:80'.
penvoyage-morpheus-envoy | ':path', '/mqtt_drinks_are_free'.
penvoyage-morpheus-envoy | ':método', 'GET'.
penvoyage-morpheus-envoy | ':esquema', 'http
penvoyage-morpheus-envoy | 'sec-websocket-version', '13
penvoyage-morpheus-envoy | 'sec-websocket-key', 'HQFiCTWiFMktGDPFXwzrjQ=='
penvoyage-morpheus-envoy | 'conexão', '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' (verdadeiro)
penvoyage-morpheus-envoy | 'x-request-id', 'ca8a765c-e549-4c45-988c-58b6c853df7b'.
penvoyage-morpheus-envoy | 'x-envoy-expectedd-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] criando uma nova conexão
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][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] connection in progress
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.919][23][debug][pool] [source/common/http/conn_pool_base.cc:20] pedido de enfileiramento devido à falta de conexões disponíveis
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][client] [source/common/http/codec_client.cc:64] [C1] connected
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.919][23][debug][pool] [source/common/http/http1/conn_pool.cc:236] [C1] anexando à próxima solicitação
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] cabeçalhos upstream completos: end_stream=false
penvoyage-morpheus-envoy | [2019-05-18 14:21:14.920][23][debug][http] [source/common/http/conn_manager_implpl.cc:1234] [C0][S4222457489265630919] encoding headers via codec (end_stream=false):
penvoyage-morpheus-envoy | ':status', '101'
penvoyage-morpheus-envoy | 'upgrade', 'websocket
penvoyage-morpheus-envoy | 'conexão', 'Upgrade'.
penvoyage-morpheus-envoy | 'sec-websocket-accept', '2W9caJQU0JKW3MhWV6T8FHychjk='
penvoyage-morpheus-envoy | 'data', 'Sábado, 18 de Maio de 2019 14:21:14 GMT'.
penvoyage-morpheus-envoy | 'server', 'envoy' (servidor)
penvoyage-morpheus-envoy | 'content-length', '0
penvoyage-morpheus-envoy |

Observe o cabeçalho :autoridade como mencionei acima:

':autoridade', 'debug.local:80'.

Aqui está um cabeçalho de autoridade para um pedido normal de website:

penvoyage-morpheus-envoy | ':método', 'GET'.
penvoyage-morpheus-envoy | ':autoridade', 'morpheus.local'.
penvoyage-morpheus-envoy | ':esquema', 'https'

Observe a presença do porto no primeiro caso (pedido via websocket), e a ausência do porto no segundo caso.

Testcontainer

Note, por favor, que https://hub.docker.com/r/dataferret/websocket-echo a imagem dataferret/websocket-echo parece ser quebrado!

Parece haver alguma incompatibilidade no twisted / Autobahn para a versão particular que foi usada, e por isso decidi apenas rolar a minha própria:

Vamos construir um recipiente de teste que irá ecoar a entrada que lhe enviamos como um websocket.

Aqui está o Dockerfile:

DE píton:esticar
COPY assetss/websocketd /usr/bin/websocketd
EXECUÇÃO chmod +x /usr/bin/websocketd
COPY ativo/corrida.py /opt/corrida.py
WORKDIR /opt

EXPOSIÇÃO 8080
ENTRYPOINT ["websocketd", "-port=8080", "python3", "/opt/run.py"]

Note que usando ENTRYPOINT, quando o recipiente é iniciado com CMD do docker-compose.yml, o valor CMD será passado como parâmetro para o comando ENTRYPOINT.

websocketd é um daemon websocket, você pode obter o executável autônomo a partir daqui:

http://websocketd.com/

Eu baixei https://github.com/joewalnes/websocketd/releases/download/v0.3.0/websocketd-0.3.0-linux_amd64.zip e descompactou-a na pasta de activos que criei para o meu contentor portuário.

A ideia geral desta aplicação sofisticada é fornecer um invólucro em torno da sua aplicação para poder servir os seus websockets.

Tudo o que sua aplicação tem que fazer é ouvir em stdin, e escrever as respostas / qualquer comunicação em stdout.

correr.py - este é um script simples que ecoará tudo o que lhe for escrito:

#!/usr/bin/pithon
sistema de importação

enquanto é verdade:
     read_co = sys.stdin.readline()
     print(read_co, end="")
     sys.stdout.flush()

Observe o uso da readline(), que retornará a string imediatamente após uma nova linha. Caso contrário você tem que lidar com buffers / esperar que a entrada se acumule.

Observe também o sys.stdout.flush()

Finalmente, aqui está o docker-compose.yml:

versão: '3.6'.

serviços:
   Dormir:
     construir:
       contexto: ./
       Arquivo de docas: Arquivo de doca
     container_name: dwse
     hostname: dwse
     portos:
       – “8078:8080”
     redes:
       - envoy_net
     reinício: sempre
     ...o comando: “80”
     #user: "raiz:raiz"

redes:
   envoy_net:
     externo:
       nome: my-bridge-network

Note que eu o coloquei na mesma rede "my-bridge-network" que o enviado acima.

o comando aqui é um "80" aleatório que não é interpretado pela aplicação, mas você poderia criar linhas de comando mais elaboradas, etc. usando isto.

depuração

Finalmente, queremos que um cliente se conecte e seja capaz de ver as mensagens que enviamos para o nosso serviço de teste websocket, o qual é proxied através do envoy.

Eu sugiro wscat para isto.

O wscat é um pacote npm (node.js). Portanto, você tem que instalar o npm primeiro:

apt install npm

Então você pode instalar o wscat:

npm install -g wscat

Teste o wscat com um serviço público de eco de websocket:

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

imagem

obter uma lista de opções de linha de comando para o wscat:

wscat -help

imagem

Para verificar uma conexão segura via websocket através de um enviado com certificados autografados faça:

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

imagem

Dicas

Resposta inesperada do servidor

erro: Resposta inesperada do servidor: 404

Dê uma olhada na sua rota: você coloca o porto no domínio para combinar, como discutido acima? Aparentemente, parece ser necessário para clientes websocket.

Quando em dúvida, inclua uma correspondência de todos os domínios no final dos seus hosts virtuais, para facilitar a depuração. Eu sugiro definir um caminho fixo de correspondência nisto, ao invés de prefixo - alterando o caminho que você usa no wscat, desta forma, você pode verificar qual regra foi correspondida:

- nome: matcheverything
   domínios: [“*”]
   rotas:
     - ...partida: {\an8}{\an8}{\an8}{\an8} "/mqtt" }
       rota: { cluster: target_dwebsocket }

Incapaz de analisar JSON / não consegue encontrar o campo:

Incapaz de analisar JSON como protótipo (INVALID_ARGUMENT:(route_config.virtual_hosts[3].routes[0].route) use_websocket: Não é possível encontrar o campo.):

Como discutido acima, este é um estilo legado de configuração que foi substituído pela nova configuração:

          upgrade_configs:
            - upgrade_type: websocket

sem correspondência de cluster para URL

[source/common/router/router.cc:278] [C0][S5067510418337690596] nenhuma correspondência de cluster para a URL '/'

Veja acima - a URL é igualada incluindo a porta para Websockets aparentemente (nos meus testes), portanto inclua uma regra para combinar todos os domínios, e debug a partir daí / use a sintaxe que eu forneci acima:

          route_config:
            virtual_hosts:
            - nome: debug
              domínios: ["debug.local:80"]
              rotas:
                - ...partida: Prefixo: “/” }
                  rota:
                    cluster: target_dwebsocket

certificado autoassinado

erro: certificado autoassinado

Execute o wscat com o parâmetro -no-check da linha de comando (este é um traço duplo que o WordPress vai estragar):

imagem

A ligação Websocket falhou:

pcp-code.js:15 A ligação WebSocket a 'wss://key:[email protected]/mqtt' falhou: Erro durante o aperto de mão do WebSocket: Código de resposta inesperado: 403

Ver informação acima: muito provavelmente, esta mensagem de erro javascript deve-se a uma má configuração da correspondência do domínio (ver acima).

Erro de autobahn:

"Para usar o txaio, você deve primeiro selecionar uma estrutura " exceções.RuntimeError: Para usar o txaio, você deve primeiro selecionar um framework com .use_twisted() ou .use_txaio()

Este é o erro que recebi ao executar a imagem dataferret/websocket-echo. Eu sugiro usar meu código que eu forneci acima como uma alternativa.

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

Referências: