`
January 8, 2020 本文阅读量

SSH Tunnel小工具

理解隧道原理,造一个隧道小工具帮助提高开发效率。

背景

生产环境数据库不允许直接访问,但是又经常有需要直接操作数据库的需求😂。先不说合不合理,背景就是这个背景,因此只能通过跳板机来连接数据库,一(就)般(我)来(而)说(言)会使用ssh隧道,就轻松能解决这个问题,然鹅,事情并不简单。这里陈述一下:

  1. 生产环境数据库不让直接访问;
  2. 跳板机上没有公钥,没有权限;
  3. 我一次可能需要开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之间复制数据。

参考