Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sending plaintext response for non-TLS connection attempts #54

Open
BrandonLeeDotDev opened this issue Mar 12, 2024 · 8 comments
Open

Comments

@BrandonLeeDotDev
Copy link

This is my current attempt among others. Both print statements print. I have had intermittent success... its just not stable. Whats the correct way to approach this within the lib itself?


pub struct TlsListener(
    Vec<CertificateDer<'static>>,
    PrivateKeyDer<'static>,
    TlsAcceptor,
    TcpListener,
);

impl TlsListener {
    pub async fn bind(address: SocketAddr) -> Self {
        let listener = TcpListener::bind(address).await.unwrap();

        let certs = Path::new(CERTS_PATH);
        let cert = load_certs(&certs).unwrap();

        let key = Path::new(KEY_PATH);
        let key = load_keys(&key).unwrap();

        let config = rustls::ServerConfig::builder()
            .with_no_client_auth()
            .with_single_cert(cert.clone(), key.clone_key())
            .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))
            .unwrap();
        let acceptor = TlsAcceptor::from(Arc::new(config));

        TlsListener(cert, key, acceptor, listener)
    }

    pub async fn redirect(&self) -> io::Result<()> {
        println!("Redirecting");
        let (mut stream, _peer_addr) = self.3.accept().await?;
        let redirect =
            b"HTTP/1.1 301 Moved Permanently\r\nLocation: https://localhost:4010/\r\n\r\n";
        stream.write(redirect).await?;
        stream.flush().await?;
        println!("Redirected");
        Ok(())
    }

    pub async fn accept(&self) -> io::Result<(TlsStream<TcpStream>, SocketAddr)> {
        let listener = &self.3;
        let (stream, peer_addr) = listener.accept().await?;

        match self.2.accept(stream).await {
            Ok(stream) => Ok((stream, peer_addr)),
            Err(error) => {
                self.redirect().await?;
                Err(error)
            }
        }
    }
}
@BrandonLeeDotDev
Copy link
Author

this is ~(the response) in Firefox

���

@BrandonLeeDotDev
Copy link
Author

BrandonLeeDotDev commented Mar 12, 2024

I see why the above failed. By calling accept I was attempting to accept the next incoming connection. Which is why I tried the following, non preferred, Fd path...

Even if I create a stream from an OwnedFd I get the following:

������2HTTP/1.1 301 Moved Permanently Location: https://localhost:4010/

What are the leading bytes? How do I prevent them from being sent? How do I access the underlying stream should TLS fail?

    pub async fn accept(&self) -> io::Result<(TlsStream<TcpStream>, SocketAddr)> {
        let listener = &self.3;
        let (mut stream, peer_addr) = listener.accept().await?;

        let owned_fd: OwnedFd = stream.as_fd().try_clone_to_owned().unwrap();
        let std_stream = unsafe { std::net::TcpStream::from_raw_fd(owned_fd.into_raw_fd()) };
        std_stream.set_nonblocking(true)?;
        let tokio_stream = TcpStream::from_std(std_stream)?;

        match self.2.accept(tokio_stream).await {
            Err(_error) => {
                let redirect =
                    b"HTTP/1.1 301 Moved Permanently\r\nLocation: https://localhost:4010/\r\n\r\n";
                stream.write(redirect).await?;
                stream.flush().await?;
                stream.shutdown().await?;
                Err(io::Error::new(io::ErrorKind::Other, "Redirected"))
            },
            Ok(tls_stream) => Ok((tls_stream, peer_addr))
            
        }
    }

@djc djc changed the title Assuming a Non TLS attempt how do I sent a 301 response? Sending plaintext response for non-TLS connection attempts Mar 12, 2024
@djc
Copy link
Member

djc commented Mar 12, 2024

So you want to achieve that plaintext (HTTP) clients connecting to your TLS server socket get a HTTP redirect? I don't think tokio-rustls will facilitate that use case, because it will ~always send a TLS alert when it fails to parse the client's stream as TLS.

If you drop down directly to the rustls API you can probably make it work, but I'm not sure you can easily wrap a tokio-rustls connection around that after the fact. I suppose we could maybe support this use case in the LazyConfigAcceptor, optionally postponing the sending of the alert until the caller has okayed it?

