用戶態(tài) tcpdump 如何實(shí)現(xiàn)抓到內(nèi)核網(wǎng)絡(luò)包的?
今天聊聊大家工作中經(jīng)常用到的 tcpdump。
在網(wǎng)絡(luò)包的發(fā)送和接收過(guò)程中,絕大部分的工作都是在內(nèi)核態(tài)完成的。那么問(wèn)題來(lái)了,我們常用的運(yùn)行在用戶態(tài)的程序 tcpdump 是那如何實(shí)現(xiàn)抓到內(nèi)核態(tài)的包的呢?有的同學(xué)知道 tcpdump 是基于 libpcap 的,那么 libpcap 的工作原理又是啥樣的呢。如果讓你裸寫一個(gè)抓包程序,你有沒(méi)有思路?
按照飛哥的風(fēng)格,不搞到最底層的原理咱是不會(huì)罷休的。所以我對(duì)相關(guān)的源碼進(jìn)行了深入分析。通過(guò)本文,你將徹底搞清楚了以下這幾個(gè)問(wèn)題。
tcpdump 是如何工作的? netfilter 過(guò)濾的包 tcpdump 是否可以抓的到? 讓你自己寫一個(gè)抓包程序的話該如何下手?
借助這幾個(gè)問(wèn)題,我們來(lái)展開(kāi)今天的探索之旅!
一、網(wǎng)絡(luò)包接收過(guò)程

找到 tcpdump 抓包點(diǎn)
我們?cè)诰W(wǎng)絡(luò)設(shè)備層的代碼里找到了 tcpdump 的抓包入口。在 __netif_receive_skb_core 這個(gè)函數(shù)里會(huì)遍歷 ptype_all 上的協(xié)議。還記得上文中我們提到 tcpdump 在 ptype_all 上注冊(cè)了虛擬協(xié)議。這時(shí)就能執(zhí)行的到了。來(lái)看函數(shù):
//file:?net/core/dev.c
static?int?__netif_receive_skb_core(struct?sk_buff?*skb,?bool?pfmemalloc)
{
????......
????//遍歷?ptype_all?(tcpdump?在這里掛了虛擬協(xié)議)
????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;
????????}
????}
}
在上面函數(shù)中遍歷 ptype_all,并使用 deliver_skb 來(lái)調(diào)用協(xié)議中的回調(diào)函數(shù)。
//file:?net/core/dev.c?
static?inline?int?deliver_skb(...)
{
?return?pt_prev->func(skb,?skb->dev,?pt_prev,?orig_dev);
}
對(duì)于 tcpdump 來(lái)說(shuō),就會(huì)進(jìn)入 packet_rcv 了(后面我們?cè)僬f(shuō)為啥是進(jìn)入這個(gè)函數(shù))。這個(gè)函數(shù)在 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);
?......
}
可見(jiàn) packet_rcv 把收到的 skb 放到了當(dāng)前 packet socket 的接收隊(duì)列里了。這樣后面調(diào)用 recvfrom 的時(shí)候就可以獲取到所抓到的包!!
再找 netfilter 過(guò)濾點(diǎn)
為了解釋我們開(kāi)篇中提到的問(wèn)題,這里我們?cè)偕晕⒌絽f(xié)議層中多看一些。在 ip_rcv 中我們找到了一個(gè) netfilter 相關(guān)的執(zhí)行邏輯。
//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 作為關(guān)鍵詞來(lái)搜索,還能搜到不少 netfilter 的過(guò)濾點(diǎn)。不過(guò)所有的過(guò)濾點(diǎn)都是位于 IP 協(xié)議層的。

所以,在接收包的過(guò)程中,netfilter 過(guò)濾并不會(huì)影響 tcpdump 的抓包!
二、網(wǎng)絡(luò)包發(fā)送過(guò)程

找到 netfilter 過(guò)濾點(diǎn)
在發(fā)送的過(guò)程中,同樣是在 IP 層進(jìn)入各種 netfilter 規(guī)則的過(guò)濾。
//file:?net/ipv4/ip_output.c??
int?ip_local_out(struct?sk_buff?*skb)
{
?//執(zhí)行?netfilter?過(guò)濾
?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);
}
在這個(gè)文件中,還能看到若干處 netfilter 過(guò)濾邏輯。
找到 tcpdump 抓包點(diǎn)
發(fā)送過(guò)程在協(xié)議層處理完畢到達(dá)網(wǎng)絡(luò)設(shè)備層的時(shí)候,也有 tcpdump 的抓包點(diǎn)。
//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 中的協(xié)議,并依次調(diào)用 deliver_skb。這就會(huì)執(zhí)行到 tcpdump 掛在上面的虛擬協(xié)議。

