How Does tcpdump Capture Packets Inside the Linux Kernel?
This article explains the low‑level mechanisms by which tcpdump intercepts network packets in the Linux kernel, covering both the receive and transmit paths, the interaction with netfilter, and the steps required to implement a custom packet‑sniffing program.
Packet Reception Path
The kernel receives a packet in __netif_receive_skb_core(), which iterates over the global list ptype_all of registered protocol handlers. tcpdump registers a virtual protocol on this list, so the loop reaches tcpdump’s callback via deliver_skb() and eventually calls packet_rcv() in net/packet/af_packet.c. This function queues the skb into the packet socket’s receive queue for user‑space retrieval.
// file: net/core/dev.c
static int __netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc)
{
// ...
// iterate ptype_all (tcpdump registers a virtual protocol here)
list_for_each_entry_rcu(ptype, &ptype_all, list) {
if (!ptype->dev || ptype->dev == skb->dev) {
if (pt_prev)
ret = deliver_skb(skb, pt_prev, orig_dev);
pt_prev = ptype;
}
}
} // file: net/packet/af_packet.c
static int packet_rcv(struct sk_buff *skb, ...)
{
__skb_queue_tail(&sk->sk_receive_queue, skb);
// ...
}Because tcpdump hooks into the device layer before netfilter processing, packets filtered by netfilter are still visible to tcpdump during reception.
Packet Transmission Path
When sending, the packet first passes the IP layer where netfilter may drop it (e.g., NF_INET_LOCAL_OUT). If the packet survives, it reaches the device layer and is processed by dev_hard_start_xmit(), which again walks ptype_all and invokes tcpdump’s callback via dev_queue_xmit_nit().
// file: net/ipv4/ip_output.c
int ip_local_out(struct sk_buff *skb)
{
// execute netfilter filtering
err = __ip_local_out(skb);
}
int __ip_local_out(struct sk_buff *skb)
{
// ...
return nf_hook(NFPROTO_IPV4, NF_INET_LOCAL_OUT, skb, NULL,
skb_dst(skb)->dev, dst_output);
} // file: net/core/dev.c
int dev_hard_start_xmit(struct sk_buff *skb, struct net_device *dev,
struct netdev_queue *txq)
{
// ...
if (!list_empty(&ptype_all))
dev_queue_xmit_nit(skb, dev);
}
static void dev_queue_xmit_nit(struct sk_buff *skb, struct net_device *dev)
{
list_for_each_entry_rcu(ptype, &ptype_all, list) {
if ((ptype->dev == dev || !ptype->dev) &&
(!skb_loop_sk(ptype, skb))) {
if (pt_prev) {
deliver_skb(skb2, pt_prev, skb->dev);
pt_prev = ptype;
continue;
}
// ...
}
}
}If netfilter drops the packet at the IP layer, the later device‑layer hook never runs, so tcpdump cannot capture the packet on the transmit side.
How tcpdump Starts
Running strace tcpdump -i eth0 shows a call to socket(AF_PACKET, SOCK_RAW, 768). The third argument 768 is ETH_P_ALL, meaning the socket receives all Ethernet protocols.
# strace tcpdump -i eth0
socket(AF_PACKET, SOCK_RAW, 768)
...The kernel creates the socket via sock_create(), which looks up the protocol family in net_families and calls the family’s create method. For AF_PACKET, this is packet_create() in packet/af_packet.c. The function registers a protocol hook that points to packet_rcv and adds it to ptype_all via dev_add_pack().
// file: net/socket.c
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
// ...
retval = sock_create(family, type, protocol, &sock);
}
int __sock_create(struct net *net, int family, int type, ...)
{
// ...
pf = rcu_dereference(net_families[family]);
err = pf->create(net, sock, protocol, kern);
} // file: packet/af_packet.c
static int packet_create(struct net *net, struct socket *sock,
int protocol, int kern)
{
// ...
po = pkt_sk(sk);
po->prot_hook.func = packet_rcv;
if (proto) {
po->prot_hook.type = proto;
register_prot_hook(sk);
}
}
static void register_prot_hook(struct sock *sk)
{
struct packet_sock *po = pkt_sk(sk);
dev_add_pack(&po->prot_hook);
} // file: net/core/dev.c
void dev_add_pack(struct packet_type *pt)
{
struct list_head *head = ptype_head(pt);
list_add_rcu(&pt->list, head);
}
static inline struct list_head *ptype_head(const struct packet_type *pt)
{
if (pt->type == htons(ETH_P_ALL))
return &ptype_all;
else
return &ptype_base[ntohs(pt->type) & PTYPE_HASH_MASK];
}Protocol families (PF_*) and address families (AF_*) share the same numeric values; they are often used interchangeably.
Practical Takeaways
How tcpdump works : It creates a raw packet socket ( AF_PACKET, ETH_P_ALL), registers a virtual protocol in ptype_all, and receives packets via the kernel’s packet‑socket receive queue.
Can tcpdump see netfilter‑filtered packets? During reception, yes – tcpdump captures before netfilter runs. During transmission, no – netfilter may drop the packet before the device‑layer hook.
Writing a custom sniffer : Use a packet socket, register a protocol hook, and read from the socket’s receive queue. A minimal C demo is available at https://github.com/yanfeizhang/coder-kung-fu/blob/main/tests/network/test04/main.c. Compile with gcc -o main main.c and run as root.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Liangxu Linux
Liangxu, a self‑taught IT professional now working as a Linux development engineer at a Fortune 500 multinational, shares extensive Linux knowledge—fundamentals, applications, tools, plus Git, databases, Raspberry Pi, etc. (Reply “Linux” to receive essential resources.)
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
