#635 转载:用户态 tcpdump 如何实现抓到内核网络包的?

2021-09-08

在网络包的发送和接收过程中,绝大部分的工作都是在内核态完成的。那么问题来了,我们常用的运行在用户态的程序 tcpdump 是那如何实现抓到内核态的包的呢?有的同学知道 tcpdump 是基于 libpcap 的,那么 libpcap 的工作原理又是啥样的呢。如果让你裸写一个抓包程序,你有没有思路?

按照飞哥的风格,不搞到最底层的原理咱是不会罢休的。所以我对相关的源码进行了深入分析。通过本文,你将彻底搞清楚了以下这几个问题。

  • tcpdump 是如何工作的?
  • netfilter 过滤的包 tcpdump 是否可以抓的到?
  • 让你自己写一个抓包程序的话该如何下手?

借助这几个问题,我们来展开今天的探索之旅!

一、网络包接收过程

图解 Linux 网络包接收过程一文中我们详细介绍了网络包是如何从网卡到达用户进程中的。这个过程我们可以简单用如下这个图来表示。

找到 tcpdump 抓包点

我们在网络设备层的代码里找到了 tcpdump 的抓包入口。在 __netif_receive_skb_core 这个函数里会遍历 ptype_all 上的协议。还记得上文中我们提到 tcpdump 在 ptype_all 上注册了虚拟协议。这时就能执行的到了。来看函数:

//file: net/core/dev.c
static int __netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc)
{
    ......
    //遍历 ptype_all (tcpdump 在这里挂了虚拟协议)
    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;
        }
    }
}

在上面函数中遍历 ptype_all,并使用 deliver_skb 来调用协议中的回调函数。

//file: net/core/dev.c
static inline int deliver_skb(...)
{
 return pt_prev->func(skb, skb->dev, pt_prev, orig_dev);
}

对于 tcpdump 来说,就会进入 packet_rcv 了(后面我们再说为啥是进入这个函数)。这个函数在 net/packet/af_packet.c 文件中。

//file: net/packet/af_packet.c
static int packet_rcv(struct sk_buff *skb, ...)
{
 __skb_queue_tail(&sk->sk_receive_queue, skb);
 ......
}

可见 packet_rcv 把收到的 skb 放到了当前 packet socket 的接收队列里了。这样后面调用 recvfrom 的时候就可以获取到所抓到的包!!

再找 netfilter 过滤点

为了解释我们开篇中提到的问题,这里我们再稍微到协议层中多看一些。在 ip_rcv 中我们找到了一个 netfilter 相关的执行逻辑。

//file: net/ipv4/ip_input.c
int ip_rcv(...)
{
 ......
 return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, dev, NULL,
         ip_rcv_finish);
}

如果你用 NF_HOOK 作为关键词来搜索,还能搜到不少 netfilter 的过滤点。不过所有的过滤点都是位于 IP 协议层的。

在接收包的过程中,数据包是先经过网络设备层然后才到协议层的。

那么我们开篇中的一个问题就有了答案了。假如我们设置了 netfilter 规则,在接收包的过程中,工作在网络设备层的 tcpdump 先开始工作。还没等 netfilter 过滤,tcpdump 就抓到包了!

所以,在接收包的过程中,netfilter 过滤并不会影响 tcpdump 的抓包!

二、网络包发送过程

我们接着再来看网络包发送过程。在25 张图,一万字,拆解 Linux 网络包发送过程一文中,我们详细描述过网络包的发送过程。发送过程可以汇总成简单的一张图。

找到 netfilter 过滤点

在发送的过程中,同样是在 IP 层进入各种 netfilter 规则的过滤。

//file: net/ipv4/ip_output.c
int ip_local_out(struct sk_buff *skb)
{
    //执行 netfilter 过滤
    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);
}

在这个文件中,还能看到若干处 netfilter 过滤逻辑。

找到 tcpdump 抓包点

发送过程在协议层处理完毕到达网络设备层的时候,也有 tcpdump 的抓包点。

//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;
            }
            ......
        }
    }
}

在上述代码中我们看到,在 dev_queue_xmit_nit 中遍历 ptype_all 中的协议,并依次调用 deliver_skb。这就会执行到 tcpdump 挂在上面的虚拟协议。

在网络包的发送过程中,和接收过程恰好相反,是协议层先处理、网络设备层后处理。

如果 netfilter 设置了过滤规则,那么在协议层就直接过滤掉了。在下层网络设备层工作的 tcpdump 将无法再捕获到该网络包

三、TCPDUMP 启动

