OpenVPN的Linux内核版,鬼魅的残缺 part III rework with Netfilter

哥们儿拿到了juniper的offer,由衷祝福,酒足饭饱后的我,在Netfilter的路上却根本停不下来。已经是深夜,回忆这些年在Netfilter上的探索,结合目前的一些状况,突然觉得,既然我已经想把OpenVPN弄到内核了,那为何上一个鬼魅的实现没有使用Netfilter呢?当时我就觉得采用UDP的encap_rcv HOOK这种方式太鬼魅,又有些残缺,缺了什么却不知道,谁说没钱就不能任性,我当时可能就觉得:我就是不用Netfilter,我就是要用另一个方法!就好像上高中的时候,数学课上,我总是能提出一个最简便但却多少有些YD的解法,当老师说这种方法没法得分的时候,我就大喊:我还有一种更复杂的方法...然后那些学习不好但是成绩却很高的人们就有些不高兴了,其实哪知道我真的是在有针对地嘲讽他们啊...现在好了,当那些人都穿着西装不再跟我玩这种幼稚的游戏的时候,我依然在跟自己玩,只是我是自己嘲讽自己而已。
       整个城市,向黑暗中退去,你我都放弃忍耐!

我将OpenVPN顺利移植进了内核,然而却对tun.c做了比较大的手术,在那个实现中,我借用了一个udp_encap_rcv钩子点,然后在tun驱动的xmit函数中增加了一个新的钩子点,两个钩子点对接完成了OpenVPN数据通道处理流程的短路。然而仔细想想,总觉得哪里不妥,所谓的优化无极限并非仅仅针对性能,还针对美感,如果自己都觉得它丑陋无比,那么它就一定可以换种方式重新实现。
       在《OpenVPN的Linux内核版,鬼魅的残缺 part I:The PROTOCOL》中,那种短路方式确实绕开了OpenVPN这个大电阻,但是在udp的encap_rcv增加了一个通过real source address以及real source port为主键查找multi_instance的过程,我们一向都认为这个过程是绕不开,因此它作为一个小电阻存在在那里。我们还知道,Linux的内核协议栈将所有的同一种协议的socket链在了一张hash表中,每一个到达本地的数据包都要根据4元组在这个hash表中检索到唯一的一个socket并和skb本身关联起来,因此这个查找过程是绝对绕不开的,这不是说我认为它绕不开,而是socket层的例行过程。那么,数据包是怎么知道自己要到达本地的呢?很显然,还有一种绝对绕不开的过程,那就是路由查找,当然这个绝对也并不绝对,因为存在很多的路由板卡,专门在链路层cache路由结果,当然这是后话,也并不是每一台设备都拥有这种板卡。到目前为止,我总结一下OpenVPN内核版本的数据通道处理流程的三次查找操作:
1.路由查找:决定了数据包是到达本地的,这是这个数据包是OpenVPN数据通道数据包的前提;
2.UDP的socket查找:这是这个数据包是OpenVPN数据通道数据包的另一个前提;
3.multi_instance查找:这是一个短路操作的查找,原版的OpenVPN的这个查找在multi.c文件的一个multi_process_incoming_link函数中。
好了,在我描述了以上的3个所谓的必不可少的查找之后,接下来就是我的风格展示了,做掉它们!我并不认为会有奇迹,我也同样不相信程序可以智能到自己寻找加密参数,那么查找肯定是必不可少的了,问题是,如何将查找次数降低到最少!我的方式是,我的方式还是Netfilter,一个极端优秀的框架!优秀到多么极端的程度仅仅和你的想象力有关!我没有发明什么新的东西,而是使用了conntrack!然后将所有的信息都保存在conntrack的extend中,这种方式是我一直都在思考的,并且还真的用到了工作中,这是绝对值得欣慰的。
       关于ip conntrack,我已经不想再多说,之前的文章已经说了不少,我对它的掌控虽谈不上出神入化但也稍微有点说到它就排他了...conntrack可谓说是“一次慢速的多层查询以建立快速通道”的绝佳实例,因此唯一耗时的操作过程仅仅在第一次建立conntrack结构体的时候,在这个慢速的过程中,所有的查询结果都可以保存在conntrack中,虽然Linux的ip conntrack中没有所谓的private_data之类的void指针,但是却可以通过extend机制去扩展,此后所有的信息都可以在conntrack结构体的对应extend中直接查到,因此,所有的查询操作都可以归结为单一的conntrack查询操作了。至于conntrack查询的效率,我可以说它非常不高,也非常影响性能,但是正因为如此,我才会说它还有很大优化空间,至于如何优化,我曾经写过一个方法,那就是多张conntrack hash表,典型地可以是按照协议来区分不同的conntrack hash表,从一个skb中取到传输层协议是再简单不过的事了,如果每一种协议建立一张表,岂不是很好的思路?!
       好了,现在假设你已经明白我的大致思路,那么具体的实现该如何呢?那当然最简单不过了,解密流程如下:
0.conntrack建立:我们要明白,OpenVPN的数据通道和控制通道只是应用层的区分,因此它们共享一个conntrack结构体,在数据通道启用之前,控制通道已经建立(SSL握手完成),控制通道的建立意味着和它关联的conntrack结构已经被confirm了。
1.数据包拦截:PREROUTING上在CONNTRACK优先级之后建立一个OpenVPN HOOK,拦截OpenVPN的数据通道的skb。此时取出skb的conntrack结构体,进一步取出自定义的ovpn extend;
2.数据包解密:所有的信息都在ovpn extend中,包括加密密钥和解密密钥,此时我们需要的是解密密钥。将数据包进行常规检查(IP层校验已经在PREROUTING前做过,因此此处只要校验UDP相关的即可)之后,脱掉UDP/IP西服,然后对其进行解密解封装,OpenVPN校验,包括replay攻击校验等,最终露出一个内层被封装的IP数据报文(目前我没有实现TAP mode);
3.短路到tun网卡:这个步骤比较复杂,分为下面的子步骤:
3.0.提出一个问题:第2步最终的skb此时其实已经可以被tun网卡接收了,即模拟一个netif_rx_ni调用。但是且慢!人无远虑必有近忧!我们要考虑属于此时skb的conntrack的返回包回来的时候如何被加密,即按照我最初的思路,需要一个extend和此时的内层skb的conntrack关联起来,问题是如何拿到该conntrack,如果在tun的netif_rx之后自然到达PREROUTING之后拿到,我们将丢失内层skb和外层skb之间的关联。必须另想它法。注意,此时我们依然在外层skb的PREROUTING的OpenVPN HOOK中;

3.1.获取内层skb的conntrack:在不逃出这个外层skb的OpenVPN HOOK的位置,手动让内层skb到PREROUTING中溜达一圈,一直溜达到它的conntrack被指定为止。其实懂行的都知道,过了ipv4_conntrack_in这个函数,它的conntrack就被指定了。注意,此时我们依然在外层skb的PREROUTING的OpenVPN HOOK中;
3.2.设置内层skb conntrack的extend:如果发现内层skb的conntrack OpenVPN extend已经被设置,就直接通过,否则就设置它,注意,此时我们依然在外层skb的PREROUTING的OpenVPN HOOK中,所以很容易关联内层skb conntrack OpenVPN extend以及外层skb conntrack OpenVPN extend;
3.3.模拟tun网卡接收:这是真正的短路操作。在tun数据包接收了内层skb之后,总是还是会到PREROUTING的ipv4_conntrack_in中的,但是无所谓,不会被执行的,会直接退出,这是因为它的conntrack已经有了,previous seen??
4.正向短路至此完成:怎么庆祝一下呢?最好的方法就是顺利让内层skb经过路由,local deliver或者forward,放走它,并等待和它同属一个conntrack的另一个skb的返回!
紧接着,我们看一下加密流程:
0.反向数据包的返回:我等待的数据包终于返回了,好的,这正是我想要的!
1.数据包拦截:PREROUTING上在CONNTRACK优先级之后已经建立了一个OpenVPN HOOK,取出skb的conntrack之后,发现其拥有OpenVPN的extend,其中的信息正是正向解密流程的第3.2步骤建立的,此时我敢保证,这个skb就是需要加密的,好的;但是且慢,如果没有找到conntrack的OpenVPN extend怎么办?这说明这个conntrack是从OpenVPN服务端方向主动发起的,那么这时就要进入慢速路径了,因为短路操作所需的信息不足。所谓的慢速路径就是依旧和往常一样经由IP路由将其发往tun虚拟网卡,然后是字符设备,最后被OpenVPN用户态进程读取,加密,然后写入socket...
2.数据包加密:用第1步中的OpenVPN extend信息中的加密信息对该skb进行封装,加密以及HMAC处理。具体过程和OpenVPN中的几乎一样;
3.数据包外层封装:从第1步中查到的OpenVPN extend获取连接信息,即外层skb的UDP 4元组信息,将其进行UDP封装,IP封装,然后直接调用ip_local_out将其发出,这是反向加密流程的短路操作;
4.反向短路至此完成:怎么庆祝一下呢?...
至此,内核中的所有的操作均已经介绍完毕,那么内核中的所谓的OpenVPN extend信息是什么时候注入进去的呢?以下是一些个注入点:
1.multi_instance建立的时候:此时OpenVPN已经接收到了RESET,但是SSL握手或者password验证等还没有完成,密钥协商还未完成,但是五元组已经确定,这个point可以注入的信息有:该conntrack确实是路由到本地OpenVPN进程的;tun网卡的信息;
2.密钥协商完成:此时的几个密钥对均已经协商完成,包括加密密钥,解密密钥,HMAC相关的密钥...这个point可以注入的信息有:密钥对信息;密钥操作方向信息等;
所有的注入操作我并没有用ioctl,因为这样需要建立一个字符设备,或者修改tun驱动或者建立一个socket,而我不想污染tun,也不想污染devfs,其实ioctl本身就是一种带有京味的污染源,于是我采用了netlink,如果是在脚本中操作,我则更喜欢选择procfs。
       写到这里,发现文字描述真的太累,任何文字在示意图和代码面前都是苍白的,这就是程序员为何喜欢写代码或者画图的根本原因,别跟程序员扯理论,先把代码跑起来再说吧。其实,看了GEB之后明白了一个真理,为何人对图的敏感度要优于文字,因为图是一个二维或者三维的being,而文字则是一个抽象的一维线索,必须串行处理,也就是说,理解文字意味着你的大脑需要不断进行压栈,弹出操作,而理解图片则真正的可以动用大脑的并行处理优势!我将上面的一大堆文字表示成一个图:

