天天看點

防火牆之filter表(一)---AF_INET協定族1. 初始化2. filter表的注冊

這篇筆記記錄了AF_INET協定族的filter表的初始化,通過filter表,可以看出協定族是如何與Netfilter架構配合,完成防火牆規則的過濾和管理的。filter表涉及内容較多,分兩篇筆記完成,涉及的核心代碼檔案有:

代碼路徑 說明
net/ipv4/netfilter/iptable_filter.c IPv4 filter表實作

filter表以子產品的形式存在,其控制開關為:CONFIG_IP_NF_FILTER。

1. 初始化

初始化函數即子產品初始化函數,如下:

static int __net_init iptable_filter_net_init(struct net *net)
{
	//注冊filter表
	net->ipv4.iptable_filter =
		ipt_register_table(net, &packet_filter, &initial_table.repl);
	if (IS_ERR(net->ipv4.iptable_filter))
		return PTR_ERR(net->ipv4.iptable_filter);
	return 0;
}
static struct pernet_operations iptable_filter_net_ops = {
	.init = iptable_filter_net_init,
	.exit = iptable_filter_net_exit,
};
static int __init iptable_filter_init(void)
{
	int ret;

	if (forward < 0 || forward > NF_MAX_VERDICT) {
		printk("iptables forward must be 0 or 1\n");
		return -EINVAL;
	}
	/* Entry 1 is the FORWARD hook */
	initial_table.entries[1].target.verdict = -forward - 1;
	ret = register_pernet_subsys(&iptable_filter_net_ops);
	if (ret < 0)
		return ret;
	//向Netfilter架構注冊三個鈎子:INPUT、OUTPUT、FORWARD
	ret = nf_register_hooks(ipt_ops, ARRAY_SIZE(ipt_ops));
	if (ret < 0)
		goto cleanup_table;
	return ret;
...
}
           

初始化幹了兩件重要的事情:

  1. 注冊filter表
  2. 注冊鈎子

内容較多,這篇筆記隻看filter表的注冊過程,鈎子的注冊過程見下一篇筆記。

2. filter表的注冊

在看具體的實作之前,需要先來看看系統資料庫時傳入的兩個參數:packet_filter和initial_table.repl。

//filter表中的規則隻能在LOCAL_IN、FORWARD、LOCAL_OUT三個點工作
#define FILTER_VALID_HOOKS ((1 << NF_INET_LOCAL_IN) | \
			    (1 << NF_INET_FORWARD) | \
			    (1 << NF_INET_LOCAL_OUT))
//packet_filter就是Netfilter架構定義的表結構struct xt_table對象,這裡表名字為filter
static struct xt_table packet_filter = {
	.name		= "filter",
	.valid_hooks	= FILTER_VALID_HOOKS,
	.lock		= RW_LOCK_UNLOCKED,
	.me		= THIS_MODULE,
	.af		= AF_INET,
};
           

該結構正是Netfilter對table的定義,那麼額外的initial_table.repl又是幹什麼的呢?

static struct
{
	struct ipt_replace repl;
	//這個組織是很有講究的,緊挨着在repl的後面放了4條規則,因為repl的最後一個成員
	//entries是零長度數組,是以其直接指向了這裡的規則。filter表注冊時内置了4調規則
	struct ipt_standard entries[3];
	struct ipt_error term;
} initial_table;
           

和注冊相關的核心資料結構是struct ipt_replace。iptables工具在更新規則時實際上是整體替換的,即先從核心dump所有規則,然後修改,最後整體替換,并非單獨修改某一條規則。注冊也可以了解為是一種替換,隻是要替換表原本不存在而已,是以隻要設計上面在某些地方做些許相容,注冊和替換的代碼邏輯是可以複用的。而struct ipt_replace結構就是AF_INET協定族專門為了實作table替換而定義的輔助結構。

