Fragmented packets during stream setup are not handled correctly #10

Open
opened 2024-12-24 14:55:10 +00:00 by jjkiers · 2 comments
Owner

When the tcp stream has started with fragmented packets, the code fails with the message that a packet is too long or too short to have a full TLS message.

This then leads to the proxy not being able to find the backend, and therefore going to the default backend (which is usually ban, meaning the connection will be unceremoniously closed).

When the tcp stream has started with fragmented packets, the code fails with the message that a packet is too long or too short to have a full TLS message. This then leads to the proxy not being able to find the backend, and therefore going to the default backend (which is usually _ban_, meaning the connection will be unceremoniously closed).
jjkiers added the
bug
label 2024-12-24 14:55:10 +00:00
Author
Owner

Some example code to try, based on ChatGPT:

To use a dedicated library like rustls for accurate TLS ClientHello parsing without terminating TLS, you'll need to focus on extracting the information from the packets rather than handling the full TLS handshake. A more efficient method is to use the tls-parser crate, which is specifically designed for parsing TLS packets.

Here's an example demonstrating how you can parse the TLS ClientHello message to extract the SNI using tls-parser without modifying incoming traffic:

First, add the dependencies to your Cargo.toml:

[dependencies]
tokio = { version = "1", features = ["full"] }
tls-parser = "0.10"

Now, you can create the proxy application:

use tokio::net::{TcpListener, TcpStream};
use tokio::io::{self, AsyncReadExt, AsyncWriteExt};
use tls_parser::parse_tls_plaintext;
use tls_parser::tls_extensions::*;
use tls_parser::TlsMessage;
use std::net::SocketAddr;

const BUFFER_SIZE: usize = 4096;

async fn handle_connection(mut inbound: TcpStream) -> io::Result<()> {
    let mut buffer = [0; BUFFER_SIZE];
    let mut total_read = 0;

    loop {
        let n = inbound.read(&mut buffer[total_read..]).await?;
        if n == 0 {
            return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "Connection closed"));
        }
        total_read += n;

        // Try to parse TLS ClientHello and extract SNI
        if let Some(sni) = extract_sni(&buffer[..total_read]) {
            println!("SNI: {}", sni);
            let backend_address = determine_backend(&sni)?;
            let mut outbound = TcpStream::connect(backend_address).await?;

            // Forward the buffered data to the backend
            outbound.write_all(&buffer[..total_read]).await?;

            // Forward the remaining traffic
            forward_traffic(inbound, outbound).await?;
            return Ok(());
        }

        if total_read >= BUFFER_SIZE {
            return Err(io::Error::new(io::ErrorKind::InvalidData, "Failed to extract SNI"));
        }
    }
}

async fn forward_traffic(mut inbound: TcpStream, mut outbound: TcpStream) -> io::Result<()> {
    let (mut ri, mut wi) = inbound.split();
    let (mut ro, mut wo) = outbound.split();

    let client_to_server = async {
        io::copy(&mut ri, &mut wo).await?;
        wo.shutdown().await
    };

    let server_to_client = async {
        io::copy(&mut ro, &mut wi).await?;
        wi.shutdown().await
    };

    tokio::try_join!(client_to_server, server_to_client)?;
    Ok(())
}

fn extract_sni(data: &[u8]) -> Option<String> {
    // Use tls-parser to extract SNI from the TLS ClientHello
    if let Ok((_, messages)) = parse_tls_plaintext(data) {
        for message in messages {
            if let TlsMessage::Handshake(handshake) = message {
                if let Ok((_, hello)) = handshake.client_hello() {
                    for ext in hello.ext {
                        if let SNI(_, host_name) = ext {
                            return String::from_utf8(host_name.clone()).ok();
                        }
                    }
                }
            }
        }
    }
    None
}

fn determine_backend(sni: &str) -> io::Result<SocketAddr> {
    match sni {
        _ => Ok("127.0.0.1:12345".parse().unwrap()),
    }
}

#[tokio::main]
async fn main() -> io::Result<()> {
    let listener = TcpListener::bind("127.0.0.1:8080").await?;
    println!("Listening on 127.0.0.1:8080");

    loop {
        let (inbound, _) = listener.accept().await?;
        tokio::spawn(async move {
            if let Err(e) = handle_connection(inbound).await {
                eprintln!("Failed to handle connection: {}", e);
            }
        });
    }
}

Explanation:

  1. tls-parser: This crate is used to parse the TLS ClientHello message and extract the SNI. It does not perform any certificate authentication or encrypted communication.

  2. SNI Extraction: The extract_sni function uses tls-parser to locate the SNI within the ClientHello message. It checks for the TLS extension containing the SNI and extracts the host name.

  3. Traffic Forwarding: The initial buffer reads and forwards traffic to the backend without any modification, ensuring connection integrity.

