A case study in making a raw IP socket

A case study in making a raw IP socket

This page is a demonstration of exactly how to make a raw TCP/IP socket, and form the IP packets necessary to use one. In particular, it will focus on the somewhat difficult topic of calculating checksums for IP and TCP headers, something which is necessary since otherwise routers will usually instantly reject packets with invalid checksums.

What you really want is code. To that end, here is a complete C program which has been tested under Linux. It will send a TCP SYN connection request to port 80 of IP address 127.0.0.1 (which is the loopback address, of course). If you have enabled kernel logging with ipchains or iptables, you can see the results of this.

#include <netinet/in.h>
#include <sys/socket.h>
#include <netinet/ip.h>
#include <netinet/tcp.h>

main()

{

int tcp_socket;

struct sockaddr_in peer;

struct send_tcp
   {
      struct iphdr ip;
      struct tcphdr tcp;
   } packet;

/* The above makes a struct called "packet" which will be the packet we
construct. Below are all the lines we use to actually build this packet.
See RFCs 791 and 793 for more info on the fields here and what they
mean. */

packet.ip.version = 4; /* version of IP used */
packet.ip.ihl = 5; /* Internet Header Length (IHL) */
packet.ip.tos = 0; /* Type Of Service (TOS) */
packet.ip.tot_len = htons(40); /* total length of the IP datagram */
packet.ip.id = 1; /* identification */
packet.ip.frag_off = 0; /* fragmentation flag */
packet.ip.ttl = 255; /* Time To Live (TTL) */
packet.ip.protocol = IPPROTO_TCP; /* protocol used (TCP in this case) */
packet.ip.check = 14536; /* IP checksum */
packet.ip.saddr = inet_addr("1.2.3.4"); /* source address */
packet.ip.daddr = inet_addr("127.0.0.1"); /* destination address */

packet.tcp.source = htons(2000); /* source port */
packet.tcp.dest = htons(80); /* destination port */
packet.tcp.seq = 1; /* sequence number */
packet.tcp.ack_seq = 2; /* acknowledgement number */
packet.tcp.doff = 5; /* data offset */
packet.tcp.res1 = 0; /* reserved for future use (must be 0) */
packet.tcp.fin = 0; /* FIN flag */
packet.tcp.syn = 1; /* SYN flag */
packet.tcp.rst = 0; /* RST flag */
packet.tcp.psh = 0; /* PSH flag */
packet.tcp.ack = 0; /* ACK flag */
packet.tcp.urg = 0; /* URG flag */
packet.tcp.res2 = 0;  /* reserved (must be 0) */
packet.tcp.window = htons(512); /* window */
packet.tcp.check = 8889; /* TCP checksum */
packet.tcp.urg_ptr = 0; /* urgent pointer */

/* That's got the packet formed. Now we go on making the "peer" struct
just as usual. */

peer.sin_family = AF_INET;
peer.sin_port = htons(80);
peer.sin_addr.s_addr = inet_addr("127.0.0.1");

tcp_socket = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);

sendto(tcp_socket, &packet, sizeof(packet), 0, (struct sockaddr *)&peer,
  sizeof(peer));

/* the 0 is for the flags */

close(tcp_socket);

}

That's all you really need. But now you need to know: Exactly how did I get those checksums? They are listed in decimal format in the program; The IP checksum of 14536 is converted from 38C8 hexadecimal, and the TCP checksum of 8889 is from 22B9 hex. So, again, how did we arrive at those numbers? To answer this, here is a fairly thorough explanation of the entire process of analyzing and calculating an entire IP packet.

Before we begin, you may want to reference RFC 791 and RFC 793 for their excellent diagrams of the bit-by-bit structure of the IP and TCP headers. Also read my own information bits, Calculating IP checksums and Calculating TCP checksums.

To figure out the checksum for this IP header, let's break it down into a series of hex bytes so we can see exactly what's being sent over the connection, bit by bit.

The first entry in the IP header is the IP version. We're using version 4, as just about everybody does for now, until IPv6 gets off the ground. Anyway, the first 4 bits of the IP header are for the version, and 4 is 0100 in binary.

