sshdb

package module
v0.6.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 11, 2024 License: BSD-3-Clause Imports: 11 Imported by: 3

README

sshdb

Go Reference Build Status codecov Go Report Card

A pure go library that allows connections to remote sql databases via ssh tunnels. The package works with db packages by creating an ssh connection and a dial function for connecting to the tunnel.

install

go get -v github.com/jfcote87/sshdb

making connections

To setup connections, create a Tunnel object with the sshdb.New method using an ssh.ClientConfig (see documentation on the "golang.org/x/crypto/ssh" package) and the remote address of the ssh server. The returned Tunnel is safe for concurrent use by multiple goroutines and maintains its own pool of db connections. Thus, the New function should be called just once for each remote server. It is rarely necessary to close a Tunnel.

Create a dsn string using addresses based upon the remote server. The connections will be created on the remote ssh server to the database server. Select the appropriate driver for your database. If the database is not in included drivers, review the sshdb.Driver interface{} and the existing driver code. Essentially the passed dialer in the OpenConnector function should replace the default dialer for the db. Use the db.Connector to create a sql.DB object.

Example for creating connections may be found in the code below and the example_test.go file.

imports (
	"golang.org/x/crypto/ssh"
	"github.com/jfcote87/sshdb"
)

func main() {
	exampleCfg := &ssh.ClientConfig{
		User:            "jfcote87",
		Auth:            []ssh.AuthMethod{ssh.Password("my second favorite password")},
		HostKeyCallback: ssh.InsecureIgnoreHostKey(),
	}
	// New creates a "tunnel" for database connections. A Tunnel can support
	// multiple database connections.
	tunnel, err := sshdb.New(exampleCfg, remoteAddr)
	if err != nil {
		log.Fatalf("new tunnel create failed: %v", err)
	}
    // serverAddr is a valid hostname for the db server from the remote ssh server (often localhost).
	dsn := "username:dbpassword@tcp(serverAddress:3306)/schemaName?parseTime=true"

	// open connector and then new DB
	connector, err := tunnel.OpenConnector(mysql.TunnelDriver, dsn)
	if err != nil {
		return fmt.Errorf("open connector failed %s - %v", dsn, err)
	}
	db := sql.OpenDB(connector)
}

testing

$ go test ./...

Documentation

Overview

Package sshdb provides database connections to tunnel through an ssh connection to a remove server

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func RegisterDriver

func RegisterDriver(key string, driver Driver)

RegisterDriver associates a database's Driver interface with the key. This allows a TunnelConfig to handle multiple database types per host connection.

Types

type ConfigError

type ConfigError struct {
	Msg        string
	Idx        int
	Addr       string
	DriverName string
	DSN        string
	Err        error
}

ConfigError used to describe errors when opening DBs based upon a Config

func (*ConfigError) Error

func (ce *ConfigError) Error() string

Error make the ConfigError an error

func (*ConfigError) Unwrap

func (ce *ConfigError) Unwrap() error

Unwrap returns the internal error

type Datasource

type Datasource struct {
	DriverName       string `yaml:"driver_name" json:"driver_name,omitempty"`
	ConnectionString string `yaml:"dsn" json:"dsn,omitempty"`
	// tests use this parameter
	Queries []string `yaml:"queries,omitempty" json:"queries,omitempty"`
}

Datasource defines a database connection using the Driver name and a connection string for use by the underlying sql driver. The DriverName must be registered or the TunnelConfig.OpenDB will return an error.

func (Datasource) Driver

func (cd Datasource) Driver() (Driver, error)

Driver returns the Driver associated with the ConnDefinition.DriverName. Will return error if the name was not associated using the RegisterDriver func.

type Dialer

type Dialer interface {
	DialContext(context.Context, string, string) (net.Conn, error)
}

Dialer creates a net.Conn via the tunnel's ssh client

type DialerFunc

type DialerFunc func(context.Context, string, string) (net.Conn, error)

DialerFunc allows a func to fulfill the Dialer interface.

func (DialerFunc) DialContext

func (d DialerFunc) DialContext(ctx context.Context, net, addr string) (net.Conn, error)

DialContext calls the underlying dialerfunc.

type Driver

type Driver interface {
	OpenConnector(dialer Dialer, dsn string) (driver.Connector, error)
	Name() string
}

Driver creates a Connector for a specific database type using the dialer passed from a Tunnel and a dsn string that defines a connection from the remote server to the desired database.

This package includes drivers for mysql, mssql, postgress (v3 and v4).

type Tunnel

type Tunnel struct {
	// contains filtered or unexported fields
}

Tunnel manages an ssh client connections and creates and tracks db connections made through the client