/* The argument to IPT_SO_SET_REPLACE. */
struct ipt_replace
{
	char name[IPT_TABLE_MAXNAMELEN];
	/* Which hook entry points are valid: bitmask.  You can't change this. */
	unsigned int valid_hooks;
	/* Number of entries */
	unsigned int num_entries;
	/* Total size of new entries */
	unsigned int size;
	/* Hook entry points. */
	unsigned int hook_entry[NF_INET_NUMHOOKS];
	/* Underflow points. */
	unsigned int underflow[NF_INET_NUMHOOKS];
	/* Information about old entries: */
	/* Number of counters (must be equal to current number of entries). */
	unsigned int num_counters;
	/* The old entries' counters. */
	struct xt_counters __user *counters;
	//末尾指針很重要,指向規則,對于initial_table來說就是repl後面的4條規則
	struct ipt_entry entries[0];
};
           

如Netfilter之table、rule、match、target資料結構中關于struct xt_table和struct xt_table_info的介紹,可以發現struct ipt_replace差不多是這兩個結構的綜合,而且它們共同的字段的含義确實是一緻的。

回頭再來看filter表對struct ipt_replace的初始化:

.repl = {
	.name = "filter",
	.valid_hooks = FILTER_VALID_HOOKS,
	//初始化時有4條規則,如上,initial_table中在repl後面定義了4條規則
	.num_entries = 4,
	//size大小就是所有規則大小的累加
	.size = sizeof(struct ipt_standard) * 3 + sizeof(struct ipt_error),
	//因為hook_entry[]和underflow[]分别記錄的是該表在每個hook點上的第一條和
	//最後一條規則距離表起點的偏移量,并且目前表中每個hook點隻有一條規則,是以
	//它們的初始化值都是相同的
	.hook_entry = {
		[NF_INET_LOCAL_IN] = 0,
		[NF_INET_FORWARD] = sizeof(struct ipt_standard),
		[NF_INET_LOCAL_OUT] = sizeof(struct ipt_standard) * 2,
	},
	.underflow = {
		[NF_INET_LOCAL_IN] = 0,
		[NF_INET_FORWARD] = sizeof(struct ipt_standard),
		[NF_INET_LOCAL_OUT] = sizeof(struct ipt_standard) * 2,
	},
	//下面是對預設的4條規則的初始化。雖然entries和term并不屬于repl,但是因為它們
	//在記憶體上面是連續的,是以可以使用這種方式進行初始化,核心中有很多這樣的小技巧

	//三個hook點各放一條無條件接受的規則。這三條規則實際上就是hook點的政策規則,
	//它們會永遠位于給hook點上所有規則的末尾,當其它規則都不比對時,交由該規則
	//覺得資料包最後的命運
	.entries = {
		IPT_STANDARD_INIT(NF_ACCEPT),	/* LOCAL_IN */
		IPT_STANDARD_INIT(NF_ACCEPT),	/* FORWARD */
		IPT_STANDARD_INIT(NF_ACCEPT),	/* LOCAL_OUT */
	},
	//這裡放置了一條錯誤規則。個人了解:這條規則應該永遠都不會被周遊到,因為在周遊
	//規則時是根據hook點周遊的,而hook點規則由hook_entry[]和underflow[]定界,永遠
	//都不應該走到這裡,放着這麼一條應該是為了對代碼異常場景提供一種保護
	.term = IPT_ERROR_INIT,			/* ERROR */
},
           

2.1 ipt_register_table()

表的注冊并不是直接調用Netfilter架構提供的xt_register_table(),AF_INET協定族對注冊過程有特殊處理,是以對注冊過程用ipt_register_table()進行了封裝。

