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 <tiago.silva@goteleport.com>

* add proxier helper

---------

Signed-off-by: Tiago Silva <tiago.silva@goteleport.com>
This commit is contained in:
Tiago Silva 2023-08-29 10:28:00 +02:00 committed by GitHub
parent c1050a265c
commit 3dfaa8f1c1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 88 additions and 24 deletions

View file

@ -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(

View file

@ -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 {