Ruby Write a TCP stack in Ruby

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

Blog post: http://blog.larrylv.com/write-a-tcp-stack-in-ruby/

I read Julia's article What happens if you write a TCP stack in Python? last year, and since then I really wanted to implement a TCP stack in Ruby language. I saved Julia's article in my 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, the name teeceepee is borrowed from Julia's repo: jvns/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 google.com
  3. get and parse a response
  4. celebrate!

I use a gem called 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:

send_syn_packet
read_response
send_ack_packet

With PacketFu, sending a packet is pretty simple:

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, just set pkt.tcp_flags.ack = 1.

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

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 very interesting, it's a bpf filter and you could find the syntax documentation here. tcp and src 216.58.221.142 means we would like to filter tcp packets from ip 216.58.221.142, 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 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 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 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.

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 = 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 codes on GitHub, this 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!

共收到 4 条回复
96

占东学长棒棒哒~

A908ae

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

115

不错 👍

18464

这个好,正想找呢

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