Net::QUIC

Net::QUIC is a QUIC transport library for Perl.

It gives Perl applications QUIC connections and QUIC streams without choosing an event loop for them.

Net::QUIC handles:

Your event loop still owns the UDP socket.

Net::QUIC is not HTTP/3, a web framework, or an application message protocol. QUIC streams carry ordered bytes. Applications decide what those bytes mean.

Installation

From CPAN:

cpanm Net::QUIC

Net::QUIC uses Alien::ngtcp2 for its native QUIC and TLS dependencies. A normal Net::QUIC install does not require you to separately find or configure ngtcp2.

The event-loop modules shown in examples/ are optional. Net::QUIC itself does not require Linux::Event, AnyEvent, IO::Async, Mojolicious, or EV.

Start here

Most applications only need to understand three objects:

Net::QUIC::Driver
        |
        +-- Net::QUIC::Connection
                    |
                    +-- Net::QUIC::Stream

Use Net::QUIC::Driver to connect Net::QUIC to an event loop.

Use Net::QUIC::Connection to open or accept QUIC streams.

Use Net::QUIC::Stream to send and receive application bytes.

There is also a lower-level Net::QUIC::Endpoint. Most applications do not need to drive it directly.

The full internal relationship is:

Net::QUIC::Driver
        |
        +-- Net::QUIC::Endpoint
                |
                +-- Net::QUIC::Connection
                            |
                            +-- Net::QUIC::Stream

The event-loop contract

A Net::QUIC adapter needs only:

The adapter gives Driver two callbacks:

send => sub {
    my ($datagram) = @_;

    # Accept one complete UDP datagram for output.
    # Return true when another datagram can be accepted immediately.
    # Return false after accepting this datagram if output is backpressured.
},

set_timeout => sub {
    my ($seconds) = @_;

    # Replace the current one-shot QUIC timeout.
    # undef means cancel the current timeout.
},

The send callback receives one complete Net::QUIC::Datagram.

Its useful values are:

$datagram->data;     # complete UDP payload bytes
$datagram->peer;     # packed destination socket address
$datagram->local;    # packed local socket address chosen by QUIC

The adapter sends data as one UDP datagram to peer. local describes the local path associated with that packet and is useful to integrations that manage more than one local address.

The event loop reports four events back to Driver:

$driver->start;

$driver->receive(
    $bytes,
    $packed_local_address,
    $packed_peer_address,
);

$driver->timeout;

$driver->writable;

That is the complete ordinary adapter contract.

There is no application-visible QUIC pump loop.

Driver drains QUIC output, pauses when the adapter reports backpressure, resumes when writable is called, and replaces the QUIC timer whenever the deadline changes.

Stream operations such as send, finish, reset, and next_data automatically notify Driver when more QUIC work may be needed.

Driver handles transport servicing; it does not decide what your application should do. After receive, timeout, or writable returns, application code can inspect the Connection and Streams normally:

$driver->receive($bytes, $local, $peer);

if ($connection->ready) {
    while (my $stream = $connection->next_stream) {
        ...
    }
}

The examples use this pattern through a small application-service callback.

Local addresses and wildcard UDP sockets

The local address passed to Net::QUIC is part of the QUIC network path. It must be the concrete IPv4 or IPv6 address for that packet.

These are not valid QUIC local paths:

0.0.0.0
::

They are wildcard bind addresses. They mean "accept traffic for any local address"; they do not identify the address on which one particular UDP packet arrived.

A client normally avoids this issue by connecting its UDP socket first and using getsockname after the kernel has selected the concrete local address.

A server may bind its UDP socket to a wildcard address, but its adapter must recover the actual destination address of every received packet and pass that packed address to:

$driver->receive($bytes, $local, $peer);

The adapter must also send each outbound Datagram using the local source address reported by:

$datagram->local;

For a socket bound to one concrete address, the socket already fixes the local path and no special source-address selection is normally needed.

For a wildcard-bound socket this usually requires packet-info support. On Linux, IPv4 adapters can use IP_PKTINFO with recvmsg / sendmsg; IPv6 has the corresponding packet-info mechanism. Other operating systems have their own destination-address ancillary-data APIs.

