iptables源代码分析之hashlimit模块

在读源代码之前,我们提出几个疑问
1.hashlimit模块的参数在应用层和内核层之间是如何传递的?
2.hashlimit模块的实现原理是什么?
3.hashlimit模块限速针对的桶还是每一个元素?
4.hashlimit模块是根据什么来创建哈希表的吗?五元组吗?

使用的内核源代码的版本如下:
iptables:1.4.21
内核代码:3.10.1

一、主线流程

hashlimit模块在内核中的实现是在net/netfilter/xt_hashlimit.c文件
hashlimit模块注册的match结构体
定位到hashlimit_mt函数

static bool
hashlimit_mt(const struct sk_buff *skb, struct xt_action_param *par)
{
	const struct xt_hashlimit_mtinfo1 *info = par->matchinfo;
	struct xt_hashlimit_htable *hinfo = info->hinfo;
	unsigned long now = jiffies;
	struct dsthash_ent *dh;
	struct dsthash_dst dst;
	bool race = false;
	u32 cost;

	//解析skb获取src ip,src port,dst ip,dst port赋值给dst结构体
	if (hashlimit_init_dst(hinfo, &dst, skb, par->thoff) < 0)
		goto hotdrop;

	//加rcu锁,查找dst对应的元素是否存在
	rcu_read_lock_bh();
	dh = dsthash_find(hinfo, &dst);
	if (dh == NULL) {
		//没找到,分配新的节点,race是什么
		dh = dsthash_alloc_init(hinfo, &dst, &race);
		if (dh == NULL) {
			rcu_read_unlock_bh();
			goto hotdrop;
		} else if (race) {
			/* Already got an entry, update expiration timeout */
			dh->expires = now + msecs_to_jiffies(hinfo->cfg.expire);
			rateinfo_recalc(dh, now, hinfo->cfg.mode);
		} else {
			dh->expires = jiffies + msecs_to_jiffies(hinfo->cfg.expire);
			rateinfo_init(dh, hinfo);
		}
	} else {
		//找到了,则更新其超时时间,更新dsthash_ent的rateinfo结构体的元素
		/* update expiration timeout */
		dh->expires = now + msecs_to_jiffies(hinfo->cfg.expire);
		rateinfo_recalc(dh, now, hinfo->cfg.mode);
	}

	if (info->cfg.mode & XT_HASHLIMIT_BYTES)
		cost = hashlimit_byte_cost(skb->len, dh);
	else
		cost = dh->rateinfo.cost;

	//cost:每通过一个包,消耗多少个令牌
	//credit:可使用的令牌数,是根据单位时间定时刷新的
	if (dh->rateinfo.credit >= cost) {
		/* below the limit */
		dh->rateinfo.credit -= cost;
		spin_unlock(&dh->lock);
		rcu_read_unlock_bh();
		return !(info->cfg.mode & XT_HASHLIMIT_INVERT);
	}

	spin_unlock(&dh->lock);
	rcu_read_unlock_bh();
	/* default match is underlimit - so over the limit, we need to invert */
	return info->cfg.mode & XT_HASHLIMIT_INVERT;

 hotdrop:
	par->hotdrop = true;
	return false;
}

xt_hashlimit_mtinfo1是hashlimit模块在用户态iptables和内核态netfilter之间通信的结构体
其处理流程如下:
1./解析skb获取源/目的ip,源/目的端口赋值给dst结构体
2.加锁查找dst对应的元素是否存在
2.1 不存在,则分配新的节点,并更新超时时间间隔和速率信息
2.2 存在,则更新超时时间间隔和速率信息
3.计算每个数据包要消耗多少令牌cost,credit是桶中可使用(发放)的令牌数
4.如cost小于credit,则从credit中减去cost个令牌
5.桶最大的令牌数为credit_cap

二、疏通分支流程细节

下面逐一细看下每个函数

2.1 hashlimit_init_dst函数

解析skb获取源/目的ip,源/目的端口赋值给dst结构体

static int
hashlimit_init_dst(const struct xt_hashlimit_htable *hinfo,
		   struct dsthash_dst *dst,
		   const struct sk_buff *skb, unsigned int protoff)
{
	__be16 _ports[2], *ports;
	u8 nexthdr;
	int poff;

	memset(dst, 0, sizeof(*dst));

	//判断ipv4/ipv6,解析源地址,目的地址
	switch (hinfo->family) {
	case NFPROTO_IPV4:
		//hashlimit-mode设置了dstip
		if (hinfo->cfg.mode & XT_HASHLIMIT_HASH_DIP)
			dst->ip.dst = maskl(ip_hdr(skb)->daddr,
			              hinfo->cfg.dstmask);
		
		//hashlimit-mode设置了srcip
		if (hinfo->cfg.mode & XT_HASHLIMIT_HASH_SIP)
			dst->ip.src = maskl(ip_hdr(skb)->saddr,
			              hinfo->cfg.srcmask);
		//如果hashlimit-mode既没设置srcport,也没设置dstport,则直接返回
		if (!(hinfo->cfg.mode &
		      (XT_HASHLIMIT_HASH_DPT | XT_HASHLIMIT_HASH_SPT)))
			return 0;
		nexthdr = ip_hdr(skb)->protocol;
		break;
	//ipv6处理函数
	default:
		BUG();
		return 0;
	}

	poff = proto_ports_offset(nexthdr);
	if (poff >= 0) {
		//通过skb结构体获取源/目的端口
		//_ports[0] 源端口
		//_ports[1] 目的端口
		ports = skb_header_pointer(skb, protoff + poff, sizeof(_ports),
					   &_ports);
	} else {
		_ports[0] = _ports[1] = 0;
		ports = _ports;
	}
	if (!ports)
		return -1;

	//hashlimit-mode设置了srcport或dstport,则解析srcport或dstport
	if (hinfo->cfg.mode & XT_HASHLIMIT_HASH_SPT)
		dst->src_port = ports[0];
	if (hinfo->cfg.mode & XT_HASHLIMIT_HASH_DPT)
		dst->dst_port = ports[1];
	return 0;
}

