--------------[ Readings ]--------------
Read textbook's chapter 12 (TCP preliminaries). You may read ahead to
chapters 13--15 that cover more of TCP.
We followed Ch.12 pretty closely today in class, with pcaps.
As you read about TCP handshake, look through the packet capture
(I accidentally deleted it but re-created it after class,
with the same sessions and payload; in case you wonder about
Packets 1--9 are my session "nc www.cs.dartmouth.edu 80"
that expired before I typed anything. Netcat called connect(),
which resulted in the SYN packet being sent; server accept()-ed
and sent SYN+ACK, my kernel completed the handshake with an
ACK. No typing meant no write()s, and the server resent SYN+ACK
just in case; then it timed out and sent a FIN for a graceful
teardown of the connection. That was ACK-ed, and it took till
Packets 10--20 are from another connection, which gave me a 302 HTTP redirect:
$ nc www.cs.dartmouth.edu 80
The document has moved here.
Apache/2.4.23 (Fedora) Server at www.cs.dartmouth.edu Port 80
See this stream reconstructed by Wireshark by going through "Analyze >
Follow > TCP stream" with one of these packets selected. You will get
a representation of the full stream going both ways ("full duplex").
Finally, packet 21 is a SYN from connect() caused by
"nc www.cs.dartmouth.edu 81" . With no program listening at port 81/tcp,
the other side sends a RST. TCP uses it's own packets to signal
"no one is listening, go away" (packet 22).
For UDP, this is different: a UDP packet to a port no program claimed
with bind(), the response is an ICMP Port Unreachable (packets 23 & 24).
I caused the UDP packet to be sent with -u option for nc:
"nc -u www.cs.dartmouth.edu 81"
Note that packet 24 contains a part (the headers) of the offending UDP
packet 23 as its payload. This is done to make 'net diagnosis easier.
----------------[ TCP server skeleton ]----------------
See http://www.cs.dartmouth.edu/~sergey/cs60/tcpserv.c for
the typical TCP server skeleton (socket -> bind -> listen -> accept,
then fork and continue listening in the parent, process data in child).
Note that the original listening socket is closed in child, and
the child's new socket returned by accept is closed in the parent,
since these are not used.
For a server without a fork(), see
Note the slightly different styles; also note that ALL system
calls have their return values checked! This is a style requirement
not to be ignored.
----------------[ Parsing a binary TCP protocol ]----------------
In http://www.cs.dartmouth.edu/~sergey/cs60/tcp-pow/ find client
and server code for a silly client/server pair that uses a binary
protocol with unnaturally aligned uint32_t fields; there is also
a uint16_t length field.
This protocol is written so that forgetting ntohl()/htonl() or
htons()/ntohs() anywhere would lead to bugs such as too much
memory being printed, wrong numbers being multiplied or the
result interpreted wrongly. Try recompiling the code without
any of these and observe the results; see also README.txt.
The lesson of this simple protocol is that in network programming
C makes one care extra hard about its memory model: how values
and variables are laid out byte-wise in memory, and how they are
aligned. C network programming is more about its memory model
than about socket()/connect()/bind()/listen()/accept() system calls!
These two files also show basic approaches to parsing binary
content (neither of which is my favorite; I prefer to write
declarative code that explains what valid input is, not imperative
code where one can easily forget to step forward a char* pointer).
The approach with defining structs corresponding to the per-byte
layout of arriving bytes in memory is slightly less error-prone. Yet
even so you must explicitly (and continually) covert multi-byte fields
from/to network/host byte order. This is what the client mostly does.
The approach that steps a pointer through an array of bytes to point
at where fields start, and casts the pointer to the respective field
value types and dereferences it is somewhat more error-prone. What
if you forget to step the pointer, or step it off by one? This is
what the server mostly does.
Newer architectures try to wrap binary data marshaling & unmarshaling
with less error-prone code. See Google's Protocol Buffers (ProtoBuffs)
as an example. The more general approach is the Parser Combinator
approach, pursued for systems languages by Hammer (C/C++ & more),
and Nom (the up-and-coming systems language Rust).
Still, the internals of these systems are programmed in C or similar;
there is no getting around the memory model when working with packets.
Extra credit will be given for any bugs still left in my code :)
----------------[ Read ahead: DNS ]----------------
A good concise intro to DNS is found in Shalunov; a great summary
with a dive-in into specific fields is at
The latter describes the format at enough depth to cover the
caching vulnerability discovered by Dan Kaminsky in 2008.
DNS failed to resist spoofing where TCP generally succeeded;
the difference was down to using 2-byte integers vs 4-byte ones
(for syn and ack numbers in TCP).
DNS is the very devil to parse (and has seen many parsing vulns over
the years). I wrote some code (in
http://www.cs.dartmouth.edu/~sergey/cs60/dns/) to parse a simple DNS
response, lifting the request from a pcap (dns-tcp.pcap). This will
give you some idea of how hard manual DNS parsing is!
This code parses only one particular case of DNS packet composition,
but it already has to do with domain names encoded as
<0x04> cs60 <0x02> cs <0x09> dartmouth <0x03> edu <0x00>
the next-part's-length byte being used where you expert the dot.
Moreover, instead of repeating a name or its part, DNS uses
<0xc0> <0xYY> where YY is the offset from the start of the DNS
payload (the Transaction ID field). Hence <0xc0> <0x0c>, offset
12, used the second time the above domain name would appear in
Have fun with parsing!