Skip to content

Commit 9e36be7

Browse files
Add secure socks proxy to sdk (#667)
1 parent 603cb95 commit 9e36be7

File tree

7 files changed

+567
-0
lines changed

7 files changed

+567
-0
lines changed

backend/http_settings.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"time"
77

88
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
9+
"github.com/grafana/grafana-plugin-sdk-go/backend/proxy"
910
)
1011

1112
// HTTPSettings is a convenient struct for holding decoded HTTP settings from
@@ -45,6 +46,10 @@ type HTTPSettings struct {
4546
SigV4AccessKey string
4647
SigV4SecretKey string
4748

49+
SecureSocksProxyEnabled bool
50+
SecureSocksProxyUsername string
51+
SecureSocksProxyPass string
52+
4853
JSONData map[string]interface{}
4954
SecureJSONData map[string]string
5055
}
@@ -98,6 +103,20 @@ func (s *HTTPSettings) HTTPClientOptions() httpclient.Options {
98103
}
99104
}
100105

106+
if s.SecureSocksProxyEnabled {
107+
opts.ProxyOptions = &proxy.Options{
108+
Enabled: s.SecureSocksProxyEnabled,
109+
Timeouts: &proxy.TimeoutOptions{
110+
Timeout: s.Timeout,
111+
KeepAlive: s.KeepAlive,
112+
},
113+
Auth: &proxy.AuthOptions{
114+
Username: s.SecureSocksProxyUsername,
115+
Password: s.SecureSocksProxyPass,
116+
},
117+
}
118+
}
119+
101120
return opts
102121
}
103122

@@ -265,6 +284,20 @@ func parseHTTPSettings(jsonData json.RawMessage, secureJSONData map[string]strin
265284
}
266285
}
267286

287+
// secure socks proxy
288+
if v, exists := dat["enableSecureSocksProxy"]; exists {
289+
s.SecureSocksProxyEnabled = v.(bool)
290+
}
291+
292+
if s.SecureSocksProxyEnabled {
293+
if v, exists := dat["secureSocksProxyUsername"]; exists {
294+
s.SecureSocksProxyUsername = v.(string)
295+
}
296+
if v, exists := secureJSONData["secureSocksProxyPassword"]; exists {
297+
s.SecureSocksProxyPass = v
298+
}
299+
}
300+
268301
// headers
269302
index := 1
270303
for {

backend/httpclient/http_client.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"errors"
77
"net"
88
"net/http"
9+
10+
"github.com/grafana/grafana-plugin-sdk-go/backend/proxy"
911
)
1012

1113
// New creates a new http.Client.
@@ -84,6 +86,11 @@ func GetTransport(opts ...Options) (http.RoundTripper, error) {
8486
clientOpts.Middlewares = clientOpts.ConfigureMiddleware(clientOpts, clientOpts.Middlewares)
8587
}
8688

89+
err = proxy.ConfigureSecureSocksHTTPProxy(transport, clientOpts.ProxyOptions)
90+
if err != nil {
91+
return nil, err
92+
}
93+
8794
return roundTripperFromMiddlewares(clientOpts, clientOpts.Middlewares, transport), nil
8895
}
8996

@@ -152,6 +159,17 @@ func createOptions(providedOpts ...Options) Options {
152159
opts.Middlewares = DefaultMiddlewares()
153160
}
154161

162+
if proxy.SecureSocksProxyEnabled(opts.ProxyOptions) {
163+
// default username is the datasource uid, this can be updated
164+
// by setting `secureSocksProxyUsername` in the datasource json
165+
if opts.ProxyOptions.Auth == nil {
166+
opts.ProxyOptions.Auth = &proxy.AuthOptions{}
167+
}
168+
if opts.ProxyOptions.Auth.Username == "" {
169+
opts.ProxyOptions.Auth.Username = opts.Labels["datasource_uid"]
170+
}
171+
}
172+
155173
return opts
156174
}
157175

backend/httpclient/options.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"crypto/tls"
55
"net/http"
66
"time"
7+
8+
"github.com/grafana/grafana-plugin-sdk-go/backend/proxy"
79
)
810

