Create initial CONNECT proxy

This commit is contained in:
Sander Declerck 2025-12-10 16:55:10 +01:00
parent cef2194427
commit a60c68074a
No known key found for this signature in database
5 changed files with 3875 additions and 0 deletions

View file

@ -80,3 +80,62 @@ jobs:
with: with:
name: safe-chain-${{ matrix.os }}-${{ matrix.arch }} name: safe-chain-${{ matrix.os }}-${{ matrix.arch }}
path: dist/* path: dist/*
create-proxy-binaries:
name: Create proxy binary for ${{ matrix.os }}-${{ matrix.arch }}
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- os: macos
arch: x64
runner: macos-15-intel
target: x86_64-apple-darwin
extension: ""
- os: macos
arch: arm64
runner: macos-latest
target: aarch64-apple-darwin
extension: ""
- os: linux
arch: x64
runner: ubuntu-latest
target: x86_64-unknown-linux-gnu
extension: ""
- os: linux
arch: arm64
runner: ubuntu-24.04-arm
target: aarch64-unknown-linux-gnu
extension: ""
- os: win
arch: x64
runner: windows-latest
target: x86_64-pc-windows-msvc
extension: ".exe"
- os: win
arch: arm64
runner: windows-11-arm
target: aarch64-pc-windows-msvc
extension: ".exe"
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Build proxy
working-directory: proxy
run: cargo build --release --target ${{ matrix.target }}
- name: Upload proxy artifact
uses: actions/upload-artifact@v4
with:
name: proxy-${{ matrix.os }}-${{ matrix.arch }}
path: proxy/target/${{ matrix.target }}/release/proxy${{ matrix.extension }}

4
.gitignore vendored
View file

@ -151,3 +151,7 @@ dist/
# Jetbrains IDEs # Jetbrains IDEs
.idea/** .idea/**
# Rust
target

3681
proxy/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

8
proxy/Cargo.toml Normal file
View file

@ -0,0 +1,8 @@
[package]
name = "proxy"
version = "0.1.0"
edition = "2021"
[dependencies]
rama = { git = "https://github.com/plabayo/rama", features = ["http-full", "dns", "boring"] }
tokio = { version = "1", features = ["full"] }

123
proxy/src/main.rs Normal file
View file

@ -0,0 +1,123 @@
use std::{convert::Infallible, time::Duration};
use rama::{
Layer, Service,
extensions::ExtensionsMut,
http::{
Request, Response, StatusCode,
client::EasyHttpWebClient,
layer::{
remove_header::{RemoveRequestHeaderLayer, RemoveResponseHeaderLayer},
trace::TraceLayer,
upgrade::UpgradeLayer,
},
matcher::MethodMatcher,
server::HttpServer,
service::web::response::IntoResponse,
},
layer::ConsumeErrLayer,
net::{http::RequestContext, proxy::ProxyTarget, stream::layer::http::BodyLimitLayer},
rt::Executor,
service::service_fn,
tcp::{client::service::Forwarder, server::TcpListener},
telemetry::tracing::{
self,
metadata::LevelFilter,
subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt},
},
};
#[tokio::main]
async fn main() {
setup_tracing();
run_server().await;
}
fn setup_tracing() {
tracing::subscriber::registry()
.with(fmt::layer())
.with(
EnvFilter::builder()
.with_default_directive(LevelFilter::INFO.into())
.from_env_lossy(),
)
.init();
tracing::info!("Tracing is set up");
}
async fn run_server() {
let graceful = rama::graceful::Shutdown::default();
graceful.spawn_task_fn(server_task);
graceful
.shutdown_with_limit(Duration::from_secs(30))
.await
.expect("graceful shutdown");
}
async fn server_task(guard: rama::graceful::ShutdownGuard) {
let tcp_service = TcpListener::build()
.bind("127.0.0.1:3128")
.await
.expect("bind tcp proxy to 127.0.0.1:3128");
let exec = Executor::graceful(guard.clone());
let http_service = HttpServer::auto(exec).service(
(
TraceLayer::new_for_http(),
ConsumeErrLayer::default(),
UpgradeLayer::new(
MethodMatcher::CONNECT,
service_fn(http_connect_accept),
ConsumeErrLayer::default().into_layer(Forwarder::ctx()),
),
RemoveResponseHeaderLayer::hop_by_hop(),
RemoveRequestHeaderLayer::hop_by_hop(),
)
.into_layer(service_fn(http_plain_proxy)),
);
tcp_service
.serve_graceful(
guard,
(
// protect the http proxy from too large bodies, both from request and response end
BodyLimitLayer::symmetric(500 * 1024 * 1024),
)
.into_layer(http_service),
)
.await;
}
async fn http_connect_accept(mut req: Request) -> Result<(Response, Request), Response> {
match RequestContext::try_from(&req).map(|ctx| ctx.host_with_port()) {
Ok(authority) => {
tracing::info!(
server.address = %authority.host,
server.port = authority.port,
"accept CONNECT",
);
req.extensions_mut().insert(ProxyTarget(authority));
}
Err(err) => {
tracing::error!("error extracting authority: {err:?}");
return Err(StatusCode::BAD_REQUEST.into_response());
}
}
return Ok((StatusCode::OK.into_response(), req));
}
async fn http_plain_proxy(req: Request) -> Result<Response, Infallible> {
let client = EasyHttpWebClient::default();
return match client.serve(req).await {
Ok(resp) => Ok(resp),
Err(err) => {
tracing::error!("Error forwarding request: {err:?}");
let resp = StatusCode::BAD_GATEWAY.into_response();
Ok(resp)
}
};
}