Reading List

The most recent articles from a list of feeds I subscribe to.

How to send raw network packets in Python with tun/tap

Hello!

Recently I’ve been working on a project where I implement a bunch of tiny toy working versions of computer networking protocols in Python without using any libraries, as a way to explain how computer networking works.

I’m still working on writing up that project, but today I wanted to talk about how to do the very first step: sending network packets in Python.

In this post we’re going to send a SYN packet (the first packet in a TCP connection) from a tiny Python program, and get a reply from example.com. All the code from this post is in this gist.

what’s a network packet?

A network packet is a byte string. For example, here’s the first packet in a TCP connection:

b'E\x00\x00,\x00\x01\x00\x00@\x06\x00\xc4\xc0\x00\x02\x02"\xc2\x95Cx\x0c\x00P\xf4p\x98\x8b\x00\x00\x00\x00`\x02\xff\xff\x18\xc6\x00\x00\x02\x04\x05\xb4'

I’m not going to talk about the structure of this byte string in this post (though I’ll say that this particular byte string has two parts: the first 20 bytes are the IP address part and the rest is the TCP part)

The point is that to send network packets, we need to be able to send and receive strings of bytes.

why tun/tap?

The problem with writing your own TCP implementation on Linux (or any operating system) is – the Linux kernel already has a TCP implementation!

So if you send out a SYN packet on your normal network interface to a host like example.com, here’s what will happen:

  1. you send a SYN packet to example.com
  2. example.com replies with a SYN ACK (so far so good!)
  3. the Linux kernel on your machine gets the SYN ACK, thinks “wtf?? I didn’t make this connection??”, and closes the connection
  4. you’re sad. no TCP connection for you.

I was talking to a friend about this problem a few years ago and he said “you should use tun/tap!“. It took quite a few hours to figure out how to do that though, which is why I’m writing this blog post :)

tun/tap gives you a “virtual network device”

The way I like to think of tun/tap is – imagine I have a tiny computer in my network which is sending and receiving network packets. But instead of it being a real computer, it’s just a Python program I wrote.

That explanation is honestly worse than I would like. I wish I understood exactly how tun/tap devices interfaced with the real Linux network stack but unfortunately I do not, so “virtual network device” is what you’re getting. Hopefully the code examples below will make all it a bit more clear.

tun vs tap

The system called “tun/tap” lets you create two kinds of network interfaces:

  • “tun”, which lets you set IP-layer packets
  • “tap”, which lets you set Ethernet-layer packets

We’re going to be using tun, because that’s what I could figure out how to get to work. It’s possible that tap would work too.

how to create a tun interface

Here’s how I created a tun interface with IP address 192.0.2.2.

sudo ip tuntap add name tun0 mode tun user $USER
sudo ip link set tun0 up
sudo ip addr add 192.0.2.1 peer 192.0.2.2 dev tun0

sudo iptables -t nat -A POSTROUTING -s 192.0.2.2 -j MASQUERADE
sudo iptables -A FORWARD -i tun0 -s 192.0.2.2 -j ACCEPT
sudo iptables -A FORWARD -o tun0 -d 192.0.2.2 -j ACCEPT

These commands do two things:

  1. Create the tun device with the IP 192.0.2.2 (and give your user access to write to it)
  2. set up iptables to proxy packets from that tun device to the internet using NAT

The iptables part is very important because otherwise the packets would only exist inside my computer and wouldn’t be sent to the internet, and what fun would that be?

I’m not going to explain this ip addr add command because I don’t understand it, I find ip to be very inscrutable and for now I’m resigned to just copying and pasting ip commands without fully understanding them. It does work though.

how to connect to the tun interface in Python

Here’s a function to open a tun interface, you call it like openTun('tun0'). I figured out how to write it by searching through the scapy source code for “tun”.

import struct
from fcntl import ioctl

def openTun(tunName):
    tun = open("/dev/net/tun", "r+b", buffering=0)
    LINUX_IFF_TUN = 0x0001
    LINUX_IFF_NO_PI = 0x1000
    LINUX_TUNSETIFF = 0x400454CA
    flags = LINUX_IFF_TUN | LINUX_IFF_NO_PI
    ifs = struct.pack("16sH22s", tunName, flags, b"")
    ioctl(tun, LINUX_TUNSETIFF, ifs)
    return tun

All this is doing is

  1. opening /dev/net/tun in binary mode
  2. calling an ioctl to tell Linux that we want a tun device, and that the one we want is called tun0 (or whatever tunName we’ve passed to the function).

Once it’s open, we can read from and write to it like any other file in Python.

let’s send a SYN packet!

Now that we have the openTun function, we can send a SYN packet!

Here’s what the Python code looks like, using the openTun function.

syn = b'E\x00\x00,\x00\x01\x00\x00@\x06\x00\xc4\xc0\x00\x02\x02"\xc2\x95Cx\x0c\x00P\xf4p\x98\x8b\x00\x00\x00\x00`\x02\xff\xff\x18\xc6\x00\x00\x02\x04\x05\xb4'
tun = openTun(b"tun0")
tun.write(syn)
reply = tun.read(1024)
print(repr(reply))

If I run this as sudo python3 syn.py, it prints out the reply from example.com:

b'E\x00\x00,\x00\x00@\x00&\x06\xda\xc4"\xc2\x95C\xc0\x00\x02\x02\x00Px\x0cyvL\x84\xf4p\x98\x8c`\x12\xfb\xe0W\xb5\x00\x00\x02\x04\x04\xd8'

