[轉帖]Linux網絡之 netfilter 框架學習研究

https://zhuanlan.zhihu.com/p/567556545

 

netfilter 框架

netfilter 是 linux 內核中的一個數據包處理框架,用於替代原有的 ipfwadm 和 ipchains 等數據包處理程序。

netfilter 的功能包括數據包過濾,修改,SNAT/DNAT 等。

netfilter 在內核協議棧的不同位置實現了 5 個 hook 點,其它內核模塊 (比如 ip_tables) 可以向這些 hook 點註冊處理函數,這樣當數據包經過這些 hook 點時,其上註冊的處理函數就被依次調用,用戶層工具像 iptables 一般都需要相應內核模塊 ip_tables 配合以完成與 netfilter 的交互。

netfilter hooksip {6}_tablesconnection tracking、和 NAT 子系統一起構成了 netfilter 框架的主要部分。

netfilter 在內核協議中的 5 個 hook 點:

  • NF_IP_PRE_ROUTING:剛剛進入網絡層的數據包通過此點(剛剛進行完版本號,校驗和等檢測), 源地址轉換在此點進行。
  • NF_IP_LOCAL_IN:經路由查找後,送往本機的通過此檢查點,INPUT 包過濾在此點進行。
  • NF_IP_FORWARD:要轉發的包通過此檢測點,FORWARD 包過濾在此點進行。
  • NF_IP_POST_ROUTING:所有馬上要通過 NIC 出去的包通過此檢測點,內置的目的地址轉換功能(包括地址僞裝)在此點進行。
  • NF_IP_LOCAL_OUT:本機進程發出的包通過此檢測點,OUTPUT 包過濾在此點進行。
netfilter 架構: 

可能這麼直接去講 netfilter 還是不太容易理解,爲了更直觀理解,我們使用 iptables 舉例。

Iptables 是一個配置 IPv4 包過濾和 NAT 的管理工具,iptables 包含 4 個表,5 個鏈。

其中表是按照對數據包的操作區分的,鏈是按照不同的 Hook 點來區分的,表和鏈實際上是 netfilter 的兩個維度。

iptables 4表

  • filter:一般的過濾功能
  • nat: 用於 nat 功能(端口映射,地址映射等)
  • mangle: 用於對特定數據包的修改
  • raw: 優先級最高,設置 raw 時一般是爲了不再讓 iptables 做數據包的鏈接跟蹤處理,提高性能

注意:iptables 表處理是有優先級的(raw > mangle > nat > filter)。

iptables 5鏈

  • PREROUTING: 數據包進入路由表之前
  • INPUT: 通過路由表後目的地爲本機
  • FORWARDING: 通過路由表後,目的地不爲本機
  • OUTPUT: 由本機產生,向外轉發
  • POSTROUTIONG: 發送到網卡接口之前

所以,對於 iptables 而言,5 個鏈正對應在了 netfilter 框架中的 5 個 hook 點,以此來實現各種防火牆和數據包處理功能。

下面這個圖展示了 netfilter 框架在協議棧的位置,它可以清楚地看到 netfilter 框架是如何處理通過不同協議棧路徑上的數據包。

Packet flow in Netfilter
Packet flow in Netfilter

我們知道 iptables 的定位是 IPv4 packet filter,它只處理 IP 數據包,而 ebtables 只工作在鏈路層 Link Layer 處理的是以太網幀 (比如修改源目 mac 地址)。

圖中用有顏色的長方形方框表示 iptables 或 ebtables 的表和鏈,綠色小方框表示 network level,即 iptables 的表和鏈。藍色小方框表示 bridge level,即 ebtables 的表和鏈,由於處理以太網幀相對簡單,因此鏈路層的藍色小方框相對較少。

我們還注意到一些代表 iptables 表和鏈的綠色小方框位於鏈路層,這是因爲 bridge_nf 代碼的作用 (從 2.6 kernel 開始),bridge_nf 的引入是爲了解決在鏈路層 Bridge 中處理 IP 數據包的問題 (需要通過內核參數開啓),那爲什麼要在鏈路層 Bridge 中處理 IP 數據包,而不等數據包通過網絡層時候再處理呢,這是因爲不是所有的數據包都一定會通過網絡層,比如外部機器與主機上虛擬機的通信流量,bridge_nf 也是 openstack 中實現安全組功能的基礎。