当然了,我在图上写下了太多的文字,以至于显得很乱,但是如果不加那些文字,可能会更乱,因为Netfilter的效果就是你可以“在任何地方将skb带到任何地方”,两个任何现实了“乱”的精髓,但是也同样表达了,真正的出神入化就是“乱得可以”。好了,如果连图都觉得很乱,那么就跑代码吧,在给出代码之前,先给出用法:
1.启动一个OpenVPN服务端:cipher设置为none,auth设置为none,你知道,如果协议跑通了,这些都不是事儿,毕竟那些算法只是独立的另一个库实现,相当独立。加载nf_ovpn_helper.ko,即可
2.启动OpenVPN客户端:cipher设置为none,auth设置为none
3.在虚拟IP上跑一些数据:比如用iperf试试看...panic?oh no!
4.开启真正的debug之旅...

代码和图一样,依然给出了大量的文字说明,其分量和代码差不多...代码如下:

/*
 * 整个城市向黑暗中退去,你我都放弃忍耐
 */
#include <linux/module.h>
#include <linux/netfilter.h>
#include <linux/ip.h>
#include <linux/inet.h>
#include <net/net_namespace.h>

#include <net/netfilter/nf_conntrack.h>

#include "ovpn_func.h"

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Wangran <[email protected]>");
MODULE_DESCRIPTION("OpenVPN connection helper");
MODULE_ALIAS("ip_conntrack_ovpn");
MODULE_ALIAS_NFCT_HELPER("ovpn");

/*
 * 此端口难道就这么写死成1194吗?难道不是需要注册的吗 :(
 **/
#define OVPN_PORT	1194

struct ovpn_instance {
	__be32 saddr;
	__be32 daddr;
	__be16 sport;
	__be16 dport;
	__be32 packet_id;
    /* 请恕我这么写,反正没有实际实现 */
	unsigned char *info[0];
};

/* fake struct nf_conn_counter*/
struct instance_info {
	u_int64_t i;
	u_int64_t j;
	__be32 saddr;
	__be32 daddr;
	__be16 sport;
	__be16 dport;
	__be32 packet_id;
};

/* 真实的packet_id应该从慢速路径或者控制路径的Netlink消息传递到conn的extend中 */
__be32 static_fake_packet_id = 0;

static int ovpn_nf_pre_routing(struct sk_buff *skb)
{
	/* nothing to do */
	return NF_ACCEPT;
}

/* 第二个参数真的需要吗,因为加密所需要的cipher_info以及封装需要的udp头,ip头信息全部
 * 而不是部分地保存在了conntrack info信息中了啊啊啊
 *
 * 注意:这里的参数并不符合规范,因为我盗取了heler的结构体,但是思想是一致的!
 **/
