用netcat,SSH构建IP层Tunnel

2017/05/06

【关于题外话在最后】

写作本文主要基于两点,首先是因为我前段时间写了几篇关于XXN的新解,收到了很多的邮件反馈,我也思考了很多,另一个方面是因为很多人问我怎么用QQ,P2P搭建一个IP层的Tunnel,我的回答是“我也不知道”。我确实不知道,根本就没有试过,只是有个这样那样的想法…我主要是没有动力和能力去Hack这些非Linux上的东西…所以说,我写这篇文章,用UNIX的方法 “将多个小工具结合起来” 实现我的那些没有实现的想法,抛砖引玉一下。

声明:

本文没有技术含量,甚至不会有什么代码,本文只是一些Linux bash和netcat的使用技巧之堆砌,但我觉得只有这种是理解Everything over App的最好方式,虽然我没有能力将TAP对接到QQ上,但我有能力将其对接到netcat以及ssh上,而netcat,ssh和QQ并没有什么本质的区别,结论是,只要你能找一个通信软件,Tunnel便可以嫁接于其上。

在这里插入图片描述

Tunnel的种类

一般而言,传统的Tunnel都是用新的IP报文来运输原始IP报文的,这种新的IP报文往往封装一个新的四层头。但是也有用TCP,UDP来运输原始IP报文的,典型的例子就是OpenXXN,BadXXN等。
事实上,你可以用任何的报文来运输原始IP报文,在上一篇文章《从一个简单的聊天程序SimpleChat看XXX技术》中,我提到你可以用QQ,P2P协议来运输IP报文,并且给出了一个应景的设想。本文将通过简单的实例来实现一下这种设想。

发展阶段

像类似PPPoE,L2TP什么的,提出了一个很好的框架和概念,说明IP over XXX的可行性,然后OpenXXN掀开了一次Revolution,说明IP可以over UDP,并且和SSL/TLS进行了深度结合,小众的BadXXN应该是另一次Revolution的,但是作者好像没有把握住机会,和OpenXXN不同的是,BadXXN旨在提供灵活且独立的点对点组网能力。

在研究了BadXXN之后,我自己写了一个简单的Demo,即SimpleXXN
https://github.com/marywangran/overlay

其实,BadXXN的思路很简单,这个和我们平时的聊天软件的思路完全一致,不同的是聊天软件运输的是我们键盘输入的消息,而BadXXN运输的则是从TAP网卡中读取的IP报文。借着这个思路,我觉得任何的可以进行网络通信的软件都可以运输IP报文,只需要将程序的输入和输出重定向到TAP网卡的输入和输出即可,这非常简单。

准备工作

如果说TAP网卡在文件系统中以一个文件的形式存在,那么事情会非常简单,比如tap0网卡在文件系统中有一个字符设备文件/dev/tap0,那么以scp为例,我便可以通过以下的命令来运输通过tap0传输的IP报文或者以太帧:

scp /dev/tap0 [email protected]:/dev/tap0

然而自打Linux 2.6.x(8<x<32)起,内核便不再提供这种机制了。至少在2.6.8的内核中,有一个ethertap的模块,它提供了我上述所说的“理想方法”,你只需要以下的命令便可以构建一个tap0文件:

modprobe ethertap
ifconfig tap0 10.10.10.10/24 up
mknod /dev/tap0 c 36 16
...

可是到了2.6.32内核,ethertap模块没有了,只剩下了tun模块,使用tun模块,无法直接建立tap0的设备文件,必须通过ioctl来显式设置它:

modprobe tun
tunctl -u root -t tap0
# 注意,tun模块让你无法通过命令构建一个可以直接读写的tap0设备文件,你必须写一个程序显式执行open/ioctl对,然后操作其文件描述符进行读写。
...

这意味着我不得不写一个程序了,而这令不会编程的我感到悲哀!我把该文件命名为tunio.c,如下所示:

#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <net/if.h>
#include <linux/if_tun.h>
#include <sys/select.h>
#include <sys/ioctl.h>
#include <fcntl.h>

struct frame {
        unsigned int len;
        char data[2000];
};

