Skip to content

Commit 93331d8

Browse files
committed
feat: refactor into a universal proxy architecture with single cli binary
1 parent 2e5f719 commit 93331d8

21 files changed

Lines changed: 150 additions & 134 deletions

File tree

Cargo.lock

Lines changed: 0 additions & 20 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
[workspace]
2-
members = ["h3proxy-lib", "h3proxy-cli", "h3client"]
2+
members = ["h3proxy-lib", "h3proxy-cli"]
33
resolver = "2"

h3client/Cargo.toml

Lines changed: 0 additions & 20 deletions
This file was deleted.

h3client/src/main.rs

Lines changed: 0 additions & 59 deletions
This file was deleted.

h3proxy-cli/src/main.rs

Lines changed: 67 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,89 @@
11
use anyhow::{Context, Result};
2-
use clap::Parser;
2+
use clap::{Parser, Subcommand};
3+
use h3proxy_lib::client::{ClientConfig, ProxyClient};
34
use h3proxy_lib::config::ProxyConfig;
45
use h3proxy_lib::server::ProxyServer;
56
use std::fs;
67
use std::net::SocketAddr;
78

89
#[derive(Parser, Debug)]
9-
#[command(author, version, about, long_about = None)]
10-
struct Args {
11-
#[arg(short, long, default_value = "0.0.0.0:4433")]
12-
listen: SocketAddr,
10+
#[command(author, version, about = "h3proxy - A universal HTTP/3 proxy platform", long_about = None)]
11+
struct Cli {
12+
#[command(subcommand)]
13+
command: Commands,
14+
}
15+
16+
#[derive(Subcommand, Debug)]
17+
enum Commands {
18+
/// Run the proxy server (inbound)
19+
Server {
20+
#[arg(short, long, default_value = "0.0.0.0:4433")]
21+
listen: SocketAddr,
22+
23+
#[arg(short, long, default_value = "cert.pem")]
24+
cert: String,
1325

14-
#[arg(short, long, default_value = "cert.pem")]
15-
cert: String,
26+
#[arg(short, long, default_value = "key.pem")]
27+
key: String,
28+
},
29+
/// Run the proxy client (outbound test tunnel)
30+
Client {
31+
#[arg(short, long, default_value = "127.0.0.1:4433")]
32+
proxy: SocketAddr,
1633

17-
#[arg(short, long, default_value = "key.pem")]
18-
key: String,
34+
#[arg(short, long, default_value = "https://example.com:443")]
35+
target: String,
36+
37+
#[arg(short, long, default_value = "cert.pem")]
38+
cert: String,
39+
},
1940
}
2041

2142
#[tokio::main]
2243
async fn main() -> Result<()> {
2344
tracing_subscriber::fmt::init();
24-
let args = Args::parse();
45+
let cli = Cli::parse();
46+
47+
match cli.command {
48+
Commands::Server { listen, cert, key } => {
49+
let cert_data = fs::read(&cert).with_context(|| format!("failed to read cert file: {}", cert))?;
50+
let key_data = fs::read(&key).with_context(|| format!("failed to read key file: {}", key))?;
51+
52+
let certs: Vec<_> = rustls_pemfile::certs(&mut &*cert_data)
53+
.collect::<Result<_, _>>()
54+
.context("failed to parse certs")?;
2555

26-
let cert_data = fs::read(&args.cert).with_context(|| format!("failed to read cert file: {}", args.cert))?;
27-
let key_data = fs::read(&args.key).with_context(|| format!("failed to read key file: {}", args.key))?;
56+
let mut keys: Vec<_> = rustls_pemfile::private_key(&mut &*key_data)
57+
.context("failed to parse key")?
58+
.into_iter()
59+
.collect();
60+
let priv_key = keys.remove(0);
2861

29-
let certs: Vec<_> = rustls_pemfile::certs(&mut &*cert_data)
30-
.collect::<Result<_, _>>()
31-
.context("failed to parse certs")?;
62+
let config = ProxyConfig {
63+
listen_addr: listen,
64+
cert_chain: certs,
65+
priv_key,
66+
};
3267

33-
let mut keys: Vec<_> = rustls_pemfile::private_key(&mut &*key_data)
34-
.context("failed to parse key")?
35-
.into_iter()
36-
.collect();
37-
let priv_key = keys.remove(0);
68+
let server = ProxyServer::new(config);
69+
server.serve().await?;
70+
}
71+
Commands::Client { proxy, target, cert } => {
72+
let cert_data = fs::read(&cert).with_context(|| format!("failed to read cert file: {}", cert))?;
73+
let certs: Vec<_> = rustls_pemfile::certs(&mut &*cert_data)
74+
.collect::<Result<_, _>>()
75+
.context("failed to parse certs")?;
3876

39-
let config = ProxyConfig {
40-
listen_addr: args.listen,
41-
cert_chain: certs,
42-
priv_key,
43-
};
77+
let config = ClientConfig {
78+
proxy_addr: proxy,
79+
target_url: target,
80+
root_certs: certs,
81+
};
4482

45-
let server = ProxyServer::new(config);
46-
server.serve().await?;
83+
let client = ProxyClient::new(config);
84+
client.run().await?;
85+
}
86+
}
4787

4888
Ok(())
4989
}

h3proxy-lib/src/client.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
use anyhow::Result;
2+
use bytes::Buf;
3+
use h3_quinn::Connection;
4+
use http::Request;
5+
use quinn::Endpoint;
6+
use rustls_pki_types::CertificateDer;
7+
use std::{net::SocketAddr, sync::Arc};
8+
use tracing::info;
9+
10+
pub struct ClientConfig {
11+
pub proxy_addr: SocketAddr,
12+
pub target_url: String,
13+
pub root_certs: Vec<CertificateDer<'static>>,
14+
}
15+
16+
pub struct ProxyClient {
17+
config: ClientConfig,
18+
}
19+
20+
impl ProxyClient {
21+
pub fn new(config: ClientConfig) -> Self {
22+
Self { config }
23+
}
24+
25+
pub async fn run(&self) -> Result<()> {
26+
let mut endpoint = Endpoint::client("0.0.0.0:0".parse()?)?;
27+
28+
let mut roots = rustls::RootCertStore::empty();
29+
for c in self.config.root_certs.clone() {
30+
roots.add(c)?;
31+
}
32+
33+
let mut crypto = rustls::ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider()))
34+
.with_safe_default_protocol_versions()?
35+
.with_root_certificates(roots)
36+
.with_no_client_auth();
37+
crypto.alpn_protocols = vec![b"h3".to_vec(), b"h3-29".to_vec()];
38+
39+
let client_config = quinn::ClientConfig::new(Arc::new(
40+
quinn::crypto::rustls::QuicClientConfig::try_from(crypto)?
41+
));
42+
endpoint.set_default_client_config(client_config);
43+
44+
let quinn_conn = endpoint.connect(self.config.proxy_addr, "localhost")?.await?;
45+
info!("connected to proxy at {}", self.config.proxy_addr);
46+
47+
let h3_conn_wrapped = Connection::new(quinn_conn);
48+
let (mut driver, mut send_request) = h3::client::new(h3_conn_wrapped).await?;
49+
tokio::spawn(async move {
50+
let _ = std::future::poll_fn(|cx| driver.poll_close(cx)).await;
51+
});
52+
53+
let uri = self.config.target_url.parse::<http::Uri>()?;
54+
let host = uri.host().unwrap_or("");
55+
let port = uri.port_u16().unwrap_or(443);
56+
let authority = format!("{}:{}", host, port);
57+
58+
let req = Request::builder()
59+
.method("CONNECT")
60+
.uri(self.config.target_url.clone())
61+
.header("host", authority)
62+
.body(())?;
63+
64+
info!("sending CONNECT request to {}", self.config.target_url);
65+
let mut request_stream = send_request.send_request(req).await?;
66+
let resp = request_stream.recv_response().await?;
67+
info!("response status: {}", resp.status());
68+
69+
while let Some(chunk) = request_stream.recv_data().await? {
70+
let bytes = chunk;
71+
print!("{}", String::from_utf8_lossy(bytes.chunk()));
72+
}
73+
74+
Ok(())
75+
}
76+
}

