mirror of https://github.com/grafana/grafana
Plugins: Add sql support for the secure socks proxy (#64630)
parent
68e38aad6a
commit
10db808ea1
@ -0,0 +1,97 @@ |
||||
package proxy |
||||
|
||||
import ( |
||||
"net/http" |
||||
"os" |
||||
"path/filepath" |
||||
"testing" |
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend" |
||||
"github.com/grafana/grafana/pkg/infra/proxy/proxyutil" |
||||
"github.com/stretchr/testify/assert" |
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
func TestNewSecureSocksProxy(t *testing.T) { |
||||
settings := proxyutil.SetupTestSecureSocksProxySettings(t) |
||||
|
||||
// create empty file for testing invalid configs
|
||||
tempDir := t.TempDir() |
||||
tempEmptyFile := filepath.Join(tempDir, "emptyfile.txt") |
||||
// nolint:gosec
|
||||
// The gosec G304 warning can be ignored because all values come from the test
|
||||
_, err := os.Create(tempEmptyFile) |
||||
require.NoError(t, err) |
||||
|
||||
t.Run("New socks proxy should be properly configured when all settings are valid", func(t *testing.T) { |
||||
require.NoError(t, NewSecureSocksHTTPProxy(settings, &http.Transport{})) |
||||
}) |
||||
|
||||
t.Run("Client cert must be valid", func(t *testing.T) { |
||||
clientCertBefore := settings.ClientCert |
||||
settings.ClientCert = tempEmptyFile |
||||
t.Cleanup(func() { |
||||
settings.ClientCert = clientCertBefore |
||||
}) |
||||
require.Error(t, NewSecureSocksHTTPProxy(settings, &http.Transport{})) |
||||
}) |
||||
|
||||
t.Run("Client key must be valid", func(t *testing.T) { |
||||
clientKeyBefore := settings.ClientKey |
||||
settings.ClientKey = tempEmptyFile |
||||
t.Cleanup(func() { |
||||
settings.ClientKey = clientKeyBefore |
||||
}) |
||||
require.Error(t, NewSecureSocksHTTPProxy(settings, &http.Transport{})) |
||||
}) |
||||
|
||||
t.Run("Root CA must be valid", func(t *testing.T) { |
||||
rootCABefore := settings.RootCA |
||||
settings.RootCA = tempEmptyFile |
||||
t.Cleanup(func() { |
||||
settings.RootCA = rootCABefore |
||||
}) |
||||
require.Error(t, NewSecureSocksHTTPProxy(settings, &http.Transport{})) |
||||
}) |
||||
} |
||||
|
||||
func TestSecureSocksProxyEnabledOnDS(t *testing.T) { |
||||
t.Run("Secure socks proxy should only be enabled when the json data contains enableSecureSocksProxy=true", func(t *testing.T) { |
||||
tests := []struct { |
||||
instanceSettings *backend.AppInstanceSettings |
||||
enabled bool |
||||
}{ |
||||
{ |
||||
instanceSettings: &backend.AppInstanceSettings{ |
||||
JSONData: []byte("{}"), |
||||
}, |
||||
enabled: false, |
||||
}, |
||||
{ |
||||
instanceSettings: &backend.AppInstanceSettings{ |
||||
JSONData: []byte("{ \"enableSecureSocksProxy\": \"nonbool\" }"), |
||||
}, |
||||
enabled: false, |
||||
}, |
||||
{ |
||||
instanceSettings: &backend.AppInstanceSettings{ |
||||
JSONData: []byte("{ \"enableSecureSocksProxy\": false }"), |
||||
}, |
||||
enabled: false, |
||||
}, |
||||
{ |
||||
instanceSettings: &backend.AppInstanceSettings{ |
||||
JSONData: []byte("{ \"enableSecureSocksProxy\": true }"), |
||||
}, |
||||
enabled: true, |
||||
}, |
||||
} |
||||
|
||||
for _, tt := range tests { |
||||
opts, err := tt.instanceSettings.HTTPClientOptions() |
||||
assert.NoError(t, err) |
||||
|
||||
assert.Equal(t, tt.enabled, SecureSocksProxyEnabledOnDS(opts)) |
||||
} |
||||
}) |
||||
} |
||||
@ -0,0 +1,91 @@ |
||||
package mssql |
||||
|
||||
import ( |
||||
"context" |
||||
"database/sql" |
||||
"database/sql/driver" |
||||
"errors" |
||||
|
||||
mssql "github.com/denisenkom/go-mssqldb" |
||||
iproxy "github.com/grafana/grafana/pkg/infra/proxy" |
||||
"github.com/grafana/grafana/pkg/setting" |
||||
"github.com/grafana/grafana/pkg/tsdb/sqleng" |
||||
"github.com/grafana/grafana/pkg/util" |
||||
"golang.org/x/net/proxy" |
||||
"xorm.io/core" |
||||
) |
||||
|
||||
// createMSSQLProxyDriver creates and registers a new sql driver that uses a mssql connector and updates the dialer to
|
||||
// route connections through the secure socks proxy
|
||||
func createMSSQLProxyDriver(settings *setting.SecureSocksDSProxySettings, cnnstr string) (string, error) { |
||||
sqleng.XormDriverMu.Lock() |
||||
defer sqleng.XormDriverMu.Unlock() |
||||
|
||||
// create a unique driver per connection string
|
||||
hash, err := util.Md5SumString(cnnstr) |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
driverName := "mssql-proxy-" + hash |
||||
|
||||
// only register the driver once
|
||||
if core.QueryDriver(driverName) == nil { |
||||
connector, err := mssql.NewConnector(cnnstr) |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
|
||||
driver, err := newMSSQLProxyDriver(settings, connector) |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
sql.Register(driverName, driver) |
||||
core.RegisterDriver(driverName, driver) |
||||
} |
||||
|
||||
return driverName, nil |
||||
} |
||||
|
||||
// mssqlProxyDriver is a regular mssql driver with an updated dialer.
|
||||
// This is needed because there is no way to save a dialer to the mssql driver in xorm
|
||||
type mssqlProxyDriver struct { |
||||
c *mssql.Connector |
||||
} |
||||
|
||||
var _ driver.DriverContext = (*mssqlProxyDriver)(nil) |
||||
var _ core.Driver = (*mssqlProxyDriver)(nil) |
||||
|
||||
// newMSSQLProxyDriver updates the dialer for a mssql connector with a dialer that proxys connections through the secure socks proxy
|
||||
// and returns a new mssql driver to register
|
||||
func newMSSQLProxyDriver(cfg *setting.SecureSocksDSProxySettings, connector *mssql.Connector) (*mssqlProxyDriver, error) { |
||||
dialer, err := iproxy.NewSecureSocksProxyContextDialer(cfg) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
contextDialer, ok := dialer.(proxy.ContextDialer) |
||||
if !ok { |
||||
return nil, errors.New("unable to cast socks proxy dialer to context proxy dialer") |
||||
} |
||||
|
||||
connector.Dialer = contextDialer |
||||
return &mssqlProxyDriver{c: connector}, nil |
||||
} |
||||
|
||||
// Parse uses the xorm mssql dialect for the driver (this has to be implemented to register the driver with xorm)
|
||||
func (d *mssqlProxyDriver) Parse(a string, b string) (*core.Uri, error) { |
||||
sqleng.XormDriverMu.RLock() |
||||
defer sqleng.XormDriverMu.RUnlock() |
||||
|
||||
return core.QueryDriver("mssql").Parse(a, b) |
||||
} |
||||
|
||||
// OpenConnector returns the normal mssql connector that has the updated dialer context
|
||||
func (d *mssqlProxyDriver) OpenConnector(name string) (driver.Connector, error) { |
||||
return d.c, nil |
||||
} |
||||
|
||||
// Open uses the connector with the updated dialer context to open a new connection
|
||||
func (d *mssqlProxyDriver) Open(dsn string) (driver.Conn, error) { |
||||
return d.c.Connect(context.Background()) |
||||
} |
||||
@ -0,0 +1,66 @@ |
||||
package mssql |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
"testing" |
||||
|
||||
mssql "github.com/denisenkom/go-mssqldb" |
||||
"github.com/grafana/grafana/pkg/infra/proxy/proxyutil" |
||||
"github.com/stretchr/testify/require" |
||||
"xorm.io/core" |
||||
) |
||||
|
||||
func TestMSSQLProxyDriver(t *testing.T) { |
||||
settings := proxyutil.SetupTestSecureSocksProxySettings(t) |
||||
dialect := "mssql" |
||||
cnnstr := "server=127.0.0.1;port=1433;user id=sa;password=yourStrong(!)Password;database=db" |
||||
driverName, err := createMSSQLProxyDriver(settings, cnnstr) |
||||
require.NoError(t, err) |
||||
|
||||
t.Run("Driver should not be registered more than once", func(t *testing.T) { |
||||
testDriver, err := createMSSQLProxyDriver(settings, cnnstr) |
||||
require.NoError(t, err) |
||||
require.Equal(t, driverName, testDriver) |
||||
}) |
||||
|
||||
t.Run("A new driver should be created for a new connection string", func(t *testing.T) { |
||||
testDriver, err := createMSSQLProxyDriver(settings, "server=localhost;user id=sa;password=yourStrong(!)Password;database=db2") |
||||
require.NoError(t, err) |
||||
require.NotEqual(t, driverName, testDriver) |
||||
}) |
||||
|
||||
t.Run("Parse should have the same result as xorm mssql parse", func(t *testing.T) { |
||||
xormDriver := core.QueryDriver(dialect) |
||||
xormResult, err := xormDriver.Parse(dialect, cnnstr) |
||||
require.NoError(t, err) |
||||
|
||||
xormNewDriver := core.QueryDriver(driverName) |
||||
xormNewResult, err := xormNewDriver.Parse(dialect, cnnstr) |
||||
require.NoError(t, err) |
||||
require.Equal(t, xormResult, xormNewResult) |
||||
}) |
||||
|
||||
t.Run("Connector should use dialer context that routes through the socks proxy to db", func(t *testing.T) { |
||||
connector, err := mssql.NewConnector(cnnstr) |
||||
require.NoError(t, err) |
||||
driver, err := newMSSQLProxyDriver(settings, connector) |
||||
require.NoError(t, err) |
||||
|
||||
conn, err := driver.OpenConnector(cnnstr) |
||||
require.NoError(t, err) |
||||
|
||||
_, err = conn.Connect(context.Background()) |
||||
require.Contains(t, err.Error(), fmt.Sprintf("socks connect tcp %s->127.0.0.1:1433", settings.ProxyAddress)) |
||||
}) |
||||
|
||||
t.Run("Open should use the connector that routes through the socks proxy to db", func(t *testing.T) { |
||||
connector, err := mssql.NewConnector(cnnstr) |
||||
require.NoError(t, err) |
||||
driver, err := newMSSQLProxyDriver(settings, connector) |
||||
require.NoError(t, err) |
||||
|
||||
_, err = driver.Open(cnnstr) |
||||
require.Contains(t, err.Error(), fmt.Sprintf("socks connect tcp %s->127.0.0.1:1433", settings.ProxyAddress)) |
||||
}) |
||||
} |
||||
@ -0,0 +1,57 @@ |
||||
package mysql |
||||
|
||||
import ( |
||||
"context" |
||||
"net" |
||||
|
||||
"github.com/go-sql-driver/mysql" |
||||
iproxy "github.com/grafana/grafana/pkg/infra/proxy" |
||||
"github.com/grafana/grafana/pkg/setting" |
||||
"github.com/grafana/grafana/pkg/util" |
||||
"golang.org/x/net/proxy" |
||||
) |
||||
|
||||
// registerProxyDialerContext registers a new dialer context to be used by mysql when the proxy network is
|
||||
// specified in the connection string
|
||||
func registerProxyDialerContext(settings *setting.SecureSocksDSProxySettings, protocol, cnnstr string) (string, error) { |
||||
// the dialer contains the true network used behind the scenes
|
||||
dialer, err := getProxyDialerContext(settings, protocol) |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
|
||||
// the dialer context can be updated everytime the datasource is updated
|
||||
// have a unique network per connection string
|
||||
hash, err := util.Md5SumString(cnnstr) |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
network := "proxy-" + hash |
||||
mysql.RegisterDialContext(network, dialer.DialContext) |
||||
|
||||
return network, nil |
||||
} |
||||
|
||||
// mySQLContextDialer turns a golang proxy driver into a MySQL proxy driver
|
||||
type mySQLContextDialer struct { |
||||
dialer proxy.ContextDialer |
||||
network string |
||||
} |
||||
|
||||
// getProxyDialerContext returns a context dialer that will send the request through to the secure socks proxy
|
||||
func getProxyDialerContext(cfg *setting.SecureSocksDSProxySettings, actualNetwork string) (*mySQLContextDialer, error) { |
||||
dialer, err := iproxy.NewSecureSocksProxyContextDialer(cfg) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
contextDialer, ok := dialer.(proxy.ContextDialer) |
||||
if !ok { |
||||
return nil, err |
||||
} |
||||
return &mySQLContextDialer{dialer: contextDialer, network: actualNetwork}, nil |
||||
} |
||||
|
||||
// DialContext implements the MySQL requirements for a proxy driver, and uses the underlying golang proxy driver with the assigned network
|
||||
func (d *mySQLContextDialer) DialContext(ctx context.Context, addr string) (net.Conn, error) { |
||||
return d.dialer.DialContext(ctx, d.network, addr) |
||||
} |
||||
@ -0,0 +1,51 @@ |
||||
package mysql |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
"testing" |
||||
|
||||
"github.com/go-sql-driver/mysql" |
||||
"github.com/grafana/grafana/pkg/infra/proxy/proxyutil" |
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
func TestMySQLProxyDialer(t *testing.T) { |
||||
settings := proxyutil.SetupTestSecureSocksProxySettings(t) |
||||
|
||||
protocol := "tcp" |
||||
network, err := registerProxyDialerContext(settings, protocol, "1") |
||||
require.NoError(t, err) |
||||
driver := mysql.MySQLDriver{} |
||||
dbURL := "localhost:5432" |
||||
cnnstr := fmt.Sprintf("test:test@%s(%s)/db", |
||||
network, |
||||
dbURL, |
||||
) |
||||
t.Run("Network is available", func(t *testing.T) { |
||||
_, err = driver.OpenConnector(cnnstr) |
||||
require.NoError(t, err) |
||||
}) |
||||
|
||||
t.Run("Multiple networks can be created", func(t *testing.T) { |
||||
network, err := registerProxyDialerContext(settings, protocol, "2") |
||||
require.NoError(t, err) |
||||
cnnstr2 := fmt.Sprintf("test:test@%s(%s)/db", |
||||
network, |
||||
dbURL, |
||||
) |
||||
// both networks should exist
|
||||
_, err = driver.OpenConnector(cnnstr) |
||||
require.NoError(t, err) |
||||
_, err = driver.OpenConnector(cnnstr2) |
||||
require.NoError(t, err) |
||||
}) |
||||
|
||||
t.Run("Connection should be routed through socks proxy to db", func(t *testing.T) { |
||||
conn, err := driver.OpenConnector(cnnstr) |
||||
require.NoError(t, err) |
||||
_, err = conn.Connect(context.Background()) |
||||
require.Error(t, err) |
||||
require.Contains(t, err.Error(), fmt.Sprintf("socks connect %s %s->%s", protocol, settings.ProxyAddress, dbURL)) |
||||
}) |
||||
} |
||||
@ -0,0 +1,107 @@ |
||||
package postgres |
||||
|
||||
import ( |
||||
"context" |
||||
"database/sql" |
||||
"database/sql/driver" |
||||
"net" |
||||
"time" |
||||
|
||||
iproxy "github.com/grafana/grafana/pkg/infra/proxy" |
||||
"github.com/grafana/grafana/pkg/setting" |
||||
"github.com/grafana/grafana/pkg/tsdb/sqleng" |
||||
"github.com/grafana/grafana/pkg/util" |
||||
"github.com/lib/pq" |
||||
"golang.org/x/net/proxy" |
||||
"xorm.io/core" |
||||
) |
||||
|
||||
// createPostgresProxyDriver creates and registers a new sql driver that uses a postgres connector and updates the dialer to
|
||||
// route connections through the secure socks proxy
|
||||
func createPostgresProxyDriver(settings *setting.SecureSocksDSProxySettings, cnnstr string) (string, error) { |
||||
sqleng.XormDriverMu.Lock() |
||||
defer sqleng.XormDriverMu.Unlock() |
||||
|
||||
// create a unique driver per connection string
|
||||
hash, err := util.Md5SumString(cnnstr) |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
driverName := "postgres-proxy-" + hash |
||||
|
||||
// only register the driver once
|
||||
if core.QueryDriver(driverName) == nil { |
||||
connector, err := pq.NewConnector(cnnstr) |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
|
||||
driver, err := newPostgresProxyDriver(settings, connector) |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
|
||||
sql.Register(driverName, driver) |
||||
core.RegisterDriver(driverName, driver) |
||||
} |
||||
return driverName, nil |
||||
} |
||||
|
||||
// postgresProxyDriver is a regular postgres driver with an updated dialer.
|
||||
// This is done because there is no way to save a dialer to the postgres driver in xorm
|
||||
type postgresProxyDriver struct { |
||||
c *pq.Connector |
||||
} |
||||
|
||||
var _ driver.DriverContext = (*postgresProxyDriver)(nil) |
||||
var _ core.Driver = (*postgresProxyDriver)(nil) |
||||
|
||||
// newPostgresProxyDriver updates the dialer for a postgres connector with a dialer that proxys connections through the secure socks proxy
|
||||
// and returns a new postgres driver to register
|
||||
func newPostgresProxyDriver(cfg *setting.SecureSocksDSProxySettings, connector *pq.Connector) (*postgresProxyDriver, error) { |
||||
dialer, err := iproxy.NewSecureSocksProxyContextDialer(cfg) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
// update the postgres dialer with the proxy dialer
|
||||
connector.Dialer(&postgresProxyDialer{d: dialer}) |
||||
|
||||
return &postgresProxyDriver{connector}, nil |
||||
} |
||||
|
||||
// postgresProxyDialer implements the postgres dialer using a proxy dialer, as their functions differ slightly
|
||||
type postgresProxyDialer struct { |
||||
d proxy.Dialer |
||||
} |
||||
|
||||
// Dial uses the normal proxy dial function with the updated dialer
|
||||
func (p *postgresProxyDialer) Dial(network, addr string) (c net.Conn, err error) { |
||||
return p.d.Dial(network, addr) |
||||
} |
||||
|
||||
// DialTimeout uses the normal postgres dial timeout function with the updated dialer
|
||||
func (p *postgresProxyDialer) DialTimeout(network, address string, timeout time.Duration) (net.Conn, error) { |
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout) |
||||
defer cancel() |
||||
|
||||
return p.d.(proxy.ContextDialer).DialContext(ctx, network, address) |
||||
} |
||||
|
||||
// Parse uses the xorm postgres dialect for the driver (this has to be implemented to register the driver with xorm)
|
||||
func (d *postgresProxyDriver) Parse(a string, b string) (*core.Uri, error) { |
||||
sqleng.XormDriverMu.RLock() |
||||
defer sqleng.XormDriverMu.RUnlock() |
||||
|
||||
return core.QueryDriver("postgres").Parse(a, b) |
||||
} |
||||
|
||||
// OpenConnector returns the normal postgres connector that has the updated dialer context
|
||||
func (d *postgresProxyDriver) OpenConnector(name string) (driver.Connector, error) { |
||||
return d.c, nil |
||||
} |
||||
|
||||
// Open uses the connector with the updated dialer to open a new connection
|
||||
func (d *postgresProxyDriver) Open(dsn string) (driver.Conn, error) { |
||||
return d.c.Connect(context.Background()) |
||||
} |
||||
@ -0,0 +1,70 @@ |
||||
package postgres |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
"testing" |
||||
|
||||
"github.com/grafana/grafana/pkg/infra/proxy/proxyutil" |
||||
"github.com/lib/pq" |
||||
"github.com/stretchr/testify/require" |
||||
"xorm.io/core" |
||||
) |
||||
|
||||
func TestPostgresProxyDriver(t *testing.T) { |
||||
dialect := "postgres" |
||||
settings := proxyutil.SetupTestSecureSocksProxySettings(t) |
||||
dbURL := "localhost:5432" |
||||
cnnstr := fmt.Sprintf("postgres://auser:password@%s/db?sslmode=disable", dbURL) |
||||
driverName, err := createPostgresProxyDriver(settings, cnnstr) |
||||
require.NoError(t, err) |
||||
|
||||
t.Run("Driver should not be registered more than once", func(t *testing.T) { |
||||
testDriver, err := createPostgresProxyDriver(settings, cnnstr) |
||||
require.NoError(t, err) |
||||
require.Equal(t, driverName, testDriver) |
||||
}) |
||||
|
||||
t.Run("A new driver should be created for a new connection string", func(t *testing.T) { |
||||
testDriver, err := createPostgresProxyDriver(settings, "server=localhost;user id=sa;password=yourStrong(!)Password;database=db2") |
||||
require.NoError(t, err) |
||||
require.NotEqual(t, driverName, testDriver) |
||||
}) |
||||
|
||||
t.Run("Parse should have the same result as xorm mssql parse", func(t *testing.T) { |
||||
xormDriver := core.QueryDriver(dialect) |
||||
xormResult, err := xormDriver.Parse(dialect, cnnstr) |
||||
require.NoError(t, err) |
||||
|
||||
xormNewDriver := core.QueryDriver(driverName) |
||||
xormNewResult, err := xormNewDriver.Parse(dialect, cnnstr) |
||||
require.NoError(t, err) |
||||
require.Equal(t, xormResult, xormNewResult) |
||||
}) |
||||
|
||||
t.Run("Connector should use dialer context that routes through the socks proxy to db", func(t *testing.T) { |
||||
connector, err := pq.NewConnector(cnnstr) |
||||
require.NoError(t, err) |
||||
driver, err := newPostgresProxyDriver(settings, connector) |
||||
require.NoError(t, err) |
||||
|
||||
conn, err := driver.OpenConnector(cnnstr) |
||||
require.NoError(t, err) |
||||
|
||||
_, err = conn.Connect(context.Background()) |
||||
require.Contains(t, err.Error(), fmt.Sprintf("socks connect %s %s->%s", "tcp", settings.ProxyAddress, dbURL)) |
||||
}) |
||||
|
||||
t.Run("Connector should use dialer context that routes through the socks proxy to db", func(t *testing.T) { |
||||
connector, err := pq.NewConnector(cnnstr) |
||||
require.NoError(t, err) |
||||
driver, err := newPostgresProxyDriver(settings, connector) |
||||
require.NoError(t, err) |
||||
|
||||
conn, err := driver.OpenConnector(cnnstr) |
||||
require.NoError(t, err) |
||||
|
||||
_, err = conn.Connect(context.Background()) |
||||
require.Contains(t, err.Error(), fmt.Sprintf("socks connect %s %s->%s", "tcp", settings.ProxyAddress, dbURL)) |
||||
}) |
||||
} |
||||
Loading…
Reference in new issue