2012年2月9日 星期四

iptables技術分享



這里以一條iptabls的命令來分析Iptables的代碼,該條規則盡量設計比較多的iptables的命令行參數。
假設要下的規則為:
iptabls –A INPUT –i eth0 –p tcp --syn –s 10.0.0.0/8 –d 10.1.28.184 –j ACCEPT 
該規則是將源地址段在10.0.0.0/8範圍之內的主機發送的SYN包,接受的網口為eth0,且目的地址為10.1.28.184的,執行ACCEPT
以下是執行該條命令時,iptables的流程分析。這裡比較不注重分析代碼,而注重執行的流程及結果。
一、命令行的入口
int main(int argc, char *argv[]) (iptables-standalone.c) 
整個iptables命令行的入口。該函數設置了默認的table=”filter”,即當命令行中沒有-t選項是,就使用默認的filter表。同時還初始化了該應用程序的名字,版本號等。
#ifdef NO_SHARED_LIBS 
init_extensions(); 
#endif 
以上代碼為沒有定義共享庫的話,要執行init_extensions()。這裡我們假設不使用共享庫,所以調用該函數。該函數實在執行make的時候extensions/自動生成的initext.c中的函數。在該函數里調用了所有擴展模塊的init函數。
註冊所有的match,以及標準和擴展的target。
所有的match和target都加入到iptables.c中對應的全局鍊錶之中。以後find_match和find_target是就是搜索的這兩個鍊錶。
/* Keeping track of external matches and targets: linked lists. */ 
struct iptables_match *iptables_matches = NULL; 
struct iptables_target *iptables_targets = NULL; 
這樣在不使用共享庫的情況下,每次下命令之前都要初始化一下全局的鍊錶,當然已經存在的話,就不會再次register的。

二、命令行的核心處理
do_command()是處理Iptables命令的核心部分。
int do_command(int argc, char *argv[], char **table, iptc_handle_t *handle) 
(iptables.c)
1.函數首先對一些結構、變量進行初始化。基本上涵蓋了一條規則可能出現的大部分基本參數。其中,重要的結構體有struct ipt_entry fw, *e,這兩個應該是存儲一條防火牆規則的;struct iptables_match *m,struct iptables_target *target,*t; 
分別用於存儲match和target. 
並將全局的match和target鍊錶的對應flags和used為初始化為0.
2.命令行解析。初始化完畢後,進入while循環,分析用戶輸入的命令,設置相關的標誌變量,然後根據相應標誌,調用對應的處理函數。這裡是我們要進行詳細分析的地方。
我們要分析的命令為:
iptabls –A INPUT –i eth0 –p tcp --syn –s 10.0.0.0/8 –d 10.1.28.184 –j ACCEPT 
以下開始命令行解析:
(1)處理–A選項
調用add_command函數,主要是對command變量進行邏輯處理。該函數的輸入參數
newcmd = CMD_APPEND=0x0010,othercmds= CMD_NONE=0x0000,invert = 0;
輸出參數command(初始值為0),
執行完該函數之後,
command = CMD_APPEND=0x0010,
chain = “INPUT” 
然後程序break跳出switch語句,並將invert = FALSE,進行while的下一個循環,及處理下一個選項。
(2)處理–i選項
調用check_inverse函數。該函數主要是檢查-i對應的參數中是否使用了取反標誌”!”,因此這裡我們使用的是”eth0”,因此該函數直接返回FALSE。invert=0。
調用set_option函數是指對應的選項。該函數的輸入參數option= OPT_VIANAMEOUT(0x0080), invert=0,輸出參數options(初始化為0),fw.ip.invflags(初始化為0)。
經過該函數處理之後,
options = OPT_VIANAMEIN=0x0080; 
fw.ip.invflags=0. 
調用parse_interface函數進行網絡接口的解析。該函數的參數輸入參數argv[optind-1]=”eth0”,輸出參數fw.ip.iniface[IFNAMSIZ]={“”}, fw.ip.iniface_mask[IFNAMSIZ]={“”},其中IFNAMSIZ= 15。
處理之後,
fw.ip.iniface[IFNAMSIZ]={“eth0”}; 
fw.ip.iniface_mask[IFNAMSIZ]={0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; 
fw.nfcache = NFC_IP_IF_IN = 0x0004;
(3)處理–p選項
調用check_inverse函數,檢查這裡設置的協議參數是否使用了取反標誌”!”。我們這裡使用的是tcp,因此該函數直接返回FALSE。invert仍舊為0。
調用set_option函數是指對應的選項。可以參照(2)中該函數的處理方法。options的初始值為0x0080,最終處理完畢之後
options |= OPT_PROTOCOL(0x0008) = 0x0088; 
fw.ip.invflags=0 
然後協議字串tcp全部轉化為小寫,protocol = “tcp”。調用parse_protocol函數將該協議字符串轉換為TCP對應的協議號6, 
fw.ip.proto = 6; 
fw.nfcache |= NFC_IP_PROTO = 0x0004 | 0x0020 = 0x0024;
(4)處理--syn選項
由於該選項並不在全局變量opts裡面,所以程序會進入switch語句的default分支執行。
進行該分支處理時一些變量的初始值:
target==NULL, m==NULL,
全局鍊錶iptables_matches的used選項都為0 
protocol = “tcp” ,options = 0x0088, proto_used = 0 
根據以上參數的值,這裡要執行的動作為加載該協議(m->init(),即libipt_tcp.c中init()),並且將協議的一些選項擴展到全局變量opts裡面。具體代碼執行是進入該分支的if(m==NULL& amp;&…)裡面,具體執行的結果如下:
m指向全局match鍊錶中tcp的match。(libipt_tcp.c) 
proto_used = 1; 
size = IPT_ALIGN(sizeof(struct ipt_entry_match)) + m->size; 
m->m = fw_calloc(1, size); (m->m為內核總的match部分) 
m ->m->u.match_size = size; 
m->m->u.user.name = “tcp”; 
m->used = 1; 
調用m->init(m->m, &fw.nfcache),執行init()(libipt_tcp.c)。僅是將match中的tcp的sport和dport的最大值置為0xFFFF. struct ipt_entry_match中的data主要用於表示真正數據部分的開始,是通過fw_calloc為整個結構體以及data申請的內存,以後對數據的操作都可以通過data來索引。
init(struct ipt_entry_match *m, unsigned int *nfcache) 

struct ipt_tcp *tcpinfo = (struct ipt_tcp *)m->data; 
tcpinfo->spts[1] = tcpinfo->dpts[1] = 0xFFFF; 

merge_options將tcp的一些選項擴展到全局變量opts裡面。這樣就可以解析--syn選項了。opts包含了tcp協議的選項參數。
並且全局變量global_option_offset += OPTION_OFFSET = 256; 
m->option_offset= global_option_offset =256; 
這裡有點需要注意,就是新加到opts全局結構體中的部分,即tcp對應的option結構中所有的val成員都被賦值為:
merge[num_old + i].val += *option_offset; (見merge_options函數) 
這裡主要是為了區別和原先已有option中的val成員。而且在以後添加更多match模塊的時候,都要做這樣的動作。因此在命令行解析的時候,通過getopt_long得到的這些match中的命令行參數值的時候,需要先減去對應的m->option_offset,然後才能正確的parse. 
然後兩行代碼: optind--; 
continue ; 
是讓程序進入下一個while循環,並再次處理--syn參數。因為以上的處理只是將tcp協議的match進行了初始化工作,並沒有處理該參數。
因此,程序再次進入switch的default分支,不過這次是在if(!target&&…),由於對應tcp的match結構體的used被置1,即m->used =1;因此程序要進行m-> parse進行libipt_tcp中的parse()函數對命令行參數進行處理。該函數的第一個參數的使用方法上面已經解釋過了。以下就是parse()函數對--syn的處理結果:
((struct ipt_tcp *)m->data)->flg_mask = 0x16(SYN,ACK,RST); 
((struct ipt_tcp *)m->data)- >flg_cmp = 0x02(SYN) 
m->flags |= TCP_FLAGS = 0x04; 
fw.nfcache |= NF_IP_TCP_FLAGS(0x0100) = 0x0024 | 0x0100=0x0124 
至此,iptables對—syn選項的解析已經完成。
(5)處理-s選項
同樣還是check_inverse,set_option的處理,只有options變量改變;
options |= OPT_SOURCE(0x0002) = 0x0088 | 0x0002 = 0x008a; 
shostnetworkmask= “10.0.0.0/24”; 
fw.nfcache |= NFC_IP_SRC(0x0001)= 0x0124 | 0x0001 = 0x0125;
(6)處理-d選項
同樣還是check_inverse,set_option的處理,只有options變量改變;
options |= OPT_DESTINATION (0x0004) = 0x008a | 0x0004 = 0x008e; 
dhostnetworkmask= “10.1.28.184”; 
fw.nfcache |= NFC_IP_DST ( 0x0002)= 0x0125 | 0x0002 = 0x0127;
(7)處理-j選項
首先還是set_option函數的處理:
options |= OPT_JUMP(0x0010) = 0x008e | 0x0010 = 0x009e; 
然後jumpto =parse_target(“ACCEPT”),主要是檢查一下該字符串是否合法;
接著target =find_target(jumpto, TRY_LOAD);該函數返回target為”standard”(包含了ACCEPT, DROP, QUEUE,RETURN等目標)的結構體指針。
並且: target->loaded = 1; target->used = 1 
jumpto = “ACCEPT” 
size = IPT_ALIGN(sizeof(struct ipt_entry_target))+ target->size; 
target->t = fw_calloc(1, size); //分配內存給struct ipt_entry_target 
target->t->u.target_size = size; 
target->t->u.user.name = “ACCEPT” 
這裡的target->init指向了libipt_standard.c中的init(),該函數並未執行任何動作。
merge_options又是將該libipt_standard.c的opts加入到全局的opts中。由於ACCEPT目標是標準的,這裡實際上並未往全局的opts中添加任何內容。
僅修改瞭如下變量
global_option_offset += OPTION_OFFSET = 256+256=512; 
target->option_offset= global_option_offset =512; 
至此,命令行已經解析完畢,下面要接著對解析出來的各個參數進行進一步處理。
3. 相關參數的檢查
/*執行libipt_tcp.c中的final_check*/ 
m->final_check(m->mflags); 
/*執行libipt_standard.c中的final_check*/ 
target->final_check(target->tflags); 
隨後是對optind,invert ,command的檢查,接著的if (command &…)對我們的參數沒有產生任何影響。
分別對shostnetworkmask,dhostnetworkmask調用parse_hostnetworkmask函數,執行的結果如下:
saddrs[0]->s_addr = 0x0a000000 (10.0.0.0); 
fw.ip.smsk.s_addr = 0xff000000 (255.0.0.0); 
nsaddrs = 1; 
daddrs[ 0]->s_addr = 0x0a011cb8 (10.1.28.184); 
fw.ip.dmsk.s_addr = 0xffffffff (255.255.255.255); 
ndaddrs = 1; 
最後調用generic_opt_check(command, options)對command和options進行檢查。主要是檢查iptables中的command和options是否搭配。所有的搭配情況保存在全局的數組
static char commands_v_options[NUMBER_OF_CMD][NUMBER_OF_OPT]. 
下面則要從內核表裡取出對應表的全部信息。
4.取出內核中相應表的全部信息
因為在main函數中iptc_handle_t handle = NULL;所以這裡要執行的代碼為:
*handle = iptc_init(*table) 
這裡返回一個iptc_handle_t *handle的結構體指針,該指針指向從內核中去中的”filter”(默認)表對應的所有信息。先取出info結構,獲取表的一些大概信息,然後再獲取整個表的規則。
隨後的兩個if (!*handle)都是判斷*handle是否為NULL,如果是的話則說明有錯誤,獲取不到對應表的相關信息。我們這裡應該指向了獲取的”filter”的信息的指針。因此這兩個if判斷都為FALSE。
5.檢查chain與相關options的搭配,target的合法等問題
整個檢查都在if (command == CMD_APPEND…){…}裡面進行。
首先檢查chain和options的匹配情況,這裡chain=”INPUT”,對於本例中設置的規則,應該都沒有問題。
然後是if (target && iptc_is_chain(jumpto, *handle))。其中target為”standard”(包含了ACCEPT, DROP, QUEUE,RETURN等目標)的結構體指針。
(libiptc.c)。由於我們這裡的jumpto為目標ACCEPT,而並非chain,所以該if為FALSE. 
因此,程序真正執行的部分是
e = generate_entry(&fw, iptables_matches, target->t); 
用來生成一個struct ipt_entry e。e中包括了struct ipt_entry,並利用最後一個元素unsigned char elems[0]申請了一塊內存,該內存里首先是若干個match的結構體,然後是一個target結構體,因此該塊內存的大小為n *match + target.
6.具體命令的執行
這部分也是do_command函數的最後。用一個swicth(command)來判斷具體執行什麼動作。我們這裡command=CMD_APPEND,因此調用append_entry函數來將該條iptables規則加入進去。大致介紹一下append_entry函數的功能:
源碼中具體對應的函數名為TC_APPEND_ENTRY()。該函數首先調用find_label找到整個規則中指定chain的struct chain_cache結構,然後做一下target的映射,我們這裡是標準的target。真正執行添加規則的是insert_rules函數。該函數找到插入規則的entry點,並將該規則插入。
調整後的所有的規則都保存在結構體指針handle之中。
三、iptables規則的提交
do_command函數執行完畢,接著調用iptc_commit(&handle)將filter調整後的所有規則提交給內核。
這裡只分析了ipt_init(),append_entry和iptc_commit的功能,沒有具體分析其源代碼。因為這三個函數的源代碼都比較複雜。這裡作為Iptables的流程分析,不具體討論。
整個iptables的工作流程解析完畢。這樣,對iptables的整個命令行的核心代碼從已經有了相當的掌握

link from:http://www.cnblogs.com/500ju/archive/2011/12/28/2305209.html

沒有留言:

張貼留言

How to use simple speedtest in RaspberryPi CLI

  pi@ChunchaiRPI2:/tmp $  wget -O speedtest-cli https://raw.githubusercontent.com/sivel/speedtest-cli/master/speedtest.py --2023-06-26 10:4...