Obviously this is a pretty silly way to send a SYN packet – a real implementation would have actual code to generate that byte string instead of hardcoding it, and we would parse the reply instead of just printing out the raw byte string. But I didn’t want to go into the structure of TCP in this post so that’s what we’re doing.

looking at these packets with tcpdump

If we run tcpdump on the tun0 interface, we can see the packet we sent and the answer from example.com:

$ sudo tcpdump -ni tun0
12:51:01.905933 IP 192.0.2.2.30732 > 34.194.149.67.80: Flags [S], seq 4101019787, win 65535, options [mss 1460], length 0
12:51:01.932178 IP 34.194.149.67.80 > 192.0.2.2.30732: Flags [S.], seq 3300937416, ack 4101019788, win 64480, options [mss 1240], length 0

Flags [S] is the SYN we sent, and Flags [S.] is the SYN ACK packet in response! We successfully communicated! And the Linux network stack didn’t interfere at all!

tcpdump also shows us how NAT is working

We can also run tcpdump on my real network interface (wlp3so, my wireless card), to see the packets being sent and received. We’ll pass -i wlp3s0 instead of -i tun0.

$ sudo tcpdump -ni wlp3s0 host 34.194.149.67
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on wlp3s0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
12:56:01.204382 IP 192.168.1.181.30732 > 34.194.149.67.80: Flags [S], seq 4101019787, win 65535, options [mss 1460], length 0
12:56:01.228239 IP 34.194.149.67.80 > 192.168.1.181.30732: Flags [S.], seq 144769955, ack 4101019788, win 64480, options [mss 1240], length 0
12:56:05.334427 IP 34.194.149.67.80 > 192.168.1.181.30732: Flags [S.], seq 144769955, ack 4101019788, win 64480, options [mss 1240], length 0
12:56:13.524973 IP 34.194.149.67.80 > 192.168.1.181.30732: Flags [S.], seq 144769955, ack 4101019788, win 64480, options [mss 1240], length 0
12:56:29.705007 IP 34.194.149.67.80 > 192.168.1.181.30732: Flags [S.], seq 144769955, ack 4101019788, win 64480, options [mss 1240], length 0

A couple of things to notice here:

  • The IP addresses are different – that IPtables rule from above has rewritten them from 192.0.2.2 to 192.168.1.181. This rewriting is called “network address translation”, or “NAT”.
  • We’re getting a bunch of replies from example.com – it’s doing an exponential backoff where it retries after 4 seconds, then 8 seconds, then 16 seconds. This is because we didn’t finish the TCP handshake – we just sent a SYN and left it hanging! There’s actually a type of DDOS attack like this called SYN flooding, but just sending one or two SYN packets isn’t a big deal.
  • I had to add host 34.194.149.67 because there are a lot of TCP packets being sent on my real wifi connection so I needed to ignore those

I’m not totally sure why we see more SYN replies on wlp3s0 than on tun0, my guess is that it’s because we only read 1 reply in our Python program.

this is pretty easy and really reliable

The last time I tried to implement TCP in Python I did it with something called “ARP spoofing”. I won’t talk about that here (there are some posts about it on this blog back in 2013), but this way is a lot more reliable.

And ARP spoofing is kind of a sketchy thing to do on a network you don’t own.

here’s the code

I put all the code from this blog post in this gist, if you want to try it yourself, you can run

bash setup.sh # needs to run as root, has lots of `sudo` commands
python3 syn.py # runs as a regular user

It only works on Linux, but I think there’s a way to set up tun/tap on Mac too.

a plug for scapy

I’ll close with a plug for scapy here: it’s a really great Python networking library for doing this kind of experimentation without writing all the code yourself.

This post is about writing all the code yourself though so I won’t say more about it than that.

Some ways to get better at debugging

Hello! I’ve been working on writing a zine about debugging for a while (here’s an early draft of the table of contents).

As part of that I thought it might be fun to read some academic papers about debugging, and last week Greg Wilson sent me some papers about academic research into debugging.

One of those papers (Towards a framework for teaching debugging [paywalled]) had a categorization I really liked of the different kinds of knowledge/skills we need to debug effectively. It comes from another more general paper on troubleshooting: Learning to Troubleshoot: A New Theory-Based Design Architecture.

I thought the categorization was a very useful structure for thinking about how to get better at debugging, so I’ve reframed the five categories in the paper into actions you can take to get better at debugging.

Here they are:

1. learn the codebase

To debug some code, you need to understand the codebase you’re working with. This seems kind of obvious (of course you can’t debug code without understanding how it works!).

This kind of learning happens pretty naturally over time, and actually debugging is also one of the best ways to learn how a new codebase works – seeing how something breaks helps you learn a lot about how it works.

The paper calls this “System Knowledge”.

2. learn the system

The paper mentions that you need to understand the programming language, but I think there’s more to it than that – to fix bugs, often you need to learn a lot about the broader environment than just the language.

For example, if you’re a backend web developer, some “system” knowledge you might need includes:

  • how HTTP caching works
  • CORS
  • how database transactions work

I find that I often have to be a bit more intentional about learning systemic things like this – I need to actually take the time to look them up and read about them.

The paper calls this “Domain Knowledge”.

3. learn your tools