This setup should meet your requirements by checking the SNI without altering traffic and without dealing with certificates in the proxy.

Some example code to try, based on ChatGPT: To use a dedicated library like `rustls` for accurate TLS ClientHello parsing without terminating TLS, you'll need to focus on extracting the information from the packets rather than handling the full TLS handshake. A more efficient method is to use the `tls-parser` crate, which is specifically designed for parsing TLS packets. Here's an example demonstrating how you can parse the TLS ClientHello message to extract the SNI using `tls-parser` without modifying incoming traffic: First, add the dependencies to your `Cargo.toml`: ```toml [dependencies] tokio = { version = "1", features = ["full"] } tls-parser = "0.10" ``` Now, you can create the proxy application: ```rust use tokio::net::{TcpListener, TcpStream}; use tokio::io::{self, AsyncReadExt, AsyncWriteExt}; use tls_parser::parse_tls_plaintext; use tls_parser::tls_extensions::*; use tls_parser::TlsMessage; use std::net::SocketAddr; const BUFFER_SIZE: usize = 4096; async fn handle_connection(mut inbound: TcpStream) -> io::Result<()> { let mut buffer = [0; BUFFER_SIZE]; let mut total_read = 0; loop { let n = inbound.read(&mut buffer[total_read..]).await?; if n == 0 { return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "Connection closed")); } total_read += n; // Try to parse TLS ClientHello and extract SNI if let Some(sni) = extract_sni(&buffer[..total_read]) { println!("SNI: {}", sni); let backend_address = determine_backend(&sni)?; let mut outbound = TcpStream::connect(backend_address).await?; // Forward the buffered data to the backend outbound.write_all(&buffer[..total_read]).await?; // Forward the remaining traffic forward_traffic(inbound, outbound).await?; return Ok(()); } if total_read >= BUFFER_SIZE { return Err(io::Error::new(io::ErrorKind::InvalidData, "Failed to extract SNI")); } } } async fn forward_traffic(mut inbound: TcpStream, mut outbound: TcpStream) -> io::Result<()> { let (mut ri, mut wi) = inbound.split(); let (mut ro, mut wo) = outbound.split(); let client_to_server = async { io::copy(&mut ri, &mut wo).await?; wo.shutdown().await }; let server_to_client = async { io::copy(&mut ro, &mut wi).await?; wi.shutdown().await }; tokio::try_join!(client_to_server, server_to_client)?; Ok(()) } fn extract_sni(data: &[u8]) -> Option<String> { // Use tls-parser to extract SNI from the TLS ClientHello if let Ok((_, messages)) = parse_tls_plaintext(data) { for message in messages { if let TlsMessage::Handshake(handshake) = message { if let Ok((_, hello)) = handshake.client_hello() { for ext in hello.ext { if let SNI(_, host_name) = ext { return String::from_utf8(host_name.clone()).ok(); } } } } } } None } fn determine_backend(sni: &str) -> io::Result<SocketAddr> { match sni { _ => Ok("127.0.0.1:12345".parse().unwrap()), } } #[tokio::main] async fn main() -> io::Result<()> { let listener = TcpListener::bind("127.0.0.1:8080").await?; println!("Listening on 127.0.0.1:8080"); loop { let (inbound, _) = listener.accept().await?; tokio::spawn(async move { if let Err(e) = handle_connection(inbound).await { eprintln!("Failed to handle connection: {}", e); } }); } } ``` ### Explanation: 1. **`tls-parser`**: This crate is used to parse the TLS ClientHello message and extract the SNI. It does not perform any certificate authentication or encrypted communication. 2. **SNI Extraction**: The `extract_sni` function uses `tls-parser` to locate the SNI within the ClientHello message. It checks for the TLS extension containing the SNI and extracts the host name. 3. **Traffic Forwarding**: The initial buffer reads and forwards traffic to the backend without any modification, ensuring connection integrity. This setup should meet your requirements by checking the SNI without altering traffic and without dealing with certificates in the proxy.
jjkiers referenced this issue from a commit 2025-01-09 19:38:41 +00:00
jjkiers referenced this issue from a commit 2025-01-09 19:57:03 +00:00
Author
Owner

The fix from #11 didn't work in the end. Not sure why though.

The fix from #11 didn't work in the end. Not sure why though.
jjkiers reopened this issue 2025-01-09 21:06:21 +00:00
Sign in to join this conversation.
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: jjkiers/layer4-proxy#10
No description provided.