iptables用戶空間和內核空間的交互
iptables目前已經支持IPv4和IPv6兩個版本了,因此它在實現上也需要同時兼容這兩個版本。iptables-1.4.0在這方面做了很好的設計,主要是由libiptc庫來實現。libiptc是iptables control library的簡稱,是Netfilter的一個編程接口,通常被用來顯示、操作(查詢、修改、添加和刪除)netfilter的規則和策略等。使用libipq庫和ip_queue模塊,幾乎可以實現任何在內核中所實現的功能。
? ? ? ? libiptc庫位于iptables源碼包里的libiptc目錄下,共六個文件還是比較容易理解。我們都知道,運行在用戶上下文環境中的代碼是可以阻塞的,這樣,便可以使用消息隊列和?UNIX?域套接字來實現內核態與用戶態的通信。但這些方法的數據傳輸效率較低,Linux?內核提供?copy_from_user()/copy_to_user()?函數來實現內核態與用戶態數據的拷貝,但這兩個函數會引發阻塞,所以不能用在硬、軟中斷中。一般將這兩個特殊拷貝函數用在類似于系統調用一類的函數中,此類函數在使用中往往"穿梭"于內核態與用戶態。此類方法的工作原理為:
?
? ? ? ??其中相關的系統調用是需要用戶自行編寫并載入內核。一般情況都是,內核模塊注冊一組設置套接字選項的函數使得用戶空間進程可以調用此組函數對內核態數據進行讀寫。我們的libiptc庫正是基于這種方式實現了用戶空間和內核空間數據的交換。
????為了后面便于理解,這里我們簡單了解一下在socket編程中經常要接觸的兩個函數:
int setsockopt(int sockfd, int proto, int cmd, void *data, int datalen)
int getsockopt(int sockfd, int proto, int cmd, void *data, int datalen)
這個兩個函數用來控制相關socket文件描述符的一些選項值,如設置(獲取)接受或發送緩沖區的大小、設置(獲取)接受或發送超時值、允許(禁止)重用本地端口和地址等等。
參數說明:
sockfd:為socket的文件描述符;
proto:sock協議,IP RAW的就用SOL_SOCKET/SOL_IP等,TCP/UDP socket的可用SOL_SOCKET/SOL_IP/SOL_TCP/SOL_UDP等,即高層的socket是都可以使用低層socket的命令字?的;
cmd:操作命令字,由自己定義,一般用于擴充;
data:數據緩沖區起始位置指針,set操作時是將緩沖區數據寫入內核,get的時候是將內核中的數據讀入該緩沖區;
datalen:參數data中的數據長度。
?
我們可以通過擴充新的命令字(即前面的cmd字段)來實現特殊應用程序的內核與用戶空間的數據交換,內核實現新的sockopt命令字有兩類:一類是添加完整的新的協議后引入;一類是在原有協議命令集的基礎上增加新的命令字。以netfilter為例,它就是在原有的基礎上擴展命令字,實現了內核與用戶空間的數據交換。Netfilter新定義的命令字如下:
setsockopt新增命令字:
#define IPT_SO_SET_REPLACE //設置規則
#define IPT_SO_SET_ADD_COUNTERS???//加入計數器
getsockopt新增命令字;
#define IPT_SO_GET_INFO???????????????//獲取ipt_info
#define IPT_SO_GET_ENTRIES?????????//獲取規則
#define IPT_SO_GET_REVISION_MATCH?//獲取match
#define IPT_SO_GET_REVISION_TARGET??????//獲取target
???????一個標準的setsockopt()操作的調用流程如下:
?
在ip_setsockopt調用時,如果發現是一個沒有定義的協議,并且判斷現在這個optname是否為netfilter所設置,如果是則調用netfilter所設置的特殊處理函數,于是加入netfilter對sockopt特殊處理后,新的流程如下:
?
? ? ? ? netfitler對于會實例化一些struct nf_sockopt_ops{}對象,然后通過nf_register_sockopt()將其注冊到全局鏈表nf_sockopts里。
struct?nf_sockopt_ops { ?????????struct?list_head?list; ?????????int pf; ? ?????????/* Non-inclusive ranges: use 0/0/NULL to never get called. */ ?????????int set_optmin; ?????????int set_optmax; ?????????int (*set)(struct sock *sk, int optval, void __user *user, unsigned int len); ?????????int (*compat_set)(struct sock *sk, int optval,void __user *user, unsigned int len); ? ?????????int get_optmin; ?????????int get_optmax; ?????????int (*get)(struct sock *sk, int optval, void __user *user, int *len); ?????????int (*compat_get)(struct sock *sk, int optval,void __user *user, int *len); ? ?????????/* Number of users inside set() or get(). */ ?????????unsigned int use; ?????????struct task_struct *cleanup_task; }; |
?
? ? ? ? ?繼續回到libiptc中。libiptc庫中的所有函數均以“iptc_”開頭,主要有下面一些接口(節選自libiptc.h):
typedef?struct iptc_handle?*iptc_handle_t; ? /* Does this chain exist? */ int?iptc_is_chain(const char *chain, const?iptc_handle_t?handle); ? /* Take a snapshot of the rules.??Returns NULL on error. */ iptc_handle_t?iptc_init(const char *tablename); ? /* Cleanup after iptc_init(). */ void?iptc_free(iptc_handle_t?*h); ? /* Iterator functions to run through the chains.??Returns NULL at end. */ const char *iptc_first_chain(iptc_handle_t?*handle); const char *iptc_next_chain(iptc_handle_t?*handle); ? /* Get first rule in the given chain: NULL for empty chain. */ const struct ipt_entry *iptc_first_rule(const char *chain,iptc_handle_t?*handle); /* Returns NULL when rules run out. */ const struct ipt_entry *iptc_next_rule(const struct ipt_entry *prev,iptc_handle_t?*handle); ? /* Returns a pointer to the target name of this entry. */ const char *iptc_get_target(const struct ipt_entry *e,iptc_handle_t?*handle); ? /* Is this a built-in chain? */ int?iptc_builtin(const char *chain, const?iptc_handle_t?handle); ? int?iptc_append_entry(const ipt_chainlabel chain, ?????????????????????????const struct ipt_entry *e, ?????????????????????????iptc_handle_t?*handle); ? /* Zeroes the counters in a chain. */ int?iptc_zero_entries(const ipt_chainlabel chain,iptc_handle_t?*handle); ? /* Creates a new chain. */ int?iptc_create_chain(const ipt_chainlabel chain,iptc_handle_t?*handle); ? /* Makes the actual changes. */ int?iptc_commit(iptc_handle_t?*handle); ? |
? ? ? ? 上面這些接口都是為IPv4定義了,同樣的IPv6的接口均定義在libip6tc.h頭文件中,都以“ip6tc_”開頭(如此說來,IPv4的頭文件應該叫libip4tc.h才比較合適)。然后在libip4tc.c和libip6tc.c文件中分別通過宏定義的形式將IPv4和IPv6對外的接口均統一成“TC_”開頭的宏,并在libiptc.c中實現這些宏即可。如下圖所示:
?
? ? ? ? 這里我們看到iptables-v4和iptables-v6都和諧地統一到了libiptc.c中,后面我們分析的時候只要分析這些相關的宏定義的實現即可。
? ? ? 在繼續往下分析之前我們先看一下STRUCT_TC_HANDLE這個比較拉風的結構體,它用于存儲我們和內核所需要交換的數據。說的通俗一些,就是從內核中取出的表的信息會存儲到該結構體類型的變量中;當我們向內核提交iptables變更時,也需要一個該結構體類型的變量用于存儲我們所要提交的數據。(定義在ip_tables.h頭文件中)
適用于當getsockopt的參數為IPT_SO_GET_INFO,用于從內核讀取表信息 struct ipt_getinfo???????????????#define?STRUCT_GETINFO?struct ipt_getinfo { ?????????/* Which table: caller fills this in. */????#從內核取出的表信息會存儲在該結構體中 ?????????char name[IPT_TABLE_MAXNAMELEN]; ?????????/* Kernel fills these in. */ ?????????unsigned int?valid_hooks; /* Which hook entry points are valid: bitmask */ ?????????unsigned int?hook_entry[NF_IP_NUMHOOKS]; // Hook entry points: one per netfilter hook. ?????????unsigned int?underflow[NF_IP_NUMHOOKS]; /* Underflow points. */ ?????????unsigned int?num_entries; /* Number of entries */ ?????????unsigned int?size; /* Size of entries. */ }; |
還有一個成員entries用保存表中的所有規則信息,每條規則都是一個ipt_entry的實例:
/* The argument to IPT_SO_GET_ENTRIES. */ struct ipt_get_entries { ?????????/* Which table: user fills this in. */ ?????????char?name[IPT_TABLE_MAXNAMELEN]; ?????????unsigned int?size; /* User fills this in: total entry size. */ ? ?????????struct?ipt_entry?entrytable[0]; /*內核里表示規則的結構,參見博文三. */ }; |
????????
(一)、從內核獲取數據:iptc_init()
都說“磨刀不誤砍柴工”,接下來我們繼續上一篇中do_command()函數里剩下的部分。*handle?= iptc_init(*table);?這里即根據表名table去從內核中獲取該表的自身信息和表中的所有規則。關于表自身的一些信息存儲在handle->info成員里;表中所有規則的信息保存在handle->entries成員里。
? ? ? 如果handle獲取失敗,則嘗試加載完內核中相應的ko模塊后再次執行iptc_init()函數。
? ? ? 然后,針對“ADRI”操作需要做一些合法性檢查,諸如-o選項不能用在PREROUTING和INPUT鏈中、-i選項不能用在POSTROUTING和OUTPUT鏈中。
if (target &&?iptc_is_chain(jumpto, *handle)) { ???????????????????fprintf(stderr,"Warning: using chain %s, not extension\n",jumpto); ?????????????if (target->t) ????????????????????????????free(target->t); ??????????????????????????? ???????????????????printf("Target is a chain,but we have gotten a target,then free it!\n"); ? ???????????????????target = NULL; } |
如果-j XXX?后面的XXX是一條用戶自定義規則鏈,但是之前卻解析出了標準target,那么需要將target的空間釋放掉。很明顯,目前我們的-j ACCEPT不會執行到這里。
if (!target??#如果沒有指定target。同樣,我們的規則也不會執行到這里 && (strlen(jumpto) == 0|| iptc_is_chain(jumpto, *handle)) #或者target是一條鏈或為空 ) { ?????????size_t size; ????… …???????? } |
? ? ? ? 因為我們的target為ACCEPT,已經被完全正確解析,即target!=NULL。后面我們會執行else條件分子如下的代碼:
e = generate_entry(&fw, matches, target->t);
用于生成一條iptables的規則,它首先會為e去申請一塊大小n*match+target的空間,其中n為用戶輸入的命令行中的match個數,target為最后的動作。這里很明顯,我們的命令只有一個tcp的match,target是標準target,即ACCEPT。將已經解析的fw賦給e,并對結構體e中其他的成員進行初始化,然后將相應的match和target的數據拷貝到e中對應的成員中。
size = sizeof(struct ipt_entry); for (matchp = matches; matchp; matchp = matchp->next) ?????????size += matchp->match->m->u.match_size; ? e = fw_malloc(size + target->u.target_size); *e = *fw; e->target_offset = size; e->next_offset = size + target->u.target_size; ? size = 0; for (matchp = matches; matchp; matchp = matchp->next) { ?????????memcpy(e->elems + size, matchp->match->m, matchp->match->m->u.match_size); ?????????size += matchp->match->m->u.match_size; } memcpy(e->elems + size, target, target->u.target_size); |
? ? ? ? 最后所生成的規則e,其內存結構如下圖所示:
?
? ? ? ? 這里再聯系我們對內核中netfilter的分析就很容易理解了,一旦我們獲取一條規則ipt_entry的首地址,那么我們能通過target_offset很快獲得這條規則的target地址,同時也可以通過next_offset獲得下一條ipt_entry規則的起始地址,很方便我們到時候做數據包匹配的操作。
?緊接著就是對解析出來的command命令進行具體操作,這里我們是-A命令,因此最后command命令就是CMD_APPEND,這里則執行append_entry()函數。
ret =?append_entry(chain,??#鏈名,這里為INPUT
e,??????#將用戶的命令解析出來的最終的規則對象
nsaddrs,??#-s?后面源地址的個數
saddrs,???#用于保存源地址的數組
ndaddrs,??#-d?后面的目的地址的個數
daddrs,???#用于保存目的地址的數組
options&OPT_VERBOSE, #iptables命令是否有-v參數
handle???#從內核中取出來的規則表信息
);
?在append_entry內部調用了iptc_append_entry(chain, fw, handle),其實就是由宏即TC_APPEND_ENTRY所表示的那個函數。該函數內部有兩個值得注意的結構體類型struct chain_head{}和struct rule_head{},分別用于保存我們所要操作的鏈以及鏈中的規則:
struct chain_head { ?????????struct list_head list; ?????????char name[TABLE_MAXNAMELEN]; ?????????unsigned int hooknum;?????????????/* hook number+1 if builtin */ ?????????unsigned int references;?/*?有多少-j?指定了我們的名字?*/ ?????????int verdict;??????????????????????????/* verdict if builtin */ ?????????STRUCT_COUNTERS counters;????????/* per-chain counters */ ?????????struct counter_map counter_map; ?????????unsigned int num_rules;???????????/*?本鏈中的規則數*/ ?????????struct list_head rules;?????????????/*?本鏈中所有規則的入口點?*/ ? ?????????unsigned int index;???????????/* index (needed for jump resolval) */ ?????????unsigned int head_offset;????????/* offset in rule blob */ ?????????unsigned int foot_index;?/* index (needed for counter_map) */ ?????????unsigned int foot_offset;??????????/* offset in rule blob */ }; ? struct rule_head { ?????????struct list_head list; ?????????struct chain_head *chain; ?????????struct counter_map counter_map; ?????????unsigned int index;???????????/* index (needed for counter_map) */ ?????????unsigned int offset;??????????/* offset in rule blob */ ? ?????????enum iptcc_rule_type type; ?????????struct chain_head?*jump;??????/* jump target, if IPTCC_R_JUMP */ ? ?????????unsigned int size;??????????????/* size of entry data */ ?????????STRUCT_ENTRY entry[0];???#真正的規則入口點?sizeof計算時不會包含這個字段 }; |
TC_APPEND_ENTRY的函數實現:
int TC_APPEND_ENTRY(const?IPT_CHAINLABEL?chain, ???????????????????const?STRUCT_ENTRY?*e, ???????????????????TC_HANDLE_T?*handle)????#注意:這里的handle是個二級指針 { ?????????struct chain_head?*c; ?????????struct rule_head?*r; ? ?????????iptc_fn = TC_APPEND_ENTRY; ?????????if (!(c =?iptcc_find_label(chain, *handle))) {?????? #根據鏈名查找真正的鏈地址賦給c,此時c就指向了INPUT鏈的內存, #包括INPUT中的所有規則和它的policy等 ???????????????????DEBUGP("unable to find chain `%s'\n", chain); ???????????????????errno = ENOENT; ???????????????????return 0; ?????????} ? ?????????if (!(r =?iptcc_alloc_rule(c,?e->next_offset))) { #ipt_entry的next_offset即指明了下一條規則的起始地址,同時這個值也說明了本條規則所占了存儲空間的大小。這里所申請的空間大小=sizeof(rule_head)+當前規則所占的空間大小。 ???????????????????DEBUGP("unable to allocate rule for chain `%s'\n", chain); ???????????????????errno = ENOMEM; ???????????????????return 0; ?????????} ? ?????????memcpy(r->entry, e, e->next_offset);???????#把規則拷貝到柔性數組entry中去 ?????????r->counter_map.maptype = COUNTER_MAP_SET; ? ?????????if (!iptcc_map_target(*handle, r)) {???#主要是設置規則r的target,后面分析。 ???????????????????DEBUGP("unable to map target of rule for chain `%s'\n", chain); ???????????????????free(r); ???????????????????return 0; ?????????} ? ?????????list_add_tail(&r->list, &c->rules); #將新規則r添加在鏈c的末尾 ?????????c->num_rules++;??????????????#同時將鏈中的規則計數增加 ? ?????????set_changed(*handle);????#因為INPUT鏈中的規則已經被改變,則handle->changed=1; ?????????return 1; } |
? ? ? ? ?接下來分析一下設置target時其函數內部流程:
static int iptcc_map_target(const TC_HANDLE_T handle, ????????????struct rule_head *r) { ?????????STRUCT_ENTRY?*e = r->entry;?????????????????#取規則的起始地址 ?????????STRUCT_ENTRY_TARGET?*t = GET_TARGET(e);????#取規則的target ? ?????????/* Maybe it's empty (=> fall through) */ ?????????if (strcmp(t->u.user.name, "") == 0) { #如果沒有指定target,則將規則類型設為“全放行” ???????????????????r->type = IPTCC_R_FALLTHROUGH; ???????????????????return 1; ?????????} ? ?????????/* Maybe it's a standard target name... */ #因為都是標準target,因此將target中用戶空間的user.name都置為空,設置verdict, #并將rule_head中的type字段為IPTCC_R_STANDARD ?????????else if (strcmp(t->u.user.name, LABEL_ACCEPT) == 0) ???????????????????return?iptcc_standard_map(r, -NF_ACCEPT - 1); ?????????else if (strcmp(t->u.user.name, LABEL_DROP) == 0) ???????????????????return?iptcc_standard_map(r, -NF_DROP - 1); ?????????else if (strcmp(t->u.user.name, LABEL_QUEUE) == 0) ???????????????????return?iptcc_standard_map(r, -NF_QUEUE - 1); ?????????else if (strcmp(t->u.user.name, LABEL_RETURN) == 0) ???????????????????return?iptcc_standard_map(r, RETURN); ?????????else if (TC_BUILTIN(t->u.user.name, handle)) { ???????????????????/* Can't jump to builtins. */ ???????????????????errno = EINVAL; ???????????????????return 0; ?????????} else { ???????????????????/*?如果跳轉的目標是一條用戶自定義鏈,則執行下列操作*/ ???????????????????struct chain_head *c; ???????????????????DEBUGP("trying to find chain `%s': ", t->u.user.name); ???????????????????c =?iptcc_find_label(t->u.user.name, handle); #找到要跳轉的目的鏈的入口地址 ???????????????????if (c) { ????????????????????????????DEBUGP_C("found!\n"); ????????????????????????????r->type =?IPTCC_R_JUMP;??#將rule_head結構的type字段置為“跳轉” ????????????????????????????r->jump = c;?????????????#跳轉的目標為t->u.user.name所指示的鏈 ????????????????????????????c->references++;?????????#跳轉到的目的鏈因此而被引用了一次,則計數器++ ????????????????????????????return 1; ???????????????????} ???????????????????DEBUGP_C("not found :(\n"); ?????????} ? ?????????/*?如果不是用戶自定義鏈,它一定一個用戶自定義開發的target模塊,比如SNAT、LOG等。If not, kernel will reject... */ ?????????/* memset to all 0 for your memcmp convenience: don't clear version */ ?????????memset(t->u.user.name + strlen(t->u.user.name), ????????????????0, ????????????????FUNCTION_MAXNAMELEN - 1 - strlen(t->u.user.name)); ?????????r->type =?IPTCC_R_MODULE;??#比如SNAT,LOG等會執行到這里 ?????????set_changed(handle); ?????????return 1; } |
?在append_entry()函數最后,將執行的執行結果返回給ret,1表示成功;0表示失敗。然后在做一下善后清理工作,如果命令行中有-v則將內核中表的快照dump一份詳細信息出來顯示給用戶看:
if (verbose > 1)
?????dump_entries(*handle);
clear_rule_matches(&matches);?//釋放matches所占的存儲空間
?由struct ipt_entry e;所存儲的規則信息已經被提交給了handle對象對應的成員,因此將e所占的存儲空間也釋放:
if (e != NULL) {
??????????????free(e);
??????????????e = NULL;
}
?將全局變量opts復位,初始化時opts=original_opts。因為在解析--syn時tcp的解析參數被加進來了:
static struct option?original_opts[] = {
??????????????????{ "append", 1, NULL, 'A' },
??????????????????{ "delete", 1, NULL,??'D' },
??????????????????… …
}
至此,do_command()函數的執行就算全部完成了。
?
(二)、向內核提交變更:iptc_commit()
執行完do_command()解析完命令行參數后,用戶所作的變更僅被提交給了handle這個結構體變量,這個變量里的所有數據在執行iptc_commit()函數前都駐留在內存里。因此,在iptables-standalone.c里有如下的代碼語句:
ret =?do_command(argc, argv, &table, &handle); if (ret) ?????????ret =?iptc_commit(&handle); |
? 當do_command()執行成功后才會去執行iptc_commit()函數,將handle里的數據提交給Netfilter內核。
? ? ? ? iptc_commit()的實現函數為int TC_COMMIT(TC_HANDLE_T *handle),我們只分析IPv4的情形,因此專注于libiptc.c文件中該函數的實現。
? ? ? ? 在TC_COMMIT()函數中,又出現了我們在分析Netfilter中filter表時所見到的一些重要結構體STRUCT_REPLACE?*repl;STRUCT_COUNTERS_INFO?*newcounters;還有前面出現的struct chain_head?*c;結構體。
new_number =?iptcc_compile_table_prep(*handle, &new_size);
iptcc_compile_table_prep()該函數主要做的工作包含幾個方面:
a.初始化handle里每個struct chain_head{}結構體成員中的head_offset、foot_index和foot_offset。
b.對每個鏈(struct chain_head{})中的每條規則,再分別計算它們的offset和index。
c.計算handle所指示的表中所有規則所占的存儲空間的大小new_size,以及規則的總條數new_number。
?接下來,為指針repl;申請存儲空間,所申請的大小為sizeof(struct ipt_replace)+new_size。因為struct ipt_replace{}結構的末尾有一個柔性數組struct ipt_entry entries[0];?它是不計入sizeof的計算結果的。因此,iptables的所有規則實際上是存儲在struct ipt_entry entries[0]柔性數組中的,這里所有規則所占大小已經得到:new_size。
? 因為,每條規則entry都一個計數器,用來記錄該規則處理了多少數據包,注意結構體STRUCT_COUNTERS_INFO{}的末尾也有一個柔性數組struct xt_counters?counters[0];其中struct xt_counters{}才是真正的用于統計數據包的計數器。
然后開始初始化repl結構:
strcpy(repl->name, (*handle)->info.name); repl->num_entries = new_number; repl->size = new_size; ? repl->num_counters = (*handle)->info.num_entries; repl->valid_hooks = (*handle)->info.valid_hooks; |
? ? ? ? 緊接著對repl結構體中剩下的成員進行初始化,hook_entry[]、underflow[]等。對于用戶自定義鏈,其末尾的target.verdict=RETURN。
setsockopt(sockfd, TC_IPPROTO,?SO_SET_REPLACE,?repl,sizeof(*repl) + repl->size);
會觸發內核去執行前面我們看到的do_ipt_set_ctl()函數,如下:
static struct nf_sockopt_ops ipt_sockopts = { ?????????.pf??????????????= PF_INET, ?????????.set_optmin?????= IPT_BASE_CTL,???? ? .set_optmax????= IPT_SO_SET_MAX+1, ?????????.set???????????=?do_ipt_set_ctl, ?????????.get_optmin?????= IPT_BASE_CTL, ?????????.get_optmax????= IPT_SO_GET_MAX+1, ?????????.get???????????=?do_ipt_get_ctl, }; |
? ? ? ? 在do_ipt_set_ctl()中其核心還是執行do_replace()函數:
static int do_replace(void __user *user, unsigned int len) { ?????????int ret; ?????????struct ipt_replace tmp; ?????????struct xt_table_info *newinfo; ?????????void *loc_cpu_entry; ? ?????????if (copy_from_user(&tmp, user, sizeof(tmp)) != 0) ???????????????????return -EFAULT; ? ?????????/* Hack: Causes ipchains to give correct error msg --RR */ ?????????if (len != sizeof(tmp) + tmp.size) ???????????????????return -ENOPROTOOPT; … … } |
? ? ? ?其中copy_from_user()負責將用戶空間的repl變量中的內容拷貝到內核中的tmp中去。然后設置規則計數器newcounters,通過setsockopt系統調用將newcounters設置到內核:
setsockopt(sockfd, TC_IPPROTO,?SO_SET_ADD_COUNTERS,?newcounters, counterlen);
?此時,在do_ipt_set_ctl()中執行的是do_add_counters()函數。至此,iptables用戶空間的所有代碼流程就算分析完了。命令:
iptables –A INPUT –i eth0 –p tcp --syn –s?10.0.0.0/8 –d 10.1.28.184 –j ACCEPT
即被設置到內核的Netfilter規則中去了。
未完,待續…
?