« Back to the main CSCI1680 website
Milestone 1: Arrange meeting on/before Monday, November 7
Milestone 2: Arrange meeting on/before Thursday, November 17
Final deadline: Tuesday, November 22 by 11:59pm EST
In this project, you will implement an RFC-compliant version TCP on top of your virtual IP layer from the previous assignment. In doing so, you will extend your virtual network stack to implement sockets, the key networking abstraction at the transport layer that allows hosts to keep track of multiple, simultaneous connections. Thus, you will not only learn about how TCP works, but you'll learn about the OS infrastructure involved to support a networking stack.
Students report that this project is very challenging, but also very rewarding. When you are done here, you will really understand TCP, and the challenges involved in building a real-world network protocol.
You will work on this project in the same group you used for the IP project, and continuing from your existing codebase and repository. If this would be problematic, please contact Nick ASAP.
Warning: You have roughly 4 weeks to complete this assignment, with two intermediate milestone deadlines for you to implement part of the functionality and check in with the course staff. We strongly recommend that you start early and take full advantage of this time, as you will need it!
During this time, we remind you that we are here to help at all stages of the process! If you have questions at the design or implementation stage, we implore you to ask for help early so that we can make sure you are on the right track.
In the IP project, you implemented handling to deliver virtual IP packets between nodes. To implement TCP, you will build on top of these services to provide TCP as a reliable transport layer to send data between nodes.
Your TCP implementation will have four major components:
Note: Additionally, capstone students must also implement a congestion control algorithm and document its performance.
The first three items make up what we call the "TCP stack," which is another "layer" in your node that implements TCP. You can think about how your TCP stack fits into your existing IP implementation based on the figure below:
Fundamentally, your TCP stack will be another component in your node that receives packets from the IP layer, similar to how you handled RIP and test packets in the IP project. We will add several new driver commands to create and read from/write to sockets, which act as the "applications" that will use our new transport layer. The ultimate test will be commands to send a file between nodes–your goal is to use your TCP to send a file between nodes and have it received completely and accurately on the other side!
In keeping with our emphasis on building good abstractions, you will build a socket API that provides the interface for your driver commands to interact with your TCP stack. This API should be similar to the socket API for the language you are using.
Your first step will involve building a high-level for your TCP stack and how to implement sockets, after which you can begin implementing the actual mechanics of TCP. As a guide for your TCP implementation, we will refer you to various IETF RFCs that provide the de-facto standard for TCP implementations. For more details on how to find information on specific components inside the RFCs, see Relevant RFCs. You are also encouraged to consult our class notes and textbooks, as well as any other online resources–-so long as the code you write is your own work.
To interface with your TCP stack, you will create a representation for sockets within your node and an API to interact with them. Just like in a real OS, our virtual sockets will allow your node to maintain multiple simultaneous connections with other nodes on which you can send and receive data.
To do this, you will build an API for creating and operating on
virtual sockets, similar to the socket functions you have been using
all semester, such as Listen
, Dial
/Connect
, Read
, Write
,
Close
). You will add commands in your driver program that will
interact with your sockets using your API, similar to how
applications interact with the OS.
The API you design should look similar to the socket API in the language you're using and have the basic features for creating listen sockets, connecting to ports, and reading/writing to a socket, and closing the connection. While the API will differ slightly for each language, the required elements described here should be essentially the same.
To show you what we mean, this section contains an example socket API similar to the API provided by Go. You do not need to follow this exact specification, so long as you provide some kind of API that implements the features described here.
In this example, we create two structs that represent virtual sockets:
VTCPListener
represents a listener socket (similar to Go's
net.TCPListener)VTCPConn
represents a "normal" socket for a TCP connection
between two endpoints (similar to Go's
net.TCPConn)The example below describes the basic socket functions you need to
implement, and how they might look when implemented for these types.
This API is designed to represent the most basic socket
opreations–you are NOT required to implement other extra features
that your language's
socket API may support, such as SetReadDeadline
or similar.
The main idea here is that an independent thread in your program (ie,
outside your TCP stack) should be able to use your
socket API in a similar way to how you would use the normal API in your
language. In Go, for example, your functions should return an error value that is nil
on success, or contains an error with a reasonably descriptive message describing what happened. For more details, see this example.
Note: For clarity, we prefix each function's name with
V
to ensure it's clear when we're talking about the virtual socket API in this document–you do not need to adhere to this requirement, we only use it to ensure consistent notation across languages.
Go defines method signatures like this:
Receiver
is used to declare functions as methods of a certain
object. For example:
You will implement a state machine that keeps track of the fundamental states of each connection. Here is one representation of the state diagram (courtesy of Wikipedia):
State transitions are labeled as Event/Action
, where Event
is what triggers the state transition, and Action
indicates a packet that gets sent in response. For example, in the SYN_SENT
state, the transition SYN+ACK/ACK
indicates "When a SYN+ACK
is received, send an ACK
and go to the ESTABLISHED
state."
Events
in bold indicate an operation performed by the user (ie, the socket API) using the event terms listed in the RFC (see Section 3.9.1). For example, in the SYN_RECEIVED
state, the transition CLOSE/FIN
means, "when the user performed a CLOSE
, send a FIN
and go to the FIN_WAIT_1
state".
The state machine is not as complicated as it may seem at first. To begin, we recommend that you start coding by just using the diagram and getting connections to set up and close under ideal conditions.
For the purposes of the assignment, we are asking you to implement a minimal form of TCP that implements most of the features that the RFC considers, with a few exceptions to help reduce the complexity.
In general, you are expected to follow the state machine and features described by RFC9293, except for the parts of referring to:
PSH
flagsRST
packetsURG
flags)Once you have the rest of your implementation working, there are some
edge cases to consider: for example, what happens when, after a call to connect
, you've sent a SYN, but you receive a packet that has an
incorrect ACK in it? Once your basic state diagram is working, we
recommend that you look at the RFC for answers to questions such as
these.
In particular, Section 3.10 of RFC9293 contains info on exactly what you should do in such scenarios for each state.
Tip: As with the IP project, you don't need to build/parse the TCP header yourself. See the Implementation Notes for details.
Not sure if something is required? Ask on Edstem! We are making an effort to expand documentation here, so questions are encouraged!
Your sliding window protocol controls how you send and receive data–-this is the "heart" of your TCP stack.
Be sure that you can accept out-of-order packets. That is, a packet's sequence number doesn't have to be exactly the sequence number of the start of the window. It can be fully contained within the window, somewhere in the middle. The easiest way to handle such packets is to place them on a queue of potentially valid packets, and then deal with them once the window has caught up to the beginning of that segment's sequence number.
You are not required to implement slow start, but you should detect dropped or un'acked packets and adjust your flow accordingly.
You should strictly adhere to the flow control window as specified in the RFC, e.g. do not send packets outside of your window, etc. Similarly, you should implement zero window probing to ensure your sender can recover when the receiver's window is full. Overall, your goal is to ensure reliability–-all data must get to its destination in order, uncorrupted.
As you implement your protocol, keep in mind how sliding windows will interact with the rest of TCP. For example, a call to CLOSE only closes data flow in one direction. Because data will still be flowing in the other direction, the closed side will need to send acknowledgments and window updates until both sides have closed.
Your driver should support the following commands to create and work with sockets.
Command | Description |
---|---|
h |
Print this list of commands. |
li |
Print information about each interface, one per line. |
lr |
Print information about the route to each known destination, one per line. |
ls |
List all sockets, along with the state the TCP connection associated with them is in, and their window sizes (one should be the socket's receiving window size, and the other should be the peer's receiving window size). |
a <port> |
Open a socket, bind it to the given port on any interface, and start accepting connections on that port. Your driver must continue to accept other commands. |
c <ip> <port> |
Attempt to connect to the given IP address, in dot notation, on the given port. Example: c 10.13.15.24 1056 . If the connection is established successfully, this command should print out an ID number that is used to refer to the socket for other commands (0, 1, 2, ...). |
s <socket ID> <data> |
Send a string on a socket. This should block until VWrite() returns. |
r <socket ID> <numbytes> <y|N> |
Try to read data from a given socket. If the last argument is y , then you should block until numbytes is received, or the connection closes. If n , then don't block; return whenever and whatever VRead() returns. Default is n . |
sd <socket ID> <read|write|both> |
VShutdown on the given socket. If read or r is given, close only the reading side. If write or w is given, close only the writing side. If both is given, close both sides. Default is write . |
cl <socket ID> |
VClose() on the given socket. |
sf <filename> <ip> <port> |
Connect to the given ip and port , send the entirety of the specified file, and close the connection. Your driver must continue to accept other commands. |
rf <filename> <port> |
Listen for a connection on the given port . Once established, write everything you can read from the socket to the given file. Once the other side closes the connection, close the connection as well. Your driver must continue to accept other commands. Hint: give /dev/stdout as the filename to print to the screen. |
q |
Quit cleanly, closing all open sockets. |
Note: This list does not include the "up" or "down" commands (as TCP sockets are rarely well defined when interfaces are changed), but we recommend keeping the code for it.
Each student taking this course for capstone is responsible for implementing one of the following congestion control algorithms. Your TCP design should be able to selectively enable and disable any congestion control module that is available, and only 1 congestion control algorithm can be enabled per tcp socket at any given time. If you would like to implement a different congestion control algorithm than the two provided below (since there are many more out there), first seek approval from the staff.
The algorithms we recommend are:
If both students on a team are taking the course for capstone credit, they may not share parts of their code for the congestion control algorithm with each other. All code for each congestion control algorithm must be written individually. Your TCP driver must implement the following commands to demonstrate your congestion control algorithm:
Command | Description |
---|---|
lc |
Prints the available congestion control algorithm names, eg. reno , tahoe , … |
sc <socket ID> <string> |
Sets the congestion control algorithm for the given socket. To disable congestion control, use the string: none |
*s |
You should modify your ls command to also list the congestion control algorithm (if any) each socket is using, and the congestion window size as well. |
*sf |
You should modify your sf command to optionally take in a congestion control algorithm, with the options being: reno , tahoe , … The default for no argument is none . |
Lastly, you will be required to provide Wireshark capture files as well as a summary of how your congestion control algorithm fared against your implementation without congestion control.
A few notes (skim them now, then return here later):
You must use the TCP packet format, exactly as-is. Like with IP, you are welcome to use a library serialize/deserialize the TCP header for you. One example library in Go is the netstack's package TCPFields
struct–though you are free to use other libraries. Take a look at the TCP-in-IP example for a demo of how to use this library to build/parse TCP packets. In C, you should use the header found in netinet/tcp.h
(demo here).
There are several places in the RFCs that leave room for flexibility in implementation. We extend the same flexibility to your projects, as long as you can justify your design decisions (in a README
). A good rule of thumb is to be liberal in what you accept but conservative in what you output. For example, your program shouldn't crash if, say, a packet has the URG flag set, even if you don't support it (instead, just ignore the flag, and set it to 0 for packets you send).
TCP uses a "pseudo-header" in its checksum calculation. Make sure you understand how TCP checksumming works to ensure interoperability with the reference implementation. Section 3.1 of RFC9293 or this link may be helpful. A code example about this will be posted soon.
You MUST NOT use arbitrary sleeps or busy-waiting to handle sending/receiving packets. For example, you might have a thread which takes care of sending out packets for a particular socket. You MUST NOT have this thread check whether there is something to be sent every, say, 1 ms–this may unnecessarily wake up the thread and waste a lot of CPU cycles! Instead, your thread should send whenever data is available, or or when a retransmission timer expires. Channels, mutexes, and condition variables are your friends.
As in the IP assignment, never send packets greater than the MTU. For our link layer, the maximum MTU is 1400 bytes: any TCP segments you send must be no larger than the MTU–therefore, the maximum TCP payload size is:
1400 bytes - (size of IP header) - (size of TCP header)
You don't have to handle any TCP options. You should ignore any options that you see in incoming packets, but your program shouldn't crash if you encounter them.
When should VConnect()
timeout? A good metric is after 3 re-transmitted SYNs fail to be ACKed. The idea is that if your connection is so faulty that 4 packets get dropped in a row, you wouldn't do very well anyway. How long should you wait in between
sending SYNs? You can have a constant multi-second timeout, e.g. 3
seconds. Or, you can start off at 2 seconds, and double the time
with each SYN you retransmit.
The RFC states that a lower bound for your RTO should be 1 second. This is way too long! A common RTT is 350 microseconds for two nodes running on the same computer. Use 1 millisecond as the lower bound, instead. By a similar principle, you do not need to be overzealous in precisely measuring RTT; it is reasonable to tolerate small processing delays (1-10ms).
Debugging TCP can be very difficult–-we strongly recommend using Wireshark to observe your TCP connections. Wireshark has many tools to help analyze TCP connection state, which may prove useful–-we will discuss a few of these in class. If you have questions how to use wireshark to accomplish a particular task, please feel free to ask!
Log as much as you can, and make it possible to filter out what you care about. For example, you may only want to log information related to a specific connection, or you may only want to see logs from TCP, and not IP.
Similar to the IP milestone, you will complete this part by scheduling a meeting with the course staff (preferably your mentor TA) on or before Monday, November 7.
For this meeting, your implementation should be able to demonstrate the following:
a
, c
, and (partially) ls
commands in your TCP driver to listen for, create, and list connections, respectively. For the ls
command, you need not list the window sizes for the milestone.--disable-checksum
.In addition, try to consider how you will tackle these problems, which we will discuss:
You should schedule a subsequent milestone checkin with the course staff on or before Thursday, November 17.
For this meeting, students should have the send and receive commands working over non-lossy links. That is, send and receive should each be utilizing the sliding window and ACKing the data received to progress the window. This also means that sequence numbers, circular buffers, etc. should be in place and working.
Retransmission, connection teardown, packet logging and the ability to send and receive at the same time are not yet required.
As usual, most of your grade depends on how well your implementation adheres to these specifications. Some key points:
The idea is that having full basic functionality means that any existing valid TCP implementation should be able to talk with yours and eventually get data across, regardless of how faulty the link is.
We want you to understand how your design decisions affect your TCP's behavior. In your README
, you should document your major design decisions and your reasoning for using them.
In addition, we ask that you also include in your README
a brief analysis of your TCP traffic, as follows:
Measuring performance: When you submit, we also ask that you investigate your implementation's performance. However, we are NOT asking you to highly optimize your implementation and meet a certain performance goal. While performance is key in all systems design, spending many iterations on optimizing your design is beyond the scope of this project. Instead, we only ask that you measure your implementation's performance relative to the reference node and comment on it in your README
.
Since our implementation runs on a single host's loopback network, performance is based entirely on CPU speed. To get a baseline for performance, run two reference nodes connected directly to each other with no packet loss and compare the time to send a file of a few megabytes in size (you can also directly measure the throughput in Wireshark). Your implementation should have performance on the same order of magnitude as the reference under the same test conditions.
Packet capture: Finally, you should submit a packet capture of a 1 megabyte file transmission between two of your nodes. To do this, run two of your nodes in the ABC network with the lossy node in the middle, configured with a 2% drop rate.
After filtering your packet capture to show only one side of the transmission, you should "annotate" the following items in the capture file:
To do this, list the frame numbers for each item in your README
with a
description. For each annotation, you should evaluate if your
implementation is responding appropriately per the specification. If you
notice any issues, you should document them accordingly.
An example packet capture will be demonstrated in class before the deadline.
Capstone students will also need to provide packet captures for their congestion control implementation. For these you should run your congestion control algorithm in a drop-free network, and also run it with the faulty node in the middle. Similarly, you should run your algorithm with no other competition, as well as run multiple instances of your algorithm intermixed with simple flow control TCP streams simultaneously. In your write up you should explain the behaviour of your node in all these situations, and try to explain the strengths and weaknesses of your algorithm.
Since this project is a continuation of your work from the IP assignment, you should continue development in your IP repository.
Similar to IP, you should work on this project using the course container environment.
Note: Wireshark will be extremely helpful for debugging your work in this project. See this section for some important instructions on how to use it for TCP. If you have issues running the container environment, or Wireshark, (ie, if it's slow, hard to use, etc.)–please talk to us and let us know! This is a relatively new element of the course: we are happy to help debug issues or work on improving your workflow, but we'll only know about issues if you come to us!
We have provided a few additional reference binaries for this assignment, which are available here: https://github.com/brown-csci1680/ip-tcp-starter/tree/main/tcp_tools.
Please copy these files into your IP repository–-there is no need to start a new repository for this assignment.
M1 mac users: Please use the binaries located in the arm64
directory instead.
ip_node_lossy
)The starter repository contains an IP node called ip_node_lossy
that
can be configured to drop a fraction of outgoing packets. This will be
useful when testing your retransmission and timeout logic. You can
specify the drop rate with the command "lossy". The drop rate should be
a value between 0.0 and 1.0, where 1.0 means every packet will be
dropped by the node.
ref_tcp_node
)The starter repository also contains a reference TCP implementation.
We must emphasize that your node MUST be able to operate with the reference node, so please test using this node frequently!
In addition, you should fix any lingering issues in IP preventing your node from working with the reference IP node. If you have questions on how to do this, please contact the course staff–we can help!
Note that the reference implementation does not implement congestion control
Warning: Our current reference implementation has a few quirks where it deviates from our current spec. See this section for a list.
If you do not feel confident in extending your work from the previous assignment to support TCP, please talk to us. We can advise you on critical areas of your IP ipmlementation to prioritize, or we may be able to provide a reference implementation that you can use instead. If you are interested in this, please contact the course staff.
The original standard for TCP (RFC793) was released in 1981, and has been amended and extended over the past four decades by many others. Accordingly, in the past, this project has required sifting through a lot of RFCs.
However: this year, there is finally a better way. As of August 2022, a new RFC has been forged that consolidates four decades of updates into a new document: RFC9293. This should be a definitive source for most information about most parts of your implementation.
Otherwise, the list below lists how to find the most important parts of the other RFCs–this list is now obsolete, you can probably get away with only RFC9293 now. We will be checking this in the next couple of days and will update this soon!
Links:
RFC 793: pp. 4-5, 2.6, 2.7 and 2.10.
RFC 793: pp. 27-28 (Initial Sequence Number Selection), 3.4, 3.5, 3.8, 3.9 and RFC 1122: section 4.2.2.9, 4.2.2.10
RFC 793: section 3.2, 3.3, 3.7, 3.9 and RFC 1122: section 4.2.2.16, 4.2.2.17, 4.2.2.20, 4.2.2.21
RFC 5681
RFC 793: section 3.8; Beej's Guide to Network Programming: Ch. 5
RFC 793: section 3.1 and RFC 1122: section 4.2.2.3
RFC 793: p. 41 and RFC 1122: section 4.2.3.1
Before each milestone and before the final deadline, once you have completed the requirements for that part of the project, you should commit and push your Git repository.
Your mentor TA will arrange to meet with you for each interactive grading session (milestones and final demo) to demonstrate the functionality of your program and grade the majority of it. This meeting will take place at some point shortly after the project deadline.
Between the time you've handed in and the final demo meeting, you can continue to make minor tweaks and bug fixes. However, the version you've handed in should be nearly complete since it could be referenced for portions of the grading.
Once again, we highly recommend getting started on this assignment soon–-don't wait to start until a few days before the deadlines! If you have questions, or need help with your implementation, always feel free ask for help or clarification or debugging. This is a hard assignment, but we are here to help you and provide resources, and we will be most effective at doing so if you ask early!
Although we expect compatibility between your TCP implementation and our own, do not get bogged down in the RFC from the start. It is much more important that you understand how TCP works on an algorithmic/abstract level and design the interface to your buffers from your TCP stack and from the virtual socket layer. For any corner cases or small details, the RFC will be your best friend, and our reference implementation should come in handy. Always feel free to consult the course staff if you have any questions about what you are required to do, or how to handle corner cases. It is not OK to just make assumptions as to how things will work, because we will be testing your code for interoperability with the reference node and other groups in the class.
Any updates to the assignment after release will be listed here:
This section contains various notes for certain project components.
For instructions on using Wireshark with this project, please see Lab 2–all of the instructions on using Wireshark apply here (such as capture filters, "Decode As" rules, etc.).
In addition, there are two more small items you will need to configure, as described below:
Wireshark has a LOT of great tools for monitoring TCP connection state. However, Wireshark can get a bit confused by our virtual IP network: since we have multiple nodes running on the same machine, and Wireshark by default sees all packets from all nodes–which confuses its TCP analyses.
To fix this, we can use a capture filter when you start Wireshark to restrict it to see only traffic for only one node. For example:
udp port 5000
.udp port 5001
, etc. In most cases, it should be sufficient for debugging to only look at traffic for one node.For instructions on configuring capture filters, see here.
Wireshark can validate the TCP checksum, similar to how it validates the IP checksum. To configure this, right-click on a TCP header and select Protocol Preferences… > Validate TCP checksum if possible.
For visual instructions, please see this section of lab 2–just start by clicking on a TCP header instead.
There are a few places where our current reference node deviates from the specification described in the assignment. Any known issues will be listed here.
Note: if the reference does something listed here that doesn't agree with this document, you should follow the document. If you find any other issues, please post on Edstem and we will review them!
c
command is non-blocking: that is, when establishing a TCP connection, it doesn't wait for the other side to respond (or an error to occur) before returning. This behavior is not required: your implementation for VConnect
(and the c
command) should block until the connection is ESTABLISHED
, or an error occurs.cl
command immediately removes a socket from the socket table, regardless of the current connection state. Instead, cl
should trigger a shutdown on both the reading and writing side of the socket, if this has not yet been done so already, and then return. Then, the socket should only be removed from the socket table when it enters the CLOSED
state.Here's an example function that returns a value and an optional custom error message. For more details on creating errors, see the errors library. Another good way is to look at go's own source code for function in the net
package.
This section describes a virtual socket API similar to C's socket API. In C, you should create new connections as virtual sockets using your own socket table and virtual file descriptors to allow connecting and listening, reading and writing into buffers, etc.
An independent thread in your program should be able to use this your
socket API in a similar way to how you would use the normal API in your
language. These functions, on error, should return appropriate error
codes (such as negative values in C, error values in Go, etc.). For C,
you should use standard error codes (such as EBADF
).