There are lots of debugging tools out there, for example:

  • debuggers (gdb etc)
  • browser developer tools
  • profilers
  • strace / ltrace
  • tcpdump / wireshark
  • core dumps
  • and even basic things like error messages (how do you read them properly)

I’ve written a lot about debugging tools on this blog, and definitely learning these tools has made a huge difference to me.

The paper calls this “Procedural Knowledge”.

4. learn strategies

This is the fuzziest category, we all have a lot of strategies and heuristics we pick up along the way for how to debug efficiently. For example:

  • writing a unit test
  • writing a tiny standalone program to reproduce the bug
  • finding a working version of the code and seeing what changed
  • printing out a million things
  • adding extra logging
  • taking a break
  • explaining the bug to a friend and then figuring out what’s wrong halfway through
  • looking through the github issues to see if anything matches

I’ve been thinking a lot about this category while writing the zine, but I want to keep this post short so I won’t say more about it here.

The paper calls this “Strategic Knowledge”.

5. get experience

The last category is “experience”. The paper has a really funny comment about this:

Their findings did not show a significant difference in the strategies employed by the novices and experts. Experts simply formed more correct hypotheses and were more efficient at finding the fault. The authors suspect that this result is due to the difference in the programming experience between novices and experts.

This really resonated with me – I’ve had SO MANY bugs that were really frustrating and difficult the first time I ran into them, and very straightforward the fifth or tenth or 20th time.

This also feels like one of the most straightforward categories of knowledge to acquire to me – all you need to do is investigate a million bugs, which is our whole life as programmers anyway :). It takes a long time but I feel like it happens pretty naturally.

The paper calls this “Experiential Knowledge”.

that’s all!

I’m going to keep this post short, I just really liked this categorization and wanted to share it.

A toy remote login server

Hello! The other day we talked about what happened when you press a key in your terminal.

As a followup, I thought it might be fun to implement a program that’s like a tiny ssh server, but without the security. You can find it on github here, and I’ll explain how it works in this blog post.

the goal: “ssh” to a remote computer

Our goal is to be able to login to a remote computer and run commands, like you do with SSH or telnet.

The biggest difference between this program and SSH is that there’s literally no security (not even a password) – anyone who can make a TCP connection to the server can get a shell and run commands.

Obviously this is not a useful program in real life, but our goal is to learn a little more about how terminals works, not to write a useful program.

(I will run a version of it on the public internet for the next week though, you can see how to connect to it at the end of this blog post)

let’s start with the server!

We’re also going to write a client, but the server is the interesting part, so let’s start there. We’re going to write a server that listens on a TCP port (I picked 7777) and creates remote terminals for any client that connects to it to use.

When the server receives a new connection it needs to:

  1. create a pseudoterminal for the client to use
  2. start a bash shell process for the client to use
  3. connect bash to the pseudoterminal
  4. continuously copy information back and forth between the TCP connection and the pseudoterminal

I just said the word “pseudoterminal” a lot, so let’s talk about what that means.

what’s a pseudoterminal?

Okay, what the heck is a pseudoterminal?

A pseudoterminal is a lot like a bidirectional pipe or a socket – you have two ends, and they can both send and receive information. You can read more about the information being sent and received in what happens if you press a key in your terminal

Basically the idea is that on one end, we have a TCP connection, and on the other end, we have a bash shell. So we need to hook one part of the pseudoterminal up to the TCP connection and the other end to bash.

The two parts of the pseudoterminal are called:

  • the “pseudoterminal master”. This is the end we’re going to hook up to the TCP connection.
  • the “slave pseudoterminal device”. We’re going to set our bash shell’s stdout, stderr, and stdin to this.

Once they’re conected, we can communicate with bash over our TCP connection and we’ll have a remote shell!

why do we need this “pseudoterminal” thing anyway?

You might be wondering – Julia, if a pseudoterminal is kind of like a socket, why can’t we just set our bash shell’s stdout / stderr / stdin to the TCP socket?

And you can! We could write a TCP connection handler like this that does exactly that, it’s not a lot of code (server-notty.go).


func handle(conn net.Conn) {
	tty, _ := conn.(*net.TCPConn).File()
	// start bash with tcp connection as stdin/stdout/stderr
	cmd := exec.Command("bash")
	cmd.Stdin = tty
	cmd.Stdout = tty
	cmd.Stderr = tty
	cmd.Start()
}

It even kind of works – if we connect to it with nc localhost 7778, we can run commands and look at their output.

But there are a few problems. I’m not going to list all of them, just two.

problem 1: Ctrl + C doesn’t work

The way Ctrl + C works in a remote login session is

  • you press ctrl + c
  • That gets translated to 0x03 and sent through the TCP connection
  • The terminal receives it
  • the Linux kernel on the other end notes “hey, that was a Ctrl + C!”
  • Linux sends a SIGINT to the appropriate process (more on what the “appropriate process” is exactly later)

If the “terminal” is just a TCP connection, this doesn’t work, because when you send 0x04 to a TCP connection, Linux won’t magically send SIGINT to any process.

problem 2: top doesn’t work

When I try to run top in this shell, I get the error message top: failed tty get. If we strace it, we see this system call:

ioctl(2, TCGETS, 0x7ffec4e68d60)        = -1 ENOTTY (Inappropriate ioctl for device)

So top is running an ioctl on its output file descriptor (2) to get some information about the terminal. But Linux is like “hey, this isn’t a terminal!” and returns an error.

