记一次打通连接 MySQL Server 堵点的组合拳

- 引 -

月黑风高,凌晨三点,屏幕报错:

- 起 -

由于某些原因,我需要从外网访问内网的 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 字节:

Image

实测管用。

正准备整理整理,完善下方案(加个参数),给 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。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值