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:

  1. open a raw network socket that lets me send TCP packets
  2. send a HTTP request to GET google.com
  3. get and parse a response
  4. 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
require 'packetfu'

config = PacketFu::Utils.whoami?

synpkt = PacketFu::TCPPacket.new(config: config, flavor: "Linux")
synpkt.ip_daddr      = "216.58.221.142" # ip of google.com
synpkt.tcp_dst       = 80               # port of google.com
synpkt.tcp_flags.syn = 1                # SYN
synpkt.recalc

synpkt.to_w

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
require 'packetfu'

cap = PacketFu::Capture.new(
  iface: config[:iface],
  start: true,
  filter: "tcp and src 216.58.221.142"
)

cap.stream.each do |pkt|
  # parse pkt and decide what to do next
  puts pkt
end

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'

tcpdump synack packet

What does the tcp[13]=18 argument mean? Here is the TCP Header Format:

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.

syn synack rst

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!

syn synack ack

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Listener
  def initialize(conn, config, dst_ip)
    @conn = conn
    @cap = PacketFu::Capture.new(
      iface: config[:iface],
      start: true,
      filter: "tcp and dst #{config[:ip_saddr]} and src #{dst_ip}"
    )
  end

  def listen
    @cap.stream.each do |pkt|
      state = @conn.handle(PacketFu::Packet.parse pkt)
      return if state == Teeceepee::CLOSED_STATE
    end
  end
end

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!