Depurando MQTT sobre websockets en Envoy 1.28.0

He migrado nuestra instalación de Envoy de Envoy 1.11.1 a 1.28.0, y ahora también utilizo SNI para seleccionar el certificado correcto.

Gran parte de esa migración consiste en actualizar la sintaxis de la configuración de Envoy de la API v2 a la API v3.

La actualización fue bien, excepto porque nuestro servicio MQTT basado en websocket (basado en VerneMQ) no funcionaba como esperábamos.

Al principio supuse que el problema estaba en envoy. Después de probar muchas opciones de tiempo de espera, y mirando a la documentación envoy, He decidido experimentar con una nueva ruta, y un corredor diferente (Mosquitto) detrás de él.

La siguiente configuración funciona con Mosquitto como broker, por si alguien más tropieza con el mismo problema.

Aquí hay un extracto de mi envoy.yaml (la configuración completa tiene más de 87000 líneas, generadas por un script de plantilla, debido al SNI y a tener que tener escuchas individuales por dominio como mencioné anteriormente):

recursos_estáticos:
  oyentes:
  - address:
      socket_address:
        address: 0.0.0.0
        valor_puerto: 443
    per_connection_buffer_limit_bytes: 32768 # 32 KiB
    listener_filters:
    - name: tls_inspector
      typed_config:
        "@type": type.googleapis.com/envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector
    cadenas_de_filtro:
    - filter_chain_match:
        server_names: ["picockpit.com", "www.picockpit.com", "picockpit.com:443", "www.picockpit.com:443"]
      transport_socket:
        nombre: envoy.transport_sockets.tls
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
          common_tls_context:
            tls_certificates:
            - cadena_certificados: { nombre_archivo: "/certs/letsencrypt/live/picockpit.com/fullchain.pem" }
              clave_privada: { nombre_archivo: "/certs/letsencrypt/live/picockpit.com/privkey.pem" }
            alpn_protocols: [ "h2,http/1.1" ]
      filters:
      - nombre: 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
          - nombre: envoy.filters.http.compressor
            typed_config:
              '@type': type.googleapis.com/envoy.extensions.filters.http.compressor.v3.Compressor
              compressor_library:
                name: text_optimized
                typed_config:
                  '@type': type.googleapis.com/envoy.extensions.compression.gzip.compressor.v3.Gzip
                  nivel_compresión: BEST_SPEED
                  estrategia_compresión: DEFAULT_STRATEGY
                  nivel_memoria: 9
                  bits_ventana: 15
                  chunk_size: 16384
          - nombre: 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:
            - name: backend
              dominios: ["picockpit.com", "www.picockpit.com", "picockpit.com:443", "www.picockpit.com:443"]
              rutas:

              - coinciden: { ruta: "/pidoctor"}
                redirect:
                  path_redirect: "/raspberry-pi/pidoctor-raspberry-pi-system-health-monitor/"
              - coincidencia: { prefix: "/pidoctor/"}
                redirigir:
                  path_redirect: "/raspberry-pi/pidoctor-raspberry-pi-system-health-monitor/"


              - match: { prefix: "/mqtt/test" }
                route:
                  prefix_rewrite: "/mqtt"
                  clúster: target_test
                  tiempo de espera: 0s
                  idle_timeout: 0s
                  upgrade_configs:
                    - upgrade_type: "websocket"
                      enabled: true
                      
              - coincidencia: { prefijo: "/" }
                ruta:
                  cluster: target_main
                  tiempo de espera: 0s
  clusters:

    - nombre: target_test
      connect_timeout: 5s
      per_connection_buffer_limit_bytes: 32768 # 32 KiB
      type: STRICT_DNS
      lb_policy: ROUND_ROBIN
      load_assignment:
        cluster_name: target_test
        puntos_finales:
        - lb_endpoints:
          - endpoint:
              address
                socket_address:
                  dirección: mosquitto-test.test-network
                  valor_puerto: 8025

Ten en cuenta que he omitido gran parte de la configuración de otros servicios, rutas, y no te he dado la información del cluster target_main (porque es irrelevante para la situación de MQTT sobre websockets).

Observe el valor timeout: 0s valor, que es importante para las conexiones MQTT para continuar en lugar de ser el tiempo de espera después de 15seg como es el valor predeterminado.

También he resaltado otras partes que son, en mi opinión, relevantes para permitir que las conexiones se actualicen a websockets (para que MQTT pueda ser transportado a través de ellas). Tenga en cuenta también la números de puerto como coincidencias de dominio adicionales.

Mosquitto docker-compose.yml:

versión: '3.6

servicios:
  mosquitto:
    imagen: eclipse-mosquitto
    nombre_contenedor: mosquitto-test
    nombre_host: mosquitto-test
    redes:

      - test_net
    reiniciar: "no"
    usuario: "root:root"
    volúmenes:
      - tipo: bind
        fuente: ./mosquitto.conf
        target: /mosquitto/config/mosquitto.conf


redes:

  test_net:
    externa:
      nombre: red_prueba


mosquitto.conf:

receptor 8025
protocolo websockets

allow_anonymous true
log_type todos

Herramienta utilizada para verificar la conexión:

Cliente de websocket HiveMQ

Actualización 13.11.2023

MQTT está en línea de nuevo, con VerneMQ también:

Aunque había reiniciado VerneMQ varias veces, aparentemente no esperé lo suficiente para que se estabilizara. Un colega lo reinició hoy, y ahora funciona. Parece que tarda entre 10 y 15 minutos (en nuestra configuración) en responder y funcionar correctamente.

Por lo tanto, puedo confirmar que la configuración anterior para envoy también funciona con VerneMQ.

Lección aprendida

Si algo no funciona, intentar reproducir el problema en la interacción con otra herramienta - si ahí funciona, posiblemente el problema no esté en la primera herramienta que has cambiado, sino en la segunda con la que tiene que funcionar.

y algunas golosinas adicionales:

Documentación en línea

Herramientas útiles