在写这个程序的时候,报道TeamViewer的服务器被攻陷,黑客借此操控用户的电脑以盗取用户的数据,乃至操控用户的资金账户等敏感信息,然后TeamViewer官方出来辟谣,说是用户自己的弱口令导致的安全漏洞。怎么说呢,TeamViewer用起来确实很方便,而且几乎是全平台支持,不过我不满意的是Linux平台貌似是Wine的,安装的时候还需要安装一大堆的32位的依赖库,作为Gentoo的洁癖佬让我对此无法忍受,所以必要的时候,我愿意用KVM启动一个Windows的虚拟机来应急;而来在Linux上宣称图形界面,浪费带宽,暴殄天物啊!
内网主机要可以被外网访问,通常实现的路径有:(1)配置内网出口的端口映射表,让内网特定主机的端口暴露出来;(2)在外网主机架设VPN,然后需要通信的主机连接VPN后就在同一个网段了;(3)借助TeamViewer这类的方式,用外网的主机进行数据中转。在现实使用中,(1)比较的难办,除非你在公司很牛逼,或者和网管有非一般的亲密关系才行;(2)用的是比较多的,很多企业WFH(Work From Home)就是这么办的,但是一旦连上VPN,就是一个可信的网段,对于多用户共享十分不安全;于是,(3)这种方式算是最经济最高效的实现方式了。
其实自己之前写了一个local_forward的小程序来实现这个功能的。现在看看,当时写的真是简单、幼稚啊。正好前面一段时间学了一下Libevent( Libevent学习笔记(一):基本使用 , Libevent学习笔记(二):Memcached中Libevent和线程池使用初探 ),这家伙的高效是出了名的,不仅封装了IO复用的细节,而且封装了bufferevent/evbuffer这类数据接口和操作接口,对于网络数据转发正是其用武之地了,于是就谋划着用Libevent这个库写一个通用一点的端口转发框架程序。今天基本完成了,测试FTP(21)、MySQL(3306)工作很好,SSH(22)终端可以工作,但是比较卡顿,而且控制字符异常,80端口用浏览器异常,尚未跟踪结果。
整个程序包括SRV/CLT_DAEMON/CLT_USR三个角色,代码由server/client两个部分,配置文件为运行目录下的settings.json,下面模拟场景来说明吧。
(1) 要下班了,首先在远程服务器192.3.90.76端运行SRV,其读取本地的settings.json文件,决定自己监听在8900端口;
(2) 把公司内网的电脑启动client -D作为为CLT_DAEMON角色,程序读取settings.json,发现服务器地址为192.3.90.76:8900,读取配置文件中的username、userid,以及本机的mach-uuid信息,连接服务器并发送这些信息;
(3) 服务器接收到该请求,然后读取数据头和数据体,解析后将该请求添加到某个线程的处理队列,并向该线程发送通知信息;处理线程被激活后检查该会话是否存在,然后建立相应的数据结构和事件侦听;然后向CLT_DAEMON发送OK确认信息;
(4) CLT_DAEMON接收到服务器确认消息后,就处于等待SRV数据/命令的状态;
(5) 你吃过饭回家了,想连一下公司的电脑,这时候启动client程序作为CLT_USR,这时候电脑萌逼了,我怎么知道你要跟哪台电脑通信呢?所以你在公司启动的时候,会打印出mach-uuid,你需要把这个记录下来,写到本地的配置文件中再启动;
(6) CLT_USR带着要会话的mach-uuid连接到服务器。服务器会检查这个mach-uuid是否已经就绪了,如果就绪了就分配到对应的线程,创建bufferevent侦听事件,于是就行成了USR/DAEMON端都被侦听的双工通信管道;接着工作线程向CLT_USR发送OK确认;
(7) CLT_USR接下来会对每个本地感兴趣的端口都建立listen侦听事件了,然后就默默的“看着你”——你想要做甚?
(8) 此时的你华丽丽地带端口运行FTP/MySQL/SSH,就会触发USR端的listen事件,在这个时间中会对你连接的套接字添加读事件侦听,同时沿着USR->SRV->DAEMON端发送一个特殊的控制帧’T’,触发DAEMON端连接本地的服务,并创建读事件侦听;
(9) Enjoy yourself。
借鉴之前的Memcached的实现方式。
Memcached比较简单,就是轮流的分配任务。本任务中由于通信的两端要依靠同一个数据结构,就应该将两端分配在同一个线程中,这里采用了一个简单的方式,将会话的mach-uuid进行hash映射取余映射分配到唯一的一个线程中,之前看了一篇文章,对于负载均衡分配任务方面,还是有不小的讲究的。那么要分配线程需要了解mach-uuid,需要mach-uuid必须客户端传递,所以listen套接字会接受一个数据包,分析得到mach-uuid之后,再将套接字侦听删除,并转移给对应的工作线程。
主线程和工作线程采用pipe管道的方式进行通信,而且管道的读取也是采用Libevent来进行事件驱动的哦!
数据转发的追求无非就是:完整、高效!
操作接口采用bufferevent_read和bufferevent_write,比较的简单。这里将数据从读到用户空间,然后再写入传输,实际是低效的,但是我需要分析数据包头,得知消息负载的长度等信息,使用这些函数可以精准控制读取长度,bufferevent_read_buffer、bufferevent_write_buffer这些函数虽然更高效,但是没法控制数据包的长度。
数据完整性,在传输之前会计算负载的CRC32并记录在帧头部,然后接收到的数据包会计算CRC32并比较,如果错误就丢弃数据。这里没有对数据头进行校验,而且也没有校验失败重传机制……
Libevent传输的最大数据包长度是4096,所以这里每次bufferevent_write的最大长度也是头部+负载=4096。
在每隔数据包的头部,会记录该数据的daemonport和userport,然后CLT就知道这些包应该被转送给哪些本地的应用程序了。
这里还需要注意的是,如上面(8)描述的,ssh在三次握手连接之后,是client首先发数据的,所以之前的设计是,在DAEMON接收到USR数据后如果没有连接本地程序,就连接本地程序,然后将套接字添加侦听事件;后来测试MySQL发现没有反应,wireshark抓包发现,在建立连接之后,是服务器先发送信息,然后客户端再发送登陆数据。于是后面的流程改为:一旦USR发现有请求,那么建立连接和事件侦听后,强行向DAEMON端发送一个Trigger事件,DAEMON在接收到这个控制帧后,立即和本地程序请求连接,建立事件侦听,这时候即使服务端先有数据发送,也可以及时传递到USR端了。
DigitalOcean的服务器装的是Ubuntu 16.04,居然自己的程序编译不过,明明安装了json-c和Libevent的开发库。所以目前还是在本地测试的,如果有好心人在外网部署出错后,请赶紧AT我调试修正哦。不同程序的端口工作的效果大同小异,这里贴出工作的效果图吧!
后面还有很多可以完善的方面,现在想到的有:
老规矩,项目的代码放到了 Github sshinner 上面了。关于这个项目名字,本来是只想做ssh转发的,但是后面发现为啥不把它写的更通用一点呢?然后写是写通用了,但是项目名字就懒得改了,因为我也不知道取什么名字好,凑合着用吧!
本文完!