- 引 -
月黑风高,凌晨三点,屏幕报错:
- 起 -
由于某些原因,我需要从外网访问内网的 MySQL Server。
最直接的方式当然是在路由器配个端口映射,直接将 MySQL 端口暴露出来,但这样风险较大,可能有潜在的 MySQL 漏洞或者弱密码,导致被坑(是的,10 年前被坑过)。
所以对于这类需求我通常会绕一道:
先在网关服务器上搭建一个 shadowsocks server 用于转发:
$ gost -L ss://aes-256-cfb:123456@:8388
注:gost 命令详见 https://github.com/go-gost/gost/
然后在本地起一个 client 做将本地 3309 端口映射到目标服务器的 3306 端口:
$ gost -L tcp://127.0.0.1:3309/mysql.xxx.com:3306 \ -F ss://aes-256-cfb:123456@gateway.xxx.com:8388
最后用 MySQL Client 发起连接,于是就见到了开头的报错:
$ mysql -h 127.0.0.1 -P 3309 -uroot -p123456ERROR 2013 (HY000): Lost connection to server at 'handshake: reading initial communication packet', system error: 11
gost 这个工具我用了好多年,属于轻车熟路了,没想到还能翻车,属实奇怪,必须得查个明白。
- 查 -
首先使用排除法。
先排除链路的连通性问题:目标服务器上也有 Redis,用相同的套路转发了 6379 端口,本地的 Redis Client 可以正常 GET、SET。
$ gost -L tcp://127.0.0.1:6379/mysql.xxx.com:6379 \ -F ss://aes-256-cfb:123456@gateway.xxx.com:8388
然后再使用 socks5 作为网关代理:
# @Gateway$ gost -L socks5://user:pass@:1080
# @Client$ gost -L tcp://127.0.0.1:3309/mysql.xxx.com:3306 \ -F socks5://user:pass@gateway.xxx.com:1080
—— 也能够正常转发。
于是问题就被缩小到了「MySQL协议 + 使用 Shadowsocks 协议转发」这个组合身上了。
然后使用 strace 搞一把:
$ strace mysql -h 127.0.0.1 -P 3309 -uroot -p123456
可以看到,MySQL Client 在创建连接以后,就调用 recv 等待服务端的推送。
MySQL 的官方文档是这么说的:
The initial handshake starts with the server sending the Protocol::Handshake packet.
https://dev.mysql.com/doc/dev/mysql-server/9.1.0/page_protocol_connection_phase.html
符合预期。
然后再看一下 gost 的 log:
- 本地的 gost 确实收到了请求
- 但是 gateway 上的 gost shadowsocks server 却没有收到请求。
也就是说,问题已经被初步定位了:在使用 shadowsocks 协议作为转发代理时,如果客户端不发送消息,实际上并没有和 shadowsocks server 真正建立连接,因此自然无法收到 MySQL Server 的响应(handshake packet)。
那么该如何解决该问题呢?——逃げるは恥だが役に立つ。改用 socks5 转发,立竿见影。
本文完。
socks5 协议虽然不存在上述问题,但其标准实现中,用户名和密码默认是明文传输的,也是个坑。gost 某个较早的版本扩展了一个 tls-auth 方法,一定程度上解决了这个问题,但并不保险 —— 要是遇到个神人用了个早期版本,依然会坑。
这就是一根筋变成两头堵了啊,所以我决定还是得继续磕这个问题。
- 解 -
兵分两路,饱和式救援:一方面给 gost 提 issue(#680),一方面自己也修改代码尝试解决这个问题。
在 gost 加了个几个断点,找到了 foward.go 155 行(就是前面日志截图的位置):
log.Logf("[tcp] %s <-> %s", conn.RemoteAddr(), addr)transport(conn, cc)log.Logf("[tcp] %s >-< %s", conn.RemoteAddr(), addr)
既然客户端不主动发包,我们只要稍稍越俎代庖一下,主动发点啥给服务端,gost 就不得不和 shadowsocks server 建立连接了。但是如果发的报文不符合 MySQL 规范,又会导致服务端报错;以及如果要用这个方案解决其他协议,那就更麻烦了。
不过好在 TCP 协议是允许发送“空报文”的,只发送一个 tcp 头,但是数据为 0 字节:
实测管用。
正准备整理整理,完善下方案(加个参数),给 gost 提个 fix 混个 contributor,issue #680 也收到了 gost 作者 ginuerzh 大佬的回复:
试试加上nodelay参数:https://gost.run/tutorials/protocols/ss/#shadowsocks_1
关联:#319 (comment), #439
https://github.com/go-gost/gost/issues/680
gost 文档的 shadowsocks 章节开头写着:
默认情况下shadowsocks协议会等待请求数据,当收到请求数据后会把协议头部信息与请求数据一起发给服务端。当客户端nodelay选项设为true后,协议头部信息会立即发给服务端,不再等待用户的请求数据。当通过代理连接的服务端会主动发送数据给客户端时(例如FTP,VNC,MySQL)需要开启此选项,以免造成连接异常。
https://gost.run/tutorials/protocols/ss/#shadowsocks_1
- 完 -
这个故事的教训:RTFM —— Return To the F*cking Manual。