hur.st's bl.aagh

BSD, Ruby, Rust, Rambling

tarssh

An async Rust SSH tarpit

[rust]

tarssh is an SSH tarpit – a server that trickles an endlessly repeating introductory banner to clients for as long as it remains connected, in order to expend the resources of attackers.

It’s based on the same concept as Chris Wellons’ Endlessh, a similar service written in C.

Tarssh is my first Rust program using Tokio, an asynchronous runtime that allows for the construction of highly scalable event-driven programs using kqueue, epoll and similar APIs, using Rust’s Futures API.

The first release of tarssh predates Rust’s async/await syntax, and so the result was a quite unwieldy tree of method chains building up large types to implement the required Futures:

    loop_fn((sock, 0), move |(sock, i)| {
        Delay::new(Instant::now() + Duration::from_secs(delay))
            .map_err(Error::from)
            .and_then(move |_| {
                tokio::io::write_all(sock, BANNER[i % BANNER.len()])
                    .timeout(Duration::from_secs(timeout))
                    .from_err()
            })
            .and_then(move |(sock, _)| {
                tokio::io::flush(sock)
                    .timeout(Duration::from_secs(timeout))
                    .from_err()
            })
            .map(move |sock| Loop::Continue((sock, i.wrapping_add(1))))
            .or_else(move |err| {
                let connected = NUM_CLIENTS.fetch_sub(1, Ordering::Relaxed) - 1;
                info!(
                    "disconnect, peer: {}, duration: {:.2?}, error: \"{}\", clients: {}",
                    peer,
                    start.elapsed(),
                    err,
                    connected
                );
                Ok(Loop::Break(()))
            })
    })

For contrast here is how I rewrote that to use async/await:

    for chunk in BANNER.iter().cycle() {
        delay_for(delay).await;

        let res = timeout(time_out, sock.write_all(chunk.as_bytes()))
            .await
            .unwrap_or_else(|_| Err(std::io::Error::new(std::io::ErrorKind::Other, "timed out")));

        if let Err(err) = res {
            let connected = NUM_CLIENTS.fetch_sub(1, Ordering::Relaxed) - 1;
            info!(
                "disconnect, peer: {}, duration: {:.2?}, error: \"{}\", clients: {}",
                peer,
                start.elapsed(),
                err,
                connected
            );
            break;
        }
    }

I ended up forgoing this future-per-connection approach in the name of efficiency: now instead of a timer delay for each connection, there is an array of slots corresponding to each delay interval. Connections and their associated state are placed in slots and polled as the daemon iterates through its delay loop.

This more closely resembles how Endlessh functions. It’s less straight-forward, but since the entire point of a tarpit is to maintain lots of connections without expending too many resources I felt it made sense here.

As of 2022 I’ve largely stopped running tarssh – almost all clients have wised up to the problem these simple tarpits cause and disconnect quite quickly. In its time, though, it wasn’t uncommon for me to see many hundreds of clients to remain trapped for hours, even days, regaled by Yon Yonson’s riveting tale.

-% tarssh -v --disable-timestamp &
[INFO  tarssh] listen, addr: 0.0.0.0:2222
[INFO  tarssh] privdrop, enabled: false
[INFO  tarssh] sandbox, enabled: true
[INFO  tarssh] start, servers: 1, max_clients: 4096, delay: 10s, timeout: 30s
-% telnet 0 2222
Trying 0.0.0.0...
[INFO  tarssh] connect, peer: 127.0.0.1:21491, clients: 1
Connected to 0.
Escape character is '^]'.
My name is Yon Yonson
I liv^]
telnet> close
Connection closed.
-% [INFO  tarssh] disconnect, peer: 127.0.0.1:21491, duration: 40.02s, error: Broken pipe (os error 32), clients: 0