協(xié)議棧的細(xì)節(jié)
下面將介紹一些內(nèi)核網(wǎng)絡(luò)協(xié)議棧中常常涉及到的概念。
sk_buff
內(nèi)核顯然需要一個(gè)數(shù)據(jù)結(jié)構(gòu)來(lái)表示報(bào)文,這個(gè)結(jié)構(gòu)就是 sk_buff ( socket buffer 的簡(jiǎn)稱(chēng)),它等同于在中描述的 BSD 內(nèi)核中的 mbuf。
sk_buff 結(jié)構(gòu)自身并不存儲(chǔ)報(bào)文內(nèi)容,它通過(guò)多個(gè)指針指向真正的報(bào)文內(nèi)存空間:

sk_buff 是一個(gè)貫穿整個(gè)協(xié)議棧層次的結(jié)構(gòu),在各層間傳遞時(shí),內(nèi)核只需要調(diào)整 sk_buff 中的指針位置就行。
net_device
內(nèi)核使用 net_device 表示網(wǎng)卡。網(wǎng)卡可以分為物理網(wǎng)卡和虛擬網(wǎng)卡。物理網(wǎng)卡是指真正能把報(bào)文發(fā)出本機(jī)的網(wǎng)卡,包括真實(shí)物理機(jī)的網(wǎng)卡以及VM虛擬機(jī)的網(wǎng)卡,而像 tun/tap,vxlan、veth pair 這樣的則屬于虛擬網(wǎng)卡的范疇。
如下圖所示,每個(gè)網(wǎng)卡都有兩端,一端是協(xié)議棧(IP、TCP、UDP),另一端則有所區(qū)別,對(duì)物理網(wǎng)卡來(lái)說(shuō),這一端是網(wǎng)卡生產(chǎn)廠商提供的設(shè)備驅(qū)動(dòng)程序,而對(duì)虛擬網(wǎng)卡來(lái)說(shuō)差別就大了,正是由于虛擬網(wǎng)卡的存在,內(nèi)核才能支持各種隧道封裝、容器通信等功能。

socket & sock
用戶空間通過(guò) socket()、bind()、listen()、accept() 等庫(kù)函數(shù)進(jìn)行網(wǎng)絡(luò)編程。而這里提到的 socket 和 sock 是內(nèi)核中的兩個(gè)數(shù)據(jù)結(jié)構(gòu),其中 socket 向上面向用戶,而 sock 向下面向協(xié)議棧。
如下圖所示,這兩個(gè)結(jié)構(gòu)實(shí)際上是一一對(duì)應(yīng)的。

注意到,這兩個(gè)結(jié)構(gòu)上都有一個(gè)叫 ops 的指針, 但它們的類(lèi)型不同。socket 的 ops 是一個(gè)指向 struct proto_ops 的指針,sock 的 ops 是一個(gè)指向 struct proto 的指針, 它們?cè)诮Y(jié)構(gòu)被創(chuàng)建時(shí)確定。
回憶網(wǎng)絡(luò)編程中 socket() 函數(shù)的原型:
#include? sockfd?=?socket(int?socket_family,?int?socket_type,?int?protocol);
實(shí)際上, socket->ops 和 sock->ops 由前兩個(gè)參數(shù) socket_family 和 socket_type 共同確定。
如果 socket_family 是最常用的 PF_INET 協(xié)議簇, 則 socket->ops 和 sock->ops 的取值就記錄在 INET 協(xié)議開(kāi)關(guān)表中:
static?struct?inet_protosw?inetsw_array[]?= { ????{ ????????.type?=?????SOCK_STREAM, ????????.protocol?=?IPPROTO_TCP, ????????.prot?=?????&tcp_prot,?????????????????//?對(duì)應(yīng)?sock->ops ????????.ops?=??????&inet_stream_ops,??????????//?對(duì)應(yīng)?socket->ops ????????.flags?=????INET_PROTOSW_PERMANENT?|?INET_PROTOSW_ICSK, ????}, ????{ ????????.type?=?????SOCK_DGRAM, ????????.protocol?=?IPPROTO_UDP, ????????.prot?=?????&udp_prot,?????????????????//?對(duì)應(yīng)?sock->ops ????????.ops?=??????&inet_dgram_ops,???????????//?對(duì)應(yīng)?socket->ops ????????.flags?=????INET_PROTOSW_PERMANENT, ????}, } .......
L3->L4
我們知道網(wǎng)絡(luò)協(xié)議棧是分層的,但實(shí)際上,具體到實(shí)現(xiàn),內(nèi)核協(xié)議棧的分層只是邏輯上的,本質(zhì)還是函數(shù)調(diào)用。發(fā)送流程(上層調(diào)用下層)通常是直接調(diào)用(因?yàn)闆](méi)有不確定性,比如TCP知道下面一定IP),但接收過(guò)程不一樣了,比如報(bào)文在 IP 層時(shí),它上面可能是 TCP,也可能是 UDP,或者是 ICMP 等等,所以接收過(guò)程使用的是注冊(cè)-回調(diào)機(jī)制。
還是以 INET 協(xié)議簇為例,注冊(cè)接口是:
int?inet_add_protocol(const?struct?net_protocol?*prot,?unsigned?char?protocol);
在內(nèi)核網(wǎng)絡(luò)子系統(tǒng)初始化時(shí),L4 層協(xié)議(如下面的 TCP 和 UDP)會(huì)被注冊(cè):
static?struct?net_protocol?tcp_protocol?=?{
????......
????.handler?=?tcp_v4_rcv,
????......
};
static?struct?net_protocol?udp_protocol?=?{
????.....
????.handler?=?udp_rcv,
????.....
};
.......
而在IP層,查詢過(guò)路由后,如果該報(bào)文是需要上送本機(jī)的,則會(huì)根據(jù)報(bào)文的 L4 協(xié)議,送給不同的 L4 處理:
static?int?ip_local_deliver_finish(struct?net?*net,?struct?sock?*sk,?struct?sk_buff?*skb)
{
????......
????ipprot?=?rcu_dereference(inet_protos[protocol]);
????......
????ret?=?ipprot->handler(skb);?????
????......
}
.......
L2->L3
L2->L3 如出一轍。只不過(guò)注冊(cè)接口變成了:
void?dev_add_pack(struct?packet_type?*pt)
誰(shuí)會(huì)注冊(cè)呢?顯然至少 IP 會(huì):
static?struct?packet_type?ip_packet_type?=?{
????.type?=?cpu_to_be16(ETH_P_IP),
????.func?=?ip_rcv,
}
.......
而在報(bào)文接收過(guò)程中,設(shè)備驅(qū)動(dòng)程序會(huì)將報(bào)文的 L3 類(lèi)型設(shè)置到 skb->protocol,然后在內(nèi)核 netif_receive_skb 收包時(shí),會(huì)根據(jù)這個(gè) protocol 調(diào)用不同的回調(diào)函數(shù):
__netif_receive_skb(struct?sk_buff?*skb)
{
????......
????type?=?skb->protocol;
????......
????ret?=?pt_prev->func(skb,?skb->dev,?pt_prev,?orig_dev);
}
.......
Netfilter 是報(bào)文在內(nèi)核協(xié)議棧必然會(huì)通過(guò)的路徑,我們從下面這張圖就可以看到,Netfilter 在內(nèi)核的 5 個(gè)地方設(shè)置了 HOOK 點(diǎn),用戶可以通過(guò)配置 iptables 規(guī)則,在 HOOK 點(diǎn)對(duì)報(bào)文進(jìn)行過(guò)濾、修改等操作。

在內(nèi)核代碼中,我們時(shí)??梢?jiàn) NF_HOOK 這樣的調(diào)用。我的建議是,如果你暫時(shí)不考慮 Netfilter,那么就直接跳過(guò), 跟蹤 okfn 就行。
static?inline?int?NF_HOOK(uint8_t?pf,?unsigned?int?hook,?struct?net?*net,?struct?sock?*sk,?
????struct?sk_buff?*skb,?struct?net_device?*in,?struct?net_device?*out,
????int?(*okfn)(struct?net?*,?struct?sock?*,?struct?sk_buff?*))
{
????int?ret?=?nf_hook(pf,?hook,?net,?sk,?skb,?in,?out,?okfn);
????if?(ret?==?1)
????????ret?=?okfn(net,?sk,?skb);
????return?ret;
}
.......
dst_entry
內(nèi)核需要確定收到的報(bào)文是應(yīng)該本地上送(local deliver)還是轉(zhuǎn)發(fā)(forward),對(duì)本機(jī)發(fā)送(local out)的報(bào)文需要確定是從哪個(gè)網(wǎng)卡發(fā)送出去,這都是內(nèi)核通過(guò)查詢 fib (forward information base, 轉(zhuǎn)發(fā)信息表) 確定。fib 可以理解為一個(gè)數(shù)據(jù)庫(kù),數(shù)據(jù)來(lái)源是用戶配置或者內(nèi)核自動(dòng)生成的路由。
fib 查詢的輸入是報(bào)文 sk_buff,輸出是 dst_entry. dst_entry 會(huì)被設(shè)置到 skb 上:
static?inline?void?skb_dst_set(struct?sk_buff?*skb,?struct?dst_entry?*dst)
{
????skb->_skb_refdst?=?(unsigned?long)dst;
}
而 dst_entry 中最重要的是一個(gè) input 指針和 output 指針:
struct?dst_entry?
{
????......
????int?(*input)(struct?sk_buff?*);
????int?(*output)(struct?net?*net,?struct?sock?*sk,?struct?sk_buff?*skb);
????......
}
對(duì)于需要本機(jī)上送的報(bào)文:
rth->dst.input?=?ip_local_deliver;
對(duì)需要轉(zhuǎn)發(fā)的報(bào)文:
rth->dst.input?=?ip_forward;
對(duì)本機(jī)發(fā)送的報(bào)文:
rth->dst.output?=?ip_output;
編輯:黃飛
電子發(fā)燒友App














評(píng)論