Implementation start guide

This guide demonstrates the most important things to keep in mind as you writing your implementation, including how to think about sending IP packets on our virtual network, and super important debugging techniques for checking your work.

When to use: You should do this tutorial as soon as you start your actual implementation (usually, right after your milestone meeting). Once you have an idea of what you need to build, this guide can help you get started with the most important details on implementation and testing (eg. with Wireshark). You’re also welcome to start it earlier, if you want a more hands-on demo, but some of the concepts involved might not be too clear until after Lecture 8 (Tuesday, October 1).

For a live demo of many of the features here, see Gearup II.

This tutorial will cover:

  1. How to send well-formed virtual IP packets encapsulated in UDP packets
  2. How to view and debug virtual IP packets in Wireshark
  3. How to view and debug RIP packets in Wireshark

Why are these topics important?

  1. Sending properly formatted IP packets ensures that you can interoperate with our reference node, which is how we’ll be grading. You MUST conform to the specification on sending IP packets in order for us to grade your work in a reasonable way.

  2. For this project and the next, Wireshark is one of the best ways to debug your work, and the best way to make sure you’re conforming to the specification. Spending a bit of time now getting accustomed to these tools now will save you a lot of time later, we promise!!!

First step: starting wireshark (again)

In this tutorial (and for most of the work you do on IP and TCP), you’ll want to have Wireshark open and ready to use at all times. Before we start, you should open Wireshark now to make sure it’s working. To do this:

  1. (MacOS/Windows users) Make sure your X server is running: if you are on MacOS, start the XQuartz app. If you are on Windows, run XLaunch or VcXsrv from the start menu and click Next on all the prompts.

  2. From a terminal on your host machine (or a WSL terminal on Windows), enter your container environment using the run-container script.

    Warning: DO NOT use the in-container VSCode terminal for this! Before running wireshark, you MUST enter the container with the run-container at least once–this script sets up your X server so that the container can connect to it. After that, you can use the VSCode terminal as much as you want.

  3. In your container terminal run wireshark.

If your wireshark opens, things are good, yay!

If Wireshark doesn’t open

If this doesn’t work, please try the following:

  1. Close your container terminal and run it again with ./run-container. This should force the script to setup your X server again.

  2. Make sure your X server is running. On MacOS, you should have the XQuartz application running (look in your Dock). On Windows, you should see an icon for XLaunch in your system tray.

  3. Try running Wireshark using the backup method If you encounter issues with the backup method, please let us know!

If you encounter issues with running wireshark after have it work earlier in the course, please post on Ed to let us know! This sometimes happens, and we’re still trying to learn about all the edge cases. We want to make sure that everyone can use this important tool.

Virtual network background: an example topology

In th rest of this guide, we’ll talk about forwarding packets using the doc-example network, as shown here:

Now that you’ve done your milestone, let’s review some key points about our virtual network works:

  • Each node (ie, host or router) has some number of interfaces on which it receives packets.
  • Each interface has a virtual IP address, which is part of a specific subnet. Each subnet has a specific IP prefix, denoting the range of IP virtual addresses on that subnet. In this example, there are three subnets: r1-hosts, r1-r2, and r2-hosts.
  • Each interface has a UDP port. All nodes send and receive packets on an interface by sending/receiving from that UDP port
  • All nodes on the same subnet are considered neighbors and can communicate with one another directly by sending to their neighbor’s UDP port. Nodes always know the UDP port numbers for their neighbors.

For more information on how virtual networks are set up, we recommend looking at the notes and recording for Gearup II.

Sending on a virtual interface

Each interface (if0, if1, …) is assigned a specific UDP port based on the lnx file.

You will create a socket that listens on this UDP port and use it in two ways:

  • Your node should continuously listen on this port for packets. Receiving a packet means you received a packet on this specific interface
  • To send a packet on this interface, you must use this socket–this is how you can tell where the packet came from!

For more information on how interfaces work, see here.

In the next few sections, we’ll describe how to think about viewing packets on our virtual links so you can test your code.

We can see the virtual link layer in action by viewing the traffic in Wireshark. To get a sense of what it should look like, we’ll first demonstrate it using the reference node, and then you can check your own traffic in the same way as you work on your node.

This guide assumes that you can already start Wireshark in your container. For instructions on starting Wireshark, take a look at Testing Wireshark above, and the container setup guide.

