介绍
本项目是一个基于xindong/frontd
重制的通用网关程序。
本网关只有TCP流量转发功能,负责为每个客户端连接建立一个后端连接进行流量转发。
本网关有以下用途:
- 避免应用服务器直接暴露到公网
- 提高故障转移的效率
本网关有以下特性:
- 易接入,只需要对客户端做小量修改即可接入,不需要修改已又通讯协议
- 可扩展,可以任意多开水平扩展以实现负载均衡和高可
- 零配置,运维人员无需手工进行后端服务器列表配置
- 端口重用,利用高版本Linux内核的
reuseport
机制,可以开多个网关进程守候同一个端口,以提高多核利用率
协议
客户端连接网关后,发送一行base64
编码过的服务器地址密文到网关,并等待网关回发状态码。
可能收到的状态码如下:
状态码 | 状态说明 |
---|---|
200 | 握手完成,可以开始传输数据 |
400 | 请求数据读取过程中发生错误 |
401 | 网关解密地址信息失败 |
502 | 网关无法连接后端服务器 |
504 | 网关连接后端服务器超时 |
客户端收到成功状态后,即可开始和目标服务器进行通讯了。
基本通信流程:
- 客户端连接网关
- 客户端发送目标服务器地址密文
- 如果读取失败,回发
400
状态码给客户端
- 如果读取失败,回发
- 网关解密目标服务器地址
- 如果解密失败,回发
401
状态码给客户端
- 如果解密失败,回发
- 网关连接目标服务器
- 如果发生错误,回发
502
状态码给客户端 - 如果发生超时,回发
504
状态码给客户端
- 如果发生错误,回发
- 网关回发成功状态码
200
给客户端 - 网关发送缓存中残余数据给目标服务器
- 客户端和目标服务器之间开始对传数据
加密
客户端发送到网关的目标服务器地址使用AES256-CBC
加密并进行base64
编码,密文以换行符结尾。
示例:
U2FsdGVkX19KIJ9OQJKT/yHGMrS+5SsBAAjetomptQ0=\n
进行加密目的是让外网攻击者无法对网关后的内网服务器进行猜测和任意连接。
接入流程:
- 生成
Secret
,并保存在安全的文档中 - 使用上述
Secret
部署网关 - 使用
AES
算法加密文本格式的后端地址,生成base64
编码的密文- 可以在线生成:http://tool.oschina.net/encrypt
- 也可以使用
openssl
命令生成,如:
echo -n "127.0.0.1:62863" | openssl enc -e -aes-256-cbc -a -salt -k "p0S8rX680*48"
- 举例,当后端地址为
127.0.0.1:62863
并且Secret
为p0S8rX680*48
时,密文结果应类似:
注:上述方式都会使用随机Salt,这也是建议的方式。其结果是每次加密得出的密文结果并不一样,但并不会影响解密U2FsdGVkX19KIJ9OQJKT/yHGMrS+5SsBAAjetomptQ0=
加密后的服务器地址通常是在拉取服务器列表的场景中发送给客户端,客户端只会有加密后的地址,不应该有Secret
或服务器明文地址。
重要的事情说三遍:
- 切勿将
Secret
写入客户端代码! - 切勿将
Secret
写入客户端代码! - 切勿将
Secret
写入客户端代码!
设置
网关可以通过以下命令行参数进行设置:
变量 | 用途 |
---|---|
secret |
解密地址用的秘钥,必须设置 |
addr |
网关服务器地址,默认为0.0.0.0:0 |
reuse |
是否启用端口重用特性,值为1时表示启用,默认为0 |
pprof |
net/http/pprof 所使用的地址,建议是内网地址,无值的时候不开启,默认无值 |
retry |
网关连接目标服务器的重试次数,默认为1 |
timeout |
网关每次连接目标服务器的超时时间,单位是秒,默认为3 |
buffer |
用来进行io.CopyBuffer 的缓冲大小,只对Go 1.5以上版本有效 |
网关启动后,会在工作目录下生成一个gateway.pid
文件记录进程id,可以用以下命令安全退出网关:
kill `cat gateway.pid`
附录
零拷贝技术
因为此网关程序的唯一功能就是转发流量,所以如果可以利用Linux平台的“零拷贝”技术,无疑可以显著的提升网关的效率。
当前Go内置的io.Copy
最终会调用到net.TCPConn.ReadFrom()
,在Linux平台上net.TCPConn.ReadFrom()
会尝试对os.File
使用sendfile()
系统调用。
但如果传入的是一个net.Conn
,最终会回退成普通的读取发送模式,因为sendfile()
只支持文件对socket的调用形式,所以目前无法通过io.Copy()
做到零拷贝。
要做到socket对socket的零拷贝,需要用到splice
系统调用,但因为Go的CSP
模型决定了使用splice
必须和内置的netpoll
交互,否则Goroutine会因为IO阻塞独占调度线程。
已经有开发者给Go提交了一个补丁,对net.TCPConn.ReadFrom()
做了改进,当发现来源是一个socket时,内部转为splice
调用,但这个补丁还未被合并。
相关链接:
经过实际测试,这个补丁中的fSpliceMore
标记要去掉,否则会导致socket像启用了TCP_CORK选项一样拥塞发送,对于收发小消息的游戏网关来说,会导致持续性的200ms延迟。
更多关于零拷贝技术的信息可以参考这篇文章:
以下是针对Linux系统制作的epoll + splice版本的网关程序(仅提供测试学习之用):
TODO
- 连接存活检查
- 提供客户端接入代码
- 运行状况统计
- 支持consul