h3proxy-lib/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
pub mod config;
22
pub mod server;
3-
pub mod handler;
3+
pub mod handler;
4+
pub mod client;

target/.rustc_info.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"rustc_fingerprint":17866707384158916766,"outputs":{"12855261153397992890":{"success":true,"status":"","code":0,"stdout":"rustc 1.93.1 (01f6ddf75 2026-02-11)\nbinary: rustc\ncommit-hash: 01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf\ncommit-date: 2026-02-11\nhost: x86_64-pc-windows-msvc\nrelease: 1.93.1\nLLVM version: 21.1.8\n","stderr":""},"10803282065846393305":{"success":true,"status":"","code":0,"stdout":"___.exe\nlib___.rlib\n___.dll\n___.dll\n___.lib\n___.dll\nC:\\Users\\iHsin\\.rustup\\toolchains\\stable-x86_64-pc-windows-msvc\npacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"msvc\"\ntarget_family=\"windows\"\ntarget_feature=\"cmpxchg16b\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_feature=\"sse3\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"windows\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"pc\"\nwindows\n","stderr":""}},"successes":{}}
1+
{"rustc_fingerprint":9493169903018013690,"outputs":{"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.93.1 (01f6ddf75 2026-02-11)\nbinary: rustc\ncommit-hash: 01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf\ncommit-date: 2026-02-11\nhost: x86_64-pc-windows-msvc\nrelease: 1.93.1\nLLVM version: 21.1.8\n","stderr":""},"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___.exe\nlib___.rlib\n___.dll\n___.dll\n___.lib\n___.dll\nC:\\Users\\iHsin\\.rustup\\toolchains\\stable-x86_64-pc-windows-msvc\npacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"msvc\"\ntarget_family=\"windows\"\ntarget_feature=\"cmpxchg16b\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_feature=\"sse3\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"windows\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"pc\"\nwindows\n","stderr":""}},"successes":{}}
Binary file not shown.

target/debug/.fingerprint/h3proxy-lib-0bee0a6fb5d9b789/output-lib-h3proxy_lib

Lines changed: 0 additions & 3 deletions
This file was deleted.

0 commit comments

Comments
 (0)