讓 Linux 防火牆新秀 nftables 為你的 VPS 保駕護航
- 2020 年 3 月 26 日
- 筆記
上篇文章 給大家介紹了 nftables 的優點以及基本的使用方法,它的優點在於直接在用戶態把網路規則編譯成位元組碼,然後由內核的虛擬機執行,儘管和 iptables 一樣都是基於 netfilter,但 nftables 的靈活性更高。
之前用 iptables 匹配大量數據時,還得需要 ipset 配合,而 nftables 直接內置了集合和字典,可以直接匹配大量的數據,這一點比 iptables 方便多了,拿來練練魔法真是極好的,不多解釋,請直接看 Linux全局智慧分流方案。
本文將會教你如何配置 nftables 來為伺服器實現一個簡單的防火牆,本文以 CentOS 7 為例,其他發行版類似。
1. 安裝 nftables
首先需要安裝 nftables:
$ yum install -y nftables 由於 nftables 默認沒有內置的鏈,但提供了一些示例配置,我們可以將其 include 到主配置文件中。主配置文件為 /etc/sysconfig/nftables.conf,將下面一行內容取消注釋:
# include "/etc/nftables/inet-filter" 然後啟動 nftables 服務:
$ systemctl start nftables 現在再次查看規則,就會發現多了一張 filter 表和幾條鏈:
$ nft list ruleset table inet filter { chain input { type filter hook input priority 0; policy accept; } chain forward { type filter hook forward priority 0; policy accept; } chain output { type filter hook output priority 0; policy accept; } } 在 nftables 中,ipv4 和 ipv6 協議可以被合併到一個單一的地址簇 inet 中,使用了 inet 地址簇,就不需要分別為 ipv4 和 ipv6 指定兩個不同的規則了。
2. 添加 INPUT 規則
和 iptables 一樣,nftables 的 filter 表包含三條鏈:INPUT、FORWARD 和 OUTPUT,一般配置防火牆只需要配置 INPUT 鏈就好了。
迴環介面
首先允許訪問 localhost:
$ nft add rule inet filter input iif "lo" accept $ nft add rule inet filter input iif != "lo" ip daddr 127.0.0.0/8 drop 可以再優化一下,加上註解(comment)和計數器(counter):
$ nft add rule inet filter input iif "lo" accept comment "Accept any localhost traffic" $ nft add rule inet filter input iif != "lo" ip daddr 127.0.0.0/8 counter drop comment "drop connections to loopback not coming from loopback" 查看規則:
$ nft list chain inet filter input table inet filter { chain input { type filter hook input priority 0; policy accept; iif "lo" accept comment "Accept any localhost traffic" iif != "lo" ip daddr 127.0.0.0/8 counter packets 0 bytes 0 drop comment "drop connections to loopback not coming from loopback" } } 連接跟蹤模組
接下來的規則用到一個內核模組叫 conntrack(connection tracking),它被用來跟蹤一個連接的狀態。最常見的使用場景是 NAT,為什麼需要跟蹤記錄連接的狀態呢?因為 nftables 需要記住數據包的目標地址被改成了什麼,並且在返回數據包時再將目標地址改回來。
和 iptables 一樣,一個 TCP 連接在 nftables 中總共有四種狀態:NEW,ESTABLISHED,RELATED 和 INVALID。
除了本地產生的包由 OUTPUT 鏈處理外,所有連接跟蹤都是在 PREROUTING 鏈里進行處理的,意思就是, iptables 會在 PREROUTING 鏈里從新計算所有的狀態。如果我們發送一個流的初始化包,狀態就會在 OUTPUT 鏈里被設置為 NEW,當我們收到回應的包時,狀態就會在 PREROUTING 鏈里被設置為 ESTABLISHED。如果收到回應的第一個包不是本地產生的,那就會在 PREROUTING 鏈里被設置為 NEW 狀態。綜上,所有狀態的改變和計算都是在 nat 表中的 PREROUTING 鏈和 OUTPUT 鏈里完成的。
還有其他兩種狀態:
RELATED: RELATED 狀態有點複雜,當一個連接與另一個已經是ESTABLISHED的連接有關時,這個連接就被認為是 RELATED。這意味著,一個連接要想成為 RELATED,必須首先有一個已經是ESTABLISHED的連接存在。這個 ESTABLISHED 連接再產生一個主連接之外的新連接,這個新連接就是 RELATED 狀態了。INVAILD: 表示分組對應的連接是未知的,說明數據包不能被識別屬於哪個連接或沒有任何狀態。有幾個原因可以產生這種情況,比如,記憶體溢出,收到不知屬於哪個連接的 ICMP 錯誤資訊。我們需要 DROP 這個狀態的任何東西,並列印日誌:
$ nft add rule inet filter input ct state invalid log prefix "Invalid-Input: " level info flags all counter drop comment "Drop invalid connections" 查看規則:
$ nft list chain inet filter input table inet filter { chain input { type filter hook input priority 0; policy accept; iif "lo" accept comment "Accept any localhost traffic" iif != "lo" ip daddr 127.0.0.0/8 counter packets 0 bytes 0 drop comment "drop connections to loopback not coming from loopback" ct state invalid log prefix "Invalid-Input: " level info flags all counter packets 0 bytes 0 drop comment "Drop invalid connections" } } 令牌桶
為了防止有惡意攻擊者利用 ping 泛洪(ping flood)來進行攻擊,可以利用令牌桶模型來對 ping 包限速。ping 泛洪的原理很簡單,就是採用多執行緒的方法一次性發送多個 ICMP 請求報文,讓目的主機忙於處理大量這些報文而造成速度緩慢甚至宕機。
先來介紹一下令牌桶模型。
熟悉 iptables 的朋友應該知道,iptables 通過 hashlimit 模組來實現限速的功能,而 hashlimit 的匹配方式就是基於令牌桶(Token bucket)的模型,nftables 也類似,
令牌桶是一種網路通訊中常見的緩衝區工作原理,它有兩個重要的參數,令牌桶容量 n和令牌產生速率 s:
令牌桶容量 n:可以把令牌當成是門票,而令牌桶則是負責製作和發放門票的管理員,它手裡最多有n張令牌。初始時,管理員開始手裡有 n 張令牌,每當一個數據包到達後,管理員就看看手裡是否還有可用的令牌。如果有,就把令牌發給這個數據包,limit 就告訴nftables,這個數據包被匹配了,而當管理員把手上所有的令牌都發完了,再來的數據包就拿不到令牌了;這時,limit 模組就告訴 nftables ,這個數據包不能被匹配。令牌產生速率 s:當令牌桶中的令牌數量少於 n,它就會以速率 s 來產生新的令牌,直到令牌數量到達 n 為止。
通過令牌桶機制,可以有效的控制單位時間內通過(匹配)的數據包數量,又可以容許短時間內突發的大量數據包的通過(只要數據包數量不超過令牌桶 n),真是妙哉啊。
nftables 比 iptables 做的更絕,它不僅可以基於數據包來限速,也可以基於位元組來限速。為了更精確地驗證令牌桶模型,我們選擇基於位元組來限速:
$ nft add rule inet filter input ip protocol icmp icmp type echo-request limit rate 20 bytes/second burst 500 bytes counter accept comment "No ping floods" 上面的規則表示:
- 為所有
echo-request類型的 ICMP 包建立一個匹配項; - 匹配項對應的令牌桶容量為
500個位元組; - 令牌產生速率為
20位元組/s
再添加一條規則,拒絕不滿足上訴條件的數據包:
$ nft add rule inet filter input ip protocol icmp icmp type echo-request drop comment "No ping floods" 同時還要接收狀態為 ESTABLISHED 和 RELATED 的數據包:
$ nft add rule inet filter input ct state { established, related } counter accept comment "Accept traffic originated from us" 下面來做個實驗,直接 ping 該伺服器的 IP 地址,ping 包大小設置為 100 位元組,每秒發送一次:
$ ping -s 92 192.168.57.53 -i 1 PING 192.168.57.53 (192.168.57.53) 92(120) bytes of data. 100 bytes from 192.168.57.53: icmp_seq=1 ttl=64 time=0.402 ms 100 bytes from 192.168.57.53: icmp_seq=2 ttl=64 time=0.373 ms 100 bytes from 192.168.57.53: icmp_seq=3 ttl=64 time=0.465 ms 100 bytes from 192.168.57.53: icmp_seq=4 ttl=64 time=0.349 ms 100 bytes from 192.168.57.53: icmp_seq=5 ttl=64 time=0.411 ms 100 bytes from 192.168.57.53: icmp_seq=11 ttl=64 time=0.425 ms 100 bytes from 192.168.57.53: icmp_seq=17 ttl=64 time=0.383 ms 100 bytes from 192.168.57.53: icmp_seq=23 ttl=64 time=0.442 ms 100 bytes from 192.168.57.53: icmp_seq=29 ttl=64 time=0.464 ms ... 首先我們能看到前 5 個包的回應都非常正常,然後從第 6 個包開始,我們每 6 秒能收到一個正常的回應。這是因為我們設定了令牌桶的容量為 500 個位元組,令牌產生速率為 20 位元組/s,而發包的速率是每秒鐘 100 個位元組,即每個包 100 個位元組,當發完 5 個包後,令牌桶的容量變為 0,這時開始以 20 位元組/s 的速率產生新令牌(和前面提到的令牌桶演算法不太一樣,只有當令牌桶容量為 0 才開始產生新的令牌),5 秒鐘之後,令牌桶的容量變為 100 個位元組,所以 6 秒鐘後又能收到正常回應。
ICMP & IGMP
接收其他類型的 ICMP 協議數據包:
$ nft add rule inet filter input ip protocol icmp icmp type { destination-unreachable, router-advertisement, router-solicitation, time-exceeded, parameter-problem } accept comment "Accept ICMP" 接收 IGMP 協議數據包:
$ nft add rule inet filter input ip protocol igmp accept comment "Accept IGMP" 分別處理 TCP 和 UDP
這一步我們將 TCP 和 UDP 的流量拆分,然後分別處理。先創建兩條鏈:
$ nft add chain inet filter TCP $ nft add chain inet filter UDP 然後創建一個命名字典:
$ nft add map inet filter input_vmap { type inet_proto : verdict ; } 字典的鍵表示協議類型,值表示判決動作。
往字典中添加元素:
$ nft add element inet filter input_vmap { tcp : jump TCP, udp : jump UDP } 最後創建一條規則拆分 TCP 和 UDP 的流量:
$ nft add rule inet filter input meta l4proto vmap @input_vmap 其中,meta l4proto 用來匹配協議的類型。
最後再瞄一眼規則:
$ nft list ruleset table inet filter { map input_vmap { type inet_proto : verdict elements = { tcp : jump TCP, udp : jump UDP } } chain input { type filter hook input priority 0; policy accept; iif "lo" accept comment "Accept any localhost traffic" iif != "lo" ip daddr 127.0.0.0/8 counter packets 0 bytes 0 drop comment "drop connections to loopback not coming from loopback" ct state invalid log prefix "Invalid-Input: " level info flags all counter packets 95 bytes 6479 drop comment "Drop invalid connections" icmp type echo-request limit rate 20 bytes/second burst 500 bytes counter packets 17 bytes 2040 accept comment "No ping floods" icmp type echo-request drop comment "No ping floods" ct state { established, related } counter packets 172135 bytes 99807569 accept comment "Accept traffic originated from us" icmp type { destination-unreachable, router-advertisement, router-solicitation, time-exceeded, parameter-problem } accept comment "Accept ICMP" ip protocol igmp accept comment "Accept IGMP" meta l4proto vmap @input_vmap } chain forward { type filter hook forward priority 0; policy accept; } chain output { type filter hook output priority 0; policy accept; } chain TCP { } chain UDP { } } 3. 處理 TCP 流量
這一步我們來處理 TCP 流量,首當其衝的就是 ssh 了,必須得給這位大哥放行啊:
$ nft add rule inet filter TCP tcp dport 22 ct state new limit rate 15/minute log prefix "New SSH connection: " counter accept comment "Avoid brute force on SSH" 其次需要放行 Web 服務,和上面一樣,為了易於管理,方便後續動態添加埠,需要先創建一個命名集合:
$ nft add set inet filter web { type inet_service ; flags interval ; } 查看集合:
$ nft list set inet filter web table inet filter { set web { type inet_service flags interval } } 向集合中添加元素:
$ nft add element inet filter web { 80, 443 } 查看集合:
$ nft list set inet filter web table inet filter { set web { type inet_service flags interval elements = { http, https } } } 放行 Web 服務:
$ nft add rule inet filter TCP tcp dport @web counter accept comment "Accept web server" 如果你還有其他不可描述的應用,比如 xxx 之類的代理,可以按照上面的方式添加規則,先創建集合:
$ nft add set inet filter xxx { type inet_service ; flags interval ; } 再添加元素:
$ nft add element inet filter xxx { 9000-9005, 9007 } 查看集合:
$ nft list set inet filter xxx table inet filter { set xxx { type inet_service flags interval elements = { 9000-9005, 9007 } } } 現在體會到 nftables 集合的強大了吧,可以是區間,可以是單個元素組成的集合,也可以混合,iptables 麻煩讓一讓。
放行不可描述的服務:
$ nft add rule inet filter TCP tcp dport @xxx counter accept comment "Accept xxx" 4. 處理 UDP 流量
這一步我們來處理 UDP 流量,比如上面舉例的不可描述的應用,除了 TCP 埠還有 UDP 埠,具體用處我就不解釋了,自己面向Google找答案吧。
到了這一步,連集合都不用創建, 直接復用之前創建的集合,放行不可描述應用的 UDP 數據:
$ nft add rule inet filter UDP udp dport @xxx counter accept comment "Accept xxx" 查看規則:
$ nft list chain inet filter UDP table inet filter { chain UDP { udp dport @xxx counter packets 0 bytes 0 accept comment "Accept xxx" } } 其他 UDP 數據都可按此套路模組化,簡直不要太賞心悅目。
為了使系統或 nftables 重啟後能夠繼續生效,我們需要將這些規則持久化,直接將規則寫入 /etc/nftables/inet-filter:
$ echo "#! /usr/sbin/nft -f" > /etc/nftables/inet-filter $ nft list ruleset >> /etc/nftables/inet-filter 開機自動載入 nftables 服務:
$ systemctl enable nftables 5. 在 rsyslog 中記錄日誌
默認情況下,開啟日誌記錄後,日誌會直接進入 syslog,和系統日誌混在一起,不好讀取。最好的辦法是將 nftables 的日誌重定向到單獨的文件。
以本文為例,我們只開啟了 ct state invalid 和 ssh 的日誌記錄,先在 /var/log 目錄中創建一個名為 nftables 的目錄,並在其中創建兩個名為 invalid.log 和 ssh.log 的文件,分別存儲各自的日誌。
$ mkdir /var/log/nftables $ touch /var/log/nftables/{ssh.log,invalid.log} 確保系統中已安裝 rsyslog。現在進入 /etc/rsyslog.d 目錄並創建一個名為 nftables.conf 的文件,其內容如下:
:msg,regex,"Invalid-Input: " -/var/log/nftables/invalid.log :msg,regex,"New SSH connection: " -/var/log/nftables/ssh.log 最後,為了確保日誌是可管理的,需要在 /etc/logrotate.d 中創建一個 nftables 文件:
$ cat /etc/logrotate.d/nftables /var/log/nftables/* { rotate 5 daily maxsize 50M missingok notifempty delaycompress compress postrotate invoke-rc.d rsyslog rotate > /dev/null endscript } 重新通過 ssh 連接伺服器,就能看到日誌了:
$ tail -f /var/log/nftables/ssh.log Dec 19 17:15:33 [localhost] kernel: New SSH connection: IN=ens192 OUT= MAC=00:50:56:bd:2f:3d:00:50:56:bd:d7:24:08:00 SRC=192.168.57.2 DST=192.168.57.53 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=43312 DF PROTO=TCP SPT=41842 DPT=22 WINDOW=29200 RES=0x00 SYN URGP=0 6. 總結
本文教你如何使用 nftables 搭建一個簡單的防火牆,並通過集合和字典將規則集模組化,後續可動態添加埠和 IP 等元素,而不用修改規則。更複雜的規則將會在後面的文章介紹,下篇文章將會教你如何使用 nftables 來防 DDoS 攻擊,敬請期待。
微信公眾號
掃一掃下面的二維碼關注微信公眾號,在公眾號中回復◉加群◉即可加入我們的雲原生交流群,和孫宏亮、張館長、陽明等大佬一起探討雲原生技術

