Ruby Write a TCP stack in Ruby

larrylv · 2015年11月27日 · 最后由 jasontang168 回复于 2016年01月05日 · 5396 次阅读

Blog post:

I read [Julia][julia-twitter]'s article [What happens if you write a TCP stack in Python?][julia-article] last year, and since then I really wanted to implement a TCP stack in Ruby language. I saved Julia's article in my [Pocket][pocket], then moved it to my browser bookmark folder, but never touched it again.

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 blog some implementation details.

My codes are here: [larrylv/teeceepee][my-teeceepee], the name teeceepee is borrowed from Julia's repo: [jvns/teeceepee][julia-teeceepee].

What we would like to do here is, 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
  3. get and parse a response
  4. celebrate!

I use a gem called [PacketFu][packetfu] 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 codes could be something like this:


With PacketFu, sending a packet is pretty simple:

require 'packetfu'

config = PacketFu::Utils.whoami?

synpkt = config, flavor: "Linux")
synpkt.ip_daddr      = "" # ip of
synpkt.tcp_dst       = 80               # port of
synpkt.tcp_flags.syn = 1                # SYN


For ack packet, just set pkt.tcp_flags.ack = 1.

As to read response, we need to filter packets from the interface.

require 'packetfu'

cap =
  iface: config[:iface],
  start: true,
  filter: "tcp and src"
) do |pkt|
  # parse pkt and decide what to do next
  puts pkt

The filter parameter for PacketFu::Capture is very interesting, it's a bpf filter and you could find the syntax documentation [here][bpf-syntax]. tcp and src means we would like to filter tcp packets from ip, which are exactly what we want to parse.

tcpdump also uses bpf filter to filter packets you want.

Let's say we just want the SYNACK packets. With tcpdump:

$ sudo tcpdump -i eth0 'tcp[13]=18'

What does tcp[13]=18 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.

Bpf is super powerful, and if you would like to know more examples, check tcpdump manpage.

In this step, I'm gonna ignore the part of how SEQ and ACK number work, you could check this [article][ack-number-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 think of how it would work, it didn't.

As the picture shown, after receiving SYNACK packet from, 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 the two ways both, but none of them worked for me (maybe my implementations are not good). I struggled to find a way to get my kernel just ignore the packet for, like the whole afternoon. Finally, when I used my nameserver's ip 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][tcp-fsm].

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 use a seperate thread to listen for packets from the destination IP, and after parsing the packet, it will send it to main thread and let it respond based on its state and packet tcp flags.

class Listener
  def initialize(conn, config, dst_ip)
    @conn = conn
    @cap =
      iface: config[:iface],
      start: true,
      filter: "tcp and dst #{config[:ip_saddr]} and src #{dst_ip}"

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

More codes on [GitHub][my-teeceepee], this [tcp.rb][my-teeceepee-tcp-rb] file particularly, advice welcome!

Sum Up

I'm really glad that I finally give this a try and get it worked. Couldn't say more thanks to Julia for her article and codes, I really learned a lot from them.

The last time I wrote packets related codes is about 5 or 6 years ago during collegue's networking lesson. Mostly with C language back then. Thanks to PacketFu gem, I really don't need to some dirty work.

Anyway, this is much more fun than I expected. The moment it worked I was really beyond happy. Try it with your preferred language, have some fun with me too!

[julia-twitter]: [julia-article]: [pocket]: [my-teeceepee]: [my-teeceepee-tcp-rb]: [julia-teeceepee]: [packetfu]: [bpf-syntax]: [ack-number-article]: [tcp-fsm]:

1 楼 已删除

Awesome. I used to be a router software developer several years ago. I can't wait to play another protocol in ruby now.


需要 登录 后方可回复, 如果你还没有账号请 注册新账号