DATA STRUCTURES The only data structure I added was the ICMP header. This simplified the code for me because I could decompose it into just one function which send various icmp messages. This meant that theoretically if one type worked, then the rest would work. Further, Several of the fields of the ICMP header were not used or required by this lab which made it easier to boil down an ICMP header to a skeleton. /* Structure of a ICMP header */ struct sr_icmp_hdr { uint8_t icmp_type; uint8_t icmp_code; uint16_t icmp_sum; uint32_t unused; /* This field's use differs between ICMP message types, and is not needed in this lab. */ } __attribute__ ((packed)) ; typedef struct sr_icmp_hdr sr_icmp_hdr_t; DESIGN CHOICES I tried my best to decompose and reuse code. This hopefully improved the robustness of my code. For example, I have one function that encapsulates packets in an ethernet header before sending them, regardless of whether they are arps or icmp messages or forwarded ip messages. This somewhat separates the functionality of the layers as well. Almost all fields of the packets were kept in network byte order. This made the code slightly more efficient and also simplified implementation. The only exception was for lengths which are required for allocations. For these I typically used accessor functions located in sr_utils.c/.h. To implement the arp cache I followed the description in the sr_arpcache header. After reading the arp RFC I decided to add all mac, ip mappings I received that were destined for me before checking if they were replies or requests. I also chose not to update existing entries before checking if they were destined for me (as described in the arp RFC) because there was not good functionality to for updating entries, and it could result in duplicate entries. For longest prefix match i used a linear search of the list. This seemed like a good idea because of the size of the routing table, but in a production implementation I would choose a more efficient algorithm.