All our virtual network traffic is UDP, so we can start by capturing just UDP traffic. To do this:

  1. From a terminal on your container environment, open Wireshark

  2. In the menus at the top, select Capture > Options and select the loopback interface.

  3. At the bottom of the window, find the box labeled “Enter a capture filter…“ and enter udp, like this:

“What’s a “capture filter?”

Here, we use a “capture filter” to tell Wireshark to ignore everything except UDP packets when capturing, since we know that all of our packets are UDP packets. Certain Docker-related things may also use the loopback interface (like X11, and file sharing between the container and your host), filtering these out helps to reduce clutter and save memory.

Capture filters are really useful because they run inside the kernel–ie, where Wireshark hooks into the network stack to capture packets. Packets ignored by a capture filter never reach Wireshark’s memory buffer, so filtering out packets this way can save on memory and improve performance when capturing on high-traffic links.

Because capture filters run in the kernel, they are simpler than the “display filters” we learned about in Snowcast and have a simpler syntax. You can read more about capture filters here: https://wiki.wireshark.org/CaptureFilters

Now that you have Wireshark running, we can observe some virtual IP traffic by running the reference node. To try this:

  1. Make sure your wireshark capture is still running, as above

  2. Use vnet_run to start the doc-example network using the reference node. From a container terminal, you can do this by running something like: util/vnet_run --bin-dir reference doc-example
    For a step-by-step guide on how to run the doc-example network, see here.

Once the network seeing some UDP packets on ports 5002 and 5003, like this:

These packets actually correspond to RIP messages, though we can’t see that yet (we will, though!). For now, look at the UDP header of the packet in the second pane and notice the ports, which should look like this:

We can match up the UDP ports to the topology diagram above:

  • The packet was sent from port 5002, which means it came from r1’s interface if1.
  • The packet was sent to port 5003, which corresponds to r2’s if0, so this packet was being sent to r2.

We can apply the same logic to the other packets. In the current view, we should see only packets being sent between r1 and r2 (ports 5002 and 5003, respectively):

“What about the other ports? Why do we just see r1 and r2?” Great question! We only see packets from r1 and r2 because these are the only nodes sending packets right now—the traffic you see is for initial RIP requests and periodic updates. Only routers send RIP messages, so you should only see packets from routers unless you are sending test packets! We’ll see how to send test packets and view them in wireshark in the next section.

Now that you can see how the UDP packets should look, take a look at the following section for some details on how to implement it. If you have already implemented sending UDP packets in your node, try to observe the same things here using your node instead of the reference. If things look the same, continue reading at the section on Decode As rules.

Tutorial: configuring your UDP sockets

To make a UDP socket we can use as an interface, we need to “bind” the node’s UDP socket to its designated port. This will allow us to receive packets on this port, and set the source port on all packets originating from this socket. In Go, we can do this using ListenUDP:

bindAddr := "127.0.0.1" // Example address and port
bindPort := 5001

// Turn the address string into a UDPAddr for the connection
bindAddrString := fmt.Sprintf("%s:%s", bindAddr, bindPort)
bindLocalAddr, err := net.ResolveUDPAddr("udp4", bindAddrString)
if err != nil {
    // ...
}

// Bind on the local UDP port:  this sets the source port
// and creates a conn
conn, err := net.ListenUDP("udp4", bindLocalAddr)
if err != nil {
	// . . .
}

// All sends using this conn will have UDP source port == bindPort

A key detail here is that you will use this one UDP socket to both send and receive packets. Recall that UDP is connectionless: there is no need to have one socket for each endpoint like with TCP. Instead, each time we send a packet, we specify the destination address:port, like this (in Go, for example):

remoteAddr := net.ResolveUDPAddr(...)
bytesWritten, err := conn.WriteToUDP(buffer, remoteAddr)

Similarly, when we receive a UDP packet, the kernel will provide some information about the sender (ie, the source address and port). In Go, it might look like this:

// SourceAddr is a net.UDPAddr containing info about the sender
bytesRead, sourceAddr, err := conn.ReadFromUDP(buffer)
if err != nil {
    // ...
}

fmt.Printf("Received from %s:  ...", sourceAddr.String(), ...)

With this, you should be able to send and receive UDP packets on your node’s UDP ports. For more complete code example, take a look at the UDP-in-IP demo, and also take a look at the documentation for the functions listed here.

Try it: Now that you have read about how to set up your UDP sockets, try to replicate what you saw in Wireshark with the reference node using two of your nodes. Watch your traffic in Wireshark to make sure that your UDP port numbers look like the reference (and the figures above).

Making virtual IP packets

