# \[译] tcpdump在内核中的工作原理

### 译者序

近期在调研通过 eBPF 做出口网络管控的方案，demo 方案使用了基于 TC 类型 eBPF 程序实现。测试过程中发现 TC egress 处丢掉数据包以后，使用 tcpdump 无法抓到对应的包，比较好奇 tcpdump 工具在内核中的什么位置捕获数据包，于是发现了这篇文章。

原文链接：<https://medium.com/@chnhaoran/tcpdump-deep-dive-how-tcpdump-works-in-the-kernel-b1976ea39f7e>

译文内容如下。

### 背景

在使用 Cilium 时，有些情况下使用 ping 命令发现网络已经通了，但 tcpdump 却无法捕获任何数据包。本文旨在从源代码的角度深入探讨 tcpdump 在内核中的工作机制。

### 内核的网络处理流程

在深入探讨之前，我们需要了解内核是如何处理网络流量的。

<figure><img src="/files/buNH9nWiOAV3cO1xNI7Y" alt=""><figcaption></figcaption></figure>

当物理网卡（NIC）从物理链路接收到数据帧时：

* 利用直接内存访问（DMA）将数据帧写入预先分配的环形缓冲区。
* 触发硬中断，通知 CPU 有网络数据需要接收。
* CPU 收到后，触发软中断。
* 调用网卡驱动程序注册的轮询函数，从环形缓冲区中轮询数据帧。
* 数据帧进入网络子系统，经过 tc、netfilter 和 route 等子系统，处理 L2/L3/L4 协议。
* 数据帧被发送到相应的用户空间注册的套接字。

### 初始化

内核需要完成初始化以支持后续的数据包处理，包括：

* 驱动程序初始化
* 软中断线程初始化
* 网络协议栈处理函数的初始化

本文仅关注网络协议栈处理函数的初始化。

以 IP 协议为例，IPv4 和 IPv6 的初始化过程如下：

```c
// af_inet.c
static int __init inet_init(void)
{
    ...
    dev_add_pack(&ip_packet_type);
}

static struct packet_type ip_packet_type __read_mostly = {
	// Register ETH_P_IP & ip_rcv for IPv4
    .type = cpu_to_be16(ETH_P_IP),
    .func = ip_rcv,
    .list_func = ip_list_rcv,
};


// af_inet6.c
static struct packet_type ipv6_packet_type __read_mostly = {
    // Register ETH_P_IPV6 & ip_rcv for IPv6
    .type = cpu_to_be16(ETH_P_IPV6),
    .func = ipv6_rcv,
    .list_func = ipv6_list_rcv,
};

static int __init ipv6_packet_init(void)
{
    dev_add_pack(&ipv6_packet_type);
    return 0;
}
```

在函数 `dev_add_pack` 中，协议和相应的handler 函数被添加到 ptype\_all（ETH\_P\_ALL）或 ptype\_base（对应其他协议）。

```c
// dev.c
void dev_add_pack(struct packet_type *pt)
{
   struct list_head *head = ptype_head(pt);
  
   spin_lock(&ptype_lock);
   list_add_rcu(&pt->list, head);
   spin_unlock(&ptype_lock);
}

static inline struct list_head *ptype_head(const struct packet_type *pt)
{
   if (pt->type == htons(ETH_P_ALL))
      return pt->dev ? &pt->dev->ptype_all : &ptype_all;
   else
      return pt->dev ? &pt->dev->ptype_specific :
       &ptype_base[ntohs(pt->type) & PTYPE_HASH_MASK];
}

// if_ether.h
#define ETH_P_ALL 0x0003  /* Every packet (be careful!!!) */
```

### 接收端处理

在接收端，`__netif_receive_skb_core` 首先遍历 ptype\_all sniffers 确认是否注册了 ETH\_P\_ALL 类型的协议，如果有则将 skb（套接字缓冲区）传递给对应的 handler。然后根据数据包类型（例如，ETH\_P\_IP），将 skb 传递给注册到 ptype\_base 的 handler。