注意:這也是在各種使用 2 層網絡的容器環境中,我們需要打開 bridge-nf 相關內核參數的原因。(sysctl -a |grep 'bridge-nf-' 可以查看到)

bridge_nf 代碼有時候會引起困惑,就像我們在圖中看到的那樣,代表 iptables 表和鏈的綠色小方框跑到了鏈路層,netfilter 文檔對此也有說明 ebtables/iptables interaction on a Linux-based bridge

It should be noted that the br-nf code sometimes violates the TCP/IP Network Model. As will be seen later, it is possible, f.e., to do IP DNAT inside the Link Layer.

ok,圖中長方形小方框已經解釋清楚了,還有一種橢圓形的方框 conntrack,即 connection tracking,這是 netfilter 提供的連接跟蹤機制,此機制允許內核” 審查” 通過此處的所有網絡數據包,並能識別出此數據包屬於哪個網絡連接 (比如數據包 a 屬於 IP1:8888->IP2:80 這個 tcp 連接,數據包 b 屬於 ip3:9999->IP4:53 這個 udp 連接)。

因此,連接跟蹤機制使內核能夠跟蹤並記錄通過此處的所有網絡連接及其狀態。

圖中可以清楚看到連接跟蹤代碼所處的網絡棧位置,如果不想讓某些數據包被跟蹤 (NOTRACK), 那就要找位於橢圓形方框 conntrack 之前的表和鏈來設置規則。

注意: conntrack 機制是 iptables 實現狀態匹配 (-m state) 以及 NAT 的基礎,它由單獨的內核模塊 nf_conntrack 實現

接着看圖中左下方 bridge check 方框,數據包從主機上的某個網絡接口進入 (ingress)``, 在 bridge check 處會檢查此網絡接口是否屬於某個 Bridge 的 port,如果是就會進入 Bridge 代碼處理邏輯 (下方藍色區域 bridge level), 否則就會送入網絡層 Network Layer 處理。

圖中下方中間位置的 bridging decision 類似普通二層交換機的查錶轉發功能,根據數據包目的 MAC 地址判斷此數據包是轉發還是交給上層處理。

圖中中心位置的 routing decision 就是路由選擇,根據系統路由表 (ip route 查看), 決定數據包是 forward,還是交給本地處理 。

總的來看,不同 packet 有不同的 packet flow,packet 總是從主機的某個接口進入 (左下方 ingress), 然後經過 check/decision/ 一系列表和鏈處理,最後,目的地或是主機上某應用進程 (上中位置 local process),或是需要從主機另一個接口發出 (右下方 egress)。這裏的接口即可以是物理網卡 em1,也可以是虛擬網卡 tun0/vnetx,還可以是 Bridge 上的一個 port。

如上就是對在 netfilter 中數據包的流向的整體介紹。

鏈接跟蹤 (connection tracking)

當加載內核模塊 nf_conntrack 後,conntrack 機制就開始工作,如上圖,橢圓形方框 conntrack 在內核中有兩處位置 (PREROUTING 和 OUTPUT 之前) 能夠跟蹤數據包。

對於每個通過 conntrack 的數據包,內核都爲其生成一個 conntrack 條目用以跟蹤此連接,對於後續通過的數據包,內核會判斷若此數據包屬於一個已有的連接,則更新所對應的 conntrack 條目的狀態 (比如更新爲 ESTABLISHED 狀態),否則內核會爲它新建一個 conntrack 條目。

所有的 conntrack 條目都存放在一張表裏,稱爲連接跟蹤表。

Conntrack table

在 linux 系統中,我們可以使用如下指令來查看 conntrack table 相關信息:

# 查看 當前conntrack 的統計信息
$ conntrack -S 
cpu=0           found=5508 invalid=6125 ignore=106657121 insert=0 insert_failed=1 drop=1 early_drop=0 error=0 search_restart=443826 
cpu=1           found=5579 invalid=6477 ignore=104343231 insert=0 insert_failed=2 drop=2 early_drop=0 error=0 search_restart=473732 
cpu=2           found=5583 invalid=6132 ignore=107286285 insert=0 insert_failed=0 drop=0 early_drop=0 error=0 search_restart=501842 
cpu=3           found=5507 invalid=6414 ignore=106874847 insert=0 insert_failed=1 drop=1 early_drop=0 error=0 search_restart=504806 
cpu=4           found=5459 invalid=6813 ignore=105410436 insert=0 insert_failed=3 drop=3 early_drop=0 error=0 search_restart=473754 
cpu=5           found=5348 invalid=6347 ignore=110859551 insert=0 insert_failed=1 drop=1 early_drop=0 error=0 search_restart=486828 
cpu=6           found=5446 invalid=6648 ignore=107420817 insert=0 insert_failed=3 drop=3 early_drop=0 error=0 search_restart=484902 
cpu=7           found=5404 invalid=6500 ignore=105923893 insert=0 insert_failed=0 drop=0 early_drop=0 error=0 search_restart=472292 

