We’ll build the ngtcp2 example client using the wolfSSL TLS backend (straightforward on Ubuntu), plus nghttp3 for HTTP/3. Then we’ll hit your server (e.g., localhost:8443) and verify QUIC/H3.

1) Install build deps
sudo apt update
sudo apt install -y build-essential autoconf automake libtool pkg-config \
cmake git libev-dev
2) Build and install wolfSSL (TLS)
git clone --depth 1 -b v5.8.2-stable https://github.com/wolfSSL/wolfssl
cd wolfssl
autoreconf -i
# enable QUIC support & keylog export
./configure --prefix=$PWD/build --enable-all --enable-aesni --enable-harden \
--disable-ech --enable-mlkem --enable-keylog-export
make -j"$(nproc)"
make install
cd ..
3) Build and install nghttp3 (HTTP/3 layer)
git clone --recursive https://github.com/ngtcp2/nghttp3
cd nghttp3
autoreconf -i
./configure --prefix=$PWD/build --enable-lib-only
make -j"$(nproc)" check
make install
cd ..
4) Build ngtcp2 (QUIC) with wolfSSL
git clone --recursive https://github.com/ngtcp2/ngtcp2
cd ngtcp2
autoreconf -i
PKG_CONFIG_PATH=$PWD/../wolfssl/build/lib/pkgconfig:$PWD/../nghttp3/build/lib/pkgconfig \
./configure --with-wolfssl
make -j"$(nproc)" check
PASS: examplestest
============================================================================
Testsuite summary for ngtcp2 1.16.0-DEV
============================================================================
# TOTAL: 1
# PASS: 1
# SKIP: 0
# XFAIL: 0
# FAIL: 0
# XPASS: 0
# ERROR: 0
============================================================================
- After a successful build, the example clients live under examples/. You’ll use examples/wsslclient (wolfSSL client) to talk H3. (This layout and commands come straight from the ngtcp2 README.) GitHub
5) Run the client against your Nginx
Assuming your Nginx H3 lab is on https://localhost:8443/ with a self-signed cert:
cd ngtcp2
# Save a CA file if you have one; for a self-signed lab, use –insecure (-I) below.
# basic GET /
examples/wsslclient localhost 8443 /
Output:
root@sanchit:~/ngtcp2# examples/wsslclient localhost 8443
I00000000 0xa10f442eadd72c703e4f7e3d0c77a550b0 con next skip pkn=640608332
I00000000 0xa10f442eadd72c703e4f7e3d0c77a550b0 pkt tx pkn=640608302 dcid=0x3bfcac4aa687185f7627ee26564980916488 scid=0xa10f442eadd72c703e4f7e3d0c77a550b0 version=0x00000001 type=Initial len=0
I00000000 0xa10f442eadd72c703e4f7e3d0c77a550b0 frm tx 640608302 Initial CRYPTO(0x06) offset=156 len=15
I00000000 0xa10f442eadd72c703e4f7e3d0c77a550b0 frm tx 640608302 Initial CRYPTO(0x06) offset=114 len=28
I00000000 0xa10f442eadd72c703e4f7e3d0c77a550b0 frm tx 640608302 Initial CRYPTO(0x06) offset=246 len=17
I00000000 0xa10f442eadd72c703e4f7e3d0c77a550b0 frm tx 640608302 Initial CRYPTO(0x06) offset=0 len=57
I00000000 0xa10f442eadd72c703e4f7e3d0c77a550b0 frm tx 640608302 Initial CRYPTO(0x06) offset=229 len=8
I00000000 0xa10f442eadd72c703e4f7e3d0c77a550b0 frm tx 640608302 Initial CRYPTO(0x06) offset=142 len=14
I00000000 0xa10f442eadd72c703e4f7e3d0c77a550b0 frm tx 640608302 Initial CRYPTO(0x06) offset=57 len=28
I00000000 0xa10f442eadd72c703e4f7e3d0c77a550b0 frm tx 640608302 Initial CRYPTO(0x06) offset=99 len=15
I00000000 0xa10f442eadd72c703e4f7e3d0c77a550b0 frm tx 640608302 Initial PADDING(0x00) len=11
I00000000 0xa10f442eadd72c703e4f7e3d0c77a550b0 frm tx 640608302 Initial CRYPTO(0x06) offset=171 len=58
I00000000 0xa10f442eadd72c703e4f7e3d0c77a550b0 frm tx 640608302 Initial CRYPTO(0x06) offset=85 len=14
I00000000 0xa10f442eadd72c703e4f7e3d0c77a550b0 frm tx 640608302 Initial CRYPTO(0x06) offset=237 len=9
I00000000 0xa10f442eadd72c703e4f7e3d0c77a550b0 frm tx 640608302 Initial PADDING(0x00) len=817
I00000000 0xa10f442eadd72c703e4f7e3d0c77a550b0 ldc loss_detection_timer=83394990479639 timeout=999
Sent packet: local=[127.0.0.1]:50877 remote=[127.0.0.1]:8443 ecn=0x2 1200 bytes
Timer has already expired: 0.002582s
Set timer=0.996290s
Received packet: local=[127.0.0.1]:50877 remote=[127.0.0.1]:8443 ecn=0x0 1200 bytes
I00000006 0xa10f442eadd72c703e4f7e3d0c77a550b0 con recv packet len=1200
I00000006 0xa10f442eadd72c703e4f7e3d0c77a550b0 pkt rx pkn=0 dcid=0xa10f442eadd72c703e4f7e3d0c77a550b0 scid=0x0000000000000008532b39f28d1f384150a4a808 version=0x00000001 type=Initial len=119
I00000006 0xa10f442eadd72c703e4f7e3d0c77a550b0 frm rx 0 Initial ACK(0x02) largest_ack=640608302 ack_delay=0(0) ack_range_count=0
I00000006 0xa10f442eadd72c703e4f7e3d0c77a550b0 frm rx 0 Initial ACK(0x02) range=[640608302..640608302] len=0
I00000006 0xa10f442eadd72c703e4f7e3d0c77a550b0 ldc latest_rtt=6 min_rtt=6 smoothed_rtt=6 rttvar=3 ack_delay=0
I00000006 0xa10f442eadd72c703e4f7e3d0c77a550b0 con path is not ECN capable
I00000006 0xa10f442eadd72c703e4f7e3d0c77a550b0 cca 1200 bytes acked, slow start cwnd=13200
I00000006 0xa10f442eadd72c703e4f7e3d0c77a550b0 ldc loss_detection_timer=83394017334377 timeout=19
I00000006 0xa10f442eadd72c703e4f7e3d0c77a550b0 frm rx 0 Initial CRYPTO(0x06) offset=0 len=90
I00000006 0xa10f442eadd72c703e4f7e3d0c77a550b0 con the negotiated version is 0x00000001
Ordered CRYPTO data in Initial crypto level
00000000 02 00 00 56 03 03 12 19 08 2b e4 bc ad ca 9d 8a |...V.....+......|
00000010 ef 16 d8 15 b3 de 07 be 64 80 9a ca 9d 13 b8 d8 |........d.......|
00000020 85 30 5a c3 f7 2b 00 13 01 00 00 2e 00 2b 00 02 |.0Z..+.......+..|
00000030 03 04 00 33 00 24 00 1d 00 20 84 a7 86 21 97 1c |...3.$... ...!..|
00000040 b4 31 c5 e0 b6 51 bd a9 36 62 d4 e3 41 21 ab 5d |.1...Q..6b..A!.]|
00000050 4f f7 ff bb ca 40 c5 da cd 01 |O....@....|
</truncate>
Nginx’s own HTTP/3 doc explicitly suggests validating with a console client like ngtcp2 before trying browsers. Nginx
6) Know it worked
- The client prints the HTTP status/headers and transfer details.
- On the server, add this log format once (then reload):
- log_format quic ‘$remote_addr – $host “$request” $status h3=$http3’;
- access_log /var/log/nginx/access.log quic;
You’ll see h3= filled for HTTP/3 requests.
7) Quick troubleshooting
- No response / handshake fails
- Ensure UDP 8443 (or 443) is listening/open:
- ss -u -lpn | grep ‘:8443’ # or :443
- sudo ufw allow 8443/udp
- Verify your server block has:
- listen 8443 quic reuseport;
- listen 8443 ssl; http2 on;
- add_header Alt-Svc ‘h3=”:8443″; ma=86400’ always;
- Cert errors: pass -I to wsslclient for self-signed; for real domains, use a proper cert and omit -I.
- Build errors: make sure you ran autoreconf -i in each repo and set PKG_CONFIG_PATH like shown. (ngtcp2’s README lists the exact requirements and TLS backends supported.) GitHub
References
- Nginx HTTP/3 docs (recommend using ngtcp2 first). Nginx
- ngtcp2 README (build requirements, TLS backends, example clients). GitHub
- curl HTTP/3 doc (if you later want an H3-capable curl build). Curl
Useful Links
https://www.wireshark.org/docs/relnotes
https://sanchitgurukul.com/basic-networking
https://sanchitgurukul.com/network-security
