cruic v0.1.0
Cruic
cruic is a safe, fast, and modern Crystal shard wrapping Cloudflare's quiche library. It implements the IETF QUIC transport protocol and is fully integrated with Crystal's non-blocking fiber runtime.
Key Features
- RAII Memory Management: Strict encapsulation of raw C pointers (
Config,Conn) ensuring resource deallocation viafinalizecallbacks. - Fiber & Event Loop Native: Integrates with Crystal's non-blocking
UDPSocketloop and schedules timers natively usingtimeout_as_millisand non-blockingsleep. - Zero-Copy & Alignment Safe: Utilizes direct pointer offset arithmetic for
SockAddrStoragemapping to avoid stack copying memory issues and LLVM struct alignment corruption. - Resumption & 0-RTT: Supports QUIC session ticket serialization and deserialization for faster handshakes.
- Logging Traces: Out-of-the-box support for SSL key logging (
SSLKEYLOGFILEcapability) andqlogtrace logging.
Installation
- Add the dependency to your
shard.yml:
dependencies:
cruic:
github: eltony81/cruic
- Run
shards install
Make sure libquiche (version 0.29.0) is installed on your system (e.g. via pacman -S quiche on Arch Linux / Manjaro).
Usage
1. Minimal Server Example
Save this to server.cr and run it:
require "cruic"
require "socket"
# Initialize configuration
config = Quic::Config.new
config.set_application_protos(["hq-29"])
config.load_cert_chain_from_pem_file("spec/fixtures/cert.pem")
config.load_priv_key_from_pem_file("spec/fixtures/key.pem")
# Configure stream capacities and maximum data flow
config.set_initial_max_streams_bidi(100_u64)
config.set_initial_max_stream_data_bidi_local(1048576_u64)
config.set_initial_max_stream_data_bidi_remote(1048576_u64)
config.set_initial_max_data(1048576_u64)
# Bind non-blocking UDP socket
socket = UDPSocket.new
socket.bind("127.0.0.1", 4433)
socket.blocking = false
puts "QUIC server listening on 127.0.0.1:4433..."
handler = Quic::SocketHandler.new(socket)
handler.start_server_loop(config)
buffer = Bytes.new(65535)
loop do
handler.connections.values.uniq.each do |conn|
next if conn.closed?
# Listen on active readable streams
conn.readable_streams.each do |stream_id|
loop do
bytes_read, fin = conn.stream_recv(stream_id, buffer)
if bytes_read > 0
payload = String.new(buffer[0, bytes_read])
puts "Received client payload on stream #{stream_id}: #{payload.inspect}"
# Echo response back to the client
response = "ACK: #{payload}"
conn.stream_send(stream_id, response.to_slice, true)
handler.send_pending(conn)
end
break if bytes_read == 0 || fin
end
end
end
sleep 10.milliseconds
end
2. Minimal Client Example
Save this to client.cr and run it:
require "cruic"
require "socket"
config = Quic::Config.new
config.set_application_protos(["hq-29"])
config.verify_peer(false)
# Set initial limits
config.set_initial_max_streams_bidi(100_u64)
config.set_initial_max_stream_data_bidi_local(1048576_u64)
config.set_initial_max_stream_data_bidi_remote(1048576_u64)
config.set_initial_max_data(1048576_u64)
socket = UDPSocket.new
socket.blocking = false
socket.bind("0.0.0.0", 0) # Bind to ephemeral port
local_addr = socket.local_address
peer_addr = Socket::IPAddress.new("127.0.0.1", 4433)
scid = Bytes.new(16)
Random.new.random_bytes(scid)
conn = Quic::Connection.connect("localhost", scid, local_addr, peer_addr, config)
handler = Quic::SocketHandler.new(socket)
handler.register_connection(scid, conn)
handler.send_pending(conn) # Sends Initial Handshake Packet
request_sent = false
buffer = Bytes.new(65535)
# Event Loop
spawn do
while !conn.closed?
if conn.established? && !request_sent
# Connection is established, send payload on Stream 4
conn.stream_send(4_u64, "Hello QUIC!".to_slice, true)
handler.send_pending(conn)
request_sent = true
end
conn.readable_streams.each do |stream_id|
loop do
bytes_read, fin = conn.stream_recv(stream_id, buffer)
if bytes_read > 0
puts "Received server response: #{String.new(buffer[0, bytes_read]).inspect}"
end
break if bytes_read == 0 || fin
end
end
sleep 10.milliseconds
end
end
# Listen for incoming packets
loop do
begin
bytes_read, sender_addr = socket.receive(buffer)
if bytes_read > 0
conn.recv(buffer[0, bytes_read], sender_addr, socket.local_address)
handler.send_pending(conn)
end
rescue ex : IO::Error
break
end
end
3. HTTP/3 Client/Server Example
Cruic supports HTTP/3 connection mapping, headers serialization, and event loop polling. To use HTTP/3, make sure you configure both bidirectional and unidirectional stream limits on your QUIC configuration (as unidirectional streams are required for control and QPACK channels).
# Set ALPN to "h3"
config.set_application_protos(["h3"])
# Configure unidirectional streams (required for H3)
config.set_initial_max_streams_uni(100_u64)
config.set_initial_max_stream_data_uni(1048576_u64)
# Once QUIC handshake is established, initialize the HTTP/3 connection:
h3_config = Quic::H3::Config.new
h3_client = Quic::H3::Connection.new(client_conn, h3_config)
h3_server = Quic::H3::Connection.new(server_conn, h3_config)
# Sending a request from Client:
req_headers = [
{":method", "GET"},
{":scheme", "https"},
{":authority", "localhost"},
{":path", "/h3-demo"},
{"user-agent", "CruicH3Client"}
]
stream_id = h3_client.send_request(client_conn, req_headers, false)
h3_client.send_body(client_conn, stream_id.to_u64, "Body data".to_slice, true)
# Server Event Polling loop:
status, ev = h3_server.poll(server_conn)
if ev && ev.event_type == 0 # HEADERS event
headers = ev.headers
# Process headers...
elsif ev && ev.event_type == 1 # DATA event
recv_buf = Bytes.new(1024)
body_len = h3_server.recv_body(server_conn, status.to_u64, recv_buf)
# Process body...
end
For a complete runnable example showing client-server interaction over local loopback, refer to examples/h3_client_server.cr.
4. Advanced Features (Priorities, Multi-Path, Settings, and Stats)
Cruic supports the advanced features of Quiche for HTTP/3 Extensible Priorities, Trailers, Multi-path connection paths, and H3 stats/settings:
Extensible Priorities & PRIORITY_UPDATE
# Parse priority fields (RFC 9218)
priority = Quic::H3::Priority.parse("u=2,i") # Urgency 2, incremental
# Client sends a PRIORITY_UPDATE frame for a request stream
h3_client.send_priority_update(quic_conn, stream_id, priority)
# Server retrieves the last priority update sent for a request stream
priority_field = h3_server.take_last_priority_update(stream_id) # => "u=2,i"
# Respond with a custom priority
h3_server.send_response_with_priority(quic_conn, stream_id, response_headers, priority, fin)
Trailing Headers (Trailers)
# Send trailing headers (additional headers with is_trailer=true)
h3_server.send_additional_headers(quic_conn, stream_id, [{"grpc-status", "0"}], is_trailer: true, fin: true)
Multi-Path & Path Validation
# Retrieve paths list matching a local socket address
paths = quic_conn.paths(local_addr) # => Array(Socket::Address)
# Check if the path is validated
is_validated = quic_conn.path_validated?(local_addr, peer_addr) # => Bool
H3 Settings & Statistics
# Retrieve peer's HTTP/3 settings hash
settings = h3_client.settings # => Hash(UInt64, UInt64)
# Retrieve HTTP/3 Connection statistics
stats = h3_client.stats
puts stats.qpack_encoder_stream_recv_bytes
puts stats.qpack_decoder_stream_recv_bytes
For a complete runnable example showing all advanced features, refer to examples/priority_and_stats.cr.
5. Advanced Tuning & Connection Diagnostics
Cruic provides advanced APIs for dialing network settings, path probing, stream priority/shutdown, peer certificate extraction, datagram queues, and connection diagnostics.
Advanced Config Parameters
config.ack_delay_exponent = 3_u64
config.active_connection_id_limit = 4_u64
config.disable_active_migration = true
config.disable_dcid_reuse = true
config.enable_cubic_idle_restart_fix = true
config.initial_congestion_window_packets = 12_u64
config.max_ack_delay = 30_u64
config.max_amplification_factor = 3
config.max_pacing_rate = 1000000_u64
config.pmtud_max_probes = 5_u64
Global Version Check
Quic.version_supported?(1_u32) # => Bool
Connection-level Stats & Diagnostics
# Raw connection stats
c_stats = quic_conn.stats # => LibQuiche::Stats
puts c_stats.sent
puts c_stats.recv
puts c_stats.lost
puts c_stats.sent_bytes
# Specific network path statistics
p_stats = quic_conn.path_stats(0_u64) # => LibQuiche::PathStats
puts p_stats.active
puts p_stats.rtt
puts p_stats.cwnd
# Connection trace ID
trace_id = quic_conn.trace_id # => String
# Draining check
quic_conn.draining? # => Bool
Transport-level Stream Controls
# Stream transport-level priority (urgency, incremental)
quic_conn.stream_priority(stream_id, urgency: 2_u8, incremental: true)
# Stream next readable / next writable IDs
writable = quic_conn.stream_writable_next # => Int64
readable = quic_conn.stream_readable_next # => Int64
# Stream shutdown direction (direction, err)
quic_conn.stream_shutdown(stream_id, LibQuiche::Shutdown::Write, err_code)
Datagram Queues Tuning & Purge
# Inspect datagram queue bytes
front_len = quic_conn.dgram_recv_front_len # => Int64
recv_bytes = quic_conn.dgram_recv_queue_byte_size # => Int64
send_bytes = quic_conn.dgram_send_queue_byte_size # => Int64
# Purge outgoing datagram queue matching a Crystal block predicate
quic_conn.dgram_purge_outgoing do |payload|
payload.size > 1000 # Purge datagrams larger than 1000 bytes
end
Path Probing, Migration & Quantum
# Send path validation probe
probe_seq = quic_conn.probe_path(local_addr, peer_addr) # => UInt64
# Send quantum size
quantum = quic_conn.send_quantum_on_path(local_addr, peer_addr) # => UInt64
# Peer leaf certificate extraction
cert = quic_conn.peer_cert # => Bytes? (DER-encoded)
# Local & Peer error details
local_err = quic_conn.local_error # => Tuple(is_app : Bool, error_code : UInt64, reason : String)?
peer_err = quic_conn.peer_error # => Tuple(is_app : Bool, error_code : UInt64, reason : String)?
For a complete advanced demonstration, refer to examples/advanced_features_demo.cr.
6. New in v0.2.0 — Extended API
Connection ID Management
# All active source Connection IDs for this connection
scids = quic_conn.source_ids # => Array(Bytes)
# Number of unused source CID slots
left = quic_conn.scids_left # => UInt64
# Number of currently active source CIDs
active = quic_conn.active_scids # => UInt64
# Number of available destination CIDs (advertised by peer)
dcids = quic_conn.available_dcids # => UInt64
# Pop the next retired source CID (returns nil when none remain)
retired = quic_conn.retired_scid_next # => Bytes?
# Actively retire a destination CID by its sequence number
quic_conn.retire_dcid(seq_num)
Logging via File Descriptor
# Direct TLS key-log to an open file descriptor (useful for Wireshark)
fd = File.open("/tmp/keys.log", "w").fd
quic_conn.set_keylog_fd(fd) # => Bool
# Direct qlog events to an open file descriptor
quic_conn.set_qlog_fd(fd, "My Title", "Description") # => Bool
Multipath Send APIs
# Transmit on a specific local→peer path; returns bytes written
send_info = LibQuiche::SendInfo.new
# Note: the path must first be probed with quic_conn.probe_path(local, peer)
bytes = quic_conn.send_on_path(buf, local_addr, peer_addr, pointerof(send_info))
# Elicit an ACK on a specific path (probing keepalive)
quic_conn.send_ack_eliciting_on_path(local_addr, peer_addr)
# Maximum datagram payload quiche will generate for this path
quantum = quic_conn.send_quantum_on_path(local_addr, peer_addr) # => UInt64
Remote Stream Limits
# How many new bidirectional streams the peer allows us to open
bidi_left = quic_conn.peer_streams_left_bidi # => UInt64
# How many new unidirectional streams the peer allows us to open
uni_left = quic_conn.peer_streams_left_uni # => UInt64
External TLS Context
# Create a QUIC connection from an externally managed SSL* object
conn = Quic::Connection.new_with_tls(
scid, odcid, local_addr, peer_addr,
config, ssl_ptr, is_server: true
)
# Same but also supplies the client-chosen DCID (server-side retry flows)
conn = Quic::Connection.new_with_tls_and_client_dcid(
scid, odcid, local_addr, peer_addr,
config, ssl_ptr, is_server: true,
client_dcid: original_dcid
)
Varint Helpers
# Encode a QUIC variable-length integer into a buffer
buf = Bytes.new(8)
written = Quic.put_varint(buf, 12345_u64) # => Int32 (bytes written)
# Decode a QUIC variable-length integer from a buffer
value, consumed = Quic.get_varint(buf) # => {UInt64, Int32}
Extended Config Options (v0.2.0)
# Congestion control algorithm
config.set_cc_algorithm_name("bbr") # "cubic", "bbr", "bbr2", "reno"
# Initial and maximum flow-control windows
config.set_initial_max_data(10_000_000_u64)
config.set_initial_max_stream_data_bidi_local(1_000_000_u64)
config.set_initial_max_stream_data_bidi_remote(1_000_000_u64)
config.set_initial_max_stream_data_uni(500_000_u64)
# Maximum connection-level receive window
config.set_max_connection_window(25_165_824_u64)
# Maximum stream-level receive window
config.set_max_stream_window(16_777_216_u64)
# Stateless Reset Token (16-byte token for connection migration)
token = Bytes.new(16, 0x42_u8)
config.set_stateless_reset_token(token)
PathEvent Address Extraction
# Path events now include typed local/peer address accessors
while ev = quic_conn.next_path_event
case ev.kind
when Quic::PathEvent::Kind::New
local = ev.local_addr # => Socket::IPAddress?
peer = ev.peer_addr # => Socket::IPAddress?
puts "New path: #{local} → #{peer}"
when Quic::PathEvent::Kind::Validated
puts "Path validated!"
when Quic::PathEvent::Kind::FailedValidation
puts "Path validation failed"
when Quic::PathEvent::Kind::Closed
puts "Path closed"
end
end
---
### 7. New in v0.2.1 — Event Typing & Linux UDP Offloads
#### HTTP/3 Typed Events
```crystal
status, ev = h3_server.poll(server_conn)
if ev
# Complete event type mapping enum
case ev.type
when Quic::H3::Event::Type::Headers
puts "Received headers: #{ev.headers}"
when Quic::H3::Event::Type::Data
puts "Received data chunk"
when Quic::H3::Event::Type::Finished
puts "Stream finished!"
when Quic::H3::Event::Type::Goaway
puts "Peer closed connection with GoAway"
when Quic::H3::Event::Type::Reset
puts "Stream reset by peer"
when Quic::H3::Event::Type::PriorityUpdate
puts "Stream priority updated"
end
end
High-Performance UDP Offloads (GRO & GSO)
To achieve production-grade HTTP/3 throughput with minimal CPU overhead under heavy networking load, SocketHandler supports Linux Generic Receive Offload (GRO) and Generic Segmentation Offload (GSO):
# Enable GRO and GSO helpers on UDPSocket before starting event loop
sock = UDPSocket.new
sock.bind("0.0.0.0", 4433)
handler = Quic::SocketHandler.new(sock)
handler.enable_gro # => Returns true on supported Linux kernels (reduces context switches on read)
handler.enable_gso # => Returns true on supported Linux kernels (MSS segmenting offloaded to kernel)
# Start server loop with offloads active
handler.start_server_loop(config)
HTTP/3 Production Helpers (Alt-Svc & Graceful Shutdown)
To transition client and server applications smoothly to HTTP/3 production usage, cruic provides helpers for protocol negotiation and graceful shutdowns:
Protocol Negotiation (Alt-Svc Header)
# Generate Alt-Svc header value telling HTTP/1.1 and HTTP/2 clients
# that an HTTP/3 server is available on port 443
alt_svc = Quic::H3::AltSvc.format(443, "h3", 86400)
# => 'h3=":443"; ma=86400'
Graceful Shutdown Coordinator
# Set up a graceful shutdown coordinator for a connection
shutdown = Quic::H3::GracefulShutdown.new(h3_conn)
# Track active streams
shutdown.register_stream(stream_id)
# Begin shutdown sequence (sends HTTP/3 GOAWAY frame to peer)
shutdown.start(quic_conn)
# Release stream once its response/request cycle completes
shutdown.release_stream(stream_id)
# Check if connection draining has completed safely
if shutdown.complete?
quic_conn.close(app: true, err: 0, reason: "Graceful shutdown complete")
end
Production Ready HTTP/3 Server (Quic::H3::Server) & IO wrapper
cruic provides a complete, high-performance, non-blocking asynchronous HTTP/3 Engine built on Crystal Fibers:
Asynchronous HTTP/3 Server
The Quic::H3::Server features thread/fiber-safe reads/writes using a dedicated writing Fiber, stateless cryptographic token verification, and automated client validation (Stateless Retry to protect against DoS amplification attacks).
require "cruic"
# Instantiate server socket & configs
socket = UDPSocket.new
socket.bind("0.0.0.0", 4433)
config = Quic::Config.new
config.load_cert_chain_from_pem_file("cert.pem")
config.load_priv_key_from_pem_file("key.pem")
h3_config = Quic::H3::Config.new
# Inherit and override server handler
class MyServer < Quic::H3::Server
def handle_request(stream : Quic::H3::StreamIO, headers : Array(Tuple(String, String)))
puts "Request headers: #{headers}"
# StreamIO behaves like any normal Crystal IO (non-blocking yield)
stream.puts "HTTP/3 Response Body"
stream.close
end
end
server = MyServer.new(socket, config, h3_config)
server.start # Spawns read/write loops asynchronously
StreamIO (Quic::H3::StreamIO)
HTTP/3 stream multiplexing wrapped into a standard Crystal IO class. It manages flow control (QUICHE_H3_ERR_STREAM_BLOCKED) internally by yielding execution to other fibers when blocked, preventing execution stalls:
# Read from request stream
buffer = Bytes.new(1024)
bytes_read = stream.read(buffer)
# Write response block
stream.write("Hello World".to_slice)
stream.flush
8. New in v0.2.2 — SNI, PMTU Egress & Dynamic SCIDs
Extracting Server Name Indication (SNI)
Read the server name requested by the client during TLS handshake:
if name = quic_conn.server_name
puts "Client SNI: #{name}"
end
Max Send UDP Payload Size (Path MTU)
Read the maximum allowed egress payload size for this connection's path to scale write buffers efficiently:
payload_limit = quic_conn.max_send_udp_payload_size # => UInt64 (e.g. 1200)
Dynamic Connection ID Advertising
Actively generate and advertise a new Source Connection ID with an associated stateless reset token to the peer:
new_cid = Bytes.new(16)
Random.new.random_bytes(new_cid)
reset_token = Bytes.new(16)
Random.new.random_bytes(reset_token)
# seq represents the connection ID sequence allocated by quiche
seq = quic_conn.new_scid(new_cid, reset_token, retire_if_needed: true)
Development & Testing
Run the spec test suite:
export LIBRARY_PATH=/path/to/quiche/usr/lib
export LD_LIBRARY_PATH=/path/to/quiche/usr/lib
crystal spec
Performance Benchmarks
Cruic includes performance benchmarking tools comparing the Crystal wrapper directly against the raw compiled C FFI speed (compiled via GCC -O3 with no Crystal GC/Fiber overhead).
1. Local Handshake Rate & Latency
Measures the number of complete cryptographic handshakes (TLS 1.3) completed per second.
| Metric | C Native (Quiche) | Cruic (Crystal Wrapper) | Performance Ratio |
|---|---|---|---|
| Handshakes / Second | 611.37 H/s | 665.49 H/s | ~100% |
2. Stream Throughput (Bulk Transfer)
Measures the transfer speed of transmitting 50 MB of data in 16 KB chunks over a local loopback stream.
| Metric | C Native (Quiche) | Cruic (Crystal Wrapper) | Performance Ratio |
|---|---|---|---|
| Average Speed | 297.92 MB/s | 272.80 MB/s | ~91.5% |
3. HTTP/3 Setup Speed
Measures the speed of QUIC connection establishment + HTTP/3 settings negotiations.
| Metric | C Native (Quiche) | Cruic (Crystal Wrapper) | Performance Ratio |
|---|---|---|---|
| Setups / Second | 595.05 S/s | 500.08 S/s | ~84.0% |
4. HTTP/3 Request/Response Round-Trip Cycle Rate
Measures the sequential rate of complete HTTP/3 request-response cycles (sending request headers + 100-byte body from client, parsing on server, responding, and receiving on client).
| Metric | C Native (Quiche) | Cruic (Crystal Wrapper) | Performance Ratio |
|---|---|---|---|
| Cycles / Second | 68,405.25 C/s | 55,822.24 C/s | ~81.6% |
Running the Benchmarks
- To run the core QUIC benchmarks:
crystal run examples/benchmark_perf.cr - To run the HTTP/3 benchmarks:
bash run_h3_benchmark.sh
cruic
- 0
- 0
- 0
- 0
- 0
- about 4 hours ago
- June 12, 2026
MIT License
Sat, 13 Jun 2026 08:27:51 GMT