feat(x509_cert): add proxy support (#9319)
This commit is contained in:
parent
d8f2b38b27
commit
65a60855a0
|
|
@ -0,0 +1,140 @@
|
||||||
|
package proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
netProxy "golang.org/x/net/proxy"
|
||||||
|
)
|
||||||
|
|
||||||
|
// httpConnectProxy proxies (only?) TCP over a HTTP tunnel using the CONNECT method
|
||||||
|
type httpConnectProxy struct {
|
||||||
|
forward netProxy.Dialer
|
||||||
|
url *url.URL
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *httpConnectProxy) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
|
// Prevent using UDP
|
||||||
|
if network == "udp" {
|
||||||
|
return nil, fmt.Errorf("cannot proxy %q traffic over HTTP CONNECT", network)
|
||||||
|
}
|
||||||
|
|
||||||
|
var proxyConn net.Conn
|
||||||
|
var err error
|
||||||
|
if dialer, ok := c.forward.(netProxy.ContextDialer); ok {
|
||||||
|
proxyConn, err = dialer.DialContext(ctx, "tcp", c.url.Host)
|
||||||
|
} else {
|
||||||
|
shim := contextDialerShim{c.forward}
|
||||||
|
proxyConn, err = shim.DialContext(ctx, "tcp", c.url.Host)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add and strip http:// to extract authority portion of the URL
|
||||||
|
// since CONNECT doesn't use a full URL. The request header would
|
||||||
|
// look something like: "CONNECT www.influxdata.com:443 HTTP/1.1"
|
||||||
|
requestURL, err := url.Parse("http://" + addr)
|
||||||
|
if err != nil {
|
||||||
|
if err := proxyConn.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
requestURL.Scheme = ""
|
||||||
|
|
||||||
|
// Build HTTP CONNECT request
|
||||||
|
req, err := http.NewRequest(http.MethodConnect, requestURL.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
if err := proxyConn.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Close = false
|
||||||
|
if password, hasAuth := c.url.User.Password(); hasAuth {
|
||||||
|
req.SetBasicAuth(c.url.User.Username(), password)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = req.Write(proxyConn)
|
||||||
|
if err != nil {
|
||||||
|
if err := proxyConn.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.ReadResponse(bufio.NewReader(proxyConn), req)
|
||||||
|
if err != nil {
|
||||||
|
if err := proxyConn.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := resp.Body.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
if err := proxyConn.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to connect to proxy: %q", resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
return proxyConn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *httpConnectProxy) Dial(network, addr string) (net.Conn, error) {
|
||||||
|
return c.DialContext(context.Background(), network, addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHTTPConnectProxy(proxyURL *url.URL, forward netProxy.Dialer) (netProxy.Dialer, error) {
|
||||||
|
return &httpConnectProxy{forward, proxyURL}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Register new proxy types
|
||||||
|
netProxy.RegisterDialerType("http", newHTTPConnectProxy)
|
||||||
|
netProxy.RegisterDialerType("https", newHTTPConnectProxy)
|
||||||
|
}
|
||||||
|
|
||||||
|
// contextDialerShim allows cancellation of the dial from a context even if the underlying
|
||||||
|
// dialer does not implement `proxy.ContextDialer`. Arguably, this shouldn't actually get run,
|
||||||
|
// unless a new proxy type is added that doesn't implement `proxy.ContextDialer`, as all the
|
||||||
|
// standard library dialers implement `proxy.ContextDialer`.
|
||||||
|
type contextDialerShim struct {
|
||||||
|
dialer netProxy.Dialer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cd *contextDialerShim) Dial(network, addr string) (net.Conn, error) {
|
||||||
|
return cd.dialer.Dial(network, addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cd *contextDialerShim) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
|
var (
|
||||||
|
conn net.Conn
|
||||||
|
done = make(chan struct{}, 1)
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
conn, err = cd.dialer.Dial(network, addr)
|
||||||
|
close(done)
|
||||||
|
if conn != nil && ctx.Err() != nil {
|
||||||
|
_ = conn.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
err = ctx.Err()
|
||||||
|
case <-done:
|
||||||
|
}
|
||||||
|
|
||||||
|
return conn, err
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
package proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
netProxy "golang.org/x/net/proxy"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProxiedDialer struct {
|
||||||
|
dialer netProxy.Dialer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pd *ProxiedDialer) Dial(network, addr string) (net.Conn, error) {
|
||||||
|
return pd.dialer.Dial(network, addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pd *ProxiedDialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
|
if contextDialer, ok := pd.dialer.(netProxy.ContextDialer); ok {
|
||||||
|
return contextDialer.DialContext(ctx, network, addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
contextDialer := contextDialerShim{pd.dialer}
|
||||||
|
return contextDialer.DialContext(ctx, network, addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pd *ProxiedDialer) DialTimeout(network, addr string, timeout time.Duration) (net.Conn, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
if timeout.Seconds() != 0 {
|
||||||
|
var cancel context.CancelFunc
|
||||||
|
ctx, cancel = context.WithTimeout(ctx, timeout)
|
||||||
|
defer cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
return pd.DialContext(ctx, network, addr)
|
||||||
|
}
|
||||||
|
|
@ -4,21 +4,54 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
|
"golang.org/x/net/proxy"
|
||||||
)
|
)
|
||||||
|
|
||||||
type HTTPProxy struct {
|
type HTTPProxy struct {
|
||||||
HTTPProxyURL string `toml:"http_proxy_url"`
|
UseSystemProxy bool `toml:"use_system_proxy"`
|
||||||
|
HTTPProxyURL string `toml:"http_proxy_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type proxyFunc func(req *http.Request) (*url.URL, error)
|
type proxyFunc func(req *http.Request) (*url.URL, error)
|
||||||
|
|
||||||
func (p *HTTPProxy) Proxy() (proxyFunc, error) {
|
func (p *HTTPProxy) Proxy() (proxyFunc, error) {
|
||||||
if len(p.HTTPProxyURL) > 0 {
|
if p.UseSystemProxy {
|
||||||
|
return http.ProxyFromEnvironment, nil
|
||||||
|
} else if len(p.HTTPProxyURL) > 0 {
|
||||||
address, err := url.Parse(p.HTTPProxyURL)
|
address, err := url.Parse(p.HTTPProxyURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error parsing proxy url %q: %w", p.HTTPProxyURL, err)
|
return nil, fmt.Errorf("error parsing proxy url %q: %w", p.HTTPProxyURL, err)
|
||||||
}
|
}
|
||||||
return http.ProxyURL(address), nil
|
return http.ProxyURL(address), nil
|
||||||
}
|
}
|
||||||
return http.ProxyFromEnvironment, nil
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type TCPProxy struct {
|
||||||
|
UseProxy bool `toml:"use_proxy"`
|
||||||
|
ProxyURL string `toml:"proxy_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *TCPProxy) Proxy() (*ProxiedDialer, error) {
|
||||||
|
var dialer proxy.Dialer
|
||||||
|
if p.UseProxy {
|
||||||
|
if len(p.ProxyURL) > 0 {
|
||||||
|
parsed, err := url.Parse(p.ProxyURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error parsing proxy url %q: %w", p.ProxyURL, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if dialer, err = proxy.FromURL(parsed, proxy.Direct); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dialer = proxy.FromEnvironment()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dialer = proxy.Direct
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ProxiedDialer{dialer}, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,8 @@ API endpoint. In the following order the plugin will attempt to authenticate.
|
||||||
## ex: endpoint_url = "http://localhost:8000"
|
## ex: endpoint_url = "http://localhost:8000"
|
||||||
# endpoint_url = ""
|
# endpoint_url = ""
|
||||||
|
|
||||||
## Set http_proxy (telegraf uses the system wide proxy settings if it's is not set)
|
## Set http_proxy
|
||||||
|
# use_system_proxy = false
|
||||||
# http_proxy_url = "http://localhost:8888"
|
# http_proxy_url = "http://localhost:8888"
|
||||||
|
|
||||||
# The minimum period for Cloudwatch metrics is 1 minute (60s). However not all
|
# The minimum period for Cloudwatch metrics is 1 minute (60s). However not all
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package cloudwatch
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -360,13 +361,18 @@ func TestUpdateWindow(t *testing.T) {
|
||||||
|
|
||||||
func TestProxyFunction(t *testing.T) {
|
func TestProxyFunction(t *testing.T) {
|
||||||
c := &CloudWatch{
|
c := &CloudWatch{
|
||||||
HTTPProxy: proxy.HTTPProxy{HTTPProxyURL: "http://www.penguins.com"},
|
HTTPProxy: proxy.HTTPProxy{
|
||||||
|
HTTPProxyURL: "http://www.penguins.com",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
proxyFunction, err := c.HTTPProxy.Proxy()
|
proxyFunction, err := c.HTTPProxy.Proxy()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
proxyResult, err := proxyFunction(&http.Request{})
|
u, err := url.Parse("https://monitoring.us-west-1.amazonaws.com/")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
proxyResult, err := proxyFunction(&http.Request{URL: u})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, "www.penguins.com", proxyResult.Host)
|
require.Equal(t, "www.penguins.com", proxyResult.Host)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,10 @@ When using a UDP address as a certificate source, the server must support
|
||||||
# tls_cert = "/etc/telegraf/cert.pem"
|
# tls_cert = "/etc/telegraf/cert.pem"
|
||||||
# tls_key = "/etc/telegraf/key.pem"
|
# tls_key = "/etc/telegraf/key.pem"
|
||||||
# tls_server_name = "myhost.example.org"
|
# tls_server_name = "myhost.example.org"
|
||||||
|
|
||||||
|
## Set the proxy URL
|
||||||
|
# use_proxy = true
|
||||||
|
# proxy_url = "http://localhost:8888"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Metrics
|
## Metrics
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import (
|
||||||
"github.com/influxdata/telegraf"
|
"github.com/influxdata/telegraf"
|
||||||
"github.com/influxdata/telegraf/config"
|
"github.com/influxdata/telegraf/config"
|
||||||
"github.com/influxdata/telegraf/internal/globpath"
|
"github.com/influxdata/telegraf/internal/globpath"
|
||||||
|
"github.com/influxdata/telegraf/plugins/common/proxy"
|
||||||
_tls "github.com/influxdata/telegraf/plugins/common/tls"
|
_tls "github.com/influxdata/telegraf/plugins/common/tls"
|
||||||
"github.com/influxdata/telegraf/plugins/inputs"
|
"github.com/influxdata/telegraf/plugins/inputs"
|
||||||
)
|
)
|
||||||
|
|
@ -38,6 +39,7 @@ type X509Cert struct {
|
||||||
ExcludeRootCerts bool `toml:"exclude_root_certs"`
|
ExcludeRootCerts bool `toml:"exclude_root_certs"`
|
||||||
tlsCfg *tls.Config
|
tlsCfg *tls.Config
|
||||||
_tls.ClientConfig
|
_tls.ClientConfig
|
||||||
|
proxy.TCPProxy
|
||||||
locations []*url.URL
|
locations []*url.URL
|
||||||
globpaths []*globpath.GlobPath
|
globpaths []*globpath.GlobPath
|
||||||
Log telegraf.Logger
|
Log telegraf.Logger
|
||||||
|
|
@ -126,7 +128,12 @@ func (c *X509Cert) getCert(u *url.URL, timeout time.Duration) ([]*x509.Certifica
|
||||||
protocol = "tcp"
|
protocol = "tcp"
|
||||||
fallthrough
|
fallthrough
|
||||||
case "tcp", "tcp4", "tcp6":
|
case "tcp", "tcp4", "tcp6":
|
||||||
ipConn, err := net.DialTimeout(protocol, u.Host, timeout)
|
dialer, err := c.Proxy()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ipConn, err := dialer.DialTimeout(protocol, u.Host, timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/big"
|
"math/big"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
@ -320,6 +322,25 @@ func TestGatherUDPCertIntegration(t *testing.T) {
|
||||||
require.True(t, acc.HasMeasurement("x509_cert"))
|
require.True(t, acc.HasMeasurement("x509_cert"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGatherTCPCert(t *testing.T) {
|
||||||
|
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
m := &X509Cert{
|
||||||
|
Sources: []string{ts.URL},
|
||||||
|
Log: testutil.Logger{},
|
||||||
|
}
|
||||||
|
require.NoError(t, m.Init())
|
||||||
|
|
||||||
|
var acc testutil.Accumulator
|
||||||
|
require.NoError(t, m.Gather(&acc))
|
||||||
|
|
||||||
|
require.Len(t, acc.Errors, 0)
|
||||||
|
require.True(t, acc.HasMeasurement("x509_cert"))
|
||||||
|
}
|
||||||
|
|
||||||
func TestGatherCertIntegration(t *testing.T) {
|
func TestGatherCertIntegration(t *testing.T) {
|
||||||
if testing.Short() {
|
if testing.Short() {
|
||||||
t.Skip("Skipping integration test in short mode")
|
t.Skip("Skipping integration test in short mode")
|
||||||
|
|
@ -327,6 +348,7 @@ func TestGatherCertIntegration(t *testing.T) {
|
||||||
|
|
||||||
m := &X509Cert{
|
m := &X509Cert{
|
||||||
Sources: []string{"https://www.influxdata.com:443"},
|
Sources: []string{"https://www.influxdata.com:443"},
|
||||||
|
Log: testutil.Logger{},
|
||||||
}
|
}
|
||||||
require.NoError(t, m.Init())
|
require.NoError(t, m.Init())
|
||||||
|
|
||||||
|
|
@ -343,6 +365,7 @@ func TestGatherCertMustNotTimeoutIntegration(t *testing.T) {
|
||||||
duration := time.Duration(15) * time.Second
|
duration := time.Duration(15) * time.Second
|
||||||
m := &X509Cert{
|
m := &X509Cert{
|
||||||
Sources: []string{"https://www.influxdata.com:443"},
|
Sources: []string{"https://www.influxdata.com:443"},
|
||||||
|
Log: testutil.Logger{},
|
||||||
Timeout: config.Duration(duration),
|
Timeout: config.Duration(duration),
|
||||||
}
|
}
|
||||||
require.NoError(t, m.Init())
|
require.NoError(t, m.Init())
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,8 @@ This plugin writes to the [Datadog Metrics API][metrics] and requires an
|
||||||
## Write URL override; useful for debugging.
|
## Write URL override; useful for debugging.
|
||||||
# url = "https://app.datadoghq.com/api/v1/series"
|
# url = "https://app.datadoghq.com/api/v1/series"
|
||||||
|
|
||||||
## Set http_proxy (telegraf uses the system wide proxy settings if it isn't set)
|
## Set http_proxy
|
||||||
|
# use_system_proxy = false
|
||||||
# http_proxy_url = "http://localhost:8888"
|
# http_proxy_url = "http://localhost:8888"
|
||||||
|
|
||||||
## Override the default (none) compression used to send data.
|
## Override the default (none) compression used to send data.
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,10 @@ It can output data in any of the [supported output formats][formats].
|
||||||
# socks5_username = "alice"
|
# socks5_username = "alice"
|
||||||
# socks5_password = "pass123"
|
# socks5_password = "pass123"
|
||||||
|
|
||||||
|
## Optional HTTP proxy to use
|
||||||
|
# use_system_proxy = false
|
||||||
|
# http_proxy_url = "http://localhost:8888"
|
||||||
|
|
||||||
## Data format to output.
|
## Data format to output.
|
||||||
## Each data format has it's own unique set of configuration options, read
|
## Each data format has it's own unique set of configuration options, read
|
||||||
## more about them here:
|
## more about them here:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue