runnel-rs 0.2.0

A Rust proxy and tunnel toolbox with WireGuard-style, TUN, SOCKS, and TLS-based transports.
Documentation
use anyhow::{Context, Result, bail};
use tokio::{io::AsyncWriteExt, net::TcpStream, time::timeout};
use tokio_rustls::{TlsConnector, client::TlsStream};

use crate::{
    proxy::{auth::AuthProof, http, tls},
    runtime::ClientRuntime,
};

pub(crate) async fn establish_remote_tunnel(
    runtime: &ClientRuntime,
    connector: &TlsConnector,
    host_header: &str,
    server_name: &str,
    target: &str,
    transport: http::TunnelTransport,
) -> Result<TlsStream<TcpStream>> {
    let upstream = timeout(runtime.connect_timeout, TcpStream::connect(&runtime.server))
        .await
        .context("server connect timed out")??;
    upstream.set_nodelay(true)?;

    let server_name = tls::server_name(server_name)?;
    let mut tunnel = match timeout(
        runtime.handshake_timeout,
        connector.connect(server_name, upstream),
    )
    .await
    {
        Ok(Ok(stream)) => stream,
        Ok(Err(err)) => return Err(err).context("TLS handshake with server failed"),
        Err(_) => bail!("TLS handshake with server timed out"),
    };

    let proof = AuthProof::sign(&runtime.password, "POST", &runtime.path, target)?;
    let payload = http::TunnelPayload {
        target: target.to_owned(),
        transport,
        timestamp: proof.timestamp,
        nonce: proof.nonce,
        signature: proof.signature,
    };
    let request =
        http::build_tunnel_request(host_header, &runtime.path, &payload, &runtime.user_agent)?;
    tunnel.write_all(&request).await?;

    let response_head = match timeout(
        runtime.handshake_timeout,
        http::read_head(&mut tunnel, runtime.max_header_size),
    )
    .await
    {
        Ok(Ok((head, body_prefix))) => (head, body_prefix),
        Ok(Err(err)) => return Err(err).context("failed to read server response"),
        Err(_) => bail!("server response timed out"),
    };

    let response =
        http::parse_response_head(&response_head.0).context("invalid server response")?;
    if !response.is_http1 {
        bail!("server returned an unsupported HTTP version");
    }
    if response.status != 200 {
        let detail = http::read_response_body_text(
            &mut tunnel,
            &response_head.1,
            response.content_length,
            runtime.max_header_size,
        )
        .await;
        if let Some(detail) = detail {
            bail!(
                "server refused tunnel with status {} {}: {}",
                response.status,
                response.reason,
                detail
            );
        }
        bail!(
            "server refused tunnel with status {} {}",
            response.status,
            response.reason
        );
    }

    Ok(tunnel)
}