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