Skip to content

QUIC Streams and Toolchain

QUIC runs over UDP, but gives your application reliable, ordered streams rather than raw datagrams. Before sending anything, check that your Raspberry Pi can build both sides of the upcoming QUIC exercises.

Outcome: Build a C++ probe that constructs QUIC client and listener objects, reports the headers and loaded OpenSSL library, and explicitly does not claim HTTP/3 support.

Before you start

  • Complete verified TLS and review TCP message boundaries.
  • Use Linux, a C++20 compiler, CMake 3.28 or newer, and OpenSSL 3.5 or newer development files with QUIC enabled. The client API appeared earlier, but this chapter also builds a server.
  • No extra hardware, router changes, public server or administrator access is needed for the probe itself. Installing missing system development packages may need an administrator.

Standalone example on GitHub · OpenSSL QUIC methods

QUIC is not just encrypted UDP

flowchart TB accTitle: What each application receives accDescr: Raw UDP applications receive datagrams. QUIC applications receive ordered bytes on distinct streams. HTTP/3 adds requests and responses on top of QUIC. U[UDP application: individual datagrams] --> UDP[UDP socket] H[HTTP/3: requests and responses] --> Q[QUIC: ordered bytes per stream] Q --> UDP T[TLS 1.3 handshake and packet protection] --- Q UDP --> IP[IP network]
Interface What you must distinguish
UDP socket Datagram boundaries, missing messages, duplicates and reordering
QUIC stream Ordered bytes within that stream, application framing, FIN or reset
HTTP/3 library HTTP fields, request streams, response status and body completion

QUIC handles loss recovery and ordering within each stream. It does not preserve the boundaries of your SSL_write_ex() calls. A single read can contain part of a write or bytes from several writes, just as with the TCP exercise.

Different streams have separate ordering. Waiting for missing bytes on stream A does not itself require withholding ready bytes on stream B. They still share a connection, congestion control, network capacity and connection-level flow control: this is not a promise that streams never affect each other.

QUIC uses the TLS 1.3 handshake to establish keys and authenticate the server, but does not put ordinary TLS records around application data. It protects QUIC packets. DTLS is a different protocol, not the encryption layer for QUIC. See QUIC transport and TLS in QUIC.

Check the development toolchain, not only the commands

openssl version
curl -V

These describe those particular command-line programs. Neither proves which headers CMake will find or which libssl your C++ program will load.

Likewise, look for HTTP3 in curl's feature list, not just an OpenSSL or HTTP2 entry. QUIC support in OpenSSL does not provide HTTP/3 framing, control streams or QPACK. The later HTTP/3 lesson checks that separate layer.

On the course's Pi 4B, the inspected runtime was OpenSSL 3.5.7; the host runtime was 3.5.5. Those are verification records, not required patch versions or a recommendation to downgrade. Use maintained packages for your OS.

If development files are absent on Raspberry Pi OS, check the candidate before installing:

apt-cache policy libssl-dev
sudo apt install libssl-dev

The candidate must meet the 3.5 requirement for this chapter. An older OS can legitimately offer an older OpenSSL. Do not replace its system libraries by hand or mix headers and libraries from unrelated releases. The prior TLS/DTLS examples remain available without enabling this optional chapter.

Build and run the QUIC probe

From the examples checkout:

1
2
3
4
5
6
7
8
9
cd network
quic_tools_env=$(mktemp -d /tmp/network-quic-python.XXXXXX)
python3 -m venv "$quic_tools_env/venv"
"$quic_tools_env/venv/bin/python" -m pip install -r tests/requirements-quic.txt
cmake --preset native-debug -DNETWORK_BUILD_QUIC=ON \
    -DNETWORK_QUIC_PYTHON="$quic_tools_env/venv/bin/python"
cmake --build --preset native-debug --target quic_tools --parallel 2
./build/native/debug/quic_tools
ctest --preset native-debug -R '^network\.quic_tools\.' --output-on-failure

The chapter is opt-in. Leaving NETWORK_BUILD_QUIC off keeps the earlier lessons' OpenSSL 3.0 minimum; turning it on checks for 3.5 development files.

The virtual environment supplies the independent peer for this chapter's later communication tests; the probe itself does not use it. For a probe-only build without Python test dependencies, configure with -DBUILD_TESTING=OFF and omit the ctest command. CMake never downloads packages or silently skips a missing independent peer.

Example output on the Pi used for verification:

1
2
3
4
5
6
headers: OpenSSL 3.5.7 9 Jun 2026
runtime: OpenSSL 3.5.7 9 Jun 2026
QUIC client object: ready
QUIC listener object: ready
network traffic: none
HTTP/3: not tested by this probe

The version/date lines can differ. The last four lines are the important outcome. This is object construction only: no socket, certificate, handshake, stream data or HTTP exchange has been tested yet.

Read the probe

The client uses SSL_new() with OSSL_QUIC_client_method(). The server uses SSL_new_listener() with OSSL_QUIC_server_method(): a listener is not a data stream or an accepted connection. Both objects enter nonblocking mode, which the following exercises need to enforce deadlines.

Each successful allocation is freed explicitly, including partial setup failures. The compile-time checks detect old or QUIC-disabled headers; the runtime check and actual object construction catch additional configuration problems. If the loader cannot resolve a required symbol, it can fail before main(): the version printout is not a substitute for a consistent installation.

Full standalone probe: app/quic-tools/main.cpp
#include <openssl/err.h>
#include <openssl/quic.h>
#include <openssl/ssl.h>

#include <iostream>

#if OPENSSL_VERSION_NUMBER < 0x30500000L
#error "This chapter needs OpenSSL 3.5 or newer headers."
#endif
#ifdef OPENSSL_NO_QUIC
#error "This OpenSSL build has no QUIC support."
#endif

int main(int argc, char**) {
    if (argc != 1) {
        std::cerr << "usage: quic_tools\n";
        return 2;
    }

    // Headers and the loaded library can be different installations.
    std::cout << "headers: " << OPENSSL_VERSION_TEXT << '\n'
              << "runtime: " << OpenSSL_version(OPENSSL_VERSION) << '\n';
    if (OpenSSL_version_num() < 0x30500000L) {
        std::cerr << "OpenSSL runtime must be 3.5 or newer\n";
        return 1;
    }

    SSL_CTX* client_context = SSL_CTX_new(OSSL_QUIC_client_method());
    SSL_CTX* server_context = SSL_CTX_new(OSSL_QUIC_server_method());
    SSL* client = client_context ? SSL_new(client_context) : nullptr;
    // A QUIC server starts with a listener, not SSL_new(server_context).
    SSL* listener =
        server_context ? SSL_new_listener(server_context, 0) : nullptr;
    const bool ready = client && listener && SSL_is_quic(client) &&
                       SSL_is_listener(listener) &&
                       SSL_set_blocking_mode(client, 0) == 1 &&
                       SSL_set_blocking_mode(listener, 0) == 1;
    if (ready) {
        std::cout << "QUIC client object: ready\n"
                     "QUIC listener object: ready\n"
                     "network traffic: none\n"
                     "HTTP/3: not tested by this probe\n";
    } else {
        std::cerr << "QUIC object setup failed\n";
        ERR_print_errors_fp(stderr);
    }

    SSL_free(listener);
    SSL_free(client);
    SSL_CTX_free(server_context);
    SSL_CTX_free(client_context);
    return ready ? 0 : 1;
}

If it does not work

Symptom Next check
CMake reports an unsuitable OpenSSL version Confirm the development package candidate, not only openssl version
openssl/quic.h or the server method is missing Check the compiler include path and the 3.5 minimum
Linker reports an undefined QUIC symbol Inspect CMake's selected SSL/Crypto libraries and remove unintended custom search paths
Dynamic loader reports a missing symbol Check ldd ./build/native/debug/quic_tools for a conflicting runtime installation
Objects are ready but curl lacks HTTP3 Expected: transport support and HTTP/3 support are separate

When correcting a cached library selection, configure a new build directory or deliberately update the relevant CMake cache entries. Do not delete unrelated build trees or alter the system library search path to suppress an error.

Check your understanding

  1. If QUIC uses UDP, must your application retry each missing stream byte?
  2. Can one successful write be assumed to produce one matching read?
  3. Why does a ready QUIC listener not prove HTTP/3 support?
Answers
  1. No. QUIC's transport handles stream delivery and loss recovery, subject to failure and timeout. Your application still needs meaningful completion/error handling.
  2. No. Stream bytes need application framing. A FIN marks the end of one sending direction, not every individual message.
  3. HTTP/3 is a separate application protocol with HTTP frames and QPACK. This probe constructs transport objects without exchanging packets.

Continue the course

Continue with Your First Verified QUIC Stream, using a custom application protocol, not HTTP/3. Return to the course overview for the full path.

Donate