There are a bunch of other things that go wrong, but hopefully at this point you’re convinced that we actually need to set bash’s stdout/stderr to be a terminal, not some other thing like a socket.

So let’s start looking at the server code and see what creating a pseudoterminal actually looks like.

step 1: create a pseudoterminal

Here’s some Go code to create a pseudoterminal on Linux. This is copied from github.com/creack/pty, but I removed some of the error handling to make the logic a bit easier to follow:

pty, _ := os.OpenFile("/dev/ptmx", os.O_RDWR, 0)
sname := ptsname(p)
unlockpt(p)
tty, _ := os.OpenFile(sname, os.O_RDWR|syscall.O_NOCTTY, 0)

In English, what we’re doing is:

  • open /dev/ptmx to get the “pseudoterminal master” Again, that’s the part we’re going to hook up to the TCP connection
  • get the filename of the “slave pseudoterminal device”, which is going to be /dev/pts/13 or something.
  • “unlock” the pseudoterminal so that we can use it. I have no idea what the point of this is (why is it locked to begin with?) but you have to do it for some reason
  • open /dev/pts/13 (or whatever number we got from ptsname) to get the “slave pseudoterminal device”

What do those ptsname and unlockpt functions do? They just make some ioctl system calls to the Linux kernel. All of the communication with the Linux kernel about terminals seems to be through various ioctl system calls.

Here’s the code, it’s pretty short: (again, I just copied it from creack/pty)

func ptsname(f *os.File) string {
	var n uint32
	ioctl(f.Fd(), syscall.TIOCGPTN, uintptr(unsafe.Pointer(&n)))
	return "/dev/pts/" + strconv.Itoa(int(n))
}

func unlockpt(f *os.File) {
	var u int32
	// use TIOCSPTLCK with a pointer to zero to clear the lock
	ioctl(f.Fd(), syscall.TIOCSPTLCK, uintptr(unsafe.Pointer(&u)))
}

step 2: hook the pseudoterminal up to bash

The next thing we have to do is connect the pseudoterminal to bash. Luckily, that’s really easy – here’s the Go code for it! We just need to start a new process and set the stdin, stdout, and stderr to tty.

cmd := exec.Command("bash")
cmd.Stdin = tty
cmd.Stdout = tty
cmd.Stderr = tty
cmd.SysProcAttr = &syscall.SysProcAttr{
  Setsid: true,
}
cmd.Start()

Easy! Though – why do we need this Setsid: true thing, you might ask? Well, I tried commenting out that code to see what went wrong. It turns out that what goes wrong is – Ctrl + C doesn’t work anymore!

Setsid: true creates a new session for the new bash process. But why does that make Ctrl + C work? How does Linux know which process to send SIGINT to when you press Ctrl + C, and what does that have to do with sessions?

how does Linux know which process to send Ctrl + C to?

I found this pretty confusing, so I reached for my favourite book for learning about this kind of thing: the linux programming interface, specifically chapter 34 on process groups and sessions.

