如何使用go-sql-driver通过SSH通过标准TCP / IP连接到MySQL?

时间:2015-11-16 17:45:24

标签: mysql ssh go tcp

我目前正在使用Windows 8.1上的MySQL Workbench通过SSH使用标准TCP / IP访问Linux服务器上的远程MySQL数据库。基本上我有以下信息:

  • SSH主机名:dbserver.myorg.com:ssh-port
  • SSH用户名:myRemoteLoginUsername
  • SSH密码:(存储在保管库中)
  • SSH密钥文件:本地.ppk文件的路径

  • MySQL主机名:127.0.0.1

  • MySQL服务器端口:3306
  • 用户名:myRemoteDbUsername
  • 密码:(存储在保险库中)
  • 默认架构:myRemoteDatabaseName

如何使用github.com/go-sql-driver/mysql从Go命令应用程序连接到数据库?

我的sql.Open语句中的DataSourceName字符串应该是什么样的?

    db, err := sql.Open("mysql", <DataSourceName> ) {}

准备工作的DataSourceName字符串是否需要额外的工作?

在我的Windows PC上安装了putty。我读到了隧道并为端口3306添加了动态隧道(D3306)。我希望这可以让我使用连接到localhost:3306连接,并在我用putty连接到远程主机时自动将请求转发到远程数据库,但这也没有按预期工作。

2 个答案:

答案 0 :(得分:2)

我答应提供我的例子,它来了。基本上我的解决方案建立了到远程服务器的ssh隧道,并通过此隧道查询远程数据库。 ssh隧道是解决方案的一部分。

我要做的第一件事就是将我的PuTTY .ppk私钥文件转换为有效的OpenSSH .pem密钥文件。这可以使用PuTTYgen中的导出功能轻松完成。由于我想支持密码加密的私钥,我还需要一个函数来解密密钥并将其从解密的原始格式重新格式化为golang.org/x/crypto/ssh/ParsePrivateKey接受的有效格式,这是获取密钥所需的格式。用于认证的签名者名单。

解决方案本身包含两个文件中包含的包。应用程序的主要部分在main.go中完成,其中包含所有相关的数据分配以及与数据库查询相关的代码。与ssh隧道和密钥处理相关的所有内容都包含在sshTunnel.go。

该解决方案不提供安全密码存储的机制,也不会要求输入密码。密码在代码中提供。但是,为密码请求实现回调方法并不会太复杂。

请注意:从性能角度来看,这不是一个理想的解决方案。它也缺乏适当的错误处理。我已经提供了这个例子。

该示例是经过测试的工作示例。我从Windows 8.1 PC开发并使用它。数据库服务器位于远程Linux系统上。您需要更改的是main.go中的数据和查询部分。

这是main.go中包含的第一部分:

// mysqlSSHtunnel project main.go
// Establish an ssh tunnel and connect to a remote mysql server using
// go-sql-driver for database queries. Encrypted private key pem files
// are supported.
//
// This is an example to give an idea. It's far from a performant solution. It 
// lacks of proper error handling and I'm sure it could really be much better 
// implemented. Please forgive me, as I just started with Go about 2 weeks ago.
//
// The database used in this example is from a real Opensimulator installation.
// It queries the migrations table in the opensim database.
//
package main

import (
    "database/sql"
    "fmt"
    _ "github.com/go-sql-driver/mysql"
    "os"
)

// Declare your connection data and user credentials here
const (
    // ssh connection related data
    sshServerHost     = "test.example.com"
    sshServerPort     = 22
    sshUserName       = "tester"
    sshPrivateKeyFile = "testkey.pem" // exported as OpenSSH key from .ppk
    sshKeyPassphrase  = "testoster0n" // key file encrytion password

    // ssh tunneling related data
    sshLocalHost  = "localhost" // local localhost ip (client side)
    sshLocalPort  = 9000        // local port used to forward the connection
    sshRemoteHost = "127.0.0.1" // remote local ip (server side)
    sshRemotePort = 3306        // remote MySQL port

    // MySQL access data
    mySqlUsername = "opensim"
    mySqlPassword = "h0tgrits"
    mySqlDatabase = "opensimdb"
)

