Depuração de MQTT sobre websockets no Envoy 1.28.0

Migrei a nossa instalação do Envoy do Envoy 1.11.1 para o 1.28.0 e agora também estou a utilizar o SNI para selecionar o certificado correto.

Uma grande parte dessa migração consiste em atualizar a sintaxe da configuração do Envoy da API v2 para a API v3.

A atualização correu bem, exceto o facto de o nosso serviço MQTT baseado em websockets (baseado no VerneMQ) não funcionar como esperado.

No início, assumi que o problema estava no envoy. Depois de tentar várias opções de timeout e consultar a documentação do envoy, decidi experimentar uma nova rota e um broker diferente (Mosquitto) por trás dela.

A configuração seguinte funciona com o Mosquitto como intermediário, para o caso de alguém se deparar com o mesmo problema.

Aqui está um excerto do meu envoy.yaml (a configuração completa tem mais de 87000 linhas, geradas por um script de modelo, devido ao SNI e à necessidade de ter ouvintes individuais por domínio, como mencionei acima):

static_resources:
  ouvintes:
  - endereço:
      socket_address:
        endereço: 0.0.0.0
        valor_da_porta: 443
    per_connection_buffer_limit_bytes: 32768 # 32 KiB
    filtros_de_ouvinte:
    - nome: tls_inspector
      typed_config:
        "@type": type.googleapis.com/envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector
    cadeias_de_filtros:
    - filter_chain_match:
        nomes_do_servidor: ["picockpit.com", "www.picockpit.com", "picockpit.com:443", "www.picockpit.com:443"]
      transport_socket:
        nome: envoy.transport_sockets.tls
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
          common_tls_context:
            tls_certificates:
            - certificate_chain: { filename: "/certs/letsencrypt/live/picockpit.com/fullchain.pem" }
              private_key: { filename: "/certs/letsencrypt/live/picockpit.com/privkey.pem" }
            alpn_protocols: [ "h2,http/1.1" ]
      filters:
      - name: envoy.filters.network.http_connection_manager
        typed_config:
          '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
          stat_prefix: ingress_http
          codec_type: AUTO

          http_filters:
          - nome: envoy.filters.http.compressor
            typed_config:
              '@type': type.googleapis.com/envoy.extensions.filters.http.compressor.v3.Compressor
              compressor_library:
                nome: text_optimized
                typed_config:
                  '@type': type.googleapis.com/envoy.extensions.compression.gzip.compressor.v3.Gzip
                  compression_level: BEST_SPEED
                  compression_strategy: DEFAULT_STRATEGY
                  memory_level: 9
                  window_bits: 15
                  tamanho_do_conjunto: 16384
          - nome: envoy.filters.http.router
            typed_config:
             "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
          common_http_protocol_options:
            idle_timeout: 3600s # 1 hora

          use_remote_address: true
          xff_num_trusted_hops: 0
          route_config:
            virtual_hosts:
            - nome: backend
              domínios: ["picockpit.com", "www.picockpit.com", "picockpit.com:443", "www.picockpit.com:443"]
              rotas:

              - match: { path: "/pidoctor"}
                redirecionar:
                  path_redirect: "/raspberry-pi/pidoctor-raspberry-pi-system-health-monitor/"
              - match: { prefixo: "/pidoctor/"}
                redirecionar:
                  redireccionar_caminho: "/raspberry-pi/pidoctor-raspberry-pi-system-health-monitor/"


              - match: { prefixo: "/mqtt/test" }
                route:
                  prefix_rewrite: "/mqtt"
                  cluster: target_test
                  tempo esgotado: 0s
                  idle_timeout: 0s
                  upgrade_configs:
                    - upgrade_type: "websocket"
                      ativado: verdadeiro
                      
              - match: { prefixo: "/" }
                route:
                  cluster: target_main
                  timeout: 0s
  clusters:

    - nome: target_test
      connect_timeout: 5s
      per_connection_buffer_limit_bytes: 32768 # 32 KiB
      type: STRICT_DNS
      lb_policy: ROUND_ROBIN
      load_assignment:
        nome do cluster: target_test
        endpoints:
        - lb_endpoints:
          - endpoint:
              address:
                socket_address:
                  endereço: mosquitto-test.test-network
                  valor_da_porta: 8025

Note-se que omiti uma grande parte da configuração de outros serviços, rotas, e não forneci a informação do cluster target_main (porque é irrelevante para a situação do MQTT sobre websockets).

Observe o valor timeout: 0s, que é importante para que as conexões MQTT continuem em vez de serem interrompidas após 15 segundos, como é o padrão.

Também destaquei outras partes que, na minha opinião, são relevantes para permitir que as ligações sejam actualizadas para websockets (para que o MQTT possa ser transportado através delas). Observe também o números de porta sendo passados como correspondências de domínio adicionais.

Mosquitto docker-compose.yml:

versão: "3.6

serviços:
  mosquitto:
    imagem: eclipse-mosquitto
    nome_do_contêiner: mosquitto-test
    hostname: mosquitto-test
    redes:

      - rede_teste
    restart: "no"
    utilizador: "root:root"
    volumes:
      - tipo: bind
        fonte: ./mosquitto.conf
        target: /mosquitto/config/mosquitto.conf


redes:

  test_net:
    externo:
      nome: test-network


mosquitto.conf:

ouvinte 8025
protocolo websockets

allow_anonymous true
log_type all

Ferramenta utilizada para verificar a ligação:

Cliente websocket HiveMQ

Atualização 13.11.2023

O MQTT está novamente em linha, também com o VerneMQ:

Embora eu tenha reiniciado o VerneMQ várias vezes, aparentemente não esperei tempo suficiente para que ele se estabilizasse. Um colega reiniciou-o hoje e já está a funcionar. Aparentemente, demora 10 a 15 minutos (na nossa configuração) a tornar-se totalmente reativo e a funcionar corretamente.

Por conseguinte, posso confirmar que a configuração acima referida para o envoy também funciona com o VerneMQ.

Lição aprendida

Se algo não estiver a funcionar, tentar reproduzir o problema na interação com outra ferramenta - Se funcionar aí, então possivelmente o problema não está na primeira ferramenta que alterou, mas na segunda ferramenta com a qual precisa de trabalhar.

e mais alguns objectos:

Documentação Online

Ferramentas úteis