Net::QUIC deliberately does not implement those socket operations. The event loop or UDP adapter owns the socket. Net::QUIC rejects wildcard addresses at its QUIC path boundary so an adapter cannot accidentally give ngtcp2 an incorrect network path.

If an event system cannot report the destination address for a wildcard-bound socket, bind the QUIC socket to one concrete local address instead.

Event-loop examples

The examples/ directory contains complete client integrations for common Perl event systems:

examples/linux-event-client.pl
examples/anyevent-client.pl
examples/io-async-client.pl
examples/io-async-async-await-client.pl
examples/mojo-ioloop-client.pl
examples/ev-client.pl

They all implement the same Driver contract so the event-loop-specific part is easy to compare.

The two IO::Async examples deliberately show both styles: one keeps the application callback-driven, while the other uses Future::AsyncAwait so the application flow can be written sequentially without changing the Net::QUIC Driver API.

See examples/README.md for how to run them.

A client connection

A client Driver is created after the UDP socket addresses are known.

The four values that identify the connection are straightforward:

For example, a client can connect to the numeric peer address 192.0.2.20:4433 while using server_name => 'service.example.com' when that is the name on the server certificate.

The packed addresses are the ordinary native socket-address values used by Perl's Socket APIs. Event-loop socket objects can often provide them directly.

use Net::QUIC::Driver;

my $driver = Net::QUIC::Driver->client(
    local       => $packed_local_address,
    peer        => $packed_peer_address,
    alpn        => 'my-protocol',
    server_name => 'example.com',

    send => sub {
        my ($datagram) = @_;
        ...
    },

    set_timeout => sub {
        my ($seconds) = @_;
        ...
    },
);

my $connection = $driver->connection;

$driver->start;

local and peer are packed IPv4 or IPv6 socket addresses.

The Driver constructor does not send packets. start tells it the UDP transport is ready. A client normally produces its first QUIC Initial packet at that point.

The cryptographic handshake completes asynchronously:

if ($connection->ready) {
    ...
}

A server

A server uses the same Driver contract:

my $driver = Net::QUIC::Driver->server(
    alpn             => 'my-protocol',
    certificate_file => 'server-cert.pem',
    private_key_file => 'server-key.pem',

    send => sub {
        my ($datagram) = @_;
        ...
    },

    set_timeout => sub {
        my ($seconds) = @_;
        ...
    },
);

$driver->start;

Feed each received UDP packet to the same Driver:

$driver->receive($bytes, $local, $peer);

A server Driver can own many QUIC connections. Pull newly created connections with:

while (my $connection = $driver->next_connection) {
    ...
}

A server Connection may be returned before its handshake is complete. Check ready before beginning application work that requires an established connection.

Set:

validate_address => 1

to require QUIC Retry/address validation before allocating a new Connection.

Without that option, address validation is off and the extra Retry round trip is avoided.

Sending on a stream

Open a bidirectional stream:

my $stream = $connection->open_bidi_stream;

if ($stream) {
    $stream->send("hello\n");
    $stream->finish;
}

finish sends QUIC FIN after the already queued bytes. It closes only this endpoint's send side. The peer can still reply on the same bidirectional stream.

A local unidirectional stream is opened with:

my $stream = $connection->open_uni_stream;

This endpoint can send on that stream but cannot receive application bytes from it.

When opening a stream returns undef

QUIC limits how many streams an endpoint may have open at once.

Therefore:

my $stream = $connection->open_bidi_stream;

can return undef.

That is normal flow control. It does not mean the Connection failed.

If the application needs to wait for more stream credit:

$connection->on_stream_available(sub {
    my ($connection, $type) = @_;

    return if $type ne 'bidi';

    my $stream = $connection->open_bidi_stream;
    return if !defined $stream;

    ...
});

$type is bidi or uni.

Receiving peer streams

Streams opened by the peer are pulled from the Connection:

while (my $stream = $connection->next_stream) {
    ...
}

Read available bytes with:

while (defined(my $bytes = $stream->next_data)) {
    handle_bytes($bytes);
}

QUIC streams are byte streams, not message streams.

One call to:

$stream->send($message);

does not guarantee one matching next_data call on the peer.

If an application needs messages, it should put its own framing on the QUIC stream.

