iptables深入解析:filter篇

發表於2015-08-24

之前大致寫過iptables即netfilter的基本框架,在安全領域用到的比較多,一般小的裝置防火牆,也有自己開發的比如思科的ACL,簡單的包過濾什麼的,都可以支援,但是它最大的缺點就是影響效能,或許我們需要借鑑tcpdump/wireshark的機制(它們都包含了對報文的解析) ,現在流行的dpi 什麼的, 甚至工控領域也用到了。雖然它沒有那麼完美,但是框架比較好,而我們可以去改進和定製它.
參考:iptables1.4.21 kernel 3.8.13

iptables的機制分兩個部分:
1. 使用者空間的iptables工具
2. 核心netfilter機制的支援

它分ipv4部分和ipv6部分,這裡只分析ipv4部分.程式碼目錄(核心):
net/ipv4/netfilter
net/ipv6/netfilter
net/netfilter

這裡先分析核心部分。
我們都知道netfiter分四個基本模組
1. Ct 連結追蹤
2. Filter 過濾
3. Nat 地址轉換
4. Mangle 修改資料包文

CT是基礎核心模組是狀態防火牆和nat的基礎. 而其他模組會維護一個全域性表,也即我們用iptables命令的時候需要指定的表。
它們工作在核心的五個鉤子點上,即鏈的概念.

對於上面的模組在實際程式碼中是相對獨立的初始化的:
net/ipv4/netfilter/
1.iptable_filter.c –>iptable_filter_init
2. iptable_mangle.c –>iptable_mangle_init
3.iptable_nat.c –>iptable_nat_init
4.iptable_raw.c –>iptable_raw_init
5.iptable_security.c —>iptable_security_init

而關於連結追蹤它是其他的基礎,比較特殊。
Net/netfilter/nf_conntrack_core.c
nf_conntrack_init
又被nf_conntrack_standalone.c

register_pernet_subsys這個函式不多說,它是核心名稱空間註冊子系統的一個介面.
對於核心netfilter那麼多程式碼,想簡單理清一個框架思路,首先還是要看makefile:
1. Net/netfilter/Makefile

首先是一些基礎核心的程式碼。然後
1. Netfilter_netlink介面相關的
2. 連結追蹤支援的協議l3/l4等:nf_conntrack_l4proto_register、nf_conntrack_l3proto_register
3. 連結追蹤的 helpers:nf_conntrack_helper_register (主要關聯連線,其他ct的識別等)
4. 其他就是match註冊和targets註冊
5. 核心CT的初始化等.
6. nat和ct相關的基礎。
7.工具ipset、ipvs

那麼net/ipv4/netfilter/Makefile呢?
它主要針對ipv4協議的處理:
1. Nat相關的helpers和協議註冊
2. 連結追蹤
3. Filter、mangle、nat、raw、security例項
4. Matches和targets的註冊
5. Arp其他一些東西.

做了一個簡單的瞭解,那麼就從filter說起,要使配置生效比如filter那麼它會從核心呼叫ipt_do_table查詢rules配置並觸發target。

具體先看filter初始化流程:
先看iptable_filter.c中的初始化函式:
iptable_filter_init
註冊鉤子需要註冊函式介面:nf_register_hooks

很明顯filter只在local_in /local_out/ forward三個鉤子,並且它們的鉤子函式最終都呼叫了

那麼我們就從ipt_do_table函式分析吧
關於引數傳遞除了struct xt_table需要說明下外其他不需要說明了吧
1. NF_HOOK的呼叫查詢hook函式
elem = &nf_hooks[pf][hook];
找到對應的nf_hook_ops
2. hook函式的呼叫
對比2.6.32和3.8.13已結有了較大的改動,不過原理一樣。
在iptable_filter.c中 filter的鉤子函式已經統一成一個函式了即