func New

func New(clientConfig *ssh.ClientConfig, remoteHostPort string) (*Tunnel, error)

New returns a Tunnel based upon the ssh clientConfig for creating new connectors/connections via an ssh client connection. The tunnel can host multiple db connections to different database servers. the remoteHostPort defines the remote ssh server address and must be in the form "host:port", "host%zone:port", [host]:port" or "[host%zone]:port". See func net.Dial for a more detailed description of the hostport format.

Example

ExampleNew demonstrates the package's simplest usage, accessing a single mysql server on a remote host where port 3306 is blocked but the remote host is accessible via ssh.

package main

import (
	"context"
	"database/sql"
	"fmt"
	"log"

	"github.com/jfcote87/sshdb"
	"github.com/jfcote87/sshdb/mysql"
	"golang.org/x/crypto/ssh"
)

func main() {
	var (
		// values used in connecting remote host
		remoteAddr = "remote.example.com:22"

		ctx, cancelFunc = context.WithCancel(context.Background())
	)
	defer cancelFunc()

	signer, serverSigner, _ := getKeys()
	exampleCfg := &ssh.ClientConfig{
		User: "me",
		Auth: []ssh.AuthMethod{
			ssh.PublicKeys(signer),
		},
		HostKeyCallback: ssh.FixedHostKey(serverSigner.PublicKey()),
	}
	// New creates a "tunnel" for database connections.
	tunnel, err := sshdb.New(exampleCfg, remoteAddr)
	if err != nil {
		log.Fatalf("new tunnel create failed: %v", err)
	}

	configs := []struct {
		nettype      string
		dbServerAddr string
	}{
		{"tcp", "localhost:3306"},      // local database on remote server tcp connection
		{"unix", "/tmp/mysql.sock"},    // local database on remote server via unix socket
		{"tcp", "db.example.com:3306"}, // connect to db.example.com db from remote server skirt around a firewall
	}
	for _, cfg := range configs {

		// dbServerAddr is a valid address for the db server beginning from the remote ssh server.
		dsn := fmt.Sprintf("username:password@%s(%s)/schemaname?parseTime=true", cfg.nettype, cfg.dbServerAddr)

		// open connector and then new DB
		connector, err := tunnel.OpenConnector(mysql.TunnelDriver, dsn)
		if err != nil {
			log.Printf("open connector failed %s - %v", dsn, err)
			continue
		}

		db := sql.OpenDB(connector)
		defer db.Close()

		// ping tests connectivity
		if err := db.PingContext(ctx); err != nil {
			log.Printf("%v ping failed: %v", cfg.dbServerAddr, err)
		}
	}
}

func getKeys() (ssh.Signer, ssh.Signer, error) {
	signer, err := ssh.ParsePrivateKeyWithPassphrase([]byte(clientPrivateKey),
		[]byte("sshdb_example"))
	if err != nil {
		return nil, nil, fmt.Errorf("private key parse error: %v", err)
	}
	serverSigner, err := ssh.ParsePrivateKey([]byte(serverPrivateKey))
	if err != nil {
		return nil, nil, fmt.Errorf("server private key parse error: %v", err)
	}
	return signer, serverSigner, nil
}

// clientPrivateKey is used to authenticate with the remote ssh server.
// This key was generated using the following command
// ssh-keygen -f ~/sshdb_client_key -t ecdsa -b 521
const clientPrivateKey = `-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABCrg/C49e
zn3txdMKskd0JiAAAAEAAAAAEAAACsAAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlz
dHA1MjEAAACFBAHy1nPGt0hS9cS7ENbslUy28NC5ubYw/pdlm4w/ugkudkydOSbn+q6Hsk
VM8Q8RJP71oTOV2BWCYN5wMrk6LYTQ+QDpVDA0MHjs1ZHfhwciVZWG+RaJTZcLEhAHfUjL
v8JPPAc4q3ygNNHUJUSWY/37rJzJ0GNJU2aiEuO6dKzXb8Z1dwAAARDBuo7xtZHjwwMbS7
EExM4NzO45Hq21lPPhWcRhht90bpsG8pVG69Vb4PIo9khQDm4WfPLI/a0Vujrvj4oSckNP
ay7DN6sTtVWbfInJbt1Rm1FuECQMIakEapQmPrjQyMWHREfgM0GaRgHIAy/9KXSD1rq7co
MmWA8Jmmg7xa8wL/c/fgtB3q0vDBU5jdZHu5b/uQgdDoiZm7gwLxny0AVVWFTetpspTMbh
cmihTM9+44fHkIzhCpMzDVb8uR+FnSmjyj6GGghJtagwNm151Y3JXjNGPlRUi7VBnbE7LC
wXxGJwJo8diI8o0ew25P+n3K26eVHKfSvwljLjdBS5GeFyJE35ul4QsO2w+t0cAjj/SQ==
-----END OPENSSH PRIVATE KEY-----
`

