大家好,我是飛哥!
現(xiàn)在 iptables 這個(gè)工具的應(yīng)用似乎是越來(lái)越廣了。不僅僅是在傳統(tǒng)的防火墻、NAT 等功能出現(xiàn),在今天流行的的 Docker、Kubernets、Istio 項(xiàng)目中也經(jīng)常能見(jiàn)著對(duì)它的身影。正因?yàn)槿绱耍陨钊肜斫?iptables 工作原理是非常有價(jià)值的事情。
Linux 內(nèi)核網(wǎng)絡(luò)棧是一個(gè)純內(nèi)核態(tài)的東西,和用戶層功能是天然隔離。但為了迎合各種各樣用戶層不同的需求,內(nèi)核開(kāi)放了一些口子出來(lái)供用戶干預(yù)。使得用戶層可以通過(guò)一些配置,改變內(nèi)核的工作方式,從而實(shí)現(xiàn)特殊的需求。
Linux 在內(nèi)核網(wǎng)絡(luò)組件中很多關(guān)鍵位置布置了 netfilter 過(guò)濾器。Iptables 就是基于 netfilter 來(lái)實(shí)現(xiàn)的。所以本文中 iptables 和 netfilter 這兩個(gè)名詞有時(shí)候就混著用了。
飛哥也在網(wǎng)上看過(guò)很多關(guān)于 netfilter 技術(shù)文章,但是我覺(jué)得都寫的不夠清晰。所以咱們擼起袖子,自己寫一篇。Netfilter 的實(shí)現(xiàn)可以簡(jiǎn)單地歸納為四表五鏈。我們來(lái)詳細(xì)看看四表、五鏈究竟是啥意思。
一、Iptables 中的五鏈
Linux 下的 netfilter 在內(nèi)核協(xié)議棧的各個(gè)重要關(guān)卡埋下了五個(gè)鉤子。每一個(gè)鉤子都對(duì)應(yīng)是一系列規(guī)則,以鏈表的形式存在,所以俗稱五鏈。當(dāng)網(wǎng)絡(luò)包在協(xié)議棧中流轉(zhuǎn)到這些關(guān)卡的時(shí)候,就會(huì)依次執(zhí)行在這些鉤子上注冊(cè)的各種規(guī)則,進(jìn)而實(shí)現(xiàn)對(duì)網(wǎng)絡(luò)包的各種處理。
要想把五鏈理解好,飛哥認(rèn)為最關(guān)鍵是要把內(nèi)核接收、發(fā)送、轉(zhuǎn)發(fā)三個(gè)過(guò)程分開(kāi)來(lái)看。
1.1 接收過(guò)程
Linux 在網(wǎng)絡(luò)包接收在 IP 層的入口函數(shù)是 ip_rcv。網(wǎng)絡(luò)在這里包碰到的第一個(gè) HOOK 就是 PREROUTING。當(dāng)該鉤子上的規(guī)則都處理完后,會(huì)進(jìn)行路由選擇。如果發(fā)現(xiàn)是本設(shè)備的網(wǎng)絡(luò)包,進(jìn)入 ip_local_deliver 中,在這里又會(huì)遇到 INPUT 鉤子。
我們來(lái)看下詳細(xì)的代碼,先看 ip_rcv。
- //file: net/ipv4/ip_input.c
- int ip_rcv(struct sk_buff *skb, ......){
- ......
- return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, dev, NULL,
- ip_rcv_finish);
- }
NF_HOOK 這個(gè)函數(shù)會(huì)執(zhí)行到 iptables 中 pre_routing 里的各種表注冊(cè)的各種規(guī)則。當(dāng)處理完后,進(jìn)入 ip_rcv_finish。在這里函數(shù)里將進(jìn)行路由選擇。這也就是 PREROUTING 這一鏈名字得來(lái)的原因,因?yàn)槭窃诼酚汕皥?zhí)行的。
- //file: net/ipv4/ip_input.c
- static int ip_rcv_finish(struct sk_buff *skb){
- ...
- if (!skb_dst(skb)) {
- int err = ip_route_input_noref(skb, iph->daddr, iph->saddr,
- iph->tos, skb->dev);
- ...
- }
- ...
- return dst_input(skb);
- }
如果發(fā)現(xiàn)是本地設(shè)備上的接收,會(huì)進(jìn)入 ip_local_deliver 函數(shù)。接著是又會(huì)執(zhí)行到 LOCAL_IN 鉤子,這也就是我們說(shuō)的 INPUT 鏈。
- //file: net/ipv4/ip_input.c
- int ip_local_deliver(struct sk_buff *skb){
- ......
- return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN, skb, skb->dev, NULL,
- ip_local_deliver_finish);
- }
簡(jiǎn)單總結(jié)接收數(shù)據(jù)的處理流程是:PREROUTING鏈 -> 路由判斷(是本機(jī))-> INPUT鏈 -> ...
1.2 發(fā)送過(guò)程
Linux 在網(wǎng)絡(luò)包發(fā)送的過(guò)程中,首先是發(fā)送的路由選擇,然后碰到的第一個(gè) HOOK 就是 OUTPUT,然后接著進(jìn)入 POSTROUTING 鏈。
來(lái)大致過(guò)一下源碼,網(wǎng)絡(luò)層發(fā)送的入口函數(shù)是 ip_queue_xmit。
- //file: net/ipv4/ip_output.c
- int ip_queue_xmit(struct sk_buff *skb, struct flowi *fl)
- {
- // 路由選擇過(guò)程
- // 選擇完后記錄路由信息到 skb 上
- rt = (struct rtable *)__sk_dst_check(sk, 0);
- if (rt == NULL) {
- // 沒(méi)有緩存則查找路由項(xiàng)
- rt = ip_route_output_ports(...);
- sk_setup_caps(sk, &rt->dst);
- }
- skb_dst_set_noref(skb, &rt->dst);
- ...
- //發(fā)送
- ip_local_out(skb);
- }
在這里先進(jìn)行了發(fā)送時(shí)的路由選擇,然后進(jìn)入發(fā)送時(shí)的 IP 層函數(shù) __ip_local_out。
- //file: net/ipv4/ip_output.c
- int __ip_local_out(struct sk_buff *skb)
- {
- struct iphdr *iph = ip_hdr(skb);
- iph->tot_len = htons(skb->len);
- ip_send_check(iph);
- return nf_hook(NFPROTO_IPV4, NF_INET_LOCAL_OUT, skb, NULL,
- skb_dst(skb)->dev, dst_output);
- }
上面的 NF_HOOK 將發(fā)送數(shù)據(jù)包送入到 NF_INET_LOCAL_OUT (OUTPUT) 鏈。執(zhí)行完后,進(jìn)入 dst_output。
- //file: include/net/dst.h
- static inline int dst_output(struct sk_buff *skb)
- {
- return skb_dst(skb)->output(skb);
- }
在這里獲取到之前的選路,并調(diào)用選到的 output 發(fā)送。將進(jìn)入 ip_output。
- //file: net/ipv4/ip_output.c
- int ip_output(struct sk_buff *skb)
- {
- ...
- //再次交給 netfilter,完畢后回調(diào) ip_finish_output
- return NF_HOOK_COND(NFPROTO_IPV4, NF_INET_POST_ROUTING, skb, NULL, dev,
- ip_finish_output,
- !(IPCB(skb)->flags & IPSKB_REROUTED));
- }
總結(jié)下發(fā)送數(shù)據(jù)包流程是:路由選擇 -> OUTPUT鏈 -> POSTROUTING鏈 -> ...
1.3 轉(zhuǎn)發(fā)過(guò)程
其實(shí)除了接收和發(fā)送過(guò)程以外,Linux 內(nèi)核還可以像路由器一樣來(lái)工作。它將接收到網(wǎng)絡(luò)包(不屬于自己的),然后根據(jù)路由表選到合適的網(wǎng)卡設(shè)備將其轉(zhuǎn)發(fā)出去。
這個(gè)過(guò)程中,先是經(jīng)歷接收數(shù)據(jù)的前半段。在 ip_rcv 中經(jīng)過(guò) PREROUTING 鏈,然后路由后發(fā)現(xiàn)不是本設(shè)備的包,那就進(jìn)入 ip_forward 函數(shù)進(jìn)行轉(zhuǎn)發(fā),在這里又會(huì)遇到 FORWARD 鏈。最后還會(huì)進(jìn)入 ip_output 進(jìn)行真正的發(fā)送,遇到 POSTROUTING 鏈。
我們來(lái)過(guò)一下源碼,先是進(jìn)入 IP 層入口 ip_rcv,在這里遇到 PREROUTING 鏈。
- //file: net/ipv4/ip_input.c
- int ip_rcv(struct sk_buff *skb, ......){
- ......
- return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, dev, NULL,
- ip_rcv_finish);
- }
PREROUTING 鏈條上的規(guī)則都處理完后,進(jìn)入 ip_rcv_finish,在這里路由選擇,然后進(jìn)入 dst_input。
- //file: include/net/dst.h
- static inline int dst_input(struct sk_buff *skb)
- {
- return skb_dst(skb)->input(skb);
- }
轉(zhuǎn)發(fā)過(guò)程的這幾步和接收過(guò)程一模一樣的。不過(guò)內(nèi)核路徑就要從上面的 input 方法調(diào)用開(kāi)始分道揚(yáng)鑣了。非本設(shè)備的不會(huì)進(jìn)入 ip_local_deliver,而是會(huì)進(jìn)入到 ip_forward。
- //file: net/ipv4/ip_forward.c
- int ip_forward(struct sk_buff *skb)
- {
- ......
- return NF_HOOK(NFPROTO_IPV4, NF_INET_FORWARD, skb, skb->dev,
- rt->dst.dev, ip_forward_finish);
- }
在 ip_forward_finish 里會(huì)送到 IP 層的發(fā)送函數(shù) ip_output。
- //file: net/ipv4/ip_output.c
- int ip_output(struct sk_buff *skb)
- {
- ...
- //再次交給 netfilter,完畢后回調(diào) ip_finish_output
- return NF_HOOK_COND(NFPROTO_IPV4, NF_INET_POST_ROUTING, skb, NULL, dev,
- ip_finish_output,
- !(IPCB(skb)->flags & IPSKB_REROUTED));
- }
在 ip_output 里會(huì)遇到 POSTROUTING 鏈。再后面的流程就和發(fā)送過(guò)程的下半段一樣了。
總結(jié)下轉(zhuǎn)發(fā)數(shù)據(jù)過(guò)程:PREROUTING鏈 -> 路由判斷(不是本設(shè)備,找到下一跳) -> FORWARD鏈 -> POSTROUTING鏈 -> ...
1.4 iptables 匯總
理解了接收、發(fā)送和轉(zhuǎn)發(fā)三個(gè)過(guò)程以后,讓我們把上面三個(gè)流程匯總起來(lái)。
數(shù)據(jù)接收過(guò)程走的是 1 和 2,發(fā)送過(guò)程走的是 4 、5,轉(zhuǎn)發(fā)過(guò)程是 1、3、5。有了這張圖,我們能更清楚地理解 iptables 和內(nèi)核的關(guān)系。
二、Iptables 的四表
在上一節(jié)中,我們介紹了 iptables 中的五個(gè)鏈。在每一個(gè)鏈上都可能是由許多個(gè)規(guī)則組成的。在 NF_HOOK 執(zhí)行到這個(gè)鏈的時(shí)候,就會(huì)把規(guī)則按照優(yōu)先級(jí)挨個(gè)過(guò)一遍。如果有符合條件的規(guī)則,則執(zhí)行規(guī)則對(duì)應(yīng)的動(dòng)作。
而這些規(guī)則根據(jù)用途的不同,又可以raw、mangle、nat 和 filter。
row 表的作用是將命中規(guī)則的包,跳過(guò)其它表的處理,它的優(yōu)先級(jí)最高。
mangle 表的作用是根據(jù)規(guī)則修改數(shù)據(jù)包的一些標(biāo)志位,比如 TTL
nat 表的作用是實(shí)現(xiàn)網(wǎng)絡(luò)地址轉(zhuǎn)換
filter 表的作用是過(guò)濾某些包,這是防火墻工作的基礎(chǔ)
例如在 PREROUTING 鏈中的規(guī)則中,分別可以執(zhí)行 row、mangle 和 nat 三種功能。
我們?cè)賮?lái)聊聊,為什么不是全部四個(gè)表呢。這是由于功能的不同,不是所有功能都會(huì)完全使用到五個(gè)鏈。
Raw 表目的是跳過(guò)其它表,所以只需要在接收和發(fā)送兩大過(guò)程的最開(kāi)頭處把關(guān),所以只需要用到 PREROUTING 和 OUTPUT 兩個(gè)鉤子。
Mangle 表有可能會(huì)在任意位置都有可能會(huì)修改網(wǎng)絡(luò)包,所以它是用到了全部的鉤子位置。
NAT 分為 SNAT(Source NAT)和 DNAT(Destination NAT)兩種,可能會(huì)工作在 PREROUTING、INPUT、OUTPUT、POSTROUTING 四個(gè)位置。
Filter 只在 INPUT、OUTPUT 和 FORWARD 這三步中工作就夠了。
從整體上看,四鏈五表的關(guān)系如下圖。
這里再多說(shuō)一點(diǎn),每個(gè)命名空間都是有自己獨(dú)立的 iptables 規(guī)則的。我們拿 NAT 來(lái)舉例,內(nèi)核在遍歷 NAT 規(guī)則的時(shí)候,是從 net(命名空間變量)的 ipv4.nat_table 上取下來(lái)的。NF_HOOK 最終會(huì)執(zhí)行到 nf_nat_rule_find 函數(shù)。
- //file: net/ipv4/netfilter/iptable_nat.c
- static unsigned int nf_nat_rule_find(...)
- {
- struct net *net = nf_ct_net(ct);
- unsigned int ret;
- //重要!!!!!! nat_table 是在 namespace 中存儲(chǔ)著的
- ret = ipt_do_table(skb, hooknum, in, out, net->ipv4.nat_table);
- if (ret == NF_ACCEPT) {
- if (!nf_nat_initialized(ct, HOOK2MANIP(hooknum)))
- ret = alloc_null_binding(ct, hooknum);
- }
- return ret;
- }
Docker 容器就是基于命名空間來(lái)工作的,所以每個(gè) Docker 容器中都可以配置自己獨(dú)立的 iptables 規(guī)則。
三、Iptables 使用舉例
看完前面兩小節(jié),大家已經(jīng)理解了四表五鏈?zhǔn)侨绾螌?shí)現(xiàn)的了。那我們接下來(lái)通過(guò)幾個(gè)實(shí)際的功能來(lái)看下實(shí)踐中是如何使用 iptables 的。
3.1 nat
假如說(shuō)我們有一臺(tái) Linux,它的 eth0 的 IP 是10.162.0.100,通過(guò)這個(gè) IP 可以訪問(wèn)另外其它服務(wù)器。現(xiàn)在我們?cè)谶@臺(tái)機(jī)器上創(chuàng)建了個(gè) Docker 虛擬網(wǎng)絡(luò)環(huán)境 net1 出來(lái),它的網(wǎng)卡 veth1 的 IP 是 192.168.0.2。
如果想讓 192.168.0.2 能訪問(wèn)外部網(wǎng)絡(luò),則需要宿主網(wǎng)絡(luò)命名空間下的設(shè)備工作幫其進(jìn)行網(wǎng)絡(luò)包轉(zhuǎn)發(fā)。由于這是個(gè)私有的地址,只有這臺(tái) Linux 認(rèn)識(shí),所以它是無(wú)法訪問(wèn)外部的服務(wù)器的。這個(gè)時(shí)候如果想要讓 net1 正常訪問(wèn) 10.162.0.101,就必須在轉(zhuǎn)發(fā)時(shí)執(zhí)行 SNAT - 源地址替換。
SNAT 工作在路由之后,網(wǎng)絡(luò)包發(fā)送之前,也就是 POSTROUTING 鏈。我們?cè)谒拗鳈C(jī)的命名空間里增加如下這條 iptables 規(guī)則。這條規(guī)則判斷如果源是 192.168.0 網(wǎng)段,且目的不是 br0 的,統(tǒng)統(tǒng)執(zhí)行源 IP 替換判斷。
- # iptables -t nat -A POSTROUTING -s 192.168.0.0/24 ! -o br0 -j MASQUERADE
有了這條規(guī)則,我們來(lái)看下整個(gè)發(fā)包過(guò)程。
當(dāng)數(shù)據(jù)包發(fā)出來(lái)的時(shí)候,先從 veth 發(fā)送到 br0。由于 br0 在宿主機(jī)的命名空間中,這樣會(huì)執(zhí)行到 POSTROUTING 鏈。在這個(gè)鏈有我們剛配置的 snat 規(guī)則。根據(jù)這條規(guī)則,內(nèi)核將網(wǎng)絡(luò)包中 192.168.0.2(外界不認(rèn)識(shí)) 替換成母機(jī)的 IP 10.162.0.100(外界都認(rèn)識(shí))。同時(shí)還要跟蹤記錄鏈接狀態(tài)。
然后宿主機(jī)根據(jù)自己的路由表進(jìn)行判斷,選擇默認(rèn)發(fā)送設(shè)備將包從 eth0 網(wǎng)卡發(fā)送出去,直到送到 10.162.0.101。
接下來(lái)在 10.162.0.100 上會(huì)收到來(lái)自 10.162.0.101 的響應(yīng)包。由于上一步記錄過(guò)鏈接跟蹤,所以宿主機(jī)能知道這個(gè)回包是給 192.168.0.2 的。再反替換并通過(guò) br0 將返回送達(dá)正確的 veth 上。
這樣 net1 環(huán)境中的 veth1 就可以訪問(wèn)外部網(wǎng)絡(luò)服務(wù)了。
3.2 DNAT 目的地址替換
接著上面小節(jié)里的例子,假設(shè)我們想在 192.168.0.2 上提供 80 端口的服務(wù)。同樣,外面的服務(wù)器是無(wú)法訪問(wèn)這個(gè)地址的。這個(gè)時(shí)候要用到 DNAT 目的地址替換。需要在數(shù)據(jù)包進(jìn)來(lái)的時(shí)候,將其目的地址替換成 192.168.0.2:80 才行。
DNAT 工作在內(nèi)核接收到網(wǎng)絡(luò)包的第一個(gè)鏈中,也就是 PREROUTING。我們?cè)黾右粭l DNAT 規(guī)則,具體的配置如下。
- # iptables -t nat -A PREROUTING ! -i br0 -p tcp -m tcp --dport 8088 -j DNAT --to-destination 192.168.0.2:80
當(dāng)有外界來(lái)的網(wǎng)絡(luò)包到達(dá) eth0 的時(shí)候。由于 eth0 在母機(jī)的命名空間中,所以會(huì)執(zhí)行到 PREROUTING 鏈。
該規(guī)則判斷如果端口是 8088 的 TCP 請(qǐng)求,則將目的地址替換為 192.168.0.2:80。再通過(guò) br0(192.168.0.1)轉(zhuǎn)發(fā)數(shù)據(jù)包,數(shù)據(jù)包將到達(dá)真正提供服務(wù)的 192.168.0.2:80 上。
同樣在 DNAT 中也會(huì)有鏈接跟蹤記錄,所以 192.168.0.2 給 10.162.0.101 的返回包中的源地址會(huì)被替換成 10.162.0.100:8088。之后 10.162.0.101 收到包,它一直都以為自己是真的和 10.162.0.100:8088 通信。
這樣 net1 環(huán)境中的 veth1 也可以提供服務(wù)給外網(wǎng)使用了。事實(shí)上,單機(jī)的 Docker 就是通過(guò)這兩小節(jié)介紹的 SNAT 和 DNAT 配置來(lái)進(jìn)行網(wǎng)絡(luò)通信的。
3.3 filter
Filter 表主要實(shí)現(xiàn)網(wǎng)絡(luò)包的過(guò)濾。假如我們發(fā)現(xiàn)了一個(gè)惡意 IP 瘋狂請(qǐng)求我們的服務(wù)器,對(duì)服務(wù)造成了影響。那么我們就可以用 filter 把它禁掉。其工作原理就是在接收包的 INPUT 鏈位置處進(jìn)行判斷,發(fā)現(xiàn)是惡意請(qǐng)求就盡早干掉不處理。避免進(jìn)入到更上層繼續(xù)浪費(fèi) CPU 開(kāi)銷。
具體的配置方法細(xì)節(jié)如下:
- # iptables -I INPUT -s 1.2.3.4 -j DROP //封禁
- # iptables -D INPUT -s 1.2.3.4 -j DROP //解封
當(dāng)然也可以封禁某個(gè) IP 段。
- # iptables -I INPUT -s 121.0.0.0/8 -j DROP //封禁
- # iptables -I INPUT -s 121.0.0.0/8 -j DROP //解封
再比如說(shuō)假設(shè)你不想讓別人任意 ssh 登錄你的服務(wù)器,只允許你的 IP 訪問(wèn)。那就只放開(kāi)你自己的 IP,其它的都禁用掉就好了。
- # iptables -t filter -I INPUT -s 1.2.3.4 -p tcp --dport 22 -j ACCEPT
- # iptables -t filter -I INPUT -p tcp --dport 22 -j DROP
3.4 raw
Raw 表中的規(guī)則可以繞開(kāi)其它表的處理。在 nat 表中,為了保證雙向的流量都能正常完成地址替換,會(huì)跟蹤并且記錄鏈接狀態(tài)。每一條連接都會(huì)有對(duì)應(yīng)的記錄生成。使用以下兩個(gè)命令可以查看。
- # conntrack -L
- # cat /proc/net/ip_conntrack
但在高流量的情況下,可能會(huì)有連接跟蹤記錄滿的問(wèn)題發(fā)生。我就遇到過(guò)一次在測(cè)試單機(jī)百萬(wàn)并發(fā)連接的時(shí)候,發(fā)生因連接數(shù)超過(guò)了 nf_conntrack_max 而導(dǎo)致新連接無(wú)法建立的問(wèn)題。
- # ip_conntrack: table full, dropping packet
但其實(shí)如果不使用 NAT 功能的話,鏈接跟蹤功能是可以關(guān)閉的,例如。
- # iptables -t raw -A PREROUTING -d 1.2.3.4 -p tcp --dport 80 -j NOTRACK
- # iptables -A FORWARD -m state --state UNTRACKED -j ACCEPT
3.5 mangle
路由器在轉(zhuǎn)發(fā)網(wǎng)絡(luò)包的時(shí)候,ttl 值會(huì)減 1 ,該值為 0 時(shí),最后一個(gè)路由就會(huì)停止再轉(zhuǎn)發(fā)這個(gè)數(shù)據(jù)包。如若不想讓本次路由影響 ttl,便可以在 mangel 表中加個(gè) 1,把它給補(bǔ)回來(lái)。
- # ptables -t mangle -A PREROUTING -i eth0 -j TTL --ttl-inc 1
所有從 eth0 接口進(jìn)來(lái)的數(shù)據(jù)包的 ttl 值加 1,以抵消路由轉(zhuǎn)發(fā)默認(rèn)減的 1。
總結(jié)
Iptables 是一個(gè)非常常用,也非常重要的工具。Linux 上的防火墻、nat 等基礎(chǔ)功能都是基于它實(shí)現(xiàn)的。還有現(xiàn)如今流行的的 Docker、Kubernets、Istio 項(xiàng)目中也經(jīng)常能見(jiàn)著對(duì)它的身影。正因?yàn)槿绱耍陨钊肜斫?iptables 工作原理是非常有價(jià)值的事情。
今天我們先是在第一節(jié)里從內(nèi)核接收、發(fā)送、轉(zhuǎn)發(fā)三個(gè)不同的過(guò)程理解了五鏈的位置。
接著又根據(jù)描述了 iptables 從功能上看的另外一個(gè)維度,表。每個(gè)表都是在多個(gè)鉤子位置處注冊(cè)自己的規(guī)則。當(dāng)處理包的時(shí)候觸發(fā)規(guī)則,并執(zhí)行。從整體上看,四鏈五表的關(guān)系如下圖。
最后我們又分別在 raw、mangle、nat、filter 幾個(gè)表上舉了簡(jiǎn)單的應(yīng)用例子。希望通過(guò)今天的學(xué)習(xí),你能將 iptables 徹底融會(huì)貫通。相信這一定會(huì)對(duì)你的工作有很大的幫助的!
原文鏈接:https://mp.weixin.qq.com/s/O084fYzUFk7jAzJ2DDeADg