iptables防火牆偵錯,想列印個紀錄檔就這麼難

2023-07-23 21:00:39

背景

怎麼會講這個話題,這個說來真的長了。但是,長話短說,也是可以的。

我前面的文章提到,線上的服務用了c3p0資料庫連線池,會偶發連線洩露問題,而分析到最後,又懷疑是db側主動關閉連線,或者是服務所在機器和db之間有防火牆,防火牆主動關閉了連線。導致我們這邊socket看著還健康,實際在對端已經失效了,然後我們在這個socket發訊息過去,對方一直不回覆,我方沒設定超時時間,導致長時間阻塞在read方法上(等待對方響應)。

然後,為了模擬這種場景,我是想了一些辦法,我搜到的傳統一點的辦法是,我們是oracle,支援sleep命令,執行後,資料庫就像死了一樣,這樣應該可以模擬出read timeout。但是吧,開發環境就那一個資料庫,很多人用,我去sleep它,不太好,我自己搭吧,有點懶,而且,感覺這個方法不夠通用,換個別的資料庫超時,或者是什麼服務超時,豈不是又要另尋他法?

個人偏愛通用的東西,比如這種超時模擬的場景,不只是可以模擬db,也可以模擬其他遠端服務。

一開始,我想的辦法是nginx四層代理(使用stream模組),比如使用者端連線nginx,然後又nginx轉發流量到後面的db啥的,然後當後端db返回響應時,我在nginx上sleep個n秒再返回,就能達成使用者端超時的場景,但是,經過我後面多方研究,找到了一個在nginx或者openresty上支援sleep的模組(https://github.com/openresty/stream-echo-nginx-module),但是,這個模組明確寫了,不能和proxy模組一起用:

  • The commands of this module cannot be mixed with other response-generating modules like the standard ngx_stream_proxy module in the same server {} block, for obvious reasons.

另外,它也說了,它雖然sleep,但是不會阻塞nginx的eventloop執行緒。

再後來,也試過stream-lua-nginx-module模組,打算用lua程式碼來sleep,發現依然不行,應該怎麼說呢,我的理解是,nginx本身就是一個非阻塞的模型,所以,想要靠sleep這些去阻塞它,反而做不到。

所以,後面我放棄了這條路。

再後來,我是偶然間來了靈感,認為可以靠機器上的防火牆,將db返回的封包給丟掉,這樣,就可以模擬出讀超時的場景了,事實上,這條路是正確的,但是,主要是網上資料比較少,所以也踩了好多坑才折騰出來。

最近就打算好好介紹下這個防火牆,但是,我發現想要比較系統地介紹它,還是比較困難,我自己又現學了好一陣,每次想寫的時候,發現還是有太多不瞭解的,於是擱筆。

今天是打算先介紹下iptables的nat功能,結果實驗的時候,死活有問題,然後想著怎麼打紀錄檔,結果打紀錄檔又出問題,折騰了好久(搜尋引擎上關於這個的資料較少),這裡就記錄下來。

iptables簡單介紹

在centos 6時代,我記得機器上預設的防火牆就是iptables;centos 7,基本換成了firewalld。我這次最先折騰的是firewalld,後來一直沒成功,後來換到iptables結果就好了,因此最近都是在折騰iptables,這裡就先介紹iptables,後續再去研究firewalld。

安裝、啟動

一般來說,先停止firewalld,避免互相影響。

systemctl status firewalld
systemctl stop firewalld

安裝iptables並啟動:

yum install -y iptables-services
// 啟動
systemctl start iptables
// 檢視狀態
systemctl status iptables
如果提示綠色的「active (exited)」,則iptables已經啟動成功。
// 停止
systemctl stop iptables

預設規則

安裝完成後,或者啟動後,執行如下命令,會發現有一些預設規則。

所謂規則,在防火牆中,就是:針對某個滿足條件1、條件2、條件n的包,需要採取什麼動作,而我們可能有很多這樣的規則,形成一個或多個list。

而上面看到的預設規則,存放在檔案 /etc/sysconfig/iptables

vim /etc/sysconfig/iptables

*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
// 1 state如果為ESTABLISHED,可以粗略理解為tcp中三次握手後的封包,對這種包,放行。當然,防火牆不止處理tcp,所以這個概念還是略有差異
-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
// 2 對於ping包,放行
-A INPUT -p icmp -j ACCEPT
// 3 本機的loopback網路卡的包,放行
-A INPUT -i lo -j ACCEPT
// 4 對於本機的ssh連線請求包,放行
-A INPUT -p tcp -m state --state NEW -m tcp --dport 22 -j ACCEPT
// 5 其他的包,都拒絕
-A INPUT -j REJECT --reject-with icmp-host-prohibited
-A FORWARD -j REJECT --reject-with icmp-host-prohibited
COMMIT

使用trace定位規則問題

下面的一些iptables的東西,我們還沒介紹,所以可能導致看不太懂,可以簡單看下,我們本篇就是介紹,如何保證debug的紀錄檔能看到。

一般來說,對於開發同學來說,場景也就是比較簡單,如禁止/允許存取某埠,或者只禁止允許某些ip等,比如,假設我們希望放行8080埠,要怎麼設定呢?

我們得設想一下,使用者端存取8080埠的話,傳過來的packet一般長啥樣,是不是tcp協定這一層的目標埠是8080呢,對吧?那我們只要設定一個規則,將這種包篩選出來,放行即可。

我先隨便啟動一個8080的監聽:

python -m SimpleHTTPServer 8080

然後我們新建一個rule,準備放行8080埠:

iptables -i filter -I INPUT 1 -p tcp -m tcp --dport=8080 -j ACCEPT

然後本機執行telnet,結果發現連不上。。。

為啥呢,因為我命令寫錯了,假設我不知道我命令寫錯了,感覺沒匹配上我的這個規則呢,那我怎麼知道,iptables到底最終匹配到哪個rule了呢,會不會還沒到這個rule就出問題了呢?

不要著急,我們加上下面這個命令:

iptables -t raw -I PREROUTING -p tcp -m tcp --dport 8080 -j TRACE

這個命令,分段來看:

-t raw : 指定要操作的table,其實就是傳說中的三表五鏈中的三表之一,table這裡可以先不管
-I PREROUTING: -I表示插入規則,後面指定要插入的鏈(鏈就是規則的集合)
上面兩個選項,暫時不理解可以先不管,其實就是指定了一個時機,這個時機,發生在防火牆處理網路報文的最前面。
此時,我們指定一個rule:
-p tcp -m tcp --dport 8080 -j TRACE
即,對於目標埠為8080的報文,-j TRACE ,表示進行跟蹤,這樣的話,防火牆會在紀錄檔列印出最終匹配上了哪些rule

增加了上述命令後,我們重試一次telnet,然後開啟/var/log/messages紀錄檔:

能看到對於某一條使用者端報文(這裡其實就是使用者端發過來的第一次tcp握手,注意看,第二行的ID都是35701,所以都是針對同一條報文的紀錄檔),一共列印了三行:

raw:PREROUTING:policy:2
nat:PREROUTING:policy:1
filter:INPUT:rule:6

這裡其實就是表示,一個順序經過了三個規則。

首先是raw:PREROUTING,即raw表的PREROUTING鏈的預設規則(policy:2),看下圖,raw表只有我們那個trace規則,除此之外,啥都沒有,所以匹配了PREROUTING鏈的預設策略:

按照網上的說法,下圖紅框處就表示預設策略:

他麼的,為啥說是網上的說法呢,因為,我本來想實驗一下,把這塊改成DROP再看看效果:

 // 不要執行!!!這個命令就是設定預設策略為drop
 iptables -P PREROUTING DROP -t raw 

結果,他麼的,馬上shell就斷了,然後這是工作伺服器,我都以為必須找運維才能解決了,後來好歹想起來有個平臺,可以重啟伺服器,重啟後,iptables我是沒設定開機啟動的,這條rule也是沒有持久化的,所以我才能連上shell,在這裡繼續做實驗。

扯遠了,執行完第一條鏈的預設策略ACCEPT後,進入表nat的PREROUTING鏈:

[root@hx168-access ~]# iptables -nvL -t nat
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         

Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         

Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         

Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination    

這個也是空的,沒有rule,所以最終是執行了PREROUTING:policy:1預設策略:ACCEPT,然後進入到下面的:

filter:INPUT:rule:6。

這就是filter表的INPUT規則鏈的第六條規則:

第六條規則,就是簡單粗暴地拒絕,為啥會執行到第六條呢,這個是按1-6順序執行下來的,說明前面的5條,都沒匹配上條件,那肯定就是條件估計寫得有問題。

經過我認真檢查,發現自己手寫還是容易出錯,正確的應該是下面這樣:

// 正確版本
iptables -I INPUT  -p tcp --dport 8080 -j ACCEPT
// 錯誤版本
iptables  -i filter -I INPUT 1 -p tcp -m tcp --dport=8080 -j ACCEPT

錯誤的地方就是 -i filter,我本來是準備顯式指定下要操作的表(其實正確的版本里就沒指定,預設就是filter),結果語法寫錯了,應該是-t filter,重新執行下,就好了:

可以看到,雖然錯誤的規則還存在,但是我們把正確的規則放到了num=1,它會先執行,它匹配上了,後面就不會執行了。

我們重新看trace紀錄檔,這次就是匹配上rule1了:

trace規則如何設定才能正確輸出

rsyslog程序

今天其實花了很多時間在trace規則上,我在網上查到有這種規則,但是,加了後,死活在log紀錄檔裡看不到內容,查了半天,到處改改改也沒搞出來。

這個/var/log/messages檔案,一般是什麼服務在往裡面寫呢?是rsyslogd,它是一個後臺程序,

簡單一句話的介紹是,它是一個支援將訊息記錄到紀錄檔的系統工具,訊息可以通過網路或者本機unix socket的方式傳送給它。

Rsyslogd is a system utility providing support for message logging. Support of both internet and unix domain sockets enables this utility to support both local and remote logging.

一般來說,都是通過本機unix socket方式。

根據man rsyslogd的說法,這個程序主要涉及如下幾個重要檔案:

主組態檔:
/etc/rsyslog.conf
Configuration file for rsyslogd.  See rsyslog.conf(5) for exact information.

unix socket的位置,本程序就從這裡獲取要寫紀錄檔的訊息
/dev/log
The Unix domain socket to from where local syslog messages are read.

程序pid:
/var/run/syslogd.pid
The file containing the process id of rsyslogd.

程序id這個是可以看到,是匹配的:4773:

[root@hx168-access ~]# cat /var/run/syslogd.pid
4773
[root@hx168-access ~]# ps -ef|grep rsys
root       4773      1  0 18:44 ?        00:00:00 /usr/sbin/rsyslogd -n

這個程序,主要會寫如下檔案(預設設定情況下):

比如我們上面的防火牆跟蹤紀錄檔就在/var/log/messages

紀錄檔輸出遇阻

今天我遇到的問題就是,看到網上說,要改設定,將核心紀錄檔給輸出才行:

vim /etc/rsyslog.conf
增加如下一行:
kern.*     /var/log/iptables.log(這裡檔名可以自取)

結果弄了後還是沒效果。

後面我換了臺機器,結果發現可以輸出,而且也沒像上面這樣設定,我後面就在想,到底是哪裡的問題。

strace排查紀錄檔daemon是否收到紀錄檔

 # xxx為rsyslogd的pid
 strace -p xxx -q -f -s 200

strace命令可以attach到一個程序上,監控其系統呼叫。

我當時兩臺機器,一臺可以,一臺不行,於是分別在兩臺機器上開啟該命令,然後執行ping,發現正常的機器上,是可以看到有如下這些系統呼叫的,而異常的機器就沒有:

所以,我開始懷疑是不是防火牆本身的問題,並沒把紀錄檔寫到rsyslogd。

比較兩臺機器載入的mod

man iptables-extensions檔案,講解了TRACE這個target操作的資訊。

TRACE
This target marks packets so that the kernel will log every rule which match the packets as those traverse the tables, chains, rules.

A logging backend, such as nf_log_ipv4(6) or nfnetlink_log, must be loaded for this to be visible. The packets are logged with the string prefix: "TRACE:
tablename:chainname:type:rulenum " where type can be "rule" for plain rule, "return" for implicit rule at the end of a user defined chain and "policy" for
the policy of the built in chains.
It can only be used in the raw table.

這裡面提到:

A logging backend, such as nf_log_ipv4(6) or nfnetlink_log, must be loaded for this to be visible.

必須先在機器上載入nf_log_ipv4或者nfnetlink_log這兩個模組之一,其他細節就沒有了。

我在網上當時也查了不少文章,就是看看大家是怎麼設定這個TRACE規則的,然後跟著做了些操作,比如,載入模組的命令是modprobe,我搜尋shell history,發現執行了如下這麼多次:

  678  modprobe ipt_LOG
  688  modprobe ipt_LOG ip6t_LOG nfnetlink_log
  741  modprobe nfnetlink_log
  748  modprobe nf_log_ipv4

模組相關的命令有這幾個:

載入或刪除模組
modprobe - Add and remove modules from the Linux Kernel

顯示已載入的模組
lsmod - Show the status of modules in the Linux Kernel
lsmod is a trivial program which nicely formats the contents of the /proc/modules, showing what kernel modules are currently loaded.

檢視模組資訊
modinfo - Show information about a Linux Kernel module

我分別在兩臺機器執行了lsmod | sort,然後比較差異,確實有些差異:

然後在網上看到一些文章,說和核心引數有關(sysctl -a),於是也比較了一下,發現其中一個引數可能有影響:

這個引數我在網上查了好久,甚至沒查到列舉值有哪些,總的來說,資訊非常少。

後來大概知道,如下這個引數值,會把log不是發往rsyslogd,而是發往一個叫ulogd的後臺程式。

net.netfilter.nf_log.2 = nfnetlink_log

這個ulogd大概介紹下,https://www.netfilter.org/projects/ulogd/:

ulogd is a userspace logging daemon for netfilter/iptables related logging.

它是一個使用者態的紀錄檔後臺,供netfilter/iptables相關的紀錄檔使用。不管怎麼說,反正不會發往rsyslog這邊就對了。

這個為啥會變成這樣呢,大概是因為我執行了modprobe nfnetlink_log吧。正確的模組其實應該是nf_log_ipv4.

所以我這邊修改了下值:

echo nf_log_ipv4 >  /proc/sys/net/netfilter/nf_log/2
然後再檢視:
sysctl -a
發現就已經變成nf_log_ipv4了

這裡額外補充下,在如下目錄下,有12個檔案:

[root@hx168-access ~]# ll /proc/sys/net/netfilter/nf_log
-rw-r--r-- 1 root root 0 Jul 22 23:25 2
...
-rw-r--r-- 1 root root 0 Jul 22 23:25 12

其中,2就是表示IP協定族,表示遇到IP協定族時,紀錄檔列印使用的模組,其他的協定族都不用特別關心。

網上關於這個問題的討論

首先是一個feature,貌似是這塊net.netfilter.nf_log.2可以取的值太多了,要合併掉幾個:

https://patchwork.kernel.org/project/netdevbpf/patch/[email protected]/

就是以後就兩個模組,一個nfnetlink_log,一個nf_log_syslog(這個一看就是寫到rsyslog的)。

再下來,是一個bug提交,和我的問題一模一樣,最後解決辦法也一樣,是吐槽trace的檔案寫得太草了:

https://bugzilla.netfilter.org/show_bug.cgi?id=1076

問題是幾年前提的,看起來還是沒人改,bug也沒關閉。

就這樣吧,下篇再繼續講這個iptables的其他方面。