// serverPrivateKey is used to authenticate ssh clients.
// This key was generated using the following command
// ssh-keygen -f ~/sshdb_server_key -t ecdsa -b 521
const serverPrivateKey = `-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAArAAAABNlY2RzYS
1zaGEyLW5pc3RwNTIxAAAACG5pc3RwNTIxAAAAhQQAaHYSCQ8ultHfdGu2LeDfR4uM8M5r
DwNziz1bwy2J57/1fZm4j4BBBNnqEXfgQwscnn2bJqoAVS8BtSKz4uA9CrEAMbTuu6FK7m
UyEKllyZ6RfdwUjBClYRsb8qvcrC2KJDNYePASZs8ufgCASEWZ2bNoZSJHooMFwOXL5q17
vDOJHqUAAAEQaQKgqGkCoKgAAAATZWNkc2Etc2hhMi1uaXN0cDUyMQAAAAhuaXN0cDUyMQ
AAAIUEAGh2EgkPLpbR33Rrti3g30eLjPDOaw8Dc4s9W8Mtiee/9X2ZuI+AQQTZ6hF34EML
HJ59myaqAFUvAbUis+LgPQqxADG07ruhSu5lMhCpZcmekX3cFIwQpWEbG/Kr3KwtiiQzWH
jwEmbPLn4AgEhFmdmzaGUiR6KDBcDly+ate7wziR6lAAAAQgDVggCI6pefB2znhtdT187I
iWZU7LTARxroTZqJzJRT3nvmu1IBV3FY0v6VXbpYoREpRfDnp8aLt2S3cPw2x8yMOwAAAA
xyb290QEpGQy1TTUcBAgMEBQY=
-----END OPENSSH PRIVATE KEY-----
`
Output:

Example (Multipledbservers)

ExampleNew_multiplehosts demonstrates how to connect multiple remote database servers via a single ssh connection

package main

import (
	"context"
	"database/sql"
	"fmt"
	"log"

	"github.com/jfcote87/sshdb"
	"github.com/jfcote87/sshdb/mssql"
	"golang.org/x/crypto/ssh"
)

func main() {
	var (
		// values used in connecting remote host
		remoteAddr = "remote.example.com:22"

		// values used in dsn string
		dbServerAddr    = []string{"localsrv.example.com", "cloudsvr.example.com"}
		ctx, cancelFunc = context.WithCancel(context.Background())
	)
	defer cancelFunc()
	exampleCfg := &ssh.ClientConfig{
		User:            "jfcote87",
		Auth:            []ssh.AuthMethod{ssh.Password("my second favorite password")},
		HostKeyCallback: ssh.InsecureIgnoreHostKey(),
	}

	tunnelCtx, err := sshdb.New(exampleCfg, remoteAddr)
	if err != nil {
		log.Fatalf("newDriverContext00 failed: %v", err)
	}

	dsn00 := fmt.Sprintf("uid=me;password=xpwd;server=%s;database=crm", dbServerAddr[0])
	dsn01 := fmt.Sprintf("uid=me;password=ypwd;server=%s;database=web", dbServerAddr[1])

	connector00, err := tunnelCtx.OpenConnector(mssql.TunnelDriver, dsn00)
	if err != nil {
		log.Fatalf("open connector failed %s - %v", dsn00, err)
	}
	connector01, err := tunnelCtx.OpenConnector(mssql.TunnelDriver, dsn01)
	if err != nil {
		log.Fatalf("open connector failed %s - %v", dsn01, err)
	}

	db00, db01 := sql.OpenDB(connector00), sql.OpenDB(connector01)

	defer db00.Close()
	defer db01.Close()
	// ping tests connectivity
	if err := db00.PingContext(ctx); err != nil {
		log.Printf("%s ping failed: %v", "db0.example.com", err)
	}
	if err := db01.PingContext(ctx); err != nil {
		log.Printf("%s ping failed: %v", "db1.example.com", err)
	}
}
Output:

func (*Tunnel) Close

func (tun *Tunnel) Close() error

Close safely resets the tunnel. If calling func has already locked tunnel.m, it should call reset directly.

func (*Tunnel) ConnCount

func (tun *Tunnel) ConnCount() int

ConnCount returns number of active db connections managed by the tunnel

func (*Tunnel) DialContext

func (tun *Tunnel) DialContext(ctx context.Context, _, addr string) (net.Conn, error)

