TsgcQUICClient

TsgcQUICClient is a low-level QUIC transport client (RFC 9000) that opens a QUIC connection to a server over UDP and exchanges raw bytes over QUIC streams.

Introduction

QUIC is the transport protocol standardised in RFC 9000, with its TLS integration in RFC 9001 and its loss detection and congestion control in RFC 9002. It is the transport HTTP/3 runs on. Compared with TCP plus TLS it changes four things that matter to an application:

TsgcQUICClient exposes this transport directly, as raw bytes on streams. It does not speak HTTP/3 or any other application protocol: use TsgcHTTP3Client when you want HTTP semantics over QUIC. Reach for TsgcQUICClient when you are carrying your own protocol over QUIC, or testing a QUIC endpoint.

Scope worth knowing before you start. The component is a thin client over the QUIC transport, and two of its limits surprise people:

The two QUIC engines

The library can drive QUIC through either of two engines. The engine follows SSLQUICOptions.IOHandler:

  1. With iohOpenSSL (the default), if the OpenSSL build exports OSSL_QUIC_client_method, the builtin engine (qmBuiltin) is used. Here OpenSSL 3.5 or later owns the whole QUIC connection and the library drives it through the OpenSSL stream API.
  2. Otherwise, if the build exports the OpenSSL 3.5 QUIC TLS API (SSL_set_quic_tls_cbs), the sgc engine (qmCustom) is used as the fallback. This is the pure Pascal QUIC stack in the sgcQUIC_* units: it builds and parses the packets itself and uses OpenSSL only to drive the TLS 1.3 handshake. If neither API is available, for example with a quictls build, initialising the connection raises an exception.
  3. With iohNativeTLS the sgc engine is always used and the TLS 1.3 handshake runs on the native TLS engine, with session tickets, resumption and 0-RTT. The packet protection is native too, so OpenSSL is not loaded at all.

With the OpenSSL 3.5 binaries bundled with the demos qmBuiltin is selected. IsOpenSSL_QUIC_Available and IsOpenSSL_QUIC_TLS_Available, declared in sgcIdSSLOpenSSLHeaders, report which of the two OpenSSL APIs the loaded library exports. The engines compare as follows:

FeatureqmBuiltin (OpenSSL 3.5+)qmCustom (sgc engine)
Connect, disconnect, send on the default streamYesYes
EnableMultiStreamYesYes
OpenStream / WriteStreamDataYesYes
OnQUICStreamData (the read path)Yes, after EnableMultiStreamYes, after EnableMultiStream
QUICOptions.Settings tuningMostly negotiated by OpenSSL, values not consultedYes, drives the Pascal stack

See QUIC for the full engine comparison.

Quick start

1. Create a TsgcQUICClient and set Host and Port of the QUIC server. QUIC runs over UDP, so Port is a UDP port, commonly 443.

2. Configure TLS through SSLQUICOptions. Verification is off by default, so turn VerifyCertificate on and point RootCertFile at your CA for anything but a throwaway test. With verification on the certificate must now also match the host you are connecting to, so a self-signed certificate, or one issued for a different name, is rejected where it used to be accepted.

3. Handle OnQUICConnect, OnQUICDisconnect and OnQUICException. Handling OnQUICException is not optional: connect failures do not raise, they are reported there, and Connect merely returns False.

4. Open the connection with Connect (blocking, returns a result) or Start / Active := True (non-blocking).

5. If you need to receive anything, call EnableMultiStream once connected and handle OnQUICStreamData. To send, use WriteData for the default stream, or OpenStream plus WriteStreamData for a specific one.


oClient := TsgcQUICClient.Create(nil);
oClient.Host := 'www.example.com';
oClient.Port := 443;

oClient.SSLQUICOptions.VerifyCertificate := True;
oClient.SSLQUICOptions.RootCertFile := 'ca.pem';
oClient.SSLQUICOptions.ConnectTimeout := 10000;

