mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-11-21 14:44:00 +00:00
9feee15493
* Adds custom dial func for HTTP-Connect and socks5 proxy tunnels. Standard golang http.transport exposes GetProxyConnectHeader function, but it doesn't allow to use separate tls config for proxy. It also not possible to enforce HTTP-Connect with standard http lib. * For http scrape targets, by default http.Transport.Proxy function must be used. Since it has special case with full uri forward. * Adds proxy.URL json methods that allow to properly copy internal fields, like User/Password. It should fix bug with proxy_url. When credentials specified at URL was ignored. * Adds tests for scrape client proxy requests related issue https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6771
142 lines
4.1 KiB
Go
142 lines
4.1 KiB
Go
package proxy
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"golang.org/x/net/proxy"
|
|
|
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/netutil"
|
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
|
|
)
|
|
|
|
// NewDialFunc returns dial func for the given u and ac.
|
|
// Dial uses HTTP CONNECT for http and https targets https://en.wikipedia.org/wiki/HTTP_tunnel#HTTP_CONNECT_method
|
|
// And socks5 for socks5 targets https://en.wikipedia.org/wiki/SOCKS
|
|
// supports authorization and tls configuration
|
|
func (u *URL) NewDialFunc(ac *promauth.Config) (func(ctx context.Context, network, addr string) (net.Conn, error), error) {
|
|
pu := u.URL
|
|
if !isURLSchemeValid(pu.Scheme) {
|
|
return nil, fmt.Errorf("unknown scheme=%q for proxy_url=%q, must be in %s", pu.Scheme, pu.Redacted(), validURLSchemes)
|
|
}
|
|
isTLS := (pu.Scheme == "https" || pu.Scheme == "tls+socks5")
|
|
proxyAddr := addMissingPort(pu.Host, isTLS)
|
|
|
|
var tlsCfg *tls.Config
|
|
if isTLS {
|
|
var err error
|
|
tlsCfg, err = ac.GetTLSConfig()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot initialize tls config: %w", err)
|
|
}
|
|
if !tlsCfg.InsecureSkipVerify && tlsCfg.ServerName == "" {
|
|
tlsCfg.ServerName = tlsServerName(proxyAddr)
|
|
}
|
|
}
|
|
if pu.Scheme == "socks5" || pu.Scheme == "tls+socks5" {
|
|
return socks5DialFunc(proxyAddr, pu, tlsCfg)
|
|
}
|
|
dialFunc := func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
proxyConn, err := netutil.DialMaybeSRV(ctx, network, proxyAddr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot connect to proxy %q: %w", pu.Redacted(), err)
|
|
}
|
|
if isTLS {
|
|
proxyConn = tls.Client(proxyConn, tlsCfg)
|
|
}
|
|
hdr := ac.GetHTTPHeadersNoAuth()
|
|
authHeader, err := u.getAuthHeader(ac)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot obtain Proxy-Authorization header: %w", err)
|
|
}
|
|
if len(authHeader) > 0 {
|
|
if hdr == nil {
|
|
hdr = make(http.Header)
|
|
}
|
|
hdr.Add("Proxy-Authorization", authHeader)
|
|
}
|
|
|
|
conn, err := sendConnectRequest(proxyConn, proxyAddr, addr, hdr)
|
|
if err != nil {
|
|
_ = proxyConn.Close()
|
|
return nil, fmt.Errorf("error when sending CONNECT request to proxy %q: %w", pu.Redacted(), err)
|
|
}
|
|
return conn, nil
|
|
}
|
|
return dialFunc, nil
|
|
}
|
|
|
|
func socks5DialFunc(proxyAddr string, pu *url.URL, tlsCfg *tls.Config) (func(ctx context.Context, network, addr string) (net.Conn, error), error) {
|
|
var sac *proxy.Auth
|
|
if pu.User != nil {
|
|
username := pu.User.Username()
|
|
password, _ := pu.User.Password()
|
|
sac = &proxy.Auth{
|
|
User: username,
|
|
Password: password,
|
|
}
|
|
}
|
|
network := netutil.GetTCPNetwork()
|
|
var dialer proxy.Dialer = proxy.Direct
|
|
if tlsCfg != nil {
|
|
dialer = &tls.Dialer{
|
|
Config: tlsCfg,
|
|
}
|
|
}
|
|
d, err := proxy.SOCKS5(network, proxyAddr, sac, dialer)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot create socks5 proxy for url: %s, err: %w", pu.Redacted(), err)
|
|
}
|
|
dialFunc := func(_ context.Context, _, addr string) (net.Conn, error) {
|
|
return d.Dial(network, addr)
|
|
}
|
|
return dialFunc, nil
|
|
}
|
|
|
|
func addMissingPort(addr string, isTLS bool) string {
|
|
if strings.IndexByte(addr, ':') >= 0 {
|
|
return addr
|
|
}
|
|
port := "80"
|
|
if isTLS {
|
|
port = "443"
|
|
}
|
|
return addr + ":" + port
|
|
}
|
|
|
|
func tlsServerName(addr string) string {
|
|
host, _, err := net.SplitHostPort(addr)
|
|
if err != nil {
|
|
return addr
|
|
}
|
|
return host
|
|
}
|
|
|
|
// sendConnectRequest sends CONNECT request to proxyConn for the given addr and headers and returns the established connection to dstAddr.
|
|
func sendConnectRequest(proxyConn net.Conn, proxyAddr, dstAddr string, hdr http.Header) (net.Conn, error) {
|
|
r := &http.Request{
|
|
Method: http.MethodConnect,
|
|
URL: &url.URL{Opaque: dstAddr},
|
|
Host: proxyAddr,
|
|
Header: hdr,
|
|
}
|
|
|
|
if err := r.Write(proxyConn); err != nil {
|
|
return nil, fmt.Errorf("cannot send CONNECT request for dstAddr=%q: %w", dstAddr, err)
|
|
}
|
|
resp, err := http.ReadResponse(bufio.NewReader(proxyConn), r)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot read CONNECT response for dstAddr=%q: %w", dstAddr, err)
|
|
}
|
|
|
|
if statusCode := resp.StatusCode; statusCode != 200 {
|
|
return nil, fmt.Errorf("unexpected status code received: %d; want: 200; response body: %q", statusCode, resp.Status)
|
|
}
|
|
return proxyConn, nil
|
|
}
|