DialContext creates an ssh client connection to the addr. sshdb drivers must use this func when creating driver.Connectors. You may use this func to establish "raw" connections to a remote service.

func (*Tunnel) IgnoreSetDeadlineRequest

func (tun *Tunnel) IgnoreSetDeadlineRequest(val bool)

IgnoreSetDeadlineRequest exists because the ssh client package does not support deadlines and returns an error if attempting to set a deadline. If existing code contains setdeadline calls, pass true to this functions, and the tunnel ignore deadline requests.

func (*Tunnel) OpenConnector

func (tun *Tunnel) OpenConnector(tunnelDriver Driver, dataSourceName string) (driver.Connector, error)

OpenConnector fulfills the driver DriverContext interface and returns a new db connection via the ssh client connection. The dataSourceName should follow rules of the base database and must create the connection as if connecting from the remote ssh server.

type TunnelConfig

type TunnelConfig struct {
	// address of remote server must be in the form "host:port", "host%zone:port",
	// "[host]:port" or "[host%zone]:port".  See func net.Dial for a description of
	// the hostport parameter.
	HostPort string `yaml:"hostport,omitempty" json:"hostport,omitempty"`
	// login name for the remote ssh connection
	UserID string `yaml:"user_id,omitempty" json:"user_id,omitempty"`
	// password to use with UserID.  May be blank if using keys.
	Pwd string `yaml:"pwd,omitempty" json:"pwd,omitempty"`
	// file containing a PEM version of the private key used for authenticating the ssh session
	ClientKeyFile string `yaml:"client_key_file,omitempty" json:"client_key_file,omitempty"`
	// string containing PEM of private key.  May not use ClientKeyFile and ClientKey simultaneously
	ClientKey string `yaml:"client_key,omitempty" json:"client_key,omitempty"`
	// if private key is phrase protected, set this to password phrase.  Otherwise leave blank
	ClientKeyPwd string `yaml:"client_key_pwd,omitempty" json:"client_key_pwd,omitempty"`
	// file containing public key for validating remote host.  ServerPublicKeyFile and ServerPublicKey may
	// not be used simultaneously.
	ServerPublicKeyFile string `yaml:"server_public_key_file,omitempty" json:"server_public_key_file,omitempty"`
	// string containing public key definition.  If no public key specified, InsecureIgnoreHostKey is assumed
	ServerPublicKey string `yaml:"server_public_key,omitempty" json:"server_public_key,omitempty"`
	// IgnoreDeadlines tells the tunnel to ignore deadline requests as the ssh tunnel does not implement
	IgnoreDeadlines bool `yaml:"ignore_deadlines,omitempty" json:"ignore_deadlines,omitempty"`
	// a map of ConnDefinitions for each db connection using the tunnel.  Each dsn will return a corresponding *sql.DB
	Datasources map[string]Datasource `yaml:"datasources,omitempty" json:"datasources,omitempty"`
	// contains filtered or unexported fields
}

TunnelConfig describes an ssh connection to a remote host and the databases accessed via the connection. See the example_config_test.go file for examples.

func (*TunnelConfig) DB added in v0.5.3

func (tc *TunnelConfig) DB(dbname string) (*sql.DB, error)

DB returns an open DB based up the datasource defined by the name in the TunnelConfig

func (*TunnelConfig) DatabaseMap

func (tc *TunnelConfig) DatabaseMap() (map[string]*sql.DB, error)

DatabaseMap returns *sql.DBs returns a map of *sql.DBs based upon the DatabaseMap field. Either all dbs defined in the config are returned with no error or no db is returned if an error occurs. Tunnels datasources connect in a lazy fashion so that the connections are not until a database command is called.

Directories

Path Synopsis
Package internal contains a LoadTunnelConfig function that reads either a json or yaml representation of a sshdb.TunnelConfig
Package internal contains a LoadTunnelConfig function that reads either a json or yaml representation of a sshdb.TunnelConfig
Package mssql provides for mssql connection via the sshdb package
Package mssql provides for mssql connection via the sshdb package
Package mysql provide for mysql connections via the sshdb package
Package mysql provide for mysql connections via the sshdb package
Package oracle provide for mysql connections via the sshdb package
Package oracle provide for mysql connections via the sshdb package
Package pgx provides for ssh postgres connections via the github.com/jackc/pgx package
Package pgx provides for ssh postgres connections via the github.com/jackc/pgx package
Package pgxv4 provides for ssh postgres connections via the "github.com/jackc/pgx/v4"
Package pgxv4 provides for ssh postgres connections via the "github.com/jackc/pgx/v4"

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL