measurement-and-instrumentation
Building a Network Packet Sniffer in C for Network Analysis
Table of Contents
Building a Network Packet Sniffer in C for Network Analysis
Network packet sniffing is a fundamental technique for network administrators, security analysts, and embedded developers who need to inspect traffic at the wire level. Building a packet sniffer in the C programming language offers deep control over data structures and performance, making it ideal for high-throughput environments. Using the libpcap library, a C developer can capture raw packets, parse protocol headers, and analyze network behavior without relying on high-level abstractions. This article provides a comprehensive, hands-on guide to constructing a robust packet sniffer in C, from understanding packet anatomy to implementing advanced filtering and dump features.
Fundamentals of Network Packets
Every packet transmitted over a network conforms to a layered architecture. The most common model in local area networks is the Ethernet + IP + TCP/UDP stack. A packet sniffer must unwrap these layers to extract meaningful information. Understanding the binary layout of each header is essential before writing any code.
Ethernet Frame
The Ethernet frame is the lowest layer visible to a raw packet capture. It starts with a 14‑byte header comprising a 6‑byte destination MAC address, a 6‑byte source MAC address, and a 2‑byte EtherType field (e.g., 0x0800 for IPv4, 0x0806 for ARP). After the header comes the payload (typically an IP datagram) and a 4‑byte Frame Check Sequence (FCS) that is usually stripped by the network interface card. When writing a sniffer, you often skip the FCS because libpcap provides the packet without it.
IP Header (IPv4)
The IPv4 header is at least 20 bytes long. Its structure includes the version (4 bits), Internet Header Length (IHL, 4 bits), Type of Service, total length, identification, flags, fragment offset, Time‑to‑Live (TTL), protocol (e.g., 6 for TCP, 17 for UDP), header checksum, source IP address (4 bytes), and destination IP address (4 bytes). The IHL field gives the header length in 32‑bit words; if no options are present, IHL = 5, meaning a 20‑byte header. Parsing this header correctly allows the sniffer to determine the next protocol layer.
Transport Layer Headers (TCP and UDP)
TCP and UDP headers follow the IP header. The TCP header is at least 20 bytes and contains source port, destination port, sequence number, acknowledgment number, data offset, flags (SYN, ACK, FIN, etc.), window size, checksum, and urgent pointer. UDP is simpler: a fixed 8‑byte header with source port, destination port, length, and checksum. Parsing these headers enables the sniffer to display connection endpoints and application protocol information.
Setting Up the Development Environment
Developing a packet sniffer in C requires a Linux or Unix‑like system (macOS also works with some adjustments) and root or sudo privileges, because capturing raw packets normally requires elevated permissions. The following components are necessary:
- GCC (GNU Compiler Collection) – compile C source code with
gcc. - libpcap – the packet capture library that provides a portable API for low‑level network monitoring. Install the development package with your package manager (e.g.,
sudo apt-get install libpcap-devon Debian/Ubuntu). - pcap.h header – included with libpcap‑dev; provides function declarations and data structures.
- Root privileges – run the executable as root (
sudo ./sniffer) or set the CAP_NET_RAW capability on the binary.
Verify your installation by compiling a simple test program that includes <pcap.h> and links with -lpcap. If compilation succeeds, the environment is ready.
Building the Packet Sniffer
The core workflow of a libpcap‑based sniffer involves three primary phases: device selection, capture initiation, and packet processing. Below we walk through each step with code snippets.
Step 1: Finding a Network Interface
Use pcap_findalldevs() to retrieve a list of available interfaces. This function returns a linked list of pcap_if_t structures, each containing the device name (e.g., eth0, wlan0) and a description. For a simple sniffer you can either hard‑code the interface or let the user choose. Always check for errors using pcap_geterr().
pcap_if_t *alldevs;
char errbuf[PCAP_ERRBUF_SIZE];
if (pcap_findalldevs(&alldevs, errbuf) == -1) {
fprintf(stderr, "Error finding devices: %s\n", errbuf);
return 1;
}
// pick first non‑loopback device
pcap_if_t *dev = alldevs;
while (dev != NULL && strcmp(dev->name, "lo") == 0)
dev = dev->next;
Step 2: Opening the Device for Capture
Call pcap_open_live() to open the selected interface in promiscuous mode. Promiscuous mode tells the network card to capture all packets, not just those addressed to the host. The function takes the device name, snapshot length (maximum bytes to capture per packet – 65535 captures the entire frame), promiscuous flag (1 for yes), timeout in milliseconds, and an error buffer.
pcap_t *handle = pcap_open_live(dev->name, 65535, 1, 1000, errbuf);
if (handle == NULL) {
fprintf(stderr, "Could not open device %s: %s\n", dev->name, errbuf);
return 1;
}
Step 3: Setting a Capture Filter (Optional but Recommended)
libpcap supports the Berkeley Packet Filter (BPF) syntax. Filters are compiled with pcap_compile() and applied with pcap_setfilter(). Common filters include "tcp", "udp", "icmp", "port 80", or "host 192.168.1.1". A well‑crafted filter reduces overhead by discarding irrelevant packets in kernel space.
struct bpf_program fp;
char filter_exp[] = "tcp";
if (pcap_compile(handle, &fp, filter_exp, 0, PCAP_NETMASK_UNKNOWN) == -1) {
fprintf(stderr, "Filter compile error: %s\n", pcap_geterr(handle));
return 1;
}
if (pcap_setfilter(handle, &fp) == -1) {
fprintf(stderr, "Filter set error: %s\n", pcap_geterr(handle));
return 1;
}
Step 4: Starting the Capture Loop
Use pcap_loop() to repeatedly capture packets. It accepts the handle, a count (‑1 means infinite), a callback function, and a user pointer. The callback is invoked for every captured packet and receives a pcap_pkthdr struct (containing timestamp and packet length) and a pointer to the raw packet data.
void packet_handler(u_char *user, const struct pcap_pkthdr *pkthdr, const u_char *packet) {
printf("Captured packet of length %d\n", pkthdr->len);
// parsing code goes here
}
pcap_loop(handle, -1, packet_handler, NULL);
pcap_close(handle);
Parsing Captured Packets
The raw packet pointer points to the beginning of the Ethernet frame. Your sniffer must cast memory at appropriate offsets to extract headers. The following code demonstrates parsing Ethernet, IPv4, and TCP headers. Note that the network byte order (big‑endian) must be converted using ntohs() and ntohl().
Parsing the Ethernet Header
struct ethhdr {
unsigned char h_dest[6];
unsigned char h_source[6];
unsigned short h_proto;
} __attribute__((packed));
struct ethhdr *eth = (struct ethhdr *)packet;
printf("Source MAC: %02x:%02x:%02x:%02x:%02x:%02x\n",
eth->h_source[0], eth->h_source[1], eth->h_source[2],
eth->h_source[3], eth->h_source[4], eth->h_source[5]);
if (ntohs(eth->h_proto) == 0x0800) {
// IPv4 packet
}
Parsing the IP Header
struct iphdr {
unsigned char ihl:4, version:4;
unsigned char tos;
unsigned short tot_len;
unsigned short id;
unsigned short frag_off;
unsigned char ttl;
unsigned char protocol;
unsigned short check;
unsigned int saddr;
unsigned int daddr;
} __attribute__((packed));
struct iphdr *ip = (struct iphdr *)(packet + 14); // skip Ethernet header (14 bytes)
printf("Source IP: %s\n", inet_ntoa(*(struct in_addr *)&ip->saddr));
printf("Destination IP: %s\n", inet_ntoa(*(struct in_addr *)&ip->daddr));
Parsing TCP and UDP Headers
The IP header length is ip->ihl * 4. The transport header starts at that offset. For TCP:
struct tcphdr {
unsigned short source;
unsigned short dest;
unsigned int seq;
unsigned int ack_seq;
unsigned short res1:4, doff:4, fin:1, syn:1, rst:1, psh:1, ack:1, urg:1, ece:1, cwr:1;
unsigned short window;
unsigned short check;
unsigned short urg_ptr;
} __attribute__((packed));
int ip_header_len = ip->ihl * 4;
struct tcphdr *tcp = (struct tcphdr *)(packet + 14 + ip_header_len);
printf("Source port: %d\n", ntohs(tcp->source));
printf("Destination port: %d\n", ntohs(tcp->dest));
For UDP, the structure is smaller; you can parse it similarly after verifying the IP protocol field (ip->protocol = 17).
Handling Payload Data
After TCP/UDP headers, the remaining bytes constitute the application payload. You can print it as hex or ASCII, respecting data boundaries. Be cautious with output: payloads may contain binary data that can disrupt terminal output. Always limit the number of bytes displayed.
Advanced Features
A production‑grade packet sniffer often includes capabilities beyond basic capture. The following enhancements can be integrated into your C program.
Packet Filtering with BPF
We already demonstrated pcap_compile() and pcap_setfilter(). You can allow the user to specify a filter string at runtime or via command‑line arguments. Complex filters such as "tcp and dst port 443" or "not arp and not icmp" improve efficiency and readability.
Saving Captures to a PCAP File
libpcap provides pcap_dump() to write captured packets in the standard pcap format. First, open a dump file with pcap_dump_open(). Then, in the packet handler, call pcap_dump() with a dump pointer. This allows your sniffer to produce files that can be opened with Wireshark or tcpdump.
pcap_dumper_t *dumpfile = pcap_dump_open(handle, "capture.pcap");
if (dumpfile == NULL) {
fprintf(stderr, "Error opening dump file: %s\n", pcap_geterr(handle));
return 1;
}
// inside packet_handler:
pcap_dump((u_char *)dumpfile, pkthdr, packet);
Handling Multiple Interfaces
You can spawn separate threads to capture on different interfaces simultaneously. However, managing shared data structures requires careful locking. An alternative is to run multiple instances of the same program, each targeting a different interface, and merge the pcap files later with mergecap.
Performance Considerations
For high‑speed networks (10 Gbps and above), the packet callback itself can become a bottleneck. Consider using pcap_set_buffer_size() to increase the kernel buffer, and reduce per‑packet processing (e.g., offload protocol parsing to a separate thread). In extreme cases, use zero‑copy mechanisms like PF_RING or AF_PACKET with mmap(), but that goes beyond libpcap’s abstraction.
Ethical and Legal Considerations
Packet sniffing has legitimate uses in network troubleshooting, security auditing, and education. However, capturing traffic on networks you do not own or without explicit permission is illegal in many jurisdictions and violates privacy. Always obtain written authorization before deploying a sniffer on a production network. When running the program on your own lab, avoid capturing personal data or sensitive information. Consider using anonymization techniques if you intend to share captured traffic.
Conclusion
Building a network packet sniffer in C with libpcap provides a deep understanding of network protocols and the internals of packet processing. Starting from device discovery and live capture, you can extend the sniffer to parse Ethernet, IP, TCP, and UDP headers, apply BPF filters, and save packets to pcap files for later analysis. The code examples in this article form a solid foundation that you can adapt for more advanced projects such as intrusion detection, traffic monitoring, or protocol fuzzing.
Further reading includes the libpcap programming documentation, Beej’s Guide to Network Programming, and the pcap file format specification. Experiment with your sniffer in a controlled environment, add features like statistics aggregation or real‑time alerting, and always respect network boundaries.