Converting HTTP/2 (h2c) without TLS to HTTP/1.1

This is for penetration testing purposes, not for any kind of prod setup.

I’d like to set up HAProxy to receive HTTP/2 traffic (h2c, HTTP/2 without TLS) coming from a native application. HAProxy should then convert this traffic to HTTP/1.1 and send it to a desired endpoint.

More particularly, the client that’ll be connecting to HTTP/2 HAProxy non-TLS listener sends the PRI request first, and as such, I’m assuming the HTTP/2 connection mode the client is using is “Starting HTTP/2 with Prior Knowledge” RFC

What settings could I use to have HAProxy listen HTTP/2 without TLS?

I’ve checked the “bind” command in the configuration file language and apart from ALPN (which I’m assuming is an incorrect option here as it implies TLS).

I’m guessing the minimalistic configuration file would be something like (listen for HTTP/2 on port 8009 and pass on to port 8000).

defaults

frontend http_front
   bind *:8009 <???>
   default_backend http_back

backend http_back
   balance roundrobin
   server localhost 127.0.0.1:8000 check

Unfortunately that’s not supported.

Currently TLS/ALPN is the only way to get H2 running.

Thank you for the quick response, clears out I should try in another direction such as nginx. If anyone knows another tool that’d do this, please post it here.

I got linked here from a stackoverflow question I asked (https://stackoverflow.com/questions/54470016/using-haproxy-to-proxy-h2c-requests).
Since this question was asked we have a new 1.9 release. Is h2c to h2c proxying now supported in the 1.9 release?

Yes it is.

Please refer to the blog post about it 1.9:

Search that blog post for “proto h2”. It’s inside the HTX section. :wink:

I have already tried the proto h2 settings and get the same error:

HAProxy config file:

frontend waiter
    mode http
    option http-use-htx
    bind *:8080
    default_backend local_node

backend local_node
    mode http
    option http-use-htx
    server localhost localhost:9090 proto h2

Using curl to send http requests with HTTP/1.1 and HTTP/2 (i.e. h2c) shows that while the HTTP/1.1 requests works, the HTTP/2 requests fail. I made HTTP/2 request directly to the backend to verify that h2c works on the backend.

# HTTP/2 directly to backend works
$ curl -v --http2-prior-knowledge --negotiate -u: -X GET "http://localhost:9090/status"
...
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x5613...)
> GET /status HTTP/1.1
> Host: localhost:9090
...
< HTTP/2 200
{"backend-protocol":"HTTP/2.0"}
 
# HTTP/2 through HAProxy does not work!
$ curl -v --http2-prior-knowledge --negotiate -u: -X GET "http://localhost:8080/status"
...
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x5613...)
> GET /status HTTP/1.1
> Host: localhost:8080
...
* http2 error: Remote peer returned unexpected data while we expected SETTINGS frame.  Perhaps, peer does not support HTTP/2 properly.
* Curl_http_done: called premature == 1
* Closing connection 0
curl: (16) Error in the HTTP2 framing layer
 
# HTTP/1.1 directly to backend reports HTTP/1.1 protocol
$ curl -v --http1.1 --negotiate -u: -X GET "http://localhost:9090/status"
> GET /status HTTP/1.1
> Host: localhost:9090
...
< HTTP/1.1 200
{"backend-protocol":"HTTP/1.1"}
 
# HTTP/1.1 through HAProxy works
$ curl -v --http1.1 --negotiate -u: -X GET "http://localhost:8080/status"
> GET /status HTTP/1.1
> Host: localhost:9090
...
< HTTP/1.1 200
{"backend-protocol":"HTTP/2.0"}

You have to put it on the Bind line also.

Sorry for the late reply.

The problem with putting it on the bind line ( bind *:8080 proto h2) is that now the client can no longer make http/1.1 requests:

$ curl --http2-prior-knowledge http://localhost:8080/status
Hello World

$ curl --http1.1 http://localhost:8080/status
curl: (52) Empty reply from server

This is in contrast to how the SSL support works. When http/1.1 and http/2 (h2) is enabled via alpn, HAProxy can handle both http/1.1 and http/2 requests.

Right, without SSL, you need to make a decision whether you want HTTP/1.1 or HTTP/2 on a specific port. You can’t have both. ALPN exists for a reason.

It is possible in Jetty (version 9.4.14.v20181114) to configure a server to use both http/1.1 and h2c listening on the same port. Afaik, it doesn’t need ALPN support to handle both http/1.1 and http/2 requests coming in to the same port.

Correct, that’s because Jetty goes to great lengths to handle all those things on a single port. Haproxy does not.

Couldn’t this be achieved with the following (sub-optimal) setup:

  • an upstream frontend listening on a single port in TCP mode;
  • two upstream backends, one for HTTP1 and another for HTTP2 without TLS; (these downstream backends have servers pointing to the downstream frontend respective port;)
  • the upstream frontend uses TCP data inspection to decide if the request is HTTP2 and if so dispatch it to the proper backend; else use the HTTP1 backend;
  • a downstream frontend with two bind statements, one for HTTP1 and one for HTTP2 without TLS;