前面两小节我们说到了内核收发包都通过遍历 ptype_all 来执行抓包的。那么我们现在来看看用户态的 tcpdump 是如何挂载协议到内 ptype_all 上的。

我们通过 strace 命令我们抓一下 tcpdump 命令的系统调用,显示结果中有一行 socket 系统调用。Tcpdump 秘密的源头就藏在这行对 socket 函数的调用里。

# strace tcpdump -i eth0
socket(AF_PACKET, SOCK_RAW, 768)
......

socket 系统调用的第一个参数表示创建的 socket 所属的地址簇或者协议簇,取值以 AF 或者 PF 开头。在 Linux 里,支持很多种协议族,在 include/linux/socket.h 中可以找到所有的定义。这里创建的是 packet 类型的 socket。

协议族和地址族:每一种协议族都有其对应的地址族。比如 IPV4 的协议族定义叫 PF_INET,其地址族的定义是 AF_INET。它们是一一对应的,而且值也完全一样,所以经常混用。

//file: include/linux/socket.h
#define AF_UNSPEC 0
#define AF_UNIX  1 /* Unix domain sockets   */
#define AF_LOCAL 1 /* POSIX name for AF_UNIX */
#define AF_INET  2 /* Internet IP Protocol  */
#define AF_INET6 10 /* IP version 6   */
#define AF_PACKET 17 /* Packet family  */
......

另外上面第三个参数 768 代表的是 ETH_P_ALL,socket.htons(ETH_P_ALL) = 768。

我们来展开看这个 packet 类型的 socket 创建的过程中都干了啥,找到 socket 创建源码。

//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);
}

在 __sock_create 中,从 net_families 中获取了指定协议。并调用了它的 create 方法来完成创建。

net_families 是一个数组,除了我们常用的 PF_INET( ipv4 ) 外,还支持很多种协议族。比如 PF_UNIX、PF_INET6(ipv6)、PF_PACKET 等等。每一种协议族在 net_families 数组的特定位置都可以找到其 family 类型。在这个 family 类型里,成员函数 create 指向该协议族的对应创建函数。

根据上图,我们看到对于 packet 类型的 socket,pf->create 实际调用到的是 packet_create 函数。我们进入到这个函数中来一探究竟,这是理解 tcpdump 工作原理的关键!

//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);
}

在 packet_create 中设置回调函数为 packet_rcv,再通过 register_prot_hook => dev_add_pack 完成注册。注册完后,是在全局协议 ptype_all 链表中添加了一个虚拟的协议进来。

我们再来看下 dev_add_pack 是如何注册协议到 ptype_all 中的。回顾我们开头看到的 socket 函数调用,第三个参数 proto 传入的是 ETH_P_ALL。那 dev_add_pack 其实最后是把 hook 函数添加到了 ptype_all 里了,代码如下。

//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];
}

我们整篇文章都以 ETH_P_ALL 为例,但其实有的时候也会有其它情况。在别的情况下可能会注册协议到 ptype_base 里了,而不是 ptype_all。同样, ptype_base 中的协议也会在发送和接收的过程中被执行到。

总结:tcpdump 启动的时候内部逻辑其实很简单,就是在 ptype_all 中注册了一个虚拟协议而已。

四、总结

现在我们再回头看开篇提到的几个问题。

1. tcpdump 是如何工作的

用户态 tcpdump 命令是通过 socket 系统调用,在内核源码中用到的 ptype_all 中挂载了函数钩子上去。无论是在网络包接收过程中,还是在发送过程中,都会在网络设备层遍历 ptype_all 中的协议,并执行其中的回调。tcpdump 命令就是基于这个底层原理来工作的。

2. netfilter 过滤的包 tcpdump 是否可以抓的到

关于这个问题,得分接收和发送过程分别来看。在网络包接收的过程中,由于 tcpdump 近水楼台先得月,所以完全可以捕获到命中 netfilter 过滤规则的包。

但是在发送的过程中,恰恰相反。网络包先经过协议层,这时候被 netfilter 过滤掉的话,底层工作的 tcpdump 还没等看见就啥也没了。

3. 让你自己写一个抓包程序的话该如何下手

如果你想自己写一段类似 tcpdump 的抓包程序的话,使用 packet socket 就可以了。我用 c 写了一段抓包,并且解析源 IP 和目的 IP 的简单 demo。

源码地址:https://github.com/yanfeizhang/coder-kung-fu/blob/main/tests/network/test04/main.c

编译一下,注意运行需要 root 权限。

