> For the complete documentation index, see [llms.txt](https://marswang.gitbook.io/blog/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://marswang.gitbook.io/blog/ebpf/yi-tcpdump-zai-nei-he-zhong-de-gong-zuo-yuan-li.md).

# \[译] 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/>