That chapter contains a few key facts: (#3, #4, and #5 are direct quotes from the book)

  1. Every process has a session id and a process group id (which may or may not be the same as its PID)
  2. A session is made up of multiple process groups
  3. All of the processes in a session share a single controlling terminal.
  4. A terminal may be the controlling terminal of at most one session.
  5. At any point in time, one of the process groups in a session is the foreground process group for the terminal, and the others are background process groups.
  6. When you press Ctrl+C in a terminal, SIGINT gets sent to all the processes in the foreground process group

What’s a process group? Well, my understanding is that:

  • processes in the same pipe x | y | z are in the same process group
  • processes you start on the same shell line (x && y && z) are in the same process group
  • child processes are by default in the same process group, unless you explicitly decide otherwise

I didn’t know most of this (I had no idea processes had a session ID!) so this was kind of a lot to absorb. I tried to draw a sketchy ASCII art diagram of the situation

(maybe)  terminal --- session --- process group --- process
                               |                 |- process
                               |                 |- process
                               |- process group 
                               |
                               |- process group 

So when we press Ctrl+C in a terminal, here’s what I think happens:

  • \x04 gets written to the “pseudoterminal master” of a terminal
  • Linux finds the session for that terminal (if it exists)
  • Linux find the foreground process group for that session
  • Linux sends SIGINT

If we don’t create a new session for our new bash process, our new pseudoterminal actually won’t have any session associated with it, so nothing happens when we press Ctrl+C. But if we do create a new session, then the new pseudoterminal will have the new session associated with it.

how to get a list of all your sessions

As a quick aside, if you want to get a list of all the sessions on your Linux machine, grouped by session, you can run:

$ ps -eo user,pid,pgid,sess,cmd | sort -k3

This includes the PID, process group ID, and session ID. As an example of the output, here are the two processes in the pipeline:

bork       58080   58080   57922 ps -eo user,pid,pgid,sess,cmd
bork       58081   58080   57922 sort -k3

You can see that they share the same process group ID and session ID, but of course they have different PIDs.

That was kind of a lot but that’s all we’re going to say about sessions and process groups in this post. Let’s keep going!

step 3: set the window size

We need to tell the terminal how big to be!

Again, I just copied this from creack/pty. I decided to hardcode the size to 80x24.

Setsize(tty, &Winsize{
		Cols: 80,
		Rows: 24,
	})

Like with getting the terminal’s pts filename and unlocking it, setting the size is just one ioctl system call:

func Setsize(t *os.File, ws *Winsize) {
	ioctl(t.Fd(), syscall.TIOCSWINSZ, uintptr(unsafe.Pointer(ws)))
}

Pretty simple! We could do something smarter and get the real window size, but I’m too lazy.

step 4: copy information between the TCP connection and the pseudoterminal

As a reminder, our rough steps to set up this remote login server were:

  1. create a pseudoterminal for the client to use
  2. start a bash shell process
  3. connect bash to the pseudoterminal
  4. continuously copy information back and forth between the TCP connection and the pseudoterminal

We’ve done 1, 2, and 3, now we just need to ferry information between the TCP connection and the pseudoterminal.

There are two io.Copy calls, one to copy the input from the tcp connection, and one to copy the output to the TCP connection. Here’s what the code looks like:

	go func() {
			io.Copy(pty, conn)
	}()
  io.Copy(conn, pty)

The first one is in a goroutine just so they can both run in parallel.

Pretty simple!

step 5: exit when we’re done

I also added a little bit of code to close the TCP connection when the command exits

go func() {
  cmd.Wait()
  conn.Close()
}()

And that’s it for the server! You can see all of the Go code here: server.go.

next: write a client

Next, we have to write a client. This is a lot easier than the server because we don’t need to do quite as much terminal setup. There are just 3 steps:

  1. Put the terminal into raw mode
  2. copy stdin/stdout to the TCP connection
  3. reset the terminal

client step 1: put the terminal into “raw” mode

We need to put the client terminal into “raw” mode so that every time you press a key, it gets sent to the TCP connection immediately. If we don’t do this, everything will only get sent when you press enter.

“Raw mode” isn’t actually a single thing, it’s a bunch of flags that you want to turn off. There’s a good tutorial explaining all the flags we have to turn off called Entering raw mode.

Like everything else with terminals, this requires ioctl system calls. In this case we get the terminal’s current settings, modify them, and save the old settings so that we can restore them later.

I figured out how to do this in Go by going to https://grep.app and typing in syscall.TCSETS to find some other Go code that was doing the same thing.

func MakeRaw(fd uintptr) syscall.Termios {
	// from https://github.com/getlantern/lantern/blob/devel/archive/src/golang.org/x/crypto/ssh/terminal/util.go
	var oldState syscall.Termios
	ioctl(fd, syscall.TCGETS, uintptr(unsafe.Pointer(&oldState)))

	newState := oldState
	newState.Iflag &^= syscall.ISTRIP | syscall.INLCR | syscall.ICRNL | syscall.IGNCR | syscall.IXON | syscall.IXOFF
	newState.Lflag &^= syscall.ECHO | syscall.ICANON | syscall.ISIG
	ioctl(fd, syscall.TCSETS, uintptr(unsafe.Pointer(&newState)))
	return oldState
}

client step 2: copy stdin/stdout to the TCP connection

This is exactly like what we did with the server. It’s very little code:

go func() {
		io.Copy(conn, os.Stdin)
	}()
	io.Copy(os.Stdout, conn)

client step 3: restore the terminal’s state

We can put the terminal back into the mode it started in like this (another ioctl!):

func Restore(fd uintptr, oldState syscall.Termios) {
	ioctl(fd, syscall.TCSETS, uintptr(unsafe.Pointer(&oldState)))
}

we did it!

We have written a tiny remote login server that lets anyone log in! Hooray!

Obviously this has zero security so I’m not going to talk about that aspect.

it’s running on the public internet! you can try it out!

For the next week or so I’m going to run a demo of this on the internet at tetris.jvns.ca. It runs tetris instead of a shell because I wanted to avoid abuse, but if you want to try it with a shell you can run it on your own computer :).

If you want to try it out, you can use netcat as a client instead of the custom Go client program we wrote, because copying information to/from a TCP connection is what netcat does. Here’s how:

stty raw -echo && nc tetris.jvns.ca 7777 && stty sane

This will let you play a terminal tetris game called tint.

You can also use the client.go program and run go run client.go tetris.jvns.ca 7777.

this is not a good protocol

This protocol where we just copy bytes from the TCP connection to the terminal and nothing else is not good because it doesn’t allow us to send over information information like the terminal or the actual window size of the terminal.

I thought about implementing telnet’s protocol so that we could use telnet as a client, but I didn’t feel like figuring out how telnet works so I didn’t. (the server 30% works with telnet as is, but a lot of things are broken, I don’t quite know why, and I didn’t feel like figuring it out)

it’ll mess up your terminal a bit

As a warning: using this server to play tetris will probably mess up your terminal a bit because it sets the window size to 80x24. To fix that I just closed the terminal tab after running that command.

If we wanted to fix this for real, we’d need to restore the window size after we’re done, but then we’d need a slightly more real protocol than “just blindly copy bytes back and forth with TCP” and I didn’t feel like doing that.

Also it sometimes takes a second to disconnect after the program exits for some reason, I’m not sure why that is.

other tiny projects

That’s all! There are a couple of other similar toy implementations of programs I’ve written here:

What happens when you press a key in your terminal?

I’ve been confused about what’s going on with terminals for a long time.

But this past week I was using xterm.js to display an interactive terminal in a browser and I finally thought to ask a pretty basic question: when you press a key on your keyboard in a terminal (like Delete, or Escape, or a), which bytes get sent?

As usual we’ll answer that question by doing some experiments and seeing what happens :)

remote terminals are very old technology

First, I want to say that displaying a terminal in the browser with xterm.js might seem like a New Thing, but it’s really not. In the 70s, computers were expensive. So many employees at an institution would share a single computer, and each person could have their own “terminal” to that computer.

For example, here’s a photo of a VT100 terminal from the 70s or 80s. This looks like it could be a computer (it’s kind of big!), but it’s not – it just displays whatever information the actual computer sends it.

DEC VT100 terminal

Of course, in the 70s they didn’t use websockets for this, but the information being sent back and forth is more or less the same as it was then.

(the terminal in that photo is from the Living Computer Museum in Seattle which I got to visit once and write FizzBuzz in ed on a very old Unix system, so it’s possible that I’ve actually used that machine or one of its siblings! I really hope the Living Computer Museum opens again, it’s very cool to get to play with old computers.)

what information gets sent?

It’s obvious that if you want to connect to a remote computer (with ssh or using xterm.js and a websocket, or anything else), then some information needs to be sent between the client and the server.

Specifically:

  • the client needs to send the keystrokes that the user typed in (like ls -l)
  • the server needs to tell the client what to display on the screen

Let’s look at a real program that’s running a remote terminal in a browser and see what information gets sent back and forth!

we’ll use goterm to experiment

I found this tiny program on GitHub called goterm that runs a Go server that lets you interact with a terminal in the browser using xterm.js. This program is very insecure but it’s simple and great for learning.

I forked it to make it work with the latest xterm.js, since it was last updated 6 years ago. Then I added some logging statements to print out every time bytes are sent/received over the websocket.

Let’s look at sent and received during a few different terminal interactions!

example: ls

First, let’s run ls. Here’s what I see on the xterm.js terminal:

bork@kiwi:/play$ ls
file
bork@kiwi:/play$

and here’s what gets sent and received: (in my code, I log sent: [bytes] every time the client sends bytes and recv: [bytes] every time it receives bytes from the server)

sent: "l"
recv: "l"
sent: "s"
recv: "s"
sent: "\r"
recv: "\r\n\x1b[?2004l\r"
recv: "file\r\n"
recv: "\x1b[?2004hbork@kiwi:/play$ "

I noticed 3 things in this output:

  1. Echoing: The client sends l and then immediately receives an l sent back. I guess the idea here is that the client is really dumb – it doesn’t know that when I type an l, I want an l to be echoed back to the screen. It has to be told explicitly by the server process to display it.
  2. The newline: when I press enter, it sends a \r (carriage return) symbol and not a \n (newline)
  3. Escape sequences: \x1b is the ASCII escape character, so \x1b[?2004h is telling the terminal to display something or other. I think this is a colour sequence but I’m not sure. We’ll talk a little more about escape sequences later.

Okay, now let’s do something slightly more complicated.

example: Ctrl+C

Next, let’s see what happens when we interrupt a process with Ctrl+C. Here’s what I see in my terminal:

bork@kiwi:/play$ cat
^C
bork@kiwi:/play$

And here’s what the client sends and receives.

sent: "c"
recv: "c"
sent: "a"
recv: "a"
sent: "t"
recv: "t"
sent: "\r"
recv: "\r\n\x1b[?2004l\r"
sent: "\x03"
recv: "^C"
recv: "\r\n"
recv: "\x1b[?2004h"
recv: "bork@kiwi:/play$ "

When I press Ctrl+C, the client sends \x03. If I look up an ASCII table, \x03 is “End of Text”, which seems reasonable. I thought this was really cool because I’ve always been a bit confused about how Ctrl+C works – it’s good to know that it’s just sending an \x03 character.

I believe the reason cat gets interrupted when we press Ctrl+C is that the Linux kernel on the server side receives this \x03 character, recognizes that it means “interrupt”, and then sends a SIGINT to the process that owns the pseudoterminal’s process group. So it’s handled in the kernel and not in userspace.

example: Ctrl+D

Let’s try the exact same thing, except with Ctrl+D. Here’s what I see in my terminal:

bork@kiwi:/play$ cat
bork@kiwi:/play$

And here’s what gets sent and received:

sent: "c"
recv: "c"
sent: "a"
recv: "a"
sent: "t"
recv: "t"
sent: "\r"
recv: "\r\n\x1b[?2004l\r"
sent: "\x04"
recv: "\x1b[?2004h"
recv: "bork@kiwi:/play$ "

It’s very similar to Ctrl+C, except that \x04 gets sent instead of \x03. Cool! \x04 corresponds to ASCII “End of Transmission”.

what about Ctrl + another letter?

Next I got curious about – if I send Ctrl+e, what byte gets sent?

It turns out that it’s literally just the number of that letter in the alphabet, like this:

  • Ctrl+a => 1
  • Ctrl+b => 2
  • Ctrl+c => 3
  • Ctrl+d => 4
  • Ctrl+z => 26

Also, Ctrl+Shift+b does the exact same thing as Ctrl+b (it writes 0x2).

What about other keys on the keyboard? Here’s what they map to:

  • Tab -> 0x9 (same as Ctrl+I, since I is the 9th letter)
  • Escape -> \x1b
  • Backspace -> \x7f
  • Home -> \x1b[H
  • End: \x1b[F
  • Print Screen: \x1b\x5b\x31\x3b\x35\x41
  • Insert: \x1b\x5b\x32\x7e
  • Delete -> \x1b\x5b\x33\x7e
  • My Meta key does nothing at all

What about Alt? From my experimenting (and some Googling), it seems like Alt is literally the same as “Escape”, except that pressing Alt by itself doesn’t send any characters to the terminal and pressing Escape by itself does. So:

  • alt + d => \x1bd (and the same for every other letter)
  • alt + shift + d => \x1bD (and the same for every other letter)
  • etcetera

Let’s look at one more example!

example: nano

Here’s what gets sent and received when I run the text editor nano:

recv: "\r\x1b[Kbork@kiwi:/play$ "
sent: "n" [[]byte{0x6e}]
recv: "n"
sent: "a" [[]byte{0x61}]
recv: "a"
sent: "n" [[]byte{0x6e}]
recv: "n"
sent: "o" [[]byte{0x6f}]
recv: "o"
sent: "\r" [[]byte{0xd}]
recv: "\r\n\x1b[?2004l\r"
recv: "\x1b[?2004h"
recv: "\x1b[?1049h\x1b[22;0;0t\x1b[1;16r\x1b(B\x1b[m\x1b[4l\x1b[?7h\x1b[39;49m\x1b[?1h\x1b=\x1b[?1h\x1b=\x1b[?25l"
recv: "\x1b[39;49m\x1b(B\x1b[m\x1b[H\x1b[2J"
recv: "\x1b(B\x1b[0;7m  GNU nano 6.2 \x1b[44bNew Buffer \x1b[53b \x1b[1;123H\x1b(B\x1b[m\x1b[14;38H\x1b(B\x1b[0;7m[ Welcome to nano.  For basic help, type Ctrl+G. ]\x1b(B\x1b[m\r\x1b[15d\x1b(B\x1b[0;7m^G\x1b(B\x1b[m Help\x1b[15;16H\x1b(B\x1b[0;7m^O\x1b(B\x1b[m Write Out   \x1b(B\x1b[0;7m^W\x1b(B\x1b[m Where Is    \x1b(B\x1b[0;7m^K\x1b(B\x1b[m Cut\x1b[15;61H"

You can see some text from the UI in there like “GNU nano 6.2”, and these \x1b[27m things are escape sequences. Let’s talk about escape sequences a bit!

ANSI escape sequences

These \x1b[ things above that nano is sending the client are called “escape sequences” or “escape codes”. This is because they all start with \x1b, the “escape” character. . They change the cursor’s position, make text bold or underlined, change colours, etc. Wikipedia has some history if you’re interested.

As a simple example: if you run

echo -e '\e[0;31mhi\e[0m there'

in your terminal, it’ll print out “hi there” where “hi” is in red and “there” is in black. This page has some nice examples of escape codes for colors and formatting.

I think there are a few different standards for escape codes, but my understanding is that the most common set of escape codes that people use on Unix come from the VT100 (that old terminal in the picture at the top of the blog post), and hasn’t really changed much in the last 40 years.

Escape codes are why your terminal can get messed up if you cat a bunch of binary to your screen – usually you’ll end up accidentally printing a bunch of random escape codes which will mess up your terminal – there’s bound to be a 0x1b byte in there somewhere if you cat enough binary to your terminal.

can you type in escape sequences manually?

A few sections back, we talked about how the Home key maps to \x1b[H. Those 3 bytes are Escape + [ + H (because Escape is \x1b).

And if I manually type Escape, then [, then H in the xterm.js terminal, I end up at the beginning of the line, exactly the same as if I’d pressed Home.

I noticed that this didn’t work in fish on my computer though – if I typed Escape and then [, it just printed out [ instead of letting me continue the escape sequence. I asked my friend Jesse who has written a bunch of Rust terminal code about this and Jesse told me that a lot of programs implement a timeout for escape codes – if you don’t press another key after some minimum amount of time, it’ll decide that it’s actually not an escape code anymore.

Apparently this is configurable in fish with fish_escape_delay_ms, so I ran set fish_escape_delay_ms 1000 and then I was able to type in escape codes by hand. Cool!

terminal encoding is kind of weird

I want to pause here for a minute here and say that the way the keys you get pressed get mapped to bytes is pretty weird. Like, if we were designing the way keys are encoded from scratch today, we would probably not set it up so that:

  • Ctrl + a does the exact same thing as Ctrl + Shift + a
  • Alt is the same as Escape
  • control sequences (like colours / moving the cursor around) use the same byte as the Escape key, so that you need to rely on timing to determine if it was a control sequence of the user just meant to press Escape

But all of this was designed in the 70s or 80s or something and then needed to stay the same forever for backwards compatibility, so that’s what we get :)

changing window size

Not everything you can do in a terminal happens via sending bytes back and forth. For example, when the terminal gets resized, we have to tell Linux that the window size has changed in a different way.

Here’s what the Go code in goterm to do that looks like:

syscall.Syscall(
    syscall.SYS_IOCTL,
    tty.Fd(),
    syscall.TIOCSWINSZ,
    uintptr(unsafe.Pointer(&resizeMessage)),
)

This is using the ioctl system call. My understanding of ioctl is that it’s a system call for a bunch of random stuff that isn’t covered by other system calls, generally related to IO I guess.

syscall.TIOCSWINSZ is an integer constant which which tells ioctl which particular thing we want it to to in this case (change the window size of a terminal).

this is also how xterm works

In this post we’ve been talking about remote terminals, where the client and the server are on different computers. But actually if you use a terminal emulator like xterm, all of this works the exact same way, it’s just harder to notice because the bytes aren’t being sent over a network connection.

that’s all for now!

There’s definitely a lot more to know about terminals (we could talk more about colours, or raw vs cooked mode, or unicode support, or the Linux pseudoterminal interface) but I’ll stop here because it’s 10pm, this is getting kind of long, and I think my brain cannot handle more new information about terminals today.

Thanks to Jesse Luehrs for answering a billion of my questions about terminals, all the mistakes are mine :)

Monitoring tiny web services

Hello! I’ve started to run a few more servers recently (nginx playground, mess with dns, dns lookup), so I’ve been thinking about monitoring.

It wasn’t initially totally obvious to me how to monitor these websites, so I wanted to quickly write up what how I did it.

I’m not going to talk about how to monitor Big Serious Mission Critical websites at all, only tiny unimportant websites.

goal: spend approximately 0 time on operations

I want the sites to mostly work, but I also want to spend approximately 0% of my time on the ongoing operations.

I was initially very wary of running servers at all because at my last job I was on a 247 oncall rotation for some critical services, and in my mind “being responsible for servers” meant “get woken up at 2am to fix the servers” and “have lots of complicated dashboards”.

So for a while I only made static websites so that I wouldn’t have to think about servers.

But eventually I realized that any server I was going to write was going to be very low stakes, if they occasionally go down for 2 hours it’s no big deal, and I could just set up some very simple monitoring to help keep them running.

not having monitoring sucks

At first I didn’t set up any monitoring for my servers at all. This had the extremely predictable outcome of – sometimes the site broke, and I didn’t find out about it until somebody told me!

step 1: an uptime checker

The first step was to set up an uptime checker. There are tons of these out there, the ones I’m using right now are updown.io and uptime robot. I like updown’s user interface and pricing structure more (it’s per request instead of a monthly fee), but uptime robot has a more generous free tier.

These

  1. check that the site is up
  2. if it goes down, it emails me

I find that email notifications are a good level for me, I’ll find out pretty quickly if the site goes down but it doesn’t wake me up or anything.

step 2: an end-to-end healthcheck

Next, let’s talk about what “check that the site is up” actually means.

At first I just made one of my healthcheck endpoints a function that returned 200 OK no matter what.

This is kind of useful – it told me that the server was on!

But unsurprisingly I ran into problems because it wasn’t checking that the API was actually working – sometimes the healthcheck succeeded even though the rest of the service had actually gotten into a bad state.

So I updated it to actually make a real API request and make sure it succeeded.

All of my services do very few things (the nginx playground has just 1 endpoint), so it’s pretty easy to set up a healthcheck that actually runs through most of the actions the service is supposed to do.

Here’s what the end-to-end healthcheck handler for the nginx playground looks like. It’s very basic: it just makes another POST request (to itself) and checks if that request succeeds or fails.

func healthHandler(w http.ResponseWriter, r *http.Request) {
	// make a request to localhost:8080 with `healthcheckJSON` as the body
	// if it works, return 200
	// if it doesn't, return 500
	client := http.Client{}
	resp, err := client.Post("http://localhost:8080/", "application/json", strings.NewReader(healthcheckJSON))
	if err != nil {
		log.Println(err)
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
	if resp.StatusCode != http.StatusOK {
		log.Println(resp.StatusCode)
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
	w.WriteHeader(http.StatusOK)
}

healthcheck frequency: hourly

Right now I’m running most of my healthchecks every hour, and some every 30 minutes.

I run them hourly because updown.io’s pricing is per healthcheck, I’m monitoring 18 different URLs, and I wanted to keep my healthcheck budget pretty minimal at $5/year.

Taking an hour to find out that one of these websites has gone down seems ok to me – if there is a problem there’s no guarantee I’ll get to fixing it all that quickly anyway.

If it were free to run them more often I’d probably run them every 5-10 minutes instead.

step 3: automatically restart if the healthcheck fails

Some of my websites are on fly.io, and fly has a pretty standard feature where I can configure a HTTP healthcheck for a service and restart the service if the healthcheck starts failing.

“Restart a lot” is a very useful strategy to paper over bugs that I haven’t gotten around to fixing yet – for a while the nginx playground had a process leak where nginx processes weren’t getting terminated, so the server kept running out of RAM.

With the healthcheck, the result of this was that every day or so, this would happen:

  • the server ran out of RAM
  • the healthcheck started failing
  • it get restarted
  • everything was fine again
  • repeat the whole saga again some number of hours later

Eventually I got around to actually fixing the process leak, but it was nice to have a workaround in place that could keep things running while I was procrastinating fixing the bug.

These healthchecks to decide whether to restart the service run more often: every 5 minutes or so.

this is not the best way to monitor Big Services

This is probably obvious and I said this already at the beginning, but “write one HTTP healthcheck” is not the best approach for monitoring a large complex service. But I won’t go into that because that’s not what this post is about.

it’s been working well so far!

I originally wrote this post 3 months ago in April, but I waited until now to publish it to make sure that the whole setup was working.

It’s made a pretty big difference – before I was having some very silly downtime problems, and now for the last few months the sites have been up 99.95% of the time!