# gcc -o main main.c
# ./main 

运行结果预览如下。

#634 TOML

2021-09-07

我之前说过,YAML 太过复杂,复杂到根本不适合用来作为配置文件。应该是有很多人和我一样想,然后就设计了 TOML 语言。

TOML 就是 Tom's Obvious Minimal Language 的缩写,翻译过来就是汤姆的最精简语言(作者全名 Tom Preston-Werner)。
然后,Slogan 就是 A config file format for humans 也就是:以人为本的配置文件格式。
它的定位已经很明确了吧!

PS: 据说这个语言来自 GitHub(作者的另一个身份是 GitHub 联合创始人)。

其语法说明就只有一个不长的单页面:https://toml.io/cn/v1.0.0
其语法 ABNF 文件,算上注释和空行,一共 243 行。

#633 阮一峰的 C 语言教程学习

2021-09-07
  1. https://wangdoc.com/clang/
  2. GitHub, https://github.com/wangdoc/clang-tutorial

目录

  • 1. 简介
    历史,标准,Hello World
  • 2. 基本语法
    基本语法,printf 格式化
  • 3. 变量
  • 4. 运算符
  • 5. 流程控制
  • 6. 数据类型
  • 整型的不同进制表示
  • limits.h 中的极限值
  • 关于布尔型,int, _Bool(C99, int 别名), bool/true/false (stdbool.h)
  • 字面量后缀 (l, ll, u, f)
  • 类型转换
  • 可移植类型 (stdint.h)
  • 7. 指针
  • 8. 函数
  • 9. 数组
  • 10. 字符串
  • 11. 内存管理
  • 12. struct 结构
  • 13. typedef 命令
  • 14. Union 结构
  • 15. Enum 结构
  • 16. 预处理器
  • 17. I/O 函数
  • 18. 文件操作
  • 19. 变量说明符
  • 20. 多文件项目
  • 21. 命令行环境
  • 22. 多字节字符
  • 23. 标准库
  • 23.1. assert.h
  • 23.2. ctype.h
  • 23.3. errno.h
  • 23.4. float.h
  • 23.5. inttypes.h
  • 23.6. iso646.h
  • 23.7. limits.h
  • 23.8. locale.h
  • 23.9. math.h
  • 23.10. signal.h
  • 23.11. stdint.h
  • 23.12. stdlib.h
  • 23.13. stdio.h
  • 23.14. string.h
  • 23.15. time.h
  • 23.16. wchar.h
  • 23.17. wctype.h

关键字

// 数据类型 12
short   int         long    double  float
char    struct      union   enum    typedef
signed  unsigned

// 变量 6
auto    register    extern  const   volatile
static

// 函数 2
void    return

// 控制语句 11
if      else    for     continue    while  do
switch  case    default goto        break

// 其他 1
sizeof

数据类型

  • 整数:short, int, long, long long
  • signed, unsigned
  • 浮点数:float, double
  • 字符:char, char *
#include <stdio.h>

int main(){
    printf("size of int:         %2ld\n", sizeof(int));
    printf("size of short:       %2ld\n", sizeof(short));
    printf("size of long:        %2ld\n", sizeof(long));
    printf("size of long long:   %2ld\n", sizeof(long long));
    printf("size of float:       %2ld\n", sizeof(float));
    printf("size of double:      %2ld\n", sizeof(double));
    printf("size of long double: %2ld\n", sizeof(long double));
    printf("size of char:        %2ld\n", sizeof(char));
}
$ uname -a
Linux dell 5.11.0-34-generic #36-Ubuntu SMP Thu Aug 26 19:22:09 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

$ gcc --version
gcc (Ubuntu 10.3.0-1ubuntu1) 10.3.0
Copyright (C) 2020 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ gcc /tmp/test.c -o /tmp/test && /tmp/test
size of int:          4
size of short:        2
size of long:         8
size of long long:    8
size of float:        4
size of double:       8
size of long double: 16
size of char:         1

#632 转载:Diss Golang

2021-09-06

作者可能比较喜欢 C# (C# 的特性你让我丢掉哪一个我都觉得少块肉), 对 Golang 进行了一些批评,认为其设计缺乏远见,存在很多缺陷:

Anders Hejlsberg 和 Microsoft 把最佳设计都端到眼前了,其他语言纷纷各取所需,但是 Golang 的设计者却不为所动。

#630 Python 源码学习 08: dict

2021-09-02

类型定义

INIT_TYPE(&PyDict_Type, "dict");
SETBUILTIN("dict", &PyDict_Type);