Now that we have actual UDP sockets, we need to send some actual data to our nodes. All of our messages are sent as IP packets, so each UDP message you send should start with an IP header, and then the actual message content. In this project, the “actual message content” is either a “test packet” generated by the send command (routers and hosts) or a RIP packet (routers only).

Another way to say this is that our virtual IP packets are “encapsulated” inside UDP packets. Let’s start by seeing what this means in Wireshark.

Decode As: Viewing virtual IP packets in Wireshark

Warning: This step is annoying, but you only need to do it once.

We would like to be able to view our virtual IP packets in Wireshark to see what they look like. Fortunately for us, Wireshark already knows how to parse (or “dissect”) IP packets–it actually knows how to parse hundreds of different protocols, and it has rules to decide when to run them. Usually, these are based on common port numbers: for example, HTTP traffic normally runs on TCP port 80, so Wireshark will try to interpret anything on port 80 as HTTP traffic.

However, Wireshark doesn’t know about our project, so we need to tell it to expect to see IP packets on the port numbers we’re using. We can configure Wireshark do this using “Decode As” rules, like this:

  1. If you haven’t done so already, start a wireshark capture to view UDP traffic while running the doc-example network (as in the previous section)

  2. In Wireshark’s main window, right click on one of the packets from r1 to r2 and select Decode As…. Like this:

  1. After you open the menu, you should see a window that looks like this:

With this window open, do the following:

  1. Make sure the “Value” column in the entry is set to one of your node’s UDP ports (eg. 5002)
  2. Click on the protocol in the last column and set the type IPv4
    (note: you’ll need to scroll, it’s a big list)

  3. Now we need to do the same thing for the other port numbers we might see: click the copy button in the lower-left corner to duplicate this rule and change the port number to another UDP port for this network. You can edit the port number just by clicking on it.

  4. Repeat step 3 for each port in the example-doc network to ensure that Wireshark knows how to decode all the traffic we might see.

After adding all the rules, your “Decode As…” window should look like this:

  1. Once you’re sure your settings are correct, click Save, then OK.

You should now see decoded versions of all the UDP packets, like the figure below. Yay! :grin:

Now let’s break down what all of this should look like.

Note: As you run larger networks, like the loop network, you may need to add a few more ports to your Decode As rules–you’ll know this is the case if you see UDP packets in the 5000-range that don’t look like a test packet (see below) or RIP packet. To do this, you can follow the same procedure as shown here.

Examining IP packets (and test packets)

A good place to start sending IP packets is our “test packets”, which just send a string from one node to another.

To view test packets in Wireshark, it helps to filter out all of the RIP traffic and just focus on the test packets. We can do this by filtering out RIP traffic, which uses the cs168rip protocol. Then, we can inspect some test packets. To do this:

  1. In Wireshark, enter the display filter not cs168rip, which should hide all of the RIP traffic. Unless you have sent any packets, this will leave an empty Wireshark buffer.

  2. With the doc-example network running, send a test packet from h1 to r1. You can do this by entering something like send 10.0.0.2 hello world! in h1’s terminal. For info on how to navigate between terminals in vnet_run, see here.

What to expect: After sending the test packet, you should see a packet in Wireshark listed as IPv6 hop-by-hop options [Malformed], like this:

Wireshark thinks this is a malformed packet, but this is okay! This is simply Wireshark trying to parse the string in the packet–it doesn’t know what test packets are, so it’s just doing its best!

To view useful the packet contents, click on the packet and look at the various headers in Wireshark’s middle and lower pane. As you expand and click on the different headers, you should see something like this:

Let’s break down what’s happening here:

  1. As before, you should see the “real” IP and UDP headers that direct the packet from 127.0.0.1:5000 to 127.0.0.1:5001, which corresponds to sending the packet across our virtual link layer.
  2. Below this, you should see another IP header–this is our virtual IP header, the one you are building!
  3. After that, click on the IPv6 Hop-by-Hop Option header and look at the bytes in the lower pane–this is the test packet, ie. the string you sent!

What to take away from this: The key part for debugging is #2, the virtual IP header—these are the values Wireshark found when it parsed the IP header sent by h1. When you make your own IP packets, come back to Wireshark to make sure the values are what you expect.

Examining IP forwarding

Now let’s look at what happens when we forward packets by across the network. To test this:

  1. If you already have a wireshark capture running, you can clear your capture window by selecting Capture > Restart.. from the menus.

  2. With the doc-example network running, send a test packet from h1 to h3. You can do this by entering something like send 10.2.0.3 hello in h1’s terminal. For info on how to navigate between terminals in vnet_run, see here.

