kk Blog —— 通用基础


date [-d @int|str] [+%s|"+%F %T"]
netstat -ltunp
sar -n DEV 1

Linux内核之GRE处理分析

https://blog.csdn.net/s2603898260/article/details/115773153

https://blog.csdn.net/u014044624/article/details/106596000


GRE

GRE(Generic Routing Encapsulation,通用路由封装)协议是对某些网络层协议(如IP 和IPX)的数据报文进行封装,使这些被封装的数据报文能够在另一个网络层协议(如IP)中传输。

在大多数常规情况下,系统拥有一个有效载荷(或负载)包,需要将它封装并发送至某个目的地。首先将有效载荷封装在一个 GRE 包中,然后将此 GRE 包封装在其它某协议中并进行转发。此外发协议即为发送协议。当 IPv4 被作为 GRE 有效载荷传输时,协议类型字段必须被设置为 0x800 。当一个隧道终点拆封此含有 IPv4 包作为有效载荷的 GRE 包时, IPv4 包头中的目的地址必须用来转发包,并且需要减少有效载荷包的 TTL 。值得注意的是,在转发这样一个包时,如果有效载荷包的目的地址就是包的封装器(也就是隧道另一端),就会出现回路现象。在此情形下,必须丢弃该包。当 GRE 包被封装在 IPv4 中时,需要使用 IPv4 协议 47 。

GRE采用了Tunnel(隧道)技术,是VPN(Virtual Private Network)的第三层隧道协议。Tunnel 是一个虚拟的点对点的连接,提供了一条通路使封装的数据报文能够在这个通路上传输,并且在一个Tunnel 的两端分别对数据报进行封装及解封装。

GRE包发送过程:

发送过程是很简单的,因为 router A 上配置了一条路由规则,凡是发往 10.0.2.0 网络的包都要经过 netb 这个 tunnel 设备,在内核中经过 forward 之后就最终到达这个 GRE tunnel 设备的 ndo_start_xmit(),也就是 ipgre_tunnel_xmit() 函数。这个函数所做的事情无非就是通过 tunnel 的 header_ops 构造一个新的头,并把对应的外部 IP 地址填进去,最后发送出去。

Linux kernel函数调用分析:

GRE包接收过程:

接收过程,即 router B 上面进行的操作。这里需要指出的一点是,GRE tunnel 自己定义了一个新的 IP proto,也就是 IPPROTO_GRE。当 router B 收到从 router A 过来的这个包时,它暂时还不知道这个是 GRE 的包,它首先会把它当作普通的 IP 包处理。因为外部的 IP 头的目的地址是该路由器的地址,所以它自己会接收这个包,把它交给上层,到了 IP 层之后才发现这个包不是 TCP,UDP,而是 GRE,这时内核会转交给 GRE 模块处理。

ipgre_rcv() 所做的工作是:通过外层IP 头找到对应的 tunnel,然后剥去外层 IP 头,把这个“新的”包重新交给 IP 栈去处理,像接收到普通 IP 包一样。到了这里,“新的”包处理和其它普通的 IP 包已经没有什么两样了:根据 IP 头中目的地址转发给相应的 host。

注:在这里可以把gre当做L4层协议。

Linux kernel函数调用分析:

Linux nf_conntrack连接跟踪的实现

http://bbs.chinaunix.net/thread-4082396-1-1.html

连接跟踪,顾名思义,就是识别一个连接上双方向的数据包,同时记录状态。下面看一下它的数据结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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;

	/* Connection tracking(链接跟踪)用来跟踪、记录每个链接的信息(目前仅支持IP协议的连接跟踪)。
	    每个链接由“tuple”来唯一标识,这里的“tuple”对不同的协议会有不同的含义,例如对tcp,udp
		 来说就是五元组: (源IP,源端口,目的IP, 目的端口,协议号),对ICMP协议来说是: (源IP, 目
	    的IP, id, type, code), 其中id,type与code都是icmp协议的信息。链接跟踪是防火墙实现状态检
	    测的基础,很多功能都需要借助链接跟踪才能实现,例如NAT、快速转发、等等。*/
	struct  nf_conntrack_tuple_hash  tuplehash[IP_CT_DIR_MAX];

	unsigned long  status;               /* 可以设置由enum ip_conntrack_status中描述的状态 */

	struct  nf_conn  *master;         /* 如果该连接是某个连接的子连接,则master指向它的主连接 */
	/* Timer function; drops refcnt when it goes off. */
	struct  timer_list  timeout;

	union nf_conntrack_proto proto;       /* 用于保存不同协议的私有数据 */
	/* Extensions */
	struct nf_ct_ext *ext;            /* 用于扩展结构 */
};

这个结构非常简单,其中最主要的就是tuplehash(跟踪连接双方向数据)和status(记录连接状态),这也连接跟踪最主要的功能。