# 實時查看 conntrack 表中的鏈接信息
$ conntrack -E 
    [NEW] tcp      6 120 SYN_SENT src=100.126.47.26 dst=9.144.119.20 sport=26428 dport=32178 [UNREPLIED] src=9.144.119.20 dst=100.126.47.26 sport=443 dport=26428
 [UPDATE] tcp      6 60 SYN_RECV src=100.126.47.26 dst=9.144.119.20 sport=26428 dport=32178 src=9.144.119.20 dst=100.126.47.26 sport=443 dport=26428
 [UPDATE] tcp      6 10 CLOSE src=100.126.47.26 dst=9.144.119.20 sport=26428 dport=32178 src=9.144.119.20 dst=100.126.47.26 sport=443 dport=26428
[DESTROY] tcp      6 src=9.144.85.79 dst=11.166.65.134 sport=15498 dport=7443 src=11.166.65.134 dst=9.144.85.79 sport=7443 dport=15498
[DESTROY] tcp      6 src=9.144.119.182 dst=11.166.65.131 sport=33905 dport=50052 src=11.166.65.131 dst=9.144.119.182 sport=50052 dport=33905
[DESTROY] tcp      6 src=9.144.85.39 dst=11.166.65.130 sport=33682 dport=8008 src=11.166.65.130 dst=9.144.85.39 sport=8008 dport=33682
[DESTROY] icmp     1 src=100.126.0.94 dst=9.144.119.20 type=8 code=0 id=15060 src=9.144.119.20 dst=100.126.0.94 type=0 code=0 id=15060
[DESTROY] udp      17 src=9.144.119.20 dst=9.146.24.15 sport=41863 dport=53 src=9.146.24.15 dst=9.144.119.20 sport=53 dport=41863
[DESTROY] tcp      6 src=11.166.65.129 dst=11.166.65.131 sport=37122 dport=50052 src=11.166.65.131 dst=11.166.65.129 sport=50052 dport=37122 [ASSURED]
[DESTROY] tcp      6 src=9.144.85.153 dst=11.166.65.131 sport=64221 dport=50052 src=11.166.65.131 dst=9.144.85.153 sport=50052 dport=64221
[DESTROY] tcp      6 src=9.144.85.79 dst=11.166.65.130 sport=56999 dport=8008 src=11.166.65.130 dst=9.144.85.79 sport=8008 dport=56999
    [NEW] tcp      6 120 SYN_SENT src=9.144.119.97 dst=11.166.65.134 sport=22611 dport=7443 [UNREPLIED] src=11.166.65.134 dst=9.144.119.97 sport=7443 dp
    ....
    ....

連接跟蹤表存放於系統內存中,因此也可以直接查看文件 /proc/net/nf_conntrack 查看當前跟蹤的所有 conntrack 條目。

如下是代表一個 tcp 連接的 conntrack 條目,根據連接協議不同,下面顯示的字段信息也不一樣,比如 icmp 協議。

ipv4     2 icmp     1 3 src=169.254.128.93 dst=9.144.119.20 type=8 code=0 id=13037 src=9.144.119.20 dst=169.254.128.93 type=0 code=0 id=13037 mark=0 zone=0 use=2

每個 conntrack 條目表示一個連接,連接協議可以是 tcp,udp,icmp 等,它包含了數據包的原始方向信息 和期望的響應包信息,這樣內核能夠在後續到來的數據包中識別出屬於此連接的雙向數據包,並更新此連接的狀態,各字段意思的具體分析後面會說。

連接跟蹤表中能夠存放的 conntrack 條目的最大值,即系統允許的最大連接跟蹤數記作 CONNTRACK_MAX

Conntrack Table

