Files
teleport/lib/utils/proxy/proxy.go
T
2023-12-01 17:48:14 +00:00

213 lines
7.2 KiB
Go

/*
* Teleport
* Copyright (C) 2023 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package proxy
import (
"context"
"net"
"net/url"
"time"
"github.com/gravitational/trace"
"github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
"github.com/gravitational/teleport"
apiclient "github.com/gravitational/teleport/api/client"
tracessh "github.com/gravitational/teleport/api/observability/tracing/ssh"
apiutils "github.com/gravitational/teleport/api/utils"
)
var log = logrus.WithFields(logrus.Fields{
trace.Component: teleport.ComponentConnectProxy,
})
// A Dialer is a means for a client to establish a SSH connection.
type Dialer interface {
// Dial establishes a client connection to a SSH server.
Dial(ctx context.Context, network string, addr string, config *ssh.ClientConfig) (*tracessh.Client, error)
// DialTimeout acts like Dial but takes a timeout.
DialTimeout(ctx context.Context, network, address string, timeout time.Duration) (net.Conn, error)
}
type directDial struct {
// alpnDialer is the dialer used for TLS routing.
alpnDialer apiclient.ContextDialer
// proxyHeaderGetter is used if present to get signed PROXY headers to propagate client's IP.
// Used by proxy's web server to make calls on behalf of connected clients.
proxyHeaderGetter apiclient.PROXYHeaderGetter
}
// Dial returns traced SSH client connection
func (d directDial) Dial(ctx context.Context, network string, addr string, config *ssh.ClientConfig) (*tracessh.Client, error) {
conn, err := d.DialTimeout(ctx, network, addr, config.Timeout)
if err != nil {
return nil, trace.Wrap(err)
}
// Works around the case when net.DialWithTimeout succeeds, but key exchange hangs.
// Setting deadline on connection prevents this case from happening
return tracessh.NewClientConnWithDeadline(ctx, conn, addr, config)
}
// DialTimeout acts like Dial but takes a timeout.
func (d directDial) DialTimeout(ctx context.Context, network, address string, timeout time.Duration) (net.Conn, error) {
if d.alpnDialer != nil {
conn, err := d.alpnDialer.DialContext(ctx, network, address)
return conn, trace.Wrap(err)
}
dialer := apiclient.NewPROXYHeaderDialer(&net.Dialer{
Timeout: timeout,
}, d.proxyHeaderGetter)
conn, err := dialer.DialContext(ctx, network, address)
return conn, trace.Wrap(err)
}
type proxyDial struct {
// proxyHost is the HTTPS proxy address.
proxyURL *url.URL
// insecure is whether to skip certificate validation.
insecure bool
// proxyHeaderGetter is used if present to get signed PROXY headers to propagate client's IP.
// Used by proxy's web server to make calls on behalf of connected clients.
proxyHeaderGetter apiclient.PROXYHeaderGetter
// alpnDialer is the dialer used for TLS routing.
alpnDialer apiclient.ContextDialer
}
// DialTimeout acts like Dial but takes a timeout.
func (d proxyDial) DialTimeout(ctx context.Context, network, address string, timeout time.Duration) (net.Conn, error) {
// Build a proxy connection first.
if timeout > 0 {
timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
ctx = timeoutCtx
}
// ALPN dialer handles proxy URL internally.
if d.alpnDialer != nil {
tlsConn, err := d.alpnDialer.DialContext(ctx, network, address)
return tlsConn, trace.Wrap(err)
}
conn, err := apiclient.DialProxy(ctx, d.proxyURL, address, apiclient.WithInsecureSkipVerify(d.insecure),
apiclient.WithPROXYHeaderGetter(d.proxyHeaderGetter))
if err != nil {
return nil, trace.Wrap(err)
}
return conn, nil
}
// Dial first connects to a proxy, then uses the connection to establish a new
// SSH connection.
func (d proxyDial) Dial(ctx context.Context, network string, addr string, config *ssh.ClientConfig) (*tracessh.Client, error) {
// Build a proxy connection first.
pconn, err := d.DialTimeout(ctx, network, addr, config.Timeout)
if err != nil {
return nil, trace.Wrap(err)
}
if config.Timeout > 0 {
if err := pconn.SetReadDeadline(time.Now().Add(config.Timeout)); err != nil {
return nil, trace.Wrap(err)
}
}
// Do the same as ssh.Dial but pass in proxy connection.
c, chans, reqs, err := tracessh.NewClientConn(ctx, pconn, addr, config)
if err != nil {
return nil, trace.Wrap(err)
}
if config.Timeout > 0 {
if err := pconn.SetReadDeadline(time.Time{}); err != nil {
return nil, trace.Wrap(err)
}
}
return tracessh.NewClient(c, chans, reqs), nil
}
type dialerOptions struct {
// insecureSkipTLSVerify is whether to skip certificate validation.
insecureSkipTLSVerify bool
// alpnDialer is the dialer used for TLS routing.
alpnDialer apiclient.ContextDialer
proxyHeaderGetter apiclient.PROXYHeaderGetter
}
// DialerOptionFunc allows setting options as functional arguments to DialerFromEnvironment
type DialerOptionFunc func(options *dialerOptions)
// WithALPNDialer creates a dialer that allows to Teleport running in single-port mode.
func WithALPNDialer(alpnDialerConfig apiclient.ALPNDialerConfig) DialerOptionFunc {
return func(options *dialerOptions) {
options.alpnDialer = apiclient.NewALPNDialer(alpnDialerConfig)
}
}
// WithInsecureSkipTLSVerify skips the certs verifications.
func WithInsecureSkipTLSVerify(insecure bool) DialerOptionFunc {
return func(options *dialerOptions) {
options.insecureSkipTLSVerify = insecure
}
}
// WithPROXYHeaderGetter adds PROXY headers getter, which is used to propagate client's real IP
func WithPROXYHeaderGetter(proxyHeaderGetter apiclient.PROXYHeaderGetter) DialerOptionFunc {
return func(options *dialerOptions) {
options.proxyHeaderGetter = proxyHeaderGetter
}
}
// DialerFromEnvironment returns a Dial function. If the https_proxy or http_proxy
// environment variable are set, it returns a function that will dial through
// said proxy server. If neither variable is set, it will connect to the SSH
// server directly.
func DialerFromEnvironment(addr string, opts ...DialerOptionFunc) Dialer {
// Try and get proxy addr from the environment.
proxyURL := apiutils.GetProxyURL(addr)
var options dialerOptions
for _, opt := range opts {
opt(&options)
}
// If no proxy settings are in environment return regular ssh dialer,
// otherwise return a proxy dialer.
if proxyURL == nil {
log.Debugf("No proxy set in environment, returning direct dialer.")
return directDial{
alpnDialer: options.alpnDialer,
proxyHeaderGetter: options.proxyHeaderGetter,
}
}
log.Debugf("Found proxy %q in environment, returning proxy dialer.", proxyURL)
return proxyDial{
proxyURL: proxyURL,
insecure: options.insecureSkipTLSVerify,
alpnDialer: options.alpnDialer,
proxyHeaderGetter: options.proxyHeaderGetter,
}
}
type DirectDialerOptFunc func(dial *directDial)