Can't get h2 out of the tls handler, has tls_options.alpn changed?

I’m trying to get HTTP/2 while keeping proxy_proto. We use the TLS TLVs (version, cipher, ALPN, SNI) in the PROXY v2 header, so losing those isn’t an option.

Found the old threads where Kurt gives this config:

[[services.ports]]

handlers = ["tls"]

port = "443"

tls_options = { alpn = ["h2", "http/1.1"] }

Can’t get it to negotiate h2. Always picks http/1.1, and if I offer only h2 I get nothing:

$ openssl s_client -connect myapp.fly.dev:8443 -alpn h2,http/1.1 -servername myapp.fly.dev

ALPN protocol: http/1.1

$ openssl s_client -connect myapp.fly.dev:8443 -alpn h2 -servername myapp.fly.dev

No ALPN negotiated

flyctl config show says it’s stored fine:

443  ['tls', 'proxy_proto']  tls_options= {'alpn': ['h2', 'http/1.1']}

8443 ['tls']                 tls_options= {'alpn': ['h2', 'http/1.1']}

Same result on a shared and a dedicated IPv4, with tls on its own and with tls + proxy_proto, and with tls_options written as an inline table or a block. Both ports serve normal 200s otherwise. flyctl v0.4.95.

Adding the http handler does get me h2 immediately, but then no PROXY header arrives at all, even for http/1.1 clients, so the TLS TLVs are gone. I don’t think h2_backend is what I’m after either, since the docs describe that as Fly speaking h2c to the app behind the http handler rather than what gets advertised to the browser.

So is tls_options.alpn still supposed to work? And if h2 passthrough has gone, is there any way to get the TLS version/cipher/ALPN through the http handler? All I can see there is Fly-Client-IP and `Via: 2 fly.io`.

Links for reference:

- Fly Proxy H/2 Details - #2 by jerome

- tls + proxy_protocol How to set HTTP/2 ALPN? - #20 by jerome

H2 over raw TLS handler is not supported on *.fly.dev because that is a security vulnerability. Browsers may reuse H2 connections under one domain and do not know about the concept of separate “apps” on Fly. When you use the http handler, this is safe because our proxy blocks these reuse attempts. But there’s nothing we can do when you use the tls handler only without http.

You can use H2 ALPN over the raw TLS handler if you add a custom domain.

…although, I am pretty sure that the proxy_proto handler can coexist with http. Can you post your full config when you tried to enable http along with proxy_proto which didn’t work?

Oh, you also need to make sure your concurrency type is set to connections, not requests, and the h2 backend option isn’t enabled (because in either of these cases we’d start pooling connections to your app). proxy_proto only applies to concurrency type connections because those TLVs only make sense per connection, and if we pool requests onto connections it no longer makes sense.

We could also consider exposing the client-side connection metadata (I assume what you want here is mostly TLS ciphers etc.?) as HTTP headers.

That was it, thanks. The custom domain sorted it

Raw tls handler, custom domain, and I get h2 and the PROXY header together:

[[services.ports]]

port = 443

handlers = ["tls", "proxy_proto"]

proxy_proto_options = { version = "v2" }

tls_options = { alpn = ["h2", "http/1.1"] }


What the app sees:

HTTP Version: HTTP/2.0

Proxied Address: source: 90.x.x.x:49276, destination: 169.x.x.x:443

ALPN TLV: Some("h2")

TLS version TLV: Some("TLSv1.3")

TLS cipher TLV: Some("TLS13_AES_256_GCM_SHA384")


http/1.1 clients on the same port still come through fine with their own TLVs, so mixed traffic is no problem. Nice touch that the ALPN TLV says which one was negotiated, means I can just read that instead of sniffing the first bytes myself.

On http + proxy_proto, I did try it with `type = “connections”` and h2_backend unset and the header still doesn’t turn up,on either protocol. Not blocking me any more, just flagging in case that’s not intended.

And yes please on exposing the connection metadata as headers. TLS version, cipher, ALPN and SNI would cover it, as long as the proxy strips any client sent copies of them.