协议栈数据包快速转发的实现(2)

前言

上一篇博客大体上讲解了什么是SNAT和DNAT。然后在博客的最后引入了一个知识点-连接跟踪(conntrack)。今天我们就来看看连接跟踪和我们的数据包快转实现有什么关系,怎么利用连接跟踪来实现数据包快转的功能

数据报文唯一性

四元组是:
源IP地址、目的IP地址、源端口、目的端口

五元组是:
源IP地址、目的IP地址、协议号、源端口、目的端口

七元组是:
源IP地址、目的IP地址、协议号、源端口、目的端口,服务类型以及接口索引

协议号:IP是网络层协议,IP头中的协议号用来说明IP报文中承载的是哪种协议,协议号标识上层是什么协议(一般是传输层协议,比如6 TCP,17 UDP;但也可能是网络层协议,比如1 ICMP;也可能是应用层协议,比如89 OSPF)。

TCP/UDP是传输层协议,TCP/UDP的端口号用来说明是哪种上层应用,比如TCP 80代表WWW,TCP 23代表Telnet,UDP 69代表TFTP。
目的主机收到IP包后,根据IP协议号确定送给哪个模块(TCP/UDP/ICMP…)处理,送给TCP/UDP模块的报文根据端口号确定送给哪个应用程序处理。

我们这里的快速转发tcp和udp,其实就是利用了发送和接收报文的五元组来实现手动的快速转发。而五元组的信息都可以通过连接跟踪拿到。这也是链接跟踪对于我们自己的快速转发的重要性。

连接跟踪

连接跟踪是netfilter中重要的一部分(关于netfilter在这里不会进行深入的讲解,只会讲解和连接跟踪相关的)。连接跟踪顾名思义代表的是数据包的链接的状态。
在这里贴出几篇博客,该博客中讲解netfilter和连接跟踪的相关的基础知识,希望大家可以仔细看看

http://www.zsythink.net/archives/1199

https://segmentfault.com/a/1190000019605260
http://blog.chinaunix.net/uid-20786208-id-5137728.html

上面这三篇文章讲解的比价好,在我学习连接跟踪的时候,我也是查了比较多的资料然后结合代码学习。

连接跟踪数据结构

虽然在上面的博客中都讲解了连接跟踪的数据结构。但是我觉得在这里还是必须要提及一下。算是对于上面博客的一个补充。

struct nf_conn {
	/* Usage count in here is 1 for hash table/destruct timer, 1 per skb,
           plus 1 for any connection(s) we are `master' for */
	struct nf_conntrack ct_general;

	spinlock_t lock;

	/* XXX should I move this to the tail ? - Y.K */
	/* These are my tuples; original and reply */
	struct nf_conntrack_tuple_hash tuplehash[IP_CT_DIR_MAX];

	/* Have we seen traffic both ways yet? (bitset) */
	unsigned long status;

	/* If we were expected by an expectation, this will be it */
	struct nf_conn *master;

	/* Timer function; drops refcnt when it goes off. */
	struct timer_list timeout;

#if defined(CONFIG_NF_CONNTRACK_MARK)
	u_int32_t mark;
#endif

#ifdef CONFIG_NF_CONNTRACK_SECMARK
	u_int32_t secmark;
#endif

	/* Extensions */
	struct nf_ct_ext *ext;
#ifdef CONFIG_NET_NS
	struct net *ct_net;
#endif

	/* Storage reserved for other modules, must be the last member */
	union nf_conntrack_proto proto;
};

在上一篇博客中我们讲解连接跟踪会记录发送的tuple和接收tuple的信息。该字段就是struct nf_conntrack_tuple_hash tuplehash[IP_CT_DIR_MAX];该数组有两个取值,分别是发送和接收方向的tuple.

enum ip_conntrack_dir {
	IP_CT_DIR_ORIGINAL,
	IP_CT_DIR_REPLY,
	IP_CT_DIR_MAX
};
  • IP_CT_DIR_ORIGINAL代表的是发送方向的tuple
  • IP_CT_DIR_REPLY代表的是接收发现的tuple

我们在来看看struct nf_conntrack_tuple_hash 结构

struct nf_conntrack_tuple_hash {
	struct hlist_nulls_node hnnode;
	struct nf_conntrack_tuple tuple;
};
struct nf_conntrack_tuple {
	struct nf_conntrack_man src;

