This commit is contained in:
2026-03-26 21:23:22 +04:00
commit 41f4fa65b5
25 changed files with 5009 additions and 0 deletions
+120
View File
@@ -0,0 +1,120 @@
use std::net::SocketAddr;
use std::sync::Arc;
use axum::Router;
use axum::body::Body;
use axum::extract::{ConnectInfo, Request, State};
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use color_eyre::Result;
use hyper_util::rt::TokioIo;
use tokio_util::sync::CancellationToken;
use tracing::{info, warn};
use crate::protocol::{StreamHeader, write_stream_header};
use crate::relay::QuicBiStream;
use crate::server::state::ServerState;
pub async fn run(
addr: SocketAddr,
state: Arc<ServerState>,
cancel: CancellationToken,
) -> Result<()> {
let app = Router::new().fallback(proxy_handler).with_state(state);
let listener = tokio::net::TcpListener::bind(addr).await?;
info!(%addr, "HTTP server listening");
axum::serve(
listener,
app.into_make_service_with_connect_info::<SocketAddr>(),
)
.with_graceful_shutdown(async move { cancel.cancelled().await })
.await?;
Ok(())
}
async fn proxy_handler(
State(state): State<Arc<ServerState>>,
ConnectInfo(peer): ConnectInfo<SocketAddr>,
req: Request<Body>,
) -> Response {
match do_proxy(state, peer, req).await {
Ok(resp) => resp,
Err(e) => {
warn!("HTTP proxy error: {e:#}");
(StatusCode::NOT_FOUND, format!("{e}")).into_response()
}
}
}
async fn do_proxy(
state: Arc<ServerState>,
peer: SocketAddr,
req: Request<Body>,
) -> Result<Response> {
// Use X-Forwarded-For if present (from Traefik), otherwise direct peer
let peer_str = req
.headers()
.get("x-forwarded-for")
.and_then(|v| v.to_str().ok())
.map(|v| v.split(',').next().unwrap_or(v).trim().to_string())
.unwrap_or_else(|| peer.to_string());
// Extract subdomain from Host header
let host_header = req
.headers()
.get("host")
.and_then(|v| v.to_str().ok())
.ok_or_else(|| color_eyre::eyre::eyre!("missing Host header"))?;
// Strip port if present (e.g. "myapp.bore.localhost:8080" -> "myapp.bore.localhost")
let host = host_header.split(':').next().unwrap_or(host_header);
let suffix = format!(".{}", state.base_domain);
let subdomain = host
.strip_suffix(&suffix)
.filter(|s| !s.is_empty())
.ok_or_else(|| color_eyre::eyre::eyre!("no tunnel found for host {host}"))?
.to_string();
// Look up the tunnel
let tunnel_id = *state
.http_routes
.get(&subdomain)
.ok_or_else(|| color_eyre::eyre::eyre!("no tunnel for subdomain {subdomain}"))?;
let entry = state
.tunnels
.get(&tunnel_id)
.ok_or_else(|| color_eyre::eyre::eyre!("tunnel {tunnel_id} not found in state"))?;
let connection = entry.connection.clone();
drop(entry);
// Open QUIC stream to client
let (mut quic_send, quic_recv) = connection.open_bi().await?;
write_stream_header(
&mut quic_send,
&StreamHeader {
tunnel_id,
peer_addr: peer_str,
},
)
.await?;
// Use hyper to proxy the HTTP request over the QUIC stream
let io = TokioIo::new(QuicBiStream {
send: quic_send,
recv: quic_recv,
});
let (mut sender, conn) = hyper::client::conn::http1::handshake(io).await?;
tokio::spawn(async move {
if let Err(e) = conn.await {
warn!("HTTP proxy connection error: {e}");
}
});
let resp = sender.send_request(req).await?;
Ok(resp.map(Body::new))
}