Due September 20, 2022 at 11:59PM EST
In this assignment, you will go through the basics of socket programming, and create a basic client and server that you can use for your Snowcast project. You will also learn how to use Wireshark to inspect network packets.
You may implement Lab 1 in Go, C, C++, or Rust. If you are unsure, we recommend using Go, even if it is new to you, as class examples this year will use it. We have curated a list of resources for each language here.
If you want to use Rust, note that only some members of the course staff can provide language support (though we are all happy to discuss conceptual questions!). For details, see the course staff list.
This assignment has two parts:
Before starting this lab, please set up your course development environment using this guide. This will set up a development environment you can use to build your projects.
The guide also explains how to run Wireshark within your container, which will be helpful for debugging your projects and understanding network traffic. This lab will also help you get started using Wireshark for debugging.
This lab is designed to be your starting point for the Snowcast project. Since you will likely end up using what you implement here in your project, we recommend starting work in your Snowcast repository. To create it, use the Github Classroom link in the Snowcast Assignment.
Beej's Guide is an excellent resource for understanding the basics of socket programming - although it uses C as the main language, many of the concepts presented in the guide are transferrable to other languages.
Here is how Beej's Guide defines a socket:
You hear talk of “sockets” all the time, and perhaps you are wondering just what they are exactly. Well, they’re this: a way to speak to other programs using standard Unix file descriptors.
What?
Ok—you may have heard some Unix hacker state, “Jeez, everything in Unix is a file!” What that person may have been talking about is the fact that when Unix programs do any sort of I/O, they do it by reading or writing to a file descriptor. A file descriptor is simply an integer associated with an open file. But (and here’s the catch), that file can be a network connection, a FIFO, a pipe, a terminal, a real on-the-disk file, or just about anything else. Everything in Unix is a file! So when you want to communicate with another program over the Internet you’re gonna do it through a file descriptor, you’d better believe it.
TL;DR sockets act just like file descriptors between two endpoints of a network connection - you can read data from the socket (like you would read data from a file descriptor), and you can write data to the socket (like you would write data to a file descriptor). In our project, a socket can also be thought of as a pipe between a client and the server that either endpoint can send data through.
Roughly,
A "network connection endpoint" here refers to one end of a network connection; for example, if your computer is connected to a remote server, then each of your computer and the remote server are both connection endpoints.
Beej's Guide has a good analogy for how IP addresses and ports are used to deliver network packets:
Think of the IP address as the street address of a hotel, and the port number as the room number.
In other words, the IP address is used for forwarding a packet to the destination endpoint, while the port is used for sending the packet to the application using the port at that endpoint. The kernel is responsible for knowing which applications map to which ports currently running on the system.
The next few sections will guide you through building a client-server program and sending the first few messages of your Snowcast protocol.
Note: Much of the content here mirrors the sockets demo in Lectures 2 and 3, which implement a different protocol using the same mechanics described here. While you shouldn't just copy the lecture code into your repository, you're welcome to follow along with the lecture example and use it as a reference to get started!
Our first step is to create a socket for the server to “listen” for new connections. From there, clients can send your server a request to start a connection, and then your server can set up a connection to each client.
To do this, we need to pick a port number on which the server will listen. In general, port numbers can be any 16-bit number (ie, 0-65535); port numbers less than 1024 can only be used by root, so you the port you use must be higher. Otherwise, you can pick any port number you like.[1] For example, some port numbers you could pick from are 1234, 6666, 7777, 8888, 9999, etc.
For now, you can hard-code the port number you choose (i.e. define it as a constant in your server program), or you could also pass it in as an argument.
The next step is to create the listening socket, which requires telling the kernel some information about the type of socket we want. The kernel has many types of sockets, not all of which involve IP, or even networking. Right now, since we want to establish TCP connections between clients and the server, we need to create TCP socket.
The API to create sockets varies by language. The diagram and sections below describe the most relevant functions you need in Go, C/C++, and Rust.
Regardless of the language you use, there are a few steps that neeed to happen to set up a listening socket. Expand the steps below for your language, which outlines what the process should look like. (Hint: Click the names of the functions for links to relevant documentation!)
First, you can use ResolveTCPAddr
from Go's net
package (net.ResolveTCPAddr
) to create the TCP socket and bind it to your chosen listen port. This returns a TCPAddr
that represents the new socket.
Next, we need to tell the kernel that we want the socket to listen for new connections. You can do this with net.ListenTCP
, which takes in the TCPAddr
from the previous call.
In C/C++, this process requires a few steps:
First, use getaddrinfo
here to tell the kernel what kind of socket you want. Note that getaddrinfo
essentially gives you back a struct you then need to use to make a socket.
Next, we need to create the socket itself. Here, you should pass in parameters from the struct returned from getaddrinfo
into the system call socket
. This returns the file descriptor for the listen socket.
Use bind
to bind the socket to the port you chose as the server's listening port.
Finally (for now), we need to tell the kernel that we want the socket to listen for new connections. For this part, we use listen
; the second argument to listen
specify the maximum number of waiting connections to clients your server can have at any given time (called the backlog, more on this later).
Why are there so many steps for C/C++ compared to the other languages?! The C API most directly mirrors the Linux kernel API. The other languages are all doing the same steps, they just hide some of them from you! In addition, since the C API predates all of the other languages, certain aspects are a bit more complex in order to maintain compatibility with older programs.
Use TcpListener.bind
. This function creates a new TcpListener
, which, as the name suggests, is a TCP socket that is set to listen for incoming connections on the specified address/port.
For any language, socket APIs can have many different features for convenience or historical reasons–all interact with the same low-level kernel API to perform essentially the same functions. We have done our best to outline a series of function calls that will get the job done, but there are certainly many ways to solve the same problem–you are free to use whichever version you like, so long as it meets the assignment requirements. If you have questions, feel free to ask!
Task: Start writing your Snowcast server program! Use the functions mentioned so far in the language of your choice to set up a listening socket for your server (in your server program).
To use the above functions, you may need to provide some additional parameters to specify how the socket is created. For these, keep in mind the following guidelines, which are good "default values" to use for this lab, and the class in general (so just skim this list over and return here if you need it):
Hint: A big part of this task, and systems programming in general, will be figuring out how to structure your programs well (since we don't give you stencil code, hehehe). To do this, reading up on function documentation and looking at some of the examples given in documentation can be really useful for creating a basic structure.
Also, don't be afraid to come to office hours if you're struggling with the question "so where should I even be calling this function?" or similar ones!
This is a skill we want you to learn, and it's okay to ask for help!
Warning: Don't forget error handling code! You should check if any of these functions (and any networking functions you use in general) return an error and handle them accordingly. For this part, if any functions encounter an error, you should just print out the error and exit the program.
You can usually do this in one step with a function like log.Fatal
in Go or perror
in C/C++. Error handling is an essential practice in systems code, and it will save you a lot of debugging time to add this now!
Once you've written an implementation (even if you're not fully confident in it yet), continue onto the next step. After this, we'll show you a way you can test your program.
If you run your program right now, it will just create the socket and then exit! To actually handle clients, we need to tell the server how to "accept" incoming connections.
Each of Go, C/C++, and Rust have a single function call (named "accept", or something similar) to handle a client's connection request, as listed below in the diagram.
Either way, the principle of accepting new connections is the same: calling "accept" will normally block until a client connects. Once a client connects, "accepting" the connection creates a new socket specifically for the connection between the server and the new client, while the listen socket remains unchanged. This is important, because your server should now use the new socket file descriptor to communicate with the now-connected client.
However, our server also needs to handle connections from multiple clients, so it should also continue listening on the original listening socket that you made in the previous steps. In other words, your server should not stop listening for other clients once a new client connects.
Conceptually, you'll want your code to behave something like this (in pesudocode):
. . .
listen_sock = create_listen_socket()
while true {
client_sock = do_accept(listen_sock, ...)
// . . . Do something with client_sock (later) . . .
}
To do this for real, consult the following instructions for your language:
We recommend using net.TCPListener.AcceptTCP
, which behaves similar to C/C++'s accept
in C/C++ has, except it is specific to incoming TCP connections.
net.TCPListener.AcceptTCP
will block until the next incoming TCP connection request and return a new TCPConn
, which represents the TCP socket for the connection between your server and the newly connected client.
In Rust, TcpListener.incoming
works a little bit differently.
TcpListener.incoming
"returns an iterator over the connections being received on this listener." In essence, this method calls TcpListener.accept
in a loop for you, so then all you have to handle is the TcpStream
returned by TcpListener.accept
. This TcpStream
represents the new socket made for the new client connection, and you will later use it (as a stream) to communicate with the client.
The example given in the documentation for TcpListener.incoming
is pretty good and will help you understand how to use the method.
Task: Extend your Snowcast server program so that it can accept new client connections, while continuing to listen for other connection requests!
When you run your program now, it should just hang. This is good, it means the server is waiting for new connections!
The main command you would use to do this is To see if your server is actually listening on the port you chose, we can use the command ss
, which stands for "socket statistics." ss
can view the kernel's table of open sockets to tell us if our server port is open.
While your server is running, open up another terminal and run the following:
ss -ln | grep <server port number>
You should see something like:
tcp LISTEN 0 20 0.0.0.0:8888 0.0.0.0:*
This means that one of the sockets on your machine is listening on port 8888 (or whichever port you used) for TCP connections! In other words, if you can see this output, then your server is running and listening for client connections!
You should be able to see a bunch of the sockets being used for various processes on your system. If you run it with sudo
, ss
will also output the names of the processes as well.
Now that your server is listening for clients and can accept new connections, all that's left to set up is your client program!
On start up, your client program should create a new TCP socket and connect to your server.
To connect to the server, you'll need to provide its IP address and port so that the client knows how to reach it. Since our client and server are running on the same system, we will connect using Linux's loopback interface. This is a special, virtual network interface that exists only within the local system. This is useful so that programs on the same system can still communicate using networking protocols, even when there's no network!
When making connections, the loopback interface always has the same IP address: 127.0.0.1
. We will use this IP when connecting to the server–for now, you can either pass it into your client program as an argument, or define it as a constant.
Note: We can also refer to this address using the hostname localhost
. Hostnames like localhost
(or google.com
or cs.brown.edu
) are translated into IP addresses by the OS (often using protocols like DNS, which we'll learn about later).
If you've been following the functions in this lab, your code should be set up to also translate hostnames automatically, so you can also pass in the name localhost
instead of 127.0.0.1
.
The server's port should be the one you originally chose for your server.
To build your client, you will create a socket in a similar manner to creating the server. However, instead of creating a listening socket, we will be creating one to connect to a specific destination, the server.
To do this, the relevant function calls are outlined in the diagram below and in the following sections:
First, call getaddrinfo
once again to tell the kernel what type of socket you want. Here however, you will need to pass both the server’s address and port in as part of the specifications for the socket (along with TCP, IPv4 etc).
getaddrinfo
actually returns a list of possible socket options for making the connection. You will need to iterate over the results to find one that allows you to connect–take a look at Beej's guide
or this old class example for details.
In the loop over getaddrinfo
's results, you should attempt to create a socket using the current result (by calling socket
) and then attempt to connect to the server using the new socket. To connect to the server, you will need to use the system call connect
, which takes in a socket file descriptor (i.e. the one returned from socket
) along with the result from getaddrinfo
. Once connect
successfully returns, you have connected to the server! You can then exit out of your loop, and begin sending data to the server.
In Go, use net.Dial
, which takes in a network type (i.e. TCP) and an IP address/port number, and attempts to connect to the specified address. The address/port string should be of the form address:port
–-ie. 127.0.0.1:9999
or localhost:9999
, to connect to your system's loopback address on port 9999.
In Rust, use TcpStream.connect
, which takes in an address (see the examples in the documentation for the structure of the input address) and attempts to connect to it.
Task: Start writing your Snowcast client program. Use the functions mentioned in the language of your choice to create a TCP socket for your client control program and connect the new socket to the server using the server's IP address and port number.
For the purpose of testing, print out something when the connection is established (ie, after Dial
/connect
on the client, and after accept
on the server). If you see your message printed (and nothing returned with an error), your connection was established!
Now your client should be able to connect to your server! The two arrows in the diagram below represent the initial TCP packets that should be sent between your client and server when your client attempts to connect to your server. Don't worry too much about understanding these just yet! We will cover what these packets mean closer to the TCP project.
At this point, your server program can accept a new client connection, but we haven't sent any data yet! In this section, you will add to your server's current functionality so that it can handle communication with multiple clients concurrently.
You could implement this in many ways, but we recommend you start by making a thread per new client connection, which can then do any client-specific tasks.
The important thing here is to make sure you store enough data about the client somewhere, perhaps by making a new data structure. When you create the new thread, you will need to pass this information to the thread so that it can communicate with the client.
For now, the most important information to pass to the thread is the client's socket. For this, see the instructions based on your language:
All of the information about your client's socket is contained in the net.Conn
object returned by net.AcceptTCP
. This object has various helper methods for sending/receiving on the socket, getting the client's IP address, and more.
Consider creating a goroutine for each new client, which takes the client's net.Conn
as an argument. Later, you may wish to pass a custom struct
instead that also includes more information.
Right now, you should probably store:
accept
)We recommend making a struct
to hold these and passing them to the client thread. Take a look at this old class example for details.
All of the information about your client's socket is contained in the TcpListener
returned when accepting the client's connection This object has various helper methods for sending/receiving on the socket, getting the client's IP address, and more.
As you build your snowcast project, it will be important to consider what state you need to keep track of for each client. For example, when you start sending data from radio stations to client listener programs, you may also want to store the UDP port of the client's listener. Keep this in mind as you think about how you'll implement the protocol.
Task: In your server program, for each new client connection, store the new client's data and initialize a thread to handle communication with the new client.
For now, just make your client thread print out something like "Client connected!" and then exit. If you see your message being printed when a client connects, your threads are being created!
Hello
and Welcome
Now that your server can handle communication with multiple clients concurrently, it's time to send something useful over the connection.
As seen in the diagram below, the protocol you are asked to implement in Snowcast requires your client to first send a Hello
message, and once your server receive's the client's Hello
, it should respond with a Welcome
. Other message types (such as Announce
, SetStation
, and InvalidCommand
) should be sent for other parts of the protocol, but you don't need to worry about them for this lab.
As a reminder, a client's Hello
message should look like:
Hello:
uint8 commandType = 0;
uint16 udpPort;
And this is what a server's Welcome
message should look like:
Welcome:
uint8 replyType = 2;
uint16 numStations;
Therefore, to send a message, you should send a series of bytes corresponding to one of these messages.
Each of C/C++, Go, and Rust have function calls for sending and receiving data over a socket, as summarised in the table below:
Function | C/C++ | Go | Rust |
---|---|---|---|
Send | send |
(net.Conn).Write |
TcpStream.write |
Receive | recv |
(net.Conn).Read |
TcpStream.read |
There are several ways to "compose" the data to send: for example, you can create a data-type (such as a struct
) to represent the message, and then "serialize" or "marshal" it into a byte array. Alternatively, you can call your language's send
function once for each message field, after converting each field into the appropriate size.
There are many ways to do these operations in each language. For examples, we recommend starting by taking a look at the notes for Lectures 2 and 3, and then consulting your language's resources for the method you like best.
Task:
In your client program, once your client has successfully connected to your server, send a Hello
message to the server and wait for a Welcome
in response.
In your server program, once your server receives a client Hello
, send a Welcome
response back to the client.
For both of these, you can start by hard-coding any value for udpPort
and numStations
, in the Hello
and Welcome
messages, respectively.
Note: On receiving a Welcome message, please print a message to stdout in this exact format:
Welcome to Snowcast! The server has N stations.
where N
is the NumStations
received in the Welcome message. This will ensure that your code is compatible with the autograder.
Hint: Make sure you are using network byte order to send packets, and that you are converting data back to host byte order once you have received a packet.
We'd recommend taking a peek at the snowcast dissector section of part 2 of this lab to ensure that your snowcast messages are being properly formatted and sent!
Congratulations, you have your Snowcast server and client programs exchanging messages!
The client and server programs you created here can serve as your starting point for Snowcast: your TCP client is the start of the Snowcast control client (ie snowcast_control
), and your server implements (you guessed it!) snowcast_server
!
Before we can check off your work, we ask that you refactor your code a bit so that your programs have the same names and command-line arguments as the Snowcast programs.
While this may seem annoying now, we want to make sure that you can interact with the autograder so that you can test your work–and so we can test our autograder!
More concretely:
As described in the assignment, create a Makefile
such that make
compiles your client and server and gives them the names snowcast_control
and snowcast_server
, respectively.
Update your client and server program to accept the same command-line arguments as snowcast_control
and snowcast_server
.
For example, your server should be able to run as:
$ ./snowcast_server <listen port> <file0> [file 1] [file 2] ...
Your client program should run as:
$ ./snowcast_control <server IP> <server port> <listener port>
(If you have not done so already) Update your programs to take in the client and server's address and port numbers as arguments, as shown in the spec above
Update your server's Welcome message with the number of files passed in as arguments. You don't need to handle opening/reading from files right now, but make sure that the number of stations aligns with the command-line arguments (i.e. NumStations == len(args[2:])
).
Update your client's Hello message such that value for udpPort
is the value passsed as <listener port>
on the command line. For the final version, the value you will pass for <listener port>
will be the port on which you run the listener client–for now, you can just pass in any number.
Head over to the grading server. You will receive email from Nick shortly with the subject [CS 1680] New account information
. An announcement will be posted, and this document updated, when you should have received your credentials.
We're still rolling out account invitations to those who have filled out HW0. If you have not received an invitation yet, please fill out HW0 as soon as possible and wait until you receive your credentials.
Step 1: Log into the grading server with the username and password you received from the email. Once you're logged in, enter your GitHub username (at the top).
Step 2: Add your Snowcast repository URL (e.g. brown-csci1680/snowcast-<GitHub username>
under the "Project 1: Snowcast" section. Click on the "Project 1: Snowcast" button to initiate a fetch of your repository from GitHub.
Note: If your GitHub account is not linked to your Brown email address, the grading server will give you a command to run to verify your repository.
Step 3: You should see your commit listed on the Snowcast page, as shown above. Next to this, you should see a button labeled "Compile and locate binaries". This will attempt to compile your code using your makefile (ie, using make clean && make
), and then check for the presence of snowcast_control
and snowcast_server
. (It will also check for snowcast_listener
, but ignore this for now.)
For example, your output from the "Compile and locate binaries" chec should look like this:
Cleaning repository with 'make clean'...
rm -f snowcast_client snowcast_control snowcast_server
Building repository with 'make'...
go build cmd/snowcast_server/snowcast_server.go
go build cmd/snowcast_control/snowcast_control.go
Checking for executable snowcast_control... OK
Checking for executable snowcast_server... OK
Checking for executable snowcast_listener... FAIL
Note: It's okay if you don't have snowcast_listener for the milestone!
Step 4: Next, click the button labeled “Checkoff lab milestone”. Click this button to have your lab submission checked off and graded.
We're currently finalizing grading infrastructure, so you may not immediately see output or failing tests. So long as you've made a commit that before the checkoff deadline that roughly implements these components, you'll be fine. As we finalize our tests, we'll update these instructions for more information.
If all your tests pass, they should look something like this:
stdout
the Welcome message in the format specified above.
stdout
! This can be done by appending a new line (\n
) to your print statements../snowcast_server <listen port> <file0> [file 1] [file 2] ...
, and your control must be able to run as ./snowcast_control <server IP> <server port> <listener port>
.Note: At present, the autograder will spuriously fail on certain tests (for reasons you’ll learn in IP/TCP!); we’ve added messages to the error output when this is the case. If you’re confused about the error output or think it may be a grading server issue, please comment on the grading server megathread and we’ll respond to it as soon as possible.
We thank you all for the continued patience as we shift towards a new autograding system! Keep in mind that our goal is to make this easier to test and check that your program is working as expected: your final grade will always be determined by a human looking at your code, and the test output. Good luck!
From Wireshark's website: "Wireshark is the world’s foremost and widely-used network protocol analyzer. It lets you see what’s happening on your network at a microscopic level and is the de facto (and often de jure) standard across many commercial and non-profit enterprises, government agencies, and educational institutions."
Today we will be using Wireshark to analyze the packets that come over the wire when you fetch a web page.
Start up Wireshark either from within the container as described in the setup guide (under the Running Wireshark section).
Once you have started up Wireshark, you should be presented with a window like the one below:
Packet captures are performed by attaching to one of the system's network interfaces. In this case, we will be capturing on one of the virtual network interfaces inside the container, so we will only see network traffic from container processes.
To start a capture, select Capture > Options from the menus at the top. Each of these options represents an interface on which packets can be received. In general the three interfaces you will care about are Loopback, Ethernet, and Wi-Fi:
lo
, the loopback interface: This contains traffic to and from localhost
–it lives exclusively inside the container. Your own system has its own loopback network, which is local to your own machine. You should capture on this interface if you are debugging a program that connects to localhost
, like your assignments.eth0
: This is the container's link to the outside world (i.e., your host system, and the Internet). You should capture on this interface to see traffic that leaves the container.Not all machines will have the same naming for these devices, and in some cases they may not all be available. For example, your host system may show a wired and wireless interface.
Note: for the rest of the lab, please make sure to run any commands we mention within the course container!
Task: Select the lo
interface and choose Start to start capturing packets.
Warning: We are aware of some issues where Wireshark crashes with a segfault when starting the capture. If this occurs, simply try again–this seems to work for us. We are looking into the issue and may post an update in the future.
We have created a custom dissector for snowcast that will display the snowcast command messages as their own protocol type! Follow the instructions in the starter repository's util README to get the dissector set up. If using the reference binaries, or if your server and client are sending the HELLO and WELCOME messages, you should see something similar to the following on Wireshark when a new client connects:
Wireshark should display the protocol as cs168snowcast. Notice that the messageType value is important for the dissector to recognize the command type.
Once we have Wireshark open on the appropriate interface, it automatically begins capturing packets.
Task:
Stop the lo
packet capture and start a packet capture on interface eth0
In your container, run the following commands:
$ mkdir tmp
$ cd tmp
$ wget -r -l 1 http://cs.brown.edu
This pulls the CS website's data onto your local machine in the directory tmp
. After the lab is over, you can delete this directory.
You should see a large dump of data in Wireshark. If at any time you want to clear your current log of packets you can stop the current capture by clicking the big red box, then starting another capture by clicking the blue shark fin. Note that you have the option to save the old capture when you click the fin to start a new capture (which may be useful for debugging on later projects).
Task: When the wget
command finishes, click the red box to stop the capture. Once the capture stops, Wireshark will no longer log additional packets.
Look under the protocol
column. As you scroll down the log of packets, you should see packets from at least the following protocols: DNS, TLS, TCP, and HTTP.
Looking at network traffic can leave you faced with an overwhelming amount of information. Fortunately, wireshark provides support for filtering traffic so that you can focus on only what you need. For the first section, we'll take a look at ARP packets. Address Resolution Protocol (ARP) is a protocol for use on local networks to learn about IP addresses present on a local network–we'll discuss it in a future lecture. You don't need to worry about the details of the ARP protocol now, but for now can look over the packets!
Task: In the filter bar at the top of Wireshark's window, enter "arp" and click the right facing arrow.
There are many different types of filters that can discriminate between packets based on more than just protocol; if you would like to see and select from a wide range of filters click the "Expression…" button next to the filter bar and browse through the filter options. Later in this lab we will try some more sophisticated filters.
If you do not find any ARP packets when you apply the filter it is likely because the ARP information is cached on your system. To get around this, first clear the log of packets then start a new capture (red stop button then blue shark fin). Then, run arp -n
to see your ARP table, sudo ip -s -s neigh flush all
to clear your ARP table, then arp -n
again to confirm the table has been cleared. Now run the command wget -r -l1 http://cs.brown.edu
again to fetch the webpage. As before, click the red box to stop the capture once wget
finishes.
Now, look at your ARP packets - you should see at least two. Under the info column Wireshark tells you exactly what the ARP packets are doing. We can see a request ARP packet that under Info
says, "Who has <requested IP>? Tell <requesting IP>." and a response ARP packet that under info says, "<requested IP> is at <destination Ethernet address>".
If you click on a single ARP packet you will see in the box below the packet log that Wireshark will actually dissect the packet for you (in hex), and in the third box you can see the raw bytes of the packet. If you click on certain bytes Wireshark will let you know what part of the packet those bytes belong to. Conversely if you click on a dissected field of the packet Wireshark will tell you which raw bytes it grabbed the data from to attain that field of data.
You should see something similar to the below screenshot when the Ethernet and ARP (Address Resolution Protocol) fields are expanded.
To dive deeper into dissecting, let's examine some DNS traffic. You will learn about DNS later in the course, but for now know that DNS is the primary way that a computer gets the IP address for a web domain (in our case, cs.brown.edu
). For our purposes, DNS messages will appear in two parts, a query which asks a DNS server for information about a domain (eg. cs.brown.edu
) and a response which contains an answer (usually an IP address).
Our machine will emit a DNS query, and receive a DNS response.
Task: Filter on "dns" and click on the first packet.
Collapse all the dissected fields, and note that each top-level field is a header for a different protocol. The top field is the outermost header, in the sense that for anything parsing the header, the parser would find the top most header, then the next header down the list, then so on until the parser reads all the bytes in the packet. Also note our DNS packet contains a header for each layer of the OSI model.
The first field represents the entire packet of raw bytes that Wireshark received over the wire (from the Physical layer) this is the only field not constructed from a protocol header. The next field is constructed from the Ethernet header that Wireshark parsed (Link layer), within the Ethernet frame is the IP packet (Network layer), within the IP packet is the UDP datagram (Transport layer), and the lowest header is for the DNS query (Application Layer).
If you expand any of these dissected fields, you can see the parsed data of the header plus the payload for the protocol. It is important to note how encapsulation is used here. The Ethernet frame consists of an Ethernet header plus a payload, where the payload is an IP packet. The IP packet has an IP header and a payload, which is the UDP datagram. The UDP datagram consists of a UDP header and a payload, the DNS query. The DNS query, the last level of encapsulation, contains its header and a payload, which is the query itself. This image may help you visualize it:
Now, let's explore these protocols a bit more deeply. You can explore the headers by expanding the respective protocol field.
Task: Select a DNS packet where the Info section starts with Standard Query...
. Expand all the fields corresponding to the different levels of the OSI model. Add a text file to your repository called wireshark_lab.md
in which you will take notes. (This file will be checked by a member of the course staff, so don't worry about the format.)
Pick one DNS packet and write down the following:
For each of these protocols, you can put your cursor over the addresses/ports and see which raw bytes from which these fields were extracted from the packet. Wireshark should highlight these bytes as you hover over the address/port.
Wireshark shows more than the source and destination of a packet within a network layer, you can also see protocol-related information. Next, we'll look at the content of a DNS packet, ie. the application-layer information.
Task: Expand the DNS field of the packet you selected earlier, and completely expand the Queries
subfield. In this section, you can see the domain being queried (cs.brown.edu
) and the type of record is wants to find. For this example, the type will either be A
(for an IPv4 address) or AAAA
(for an IPv6 address).
On expanding the query field, there should be blue text saying [Response in N]
, where N is another packet number. Click on this text and it should select another packet–this is the DNS server's response to this query!
Next, let's look a bit deeper at the response. Expand the DNS fields in the response, including one of the Answers
subfields. You sould see an entry for Name: cs.brown.edu
and some metadata about the answer. Later in the course you will learn exactly what this response means–for now, it's just important to see how the packet bytes can break down into different fields that have meaning for the protocol. Wireshark knows about a huge list of common protocols (like DNS, HTTP, ARP, etc.) and can know how to parse these fields so you can see them!
Task: Look at the answer fields in the DNS response packet for cs.brown.edu
. One of the subfields should be "Address", which lists an IP address. In your notes file, list the IP address associated with the response–it should be in the form of an IPv4 address (eg. 10.1.2.3) or an IPv6 addresss (2001:47f::212).
If you remove the filter and now look at the subsequent traffic, you should see a TCP connection to the IP address listed in the DNS response. Thus, you can see how wget
looked up the address for cs.brown.edu
and then established a connection to it to fetch the web page.
Use Wireshark! It is extremely helpful in dissecting and understanding the packets that are flying to and from a network interface. For IP you can use it to verify your nodes are sending packets and the packets have the form and fields that you expect. For example, you can use Wireshark to check that within a packet a number is sent with either big endian or little endian, which is much harder to do at the sending, intermediate, or receiving nodes (for example, you may need this functionality to check packet checksums). Wireshark will be very helpful for TCP to check that your nodes are following the TCP protocol. To examine TCP with Wireshark, there will be a separate Wireshark lab during the TCP assignment.
You can also look at this list of port numbers and what each one is typically used for, if you're interested. ↩︎