struct xt_table *ipt_register_table(struct net *net, struct xt_table *table,
				    const struct ipt_replace *repl)
{
	int ret;
	struct xt_table_info *newinfo;
	//該結構就是為了複用表替換代碼而虛構出來的表
	struct xt_table_info bootstrap
		= { 0, 0, 0, { 0 }, { 0 }, { } };
	void *loc_cpu_entry;
	struct xt_table *new_table;

	//按照規則大小配置設定struct xt_table_info結構
	newinfo = xt_alloc_table_info(repl->size);
	if (!newinfo) {
		ret = -ENOMEM;
		goto out;
	}
	//将repl末尾儲存的規則拷貝到struct xt_table_info中本地CPU的指針位置
	loc_cpu_entry = newinfo->entries[raw_smp_processor_id()];
	memcpy(loc_cpu_entry, repl->entries, repl->size);
	//用指定的資訊檢查新table中的規則資訊(newinfo指向)
	ret = translate_table(table->name, table->valid_hooks,
		  		newinfo, loc_cpu_entry, repl->size,
			    repl->num_entries, repl->hook_entry, repl->underflow);
	if (ret != 0)
		goto out_free;
	//調用Netfilter架構的tablle注冊接口将filter表注冊到系統中
	new_table = xt_register_table(net, table, &bootstrap, newinfo);
	if (IS_ERR(new_table)) {
		ret = PTR_ERR(new_table);
		goto out_free;
	}
	return new_table;
...
}
           

2.1.1 translate_table()

如注釋所述,該函數的作用就是用調用者指定的資訊校驗并初始化newinfo,其實就是對要注冊的表内容進行合法性檢查。

/* Checks and translates the user-supplied table segment (held in newinfo) */
static int translate_table(const char *name, unsigned int valid_hooks,
		struct xt_table_info *newinfo, void *entry0, unsigned int size,
		unsigned int number, const unsigned int *hook_entries,
		const unsigned int *underflows)
{
	unsigned int i;
	int ret;
	//規則總的占用位元組數和規則數目
	newinfo->size = size;
	newinfo->number = number;
	/* Init all hooks to impossible value. */
	for (i = 0; i < NF_INET_NUMHOOKS; i++) {
		newinfo->hook_entry[i] = 0xFFFFFFFF;
		newinfo->underflow[i] = 0xFFFFFFFF;
	}
	duprintf("translate_table: size %u\n", newinfo->size);
	i = 0;
	//檢查規則内的偏移量成員指定的是否準确
	ret = IPT_ENTRY_ITERATE(entry0, newinfo->size, check_entry_size_and_hooks,
				newinfo, entry0, entry0 + size, hook_entries, underflows, &i);
	if (ret != 0)
		return ret;
	//i記錄的是檢測通過的規則數目,如果和指定的數字不一緻,說明有規則沒有通過檢查
	if (i != number) {
		duprintf("translate_table: %u not %u entries\n", i, number);
		return -EINVAL;
	}
	//在上一步的檢測過程中如果規則檢測pass,會對hook_entry和underflow指派,
	//這裡檢查是否所有有效的HOOK點都已經指定了合理的值
	for (i = 0; i < NF_INET_NUMHOOKS; i++) {
		/* Only hooks which are valid */
		if (!(valid_hooks & (1 << i)))
			continue;
		if (newinfo->hook_entry[i] == 0xFFFFFFFF) {
			duprintf("Invalid hook entry %u %u\n", i, hook_entries[i]);
			return -EINVAL;
		}
		if (newinfo->underflow[i] == 0xFFFFFFFF) {
			duprintf("Invalid underflow %u %u\n", i, underflows[i]);
			return -EINVAL;
		}
	}
	//檢查表中的規則是否構成環路
	if (!mark_source_chains(newinfo, valid_hooks, entry0))
		return -ELOOP;
	//無論是match還是target,都有對應的check回調,這裡檢查每條規則的match和target
	i = 0;
	ret = IPT_ENTRY_ITERATE(entry0, newinfo->size, find_check_entry, name, size, &i);
	if (ret != 0) {
		//沒有通過檢查,清除所有的規則
		IPT_ENTRY_ITERATE(entry0, newinfo->size, cleanup_entry, &i);
		return ret;
	}
	//通過了所有的檢查,将規則資訊拷貝到其它CPU的記憶體中
	for_each_possible_cpu(i) {
		if (newinfo->entries[i] && newinfo->entries[i] != entry0)
			memcpy(newinfo->entries[i], entry0, newinfo->size);
	}
	return ret;
}
           

如上,注冊過程中,會逐個檢查規則資訊,而周遊過程都是用宏IPT_ENTRY_ITERATE實作的,該宏的定義比較複雜,難以了解,這裡直接看展開後的結果:

{
	unsigned int __i, __n;
	int __ret = 0;
	struct ipt_entry *__entry;
	//size是表中所有規則的大小,__entry->next_offset儲存的是距下一條規則的偏移
	for (__i = 0, __n = 0; __i < (size); __i += __entry->next_offset, __n++) {
		//指向一條規則的起點
		__entry = (void *)(entries) + __i;
		//__n在本場景不可能會小于0
		if (__n < 0)
			continue;
		//如果回調函數fn()傳回非0,結束周遊,并且傳回非0值
		__ret = fn(__entry , ## args);
		if (__ret != 0)	
			break;
	}
	__ret;
}
           

2.1.1.1 規則大小檢查:check_entry_size_and_hooks()

static int check_entry_size_and_hooks(struct ipt_entry *e,
	struct xt_table_info *newinfo, unsigned char *base, unsigned char *limit,
	const unsigned int *hook_entries, const unsigned int *underflows,
	unsigned int *i)
{
	unsigned int h;
	//邊界對齊和規則長度是否超過了總的大小
	if ((unsigned long)e % __alignof__(struct ipt_entry) != 0
	    || (unsigned char *)e + sizeof(struct ipt_entry) >= limit) {
		duprintf("Bad offset %p\n", e);
		return -EINVAL;
	}
	//距下一條規則的偏移不合理,因為一條規則至少包含struct ipt_entry和struct
	//ipt_entry_target,是以next_offset一定大于這兩個結構體的長度
	if (e->next_offset < sizeof(struct ipt_entry) + sizeof(struct ipt_entry_target)) {
		duprintf("checking: element %p size %u\n", e, e->next_offset);
		return -EINVAL;
	}
	//如果指定規則屬于某個HOOK點的邊界規則,指派hook_entry和underflow
	for (h = 0; h < NF_INET_NUMHOOKS; h++) {
		if ((unsigned char *)e - base == hook_entries[h])
			newinfo->hook_entry[h] = hook_entries[h];
		if ((unsigned char *)e - base == underflows[h])
			newinfo->underflow[h] = underflows[h];
	}
	//規則命中的計數器清零
	e->counters = ((struct xt_counters) { 0, 0 });
	e->comefrom = 0;
	//規則檢查通過,計數器累加1
	(*i)++;
	return 0;
}
           

2.1.1.2 校驗規則:find_check_entry()

每條規則都是有0個或多個match以及1個target組成,每個match和target都有自己特有的參數,在校驗規則時需要回調match和target的check_xxx()回調對規則進行校驗。

static int find_check_entry(struct ipt_entry *e, const char *name, 
	unsigned int size, unsigned int *i)
{
	struct ipt_entry_target *t;
	struct xt_target *target;
	int ret;
	unsigned int j;
	//對規則中的标準比對以及target之間的偏移量進行校驗
	ret = check_entry(e, name);
	if (ret)
		return ret;
	//對規則中match進行校驗,包括該match是否注冊以及調用match自身check()回調
	j = 0;
	ret = IPT_MATCH_ITERATE(e, find_check_match, name, &e->ip, e->comefrom, &j);
	if (ret != 0)
		goto cleanup_matches;
	//擷取規則中的target,檢查該target是否存在
	t = ipt_get_target(e);
	target = try_then_request_module(xt_find_target(AF_INET, t->u.user.name,
				t->u.user.revision), "ipt_%s", t->u.user.name);
	if (IS_ERR(target) || !target) {
		duprintf("find_check_entry: `%s' not found\n", t->u.user.name);
		ret = target ? PTR_ERR(target) : -ENOENT;
		goto cleanup_matches;
	}
	//找到對應的target子產品,用u.kernel.target指向該target
	t->u.kernel.target = target;
	//調用target的check()回調校驗target
	ret = check_target(e, name);
	if (ret)
		goto err;
	(*i)++;
	return 0;
...
}