(十一)洞悉linux下的Netfilter&iptables:iptables命令行工具源碼解析【上】

預備知識:

part1: 初見getopt_long()   

在分析iptables源碼時,作爲命令解析的核心函數getopt_long()不得不提。隨便百度或google搜索關於該函數的介紹有很多例子和解釋,這裏我只舉一例,目的是讓大家瞭解傳遞給iptables命令的每個參數是如何被正確識別並處理的。

getopt_long(int argc,char * const argv[],const char *optstring,const struct option *longopts,int *longindex)

    參數說明:

    argcargv來自main函數的輸入;

    optstring:表示可以接受的參數。可以是下列值:1.單個字符,表示選項;2.單個字符後接一個冒號“:”表示該選項後必須跟一個參數。參數緊跟在選項後或者以空格隔開,該參數的指針賦給optarg3.單個字符後跟兩個冒號,表示該選項後必須跟一個參數。參數必須緊跟在選項後不能以空格隔開。該參數的指針賦給optarg。(這個特性是GNU的擴展)。例如,"a:b:cd",表示可以接受的參數選項是a,b,c,d,其中a和b參數後面跟有參數值。

    longopts:是一個結構類型,描述如下:
        struct option {
                const char *name;    //name表示的是長參數名
                int has_arg;         //0-無參;1-一定要有;2-可有可無
                int *flag;          //用來決定,getopt_long()的返回值到底是什麼。                                                                                         //如果flag是null,則函數會返回與該項option匹配的val值。                    

                int val; //和flag聯合決定返回值

        }

    在iptables的do_command()命令解析函數中,見到最多的就是optargoptind

    optarg: 如果字符串optstring中某個選項後面需要跟參數,該參數值一般保存在optarg中;

    optind: 該參數比較費神,輸入參數列表不同,其取值也不一樣。

    說了半天估計大家都快暈了,還是通過例子來說明這兩個值隨着輸入參數的不同其變化情況吧。

#include

#include

char *l_opt_arg;

char* const short_options = "nb:ls:";

struct option long_options[] = {

     { "name",     0,   NULL,    'n'     },

     { "bf_name",  1,   NULL,    'b'     },

     { "love",     0,   NULL,    'l'     },

     { "speed",     1,   NULL,    's'     },

     {      0,     0,     0,     0},

};

 

int main(int argc, char *argv[])

{

     int c;

    printf("init otpind=%d\n",optind);

     while((c = getopt_long (argc, argv, short_options, long_options, NULL)) != -1)

     {

        printf("option=%c,optind=%d,optarg=%s\n",c,optind,optarg);

        printf("args[%d]=%s\n",optind,argv[optind]);

         switch (c)

         {

         case 'n':

             printf("My name is XL.\n");

             break;

         case 'b':

             printf("His name is ST.\n");

             break;

         case 'l':

             printf("Our love is ok!\n");

             break;

         case 's':

             printf("SHit of son.\n");

             break;

         }

     }

     return 0;

}

   

    該測試程序接受的可選參數爲-n -b -l -s,其中b和s選項後面要跟值。

    如果我們執行./test -n -b boy -l -s son

    optind依次取值爲1、2、4、5、7。默認值就是1,當解析-n時,因爲發現-n不需要值,所以當調用了getopt_long後,optind自動加1,指向-b選項。而-b是需要參數的,那麼跟在-b後面的一定是它的值,所以當解析-b時,optind自動跳到下一個選項-l所在位置。同樣-s也需要跟參數,那麼當解析-l時optind就自動跳到-s所在的位置了,即5。

 

    如果我們執行./test -n --b=boy -l -s son

    這樣的格式,optind依次爲1、2、3、4、6。大家基本已經可以看出些眉目了吧。因爲-b參數用長參格式加等號的賦值方式,所以optind的移動稍微有些變化。但它終歸可以正確識別傳給它的所有命令行參數及其格式。

 

    如果我們執行./test -nl -b boy -s son

    optind依次取值1、1、2、4、6。第一個選項是nl組合項,而且這兩個選項都不需要跟參數。

關於getopt_long函數的更多用法參見man幫助手冊。自己再對上面這個程序摸索摸索體會要更深刻些。

 

part2:iptables的自動加載模塊原理

       無論是match還是target,在用戶空間都有其對應的so庫文件,關於動態庫大家可以參閱讀我的另一篇博文《Linux系統中“動態庫”和“靜態庫”那點事兒》。這裏我們注意到一點的就是無論是諸如libxt_tcp.c這樣的協議模塊,還是libxt_limit.c這樣的match模塊,又抑或libipt_REJECT.c這樣的target模塊,每個模塊中都有一個名爲_init()的函數。爲什麼我們的自己平時開發so庫時,怎麼沒見過這個傢伙?大家可能會有這疑問。接下來我們就來跟您抽絲剝繭,步步深入,看看它到底是何方妖孽。

       iptables在加載動態庫時用的是dlopen()函數,在這篇博文中我有介紹。_init()定義在xtables.h中,是共享庫用來初始化全局變量和對象用的,其定義如下:

    #define _init __attribute__((constructor)) my_init

       用__attribute__((constructor))來定義的函數,表示函數是構造函數,在main執行之前被調用;相應的用__attribute__ ((destructor))析構函數,在main退出時執行。void _init(void)就相當於是__attribute__((constructor)) _INIT(void),其實不管函數名定義成什麼都會被執行到。

       在iptables中當我們調用dlopen函數來加載動態庫時,率先執行每個動態庫裏的_init()函數,而該函數要麼是將該match註冊到全局鏈表xtables_matches裏,或者是將target註冊到全局鏈表xtables_targets中。
 
 
=================================華麗麗的開始=======================================

 

iptables的命令解析流程

      這裏我們僅以ipv4協議爲例進行分析。iptables-1.4.0.tar.gz源代碼中,iptables命令的入口文件爲iptables-standalone.c,其中主函數爲main或者iptables_main。主函數中,所作的事情也很明瞭,其流程如下:

       當前,用戶空間的iptables工具的絕大多數模塊都是以動態共享庫so的形式。使用動態庫的優點也是顯而易見的:編譯出來的iptables命令比較小,動態庫方式使得對於iptables的擴充非常方便。如果你非要去研究一下init_extensions函數的話,那麼可以在iptables源碼包的extensions/Makefile文件裏找點思路。這裏,我不會對其進行分析。

 

       命令行參數解析do_command()【位於iptable.c文件中】

該函數是iptables用於解析用戶輸入參數的核心接口函數,其函數原型爲:

int do_command(int argc, char *argv[], char **table, iptc_handle_t *handle);

argc和argv是由用戶傳遞過來的命令行參數;

table所操作的表名,對應命令行就是-t參數後面的值,如果用戶沒有指定-t參數時,默認爲filter表;

       handle這個結構比較重要,它用於保存從內核返回的由table所指定的表的所有信息,後續對錶及其其中的規則操作時都用的該變量;

前面我們在分析netfilter的時候提到過,用戶空間和內核空間在表示match以及target時採用了不同的結構體定義。用戶空間的match結構體定義爲:

struct xtables_match                       #define iptables_target xtables_target

{

         struct xtables_match *next;

         …

         void (*help)(void);

         /* Initialize the match. */

         void (*init)(struct xt_entry_match *m);

         …

         /* Ignore these men behind the curtain: */

         unsigned int option_offset;

         struct xt_entry_match *m;  #內核中的match結構

         unsigned int mflags;

    …

};

       該結構是iptables在加載它所支持的所有match模塊的時候所用到的結構體,例如time匹配模塊、iprange匹配模塊等。也就是說,如果你要開發自己的用戶空間match的話,那麼你必須實例化上面這樣一個結構體對象,然後實現它相應的方法,諸如init、help、parse等等。

       真正用在我們所配置的iptables規則裏的匹配條件,是由下列類型表示:

struct xtables_rule_match        #define iptables_rule_match xtables_rule_match

{

         struct xtables_rule_match *next;

         struct xtables_match *match;

         unsigned int completed;

};

可以看到,xtables_rule_match是將xtables_match組織成了一個鏈表而已。這也正和我們的意願,因爲一條規則裏有可能會有多個match條件,而在解析的時候我們只要將我們規則裏所用的match通過一個指針指向iptables目前所支持的那個模塊,在後面的使用過程中就可以直接調用那個match模塊裏的所有函數了。這樣即提高的訪問效率,又節約了系統內存空間。

       同樣的,用戶空間的target也類似,留給大家自己去研究。

 

       iptables最常用的命令格式無非就是顯示幫助信息,或者操作規則,例如:

【幫助信息格式】

iptables [-[m|j|p] name ] -h 顯示名爲name的match模塊(m)、target模塊(j)或協議(p)的詳細幫助信息。

OK,我們以下面的規則爲例,和大家探討一下iptables對其的解析流程。

       iptables –A INPUT –i eth0 –p tcp --syn –s 10.0.0.0/8 –d 10.1.28.184 –j ACCEPT

       在博文三中,我們知道內核中用於表示一條規則的數據結構是struct ipt_entry{}類型,那麼iptables對於輸入給它的所有參數最終也要變成這樣的格式。而我們在閱讀iptables源碼時發現,它確實在do_command()函數開始部分定義了一個struct ipt_entry fw;後面當iptables解析傳遞給它的輸入參數時,主要做的事情,就是對該結構體相關成員變量的初始化填充。閒話不多說,let's rock。

 

(1)、命令控制解析:-A INPUT

      iptables –A INPUT –i eth0 –p tcp --syn –s 10.0.0.0/8 –d 10.1.28.184 –j ACCEPT

 

       對於“ADRILFZNXEP”這些控制命令來說,其核心處理函數爲add_command()函數。

       該函數主要將命令行的控制參數解析出來,然後賦值給一個位圖變量command,該變量的每一位bit表示一種操作。add_command()的函數原型定義如下(iptables.c):

static void add_command(unsigned int *cmd, const int newcmd, const int othercmds, int invert)

參數說明:

       cmd:用於保存控制參數解析結果的位圖標誌變量;

       newcmd:用戶所輸入的控制變量,是一些預定義的宏,定義在iptables.c文件中,如下:

#define CMD_NONE                          0x0000U

#define CMD_INSERT                       0x0001U

#define CMD_DELETE                       0x0002U

#define CMD_DELETE_NUM            0x0004U

#define CMD_REPLACE                0x0008U

#define CMD_APPEND                     0x0010U

#define CMD_LIST                    0x0020U

#define CMD_FLUSH                         0x0040U

#define CMD_ZERO                           0x0080U

#define CMD_NEW_CHAIN                  0x0100U

#define CMD_DELETE_CHAIN             0x0200U

#define CMD_SET_POLICY                    0x0400U

#define CMD_RENAME_CHAIN          0x0800U

       othercmd:在上面這11個控制參數中,只有CMD_ZERO需要輔助額外參數,因爲從iptables -Z chainname的執行結果來看,它最後還會輸出清空後的鏈的實際情況。因此,當用戶的iptables命令中有-Z參數時,cmd默認的會被附加一個CMD_LIST特性。其他10個控制參數時,othercmd參數均爲CMD_NONE。

       invert:表示命令中是否有取反標誌“!”。因爲這11個控制參數是沒有取反操作的,因此,這個值均爲FALSE(即0)。

    當解析完iptables -A INPUT … 後,command=0x0010U,chain=“INPUT”。然後將invert=FALSE,重新進入while循環,解析剩下的參數。

 

(2)、解析接口:-i eth0

iptables –A INPUT –i eth0 –p tcp --syn –s 10.0.0.0/8 –d 10.1.28.184 –j ACCEPT

      注意前面講解的關於getopt_long()函數在執行過程中兩個關鍵參數的值及其變化情況。當解析接口的時候optarg=“eth0”,optind=indexof(-p)。

check_inverse(optarg, &invert, &optind, argc);函數用於判斷接口是否有取反標誌,如果有取反標誌,則將invert=TRUE,同時optind++,然後它指向後面的接口名,並返回TRUE;如果沒有,則直接返回FALSE。

在接下來執行set_option(&options, OPT_VIANAMEIN, &fw.ip.invflags,invert);同樣的,options也是一個位圖標誌變量,其取值分別如下(定義在iptables.c文件中):

#define OPT_NONE                      0x00000U

#define OPT_NUMERIC                    0x00001U

#define OPT_SOURCE                           0x00002U