// The main entry point of the application
func main() {
    fmt.Println("-> mysqlSSHtunnel")

    tunnel := sshTunnel() // Initialize sshTunnel
    go tunnel.Start()     // Start the sshTunnel

    // Declare the dsn (aka database connection string)
    // dsn := "opensim:h0tgrits@tcp(localhost:9000)/opensimdb"
    dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s",
        mySqlUsername, mySqlPassword, sshLocalHost, sshLocalPort, mySqlDatabase)

    // Open the database
    db, err := sql.Open("mysql", dsn)
    if err != nil {
        dbErrorHandler(err)
    }
    defer db.Close() // keep it open until we are finished

    // Simple select query to check migrations (provided here as an example)
    rows, err := db.Query("SELECT * FROM migrations")
    if err != nil {
        dbErrorHandler(err)
    }
    defer rows.Close()

    // Iterate though the rows returned and print them
    for rows.Next() {
        var version int
        var name string
        if err := rows.Scan(&name, &version); err != nil {
            dbErrorHandler(err)
        }
        fmt.Printf("%s, %d\n", name, version)
    }
    if err := rows.Err(); err != nil {
        dbErrorHandler(err)
    }

    // Done for now
    fmt.Println("<- mysqlSSHtunnel")
}

// Simple mySql error handling (yet to implement)
func dbErrorHandler(err error) {
    switch err := err.(type) {
    default:
        fmt.Printf("Error %s\n", err)
        os.Exit(-1)
    }
}

现在是sshTunnel.go中的第二部分:

// mysqlSSHtunnel project sshTunnel.go
//
// Everything regarding the ssh tunnel goes here. Credits go to Svett Ralchev.
// Look at http://blog.ralch.com/tutorial/golang-ssh-tunneling for an excellent
// explanation and most ssh-tunneling related details used in this code.
//
// PEM key decryption is valid for password proected SSH-2 RSA Keys generated as
// .ppk files for putty and exported as OpenSSH .pem keyfile using PuTTYgen.
//
package main

import (
    "bytes"
    "crypto/x509"
    "encoding/base64"
    "encoding/pem"
    "fmt"
    "golang.org/x/crypto/ssh"
    "io"
    "io/ioutil"
    "net"
)

// Define an endpoint with ip and port
type Endpoint struct {
    Host string
    Port int
}

// Returns an endpoint as ip:port formatted string
func (endpoint *Endpoint) String() string {
    return fmt.Sprintf("%s:%d", endpoint.Host, endpoint.Port)
}

// Define the endpoints along the tunnel
type SSHtunnel struct {
    Local  *Endpoint
    Server *Endpoint
    Remote *Endpoint
    Config *ssh.ClientConfig
}

// Start the tunnel
func (tunnel *SSHtunnel) Start() error {
    listener, err := net.Listen("tcp", tunnel.Local.String())
    if err != nil {
        return err
    }
    defer listener.Close()

    for {
        conn, err := listener.Accept()
        if err != nil {
            return err
        }
        go tunnel.forward(conn)
    }
}

// Port forwarding
func (tunnel *SSHtunnel) forward(localConn net.Conn) {
    // Establish connection to the intermediate server
    serverConn, err := ssh.Dial("tcp", tunnel.Server.String(), tunnel.Config)
    if err != nil {
        fmt.Printf("Server dial error: %s\n", err)
        return
    }

    // access the target server
    remoteConn, err := serverConn.Dial("tcp", tunnel.Remote.String())
    if err != nil {
        fmt.Printf("Remote dial error: %s\n", err)
        return
    }

    // Transfer the data between  and the remote server
    copyConn := func(writer, reader net.Conn) {
        _, err := io.Copy(writer, reader)
        if err != nil {
            fmt.Printf("io.Copy error: %s", err)
        }
    }

    go copyConn(localConn, remoteConn)
    go copyConn(remoteConn, localConn)
}

// Decrypt encrypted PEM key data with a passphrase and embed it to key prefix
// and postfix header data to make it valid for further private key parsing.
func DecryptPEMkey(buffer []byte, passphrase string) []byte {
    block, _ := pem.Decode(buffer)
    der, err := x509.DecryptPEMBlock(block, []byte(passphrase))
    if err != nil {
        fmt.Println("decrypt failed: ", err)
    }
    encoded := base64.StdEncoding.EncodeToString(der)
    encoded = "-----BEGIN RSA PRIVATE KEY-----\n" + encoded +
        "\n-----END RSA PRIVATE KEY-----\n"
    return []byte(encoded)
}

