Lab 04: Libpcap Tutorial

Table of contents
  1. Lab 04: Libpcap Tutorial
  2. Introduction
    1. Logistics
    2. Learning objectives
    3. Getting the config
    4. Generating your .env file
    5. Network topology
  3. libpcap tutorial
    1. Sample comparison
    2. Directory structure
    3. Compilation
    4. Sniffing and printing packets
      1. The sniffing loop
      2. Trying it out
      3. Printing ARP Requests
      4. Extracting the Ethernet header
      5. Parsing the ARP header
      6. Formatting addresses
      7. Running the code
  4. Task 1: Print an IP packet content
    1. Step-by-step instructions
  5. Task 2: Print an ICMP packet content
    1. Implementation steps
  6. Submission

Introduction

This lab serves as an introduction to working with libpcap to capture and explore packets in C.

Logistics

We will continue with the same set of tools from Lab01, these are namely:

  1. Wireshark to visually see packets and protocols.
    • Install this on your local machine, so you can see things visually.
  2. If you are comfortable with command line, you can also use tshark to observe the same packets and protocols, directly on the server machine.

  3. scp or rsync will prove to be useful to obtain packet captures from the server and download them on your local machine. They should be installed by default on your Linux distribution that you are running.

Learning objectives

After completing this lab, you should be able to:

  • Use libpcap to capture and manipulate packets on the wire.

  • Examine network packets captured on the wire.

Getting the config

You can find the starter setup for this lab under the 04_Lab04 directory of the csse341-labs repository. If you have set up your private repository correctly, you can fetch the latest version of the labs using the following sequence of commands:

  1. Synchronize with the labs remote using git fetch upstream.

  2. Pull the latest changes from the main branch of the class repository:

    $ git pull upstream main
    
  3. Push the starter setup to your repository so you can start modifying it:

    $ git add 04_Lab04
    $ git push origin main
    

Generating your .env file

Before we spin up our containers, there are some configuration variables that we must generate. To do so, please run the gen_env_file.sh script from the lab repository directory as follows:

$ ./gen_env_file.sh

If run correctly, you will find the following new files:

  1. .env (hidden file - use ls -al to see it) contains your UID and GID variables.

  2. connect_*.sh a utility script to connect to each container in this lab.

Network topology

In this lab, we will be working with three machines connected to the same local network. They will live on the same subnet and all can access each other directly. The machines are:

  1. hostA with IP address 10.10.0.4
  2. hostB with IP address 10.10.0.5
  3. attacker with IP address 10.10.0.10

libpcap tutorial

As you might have noticed in the previous lab, running our exploit using python is very slow. On average, to get a response from a container on the same (virtual) network, it took us an average of 35.435 ms; that is terrible. It is even slower than me trying to access 8.8.8.8 (average of 15 ms). It will also raise alarms in case traffic is this slow on a local network, thus compromising an attacker’s ability to hide their tracks.

We would like to do better in this lab, especially that we will start running into cases where a successful exploit is performance-dependent. Therefore, we will use C as our programming language, and make use of libpcap (same thing provided by tcpdump) to write our exploit. This section serves as an introduction and tutorial for libpcap in C.

The libpcap library is not the fastest either, but it is much faster than python. If we really want things to run at line speed (i.e., as if there was an actual ghost machine there), it would require a bit more hacking that is beyond what we will cover in this class. If you are interested, take a look at eBPF and AF_XDP.

Sample comparison

When running the previous lab’s exploit using C we get:

PING 10.10.0.13 (10.10.0.13) 56(84) bytes of data.
64 bytes from 10.10.0.13: icmp_seq=1 ttl=64 time=12.6 ms
64 bytes from 10.10.0.13: icmp_seq=2 ttl=64 time=2.88 ms
64 bytes from 10.10.0.13: icmp_seq=3 ttl=64 time=0.931 ms
64 bytes from 10.10.0.13: icmp_seq=4 ttl=64 time=4.00 ms
64 bytes from 10.10.0.13: icmp_seq=5 ttl=64 time=2.93 ms
64 bytes from 10.10.0.13: icmp_seq=6 ttl=64 time=0.946 ms
64 bytes from 10.10.0.13: icmp_seq=7 ttl=64 time=4.03 ms
64 bytes from 10.10.0.13: icmp_seq=8 ttl=64 time=2.93 ms
64 bytes from 10.10.0.13: icmp_seq=9 ttl=64 time=0.963 ms
64 bytes from 10.10.0.13: icmp_seq=10 ttl=64 time=7.93 ms

--- 10.10.0.13 ping statistics ---
10 packets transmitted, 10 received, 0% packet loss, time 9029ms
rtt min/avg/max/mdev = 0.931/4.008/12.555/3.466 ms

While when running it using python we get:

PING 10.10.0.13 (10.10.0.13) 56(84) bytes of data.
64 bytes from 10.10.0.13: icmp_seq=1 ttl=64 time=96.7 ms
64 bytes from 10.10.0.13: icmp_seq=2 ttl=64 time=24.0 ms
64 bytes from 10.10.0.13: icmp_seq=3 ttl=64 time=30.9 ms
64 bytes from 10.10.0.13: icmp_seq=4 ttl=64 time=33.0 ms
64 bytes from 10.10.0.13: icmp_seq=5 ttl=64 time=32.0 ms
64 bytes from 10.10.0.13: icmp_seq=6 ttl=64 time=30.9 ms
64 bytes from 10.10.0.13: icmp_seq=7 ttl=64 time=33.2 ms
64 bytes from 10.10.0.13: icmp_seq=8 ttl=64 time=31.6 ms
64 bytes from 10.10.0.13: icmp_seq=9 ttl=64 time=17.9 ms
64 bytes from 10.10.0.13: icmp_seq=10 ttl=64 time=24.0 ms

--- 10.10.0.13 ping statistics ---
10 packets transmitted, 10 received, 0% packet loss, time 9014ms
rtt min/avg/max/mdev = 17.937/35.435/96.741/20.985 ms

You can clearly see how big the difference is.

Directory structure

Under the volumes/ directory, you will see a code/ directory that contains the demo source code in addition to a bunch of utility helpers that will be useful for you later on.

As of writing this document, my directory tree looks like the following:

$ tree .
.
├── CMakeLists.txt
├── include
│   ├── log.h
│   ├── pcap_util.h
│   ├── print_arp.h
│   ├── print_icmp.h
│   ├── print_ip.h
│   └── util.h
└── src
    ├── CMakeLists.txt
    ├── main.c
    ├── pcap_util.c
    ├── print_arp.c
    ├── print_icmp.c
    ├── print_ip.c
    └── util.c

3 directories, 14 files

The directories we care about are the following:

  1. src: This directory contains source code for all of the utitilies and main files. If you wish to add more files to this project, then this is the place to add them.
  2. include: This includes header files containing definitions for the various functions in this project.

Of particular interest for us at this point is the include/log.h header file. It contains a bunch macros that you can use to color your screen output to make things more obvious. It provides three macros that can be used exactly as you would use printf.

  1. print_log: Prints the output in green color. It prepends the file name, the function name, and the line of code to the output.
  2. print_err: Prints the output in red color with an ERROR label.
  3. print_warn: Prints the output in yellow color with a WARNING label.

Feel free to use these functions to your desire, you just need #include "log.h" in your list of headers included.

Compilation

This project uses cmake to generate build files and resolve all dependencies. I have already provided you with a CMakeLists.txt file that builds one main target and all of the helper libraries. Here is the workflow to get it up and running:

  1. Create a build/ directory to hold all generated file.

    $ mkdir -p build && cd build/
    
  2. Call on cmake from the build directory to generate the build files:

    $ cmake ..
    -- The C compiler identification is GNU 14.2.0
    -- Detecting C compiler ABI info
    -- Detecting C compiler ABI info - done
    -- Check for working C compiler: /usr/bin/cc - skipped
    -- Detecting C compile features
    -- Detecting C compile features - done
    -- Found libpcap library!
    -- Configuring done (0.1s)
    -- Generating done (0.0s)
    -- Build files have been written to: /home/noureddi/git/github/rose-hulman/courses/csse341/csse341-labs/04_lab04/volumes/code/build
    
  3. Build the demo using make:

    $ make
    [  8%] Building C object src/CMakeFiles/util.dir/util.c.o
    [ 16%] Linking C shared library ../lib/libutil.so
    [ 16%] Built target util
    [ 25%] Building C object src/CMakeFiles/pcap_util.dir/pcap_util.c.o
    [ 33%] Linking C shared library ../lib/libpcap_util.so
    [ 33%] Built target pcap_util
    [ 41%] Building C object src/CMakeFiles/print_arp.dir/print_arp.c.o
    [ 50%] Linking C shared library ../lib/libprint_arp.so
    [ 50%] Built target print_arp
    [ 58%] Building C object src/CMakeFiles/print_ip.dir/print_ip.c.o
    [ 66%] Linking C shared library ../lib/libprint_ip.so
    [ 66%] Built target print_ip
    [ 75%] Building C object src/CMakeFiles/print_icmp.dir/print_icmp.c.o
    [ 83%] Linking C shared library ../lib/libprint_icmp.so
    [ 83%] Built target print_icmp
    [ 91%] Building C object src/CMakeFiles/sniff.dir/main.c.o
    [100%] Linking C executable ../bin/sniff
    [100%] Built target sniff
    

As you can see, cmake has generated a bunch of makefiles and then we could use make to compiled everything. This will generate a bunch of helper libraries and then link on executable called sniff that will be available under bin/sniff.

Running the sniff executable on your server will fail as it needs sudo privileges. You should only run this executable from inside of a container.

Sniffing and printing packets

We will first start by looking at src/main.c. This is a simple sniffer that listens on the network for incoming packets, and then simply prints the timestamp of when the packet was received, along with the packet’s length in bytes.

The code is well documented and does things methodically using helper functions. The first helper function we employ is the find_pcap_dev function that you can find in include/pcap_util.h and implemented in src/pcap_util.c. Here are the highlights of this function:

  1. Fist, we’d like to find a device we listen on. By default, we listen on eth0 (configured by the ifname variable in the code). Lines 21 through 32 loop through all of the container’s interfaces to find eth0, and return an error if they can’t find it. You will rarely, if never, have to mess with this piece of code.
  2. Second, we open the interface eth0 for listening. We use the pcap_open_live function on line 37. This line will rarely change, and will write an error message into errbuf if it fails.

    However, of particular interest to us is the PCAP_OPENFLAG_PROMISCUOUS flag (the fourth argument). This will indicate that our interface should capture all packets, even those intended for other machines or non-existing machines. That is crucial for us to be able to run our exploits.

  3. Third, we’d like to compile our packet filter. We only care about a certain subset of packets and not everything. In this demo, we only care about capturing ARP and ICMP packets. Lines 64 through 76 do just that.

    Of particular interest to us is the filter expression itself. It is defined at the top of the file src/main.c in:

    static const char *filter_expr = "arp or icmp";
    

    If you’d like to change that expression, you can either (1) change the variable directly, or (2) pass the filter expression as an argument to the program when you run it. For example to capture all IPv4 and ARP packets, we’d do

    sudo ./sniff "ip or arp"
    

    Note that for expressions with spaces, you need to use the quotes.

    Please note that the connect_*.sh scripts log you in as a non root user. This user has the same UID and GID as your own user on the virtual machine server for the class. This way, any files generated using make on your container will be owned by your user on the virtual machine. This will be very helpful if you edit some of the source files from the container; proper permissions will be preserved and you will retain access to them from outside the container. This is why we require the use of sudo here since sniffing requires higher privileges.

    However, if you connect to the container using docer exec -it attacker bash, then you will be logged in as root. As root, you do not need to use sudo here, but permissions on files will be mangled if you edit any of the files or generate a new file.

    Moral of the story, use the connect_*.sh scripts to connect to the containers and then use sudo when doing anything that requires higher privileges.

  4. Finally, our main loop lives on line 37, it is the following:

    // This is the main loop to listen for packets.
    while((rc = pcap_next_ex(handle, &hdr, &pkt)) >= 0) {
      tsstr = fmt_ts(&hdr->ts);
      print_log("(%s) Got a packet of len %d that is not an APR packet!\n", tsstr,
                hdr->len);
    }
    

    This loop will continue listening for packets until it receives an error, or you exit the program. For every captured packet, it will execute the body of the loop. We will talk more about this loop in the next section.

The sniffing loop

Our loop touches on the three following variables:

  1. pcap_t *handle: This is a pointer to a pcap_t structure. It is returned to us by the find_pcap_dev function. It contains metadata and config options for our sniffing session. You will never need to edit anything with this, you just need to pass it around sometimes to do pcap specific things.
  2. struct pcap_pkthdr *hdr: This is a pointer to a struct pcap_pkthdr structure.

    This structure contains the following members:

    1. ts: a struct timeval representing the time when the packet got captured.
    2. caplen: the number of bytes that are available from the packet.
    3. len: the length of the packets, in bytes. This might be larger than caplen if the packet is bigger than what libpcap can handle.
  3. const u_char *pkt: This will be a pointer to the actual bytes in the packet, we will be mostly working with this one.

In this loop, we are doing two things:

  1. First, we use the utility function fmt_ts to read the packet’s timestamp and format it into a nice string. You can check out the code for fmt_ts in nslib/util.c. Feel free to use this function as you see fit.
  2. Then, we just print the formatted timestamp along with the length of the packet in bytes.

Trying it out

Let’s go ahead and try it out. First, compile the code from the build/ directory:

$ make

Then, bring up the experiment from the 04_lab04/ directory:

(04_lab04/) $ dcupd

Then, login to the attacker container, and start the program. First move into the /volumes/code/build directory and then run:

(attacker) $ sudo ./bin/sniff

Then, from hostA, try to ping the attacker container. Note that as we mentioned above, that container does not respond to ICMP pings, so you will not receive a reply.

$ ping -c1 attacker
PING attacker (10.10.0.10) 56(84) bytes of data.
64 bytes from attacker.local-net (10.10.0.10): icmp_seq=1 ttl=64 time=0.128 ms

--- attacker ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.128/0.128/0.128/0.000 ms

Once the ping had started, we see some packets at the attacker, looking like the following:

[LOG:find_pcap_dev:35] Starting sniffer on interface eth0
[LOG:find_pcap_dev:73] Setup done successfully and ready for listening...
[LOG:main:39] (18:30:03.178828) Got a packet of len 42!
[LOG:main:39] (18:30:03.178851) Got a packet of len 42!
[LOG:main:39] (18:30:03.178873) Got a packet of len 98!
[LOG:main:39] (18:30:03.178895) Got a packet of len 98!
[LOG:main:39] (18:30:08.222276) Got a packet of len 42!
[LOG:main:39] (18:30:08.222520) Got a packet of len 42!

Note that your output will show the full path for the file in the log message, that is okay. I just removed them from the output for brevity.

Printing ARP Requests

Now, let’s make it more useful, we’d like to print the content of the packet we receive. We will now be looking into src/print_arp.c.

Recall from previous labs that an ARP packets sits on top of the underlying data link layer, which is Ethernet in our case. So our packet would look something like this:

+ ------------------------------------------------ +
+               ETHERNET HEADER                    +
+ ------------------------------------------------ +
+                 ARP HEADER                       +
+ ------------------------------------------------ +

So we must peel those layers one by one to extract the information we care about.

Extracting the Ethernet header

First, let’s peel off the Ethernet header. To do this, we will use a nifty C trick, which is pointer casting. The main idea behind this is the following: the packet is nothing but a bunch of bytes, so I will cast different parts of the packets into different pointers, thus allowing me to access the packet bytes in a more readable way.

In src/main.c, uncomment lines 40 to 52 and line 10. Then comment out the lines that print the packet from the previous demo.

To represent an Ethernet header, we use the struct ether_header structure. You can find the definition of that structure below:

struct ether_header
{
uint8_t  ether_dhost[ETH_ALEN];	/* destination eth addr	*/
uint8_t  ether_shost[ETH_ALEN];	/* source ether addr	*/
uint16_t ether_type;		        /* packet type ID field	*/
} __attribute__ ((__packed__));

The header simply contains the destination mac address (as 6 bytes or 48 bits), the source mac address, and then the type of the protocol coming after that header.

Therefore, all we need to do is to cast the packet into a pointer to a struct ether_header, and we can access those fields easily, as follows: eth_hdr->ether_type. You can check the source code of this structure by following the link here.

Use the elixir link above to find the source code and documentation of any of the headers and address structures we use in this class; it is very useful.

Now, we would need to check if the packet is an ARP packet, or something else. Therefore, we can read the ether_type field. However, we have an issue here.

Network packets are always in Big Endian order. This becomes a problem if our machines are Little Endian, which would lead us to see incorrect values. Therefore, anytime you are accessing anything that is larger than a byte in this class, use ntohs, ntohl, htons, or htonl as you see fit.

We will need to ntohs to get the type field in the correct order that we can read. Here are the common function you would use:

  1. ntohs: Network to host order short. short stands for 16 bits, or 2 bytes.
  2. ntohl: Network to host order long. long stands for 32 bits, or 4 bytes.
  3. htons: Host to network order short.
  4. htonl: Host to network order long.

So now, we can check the value of the field we extracted and compare it to the ARP type we are looking for. Luckily, all those constants have been defined for us, you can check them out at the same link above, but here they are for quick reference:

/* Ethernet protocol ID's */
#define	ETHERTYPE_IP		0x0800		/* IP */
#define	ETHERTYPE_ARP		0x0806		/* Address resolution */

If the type field matches ETHERTYPE_ARP then we will call the function parse_arp provided in src/print_arp.c. Otherwise, we just print the same thing we did in the previous exercise.

Parsing the ARP header

Now, let’s check out parse_arp function, you can find it under src/print_arp.c. It pretty much operates in the same way that Ethernet parsing works, we are just dealing with a different protocol header.

int
parse_arp(const u_char *pkt, struct pcap_pkthdr *hdr, pcap_t *handle)
{
  static char logfmt[1024];
  char *str = logfmt;
  struct ether_header *eth;
  struct ether_arp *arp;
  u_short a_op;
  const char *ip, *mac;

  // Grab the Ethernet header from the packet, simply cast the bytes to be
  // interpreted as an Ethernet header.
  eth  = (struct ether_header *)pkt;
  arp  = (struct ether_arp *)(pkt + sizeof *eth);
  // Watch out here that we must translate from network order to host order.
  a_op = ntohs(arp->ea_hdr.ar_op);

  if(a_op == ARPOP_REQUEST) {
    // The ARP request has the following meaningful fields:
    //  - spa: Source physical address.
    //  - sha: Source hardware address.
    //  - tpa: Target physical address.
    //  - tha: Target hardware address.
    ip = ip_to_str((void *)arp->arp_tpa);
    str += sprintf(str, "Who has %s? ", ip);

    ip = ip_to_str((void *)arp->arp_spa);
    str += sprintf(str, "tell %s!\n", ip);

    mac = mac_to_str((void *)arp->arp_sha);
    str += sprintf(str, "\t\tFrom %s ", mac);

    mac = mac_to_str((void *)arp->arp_tha);
    str += sprintf(str, "to %s.", mac);

    print_log("(%s) %s\n", fmt_ts(&hdr->ts), logfmt);
  } else if(a_op == ARPOP_REPLY) {
    // ARP Reply, simpy print out where the target is.
    ip  = ip_to_str((void *)arp->arp_spa);
    mac = mac_to_str((void *)arp->arp_sha);

    print_log("(%s) %s is at %s\n", fmt_ts(&hdr->ts), ip, mac);
  }

  return 0;
}

The first thing we notice is that we are now using struct ether_arp structure. Here’s the source code for that structure:

struct	ether_arp {
	struct	arphdr ea_hdr;     /* fixed-size header */
	uint8_t arp_sha[ETH_ALEN]; /* sender hardware address */
	uint8_t arp_spa[4];        /* sender protocol address */
	uint8_t arp_tha[ETH_ALEN]; /* target hardware address */
	uint8_t arp_tpa[4];        /* target protocol address */
};

and for the inner structure, the source code is here:

struct arphdr {
  unsigned short int ar_hrd;		/* Format of hardware address.  */
  unsigned short int ar_pro;		/* Format of protocol address.  */
  unsigned char ar_hln;		      /* Length of hardware address.  */
  unsigned char ar_pln;		      /* Length of protocol address.  */
  unsigned short int ar_op;		  /* ARP opcode (command).  */
};

Assuming we have a pointer to the ARP header called arp, here are the members we care about:

  1. arp->ea_hdr.ar_op: This is the operation that the ARP packet is doing, telling us whether it’s ARP request, ARP reply, or any other parts of the ARP protocol.
  2. arp->arp_sha: This is the packet sender’s MAC address.
  3. arp->arp_spa: This is the packet sender’s IPv4 address (in this class).
  4. arp->arp_tha: This is the packet target’s MAC address.
  5. arp->arp_tpa: This is the packet target’s IPv4 address.

We can now start the parsing. We know that the ARP header is on top of the Ethernet header, so we simply need to move the packet points by the size of an Ethernet header to be able to access the ARP header. This is exactly what the following line of code is doing:

arp = (struct ether_arp*)(pkt + sizeof *eth);

You can also write this one alternatively as:

arp = (struct ether_arp*)(pkt + sizeof(struct ether_header));

Now, we can parse the fields, but be aware that we will face the issue with network byte order again, so we must use the appropriate functions to handle it. The line below achieves that:

u_short a_op = ntohs(arp->ea_hdr.ar_op);

