Write a TCP Stack in Ruby
Last year since reading Julia’s article What happens if you write a TCP stack in Python?, and I have been planning to implement a TCP stack in Ruby language. But my procrastination has been winning since then.
This week, I decided to give it a try and it turns out to be really fun. In this post, I’m going to follow Julia’s steps and write down some implementation details.
My code is here: larrylv/teeceepee – the name teeceepee
is borrowed from Julia’s repo: jvns/teeceepee.
What we would like to do here is, and I quote from Julia’s blog:
- open a raw network socket that lets me send TCP packets
- send a HTTP request to
GET
google.com - get and parse a response
- celebrate!
In the implementation, a gem called PacketFu was used to read and write packets. I don’t think I could write the stack in such a short time without it, it’s really awesome.
Step 1: the TCP handshake
The TCP three-way handshake is:
- me: SYN
- google: SYNACK
- me: ACK
Pseduo code could be something like below:
send_syn_packet()
read_response()
send_ack_packet()
With PacketFu
, sending a packet is pretty simple:
|
|
For ack packet, we could just set pkt.tcp_flags.ack
to be 1
.
As to read response, we need to filter packets from the network interface in a different process/thread.
|
|
The filter
parameter for PacketFu::Capture
is pretty interesting. It’s a bpf filter
and you could learn more about the syntax in the documentation here. tcp and src 216.58.221.142
means that we’d like to filter tcp packets from ip 216.58.221.142
, which is the ip of google.com.
tcpdump
also uses bpf filter
to filter packets. Let’s say we only care about the SYNACK
packets. With tcpdump, we could do:
$ sudo tcpdump -i eth0 'tcp[13]=18'
What does the tcp[13]=18
argument mean? Here is the TCP Header Format:
We could see that the last six bits of fourteenth byte stand for tcp flags, and the previous two bits are both 0. So for SYNACK
packets, tcp flags would be: 010010
, the value would be 16 + 2 = 18. So tcp[13]=18
means only dumping SYNACK
packets.
Bpf is super powerful, and if you would like to know more examples, read tcpdump
manpage.
In this step, I’m gonna ignore the part of how SEQ and ACK number work, you could read this article to learn more.
Step 2: Kernel sends a RST after receiving the SYNACK packet
Julia described this in her article, instead of what we expect how it would work, it didn’t.
As the picture shown, after receiving SYNACK
packet from google.com, a RST
packet was sent (obviously not by us).
I will just quote Julia’s explaining here:
my Python/Ruby program: SYN
google: SYNACK
my kernel: lol wtf I never asked for this! RST!
my Python/Ruby program: ... :(
Julia used ARP spoofing
to pretend a different IP address, and someone commented about using tap/tun interfaces instead. I tried both, but none of them worked for me (maybe sth is wrong with my implementations). I failed to find a way to let the kernel ignore the packet. And after digging for a while, if I used the ip of my nameserver as src_ip
in the packet, it worked! I’m not exactly sure how this works, but it fixed my problem. I will leave this as a question and ask some network folks later.
Now, the three-way handshake works!
Step 3: get a web page!
This is pretty easy, what we should do is:
- send a packet containing a HTTP GET request
- listen for packets in response
- parse the packet
- decide what to do based on tcp flag
Implementing the last one will cost you some time probably, since it comes down to some parts of TCP Finite State Machine.
Constructing a HTTP Get
request is quite easy, just include GET some_path HTTP/1.0\r\nHost: hostname\r\n\r\n
in your PSH
packet.
I used a seperate thread to listen for packets from the destination IP, and after parsing the packet, it will send the result back to main thread and let it respond based on its state and packet tcp flags.
|
|
More code is on GitHub, and this tcp.rb file contains most of the logics.
Sum Up
I’m really glad that I finally give this a try and got it working. Thanks to Julia for her article and code, I really learned a lot from them.
The last time I wrote packet related codes was like 5 or 6 years ago during collegue’s networking class. Mostly with C language back then. Thanks to the PacketFu
gem, I could avoid most of the dirty work.
Anyway, this is much more fun than I expected. Try it with your preferred language, and have some fun too!