rustls / tokio-rustls

Async TLS for the Tokio runtime
Apache License 2.0
122 stars 69 forks source link

Sending plaintext response for non-TLS connection attempts #54

Open BrandonLeeDotDev opened 7 months ago

BrandonLeeDotDev commented 7 months ago

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 commented 7 months ago

this is ~(the response) in Firefox



BrandonLeeDotDev commented 7 months ago

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 commented 7 months ago

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 commented 7 months ago

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 commented 7 months ago

@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 commented 7 months ago

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 commented 7 months ago

@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 commented 7 months ago

@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...