在status中可以设置的标志,由下面的enum ip_conntrack_status描述,它们可以共存。这些标志设置后就不会再被清除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
enum ip_conntrack_status {
	IPS_EXPECTED_BIT = 0,     /* 表示该连接是个子连接 */
	IPS_SEEN_REPLY_BIT = 1,       /* 表示该连接上双方向上都有数据包了 */
	IPS_ASSURED_BIT = 2,      /* TCP:在三次握手建立完连接后即设定该标志。UDP:如果在该连接上的两个方向都有数据包通过,
								则再有数据包在该连接上通过时,就设定该标志。ICMP:不设置该标志 */
	IPS_CONFIRMED_BIT = 3,        /* 表示该连接已被添加到net->ct.hash表中 */
	IPS_SRC_NAT_BIT = 4,      /*在POSTROUTING处,当替换reply tuple完成时, 设置该标记 */
	IPS_DST_NAT_BIT = 5,      /* 在PREROUTING处,当替换reply tuple完成时, 设置该标记 */
	/* Both together. */
	IPS_NAT_MASK = (IPS_DST_NAT | IPS_SRC_NAT),
	/* Connection needs TCP sequence adjusted. */
	IPS_SEQ_ADJUST_BIT = 6,
	IPS_SRC_NAT_DONE_BIT = 7, /* 在POSTROUTING处,已被SNAT处理,并被加入到bysource链中,设置该标记 */
	IPS_DST_NAT_DONE_BIT = 8, /* 在PREROUTING处,已被DNAT处理,并被加入到bysource链中,设置该标记 */
	/* Both together */
	IPS_NAT_DONE_MASK = (IPS_DST_NAT_DONE | IPS_SRC_NAT_DONE),
	IPS_DYING_BIT = 9,        /* 表示该连接正在被释放,内核通过该标志保证正在被释放的ct不会被其它地方再次引用。有了这个标志,当某个连接要被删除时,即使它还在net->ct.hash中,也不会再次被引用。*/
	IPS_FIXED_TIMEOUT_BIT = 10,   /* 固定连接超时时间,这将不根据状态修改连接超时时间。通过函数nf_ct_refresh_acct()修改超时时间时检查该标志。 */
	IPS_TEMPLATE_BIT = 11,        /* 由CT target进行设置(这个target只能用在raw表中,用于为数据包构建指定ct,并打上该标志),用于表明这个ct是由CT target创建的 */
};

连接跟踪对该连接上的每个数据包表现为以下几种状态之一,由enum ip_conntrack_info表示,被设置在skb->nfctinfo中。

1
2
3
4
5
6
7
8
9
enum ip_conntrack_info {
	IP_CT_ESTABLISHED(0),  /* 表示这个数据包对应的连接在两个方向都有数据包通过,并且这是ORIGINAL初始方向数据包(无论是TCP、UDP、ICMP数据包,只要在该连接的两个方向上已有数据包通过,就会将该连接设置为IP_CT_ESTABLISHED状态。不会根据协议中的标志位进行判断,例如TCP的SYN等)。但它表示不了这是第几个数据包,也说明不了这个CT是否是子连接。*/
	IP_CT_RELATED(1),     /* 表示这个数据包对应的连接还没有REPLY方向数据包,当前数据包是ORIGINAL方向数据包。并且这个连接关联一个已有的连接,是该已有连接的子连接,(即status标志中已经设置了IPS_EXPECTED标志,该标志在init_conntrack()函数中设置)。但无法判断是第几个数据包(不一定是第一个)*/
	IP_CT_NEW(2),         /* 表示这个数据包对应的连接还没有REPLY方向数据包,当前数据包是ORIGINAL方向数据包,该连接不是子连接。但无法判断是第几个数据包(不一定是第一个)*/
	IP_CT_IS_REPLY(3),        /* 这个状态一般不单独使用,通常以下面两种方式使用 */
	IP_CT_ESTABLISHED + IP_CT_IS_REPLY(3),    /* 表示这个数据包对应的连接在两个方向都有数据包通过,并且这是REPLY应答方向数据包。但它表示不了这是第几个数据包,也说明不了这个CT是否是子连接。*/
	IP_CT_RELATED + IP_CT_IS_REPLY(4),        /* 这个状态仅在nf_conntrack_attach()函数中设置,用于本机返回REJECT,例如返回一个ICMP目的不可达报文, 或返回一个reset报文。它表示不了这是第几个数据包。*/
	IP_CT_NUMBER = IP_CT_IS_REPLY * 2 - 1(5)  /* 可表示状态的总数 */
};

以上就是连接跟踪里最重要的数据结构了,用于跟踪连接、记录状态、并对该连接的每个数据包设置一种状态。

除了上面的主要数据结构外,还有一些辅助数据结构,用于处理不同协议的私有信息、处理子连接、对conntrack进行扩展等。

三层协议(IPv4/IPv6)

利用nf_conntrack_proto.c文件中的

1
2
3
nf_conntrack_l3proto_register(struct nf_conntrack_l3proto *proto)
nf_conntrack_l3proto_unregister(struct nf_conntrack_l3proto *proto)

在nf_ct_l3protos[]数组中注册自己的三层协议处理函数。

四层协议(TCP/UDP)

利用nf_conntrack_proto.c文件中的

1
2
3
nf_conntrack_l4proto_register(struct nf_conntrack_l4proto *l4proto)
nf_conntrack_l4proto_unregister(struct nf_conntrack_l4proto *l4proto)

在nf_ct_protos[]数组中注册自己的四层协议处理函数。