The next entry is the header length. Now, header length is actually measured in 32-bit multiples, so for example, a value of 10 for header length would mean a 320-bit header. The smallest possible IP header is 160 bits (20 bytes), which is 5 times 32 bits, so the smallest value possible for header length is 5. Notice that that's what we're using here (and what most IP packets use, unless they add some weird options to the header to make it longer). 5 is 0101 is binary. Add that to the 0100 we had for the version, and we end up with 01000101, our first byte! 01000101 is 45 hexadecimal, so the first hex byte in the header is 45.

Now we move on to Type Of Service (TOS). This is one of those weird fields which very few people actually use and very few applications even recognize. Thus, they're pretty much a special-purpose thing, so we usually just set them to zero (as is the case here). A whole 8 bits is devoted to TOS, so we know that the whole next byte of the header is just 0. 0 binary is also 0 hex.

So far, we have:

45 00

Next comes the total length of the packet (both the header and the data together) in bytes. This can sometimes be a little tricky to calculate, but in this case it's not too hard. The only data in this packet is the TCP header we're using, and as we'll see later, a TCP header's minimum size is also 20 bytes (just like an IP header's), and the one we're using is 20 bytes, so the total IP packet length is 40 bytes. 16 bits are devoted to the total packet length, so 40 bytes becomes 0000000000101000, or 0028 hex.

So far, we have:

45 00 00 28

Next comes the "identification" field. This is for identifying individual IP packets as they come in so they don't get mixed up. For this purpose we're just using 1. Another 16 bits are devoted totally to the identification number, so what we have is 0000000000000001, which is also 0001 hex.

So far, we have:

45 00 00 28 00 01

Next come the fragmentation flags and the fragment offset, two weird fields devoted to fragmentation which we don't have to worry about. Just set 'em to zero. There goes another 16 bits, all of 'em zeros, which would be 0000 hex.

So far, we have:

45 00 00 28 00 01 00 00

Next is the Time To Live (TTL), which is basically how many hops the packet is allowed to take before its destination is considered unreachable. Sometimes a packet can just get bounced around a lot between routers, and if it doesn't get there within a certain number of bounces, it's considered unreachable. Just to be obnoxious, I've set this to the highest possible value of 255. (That's the highest number you can put into 8 bits.) This is 11111111, or FF hex.

So far, we have:

45 00 00 28 00 01 00 00 FF

Next comes the protocol number. This is used to identify what higher-level protocol is being transported in this IP packet. All the common higher-level protocols have a unique ID number. We're using TCP, and TCP's number happens to be 6. 6 decimal is also 6 hex.

So far, we have:

45 00 00 28 00 01 00 00 FF 06

Now we come to the field for the checksum. Now we can't actually calculate the checksum yet, because we have to do the math first. While the checksum is actually being calculated, the checksum field is taken to be zero. It will be changed to the actual checksum value after everything else. For now, it's just 16 bits of zero.

So far, we have:

45 00 00 28 00 01 00 00 FF 06 00 00

Almost there! Just the two addresses to take care of, and they're easy enough if you can convert between decimal and hexadecimal. First, the source address, which in this case is 1.2.3.4. Each decimal number can just be converted directly into a hex byte. And since these numbers are so low, they can be converted directly!

So far, we have:

45 00 00 28 00 01 00 00 FF 06 00 00 01 02 03 04

Last one! The destination address is 127.0.0.1. All of these are obvious, except the 127 (it's 7F in hex).

Now we have:

45 00 00 28 00 01 00 00 FF 06 00 00 01 02 03 04 7F 00 00 01

And there we have our completed 20-byte IP header, all ready for calculation! So let's do it!

Remember, the first step to calculating the IP header's checksum, after you've gotten it nicely written out as a series of hex bytes, is to group them into pieces of 16 bits each. Looking at the string above and doing this, we can make a little addition column:

4500
0028
0001
0000
FF06
0000
0102
0304
7F00
0001

If you actually add these up (a calculator is allowed), you get 1C736.

The next step is to remove everything to the left of the last 4 digits, and add it to them. Take off the 1, and add it to C736. You get C737.

Now we subtract that from FFFF hex. FFFF minus C737 is 38C8. Ta-daa! All done! Our checksum value is 38C8 hexadecimal!

If you thought that was involved, wait until you see how we get the TCP checksum.

Let's take it one step at a time. We'll start with the source port, since that's the first item in the TCP header. In this case, our source port is 2000 decimal, or 7D0 hex. That field is 16 bits (2 bytes).

So far, we have:

07 D0

Next is the destination port. It's 80 in this case, or 50 hex. Again, 2 bytes.

So far, we have:

07 D0 00 50

Next is the sequence number, which is similar to the identification number in IP. We'll just use 1. This field is a whopping 32 bits (4 bytes). The next is the acknowledgement number, which we've arbitrarily set to 2, and which is also 32 bits.

So far, we have:

07 D0 00 50 00 00 00 01 00 00 00 02

Next is the "data offset", which basically means "header length". ("Data offset means "where the data begins", in other words, "where the header ends".) The minimum TCP header length, just like the minimum IP header length, is 20 bytes. Also just like the IP header length field, this field is measured in 32-bit multiples, making its minimum value 5 (because 5 times 32 bits is 160 bits, or 20 bytes). That's what we'll use. The data offset field is 4 bits, so since we're setting it to 5, that leaves us with 0101 binary.

The data offset field is followed by a "reserved" field that must be set to zero. This field is 6 bits long, so it overflows 2 bits past the data offset field. We'll worry about how that affects the next byte later... For now, we have the 0101 of the data offset, plus the 0000 for the first 4 reserved bits, making this byte 01010000 binary, or 50 hex.

So far, we have:

07 D0 00 50 00 00 00 01 00 00 00 02 50

After the reserved field are the TCP flags. Each flag is just one bit, and affects the function of the TCP packet in some way. There are six of them, so when combined with the two bits left over from the reserved field, we can form our next byte in the header. The order of the six bits for the flags are: URG, ACK, PSH, RST, SYN, FIN. In this header, only the SYN flag is on. Thus, the two reserved bits plus these flag bits are 00000010 binary, or 2 hex.

So far, we have:

07 D0 00 50 00 00 00 01 00 00 00 02 50 02

Next is the window. We'll just set this to 512, a nice middling value. As this field is 16 bits, 512 decimal gives us 0000001000000000 binary, or 0200 hex.

So far, we have:

07 D0 00 50 00 00 00 01 00 00 00 02 50 02 02 00

Almost there! Now we have the checksum. Once again, this is left at 0 while actually calculating the checksum. 16 bits, 2 bytes.

So far, we have:

07 D0 00 50 00 00 00 01 00 00 00 02 50 02 02 00 00 00

And now comes the urgent pointer. We'll just leave this at zero as well. Another 16 bits, or 2 bytes.

Finally, we have:

07 D0 00 50 00 00 00 01 00 00 00 02 50 02 02 00 00 00 00 00

If this TCP segment had any data, we'd need to include that in the checksum calculation. Mercifully, it has none, so we can go ahead and make 16-bit words out of these bytes, just as we did when calculating the IP checksum.

07D0
0050
0000
0001
0000
0002
5002
0200
0000
0000

But wait! We're not done yet. TCP checksums also use a 12-byte "pseudo-header" which takes information from the IP header. It has 4 fields:

1. The source IP address.
2. The destination IP address.
3. The protocol number. (This will always be 6 for TCP.)
4. The 16-bit length of the entire TCP segment, in bytes.

Let's see... Our source IP address is 1.2.3.4, so the first four bytes in the pseudo-header are:

01 02 03 04

Our destination IP address is 127.0.0.1, so now we have:

01 02 03 04 7F 00 00 01

The protocol number is 6. Note that this field is always preceded by an 8-bit string of zeros. So that adds 00 06 to the line:

01 02 03 04 7F 00 00 01 00 06

And finally, the segment length. This includes both the header and the data, but remember, we have no data, and our header is 20 bytes. 20 decimal is 14 hex, so:

01 02 03 04 7F 00 00 01 00 06 00 14

There we have our completed TCP pseudo-header. Now we can make that into 16-bit words and add it to the previous addition list. The whole list now is:

07D0
0050
0000
0001
0000
0002
5002
0200
0000
0000
0102
0304
7F00
0001
0006
0014

That's a lot of addition. However, we see that the sum of these hexadecimal numbers is DD46. In this case, our answer is only 4 digits, so we don't need to strip off anything. The final step is to subtract this from FFFF hex. FFFF minus DD46 equals 22B9. And so 22B9 hex is the final TCP checksum for this segment.

Phew! Well, I hope that this cleared up some things for a lot of people. If not, at least it was a fun read, I'm sure.

Back to the main page