連接跟蹤(conntrack):原理、應用及 Linux 內核實現(xiàn)


鏈接:http://arthurchiao.art/blog/conntrack-design-and-implementation-zh/
本文介紹連接跟蹤(connection tracking,conntrack,CT)的原理,應用,及其在 Linux 內核中的實現(xiàn)。
代碼分析基于內核 4.19。為使行文簡潔,所貼代碼只保留了核心邏輯,但都給出了代碼 所在的源文件,如有需要請查閱。
水平有限,文中不免有錯誤之處,歡迎指正交流。
1 引言
連接跟蹤是許多網絡應用的基礎。例如,Kubernetes Service、ServiceMesh sidecar、 軟件四層負載均衡器 LVS/IPVS、Docker network、OVS、iptables 主機防火墻等等,都依賴 連接跟蹤功能。
1.1 概念
連接跟蹤(conntrack)
連接跟蹤,顧名思義,就是跟蹤(并記錄)連接的狀態(tài)。
例如,圖 1.1 是一臺 IP 地址為 10.1.1.2 的 Linux 機器,我們能看到這臺機器上有三條 連接:
機器訪問外部 HTTP 服務的連接(目的端口 80) 外部訪問機器內 FTP 服務的連接(目的端口 21) 機器訪問外部 DNS 服務的連接(目的端口 53)
連接跟蹤所做的事情就是發(fā)現(xiàn)并跟蹤這些連接的狀態(tài),具體包括:
從數(shù)據(jù)包中提取元組(tuple)信息,辨別數(shù)據(jù)流(flow)和對應的連接(connection) 為所有連接維護一個狀態(tài)數(shù)據(jù)庫(conntrack table),例如連接的創(chuàng)建時間、發(fā)送 包數(shù)、發(fā)送字節(jié)數(shù)等等 回收過期的連接(GC) 為更上層的功能(例如 NAT)提供服務
需要注意的是,連接跟蹤中所說的“連接”,概念和 TCP/IP 協(xié)議中“面向連接”( connection oriented)的“連接”并不完全相同,簡單來說:
TCP/IP 協(xié)議中,連接是一個四層(Layer 4)的概念。 TCP 是有連接的,或稱面向連接的(connection oriented),發(fā)送出去的包都要求對端應答(ACK),并且有重傳機制 UDP 是無連接的,發(fā)送的包無需對端應答,也沒有重傳機制 CT 中,一個元組(tuple)定義的一條數(shù)據(jù)流(flow )就表示一條連接(connection)。 后面會看到 UDP 甚至是 ICMP 這種三層協(xié)議在 CT 中也都是有連接記錄的 但不是所有協(xié)議都會被連接跟蹤
本文中用到“連接”一詞時,大部分情況下指的都是后者,即“連接跟蹤”中的“連接”。
網絡地址轉換(NAT)
網絡地址轉換(NAT),意思也比較清楚:對(數(shù)據(jù)包的)網絡地址(IP + Port)進行轉換。
例如,圖 1.2 中,機器自己的 IP 10.1.1.2 是能與外部正常通信的,但 192.168 網段是私有 IP 段,外界無法訪問,也就是說源 IP 地址是 192.168 的包,其應答包是無 法回來的。
因此當源地址為 192.168 網段的包要出去時,機器會先將源 IP 換成機器自己的 10.1.1.2 再發(fā)送出去;收到應答包時,再進行相反的轉換。這就是 NAT 的基本過程。
Docker 默認的 bridge 網絡模式就是這個原理 [4]。每個容器會分一個私有網段的 IP 地址,這個 IP 地址可以在宿主機內的不同容器之間通信,但容器流量出宿主機時要進行 NAT。
NAT 又可以細分為幾類:
SNAT:對源地址(source)進行轉換 DNAT:對目的地址(destination)進行轉換 Full NAT:同時對源地址和目的地址進行轉換
以上場景屬于 SNAT,將不同私有 IP 都映射成同一個“公有 IP”,以使其能訪問外部網絡服 務。這種場景也屬于正向代理。
NAT 依賴連接跟蹤的結果。連接跟蹤最重要的使用場景就是 NAT。
四層負載均衡(L4 LB)
再將范圍稍微延伸一點,討論一下 NAT 模式的四層負載均衡。
四層負載均衡是根據(jù)包的四層信息(例如 src/dst ip, src/dst port, proto)做流量分發(fā)。
VIP(Virtual IP)是四層負載均衡的一種實現(xiàn)方式:
多個后端真實 IP(Real IP)掛到同一個虛擬 IP(VIP)上 客戶端過來的流量先到達 VIP,再經負載均衡算法轉發(fā)給某個特定的后端 IP
如果在 VIP 和 Real IP 節(jié)點之間使用的 NAT 技術(也可以使用其他技術),那客戶端訪 問服務端時,L4LB 節(jié)點將做雙向 NAT(Full NAT),數(shù)據(jù)流如圖 1.3。
1.2 原理
了解以上概念之后,我們來思考下連接跟蹤的技術原理。
要跟蹤一臺機器的所有連接狀態(tài),就需要
攔截(或稱過濾)流經這臺機器的每一個數(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 設計:Netfilter
Linux 的連接跟蹤是在 Netfilter 中實現(xiàn)的。
Netfilter 是 Linux 內核中一個對數(shù)據(jù) 包進行控制、修改和過濾(manipulation and filtering)的框架。它在內核協(xié)議 棧中設置了若干hook 點,以此對數(shù)據(jù)包進行攔截、過濾或其他處理。
“說地更直白一些,hook 機制就是在數(shù)據(jù)包的必經之路上設置若干檢測點,所有到達這 些檢測點的包都必須接受檢測,根據(jù)檢測的結果決定:
放行:不對包進行任何修改,退出檢測邏輯,繼續(xù)后面正常的包處理 修改:例如修改 IP 地址進行 NAT,然后將包放回正常的包處理邏輯 丟棄:安全策略或防火墻功能 連接跟蹤模塊只是完成連接信息的采集和錄入功能,并不會修改或丟棄數(shù)據(jù)包,后者是其 他模塊(例如 NAT)基于 Netfilter hook 完成的。
”
Netfilter 是最古老的內核框架之一,1998 年開始開發(fā),2000 年合并到 2.4.x 內 核主線版本 [5]。
1.4 設計:進一步思考
現(xiàn)在提到連接跟蹤(conntrack),可能首先都會想到 Netfilter。但由 1.2 節(jié)的討論可知, 連接跟蹤概念是獨立于 Netfilter 的,Netfilter 只是 Linux 內核中的一種連接跟蹤實現(xiàn)。
換句話說,只要具備了 hook 能力,能攔截到進出主機的每個包,完全可以在此基礎上自 己實現(xiàn)一套連接跟蹤。
云原生網絡方案 Cilium 在 1.7.4+ 版本就實現(xiàn)了這樣一套獨立的連接跟蹤和 NAT 機制 (完備功能需要 Kernel 4.19+)。其基本原理是:
基于 BPF hook 實現(xiàn)數(shù)據(jù)包的攔截功能(等價于 netfilter 里面的 hook 機制) 在 BPF hook 的基礎上,實現(xiàn)一套全新的 conntrack 和 NAT
因此,即便卸載掉 Netfilter ,也不會影響 Cilium 對 Kubernetes ClusterIP、NodePort、ExternalIPs 和 LoadBalancer 等功能的支持 [2]。
由于這套連接跟蹤機制是獨立于 Netfilter 的,因此它的 conntrack 和 NAT 信息也沒有 存儲在內核的(也就是 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 里相應的 entry 回收掉,而非為 NAT 做單獨的 GC。
以上是理論篇,接下來看一下內核實現(xiàn)。
2 Netfilter hook 機制實現(xiàn)
Netfilter 由幾個模塊構成,其中最主要的是連接跟蹤(CT) 模塊和網絡地址轉換(NAT)模塊。
CT 模塊的主要職責是識別出可進行連接跟蹤的包。CT 模塊獨立于 NAT 模塊,但主要目的是服務于后者。
2.1 Netfilter 框架
5 個 hook 點
如上圖所示,Netfilter 在內核協(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ù)包經過 hook 點時, 就會調用相應的 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ù)對包進行判斷或處理之后,需要返回一個判斷結果,指導接下來要對這個包做什 么。可能的結果有:
//?include/uapi/linux/netfilter.h
#define?NF_DROP???0??//?已丟棄這個包
#define?NF_ACCEPT?1??//?接受這個包,繼續(xù)下一步處理
#define?NF_STOLEN?2??//?當前處理函數(shù)已經消費了這個包,后面的處理函數(shù)不用處理了
#define?NF_QUEUE??3??//?應當將包放到隊列
#define?NF_REPEAT?4??//?當前處理函數(shù)應當被再次調用
hook 優(yōu)先級
每個 hook 點可以注冊多個處理函數(shù)(handler)。在注冊時必須指定這些 handlers 的優(yōu)先級,這樣觸發(fā) hook 時能夠根據(jù)優(yōu)先級依次調用處理函數(shù)。
2.2 過濾規(guī)則的組織
iptables 是配置 Netfilter 過濾功能的用戶空間工具。為便于管理, 過濾規(guī)則按功能分為若干 table:
raw filter nat mangle
這不是本文重點。更多信息可參考 (譯) 深入理解 iptables 和 netfilter 架構
3 Netfilter conntrack 實現(xiàn)
連接跟蹤模塊用于維護可跟蹤協(xié)議(trackable protocols)的連接狀態(tài)。也就是說, 連接跟蹤針對的是特定協(xié)議的包,而不是所有協(xié)議的包。稍后會看到它支持哪些協(xié)議。
3.1 重要結構體和函數(shù)
重要結構體:
struct nf_conntrack_tuple {}: 定義一個 tuple。
struct nf_conntrack_man_proto {}:manipulable part 中協(xié)議相關的部分。struct nf_conntrack_man {}:tuple 的 manipulable part。
struct nf_conntrack_l4proto {}: 支持連接跟蹤的協(xié)議需要實現(xiàn)的方法集(以及其他協(xié)議相關字段)。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。內核代碼中有如下注釋:
“//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.
結構體定義
//include/net/netfilter/nf_conntrack_tuple.h
//?為方便?NAT?的實現(xiàn),內核將?tuple?結構體拆分為?"manipulatable"?和?"non-manipulatable"?兩部分
//?下面結構體中的?_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é)議相關的部分
};????????????????????????????????????????????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 結構體中只有兩個字段 src 和 dst,分別保存源和目的信息。src 和 dst 自身也是結構體,能保存不同類型協(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 {} 結構體 中定義的方法,例如 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);
????//?對包進行判決,返回判決結果(returns?verdict?for?packet)
????int?(*packet)(struct?nf_conn?*ct,?const?struct?sk_buff?*skb?...);
????//?創(chuàng)建一個新連接。如果成功返回 TRUE;如果返回的是 TRUE,接下來會調用 packet()?方法
????bool?(*new)(struct?nf_conn?*ct,?const?struct?sk_buff?*skb,?unsigned?int?dataoff);
????//?判斷當前數(shù)據(jù)包能否被連接跟蹤。如果返回成功,接下來會調用 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
//?每條連接在哈希表中都對應兩項,分別對應兩個方向(egress/ingress)
//?Connections?have?two?entries?in?the?hash?table:?one?for?each?way
struct?nf_conntrack_tuple_hash?{
????struct?hlist_nulls_node???hnnode;???//?指向該哈希對應的連接?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:調用nf_conntrack_in()開始連接跟蹤,正常情況 下會創(chuàng)建一條新連接記錄,然后將 conntrack entry 放到 unconfirmed list。為什么是這兩個 hook 點呢?因為它們都是新連接的第一個包最先達到的地方,
PRE_ROUTING是外部主動和本機建連時包最先到達的地方LOCAL_OUT是本機主動和外部建連時包最先到達的地方POST_ROUTING和LOCAL_IN:調用nf_conntrack_confirm()將nf_conntrack_in()創(chuàng)建的連接移到 confirmed list。同樣要問,為什么在這兩個 hook 點呢?因為如果新連接的第一個包沒有被丟棄,那這 是它們離開 netfilter 之前的最后 hook 點:
外部主動和本機建連的包,如果在中間處理中沒有被丟棄, LOCAL_IN是其被送到應用(例如 nginx 服務)之前的最后 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,???????//?調用?nf_conntrack_in()?進入連接跟蹤
??.pf??=?NFPROTO_IPV4,
??.hooknum?=?NF_INET_PRE_ROUTING,?????//?PRE_ROUTING?hook?點
??.priority?=?NF_IP_PRI_CONNTRACK,
?},
?{
??.hook??=?ipv4_conntrack_local,????//?調用?nf_conntrack_in()?進入連接跟蹤
??.pf??=?NFPROTO_IPV4,
??.hooknum?=?NF_INET_LOCAL_OUT,???????//?LOCAL_OUT?hook?點
??.priority?=?NF_IP_PRI_CONNTRACK,
?},
?{
??.hook??=?ipv4_confirm,????????????//?調用?nf_conntrack_confirm()
??.pf??=?NFPROTO_IPV4,
??.hooknum?=?NF_INET_POST_ROUTING,????//?POST_ROUTING?hook?點
??.priority?=?NF_IP_PRI_CONNTRACK_CONFIRM,
?},
?{
??.hook??=?ipv4_confirm,????????????//?調用?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?對應的?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é)議相關的?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é)議相關的處理,例如?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 對應的連接跟蹤記錄 判斷是否需要對這個包做連接跟蹤,如果不需要,更新 ignore 計數(shù),返回 NF_ACCEPT;如果需要,就初始化這個 skb 的引用計數(shù)。從包的 L4 header 中提取信息,初始化協(xié)議相關的 struct nf_conntrack_l4proto {}變量,其中包含了該協(xié)議的連接跟蹤相關的回調方法。調用該協(xié)議的 error()方法檢查包的完整性、校驗和等信息。調用 resolve_normal_ct()開始連接跟蹤,它會創(chuàng)建新 tuple,新 conntrack entry,或者更新已有連接的狀態(tài)。調用該協(xié)議的 packet()方法進行一些協(xié)議相關的處理,例如對于 UDP,如果 status bit 里面設置了IPS_SEEN_REPLY位,就會更新 timeout。timeout 大小和協(xié) 議相關,越小越越可以防止 DoS 攻擊(DoS 的基本原理就是將機器的可用連接耗盡)
3.7 init_conntrack():創(chuàng)建新連接記錄
如果連接不存在(flow 的第一個包),resolve_normal_ct() 會調用 init_conntrack ,后者進而會調用 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é)議相關的方法
?local_bh_disable();?????????????//?關閉軟中斷
?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() 會設置 struct nf_conn 的 master 字段。面向連接的協(xié)議會用到這個特性,例如 TCP。
3.8 nf_conntrack_confirm():確認包沒有被丟棄
nf_conntrack_in() 創(chuàng)建的新 conntrack entry 會插入到一個 未確認連接( unconfirmed connection)列表。
如果這個包之后沒有被丟棄,那它在經過 POST_ROUTING 時會被 nf_conntrack_confirm() 方法處理,原理我們在分析過了 3.6 節(jié)的開頭分析過了。nf_conntrack_confirm() 完成之后,狀態(tài)就變?yōu)榱?IPS_CONFIRMED,并且連接記錄從 未確認列表移到正常的列表。
之所以要將創(chuàng)建一個合法的新 entry 的過程分為創(chuàng)建(new)和確認(confirm)兩個階段 ,是因為包在經過 nf_conntrack_in() 之后,到達 nf_conntrack_confirm() 之前 ,可能會被內核丟棄。這樣會導致系統(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();???????????????//?關閉軟中斷
?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ù)?
?ct->status?|=?IPS_CONFIRMED;??????//?設置連接狀態(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;
}
可以看到,連接跟蹤的處理邏輯中需要頻繁關閉和打開軟中斷,此外還有各種鎖, 這是短連高并發(fā)場景下連接跟蹤性能損耗的主要原因?
4 Netfilter NAT 實現(xiàn)
NAT 是與連接跟蹤獨立的模塊。
4.1 重要數(shù)據(jù)結構和函數(shù)
重要數(shù)據(jù)結構:
支持 NAT 的協(xié)議需要實現(xiàn)其中的方法:
struct nf_nat_l3proto {}struct nf_nat_l4proto {}
重要函數(shù):
nf_nat_inet_fn():NAT 的核心函數(shù)是,在除 NF_INET_FORWARD 之外的其他 hook 點都會被調用。
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é)議相關的 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é)議相關的 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 點被調用:
NF_INET_PRE_ROUTINGNF_INET_POST_ROUTINGNF_INET_LOCAL_OUTNF_INET_LOCAL_IN
也就是除了 NF_INET_FORWARD 之外其他 hook 點都會被調用。
在這些 hook 點的優(yōu)先級:Conntrack > NAT > Packet Filtering。連接跟蹤的優(yōu)先 級高于 NAT 是因為 NAT 依賴連接跟蹤的結果。
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?的結果
????????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ù)會進一步調用 manip_pkt 完成對包的修改,如果失敗,包將被丟棄。
Masquerade
NAT 模塊一般配置方式:Change IP1 to IP2 if matching XXX。
此次還支持一種更靈活的 NAT 配置,稱為 Masquerade:Change IP1 to dev1's IP if matching XXX。與前面的區(qū)別在于,當設備(網卡)的 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é)議相關處理
??return?NF_DROP;
?return?NF_ACCEPT;
}
5. 總結
連接跟蹤是一個非常基礎且重要的網絡模塊,但只有在少數(shù)場景下才會引起普通開發(fā)者的注意。
例如,L4LB 短時高并發(fā)場景下,LB 節(jié)點每秒接受大量并發(fā)短連接,可能導致 conntrack table 被打爆。此時的現(xiàn)象是:
客戶端和 L4LB 建連失敗,失敗可能是隨機的,也可能是集中在某些時間點。 客戶端重試可能會成功,也可能會失敗。 在 L4LB 節(jié)點抓包看,客戶端過來的 TCP SYNC 包 L4LB 收到了,但沒有回 ACK。即,包 被靜默丟棄了(silently dropped)。
此時的原因可能是 conntrack table 太小,也可能是 GC 不夠及 時,甚至是 GC 有bug。
推薦閱讀:
5T技術資源大放送!包括但不限于:C/C++,Linux,Python,Java,PHP,人工智能,單片機,樹莓派,等等。在公眾號內回復「1024」,即可免費獲取!!