	/* These are the parts of the tuple which are fixed. */
	struct {
		union nf_inet_addr u3;
		union {
			/* Add other protocols here. */
			__be16 all;

			struct {
				__be16 port;
			} tcp;
			struct {
				__be16 port;
			} udp;
			struct {
				u_int8_t type, code;
			} icmp;
			struct {
				__be16 port;
			} dccp;
			struct {
				__be16 port;
			} sctp;
			struct {
				__be16 key;
			} gre;
		} u;

		/* The protocol. */
		u_int8_t protonum;

		/* The direction (for tuplehash) */
		u_int8_t dir;
	} dst;
};
union nf_inet_addr {
	__u32		all[4];
	__be32		ip;
	__be32		ip6[4];
	struct in_addr	in;
	struct in6_addr	in6;
};

struct nf_conntrack_man {
	union nf_inet_addr u3;
	union nf_conntrack_man_proto u;
	/* Layer 3 protocol */
	u_int16_t l3num;
};

数据包文到达内核协议栈时,使用sk_buff{}(即skb),其类型为struct nf_conntrack *;该结构记录了连接记录被公开应用的计数,也方便其他地方对连接跟踪的引用;

在这里就必须做一个总结了。这里也是说明为什么我们的快转模块需要借助连接跟踪的信息。

  • 在连接跟踪的结构体中分别有两个方向的tuple-发送和接收
  • 每一个tuple我们可以获取到发送的源ip地址以及目的ip地址,根据协议类型获取源端口号以及目的端口号,以及协议号。

从连接跟踪中我们可以获取到什么?很明显我们已经获取了决定一个数据包(TCP/UDP)唯一性的五元组。

那么是不是只靠连接跟踪,我们就可以写出我们的快转模块了?答案是不行的,我们还需要一些其他的额外信息,这个后面我们在引入和讲解。



连接跟踪的建立

在这里我直接应用了第三篇博客中的图,第三篇博客其实已经讲解了初始化的过程。但是有些相关的知识点还是需要补充和提及一下
在这里插入图片描述

同样的我们通过上面的博客知道,连接跟踪的初始化是注册在PRE_ROUTING链。而PRE_ROUTING链是在ip_rcv函数中调用的。

在这里需要说明一下ip_rcv函数。该函数是ip层的入口函数,该函数十分的重要。他首先会检测数据报文是否合法。然后会调用注册在PRE_ROUTING链的函数。最后调用ip_rcv_finish(在linux内核中一般都是先调用检测函数,再调用处理函数。而且命名有一定的规律可循。检测函数一般为do_something,处理函数一般为do_something_finish。或者是do_something和do_something2)。


我们来看看ip_rcv的函数源码

int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev)
{
	const struct iphdr *iph;
	u32 len;

	/* When the interface is in promisc. mode, drop all the crap
	 * that it receives, do not try to analyse it.
	 */
	if (skb->pkt_type == PACKET_OTHERHOST)
		goto drop;


	IP_UPD_PO_STATS_BH(dev_net(dev), IPSTATS_MIB_IN, skb->len);

	if ((skb = skb_share_check(skb, GFP_ATOMIC)) == NULL) {
		IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_INDISCARDS);
		goto out;
	}

	if (!pskb_may_pull(skb, sizeof(struct iphdr)))
		goto inhdr_error;

	iph = ip_hdr(skb);

	/*
	 *	RFC1122: 3.2.1.2 MUST silently discard any IP frame that fails the checksum.
	 *
	 *	Is the datagram acceptable?
	 *
	 *	1.	Length at least the size of an ip header
	 *	2.	Version of 4
	 *	3.	Checksums correctly. [Speed optimisation for later, skip loopback checksums]
	 *	4.	Doesn't have a bogus length
	 */

	if (iph->ihl < 5 || iph->version != 4)
		goto inhdr_error;

	if (!pskb_may_pull(skb, iph->ihl*4))
		goto inhdr_error;

	iph = ip_hdr(skb);

	if (unlikely(ip_fast_csum((u8 *)iph, iph->ihl)))
		goto inhdr_error;

	len = ntohs(iph->tot_len);
	if (skb->len < len) {
		IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_INTRUNCATEDPKTS);
		goto drop;
	} else if (len < (iph->ihl*4))
		goto inhdr_error;

	/* Our transport medium may have padded the buffer out. Now we know it
	 * is IP we can trim to the true length of the frame.
	 * Note this now means skb->len holds ntohs(iph->tot_len).
	 */
	if (pskb_trim_rcsum(skb, len)) {
		IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_INDISCARDS);
		goto drop;
	}

	/* Remove any debris in the socket control block */
	memset(IPCB(skb), 0, sizeof(struct inet_skb_parm));

	/* Must drop socket now because of tproxy. */
	skb_orphan(skb);

	return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, dev, NULL,
		       ip_rcv_finish);

inhdr_error:
	IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_INHDRERRORS);
drop:
	kfree_skb(skb);
out:
	return NET_RX_DROP;
}

这里需要说明一下。ip_rcv_finish函数是路由的重要函数。他会判断数据包是发往本地的,方式需要nat转发,发送到上一级路由器的。大家有兴趣可以去了解一下数据包在协议栈中的流向。

NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, dev, NULL,
		       ip_rcv_finish);

这里就是在循环调用Pre_ROUTING注册的函数(会根据优先级调用)。那这里我们也得出了一个重要的结论。PRE_ROUTING是在ip_rcv函数中被调用的,即ip层封包的入口处。

连接跟踪的初始化

通过上文,我们知道在PRE_ROUTING处注册了2个函数(参照给出的第三个博客)。分别为ipv4_conntrack_defrag和ipv4_conntrack_in。并且ipv4_conntrack_defrag优先级更高(netfilter注册的每一条规则都是有优先级的,由priority字段指定)。
在这里我就直接给出结论了。ipv4_conntrack_defrag函数主要是检测是否被分片,如果被分片就重组。而连接跟踪的建立实际上在第二个函数ipv4_conntrack_in中。ipv4_conntrack_in实际调用了nf_conntrack_in函数。


关于nf_conntrack_in函数的分析,可以参考https://blog.csdn.net/City_of_skey/article/details/84934016
http://blog.chinaunix.net/uid-26517122-id-4293135.html
在这里我就不在详细介绍了。在这里只是说明几个点。

  • ipv4_conntrack_in函数会调用resolve_normal_ct函数
  • resolve_normal_ct函数会判断连接跟踪是否存在,不存在就去创建。然后设置连接的状态

在这里我只分析到连接跟踪的建立过程。因为我们最终的快转模块在需要利用到连接跟踪的建立。

连接跟踪发送tuple和接收tuple

这里我还是以画图的方式来讲解一下发送和接收tuple的变化过程。这个过程非常重要。希望大家能够理解
现在在这里假设我们的pc 192.168.100.100 访问百度 14.215.177.38网页
路由器的lan口ip地址为192.168.100.254, 路由器wan口的ip地址为192.168.1.2。

发送tuple

根据我们上面讲解的。首先我们的pc发送的数据包到达路由器。由路由器的ip_rcv函数接收。这因为是个新的数据包。所以会创建一条新的连接跟踪
在这里插入图片描述

这里的发送tuple正如上图所示。

接收tuple

在这里我需要提醒一下大家,ip_rcv新建tuple。ip_rcv是在ip层数据包入口处。那么此时肯定没有经过nat转换。接收的tuple会有一点出乎大家的意料。说实话最开始我也难以理解。大家理解是在nat之前,ip_rcv新建的就不难了

当pc访问到百度服务器的时候。百度服务器会将数据回复到我们。这个时候就会新建接收的tuple。
在这里插入图片描述
此时接收tuple的源地址变成了百度的。目的地址不再是路由器lan口的地址。而是wan口的地址。源端口号,目的端口号也发生了改变。只有当该数据包经过nat转发之后,目的地址才会编程pc的ip地址。目的端口号编程以前的5678。这一点大家必须注意。


连接跟踪中的helper

在这里还需要引入一个新的模块-期望连接(helper)。那么该模块有什么用呢?其实是为了根据现存的一个连接去创建一个新的连接。比如我们用的FTP协议。FTP协议使用了两个端口号(20 21)。为了将这两个端口号联系起来。同时也是将20和21创建的连接跟踪进行一个绑定。所以内核设计出来一套期望连接的模块
关于期望连接,内核最经典的即是FTP的实现。这里贴出两篇博客,希望对大家有用
https://blog.csdn.net/a1558451960/article/details/95329827
https://blog.csdn.net/jasonchen_gbd/article/details/44877343?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.add_param_isCf&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.add_param_isCf

结束语

在下一篇博客中,我就会上实际的代码。到时候我们再来分析。怎样实现我们的快转模块。欢迎大家一起交流。欢迎加入qq群:610849576

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值