三、TCPDUMP 啟動(dòng)
前面兩小節(jié)我們說(shuō)到了內(nèi)核收發(fā)包都通過(guò)遍歷 ptype_all 來(lái)執(zhí)行抓包的。那么我們現(xiàn)在來(lái)看看用戶態(tài)的 tcpdump 是如何掛載協(xié)議到內(nèi) ptype_all 上的。
我們通過(guò) strace 命令我們抓一下 tcpdump 命令的系統(tǒng)調(diào)用,顯示結(jié)果中有一行 socket 系統(tǒng)調(diào)用。Tcpdump 秘密的源頭就藏在這行對(duì) socket 函數(shù)的調(diào)用里。
#?strace?tcpdump?-i?eth0
socket(AF_PACKET,?SOCK_RAW,?768)
......
socket 系統(tǒng)調(diào)用的第一個(gè)參數(shù)表示創(chuàng)建的 socket 所屬的地址簇或者協(xié)議簇,取值以 AF 或者 PF 開(kāi)頭。在 Linux 里,支持很多種協(xié)議族,在 include/linux/socket.h 中可以找到所有的定義。這里創(chuàng)建的是 packet 類型的 socket。
協(xié)議族和地址族:每一種協(xié)議族都有其對(duì)應(yīng)的地址族。比如 IPV4 的協(xié)議族定義叫 PF_INET,其地址族的定義是 AF_INET。它們是一一對(duì)應(yīng)的,而且值也完全一樣,所以經(jīng)?;煊?。
//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??*/
......
另外上面第三個(gè)參數(shù) 768 代表的是 ETH_P_ALL,socket.htons(ETH_P_ALL) = 768。
我們來(lái)展開(kāi)看這個(gè) packet 類型的 socket 創(chuàng)建的過(guò)程中都干了啥,找到 socket 創(chuàng)建源碼。
//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 中獲取了指定協(xié)議。并調(diào)用了它的 create 方法來(lái)完成創(chuàng)建。

//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;
?//注冊(cè)鉤子
?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];
}
我們整篇文章都以 ETH_P_ALL 為例,但其實(shí)有的時(shí)候也會(huì)有其它情況。在別的情況下可能會(huì)注冊(cè)協(xié)議到 ptype_base 里了,而不是 ptype_all。同樣, ptype_base 中的協(xié)議也會(huì)在發(fā)送和接收的過(guò)程中被執(zhí)行到。
總結(jié):tcpdump 啟動(dòng)的時(shí)候內(nèi)部邏輯其實(shí)很簡(jiǎn)單,就是在 ptype_all 中注冊(cè)了一個(gè)虛擬協(xié)議而已。
四、總結(jié)
現(xiàn)在我們?cè)倩仡^看開(kāi)篇提到的幾個(gè)問(wèn)題。
1. tcpdump是如何工作的
用戶態(tài) tcpdump 命令是通過(guò) socket 系統(tǒng)調(diào)用,在內(nèi)核源碼中用到的 ptype_all 中掛載了函數(shù)鉤子上去。無(wú)論是在網(wǎng)絡(luò)包接收過(guò)程中,還是在發(fā)送過(guò)程中,都會(huì)在網(wǎng)絡(luò)設(shè)備層遍歷 ptype_all 中的協(xié)議,并執(zhí)行其中的回調(diào)。tcpdump 命令就是基于這個(gè)底層原理來(lái)工作的。
關(guān)于這個(gè)問(wèn)題,得分接收和發(fā)送過(guò)程分別來(lái)看。在網(wǎng)絡(luò)包接收的過(guò)程中,由于 tcpdump 近水樓臺(tái)先得月,所以完全可以捕獲到命中 netfilter 過(guò)濾規(guī)則的包。

但是在發(fā)送的過(guò)程中,恰恰相反。網(wǎng)絡(luò)包先經(jīng)過(guò)協(xié)議層,這時(shí)候被 netfilter 過(guò)濾掉的話,底層工作的 tcpdump 還沒(méi)等看見(jiàn)就啥也沒(méi)了。

如果你想自己寫一段類似 tcpdump 的抓包程序的話,使用 packet socket 就可以了。我用 c 寫了一段抓包,并且解析源 IP 和目的 IP 的簡(jiǎn)單 demo。
源碼地址:https://github.com/yanfeizhang/coder-kung-fu/blob/main/tests/network/test04/main.c
編譯一下,注意運(yùn)行需要 root 權(quán)限。
#?gcc?-o?main?main.c
#?./main?
運(yùn)行結(jié)果預(yù)覽如下。

推薦閱讀:
完全整理 | 365篇高質(zhì)技術(shù)文章目錄整理
專注服務(wù)器后臺(tái)技術(shù)棧知識(shí)總結(jié)分享
歡迎關(guān)注交流共同進(jìn)步
碼農(nóng)有道?coding
碼農(nóng)有道,為您提供通俗易懂的技術(shù)文章,讓技術(shù)變的更簡(jiǎn)單
嘿,你在看嗎?
