Skip to content

Commit

Permalink
add quinn,quic http3 lib
Browse files Browse the repository at this point in the history
  • Loading branch information
youngday committed Jun 6, 2024
1 parent dd7d3d9 commit 57f0360
Show file tree
Hide file tree
Showing 10 changed files with 1,204 additions and 5 deletions.
318 changes: 314 additions & 4 deletions Cargo.lock

Large diffs are not rendered by default.

55 changes: 55 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@ version = "0.4.0"
authors = ["youngday"]
edition = "2021"

[features]
default = ["log", "platform-verifier", "ring", "runtime-tokio", "rustls"]
# Records how long locks are held, and warns if they are held >= 1ms
lock_tracking = []
# Provides `ClientConfig::with_platform_verifier()` convenience method
platform-verifier = ["proto/platform-verifier"]
rustls = ["dep:rustls", "proto/rustls", "proto/ring"]
# Enables `Endpoint::client` and `Endpoint::server` conveniences
ring = ["proto/ring"]
runtime-tokio = ["tokio/time", "tokio/rt", "tokio/net"]
runtime-async-std = ["async-io", "async-std"]
runtime-smol = ["async-io", "smol"]

# Write logs via the `log` crate when no `tracing` subscriber exists
log = ["tracing/log", "proto/log", "udp/log"]