// 为了便于重定向操作,我在本程序中只使用了3个文件描述符:TAP网卡字符设备,标准输入,标准输出
int main(int argc, char **argv)
{
        int fd = -1;
        struct ifreq ifr;
        size_t len;
        char buf[2004];
        struct frame frm;
        int i;
        fd_set rd_set;

        if( (fd = open("/dev/net/tun", O_RDWR)) < 0) {
                exit(-1);
        }
        memset(&ifr, 0, sizeof(ifr));
        ifr.ifr_flags |= IFF_NO_PI;
        ifr.ifr_flags |= IFF_TAP;
        snprintf(ifr.ifr_name, IFNAMSIZ, "%s", "tap0");
        ioctl(fd, TUNSETIFF, (void *)&ifr);
        while(1) {
                int nfds;
                int j;
                FD_ZERO(&rd_set);
                FD_SET(0, &rd_set);
                FD_SET(fd, &rd_set);
                char *tmp;

                nfds = select(1024, &rd_set, NULL, NULL, NULL);
                for (j = 0;j < nfds; j++) {
                        // 如果标准输入有动静的话
                        if(FD_ISSET(0, &rd_set)) {
                                unsigned int dlen;
                                // 从标准输入中读取数据长度
                                len = read(0, &dlen, sizeof(unsigned int));
                                // 按照指示的长度读取原始IP报文或者数据帧
                                len = read(0, frm.data, dlen);
                                // 将IP报文或者数据帧写入TAP设备
                                len = write(fd, frm.data, len);
                        }
                        // 如果虚拟网卡字符设备有动静
                        if(FD_ISSET(fd, &rd_set)) {
                                // 从网卡读取IP报文或者以太帧
                                len = read(fd, buf, sizeof(buf));
                                // 为了对端可以区分数据边界,加入了长度头
                                frm.len = len;
                                memcpy(frm.data, buf, len);
                                tmp = (char *)&frm;
                                // 将加入长度头的原始数据输出到标准输出
                                for (i = 0; i < len+sizeof(unsigned int); i++) {
                                        printf("%c", tmp[i]);
                                }
                        }
                        // 刷新输出
                        fflush(NULL);
                }
        }
        return 0;
}

将以上的代码编译成tunio即可:

gcc tunio.c -o tunio

然后就可以用这个tunio和其它的既有程序进行基于UNIX哲学的组合从而构建出一个看起来像那么回事的Tunnel了。

在继续之前,我不得不说一下不能继续使用ethertap的原因。

一个好好的ethertap为什么就下课了呢?其实很大的原因在于ethertap所依赖的Netlink机制被重构了。在2.4版本以及2.6的早期版本的内核中,Netlink消息是通过字符设备进行读写的,然而后来Netlink便不再采用字符设备了,而是采用socket API进行读写了,而socket并没有导出文件到文件系统这也是众所周知,socket仅仅导出文件到进程。然而这跟ethertap下课有关吗?有的!

ethertap之所以可以导出文件到文件系统,就是使用的Netlink字符设备,你看看上述的mknod命令,其major设备号36就是Netlink的major设备号:

#define NETLINK_MAJOR        36

在init_netlink中:

if (register_chrdev(NETLINK_MAJOR,"netlink", &netlink_fops)) {
...

在ethertap模块代码中,你看不到任何关于字符设备的逻辑,事实上ethertap的字符设备文件的读写逻辑已经被Netlink接管,这个从hard_xmit以及netif_rx这两个协议栈的读写接口中便可获知,它们都是直接与Netlink接口。

好了,现在Netlink不再采用字符设备的方式了,改由API调用的方式控制了,那么ethertap便面临两个选择,一个是也跟着采用API的方式进行控制,而这个与tun模块功能重复,另一个选择是为ethertap单独注册一个字符设备类型,或者将其注册到misc设备,但这样会使字符设备集合越来越臃肿…再者说了,不还有tun的吗?编个程序封装一下读写控制逻辑有那么难吗?所以说,只能下课了…

其实,虽然ethertap好用,但其实现方式是有问题的,试想如果大家都为每一个功能注册一个Netlink号码,或者注册一种字符设备,那么几乎可以肯定,这种横向的平坦拓展方式总有一天会让Netlink或者字符设备管理机制不堪重负…之前的ioctl问题就是一个例子。

下面开始真正有意思的东西。

使用netcat实现一个简单的Demo

小用一下这个被誉为“瑞士军刀”的小巧nc,我来展示一下IP报文是如何通过nc构建的连接运输到另一个任意节点的。

我把tunio的标准输出重定向到netcat的标准输入,然后把netcat的标准输出重定向到tunio的标准输入,事情就成了,这让整个事情变成了一个环:
在这里插入图片描述
是不是很简单呢?

那么怎么实现呢?虽然逻辑上上图已经足够简单,但是实现上还是需要一个pipe作为过渡的,因此我们先在两台机器上分别创建一个pipe:

mkfifo /tmp/tmp_fifo

接下来的玩法完全取决于你对netcat的熟悉程度,本文不讲netcat,只说Tunnel,所以直接给出命令:

# 机器1:
cat /tmp/tmp_fifo | ./tunio 2>&1 | nc -l 1567 >/tmp/tmp_fifo
ifconfig tap0 10.10.10.10/24 up
# 机器2:
cat /tmp/tmp_fifo | ./tunio 2>&1 | nc 192.168.44.100 1567 >/tmp/tmp_fifo
ifconfig tap0 10.10.10.20/24 up

然后你来试试在机器2上去ping机器1的tap0的地址,绝对通了。这下,我们在物理网络的TCP连接上成功构建了一个承载以太帧的Ethernet over TCP的Overlay,显然这还远远不够,我们来抓包看看。在机器2上执行:

curl 10.10.10.10

同时在机器1上抓包:

tcpdump -i any tcp port 1567 -n -w overlay1.pcap

打开这个overlay1.pcap,我们可见:
在这里插入图片描述

好吧,虽然很完美的完成了隧道封装,然而没有实现加密…某种意义上,这并不是XXN,具体为啥我在这里多说无益。技术归技术,我们现在的目标是实现加密传输。这简直太简单了。

在继续展示加密隧道之前,我必须来点题外话为netcat做点推广。在前几篇文章中,我提到了QQ,P2P,甚至差点连飞鸽传书都扯上,但这些对Linux用户来讲,都是被鄙视的。在Linux上难道不是netcat这把瑞士军刀最靠谱吗?如果两人都使用Linux,最轻便的聊天工具就是netcat,根本不用装别的什么乱七八糟的东西。

实现一个加密隧道:使用ncat

当然,按照UNIX的传统方法,使用netcat和一个加密解密的命令加上输入输出重定向,就能在上节例子的基础上实现加密隧道,比如可以nc…|mcrypt…,然而我没有成功!所以我不得不使用别的方案。

幸亏有一个好用的ncat可以使用,我直接就用了。其实还有一个cryptcat可用,但我没有用成功,如果用成功的,希望可以告诉我。我本人对bash重定向的掌握一直都是半吊子水平,所以用起来当然没有玩Netfilter,XXN这般得心应手。决定使用ncat,命令如下:

# 机器1:
cat /tmp/tmp_fifo | ./tunio 2>&1 | ncat -vnl 333 --ssl >/tmp/tmp_fifo
#【以下特意给出屏幕输出,以显示正在初始化加密套件】
ifconfig tap0 10.10.10.10/24 up

在这里插入图片描述


# 机器2:
cat /tmp/tmp_fifo | ./tunio 2>&1 |ncat -nv 192.168.44.100 333 --ssl  >/tmp/tmp_fifo
#【以下特意给出屏幕输出,以显示正在初始化加密套件】
ifconfig tap0 10.10.10.20/24 up

在这里插入图片描述

同上面的netcat明文实例一样,依然在机器2上执行curl,在机器1上抓包,得到如下结果:

在这里插入图片描述
我不可能去解释这些密文是什么个含义,总之,这就是一条加密的隧道,构建成功了。至于说加密强度如何,不属于本文的范畴。

实现一个加密隧道:使用ssh

其实一开始我是希望使用scp来做tap0字符设备文件的传输的,中间我采用了一个fifo pipe来过渡,但是没有成功,貌似scp的源文件必须是Regular file才可以,不能是pipe。后来我决定放弃,进而寻找别的方案。毕竟,搞清楚scp并不是我的目的,其实我完全可以修改scp的源码使之适应pipe传输的,但是这样做毫无意义。

放弃了scp后,我转向了直接使用ssh来执行远程命令,ssh执行远程命令要比scp更加通用,而不仅仅只是一个“文件传输”机制。废了大力之后,也算是小成功了,仍然是上述的机制,我的命令如下:

# 机器1:
mkfifo /tmp/tmp_fifo
./tunio </tmp/tmp_fifo |ssh [email protected] "cat >/tmp/tmp_fifo"
ifconfig tap0 10.10.10.10/24
# 机器2:
mkfifo /tmp/tmp_fifo
./tunio </tmp/tmp_fifo |ssh [email protected] "cat >/tmp/tmp_fifo"
ifconfig tap0 10.10.10.20/24

这个更加对称,比使用netcat/ncat的方式更加对称,更加优雅,但是效率去不咋地。抓包我就不给出了,大家自己品鉴吧。

本质

总的来讲,扯以上这些,我就是想说明一个观点,构建一个加密的隧道其实非常简单,方法更是多种多样,如果有人问你什么是XXN,你给他展示一下用Linux2.4内核ethertap模块+netcat+mcrypt或者快速手写一个tunio,然后配合ncat或者配合ssh构建一条加密隧道,那么你就算彻底理解了XXN的本质,如果你能快速让QQ和tunio这类程序对接,那你就是百折不扣的高手!当然,我达不到这样的水平,我只是指路人,我只是鼓手。当然,如果你真的拿netcat,ncat,ssh和tunio,ethertap这类对接了,很多学院派,科班生会觉得你这根本不是XXN,因为在他们眼里,XXN就是L2TP,PPTP,…,MPLS之流,你这自己搭建的,根本就什么都不是…其实我觉得这种学院派才是什么都不懂。

XXN在概念上满足两点即可,第一就是V(虚拟),第二是P(专用),这两点分别用Overlay和加解密技术完全可以完美表达。至于是L2TP,…等,它们只是成熟的标准之作罢了,但完全没有做到大道至简。

感谢老一辈程序员使我可以写就本文

这部分本想放在文章最前面的,但是怕喧宾夺主,所以移到了最后。

说实话,本文根本没有任何技术含量,但是绝对可以引发人们的思考。在我2006年刚刚参加工作的时候,我什么都不懂。但我跟一帮上世纪90年代以及21世纪前5年的程序员一起学到了很多的东西,比如用鼠标线上网,用串口联网,…他们说以前的时候,以太网卡和网线是稀缺的,反而PS/2,RS232是常见的,人们普遍都是使用物理单机,然后依靠软盘,刻录光盘,后来的U盘作为介质来传输数据,这就好比我之前说的用卡车运数据时一样的。

如果说我在一块没有接线网卡(假设它没有接线依然可以运行)上抓到了一个数据帧,然后把这个帧放到了一个U盘里,将U盘通过卡车,轮船运送到了外国,在外国有人将这个数据帧注入到了一个没有接线的网卡里,那么在协议栈看来,这个主机就跟从网线上收到了这个数据帧是一样的…所以说,网线只是个介质。

在2010年的时候,我第一次知道了TAP网卡这种东西,发现IP数据报文还可以通过一个字符设备读到用户态应用程序,进而被加密后通过socket传输出去,我觉得这太妙了!这难道不是跟用串口联网一样的道理吗?用串口和用socket唯一的区别就是,前者将原始IP数据写进了串口,而后者将原始IP数据写入socket,在应用程序看来,这没什么不同,都是写入了一个文件而已。之所以可以这么理解,得益于UNIX的两个传统,一个是IO的文件本质,一切皆文件,读写文件即可,第二个是分层的协议栈模型,这使得Overlay成为了可能!

好吧,我来总结一下,其实没什么好总结的,几乎都一样:

  • 串口联网:IP字符设备fd<—>APP<—>串口字符设备
  • OpenXXN:IP字符设备fd(tun/tap)<—>OpenXXN<—>socket
  • 本文范例:IP字符设备fd(tun/tap)<—>netcat/ncat/ssh<—>socket

大家都一样,最终大家都回归到了最开始的状态!中间的IPSec之流实乃昙花一现也。

鉴于此,我将我本文的简单返璞归真的方法自诩为另一种革命,承接于IPXXc,OpenXXN,BadXXN。如果你能玩的得心应手,就可以出神入化,随心所欲了,你的XXN数据将不再建立在协议的基础上,它甚至没有自己的协议,完全传输裸数据,因此也就没有任何的可以识别的Pattern。

不管是用USB联网,还是用串口联网,或者用任意APP上网,我们都要记住,这完全得益于一切皆文件以及分层协议栈的馈赠啊。你和想建立XXN连接的主机之间,无非有两种连接方式,第一种就是物理直连,即通过网线,串口线,USB线,无线等连接起来的,另一种就是逻辑连接,即虽然不是物理介质之间连接的,但却是可以通过TCP/IP协议栈连接在一起的,比如用一对socket就能互相通信。不管以上哪一类,XXN连接在乎的是只要能和对端保证TCP/IP可达即可。

推荐一本书,《全球城市史》,今天周六刚刚看完,虽然篇幅比较短小,但确实很好看,从这本书里思考,你可以看到我本文一样的观点。古代大家自给自足,都待在家里做工,后来大家集中式地先后走向工厂,走向写字楼,走向园区,如今随着交通和通讯技术越来越发达,大家再次回到了家里工作,同样都是在家工作,但层次却完全不同,可能在未来几十年,我们会再次回到男耕女织的生活,但是和古代的男耕女织却完全不同,古代的男耕女织是无组织的个体行为,而未来的男耕女织却是一个整体组织中的一环…以前,没有网线,网卡匮乏,大家用串口,USB,鼠标线等做Overlay,如今网络资源过剩,大家用TCP/IP做Overlay…网络技术在更高的层次上实现了回归。

想象一下大型机工作站时代,大家通过哑终端接入这些超级计算机,自己的终端可能离计算机很远,终端仅仅提供输入输出功能,根本没有计算功能,慢慢的计算资源逐渐移到了终端,这便开启了个人计算机时代,接下来,随着计算需求的增加,计算资源有一次集中了,这次不叫大型机了,这次叫云端,同样的,云计算也是在更高的层次实现了回归。

猜你喜欢

转载自blog.csdn.net/dog250/article/details/107641404