PyTypeObject PyDict_Type 的定义在 Objects/dictobject.c 中。

Include/cpython/dictobject.h

typedef struct {
    PyObject_HEAD

    /* Number of items in the dictionary */
    Py_ssize_t ma_used;

    /* Dictionary version: globally unique, value change each time
       the dictionary is modified */
    uint64_t ma_version_tag;

    PyDictKeysObject *ma_keys;

    /* If ma_values is NULL, the table is "combined": keys and values
       are stored in ma_keys.

       If ma_values is not NULL, the table is splitted:
       keys are stored in ma_keys and values are stored in ma_values */
    PyObject **ma_values;
} PyDictObject;

成员方法

setdefault

#define DICT_SETDEFAULT_METHODDEF    \
    {"setdefault", (PyCFunction)(void(*)(void))dict_setdefault, METH_FASTCALL, dict_setdefault__doc__},

static PyObject *
dict_setdefault(PyDictObject *self, PyObject *const *args, Py_ssize_t nargs)
{
    PyObject *return_value = NULL;
    PyObject *key;
    PyObject *default_value = Py_None;

    if (!_PyArg_CheckPositional("setdefault", nargs, 1, 2)) {
        goto exit;
    }
    key = args[0];
    if (nargs < 2) {
        goto skip_optional;
    }
    default_value = args[1];
skip_optional:
    return_value = dict_setdefault_impl(self, key, default_value);

exit:
    return return_value;
}

static PyObject *
dict_setdefault_impl(PyDictObject *self, PyObject *key,
                     PyObject *default_value)
/*[clinic end generated code: output=f8c1101ebf69e220 input=0f063756e815fd9d]*/
{
    PyObject *val;

    val = PyDict_SetDefault((PyObject *)self, key, default_value);
    Py_XINCREF(val);
    return val;
}

#629 UMIDIGI 与 GPL 协议

2021-09-01

最近发生的一件事情挺有意思的,在得到 “上门自取” 的回复之后,有位美女替外国程序员依照 GPL 协议上门向国内的一家厂商索要内核代码。

深圳公司 UMIDIGI 生产安卓手机(联发科 Mediatek),面向海外用户。

波兰开发者 Patrycja (@ptrcnull) 想将移植 postmarketOS (基于 Alphine Linux) 到 UMIDIGI 的一款设备上,结果发现缺少 ft8719_dsi_fhdplus 屏幕驱动。

Patrycja 尝试联系 UMIDIGI,得到了以下回复:

Twitter

就是说,如果你要的话,你可以上门来取。我们可以认为这是想让 Patrycja 知难而退。

可是 Patrycja 八月 17 号在 Twitter 上抱怨之后,深圳科技博主 Naomi Wu 机械妖姬(@RealSexyCyborg,混 Youtube)主动提供帮忙,表示愿意代替 Patrycja 上门索取源代码。

然后机械妖姬 08/20 就拿着自拍杆勇闯 UMIDIGI 公司。
UMIDIGI 行政人员表示需要之前发邮件的 BEN 已经离职(我觉得很可能就坐在旁边看美女),然后提供源代码的事情需要先向老板请示,之后在相对友好的氛围下,机械妖姬离开了。

机械妖姬前往 UMIDIGI 公司

随后:

  • 08/25 UMIDIGI 向机械妖姬提供了相关文件。
  • 08/26 Patrcja 完成了系统移植,并向机械妖姬和 UMIDIGI 表示感谢。

这件事件我的评价是:

UMIDIGI 之前的邮件回复十分愚蠢,直接向法务部门咨询之后,通过合法的途径(可能涉及联发科的知识产权)将代码提供给他不就好了吗?
不过,如果不是事情闹大了,UMIDIGI 会不会提供源代码呢?这就不知道了,我们也不能以恶意揣度之。只能就事论事,在这次事件中,各方的表现都非常好。

#628 美国之声谈美国对阿富汗难民的帮助

2021-09-01

美国之声中文网 @VOAChinese

百万阿富汗难民主要流向巴基斯坦、伊朗,世界多国纷纷提供人道庇护,但并不包括中国。美国尚未宣布接受难民的确切数字,总统拜登批准了5亿美元应急资金,而美国北方司令部正为多达 5万阿富汗撤离人员提供交通、医疗、临时住房和其他支持,协助他们安顿到永久或临时设施中,多个州也参与到安置工作中。

瞧瞧这个 “并不包括中国”,cue 得多自然,值得我们的新闻工作者学习。

#627 vCard (.vcf)

