quiche: QUIC and HTTP/3 Implementation

repository·master·Indexed 11 days ago

https://github.com/cloudflare/quiche

An implementation of the QUIC transport protocol and HTTP/3 providing a low-level API for processing QUIC packets and managing connection state. Designed for integration into applications with their own I/O and event loops, it includes tools like h3i for interactive HTTP/3 testing and qlog-dancer for generating reports and charts from qlog and Chrome netlogs.

Tokens
51.6K
Snippets
138
Records
188
Agent score
95%

What's inside quiche

  1. How to parse netlog events and handle sessions

    master

    Netlog files contain various events that can be parsed into Rust structures using Serde.

    Workflow

    1. Read Header: Use read_netlog_record to get the raw bytes of a record. Use serde_json::from_slice to parse the record into an EventHeader.
    2. Populate Strings: Call event_hdr.populate_strings(&constants) to resolve compressed strings using the constants extracted from the file header.
    3. Handle Session Events: Netlogs often contain session-specific data. When an EventHeader indicates a PHASE_BEGIN and a specific type (like HTTP2_SESSION or QUIC_SESSION), you can deserialize the same record into specialized event types like Http2SessionEvent or QuicSessionEvent.
    4. Generic Parsing: Alternatively, use netlog::parse_event(&event_hdr, &record) to attempt to parse the event into a more general structure.
    // Example of handling a specific session type
    if event_hdr.phase_string == "PHASE_BEGIN" {
        match event_hdr.ty_string.as_str() {
            "HTTP2_SESSION" => {
                let ev: Http2SessionEvent = serde_json::from_slice(&record).unwrap();
                // Handle HTTP2 session
            },
            "QUIC_SESSION" => {
                let ev: QuicSessionEvent = serde_json::from_slice(&record).unwrap();
                // Handle QUIC session
            },
            _ => (),
        }
    }
  2. How qlog data models work

    master

    qlog is a hierarchical logging format used for QUIC and HTTP/3 protocol events. The structure follows a hierarchy of:

    • Log: The top-level container (applications can combine multiple traces into one log).
    • Trace(s): A single collection of events, typically mapping to one QUIC connection.
    • Event(s): Individual protocol events (e.g., packet_sent, metrics_updated).

    There are two primary ways to use qlog depending on your IO requirements:

    1. Buffered Traces (Standard JSON): A single Trace is a single JSON object. You append events to a Trace in memory and serialize the entire object at once. This is suitable for small traces or when you can wait until the connection closes to write to disk.
    2. Streaming Traces (JSON-SEQ): Uses TraceSeq and QlogStreamer to support RFC 7464 JSON Text Sequences. This allows you to stream events to a Write target (like a file or network socket) as they occur, which is more memory-efficient for long-lived connections.
  3. Configure a QUIC connection with `quiche::Config`

    master

    Establishing a QUIC connection requires a quiche::Config object. This object manages QUIC versions, ALPN IDs, flow control, congestion control, and idle timeouts.

    Important: Many properties default to zero and must be explicitly set for your application to function. You should typically configure:

    • set_initial_max_streams_bidi()
    • set_initial_max_streams_uni()
    • set_initial_max_data()
    • set_initial_max_stream_data_bidi_local()
    • set_initial_max_stream_data_bidi_remote()
    • set_initial_max_stream_data_uni()

    Config also handles TLS configuration, which can be managed via mutators or by using with_boring_ssl_ctx_builder().

    let mut config = quiche::Config::new(quiche::PROTOCOL_VERSION)?;
    config.set_application_protos(&[b"example-proto"]);
    
    // Additional configuration specific to application and use case...
  4. Record and replay HTTP/3 sessions with h3i

    master

    h3i automatically records all actions to a qlog file named <timestamp>-qlog.sqlog by default. You can replay these recorded sessions against a server using the --qlog-input option.

    This is useful for reproducing specific server behaviors or testing the same sequence of actions against different servers.

    Note: When replaying against a different server, you may need to manually rewrite :authority or host headers within the log to match the new target.

    Replay Commands

    # Replay against the original server
    cargo run cloudflare-quic.com --qlog-input <timestamp>-qlog.sqlog
    
    # Replay the same sequence against a different server
    cargo run blog.cloudflare.com --qlog-input <timestamp>-qlog.sqlog
  5. Manage connection timeouts

    master

    The application must maintain a timer to react to time-based connection events. Use conn.timeout() to determine when the next timeout event will occur. When the timer expires, call conn.on_timeout() and then call conn.send() again to process any resulting packets.

    let timeout = conn.timeout();
    // ... wait for timer ...
    
    // Timeout expired, handle it.
    conn.on_timeout();
    
    // Send more packets as needed after timeout.
    loop {
        let (write, send_info) = match conn.send(&mut out) {
            Ok(v) => v,
            Err(quiche::Error::Done) => break,
            Err(e) => { break; },
        };
        socket.send_to(&out[..write], &send_info.to).unwrap();
    }
  6. Starting an HTTP/3 Server with Tokio Quiche

    master

    To start an HTTP/3 server, bind a tokio::net::UdpSocket and use the listen function to create a listener. The listener provides a stream of connections. For each connection, you should initialize a ServerH3Driver and spawn a dedicated tokio task to handle the connection using a ServerH3Controller.

    use bytes::Bytes;
    use foundations::telemetry::log;
    use tokio_quiche::http3::driver::{H3Event, IncomingH3Headers, OutboundFrame, ServerH3Event};
    use tokio_quiche::http3::settings::Http3Settings;
    use tokio_quiche::listen;
    use tokio_quiche::metrics::DefaultMetrics;
    use tokio_quiche::quic::SimpleConnectionIdGenerator;
    use tokio_quiche::quiche::h3;
    use tokio_quiche::{ConnectionParams, ServerH3Controller, ServerH3Driver};
    
    let socket = tokio::net::UdpSocket::bind("0.0.0.0:4043").await?;
    let mut listeners = listen(
        [socket],
        ConnectionParams::new_server(
            Default::default(),
            tokio_quiche::settings::TlsCertificatePaths {
                cert: "/path/to/cert.pem",
                private_key: "/path/to/key.pem",
                kind: tokio_quiche::settings::CertificateKind::X509,
            },
            Default::default(),
        ),
        SimpleConnectionIdGenerator,
        DefaultMetrics,
        )?;
    let accept_stream = &mut listeners[0];
    
    while let Some(conn) = accept_stream.next().await {
        let (driver, controller) = ServerH3Driver::new(Http3Settings::default());
        conn?.start(driver);
        tokio::spawn(handle_connection(controller));
    }
    
    async fn handle_connection(mut controller: ServerH3Controller) {
        while let Some(ServerH3Event::Core(event)) = controller.event_receiver_mut().recv().await {
            match event {
                H3Event::IncomingHeaders(IncomingH3Headers {
                    mut send, headers, ..
                }) => {
                    log::info!("incoming headers"; "headers" => ?headers);
                    send.send(OutboundFrame::Headers(
                        vec![h3::Header::new(b":status", b"200")],
                        None,
                    ))
                    .await
                    .unwrap();
    
                    send.send(OutboundFrame::Body(
                        Bytes::copy_from_slice(b"hello from TQ!"),
                        true,
                    ))
                    .await
                    .unwrap();
                }
                event => {
                    log::info!("event: {event:?}");
                }
            }
        }
    }
  7. Run fuzzing on Mayhem

    master

    To use Mayhem for fuzzing, you must first build and publish the fuzzing Docker image, then execute the run command from the fuzz/mayhem/ directory.

    # Build and publish the Docker image from the repository root
    make docker-fuzz docker-fuzz-publish
    
    # Run the fuzzer from the fuzz/mayhem/ directory
    cd fuzz/mayhem/
    mayhem run --all <target>
  8. Sync and minimize test cases from Mayhem

    master

    To synchronize test cases from Mayhem and then minimize the inputs using cargo fuzz, follow these steps from the fuzz/mayhem/ directory.

    # From the fuzz/mayhem/ directory
    mayhem sync <target>
    
    # Minimize the inputs
    cargo +nightly fuzz cmin -Oa <target>
  9. Use the h3i command-line tool for interactive HTTP/3 testing

    master

    The h3i command-line tool is designed for ad-hoc, interactive exploration of HTTP/3 server behavior. It allows you to manually construct and queue a sequence of Actions (like sending headers or data) and then execute them against a server.

    To start an interactive session with a specific host:

    cargo run <hostname>

    Once the interactive prompt is open, you can select from various actions to build your request sequence. For example, to send an HTTP/3 request, you would typically select the headers action followed by commit to execute the sequence.

    Available Actions

    • headers: HTTP/3 HEADERS frame with mandatory pseudo headers
    • headers_no_pseudo: HTTP/3 HEADER frame without mandatory pseudo headers
    • data: HTTP/3 DATA frame
    • settings: HTTP/3 SETTINGS frame
    • goaway: HTTP/3 GOAWAY frame
    • priority_update: HTTP/3 PRIORITY_UPDATE frame
    • push_promise: HTTP/3 PUSH_PROMISE frame
    • cancel_push: HTTP/3 CANCEL_PUSH frame
    • max_push_id: HTTP/3 MAX_PUSH_ID frame
    • grease: HTTP/3 GREASE frame
    • extension_frame: HTTP/3 extension frame
    • open_uni_stream: Opens an HTTP/3 unidirectional stream with a specific type
    • stream_bytes: Sends arbitrary data on a stream
    • reset_stream: Resets a uni or bidi stream
    • stop_sending: Stops a bidi stream
    • connection_close: Closes the QUIC connection
    • flush_packets: Forces a QUIC packet flush to emit buffered actions
    • commit: Finishes action input, opens the connection, and executes all actions
    • wait: Specifies a client-side delay between action emits
    • quit: Quits without opening a connection

    Useful CLI Options and Environment Variables

    • --connect-to <IP:PORT>: Connect to a specific IP and port, ignoring server name resolution (useful for specifying SNI manually).
    • RUST_LOG=trace: Enables trace logging, which emits a JSON-serialized ConnectionSummary.
    • QLOGDIR=<path>: If set, writes a qlog file containing full QUIC and HTTP/3 details to the specified directory.
    • SSLKEYLOGFILE=<path>: Use this to log TLS session keys, allowing tools like Wireshark to decrypt the QUIC traffic.
    cargo run cloudflare-quic.com
  10. Generate code coverage for fuzzers

    master

    You can generate code coverage reports using cargo fuzz. Run the coverage command from the root of the repository, specifying the target and the corpus directory.

    To view the results as an HTML report, use llvm-cov. Ensure that the version of llvm-cov matches the one used by cargo-fuzz (typically found within your rustup toolchain directory).

    # Generate coverage data
    cargo +nightly fuzz coverage <target> fuzz/corpus/<target>
    
    # Example: generating an HTML report with llvm-cov
    # Replace the path to llvm-cov with your actual toolchain path
    ~/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/bin/llvm-cov show \
      --ignore-filename-regex='cargo/registry' \
      --ignore-filename-regex='/rustc' \
      --show-instantiations \
      --show-line-counts-or-regions \
      --Xdemangler=rustfilt \
      --instr-profile /path/to/coverage.profdata \
      /path/to/binary \
      --format=html \
      --output-dir "/tmp/cov"
  11. Handle incoming and outgoing QUIC packets

    master

    Quiche is a low-level API; the application is responsible for I/O (sockets) and the event loop.

    Receiving Packets: Use conn.recv() to process incoming packets from the network. You must provide a quiche::RecvInfo containing the source and destination addresses.

    Sending Packets: Use conn.send() to generate outgoing packets. The application must then write these packets to the socket.

    Pacing: To avoid congestion, use the at field in the SendInfo returned by send() to determine when a packet should be sent. You can implement pacing using platform-specific mechanisms like SO_TXTIME on Linux or user-space timers.

    // Handling incoming packets
    let (read, from) = socket.recv_from(&mut buf).unwrap();
    let recv_info = quiche::RecvInfo { from, to };
    let read = match conn.recv(&mut buf[..read], recv_info) {
        Ok(v) => v,
        Err(e) => { /* handle error */ break; },
    };
    
    // Generating outgoing packets
    loop {
        let (write, send_info) = match conn.send(&mut out) {
            Ok(v) => v,
            Err(quiche::Error::Done) => break,
            Err(e) => { /* handle error */ break; },
        };
        socket.send_to(&out[..write], &send_info.to).unwrap();
    }
  12. Stream traces using JSON-SEQ with QlogStreamer

    master

    For streaming logs, use qlog::TraceSeq and qlog::QlogStreamer. This method writes events as individual JSON lines separated by a record separator, making it ideal for real-time logging.

    Workflow:

    1. Create a TraceSeq.
    2. Initialize a QlogStreamer with a Write implementation (e.g., std::fs::File).
    3. Call start_log() to begin.
    4. Use add_event() for simple events.
    5. For events containing frames (like PacketSent with frames: Some(...)), use add_frame() for each frame and call finish_frames() to exit frame-serialization mode.
    6. Call finish_log() to finalize the stream.
    // 1. Setup TraceSeq and File
    let mut trace = qlog::TraceSeq::new(/* ... metadata ... */);
    let mut file = std::fs::File::create("foo.sqlog").unwrap();
    
    // 2. Initialize Streamer
    let mut streamer = qlog::QlogStreamer::new(
        qlog::QLOG_VERSION.to_string(),
        Some("Example qlog".to_string()),
        Some("Example qlog description".to_string()),
        None,
        std::time::Instant::now(),
        trace,
        qlog::EventImportance::Base,
        Box::new(file),
    );
    streamer.start_log().ok();
    
    // 3. Stream simple events
    let event = qlog::events::Event::with_time(0.0, event_data);
    streamer.add_event(event).ok();
    
    // 4. Stream events with frames
    // If an event has frames: Some(vec![...]), you must use add_frame
    streamer.add_frame(qlog::events::quic::QuicFrame::Ping, false).ok();
    streamer.finish_frames().ok();
    
    // 5. Finalize
    streamer.finish_log().ok();