In Wireshark, you should see the same IP packet three times–once for each time it was forwarded, like this:

How do we know where each packet came from? We can see which interface each packet was sent from, and where it was sent, by looking at the UDP ports. For example, in the figure above, the second packet was sent from UDP source port 5002 (r1’s if0) and to UDP port 5002 (r2’s if1), indicating it correctly follows the expected path the packet should take!

For example, here’s the expected path for this packet, annotated on the topology diagram:

Try it (seriously): Look at the virtual IP headers in each packet. What fields changed with each hop? More importantly, which fields didn’t change?

As you implement IP forwarding, you should be able to do something similar to check your work and make sure it matches the reference. Try this by sending packets to other nodes, using the reference, and using different networks, to see what you get!

For examples on how to test interoperability by running your node and the reference node, see here as a starting point.

Making your own IP packets

In your implementation, will need to create and parse IP packets using the same IPv4 header format you have been seeing here. Note that you do not need to manually define a struct or write serialization/deserialization code for the header yourself. Any language you choose should have a library that can do this for you, and you can use it:

  • In Go, see the IP-in-UDP example for a demo on how to parse the IP header. This example uses a go module to parse the IP header that we have posted that works just like one built-into Go, but with a few changes to make it more compatible for our project.
  • In C/C++, a struct for the IP packet header is available in /usr/include/netinet/ip.h as struct ip. For an example of how to use it, see this code example from last year’s version of the course.
  • In Rust, the etherparse crate provides an IP packet header struct.

Next Step: Start building some IP packets in your own implementation! Use what you’ve learned about dissecting IP packets in Wireshark to explore the fields and make sure your packets look correct. You don’t need to do this just now, but you now know enough to get started!

As you work on this part, see the note below about working with the IP checksum.

The IP checksum

How do you compute the checksum?

The IP checksum is defined in RFC1071, but you do NOT need to implement the IP checksum algorithm yourself. The IP-in-UDP example shows an example for a library you can use for this in Go—see the README in this example for a link to a C version. If you find another library other than the one listed here, feel free to use it.

Usually, the checksum function takes in a byte array and produces a 16-bit result, which should be filled into the “checksum” field in the IP header.

Some things to keep in mind for computing the checksum:

  • The checksum is computed over the IP header only, not the whole virtual IP packet. See the IP-in-UDP example for details
  • When computing the checksum over the header, the bytes for the checksum field should be set to zero
  • When you forward a packet, you must decrement the TTL. Since this changes the IP header, you MUST recompute the checksum each time you forward a packet.

Checking the checksum in Wireshark

Wireshark can also validate your IP header’s checksum, just like a real IP stack.

By default, this functionality is disabled in Wireshark, so we just need to enable it. To do this:

  1. When examining a virtual IP packet, right click on the header (as shown in the figure) and select Protocol Preferences > Validate IPv4 checksum if possible

  2. Expand the IP header to look at the checksum field. You should see an annotation from wireshark about the checksum status, like this:

(If this figure is too small to see, right-click and select “open Image in new tab”)

For all of your IP packets, you should now see Wireshark reports if the checksum is correct. When you send your own IP packets, use this to make sure you are using the checksum correctly!

Examining RIP packets

Once you feel comfortable with IP forwarding, you can begin to implement our version of the RIP protocol to exchange routing information between routers.

For information on the RIP protocol format we will use, as well as the mechanics for how to send messages, see the RIP section of the handout and the RIP specification.

For this project, we are using a slightly modified version of RIP compared to the standard version. To help you view and debug RIP messages, we created a custom plugin (called a “dissector”) for Wireshark to decode this protocol. In wireshark, we call our version the CS168 RIP protocol.

If you are using the course container, the CS168RIP dissector should be installed automatically in Wireshark. If you don’t see CS168RIP packets show up automatically, see these instructions to install it manually.

To see RIP packets in Wireshark, we can do the following:

  1. With Wireshark open, run the doc-example network, or any other network with multiple routers
  2. If you have been following this guide from earlier, be sure to remove the not cs168rip filter from the previous step and press Enter. Your wireshark view should show some RIP packets, like the figure below.

    If you do not see any RIP packets and just see UDP packets, make sure you have added [“Decode As” rules] to your UDP ports.

  3. Click on a RIP packet and expand the fields to see the full RIP message!

Now that you can see RIP messages for all your nodes, you should have a good view into how routing messages are exchanged–which you should find useful for debugging.