```
//dev.c
static int __netif_receive_skb_core(struct sk_buff **pskb, bool pfmemalloc,
        struct packet_type **ppt_prev)
{
    ...
    // XDP processing
    // ret2 = do_xdp_generic(rcu_dereference(skb->dev->xdp_prog), skb);
    // deliver skb to ptype_all
    list_for_each_entry_rcu(ptype, &ptype_all, list) {
        if (pt_prev)
            ret = deliver_skb(skb, pt_prev, orig_dev);
        pt_prev = ptype;
    }

    list_for_each_entry_rcu(ptype, &skb->dev->ptype_all, list) {
        if (pt_prev)
            ret = deliver_skb(skb, pt_prev, orig_dev);
        pt_prev = ptype;
    }
    // tc handling
    // skb = sch_handle_ingress(skb, &pt_prev, &ret, orig_dev, &another);
    ...
    // deliver skb to ptype_base
    if (likely(!deliver_exact)) {
        deliver_ptype_list_skb(skb, &pt_prev, orig_dev, type, &ptype_base[ntohs(type) & PTYPE_HASH_MASK]);
    }
}    
```

查看 [libpcap](https://github.com/the-tcpdump-group/libpcap) 源代码，我们发现它使用 `ETH_P_ALL` 作为协议类型。

```
//libpcap-linux.c
static int pcap_protocol(pcap_t *handle)
{
    int protocol;

    protocol = handle->opt.protocol;
    if (protocol == 0)
    protocol = ETH_P_ALL;
    return htons(protocol);
}

static int iface_bind(int fd, int ifindex, char *ebuf, int protocol)
{
    ...
    sll.sll_family  = AF_PACKET;
    sll.sll_ifindex = ifindex < 0 ? 0 : ifindex;
    sll.sll_protocol    = protocol;
    ...
}
```

在 Linux 内核中，注册套接字时需要提供协议和对应的 handler，对应的 skb 会发送给该协议的所有套接字。这就是为什么 tcpdump 能够从网卡捕获数据包的原因。

需要注意的是，XDP hook 点位于 ETH\_P\_ALL 处理之前。这就是为什么在某些情况下，我们在 Cilium 中使用 tcpdump 无法捕获数据包，因为数据在最开始的入口点就被重定向了。

```
// af_packet.c
static void __register_prot_hook(struct sock *sk)
{
    struct packet_sock *po = pkt_sk(sk);
  
    if (!po->running) {
        if (po->fanout)
            __fanout_link(sk, po);
    else
        dev_add_pack(&po->prot_hook);
        sock_hold(sk);
        po->running = 1;
   }
}

static void register_prot_hook(struct sock *sk)
{
    lockdep_assert_held_once(&pkt_sk(sk)->bind_lock);
    __register_prot_hook(sk);
}
```

### 发送端处理

网络协议栈处理之后，最后一步是 `__dev_queue_xmit`。`dev_queue_xmit_nit` 遍历每个与 ETH\_P\_ALL 相关的套接字，并将数据包传递给相应的 handler 函数。

```
// dev.c
static int __dev_queue_xmit(struct sk_buff *skb, struct net_device *sb_dev)
{
    ...
    // tc processing
    //skb = sch_handle_egress(skb, &rc, dev);
    skb = dev_hard_start_xmit(skb, dev, txq, &rc);
}
dev_hard_start_xmit
   | dev_queue_xmit_nit
      | xmit_one
         | dev_queue_xmit_nit
void dev_queue_xmit_nit(struct sk_buff *skb, struct net_device *dev)
{
    list_for_each_entry_rcu(ptype, ptype_list, list) {
        ...
        if (pt_prev) {
            deliver_skb(skb2, pt_prev, skb->dev);
            pt_prev = ptype;
            continue;
        }
    }
}
```

### 结论

我们深入研究了源代码，可以更好地理解 tcpdump 的工作原理。简要总结如下：

* 收包路径: XDP -> `Tcpdump` -> TC -> network stack
* 发包路径: network stack -> TC -> `Tcpdump`

### 参考资料

<https://chnhaoran.github.io/blog/how-is-tcpdump-working/>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://marswang.gitbook.io/blog/ebpf/yi-tcpdump-zai-nei-he-zhong-de-gong-zuo-yuan-li.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
