基于Libevent转发的内网端口暴露(一):基本实现

  在写这个程序的时候,报道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的小程序来实现这个功能的。现在看看,当时写的真是简单、幼稚啊。正好前面一段时间学了一下LibeventLibevent学习笔记(一):基本使用以及Libevent学习笔记(二):Memcached中Libevent和线程池使用初探、MySQL(3306)工作很好,SSH(22)终端可以工作,但是比较卡顿,而且控制字符异常,80端口用浏览器异常,尚未跟踪结果。

sshinner

1. 基本原理

  整个程序包括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。

2. 实现细节

2.1 线程池

  借鉴之前的Memcached的实现方式。
  Memcached比较简单,就是轮流的分配任务。本任务中由于通信的两端要依靠同一个数据结构,就应该将两端分配在同一个线程中,这里采用了一个简单的方式,将会话的mach-uuid进行hash映射取余映射分配到唯一的一个线程中,之前看了一篇文章,对于负载均衡分配任务方面,还是有不小的讲究的。那么要分配线程需要了解mach-uuid,需要mach-uuid必须客户端传递,所以listen套接字会接受一个数据包,分析得到mach-uuid之后,再将套接字侦听删除,并转移给对应的工作线程。
主线程和工作线程采用pipe管道的方式进行通信,而且管道的读取也是采用Libevent来进行事件驱动的哦!

2.2 数据转发

  数据转发的追求无非就是:完整、高效!
  操作接口采用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端了。

3. 测试

  DigitalOcean的服务器装的是Ubuntu 16.04,居然自己的程序编译不过,明明安装了json-c和Libevent的开发库。所以目前还是在本地测试的,如果有好心人在外网部署出错后,请赶紧AT我调试修正哦。
  不同程序的端口工作的效果大同小异,这里贴出工作的效果图吧!

  • 服务端启动(包含后面的数据转发消息)
    server
  • 客户端Daemon启动(接到连接后连接本地服务,创建Event)
    client daemon
  • 客户端Usr启动(启动时候打开本地监测端口)
    client usr
  • MySQL连接示例
    mysql

4. 后续工作(TODO)

  后面还有很多可以完善的方面,现在想到的有:

  • 实现登陆认证接口
      项目本身就有username/userid接口了,可以利用前面的readmeinfo项目的前端部分,实现用户注册、认证的形式,那么挂到VPS上面就可以服务广大群众了;
  • 加密数据传输
      如果是SSH的转发还好,因为SSH本身的数据就是加密的,即使这边明文传输也无所谓,但是对于其他未加密的数据还是有危险。之前在st_utils库里面已经调试了SSL加密传输的接口,可以移植过来使用,并且bufferevent本身也有SSL加密传输的接口。
  • 多线程模式
      用Chrome测试的时候,发现这货是多线程建立多个连接。要实现这个功能,就需要在USR端和DAEMON端同时模拟建立这些并发连接,用处不大,闲着可以写的玩玩。
  • 程序的稳定性
      比如DAEMON端和SRV端进行心跳机制,增加DAEMON的容错性和自动重连机制,DAEMON是你接触不到的主机,所以这个改进会很有价值!


  
  上面了。关于这个项目名字,本来是只想做ssh转发的,但是后面发现为啥不把它写的更通用一点呢?然后写是写通用了,但是项目名字就懒得改了,因为我也不知道取什么名字好,凑合着用吧!

本文完!