Stream completion and reset

A clean peer FIN is visible through:

if ($stream->remote_finished) {
    ...
}

A stream can be aborted with:

$stream->reset($application_error_code);

The local and remote reset codes are available separately:

my $local_code  = $stream->local_reset_code;
my $remote_code = $stream->remote_reset_code;

closed becomes true after ngtcp2 reports the stream completely closed.

A Stream object keeps its Connection alive. Final stream state and unread buffered receive data remain available while the Stream object still exists.

Closing a connection

Start a normal application close with:

$connection->close;

or:

$connection->close($application_error_code);

The default application error code is zero.

Closing is not immediate destruction. QUIC has a closing/draining period during which late packets still need to be handled.

closed becomes true only after the Connection no longer needs network or timer service.

Connection errors and close information

Remote protocol errors, TLS failures, certificate failures, idle timeout, and normal application close are connection outcomes rather than generic Perl exceptions.

Inspect them with:

my $info = $connection->close_info;

It returns undef while no close or failure has been recorded.

A normal peer application close can look like:

{
    type      => 'application',
    initiator => 'peer',
    code      => 0,
}

Possible type values are:

application
transport
tls
certificate
handshake
idle
drop

initiator is local or peer.

Local API misuse, invalid configuration, allocation failure, and internal implementation failures still throw exceptions. Those are programming or system failures rather than normal remote connection outcomes.

TLS and certificate verification

QUIC always uses TLS 1.3.

Net::QUIC uses Picotls for QUIC TLS. Picotls uses OpenSSL underneath for cryptography and certificate verification.

Clients verify server certificates by default.

server_name is used for DNS-name or IP-address verification:

my $driver = Net::QUIC::Driver->client(
    ...
    server_name => 'example.com',
);

OpenSSL's default trust locations are used.

For a private or test CA:

ca_file => '/path/to/private-ca.pem'

adds that PEM file to the trust store.

Net::QUIC does not provide an insecure skip-verification switch.

Server certificate and key files are loaded when the server Endpoint is created. Accepted Connections reuse the shared server TLS credential context; the files are not reopened for every Connection.

Transport defaults

Client and server constructors accept an optional transport hash:

transport => {
    handshake_timeout => 10,
    idle_timeout      => 30,
    connection_window => 1024 * 1024,
    stream_window     => 256 * 1024,
    max_bidi_streams  => 100,
    max_uni_streams   => 100,
}

Those values are the Net::QUIC defaults.

Timeout values are seconds and may be fractional.

idle_timeout => 0 disables the advertised idle timeout.

The receive windows are bytes. They are starting flow-control windows, not lifetime transfer limits. Net::QUIC returns receive credit as application data is consumed.

The stream counts are initial concurrent peer-stream limits. Stream credit is returned as peer streams close.

Active connection migration is currently advertised as disabled.

ACK timing, congestion control, PMTU policy, packet-size shaping, and connection-ID management remain Net::QUIC/ngtcp2 policy rather than public constructor knobs.

The lower-level Endpoint

Most event-loop adapters should use Driver.

Net::QUIC::Endpoint remains available when direct control is needed.

Its integration API is:

$endpoint->receive_datagram($bytes, $local, $peer);

while (my $datagram = $endpoint->next_datagram) {
    ...
}

my $seconds = $endpoint->timeout_after;

$endpoint->handle_timeout;

When using Endpoint directly, the caller is responsible for repeatedly draining output and replacing the timer after every state change.

Driver exists specifically so ordinary adapters do not have to repeat those rules.

Native dependency

Net::QUIC requires Alien::ngtcp2 0.03 or newer.

Alien::ngtcp2 supplies the tested ngtcp2 and Picotls build.

Normal Net::QUIC applications do not choose a TLS backend.

Scope

Net::QUIC is the transport layer.

HTTP/3 belongs in a separate distribution above it.

The first transport release does not need to include later QUIC features such as:

session resumption and 0-RTT
connection migration
QUIC DATAGRAM
qlog
ECN exposure
advanced congestion-control tuning

These can be added without changing the basic Driver, Connection, and Stream model.

Development

perl Makefile.PL
make
make test

License

Net::QUIC is MIT licensed.