預備知識:
part1: 初見getopt_long()
在分析iptables源碼時,作爲命令解析的核心函數getopt_long()不得不提。隨便百度或google搜索關於該函數的介紹有很多例子和解釋,這裏我只舉一例,目的是讓大家瞭解傳遞給iptables命令的每個參數是如何被正確識別並處理的。
getopt_long(int argc,char * const argv[],const char *optstring,const struct option *longopts,int *longindex)
參數說明:
argc和argv來自main函數的輸入;
optstring:表示可以接受的參數。可以是下列值:1.單個字符,表示選項;2.單個字符後接一個冒號“:”表示該選項後必須跟一個參數。參數緊跟在選項後或者以空格隔開,該參數的指針賦給optarg。3.單個字符後跟兩個冒號,表示該選項後必須跟一個參數。參數必須緊跟在選項後不能以空格隔開。該參數的指針賦給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()命令解析函數中,見到最多的就是optarg和optind。
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(&options, OPT_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(&options, OPT_SOURCE, &fw.ip.invflags,invert); shostnetworkmask = argv[optind-1]; #暫存源地址,後面要做進一步分析x.x.x.x/xx
解析目的地址: check_inverse(optarg, &invert, &optind, argc); set_option(&options, OPT_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如何與內核交互的問題。
未完,待續…