Skip to content

WebSocket upgrades only work for the first stream when allow_connect is enabled in http2_protocol_options #38645

Open
@uhthomas

Description

@uhthomas

If you are reporting any crash or any potential security issue, do not
open an issue in this repo. Please report the issue via emailing
[email protected] where the issue will be triaged appropriately.

Title: WebSocket upgrades only work for the first stream when allow_connect is enabled in http2_protocol_options

Description:

It looks like the following configuration breaks WebSocket support:

http2_protocol_options:
  allow_connect: true

The first stream of a http/2 connection can upgrade to a WebSocket, but subsequent streams cannot. I don't think this is always true, though. If the WebSocket is closed, then a new WebSocket can upgrade immediately after. This seems to be easily reproducible when lots of streams are opened, like when loading a web page.

The behaviour is pretty strange, as envoy does end up sending requests to the upstream, but they're broken and the upstream (in this case, self hosted GitLab) gets really confused and responds with both a 400 and 404 depending on where you look.

Repro steps:

  1. Have a plain HTTP/1.1 upstream which handles WebSockets.
  2. Configure envoy as a HTTP/2 server, with support for WebSockets.
  3. Enable http2_protocol_options.allow_connect in the HTTP connection manager.
  4. Load a web page which connects to a WebSocket. The WebSocket will never upgrade.
  5. Restart envoy, and let the client create a new WebSocket without making any other requests. It will succeed.
  6. Disable http2_protocol_options.allow_connect and restart envoy. WebSockets will work as normal.
Image

Admin and Stats Output:

Unfortunately the trace logs gave literally no information.

Config:

admin:
  access_log:
    - name: envoy.access_loggers.stdout
      typed_config:
        "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
  address:
    socket_address:
      address: "::"
      port_value: 9901
      ipv4_compat: true
static_resources:
  listeners:
    - name: listener_http
      address:
        socket_address:
          address: "::"
          port_value: 8080
          ipv4_compat: true
      filter_chains:
        - 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_redirect
                route_config:
                  name: redirect_route
                  virtual_hosts:
                    - name: redirect_to_https
                      domains: ["*"]
                      routes:
                        - match:
                            prefix: /
                          redirect:
                            https_redirect: true
                http_filters:
                  - name: envoy.filters.http.router
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
                access_log:
                  - name: envoy.access_loggers.stdout
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
    - name: listener_h2
      address:
        socket_address:
          address: "::"
          port_value: 8443
          ipv4_compat: true
      filter_chains:
        - 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
                route_config:
                  name: local_route
                  virtual_hosts:
                    - name: backend
                      domains: ["*"]
                      routes:
                        - match:
                            prefix: /
                          route:
                            cluster: http_backend
                            timeout: 360s
                            retry_policy:
                              retry_on: 5xx
                              num_retries: 3
                              per_try_timeout: 120s
                http_filters:
                  - name: envoy.filters.http.router
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
                http2_protocol_options:
                  allow_connect: true
                access_log:
                  - name: envoy.access_loggers.stdout
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
                upgrade_configs:
                  - upgrade_type: websocket
          transport_socket:
            name: 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: /etc/envoy/tls.crt
                    private_key:
                      filename: /etc/envoy/tls.key
                alpn_protocols:
                  - h2
                  - http/1.1
  clusters:
    - name: http_backend
      connect_timeout: 0.25s
      type: logical_dns
      load_assignment:
        cluster_name: http_backend
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      address: "gitlab"
                      port_value: 8181

Logs:

Envoy:

[2025-03-05T00:14:32.325Z] "GET /-/cable HTTP/2" 400 - 0 0 38 - "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36" "76c9abda-3905-48fd-a9ec-acc4d77184cd" "<redacted>" "10.32.151.111:8181"

From the upstream (GitLab):

{"backend_id":"rails","content_type":"text/plain","correlation_id":"01JNHTDEYZDDQ8XM0F62R6501H","duration_ms":20,"host":"<redacted>","level":"info","method":"GET","msg":"access","proto":"HTTP/1.1","referrer":"","remote_addr":"172.18.181.141:45314","remote_ip":"172.18.181.141","route":"^/-/cable\\z","route_id":"action_cable","status":400,"system":"http","time":"2025-03-05T00:13:46Z","ttfb_ms":20,"uri":"/-/cable","user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36","written_bytes":51}

Call Stack:

N/A

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions