[Golang]通过TCP代理实现防火墙穿透

背景

最近在弄Tensorflow[1],想自己找开源项目自己跑一下模型实现目标检测,但是没有钱去租服务器的,只好自己买个显卡装到家里电脑来训练。在跑模型的时候,需要访问tensorboard看看当前的状态。由于家里是动态IP上网的,在外面无法直接访问到家里的tensorboard服务,于是我用Golang实现了一个TCP代理工具(项目名:bridge),把家里tensorboard服务端口的数据转发到服务器上。

完整项目代码可以在 https://github.com/juxuny/bridge 里找到

自定义通信协议

开始标记或结束标记:FlagStart=0xE0转义标记:FlagEsc = 0xF0转义后的开始(结束)标记:FlagEscStart = 0xEF转义后的转义标记:FlagEscEsc = 0xFF

协议定义如下:

[开始标记(1)| 指令(2)|数据长度(4)|数据(n)|结束标记(1)]

Note:其中小括号后面表示字节数

数据默认都采用大端模式[2]

来自百度百科:下面以unsigned int value = 0x为例,分别看看在两种字节序下其存储情况,我们可以用unsigned char buf[4]来表示valueBig-Endian: 低地址存放高位,如下:高地址  ---------------  buf[3] (0x78) -- 低位  buf[2] (0x56)  buf[1] (0x34)  buf[0] (0x12) -- 高位  ---------------

协议指令(CMD)定义如下:

Auth: 认证连接。客户端连接上之后,首先要把自己的token发送到服务器,服务器通过配置文件获取到这个token所对应的端口,验证通过之后服务端开始监听对应的端口。(整个工作流程会在后面详细说明)Data: 数据转发包Msg: 文本消息包,收到这个指令就把数据字段转换成string显示到terminalConnect: 发送连接请求Close: 断开连接

以发送认证包为例子:

cmd=1: [0x00 0x01]token: [0x0A 0x0A 0x0A 0x0A]length = 4: [0x00 0x00 0x00 0x04]

所以最后打包发送的字节数组为:

[0xE0 0x00 0x01 0x00 0x00 0x00 0x04 0x0A 0x0A 0x0A 0x0A 0xE0]

接收端是按先后顺序读取的,遇到第一个0xE0之前,都是无效数据,可以直接过滤掉。通过循环把主要部分截取出来:0x00 0x01 0x00 0x00 0x00 0x04 0x0A 0x0A 0x0A 0x0A。根据前面的协议定义,前面两个字节是cmd,后面4个字节是length,剩下的就是数据下文部分,即[0x0A 0x0A 0x0A 0x0A]就是下文。

另外,前面还提到了一个转义字符,主要用来解决起始标记冲突问题。下面举另一个例子:

token 改为 [0x0A 0x0A 0xE0 0x0A]

其它字段都不变,按协议打包之后得到以下字节数组:

[0xE0 0x00 0x01 0x00 0x00 0x00 0x04 0x0A 0x0A 0xE0 0x0A 0xE0]

接收端读取数据的时候,读到第一个 0xE0 开始解释数据,第二个 0xE0 就结束,则截取到下面这个数组:

[0xE0 0x00 0x01 0x00 0x00 0x00 0x04 0x0A 0x0A 0xE0]

缓冲里还会剩下 [0x0A 0xE0]

按照截取到的数组解释得到token = [0x0A 0x0A],而length明明是4,所以解释就错误了。或者你又可能说直接根据length来读取指定长度的数据就好,但是如果length刚好也是 0xE0呢?数据包就是这样子的:[0xE0 0x00 0x01 0x00 0x00 0x00 0xE0 0x0A ...... 0x0A 0xE0 0x0A 0xE0]。

截取出来的数据是这样子的:[0x00 0x01 0x00 0x00 0x00 ](还是解释错误)

所以这里就需要用转义字符,把特殊符号替换掉。

0xE0 => 0xF0 0xEF0xF0 => 0xF0 0xFF

回到上面说的例子,token = [0x0A 0x0A 0xE0 0x0A],打包出来的数据应该是:

[0xE0 0x00 0x01 0x00 0x00 0x00 0x04 0x0A 0x0A 0xF0 0xEF 0x0A 0xE0]

接收端就遇到0xF0字符的时候就按下面规则转换:

0xF0 0xEF => 0xE0 0xF0 0xFF => 0xF0

这就可以解释出正确的数据包:[0x00 0x01 0x00 0x00 0x00 0x04 0x0A 0x0A 0xE0 0x0A]

整体设计

主要组成部分分别为:

bridge-server:

slave manager: bridge-client也叫做slave,这里为了区分好bridge-client 和 web server,所以代码里就写成了slaveManagerconnection manager: listener manager:监听客户端连接,所有connection都交给connection manager处理token manager:根据bridge-client提交的token获取对应的main listener:默认监听9090等待bridge-client连接,连接成功之后把connection交给connection manager管理

bridge-client:

connection managermain connection

下面用个图片来展示一下各组件之间如何工作的,其中图2为系统简化模型。

图1图2

具体实现

上面已经提到了共有5类数据包发送,分别为:CmdAuth, CmdData, CmdMsg, CmdConnect, CmdClose。不同的指令,对应的Data部分也是有不同含意的:

CmdAuth(请求认证):

字段:

token: 传给bridge-server然后通过配置文件获取要代理的端口

假设token为[0x0A 0x0A 0x0A 0x0A],要发送的数据为应该:

[0xE0 0x00 0x01 0x00 0x00 0x00 0x04 0x0A 0x0A 0x0A 0x0A 0xE0]

CmdData(数据包):

字段:

fromAddress: 客户端地址(IP+port),8字节toAddress: 目标地址(保留),8字节data: 二进制数据,根据具体数据长度定义字节数

由于toAddress字段保留,这里可以直接传 [0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00],表示地址 0.0.0.0:0

例如:[0xE0 0x00 0x02 0x00 0x00 0x00 0x14 0x0A 0x00 0x00 0x01 0x00 0x00 0x03 0xE8 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x01 0x02 0x03 0x04 0xE0]

解包得到:

cmd=2, length = 20, fromAddress = 10.0.0.1:1000, toAddress = 0.0.0.0:0, data = [0x01 0x02 0x03 0x04]

CmdMsg(消息提示):

字段:

msg: 消息文本,(n字节长)

例如:[0xE0 0x00 0x03 0x00 0x00 0x00 0x07 0x73 0x75 0x63 0x63 0x65 0x73 0x73 0xE0]

解包得到:

cmd = 3, length = 7, msg = [0x73 0x75 0x63 0x63 0x65 0x73 0x73],转换为string类型得到文本:"success"

CmdConnect(创建连接):

fromAddress: 客户端地址(IP+port),8字节

例如:[0xE0 0x00 0x04 0x00 0x00 0x00 0x08 0x0A 0x00 0x00 0x01 0x00 0x00 0x03 0xE8 0xE0]

CmdClose(关闭连接):

fromAddress: 客户端地址(IP+port),8字节

例如:[0xE0 0x00 0x05 0x00 0x00 0x00 0x08 0x0A 0x00 0x00 0x01 0x00 0x00 0x03 0xE8 0xE0]

指令常量定义如下:

const ( FlagStart= 0xE0 FlagEscStart = 0xEF FlagEsc= 0xF0 FlagEscEsc = 0xFF )

为了更方便地解释数据包,并且一个数据包一个数据读取,特意定义了一个DataReader处理bridge-server 与 bridge-client 之间的TCP通信。

type DataReader struct { conn net.Conn buffer *bytes.Buffer } func NewDataReader(conn net.Conn) (ret *DataReader) { ret = new(DataReader) ret.conn = conn ret.buffer = bytes.NewBuffer(nil) return } func (t *DataReader) ReadOne() (ret Data, isEnd bool, e error) { startCount := 0 block := make([]byte, blockSize) retBuf := bytes.NewBuffer(nil) var n int for startCount < 2 { if t.buffer.Len() == 0 { n, e = t.conn.Read(block) if e != nil { isEnd = true return } t.buffer.Write(block[:n]) } //debug("buffer data:", t.buffer.Len(), t.buffer.Bytes()) b, err := t.buffer.ReadByte() if err != nil { continue } if b == FlagStart { startCount += 1 continue } retBuf.WriteByte(b) } //debug("finished:", retBuf.Bytes()) e = ret.Unpack(retBuf.Bytes()) debug(ret) return }

conn 是连接bridge-server的TCP连接

buffer表示网络数据缓存,为空的时候就从connection里读一块数据

每次读取数据包的时候startCount都重新计数,大于等于2的时候说明已经读到了第二个开始标记了,表示已经获取了一个完整的数据包到retBuf,使用Data的Unpack方法就可以解释这个数据包。Data结构定义如下:

type Data struct { Cmdint16 Data []byte } func (t *Data) Pack() (ret []byte, e error) { length := len(t.Data) ret = make([]byte, length+2+4) //cmd ret[0] |= byte(t.Cmd >> 8) ret[1] |= byte(t.Cmd) //length ret[2] |= byte(length >> 24) ret[3] |= byte(length >> 16) ret[4] |= byte(length >> 8) ret[5] |= byte(length) //copy data tmp := ret[2+4:] copy(tmp, t.Data) return } func (t *Data) Unpack(data []byte) (e error) { in := bytes.NewBuffer(data) out := bytes.NewBuffer(nil) for b, err := in.ReadByte(); err == nil; b, err = in.ReadByte() { if b == FlagEsc { b, err = in.ReadByte() if err != nil { break } if b == FlagEscStart { out.WriteByte(FlagStart) } else if b == FlagEscEsc { out.WriteByte(FlagEsc) } } else { out.WriteByte(b) } } data = out.Bytes() //cmd t.Cmd = 0 t.Cmd |= int16(data[0] << 8) t.Cmd |= int16(data[1]) //length length := 0 length |= int(data[2]) << 24 length |= int(data[3]) << 16 length |= int(data[4]) << 8 length |= int(data[5]) if out.Len() - 6 != length { return fmt.Errorf("invalid package, get data: %d, read length: %d", out.Len(), length) } t.Data = data[6:] return } //把打包后的字节流通过TCP socket发送出去 func (t *Data) Write(out io.Writer) (e error) { data, e := t.Pack() if e != nil { return } buffer := bytes.NewBuffer(nil) buffer.WriteByte(FlagStart) for i := 0; i < len(data); i++ { if data[i] == FlagStart { buffer.WriteByte(FlagEsc) buffer.WriteByte(FlagEscStart) } else if data[i] == FlagEsc { buffer.WriteByte(FlagEsc) buffer.WriteByte(FlagEscEsc) } else { buffer.WriteByte(data[i]) } } buffer.WriteByte(FlagStart) debug("send data:", len(buffer.Bytes()), buffer.Bytes()) _, e = out.Write(buffer.Bytes()) return }

由于在代理的时间,浏览器可能会创建多个连接到bridge-server,最后bridge-client也会创建相同数量的连接到指定的Web Server。为了更方便管理各个连接,这里创建了ConnManager

type ConnManager struct { connSet sync.Map } func (t *ConnManager) GetConn(addr string) (conn net.Conn, ok bool) { v, ok := t.connSet.Load(addr) if ok { conn, ok = v.(net.Conn) } return } func (t *ConnManager) Close(addr string) { conn, ok := t.GetConn(addr) if !ok { return } conn.Close() t.connSet.Delete(addr) } func (t *ConnManager) AddConn(addr string, conn net.Conn) { t.connSet.Store(addr, conn) } func (t *ConnManager) SendData(addr string, data []byte) (disconnected bool, err error) { conn, ok := t.GetConn(addr) if !ok { err = fmt.Errorf("conn not found: %s", addr) return } _, err = conn.Write(data) if err != nil { disconnected = true } return }

实质上,ConnManager就是给远程地址和TCP socket做了一个映射,让bridge-server或者bridge-client可以通过远程地址查询对应的socket。当浏览器连接到bridge-sever的时候,假设连接地址是10.0.0.1:50001,bridge-sever可以通过下面代码获取到对应的地址:

addr := conn.RemoteAddr() //var conn net.Conn

ConnManager就负责把addrconn绑定,所以bridge-server可以通过ConnManager给指定客户返回数据,bridge-client也可以通过ConnManager把数据转发给最终的Web Server。

bridge-server为每个bridge-client创建了一个listener,用于接收Browser连接,每个token对应一个port,listenerManager把port与listener绑定,bridge-client断开连接的时候,可以通过对应的port清理listener,例如调用Remove方法。listenerManager定义如下:

type listenerManager struct { lnSet sync.Map } func (t *listenerManager) GetListener(port int) (l net.Listener, ok bool) { v, ok := t.lnSet.Load(port) if ok { l, ok = v.(net.Listener) return } return } func (t *listenerManager) Add(port int, listener net.Listener) (err error) { _, found := t.GetListener(port) if found { err = fmt.Errorf("listener exists, listening port: %d", port) return } t.lnSet.Store(port, listener) return } func (t *listenerManager) Remove(port int) (err error) { debug("remove listener, port:", port) l, found := t.GetListener(port) if found { t.lnSet.Delete(port) err = l.Close() return } return }

另外还需要一个slaveManager管理所有bridge-client:

type slaveManager struct { set sync.Map mutex *sync.Mutex } func newSlaveManager() (ret *slaveManager) { ret = new(slaveManager) ret.mutex = new(sync.Mutex) return } func (t *slaveManager) GetConn(port int) (conn net.Conn, ok bool) { var v interface{} v, ok = t.set.Load(port) if !ok { return } conn, ok = v.(net.Conn) return } func (t *slaveManager) Bind(port int, conn net.Conn) { t.set.Store(port, conn) } func (t *slaveManager) Unbind(port int) { conn, ok := t.GetConn(port) if ok { _ = conn.Close() } t.set.Delete(port) } func (t *slaveManager) sendMsg(port int, msg string) (disconnected bool, e error) { conn, ok := t.GetConn(port) if !ok { disconnected = true e = fmt.Errorf("connection not found") return } m := newMsg(msg) e = m.Write(conn) return } func (t *slaveManager) sendConnect(port int, addr string) (disconnected bool, e error) { conn, ok := t.GetConn(port) if !ok { disconnected = true e = fmt.Errorf("slave not found") return } d := Data{Cmd: CmdConnect} d.Data, e = hostToBytes(addr) if e != nil { return } e = d.Write(conn) if e != nil { disconnected = true } return } func (t *slaveManager) sendClose(port int, addr string) (disconnected bool, e error) { disconnected = false conn, found := t.GetConn(port) if !found { disconnected = true e = fmt.Errorf("slave not found, listen port: %s", port) return } d := Data{} d.Cmd = CmdClose d.Data, e = hostToBytes(addr) if e != nil { return } e = d.Write(conn) if e != nil { disconnected = true } return } func (t *slaveManager) WriteData(port int, fromAddr, toAddr string, data []byte) (disconnected bool, e error) { conn, ok := t.GetConn(port) if !ok { disconnected = true e = fmt.Errorf("connection not found") return } fromBytes, e := hostToBytes(fromAddr) if e != nil { return } toBytes, e := hostToBytes(toAddr) if e != nil { return } debug("slaveManager: write data, from:", fromBytes, " to:", toBytes, " data:", data) d := Data{} d.Cmd = CmdData d.Data = make([]byte, len(fromBytes)+len(toBytes)+len(data)) k := 0 for i := 0; i < len(fromBytes); i++ { d.Data[k] = fromBytes[i] k++ } for i := 0; i < len(toBytes); i++ { d.Data[k] = toBytes[i] k++ } for i := 0; i < len(data); i++ { d.Data[k] = data[i] k++ } e = d.Write(conn) return }

其中sendMsg, sendConnect, sendClose, WriteData等方法用于对指定的bridge-client发送不同的数据包。因为每个bridge-client都会有一个特定的token,一个token和bridge-server 监听的端口唯一对应,所以slaveManager可以通过port参数找到对应的bridge-client,然后进行发数据。

最后bridge-server通过一个struct, Server,整合各个manager struct,包括:ConnManager, listenerManager, TokenManager(这个可以参考源码里的定义),slaveManager:

type Server struct { slaves *slaveManager tokenManager *TokenManager config ServerConfig ln net.Listener connMgr *ConnManager lnMgr *listenerManager } func NewServer(c ServerConfig) (ret *Server) { // do something ... return } func (t *Server) Start() { var e error t.ln, e = net.Listen("tcp", ":"+fmt.Sprint(t.config.Port)) if e != nil { panic(e) } _, _ = log("listen on :", t.config.Port) for { conn, e := t.ln.Accept() if e != nil { fmt.Println(e) continue } debug("accept from:", conn.RemoteAddr()) go t.checkAuthorization(conn) } } func (t *Server) bindPort(port int, conn net.Conn) { t.slaves.Bind(port, conn) } func (t *Server) unbindPort(port int) { t.slaves.Unbind(port) } func (t *Server) checkAuthorization(conn net.Conn) { //do something ... } mh func (t *Server) sendMsg(port int, msg string) (disconnected bool, e error){ //do something ... return } func (t *Server) handleMsg(d Data) { log(string(d.Data)) } func (t *Server) handleClose(d Data) { //do something ... } func (t *Server) handleData(d Data) { //do something ... } func (t *Server) handle(d Data) { //do something ... func (t *Server) serveConn(port int, conn net.Conn) { //do something ... } func (t *Server) serveSlave(port int, reader *DataReader) { // do something... }

由于篇幅太长,Server struct的源码只能到github上看了,点这里:server.go

安装与配置

安装go(略)go get -u github.com/juxuny/bridgego install github.com/juxuny/bridge/cmd/bridge-servergo install github.com/juxuny/bridge/cmd/bridge-client如果$GOPATH/bin已经加到了PATH变量里的话,可以直接执行bridge-server和bridge-client命令了,否则还需要:export PATH=$PATH:$GOPATH/bin

假设外网服务器IP为10.0.0.1,bridge-server监听端口是9090,家里外网的动态IP为10.0.0.x,家里运行的bridge-client的设备是192.168.0.1,内部提供Web 服务的设备是192.168.0.2,网页端口是80。现在要将10.0.0.1:10001的数据转发到家里内部网络的192.168.0.2:8888上。

server.json:

{ "port": 9090, "tokenConf": "token.conf" }

token.conf:

#token|aes-key | port nooF2nXPawgx1LUsKTnrtvdlxY2OZgN3sOuv80kduWpVZOl66uSehv9CGnoL8CH5 uNH8RGSW86rRt85b 10001

client.json:

{ "token": "nooF2nXPawgx1LUsKTnrtvdlxY2OZgN3sOuv80kduWpVZOl66uSehv9CGnoL8CH5", "key": "uNH8RGSW86rRt85b", "host": "10.0.0.1:9090", "local": "192.168.0.2:8888" }

Note:由于每次传输都执行aes加密解密很影响性能,后来把加密代码去掉了

在10.0.0.1服务器上执行:

bridge-server -c=server.json

在192.168.0.1设备上执行:

bridge-client -c=client.json

性能测试

自定义Web Server,直接获取某个json数据,浏览器访问得到如下结果:

不使用代理访问使用代理访问

根据上面的安装配置,8888端口表示源服务,10001端口是bridge-server,使用ab压力测试得到如下结果:

(1)测试100次请求,执行命令:

ab -n 100 -c 100 :8888/Index/TestSum

输出结果:

Server Software: Server Hostname:127.0.0.1 Server Port:8888 Document Path:/Index/TestSum Document Length:10 bytes Concurrency Level:100 Time taken for tests: 0.053 seconds Complete requests:100 Failed requests:0 Total transferred:22400 bytes HTML transferred: 1000 bytes Requests per second:1888.25 [#/sec] (mean) Time per request: 52.959 [ms] (mean) Time per request: 0.530 [ms] (mean, across all concurrent requests) Transfer rate:413.06 [Kbytes/sec] received Connection Times (ms) minmean[+/-sd] median max Connect:33 0.43 4 Processing: 3 3811.2 4448 Waiting:3 3711.5 4448 Total:7 4111.2 4752

通过代理访问:

ab -n 100 -c 100 :10001/Index/TestSum

输出结果:

Server Software: Server Hostname:127.0.0.1 Server Port:10001 Document Path:/Index/TestSum Document Length:10 bytes Concurrency Level:100 Time taken for tests: 0.931 seconds Complete requests:100 Failed requests:0 Total transferred:22400 bytes HTML transferred: 1000 bytes Requests per second:107.40 [#/sec] (mean) Time per request: 931.130 [ms] (mean) Time per request: 9.311 [ms] (mean, across all concurrent requests) Transfer rate:23.49 [Kbytes/sec] received Connection Times (ms) minmean[+/-sd] median max Connect: 14 17 1.0 1718 Processing:37491 256.1522 901 Waiting: 37489 256.1521 901 Total: 54507 255.6540 915

由上面的压力测试结果可以得出,不使用代理性能比使用代理快17倍有多(),还有很多优化的空间,但终究实现了穿透防火墙功能。

参考

^Tensorflow ^大端模式