1. 程式人生 > >繼iptables之後的新一代包過濾框架是nftables

繼iptables之後的新一代包過濾框架是nftables

                夜已深然未央,準備接著講述有關Netfilter的故事,行文有點鬆散,由於未打草稿,有點隨意識而流,一氣呵成不知是自誇還是自嘲,權當小時候寫的日記吧,自幼喜歡每天寫日記,中學時更是以退士為名折騰了幾箱子抄本,前幾年由於喝酒就改為週記了,現在意識到了生命短暫,時間甚是不夠用,不能在迷迷糊糊中得過且過,就準備把自己知道的關於Linux網路的東西一點一滴記錄下來,本來想繼續行文於紙上,然而發現在個人電腦智慧手機時代,很多字早就不會寫了...上回沒有說完關於iptables的故事,本文繼續...

一.nftables前傳-iptables之弊端

iptables幾乎是無人不知無人不曉,人們被圈入了框框也就覺得任何事情都是理所當然,但我例外,和其他很多事情一樣,在這個領域,我依然做並且樂於做那個“被排除的人”。
       iptables的諸多弊端已經不能再視而不見,然而只有很少的人看到了這些,大多數的人作為使用者,僅僅是使用罷了。在此我不會吐嘈很多了。以下的弊端來自nftables的宣傳文件,但是即使是在國外,也引發了超級多的爭論:
1.iptables框架在核心態知道的太多,以至於產生了大量的程式碼冗餘。
這一點是顯而易見的,比如對於TCP和UDP而言,取sport,dport沒有什麼不同,但是iptables卻使用了兩套程式碼,這只是一個例子,類似的還有很多。
2.iptables的rule結構設計不合理。

這是要著重說明的。

1.iptables的結構

iptables由表,鏈,規則組成,其中規則又由match,target組成。如下面的結構所示:
Table{
    Chain[
        Rule
            (match,match,match,...)
            ->target,
        Rule
            (match,match,match,...)
            ->target,
        ...
    ],
    Chain[
        ...
    ],
    ...
}

2.iptables的規則匹配執行流程

iptables的規則是按照配置順序順序匹配的,在每一張表的每一個鏈上依次匹配每一條規則,在每一條規則依次匹配每一個match,全部匹配的match執行該規則的target,由target決定:
a.繼續匹配下一條規則
b.對資料包做一些修改
c.跳轉到其它的鏈(即開始從該鏈依次匹配該鏈上的每一條規則)
d.返回引發跳轉的鏈(即繼續匹配跳轉前的鏈的下一條規則)
e.丟棄資料包
f.接收資料包(即不再繼續往下匹配,直接返回)
g.記錄日誌
h....

整個iptables框架執行的流程如下:
迴圈1: static breakrule = 0; 遍歷一個chain的每一條rule { nomatch = 0; 迴圈2:遍歷一條rule的每一個match {   result = rule->match[curr](skb, info);   if(result != MATCH) {    nomatch = 1;    break;   } } if (nomatch == 1) {  continue該chain的下一條rule; } result = rule->target(skb, info); if (result == DROP) {  break丟棄資料包  } else if (result == ACCEPT) {  break接受資料包 } else if (result == GOTO) {  breakrule = rule;  跳轉到相應的chain,執行迴圈1 } else if (result == RETURN) {  break返回呼叫chain,執行其breakrule的下一條rule } ...}
看了上述的程式碼就基本知道了iptables的命令實現,程式設計師能做的就是擴充套件iptables的功能,具體的做法有兩個:寫一個match以及寫一個target。除此之外,程式設計師就沒轍了,剩下的就看使用者的想象力了...
       通過上面的流程,可以發現,包過濾的流程最終要落實到規則匹配,而過濾的動作最終落實到了該規則的target,前面的所有的match匹配返回結果就是0或者非0表示是否匹配,只有所有的match均匹配,才會執行target。這就決定了下面幾件事:
a.如果你想實現多個target,就不得不寫多條規則
比如實現log和drop,那麼就要寫兩條規則,或者擴充套件一個LOG_and_DROP target,前者影響效率,後者需要程式設計。你很在乎效能,同時你又不是程式設計師不懂程式設計,你就抓狂了...
b.你可以寫一個match,在裡面偷偷摸摸做一點事情,但是外部不知道
這一切太不正規了,你可以在一個match裡面把一個數據包的校驗碼改掉,也可以在裡面做log,做NAT什麼的,但是iptables的框架的本意雖不允許你這麼做但是又沒有阻止你的行為。
       我們可以在iptables執行流的一個細節(上述的流程中未畫出)中看到另一個細節,即iptables在match中僅僅確定“是否匹配”真的已經很不夠,就連程式碼都設計得很勉強。如果你看ipt_do_table這個核心函式,會發現一個控制變數名叫hotdrop,這個變數是幹什麼的呢?按照註釋的意思是:
@hotdrop:    drop packet if we had inspection problems
這個hotdrop作為傳出引數傳入每一個match回撥函式,用於在match內部指示將一個數據包丟棄。這就暴露出了設計的不足,丟棄一個數據包不是target要做的嗎?一個match的職責是抉擇該資料包是否匹配,幹嘛要指示丟棄它呢?這不是越級麼?這只是一個細節,你可以說出一千個理由表明它是合理的,但是它卻是醜陋的!

二.一點小歷史

弄清楚歷史總是能明白更多,這絕對是一句真話,但是恰恰是專業化阻止了大多數的程式設計師去讀歷史,哪怕是IT的歷史。最好的歷史資料就是原著,Netfilter的歷史不長,從Linux 2.3.15核心版本被引入至今,不會像老子莊子那樣被篡改地體無完膚。
       我們當然要看iptables被引入的那段歷史。
       iptables被引入旨在替掉ipchains,因為當時ipchains的維護者Rusty Russell認識到它擁有諸多的弊端。總的說來,弊端有兩個,其它的都是由這兩個而發:
a.核心的firewall框架僅僅設定了3個檢查點,即input,forward,output,對於環回包以及indev,outdev的控制力很弱;
b.程式碼寫死,匹配項固定,沒有可擴充套件性。
問題就在這裡的b。針對問題a,Rusty Russell提出了Netfilter的設計,精心設計了5個HOOK點,解決了幾乎所有的控制點的問題,特別是OUTPUT點的設計頂級絕妙,它被安放在路由之後,原因在於Linux協議棧的路由操作之後才會給出完整的過濾匹配項,比如源IP地址,出口裝置等,路由之後的OUTPUT同時給了呼叫者再次路由的許可權。FORWARD和INPUT作為路由的二分,同時保持了無用功最少化,因為如果你沒有開啟ip_forward選項,即便不是INPUT的資料包也不會進入FORWARD,如果根本就沒有找到路由,則既不會到達INPUT,也不會到達FORWARD。對於PREROUTING而言,它可以通過conntrack區分本地環迴流量和網絡卡進入流量...不管怎麼說,這是核心的工作,這個Netfilter的設計十分完美,至今依然被使用。
       對於問題b,Rusty Russell提出了iptables,它是一個高度可擴充套件的框架,也就是從此時起,iptables擁有了match/target配對的擴充套件方式,每當需要擴充套件的時候,每一個match/target除了有使用者態的lib之外,還有用核心態的支援,它將ipchains時代的固定匹配模式變成了可以自己程式設計擴充套件的了。
       針對ipchains的弊端,Rusty Russell可謂是給出了完美的解決方案,然而僅此而已!任何一個來自同一作者的新的框架幾乎均是為了解決上一個框架的弊端的,iptables作為一個新秀,在獲得歡呼的時候,不會有人去考慮它的弊端,任何事情都是這樣,不是嗎?
       iptables的弊端是被逐步發現的,Rusty Russell作為ipchains和iptables的共同作者,它對待後者取代前者的態度永遠都是保守的,一個全新的框架需要另一個人或者團隊來提出,而不可能出現在Rusty Russell本人手裡以及iptables團隊的內部。
       針對Netfilter,Rusty寫了大量的文件,均在Netfilter網站上可以找到:http://people.netfilter.org/rusty/unreliable-guides/不可否認,這些都是珍貴的第一手資料,對於我們理解Netfilter,可能沒有比這些更好的了。任何人都可以從這些原著中找到“XX為何會這樣”這種問題的蛛絲馬跡,同時它們也是指導你如何改進現有框架的明燈。

三.nftables登場

鑑於iptables的諸多缺點(其實並不是缺點,但是match/target配對的擴充套件方式導致了開發者延伸了劣勢,最終將其確定為缺點),nftables旨在一個個地改進它們。首先是兩個問題:
a.如何使用一種統一的方式來解析資料包
在這個問題的解決上,u32 match給了作者思路
b.如何執行多個action
事實上,是iptables的matches/target配對的方式限制了開發者的思路。為何非要區分match和target呢?iptables框架的執行流程限制了match的結果就是個布林型,所有的動作都在target中執行,如果去掉了這個限制,將整個流程都開放給開發者,那就靈活多了。nftables就在這樣的背景下登場了。事實證明,nftables做的比修正弊端更多,走的更遠。
       首先nftables採用了“虛擬機器解釋位元組碼”的方式,使一條rule真的成了“為一個數據包做一些事情”這樣靈活的命令,而去掉了“匹配所有的match之後執行一個target”這樣的限制。虛擬機器執行位元組碼的方式早就被BPF採用了,我們熟知的tcpdump抓包程式就是利用的它來過濾的資料包。我們來看一下nftables框架的執行流程:
迴圈1: static breakrule = 0; 遍歷一個chain的每一條rule { nomatch = 0; reg[MAX] 迴圈2:遍歷一條rule的每一個expression {   void rule->expression[curr]->operations->expr(skb, info, reg)   if(reg[VERDICT] != CONTINUE) {    break;   } } if (reg[VERDICT] == CONTINUE) {  continue該chain的下一條rule; } else if (reg[VERDICT] == DROP) {  break丟棄資料包  } else if (reg[VERDICT] == ACCEPT) {  break接受資料包 } else if (reg[VERDICT] == GOTO) {  breakrule = rule;  跳轉到相應的chain,執行迴圈1 } else if (reg[VERDICT] == RETURN) {  break呼叫chain,執行其breakrule的下一條rule } ...}
光從這個流程上看,就已經可以和iptables分出勝負了。可以看到,nftables沒有match和target,而是將一條rule抽象成了若干條的表示式,即expression,所謂的表示式就是一個主語加謂詞的式子,它是“可執行”的,它可以“做任何事情”,而不僅僅是計算一個匹配結果。除此之外,nftables內建了一組暫存器,其中之一是verdict暫存器,它指示了“下一步要怎麼做”。每一條expression執行完了之後,會取出該暫存器,由該暫存器的值來採取下一步的行動。這個verdict暫存器替換了iptables中target返回值,這就可以在一條rule中採取多個動作,每條動作可以解析成一個expression,每一個expression在執行後將verdict暫存器設定為CONTINUE即可!
       除了執行流程的顯著區別之外,nftables最大的意義在於它對expression進行了抽象,nftables的核心框架可以註冊很多種的expression,每一種都有一個操作集,其中expr回撥函式執行具體的expression表示式。典型的expression有:
payload表示式:
將一個數據包的某一段資料拷貝到一個nftables暫存器指示的緩衝區,除了出錯之外verdict暫存器均為CONTINUE。
compare表示式:
將某段資料和nftables暫存器指示的緩衝區作比較,若不相等則設定verdict暫存器為BREAK。
counter表示式:
按照資料包的大小遞增位元組計數器以及包計數器的值,保持verdict暫存器為CONTINUE。
log表示式:
對資料包記錄日誌,保持verdict暫存器為CONTINUE。
nat表示式:
按照nftables暫存器的數值對資料包做NAT,verdict暫存器設定為NAT操作的結果,注意,NAT的參考資料均來自nftables暫存器。
compat表示式:
保持對iptables的相容性。細分為match暫存器以及target暫存器,其中match表示式呼叫iptables rule的match,若匹配設定verdict暫存器為CONTINUE,否則為BREAK;target表示式呼叫iptables rule的target,並根據target的結果設定verdict暫存器。
...
回到nftables的執行流程,結合上述的表示式,看看這一切像什麼?
       這難道不就是一個直譯器嗎?類似高階語言比如Python的直譯器,將每一個表示式解釋執行,我們可以將一條nftables rule分解為一系列的表示式,僅此而已,如下所示:
expr1:reg[verdict] = CONTINUE;reg[0] = skb[m...n];expr2:info[0] = something from userspace;ret = compare(reg[0], info[0]);if (ret == true) ; then reg[verdict] = CONTINUE; else reg[verdict] = BREAK; break; fiexpr3:log_packet(skb);expr4:ret = do_nat_packet(skb, reg[i]/*address to trans*/...); if (ret == true) ; then reg[verdict] = CONTINUE; else reg[verdict] = BREAK; break; fi...
看看吧,一條規則做了多少事情啊啊啊!直譯器按照expr1到expr4的順序執行expression,每次執行下一個expression之前要檢查verdict暫存器,那麼誰是直譯器,當然就是上面的nftables的執行流程啦!
       nftables到底是什麼玩意兒?實則一個虛擬機器!那麼這部虛擬機器執行的指令來自何方?來自使用者態的配置。使用者態怎麼配置?當然是使用nft命令。nft是什麼命令?是類似iptables的命令。nft命令能否舉一個例子?能:
nft add rule ip filter input ip saddr 1.1.1.1 drop
這條命令怎麼和諸多的表示式對應?答案是nft命令列工具內建了一個”編譯器“,將一條human readable命令編譯成了一個個的expression程式碼。編譯的細節是什麼?可以寫一本書,但是瞭解一下tcpdump的方式也就該能理解了。tcpdump命令最終會將編譯好的指令注入到核心的BPF系統,以下是一條很常見的tcpdump命令:
tcpdump -i eth0 dst 1.1.1.1
它會翻譯成什麼程式碼呢?後面跟上-dd引數就可以看出來:
[email protected]:/usr/local/etc/nftables# tcpdump -i eth0 dst 1.1.1.1 -dd
{ 0x28, 0, 0, 0x0000000c },
{ 0x15, 0, 2, 0x00000800 },
{ 0x20, 0, 0, 0x0000001e },
{ 0x15, 4, 5, 0x01010101 },
{ 0x15, 1, 0, 0x00000806 },
{ 0x15, 0, 3, 0x00008035 },
{ 0x20, 0, 0, 0x00000026 },
{ 0x15, 0, 1, 0x01010101 },
{ 0x6, 0, 0, 0x0000ffff },
{ 0x6, 0, 0, 0x00000000 },

具體是什麼意思請參考BPF的手冊。nftables設定的規則最終也會被”編譯“成類似的”指令“注入到核心的nftables系統,形成一個個的expression。要注意,並不是所有的規則指令都是可以編譯的,比如iptables相容指令就不能編譯,log指令也不能被編譯。
       nftables就是這樣一個具有美感的包過濾框架,理解了它的執行方式之後,你就可以擴充套件它了,和iptables擴充套件match/target不同,對於nftables,你只需要擴充套件expression即可,就是說你要自己編寫expression,然後讓nftables虛擬機器(即上面的執行流程)執行它就可以了。最後,我們來看一下nftables框架的結構:
Table{
    Chain[
        Rule
            (expression,expression,expression,...)
        Rule
            (expression,expression,expression,...)
        ...
    ],
    Chain[
        ...
    ],
    ...
}
expression := expression | datatype | operation | expression | datatype
operation := + | - | * | / | memcpy | contain | ...
datatype := u8 | u16 | u16 | ... | container | ...
container := hash | map | tree | set | list | array | ...

四.這一切到底怎麼了

值得注意的是,雖然nftables在美學角度上完勝iptables,但是作為一個框架,它的效能並不十分高效。和nf-hipac相比,iptables並不比nftables輸得更慘些。事實上,nftables和iptables一樣,對於一條chain上的所有rule,也是逐條遍歷的,所不同的只是遍歷每條rule時執行具體匹配的方式有所不同。那麼和nf-hipac相比,nftables為何成功了?
       其實nftables還遠遠沒有成功,它的阻力不是來自效能,而是來自iptables的陣營!nftables作為一個優美的框架,考慮的不僅僅是效能,事實上效能只是其考慮的極小的一方面。作為一個框架,它首先要考慮的是擴充套件性以及和iptables的相容性。反對的聲音分分鐘響徹於耳,iptables並沒有錯,match/target配對的方式並沒有錯,match就是要返回true or false,最終的結果就是要target來執行,總之就是要區分match和target,並且各司其職!!
       覺得iptables不能執行多個動作的為何不自己寫一個可以執行多個動作的target啊啊啊?!
       覺得iptables逐條執行且不能完成nf-hipac創舉的為何不將nf-hipac封裝成一個單獨的match啊啊啊?!
       封裝成單獨match的nf-hipac看起來會是:
iptables -A INPUT -m hipac --match-hipac hipac_test -j NOTHING
nf-hipac create hipac_test
nf-hipac add hipac_test -s 1.1.1.1 -j DROP
...

看到iptables的優勢了吧,人家根本就不是來和nf-hipac拼效能的,人家是海納百川的,人家有容乃大!難道姓毛的椅子男要上戰場拚刺刀嗎?NO!NO!NO!姑且不談nf-hipac,和上面類似的還有ipset,ipset不就是被封裝成一個match而和iptables聯動的嗎?iptables並不差,錯在人們根本就不該直接將每一個簡單功能擴充套件成一套match/target聯合體,最終形成令人作嘔的程式碼!是這樣嗎?
       好吧!我承認上面的YY都是對的!但是看看nftables,它是不是也可以這麼玩並且玩得更high呢?!是啊!是吧!nftables內部直接內建了諸多的容器類資料型別,比如rbtree,hash等,作為一種複合容器,你往裡面放東西就是了,還用寫match嗎?我這麼說的意思是,寫過match/target的都知道,要例行多少公事啊,你要註冊match或者target,還要複製很多管理程式碼,看過xtables-addons的都知道其中之苦。使用nftables的話,如果你想為iptables擴充套件一個功能模組,很多工作都可以在使用者態完成,換句話說,如果僅僅是基於skb(即資料包)的內容做過濾,那麼nftables便是協議無關的,因為不管什麼協議,你都可以將過濾表示式用payload,compare,bit等簡單的expression開表示,協議解析的工作在使用者態編譯nftables指令的時候完成即可,到了核心態,nftables虛擬機器只執行表示式,不管協議!
       世界在向前走,我們要向前看!看看Linux的包過濾框架,從最初的移植BSD的實現,到現在的nftables,中間經歷了多少的坎坷曲折,每次有新東西進來總是會有復古者的謾罵!這下可好,這下可好,Linux核心在主幹上直接內建了nftables的支援,正如當初Netfilter進入主幹時的情形一樣。
       Just do it,劃時代的nftables,我並不是說它有多麼猛,而是說,它真的很乾淨。
       tables,chains,這名字叫得真不好,可是無論如何它也只是個名字而已。iptables要不是因為名字,我也不會為了理解它糾結那麼久,現在又來個nftables...Cisco管類似的東西叫做list,即ACL,也只是個名字,如果說tables不好聽,list是不是顯得更低階呢?不管低階不低階,華為也延續了Cisco的叫法。因此下一代的包過濾框架也叫做tables,估計顯示文化上的認同要比其實質更加有用吧,特別是對待起名字這件事上。iptables已經深入人心,nftables這個名字會讓人更容易接受。當初iptables替代ipchains,那是革命性的替換,而這次,更多的顯示出來的是成熟Linux機制的一種自然而然的演變,或者說進化更合適些吧。

五.nftables用起來

我第一時間想的是在2.6.32上將nftables跑起來,然而失敗了,根本就沒有辦法編譯。那麼下面就是想辦法了,看了很多的宣傳文件和HOWTO以及nftables的主站,花了好久clone了git映像,編譯通過,終於跑起來了。
       後來我乾脆直接在kernel.org上下載3.17版本的核心,在make config的時候將nft相關的東西都給選上,然後編譯更新核心。同時下載使用者態的nftables-0.3版本utils,編譯之,過程中缺什麼補什麼,最終很順利。在make install之後,首先執行:
nft -f /usr/local/etc/nftables/ipv4-filter
這條命令是在核心中載入了filter表,除了執行預先配置好的檔案,你也可以手工載入table。
       在table,chain就緒之後,就是在自己希望的chain上新增rule了:
nft add rule ip filter output  ip daddr 1.2.3.4 drop
其它的用法請man nft,非常詳細但不詳盡的文件,另外的好資料在https://home.regit.org/netfilter-en/nftables-quick-howto以及https://home.regit.org/2014/01/why-you-will-love-nftables
       我為何沒有將其移植到低版本的核心上呢?因為我覺得這太簡單了,為何出此狂言?因為nftables僅僅和Netfilter的nf_register/unregister_hooks介面,其它的都是其框架內部做的,其複雜性在於nft_expr_ops,而這個是非常獨立的,和既有的核心沒有任何關係。對於使用者態utils,本身就有一個nftables專案存在,就是一個編譯問題,而這個編譯是本來就能通過編譯的。

六.配置防火牆變成了程式設計

本文的最後,我來從全域性的角度看一下nftables和iptables的最終區別。
       我已經從內部原理的角度分析了nftables帶來的改變,那麼這些給使用者到底能帶來多少實惠呢?如果沒有實惠,那麼在使用者群中是不會有動力切換到nftables的。實惠不多,只有一個,但是僅此就夠了,那就是:nftables讓使用者可以按照程式設計的思想來組織自己的配置邏輯。
       怎麼說呢?我們來看一個wiki上展示的例子吧:
nft add rule ip filter input ip protocol vmap { tcp : jump tcp-chain, udp : jump udp-chain , icmp : jump icmp-chain }
這是什麼?這是一條“程式設計語句”,它擁有一個簡單的if-else if-else if邏輯,或者你把它當成switch-case也可以。注意,這可是在一條規則中完成的!如果用iptables的話,你不得不獨立寫多條規則。以上的語句多麼像是:
proto = skb->net_hdr->proto;if (proto == tcp) {    tcp_chain(skb);} else if (proto == udp) {    udp_chain(skb);} else if (proto == icmp) {    icmp_chain(skb);}
nftables變成了真正的程式語言!既然成了程式語言,如果支援變數將會是多麼靈活的一件事啊,幸運的是,哦,不,不能說幸運,而是nftables原生的性質,nftables支援“變數”!注意下面的命令:
nft add set filter blackhole { type ipv4_addr\;}
nft add rule ip input ip saddr @blackhole drop
nft add element filter blackhole { 192.168.1.4, 192.168.1.5 }

雖然iptables的ipset match也可以這樣做,但是那畢竟只是一個match而已,nftables原生就支援這種語法!甚至,甚至還可以用字典對映策略的語法:
nft add map filter mydict { type ipv4_addr : verdict\; }
nft add rule filter input ip saddr vmap @mydict
nft add element filter mydict { 192.168.0.10 : drop, 192.168.0.11 : accept }

這樣一來,管理員將會像程式設計師一樣靈活組織自己的邏輯。

七.一個有點悲觀的事實

去搜一下ipchain的文件,幾乎沒有幾個能開啟的,然後去搜iptables的,目前人氣還很旺,nftables的呢?能搜到結果,但是想用起來要費點勁。這就是前仆後繼的過程,可以設想將來的某天,nftables也會像ipchain一樣逐漸冷淡,淡出人們的視線...這難道就是Linux環境包過濾框架的宿命嗎?
       這並不是技術發展的必然,老牌的UNIX工具vi,Emacs直到現在依然是黑客們的利器,網路工具netcat也是小巧便攜經久不衰...而Linux的包過濾框架短短的15年間更新換代了多次。令人感到希望的是Netfilter這個底層的框架基本已經穩定,不管是iptables還是nftables,都是基於Netfilter來開發的,而早期的ipfw則不是,那時的(Linux 2.3.15核心之前的)底層包過濾框架及其簡陋,因此Netfilter一出現就上位了。值得注意的是,這裡麵包含了太多的是開發者Rusty Russell的個人風格,Netfilter是他完成的,ipchains也是他,這不禁讓人想起了破立有秩的Ingo Molnar,引入了O(1)排程器,然後卻用更好的CFS排程器替換了它...UNIX則非常不同,個人因素少之又少...