static int encap_xmit(struct sk_buff *skb, struct ovpn_instance *ovpn)
{
	/*
	 * 正如我一贯的风格,我总是无情推翻昨天的自大,将欢乐瞬间变成悲哀
	 * 我没有调用ovpn_data_channel_encap_xmit这个tun网卡的HOOK函数,因为
	 * 我觉得太垃圾了,其实我正在逐步还原tun.c,就像这次一样,我力图使用
	 * Netfilter进行短路操作,而不再触摸tun.c以及UDP socket。就像你看到
	 * 的那样,你依然可以加载系统自带的原生态tun.ko
	 **/
	int ret = NF_DROP;
	int copy = 0;
	unsigned int max_headroom;
	struct sk_buff *skb_to_encap;
	__be32 saddr = ovpn->saddr;
	__be32 daddr = ovpn->daddr;
	__be16 sport = ovpn->sport;
	__be16 dport = ovpn->dport;
	__be32 packet_id = ovpn->packet_id;
	struct iphdr *old = ip_hdr(skb);
	/* but 怎么判断OpenVPN是UDP的
     * 很简单,一切都在nf_conn的extend中,
     * 只是,我这里没有写而已!
     **/

#define I_THINK_THIS_LENGTH_ENOUGH_BECAUSE_OF_XXX  78
	max_headroom = (I_THINK_THIS_LENGTH_ENOUGH_BECAUSE_OF_XXX +
				    sizeof(struct iphdr)                +
				    sizeof(struct udphdr)               +
				    sizeof(struct ovpnhdr));

    if (skb_headroom(skb) < max_headroom || !skb_clone_writable(skb, 0)) {
        struct sk_buff *new_skb = skb_realloc_headroom(skb, max_headroom);
        if (!new_skb) {
            goto out;
        }
        skb_dst_set(new_skb, skb_dst(skb));

        skb_to_encap = new_skb;
        copy = 1;
    } else {
        skb_to_encap = skb;
	}
	/* ##################### encap OpenVPN #################### */
	{
        struct ovpnhdr *ohdr;
		skb_push(skb_to_encap, sizeof(struct ovpnhdr));
	    ohdr = ovpn_hdr(skb_to_encap);
		/* 慢速路径的packet_id必须反映到快速路径中来!! */
	    ohdr->id = htonl(packet_id);
	    ohdr->ocode = (P_DATA_V1 << P_OPCODE_SHIFT) | 0x0;
	}
	/* ##################### encap UDP #################### */
	{
		struct udphdr *uh;

		skb_push(skb_to_encap, sizeof(struct udphdr));
		skb_reset_transport_header(skb_to_encap);

		uh = udp_hdr(skb_to_encap);
		uh->source = sport;
		uh->dest = dport;
		uh->len = htons(skb_to_encap->len);
		uh->check = 0;
		uh->check = csum_tcpudp_magic(saddr, daddr, skb_to_encap->len,
								IPPROTO_UDP, csum_partial(uh,
						                                   skb_to_encap->len,
							                                0));
	}
	/* ##################### encap IP #################### */
	{
		struct iphdr *iph;
		struct dst_entry *dst;

		skb_push(skb_to_encap, sizeof(struct iphdr));
		skb_reset_network_header(skb_to_encap);
		iph = ip_hdr(skb_to_encap);
		iph->version		=	4;
		iph->ihl		=	sizeof(struct iphdr)>>2;
		iph->frag_off		=	0;//old->frag_off;
		iph->protocol		=	IPPROTO_UDP;
		iph->tos		=	old->tos;
		iph->daddr		=	daddr;
		iph->saddr		=	saddr;
		iph->ttl		=	old->ttl;
		/* 这个reroute频繁用于OUTPUT Netfilter HOOK,但问Rusty本人,
		 * Netfilter的OUTPUT设计为何如何之好 */
		if (ip_route_me_harder(skb_to_encap, RTN_LOCAL)!= 0) {
			/* 无论如何都要STOLEN的 */
            if (copy) {
			    kfree_skb(skb_to_encap);
            }
			goto out;
		}
		dst = skb_dst(skb_to_encap);	

		ip_select_ident(iph, dst, NULL);
	}
	ip_local_out(skb_to_encap);
    /* 偷走数据包,不再在曾经的路上继续 */
    ret = NF_STOLEN;
out:
	return ret;

}

static unsigned int ipv4_ovpn_in_local( unsigned int hook,
					struct sk_buff *skb,
					const struct net_device *in,
					const struct net_device *out,
					int (*okfn)(struct sk_buff *))
{
	int ret = NF_ACCEPT;
	struct nf_conn *ct;
	enum ip_conntrack_info ctinfo;
	struct net_device *dev = NULL;
	struct tun_struct *tun = NULL;

	ct = nf_ct_get(skb, &ctinfo);
	if (!ct) {
		goto out;
	}
	if (ct == &nf_conntrack_untracked) {
		goto out;
	}
	/* 注意,这个dev不该这么写死,应该从extend中获取,但是debug之旅如此奇幻,
	 * 你难道不想试试看吗?
	 */
	dev = dev_get_by_name(&init_net, "tun0");
	if (!dev) {
		goto out_not_put;
	}

	tun = netdev_priv(dev);;
	if (!tun) {
		goto out;
	}

	if (out && dev == out) {
		/*
		 * 这里要取出保存在conn中的所有信息,包括加密密钥
		 */
        /*
		 * cipher_info = (struct instance_info *)nf_conn_acct_find(ct);
		 * if (cipher_info != NULL) {
		 * 	if (cipher_info->saddr != 0 &&
		 *			cipher_info->daddr != 0) {
         */
				struct ovpn_instance ovpn;
				ovpn.saddr = 0x32c7a8c0;/*cipher_info->daddr;*/
				ovpn.daddr = 0xe9c7a8c0;/*cipher_info->saddr;*/
				ovpn.sport = 0xaa04;/*cipher_info->dport;*/
				ovpn.dport = 0xaa04;/*cipher_info->sport;*/
				++ static_fake_packet_id;/*cipher_info->packet_id;*/
				ovpn.packet_id = static_fake_packet_id;/*cipher_info->packet_id;*/
				ret = encap_xmit(skb, &ovpn);
				goto out;	

	}
out:
    dev_put(dev);
out_not_put:
    return ret;
}
static unsigned int ipv4_ovpn_in( unsigned int hook,
				struct sk_buff *skb,
				const struct net_device *in,
				const struct net_device *out,
				int (*okfn)(struct sk_buff *))
{
	int ret = NF_ACCEPT;
	struct nf_conn *ct;
	enum ip_conntrack_info ctinfo;
	struct iphdr *hdr = ip_hdr(skb);
	struct udphdr *uh;
	struct net_device *dev = NULL;
	struct tun_struct *tun = NULL;
	__be32 saddr, daddr;
	__be16 sport, dport;
	int dir;

	ct = nf_ct_get(skb, &ctinfo);
	if (!ct) {
		goto out;
	}
	if (ct == &nf_conntrack_untracked) {
		goto out;
	}
	/* 注意,这个dev不该这么写死,应该从extend中获取,但是debug之旅如此奇幻,
	 * 你难道不想试试看吗?
	 */
	dev = dev_get_by_name(&init_net, "tun0");
	if (!dev) {
		goto out_not_put;
	}

	if ((in && in == dev) || (in && in == init_net.loopback_dev)) {
		goto out;
	}

	tun = netdev_priv(dev);;
	if (!tun) {
		goto out;
	}

	switch (tun->flags & TUN_TYPE_MASK) {
	case TUN_TAP_DEV:
		goto out;
	}

	saddr = hdr->saddr;
	daddr = hdr->daddr;

	/* 到达此处的数据包有以下几类:
	 *	1.正方向的UDP到将欲到达OpenVPN的数据包
	 *		1.1.控制通道数据包
	 *			这类数据包将最终穿过INPUT,完成conntrack的confirm,至此conntrack建立
	 *		1.2.数据通道的数据包
	 *			这类数据包就是我要截获,解密,进而STOLEN的。This is it!!!
	 *	2.从OpenVPN进程socket发出的数据包
	 *		2.1.控制通道数据包
	 *			这类数据包来自OpenVPN进程,用于SSL握手以及PING(keepablive)
	 *		2.2.数据通道数据包
	 *			这类数据包本来来自OpenVPN进程,由其加密,但是由于它们将在tun的xmit中被截获自行进行OpenVPN/UDP/IP封装,
	 *			因此并不会到达此处,也可以在OUTPUT/PREROUTING中被识别并自行进行OpenVPN/UDP/IP封装并被STOLEN到dev_queue_xmit
	 *			......
	 *
	 **/
	dir = CTINFO2DIR(ctinfo);
	if (dir != IP_CT_DIR_ORIGINAL) {
		goto check_encap_xmit;
	}
	/* 此处没加锁啊没加锁!!! */
	if (hdr->protocol != IPPROTO_UDP) {
	/*
	 * 这里彻底呈现了UDP的优势
	 * 你可能不信!但是如果是TCP,你将不能在中间任何地方截获(STOLEN)数据!
	 * 因为TCP是端到端流协议,你要是截获了数据,怎么发送回执??
	 * 你没法ACK数据,TCP将不再继续!除非...
	 * 除非你连ACK也伪造!连带的,你难道要自己实现TCP的语义??
	 **/
		goto check_encap_xmit;
	}

	skb_pull(skb, ip_hdrlen(skb));
	skb_reset_transport_header(skb);
	/* 此处省略了UDP接收的例行校验检查 */
	uh = udp_hdr(skb);

	if (uh->dest != htons(OVPN_PORT)) {
		skb_push(skb, ip_hdrlen(skb));
		skb_reset_network_header(skb);
		goto check_encap_xmit;
	}
	sport = uh->source;
	dport = uh->dest;
	{
		/*
		 *  这里要取出保存在conn中的所有信息,包括解密密钥
		 *	ct_inner = nf_ct_get(skb, &ctinfo_inner);
		 *	cipher_info = nf_conn_acct_find((const struct nf_conn *)ct_inner);
		 *	if (cipher_info == NULL) {
		 *		...
		 *		...
		 *
		 */
	}
    /* decrypt
     * 很显然,这是关键!数据解密!
     * 但是谁能告诉我内核中怎么高效使用加解密,如果不能高效,
     * 那么起码保证灵活,就像OpenSSL那样!进入了内核态,我突然
     * 突然想到了OpenSSL的好,人,不能忘本啊  :<
     */
	/*
	 *  以上是我在udp_encap_rcv版本中的注释!!但是,但是
	 *  天啊!饶恕我的贪婪吧!
	 *  在nf_conntrack_helper版本中,我连封装的力气都没有了,为了尽快验证,
	 *  我将代码写死!
	 *  解密算法:AES-128-ECB
	 *  解密密钥:128位的0!
	 */
	/* ################################################################### */
	/* 验证伊始,推进一个udp头 */
	skb_pull(skb, sizeof(struct udphdr));
	{
		/* PRE Decrypt--对齐数据,验证操作码 */
	    u8 *data = skb->data;
	    u8 ocode = data[0];
	    int op = ocode >> P_OPCODE_SHIFT;
	    if (op != P_DATA_V1) {
			skb_push(skb, sizeof(struct udphdr));
			skb_push(skb, ip_hdrlen(skb));
			skb_reset_network_header(skb);
			skb_reset_transport_header(skb);
		    goto out;
	    }
	}
	/* ################################################################### */
	{
		/* Decrypt--调用内核接口解密数据 */
        /*int i;
		struct crypto_cipher *tfm;
		unsigned char key1[16] = {0};
        unsigned char *data;

		tfm = crypto_alloc_cipher("aes", 0, CRYPTO_ALG_ASYNC);
		if (!tfm) {
			return NF_DROP;
		}
		crypto_cipher_setkey(tfm, (const u8 *)&key1[0], 16);
        data = skb->data + 1;
        for (i = 0; i < skb->len - 1; i += crypto_cipher_blocksize(tfm)) {
            crypto_cipher_decrypt_one(tfm, data + i, data + i);
        }
		crypto_free_cipher(tfm);
		*/
		/* 解密完成,推进一个OpenVPN头的长度 */
		skb_pull(skb, sizeof(struct ovpnhdr));
	}
	/* ################################################################### */

	switch (tun->flags & TUN_TYPE_MASK) {
	case TUN_TUN_DEV:
        switch (skb->data[0] & 0xf0) {
                /* 当前只支持IPv4 */
        case 0x40:
            break;
        default:
			/* 解密发现不是IPv4,不再恢复skb指针 */
			ret = NF_DROP;
            goto out;

        }
        skb_reset_mac_header(skb);
		skb_reset_network_header(skb);
		skb_reset_transport_header(skb);
            /* 是时候丢掉西装外衣了,口袋里的通行证会将你引入深渊,
             * 不信的话,注释此言,在OpenVPN客户端机器上ping一下
             * 服务端的虚拟IP试一试
             **/
        skb_dst_drop(skb);
        skb->protocol = htons(ETH_P_IP);;
        skb->dev = dev;
        ret = NF_STOLEN;
        break;
	}

    /* 模拟TUN虚拟网卡接收,此时截获处理正式完成,
     * 告诉UDP,嗨,你的数据我已经帮你处理了
     **/
	/*	遍历PREROUTING旨在创建被OpenVPN封装流量的conntrack,
	 *	因为只有在这里才能从OpenVPN数据通道的conntrack中的info信息得到加密密钥:
	 *	1.该类流量在netif_rx_ni->netif_receive_skb->ip_rcv...路径中径直通过PREROUTING;
	 *	2.该类流量的reply流量直接使用其conntrack info中的加密密钥进行加密
	 **/
	nf_reset(skb);
    /* 溜达溜达,一直溜达到skb的conntrack被设置,所以我使用了带有condition的版本 */
	NF_HOOK_COND(PF_INET, NF_INET_PRE_ROUTING, skb, skb->dev, NULL,
						ovpn_nf_pre_routing, skb->nfct != NULL);
	{
		/*struct nf_conn *ct_inner;*/
		/*enum ip_conntrack_info ctinfo_inner;*/
		/*struct instance_info *cipher_info; */
		/* OK! 此时的ct应该就是OpenVPN裸skb的ct了! */
		/*
		 *  注意,这里可能比较绕!对于ct_inner,很显然它是OpenVPN数据协议封装的内部skb的ct,那么
		 *  它的方向有两个,一个是正一个反,
		 *  1.对于正方向,很显然它是我们在上面的ovpn_nf_pre_routing
		 *	  这个fake HOOK中建立的,理所当然它的cipher_info就是在这里创建的
		 *	2.对于反方向,它走的是慢速路径,即它走的是OpenVPN进程(这是为什么呢?为什么呢?
		 *			因为:
		 *				skb来自某个物理网口,显然最终它要从tun0中xmit出去,这一路上它是不可能获得
		 *				任何关于multi_instance的信息的,所以只好走入慢速路径中,由OpenVPN进程从字符
		 *				设备读取该数据包,然后由OpenVPN进程加密,封装,传输之)
		 *	  只要有反向发起的数据包的正向(即从OpenVPNclient到OpenVPNserver方向)返回包经由此处,它将
		 *	  建立cipher_info。
		 *	因此,此处并不区分对待ct的方向!
		 **/

        /*
		ct_inner = nf_ct_get(skb, &ctinfo_inner);
		cipher_info = (struct instance_info *)nf_conn_acct_find((const struct nf_conn *)ct_inner);
		if (cipher_info == NULL) {
			cipher_info = (struct instance_info *)nf_ct_acct_ext_add(ct_inner, GFP_ATOMIC);
			if (cipher_info == NULL) {
				ret = NF_DROP;
				goto out;
			}
			goto alloc_info;
		} else {
			// 注意:最终的成型info extend中,需要在destroy里面释放 JUST test!!
			if (cipher_info->saddr == 0 && cipher_info->daddr == 0) {
alloc_info:
				cipher_info->saddr = saddr;
				cipher_info->daddr = daddr;
				cipher_info->sport = sport;
				cipher_info->dport = dport;
				// info 就是cipher
			}
		}
        */
	}
    /* 真是谢天谢地!谢什么?答曰:
     * 在调用netif_rx的时候竟然还能保留nf信息,比如保留nf_conn...
     * 其实这也没什么大不了的,难道bridge模块没有这么玩吗?难道bonding,vlan没有这么玩吗?
     * 如果你不懂,没关系,试试看:
     * sysctl -w net.bridge.bridge-nf-call-iptables=1
     * 然后跟一下代码...
     **/
	netif_rx_ni(skb);
    goto out;

check_encap_xmit:
    /* 此处find conntrack的info信息,如果数据包从物理网卡接收,最终需要通过tun网卡发出进行加密,那么:
     * 1.该数据包所属的流在从OpenVPN客户端过来的时候在PREROUTING中被解密,然后在PREROUTING中溜达到conn创建,
     *   此时,该流可以查到,直接取出info信息,调用encap_xmit进行加密;
     * 2.该数据包所属的流是主动从OpenVPN服务端发往OpenVPN客户端方向的,那么它在这个HOOK就应该直接返回,进入
     *   OpenVPN这个慢速路径进行加密,如果有从OpenVPN客户端回来的包,那么在这个HOOK中就会被在conntrack的info
     *   中设置info信息。
     * :)也已经深了,我以上如此清晰的思路想必可以代替代码吧,此处我就直接通过了。
     **/
    {
        int check = 0; /* 真正的check! */
        if (check) {
			struct ovpn_instance ovpn;
            ret = encap_xmit(skb, &ovpn);
        }
    }

out:
	dev_put(dev);
out_not_put:
    return ret;
}

static struct nf_hook_ops ipv4_ovpn_ops[] __read_mostly = {
	{	.hook		= ipv4_ovpn_in,
		.owner		= THIS_MODULE,
		.pf		=	NFPROTO_IPV4,
		.hooknum	= NF_INET_PRE_ROUTING,
		.priority	= NF_IP_PRI_CONNTRACK + 1,
	},
	{	.hook		= ipv4_ovpn_in_local,
		.owner		= THIS_MODULE,
		.pf		=	NFPROTO_IPV4,
		.hooknum	= NF_INET_LOCAL_OUT,
		.priority	= NF_IP_PRI_CONNTRACK + 1,
	},
};

static void nf_conntrack_openvpn_fini(void)
{
	nf_unregister_hooks(ipv4_ovpn_ops, ARRAY_SIZE(ipv4_ovpn_ops));
}

static int __init nf_conntrack_openvpn_init(void)
{
	int ret = 0;

	ret = nf_register_hooks(ipv4_ovpn_ops, ARRAY_SIZE(ipv4_ovpn_ops));
	if (ret) {
		printk("nf_ct_ovpn: failed to register\n");
		return ret;
	}
		printk("nf_ct_ovpn: OKOK\n");
	return 0;
}

module_init(nf_conntrack_openvpn_init);
module_exit(nf_conntrack_openvpn_fini);
时间: 2024-10-06 18:34:17

OpenVPN的Linux内核版,鬼魅的残缺 part III rework with Netfilter的相关文章

OpenVPN的Linux内核版,鬼魅的残缺 part II:The encrypt engine

一夜入冬啊一夜入冬,医院回来的路上已经困得不行了,不过还是仔细思考了我的OpenVPN内核版.和那些天天在微信朋友圈晒工作的相比我简直弱爆了,我 本应该说今天没有去上班,然后心里多么多么内疚的,什么耽误工作之类的,不过那不是我的style,就像我同样不喜欢在工位上贴大量的任务计划一样...       把本已不堪的OpenSSL移植到Linux Kernel,这真是一个疯狂的想法,极其疯狂的想法,过于疯狂的想法,以至于作为一个还算正常的人只好作罢!我需要的是使用现有的内核中的加密引擎或者 简单的

OpenVPN的Linux内核版,鬼魅的残缺 part IV:Normal Method

谁能在上海请我吃一顿烤全羊!又折腾了几乎个通宵...对于将OpenVPN弄进内核这件事,我已经找到了两个思路:1.使用UDP的encap rcv HOOK/tun xmit HOOK:2.使用Netfilter的PREROUTING HOOK分离控制通道和数据通道:但 是有个问题,那就是它们都仅仅对UDP有效,而OpenVPN虽不建议但是也是对TCP提供支持的.然而如果实现TCP模式的内核OpenVPN数据通道 处理,就必须在传输层之上来做,因为TCP必须要完成它自己的协议事务,比如确认,重传等

OpenVPN的Linux内核版,鬼魅的残缺 Prelude

首先向James yonan致敬!其次吐个槽,这都12月份了,还打雷,看来这股冷空气威力不是一般的大啊,大气环流和气压都被它改变了,我仿 佛看到了热空气和雾霾被这股异常强大的寒潮挤压得像难民一样背井离乡....这对于我而言,意义十分重大,从下周一开始,我将告别短袖T恤,穿上衬衫 了...再次重申,我这个时候穿短袖真的不冷,我能坚持到5摄氏度穿短袖,真的不冷,我在7摄氏度的时候偶尔穿一次长袖是因为压力实在太大... OpenVPN不好,代码简单却不好读,逻辑清晰却不好改,你明明知道该怎么改了,却迟

OpenVPN的Linux内核版,鬼魅的残缺 View

是时候给出一个总的图景了,是时候了.我的意思是说,为什么非要将OpenVPN移植到内核,而不是在用户态,即在它本身优化它.为什么呢?事实上我已经在用户态优化了它,虽然有些难以定位的segment fault,但是并不是说我没有时间没有能力搞定这些,知难而退到内核(退到内核碰到panic岂不是更难搞),不是这样的.我的本意是,且一直都是,我要缩短处理路径的长度,我从来都不相信什么软件神话,事实上我憎恨这些神话.正如一位网友所说,干嘛非要区分什么内核态,用户态!关键点不在哪个态处理效率高,关键在处理

OpenVPN的Linux内核版,鬼魅的残缺 part I:The PROTOCOL

OpenVPN的多处理一直都是问题,但是作为轻量级VPN,这无所谓,但是如果你要将其作为重量级VPN来用,那就必须考虑了.       之前,我将OpenVPN分裂成了多线程版本,但是由于OpenVPN原本的buffer管理粒度就很粗,以至于我很难在多个线程中能够同时处理一个 multi_instance,所以我不得不采用一个multi_instance绑定一个线程的做法,为此还特意实现了一个自己版本的多队列TUN网卡 已经UDP的hash reuseport机制,这一切耦合太紧密,以至于牵一发

Linux内核工程导论——网络:Netfilter概览

简介 最早的内核包过滤机制是ipfwadm,后来是ipchains,再后来就是iptables/netfilter了.再往后,也就是现在是nftables.不过nftables与iptables还处于争雄阶段,谁能胜出目前还没有定论.但是他们都属于netfilter项目的子成员. 钩子 netfilter基于钩子,在内核网络协议栈的几个固定的位置由netfilter的钩子.我们知道数据包有两种流向,一种是给本机的:驱动接收-->路由表-->本机协议栈-->驱动发送.一种是要转发给别人的:

关于linux内核2.4.xx 直接升级到 2.6.xx 版出现的问题

2.4.x 与 2.6.x 是两个具有相当大差异的核心版本, 两者之间使用到的函式库基本上已经不相同了,所以在升级之前,如果你的核心原本是 2.4.xx 版,那么就升级到 2.4.xx 版本的最新版,不要由 2.4.xx 直接升级到 2.6.xx 版,否则会出现很多未知的错误,直接导致系统不能使用. 关于linux内核2.4.xx 直接升级到 2.6.xx 版出现的问题,布布扣,bubuko.com

linux发行版和内核的关系

转自:http://m.blog.csdn.net/article/details?id=50595230 Linux内核是计算机操作系统的核心.一个完整的 Linux发行版包括了内核与一些其他与文件相关的操作,用户管理系统,和软件包管理器等一系列软件.每个工具都是整个系统的一小部分.这些工具通常都是一个个独立的项目,有相应的开发者来开发及维护. 前面提到的Linux内核,包括现行版本,以及历史版本(即更早发布的版本)都可以在 www.kernel.org 找到.Linux的众多发行版可能是基于

Linux内核LTS长期支持版生命周期

Longterm release kernels Version Maintainer Released Projected EOL 4.9 Greg Kroah-Hartman 2016-12-11 Jan, 2019 4.4 Greg Kroah-Hartman 2016-01-10 Feb, 2018 4.1 Sasha Levin 2015-06-21 Sep, 2017 3.18 Sasha Levin 2014-12-07 Jan, 2017 3.16 Ben Hutchings 2