2021-09-01

VCF 是 Virtual Contact File 的简写,虚拟通讯录文件
PS:.vcf 后缀还有一个含义是 Variant Call Format,用在基因生物学方面。

#626 SCO 与 IBM 之间的和解

2021-08-31

开源中国上看到一篇资讯:《18 年后,起诉 IBM 盗用 System V 源码的 SCO 选择以 1425 万美元和解》, 提到 SCO 将要和 IBM 和解,只需要 IBM 向 SCO 托管人(SCO 早破产了,只剩这点官司一直拖着没有结束,根据美国破产法,由托管人代理)支付 1425 万美元。虽然他们继续坚持自己的立场,但是最后都认为这一次的和解协议是符合双方最大利益的,实在是心累,不愿再继续到下一个十年。
这一次总算把这片狗皮膏药彻底给撕掉了,SCO 也可以安心的死去了,Linux 基金会也可以摆酒庆祝一下了。

开源界两件最大的纠纷就是 AT&T Unix 和和这次的 SCO Linux,并且这两个纠纷有很大的渊源。

前者源自 AT&T 管理层所做的一个愚蠢决定,为了一个自己根本不在乎的市场,向一个庞大用户群简单粗暴地发起一场战争。折腾十年(从 1984 年 AT&T 分拆之后向 BSD 提出版权要求,到 1994 年收购 AT&T Unix 资产的 Novell 与 BSD 和解)也没有获得任何好处,BSD 最后也只是做了一点简单的修改就算了事,而发起这场战争的 AT&T 却在官司结束之前,提前抽身去做别的事情去了。但这却直接导致了 Unix 市场长达十年的沉寂,以及 Linux 的诞生,或许 MS-DOS 以及后来 Windows 的诞生都与此有关(我相信如果有合法的、免费的 BSD 可用,IBM 不会选择 MS-DOS,微软可能也会作为一家 Unix 厂商继续经营自己的 Xenix)。

后者也是悬挂在 Linux 厂商头上的一柄达摩克利斯之剑。
SCO 发起诉讼的理由是,SCO 拥有 Unix 版权(可能是部分),IBM 向 Linux 贡献了大量代码,其中有一部分侵犯了 SCO 的版权(IBM 曾经和 SCO 联合开发 X86 架构的 Unix),从而 SCO 拥有 Linux 的部分版权。
不过时代变了,Linux 厂商与 SCO 的体量对比,相较于 AT&T 那会儿完全是反过来的。

SCO 先后多次发起对 Unix/Linux 厂商的诉讼,一旦获胜,就可以在巨大的 Linux 市场撕下来一块巨大的蛋糕。

  1. 最初的 SCO 是一家美国软件公司,因其 Unix 业务而知名。
  2. SCO 一开始是专注于 Unix 的 X86 架构移植
  3. Xenix 是微软开发的 Unix 系统,后来以技术入股的方式,将 Xenix 转让给了合作伙伴 SCO 公司(25% 股份)
  4. SCO 还收购了 UnixWare
  5. SCO 整合手上的 Unix 资源,开发了 SCO Unix,后来又改名为 SCO OpenServer(现在还活着,由 Xinuos 所有,改成基于 FreeBSD)。
  6. SCO 将 Unix 业务(可能包括 SCO 这个名字)出售给了 Caldera Systems,然后就改名了。
  7. 后来,Caldera 更名为 The SCO Group,破产之后又更名为 The TSG Group (后面再提到 SCO 都是指这家公司)。
  8. SCO 的主要业务就是像各大 Linux 厂商发起诉讼
    PS: 一度穷得差点没钱发起诉讼,微软立即宣布花 1300 万美元购买 SCO 许可

SCO 对很多大公司发起了诉讼,包括:

  1. SCO 起诉 IBM:IBM 向 Linux 贡献的代码是否侵权?
  2. Red Hat 起诉 SCO:红帽要求法院禁止 SCO 发起对 Linux 版权的诉讼,被驳回。
  3. SCO 起诉 Novell:Novell 曾经将 Unix 资产打包买给了 SCO,这次的争议是,那一次出售的范围是否包含 Novell 从 AT&T 那里获得的 Unix 版权。最后 SCO 败诉,法院判决 Novell 拥有他那一部分 Unix 版权(后来连带着 Unix 商标一同移交给了 The Open Group)。这个判决直接导致 SCO 在之后的其他官司中胜率大幅降低,可能美国的司法机关也有自己的倾向性,不希望 IT 界遭到重创。

参考资料与拓展阅读