C++ Network Packet Sniffer Project
Capture live traffic and decode Ethernet, IP, and TCP headers by hand.
What you will build
Open a raw socket, capture packets flying past your machine, and decode their headers field by field — protocol, source, destination, ports, flags. It's how Wireshark begins, and it will permanently change how you think about 'the network'. (Run it only on networks you own or have permission to monitor.)
What you will learn
- Raw sockets and packet capture (AF_PACKET / libpcap)
- Ethernet, IPv4, TCP/UDP header layouts
- Network byte order (ntohs/ntohl)
- Filtering by protocol or port
- Hex dumps and header decoding
Feature checklist
- Captures packets on a chosen interface
- Decodes Ethernet + IPv4 headers
- Decodes TCP/UDP ports and TCP flags
- Filter: only DNS / only port 80 / only ICMP
- Live counter per protocol
- Hex dump view of packet bytes
Starter code
// Linux raw-socket sniffer sketch (needs sudo / CAP_NET_RAW)
#include <iostream>
#include <sys/socket.h>
#include <netinet/ip.h>
#include <netinet/tcp.h>
#include <linux/if_packet.h>
#include <arpa/inet.h>
#include <unistd.h>
int main() {
int sock = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
if (sock < 0) { std::perror("socket (run with sudo)"); return 1; }
unsigned char buf[65536];
while (true) {
ssize_t n = recvfrom(sock, buf, sizeof(buf), 0, nullptr, nullptr);
if (n < 14) continue; // too small for Ethernet
unsigned short ethType = (buf[12] << 8) | buf[13];
if (ethType != 0x0800) continue; // IPv4 only
auto* ip = (iphdr*)(buf + 14);
char src[INET_ADDRSTRLEN], dst[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &ip->saddr, src, sizeof(src));
inet_ntop(AF_INET, &ip->daddr, dst, sizeof(dst));
std::cout << "IPv4 " << src << " -> " << dst
<< " proto=" << (int)ip->protocol
<< " len=" << ntohs(ip->tot_len) << "\n";
if (ip->protocol == 6) { // TCP
auto* tcp = (tcphdr*)(buf + 14 + ip->ihl * 4);
std::cout << " TCP " << ntohs(tcp->source)
<< " -> " << ntohs(tcp->dest) << "\n";
}
}
}Full walkthrough
Step 1: Open a raw packet socket
socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)) asks the kernel for a copy of EVERY Ethernet frame on the interface — ETH_P_ALL is the firehose setting. This requires root/CAP_NET_RAW because it genuinely is wiretapping hardware; the program should check for failure and say 'run with sudo' instead of dying cryptically on EPERM.
Step 2: recvfrom() into a buffer, in a loop
Each recvfrom returns one complete frame into a byte buffer, with the sender address telling you the interface and packet type. The loop is the whole main(): receive, decode, print, count, repeat, until a packet budget runs out or Ctrl+C. Everything interesting happens in the decode.
Step 3: Decode the Ethernet header
The first 14 bytes are struct ethhdr (from <linux/if_ether.h> — the starter pulls the pieces from <netinet> headers): 6 bytes destination MAC, 6 source MAC, 2 bytes EtherType. EtherType 0x0800 means the payload is IPv4 — print MACs in hex and branch on the type. Bytes are in NETWORK order; multi-byte fields need ntohs() before they mean anything.
auto* eth = (ethhdr*)buf;
std::cout << "ETH " << macStr(eth->h_source) << " -> "
<< macStr(eth->h_dest)
<< " type 0x" << std::hex << ntohs(eth->h_proto) << std::dec;Step 4: Decode IPv4 on top of it
If EtherType is IPv4, reinterpret the bytes after the Ethernet header as struct iphdr: source/dest addresses (inet_ntoa), total length, TTL, and the protocol field — 6 = TCP, 17 = UDP, 1 = ICMP. The header length is ihl × 4 bytes, NOT a fixed 20 — options exist, and hard-coding 20 misaligns everything downstream on the rare packets that carry them.
Step 5: Decode TCP/UDP ports and TCP flags
The transport header begins at eth + ip-header-length: struct tcphdr gives source/dest ports (ntohs both) and the flag bits — SYN, ACK, FIN, RST — which tell the story of the connection (a lone SYN is a knock; SYN+ACK is the answer). UDP's header is a fixed 8 bytes with ports and length. Print the five-tuple (src:port → dst:port, protocol) — that line IS a packet summary, in every tool from tcpdump down to this one.
Step 6: Filter, count, and hex-dump on demand
A filter option (only DNS = UDP port 53, only port 80, only ICMP) compares against the decoded fields before printing. Counters per protocol accumulate regardless of the filter, so the summary ('TCP 812 · UDP 143 · ICMP 6 · other 21') describes ALL traffic while the display shows the slice you asked for. A hex+ASCII dump of the first N bytes per packet is the final feature — the moment the bytes stop being abstract.
Step 7: Test where you know the traffic
Run it, then generate known packets: ping (ICMP appears, once per second, countable), load one web page over HTTP on a test setup (a SYN burst, then ACKs with growing payload sizes), do one DNS lookup (a single UDP pair to port 53). If your sniffer's story matches the story you deliberately caused, the decoders are aligned — that cause-and-effect test is the only trustworthy one for code that reads the world.
Common mistakes
- Assuming the IP header is always 20 bytes — ihl is a field, read it; misaligned TCP parsing follows otherwise.
- Forgetting ntohs()/ntohl() on ports and lengths, producing port 20480 instead of 80 (byte-swapped) and concluding the decode is random.
- Printing every packet with no filter or counter design, drowning the one packet you care about in a thousand you do not.
Compile and run
g++ -std=c++17 -o sniffer sniffer.cppLinux only, and it must run with raw-socket privileges: sudo ./sniffer (or set CAP_NET_RAW). It uses AF_PACKET sockets, which Windows does not provide (the Windows equivalent is WinPcap/Npcap with a different API). Pick a quiet interface for your first run or the output will be a firehose.
Sample run
$ sudo ./sniffer
Sniffing on eth0 — all protocols (filter: none). Ctrl+C to stop.
ETH 3c:7a:9f:11:22:08 -> a4:83:e7:5d:90:1b type 0x0800 (IPv4)
TCP 192.168.1.5:52344 -> 142.250.74.46:443 [SYN]
TCP 142.250.74.46:443 -> 192.168.1.5:52344 [SYN, ACK]
TCP 192.168.1.5:52344 -> 142.250.74.46:443 [ACK] (612 bytes of payload)
UDP 192.168.1.5:53110 -> 8.8.8.8:53 (DNS query, 41 bytes)
UDP 8.8.8.8:53 -> 192.168.1.5:53110 (DNS answer, 57 bytes)
ICMP 192.168.1.5 -> 192.168.1.1 (echo request)
ICMP 192.168.1.1 -> 192.168.1.5 (echo reply)
^C
Captured 982 packets — TCP 701 · UDP 233 · ICMP 12 · other 36Try it in your browser
No browser demo is included for this project. Build the C++ version below — that is the real program.