您好,登錄后才能下訂單哦!
小編給大家分享一下Linux下如何實現連接跟蹤,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!
1 引言
連接跟蹤是許多網絡應用的基礎。例如,Kubernetes Service、ServiceMesh sidecar、 軟件四層負載均衡器 LVS/IPVS、Docker network、OVS、iptables 主機防火墻等等,都依賴 連接跟蹤功能。
1.1 概念
連接跟蹤(conntrack)
圖 1.1. 連接跟蹤及其內核位置
連接跟蹤,顧名思義,就是跟蹤(并記錄)連接的狀態。
例如,圖 1.1 是一臺 IP 地址為 10.1.1.2 的 Linux 機器,我們能看到這臺機器上有三條 連接:
鴻蒙官方戰略合作共建——HarmonyOS技術社區
機器訪問外部 HTTP 服務的連接(目的端口 80)
外部訪問機器內 FTP 服務的連接(目的端口 21)
機器訪問外部 DNS 服務的連接(目的端口 53)
連接跟蹤所做的事情就是發現并跟蹤這些連接的狀態,具體包括:
從數據包中提取元組(tuple)信息,辨別數據流(flow)和對應的連接(connection)
為所有連接維護一個狀態數據庫(conntrack table),例如連接的創建時間、發送 包數、發送字節數等等
回收過期的連接(GC)
為更上層的功能(例如 NAT)提供服務
需要注意的是,連接跟蹤中所說的“連接”,概念和 TCP/IP 協議中“面向連接”( connection oriented)的“連接”并不完全相同,簡單來說:
TCP/IP 協議中,連接是一個四層(Layer 4)的概念。
TCP 是有連接的,或稱面向連接的(connection oriented),發送出去的包都要求對端應答(ACK),并且有重傳機制
UDP 是無連接的,發送的包無需對端應答,也沒有重傳機制
CT 中,一個元組(tuple)定義的一條數據流(flow )就表示一條連接(connection)。
后面會看到 UDP 甚至是 ICMP 這種三層協議在 CT 中也都是有連接記錄的
但不是所有協議都會被連接跟蹤
本文中用到“連接”一詞時,大部分情況下指的都是后者,即“連接跟蹤”中的“連接”。
網絡地址轉換(NAT)
圖 1.2. NAT 及其內核位置
網絡地址轉換(NAT),意思也比較清楚:對(數據包的)網絡地址(IP + Port)進行轉換。
例如,圖 1.2 中,機器自己的 IP 10.1.1.2 是能與外部正常通信的,但 192.168 網段是私有 IP 段,外界無法訪問,也就是說源 IP 地址是 192.168 的包,其應答包是無 法回來的。
因此當源地址為 192.168 網段的包要出去時,機器會先將源 IP 換成機器自己的 10.1.1.2 再發送出去;收到應答包時,再進行相反的轉換。這就是 NAT 的基本過程。
Docker 默認的 bridge 網絡模式就是這個原理 [4]。每個容器會分一個私有網段的 IP 地址,這個 IP 地址可以在宿主機內的不同容器之間通信,但容器流量出宿主機時要進行 NAT。
NAT 又可以細分為幾類:
SNAT:對源地址(source)進行轉換
DNAT:對目的地址(destination)進行轉換
Full NAT:同時對源地址和目的地址進行轉換
以上場景屬于 SNAT,將不同私有 IP 都映射成同一個“公有 IP”,以使其能訪問外部網絡服 務。這種場景也屬于正向代理。
NAT 依賴連接跟蹤的結果。連接跟蹤最重要的使用場景就是 NAT。
四層負載均衡(L4 LB)
圖 1.3. L4LB: Traffic path in NAT mode [3]
再將范圍稍微延伸一點,討論一下 NAT 模式的四層負載均衡。
四層負載均衡是根據包的四層信息(例如 src/dst ip, src/dst port, proto)做流量分發。
VIP(Virtual IP)是四層負載均衡的一種實現方式:
多個后端真實 IP(Real IP)掛到同一個虛擬 IP(VIP)上
客戶端過來的流量先到達 VIP,再經負載均衡算法轉發給某個特定的后端 IP
如果在 VIP 和 Real IP 節點之間使用的 NAT 技術(也可以使用其他技術),那客戶端訪 問服務端時,L4LB 節點將做雙向 NAT(Full NAT),數據流如圖 1.3。
1.2 原理
了解以上概念之后,我們來思考下連接跟蹤的技術原理。
要跟蹤一臺機器的所有連接狀態,就需要
鴻蒙官方戰略合作共建——HarmonyOS技術社區
攔截(或稱過濾)流經這臺機器的每一個數據包,并進行分析。
根據這些信息建立起這臺機器上的連接信息數據庫(conntrack table)。
根據攔截到的包信息,不斷更新數據庫
例如,
鴻蒙官方戰略合作共建——HarmonyOS技術社區
攔截到一個 TCP SYNC 包時,說明正在嘗試建立 TCP 連接,需要創建一條新 conntrack entry 來記錄這條連接
攔截到一個屬于已有 conntrack entry 的包時,需要更新這條 conntrack entry 的收發包數等統計信息
除了以上兩點功能需求,還要考慮性能問題,因為連接跟蹤要對每個包進行過濾和分析 。性能問題非常重要,但不是本文重點,后面介紹實現時會進一步提及。
之外,這些功能最好還有配套的管理工具來更方便地使用。
1.3 設計:Netfilter
圖 1.4. Netfilter architecture inside Linux kernel
Linux 的連接跟蹤是在 Netfilter 中實現的。
Netfilter 是 Linux 內核中一個對數據 包進行控制、修改和過濾(manipulation and filtering)的框架。它在內核協議 棧中設置了若干hook 點,以此對數據包進行攔截、過濾或其他處理。
“ 說地更直白一些,hook 機制就是在數據包的必經之路上設置若干檢測點,所有到達這 些檢測點的包都必須接受檢測,根據檢測的結果決定:
鴻蒙官方戰略合作共建——HarmonyOS技術社區
放行:不對包進行任何修改,退出檢測邏輯,繼續后面正常的包處理
修改:例如修改 IP 地址進行 NAT,然后將包放回正常的包處理邏輯
丟棄:安全策略或防火墻功能
連接跟蹤模塊只是完成連接信息的采集和錄入功能,并不會修改或丟棄數據包,后者是其 他模塊(例如 NAT)基于 Netfilter hook 完成的。 ”
Netfilter 是最古老的內核框架之一,1998 年開始開發,2000 年合并到 2.4.x 內 核主線版本 [5]。
1.4 設計:進一步思考
現在提到連接跟蹤(conntrack),可能首先都會想到 Netfilter。但由 1.2 節的討論可知, 連接跟蹤概念是獨立于 Netfilter 的,Netfilter 只是 Linux 內核中的一種連接跟蹤實現。
換句話說,只要具備了 hook 能力,能攔截到進出主機的每個包,完全可以在此基礎上自 己實現一套連接跟蹤。
圖 1.5. Cilium's conntrack and NAT architectrue
云原生網絡方案 Cilium 在 1.7.4+ 版本就實現了這樣一套獨立的連接跟蹤和 NAT 機制 (完備功能需要 Kernel 4.19+)。其基本原理是:
鴻蒙官方戰略合作共建——HarmonyOS技術社區
基于 BPF hook 實現數據包的攔截功能(等價于 netfilter 里面的 hook 機制)
在 BPF hook 的基礎上,實現一套全新的 conntrack 和 NAT
因此,即便卸載掉 Netfilter ,也不會影響 Cilium 對 Kubernetes ClusterIP、NodePort、ExternalIPs 和 LoadBalancer 等功能的支持 [2]。
由于這套連接跟蹤機制是獨立于 Netfilter 的,因此它的 conntrack 和 NAT 信息也沒有 存儲在內核的(也就是 Netfilter 的)conntrack table 和 NAT table。所以常規的 conntrack/netstats/ss/lsof 等工具是看不到的,要使用 Cilium 的命令,例如:
$ cilium bpf nat list $ cilium bpf ct list global
配置也是獨立的,需要在 Cilium 里面配置,例如命令行選項 --bpf-ct-tcp-max。
另外,本文會多次提到連接跟蹤模塊和 NAT 模塊獨立,但出于性能考慮,具體實現中 二者代碼可能是有耦合的。例如 Cilium 做 conntrack 的垃圾回收(GC)時就會順便把 NAT 里相應的 entry 回收掉,而非為 NAT 做單獨的 GC。
以上是理論篇,接下來看一下內核實現。
2 Netfilter hook 機制實現
Netfilter 由幾個模塊構成,其中最主要的是連接跟蹤(CT) 模塊和網絡地址轉換(NAT)模塊。
CT 模塊的主要職責是識別出可進行連接跟蹤的包。CT 模塊獨立于 NAT 模塊,但主要目的是服務于后者。
2.1 Netfilter 框架
5 個 hook 點
圖 2.1. The 5 hook points in netfilter framework
如上圖所示,Netfilter 在內核協議棧的包處理路徑上提供了 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 點注冊自己的處理函數(handlers)。當有數據包經過 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 函數對包進行判斷或處理之后,需要返回一個判斷結果,指導接下來要對這個包做什 么。可能的結果有:
// include/uapi/linux/netfilter.h #define NF_DROP 0 // 已丟棄這個包 #define NF_ACCEPT 1 // 接受這個包,繼續下一步處理 #define NF_STOLEN 2 // 當前處理函數已經消費了這個包,后面的處理函數不用處理了 #define NF_QUEUE 3 // 應當將包放到隊列 #define NF_REPEAT 4 // 當前處理函數應當被再次調用
hook 優先級
每個 hook 點可以注冊多個處理函數(handler)。在注冊時必須指定這些 handlers 的優先級,這樣觸發 hook 時能夠根據優先級依次調用處理函數。
2.2 過濾規則的組織
iptables 是配置 Netfilter 過濾功能的用戶空間工具。為便于管理, 過濾規則按功能分為若干 table:
raw
filter
nat
mangle
這不是本文重點。更多信息可參考 (譯) 深入理解 iptables 和 netfilter 架構
3 Netfilter conntrack 實現
連接跟蹤模塊用于維護可跟蹤協議(trackable protocols)的連接狀態。也就是說, 連接跟蹤針對的是特定協議的包,而不是所有協議的包。稍后會看到它支持哪些協議。
3.1 重要結構體和函數
重要結構體:
struct nf_conntrack_tuple {}
: 定義一個 tuple。
struct nf_conntrack_man_proto {}:manipulable part 中協議相關的部分。
struct nf_conntrack_man {}
:tuple 的 manipulable part。
struct nf_conntrack_l4proto {}: 支持連接跟蹤的協議需要實現的方法集(以及其他協議相關字段)。
struct nf_conntrack_tuple_hash {}:哈希表(conntrack table)中的表項(entry)。
struct nf_conn {}:定義一個 flow。
重要函數:
hash_conntrack_raw():根據 tuple 計算出一個 32 位的哈希值(hash key)。
nf_conntrack_in():連接跟蹤模塊的核心,包進入連接跟蹤的地方。
resolve_normal_ct() -> init_conntrack() -> l4proto->new():創建一個新的連接記錄(conntrack entry)。
nf_conntrack_confirm():確認前面通過 nf_conntrack_in() 創建的新連接。
3.2 struct nf_conntrack_tuple {}:元組(Tuple)
Tuple 是連接跟蹤中最重要的概念之一。
一個 tuple 定義一個單向(unidirectional)flow。內核代碼中有如下注釋:
“ //include/net/netfilter/nf_conntrack_tuple.h
A tuple is 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 的實現,內核將 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 \ // 協議相關的部分 }; 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 自身也是結構體,能保存不同類型協議的數據。以 IPv4 UDP 為例,五元組分別保存在如下字段:
dst.protonum:協議類型
src.u3.ip:源 IP 地址
dst.u3.ip:目的 IP 地址
src.u.udp.port:源端口號
dst.u.udp.port:目的端口號
CT 支持的協議
從以上定義可以看到,連接跟蹤模塊目前只支持以下六種協議:TCP、UDP、ICMP、DCCP、SCTP、GRE。
注意其中的 ICMP 協議。大家可能會認為,連接跟蹤模塊依據包的三層和四層信息做 哈希,而 ICMP 是三層協議,沒有四層信息,因此 ICMP 肯定不會被 CT 記錄。但實際上 是會的,上面代碼可以看到,ICMP 使用了其頭信息中的 ICMP type和 code 字段來 定義 tuple。
3.3 struct nf_conntrack_l4proto {}:協議需要實現的方法集合
支持連接跟蹤的協議都需要實現 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 ...); // 創建一個新連接。如果成功返回 TRUE;如果返回的是 TRUE,接下來會調用 packet() 方法 bool (*new)(struct nf_conn *ct, const struct sk_buff *skb, unsigned int dataoff); // 判斷當前數據包能否被連接跟蹤。如果返回成功,接下來會調用 packet() 方法 int (*error)(struct net *net, struct nf_conn *tmpl, struct sk_buff *skb, ...); ... };
3.4 struct nf_conntrack_tuple_hash {}:哈希表項
conntrack 將活動連接的狀態存儲在一張哈希表中(key: value)。
hash_conntrack_raw() 根據 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 jhash3((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,即使是對那些非面向連接的協議(例 如 UDP)。每個 connection 用 struct nf_conn {} 表示,主要字段如下:
// include/net/netfilter/nf_conntrack.h // include/linux/skbuff.h ------> struct nf_conntrack { | atomic_t use; // 連接引用計數? | }; struct nf_conn { | struct nf_conntrack ct_general; struct nf_conntrack_tuple_hash tuplehash[IP_CT_DIR_MAX]; // 哈希表項,數組是因為要記錄兩個方向的 flow unsigned long status; // 連接狀態,見下文 u32 timeout; // 連接狀態的定時器 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; };
連接的狀態集合 enum ip_conntrack_status:
// include/uapi/linux/netfilter/nf_conntrack_common.h enum ip_conntrack_status { IPS_EXPECTED = (1 << IPS_EXPECTED_BIT), IPS_SEEN_REPLY = (1 << IPS_SEEN_REPLY_BIT), IPS_ASSURED = (1 << IPS_ASSURED_BIT), IPS_CONFIRMED = (1 << IPS_CONFIRMED_BIT), IPS_SRC_NAT = (1 << IPS_SRC_NAT_BIT), IPS_DST_NAT = (1 << IPS_DST_NAT_BIT), IPS_NAT_MASK = (IPS_DST_NAT | IPS_SRC_NAT), IPS_SEQ_ADJUST = (1 << IPS_SEQ_ADJUST_BIT), IPS_SRC_NAT_DONE = (1 << IPS_SRC_NAT_DONE_BIT), IPS_DST_NAT_DONE = (1 << IPS_DST_NAT_DONE_BIT), IPS_NAT_DONE_MASK = (IPS_DST_NAT_DONE | IPS_SRC_NAT_DONE), IPS_DYING = (1 << IPS_DYING_BIT), IPS_FIXED_TIMEOUT = (1 << IPS_FIXED_TIMEOUT_BIT), IPS_TEMPLATE = (1 << IPS_TEMPLATE_BIT), IPS_UNTRACKED = (1 << IPS_UNTRACKED_BIT), IPS_HELPER = (1 << IPS_HELPER_BIT), IPS_OFFLOAD = (1 << IPS_OFFLOAD_BIT), 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():進入連接跟蹤
Fig. Netfilter 中的連接跟蹤點
如上圖所示,Netfilter 在四個 Hook 點對包進行跟蹤:
1. PRE_ROUTING 和 LOCAL_OUT:調用 nf_conntrack_in() 開始連接跟蹤,正常情況 下會創建一條新連接記錄,然后將 conntrack entry 放到 unconfirmed list。
為什么是這兩個 hook 點呢?因為它們都是新連接的第一個包最先達到的地方,
PRE_ROUTING 是外部主動和本機建連時包最先到達的地方
LOCAL_OUT 是本機主動和外部建連時包最先到達的地方
2. POST_ROUTING 和 LOCAL_IN:調用 nf_conntrack_confirm() 將 nf_conntrack_in() 創建的連接移到 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 函數是連接跟蹤模塊的核心。
// 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 計數 return NF_ACCEPT; // 返回 NF_ACCEPT,繼續后面的處理 } skb->_nfct = 0; // 不屬于 ignore 類型,計數器置零,準備后續處理 } struct nf_conntrack_l4proto *l4proto = __nf_ct_l4proto_find(...); // 提取協議相關的 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;創建新連接記錄,或者更新已有連接的狀態 resolve_normal_ct(net, tmpl, skb, ... l4proto); l4proto->packet(ct, skb, dataoff, ctinfo); // 進行一些協議相關的處理,例如 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 的引用 }
大致流程:
鴻蒙官方戰略合作共建——HarmonyOS技術社區
嘗試獲取這個 skb 對應的連接跟蹤記錄
判斷是否需要對這個包做連接跟蹤,如果不需要,更新 ignore 計數,返回 NF_ACCEPT;如果需要,就初始化這個 skb 的引用計數。
從包的 L4 header 中提取信息,初始化協議相關的 struct nf_conntrack_l4proto {} 變量,其中包含了該協議的連接跟蹤相關的回調方法。
調用該協議的 error() 方法檢查包的完整性、校驗和等信息。
調用 resolve_normal_ct() 開始連接跟蹤,它會創建新 tuple,新 conntrack entry,或者更新已有連接的狀態。
調用該協議的 packet() 方法進行一些協議相關的處理,例如對于 UDP,如果 status bit 里面設置了 IPS_SEEN_REPLY 位,就會更新 timeout。timeout 大小和協 議相關,越小越越可以防止 DoS 攻擊(DoS 的基本原理就是將機器的可用連接耗盡)
3.7 init_conntrack():創建新連接記錄
如果連接不存在(flow 的第一個包),resolve_normal_ct() 會調用 init_conntrack ,后者進而會調用 new() 方法創建一個新的 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); // 協議相關的方法 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]; }
每種協議需要實現自己的 l4proto->new() 方法,代碼見:net/netfilter/nf_conntrack_proto_*.c。
如果當前包會影響后面包的狀態判斷,init_conntrack() 會設置 struct nf_conn 的 master 字段。面向連接的協議會用到這個特性,例如 TCP。
3.8 nf_conntrack_confirm():確認包沒有被丟棄
nf_conntrack_in() 創建的新 conntrack entry 會插入到一個 未確認連接( unconfirmed connection)列表。
如果這個包之后沒有被丟棄,那它在經過 POST_ROUTING 時會被 nf_conntrack_confirm() 方法處理,原理我們在分析過了 3.6 節的開頭分析過了。nf_conntrack_confirm() 完成之后,狀態就變為了 IPS_CONFIRMED,并且連接記錄從 未確認列表移到正常的列表。
之所以要將創建一個合法的新 entry 的過程分為創建(new)和確認(confirm)兩個階段 ,是因為包在經過 nf_conntrack_in() 之后,到達 nf_conntrack_confirm() 之前 ,可能會被內核丟棄。這樣會導致系統殘留大量的半連接狀態記錄,在性能和安全性上都 是很大問題。分為兩步之后,可以加快半連接狀態 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); // 設置連接引用計數? ct->status |= IPS_CONFIRMED; // 設置連接狀態為 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; }
可以看到,連接跟蹤的處理邏輯中需要頻繁關閉和打開軟中斷,此外還有各種鎖, 這是短連高并發場景下連接跟蹤性能損耗的主要原因?。
4 Netfilter NAT 實現
NAT 是與連接跟蹤獨立的模塊。
4.1 重要數據結構和函數
重要數據結構:
支持 NAT 的協議需要實現其中的方法:
struct nf_nat_l3proto {}
struct nf_nat_l4proto {}
重要函數:
nf_nat_inet_fn():NAT 的核心函數是,在除 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 {}:協議相關的 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 {}:協議相關的 NAT 方法集
// include/net/netfilter/nf_nat_l4proto.h struct nf_nat_l4proto { u8 l4proto; // Protocol number,例如 IPPROTO_UDP, IPPROTO_TCP // 根據傳入的 tuple 和 NAT 類型(SNAT/DNAT)修改包的 L3/L4 頭 bool (*manip_pkt)(struct sk_buff *skb, *l3proto, *tuple, maniptype); // 創建一個唯一的 tuple // 例如對于 UDP,會根據 src_ip, dst_ip, src_port 加一個隨機數生成一個 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); };
各協議實現的方法,見:net/netfilter/nf_nat_proto_*.c。例如 TCP 的實現:
// 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 的核心函數是 nf_nat_inet_fn(),它會在以下 hook 點被調用:
NF_INET_PRE_ROUTING
NF_INET_POST_ROUTING
NF_INET_LOCAL_OUT
NF_INET_LOCAL_IN
也就是除了 NF_INET_FORWARD 之外其他 hook 點都會被調用。
在這些 hook 點的優先級:Conntrack > NAT > Packet Filtering。連接跟蹤的優先 級高于 NAT 是因為 NAT 依賴連接跟蹤的結果。
Fig. 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 規則 if (!e) goto null_bind; for (i = 0; i < e->num_hook_entries; i++) { // 依次執行 NAT 規則 if (e->hooks[i].hook(e->hooks[i].priv, skb, state) != NF_ACCEPT ) return ret; // 任何規則返回非 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 狀態,就去獲取 NAT 規則。如果沒有規則,直接返回 NF_ACCEPT,對包不 做任何改動;如果有規則,最后執行 nf_nat_packet,這個函數會進一步調用 manip_pkt 完成對包的修改,如果失敗,包將被丟棄。
Masquerade
NAT 模塊一般配置方式:Change IP1 to IP2 if matching XXX。
此次還支持一種更靈活的 NAT 配置,稱為 Masquerade:Change IP1 to dev1's IP if matching XXX。與前面的區別在于,當設備(網卡)的 IP 地址發生變化時,這種方式無 需做任何修改。缺點是性能比第一種方式要差。
4.6 nf_nat_packet():執行 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)) // 協議相關處理 return NF_DROP; return NF_ACCEPT; }
以上是“Linux下如何實現連接跟蹤”這篇文章的所有內容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內容對大家有所幫助,如果還想學習更多知識,歡迎關注億速云行業資訊頻道!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。