Blog post: http://blog.larrylv.com/write-a-tcp-stack-in-ruby/
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:
GET
google.comI 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.
The TCP three-way handshake is:
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][bpf-syntax]. 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][ack-number-article] to learn more.
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!
This is pretty easy, what we should do is:
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 = 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][my-teeceepee], this [tcp.rb][my-teeceepee-tcp-rb] file particularly, advice welcome!
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]: https://twitter.com/b0rk [julia-article]: http://jvns.ca/blog/2014/08/12/what-happens-if-you-write-a-tcp-stack-in-python/ [pocket]: https://getpocket.com [my-teeceepee]: https://github.com/larrylv/teeceepee [my-teeceepee-tcp-rb]: https://github.com/larrylv/teeceepee/blob/master/tcp.rb [julia-teeceepee]: https://github.com/jvns/teeceepee/ [packetfu]: https://github.com/packetfu/packetfu [bpf-syntax]: http://biot.com/capstats/bpf.html [ack-number-article]: http://packetlife.net/blog/2010/jun/7/understanding-tcp-sequence-acknowledgment-numbers/ [tcp-fsm]: http://www.tcpipguide.com/free/t_TCPOperationalOverviewandtheTCPFiniteStateMachineF-2.htm