处理一个连接的子连接协议

利用nf_conntrack_helper.c文件中的

1
nf_conntrack_helper_register(struct nf_conntrack_helper *me)

来注册nf_conntrack_helper结构,

和nf_conntrack_expect.c文件中的

1
nf_ct_expect_related_report(struct nf_conntrack_expect *expect, u32 pid, int report)

来注册nf_conntrack_expect结构。

扩展连接跟踪结构(nf_conn)

利用nf_conntrack_extend.c文件中的

1
2
3
nf_ct_extend_register(struct nf_ct_ext_type *type)
nf_ct_extend_unregister(struct nf_ct_ext_type *type)

进行扩展,并修改连接跟踪相应代码来利用这部分扩展功能。

了解了上面的数据结构,我们下面来看一下nf_conntrack的执行流程以及如何利用这些数据结构的。首先来看一下nf_conntrack模块加载时的初始化流程。

nf_conntrack的初始化

就是初始化上面提到的那些数据结构,它在内核启动时调用nf_conntrack_standalone_init()函数进行初始化的。初始化完成后,构建出如下图所示的结构图,只是不包含下图中与连接有关的信息(nf_conn和nf_conntrack_expect结构)。

上图中有三个HASH桶,ct_hash、expect_hash、helper_hash这三个HASH桶大小在初始化时就已确定,后面不能再更改。其中ct_hash、expect_hash可在加载nf_conntrack.ko模块时通过参数hashsize和expect_hashsize进行设定,而helper_hash不能通过参数修改,它的默认值是page/sizeof(helper_hash)。

下面再来看一个当创建子连接时,各个数据结构之间的关系。

nf_conn和nf_conntrack_expect都有最大个数限制。nf_conn通过全局变量nf_conntrack_max限制,可通过 /proc/sys/net/netfilter/nf_conntrack_max 文件在运行时修改。nf_conntrack_expect通过全局变量nf_ct_expect_max限制,可通过 /proc/sys/net/netfilter/nf_conntrack_expect_max 文件在运行时修改。nf_conntrack_helper没有最大数限制,因为这个是通过注册不同协议的模块添加的,大小取决于动态协议跟踪模块的多少,一般不会很大。

上面两幅数据结构图中,大部分都已介绍过,下面介绍一下netns_ct数据结构,该结构主要用于linux的网络命名空间,表示nf_conntrack在不同的命名空间中都有一套独立的数据信息(这是另一个话题,这里就不再深入讨论了)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct netns_ct {
	atomic_t          count;              /* 当前连接表中连接的个数 */
	unsigned int      expect_count;           /* nf_conntrack_helper创建的期待子连接nf_conntrack_expect项的个数 */
	unsigned int      htable_size;            /* 存储连接(nf_conn)的HASH桶的大小 */
	struct kmem_cache *nf_conntrack_cachep;       /* 指向用于分配nf_conn结构而建立的高速缓存(slab)对象 */
	struct hlist_nulls_head   *hash;              /* 指向存储连接(nf_conn)的HASH桶 */
	struct hlist_head     *expect_hash;           /* 指向存储期待子连接nf_conntrack_expect项的HASH桶 */
	struct hlist_nulls_head   unconfirmed;            /* 对于一个链接的第一个包,在init_conntrack()函数中会将该包original方向的tuple结构挂入该链,这是因为在此时还不确定该链接会不会被后续的规则过滤掉,如果被过滤掉就没有必要挂入正式的链接跟踪表。在ipv4_confirm()函数中,会将unconfirmed链中的tuple拆掉,然后再将original方向和reply方向的tuple挂入到正式的链接跟踪表中,即init_net.ct.hash中,这是因为到达ipv4_confirm()函数时,应经在钩子NF_IP_POST_ROUTING处了,已经通过了前面的filter表。 通过cat  /proc/net/nf_conntrack显示连接,是不会显示该链中的连接的。但总的连接个数(net->ct.count)包含该链中的连接。当注销l3proto、l4proto、helper、nat等资源或在应用层删除所有连接(conntrack -F)时,除了释放confirmed连接(在net->ct.hash中的连接)的资源,还要释放unconfirmed连接(即在该链中的连接)的资源。*/
	struct hlist_nulls_head   dying;              /* 释放连接时,通告DESTROY事件失败的ct被放入该链中,并设置定时器,等待下次通告。 通过cat  /proc/net/nf_conntrack显示连接,是不会显示该链中的连接的。但总的连接个数(net->ct.count)包含该链中的连接。当注销连接跟踪模块时,同时要清除正再等待被释放的连接(即该链中的连接)*/
	struct ip_conntrack_stat  __percpu *stat;         /* 连接跟踪过程中的一些状态统计,每个CPU一项,目的是为了减少锁 */
	int           sysctl_events;          /* 是否开启连接事件通告功能 */
	unsigned int      sysctl_events_retry_timeout;    /* 通告失败后,重试通告的间隔时间,单位是秒 */
	int           sysctl_acct;            /* 是否开启每个连接数据包统计功能 */
	int           sysctl_checksum;
	unsigned int      sysctl_log_invalid;      /* Log invalid packets */
#ifdef CONFIG_SYSCTL
	struct ctl_table_header   *sysctl_header;
	struct ctl_table_header   *acct_sysctl_header;
	struct ctl_table_header   *event_sysctl_header;
#endif
	int           hash_vmalloc;           /* 存储连接(nf_conn)的HASH桶是否是使用vmalloc()进行分配的 */
	int           expect_vmalloc;         /* 存储期待子连接nf_conntrack_expect项的HASH桶是否是使用vmalloc()进行分配的 */
	char          *slabname;          /* 用于分配nf_conn结构而建立的高速缓存(slab)对象的名字 */
};

从nf_conntrack的框架来看,它可用于跟踪任何三层和四协议的连接,但目前在三层协议只实现了IPv4和IPv6的连接跟踪,下面我们以IPv4为例,介绍一下该协议是如何利用nf_conntrack框架和netfilter实现连接跟踪的。有关netfilter框架,可参考我的另一个帖子

linux-2.6.35.6内核netfilter框架

首先介绍一下IPv4协议连接跟踪模块的初始化。

Ipv4连接跟踪模块注册了自己的3层协议,和IPv4相关的三个4层协议TCP、UDP、ICMP。注册后的结构图如下图所示:

在netfilter框架中利用

1
nf_register_hook(struct nf_hook_ops *reg)、nf_unregister_hook(struct nf_hook_ops *reg)

函数注册自己的钩子项,调用nf_conntrack_in()函数来建立相应连接。

这样数据包就会经过ipv4注册的钩子项,并调用nf_conntrack_in()函数建立连接表项,连接表项中的tuple由ipv4注册的3/4层协议处理函数构建。

ipv4_conntrack_in() 挂载在NF_IP_PRE_ROUTING点上。该函数主要功能是创建链接,即创建struct nf_conn结构,同时填充struct nf_conn中的一些必要的信息,例如链接状态、引用计数、helper结构等。

ipv4_confirm() 挂载在NF_IP_POST_ROUTING和NF_IP_LOCAL_IN点上。该函数主要功能是确认一个链接。对于一个新链接,在ipv4_conntrack_in()函数中只是创建了struct nf_conn结构,但并没有将该结构挂载到链接跟踪的Hash表中,因为此时还不能确定该链接是否会被NF_IP_FORWARD点上的钩子函数过滤掉,所以将挂载到Hash表的工作放到了ipv4_confirm()函数中。同时,子链接的helper功能也是在该函数中实现的。

ipv4_conntrack_local() 挂载在NF_IP_LOCAL_OUT点上。该函数功能与ipv4_conntrack_in()函数基本相同,但其用来处理本机主动向外发起的链接。

nf_conntrack_ipv4_compat_init() –> register_pernet_subsys() –> ip_conntrack_net_init() 创建/proc文件ip_conntrack和ip_conntrack_expect

如上面所示,IPv4连接跟踪模块已初始化完成,下面我们来看一下它创建连接的流程图。上图中连接的建立主要由三个函数来完成,即ipv4_conntrack_in(),ipv4_confirm()与ipv4_conntrack_local()。其中ipv4_conntrack_in()与ipv4_conntrack_local()都是通过调用函数nf_conntrack_in()来实现的,所以下面我们主要关注nf_conntrack_in()与ipv4_confirm()这两个函数。nf_conntrack_in()函数主要完成创建链接、添加链接的扩展结构(例如helper, acct结构)、设置链接状态等。ipv4_confirm()函数主要负责确认链接(即将链接挂入到正式的链接表中)、执行helper函数、启动链接超时定时器等。另外还有一个定时器函数death_by_timeout(), 该函数负责链接到期时删除该链接。

nf_conntrack_in()函数流程图

ipv4_confirm()函数流程图

death_by_timeout()函数流程图

上图中有一点需要说明,由于skb会引用nf_conn,同时会增加它的引用计数,所以当skb被释放时,也要释放nf_conn的引用计数,并且在nf_conn引用计数为0时,要释放全部资源。

当数据包经过nf_conntrack_in()和ipv4_confirm()函数处理流程后,就会建立起3楼第二幅结构图所示的连接nf_conn。同时这两个函数已经包含了子连接的处理流程,即流程图中help和exp的处理。子连接建立后的结构图如3楼第三幅结构图,主链接与子连接通过helper和expect关联起来。

连接跟踪到此就介绍完了,下面介绍IPv4基于nf_conntrack框架适合实现NAT转换的。先介绍IPv4-NAT初始化的资源,然后处理流程。

IPv4-NAT连接跟踪相关部分通过函数nf_nat_init()初始化

调用nf_ct_extend_register() 注册一个连接跟踪的扩展功能。

调用register_pernet_subsys() –> nf_nat_net_init() 创建net->ipv4.nat_bysource的HASH表,大小等于net->ct.htable_size。

初始化nf_nat_protos[]数组,为TCP、UDP、ICMP协议指定专用处理结构,其它协议都指向默认处理结构。

为nf_conntrack_untracked连接设置IPS_NAT_DONE_MASK标志。

将NAT模块的全局变量l3proto指向IPV4协议的nf_conntrack_l3proto结构。

设置全局指针nf_nat_seq_adjust_hook指向nf_nat_seq_adjust()函数。

设置全局指针nfnetlink_parse_nat_setup_hook指向nfnetlink_parse_nat_setup()函数。

设置全局指针nf_ct_nat_offset指向nf_nat_get_offset()函数。

IPv4-NAT功能的iptables部分通过函数nf_nat_standalone_init()初始化

调用nf_nat_rule_init() –> nf_nat_rule_net_init()在iptables中注册一个NAT表(通过ipt_register_table()函数,参考另一个帖子iptables)

调用 nf_nat_rule_init() 注册SNAT target和DNAT target(通过xt_register_target()函数)

调用nf_register_hooks() 挂载NAT的HOOK函数,橙色部分为NAT挂载的HOOK函数(参考另一个帖子netfilter)

根据上面介绍,可以看到IPv4-NAT的主要是通过nf_nat_fn()钩子函数处理的,下面我就来看看nf_nat_fn()函数的处理流程。

针对上图中的nf_nat_setup_info()函数进一步描述

下面对NAT转换算法中重要部分做一些文字说明

每个ct在第一个包就会做好snat与dnat, nat的信息全放在reply tuple中,orig tuple不会被改变。一旦第一个包建立好nat信息后,后续再也不会修改tuple内容了。

orig tuple中的地址信息与reply tuple中的地址信息就是原始数据包的信息。例如对A->B数据包同时做snat与dnat,PREROUTING处B被dnat到D,POSTROUTING处A被snat到C。则ct的内容是:  A->B | D->C,  A->B说明了orig方向上数据包刚到达墙时的地址内容,D->C说明reply方向上数据包刚到达墙时的地址内容。

在代码中有很多!dir操作,原理是: 当为了反向的数据包做事情的时候就取反向tuple的数据,这样才能保证NAT后的tuple信息被正确使用。

bysource链中链接了所有CT(做过NAT和未做过NAT),通过ct->nat->bysource,HASH值的计算使用的是CT的orig tuple。其作用是,当为一个新连接做SNAT,需要得到地址映射时,首先对该链进行查找,查找此源IP、协议和端口号是否已经做过了映射。如果做过的话,就需要在SNAT转换时,映射为相同的源IP和端口号。为什么要这么做呢?因为对于UDP来说,有些协议可能会用相同端口和同一主机不同的端口(或不同的主机)进行通信。此时,由于目的地不同,原来已有的映射不可使用,需要一个新的连接。但为了保证通信的的正确性,此时,就要映射为相同的源IP和端口号。其实就是为NAT的打洞服务的。所以bysource就是以源IP、协议和端口号为hash值的一个表,这样在做snat时保证相同的ip+port影射到相同的ip+port。

IP_NAT_RANGE_PROTO_RANDOM指的是做nat时,当计算端口时,如果没有此random标志,则会先使用原始得tuple中的端口试一下看是否可用,如果可用就使用该原始端口作为nat后的端口, 即尽量保证转换后的端口与转换前的端口保持一致。如果不可用,再根据nat的端口算法计算出一个端口。 如果有此标记,则直接根据端口算法计算出端口。

第一个包之后,ct的两个方向的tuple内容就固定了,所有的nat操作都必须在第一个包就完成。所以会有daddr = &ct->tuplehash[!dir].tuple.dst.u3;这样的操作。

IPS_SRC_NAT与IPS_DST_NAT,如果被设置,表示经过了NAT,并且ct中的tuple被做过SNAT或DNAT。

数据包永远都是在PREROUTING链做目的地址和目的端口转换,在POSTROUTING链做原地址和原端口转换。是否要做NAT转换则要根数据包方向(dir)和NAT标志(IPS_SRC_NAT或IPS_DST_NAT)来判断。

在PREROUTING链上—>数据包是original方向、并且连接上设置IPS_DST_NAT标志,或数据包是reply方向、并且连接上设置IPS_SRC_NAT标志,则做DNAT转换。

在POSTROUTING链上—>数据包是original方向、并且连接上设置IPS_SRC_NAT标志,或数据包是reply方向、并且连接上设置IPS_DST_NAT标志,则做SNAT转换。

IPS_DST_NAT_DONE_BIT与IPS_SRC_NAT_DONE_BIT,表示该ct进入过NAT模块,已经进行了源或者目的NAT判断,但并不表示ct中的tuple被修改过。

源目的nat都是在第一个包就判断完成的,假设先添加了snat策略,第一个包通过,这时又添加了dnat策略, 第二个包到来时是不会匹配dnat策略的 。

对于一个ct,nf_nat_setup_info函数最多只能进入2次,第一次DNAT,第二次SNAT。在nf_nat_follow_master函数中,第一次SNAT,第二次DNAT。

下面介绍有子连接的NAT实现。有两个关键点:1.主链接能正确的构建出NAT后的expect来识别子连接。2.能够修改主链接数据通道的信息为NAT后的信息。这两点都在动态协议的help中完成,下面我们来看一下它的流程图:

下面针对有无子连接的NAT做一下对比

无子连接的NAT

一个ct用于跟踪一个连接的双方向数据,ct->orig_tuple用于跟踪初始方向数据,ct->reply_tuple用于跟踪应答方向数据。当根据初始方向数据构建ct->orig_tuple时,同时要构建出ct->reply_tuple,用于识别同一连接上应答方向数据。

如果初始方向的数据在通过防火墙后被做了NAT转换,为识别出NAT数据的应答数据包,则对ct->reply_tuple也要做NAT转换。同时ct上做好相应NAT标记。

因此,上面的信息在初始方向第一个数据包通过后,就要求全部建立好,并且不再改变。

一个连接上不同方向的数据,都有相对应的tuple(orig_tuple和reply_tuple),所以该连接后续数据都将被识别出来。如果ct上有NAT标记,则根据要去往方向(即另一个方向)的tuple对数据做NAT转换。所以会有ct->tuplehash[!dir].tuple这样的操作。

有子连接的NAT

子连接是由主连接构建的expect项识别出来的。

help用于构建expect项,它期待哪个方向的连接,则用那个方向的tuple和数据包中数据通道信息构建expect项。例如期待和当前数据包相反方向的连接,则用相反方向的tuple中的信息(ct->tuplehash[!dir].tuple)。调用help时,NAT转换都已完成(tuple中都包含有正确的识别各自方向的信息),所以这时所使用的信息都是正确和所期望的信息。

如果子连接还可能有子连接,则构建expect项时,初始化一个helper结构,并赋值给expect->helper指针。

如果该连接已被做了NAT转换,则对数据包中数据通道信息也要做NAT转换

Linux编程之UDP SOCKET

https://www.cnblogs.com/skyfsm/p/6287787.html

一、基本的udp socket编程

1. UDP编程框架

要使用UDP协议进行程序开发,我们必须首先得理解什么是什么是UDP?这里简单概括一下。

UDP(user datagram protocol)的中文叫用户数据报协议,属于传输层。UDP是面向非连接的协议,它不与对方建立连接,而是直接把我要发的数据报发给对方。所以UDP适用于一次传输数据量很少、对可靠性要求不高的或对实时性要求高的应用场景。正因为UDP无需建立类如三次握手的连接,而使得通信效率很高。

UDP的应用非常广泛,比如一些知名的应用层协议(SNMP、DNS)都是基于UDP的,想一想,如果SNMP使用的是TCP的话,每次查询请求都得进行三次握手,这个花费的时间估计是使用者不能忍受的,因为这会产生明显的卡顿。所以UDP就是SNMP的一个很好的选择了,要是查询过程发生丢包错包也没关系的,我们再发起一个查询就好了,因为丢包的情况不多,这样总比每次查询都卡顿一下更容易让人接受吧。

UDP通信的流程比较简单,因此要搭建这么一个常用的UDP通信框架也是比较简单的。以下是UDP的框架图。

由以上框图可以看出,客户端要发起一次请求,仅仅需要两个步骤(socket和sendto),而服务器端也仅仅需要三个步骤即可接收到来自客户端的消息(socket、bind、recvfrom)。

2. UDP程序设计常用函数

1
2
3
#include <sys/types.h>          
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

参数domain:用于设置网络通信的域,socket根据这个参数选择信息协议的族

1
2
3
4
5
6
7
8
9
10
11
12
Name                                     Purpose                         
AF_UNIX, AF_LOCAL              Local communication              
AF_INET                        IPv4 Internet protocols          //用于IPV4
AF_INET6                       IPv6 Internet protocols          //用于IPV6
AF_IPX                         IPX - Novell protocols
AF_NETLINK                     Kernel user interface device     
AF_X25                         ITU-T X.25 / ISO-8208 protocol   
AF_AX25                        Amateur radio AX.25 protocol
AF_ATMPVC                      Access to raw ATM PVCs
AF_APPLETALK                   AppleTalk                        
AF_PACKET                      Low level packet interface       
AF_ALG                         Interface to kernel crypto API

对于该参数我们仅需熟记AF_INET和AF_INET6即可  

小插曲:PF_XXX和AF_XXX

我们在看Linux网络编程相关代码时会发现PF_XXX和AF_XXX会混着用,他们俩有什么区别呢?以下内容摘自《UNP》。

AF_ 前缀表示地址族(Address Family),而PF_前缀表示协议族(Protocol Family)。历史上曾有这样的想法:单个协议族可以支持多个地址族,PF_的值可以用来创建套接字,而AF_值用于套接字的地址结构。但实际上,支持多个地址族的协议族从来就没实现过,而头文件 <sys/socket.h> 中为一给定的协议定义的PF_值总是与此协议的AF_值相同。 所以我在实际编程时还是偏向于使用AF_XXX。   参数type(只列出最重要的三个):

1
2
3
SOCK_STREAM         Provides sequenced, reliable, two-way, connection-based byte streams.   //用于TCP
SOCK_DGRAM          Supports datagrams (connectionless, unreliable messages ). //用于UDP
SOCK_RAW            Provides raw network protocol access.  //RAW类型,用于提供原始网络访问

参数protocol:置0即可

返回值:

成功:非负的文件描述符

失败:-1  

1
2
3
4
#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
		const struct sockaddr *dest_addr, socklen_t addrlen);

第一个参数sockfd:正在监听端口的套接口文件描述符,通过socket获得
第二个参数buf:发送缓冲区,往往是使用者定义的数组,该数组装有要发送的数据
第三个参数len:发送缓冲区的大小,单位是字节
第四个参数flags:填0即可
第五个参数dest_addr:指向接收数据的主机地址信息的结构体,也就是该参数指定数据要发送到哪个主机哪个进程
第六个参数addrlen:表示第五个参数所指向内容的长度

返回值:

成功:返回发送成功的数据长度

失败: -1

1
2
3
4
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
		struct sockaddr *src_addr, socklen_t *addrlen);

第一个参数sockfd:正在监听端口的套接口文件描述符,通过socket获得
第二个参数buf:接收缓冲区,往往是使用者定义的数组,该数组装有接收到的数据
第三个参数len:接收缓冲区的大小,单位是字节
第四个参数flags:填0即可
第五个参数src_addr:指向发送数据的主机地址信息的结构体,也就是我们可以从该参数获取到数据是谁发出的
第六个参数addrlen:表示第五个参数所指向内容的长度

返回值:

成功:返回接收成功的数据长度

失败: -1  

1
2
3
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen);

第一个参数sockfd:正在监听端口的套接口文件描述符,通过socket获得
第二个参数my_addr:需要绑定的IP和端口
第三个参数addrlen:my_addr的结构体的大小

返回值:

成功:0

失败:-1  

1
2
#include <unistd.h>
int close(int fd);

close函数比较简单,只要填入socket产生的fd即可。

3. 搭建UDP通信框架

server:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>

#define SERVER_PORT 8888
#define BUFF_LEN 1024

void handle_udp_msg(int fd)
{
	char buf[BUFF_LEN];  //接收缓冲区,1024字节
	socklen_t len;
	int count;
	struct sockaddr_in clent_addr;  //clent_addr用于记录发送方的地址信息
	while (1) {
		memset(buf, 0, BUFF_LEN);
		len = sizeof(clent_addr);
		count = recvfrom(fd, buf, BUFF_LEN, 0, (struct sockaddr*)&clent_addr, &len);  //recvfrom是拥塞函数,没有数据就一直拥塞
		if (count == -1) {
			printf("recieve data fail!\n");
			return;
		}
		printf("client:%s\n",buf);  //打印client发过来的信息
		memset(buf, 0, BUFF_LEN);
		sprintf(buf, "I have recieved %d bytes data!\n", count);  //回复client
		printf("server:%s\n",buf);  //打印自己发送的信息给
		sendto(fd, buf, BUFF_LEN, 0, (struct sockaddr*)&clent_addr, len);  //发送信息给client,注意使用了clent_addr结构体指针

	}
}


/*
	server:
		socket-->bind-->recvfrom-->sendto-->close
*/

int main(int argc, char* argv[])
{
	int server_fd, ret;
	struct sockaddr_in ser_addr; 

	server_fd = socket(AF_INET, SOCK_DGRAM, 0); //AF_INET:IPV4;SOCK_DGRAM:UDP
	if (server_fd < 0) {
		printf("create socket fail!\n");
		return -1;
	}

	memset(&ser_addr, 0, sizeof(ser_addr));
	ser_addr.sin_family = AF_INET;
	ser_addr.sin_addr.s_addr = htonl(INADDR_ANY); //IP地址,需要进行网络序转换,INADDR_ANY:本地地址
	ser_addr.sin_port = htons(SERVER_PORT);  //端口号,需要网络序转换

	ret = bind(server_fd, (struct sockaddr*)&ser_addr, sizeof(ser_addr));
	if (ret < 0) {
		printf("socket bind fail!\n");
		return -1;
	}

	handle_udp_msg(server_fd);   //处理接收到的数据

	close(server_fd);
	return 0;
}

client:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>

#define SERVER_PORT 8888
#define BUFF_LEN 512
#define SERVER_IP "172.0.5.182"


void udp_msg_sender(int fd, struct sockaddr* dst)
{

	socklen_t len;
	struct sockaddr_in src;
	while (1) {
		char buf[BUFF_LEN] = "TEST UDP MSG!\n";
		len = sizeof(*dst);
		printf("client:%s\n",buf);  //打印自己发送的信息
		sendto(fd, buf, BUFF_LEN, 0, dst, len);
		memset(buf, 0, BUFF_LEN);
		recvfrom(fd, buf, BUFF_LEN, 0, (struct sockaddr*)&src, &len);  //接收来自server的信息
		printf("server:%s\n",buf);
		sleep(1);  //一秒发送一次消息
	}
}

/*
	client:
		socket-->sendto-->revcfrom-->close
*/

int main(int argc, char* argv[])
{
	int client_fd;
	struct sockaddr_in ser_addr;

	client_fd = socket(AF_INET, SOCK_DGRAM, 0);
	if (client_fd < 0) {
		printf("create socket fail!\n");
		return -1;
	}

	memset(&ser_addr, 0, sizeof(ser_addr));
	ser_addr.sin_family = AF_INET;
	//ser_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
	ser_addr.sin_addr.s_addr = htonl(INADDR_ANY);  //注意网络序转换
	ser_addr.sin_port = htons(SERVER_PORT);  //注意网络序转换

	udp_msg_sender(client_fd, (struct sockaddr*)&ser_addr);

	close(client_fd);

	return 0;
}

以上的框架用于一台主机不同端口的UDP通信,现象如下:

我们先建立server端,等待服务;然后我们建立client端请求服务。

server端:

  client端:   自己主机跟自己通信不是很爽,我们想跟其他主机通信怎么搞?很简单,上面client的代码的第49行的注释打开,并注释掉下面那行,在宏定义里填入自己想通信的serverip就可以了。现象如下:

server端:   client端:   这样我们就实现了主机172.0.5.183和172.0.5.182之间的网络通信。   UDP通用框架搭建完成,我们可以利用该框架跟指定主机进行通信了。   如果想学习UDP的基础知识,以上的知识就足够了;如果想继续深入学习一下UDP SOCKET一些高级知识(奇技淫巧),可以花点时间往下看。  

二、高级udp socket编程

1. udp的connect函数

什么?UDP也有conenct?connect不是用于TCP编程的吗?

是的,UDP网络编程中的确有connect函数,但它仅仅用于表示确定了另一方的地址,并没有其他含义。

有了以上认识后,我们可以知道UDP套接字有以下区分:

1)未连接的UDP套接字

2)已连接的UDP套接字

对于未连接的套接字,也就是我们常用的的UDP套接字,我们使用的是sendto/recvfrom进行信息的收发,目标主机的IP和端口是在调用sendto/recvfrom时确定的;

在一个未连接的UDP套接字上给两个数据报调用sendto函数内核将执行以下六个步骤:

1)连接套接字
2)输出第一个数据报
3)断开套接字连接
4)连接套接字
5)输出第二个数据报
6)断开套接字连接

对于已连接的UDP套接字,必须先经过connect来向目标服务器进行指定,然后调用read/write进行信息的收发,目标主机的IP和端口是在connect时确定的,也就是说,一旦conenct成功,我们就只能对该主机进行收发信息了。

已连接的UDP套接字给两个数据报调用write函数内核将执行以下三个步骤:

1)连接套接字
2)输出第一个数据报
3)输出第二个数据报

由此可以知道,当应用进程知道给同一个目的地址的端口号发送多个数据报时,显示套接字效率更高。

下面给出带connect函数的UDP通信框架

具体框架代码不再给出了,因为跟上面不带connect的代码大同小异,仅仅多出一个connect函数处理而已,下面给出处理conenct()的基本步骤。

1
2
3
4
5
6
7
8
9
10
void udp_handler(int s, struct sockaddr* to)
{
	char buf[1024] = "TEST UDP !";
	int n = 0;
	connect(s, to, sizeof(*to);

	n = write(s, buf, 1024);

	read(s, buf, n);
}

 

2. udp报文丢失问题

因为UDP自身的特点,决定了UDP会相对于TCP存在一些难以解决的问题。第一个就是UDP报文缺失问题。

在UDP服务器客户端的例子中,如果客户端发送的数据丢失,服务器会一直等待,直到客户端的合法数据过来。如果服务器的响应在中间被路由丢弃,则客户端会一直阻塞,直到服务器数据过来。

防止这样的永久阻塞的一般方法是给客户的recvfrom调用设置一个超时,大概有这么两种方法:

1)使用信号SIGALRM为recvfrom设置超时。首先我们为SIGALARM建立一个信号处理函数,并在每次调用前通过alarm设置一个5秒的超时。如果recvfrom被我们的信号处理函数中断了,那就超时重发信息;若正常读到数据了,就关闭报警时钟并继续进行下去。

2)使用select为recvfrom设置超时

设置select函数的第五个参数即可。

 

3. udp报文乱序问题

所谓乱序就是发送数据的顺序和接收数据的顺序不一致,例如发送数据的顺序为A、B、C,但是接收到的数据顺序却为:A、C、B。产生这个问题的原因在于,每个数据报走的路由并不一样,有的路由顺畅,有的却拥塞,这导致每个数据报到达目的地的顺序就不一样了。UDP协议并不保证数据报的按序接收。

解决这个问题的方法就是发送端在发送数据时加入数据报序号,这样接收端接收到报文后可以先检查数据报的序号,并将它们按序排队,形成有序的数据报。

4. udp流量控制问题

总所周知,TCP有滑动窗口进行流量控制和拥塞控制,反观UDP因为其特点无法做到。UDP接收数据时直接将数据放进缓冲区内,如果用户没有及时将缓冲区的内容复制出来放好的话,后面的到来的数据会接着往缓冲区放,当缓冲区满时,后来的到的数据就会覆盖先来的数据而造成数据丢失(因为内核使用的UDP缓冲区是环形缓冲区)。因此,一旦发送方在某个时间点爆发性发送消息,接收方将因为来不及接收而发生信息丢失。

解决方法一般采用增大UDP缓冲区,使得接收方的接收能力大于发送方的发送能力。

1
2
int n = 220 * 1024; //220kB
setsocketopt(sockfd, SOL_SOCKET, SO_RCVBUF, &n, sizeof(n));

这样我们就把接收方的接收队列扩大了,从而尽量避免丢失数据的发生。