1. 前言
本文主要對 Linux 系統內核協議棧中網路層接收,發送以及轉發數據包的流程進行簡要介紹,同時對 Netfilter 數據包過濾框架的基本原理以及使用方式進行簡單闡述。
內容如有理解錯誤而導致說明錯誤的地方,還請指正。如存在引用而沒有添加說明的,也請及時告知,非常感謝!
2. 基礎網路知識
2.1 網路分層模型
OSI 模型中將網路劃分為七層,但在目前實際廣泛使用的 TCP/IP 協議框架體系內,我們一般將網路劃分為五層,從下到上依次為物理層,鏈路層,網路層,傳輸層以及應用層。兩者的區別在於 OSI 模型在應用層對數據包做了更細緻的劃分。兩者的關係如下圖所示:

圖片來源:
https://www.cnblogs.com/qishui/p/5428938.html
在 TCP/IP 協議框架體系的五層網路模型中,每一層負責處理的數據包協議或類型均存在差異,物理層主要負責在物理載體上的數據包傳輸,如 WiFi,乙太網,光纖,電話線等;數據鏈路層主要負責鏈路層協議解析(主要為乙太網幀,其他類型此處暫不考慮),網路層主要負責 IP 協議(包括 IPv4 和 IPv6)解析,傳輸層負責傳輸層協議解析(主要為 TCP,UDP 等),而傳輸層以上我們均歸類為應用層,主要包括各類應用層協議,如我們常用的 HTTP,FTP,SMTP,DNS,DHCP 等。
在 TCP/IP 協議框架體系內,下層協議對上層協議透明,即上層協議無需關注下層協議的實現邏輯和機制。
2.2 數據包協議分層
在 TCP/IP 協議框架體系內,上層協議報文被作為下層協議的數據載荷(Data Payload),存儲在下層協議的數據段區域中進行傳輸。結合這一特性,我們常見的幾類網路協議嵌套關係如下圖所示:

從上圖我們可以清晰地看到各類協議之間的嵌套關係,如使用 HTTP 協議的應用 App1 在傳輸層封裝在 TCP 協議中,TCP 協議在網路層又封裝到 IP 協議中,最後交到數據鏈路層中。其他應用層 App 也類似。
網際報文控制協議(ICMP,使用該協議的 Ping 工具),以及網際組管理協議(IGMP,組播多播中的控制報文)是直接嵌套到 IP 數據包中,而不依賴於 TCP 或 UDP。
地址解析協議(ARP)和反解析協議(RARP)則是直接嵌套在數據鏈路層數據包中進行傳輸。
註:在本文中,我們只大概了解整體的網路框架,各協議的具體內容這裡不做贅述。
2.3 sk_buff 結構
在 Linux 內核中,系統使用 sk_buff 數據結構對數據包進行存儲和管理。在數據包接收過程中,該數據結構從網卡驅動收包開始,一直貫穿到內核網路協議棧的頂層,直到用戶態程序從內核獲取數據。使用圖形表示 sk_buff 的結構如下:

在 sk_buff 數據結構中包含了諸多關於數據包存儲,定位和管理的指針,數據包在網路協議棧各層次之間進行傳輸的過程中,內核通過操作指針的方式對數據包進行逐層解析,避免頻繁的大數據段拷貝操作,從而提高數據包處理效率(但在某些特殊情況下依然會採用數據包拷貝操作)。
2.4 收發包整體框架
這裡我們從客戶端和服務端整體框架層面來看數據收發流程:

- 用戶態(User Space)程序 Client 向另一台主機上的 Server 發送數據,需要通過調用內核態(Kernel Space)提供給用戶態的 Socket 抽象層介面發送數據;
- Socket 抽象層介面收到用戶態數據後,向下交給傳輸層介面(TCP 或 UDP);
- 傳輸層負責創建 sk_buff,並將用戶數據(應用層數據)填充到緩衝區,做合法性檢查後,添加傳輸層頭部,並通過網路層註冊的介面將數據包交給網路層處理;
- 網路層收到傳輸層數據包後,會查詢路由表,決定數據包去向,如果是需要發出的數據包,會填充網路層頭部,並交到內核虛擬網路介面設備的發送隊列中;
- 虛擬網路介面從發送隊列獲取數據,調用對應網卡驅動發送數據;
Server 端接收數據時,按照相反的過程從網卡驅動中將數據包一層層上交,直到通過 Socket 抽象層介面將用戶數據上交到用戶態 Server 進程處理。
3. 網路層(IPv4)收發包流程
數據包在實際現網傳輸過程中,會經過各類交換機,路由器的轉發處理,在這個過程中,路由器一般只處理到網路層。這裡我們僅對 Linux 內核中網路層接收,發送以及轉發數據的流程進行簡單介紹。
下圖為基於 Linux 2.6.38 版本內核的網路層相關介面在數據包收發過程的調用邏輯圖:

註:
1)不同版本內核在函數名上可能存在一定差異,但整體調用邏輯基本不變;
2)該圖僅展示 IPv4 的處理流程,IPv6 不在該圖的函數中處理,但整體流程基本相似;
3)該圖展示的流程僅為普通單播並且未進行 IP 分片的數據包處理流程,組播,多播,IP 分片的數據包在某些流程上存在差異;
- 從圖中可以看到,*ip_rcv*函數為網路層向下層開放的入口,數據包通過該函數進入網路層進行處理,該函數主要對上傳到網路層的數據包進行前期合法性檢查,通過後交由 Netfilter 的鉤子節點;
- 綠色方框內的IP_PRE_ROUTING為 Netfilter 框架的 Hook 點,該節點會根據預設的規則對數據包進行判決並根據判決結果做相關的處理,比如執行 NAT 轉換;
- IP_PRE_ROUTING節點處理完成後,數據包將交由*ip_rcv_finish*處理,該函數根據路由判決結果,決定數據包是交由本機上層應用處理,還是需要進行轉發;如果是交由本機處理,則會交由*ip_local_deliver*走本地上交流程;如果需要轉發,則交由*ip_forward*函數走轉發流程;
- 在數據包上交本地的流程中,IP_LOCAL_INPUT節點用於監控和檢查上交到本地上層應用的數據包,該節點是 Linux 防火牆的重要生效節點之一;
- 在數據包轉發流程中,Netfilter 框架的IP_FORWARD節點會對轉發數據包進行檢查過濾;
- 而對於本機上層發出的數據包,網路層通過註冊到上層的*ip_local_out*函數接收數據處理,處理 OK 進一步交由IP_LOCAL_OUT節點檢測;
- 對於即將發往下層的數據包,需要經過IP_POST_ROUTING節點處理;網路層處理結束,通過*dev_queue_xmit*函數將數據包交由 Linux 內核中虛擬網路設備做進一步處理,從這裡數據包即離開網路層進入到下一層;
4. Netfilter 框架
Netfilter 是 Linux 內核中進行數據包過濾,連接跟蹤(Connect Track),網路地址轉換(NAT)等功能的主要實現框架;該框架在網路協議棧處理數據包的關鍵流程中定義了一系列鉤子點(Hook 點),並在這些鉤子點中註冊一系列函數對數據包進行處理。這些註冊在鉤子點的函數即為設置在網路協議棧內的數據包通行策略,也就意味著,這些函數可以決定內核是接受還是丟棄某個數據包,換句話說,這些函數的處理結果決定了這些網路數據包的「命運」。
下圖為 Netfilter 框架的整體組件圖:

圖片來源:
http://wiki.dreamrunner.org/public_html/Linux/Networks/netfilter.html
從圖中我們可以看到,Netfilter 框架採用模塊化設計理念,並且貫穿了 Linux 系統的內核態和用戶態。在用戶態層面,根據不同的協議類型,為上層用戶提供了不同的系統調用工具,比如我們常用的針對 IPv4 協議 iptables,IPv6 協議的 ip6tables,針對 ARP 協議的 arptables,針對網橋控制的 ebtables,針對網路連接追蹤的 conntrack 等等。不同的用戶態工具在內核中有對應的模塊進行實現,而底層都需要調用 Netfilter hook API 介面進行實現。
從圖中我們可以看到,我們常用的 Linux 防火牆工具 iptables 其實也是 Netfilter 框架中的一個組件。接下來我們就以 IPv4 為例,描述 iptables 在 Netfilter 框架中生效的基本原理,同時,我們也看一下如果我們希望在內核中添加我們自己的處理函數,我們該怎麼做。
4.1 IPv4 網路層的 Netfilter Hook 點
在第二章已經提及,Linux 內核中,Netfiler 在網路層設置了多個 Hook 點,這裡我們不考慮實際的處理函數,僅看 Netfilter 的鉤子節點,從而將網路層處理流程進行簡化,如下圖:

其中,矩形方框中的即為 Netfilter 的鉤子節點。從圖中可以看到,三個方向的數據包需要經過的鉤子節點不完全相同:
- 發往本地:NF_INET_PRE_ROUTING–>NF_INET_LOCAL_IN
- 轉發:NF_INET_PRE_ROUTING–>NF_INET_FORWARD–>NF_INET_POST_ROUTING
- 本地發出:NF_INET_LOCAL_OUT–>NF_INET_POST_ROUTING
4.2 iptables 工具
iptables 在用戶態提供了表格和鏈的概念。包含的表格有 filter,nat,mangle 以及 raw。而每個表格下包含不同的鏈,如下圖所示:

iptables 中每個表格的作用不同,以我們比較常用的 filter 表為例,其主要起到數據包過濾和攔截作用,包含 INPUT,FORWARD 和 OUTPUT 三個鏈,根據鏈的名字我們可以知道,這三個鏈分別被放置到 Netfilter 三個不同的鉤子節點中生效。INPUT 鏈是在NF_INET_LOCAL_IN節點,FORWARD 鏈是在NF_INET_FORWARD節點,OUTPUT 鏈則是在NF_INET_LOCAL_OUT節點。其他表格的鏈也類似。
以如下 iptables 指令為例:
iptables -t filter -A INPUT -s 172.16.0.0/16 -p udp --dport 53 -j DROP
該指令是在 filter 表的 INPUT 鏈中添加一條過濾規則,凡是收到源地址為 172.16.0.0/16,傳輸層協議為 UDP 並且目的埠為 53 的數據包(即 DNS 數據包),都將該數據包丟棄。在 Linux 內核中,這一個指令會在 Netfilter 網路層NF_INET_LOCAL_IN節點生成處理操作,凡是經過這個鉤子節點的數據包,在前面規則都通過的情況下,都必須經過這一規則的檢查,如果符合這條規則的匹配條件,則該數據包會被丟棄;如果不符合,則進行下一條規則的匹配。
在 Linux 內核內部,使用 iptables 工具下發的指令規則,會存儲在內核中的 Xtables 模塊中,這部分內容這裡不再深入分析。
4.3 Netfilter 重要數據結構及相關函數
- 鉤子點枚舉類型
上面提到的網路層中 Netfilter 的幾個鉤子節點,在內核中是以枚舉數據類型進行標記的。如下:
// include/linux/netfilter.h
enum nf_inet_hooks {
NF_INET_PRE_ROUTING,
NF_INET_LOCAL_IN,
NF_INET_FORWARD,
NF_INET_LOCAL_OUT,
NF_INET_POST_ROUTING,
NF_INET_NUMHOOKS
};
- 註冊和解註冊鉤子函數
// include/linux/netfilter.h
/* Function to register/unregister hook points. */
int nf_register_hook(struct nf_hook_ops *reg);
void nf_unregister_hook(struct nf_hook_ops *reg);
int nf_register_hooks(struct nf_hook_ops *reg, unsigned int n);
void nf_unregister_hooks(struct nf_hook_ops *reg, unsigned int n);
這些函數用於將自定義的鉤子操作(struct nf_hook_ops)註冊到指定的鉤子節點中。
- 鉤子操作數據結構
// include/linux/netfilter.h
struct nf_hook_ops {
struct list_head list;
/* User fills in from here down. */
nf_hookfn *hook;
struct module *owner;
u_int8_t pf;
unsigned int hooknum;
/* Hooks are ordered in ascending priority. */
int priority;
};
這個結構體中存儲了自定義的鉤子函數(nf_hookfn),函數優先順序(priority),處理協議類型(pf),鉤子函數生效的鉤子節點(hooknum)等信息。
- 鉤子函數聲明
// include/linux/netfilter.h
typedef unsigned int nf_hookfn(unsigned int hooknum,
struct sk_buff *skb,
const struct net_device *in,
const struct net_device *out,
int (*okfn)(struct sk_buff *));
如果我們自己實現一個內核模塊,該模塊需要在 Netfilter 框架的幾個鉤子節點中對經過的數據包進行處理,則該內核模塊需要向 Netfilter 中的鉤子節點註冊鉤子函數,我們需要按照 nf_hookfn 函數的聲明類型,提供我們自己的實現,再按照之前提供的註冊介面將相關數據類型註冊到內核中使之生效。
4.4 一個 Demo
如下為在網路上找到的一個內核模塊 Demo,該模塊的基本功能是將經過 IPv4 網路層 NF_INET_LOCAL_IN 節點的數據包的源 Mac 地址,目的 Mac 地址以及源 IP,目的 IP 列印出來。代碼如下所示:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/skbuff.h>
#include <linux/ip.h>
#include <linux/udp.h>
#include <linux/tcp.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>
MODULE_LICENSE("GPLv3");
MODULE_AUTHOR("SHI");
MODULE_DESCRIPTION("Netfliter test");
static unsigned int
nf_test_in_hook(unsigned int hook, struct sk_buff *skb, const struct net_device *in,
const struct net_device *out, int (*okfn)(struct sk_buff*));
static struct nf_hook_ops nf_test_ops[] __read_mostly = {
{
.hook = nf_test_in_hook,
.owner = THIS_MODULE,
.pf = NFPROTO_IPV4,
.hooknum = NF_INET_LOCAL_IN,
.priority = NF_IP_PRI_FIRST,
},
};
void hdr_dump(struct ethhdr *ehdr) {
printk("[MAC_DES:%x,%x,%x,%x,%x,%x"
"MAC_SRC: %x,%x,%x,%x,%x,%x Prot:%x]n",
ehdr->h_dest[0],ehdr->h_dest[1],ehdr->h_dest[2],ehdr->h_dest[3],
ehdr->h_dest[4],ehdr->h_dest[5],ehdr->h_source[0],ehdr->h_source[1],
ehdr->h_source[2],ehdr->h_source[3],ehdr->h_source[4],
ehdr->h_source[5],ehdr->h_proto);
}
#define NIPQUAD(addr)
((unsigned char *)&addr)[0],
((unsigned char *)&addr)[1],
((unsigned char *)&addr)[2],
((unsigned char *)&addr)[3]
#define NIPQUAD_FMT "%u.%u.%u.%u"
static unsigned int
nf_test_in_hook(unsigned int hook, struct sk_buff *skb, const struct net_device *in,
const struct net_device *out, int (*okfn)(struct sk_buff*)) {
struct ethhdr *eth_header;
struct iphdr *ip_header;
eth_header = (struct ethhdr *)(skb_mac_header(skb));
ip_header = (struct iphdr *)(skb_network_header(skb));
hdr_dump(eth_header);
printk("src IP:'"NIPQUAD_FMT"', dst IP:'"NIPQUAD_FMT"' n",
NIPQUAD(ip_header->saddr), NIPQUAD(ip_header->daddr));
return NF_ACCEPT;
}
static int __init init_nf_test(void) {
int ret;
ret = nf_register_hooks(nf_test_ops, ARRAY_SIZE(nf_test_ops));
if (ret < 0) {
printk("register nf hook failn");
return ret;
}
printk(KERN_NOTICE "register nf test hookn");
return 0;
}
static void __exit exit_nf_test(void) {
nf_unregister_hooks(nf_test_ops, ARRAY_SIZE(nf_test_ops));
}
module_init(init_nf_test);
module_exit(exit_nf_test);
該 Demo 為網路上找到的 Demo 程序,地址:
http://wiki.dreamrunner.org/public_html/Linux/Networks/netfilter.html
這個 Demo 程序是個內核模塊,模塊入口為module_init傳入的init_nf_test函數。
在init_nf_test函數中,其通過 Netfilter 提供的 nf_register_hooks 介面將自定義的nf_test_opt註冊到鉤子節點中。nf_test_opt為struct nf_hook_ops類型的結構體數組,其內部包含了所有關鍵元素,比如鉤子函數的註冊節點(此處為NF_INET_LOCAL_IN)以及鉤子函數(**nf_test_in_hook**)。
在nf_test_in_hook函數內部,其檢查每一個傳遞過來的數據包,並將其源 Mac 地址,目的 Mac 地址,源 IP 地址以及目的 IP 地址列印出來。最後返回NF_ACCEPT,將數據包交給下一個鉤子函數處理。
4.5 NAT 和 conntrack
NAT(Network Address Translation)技術現如今被廣泛應用於路由器等網路設備中,其在解決 IPv4 地址緊缺的問題上起到了至關重要的作用,但與此同時也存在一定的安全隱患。
而 conntrack(連接追蹤)也是廣泛應用於路由器網路設備中的模塊,其根據數據包的五元組以及 NAT 的轉換結果,記錄每一條連接的狀態,在提升設備轉發效率上起到了很大的作用,但另一方面,記錄連接信息需要消耗一部分資源,也會導致設備出現性能瓶頸。
5. 總結
Linux 網路協議棧是 Linux 內核中非常重要的子系統之一,雖然上層應用的開發維護工作極少涉及修改內核網路部分的工作,但了解其設計思想,基本工作原理,也可以為我們日常工作帶來比較不少的幫助,特別是涉及到前後台網路交互,伺服器網路性能相關的工作時。
這篇文章所涉及的內容也僅僅是 Linux 網路協議棧中網路層的極小一部分,如下為 Linux 內核中數據包流向的整體脈絡圖以及 Netfilter 的整體生效節點:

圖片來源:
http://wiki.dreamrunner.org/public_html/Linux/Networks/netfilter.html
從上圖可以看到,除了在網路層,鏈路層中 Netfilter 也被廣泛地應用,ebtables 是 Netfilter 提供給用戶態的鏈路層配置介面(工具),其生效機制與 iptables 基本類似。
6. 擴展
這篇文章僅僅對 Linux 內核中網路層數據處理流程以及 Netfilter 基本原理進行簡單介紹,在此基礎上,關於 Linux 內核網路協議棧的其他技術還包括:
- Linux TC(Traffic Control)模塊:Linux 提供的 QoS 功能支持模塊;
- 網橋和 VLAN 技術;
- Wireshark(tcpdump)等網路抓包工具的基本實現原理;
7. 參考
博客
- Linux Netfilter and Traffic Control
- OSI 七層模型與 TCP/IP 五層模型
書籍
- 《TCP/IP 詳解 卷 I:協議》
- 《深入理解 Linux 網路技術內幕》
原創文章,作者:投稿專員,如若轉載,請註明出處:https://www.506064.com/zh-tw/n/226404.html
微信掃一掃
支付寶掃一掃