oClient.OnQUICConnect := OnQUICConnectEvent;
oClient.OnQUICStreamData := OnQUICStreamDataEvent;
oClient.OnQUICException := OnQUICExceptionEvent;

if oClient.Connect then
  oClient.WriteData('Hello QUIC');

Reference

Key Properties

NameDescription
ActiveOpens or closes the QUIC connection to the remote server.
HostIP address or DNS name of the QUIC server the client will connect to.
PortUDP port used to connect to the QUIC server.
SSLQUICOptionsTLS 1.3 settings: certificates, peer verification and the handshake timeout. Verification is off by default.
QUICOptionsTransport tuning for the QUIC connection, such as PMTU and flow control windows.

Key Methods

NameDescription
ConnectOpens the QUIC connection and waits until it is established or the timeout expires.
DisconnectCloses the QUIC connection to the server.
WriteDataSends data to the server over the default QUIC stream.
EnableMultiStreamSwitches the live connection into multi-stream mode. Required before anything can be received.
OpenStreamOpens a new client-initiated QUIC stream and returns it, or nil on failure.

Key Events

NameDescription
OnQUICConnectFires once the QUIC connection has been established and the client is ready to send data.
OnQUICStreamDataFires with the bytes read from a QUIC stream. Inert until EnableMultiStream has been called.
OnQUICDisconnectFires when the QUIC connection has been closed, reporting the close code and reason.
OnQUICExceptionFires when an exception is raised on the QUIC connection. The only way to learn why a connect failed.

Complete Example

Connect to a QUIC server, enable multi-stream so data can be received, send a message on a dedicated stream and log everything that comes back.


uses
  sgcHTTP, sgcQUIC_Client, sgcQUIC_Classes;

procedure TForm1.Start;
begin
  FClient := TsgcQUICClient.Create(nil);
  FClient.Host := 'www.example.com';
  FClient.Port := 443;

  // TLS 1.3. Verification is off by default, turn it on.
  FClient.SSLQUICOptions.VerifyCertificate := True;
  FClient.SSLQUICOptions.RootCertFile := 'ca.pem';
  // the handshake timeout, 10000 ms is already the default
  FClient.SSLQUICOptions.ConnectTimeout := 10000;

  FClient.OnQUICConnect := OnQUICConnectEvent;
  FClient.OnQUICStreamData := OnQUICStreamDataEvent;
  FClient.OnQUICDisconnect := OnQUICDisconnectEvent;
  FClient.OnQUICException := OnQUICExceptionEvent;

  // Connect returns False without raising, the reason arrives on OnQUICException
  if not FClient.Connect(10000) then
    Log('connect failed');
end;

procedure TForm1.OnQUICConnectEvent(Sender: TObject; const Connection: TsgcQUICConnection);
var
  oStream: TsgcQUICClientStream;
begin
  Log('connected');

  // required before OnQUICStreamData will ever fire
  FClient.EnableMultiStream;

  oStream := FClient.OpenStream; // bidirectional
  if Assigned(oStream) then
    FClient.WriteStreamData(oStream, TEncoding.UTF8.GetBytes('Hello QUIC'))
  else
    Log('could not open a stream, is the builtin OpenSSL QUIC engine active?');
end;

procedure TForm1.OnQUICStreamDataEvent(Sender: TObject; aStreamId: Int64;
  aUnidirectional: Boolean; const aData: TBytes);
begin
  // raised on the read thread, do not touch the UI directly here
  Log(Format('stream %d: %d bytes', [aStreamId, Length(aData)]));
end;

procedure TForm1.OnQUICDisconnectEvent(Sender: TObject; const Connection: TsgcQUICConnection;
  Code: Int64; const Reason: string);
begin
  if Code = 0 then
    Log('closed cleanly')
  else
    Log(Format('closed, code %d: %s', [Code, Reason]));
end;

procedure TForm1.OnQUICExceptionEvent(Sender: TObject; const Connection: TsgcQUICConnection;
  const E: Exception);
begin
  Log('error: ' + E.Message);
end;

Requirements