~/articles/rust-telnet-snake

Idea

Azelea made a Telnet animation server, so I decided to try to make one too!
In crablang of course!

Here's the link to the project, in the state when I wrote this article. To try it out, simply do telnet earth2077.fr in your terminal.

Decoding Telnet codes

It wasn't easy to start. Since there are no existing Telnet server libraries written in rust, I had to write everything related to Telnet out of nothing. With the help of some RFC found in the citation section on Wikipedia, I was able to understand the code the client side was sending just enough to implement a bodged-together server that understands some requests.

Code received from client

For example, the code 255, the largest representable 8-bit number is the IAC code. It is reserved to encode "interpret as command" 1. This code would allow the receiving peer to understand that the bytes coming through after this code is a command, not plain text.

I used pattern matching to recursively destruct the received bytes. This is a lot easier as I don't need to keep track of the current state.

match codes {
    ...
    [] => (),

    // Responsd to terminal size request
    [IAC, WILL, NAWS, rest @ ..] => {
        self.response.append(&mut vec![IAC, DO, NAWS]);
        self.read_codes(rest);
    }
    // Parse terminal size
    [IAC, SB, NAWS, 0, width, 0, height, IAC, SE, rest @ ..] => {
        self.width = *width;
        self.height = *height;
        self.read_codes(rest);
    }
    ...
}

Using the named rest @ .. wildcard allowed me to continue parsing the rest of the received bytes, without having to deal with the fact that each valid "sequence" aren't necessarily of the same length.

I don't know how to do this efficiently in a language without pattern matching. A finite state machine would be needed as we read the array byte by byte while updating the current state, instead of sequence by sequence. This method seems to be error-prone, but I might as well be inexperienced :P

If you're a C programmer, you might argue that recursion is less efficient. Well yes, you're right. However, in this case, rustc might actually be smart enough to optimize this code to a loop because it's tail recursive.

Having a slick animation is nice, but I also want the snake to be centered. The final product would be less polished if the snake were to always align to the top left corner. Hence, I implemented the NAWS sequence in the snippet above to decode the width and height of the client so that later, when the animation part of the project is done, I would be able to detect the dimension of the client's window without problem.

Creating animation

I have zero experience in ASCII art. To make my own art, I used this site where I found a template of a snake. Using this template and the online ASCII art editor of the site, I drew the snake flicking its tongue in various frames. I then copied each frame to a text file. I decided to settle on a simple file format where each frame is saved as plain text; each frame is separated by a >\n (a > and then a newline).

Some esoteric ANSI escape sequences2 were used to center the snake using the dimension of client's window obtained with telnet_parser. In order to not append the escape sequence in front of each frame everytime, I implemented a buffered_frame variable to cache all frames padded with escape sequence since last display dimension change.

Not knowing how to traverse a vector back and forth idiomatically in Rust, I implemented a basic logic using for-loops to keep track of the current index and send out the corresponding frame. 3

"Gotcha"s

Not using escape sequence but spaces

My first method to center the animation was using spaces. It worked, however it was extremely inefficient. Using escape sequences allowed me to have a jump of $ n $ characters using a single escape sequence.

Not buffering the frames

The frames padded with escape sequences were not buffered in the beginning, i.e, they were recalculated at every single frame. I added a buffering feature and noticed that the flickering of images is less present. The padding must be relatively expensive in time.

It was also a bit of a hassle to calculate the offset to center the image, I guess I'm just bad at math / at writing imperitive code :P

What helped me

This may seem completely unrelated, but the fact that I had previously set up my tmux script "tmux-sessionizer" 4 helped me tremendously. It allowed me to spin up multiple "playground" and jump between them, while having each of them organized as folders in the ~/playground/ directory in a hassle-free fashion. I was able to create new projects from scratch in no time and better test my concepts, theories and ideas. I also separated the animation part and the Telnet parsing part as two playground project in the beginning, doing so gave me the freedom to fail and learn the flaws in my logic, without being frustrated and entangled in a projet that has become too complicated during the prototype stage.

Going live

I wrote a simple systemd service file for my program, threw the compiled hsssss binary into /usr/local/bin of my server, ran it. Went to sleep.

The next day I woke up to see that the CPU / network usage of my server traces a steady $y = x$ plot. It was going up. Crazy CPU usage

Turns out, people around the world are crawling the web. With a couple simple println!() statement, I could visualize a lot of IPs comming from people I don't know are connected to my server. To add insult to injury, those connections do not close, they simply hang there and do nothing. My server was sending out more than 15Mb/s of snake frames in the end, so I decided to pull the plug on this project (at least temporairly).

I did consider to have a black list, but it would be overly complicated to share this project with people who want to try it out as they need to be white-listed before they can use it (and they probably have dynamic IP anyway). Limiting the number of connections is also not a good idea, as those crawers would hang on my server while no one else can connect to it. I think the best solution is to drop the connection after a time limit, so that at even though I can't keep those crawers away, they can't stay connected and hog the server. I wonder how an actual server handle this kind of spammy behaviour.

Who are these @#$!&} people?

Well, after realizing some people are DDoS-ing the server for unknown reason. I decided to reinforce my program by adding some connection limits.

  1. One IP can only have one open connection.
  2. One connection only lasts one minute.
...
Blocked 122.164.247.112:58432 from connecting
Closing connection from: 122.164.247.112:58431
Connection from: 122.164.247.112:58594
Blocked 122.164.247.112:58656 from connecting
Blocked 122.164.247.112:58893 from connecting
Closing connection from: 122.164.247.112:58594
Connection from: 122.164.247.112:59306
...

I updated my println!() statements so that I can track who's connecting and more importantly, who's being rate limited. It seems like there are a lot of Indians IPs connecting to my server for no reason. Prior to adding this mechanism, I would have one of these kind of IPs creating more than one connection until the connection eventually breaks off. To whomever is doing this: Yo, it's not cool. Please go out and touch some grass.

Happy ending

Here's the CPU usage graph after adding these limitations. Normal CPU usage

2

A useful list of ANSI escape sequences

3

It seems like there's a Cursor trait that can traverse a vector back and forth source. I don't know yet how to implement this to my project.

4

Stolen Improved and adapted from Prime's "tmux-sessionizer". Playground is my idea, using fuzzy-finder was his.

<newer   earlier>