背景 #
生产环境数据库不允许直接访问,但是又经常有需要直接操作数据库的需求😂。先不说合不合理,背景就是这个背景,因此只能通过跳板机来连接数据库,一(就)般(我)来(而)说(言)会使用ssh隧道,就轻松能解决这个问题,然鹅,事情并不简单。这里陈述一下:
- 生产环境数据库不让直接访问;
- 跳板机上没有公钥,没有权限;
- 我一次可能需要开3+个隧道才能启动服务【敲重点】
解决 #
本着“我不造轮子,谁来造轮子”的想法,这里就造一个小轮子:用Go
来实现SSH隧道多开,并支持配置。成果预览:
原理简要分析 #
如果代理原理有点了解,这里的原理差不多是一样的:Local <-> SSH tunnel <-> Remote Server,对于隧道来说把Local的请求传给Remote, 把Remote的响应告诉Local。直接上代码:
// Start .
// TODO: support random port by using localhost:0
func (tunnel *SSHTunnel) Start() error {
listener, err := net.Listen("tcp", tunnel.LocalAddr)
if err != nil {
return err
}
defer listener.Close()
// tunnel.Local.Port = listener.Addr().(*net.TCPAddr).Port
for {
conn, err := listener.Accept()
if err != nil {
return err
}
logger.Infof(tunnel.name() + " accepted connection")
go tunnel.forward(conn)
}
}
// 创建隧道并传递消息,分别有两个端点,一个是本地隧道口,另一个是远程服务器上的隧道口
func (tunnel *SSHTunnel) forward(localConn net.Conn) {
// 创建本地到跳板机的SSH连接
serverSSHClient, err := ssh.Dial("tcp", tunnel.ServerAddr, tunnel.SSHConfig)
if err != nil {
logger.Infof(tunnel.name()+" server dial error: %s", err)
return
}
logger.Infof(tunnel.name()+" connected to server=%s (1 of 2)", tunnel.ServerAddr)
// 创建跳板机到远程服务器的连接
remoteConn, err := serverSSHClient.Dial("tcp", tunnel.RemoteAddr)
if err != nil {
logger.Infof(tunnel.name()+" remote dial error: %s", err)
return
}
logger.Infof(tunnel.name()+" connected to remote=%s (2 of 2)", tunnel.RemoteAddr)
copyConn := func(writer, reader net.Conn) {
_, err := io.Copy(writer, reader)
if err != nil {
logger.Infof(tunnel.name()+" io.Copy error: %s", err)
}
}
// local(w) => 远程(r)
go copyConn(localConn, remoteConn)
// 远程(w) => 本地(r)
go copyConn(remoteConn, localConn)
}
代码中需要注意的是:
func forward() {
// ... some code ignored
// 下面的代码实现了消息传递,问题:为什么`io.Copy + net.Conn`就可以实现数据的持续传递呢?
copyConn := func(writer, reader net.Conn) {
_, err := io.Copy(writer, reader)
if err != nil {
logger.Infof(tunnel.name()+" io.Copy error: %s", err)
}
}
go copyConn(localConn, remoteConn)
go copyConn(remoteConn, localConn)
}
总结 #
上述场景也可以通过写脚本的方式来解决,但毕竟每个平台对shell支持并不一致,还需要安装ssh
工具,因此还是选择了造轮子。
io.Copy会一直复制直到读到EOF;TCP协议只有在处理
FIN
报文才会写入EOF,因此io.Copy会持续不断的在local和remote之间复制数据。
参考 #
- https://medium.com/@elliotchance/how-to-create-an-ssh-tunnel-in-go-b63722d682aa
- https://golang.org/x/crypto/ssh
- 如何在shell脚本中实现交互,请自行google
- 所有的代码均在:https://github.com/yeqown/infrastructure/tree/master/cmd/tunnel-helper