2.2 dsthash_find函数

static struct dsthash_ent *
dsthash_find(const struct xt_hashlimit_htable *ht,
	     const struct dsthash_dst *dst)
{
	struct dsthash_ent *ent;
	//通过dst结构体中各元素,使用jhash函数计算出hash值,即位置
	u_int32_t hash = hash_dst(ht, dst);

	//rcu遍历hash表下的链表,将链表元素和dst元素做比较,相等则代表查找成功,返回元素
	if (!hlist_empty(&ht->hash[hash])) {
		hlist_for_each_entry_rcu(ent, &ht->hash[hash], node)
			if (dst_cmp(ent, dst)) {
				spin_lock(&ent->lock);
				//ent为查找到的元素
				return ent;
			}
	}
	return NULL;
}

如果查找失败,则调用dsthash_alloc_init()函数新申请一个dsthash_ent结构体指针

2.3 dsthash_alloc_init函数

dsthash_alloc_init(struct xt_hashlimit_htable *ht,
		   const struct dsthash_dst *dst, bool *race)
{
	struct dsthash_ent *ent;

	spin_lock(&ht->lock);

	/* Two or more packets may race to create the same entry in the
	 * hashtable, double check if this packet lost race.
	 */
	//rcu_read_lock_bh()并不保证多个write的同步,此时会有多个包在哈希表中创建同样的entry节点
	ent = dsthash_find(ht, dst);
	if (ent != NULL) {
		spin_unlock(&ht->lock);
		*race = true;
		return ent;
	}

	/* initialize hash with random val at the time we allocate
	 * the first hashtable entry */
	if (unlikely(!ht->rnd_initialized)) {
		get_random_bytes(&ht->rnd, sizeof(ht->rnd));
		ht->rnd_initialized = true;
	}

	//哈希表中数量超过配置的最大值时,打印错误max count of %u reached
	if (ht->cfg.max && ht->count >= ht->cfg.max) {
		/* FIXME: do something. question is what.. */
		net_err_ratelimited("max count of %u reached\n", ht->cfg.max);
		ent = NULL;
	} else
		ent = kmem_cache_alloc(hashlimit_cachep, GFP_ATOMIC);//申请内存
	if (ent) {
		//赋值ent->dst
		memcpy(&ent->dst, dst, sizeof(ent->dst));
		spin_lock_init(&ent->lock);

		spin_lock(&ent->lock);
		hlist_add_head_rcu(&ent->node, &ht->hash[hash_dst(ht, dst)]);
		ht->count++;
	}
	spin_unlock(&ht->lock);
	return ent;
}

在调用dsthash_alloc_init函数之前,已经加rcu读锁调用dsthash_find函数进行过元素的查找,为什么在dsthash_alloc_init中还要调用dsthash_find函数再查找一遍呢?
rcu读锁并不能保证多个写者的写操作通过,所以此时可能有多个数据包在哈希表中创建同样的元素entry,所以此时需要二次查找。
查找成功后,则更新其超时时间间隔以及速率信息。

2.4 令牌的消耗和令牌的发放

static void rateinfo_recalc(struct dsthash_ent *dh, unsigned long now, u32 mode)
{
    //和当前时间的时间间隔
	unsigned long delta = now - dh->rateinfo.prev;
	u32 cap;

	if (delta == 0)
		return;
	//记录此次时间频率
	dh->rateinfo.prev = now;
	
	//hashlimit-mode设置为bytes模式
	if (mode & XT_HASHLIMIT_BYTES) {
		u32 tmp = dh->rateinfo.credit;
		dh->rateinfo.credit += CREDITS_PER_JIFFY_BYTES * delta;
		cap = CREDITS_PER_JIFFY_BYTES * HZ;
		if (tmp >= dh->rateinfo.credit) {/* overflow */
			dh->rateinfo.credit = cap;
			return;
		}
		} else {//我所采用的模式
		//更新 可使用的令牌数
		dh->rateinfo.credit += delta * CREDITS_PER_JIFFY;
		cap = dh->rateinfo.credit_cap;
	}
	//当可使用的令牌数大于最大值时,取最大值
	if (dh->rateinfo.credit > cap)
		dh->rateinfo.credit = cap;
}