#define OPT_DESTINATION                  0x00004U

#define OPT_PROTOCOL                  0x00008U

#define OPT_JUMP                      0x00010U

#define OPT_VERBOSE                     0x00020U

#define OPT_EXPANDED                  0x00040U

#define OPT_VIANAMEIN                         0x00080U

#define OPT_VIANAMEOUT                 0x00100U

#define OPT_FRAGMENT                   0x00200U

#define OPT_LINENUMBERS                0x00400U

#define OPT_COUNTERS                 0x00800U

#define NUMBER_OF_OPT                      12

       然後根據check_inverse()函數解析出來的invert的值來設置fw.ip.invflags相應的標誌位,該值也是個位圖標誌變量,其可取的值由全局數組inverse_for_options[]來限定(iptables.c):

static int inverse_for_options[NUMBER_OF_OPT] =

{

/* -n */ 0,

/* -s */ IPT_INV_SRCIP,         #這六個宏均定義在ip_tables.h文件中

/* -d */ IPT_INV_DSTIP,

/* -p */ IPT_INV_PROTO,

/* -j */ 0,

/* -v */ 0,

/* -x */ 0,

/* -i */ IPT_INV_VIA_IN,

/* -o */ IPT_INV_VIA_OUT,

/* -f */ IPT_INV_FRAG,

/*--line*/ 0,

/* -c */ 0,

};

執行parse_interface(argv[optind-1],fw.ip.iniface,fw.ip.iniface_mask);將接口名稱賦值給fw.ip.iniface,然後再設置該接口的mask。如果接口中沒有正則匹配表達式(即“+”),則mask=0xFFFFFFFF。細心的朋友到這裏可能就有疑問了:接口名不是保存在optarg中麼,爲什麼要通過argv[optind-1]來獲取呢?我們簡單分析對比一下:

如果是“-i eth0”,那麼optarg和argv[optind-1]的值相同,大家可以通過前面我給的那個demo例子去驗證一下;

如果是“-i ! eth0”,情況就不一樣了。注意看代碼,此時optarg=“!”,而arg[optind-1]纔是真正的接口名“eth0”。

 

(3)、解析協議字段:-p tcp

iptables –A INPUT –i eth0 –p tcp --syn –s 10.0.0.0/8 –d 10.1.28.184 –j ACCEPT

check_inverse(optarg, &invert, &optind, argc); 檢查協議字段是否有取反標誌

       set_option(&optionsOPT_PROTOCOL, &fw.ip.invflags,invert); 根據invert的值來設置options和fw.ip.invflags。這和前面的接口解析是類似的。

       然後,將協議名稱解析成對應的協議號,例如ICMP=1,TCP=6,UDP=17等等。

fw.ip.proto = parse_protocol(protocol);

       因爲iptables在-p參數後面支持數字格式的協議描述,因此parse_protocol()函數首先嚐試去解析數字字符串,將其轉換成一個0-255之間的整數。如果轉換成功,則將轉換結果賦值給fw.ip.proto。如果轉換失敗,首先檢查-p後面的參數是不是“all”。如果是則直接返回,否則調用getprotobyname()函數從/etc/protocols中去解析。這裏getprotobyname函數主要根據傳遞給它的協議名返回一個struct protoent{}結構體的對象(詳見man手冊)。解析成功則返回;否則,在用戶自定義的結構體數組chain_protos[]中去解析,其定義如下:

static const struct pprot chain_protos[] = {

         { "tcp", IPPROTO_TCP },

         { "udp", IPPROTO_UDP },

         { "udplite", IPPROTO_UDPLITE },

         { "icmp", IPPROTO_ICMP },

         { "esp", IPPROTO_ESP },

         { "ah", IPPROTO_AH },

         { "sctp", IPPROTO_SCTP },

         { "all", 0 },

};

 

if (fw.ip.proto == 0&& (fw.ip.invflags & IPT_INV_PROTO))

         exit_error(PARAMETER_PROBLEM,"rule would never match protocol");

如果協議類型爲“all”並且協議字段-p後面還有取反標誌,即-p ! all,表示不匹配任何協議。這樣的規則是沒有任何意義的,iptables也不允許這樣的規則存在,因此會給出錯誤提示信息並退出。

 

(4)、解析tcp協議模塊的具體控制參數:--syn

iptables –A INPUT –i eth0 –p tcp --syn –s 10.0.0.0/8 –d 10.1.28.184 –j ACCEPT

針對於--syn符號,會跳轉到switch語句的default處執行。因爲目前還沒有解析到target,因此target=NULL。命令行中沒有-m,因此matches=NULL,matchp=NULL,m=NULL。

if (m == NULL&& protocol&&

(!find_proto(protocol, DONT_LOAD,options&OPT_NUMERIC, NULL)

                     || (find_proto(protocol, DONT_LOAD,options&OPT_NUMERIC, NULL)

                                       && (proto_used == 0))

         )

         && (m = find_proto(protocol, TRY_LOAD,options&OPT_NUMERIC, &matches))) {

       這個邏輯條件判斷已經很清晰了:

       如果命令行中沒有-m,但是有-p,並且find_proto執行失敗或者執行成功且協議本身還沒有被用過proto_used=0,最後我們試圖去加載so庫之後再去執行find_proto。當第三執行find_proto函數時,會運行如下的代碼部分,因爲我們這次是以TRY_LOAD方式執行的:

#ifndef NO_SHARED_LIBS

         if (!ptr && tryload != DONT_LOAD && tryload != DURING_LOAD) {

                   char path[strlen(lib_dir) + sizeof("/.so")

                              + strlen(afinfo.libprefix) + strlen(name)];

                   sprintf(path, "%s/libxt_%s.so", lib_dir, name);

                   if (dlopen(path, RTLD_NOW) != NULL)

                            /* Found library.  If it didn't register itself,

                               maybe they specified target as match. */

                            ptr = find_match(name, DONT_LOAD, NULL);

以上代碼會將我們的…/libxt_tcp.so庫加載到當前進程的運行空間中,並導出相關環境變量,此時tcp的模塊在執行dlopen時就已經被掛到xtables_matches鏈表中了。最後再在find_match()函數(find_proto()函數的內部其實就是調的find_match()而已)裏遞歸的調用一次自己。

第二次遞歸調用自己時,首先會申請一塊大小爲struct xtables_match{}的內存空間由變量clone來指向,並將tcp.so模塊中的信息保存其中,並設置clone->mflags = 0。然後再申請一塊大小爲struct xtables_rule_match{}大小的內存空間,由變量newentry來保存,將tcp的so模塊的信息賦給結構體的相關成員變量。

for (i = matches; *i; i = &(*i)->next) {  #不會執行這個for循環

         printf("i=%s\n",(i==NULL?"NULL":i));

                   if (strcmp(name, (*i)->match->name) == 0)

                            (*i)->completed = 1;

}

newentry->match = ptr;     //就是前面的clone所指向的地址空間。

newentry->completed = 0;

newentry->next = NULL;

*i = newentry;        #因爲matches是個二級指針,因此這裏的*i即*matches=newentry

return ptr;          #ptr目前就保存了和tcp模塊所有相關的內容,ptr最後返回去會賦給

下面的變量m

然後回到do_command()中繼續執行:

/* Try loading protocol */

size_t size;

                                              

proto_used = 1;

 

printf("Ready to load %s's match\n",protocol);

 

size = IPT_ALIGN(sizeof(struct ipt_entry_match))+ m->size;

 

m->m = fw_calloc(1, size);         #爲內核態的xt_entry_match結構分配存儲空間

m->m->u.match_size = size;        #整個tcp_match的大小

strcpy(m->m->u.user.name, m->name);

set_revision(m->m->u.user.name,m->revision);

if (m->init != NULL)

         m->init(m->m);#調用tcp_init函數初始化內核中的match結構,主要是將xt_entry_match尾部的data數組進行初始化。對TCP來說就是將源、目的端口置爲0xFFFF。這並不是重點。

 

opts = merge_options(opts,m->extra_opts, &m->option_offset);

#重點是merge_options操作,將tcp_opts中的數據合併到全局變量opts中去

optind--;

continue;

#前面說過optind指向當前參數下一個緊挨着的參數的下標。目前只是完成了解析--syn的初始化工作,還並沒有對--syn進行解析,因此需要optind--,然後開始解析--syn

然後程序繼續執行while循環,這次依然進入default段進行處理,並進入if (!target||…

只不過這次matches已經不爲NULL,因此matchp就可以取到值,matchep即指向了tcp模塊。將解析的結果賦給fw結構體的相應成員,並將代表tcp模塊的iptables_match賦給m。

if (!target|| !(target->parse(c - target->option_offset,argv, invert,&target->tflags,&fw, &target->t)))

{

    for (matchp = matches; matchp; matchp = matchp->next) {

         if (matchp->completed)

              continue;

        #調用tcp模塊的parse函數,即tcp_parse

        if (matchp->match->parse(c - matchp->match->option_offset,argv, invert,

                                    &matchp->match->mflags,&fw,

                                    &matchp->match->m))

              break;

 }

 

m = matchp ? matchp->match : NULL;

 

if(m==NULL &&…)  #就不會再執行這裏了

 … …

至此,對--syn的解析就已經完成了。

 

(5)、解析源、目的地址:-s 10.0.0.0/8 -d 10.1.28.184

iptables –A INPUT –i eth0 –p tcp --syn –s 10.0.0.0/8 –d 10.1.28.184 –j ACCEPT

解析源地址:

check_inverse(optarg, &invert, &optind, argc);

set_option(&optionsOPT_SOURCE, &fw.ip.invflags,invert);

shostnetworkmask = argv[optind-1]; #暫存源地址,後面要做進一步分析x.x.x.x/xx

 

解析目的地址:

check_inverse(optarg, &invert, &optind, argc);

set_option(&optionsOPT_DESTINATION, &fw.ip.invflags,invert);

dhostnetworkmask = argv[optind-1]; #暫存目的地址,後面要做進一步分析x.x.x.x/xx

 

(6)、解析target:-j ACCEPT

iptables –A INPUT –i eth0 –p tcp --syn –s 10.0.0.0/8 –d 10.1.28.184 –j ACCEPT

首先判斷target字串是否合法,jumpto = parse_target(optarg);

然後在xtables_targets全局鏈表裏查找相應的target。因爲目前只有標準target,因此最後加載libxt_standard.so庫,對應的文件爲libxt_standard.c。

static struct xtables_target standard_target = {

         .family               = AF_INET,

         .name                = "standard",

         .version    = IPTABLES_VERSION,

         .size          = XT_ALIGN(sizeof(int)),

         .userspacesize         = XT_ALIGN(sizeof(int)),

         .help                   = standard_help,

         .parse                = standard_parse,

};

我們可以看到標準target(諸如ACCEPT、DROP、RETURN、QUEUE等)是沒有init函數和extra_opts變量的。因此,要做的操作只有下面幾個:

if (target) {

size_t size;

 

size = IPT_ALIGN(sizeof(struct ipt_entry_target))+ target->size;

 

target->t = fw_calloc(1, size);        #爲內核中的xt_entry_target分配存儲空間

target->t->u.target_size = size;

strcpy(target->t->u.user.name, jumpto);

set_revision(target->t->u.user.name,target->revision);

 

#以下操作均不執行。因爲target->init和target->extra_ops都爲NULL                            

if (target->init != NULL)

                   target->init(target->t);

opts = merge_options(opts, target->extra_opts, &target->option_offset);

}

    至此,對用戶的命令行輸入的參數就算全部解析完成了,其中:

 

  • 控制參數的解析結果保存在位圖變量command中;
  •  規則參數的解析結果保存在位圖變量options中;
  •  源地址保存在臨時變量shostnetworkmask中;
  •  目的地址保存在臨時變量dhostnetworkmask中;

 

    並完成了對struct ipt_entry{}中struct ipt_ip{}結構體成員的初始化,即對fw.ip的初始化。

 

(7)、參數和合法性檢查

如果是“ADRI”操作但是沒有指定源目的地址,默認將其置爲全網段0.0.0.0/0。然後,設置源目的掩碼fw.ip.smsk和fw.ip.dmsk。

      檢查command和options的匹配性generic_opt_check(command, options)。它們的相關性由一個二維數組commands_v_options[][]來限定:

 

 

至此,所有的解析、校驗工作都已完成。接下來我們將要探究,iptables如何與內核交互的問題。

未完,待續…

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章