-
Notifications
You must be signed in to change notification settings - Fork 57
/
secure_socks_proxy.go
353 lines (307 loc) · 10.7 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
345
346
347
348
349
350
351
352
353
package proxy
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"net"
"net/http"
"os"
"regexp"
"strconv"
"strings"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"golang.org/x/net/proxy"
)
var (
// PluginSecureSocksProxyEnabled is a constant for the GF_SECURE_SOCKS_DATASOURCE_PROXY_SERVER_ENABLED
// environment variable used to specify if a secure socks proxy is allowed to be used for datasource connections.
PluginSecureSocksProxyEnabled = "GF_SECURE_SOCKS_DATASOURCE_PROXY_SERVER_ENABLED"
// PluginSecureSocksProxyClientCert is a constant for the GF_SECURE_SOCKS_DATASOURCE_PROXY_CLIENT_CERT
// environment variable used to specify the file location of the client cert for the secure socks proxy.
PluginSecureSocksProxyClientCert = "GF_SECURE_SOCKS_DATASOURCE_PROXY_CLIENT_CERT"
// PluginSecureSocksProxyClientKey is a constant for the GF_SECURE_SOCKS_DATASOURCE_PROXY_CLIENT_KEY
// environment variable used to specify the file location of the client key for the secure socks proxy.
PluginSecureSocksProxyClientKey = "GF_SECURE_SOCKS_DATASOURCE_PROXY_CLIENT_KEY"
// PluginSecureSocksProxyRootCACert is a constant for the GF_SECURE_SOCKS_DATASOURCE_PROXY_ROOT_CA_CERT
// environment variable used to specify the file location of the root ca for the secure socks proxy.
PluginSecureSocksProxyRootCACert = "GF_SECURE_SOCKS_DATASOURCE_PROXY_ROOT_CA_CERT"
// PluginSecureSocksProxyProxyAddress is a constant for the GF_SECURE_SOCKS_DATASOURCE_PROXY_PROXY_ADDRESS
// environment variable used to specify the secure socks proxy server address to proxy the connections to.
PluginSecureSocksProxyProxyAddress = "GF_SECURE_SOCKS_DATASOURCE_PROXY_PROXY_ADDRESS"
// PluginSecureSocksProxyServerName is a constant for the GF_SECURE_SOCKS_DATASOURCE_PROXY_SERVER_NAME
// environment variable used to specify the server name of the secure socks proxy.
PluginSecureSocksProxyServerName = "GF_SECURE_SOCKS_DATASOURCE_PROXY_SERVER_NAME"
// PluginSecureSocksProxyAllowInsecure is a constant for the GF_SECURE_SOCKS_DATASOURCE_PROXY_ALLOW_INSECURE
// environment variable used to specify if the proxy should use a TLS dialer.
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"})
)
// 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 {
ClientCert string
ClientKey string
RootCA 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")
}
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), nil
}
func (p *cfgProxyWrapper) getTLSDialer() (*tls.Dialer, error) {
certPool := x509.NewCertPool()
for _, rootCAFile := range strings.Split(p.opts.ClientCfg.RootCA, " ") {
// 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, errors.New("failed to append CA certificate " + 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
}
// getConfigFromEnv gets the needed proxy information from the env variables that Grafana set with the values from the config ini
func getConfigFromEnv() *ClientCfg {
if value, ok := os.LookupEnv(PluginSecureSocksProxyEnabled); ok {
enabled, err := strconv.ParseBool(value)
if err != nil || !enabled {
return nil
}
}
proxyAddress := ""
if value, ok := os.LookupEnv(PluginSecureSocksProxyProxyAddress); ok {
proxyAddress = value
} else {
return nil
}
allowInsecure := false
if value, ok := os.LookupEnv(PluginSecureSocksProxyAllowInsecure); ok {
allowInsecure, _ = strconv.ParseBool(value)
}
// We only need to fill these fields on insecure mode.
if allowInsecure {
return &ClientCfg{
ProxyAddress: proxyAddress,
AllowInsecure: allowInsecure,
}
}
clientCert := ""
if value, ok := os.LookupEnv(PluginSecureSocksProxyClientCert); ok {
clientCert = value
} else {
return nil
}
clientKey := ""
if value, ok := os.LookupEnv(PluginSecureSocksProxyClientKey); ok {
clientKey = value
} else {
return nil
}
rootCA := ""
if value, ok := os.LookupEnv(PluginSecureSocksProxyRootCACert); ok {
rootCA = value
} else {
return nil
}
serverName := ""
if value, ok := os.LookupEnv(PluginSecureSocksProxyServerName); ok {
serverName = value
} else {
return nil
}
return &ClientCfg{
ClientCert: clientCert,
ClientKey: clientKey,
RootCA: rootCA,
ProxyAddress: proxyAddress,
ServerName: serverName,
AllowInsecure: false,
}
}
// 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 {
dialer proxy.Dialer
}
// newInstrumentedSocksDialer creates a new instrumented dialer
func newInstrumentedSocksDialer(dialer proxy.Dialer) proxy.Dialer {
return &instrumentedSocksDialer{
dialer: dialer,
}
}
// 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) {
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 oppErr *net.OpError
switch {
case err == nil:
code = "0"
case errors.As(err, &oppErr):
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 len(unknownCode) > 1:
code = unknownCode[1]
default:
code = "socks_unknown_error"
}
log.DefaultLogger.Error("received oppErr from dialer", "network", n, "addr", addr, "oppErr", oppErr, "code", code)
default:
log.DefaultLogger.Error("received err from dialer", "network", n, "addr", addr, "err", err)
code = "dial_error"
}
secureSocksRequestsDuration.WithLabelValues(code).Observe(time.Since(start).Seconds())
return c, err
}