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 via finalize callbacks.
  • Fiber & Event Loop Native: Integrates with Crystal's non-blocking UDPSocket loop and schedules timers natively using timeout_as_millis and non-blocking sleep.
  • Zero-Copy & Alignment Safe: Utilizes direct pointer offset arithmetic for SockAddrStorage mapping 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 (SSLKEYLOGFILE capability) and qlog trace logging.

Installation

  1. Add the dependency to your shard.yml:
dependencies:
  cruic:
    github: eltony81/cruic
  1. 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

  1. To run the core QUIC benchmarks:
    crystal run examples/benchmark_perf.cr
    
  2. To run the HTTP/3 benchmarks:
    bash run_h3_benchmark.sh
    
Repository

cruic

Owner
Statistic
  • 0
  • 0
  • 0
  • 0
  • 0
  • about 4 hours ago
  • June 12, 2026
License

MIT License

Links
Synced at

Sat, 13 Jun 2026 08:27:51 GMT

Languages