// Get the signers from the OpenSSH key file (.pem) and return them for use in
// the Authentication method. Decrypt encrypted key data with the passphrase.
func PublicKeyFile(file string, passphrase string) ssh.AuthMethod {
    buffer, err := ioutil.ReadFile(file)
    if err != nil {
        return nil
    }

    if bytes.Contains(buffer, []byte("ENCRYPTED")) {
        // Decrypt the key with the passphrase if it has been encrypted
        buffer = DecryptPEMkey(buffer, passphrase)
    }

    // Get the signers from the key
    signers, err := ssh.ParsePrivateKey(buffer)
    if err != nil {
        return nil
    }
    return ssh.PublicKeys(signers)
}

// Define the ssh tunnel using its endpoint and config data
func sshTunnel() *SSHtunnel {
    localEndpoint := &Endpoint{
        Host: sshLocalHost,
        Port: sshLocalPort,
    }

    serverEndpoint := &Endpoint{
        Host: sshServerHost,
        Port: sshServerPort,
    }

    remoteEndpoint := &Endpoint{
        Host: sshRemoteHost,
        Port: sshRemotePort,
    }

    sshConfig := &ssh.ClientConfig{
        User: sshUserName,
        Auth: []ssh.AuthMethod{
            PublicKeyFile(sshPrivateKeyFile, sshKeyPassphrase)},
    }

    return &SSHtunnel{
        Config: sshConfig,
        Local:  localEndpoint,
        Server: serverEndpoint,
        Remote: remoteEndpoint,
    }
}

答案 1 :(得分:1)

嗯,我认为你可以做到“完全围棋”。

SSH部分和端口转发

我从this开始(我没有谷歌更好的例子)。

请注意此代码的两个问题:

  1. 实际上并不正确:它连接到远程套接字 之前接受客户端连接 而它应该反过来:接受客户端连接 端口转发的本地套接字然后使用活动的SSH会话进行连接 到远程套接字,如果成功,产生两个goroutines铲 这两个插座之间的数据。

  2. 配置SSH客户端时,它明确允许基于密码 认证原因不明。你不需要这个,因为你正在使用 基于pubkey的auth。

  3. 可能会让您绊倒的障碍是管理对SSH密钥的访问。 它的问题是一个好的密钥应该用密码保护。

    你说密钥的密码是“存储在valut中”,老实说我不知道​​“valut”是什么。

    在我使用的系统上,SSH客户端要么输入密码来解密密钥,要么使用所谓的“SSH代理”:

    • 在基于Linux的系统上,它通常是OpenSSH的ssh-agent二进制文件,在后台工作,可通过Unix域套接字访问,并通过检查名为SSH_AUTH_SOCK的环境变量来定位。
    • 在Windows上我使用PuTTY,它有自己的代理pageant.exe。 我不知道PuTTY SSH客户端使用哪种方式来定位它。

    要访问OpenSSH代理,golang.org/x/crypto/ssh提供agent子包,可用于定位代理并与之通信。 如果您需要从pageant获取密钥,我担心您需要弄清楚使用和实现它的协议。

    MySQL部分

    下一步是将其与go-sql-driver进行整合。

    我会以最简单的方式开始:

    1. 当您的SSH端口转发工作时, 使它侦听localhost上随机端口上的传入连接。 打开连接后,从返回的连接中获取端口 对象。
    2. 使用该端口号构建连接字符串以传递给您将创建使用sql.DB的{​​{1}}实例。
    3. 然后驱动程序将连接到您的端口转发端口,您的SSH层将完成剩下的工作。

      在你完成这项工作之后,我会探讨你选择的驱动程序 允许你进行一些更细粒度的调整,比如允许你直接传递go-sql-driver(一个打开的套接字)的实例,这样你就可以完全跳过端口转发设置,只生成通过SSH转发的新TCP连接,即,跳过“本地监听”步骤。