連接跟蹤(conntrack):原理、應(yīng)用及 Linux 內(nèi)核實現(xiàn)
本文介紹連接跟蹤(connection tracking,conntrack,CT)的原理,應(yīng)用,及其在 Linux 內(nèi)核中的實現(xiàn)。
代碼分析基于內(nèi)核 4.19。為使行文簡潔,所貼代碼只保留了核心邏輯,但都給出了代碼 所在的源文件,如有需要請查閱。
水平有限,文中不免有錯誤之處,歡迎指正交流。
1 引言
連接跟蹤是許多網(wǎng)絡(luò)應(yīng)用的基礎(chǔ)。例如,Kubernetes Service、ServiceMesh sidecar、 軟件四層負載均衡器 LVS/IPVS、Docker network、OVS、iptables 主機防火墻等等,都依賴 連接跟蹤功能。
1.1 概念
連接跟蹤(conntrack)
連接跟蹤,顧名思義,就是跟蹤(并記錄)連接的狀態(tài)。
例如,圖 1.1 是一臺 IP 地址為 10.1.1.2 的 Linux 機器,我們能看到這臺機器上有三條 連接:
機器訪問外部 HTTP 服務(wù)的連接(目的端口 80) 外部訪問機器內(nèi) FTP 服務(wù)的連接(目的端口 21) 機器訪問外部 DNS 服務(wù)的連接(目的端口 53)
連接跟蹤所做的事情就是發(fā)現(xiàn)并跟蹤這些連接的狀態(tài),具體包括:
從數(shù)據(jù)包中提取元組(tuple)信息,辨別數(shù)據(jù)流(flow)和對應(yīng)的連接(connection) 為所有連接維護一個狀態(tài)數(shù)據(jù)庫(conntrack table),例如連接的創(chuàng)建時間、發(fā)送 包數(shù)、發(fā)送字節(jié)數(shù)等等 回收過期的連接(GC) 為更上層的功能(例如 NAT)提供服務(wù)
需要注意的是,連接跟蹤中所說的“連接”,概念和 TCP/IP 協(xié)議中“面向連接”( connection oriented)的“連接”并不完全相同,簡單來說:
TCP/IP 協(xié)議中,連接是一個四層(Layer 4)的概念。 TCP 是有連接的,或稱面向連接的(connection oriented),發(fā)送出去的包都要求對端應(yīng)答(ACK),并且有重傳機制 UDP 是無連接的,發(fā)送的包無需對端應(yīng)答,也沒有重傳機制 CT 中,一個元組(tuple)定義的一條數(shù)據(jù)流(flow )就表示一條連接(connection)。 后面會看到 UDP 甚至是 ICMP 這種三層協(xié)議在 CT 中也都是有連接記錄的 但不是所有協(xié)議都會被連接跟蹤
本文中用到“連接”一詞時,大部分情況下指的都是后者,即“連接跟蹤”中的“連接”。
網(wǎng)絡(luò)地址轉(zhuǎn)換(NAT)
網(wǎng)絡(luò)地址轉(zhuǎn)換(NAT),意思也比較清楚:對(數(shù)據(jù)包的)網(wǎng)絡(luò)地址(IP + Port)進行轉(zhuǎn)換。
例如,圖 1.2 中,機器自己的 IP 10.1.1.2 是能與外部正常通信的,但 192.168 網(wǎng)段是私有 IP 段,外界無法訪問,也就是說源 IP 地址是 192.168 的包,其應(yīng)答包是無 法回來的。
因此當源地址為 192.168 網(wǎng)段的包要出去時,機器會先將源 IP 換成機器自己的 10.1.1.2 再發(fā)送出去;收到應(yīng)答包時,再進行相反的轉(zhuǎn)換。這就是 NAT 的基本過程。
Docker 默認的 bridge 網(wǎng)絡(luò)模式就是這個原理 [4]。每個容器會分一個私有網(wǎng)段的 IP 地址,這個 IP 地址可以在宿主機內(nèi)的不同容器之間通信,但容器流量出宿主機時要進行 NAT。
NAT 又可以細分為幾類:
SNAT:對源地址(source)進行轉(zhuǎn)換 DNAT:對目的地址(destination)進行轉(zhuǎn)換 Full NAT:同時對源地址和目的地址進行轉(zhuǎn)換
以上場景屬于 SNAT,將不同私有 IP 都映射成同一個“公有 IP”,以使其能訪問外部網(wǎng)絡(luò)服 務(wù)。這種場景也屬于正向代理。
NAT 依賴連接跟蹤的結(jié)果。連接跟蹤最重要的使用場景就是 NAT。
四層負載均衡(L4 LB)
再將范圍稍微延伸一點,討論一下 NAT 模式的四層負載均衡。
四層負載均衡是根據(jù)包的四層信息(例如 src/dst ip, src/dst port, proto)做流量分發(fā)。
VIP(Virtual IP)是四層負載均衡的一種實現(xiàn)方式:
多個后端真實 IP(Real IP)掛到同一個虛擬 IP(VIP)上 客戶端過來的流量先到達 VIP,再經(jīng)負載均衡算法轉(zhuǎn)發(fā)給某個特定的后端 IP
如果在 VIP 和 Real IP 節(jié)點之間使用的 NAT 技術(shù)(也可以使用其他技術(shù)),那客戶端訪 問服務(wù)端時,L4LB 節(jié)點將做雙向 NAT(Full NAT),數(shù)據(jù)流如圖 1.3。
1.2 原理
了解以上概念之后,我們來思考下連接跟蹤的技術(shù)原理。
要跟蹤一臺機器的所有連接狀態(tài),就需要
攔截(或稱過濾)流經(jīng)這臺機器的每一個數(shù)據(jù)包,并進行分析。 根據(jù)這些信息建立起這臺機器上的連接信息數(shù)據(jù)庫(conntrack table)。 根據(jù)攔截到的包信息,不斷更新數(shù)據(jù)庫
例如,
攔截到一個 TCP SYNC包時,說明正在嘗試建立 TCP 連接,需要創(chuàng)建一條新 conntrack entry 來記錄這條連接攔截到一個屬于已有 conntrack entry 的包時,需要更新這條 conntrack entry 的收發(fā)包數(shù)等統(tǒng)計信息
除了以上兩點功能需求,還要考慮性能問題,因為連接跟蹤要對每個包進行過濾和分析 。性能問題非常重要,但不是本文重點,后面介紹實現(xiàn)時會進一步提及。
之外,這些功能最好還有配套的管理工具來更方便地使用。
1.3 設(shè)計:Netfilter
Linux 的連接跟蹤是在 Netfilter 中實現(xiàn)的。
Netfilter 是 Linux 內(nèi)核中一個對數(shù)據(jù) 包進行控制、修改和過濾(manipulation and filtering)的框架。它在內(nèi)核協(xié)議 棧中設(shè)置了若干hook 點,以此對數(shù)據(jù)包進行攔截、過濾或其他處理。
“說地更直白一些,hook 機制就是在數(shù)據(jù)包的必經(jīng)之路上設(shè)置若干檢測點,所有到達這 些檢測點的包都必須接受檢測,根據(jù)檢測的結(jié)果決定:
放行:不對包進行任何修改,退出檢測邏輯,繼續(xù)后面正常的包處理 修改:例如修改 IP 地址進行 NAT,然后將包放回正常的包處理邏輯 丟棄:安全策略或防火墻功能 連接跟蹤模塊只是完成連接信息的采集和錄入功能,并不會修改或丟棄數(shù)據(jù)包,后者是其 他模塊(例如 NAT)基于 Netfilter hook 完成的。
”
Netfilter 是最古老的內(nèi)核框架之一,1998 年開始開發(fā),2000 年合并到 2.4.x 內(nèi) 核主線版本 [5]。
1.4 設(shè)計:進一步思考
現(xiàn)在提到連接跟蹤(conntrack),可能首先都會想到 Netfilter。但由 1.2 節(jié)的討論可知, 連接跟蹤概念是獨立于 Netfilter 的,Netfilter 只是 Linux 內(nèi)核中的一種連接跟蹤實現(xiàn)。
換句話說,只要具備了 hook 能力,能攔截到進出主機的每個包,完全可以在此基礎(chǔ)上自 己實現(xiàn)一套連接跟蹤。
云原生網(wǎng)絡(luò)方案 Cilium 在 1.7.4+ 版本就實現(xiàn)了這樣一套獨立的連接跟蹤和 NAT 機制 (完備功能需要 Kernel 4.19+)。其基本原理是:
基于 BPF hook 實現(xiàn)數(shù)據(jù)包的攔截功能(等價于 netfilter 里面的 hook 機制) 在 BPF hook 的基礎(chǔ)上,實現(xiàn)一套全新的 conntrack 和 NAT
因此,即便卸載掉 Netfilter ,也不會影響 Cilium 對 Kubernetes ClusterIP、NodePort、ExternalIPs 和 LoadBalancer 等功能的支持 [2]。
由于這套連接跟蹤機制是獨立于 Netfilter 的,因此它的 conntrack 和 NAT 信息也沒有 存儲在內(nèi)核的(也就是 Netfilter 的)conntrack table 和 NAT table。所以常規(guī)的 conntrack/netstats/ss/lsof 等工具是看不到的,要使用 Cilium 的命令,例如:
$?cilium?bpf?nat?list
$?cilium?bpf?ct?list?global
配置也是獨立的,需要在 Cilium 里面配置,例如命令行選項 --bpf-ct-tcp-max。
另外,本文會多次提到連接跟蹤模塊和 NAT 模塊獨立,但出于性能考慮,具體實現(xiàn)中 二者代碼可能是有耦合的。例如 Cilium 做 conntrack 的垃圾回收(GC)時就會順便把 NAT 里相應(yīng)的 entry 回收掉,而非為 NAT 做單獨的 GC。
以上是理論篇,接下來看一下內(nèi)核實現(xiàn)。
2 Netfilter hook 機制實現(xiàn)
Netfilter 由幾個模塊構(gòu)成,其中最主要的是連接跟蹤(CT) 模塊和網(wǎng)絡(luò)地址轉(zhuǎn)換(NAT)模塊。
CT 模塊的主要職責(zé)是識別出可進行連接跟蹤的包。CT 模塊獨立于 NAT 模塊,但主要目的是服務(wù)于后者。
2.1 Netfilter 框架
5 個 hook 點
如上圖所示,Netfilter 在內(nèi)核協(xié)議棧的包處理路徑上提供了 5 個 hook 點,分別是:
//?include/uapi/linux/netfilter_ipv4.h
#define?NF_IP_PRE_ROUTING????0?/*?After?promisc?drops,?checksum?checks.?*/
#define?NF_IP_LOCAL_IN???????1?/*?If?the?packet?is?destined?for?this?box.?*/
#define?NF_IP_FORWARD????????2?/*?If?the?packet?is?destined?for?another?interface.?*/
#define?NF_IP_LOCAL_OUT??????3?/*?Packets?coming?from?a?local?process.?*/
#define?NF_IP_POST_ROUTING???4?/*?Packets?about?to?hit?the?wire.?*/
#define?NF_IP_NUMHOOKS???????5
用戶可以在這些 hook 點注冊自己的處理函數(shù)(handlers)。當有數(shù)據(jù)包經(jīng)過 hook 點時, 就會調(diào)用相應(yīng)的 handlers。
“另外還有一套
NF_INET_開頭的定義,include/uapi/linux/netfilter.h。這兩套是等價的,從注釋看,NF_IP_開頭的定義可能是為了保持兼容性。”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
};
hook 返回值類型
hook 函數(shù)對包進行判斷或處理之后,需要返回一個判斷結(jié)果,指導(dǎo)接下來要對這個包做什 么。可能的結(jié)果有:
//?include/uapi/linux/netfilter.h
#define?NF_DROP???0??//?已丟棄這個包
#define?NF_ACCEPT?1??//?接受這個包,繼續(xù)下一步處理
#define?NF_STOLEN?2??//?當前處理函數(shù)已經(jīng)消費了這個包,后面的處理函數(shù)不用處理了
#define?NF_QUEUE??3??//?應(yīng)當將包放到隊列
#define?NF_REPEAT?4??//?當前處理函數(shù)應(yīng)當被再次調(diào)用
hook 優(yōu)先級
每個 hook 點可以注冊多個處理函數(shù)(handler)。在注冊時必須指定這些 handlers 的優(yōu)先級,這樣觸發(fā) hook 時能夠根據(jù)優(yōu)先級依次調(diào)用處理函數(shù)。
2.2 過濾規(guī)則的組織
iptables 是配置 Netfilter 過濾功能的用戶空間工具。為便于管理, 過濾規(guī)則按功能分為若干 table:
raw filter nat mangle
這不是本文重點。更多信息可參考 (譯) 深入理解 iptables 和 netfilter 架構(gòu)
3 Netfilter conntrack 實現(xiàn)
連接跟蹤模塊用于維護可跟蹤協(xié)議(trackable protocols)的連接狀態(tài)。也就是說, 連接跟蹤針對的是特定協(xié)議的包,而不是所有協(xié)議的包。稍后會看到它支持哪些協(xié)議。
3.1 重要結(jié)構(gòu)體和函數(shù)
重要結(jié)構(gòu)體:
struct nf_conntrack_tuple {}: 定義一個 tuple。
struct nf_conntrack_man_proto {}:manipulable part 中協(xié)議相關(guān)的部分。struct nf_conntrack_man {}:tuple 的 manipulable part。
struct nf_conntrack_l4proto {}: 支持連接跟蹤的協(xié)議需要實現(xiàn)的方法集(以及其他協(xié)議相關(guān)字段)。struct nf_conntrack_tuple_hash {}:哈希表(conntrack table)中的表項(entry)。struct nf_conn {}:定義一個 flow。
重要函數(shù):
hash_conntrack_raw():根據(jù) tuple 計算出一個 32 位的哈希值(hash key)。nf_conntrack_in():連接跟蹤模塊的核心,包進入連接跟蹤的地方。resolve_normal_ct() -> init_conntrack() -> l4proto->new():創(chuàng)建一個新的連接記錄(conntrack entry)。nf_conntrack_confirm():確認前面通過nf_conntrack_in()創(chuàng)建的新連接。
3.2 struct nf_conntrack_tuple {}:元組(Tuple)
Tuple 是連接跟蹤中最重要的概念之一。
一個 tuple 定義一個單向(unidirectional)flow。內(nèi)核代碼中有如下注釋:
“//include/net/netfilter/nf_conntrack_tuple.h
A
”tupleis a structure containing the information to uniquely identify a connection. ie. if two packets have the same tuple, they are in the same connection; if not, they are not.
結(jié)構(gòu)體定義
//include/net/netfilter/nf_conntrack_tuple.h
//?為方便?NAT?的實現(xiàn),內(nèi)核將?tuple?結(jié)構(gòu)體拆分為?"manipulatable"?和?"non-manipulatable"?兩部分
//?下面結(jié)構(gòu)體中的?_man?是?manipulatable?的縮寫
???????????????????????????????????????????????//?ude/uapi/linux/netfilter.h
???????????????????????????????????????????????union?nf_inet_addr?{
???????????????????????????????????????????????????__u32????????????all[4];
???????????????????????????????????????????????????__be32???????????ip;
???????????????????????????????????????????????????__be32???????????ip6[4];
???????????????????????????????????????????????????struct?in_addr???in;
???????????????????????????????????????????????????struct?in6_addr??in6;
/*?manipulable?part?of?the?tuple?*/?????????/??};
struct?nf_conntrack_man?{??????????????????/
????union?nf_inet_addr???????????u3;?-->--/
????union?nf_conntrack_man_proto?u;??-->--\
???????????????????????????????????????????\???//?include/uapi/linux/netfilter/nf_conntrack_tuple_common.h
????u_int16_t?l3num;?//?L3?proto????????????\??//?協(xié)議相關(guān)的部分
};????????????????????????????????????????????union?nf_conntrack_man_proto?{
??????????????????????????????????????????????????__be16?all;/*?Add?other?protocols?here.?*/
??????????????????????????????????????????????????struct?{?__be16?port;?}?tcp;
??????????????????????????????????????????????????struct?{?__be16?port;?}?udp;
??????????????????????????????????????????????????struct?{?__be16?id;???}?icmp;
??????????????????????????????????????????????????struct?{?__be16?port;?}?dccp;
??????????????????????????????????????????????????struct?{?__be16?port;?}?sctp;
??????????????????????????????????????????????????struct?{?__be16?key;??}?gre;
??????????????????????????????????????????????};
struct?nf_conntrack_tuple?{?/*?This?contains?the?information?to?distinguish?a?connection.?*/
????struct?nf_conntrack_man?src;??//?源地址信息,manipulable?part
????struct?{
????????union?nf_inet_addr?u3;
????????union?{
????????????__be16?all;?/*?Add?other?protocols?here.?*/
????????????struct?{?__be16?port;?????????}?tcp;
????????????struct?{?__be16?port;?????????}?udp;
????????????struct?{?u_int8_t?type,?code;?}?icmp;
????????????struct?{?__be16?port;?????????}?dccp;
????????????struct?{?__be16?port;?????????}?sctp;
????????????struct?{?__be16?key;??????????}?gre;
????????}?u;
????????u_int8_t?protonum;?/*?The?protocol.?*/
????????u_int8_t?dir;??????/*?The?direction?(for?tuplehash)?*/
????}?dst;???????????????????????//?目的地址信息
};
Tuple 結(jié)構(gòu)體中只有兩個字段 src 和 dst,分別保存源和目的信息。src 和 dst 自身也是結(jié)構(gòu)體,能保存不同類型協(xié)議的數(shù)據(jù)。以 IPv4 UDP 為例,五元組分別保存在如下字段:
dst.protonum:協(xié)議類型src.u3.ip:源 IP 地址dst.u3.ip:目的 IP 地址src.u.udp.port:源端口號dst.u.udp.port:目的端口號
CT 支持的協(xié)議
從以上定義可以看到,連接跟蹤模塊目前只支持以下六種協(xié)議:TCP、UDP、ICMP、DCCP、SCTP、GRE。
注意其中的 ICMP 協(xié)議。大家可能會認為,連接跟蹤模塊依據(jù)包的三層和四層信息做 哈希,而 ICMP 是三層協(xié)議,沒有四層信息,因此 ICMP 肯定不會被 CT 記錄。但實際上 是會的,上面代碼可以看到,ICMP 使用了其頭信息中的 ICMP type和 code 字段來 定義 tuple。
3.3 struct nf_conntrack_l4proto {}:協(xié)議需要實現(xiàn)的方法集合
支持連接跟蹤的協(xié)議都需要實現(xiàn) struct nf_conntrack_l4proto {} 結(jié)構(gòu)體 中定義的方法,例如 pkt_to_tuple()。
//?include/net/netfilter/nf_conntrack_l4proto.h
struct?nf_conntrack_l4proto?{
????u_int16_t?l3proto;?/*?L3?Protocol?number.?*/
????u_int8_t??l4proto;?/*?L4?Protocol?number.?*/
????//?從包(skb)中提取?tuple
????bool?(*pkt_to_tuple)(struct?sk_buff?*skb,?...?struct?nf_conntrack_tuple?*tuple);
????//?對包進行判決,返回判決結(jié)果(returns?verdict?for?packet)
????int?(*packet)(struct?nf_conn?*ct,?const?struct?sk_buff?*skb?...);
????//?創(chuàng)建一個新連接。如果成功返回 TRUE;如果返回的是 TRUE,接下來會調(diào)用 packet()?方法
????bool?(*new)(struct?nf_conn?*ct,?const?struct?sk_buff?*skb,?unsigned?int?dataoff);
????//?判斷當前數(shù)據(jù)包能否被連接跟蹤。如果返回成功,接下來會調(diào)用 packet()?方法
????int?(*error)(struct?net?*net,?struct?nf_conn?*tmpl,?struct?sk_buff?*skb,?...);
????...
};
3.4 struct nf_conntrack_tuple_hash {}:哈希表項
conntrack 將活動連接的狀態(tài)存儲在一張哈希表中(key: value)。
hash_conntrack_raw() 根據(jù) tuple 計算出一個 32 位的哈希值(key):
//?net/netfilter/nf_conntrack_core.c
static?u32?hash_conntrack_raw(struct?nf_conntrack_tuple?*tuple,?struct?net?*net)
{
????get_random_once(&nf_conntrack_hash_rnd,?sizeof(nf_conntrack_hash_rnd));
????/*?The?direction?must?be?ignored,?so?we?hash?everything?up?to?the
?????*?destination?ports?(which?is?a?multiple?of?4)?and?treat?the?last?three?bytes?manually.??*/
????u32?seed?=?nf_conntrack_hash_rnd?^?net_hash_mix(net);
????unsigned?int?n?=?(sizeof(tuple->src)?+?sizeof(tuple->dst.u3))?/?sizeof(u32);
????return?jhash2((u32?*)tuple,?n,?seed?^?((tuple->dst.u.all?<16)?|?tuple->dst.protonum));
}
注意其中是如何利用 tuple 的不同字段來計算哈希的。
nf_conntrack_tuple_hash 是哈希表中的表項(value):
//?include/net/netfilter/nf_conntrack_tuple.h
//?每條連接在哈希表中都對應(yīng)兩項,分別對應(yīng)兩個方向(egress/ingress)
//?Connections?have?two?entries?in?the?hash?table:?one?for?each?way
struct?nf_conntrack_tuple_hash?{
????struct?hlist_nulls_node???hnnode;???//?指向該哈希對應(yīng)的連接?struct?nf_conn,采用?list?形式是為了解決哈希沖突
????struct?nf_conntrack_tuple?tuple;????//?N?元組,前面詳細介紹過了
};
3.5 struct nf_conn {}:連接(connection)
Netfilter 中每個 flow 都稱為一個 connection,即使是對那些非面向連接的協(xié)議(例 如 UDP)。每個 connection 用 struct nf_conn {} 表示,主要字段如下:
//?include/net/netfilter/nf_conntrack.h
??????????????????????????????????????????????????//?include/linux/skbuff.h
????????????????????????????????????????------>???struct?nf_conntrack?{
????????????????????????????????????????|???????????? atomic_t use;??//?連接引用計數(shù)?
????????????????????????????????????????|?????????};
struct?nf_conn?{????????????????????????|
????struct?nf_conntrack????????????ct_general;
????struct?nf_conntrack_tuple_hash?tuplehash[IP_CT_DIR_MAX];?//?哈希表項,數(shù)組是因為要記錄兩個方向的?flow
????unsigned?long?status;?//?連接狀態(tài),見下文
????u32?timeout;??????????//?連接狀態(tài)的定時器
????possible_net_t?ct_net;
????struct?hlist_node????nat_bysource;
????????????????????????????????????????????????????????//?per?conntrack:?protocol?private?data
????struct?nf_conn?*master;?????????????????????????????union?nf_conntrack_proto?{
????????????????????????????????????????????????????????????/*?insert?conntrack?proto?private?data?here?*/
????u_int32_t?mark;????/*?對?skb?進行特殊標記?*/????????????struct?nf_ct_dccp?dccp;
????u_int32_t?secmark;??????????????????????????????????????struct?ip_ct_sctp?sctp;
????????????????????????????????????????????????????????????struct?ip_ct_tcp?tcp;
????union?nf_conntrack_proto?proto;?---------->----->???????struct?nf_ct_gre?gre;
};??????????????????????????????????????????????????????????unsigned?int?tmpl_padto;
????????????????????????????????????????????????????????};
連接的狀態(tài)集合 enum ip_conntrack_status:
//?include/uapi/linux/netfilter/nf_conntrack_common.h
enum?ip_conntrack_status?{
????IPS_EXPECTED??????=?(1?<????IPS_SEEN_REPLY????=?(1?<????IPS_ASSURED???????=?(1?<????IPS_CONFIRMED?????=?(1?<????IPS_SRC_NAT???????=?(1?<????IPS_DST_NAT???????=?(1?<????IPS_NAT_MASK??????=?(IPS_DST_NAT?|?IPS_SRC_NAT),
????IPS_SEQ_ADJUST????=?(1?<????IPS_SRC_NAT_DONE??=?(1?<????IPS_DST_NAT_DONE??=?(1?<????IPS_NAT_DONE_MASK?=?(IPS_DST_NAT_DONE?|?IPS_SRC_NAT_DONE),
????IPS_DYING?????????=?(1?<????IPS_FIXED_TIMEOUT?=?(1?<????IPS_TEMPLATE??????=?(1?<????IPS_UNTRACKED?????=?(1?<????IPS_HELPER????????=?(1?<????IPS_OFFLOAD???????=?(1?<
????IPS_UNCHANGEABLE_MASK?=?(IPS_NAT_DONE_MASK?|?IPS_NAT_MASK?|
?????????????????IPS_EXPECTED?|?IPS_CONFIRMED?|?IPS_DYING?|
?????????????????IPS_SEQ_ADJUST?|?IPS_TEMPLATE?|?IPS_OFFLOAD),
};
3.6 nf_conntrack_in():進入連接跟蹤
如上圖所示,Netfilter 在四個 Hook 點對包進行跟蹤:
PRE_ROUTING和LOCAL_OUT:調(diào)用nf_conntrack_in()開始連接跟蹤,正常情況 下會創(chuàng)建一條新連接記錄,然后將 conntrack entry 放到 unconfirmed list。為什么是這兩個 hook 點呢?因為它們都是新連接的第一個包最先達到的地方,
PRE_ROUTING是外部主動和本機建連時包最先到達的地方LOCAL_OUT是本機主動和外部建連時包最先到達的地方POST_ROUTING和LOCAL_IN:調(diào)用nf_conntrack_confirm()將nf_conntrack_in()創(chuàng)建的連接移到 confirmed list。同樣要問,為什么在這兩個 hook 點呢?因為如果新連接的第一個包沒有被丟棄,那這 是它們離開 netfilter 之前的最后 hook 點:
外部主動和本機建連的包,如果在中間處理中沒有被丟棄, LOCAL_IN是其被送到應(yīng)用(例如 nginx 服務(wù))之前的最后 hook 點本機主動和外部建連的包,如果在中間處理中沒有被丟棄, POST_ROUTING是其離開主機時的最后 hook 點
下面的代碼可以看到這些 handler 是如何注冊的:
//?net/netfilter/nf_conntrack_proto.c
/*?Connection?tracking?may?drop?packets,?but?never?alters?them,?so?make?it?the?first?hook.??*/
static?const?struct?nf_hook_ops?ipv4_conntrack_ops[]?=?{
?{
??.hook??=?ipv4_conntrack_in,???????//?調(diào)用?nf_conntrack_in()?進入連接跟蹤
??.pf??=?NFPROTO_IPV4,
??.hooknum?=?NF_INET_PRE_ROUTING,?????//?PRE_ROUTING?hook?點
??.priority?=?NF_IP_PRI_CONNTRACK,
?},
?{
??.hook??=?ipv4_conntrack_local,????//?調(diào)用?nf_conntrack_in()?進入連接跟蹤
??.pf??=?NFPROTO_IPV4,
??.hooknum?=?NF_INET_LOCAL_OUT,???????//?LOCAL_OUT?hook?點
??.priority?=?NF_IP_PRI_CONNTRACK,
?},
?{
??.hook??=?ipv4_confirm,????????????//?調(diào)用?nf_conntrack_confirm()
??.pf??=?NFPROTO_IPV4,
??.hooknum?=?NF_INET_POST_ROUTING,????//?POST_ROUTING?hook?點
??.priority?=?NF_IP_PRI_CONNTRACK_CONFIRM,
?},
?{
??.hook??=?ipv4_confirm,????????????//?調(diào)用?nf_conntrack_confirm()
??.pf??=?NFPROTO_IPV4,
??.hooknum?=?NF_INET_LOCAL_IN,????????//?LOCAL_IN?hook?點
??.priority?=?NF_IP_PRI_CONNTRACK_CONFIRM,
?},
};
nf_conntrack_in 函數(shù)是連接跟蹤模塊的核心。
//?net/netfilter/nf_conntrack_core.c
unsigned?int
nf_conntrack_in(struct?net?*net,?u_int8_t?pf,?unsigned?int?hooknum,?struct?sk_buff?*skb)
{
??struct?nf_conn?*tmpl?=?nf_ct_get(skb,?&ctinfo);?//?獲取?skb?對應(yīng)的?conntrack_info?和連接記錄
??if?(tmpl?||?ctinfo?==?IP_CT_UNTRACKED)?{????????//?如果記錄存在,或者是不需要跟蹤的類型
??????if?((tmpl?&&?!nf_ct_is_template(tmpl))?||?ctinfo?==?IP_CT_UNTRACKED)?{
??????????NF_CT_STAT_INC_ATOMIC(net,?ignore);?????//?無需跟蹤的類型,增加?ignore?計數(shù)
??????????return?NF_ACCEPT;???????????????????????//?返回?NF_ACCEPT,繼續(xù)后面的處理
??????}
??????skb->_nfct?=?0;?????????????????????????????//?不屬于?ignore?類型,計數(shù)器置零,準備后續(xù)處理
??}
??struct?nf_conntrack_l4proto?*l4proto?=?__nf_ct_l4proto_find(...);????//?提取協(xié)議相關(guān)的?L4?頭信息
??if?(l4proto->error?!=?NULL)?{???????????????????//?skb?的完整性和合法性驗證
??????if?(l4proto->error(net,?tmpl,?skb,?dataoff,?pf,?hooknum)?<=?0)?{
??????????NF_CT_STAT_INC_ATOMIC(net,?error);
??????????NF_CT_STAT_INC_ATOMIC(net,?invalid);
??????????goto?out;
??????}
??}
repeat:
??//?開始連接跟蹤:提取 tuple;創(chuàng)建新連接記錄,或者更新已有連接的狀態(tài)
??resolve_normal_ct(net,?tmpl,?skb,?...?l4proto);
??l4proto->packet(ct,?skb,?dataoff,?ctinfo);?//?進行一些協(xié)議相關(guān)的處理,例如?UDP?會更新?timeout
??if?(ctinfo?==?IP_CT_ESTABLISHED_REPLY?&&?!test_and_set_bit(IPS_SEEN_REPLY_BIT,?&ct->status))
??????nf_conntrack_event_cache(IPCT_REPLY,?ct);
out:
??if?(tmpl)
??????nf_ct_put(tmpl);?//?解除對連接記錄?tmpl?的引用
}
大致流程:
嘗試獲取這個 skb 對應(yīng)的連接跟蹤記錄 判斷是否需要對這個包做連接跟蹤,如果不需要,更新 ignore 計數(shù),返回 NF_ACCEPT;如果需要,就初始化這個 skb 的引用計數(shù)。從包的 L4 header 中提取信息,初始化協(xié)議相關(guān)的 struct nf_conntrack_l4proto {}變量,其中包含了該協(xié)議的連接跟蹤相關(guān)的回調(diào)方法。調(diào)用該協(xié)議的 error()方法檢查包的完整性、校驗和等信息。調(diào)用 resolve_normal_ct()開始連接跟蹤,它會創(chuàng)建新 tuple,新 conntrack entry,或者更新已有連接的狀態(tài)。調(diào)用該協(xié)議的 packet()方法進行一些協(xié)議相關(guān)的處理,例如對于 UDP,如果 status bit 里面設(shè)置了IPS_SEEN_REPLY位,就會更新 timeout。timeout 大小和協(xié) 議相關(guān),越小越越可以防止 DoS 攻擊(DoS 的基本原理就是將機器的可用連接耗盡)
3.7 init_conntrack():創(chuàng)建新連接記錄
如果連接不存在(flow 的第一個包),resolve_normal_ct() 會調(diào)用 init_conntrack ,后者進而會調(diào)用 new() 方法創(chuàng)建一個新的 conntrack entry。
//?include/net/netfilter/nf_conntrack_core.c
//?Allocate?a?new?conntrack
static?noinline?struct?nf_conntrack_tuple_hash?*
init_conntrack(struct?net?*net,?struct?nf_conn?*tmpl,
????????const?struct?nf_conntrack_tuple?*tuple,
????????const?struct?nf_conntrack_l4proto?*l4proto,
????????struct?sk_buff?*skb,?unsigned?int?dataoff,?u32?hash)
{
?struct?nf_conn?*ct;
?ct?=?__nf_conntrack_alloc(net,?zone,?tuple,?&repl_tuple,?GFP_ATOMIC,?hash);
?l4proto->new(ct,?skb,?dataoff);?//?協(xié)議相關(guān)的方法
?local_bh_disable();?????????????//?關(guān)閉軟中斷
?if?(net->ct.expect_count)?{
??exp?=?nf_ct_find_expectation(net,?zone,?tuple);
??if?(exp)?{
???/*?Welcome,?Mr.?Bond.??We've?been?expecting?you...?*/
???__set_bit(IPS_EXPECTED_BIT,?&ct->status);
???/*?exp->master?safe,?refcnt?bumped?in?nf_ct_find_expectation?*/
???ct->master?=?exp->master;
???ct->mark?=?exp->master->mark;
???ct->secmark?=?exp->master->secmark;
???NF_CT_STAT_INC(net,?expect_new);
??}
?}
?/*?Now?it?is?inserted?into?the?unconfirmed?list,?bump?refcount?*/
?nf_conntrack_get(&ct->ct_general);
?nf_ct_add_to_unconfirmed_list(ct);
?local_bh_enable();??????????????//?重新打開軟中斷
?if?(exp)?{
??if?(exp->expectfn)
???exp->expectfn(ct,?exp);
??nf_ct_expect_put(exp);
?}
?return?&ct->tuplehash[IP_CT_DIR_ORIGINAL];
}
每種協(xié)議需要實現(xiàn)自己的 l4proto->new() 方法,代碼見:net/netfilter/nf_conntrack_proto_*.c。
如果當前包會影響后面包的狀態(tài)判斷,init_conntrack() 會設(shè)置 struct nf_conn 的 master 字段。面向連接的協(xié)議會用到這個特性,例如 TCP。
3.8 nf_conntrack_confirm():確認包沒有被丟棄
nf_conntrack_in() 創(chuàng)建的新 conntrack entry 會插入到一個 未確認連接( unconfirmed connection)列表。
如果這個包之后沒有被丟棄,那它在經(jīng)過 POST_ROUTING 時會被 nf_conntrack_confirm() 方法處理,原理我們在分析過了 3.6 節(jié)的開頭分析過了。nf_conntrack_confirm() 完成之后,狀態(tài)就變?yōu)榱?IPS_CONFIRMED,并且連接記錄從 未確認列表移到正常的列表。
之所以要將創(chuàng)建一個合法的新 entry 的過程分為創(chuàng)建(new)和確認(confirm)兩個階段 ,是因為包在經(jīng)過 nf_conntrack_in() 之后,到達 nf_conntrack_confirm() 之前 ,可能會被內(nèi)核丟棄。這樣會導(dǎo)致系統(tǒng)殘留大量的半連接狀態(tài)記錄,在性能和安全性上都 是很大問題。分為兩步之后,可以加快半連接狀態(tài) conntrack entry 的 GC。
//?include/net/netfilter/nf_conntrack_core.h
/*?Confirm?a?connection:?returns?NF_DROP?if?packet?must?be?dropped.?*/
static?inline?int?nf_conntrack_confirm(struct?sk_buff?*skb)
{
?struct?nf_conn?*ct?=?(struct?nf_conn?*)skb_nfct(skb);
?int?ret?=?NF_ACCEPT;
?if?(ct)?{
??if?(!nf_ct_is_confirmed(ct))
???ret?=?__nf_conntrack_confirm(skb);
??if?(likely(ret?==?NF_ACCEPT))
???nf_ct_deliver_cached_events(ct);
?}
?return?ret;
}
confirm 邏輯,省略了各種錯誤處理邏輯:
//?net/netfilter/nf_conntrack_core.c
/*?Confirm?a?connection?given?skb;?places?it?in?hash?table?*/
int
__nf_conntrack_confirm(struct?sk_buff?*skb)
{
?struct?nf_conn?*ct;
?ct?=?nf_ct_get(skb,?&ctinfo);
?local_bh_disable();???????????????//?關(guān)閉軟中斷
?hash?=?*(unsigned?long?*)&ct->tuplehash[IP_CT_DIR_REPLY].hnnode.pprev;
?reply_hash?=?hash_conntrack(net,?&ct->tuplehash[IP_CT_DIR_REPLY].tuple);
?ct->timeout?+=?nfct_time_stamp;???//?更新連接超時時間,超時后會被?GC
?atomic_inc(&ct->ct_general.use);??//?設(shè)置連接引用計數(shù)?
?ct->status?|=?IPS_CONFIRMED;??????//?設(shè)置連接狀態(tài)為?confirmed
?__nf_conntrack_hash_insert(ct,?hash,?reply_hash);??//?插入到連接跟蹤哈希表
?local_bh_enable();????????????????//?重新打開軟中斷
?nf_conntrack_event_cache(master_ct(ct)???IPCT_RELATED?:?IPCT_NEW,?ct);
?return?NF_ACCEPT;
}
可以看到,連接跟蹤的處理邏輯中需要頻繁關(guān)閉和打開軟中斷,此外還有各種鎖, 這是短連高并發(fā)場景下連接跟蹤性能損耗的主要原因?。
4 Netfilter NAT 實現(xiàn)
NAT 是與連接跟蹤獨立的模塊。
4.1 重要數(shù)據(jù)結(jié)構(gòu)和函數(shù)
重要數(shù)據(jù)結(jié)構(gòu):
支持 NAT 的協(xié)議需要實現(xiàn)其中的方法:
struct nf_nat_l3proto {}struct nf_nat_l4proto {}
重要函數(shù):
nf_nat_inet_fn():NAT 的核心函數(shù)是,在除 NF_INET_FORWARD 之外的其他 hook 點都會被調(diào)用。
4.2 NAT 模塊初始化
//?net/netfilter/nf_nat_core.c
static?struct?nf_nat_hook?nat_hook?=?{
?.parse_nat_setup?=?nfnetlink_parse_nat_setup,
?.decode_session??=?__nf_nat_decode_session,
?.manip_pkt??=?nf_nat_manip_pkt,
};
static?int?__init?nf_nat_init(void)
{
?nf_nat_bysource?=?nf_ct_alloc_hashtable(&nf_nat_htable_size,?0);
?nf_ct_helper_expectfn_register(&follow_master_nat);
?RCU_INIT_POINTER(nf_nat_hook,?&nat_hook);
}
MODULE_LICENSE("GPL");
module_init(nf_nat_init);
4.3 struct nf_nat_l3proto {}:協(xié)議相關(guān)的 NAT 方法集
//?include/net/netfilter/nf_nat_l3proto.h
struct?nf_nat_l3proto?{
????u8????l3proto;?//?例如,AF_INET
????u32?????(*secure_port????)(const?struct?nf_conntrack_tuple?*t,?__be16);
????bool????(*manip_pkt??????)(struct?sk_buff?*skb,?...);
????void????(*csum_update????)(struct?sk_buff?*skb,?...);
????void????(*csum_recalc????)(struct?sk_buff?*skb,?u8?proto,?...);
????void????(*decode_session?)(struct?sk_buff?*skb,?...);
????int?????(*nlattr_to_range)(struct?nlattr?*tb[],?struct?nf_nat_range2?*range);
};
4.4 struct nf_nat_l4proto {}:協(xié)議相關(guān)的 NAT 方法集
//?include/net/netfilter/nf_nat_l4proto.h
struct?nf_nat_l4proto?{
????u8?l4proto;?//?Protocol?number,例如?IPPROTO_UDP,?IPPROTO_TCP
????//?根據(jù)傳入的?tuple?和?NAT?類型(SNAT/DNAT)修改包的?L3/L4?頭
????bool?(*manip_pkt)(struct?sk_buff?*skb,?*l3proto,?*tuple,?maniptype);
????//?創(chuàng)建一個唯一的?tuple
????//?例如對于?UDP,會根據(jù)?src_ip,?dst_ip,?src_port?加一個隨機數(shù)生成一個?16bit?的?dst_port
????void?(*unique_tuple)(*l3proto,?tuple,?struct?nf_nat_range2?*range,?maniptype,?struct?nf_conn?*ct);
????//?If?the?address?range?is?exhausted?the?NAT?modules?will?begin?to?drop?packets.
????int?(*nlattr_to_range)(struct?nlattr?*tb[],?struct?nf_nat_range2?*range);
};
各協(xié)議實現(xiàn)的方法,見:net/netfilter/nf_nat_proto_*.c。例如 TCP 的實現(xiàn):
//?net/netfilter/nf_nat_proto_tcp.c
const?struct?nf_nat_l4proto?nf_nat_l4proto_tcp?=?{
?.l4proto??=?IPPROTO_TCP,
?.manip_pkt??=?tcp_manip_pkt,
?.in_range??=?nf_nat_l4proto_in_range,
?.unique_tuple??=?tcp_unique_tuple,
?.nlattr_to_range?=?nf_nat_l4proto_nlattr_to_range,
};
4.5 nf_nat_inet_fn():進入 NAT
NAT 的核心函數(shù)是 nf_nat_inet_fn(),它會在以下 hook 點被調(diào)用:
NF_INET_PRE_ROUTINGNF_INET_POST_ROUTINGNF_INET_LOCAL_OUTNF_INET_LOCAL_IN
也就是除了 NF_INET_FORWARD 之外其他 hook 點都會被調(diào)用。
在這些 hook 點的優(yōu)先級:Conntrack > NAT > Packet Filtering。連接跟蹤的優(yōu)先 級高于 NAT 是因為 NAT 依賴連接跟蹤的結(jié)果。
unsigned?int
nf_nat_inet_fn(void?*priv,?struct?sk_buff?*skb,?const?struct?nf_hook_state?*state)
{
????ct?=?nf_ct_get(skb,?&ctinfo);
????if?(!ct)????//?conntrack?不存在就做不了?NAT,直接返回,這也是為什么說?NAT?依賴?conntrack?的結(jié)果
????????return?NF_ACCEPT;
????nat?=?nfct_nat(ct);
????switch?(ctinfo)?{
????case?IP_CT_RELATED:
????case?IP_CT_RELATED_REPLY:?/*?Only?ICMPs?can?be?IP_CT_IS_REPLY.??Fallthrough?*/
????case?IP_CT_NEW:?/*?Seen?it?before??This?can?happen?for?loopback,?retrans,?or?local?packets.?*/
????????if?(!nf_nat_initialized(ct,?maniptype))?{
????????????struct?nf_hook_entries?*e?=?rcu_dereference(lpriv->entries);?//?獲取所有?NAT?規(guī)則
????????????if?(!e)
????????????????goto?null_bind;
????????????for?(i?=?0;?i?num_hook_entries;?i++)?{?//?依次執(zhí)行?NAT?規(guī)則
????????????????if?(e->hooks[i].hook(e->hooks[i].priv,?skb,?state)?!=?NF_ACCEPT?)
????????????????????return?ret;?????????????????????????//?任何規(guī)則返回非?NF_ACCEPT,就停止當前處理
????????????????if?(nf_nat_initialized(ct,?maniptype))
????????????????????goto?do_nat;
????????????}
null_bind:
????????????nf_nat_alloc_null_binding(ct,?state->hook);
????????}?else?{?//?Already?setup?manip
????????????if?(nf_nat_oif_changed(state->hook,?ctinfo,?nat,?state->out))
????????????????goto?oif_changed;
????????}
????????break;
????default:?/*?ESTABLISHED?*/
????????if?(nf_nat_oif_changed(state->hook,?ctinfo,?nat,?state->out))
????????????goto?oif_changed;
????}
do_nat:
????return?nf_nat_packet(ct,?ctinfo,?state->hook,?skb);
oif_changed:
????nf_ct_kill_acct(ct,?ctinfo,?skb);
????return?NF_DROP;
}
首先查詢 conntrack 記錄,如果不存在,就意味著無法跟蹤這個連接,那就更不可能做 NAT 了,因此直接返回。
如果找到了 conntrack 記錄,并且是 IP_CT_RELATED、IP_CT_RELATED_REPLY 或 IP_CT_NEW 狀態(tài),就去獲取 NAT 規(guī)則。如果沒有規(guī)則,直接返回 NF_ACCEPT,對包不 做任何改動;如果有規(guī)則,最后執(zhí)行 nf_nat_packet,這個函數(shù)會進一步調(diào)用 manip_pkt 完成對包的修改,如果失敗,包將被丟棄。
Masquerade
NAT 模塊一般配置方式:Change IP1 to IP2 if matching XXX。
此次還支持一種更靈活的 NAT 配置,稱為 Masquerade:Change IP1 to dev1's IP if matching XXX。與前面的區(qū)別在于,當設(shè)備(網(wǎng)卡)的 IP 地址發(fā)生變化時,這種方式無 需做任何修改。缺點是性能比第一種方式要差。
4.6 nf_nat_packet():執(zhí)行 NAT
//?net/netfilter/nf_nat_core.c
/*?Do?packet?manipulations?according?to?nf_nat_setup_info.?*/
unsigned?int?nf_nat_packet(struct?nf_conn?*ct,?enum?ip_conntrack_info?ctinfo,
??????unsigned?int?hooknum,?struct?sk_buff?*skb)
{
?enum?nf_nat_manip_type?mtype?=?HOOK2MANIP(hooknum);
?enum?ip_conntrack_dir?dir?=?CTINFO2DIR(ctinfo);
?unsigned?int?verdict?=?NF_ACCEPT;
?statusbit?=?(mtype?==?NF_NAT_MANIP_SRC??IPS_SRC_NAT?:?IPS_DST_NAT)
?if?(dir?==?IP_CT_DIR_REPLY)?????//?Invert?if?this?is?reply?dir
??statusbit?^=?IPS_NAT_MASK;
?if?(ct->status?&?statusbit)?????//?Non-atomic:?these?bits?don't?change.?*/
??verdict?=?nf_nat_manip_pkt(skb,?ct,?mtype,?dir);
?return?verdict;
}
static?unsigned?int?nf_nat_manip_pkt(struct?sk_buff?*skb,?struct?nf_conn?*ct,
?????????enum?nf_nat_manip_type?mtype,?enum?ip_conntrack_dir?dir)
{
?struct?nf_conntrack_tuple?target;
?/*?We?are?aiming?to?look?like?inverse?of?other?direction.?*/
?nf_ct_invert_tuplepr(&target,?&ct->tuplehash[!dir].tuple);
?l3proto?=?__nf_nat_l3proto_find(target.src.l3num);
?l4proto?=?__nf_nat_l4proto_find(target.src.l3num,?target.dst.protonum);
?if?(!l3proto->manip_pkt(skb,?0,?l4proto,?&target,?mtype))?//?協(xié)議相關(guān)處理
??return?NF_DROP;
?return?NF_ACCEPT;
}
5. 總結(jié)
連接跟蹤是一個非常基礎(chǔ)且重要的網(wǎng)絡(luò)模塊,但只有在少數(shù)場景下才會引起普通開發(fā)者的注意。
例如,L4LB 短時高并發(fā)場景下,LB 節(jié)點每秒接受大量并發(fā)短連接,可能導(dǎo)致 conntrack table 被打爆。此時的現(xiàn)象是:
客戶端和 L4LB 建連失敗,失敗可能是隨機的,也可能是集中在某些時間點。 客戶端重試可能會成功,也可能會失敗。 在 L4LB 節(jié)點抓包看,客戶端過來的 TCP SYNC 包 L4LB 收到了,但沒有回 ACK。即,包 被靜默丟棄了(silently dropped)。
此時的原因可能是 conntrack table 太小,也可能是 GC 不夠及 時,甚至是 GC 有bug。
“原文鏈接:http://arthurchiao.art/blog/conntrack-design-and-implementation-zh/
”
?點擊屏末?|?閱讀原文?|?即刻學(xué)習(xí)







