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:
- QUIC packet and connection state
- TLS 1.3
- certificate verification
- stream flow control
- retransmission and acknowledgement state
- connection IDs
- timers required by QUIC
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:
- one UDP socket
- one replaceable one-shot timer
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:
localis this UDP socket's packed local address.peeris the server's packed UDP address.alpnnames the application protocol carried over QUIC. Client and server must use a compatible ALPN value.server_nameis the DNS name or IP address that the server certificate is expected to represent. It is used for certificate verification.
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.