911
// ConfigureClientFunc function signature for configuring http.Client.
@@ -30,6 +32,9 @@ type Options struct {
3032
TLS *TLSOptions
3133
SigV4 *SigV4Config
3234

35+
// Proxy related options.
36+
ProxyOptions *proxy.Options
37+
3338
// Headers custom headers.
3439
Headers map[string]string
3540

backend/proxy/doc.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Package proxy provides utilities for updating a data source plugin connection to go through a proxy.
2+
package proxy

backend/proxy/options.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package proxy
2+
3+
import "time"
4+
5+
// Options defines per datasource options for creating the proxy dialer.
6+
type Options struct {
7+
Enabled bool
8+
Auth *AuthOptions
9+
Timeouts *TimeoutOptions
10+
}
11+
12+
// AuthOptions socks5 username and password options.
13+
// Every datasource can have separate credentials to the proxy.
14+
type AuthOptions struct {
15+
Username string
16+
Password string
17+
}
18+
19+
// TimeoutOptions timeout/connection options.
20+
type TimeoutOptions struct {
21+
Timeout time.Duration
22+
KeepAlive time.Duration
23+
}
24+
25+
// DefaultTimeoutOptions default timeout/connection options for the proxy.
26+
var DefaultTimeoutOptions = TimeoutOptions{
27+
Timeout: 30 * time.Second,
28+
KeepAlive: 30 * time.Second,
29+
}
30+
31+
func createOptions(providedOpts *Options) Options {
32+
var opts Options
33+
if providedOpts == nil {
34+
return opts
35+
}
36+
37+
opts = *providedOpts
38+
if opts.Timeouts == nil {
39+
opts.Timeouts = &DefaultTimeoutOptions
40+
}
41+
42+
return opts
43+
}
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
package proxy
2+
3+
import (
4+
"crypto/tls"
5+
"crypto/x509"
6+
"encoding/pem"
7+
"errors"
8+
"fmt"
9+
"net"
10+
"net/http"
11+
"os"
12+
"strconv"
13+
"strings"
14+
15+
"golang.org/x/net/proxy"
16+
)
17+
18+
var (
19+
20+
// PluginSecureSocksProxyEnabled is a constant for the GF_SECURE_SOCKS_DATASOURCE_PROXY_SERVER_ENABLED
21+
// environment variable used to specify if a secure socks proxy is allowed to be used for datasource connections.
22+
PluginSecureSocksProxyEnabled = "GF_SECURE_SOCKS_DATASOURCE_PROXY_SERVER_ENABLED"
23+
// PluginSecureSocksProxyClientCert is a constant for the GF_SECURE_SOCKS_DATASOURCE_PROXY_CLIENT_CERT
24+
// environment variable used to specify the file location of the client cert for the secure socks proxy.
25+
PluginSecureSocksProxyClientCert = "GF_SECURE_SOCKS_DATASOURCE_PROXY_CLIENT_CERT"
26+
// PluginSecureSocksProxyClientKey is a constant for the GF_SECURE_SOCKS_DATASOURCE_PROXY_CLIENT_KEY
27+
// environment variable used to specify the file location of the client key for the secure socks proxy.
28+
PluginSecureSocksProxyClientKey = "GF_SECURE_SOCKS_DATASOURCE_PROXY_CLIENT_KEY"
29+
// PluginSecureSocksProxyRootCACert is a constant for the GF_SECURE_SOCKS_DATASOURCE_PROXY_ROOT_CA_CERT
30+
// environment variable used to specify the file location of the root ca for the secure socks proxy.
31+
PluginSecureSocksProxyRootCACert = "GF_SECURE_SOCKS_DATASOURCE_PROXY_ROOT_CA_CERT"
32+
// PluginSecureSocksProxyProxyAddress is a constant for the GF_SECURE_SOCKS_DATASOURCE_PROXY_PROXY_ADDRESS
33+
// environment variable used to specify the secure socks proxy server address to proxy the connections to.
34+
PluginSecureSocksProxyProxyAddress = "GF_SECURE_SOCKS_DATASOURCE_PROXY_PROXY_ADDRESS"
35+
// PluginSecureSocksProxyServerName is a constant for the GF_SECURE_SOCKS_DATASOURCE_PROXY_SERVER_NAME
36+
// environment variable used to specify the server name of the secure socks proxy.
37+
PluginSecureSocksProxyServerName = "GF_SECURE_SOCKS_DATASOURCE_PROXY_SERVER_NAME"
38+
)
39+
40+
// SecureSocksProxyConfig contains the information needed to allow datasource connections to be
41+
// proxied to a secure socks proxy
42+
type secureSocksProxyConfig struct {
43+
clientCert string
44+
clientKey string
45+
rootCA string
46+
proxyAddress string
47+
serverName string
48+
}
49+
50+
// SecureSocksProxyEnabled checks if the Grafana instance allows the secure socks proxy to be used
51+
// and the datasource options specify to use the proxy
52+
func SecureSocksProxyEnabled(opts *Options) bool {
53+
if opts == nil {
54+
return false
55+
}
56+
57+
if !opts.Enabled {
58+
return false
59+
}
60+
61+
if value, ok := os.LookupEnv(PluginSecureSocksProxyEnabled); ok {
62+
enabledOnInst, err := strconv.ParseBool(value)
63+
if err != nil {
64+
return false
65+
}
66+
67+
return enabledOnInst
68+
}
69+
70+
return false
71+
}
72+
73+
// SecureSocksProxyEnabledOnDS checks the datasource json data for `enableSecureSocksProxy`
74+
// to determine if the secure socks proxy should be enabled on it
75+
func SecureSocksProxyEnabledOnDS(jsonData map[string]interface{}) bool {
76+
res, enabled := jsonData["enableSecureSocksProxy"]
77+
if !enabled {
78+
return false
79+
}
80+
81+
if val, ok := res.(bool); ok {
82+
return val
83+
}
84+
85+
return false
86+
}
87+
88+
// ConfigureSecureSocksHTTPProxy takes a http.DefaultTransport and wraps it in a socks5 proxy with TLS
89+
// if it is enabled on the datasource and the grafana instance
90+
func ConfigureSecureSocksHTTPProxy(transport *http.Transport, opts *Options) error {
91+
if !SecureSocksProxyEnabled(opts) {
92+
return nil
93+
}
94+
95+
dialSocksProxy, err := NewSecureSocksProxyContextDialer(opts)
96+
if err != nil {
97+
return err
98+
}
99+
100+
contextDialer, ok := dialSocksProxy.(proxy.ContextDialer)
101+
if !ok {
102+
return errors.New("unable to cast socks proxy dialer to context proxy dialer")
103+
}
104+
105+
transport.DialContext = contextDialer.DialContext
106+
return nil
107+
}
108+
109+
// NewSecureSocksProxyContextDialer returns a proxy context dialer that can be used to allow datasource connections to go through a secure socks proxy
110+
func NewSecureSocksProxyContextDialer(opts *Options) (proxy.Dialer, error) {
111+
var err error
112+
cfg, err := getConfigFromEnv()
113+
if err != nil {
114+
return nil, err
115+
}
116+
117+
clientOpts := createOptions(opts)
118+
119+
certPool := x509.NewCertPool()
120+
for _, rootCAFile := range strings.Split(cfg.rootCA, " ") {
121+
// nolint:gosec
122+
// The gosec G304 warning can be ignored because `rootCAFile` comes from config ini
123+
// and we check below if it's the right file type
124+
pemBytes, err := os.ReadFile(rootCAFile)
125+
if err != nil {
126+
return nil, err
127+
}
128+
129+
pemDecoded, _ := pem.Decode(pemBytes)
130+
if pemDecoded == nil || pemDecoded.Type != "CERTIFICATE" {
131+
return nil, errors.New("root ca is invalid")
132+
}
133+
134+
if !certPool.AppendCertsFromPEM(pemBytes) {
135+
return nil, errors.New("failed to append CA certificate " + rootCAFile)
136+
}
137+
}
138+
139+
cert, err := tls.LoadX509KeyPair(cfg.clientCert, cfg.clientKey)
140+
if err != nil {
141+
return nil, err
142+
}
143+
144+
tlsDialer := &tls.Dialer{
145+
Config: &tls.Config{
146+
Certificates: []tls.Certificate{cert},
147+
ServerName: cfg.serverName,
148+
RootCAs: certPool,
149+
MinVersion: tls.VersionTLS13,
150+
},
151+
NetDialer: &net.Dialer{
152+
Timeout: clientOpts.Timeouts.Timeout,
153+
KeepAlive: clientOpts.Timeouts.KeepAlive,
154+
},
155+
}
156+
157+
var dsInfo *proxy.Auth
158+
if clientOpts.Auth != nil {
159+
dsInfo = &proxy.Auth{
160+
User: clientOpts.Auth.Username,
161+
Password: clientOpts.Auth.Password,
162+
}
163+
}
164+
165+
dialSocksProxy, err := proxy.SOCKS5("tcp", cfg.proxyAddress, dsInfo, tlsDialer)
166+
if err != nil {
167+
return nil, err
168+
}
169+
170+
return dialSocksProxy, nil
171+
}
172+
173+
// getConfigFromEnv gets the needed proxy information from the env variables that Grafana set with the values from the config ini
174+
func getConfigFromEnv() (*secureSocksProxyConfig, error) {
175+
clientCert := ""
176+
if value, ok := os.LookupEnv(PluginSecureSocksProxyClientCert); ok {
177+
clientCert = value
178+
} else {
179+
return nil, fmt.Errorf("missing client cert")
180+
}
181+
182+
clientKey := ""
183+
if value, ok := os.LookupEnv(PluginSecureSocksProxyClientKey); ok {
184+
clientKey = value
185+
} else {
186+
return nil, fmt.Errorf("missing client key")
187+
}
188+
189+
rootCA := ""
190+
if value, ok := os.LookupEnv(PluginSecureSocksProxyRootCACert); ok {
191+
rootCA = value
192+
} else {
193+
return nil, fmt.Errorf("missing root ca")
194+
}
195+
196+
proxyAddress := ""
197+
if value, ok := os.LookupEnv(PluginSecureSocksProxyProxyAddress); ok {
198+
proxyAddress = value
199+
} else {
200+
return nil, fmt.Errorf("missing proxy address")
201+
}
202+
203+
serverName := ""
204+
if value, ok := os.LookupEnv(PluginSecureSocksProxyServerName); ok {
205+
serverName = value
206+
} else {
207+
return nil, fmt.Errorf("missing server name")
208+
}
209+
210+
return &secureSocksProxyConfig{
211+
clientCert: clientCert,
212+
clientKey: clientKey,
213+
rootCA: rootCA,
214+
proxyAddress: proxyAddress,
215+
serverName: serverName,
216+
}, nil
217+
}

0 commit comments

Comments
 (0)