ip_queue跟libcrypto实现另一种VPN

ip_queue和libcrypto实现另一种VPN

第一部分.现有VPN实现简述

对于VPN而言,关键在于加密和封装,而这二者的前提首先是可以获得原始IP数据包。现在有几种常见的方式:
1.使用专有传输层协议
这类典型例子就是IPSec协议族,包括AH,ESP,IKE等,其中前两者用于封装,后一种用于密钥协商。
2.使用虚拟网卡+路由
典型例子就是OpenVPN,具体就不多说了。

第二部分.ip_queue引入以及原理简介

其实,还有另一种更加直观的方式,那就是使用ip_queue,这种方式直接“截获”了原始IP数据包到用户空间,然后你可以在用户空间进行加密和封装,然后再将其注入到内核协议栈,一切都是那么的顺理成章,very good!
        ip_queue的实现很简单,使用了Linux内核的Netlink机制与用户态通信,在用户态需要一个进程不断接收被queue的原始数据包,而这一切已经被封装在libipq和libnetfilter_queue里面了,很方便使用。在Netfilter的任意钩子点,都可以使用:
iptables -t XXX -A YYY $several match -j QUEUE
就可以将数据包queue到用户态了,这里只讲原理,因此不会涉及NFQUEUE以及queue-num。用户态进程可以再将包注入到内核,注入到哪里呢?答案是从哪里来,注入到哪里,内核加载了ip_queue之后,会启动一个netlink套接字来接收用户态注入的数据包,最终进入ipq_receive_peer函数:

最终在ipq_issue_verdict中调用了nf_reinject将数据包注入到它当初被NF_QUEUE的地方。整个过程中正是nf_info结构体记住了当初数据包离开时的信息,那么这个结构体应该在数据包被NF_QUEUE的时候被初始化,然后在nf_reinject中,如果数据包被ACCEPT了,则:
这就是整个过程。
        对于内核协议栈的处理,仅仅需要明白其原理即可,我们需要做的就是在截取到数据包和重新注入数据包的逻辑之间加入那么一点点代码。不幸的是,关于libipq和libnetfilter_queue的资料很少,文档也不齐全,不幸中的万幸就在于libipq的man手册中给了一个完整的源码,在这个源码的基础上,我们可以将之扩展成任何样子。

第三部分.用户态代码以及内核态规则

下面给出先我的扩展代码:

注释0:关于ipq_packet_msg_t结构体
这个结构体在ipq_get_packet的man手册中有所介绍:
个人觉得,有了这个结构体,基本不需要什么libnetfilter_queue了,该结构体很底层,用起来也比较方便,比如可以直接取出原始数据包以及其长度,相反,使用libnetfilter以及libipq的一大堆函数更容易把人们搞晕掉。
注释1:关于源地址的填写
在填写源地址的时候,注意不能直接填写本机的IP地址,因为这样的话数据包将路由不出去,具体参见fib_validate_source函数:
意思是说,在route_input进行反向路由查找的时候,结果必须是UNICAST,如果源地址填写了本机地址,那么结果将是LOCAL,系统会在rtstat的in_marti-an_src中记下一笔。那么怎么填写源地址呢?有两种方式:
a>.填写一个本网段的非本机地址,没有被其他机器使用,这个地址需要申请,并且要告知VPN隧道对端,以便对端填写目标IP地址;
b>.使用iproute2将本机的一个被VPN使用的地址从local表删除。

注释2:关于协议号
如果你真的使用IPSec协议封装了数据包,那么请在此填写ESP或者AH的协议号,如果是别的,那么请自己定义一个。一定要修改它,否则就会出现莫名其妙的问题,比如原始数据封装了ICMP数据,如不修改protocol字段,那么你抓包会发现有以下的错误:
ICMP echo reply, id 0, seq 0, length 40 (wrong icmp cksum 0 (->ffff)!)
        有了用户态代码,接下来需要设置内核态的规则了,很简单,把需要引入隧道的感兴趣流queue到用户态即可,比如我们此时的感兴趣流是所有到达192.168.60.0/24网段的流量,那么就设置下面的规则:
iptables -t mangle -A PREROUTING -d 192.168.60.0/24 -j QUEUE
之所以使用mangle表,是因为在mangle里面引导数据包到用户态修改它更加符合其字面含义,其实凡是路由前的PREROUTING的任何位置都可以,也可以自己写一个module...这样我们就实现了一个完全在用户态实现的类似IPSec的VPN雏形,它完全可以实现IPSec的所有语义,并不像OpenVPN那样使用TCP或者UDP来封装。由于使用iptables的queue target或者自定义module的target(如果你不想被人们用iptables-save看到你的规则的话),而不像OpenVPN引入了虚拟网卡这个概念,个人觉得这种方式更加直接些,而不是通过路由来截获数据包。

第四部分.如何加密

在第三部分的代码中,我没有给出加密数据包的代码,因为这太复杂了,如果希望封装成IPSec,那么光IPSec协议本身就有很多内容,还包括密钥协商等复杂的主题。实际上,你可以使用任意方式对数据进行加密,简单的可以用B64编码一下,复杂的就多了。更加吸引人的是,这段代码可以和OpenVPN结合起来,这就是说,不再将数据包重新注入内核协议栈,而是交给OpenVPN,让OpenVPN加密封装后再通过TCP或者UDP发送出去,这样一来ip_queue的作用就是OpenVPN的tap字符设备的作用了,OpenVPN也就多了一种获取数据的方式,不是通过路由而是通过ip_queue。
        最一般的方式就是使用libcryto库中大量的加密接口来对数据进行加密。在Linux上,总是有那么多东西可以直接使用,几乎可以满足你的所有需求,既然我们已经顺利地将数据包截获并且也可以重新封装后注入内核,那么加密这一块为何不使用另一个现成易用的libcrypto呢?具体怎么做就不写了,用就是了。

第五部分.如何和网络组合起来

到目前为止,我没有给出隧道对端如何定义,也没有给出对端的代码而仅仅给出了一端的代码,因为根本没有必要操心对端。我们使用ip_queue截获了数据包,我们需要将其目标地址封装成可以经过对端设备的任意地址,只要对端设备设置了相应的规则将该目标地址的包queue到用户态解封装即可,如果我们使用这种方式实现了IPSec,那么对端就可以是一台IPSecVPN设备,比如Cisco的设备,如果我们实现了OpenVPN的协议,那么对端可以是一个运行OpenVPN的机器。总之,使用ip_queue实在太灵活了,它甚至可以兼容任何的VPN协议,只要我们能搞到这种VPN的协议即可。
        如果在非Linux平台,怎么做呢?使用PACKET套接字吧,可以使用scapy来做实验...何必呢?还是用Linux吧,何必引入那么复杂东西呢?Linux几行命令,现成的代码,为何不用呢?

神说:学好VPN,别看书,要折腾...