[dependencies]
log = "0.4"
log4rs = { version = "1.3.0", features = ["toml", "json_encoder"] }
Expand Down Expand Up @@ -66,6 +82,37 @@ tower-http = { version = "0.5.0", features = ["fs", "trace"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
#webrtc axum tokio-tungstenite -----------------


# quiche++++++++++++++++++++++++++++++++++++++++++++
async-io = { version = "2.0", optional = true }
async-std = { version = "1.11", optional = true }
# Enables futures::io::{AsyncRead, AsyncWrite} support for streams
futures-io = { version = "0.3.19", optional = true }
rustc-hash = "1.1"
pin-project-lite = "0.2"
quinn="0.11.1"
proto = { package = "quinn-proto",version = "0.11.2", default-features = false }
rustls = { version = "0.23", default-features = false, features = ["ring", "std"], optional = true }
smol = { version = "2", optional = true }
thiserror = "1.0.21"
udp = { package = "quinn-udp", version = "0.5", default-features = false }
#quiche----------------------------------------------
[dev-dependencies]
# quiche++++++++++++++++++++++++++++++++++++++++++++
anyhow = "1.0.22"
crc = "3"
bencher = "0.1.5"
directories-next = "2"
rand = "0.8"
rcgen = "0.13"
rustls-pemfile = "2"
clap = { version = "4", features = ["derive"] }
tokio = { version = "1.28.1", features = ["rt", "rt-multi-thread", "time", "macros", "sync"] }
tracing-subscriber = { version = "0.3.0", default-features = false, features = ["env-filter", "fmt", "ansi", "time", "local-time"] }
tracing-futures = { version = "0.2.0", default-features = false, features = ["std-future"] }
url = "2"
#quiche----------------------------------------------
[build-dependencies]
tonic-build = { version = "0.11", features = ["prost"] }

Expand Down Expand Up @@ -150,3 +197,11 @@ path = "examples/websocket_axum/server.rs"
[[example]]
name = "ws_client"
path = "examples/websocket_axum/client.rs"

[[example]]
name = "quic_client"
path = "examples/quic/client.rs"

[[example]]
name = "quic_server"
path = "examples/quic/server.rs"
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
|iceoryx2|dds|pubsub dds ipc for ros |new realtime(10us) ipc |
|tonic| |grpc |⚠️ proto genarate .rs files |
|axum websocket| |websocket | axum example ,tokio-tungstenite |
|quinn quic| |quic http3 | quic http3 client server |
## examples

|name|fun|note|
Expand All @@ -39,6 +40,7 @@
|discovery|iceoryx2 discovery| |
|grpc_client,grpc_server| |⚠️ proto genarate .rs files |
|ws_client,ws_server| | websocket |
|quic_server,quic_client|quic,http3|quic,http3|
## vscode build

https://code.visualstudio.com/docs/languages/rust
Expand Down Expand Up @@ -67,4 +69,12 @@ pub and sub ,test ok ,

but not c++ and python app,now are planning,waiting for release

⚠️
⚠️

### quiche

```sh
cargo run --example quic_server ./

cargo run --example quic_client https://localhost:4433/Cargo.toml
```
92 changes: 92 additions & 0 deletions examples/quic/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
## HTTP/0.9 File Serving Example

The `server` and `client` examples demonstrate fetching files using a HTTP-like toy protocol.

1. Server (`server.rs`)

The server listens for any client requesting a file.
If the file path is valid and allowed, it returns the contents.

Open up a terminal and execute:

```text
$ cargo run --example server ./
```

2. Client (`client.rs`)

The client requests a file and prints it to the console.
If the file is on the server, it will receive the response.

In a new terminal execute:

```test
$ cargo run --example client https://localhost:4433/Cargo.toml
```

where `Cargo.toml` is any file in the directory passed to the server.

**Result:**

The output will be the contents of this README.

**Troubleshooting:**

If the client times out with no activity on the server, try forcing the server to run on IPv4 by
running it with `cargo run --example server -- ./ --listen 127.0.0.1:4433`. The server listens on
IPv6 by default, `localhost` tends to resolve to IPv4, and support for accepting IPv4 packets on
IPv6 sockets varies between platforms.

If the client prints `failed to process request: failed reading file`, the request was processed
successfully but the path segment of the URL did not correspond to a file in the directory being
served.

## Minimal Example
The `connection.rs` example intends to use the smallest amount of code to make a simple QUIC connection.
The server issues it's own certificate and passes it to the client to trust.

```text
$ cargo run --example connection
```

This example will make a QUIC connection on localhost, and you should see output like:

```text
[client] connected: addr=127.0.0.1:5000
[server] connection accepted: addr=127.0.0.1:53712
```

## Insecure Connection Example

The `insecure_connection.rs` example demonstrates how to make a QUIC connection that ignores the server certificate.

```text
$ cargo run --example insecure_connection --features="rustls/dangerous_configuration"
```

## Single Socket Example

You can have multiple QUIC connections over a single UDP socket. This is especially
useful, if you are building a peer-to-peer system where you potentially need to communicate with
thousands of peers or if you have a
[hole punched](https://en.wikipedia.org/wiki/UDP_hole_punching) UDP socket.
Additionally, QUIC servers and clients can both operate on the same UDP socket.
This example demonstrates how to make multiple outgoing connections on a single UDP socket.

```text
$ cargo run --example single_socket
```

The expected output should be something like:

```text
[client] connected: addr=127.0.0.1:5000
[server] incoming connection: addr=127.0.0.1:48930
[client] connected: addr=127.0.0.1:5001
[client] connected: addr=127.0.0.1:5002
[server] incoming connection: addr=127.0.0.1:48930
[server] incoming connection: addr=127.0.0.1:48930
```

Notice how the server sees multiple incoming connections with different IDs coming from the same
endpoint.
169 changes: 169 additions & 0 deletions examples/quic/client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
//! This example demonstrates an HTTP client that requests files from a server.
//!
//! Checkout the `README.md` for guidance.
use std::{
fs,
io::{self, Write},
net::{SocketAddr, ToSocketAddrs},
path::PathBuf,
sync::Arc,
time::{Duration, Instant},
};

use anyhow::{anyhow, Result};
use clap::Parser;
use proto::crypto::rustls::QuicClientConfig;
use rustls::pki_types::CertificateDer;
use tracing::{error, info};
use url::Url;

mod common;

/// HTTP/0.9 over QUIC client
#[derive(Parser, Debug)]
#[clap(name = "client")]
struct Opt {
/// Perform NSS-compatible TLS key logging to the file specified in `SSLKEYLOGFILE`.
#[clap(long = "keylog")]
keylog: bool,

url: Url,

/// Override hostname used for certificate verification
#[clap(long = "host")]
host: Option<String>,

/// Custom certificate authority to trust, in DER format
#[clap(long = "ca")]
ca: Option<PathBuf>,

/// Simulate NAT rebinding after connecting
#[clap(long = "rebind")]
rebind: bool,

/// Address to bind on
#[clap(long = "bind", default_value = "[::]:0")]
bind: SocketAddr,
}

fn main() {
tracing::subscriber::set_global_default(
tracing_subscriber::FmtSubscriber::builder()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.finish(),
)
.unwrap();
let opt = Opt::parse();
let code = {
if let Err(e) = run(opt) {
eprintln!("ERROR: {e}");
1
} else {
0
}
};
::std::process::exit(code);
}

#[tokio::main]
async fn run(options: Opt) -> Result<()> {
let url = options.url;
let url_host = strip_ipv6_brackets(url.host_str().unwrap());
let remote = (url_host, url.port().unwrap_or(4433))
.to_socket_addrs()?
.next()
.ok_or_else(|| anyhow!("couldn't resolve to an address"))?;

let mut roots = rustls::RootCertStore::empty();
if let Some(ca_path) = options.ca {
roots.add(CertificateDer::from(fs::read(ca_path)?))?;
} else {
let dirs = directories_next::ProjectDirs::from("org", "quinn", "quinn-examples").unwrap();
match fs::read(dirs.data_local_dir().join("cert.der")) {
Ok(cert) => {
roots.add(CertificateDer::from(cert))?;
}
Err(ref e) if e.kind() == io::ErrorKind::NotFound => {
info!("local server certificate not found");
}
Err(e) => {
error!("failed to open local server certificate: {}", e);
}
}
}
let mut client_crypto = rustls::ClientConfig::builder()
.with_root_certificates(roots)
.with_no_client_auth();

client_crypto.alpn_protocols = common::ALPN_QUIC_HTTP.iter().map(|&x| x.into()).collect();
if options.keylog {
client_crypto.key_log = Arc::new(rustls::KeyLogFile::new());
}

let client_config =
quinn::ClientConfig::new(Arc::new(QuicClientConfig::try_from(client_crypto)?));
let mut endpoint = quinn::Endpoint::client(options.bind)?;
endpoint.set_default_client_config(client_config);

let request = format!("GET {}\r\n", url.path());
let start = Instant::now();
let rebind = options.rebind;
let host = options.host.as_deref().unwrap_or(url_host);

eprintln!("connecting to {host} at {remote}");
let conn = endpoint
.connect(remote, host)?
.await
.map_err(|e| anyhow!("failed to connect: {}", e))?;
eprintln!("connected at {:?}", start.elapsed());
let (mut send, mut recv) = conn
.open_bi()
.await
.map_err(|e| anyhow!("failed to open stream: {}", e))?;
if rebind {
let socket = std::net::UdpSocket::bind("[::]:0").unwrap();
let addr = socket.local_addr().unwrap();
eprintln!("rebinding to {addr}");
endpoint.rebind(socket).expect("rebind failed");
}

send.write_all(request.as_bytes())
.await
.map_err(|e| anyhow!("failed to send request: {}", e))?;
send.finish().unwrap();
let response_start = Instant::now();
eprintln!("request sent at {:?}", response_start - start);
let resp = recv
.read_to_end(usize::max_value())
.await
.map_err(|e| anyhow!("failed to read response: {}", e))?;
let duration = response_start.elapsed();
eprintln!(
"response received in {:?} - {} KiB/s",
duration,
resp.len() as f32 / (duration_secs(&duration) * 1024.0)
);
io::stdout().write_all(&resp).unwrap();
io::stdout().flush().unwrap();
conn.close(0u32.into(), b"done");

// Give the server a fair chance to receive the close packet
endpoint.wait_idle().await;

Ok(())
}

fn strip_ipv6_brackets(host: &str) -> &str {
// An ipv6 url looks like eg https://[::1]:4433/Cargo.toml, wherein the host [::1] is the
// ipv6 address ::1 wrapped in brackets, per RFC 2732. This strips those.
if host.starts_with('[') && host.ends_with(']') {
&host[1..host.len() - 1]
} else {
host
}
}

fn duration_secs(x: &Duration) -> f32 {
x.as_secs() as f32 + x.subsec_nanos() as f32 * 1e-9
}
Loading

0 comments on commit 57f0360

Please sign in to comment.