在hashlimit_mt函数的末尾,有如下代码
每通过一个数据包,消耗cost个令牌
先判断hashlimit模式是否是XT_HASHLIMIT_BYTES,我们没采用这种模式
所以走cost = dh->rateinfo.cost这条语句
当可使用的令牌数大于此次消耗的令牌数时,在可使用的令牌数的基础上减去cost值,并将自旋锁以及rcu读锁都进行解锁操作。

hashlimit模块的限速算法采用的是令牌桶算法,不熟悉的朋友可以通过视频来了解下https://zhuanlan.zhihu.com/p/39031921

三、hashlimit模块的应用态和内核态通信流程
打开iptables的源代码,定位到libxt_hashlimit.c文件的hashlimit_mt_parse函数

static void hashlimit_mt_parse(struct xt_option_call *cb)
{
	struct xt_hashlimit_mtinfo3 *info = cb->data;

	xtables_option_parse(cb);
	switch (cb->entry->id) {
	case O_BURST://--hashlimit-burst参数
		info->cfg.burst = parse_burst(cb->arg, 2);
		break;
	case O_UPTO://--hashlimit-upto参数
		if (cb->invert)
			info->cfg.mode |= XT_HASHLIMIT_INVERT;
		if (parse_bytes(cb->arg, &info->cfg.avg, cb->udata, 2))
			info->cfg.mode |= XT_HASHLIMIT_BYTES;
		else if (!parse_rate(cb->arg, &info->cfg.avg, cb->udata, 2))
			xtables_param_act(XTF_BAD_VALUE, "hashlimit",
			          "--hashlimit-upto", cb->arg);
		break;
	case O_ABOVE://--hashlimit-above参数
		if (!cb->invert)
			info->cfg.mode |= XT_HASHLIMIT_INVERT;
		if (parse_bytes(cb->arg, &info->cfg.avg, cb->udata, 2))
			info->cfg.mode |= XT_HASHLIMIT_BYTES;
		else if (!parse_rate(cb->arg, &info->cfg.avg, cb->udata, 2))
			xtables_param_act(XTF_BAD_VALUE, "hashlimit",
			          "--hashlimit-above", cb->arg);
		break;
	case O_MODE://--hashlimit-mode {srcip|srcport|dstip|dstport}参数
		if (parse_mode(&info->cfg.mode, cb->arg) < 0)
			xtables_param_act(XTF_BAD_VALUE, "hashlimit",
			          "--hashlimit-mode", cb->arg);
		break;
	case O_SRCMASK://--hashlimit-srcmask参数
		info->cfg.srcmask = cb->val.hlen;
		break;
	case O_DSTMASK://--hashlimit-dstmask参数
		info->cfg.dstmask = cb->val.hlen;
		break;
	case O_RATEMATCH://速率匹配参数
		info->cfg.mode |= XT_HASHLIMIT_RATE_MATCH;
		break;
	case O_INTERVAL:
		if (!parse_interval(cb->arg, &info->cfg.interval))
			xtables_param_act(XTF_BAD_VALUE, "hashlimit",
				"--hashlimit-rate-interval", cb->arg);
	}
}

函数代码中分别解析了常用的各个参数,赋值到struct hashlimit_cfg3 cfg;结构体中的各个值。

总结:
回答最开始提出的问题
1.hashlimit模块的参数在应用层和内核层之间是如何传递的?
答:xt_hashlimit_mtinfo3结构体,hashlimit模块参数都存在hashlimit_cfg3结构体中

2.hashlimit模块的实现原理是什么?
答:令牌桶算法

3.hashlimit模块限速针对的桶还是每一个元素?
答:每次先在哈希表中查找元素,然后针对元素,进行令牌的消耗,大致流程如下
dh = dsthash_find(hinfo, &dst);//查找元素
cost = dh->rateinfo.cost;//获取数据包的cost令牌消耗
dh->rateinfo.credit -= cost;//消耗令牌,减少可使用的令牌总数

4.hashlimit模块是根据什么来创建哈希表的吗?五元组吗?
答:使用dsthash_dst结构体来计算每个元素在哈希表中的位置,是源ip,源端口,目的ip,目的端口
而dsthash_dst结构体元素的赋值是跟–hashlimit-mode的值相关的。
–hashlimit-mode {srcip|srcport|dstip|dstport}:分别以srcip,srcport,dstip,dstport来计算每个元素在哈希表中的位置。

/* hash table crap */
struct dsthash_dst {
union {
struct {
__be32 src;
__be32 dst;
} ip;
#if IS_ENABLED(CONFIG_IP6_NF_IPTABLES)
struct {
__be32 src[4];
__be32 dst[4];
} ip6;
#endif
};
__be16 src_port;
__be16 dst_port;
};
参考链接
http://os.51cto.com/art/201109/294482.htm
http://www.voidcn.com/article/p-fmhdhpbj-beu.html

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值