From 3dfaa8f1c124213a6d4374f6772391e23752a481 Mon Sep 17 00:00:00 2001 From: Tiago Silva Date: Tue, 29 Aug 2023 10:28:00 +0200 Subject: [PATCH] Respect `[HTTP(S)|NO]_PROXY` envs when dialing directly to Kube via SPDY (#30624) * Respect `[HTTP(S)|NO]_PROXY` envs when dialing directly to Kube via SPDY This PR enables support for proxy env's when dialing directly to the Kubernetes Cluster - `kubernetes_service` and `legacy_proxy` when the cluster is local - for the SPDY protocol used by `kubectl exec` and `kubectl portforward`. PR #30583 introduced support for normal HTTP requests but missed support for SPDY requests. Fixes #30550 Signed-off-by: Tiago Silva * add proxier helper --------- Signed-off-by: Tiago Silva --- lib/kube/proxy/forwarder.go | 28 +++++++++++-- lib/kube/proxy/roundtrip.go | 84 ++++++++++++++++++++++++++++--------- 2 files changed, 88 insertions(+), 24 deletions(-) diff --git a/lib/kube/proxy/forwarder.go b/lib/kube/proxy/forwarder.go index 497d40bf69f..88880cabc51 100644 --- a/lib/kube/proxy/forwarder.go +++ b/lib/kube/proxy/forwarder.go @@ -28,6 +28,7 @@ import ( "io" "net" "net/http" + "net/url" "strconv" "strings" "sync" @@ -50,6 +51,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/util/httpstream" "k8s.io/apimachinery/pkg/util/httpstream/wsstream" + utilnet "k8s.io/apimachinery/pkg/util/net" "k8s.io/client-go/tools/remotecommand" "k8s.io/client-go/transport/spdy" kubeexec "k8s.io/client-go/util/exec" @@ -2070,6 +2072,7 @@ func (f *Forwarder) getExecutor(ctx authContext, sess *clusterSession, req *http if err != nil { return nil, trace.Wrap(err) } + upgradeRoundTripper := NewSpdyRoundTripperWithDialer(roundTripperConfig{ ctx: req.Context(), authCtx: ctx, @@ -2078,6 +2081,7 @@ func (f *Forwarder) getExecutor(ctx authContext, sess *clusterSession, req *http pingPeriod: f.cfg.ConnPingPeriod, originalHeaders: req.Header, useIdentityForwarding: useImpersonation, + proxier: sess.getProxier(), }) rt := http.RoundTripper(upgradeRoundTripper) if sess.kubeAPICreds != nil { @@ -2101,6 +2105,7 @@ func (f *Forwarder) getSPDYDialer(ctx authContext, sess *clusterSession, req *ht if err != nil { return nil, trace.Wrap(err) } + upgradeRoundTripper := NewSpdyRoundTripperWithDialer(roundTripperConfig{ ctx: req.Context(), authCtx: ctx, @@ -2109,6 +2114,7 @@ func (f *Forwarder) getSPDYDialer(ctx authContext, sess *clusterSession, req *ht pingPeriod: f.cfg.ConnPingPeriod, originalHeaders: req.Header, useIdentityForwarding: useImpersonation, + proxier: sess.getProxier(), }) rt := http.RoundTripper(upgradeRoundTripper) if sess.kubeAPICreds != nil { @@ -2205,23 +2211,37 @@ func (s *clusterSession) monitorConn(conn net.Conn, err error) (net.Conn, error) } func (s *clusterSession) Dial(network, addr string) (net.Conn, error) { - return s.monitorConn(s.dial(s.requestContext, network)) + return s.monitorConn(s.dial(s.requestContext, network, addr)) } func (s *clusterSession) DialWithContext(opts ...contextDialerOption) func(ctx context.Context, network, addr string) (net.Conn, error) { return func(ctx context.Context, network, addr string) (net.Conn, error) { - return s.monitorConn(s.dial(ctx, network, opts...)) + return s.monitorConn(s.dial(ctx, network, addr, opts...)) } } -func (s *clusterSession) dial(ctx context.Context, network string, opts ...contextDialerOption) (net.Conn, error) { +func (s *clusterSession) dial(ctx context.Context, network, addr string, opts ...contextDialerOption) (net.Conn, error) { dialer := s.parent.getContextDialerFunc(s, opts...) - conn, err := dialer(ctx, network, s.targetAddr) + conn, err := dialer(ctx, network, addr) return conn, trace.Wrap(err) } +// getProxier returns the proxier function to use for this session. +// If the target cluster is not served by this teleport service, the proxier +// must be nil to avoid using it through the reverse tunnel. +// If the target cluster is served by this teleport service, the proxier +// must be set to the default proxy function. +func (s *clusterSession) getProxier() func(req *http.Request) (*url.URL, error) { + // When the target cluster is not served by this teleport service, the + // proxier must be nil to avoid using it through the reverse tunnel. + if s.kubeAPICreds == nil { + return nil + } + return utilnet.NewProxierWithNoProxyCIDR(http.ProxyFromEnvironment) +} + // TODO(awly): unit test this func (f *Forwarder) newClusterSession(ctx context.Context, authCtx authContext) (*clusterSession, error) { ctx, span := f.cfg.tracer.Start( diff --git a/lib/kube/proxy/roundtrip.go b/lib/kube/proxy/roundtrip.go index 154a042b540..b7b460a7fbb 100644 --- a/lib/kube/proxy/roundtrip.go +++ b/lib/kube/proxy/roundtrip.go @@ -40,8 +40,8 @@ import ( utilnet "k8s.io/apimachinery/pkg/util/net" "k8s.io/apimachinery/third_party/forked/golang/netutil" + apiclient "github.com/gravitational/teleport/api/client" "github.com/gravitational/teleport/lib/auth" - "github.com/gravitational/teleport/lib/utils" ) // SpdyRoundTripper knows how to upgrade an HTTP request to one that supports @@ -88,6 +88,8 @@ type roundTripperConfig struct { // auth.TeleportImpersonateUserHeader and auth.TeleportImpersonateIPHeader // headers instead of relying on the certificate to transport it. useIdentityForwarding bool + + proxier func(*http.Request) (*url.URL, error) } // NewSpdyRoundTripperWithDialer creates a new SpdyRoundTripper that will use @@ -104,7 +106,7 @@ func (s *SpdyRoundTripper) TLSClientConfig() *tls.Config { // Dial implements k8s.io/apimachinery/pkg/util/net.Dialer. func (s *SpdyRoundTripper) Dial(req *http.Request) (net.Conn, error) { - conn, err := s.dial(req.URL) + conn, err := s.dial(req) if err != nil { return nil, err } @@ -118,37 +120,80 @@ func (s *SpdyRoundTripper) Dial(req *http.Request) (net.Conn, error) { } // dial dials the host specified by url, using TLS if appropriate. -func (s *SpdyRoundTripper) dial(url *url.URL) (net.Conn, error) { - dialAddr := netutil.CanonicalAddr(url) - - if url.Scheme == "http" { - switch { - case s.dialWithContext != nil: - return s.dialWithContext(s.ctx, "tcp", dialAddr) - default: - return net.Dial("tcp", dialAddr) +func (s *SpdyRoundTripper) dial(req *http.Request) (conn net.Conn, err error) { + var proxyURL *url.URL + if s.proxier != nil { + proxyURL, err = s.proxier(req) + if err != nil { + return nil, err } } - // TODO validate the TLSClientConfig is set up? - var conn *tls.Conn - var err error - if s.dialWithContext == nil { - conn, err = tls.Dial("tcp", dialAddr, s.tlsConfig) + if proxyURL == nil { + conn, err = s.dialWithoutProxy(req.URL) } else { - conn, err = utils.TLSDial(s.ctx, utils.DialWithContextFunc(s.dialWithContext), "tcp", dialAddr, s.tlsConfig) + conn, err = s.dialWithProxy(req, proxyURL) } if err != nil { return nil, trace.Wrap(err) } + if req.URL.Scheme == "https" { + return s.tlsConn(s.ctx, conn, netutil.CanonicalAddr(req.URL)) + } + return conn, nil +} + +func (s *SpdyRoundTripper) dialWithoutProxy(url *url.URL) (conn net.Conn, err error) { + dialAddr := netutil.CanonicalAddr(url) + switch { + case s.dialWithContext != nil: + conn, err = s.dialWithContext(s.ctx, "tcp", dialAddr) + default: + conn, err = net.Dial("tcp", dialAddr) + } + return conn, trace.Wrap(err) +} + +// tlsConn returns a TLS client side connection using rwc as the underlying transport. +func (s *SpdyRoundTripper) tlsConn(ctx context.Context, rwc net.Conn, targetHost string) (net.Conn, error) { + host, _, err := net.SplitHostPort(targetHost) + if err != nil { + return nil, err + } + + tlsConfig := s.tlsConfig + switch { + case tlsConfig == nil: + tlsConfig = &tls.Config{ServerName: host} + case len(tlsConfig.ServerName) == 0: + tlsConfig = tlsConfig.Clone() + tlsConfig.ServerName = host + } + tlsConn := tls.Client(rwc, tlsConfig) + // Client handshake will verify the server hostname and cert chain. That // way we can err our before first read/write. - if err := conn.Handshake(); err != nil { + if err := tlsConn.HandshakeContext(ctx); err != nil { + tlsConn.Close() return nil, trace.Wrap(err) } - return conn, nil + return tlsConn, nil +} + +// dialWithProxy dials the host specified by url through an http or an socks5 proxy. +func (s *SpdyRoundTripper) dialWithProxy(req *http.Request, proxyURL *url.URL) (net.Conn, error) { + // ensure we use a canonical host with proxyReq + targetHost := netutil.CanonicalAddr(req.URL) + + proxyDialConn, err := apiclient.DialProxyWithDialer( + s.ctx, + proxyURL, + targetHost, + apiclient.ContextDialerFunc(s.dialWithContext), + ) + return proxyDialConn, trace.Wrap(err) } // RoundTrip executes the Request and upgrades it. After a successful upgrade, @@ -193,7 +238,6 @@ func (s *SpdyRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) conn, ), ) - resp, err := http.ReadResponse(responseReader, nil) if err != nil { if conn != nil {