-
Notifications
You must be signed in to change notification settings - Fork 67
/
Copy pathsecure_socks_proxy.go
344 lines (301 loc) · 10.8 KB
/
secure_socks_proxy.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
package proxy
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"net"
"net/http"
"os"
"regexp"
"strings"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"github.com/grafana/grafana-plugin-sdk-go/experimental/status"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"golang.org/x/net/proxy"
)
const (
PluginSecureSocksProxyEnabled = "GF_SECURE_SOCKS_DATASOURCE_PROXY_SERVER_ENABLED"
PluginSecureSocksProxyClientCert = "GF_SECURE_SOCKS_DATASOURCE_PROXY_CLIENT_CERT"
PluginSecureSocksProxyClientCertContents = "GF_SECURE_SOCKS_DATASOURCE_PROXY_CLIENT_CERT_VAL"
PluginSecureSocksProxyClientKey = "GF_SECURE_SOCKS_DATASOURCE_PROXY_CLIENT_KEY"
PluginSecureSocksProxyClientKeyContents = "GF_SECURE_SOCKS_DATASOURCE_PROXY_CLIENT_KEY_VAL"
PluginSecureSocksProxyRootCAs = "GF_SECURE_SOCKS_DATASOURCE_PROXY_ROOT_CA_CERT"
PluginSecureSocksProxyRootCAsContents = "GF_SECURE_SOCKS_DATASOURCE_PROXY_ROOT_CA_CERT_VALS"
PluginSecureSocksProxyProxyAddress = "GF_SECURE_SOCKS_DATASOURCE_PROXY_PROXY_ADDRESS"
PluginSecureSocksProxyServerName = "GF_SECURE_SOCKS_DATASOURCE_PROXY_SERVER_NAME"
PluginSecureSocksProxyAllowInsecure = "GF_SECURE_SOCKS_DATASOURCE_PROXY_ALLOW_INSECURE"
)
var (
socksUnknownError = regexp.MustCompile(`unknown code: (\d+)`)
secureSocksRequestsDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{
Namespace: "grafana",
Name: "secure_socks_requests_duration",
Help: "Duration of requests to the secure socks proxy",
}, []string{"code", "datasource", "datasource_type"})
)
// Client is the main Proxy Client interface.
type Client interface {
SecureSocksProxyEnabled() bool
ConfigureSecureSocksHTTPProxy(transport *http.Transport) error
NewSecureSocksProxyContextDialer() (proxy.Dialer, error)
}
// ClientCfg contains the information needed to allow datasource connections to be
// proxied to a secure socks proxy.
type ClientCfg struct {
// Deprecated: ClientCert is the file path to the client certificate.
ClientCert string
// Deprecated: ClientKey is the file path to the client key.
ClientKey string
// Deprecated: RootCAs is a list of file paths to the root CA certificates.
RootCAs []string
ClientCertVal string
ClientKeyVal string
RootCAsVals []string
ProxyAddress string
ServerName string
AllowInsecure bool
}
// New creates a new proxy client from a given config.
func New(opts *Options) Client {
return &cfgProxyWrapper{
opts: opts,
}
}
type cfgProxyWrapper struct {
opts *Options
}
// SecureSocksProxyEnabled checks if the Grafana instance allows the secure socks proxy to be used
// and the datasource options specify to use the proxy
func (p *cfgProxyWrapper) SecureSocksProxyEnabled() bool {
// it cannot be enabled if it's not enabled on Grafana
if p.opts == nil {
return false
}
// if it's enabled on Grafana, check if the datasource is using it
return (p.opts != nil) && p.opts.Enabled
}
// ConfigureSecureSocksHTTPProxy takes a http.DefaultTransport and wraps it in a socks5 proxy with TLS
// if it is enabled on the datasource and the grafana instance
func (p *cfgProxyWrapper) ConfigureSecureSocksHTTPProxy(transport *http.Transport) error {
if !p.SecureSocksProxyEnabled() {
return nil
}
dialSocksProxy, err := p.NewSecureSocksProxyContextDialer()
if err != nil {
return err
}
contextDialer, ok := dialSocksProxy.(proxy.ContextDialer)
if !ok {
return errors.New("unable to cast socks proxy dialer to context proxy dialer")
}
transport.DialContext = contextDialer.DialContext
return nil
}
// NewSecureSocksProxyContextDialer returns a proxy context dialer that can be used to allow datasource connections to go through a secure socks proxy
func (p *cfgProxyWrapper) NewSecureSocksProxyContextDialer() (proxy.Dialer, error) {
p.opts.setDefaults()
if !p.SecureSocksProxyEnabled() {
return nil, errors.New("proxy not enabled")
}
if p.opts.ClientCfg == nil {
return nil, errors.New("client config is not set")
}
var dialer proxy.Dialer
if p.opts.ClientCfg.AllowInsecure {
dialer = &net.Dialer{
Timeout: p.opts.Timeouts.Timeout,
KeepAlive: p.opts.Timeouts.KeepAlive,
}
} else {
d, err := p.getTLSDialer()
if err != nil {
return nil, fmt.Errorf("instantiating tls dialer: %w", err)
}
dialer = d
}
var auth *proxy.Auth
if p.opts.Auth != nil {
auth = &proxy.Auth{
User: p.opts.Auth.Username,
Password: p.opts.Auth.Password,
}
}
dialSocksProxy, err := proxy.SOCKS5("tcp", p.opts.ClientCfg.ProxyAddress, auth, dialer)
if err != nil {
return nil, err
}
return newInstrumentedSocksDialer(dialSocksProxy, p.opts.DatasourceName, p.opts.DatasourceType), nil
}
func (p *cfgProxyWrapper) getTLSDialer() (*tls.Dialer, error) {
if len(p.opts.ClientCfg.RootCAsVals) == 0 {
// legacy file path support
if len(p.opts.ClientCfg.RootCAs) > 0 {
return p.getTLSDialerFromFiles()
}
return nil, errors.New("one or more root ca are required")
}
certPool := x509.NewCertPool()
for _, rootCA := range p.opts.ClientCfg.RootCAsVals {
pemBytes := []byte(rootCA)
pemDecoded, _ := pem.Decode(pemBytes)
if pemDecoded == nil || pemDecoded.Type != "CERTIFICATE" {
return nil, errors.New("root ca is invalid")
}
if !certPool.AppendCertsFromPEM(pemBytes) {
return nil, errors.New("failed to append CA certificate to pool")
}
}
cert, err := tls.X509KeyPair([]byte(p.opts.ClientCfg.ClientCertVal), []byte(p.opts.ClientCfg.ClientKeyVal))
if err != nil {
return nil, err
}
return &tls.Dialer{
Config: &tls.Config{
Certificates: []tls.Certificate{cert},
ServerName: p.opts.ClientCfg.ServerName,
RootCAs: certPool,
MinVersion: tls.VersionTLS13,
},
NetDialer: &net.Dialer{
Timeout: p.opts.Timeouts.Timeout,
KeepAlive: p.opts.Timeouts.KeepAlive,
},
}, nil
}
// Deprecated: getTLSDialerFromFiles is a helper function that creates a tls.Dialer from the client cert, client key, and root CA files on disk.
// As of Grafana 11 we are moving to using the root CA and client cert/key values instead of files.
func (p *cfgProxyWrapper) getTLSDialerFromFiles() (*tls.Dialer, error) {
certPool := x509.NewCertPool()
for _, rootCAFile := range p.opts.ClientCfg.RootCAs {
// nolint:gosec
// The gosec G304 warning can be ignored because `rootCAFile` comes from config ini
// and we check below if it's the right file type
pemBytes, err := os.ReadFile(rootCAFile)
if err != nil {
return nil, err
}
pemDecoded, _ := pem.Decode(pemBytes)
if pemDecoded == nil || pemDecoded.Type != "CERTIFICATE" {
return nil, errors.New("root ca is invalid")
}
if !certPool.AppendCertsFromPEM(pemBytes) {
return nil, fmt.Errorf("failed to append CA certificate %s", rootCAFile)
}
}
cert, err := tls.LoadX509KeyPair(p.opts.ClientCfg.ClientCert, p.opts.ClientCfg.ClientKey)
if err != nil {
return nil, err
}
return &tls.Dialer{
Config: &tls.Config{
Certificates: []tls.Certificate{cert},
ServerName: p.opts.ClientCfg.ServerName,
RootCAs: certPool,
MinVersion: tls.VersionTLS13,
},
NetDialer: &net.Dialer{
Timeout: p.opts.Timeouts.Timeout,
KeepAlive: p.opts.Timeouts.KeepAlive,
},
}, nil
}
// SecureSocksProxyEnabledOnDS checks the datasource json data for `enableSecureSocksProxy`
// to determine if the secure socks proxy should be enabled on it
func SecureSocksProxyEnabledOnDS(jsonData map[string]interface{}) bool {
res, enabled := jsonData["enableSecureSocksProxy"]
if !enabled {
return false
}
if val, ok := res.(bool); ok {
return val
}
return false
}
// instrumentedSocksDialer is a wrapper around the proxy.Dialer and proxy.DialContext
// that records relevant socks secure socks proxy.
type instrumentedSocksDialer struct {
// datasourceName is the name of the datasource the proxy will be used to communicate with.
datasourceName string
// datasourceType is the type of the datasourceType the proxy will be used to communicate with.
// It should be the value assigned to the type property in a datasourceType provisioning file (e.g mysql, prometheus)
datasourceType string
dialer proxy.Dialer
}
// newInstrumentedSocksDialer creates a new instrumented dialer
func newInstrumentedSocksDialer(dialer proxy.Dialer, datasourceName, datasourceType string) proxy.Dialer {
return &instrumentedSocksDialer{
dialer: dialer,
datasourceName: datasourceName,
datasourceType: datasourceType,
}
}
// Dial -
func (d *instrumentedSocksDialer) Dial(network, addr string) (net.Conn, error) {
return d.DialContext(context.Background(), network, addr)
}
// DialContext -
func (d *instrumentedSocksDialer) DialContext(ctx context.Context, n, addr string) (net.Conn, error) {
if ctx.Err() != nil {
log.DefaultLogger.Debug("context cancelled or deadline exceeded, returning context error")
return nil, ctx.Err()
}
start := time.Now()
dialer, ok := d.dialer.(proxy.ContextDialer)
if !ok {
return nil, errors.New("unable to cast socks proxy dialer to context proxy dialer")
}
c, err := dialer.DialContext(ctx, n, addr)
var code string
var opErr *net.OpError
switch {
case err == nil:
code = "0"
case errors.As(err, &opErr):
unknownCode := socksUnknownError.FindStringSubmatch(err.Error())
// Socks errors defined here: https://cs.opensource.google/go/x/net/+/refs/tags/v0.15.0:internal/socks/socks.go;l=40-63
switch {
case strings.Contains(err.Error(), "general SOCKS server failure"):
code = "1"
case strings.Contains(err.Error(), "connection not allowed by ruleset"):
code = "2"
case strings.Contains(err.Error(), "network unreachable"):
code = "3"
case strings.Contains(err.Error(), "host unreachable"):
code = "4"
case strings.Contains(err.Error(), "connection refused"):
code = "5"
case strings.Contains(err.Error(), "TTL expired"):
code = "6"
case strings.Contains(err.Error(), "command not supported"):
code = "7"
case strings.Contains(err.Error(), "address type not supported"):
code = "8"
case strings.HasSuffix(err.Error(), "EOF"):
code = "eof_error"
case strings.HasSuffix(err.Error(), "i/o timeout"):
code = "io_timeout_error"
case strings.HasSuffix(err.Error(), "context canceled"):
code = "context_canceled_error"
case strings.HasSuffix(err.Error(), "operation was canceled"):
code = "context_canceled_error"
case len(unknownCode) > 1:
code = unknownCode[1]
default:
code = "socks_unknown_error"
}
log.DefaultLogger.Error("received opErr from dialer", "network", n, "addr", addr, "opErr", opErr, "code", code)
default:
log.DefaultLogger.Error("received err from dialer", "network", n, "addr", addr, "err", err)
code = "dial_error"
}
if err != nil {
err = status.DownstreamError(err)
}
secureSocksRequestsDuration.WithLabelValues(code, d.datasourceName, d.datasourceType).Observe(time.Since(start).Seconds())
return c, err
}