@BrandonLeeDotDev
Copy link
Author

Imo, this is just default behavior... that said this is my final solution

    pub async fn accept<F, Fut>(&self, fun: F) -> io::Result<()>
    where
        F: FnOnce(TlsStream<TcpStream>, SocketAddr) -> Fut,
        Fut: Future<Output = ()> + Send + 'static,
        F: Send + 'static,
    {
        let acceptor = self.2.clone();
        let (mut stream, peer_addr) = self.3.accept().await?;

        tokio::spawn(async move {
            let mut buf = [0; 1]; // Buffer to read the first byte
            match timeout(Duration::from_secs(10), stream.peek(&mut buf)).await {
                Ok(Ok(_)) => {}
                _ => {
                    let _ = Self::redirect(&mut stream).await;
                }
            }

            if buf[0] == 0x16 {
                // 0x16 is the first byte of a TLS handshake
                if let Ok(tls_stream) = acceptor.accept(stream).await {
                    fun(tls_stream, peer_addr).await;
                }
            } else {
                let _ = Self::redirect(&mut stream).await;
            }
        });

        Ok(())
    }

@BrandonLeeDotDev
Copy link
Author

BrandonLeeDotDev commented Mar 12, 2024

@djc

I suppose we could maybe support this use case in the LazyConfigAcceptor, optionally postponing the sending of the alert until the caller has okayed it? - here

Yeah, should be supported somewhere... as arg or something... something like .force_tls(bool)

@c92s
Copy link

c92s commented Mar 29, 2024

Can you elaborate on how the peeking trick should work? For me, in both cases (connecting via HTTP & HTTPS), the timer does not run in a timeout, as the peek was successful.

Using the following:

tokio::spawn(async move {
    mut buf = [0u8; 1];
    tcp_stream.peek(&mut buf).await.unwrap();
    println!("peeked: {:?}", buf);
...

successfully peeks something with HTTP and HTTPS:

peeked: [22]             <-- HTTPS
peeked: [71]             <-- HTTP

@BrandonLeeDotDev
Copy link
Author

BrandonLeeDotDev commented Mar 29, 2024

@c92s I ended up doing this. The first byte is the type signature. 22 (peeked: [22] <-- HTTPS) or 0x16 == HTTPS. I also check the client hello byte. If not not both then 301. 'let _ = Self::redirect(&mut stream).await;'

    pub async fn accept<F, Fut>(&self, handle_client: F) -> io::Result<()>
    where
        F: FnOnce(TlsStream<TcpStream>, SocketAddr) -> Fut,
        Fut: Future<Output = ()> + Send + 'static,
        F: Send + 'static,
    {
        let (mut stream, peer_addr) = self.1.accept().await?;
        let acceptor = self.0.clone();

        tokio::spawn(async move {
            let mut buf = [0; 6];
            match timeout(Duration::from_secs(60), stream.peek(&mut buf)).await {
                Ok(Ok(_)) => {}
                _ => {
                    let _ = Self::redirect(&mut stream).await;
                }
            }

            // Check for the handshake message type (0x16) and the ClientHello byte (0x01)
            if buf[0] == 0x16 && buf[5] == 0x01 {
                crate::debug_with_time!({},"TLS connection from {peer_addr}",);
                if let Ok(tls_stream) = acceptor.accept(stream).await {
                    tokio::spawn(handle_client(tls_stream, peer_addr));
                }
            } else {
                let _ = Self::redirect(&mut stream).await;
            }
        });

        Ok(())
    }

@c92s
Copy link

c92s commented Apr 2, 2024

@BrandonLeeDotDev thanks for the info!

FYI: we could further refine that, using the available rustls enums (rustls::ContentType::Handshake & rustls::HandshakeType::ClientHello), e.g.:

if buf[0] ==  rustls::ContentType::Handshake.get_u8() && buf[5] == rustls::HandshakeType::ClientHello.get_u8() {
    crate::debug_with_time!({},"TLS connection from {peer_addr}",);
    if let Ok(tls_stream) = acceptor.accept(stream).await {
        tokio::spawn(handle_client(tls_stream, peer_addr));
    }
} else {
    let _ = Self::redirect(&mut stream).await;
}

However, IMO this looks very "handcrafted", I am still not sure, if this is the proper way to handle such cases...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants