Net::QUIC
Net::QUIC is a QUIC transport library for Perl.
QUIC is a secure network transport built on UDP. A QUIC connection can carry many independent byte streams at the same time.
If you are new to QUIC, the useful mental model is:
one QUIC connection
|
+-- stream
+-- stream
+-- stream
Each stream is a reliable ordered sequence of bytes.
Net::QUIC handles the difficult transport work:
- QUIC packets and connection state
- TLS 1.3 encryption
- certificate verification
- streams and flow control
- retransmission and loss recovery
- connection IDs
- timers
- migration between network paths
- QUIC v1 and QUIC v2
Your event loop still owns the UDP socket.
You do not need to know ngtcp2 to use Net::QUIC. It is an internal native dependency.
Net::QUIC is not HTTP/3 and is not a web framework. It gives applications connections and byte streams. Your application decides what the bytes mean.
Installation
From CPAN:
cpanm Net::QUIC
Net::QUIC uses Alien::ngtcp2 to provide its native dependencies. A normal
installation does not require you to find or configure ngtcp2 yourself.
The event-loop modules used in examples/ are optional. Net::QUIC itself
does not require Linux::Event, AnyEvent, IO::Async, Mojolicious, or EV.
Start here
Most application code only needs three classes:
Net::QUIC::Driver
|
+-- Net::QUIC::Connection
|
+-- Net::QUIC::Stream
Use:
Net::QUIC::Driverto connect Net::QUIC to UDP and a timerNet::QUIC::Connectionto represent one QUIC connectionNet::QUIC::Streamto send and receive application bytes
There is also a lower-level Net::QUIC::Endpoint. Most applications should
use Driver instead.
What application code looks like
Once a connection is ready, application code is simple.
Open a stream:
my $stream = $connection->open_bidi_stream;
if ($stream) {
$stream->send("hello\n");
$stream->finish;
}
Accept streams opened by the peer:
while (my $stream = $connection->next_stream) {
while (defined(my $bytes = $stream->next_data)) {
handle_bytes($bytes);
}
}
Close the connection:
$connection->close;
The UDP socket and QUIC timer normally stay in your event-loop adapter rather than in the application protocol code.
QUIC streams are byte streams
A QUIC stream is an ordered sequence of bytes.
It is not a sequence of messages.
This:
$stream->send("one");
$stream->send("two");
does not guarantee that the peer receives exactly two next_data results.
If your application needs messages, add its own framing. For example, it could use newline-delimited messages, fixed-size records, or a length prefix.
QUIC gives each stream its own ordering and flow control. A blocked or lost packet on one stream does not turn all other streams into one shared byte stream.
Bidirectional and unidirectional streams
A bidirectional stream allows both endpoints to send:
my $stream = $connection->open_bidi_stream;
A unidirectional stream allows only the endpoint that created it to send:
my $stream = $connection->open_uni_stream;
A stream can also tell you what this endpoint is allowed to do:
$stream->can_send;
$stream->can_receive;
Waiting for the connection
A new QUIC connection performs a TLS handshake before normal application work begins.
Check:
if ($connection->ready) {
...
}
A server can receive a new Connection object before its handshake has completed. This is normal.
A client
A client Driver needs:
local- the packed local UDP socket addresspeer- the packed server UDP addressalpn- the name of the application protocol carried over QUICserver_name- the name expected in the server certificatesend- a callback that transmits one UDP datagramset_timeout- a callback that replaces the QUIC timer
For example:
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) = @_;
send_udp($datagram);
return 1;
},
set_timeout => sub {
my ($seconds) = @_;
replace_timer($seconds);
},
);
my $connection = $driver->connection;
$driver->start;
start tells Driver that the UDP transport is ready. The client can then
produce its first QUIC packet.
What is ALPN?
ALPN is simply a short protocol name agreed on by the client and server.
For a custom protocol you might use:
alpn => 'my-protocol'
It prevents two unrelated protocols from accidentally using the same QUIC connection.
What is server_name?
server_name is the DNS name or IP address that the server certificate must
represent.
For example, the UDP peer can be a numeric address:
192.0.2.20:4433
while certificate verification uses:
server_name => 'service.example.com'
A server
A server Driver uses the same UDP/timer 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) = @_;
send_udp($datagram);
return 1;
},
set_timeout => sub {
my ($seconds) = @_;
replace_timer($seconds);
},
);
$driver->start;
Feed received UDP packets into Driver:
$driver->receive($bytes, $local, $peer);
Pull newly created connections with:
while (my $connection = $driver->next_connection) {
...
}
One server Driver can manage many QUIC connections on one UDP socket.
The event-loop contract
Net::QUIC deliberately does not choose an event loop.
The event loop owns:
- UDP I/O
- one replaceable one-shot timer
Driver needs two callbacks from the adapter:
send => sub {
my ($datagram) = @_;
# Send one complete UDP datagram.
#
# Return true if another datagram can be accepted immediately.
# Return false if this datagram was accepted but output is now blocked.
},
set_timeout => sub {
my ($seconds) = @_;
# Replace the current one-shot QUIC timer.
# undef means cancel it.
},
The event loop reports four events to Driver:
$driver->start;
$driver->receive($bytes, $local, $peer);
$driver->timeout;
$driver->writable;
That is the normal Driver contract.
There is no application-visible QUIC pump loop.
Driver automatically drains pending QUIC output, updates the timer, pauses for
UDP backpressure, and resumes after writable.
Application operations such as:
$stream->send(...);
$stream->finish;
$stream->reset(...);
$connection->close;
also notify Driver automatically when transport work is needed.
UDP packet wrapper: Net::QUIC::Datagram
The Driver send callback receives a Net::QUIC::Datagram.
This class is an outbound UDP packet wrapper for the event-loop adapter. It is not an RFC 9221 application DATAGRAM carried inside QUIC.
Useful fields are:
$datagram->data; # complete UDP payload
$datagram->local; # packed local source address
$datagram->peer; # packed destination address
$datagram->ecn; # ECN codepoint for the IP header
Do not split data. It is one complete UDP datagram.
For the common case of a socket bound to one concrete local address, the socket already supplies the correct source address.
The local value becomes especially important for wildcard sockets and
connection migration.
Local addresses
QUIC needs to know the actual network path used by each packet.
The local value passed to Net::QUIC must therefore be a concrete IPv4 or
IPv6 address.
These are wildcard bind addresses, not concrete QUIC paths:
0.0.0.0
::
A client normally avoids this problem by connecting its UDP socket and using
getsockname after the kernel chooses a local address.
A server can still bind to a wildcard address, but its adapter must discover the actual destination address of each received packet and pass that concrete address to:
$driver->receive($bytes, $local, $peer);
On Linux this is commonly done with packet-info ancillary data and
recvmsg / sendmsg.
If an event system cannot report the actual destination address, bind the QUIC socket to one concrete local address instead.
UDP backpressure
A UDP send can temporarily be unable to accept another packet.
The Driver send callback uses its return value to report that condition:
- true - another datagram can be accepted immediately
- false - this datagram was accepted, but stop sending more for now
When output becomes writable again, call:
$driver->writable;
Driver then continues where it stopped.
QUIC DATAGRAM (RFC 9221)
QUIC DATAGRAM carries unreliable, unordered application bytes inside a QUIC connection.
It is optional and directional. To advertise that this endpoint can receive DATAGRAM frames:
transport => {
max_datagram_frame_size => 65535,
}
The default is zero, so existing applications do not enable DATAGRAM implicitly.
After negotiation:
if ($connection->can_send_datagram) {
my $accepted = $connection->send_datagram($bytes);
}
send_datagram uses one bounded pending slot. It returns false if that slot
is already occupied rather than building an unbounded queue. A true result
means Net::QUIC accepted the bytes for QUIC transmission; it does not mean the
peer received them. Lost DATAGRAMs are not retransmitted.
Pull received payloads with:
while (defined(my $bytes = $connection->next_received_datagram)) {
...
}
or register:
$connection->on_datagram(sub {
my ($connection, $bytes, $early_data) = @_;
...
});
The receive fallback queue is bounded. Excess unreliable DATAGRAMs are dropped
and can be observed with datagram_receive_drops.
For the current safe payload ceiling:
my $bytes = $connection->max_datagram_payload_size;
That value reflects both the peer's DATAGRAM limit and the current QUIC path capacity. Higher protocols must subtract their own framing overhead.
A returning client with saved early_data state can use QUIC DATAGRAM in
0-RTT when the remembered peer transport parameters permit it. As with all
0-RTT, the application must treat the operation as replayable.
This is raw RFC 9221 transport only. HTTP/3 DATAGRAM flow identifiers, SETTINGS_H3_DATAGRAM, Capsules, WebTransport, and other application-protocol semantics belong above Net::QUIC.
TLS and certificate verification
QUIC always uses TLS 1.3.
Clients verify server certificates by default.
server_name is the name checked against the certificate:
server_name => 'example.com'
OpenSSL's normal 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 certificate verification" switch.
Server certificate and key files are loaded when the server is created and shared by connections accepted by that server.
Closing streams
A normal stream finish is:
$stream->finish;
This means "I am done sending after the bytes already queued."
On a bidirectional stream, the peer can still send data back.
Abort this endpoint's send side:
$stream->reset($application_error_code);
Stop receiving and ask the peer to stop its send side:
$stream->stop_sending($application_error_code);
The send and receive directions are independent.
For normal code, finish is usually what you want. reset and
stop_sending are abrupt error/abort operations.
Stream limits
QUIC limits how many streams can be open at once.
Therefore:
my $stream = $connection->open_bidi_stream;
can return undef.
That does not mean the connection failed. It means the peer has not currently given this endpoint permission to open another stream.
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;
...
});
Advanced protocol-engine integration
Ordinary applications do not need these APIs.
A protocol engine that needs tighter control over receive consumption, acknowledgement progress, Stream wake-ups, or transmit memory can use:
Connection:
on_stream_activity
next_active_stream_id
send_buffer_limit
send_buffered_bytes
Stream:
next_data_chunk
consume
acked_offset
send_some
send_buffered_bytes
The ordinary send, finish, and next_data API remains unchanged.
These are transport primitives only. Net::QUIC does not add HTTP/3 or other application-protocol semantics.
See the Net::QUIC::Connection and Net::QUIC::Stream POD for the exact
contracts.
Closing a connection
Start a normal application close with:
$connection->close;
or:
$connection->close($application_error_code);
QUIC does not destroy the connection immediately. It has a short closing/draining period so late packets can still be handled correctly.
$connection->closed becomes true when the connection no longer needs UDP or
timer service.
Connection errors
Remote errors and normal close conditions are stored on the Connection rather than thrown as ordinary Perl exceptions.
Inspect them with:
my $info = $connection->close_info;
For example:
{
type => 'application',
initiator => 'peer',
code => 0,
}
Possible type values include:
application
transport
tls
certificate
handshake
idle
drop
Programming mistakes, invalid local configuration, allocation failures, and internal failures still throw Perl exceptions.
QUIC v1 and v2
Net::QUIC supports QUIC v1 and QUIC v2.
Clients use v1 by default:
version => 1
To start directly with v2:
version => 2
A server can prefer v2:
preferred_version => 2
while still accepting compatible v1 clients.
Normally you do not need to care which version was used.
For diagnostics:
$connection->client_chosen_version;
$connection->version;
Session resumption
A completed client connection can receive an opaque TLS session ticket:
my $ticket = $connection->session_ticket;
A later connection can offer it:
session_ticket => $ticket
After the handshake:
if ($connection->resumed) {
...
}
reports whether TLS actually resumed the old session.
Treat the ticket as opaque bytes. Net::QUIC remembers the QUIC version inside the opaque value.
If the ticket is expired or no longer valid, the connection falls back to a normal full handshake.
0-RTT / early data
0-RTT lets a returning client send some application data before the new handshake has completed.
It is optional because early data can be replayed by the network.
Only use 0-RTT for operations that are safe to repeat.
Save the opaque state:
my $state = $connection->early_data_state;
Use it on a later client:
early_data => $state
The server must also allow it:
accept_early_data => 1
Check what happened:
$connection->early_data_status;
Possible values are:
none
pending
accepted
rejected
If early data is rejected, the ordinary handshake can still succeed. Open new streams after the connection becomes ready and resend only operations that are safe to repeat.
On a server, a received stream can be checked with:
$stream->early_data;
Retry and NEW_TOKEN
A server can require clients to prove that they can receive packets at their source address before it allocates full connection state:
validate_address => 1
A new client may then receive a QUIC Retry packet and repeat its Initial.
After a validated connection succeeds, the server can give the client a NEW_TOKEN.
The client exposes that opaque token as:
my $token = $connection->address_token;
A later connection can reuse it:
address_token => $token
A valid token can avoid another Retry round trip.
Most applications can simply cache this opaque value alongside the session ticket if they want faster returning connections.
Network migration
QUIC can keep a connection alive when the client's local network path changes.
For example, an application can move an established connection to another local address:
$connection->migrate($new_packed_local_address);
Net::QUIC tests the new path before switching to it.
Check progress with:
$connection->path_validation_status;
Possible values are:
none
validating
succeeded
failed
aborted
If validation fails, the old working path remains active.
The current path is available from:
my $path = $connection->path;
Preferred server address
A server can tell the client that another server address is preferred:
preferred_address => $packed_server_address
The client tests that path before switching.
The UDP adapter must actually be able to send and receive on the advertised address.
PMTU discovery
Different network paths can safely carry different UDP packet sizes.
Net::QUIC automatically discovers a useful packet size for the current path.
For diagnostics:
my $bytes = $connection->path_max_udp_payload_size;
A new path starts at QUIC's safe 1200-byte baseline. Discovery can raise that value when larger packets work.
Most applications do not need to manage this themselves.
ECN
ECN is an IP feature that can report congestion without requiring a packet to be dropped.
ECN support is optional at the adapter boundary.
An ECN-aware adapter can pass the two-bit codepoint from the received IP packet:
$driver->receive($bytes, $local, $peer, $ecn);
For outgoing traffic it applies:
$datagram->ecn;
to the IP header.
The wire values are:
0 Not-ECT
1 ECT(1)
2 ECT(0)
3 CE
If the adapter does not provide ECN metadata, the normal three-argument
receive form remains valid.
Net::QUIC tests whether ECN works correctly on the path and automatically stops using it when necessary.
Lower-level Endpoint API
Net::QUIC::Endpoint is the engine underneath Driver.
It exposes:
receive_datagram
next_datagram
timeout_after
handle_timeout
Use Endpoint directly only when you want to own output draining, backpressure handling, and timer replacement yourself.
Most event-loop integrations are simpler with Driver.
Examples
The examples/ directory contains complete integrations for:
- Linux::Event
- AnyEvent
- IO::Async
- IO::Async with Future::AsyncAwait
- Mojo::IOLoop
- EV
- a small IO::Select echo server
All client examples implement the same Driver contract.
See examples/README.md for commands and notes.
What is not part of Net::QUIC
Net::QUIC is the base QUIC transport.
It does not define:
- HTTP/3
- an RPC protocol
- an application message format
- a web framework
Raw RFC 9221 QUIC DATAGRAM transport is supported, but HTTP/3 DATAGRAM, Capsules, WebTransport, and other application-protocol meanings are not defined by Net::QUIC.
qlog and advanced congestion-control configuration remain separate tooling or advanced configuration work.
Native implementation
Net::QUIC uses ngtcp2 for the native QUIC transport and Picotls/OpenSSL for QUIC TLS.
Most application code does not need to know those APIs.
The important public boundary remains:
event loop / UDP
|
v
Net::QUIC::Driver
|
v
Net::QUIC::Connection
|
v
Net::QUIC::Stream
License
MIT