Now we can check that against ARPOP_REQUEST and ARPOP_REPLY to see if the packet contains a request or a reply.

Formatting addresses

To help you out with printing, I have provided you with two utility functions:

  1. mac_to_str: Takes a MAC address bytes and returns that address as a formatted string.

  2. ip_to_str: Takes in an IP address bytes and returns that address as a formatted string.

You can find the declaration for these functions in include/util.h and their implementation in sr/util.c.

Both functions above return a static buffer, which means that it will be reused by the next call to ip_to_str, thus overwriting whatever value was in there. If you need a value to persist, then you need to manually copy the return value into a separate buffer. Do not call free on the returned buffer.

The rest of the code in parse_arp is just using those functions to print the content of the packet in a nice format.

Running the code

Compile the code using make in the build/ directory, and then run it on the attacker container. Here is a sample output when hostA tries to ping the attacker machine.

(attacker) $ sudo ./bin/sniff
[LOG:pcap_util.c:find_pcap_dev:35] Starting sniffer on interface eth0
[LOG:pcap_util.c:find_pcap_dev:73] Setup done successfully and ready for listening...
[LOG:print_arp.c:parse_arp:46] (19:01:18.446095) Who has 10.10.0.10? tell 10.10.0.4!
[LOG:print_arp.c:parse_arp:52] (19:01:18.446118) 10.10.0.10 is at b6:07:6f:2a:37:f3
[LOG:main.c:main:48] (19:01:18.446147) Got a packet of len 98 that is not an ARP packet!
[LOG:main.c:main:48] (19:01:18.446198) Got a packet of len 98 that is not an ARP packet!

Task 1: Print an IP packet content

In this first task, implement the function parse_ip in src/print_ip.c that prints the content of an IPv4 packet, if one is detected. Model your code after the parse_arp function shown in this tutorial.

For parsing the header, use the struct iphdr structure. You can find its definition here (cleaned up a bit for clarity):

struct iphdr {
    unsigned int ihl:4;
    unsigned int version:4;
    uint8_t tos;
    uint16_t tot_len;
    uint16_t id;
    uint16_t frag_off;
    uint8_t ttl;
    uint8_t protocol;
    uint16_t check;
    uint32_t saddr;
    uint32_t daddr;
    /*The options start here. */
  };

Don’t forget to add the needed header files to access this structure:

#include <netinet/ip.h>

To parse the IP header, first check if the Ethernet header contains an IPv4 header. If it does, then follow the same process we did with ARP:

struct iphdr *ip = (struct iphdr*)(pkt + sizeof *eth_hdr);

To format and print an IPv4 address, you can use the same ip_to_str function as follows:

char *ip_str = ip_to_str((void*)&ip->saddr);

Step-by-step instructions

To help you out in this task, I have added some step-by-step instructions on how to parse the IP header. Please try to work on this problem yourself at first, then use my guide whenever you feel stuck. Here’s a direct link or you can find it on the left-hand side menu of the page.

Task 2: Print an ICMP packet content

In this second task, implement the function parse_icmp found in src/print_icmp.c that prints the content of an ICMP header, if one is found. The ICMP header structure looks as follows:

struct icmphdr
{
  uint8_t type;		/* message type */
  uint8_t code;		/* type sub-code */
  uint16_t checksum;
  union
  {
    struct
    {
      uint16_t	id;
      uint16_t	sequence;
    } echo;			/* echo datagram */
    uint32_t	gateway;	/* gateway address */
    struct
    {
      uint16_t	__glibc_reserved;
      uint16_t	mtu;
    } frag;			/* path mtu discovery */
  } un;
};

You can ignore the union for now, it simply represents the next 4 bytes of content in the packet. You can for now just print the type and code in human readable format, and then print the checksum in hex.

Don’t forget to add the needed header files to access this structure:

#include <netinet/ip_icmp.h>

Implementation steps

Here’s a breakdown of what you want to do:

  1. Implement the code for parse_icmp assuming that the packet already contains an ICMP header.
  2. Add code to parse_ip to call parse_icmp when an ICMP header is found in the IPv4 header.

Here’s what you want to do in parse_icmp:

  1. Offset into the packet by skipping over both the Ethernet and the IPv4 headers.
  2. Grab a pointer to that location, and cast it into a struct icmphdr *.
  3. Print the fields you are interested in.

Submission

Demo your code to your instructor and ask questions as you move along. Submit your modified files to the Gradescope submission box.