Building a Packet Sniffer from Scratch with libpcap
When we think about network monitoring tools like Wireshark or tcpdump, it's easy to forget that at their core, these tools are simply capturing raw packets moving across the network and decoding them. To better understand how data flows through computer networks, I built a basic packet sniffer in C using the libpcap library.
This post covers the foundational concepts of computer networks relevant to packet sniffing, a breakdown of the key network layers we parse (Ethernet, IP, TCP/UDP), and a walkthrough of my C program that captures and analyzes packets in real-time.
Understanding the Basics of Computer Networks
Before diving into the code, it's important to understand what a "packet" is. In computer networks, all communication happens in chunks of data called packets. A packet typically passes through multiple layers of the TCP/IP model:
- Link Layer (Ethernet) – Handles physical addressing (MAC addresses).
- Network Layer (IP) – Handles logical addressing and routing.
- Transport Layer (TCP/UDP) – Manages ports and ensures reliable or fast delivery.
- Application Layer (HTTP, DNS, etc.) – The human-facing protocols on top of TCP/UDP.
When a packet travels across a network interface, it carries a stack of headers:
- Ethernet header: Contains source and destination MAC addresses.
- IP header: Contains source and destination IP addresses, protocol type, TTL, etc.
- Transport header (TCP or UDP): Contains source/destination ports and flags.
- Payload: The actual data being transmitted.
A packet sniffer listens to a network interface, captures these packets, and allows us to inspect these headers.
Using libpcap for Packet Capture
libpcap is a widely used packet capture library that powers tools like tcpdump and Wireshark. It allows applications to:
- Select a network device.
- Put it in promiscuous mode (capture all packets, not just those addressed to us).
- Filter packets (e.g., only TCP or a specific port).
- Access raw packet bytes for analysis.
The workflow for our sniffer is:
- Select a network device: Either specified by the user or auto-detected.
-
Open the device: Using
pcap_create()
and configure capture options. - Apply a filter: Using Berkeley Packet Filters (BPF) to narrow down traffic.
-
Capture packets: Using
pcap_loop()
and a callback function. - Parse each packet: Decoding Ethernet, IP, TCP, and UDP headers.
Parsing Packets
Each packet starts with an Ethernet header (14 bytes), followed by an IP header (usually 20 bytes, but can vary), and optionally a TCP or UDP header. We read these headers by casting portions of the raw packet data into C structs defined for each layer.
Ethernet Header Structure
struct sniff_ethernet {
u_char ether_dhost[6]; // Destination MAC
u_char ether_shost[6]; // Source MAC
u_short ether_type; // Type (e.g., 0x0800 = IPv4)
};
The IP header provides information about the packet version, total length, source and destination IP addresses, and the protocol (6 = TCP, 17 = UDP).
Finally, we look deeper into transport headers (TCP or UDP) to extract port numbers and other details like flags in TCP.
The Packet Sniffer Implementation
The core idea of the program follows this structure:
Device selection: We either specify the network device as a command-line argument or list available devices for the user to choose.
Packet capturing:
pcap_loop(handle, 3, got_packet, NULL);
This listens for 3 packets and calls got_packet()
for each one.
Parsing: Inside got_packet()
, we:
- Extract Ethernet, IP, and TCP/UDP headers.
- Print MAC addresses, IP addresses, protocol type, and port numbers.
- Dump raw packet data in hex for inspection.
Sample Output
Packet #1: length=98
Source MAC: 90:09:d0:07:fe:44 -> a6:1e:a0:fb:63:33
Source IP: 192.168.0.97 -> Destination IP: 104.18.94.41
Protocol: TCP (6), TTL: 64
TCP Source Port: 55427 -> Destination Port: 443
This mimics a simplified tcpdump output but gives us full control over parsing and analyzing each header.
What I Learned
- Promiscuous mode lets you see all traffic on your network, not just traffic to your machine.
- Packet structure matters: Offsets (Ethernet 14 bytes, IP header length variable) are key to correct parsing.
- Filters are powerful: Using BPF, we can capture only the traffic we're interested in (like TCP port 80 for HTTP).
- Low-level networking is hands-on: Unlike using curl or high-level APIs, working directly with packets teaches you what's really happening under the hood.
Next Steps
The current sniffer works, but there's plenty of room for improvement:
- Implementing reassembly of TCP streams to see complete messages.
- Parsing higher-level protocols like HTTP or DNS.
- Adding a real-time GUI for visualization.
- Supporting IPv6 packets.
Takeaway
Building a packet sniffer from scratch is a great way to demystify networking. It bridges the gap between abstract network theory and real data flowing through your machine. By directly capturing and decoding packets, you gain a much deeper appreciation of how computers communicate.
This project reinforced my understanding of network protocols and gave me hands-on experience with low-level system programming. The ability to see raw network traffic in real-time provides invaluable insight into how modern network applications actually work under the hood.
Thank you for reading! If you're interested in the code, you can find it on my GitHub.
Posted by: Aidan Vidal
Posted on: August 5, 2025