前言
上一篇博客大体上讲解了什么是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.youkuaiyun.com/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.youkuaiyun.com/a1558451960/article/details/95329827
https://blog.youkuaiyun.com/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