sshdb

package module
v0.8.0 Latest Latest
Warning

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

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

README

sshdb

Go Reference Build Status codecov Go Report Card

A pure go library that provides an ssh wrapper (using golang.org/x/crypt) for connecting a database client to a remote database.

Packages for use with the following databases packages are included:

install

go get -v github.com/jfcote87

making connections

Initialize a TunnelConfig directly or via yaml or json formats.
A TunnelConfig defines the ssh tunnel as well as one to multiple database connections (dsn strings). Example of yaml definitions may be found in ExampleConfig func and in the testfiles/config directory.

var config sshdb.TunnelConfig
if err := yaml.Unmarshal([]byte(cfg_yaml), &config); err != nil {
	log.Fatalf("yaml decode failed: %v", err)
}
dbs, err := config.DatabaseMap()
if err != nil {
	log.Fatalf("opendbs fail: %v", err)
}
dbs["remoteDB"].Ping()

Otherwise create a tunnel directly and open connectors as needed. This method makes the most sense if TunnelConfig does not have sufficient ssh parameters to define the tunnel.

exampleCfg := &ssh.ClientConfig{
	User:            "jfcote87",
	Auth:            []ssh.AuthMethod{ssh.Password("my second favorite password")},
	HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
// New creates a "tunnel" for 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 abdatabase'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 databse connection in by 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
}

A Driver creates connectors that use the passed dialer to make connections via the ssh tunnel.

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 tunnelDriver is a sshdb.Driver for a specific database type. For included implementations (mysql, mssql, pgx and pgx4) use <package name>.TunnelDriver. remoteHostPort defines the remoteanother 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/lissteron/sshdb"
	"github.com/lissteron/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", "local: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 connect to multiple remote hosts simultaneously

package main

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

	"github.com/lissteron/sshdb"
	"github.com/lissteron/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, net, 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 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 connection.

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"`
}

TunnelConfig describes an ssh connection to a remote host and the databases accessed via the connection

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 connects in a lazy fashion so the connections are not created until needed.

Directories

Path Synopsis
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 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