在內核中,連接跟蹤表是一個二維數組結構的哈希表 (hash table),哈希表的大小記作 HASHSIZE,哈希表的每一項 (hash table entry) 稱作 bucket,因此哈希表中有 HASHSIZE 個 bucket 存在,每個 bucket 包含一個鏈表 (linked list),每個鏈表能夠存放若干個 conntrack 條目 (bucket size)。

對於一個新收到的數據包,內核使用如下步驟判斷其是否屬於一個已有連接:

  • 內核提取此數據包信息 (源目 IP,port,協議號) 進行 hash 計算得到一個 hash 值,在哈希表中以此 hash 值做索引,索引結果爲數據包所屬的 bucket (鏈表)。這一步 hash 計算時間固定並且很短
  • 遍歷 hash 得到的 bucket,查找是否有匹配的 conntrack 條目,這一步是比較耗時的操作,bucket size 越大,遍歷時間越長
需要注意的是,由於上面 netfilter 框架的作用,當 conntrack 表滿之後,就可能造成丟包,因此在實際環境中,我們需要對 conntrack table 進行調整,特別是像在容器化環境中,svc 的規模直接會影響到 conntrack table 的容量。 在我們大規模的 K8S 環境中,還是經常會出現 nf conntrack 滿了,而導致各種丟包,比較典型的場景就是 coredns 丟包超時。

conntrack max

根據上面對哈希表的解釋,系統最大允許連接跟蹤數 CONNTRACK_MAX = 連接跟蹤表大小(HASHSIZE) * Bucket大小(bucket size)

從連接跟蹤表獲取 bucket 是 hash 操作時間很短,而遍歷 bucket 相對費時,因此爲了 conntrack 性能考慮,bucket size 越小越好,默認爲 8。

#查看系統當前最大連接跟蹤數CONNTRACK_MAX
$ sysctl -a | grep net.netfilter.nf_conntrack_max
net.netfilter.nf_conntrack_max = 262144

#查看當前連接跟蹤表大小HASHSIZE
$ sysctl -a | grep net.netfilter.nf_conntrack_buckets
net.netfilter.nf_conntrack_buckets = 262144

conntrack_max 和 conntrack_buckets 的比值即爲 bucket size = 262144/262144 。

如下,現在需求是設置系統最大連接跟蹤數爲 320w,由於 bucket size 不能直接設置,爲了使 bucket size 值爲 8,我們需要同時設置 CONNTRACK_MAX 和 HASHSIZE,因爲他們的比值就是 bucket size

#HASHSIZE (內核會自動格式化爲最接近允許值)       
echo 400000 > /sys/module/nf_conntrack/parameters/hashsize

#系統最大連接跟蹤數
sysctl -w net.netfilter.nf_conntrack_max=3200000      

#注意nf_conntrack內核模塊需要加載  

爲了使 nf_conntrack 模塊重新加載或系統重啓後生效

echo "options nf_conntrack hashsize=400000" > /etc/modprobe.d/nf_conntrack.conf                  

只需要固化 HASHSIZE 值,nf_conntrack 模塊在重新加載時會自動設置 CONNTRACK_MAX = hashsize * 8,當然前提是你 bucket size 使用系統默認值 8。

如果自定義 bucket size 值,就需要同時固化 CONNTRACK_MAX,以保持其比值爲你想要的 bucket size。

上面我們沒有改變 bucket size 的默認值 8,但是若內存足夠並且性能很重要,你可以考慮每個 bucket 一個 conntrack 條目 (bucket size = 1),最大可能降低遍歷耗時,即 HASHSIZE = CONNTRACK_MAX

#HASHSIZE
echo 3200000 > /sys/module/nf_conntrack/parameters/hashsize
#CONNTRACK_MAX
sysctl -w net.netfilter.nf_conntrack_max=3200000

如何計算連接跟蹤所佔內存

連接跟蹤表存儲在系統內存中,因此需要考慮內存佔用問題,可以用下面公式計算設置不同的最大連接跟蹤數所佔最大系統內存

total_mem_used(bytes) = CONNTRACK_MAX * sizeof(struct ip_conntrack) + HASHSIZE * sizeof(struct list_head)

例如我們需要設置最大連接跟蹤數爲 320w,在 centos6/7 系統上,sizeof(struct ip_conntrack) = 376,sizeof(struct list_head) = 16,並且 bucket size 使用默認值 8,並且 HASHSIZE = CONNTRACK_MAX / 8,因此

total_mem_used(bytes) = 3200000 * 376 + (3200000 / 8) * 16
# = 1153MB ~= 1GB


因此可以得到,在 centos6/7 系統上,設置 320w 的最大連接跟蹤數,所消耗的內存大約爲 1GB,對現代服務器來說,佔用內存並不多,但 conntrack 實在讓人又愛又恨。

conntrack 條目

conntrack 從經過它的數據包中提取詳細的,唯一的信息,因此能保持對每一個連接的跟蹤。

關於 conntrack 如何確定一個連接,對於 tcp/udp,連接由他們的源目地址,源目端口唯一確定。

對於 icmp,由 type,code 和 id 字段確定。

ipv4     2 tcp      6 33 SYN_SENT src=172.16.200.119 dst=172.16.202.12 sport=54786 dport=10051 [UNREPLIED] src=172.16.202.12 dst=172.16.200.119 sport=10051 dport=54786 mark=0 zone=0 use=2

如上是一條 conntrack 條目,它代表當前已跟蹤到的某個連接,conntrack 維護的所有信息都包含在這個條目中,通過它就可以知道某個連接處於什麼狀態。

  • 此連接使用 ipv4 協議,是一條 tcp 連接 (tcp 的協議類型代碼是 6)
  • 33 是這條 conntrack 條目在當前時間點的生存時間 (每個 conntrack 條目都會有生存時間,從設置值開始倒計時,倒計時完後此條目將被清除),可以使用 sysctl -a |grep conntrack | grep timeout 查看不同協議不同狀態下生存時間設置值,當然這些設置值都可以調整,注意若後續有收到屬於此連接的數據包,則此生存時間將被重置 (重新從設置值開始倒計時),並且狀態改變,生存時間設置值也會響應改爲新狀態的值
  • SYN_SENT 是到此刻爲止 conntrack 跟蹤到的這個連接的狀態 (內核角度),SYN_SENT 表示這個連接只在一個方向發送了一初始 TCP SYN 包,還未看到響應的 SYN+ACK 包 (只有 tcp 纔會有這個字段)。
  • src=172.16.200.119 dst=172.16.202.12 sport=54786 dport=10051 是從數據包中提取的此連接的源目地址、源目端口,是 conntrack 首次看到此數據包時候的信息。
  • [UNREPLIED] 說明此刻爲止這個連接還沒有收到任何響應,當一個連接已收到響應時,[UNREPLIED] 標誌就會被移除
  • src=172.16.202.12 dst=172.16.200.119 sport=10051 dport=54786 地址和端口和前面是相反的,這部分不是數據包中帶有的信息,是 conntrack 填充的信息,代表 conntrack 希望收到的響應包信息。意思是若後續 conntrack 跟蹤到某個數據包信息與此部分匹配,則此數據包就是此連接的響應數據包。注意這部分確定了 conntrack 如何判斷響應包 (tcp/udp),icmp 是依據另外幾個字段

上面是 tcp 連接的條目,而 udp 和 icmp 沒有連接建立和關閉過程,因此條目字段會有所不同,後面 iptables 狀態匹配部分我們會看到處於各個狀態的 conntrack 條目。

注意 conntrack 機制並不能夠修改或過濾數據包,它只是跟蹤網絡連接並維護連接跟蹤表,以提供給 iptables 做狀態匹配使用,也就是說,如果你 iptables 中用不到狀態匹配,那就沒必要啓用 conntrack。

iptables 狀態匹配

先明確下 conntrack 在內核協議棧所處位置,上面也提過 conntrack 跟蹤數據包的位置在 PREROUTING 和 OUTPUT 這兩個 hook 點,主機自身進程產生的數據包會通過 OUTPUT 處的 conntrack,從主機任意接口進入 (包括 Bridge 的 port) 的數據包會通過 PREROUTING 處的 conntrack,從 netfilter 框架圖上可以看到 conntrack 位置很靠前,僅在 iptables 的 raw 表之後,raw 表主要作用就是允許我們對某些特定的數據包打上 NOTRACK 標記,這樣後面的 conntrack 就不會記錄此類帶有 NOTRACK 標籤的數據包。

conntrack 位置很靠前一方面是保證其後面的 iptables 表和鏈都能使用狀態匹配,另一方面使得 conntrack 能夠跟蹤到任何進出主機的原始數據包 (比如數據包還未 NAT/FORWARD)。

iptables 狀態匹配模塊

iptables 是帶有狀態匹配的防火牆,它使用 -m state 模塊從連接跟蹤表查找數據包狀態。

上面我們分析的那條 conntrack 條目處於 SYN_SENT 狀態,這是內核記錄的狀態,數據包在內核中可能會有幾種不同的狀態,但是映射到用戶空間 iptables,只有 5 種狀態可用:

  • NEW
  • ESTABLISHED
  • RELATED
  • INVALID
  • UNTRACKED

注意這裏說的狀態不是 tcp/ip 協議中 tcp 連接的各種狀態。下面表格說明了這 5 種狀態分別能夠匹配什麼樣的數據包,注意下面兩點:

  • 用戶空間這 5 種狀態是 iptables 用於完成狀態匹配而定義的,不關聯與特定連接協議
  • conntrack 記錄在前,iptables 匹配在後
狀態解釋
NEW NEW 匹配連接的第一個包。意思就是,iptables 從連接跟蹤表中查到此包是某連接的第一個包。判斷此包是某連接的第一個包是依據 conntrack 當前” 只看到一個方向數據包”([UNREPLIED]),不關聯特定協議,因此 NEW 並不單指 tcp 連接的 SYN 包
ESTABLISHED ESTABLISHED 匹配連接的響應包及後續的包。意思是,iptables 從連接跟蹤表中查到此包是屬於一個已經收到響應的連接 (即沒有 [UNREPLIED] 字段)。因此在 iptables 狀態中,只要發送並接到響應,連接就認爲是 ESTABLISHED 的了。這個特點使 iptables 可以控制由誰發起的連接纔可以通過,比如 A 與 B 通信,A 發給 B 數據包屬於 NEW 狀態,B 回覆給 A 的數據包就變爲 ESTABLISHED 狀態。ICMP 的錯誤和重定向等信息包也被看作是 ESTABLISHED,只要它們是我們所發出的信息的應答。
RELATED RELATED 匹配那些屬於 RELATED 連接的包,這句話說了跟沒說一樣。RELATED 狀態有點複雜,當一個連接與另一個已經是 ESTABLISHED 的連接有關時,這個連接就被認爲是 RELATED。這意味着,一個連接要想成爲 RELATED,必須首先有一個已經是 ESTABLISHED 的連接存在。這個 ESTABLISHED 連接再產生一個主連接之外的新連接,這個新連接就是 RELATED 狀態了,當然首先 conntrack 模塊要能” 讀懂” 它是 RELATED。拿 ftp 來說,FTP 數據傳輸連接就是 RELATED 與先前已建立的 FTP 控制連接,還有通過 IRC 的 DCC 連接。有了 RELATED 這個狀態,ICMP 錯誤消息、FTP 傳輸、DCC 等才能穿過防火牆正常工作。有些依賴此機制的 TCP 協議和 UDP 協議非常複雜,他們的連接被封裝在其它的 TCP 或 UDP 包的數據部分 (可以瞭解下 overlay/vxlan/gre),這使得 conntrack 需要藉助其它輔助模塊才能正確” 讀懂” 這些複雜數據包,比如 nf_conntrack_ftp 這個輔助模塊
INVALID INVALID 匹配那些無法識別或沒有任何狀態的數據包。這可能是由於系統內存不足或收到不屬於任何已知連接的 ICMP 錯誤消息。一般情況下我們應該 DROP 此類狀態的包
UNTRACKED UNTRACKED 狀態比較簡單,它匹配那些帶有 NOTRACK 標籤的數據包。需要注意的一點是,如果你在 raw 表中對某些數據包設置有 NOTRACK 標籤,那上面的 4 種狀態將無法匹配這樣的數據包,因此你需要單獨考慮 NOTRACK 包的放行規則

狀態的使用使防火牆可以非常強大和有效,來看下面這個常見的防火牆規則,它允許本機主動訪問外網,以及放開 icmp 協議。

#iptables-save  -t filter
*filter
:INPUT DROP [1453341:537074675]
:FORWARD DROP [10976649:6291806497]
:OUTPUT ACCEPT [1221855153:247285484556]
-A INPUT -p icmp -j ACCEPT 
-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT 

主機進程與外網機器通信經歷如下步驟,因爲除了 filter 表,其它表沒設置任何規則,所以下面步驟就省略其它表的匹配過程:

  • 1. 進程產生要發送的數據包,數據包通過 raw 表 OUTPUT 鏈 (可以決定是否要 NOTRACK)
  • 2. 數據包通過 conntrack,conntrack 記錄此連接到連接跟蹤表 ([UNREPLIED]) – NEW
  • 3. 通過 OUTPUT 鏈,然後從主機網卡發出 – NEW
  • 4. 外網目標主機收到請求,發出響應包
  • 5. 響應包從主機某個接口進入,到達 raw 表 PREROUTING 鏈 (可以決定是否要 NOTRACK) – NEW
  • 6. 響應包通過 conntrack,conntrack 發現此數據包爲一個連接的響應包,更新對應 conntrack 條目狀態 (去掉 [UNREPLIED],至此兩個方向都看到包了) – ESTABLISHED
  • 7. 響應包到達 filter 表 INPUT 鏈,在這裏匹配到 --state RELATED,ESTABLISHED,因此放行 – ESTABLISHED

像上面這種允許本機主動出流量的需求,如果不用 conntrack 會很難實現,那你可能會說我也可以使用 iptables

數據包在內核中的狀態

從內核角度,不同協議有不同狀態,這裏我們來具體看下三種協議 tcp/udp/icmp 在連接跟蹤表中的不同狀態

tcp 鏈接

下面是 172.16.1.100 向 172.16.1.200 建立 tcp 通信過程中,/proc/net/nf_conntrack 中此連接的狀態變化過程

ipv4     2 tcp      6 118 SYN_SENT src=172.16.1.100 dst=172.16.1.200 sport=36884 dport=8220 [UNREPLIED] src=172.16.1.200 dst=172.16.1.100 sport=8220 dport=36884 mark=0 zone=0 use=2

如上,首先 172.16.1.100 向 172.16.1.200 發送 SYN 包,172.16.1.200 收到 SYN 包但尚未回覆,由於是新連接,conntrack 將此連接添加到連接跟蹤表,並標記爲 SYN_SENT 狀態,[UNREPLIED] 表示 conntrack 尚未跟蹤到 172.16.1.200 的響應包。注意上面這條 conntrack 條目存在於兩臺主機的連接跟蹤表中 (當然,首先要兩臺主機都啓用 conntrack),對於 172.16.1.100,數據包在經過 OUTPUT 這個 hook 點時觸發 conntrack,而對於 172.16.1.200,數據包在 PREROUTING 這個 hook 點時觸發 conntrack

隨後,172.16.1.200 回覆 SYN/ACK 包給 172.16.1.100,通過 conntrack 更新連接狀態爲 SYN_RECV,表示收到 SYN/ACk 包,去掉 [UNREPLIED] 字段

ipv4     2 tcp      6 59 SYN_RECV src=172.16.1.100 dst=172.16.1.200 sport=36884 dport=8220 src=172.16.1.200 dst=172.16.1.100 sport=8220 dport=36884 mark=0 zone=0 use=2

接着,172.16.1.100 回覆 ACK 給 172.16.1.200,至此,三次握手完成,tcp 連接已建立,conntrack 更新連接狀態爲 ESTABLISHED

ipv4     2 tcp      6 10799 ESTABLISHED src=172.16.1.100 dst=172.16.1.200 sport=36884 dport=8220 src=172.16.1.200 dst=172.16.1.100 sport=8220 dport=36884 [ASSURED] mark=0 zone=0 use=2


連接跟蹤表中的 conntrack 條目不可能是永久存在,每個 conntrack 條目都有超時時間,可以如下方式查看 tcp 連接各個狀態當前設置的超時時間。

$ sysctl -a |grep 'net.netfilter.nf_conntrack_tcp_timeout_'
net.netfilter.nf_conntrack_tcp_timeout_close = 10
net.netfilter.nf_conntrack_tcp_timeout_close_wait = 60
net.netfilter.nf_conntrack_tcp_timeout_established = 432000
...

正常的 tcp 連接是很短暫的,不太可能查看到一個 tcp 連接所有狀態變化的,那如何構造處於特定狀態的 tcp 連接呢,一個方法是利用 iptables 的 --tcp-flags 參數,其可以匹配 tcp 數據包的標誌位,比如下面這兩條:

#對於本機發往172.16.1.200的tcp數據包,丟棄帶有SYN/ACK flag的包       
$ iptables -A OUTPUT -o eth0 -p tcp --tcp-flags SYN,ACK SYN,ACK -d 172.16.1.200 -j DROP

#同樣,這個是丟棄帶有ACK flag的包 
$ iptables -A OUTPUT -o eth0 -p tcp --tcp-flags ACK ACK -d 172.16.1.100 -j DROP

同時利用 tcp 超時重傳機制,待 cat /proc/net/nf_conntrack 獲取到 conntrack 條目後,使用 iptables -D OUTPUT X 刪除之前設置的 DROP 規則,這樣 tcp 連接就會正常走下去,這個很容易測試出來。

udp 鏈接

UDP 連接是無狀態的,它沒有連接的建立和關閉過程,連接跟蹤表中的 udp 連接也沒有像 tcp 那樣的狀態字段,但這不妨礙用戶空間 iptables 對 udp 包的狀態匹配,上面也說過,iptables 中使用的各個狀態與協議無關。

#只收到udp連接第一個包

ipv4     2 udp      17 28 src=172.16.1.100 dst=172.16.1.200 sport=26741 dport=8991 [UNREPLIED] src=172.16.1.200 dst=172.16.1.100 sport=8991 dport=26741 mark=0 zone=0 use=2

#收到此連接的響應包    

ipv4     2 udp      17 29 src=172.16.1.100 dst=172.16.1.200 sport=26741 dport=8991 src=172.16.1.200 dst=172.16.1.100 sport=8991 dport=26741 mark=0 zone=0 use=2

同樣可以查看 udp 超時時間。

$ sysctl -a | grep 'net.netfilter.nf_conntrack_udp_'
net.netfilter.nf_conntrack_udp_timeout = 30
net.netfilter.nf_conntrack_udp_timeout_stream = 180

icmp

icmp 請求,在用戶空間 iptables 看來,跟蹤到 echo request 時連接處於 NEW 狀態,當有 echo reply 時就是 ESTABLISHED 狀態。

#icmp請求
ipv4     2 icmp     1 28 src=103.229.215.2 dst=113.31.136.7 type=8 code=0 id=35102 [UNREPLIED] src=113.31.136.7 dst=103.229.215.2 type=0 code=0 id=35102 mark=0 zone=0 use=2

#reply
ipv4     2 icmp     1 29 src=103.229.215.2 dst=113.31.136.7 type=8 code=0 id=35102 src=113.31.136.7 dst=103.229.215.2 type=0 code=0 id=35102 mark=0 zone=0 use=2

如何管理連接跟蹤表

我們在一開始簡單介紹了使用 conntrack 命令行工具來查看 鏈接跟蹤信息,該工具提供了對連接跟蹤表的增刪改查功能。

#查看連接跟蹤表所有條目  
$ conntrack -L
#清除連接跟蹤表
$ conntrack -F
#刪除連接跟蹤表中所有源地址是1.2.3.4的條目
$ conntrack -D -s 1.2.3.4

Bridge 與 netfilter

從 netfilter 框架圖中可以看到,最下層藍色區域爲 bridge level。

Bridge 的存在,使得主機可以充當一臺虛擬的普通二層交換機來運作,這個虛擬交換機可以建多個 port,連接到多個虛擬機。

由此帶來的問題是,外部機器與其上虛擬機通信流量只會經過主機二層 (靠 Bridge 轉發,此時不經過主機 IP 層),主機上的網卡類型變得複雜 (物理網卡 em1,網橋 br0,虛擬網卡 vnetX),進入主機的數據包可選路徑變多 (bridge 轉發 / 交給主機本地進程)。

conntrack 與 LVS

LVS 的修改數據包功能也是依賴 netfilter 框架,在 LVS 機器上應用 iptables 時需要注意一個問題就是,LVS-DR (或 LVS-Tun) 模式下,不能在 director 上使用 iptables 的狀態匹配 (NEW,ESTABLISHED,INVALID,…)。

LVS-DR 模式下,client 訪問 director,director 轉發流量到 realserver,realserver 直接回復 client,不經過 director,這種情況下,client 與 direcotr 處於 tcp 半連接狀態。

因此如果在 director 機器上啓用 conntrack,此時 conntrack 只能看到 client–>director 的數據包,因爲響應包不經過 direcotr,conntrack 無法看到反方向上的數據包,就表示 iptables 中的 ESTABLISHED 狀態永遠無法匹配,從而可能發生 DROP。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章