HTTP/3 on Nginx – Be QUIC or be Dead

The Nginx mainline branch (currently version 1.25.3) has implemented support for HTTP/3 and I want it on my server. The first order of business will be to switch from the nginx stable branch (currently version 1.24.0) to the mainline branch. As Arch Linux provides both Nginx branches in their repository, it’s just a matter of performing a quick drop-in replacement.

Switching to Nginx mainline

With Arch, this is easily achieved by installing the nginx-mainline package from the extra repository. Nginx also offers pre-built packages if your distribution doesn’t offer the mainline branch containing the new features.

# nginx -V
nginx version: nginx/1.25.3
built with OpenSSL 3.1.3 19 Sep 2023 (running with OpenSSL 3.2.1 30 Jan 2024)

Prerequisites

  • TLSv1.3
  • Module ngx_http_v3_module (–with-http_v3_module)
  • Permit UDP traffic
Hetzner host firewall

Allowing UDP traffic to port 443 on a Hetzner VPS.

Make sure the port used for QUIC also allows UDP connections through the firewall. I have it from reliable sources that the opposite can be very annoying ;)

Nginx configuration for HTTP/3

There are several articles online that add unnecessary or deprecated steps to the configuration. Nginx has a page for their gx_http_v3_module that can be used as a template. Always refer to the official documentation instead of random posts like this one.

My server contains quite a few virtual hosts. The redacted configuration for my main host looks as follows:

server {
    listen 443 quic reuseport;   
    listen [::]:443 quic reuseport;
    listen 443 ssl;
    listen [::]:443 ssl; 
    ....    
    
    server_name blog.paranoidpenguin.net;
   
    # headers
    add_header Alt-Svc 'h3=":443"; ma=864000';
    add_header Content-Security-Policy "default-src..
    ....

    location / {
        limit_except GET {
            deny  all;
        }
        try_files $uri $uri/ =404;
    }
    ...

    # ssl configuration
    ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
    ssl_ciphers 'TLS-CHACHA20-POLY1305-SHA256:...
    ssl_prefer_server_ciphers on;
    ssl_early_data on;
}

A quick breakdown of the essential parts

The following directives tells Nginx to listen for HTTP/3 (QUIC) IPv4 and IPv6 HTTP/3 connections:

listen 443 quic reuseport;   
listen [::]:443 quic reuseport;

It’s important to note that you can only define reuseport once if you’re configuring multiple virtual hosts on the same IP address. If so, simply omit it from subsequent configurations.

The following HTTP header will inform the client that there is an alternative service available on a new protocol:

add_header Alt-Svc 'h3=":443"; ma=864000';

The alternate service advertised here is HTTP/3 (h3) running on port 443 on the same host. Max age is set to the default 864000 seconds and could thus be omitted. Strictly speaking, this is all you need to add support for HTTP/3 on a Nginx server. No additional QUIC headers are required.

Verifying your HTTP/3 configuration

I would recommend using curl compiled with HTTP/3 support for the most detailed insight into the connection:

Curl compiled with BoringSSL and quiche/0.17.2 for HTTP3 support

cURL 8.2.1 running the command curl –http3 https://blog.paranoidpenguin.net/

Another option is to use a web browser with HTTP/3 support. Firefox’s developer tools let you list the protocol version under the network tab.

Firefox showing an HTTP/3 connection

This approach is more reliable than using online services to check for HTTP/3 support.

Verifying HTTP/3 support using online services

There are a few online services that will connect to your server and claim to check for HTTP/3 support. However, they seem to mostly just connect to your server to check for the availability of an Alt-Svc header. There are no subsequent attempts to establish an HTTP/3 connection.

To make matters worse, the following header h3=":443"; ma=864000" will often result in a failure informing you that HTTP/3 is not available. The explanation for this is that the service expects to get a QUIC draft version returned with the Alt-Svc header.

Thus, if you provide a draft version like h3-29=":443"; ma=86400 you’ll get a message congratulating you on your successful HTTP/3 implementation.

It’s not all bad though, you can use HTTP/3 Check powered by LiteSpeed Technologies. It does what it says and establishes a connection to the specified host:

# Nginx access log
blog.paranoidpenguin.net:443 2001:19f0:1000:747c:5400:4ff:fea2:5e72 - - [04/Feb/2024:15:38:22 +0100] "GET / HTTP/1.1" 200 3700 "-" "wget/http3check.net"
blog.paranoidpenguin.net:443 2001:19f0:1000:747c:5400:4ff:fea2:5e72 - - [04/Feb/2024:15:38:23 +0100] "HEAD / HTTP/3.0" 200 0 "-" "http3check.net/lsquic/4.0.0"
blog.paranoidpenguin.net:443 2001:19f0:1000:747c:5400:4ff:fea2:5e72 - - [04/Feb/2024:15:38:23 +0100] "HEAD / HTTP/3.0" 200 0 "-" "http3check.net/lsquic/4.0.0

Is Nginx with OpenSSL viable for HTTP/3?

I think it’s fair to say that both Nginx and OpenSSL have some catching up to do. At least that’s true for OpenSSL. Try it out, but don’t remove support for HTTP/2.

References: QUIC+HTTP/3 Support for OpenSSL with NGINX