A Rust library for reverse-initiated, multiplexed streams over QUIC.
quic-reverse helps with the "reverse connection" problem: allowing services behind NAT or firewalls to accept incoming connections without exposing a public port. The library handles connection negotiation, stream lifecycle management, and multiplexing while staying out of authentication, certificate management, and application-level protocols.
quic-reverse is useful whenever a service behind NAT or a firewall needs to accept incoming connections but isn't explicitly exposed to do so. Common use cases include:
- Remote access tools - SSH, RDP, or VNC tunneling through restrictive networks
- IoT device management - Push commands to devices that can only make outbound connections
- Development tunnels - Expose localhost services to the internet (similar to ngrok)
- Edge-to-cloud connectivity - Let edge nodes receive work from a central orchestrator
- Multiplexed service proxies - Route multiple logical services over a single QUIC connection
- Connection negotiation and feature discovery
- Stream lifecycle (open requests, responses, graceful close)
- Logical stream multiplexing with service-based routing
- Keep-alive and timeout management
- The QUIC connection (quic-reverse abstracts over Quinn; you control TLS and certificates)
- Service handlers for incoming stream requests
- Application-level framing for your data streams
# Run the echo server example
cargo run --example echo-server -p quic-reverse
# In another terminal, run the echo client
cargo run --example echo-client -p quic-reverse -- echo/uppercase "Hello World"
# Output: HELLO WORLDSee Examples for more details and crates/quic-reverse/examples/ for full source code.
Add quic-reverse to your Cargo.toml:
[dependencies]
quic-reverse = "0.1"The library uses Quinn as the default QUIC implementation. If you need a different transport, disable default features:
[dependencies]
quic-reverse = { version = "0.1", default-features = false }The server accepts incoming QUIC connections and handles stream open requests from clients.
use quic_reverse::{Config, ControlEvent, Role, Session};
use quic_reverse_transport::QuinnConnection;
// Wrap your Quinn connection
let quinn_conn = QuinnConnection::new(connection);
let config = Config::new().with_agent("my-server/1.0");
// Create and start the session
let session = Session::new(quinn_conn, Role::Server, config);
let mut handle = session.start().await?;
// Process control messages
loop {
match handle.process_message().await? {
Some(ControlEvent::OpenRequest { request_id, service, .. }) => {
// Accept or reject based on service name
if service.as_str() == "ssh" {
handle.accept_open(request_id, stream_id).await?;
// Open a data stream back to the client
let (send, recv) = connection.open_bi().await?;
// Handle the stream...
} else {
handle.reject_open(request_id, RejectCode::UnsupportedService, None).await?;
}
}
Some(ControlEvent::CloseReceived { .. }) => break,
None => break,
_ => {}
}
}The client initiates connections and requests streams to specific services.
use quic_reverse::{Config, Metadata, Role, Session};
use quic_reverse_transport::QuinnConnection;
let quinn_conn = QuinnConnection::new(connection);
let config = Config::new().with_agent("my-client/1.0");
let session = Session::new(quinn_conn, Role::Client, config);
let mut handle = session.start().await?;
// Request a stream to a service
let (mut send, mut recv) = handle.open("ssh", Metadata::Empty).await?;
// Use the stream for bidirectional communication
send.write_all(b"hello").await?;quic-reverse is organized into three crates:
| Crate | Purpose |
|---|---|
quic-reverse |
Public API: Session, Config, Error types |
quic-reverse-control |
Protocol messages, framing, serialization |
quic-reverse-transport |
Transport traits, Quinn adapter, mock transport |
The protocol uses a dedicated control stream for signaling, separate from data streams:
Client Server
│ │
│──── Hello ─────────────────────► │
│◄─── Hello ─────────────────────── │
│──── HelloAck ──────────────────► │
│◄─── HelloAck ──────────────────── │
│ │
│ [Session Ready] │
│ │
│──── OpenRequest ───────────────► │
│◄─── OpenResponse (accept) ─────── │
│◄════ Data Stream ════════════════►│
│ │
See ARCHITECTURE.md for design rationale and PROTOCOL.md for wire format details.
quic-reverse uses an adapter-based design via the QuicAdapter trait. While Quinn is the default (and covers the vast majority of Rust QUIC usage), the trait allows implementing support for other QUIC libraries like quiche or s2n-quic if your use case requires it.
Sessions are configured using Config:
use quic_reverse::Config;
use std::time::Duration;
let config = Config::new()
.with_agent("my-app/1.0")
.with_open_timeout(Duration::from_secs(30))
.with_ping_timeout(Duration::from_secs(10))
.with_max_inflight_opens(16)
.with_max_concurrent_streams(256);| Setting | Default | Description |
|---|---|---|
open_timeout |
30s | How long to wait for an open request response |
stream_bind_timeout |
10s | How long to wait for the data stream after acceptance |
negotiation_timeout |
30s | How long to wait for the handshake to complete |
ping_timeout |
10s | How long to wait for a pong response |
The repository includes working examples demonstrating the library:
# Basic example using mock transport
cargo run --example basic -p quic-reverse
# Multi-service echo server (uses real QUIC)
cargo run --example echo-server -p quic-reverse
# Echo client
cargo run --example echo-client -p quic-reverse echo/uppercase "Hello World"The echo server demonstrates multiplexing with three services:
echo/plain- Returns input unchangedecho/uppercase- Returns input in uppercaseecho/reverse- Returns input reversed
The library provides structured error types for different failure modes:
use quic_reverse::{Error, TimeoutKind};
match handle.open("service", Metadata::Empty).await {
Ok((send, recv)) => { /* use streams */ }
Err(Error::Timeout(TimeoutKind::OpenRequest)) => { /* request timed out */ }
Err(Error::StreamRejected { code, reason }) => { /* server rejected */ }
Err(Error::SessionClosed) => { /* session ended */ }
Err(e) => { /* other error */ }
}quic-reverse uses the tracing crate for structured logging. Enable tracing in your application:
tracing_subscriber::fmt()
.with_max_level(tracing::Level::DEBUG)
.init();Log levels follow this convention:
trace- Low-level I/O operationsdebug- Protocol operations and state changesinfo- Session lifecycle eventswarn/error- Problems requiring attention
- ARCHITECTURE.md - Design rationale and architectural decisions
- PROTOCOL.md - Wire protocol specification
- Examples - Example documentation
Licensed under Apache-2.0.