對於ipt_do_table最需要我們關心的就是net->ipv4.iptable_filter這個東西從哪裡來的,當然我們知道它就是存放rules的地方
從搜尋的核心程式碼來看:

上述程式碼的12,13 行初始化了它,而net則是inet層初始化的時候建立的全域性變數.
net->ipv4.iptable_filter的型別是struct xt_table *
用struct xt_table表示,每一個功能模組都需要維護一個表,註冊API:ipt_register_table(它是xt_register_table的封裝)
Include/linux/netfilter中X_tables.h

還有另外一個重要的結構體: struct xt_table_info *private;
當然關於tables的rules需要使用者空間來下發配置。以及註冊match和target相關的東西.關於rules的結構和表的關係後續我們會講到.
關於全域性表的維護:通過API介面註冊的表都掛到了net->xt.tables中.
xt的型別是struct netns_xt:

還有另外一個全域性變數Struct xt_af xt;

有了表,我們看看match和target的註冊:match和target放到了xt.match 和xt.target中(和之前核心不太一樣了表和match/target已經分開處理了)
list_add(&match->list,&xt[af].match);
註冊介面:
xt_register_table
xt_register_match
xt_register_target

對應的結構體:
Struct xt_table
Struct xt_match
Struct xt_target

那麼規則呢? Rules呢?我們先看一段註釋說明:
/* This structure defines each of the firewall rules. Consists of 3 parts which are
1) general IP header stuff
2) match specificstuff
3) the target to perform if the rule
matches */
struct ipt_entry {

我們繼續看ipt_do_table函式
找到第一個ipt_entry然後呼叫ip_packet_match(五元組匹配模式開啟!知道找到匹配的才結束)
比較ip頭和 ipt_entry->ipt_ip
不匹配則返回false
1. 比較sip 和dip
然後比較入介面和出介面名字名字
2. 協議的比較
3. 判斷IPT_F_FRAG
找到匹配的entry

接著呼叫entry裡的match函式
Ipt_entry->elems :match and target

? ipt_entry:標準匹配結構,主要包含資料包的源、目的IP,出、入介面和掩碼等;
? ipt_entry_match:擴充套件匹配。一條rule規則可能有零個或多個ipt_entry_match結構;
? ipt_entry_target:一條rule規則有且僅有一個target動作。就是當所有的標準匹配和擴充套件匹配都符合之後才來執行該target.
具體程式碼:

那麼match函式從哪裡來?Ipt_entry又在什麼賦值的呢?後續我們會看到答案.

如果match匹配那麼呼叫相應的target,這裡不要把類似struct xt_match 和struct xt_entry_match搞混了。

例:

對於現在來說,它通過ipt_entry找到rules;關於rules的組成部分

Ipt_entry+ipt_entry_match+ …+target

但是iptables如何傳遞過來的呢?

又是如何和已經註冊的match和target關聯起來的呢?上面我們知道了核心通過表找到需要的rules然後解析匹配動作,還沒有和應用聯絡起來,是時候統一一下了.

我們先看一條簡單的命令比如過濾tcp協議:
iptables -A OUTPUT -p tcp –dport 31337 -j DROP

我們找到iptables1.4.21的原始碼:
iptables命令的主函式在iptables-standalone.c
Iptables_main

而它又被封裝了一層在xtables-multi.c:

編譯過iptables後,檢視sbin下iptables命令都軟連線到了xtables-multi。
先看主函式:

我們看到命令列解析是在do_command4裡

而opts是什麼?
#define opts iptables_globals.opts
還有在之前初始化的時候:xt_params = &iptables_globals;

這個結構體包含什麼呢?

關鍵點就是struct option,既然上面orig_opts和opts指向相同的地方,而xt_params又指向iptables_globals

那麼可能好奇struct option了是什麼玩意,我們來看一下:
原型在getopt.h中

因為getopt_long裡要用到,對這個函式不熟悉的,可以自己編個小程式測試下用法.加深理解。

補充說明:

extern char *optarg; //選項的引數指標

extern int optind, //下一次呼叫getopt的時,從optind儲存的位置處重新開始檢查選項。

extern int opterr, //當opterr=0時,getopt不向stderr輸出錯誤資訊。

extern int optopt; //當命令列選項字元不包括在optstring中或者選項缺少必要的引數時,該選項儲存在optopt中,getopt返回’?’

1.單個字元,表示選項,

2.單個字元後接一個冒號:表示該選項後必須跟一個引數。引數緊跟在選項後或者以空格隔開。該引數的指標賦給optarg。

3 單個字元後跟兩個冒號,表示該選項後必須跟一個引數。引數必須緊跟在選項後不能以空格隔開。該引數的指標賦給optarg。(這個特性是GNU的擴張)。

4 optind 下個引數選項的索引,可以通過argv[optind]檢視每解析一個選項optind就會加1.

getopt_long根據傳遞的opt來解析長字串引數.

對於單字母的引數很容易解析出來,但是類似–dport由於longstring裡沒有對應的說明那麼就會進入default處理。

對於tcp相關的dport處理程式碼在libxt_tcp.c:tcp match的註冊

在註冊match的時候xtables_register_match
/* place on linked list of matches pending
full registration */
me->next= xtables_pending_matches;
xtables_pending_matches= me;

這就是引數的解析過程,不論什麼引數都是這樣解析,那麼解析完如何把rules傳遞給核心呢?

我們在回顧一下iptables配置的命令:

iptables -A OUTPUT -p tcp –dport 31337 -j DROP
下面我們就逐一跟蹤命令的解析過程:
(需要說明的是預設是filter表,-t filter)
1. 預設初始化char *table = “filter”;
2. Do_command4命令解析之getopt_long

這個自測過,對於-開頭的命令可以直接解析,但是對於–的就需要進入default處理流程。

1> 第一個命令 -A OUTPUT

add_command即把command賦值為CMD_APPEND
cs.invert為0
Chain 為 OUTPUT
或許我們需要回顧下getopt_long

根據上面的初始化值來解析引數A,明顯has_arg=1表示A後面跟一個引數即OUTPUT

2>第二個命令-p tcp

cs.options = OPT_PROTOCOL;
cs.protocol = optarg; // optarg為tcp,賦值給cs.protocol
cs.fw.ip.proto =xtables_parse_protocol(cs.protocol); // IPPROTO_TCP
xtables_parse_protocol判斷這個協議是否支援
return xtables_chain_protos[i].num;返回協議號
協議初始化在libxtables中xtables.c

Cs.fw.ip.proto值為IPPROTO_TCP

3>. 第三個命令–dport 31337
進入command_default流程後
首先判斷cs->target是否為null,
然後判斷cs->matches是否為null;點選(此處)摺疊或開啟

當然第一次處理直接進入load_proto
會根據協議名字struct xtables_match * xtables_find_match(const char *name, enum xtables_tryload tryload, struct xtables_rule_match **matches)
對cs.matchs初始化為tcp match

在xtables_find_match中分兩個部分:

完成了cs.matchs初始化為tcp match(cloned)。
找到tcp註冊的match之後 ,申請xt_entry_match節點(xtables_calloc),呼叫xs_init_match(m);
對於tcp match就是:

然後呼叫xtables_merge_options把tcp match的ext_opt複製到全域性的opts中。optind– ,回退重新處理–dport。
重新進入command_default:由於cs.matchs不為null

進入xtables_option_mpcall解析

m->parse即之前已經註冊tcp_match中的tcp_parse
由於之前已經把tcp擴充套件的options新增到了全域性表中,所以重新解析後cs.c值為2,optarg即我們傳遞的埠號

struct xt_tcp *tcpinfo = (struct xt_tcp *)(*match)->data; 和optarg
記得之前找到match後會對xt_entry_match初始化data強制轉換成xt_tcp結構指標型別初始化(柔性陣列動態擴充套件)
parse_tcp_ports解析埠資訊,如果是數字字串則轉換成數字賦值,如果不是則以它為名字查詢服務獲得服務的埠號(查詢/etc/services通過getservbyname介面)
(1)cs->matchs =xtables_malloc(sizeof(struct xtables_rule_match));
(2)cs->matchs->match = tcp_match; // cloned
(3) cs->matchs->match->m->data 賦值 (dport)

4>. 接著處理第4個引數 –j DROP
cs->options | = OPT_JUMP;
Cs->jumpto=optarg (即DROP) –> “standard”
cs->target = target_standard (cloned) // 跟match find 幾乎一樣的處理流程。 struct xtable_target
在libxt_standard.c

cs->target->t =xtables_calloc(1, size); 申請空間。 // struct xt_entry_match
cs->target->t賦值strcpy(cs->target->t->u.user.name, cs->jumpto)等。
終於幾個引數初步解析完了,其他引數解析也類似。看後續的程式碼:
一些安全檢查後
Shostnetworkmask 、Dhostnetworkmask 初始化為0.0.0.0/0 預設
iptables有引數-d,可以指定網址,比如 iptables -A OUTPUT -d www.baidu.com -j DROP
則會對Dhostnetworkmask賦值處理和解析。
主要是根據主機名解析所有的ip地址,使用了gethostbyname函式. 這只是一個小插曲.
到這裡需要注意:

系統初始化時handle為null,*table為“filter”:

在Libip4tc.c中:iptc_init
#define TC_INIT iptc_init
1.建立了socket(TC_AF, SOCK_RAW, IPPROTO_RAW);
2.fcntl(sockfd, F_SETFD, FD_CLOEXEC)
3.getsockopt(sockfd, TC_IPPROTO, SO_GET_INFO, &info, &s)

info結構資訊:

它和核心的struct xt_table_info很相似

關於getinfo的操作,它下發到核心獲取資訊:

4.建立handle,並初始化。
5.getsockopt(h->sockfd, TC_IPPROTO, SO_GET_ENTRIES, h->entries,&tmp) 獲取entires資訊

一開始認識info 和entries應該為空,其實錯了,在初始化filter表的時候,即在iptable_filter_net_init裡面做了重要工作:

 

很有意思的是上面這個函式:

為什麼這麼說,還記得iptcc_find_label這個函式嗎?它去查詢handle->chains 而在TC_INIT的時候,從核心獲取info和entries之後通過parse_table賦值給handle.
補充幾個結構體:

 

 

下面看看filter表初始化的時候做了什麼工作:

Entry主要初始化了target_offset為ipt_entry大小 和 next_offset 為外加target
Target初始化了name 為XT_STANDARD_TARGET 和target_size
總共初始化了三個entry根據filter的hooknum

在xt_register_table的時候把struct xt_table_info *newinfo;賦給table->private
這也就是為什麼使用者空間為一開始獲取核心的entries。然後才能順利開展後續工作.
回到主函式命令處理:

然後進入生成entry階段,這裡比較關鍵,它初始化了iptables table ,後續很重要的部分:

因為它用程式碼說明了rules的組成:

Ipt_entry+ xt_entry_match+…+xt_entry_target (…表示match可以不止一個,但target只有一個)
申請新的ipt_entry *e空間,這裡把ipt_entry->ip 賦值,複製entry_matchs和entry_target.
接著是對command的處理,

append_entry的引數的傳遞這裡就不解釋了,前面都分析完了.

1.ip初始化 2.iptc_append_entry. 這個函式很好理解,就是附加entry.

處理完命令 ,然後釋放掉原來的空間.

在完成do_comand4後,自然是要提交我們的東西到核心裡

即通過ipt_commit函式

原理是利用Setsockopt下發配置,關於setsockopt這裡就不詳細講解了.

相關文章