英文:
How to connect to MySQL via Standard TCP/IP over SSH using go-sql-driver?
问题
我目前正在使用Windows 8.1上的MySQL Workbench来访问Linux服务器上的远程MySQL数据库,使用标准TCP/IP over SSH。基本上,我有以下信息:
-
SSH主机名:dbserver.myorg.com:ssh-port
-
SSH用户名:myRemoteLoginUsername
-
SSH密码:(存储在保险库中)
-
SSH密钥文件:本地.ppk文件的路径
-
MySQL主机名:127.0.0.1
-
MySQL服务器端口:3306
-
用户名:myRemoteDbUsername
-
密码:(存储在保险库中)
-
默认模式:myRemoteDatabaseName
如何在Go命令应用程序中使用github.com/go-sql-driver/mysql连接到数据库?
在sql.Open语句中,我的DataSourceName字符串应该是什么样子的?
db, err := sql.Open("mysql", <DataSourceName>) {}
是否需要额外的工作来准备一个可用的DataSourceName字符串?
在我的Windows PC上,我已经安装了putty。我了解了隧道技术,并添加了一个动态隧道,用于端口3306(D3306)。我期望这样做可以让我使用对localhost:3306的连接,并在我通过putty连接到远程主机时自动转发请求到远程数据库,但这个方法并没有按预期工作。
英文:
I'm currently using MySQL Workbench on Windows 8.1 to access a remote MySQL database on a Linux server using Standard TCP/IP over SSH. Basically I have the following information:
-
SSH Hostname: dbserver.myorg.com:ssh-port
-
SSH Username: myRemoteLoginUsername
-
SSH Password: (stored in vault)
-
SSH Key File: path to a local .ppk file
-
MySQL Hostname: 127.0.0.1
-
MySQL Server Port: 3306
-
Username: myRemoteDbUsername
-
Password: (stored in vault)
-
Default schema: myRemoteDatabaseName
How can I connect to the database from a Go command application using github.com/go-sql-driver/mysql?
how should my DataSourceName string in the sql.Open statement look like?
db, err := sql.Open("mysql", <DataSourceName> ) {}
Is there any extra work needed to prepare a working DataSourceName string?
On my Windows PC I have putty installed. I read about tunneling and added a Dynamic tunnel for port 3306 (D3306). I expected this will let me connect using a connection to localhost:3306 and automatically forward the request to the remote db whenever I'm connected to the remote host with putty, but this didn't work as expected either.
答案1
得分: 1
我承诺提供我的示例,下面是示例代码。基本上,我的解决方案通过建立一个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
// 建立SSH隧道并使用go-sql-driver连接到远程mysql服务器。
// 支持加密的私钥pem文件。
//
// 这只是一个示例,用于提供一个思路。它远非是一个高性能的解决方案。
// 它缺乏适当的错误处理,我相信它可以更好地实现。
// 请原谅我,因为我只是在大约两周前开始使用Go。
//
// 此示例中使用的数据库来自真实的Opensimulator安装。
// 它查询opensim数据库中的迁移表。
//
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
"os"
)
// 在这里声明您的连接数据和用户凭据
const (
// SSH连接相关数据
sshServerHost = "test.example.com"
sshServerPort = 22
sshUserName = "tester"
sshPrivateKeyFile = "testkey.pem" // 从.ppk导出为OpenSSH密钥
sshKeyPassphrase = "testoster0n" // 密钥文件加密密码
// SSH隧道相关数据
sshLocalHost = "localhost" // 本地localhost IP(客户端)
sshLocalPort = 9000 // 用于转发连接的本地端口
sshRemoteHost = "127.0.0.1" // 远程本地IP(服务器端)
sshRemotePort = 3306 // 远程MySQL端口
// MySQL访问数据
mySqlUsername = "opensim"
mySqlPassword = "h0tgrits"
mySqlDatabase = "opensimdb"
)
// 应用程序的主入口点
func main() {
fmt.Println("-> mysqlSSHtunnel")
tunnel := sshTunnel() // 初始化sshTunnel
go tunnel.Start() // 启动sshTunnel
// 声明dsn(数据库连接字符串)
// dsn := "opensim:h0tgrits@tcp(localhost:9000)/opensimdb"
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s",
mySqlUsername, mySqlPassword, sshLocalHost, sshLocalPort, mySqlDatabase)
// 打开数据库
db, err := sql.Open("mysql", dsn)
if err != nil {
dbErrorHandler(err)
}
defer db.Close() // 在完成之前保持打开状态
// 简单的选择查询以检查迁移(这里提供一个示例)
rows, err := db.Query("SELECT * FROM migrations")
if err != nil {
dbErrorHandler(err)
}
defer rows.Close()
// 遍历返回的行并打印它们
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)
}
// 现在完成了
fmt.Println("<- mysqlSSHtunnel")
}
// 简单的MySQL错误处理(尚未实现)
func dbErrorHandler(err error) {
switch err := err.(type) {
default:
fmt.Printf("错误:%s\n", err)
os.Exit(-1)
}
}
现在是sshTunnel.go中的第二部分代码:
// mysqlSSHtunnel project sshTunnel.go
//
// 所有与SSH隧道相关的内容都在这里。感谢Svett Ralchev。
// 请参阅http://blog.ralch.com/tutorial/golang-ssh-tunneling,了解关于SSH隧道的优秀解释和此代码中使用的大部分SSH隧道相关细节。
//
// 使用密码保护的SSH-2 RSA密钥生成的.ppk文件并使用PuTTYgen导出为OpenSSH .pem密钥文件,PEM密钥解密有效。
//
package main
import (
"bytes"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
"golang.org/x/crypto/ssh"
"io"
"io/ioutil"
"net"
)
// 定义具有IP和端口的端点
type Endpoint struct {
Host string
Port int
}
// 以ip:port格式返回端点字符串
func (endpoint *Endpoint) String() string {
return fmt.Sprintf("%s:%d", endpoint.Host, endpoint.Port)
}
// 定义隧道沿线的端点
type SSHtunnel struct {
Local *Endpoint
Server *Endpoint
Remote *Endpoint
Config *ssh.ClientConfig
}
// 启动隧道
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)
}
}
// 端口转发
func (tunnel *SSHtunnel) forward(localConn net.Conn) {
// 建立与中间服务器的连接
serverConn, err := ssh.Dial("tcp", tunnel.Server.String(), tunnel.Config)
if err != nil {
fmt.Printf("服务器拨号错误:%s\n", err)
return
}
// 访问目标服务器
remoteConn, err := serverConn.Dial("tcp", tunnel.Remote.String())
if err != nil {
fmt.Printf("远程拨号错误:%s\n", err)
return
}
// 在本地连接和远程服务器之间传输数据
copyConn := func(writer, reader net.Conn) {
_, err := io.Copy(writer, reader)
if err != nil {
fmt.Printf("io.Copy错误:%s", err)
}
}
go copyConn(localConn, remoteConn)
go copyConn(remoteConn, localConn)
}
// 使用密码解密PEM密钥数据,并将其嵌入到密钥前缀和后缀头数据中,以使其有效用于进一步的私钥解析。
func DecryptPEMkey(buffer []byte, passphrase string) []byte {
block, _ := pem.Decode(buffer)
der, err := x509.DecryptPEMBlock(block, []byte(passphrase))
if err != nil {
fmt.Println("解密失败:", err)
}
encoded := base64.StdEncoding.EncodeToString(der)
encoded = "-----BEGIN RSA PRIVATE KEY-----\n" + encoded +
"\n-----END RSA PRIVATE KEY-----\n"
return []byte(encoded)
}
// 从OpenSSH密钥文件(.pem)获取签名者,并将其返回以在Authentication方法中使用。
// 使用密码解密加密的密钥数据。
func PublicKeyFile(file string, passphrase string) ssh.AuthMethod {
buffer, err := ioutil.ReadFile(file)
if err != nil {
return nil
}
if bytes.Contains(buffer, []byte("ENCRYPTED")) {
// 如果密钥已加密,则使用密码解密密钥
buffer = DecryptPEMkey(buffer, passphrase)
}
// 从密钥获取签名者
signers, err := ssh.ParsePrivateKey(buffer)
if err != nil {
return nil
}
return ssh.PublicKeys(signers)
}
// 使用其端点和配置数据定义ssh隧道
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,
}
}
英文:
I promised to provide my example, here it comes. Basically my solution establishes an ssh tunnel to the remote server and queries the remote database through this tunnel. The ssh tunnel is part of the solution.
The first thing I had to do is to convert my PuTTY .ppk private key file into a valid OpenSSH .pem key file. This can easily be done using the Export feature in PuTTYgen. As I wanted to support password encrypted private keys I also needed a function to decrypt the key and reformat it from its decrypted raw format into a valid format accepted by golang.org/x/crypto/ssh/ParsePrivateKey, which is needed to get the list of signers for authentication.
The solution itself consists of a package contained in two files. The main part of the application is done in main.go which contains all relevant data assignments as well as the code related to the database query. Everything related to ssh tunneling and key handling is contained in sshTunnel.go.
The solution does not provide a mechanismn for a secure password store, nor does it ask for a password. The password is provided in the code. However, it would not be too complicated to implement a callback method for password requests.
Please note: from a performance perspective this is not an ideal solution. It also lacks of proper error handling. I have provided this as an example.
The example is a tested and working example. I developed and used this from a Windows 8.1 PC. The database server is on a remote Linux system. All you need to change is the data and the query part in main.go.
Here is the first part contained in 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)
}
}
Now the second part in 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,
}
}
答案2
得分: 0
嗯,我可以帮你翻译这段内容。以下是翻译好的部分:
嗯,我认为你可以完全使用Go来实现这个功能。
SSH部分和端口转发
我建议你从这个链接开始:http://sosedoff.com/2015/05/25/ssh-port-forwarding-with-go.html(我没有找到更好的示例)。
请注意这段代码存在两个问题:
-
实际上它是不正确的:它在接受客户端连接之前连接到了远程套接字,而实际上应该相反:接受客户端连接到一个进行端口转发的本地套接字,然后使用活动的SSH会话连接到远程套接字。如果连接成功,再启动两个goroutine来在这两个套接字之间传输数据。
-
在配置SSH客户端时,它明确允许基于密码的身份验证,但没有说明原因。因为你使用的是基于公钥的身份验证,所以不需要这样做。
在这里可能会遇到的一个障碍是管理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
集成。
我建议你从最简单的方式开始:
- 当你的SSH端口转发工作正常时,在本地主机的一个随机端口上监听传入连接。当连接打开时,从返回的连接对象中获取端口号。
- 使用该端口号构造连接字符串,传递给你将创建的
sql.DB
实例,以便使用go-sql-driver
。
驱动程序将连接到你进行端口转发的端口,而你的SSH层将处理其余的部分。
在你实现了这个功能之后,你可以探索你选择的驱动程序是否允许更精细的调整,比如直接传递一个io.ReadWriter
实例(一个已打开的套接字),这样你就可以跳过端口转发设置,直接通过SSH产生新的TCP连接,也就是跳过“本地监听”这一步骤。
英文:
Well, you could do that "full Go", I think.
The SSH part and port-forwarding
I'd start with something like this (I failed to google a better example).
Note two problems with this code:
-
It's not actually correct: it connects to a remote socket
before accepting a client connection
while it should do the reverse: accept a client connection to
a port-forwarded local socket then use the active SSH session to connect
to the remote socket and if this succeeds, spawn two goroutines to shovel
the data between those two sockets. -
When configuring the SSH client, it explicitly allows password-based
authentication for unknown reason. You don't need this as you're using
pubkey-based auth.
An obstacle which might trip you there is managing an access to your SSH key.
The problem with it is that a good key should be protected by a passphrase.
You say the key's password is "stored in valut" and I honestly have no idea what "the valut" is.
On the systems I use, an SSH client either asks for the password to decrypt the key or work with a so-called "SSH agent":
- On Linux-based systems it's typically an OpenSSH's
ssh-agent
binary working in the background which is accessed via a Unix-domain socket, and located by inspecting an environment variable namedSSH_AUTH_SOCK
. - On Windows I use PuTTY, and it has its own agent,
pageant.exe
.
I'm don't know which means PuTTY SSH client uses to locate it.
To access the OpenSSH agent, golang.org/x/crypto/ssh
provides the agent
subpackage which can be used to locate the agent and communicate with it.
If you need to obtain keys from pageant
, I'm afraid you'll need to figure out what protocol that uses and implement it.
The MySQL part
The next step would be to integrate this with go-sql-driver
.
I'd start the simplest way possible:
- When you have your SSH port forwarding working,
make it listen for incoming connections on a random port on localhost.
When the connection is opened, get the port from the returned connection
object. - Use that port number to constuct the connection string to pass to an instance of
sql.DB
you will create to usego-sql-driver
.
The driver will then connect to your port-forwarded port, and your SSH layer will do the rest.
After you have this working, I'd explore whether the driver of your choice
allows some more fine-grained tweaking like allowing you to directly pass it an instance of io.ReadWriter
(an opened socket) so that you could skip the port-forwarding setup entirely and just produce new TCP connections forwarded through SSH, that is, skip the "listening locally" step.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论