diff --git a/api/types/constants.go b/api/types/constants.go index ba7785d2ff945..53abcd3208429 100644 --- a/api/types/constants.go +++ b/api/types/constants.go @@ -1066,10 +1066,10 @@ const ( // group they should attempt to be connected to. ProxyGroupGenerationLabel = TeleportInternalLabelPrefix + "proxygroup-gen" - // ProxyPeerQUICLabel is the internal-user label for proxy heartbeats that's - // used to signal that the proxy supports receiving proxy peering - // connections over QUIC. - ProxyPeerQUICLabel = TeleportInternalLabelPrefix + "proxy-peer-quic" + // UnstableProxyPeerQUICLabel is the internal-use label for proxy heartbeats + // that's used to signal that the proxy supports receiving proxy peering + // connections over QUIC. The value should be "yes". + UnstableProxyPeerQUICLabel = TeleportInternalLabelPrefix + "proxy-peer-quic" // OktaAppNameLabel is the individual app name label. OktaAppNameLabel = TeleportInternalLabelPrefix + "okta-app-name" diff --git a/lib/config/configuration.go b/lib/config/configuration.go index 6f3f4d3fea56a..a2c86f977bd9c 100644 --- a/lib/config/configuration.go +++ b/lib/config/configuration.go @@ -2730,6 +2730,11 @@ func Configure(clf *CommandLineFlags, cfg *servicecfg.Config, legacyAppFlags boo cfg.DebugService.Enabled = false } + // TODO(espadolini): allow this when the implementation is merged + if false && os.Getenv("TELEPORT_UNSTABLE_QUIC_PROXY_PEERING") == "yes" { + cfg.Proxy.QUICProxyPeering = true + } + return nil } diff --git a/lib/proxy/clusterdial/dial.go b/lib/proxy/clusterdial/dial.go deleted file mode 100644 index dc8ce3f4b1f73..0000000000000 --- a/lib/proxy/clusterdial/dial.go +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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 . - */ - -package clusterdial - -import ( - "net" - - "github.com/gravitational/trace" - - "github.com/gravitational/teleport/lib/proxy/peer" - "github.com/gravitational/teleport/lib/reversetunnelclient" -) - -// ClusterDialerFunc is a function that implements a peer.ClusterDialer. -type ClusterDialerFunc func(clusterName string, request peer.DialParams) (net.Conn, error) - -// Dial dials makes a dial request to the given cluster. -func (f ClusterDialerFunc) Dial(clusterName string, request peer.DialParams) (net.Conn, error) { - return f(clusterName, request) -} - -// NewClusterDialer implements proxy.ClusterDialer for a reverse tunnel server. -func NewClusterDialer(server reversetunnelclient.Server) ClusterDialerFunc { - return func(clusterName string, request peer.DialParams) (net.Conn, error) { - site, err := server.GetSite(clusterName) - if err != nil { - return nil, trace.Wrap(err) - } - - dialParams := reversetunnelclient.DialParams{ - ServerID: request.ServerID, - ConnType: request.ConnType, - From: request.From, - To: request.To, - FromPeerProxy: true, - } - - conn, err := site.Dial(dialParams) - if err != nil { - return nil, trace.Wrap(err) - } - return conn, nil - } -} diff --git a/lib/proxy/peer/client.go b/lib/proxy/peer/client.go index fe3659a92bf4a..f9e9311088d6c 100644 --- a/lib/proxy/peer/client.go +++ b/lib/proxy/peer/client.go @@ -44,6 +44,7 @@ import ( streamutils "github.com/gravitational/teleport/api/utils/grpc/stream" "github.com/gravitational/teleport/lib/auth/authclient" "github.com/gravitational/teleport/lib/defaults" + "github.com/gravitational/teleport/lib/proxy/peer/internal" "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/utils" ) @@ -94,11 +95,11 @@ type ClientConfig struct { } // connShuffler shuffles the order of client connections. -type connShuffler func([]clientConn) +type connShuffler func([]internal.ClientConn) // randomConnShuffler returns a conn shuffler that randomizes the order of connections. func randomConnShuffler() connShuffler { - return func(conns []clientConn) { + return func(conns []internal.ClientConn) { rand.Shuffle(len(conns), func(i, j int) { conns[i], conns[j] = conns[j], conns[i] }) @@ -107,7 +108,7 @@ func randomConnShuffler() connShuffler { // noopConnShutffler returns a conn shuffler that keeps the original connection ordering. func noopConnShuffler() connShuffler { - return func([]clientConn) {} + return func([]internal.ClientConn) {} } // checkAndSetDefaults checks and sets default values @@ -163,32 +164,6 @@ func (c *ClientConfig) checkAndSetDefaults() error { return nil } -// clientConn manages client connections to a specific peer proxy (with a fixed -// host ID and address). -type clientConn interface { - // peerID returns the host ID of the peer proxy. - peerID() string - // peerAddr returns the address of the peer proxy. - peerAddr() string - - // dial opens a connection of a given tunnel type to a node with the given - // ID through the peer proxy managed by the clientConn. - dial( - nodeID string, - src net.Addr, - dst net.Addr, - tunnelType types.TunnelType, - ) (net.Conn, error) - - // close closes all connections and releases any background resources - // immediately. - close() error - - // shutdown waits until all connections are closed or the context is done, - // then acts like close. - shutdown(context.Context) -} - // grpcClientConn manages client connections to a specific peer proxy over gRPC. type grpcClientConn struct { cc *grpc.ClientConn @@ -205,13 +180,13 @@ type grpcClientConn struct { count int } -var _ clientConn = (*grpcClientConn)(nil) +var _ internal.ClientConn = (*grpcClientConn)(nil) -// peerID implements [clientConn]. -func (c *grpcClientConn) peerID() string { return c.id } +// PeerID implements [internal.ClientConn]. +func (c *grpcClientConn) PeerID() string { return c.id } -// peerAddr implements [clientConn]. -func (c *grpcClientConn) peerAddr() string { return c.addr } +// PeerAddr implements [internal.ClientConn]. +func (c *grpcClientConn) PeerAddr() string { return c.addr } // maybeAcquire returns a non-nil release func if the grpcClientConn is // currently allowed to open connections; i.e., if it hasn't fully shut down. @@ -234,8 +209,8 @@ func (c *grpcClientConn) maybeAcquire() (release func()) { }) } -// shutdown implements [clientConn]. -func (c *grpcClientConn) shutdown(ctx context.Context) { +// Shutdown implements [internal.ClientConn]. +func (c *grpcClientConn) Shutdown(ctx context.Context) { defer c.cc.Close() c.mu.Lock() @@ -255,13 +230,13 @@ func (c *grpcClientConn) shutdown(ctx context.Context) { } } -// close implements [clientConn]. -func (c *grpcClientConn) close() error { +// Close implements [internal.ClientConn]. +func (c *grpcClientConn) Close() error { return c.cc.Close() } -// dial implements [clientConn]. -func (c *grpcClientConn) dial( +// Dial implements [internal.ClientConn]. +func (c *grpcClientConn) Dial( nodeID string, src net.Addr, dst net.Addr, @@ -335,7 +310,7 @@ type Client struct { cancel context.CancelFunc config ClientConfig - conns map[string]clientConn + conns map[string]internal.ClientConn metrics *clientMetrics reporter *reporter } @@ -360,7 +335,7 @@ func NewClient(config ClientConfig) (*Client, error) { config: config, ctx: closeContext, cancel: cancel, - conns: make(map[string]clientConn), + conns: make(map[string]internal.ClientConn), metrics: metrics, reporter: reporter, } @@ -453,7 +428,7 @@ func (c *Client) updateConnections(proxies []types.Server) error { } var toDelete []string - toKeep := make(map[string]clientConn) + toKeep := make(map[string]internal.ClientConn) for id, conn := range c.conns { proxy, ok := toDial[id] @@ -464,7 +439,7 @@ func (c *Client) updateConnections(proxies []types.Server) error { } // peer address changed - if conn.peerAddr() != proxy.GetPeerAddr() { + if conn.PeerAddr() != proxy.GetPeerAddr() { toDelete = append(toDelete, id) continue } @@ -485,8 +460,8 @@ func (c *Client) updateConnections(proxies []types.Server) error { } // establish new connections - _, supportsQuic := proxy.GetLabel(types.ProxyPeerQUICLabel) - conn, err := c.connect(id, proxy.GetPeerAddr(), supportsQuic) + supportsQUIC, _ := proxy.GetLabel(types.UnstableProxyPeerQUICLabel) + conn, err := c.connect(id, proxy.GetPeerAddr(), supportsQUIC == "yes") if err != nil { c.metrics.reportTunnelError(errorProxyPeerTunnelDial) c.config.Log.DebugContext(c.ctx, "error dialing peer proxy", "peer_id", id, "peer_addr", proxy.GetPeerAddr()) @@ -503,7 +478,7 @@ func (c *Client) updateConnections(proxies []types.Server) error { for _, id := range toDelete { if conn, ok := c.conns[id]; ok { - go conn.shutdown(c.ctx) + go conn.Shutdown(c.ctx) } } c.conns = toKeep @@ -556,9 +531,9 @@ func (c *Client) Shutdown(ctx context.Context) { var wg sync.WaitGroup for _, conn := range c.conns { wg.Add(1) - go func(conn clientConn) { + go func(conn internal.ClientConn) { defer wg.Done() - conn.shutdown(ctx) + conn.Shutdown(ctx) }(conn) } wg.Wait() @@ -572,7 +547,7 @@ func (c *Client) Stop() error { var errs []error for _, conn := range c.conns { - if err := conn.close(); err != nil { + if err := conn.Close(); err != nil { errs = append(errs, err) } } @@ -627,7 +602,7 @@ func (c *Client) dial( var errs []error for _, clientConn := range conns { - conn, err := clientConn.dial(nodeID, src, dst, tunnelType) + conn, err := clientConn.Dial(nodeID, src, dst, tunnelType) if err != nil { errs = append(errs, trace.Wrap(err)) continue @@ -643,13 +618,13 @@ func (c *Client) dial( // otherwise. // The boolean returned in the second argument is intended for testing purposes, // to indicates whether the connection was cached or newly established. -func (c *Client) getConnections(proxyIDs []string) ([]clientConn, bool, error) { +func (c *Client) getConnections(proxyIDs []string) ([]internal.ClientConn, bool, error) { if len(proxyIDs) == 0 { return nil, false, trace.BadParameter("failed to dial: no proxy ids given") } ids := make(map[string]struct{}) - var conns []clientConn + var conns []internal.ClientConn // look for existing matching connections. c.RLock() @@ -686,8 +661,8 @@ func (c *Client) getConnections(proxyIDs []string) ([]clientConn, bool, error) { continue } - _, supportsQuic := proxy.GetLabel(types.ProxyPeerQUICLabel) - conn, err := c.connect(id, proxy.GetPeerAddr(), supportsQuic) + supportsQUIC, _ := proxy.GetLabel(types.UnstableProxyPeerQUICLabel) + conn, err := c.connect(id, proxy.GetPeerAddr(), supportsQUIC == "yes") if err != nil { c.metrics.reportTunnelError(errorProxyPeerTunnelDirectDial) c.config.Log.DebugContext(c.ctx, "error direct dialing peer proxy", "peer_id", id, "peer_addr", proxy.GetPeerAddr()) @@ -707,7 +682,7 @@ func (c *Client) getConnections(proxyIDs []string) ([]clientConn, bool, error) { defer c.Unlock() for _, conn := range conns { - c.conns[conn.peerID()] = conn + c.conns[conn.PeerID()] = conn } c.config.connShuffler(conns) @@ -715,7 +690,7 @@ func (c *Client) getConnections(proxyIDs []string) ([]clientConn, bool, error) { } // connect dials a new connection to proxyAddr. -func (c *Client) connect(peerID string, peerAddr string, supportsQUIC bool) (clientConn, error) { +func (c *Client) connect(peerID string, peerAddr string, supportsQUIC bool) (internal.ClientConn, error) { if supportsQUIC && c.config.QUICTransport != nil { panic("QUIC proxy peering is not implemented") } diff --git a/lib/proxy/peer/client_test.go b/lib/proxy/peer/client_test.go index 49df7c97b28b3..8bdc70946be09 100644 --- a/lib/proxy/peer/client_test.go +++ b/lib/proxy/peer/client_test.go @@ -30,6 +30,7 @@ import ( "github.com/gravitational/teleport/api/client/proto" clientapi "github.com/gravitational/teleport/api/client/proto" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/proxy/peer/internal" "github.com/gravitational/teleport/lib/utils" ) @@ -208,7 +209,7 @@ func TestBackupClient(t *testing.T) { require.True(t, dialCalled) } -func waitForGRPCConns(t *testing.T, conns map[string]clientConn, d time.Duration) { +func waitForGRPCConns(t *testing.T, conns map[string]internal.ClientConn, d time.Duration) { require.Eventually(t, func() bool { for _, conn := range conns { // panic if we hit a non-grpc client conn diff --git a/lib/proxy/peer/credentials.go b/lib/proxy/peer/credentials.go index eab77df29aa29..45eb4505c9b71 100644 --- a/lib/proxy/peer/credentials.go +++ b/lib/proxy/peer/credentials.go @@ -27,6 +27,7 @@ import ( "google.golang.org/grpc/credentials" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/proxy/peer/internal" "github.com/gravitational/teleport/lib/tlsca" ) @@ -74,15 +75,13 @@ func (c *clientCredentials) ClientHandshake(ctx context.Context, laddr string, c } if err := validatePeer(c.peerID, identity); err != nil { - c.log.ErrorContext(ctx, duplicatePeerMsg, "peer_addr", c.peerAddr, "peer_id", c.peerID) + internal.LogDuplicatePeer(ctx, c.log, slog.LevelError, "peer_addr", c.peerAddr, "peer_id", c.peerID) return nil, nil, trace.Wrap(err) } return conn, authInfo, nil } -const duplicatePeerMsg = "Detected multiple Proxy Peers with the same public address when connecting to a Proxy which can lead to inconsistent state and problems establishing sessions. For best results ensure that `peer_public_addr` is unique per proxy and not a load balancer." - // getIdentity returns a [tlsca.Identity] that is created from the certificate // presented during the TLS handshake. func getIdentity(authInfo credentials.AuthInfo) (*tlsca.Identity, error) { @@ -121,5 +120,5 @@ func validatePeer(peerID string, identity *tlsca.Identity) error { return nil } - return trace.AccessDenied("connected to unexpected proxy") + return trace.Wrap(internal.WrongProxyError{}) } diff --git a/lib/proxy/peer/dial/dial.go b/lib/proxy/peer/dial/dial.go new file mode 100644 index 0000000000000..22a23344bf77d --- /dev/null +++ b/lib/proxy/peer/dial/dial.go @@ -0,0 +1,37 @@ +// Teleport +// Copyright (C) 2024 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 . + +package peerdial + +import ( + "net" + + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/utils" +) + +// Dialer dials a node in the given cluster. +type Dialer interface { + Dial(clusterName string, request DialParams) (net.Conn, error) +} + +// DialParams defines the target for a [Dialer.Dial]. +type DialParams struct { + From *utils.NetAddr + To *utils.NetAddr + ServerID string + ConnType types.TunnelType +} diff --git a/lib/proxy/peer/helpers_test.go b/lib/proxy/peer/helpers_test.go index f9a7b562c5fff..8880edf428021 100644 --- a/lib/proxy/peer/helpers_test.go +++ b/lib/proxy/peer/helpers_test.go @@ -224,7 +224,7 @@ func setupServer(t *testing.T, name string, serverCA, clientCA *tlsca.CertAuthor clientCAs.AddCert(clientCA.Cert) config := ServerConfig{ - ClusterDialer: &mockClusterDialer{}, + Dialer: &mockClusterDialer{}, GetCertificate: func(*tls.ClientHelloInfo) (*tls.Certificate, error) { return &tlsCert, nil }, diff --git a/lib/proxy/peer/internal/clientconn.go b/lib/proxy/peer/internal/clientconn.go new file mode 100644 index 0000000000000..f44e64afd7b52 --- /dev/null +++ b/lib/proxy/peer/internal/clientconn.go @@ -0,0 +1,50 @@ +// Teleport +// Copyright (C) 2024 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 . + +package internal + +import ( + "context" + "net" + + "github.com/gravitational/teleport/api/types" +) + +// ClientConn manages client connections to a specific peer proxy (with a fixed +// host ID and address). +type ClientConn interface { + // PeerID returns the host ID of the peer proxy. + PeerID() string + // PeerAddr returns the address of the peer proxy. + PeerAddr() string + + // Dial opens a connection of a given tunnel type to a node with the given + // ID through the peer proxy managed by the clientConn. + Dial( + nodeID string, + src net.Addr, + dst net.Addr, + tunnelType types.TunnelType, + ) (net.Conn, error) + + // Close closes all connections and releases any background resources + // immediately. + Close() error + + // Shutdown waits until all connections are closed or the context is done, + // then acts like Close. + Shutdown(context.Context) +} diff --git a/lib/proxy/peer/internal/tls.go b/lib/proxy/peer/internal/tls.go new file mode 100644 index 0000000000000..151288d2fc259 --- /dev/null +++ b/lib/proxy/peer/internal/tls.go @@ -0,0 +1,98 @@ +// Teleport +// Copyright (C) 2024 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 . + +package internal + +import ( + "context" + "crypto/x509" + "log/slog" + "slices" + + "github.com/gravitational/trace" + + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/tlsca" +) + +// VerifyPeerCertificateIsProxy is a function usable as a +// [tls.Config.VerifyPeerCertificate] callback to enforce that the connected TLS +// client is using Proxy credentials. +func VerifyPeerCertificateIsProxy(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + if len(verifiedChains) < 1 { + return trace.AccessDenied("missing client certificate (this is a bug)") + } + + clientCert := verifiedChains[0][0] + clientIdentity, err := tlsca.FromSubject(clientCert.Subject, clientCert.NotAfter) + if err != nil { + return trace.Wrap(err) + } + + if !slices.Contains(clientIdentity.Groups, string(types.RoleProxy)) { + return trace.AccessDenied("expected Proxy client credentials") + } + return nil +} + +// VerifyPeerCertificateIsSpecificProxy returns a function usable as a +// [tls.Config.VerifyPeerCertificate] callback to enforce that the connected TLS +// server is using Proxy credentials and has the expected host ID. +func VerifyPeerCertificateIsSpecificProxy(peerID string) func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + return func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + if len(verifiedChains) < 1 { + return trace.AccessDenied("missing server certificate (this is a bug)") + } + + clientCert := verifiedChains[0][0] + clientIdentity, err := tlsca.FromSubject(clientCert.Subject, clientCert.NotAfter) + if err != nil { + return trace.Wrap(err) + } + + if !slices.Contains(clientIdentity.Groups, string(types.RoleProxy)) { + return trace.AccessDenied("expected Proxy server credentials") + } + + if clientIdentity.Username != peerID { + return trace.Wrap(WrongProxyError{}) + } + return nil + } +} + +// LogDuplicatePeer should be used to log a message if a proxy peering client +// connects to a Proxy that did not have the expected host ID. +func LogDuplicatePeer(ctx context.Context, log *slog.Logger, level slog.Level, args ...any) { + const duplicatePeerMsg = "" + + "Detected multiple Proxy Peers with the same public address when connecting to a Proxy which can lead to inconsistent state and problems establishing sessions. " + + "For best results ensure that `peer_public_addr` is unique per proxy and not a load balancer." + log.Log(ctx, level, duplicatePeerMsg, args...) +} + +// WrongProxyError signals that a proxy peering client has connected to a Proxy +// that did not have the expected host ID. +type WrongProxyError struct{} + +func (WrongProxyError) Error() string { + return "connected to unexpected proxy" +} + +func (e WrongProxyError) Unwrap() error { + return &trace.AccessDeniedError{ + Message: e.Error(), + } +} diff --git a/lib/proxy/peer/quicserver.go b/lib/proxy/peer/quic/server.go similarity index 51% rename from lib/proxy/peer/quicserver.go rename to lib/proxy/peer/quic/server.go index 0877d47e5ed90..d48b06d3a8b6a 100644 --- a/lib/proxy/peer/quicserver.go +++ b/lib/proxy/peer/quic/server.go @@ -1,22 +1,20 @@ -/* - * 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 . - */ +// Teleport +// Copyright (C) 2024 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 . -package peer +package quic import ( "context" @@ -28,22 +26,16 @@ import ( "github.com/quic-go/quic-go" "github.com/gravitational/teleport" + peerdial "github.com/gravitational/teleport/lib/proxy/peer/dial" ) -// QUICServerConfig holds the parameters for [NewQUICServer]. -type QUICServerConfig struct { +// ServerConfig holds the parameters for [NewServer]. +type ServerConfig struct { Log *slog.Logger - // ClusterDialer is the dialer used to open connections to agents on behalf + // Dialer is the dialer used to open connections to agents on behalf // of the peer proxies. Required. - ClusterDialer ClusterDialer + Dialer peerdial.Dialer - // CipherSuites is the set of TLS ciphersuites to be used by the server. - // - // Note: it won't actually have an effect, since QUIC always uses (the DTLS - // equivalent of) TLS 1.3, and TLS 1.3 ciphersuites can't be configured in - // crypto/tls, but for consistency's sake this should be passed along from - // the agent configuration. - CipherSuites []uint16 // GetCertificate should return the server certificate at time of use. It // should be a certificate with the Proxy host role. Required. GetCertificate func(*tls.ClientHelloInfo) (*tls.Certificate, error) @@ -54,7 +46,7 @@ type QUICServerConfig struct { GetClientCAs func(*tls.ClientHelloInfo) (*x509.CertPool, error) } -func (c *QUICServerConfig) checkAndSetDefaults() error { +func (c *ServerConfig) checkAndSetDefaults() error { if c.Log == nil { c.Log = slog.Default() } @@ -63,8 +55,8 @@ func (c *QUICServerConfig) checkAndSetDefaults() error { teleport.Component(teleport.ComponentProxy, "qpeer"), ) - if c.ClusterDialer == nil { - return trace.BadParameter("missing cluster dialer") + if c.Dialer == nil { + return trace.BadParameter("missing Dialer") } if c.GetCertificate == nil { @@ -77,11 +69,11 @@ func (c *QUICServerConfig) checkAndSetDefaults() error { return nil } -// QUICServer is a proxy peering server that uses the QUIC protocol. -type QUICServer struct{} +// Server is a proxy peering server that uses the QUIC protocol. +type Server struct{} -// NewQUICServer returns a [QUICServer] with the given config. -func NewQUICServer(cfg QUICServerConfig) (*QUICServer, error) { +// NewServer returns a [Server] with the given config. +func NewServer(cfg ServerConfig) (*Server, error) { if err := cfg.checkAndSetDefaults(); err != nil { return nil, trace.Wrap(err) } @@ -90,19 +82,19 @@ func NewQUICServer(cfg QUICServerConfig) (*QUICServer, error) { // Serve opens a listener and serves incoming connection. Returns after calling // Close or Shutdown. -func (s *QUICServer) Serve(t *quic.Transport) error { +func (s *Server) Serve(t *quic.Transport) error { panic("QUIC proxy peering is not implemented") } // Close stops listening for incoming connections and ungracefully terminates // all the existing ones. -func (s *QUICServer) Close() error { +func (s *Server) Close() error { panic("QUIC proxy peering is not implemented") } // Shutdown stops listening for incoming connections and waits until the // existing ones are closed or until the context expires. If the context // expires, running connections are ungracefully terminated. -func (s *QUICServer) Shutdown(ctx context.Context) error { +func (s *Server) Shutdown(ctx context.Context) error { panic("QUIC proxy peering is not implemented") } diff --git a/lib/proxy/peer/server.go b/lib/proxy/peer/server.go index 1a3a6869c8485..f798ccf26e18f 100644 --- a/lib/proxy/peer/server.go +++ b/lib/proxy/peer/server.go @@ -25,7 +25,6 @@ import ( "log/slog" "math" "net" - "slices" "time" "github.com/gravitational/trace" @@ -36,9 +35,9 @@ import ( "github.com/gravitational/teleport" "github.com/gravitational/teleport/api/client/proto" "github.com/gravitational/teleport/api/metadata" - "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/utils/grpc/interceptors" - "github.com/gravitational/teleport/lib/tlsca" + peerdial "github.com/gravitational/teleport/lib/proxy/peer/dial" + "github.com/gravitational/teleport/lib/proxy/peer/internal" "github.com/gravitational/teleport/lib/utils" ) @@ -49,8 +48,8 @@ const ( // ServerConfig configures a Server instance. type ServerConfig struct { - Log *slog.Logger - ClusterDialer ClusterDialer + Log *slog.Logger + Dialer peerdial.Dialer CipherSuites []uint16 GetCertificate func(*tls.ClientHelloInfo) (*tls.Certificate, error) @@ -71,8 +70,8 @@ func (c *ServerConfig) checkAndSetDefaults() error { teleport.Component(teleport.ComponentProxy, "peer"), ) - if c.ClusterDialer == nil { - return trace.BadParameter("missing cluster dialer server") + if c.Dialer == nil { + return trace.BadParameter("missing Dialer") } if c.GetCertificate == nil { @@ -84,8 +83,8 @@ func (c *ServerConfig) checkAndSetDefaults() error { if c.service == nil { c.service = &proxyService{ - c.ClusterDialer, - c.Log, + dialer: c.Dialer, + log: c.Log, } } @@ -94,9 +93,9 @@ func (c *ServerConfig) checkAndSetDefaults() error { // Server is a proxy service server using grpc and tls. type Server struct { - log *slog.Logger - clusterDialer ClusterDialer - server *grpc.Server + log *slog.Logger + dialer peerdial.Dialer + server *grpc.Server } // NewServer creates a new proxy server instance. @@ -117,7 +116,7 @@ func NewServer(cfg ServerConfig) (*Server, error) { tlsConfig.NextProtos = []string{"h2"} tlsConfig.GetCertificate = cfg.GetCertificate tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert - tlsConfig.VerifyPeerCertificate = verifyPeerCertificateIsProxy + tlsConfig.VerifyPeerCertificate = internal.VerifyPeerCertificateIsProxy getClientCAs := cfg.GetClientCAs tlsConfig.GetConfigForClient = func(chi *tls.ClientHelloInfo) (*tls.Config, error) { @@ -158,29 +157,12 @@ func NewServer(cfg ServerConfig) (*Server, error) { proto.RegisterProxyServiceServer(server, cfg.service) return &Server{ - log: cfg.Log, - clusterDialer: cfg.ClusterDialer, - server: server, + log: cfg.Log, + dialer: cfg.Dialer, + server: server, }, nil } -func verifyPeerCertificateIsProxy(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { - if len(verifiedChains) < 1 { - return trace.AccessDenied("missing client certificate (this is a bug)") - } - - clientCert := verifiedChains[0][0] - clientIdentity, err := tlsca.FromSubject(clientCert.Subject, clientCert.NotAfter) - if err != nil { - return trace.Wrap(err) - } - - if !slices.Contains(clientIdentity.Groups, string(types.RoleProxy)) { - return trace.AccessDenied("expected Proxy client credentials") - } - return nil -} - // Serve starts the proxy server. func (s *Server) Serve(l net.Listener) error { if err := s.server.Serve(l); err != nil { diff --git a/lib/proxy/peer/service.go b/lib/proxy/peer/service.go index 4423892ae04f1..31c08b6a7d66d 100644 --- a/lib/proxy/peer/service.go +++ b/lib/proxy/peer/service.go @@ -20,21 +20,20 @@ package peer import ( "log/slog" - "net" "strings" "github.com/gravitational/trace" "github.com/gravitational/teleport/api/client/proto" - "github.com/gravitational/teleport/api/types" streamutils "github.com/gravitational/teleport/api/utils/grpc/stream" + peerdial "github.com/gravitational/teleport/lib/proxy/peer/dial" "github.com/gravitational/teleport/lib/utils" ) // proxyService implements the grpc ProxyService. type proxyService struct { - clusterDialer ClusterDialer - log *slog.Logger + dialer peerdial.Dialer + log *slog.Logger } // DialNode opens a bidirectional stream to the requested node. @@ -75,7 +74,7 @@ func (s *proxyService) DialNode(stream proto.ProxyService_DialNodeServer) error AddrNetwork: dial.Destination.Network, } - nodeConn, err := s.clusterDialer.Dial(clusterName, DialParams{ + nodeConn, err := s.dialer.Dial(clusterName, peerdial.DialParams{ From: source, To: destination, ServerID: dial.NodeID, @@ -116,15 +115,3 @@ func splitServerID(address string) (string, string, error) { return split[0], strings.Join(split[1:], "."), nil } - -// ClusterDialer dials a node in the given cluster. -type ClusterDialer interface { - Dial(clusterName string, request DialParams) (net.Conn, error) -} - -type DialParams struct { - From *utils.NetAddr - To *utils.NetAddr - ServerID string - ConnType types.TunnelType -} diff --git a/lib/proxy/peer/service_test.go b/lib/proxy/peer/service_test.go index 687759e0892d8..1510d65d9de2d 100644 --- a/lib/proxy/peer/service_test.go +++ b/lib/proxy/peer/service_test.go @@ -31,13 +31,14 @@ import ( "github.com/gravitational/teleport/api/client/proto" "github.com/gravitational/teleport/api/types" + peerdial "github.com/gravitational/teleport/lib/proxy/peer/dial" ) type mockClusterDialer struct { - MockDialCluster func(string, DialParams) (net.Conn, error) + MockDialCluster func(string, peerdial.DialParams) (net.Conn, error) } -func (m *mockClusterDialer) Dial(clusterName string, request DialParams) (net.Conn, error) { +func (m *mockClusterDialer) Dial(clusterName string, request peerdial.DialParams) (net.Conn, error) { if m.MockDialCluster == nil { return nil, trace.NotImplemented("") } @@ -93,8 +94,8 @@ func TestSendReceive(t *testing.T) { } local, remote := net.Pipe() - service.clusterDialer = &mockClusterDialer{ - MockDialCluster: func(clusterName string, request DialParams) (net.Conn, error) { + service.dialer = &mockClusterDialer{ + MockDialCluster: func(clusterName string, request peerdial.DialParams) (net.Conn, error) { require.Equal(t, "test-cluster", clusterName) require.Equal(t, dialRequest.TunnelType, request.ConnType) require.Equal(t, dialRequest.NodeID, request.ServerID) diff --git a/lib/reversetunnelclient/peer.go b/lib/reversetunnelclient/peer.go new file mode 100644 index 0000000000000..00266d53b7df1 --- /dev/null +++ b/lib/reversetunnelclient/peer.go @@ -0,0 +1,57 @@ +// Teleport +// Copyright (C) 2024 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 . + +package reversetunnelclient + +import ( + "net" + + "github.com/gravitational/trace" + + peerdial "github.com/gravitational/teleport/lib/proxy/peer/dial" +) + +// PeerDialerFunc is a function that implements [peerdial.Dialer]. +type PeerDialerFunc func(clusterName string, request peerdial.DialParams) (net.Conn, error) + +// Dial implements [peerdial.Dialer]. +func (f PeerDialerFunc) Dial(clusterName string, request peerdial.DialParams) (net.Conn, error) { + return f(clusterName, request) +} + +// NewPeerDialer implements [peerdial.Dialer] for a reverse tunnel server. +func NewPeerDialer(server Tunnel) PeerDialerFunc { + return func(clusterName string, request peerdial.DialParams) (net.Conn, error) { + site, err := server.GetSite(clusterName) + if err != nil { + return nil, trace.Wrap(err) + } + + dialParams := DialParams{ + ServerID: request.ServerID, + ConnType: request.ConnType, + From: request.From, + To: request.To, + FromPeerProxy: true, + } + + conn, err := site.Dial(dialParams) + if err != nil { + return nil, trace.Wrap(err) + } + return conn, nil + } +} diff --git a/lib/service/service.go b/lib/service/service.go index 043ab34e4fdff..6ad43e0ab60dd 100644 --- a/lib/service/service.go +++ b/lib/service/service.go @@ -137,8 +137,8 @@ import ( "github.com/gravitational/teleport/lib/openssh" "github.com/gravitational/teleport/lib/plugin" "github.com/gravitational/teleport/lib/proxy" - "github.com/gravitational/teleport/lib/proxy/clusterdial" "github.com/gravitational/teleport/lib/proxy/peer" + peerquic "github.com/gravitational/teleport/lib/proxy/peer/quic" "github.com/gravitational/teleport/lib/resumption" "github.com/gravitational/teleport/lib/reversetunnel" "github.com/gravitational/teleport/lib/reversetunnelclient" @@ -4321,8 +4321,7 @@ func (process *TeleportProcess) initProxyEndpoint(conn *Connector) error { var peerQUICTransport *quic.Transport if !process.Config.Proxy.DisableReverseTunnel { if listeners.proxyPeer != nil { - // TODO(espadolini): allow this when the implementation is merged - if false && os.Getenv("TELEPORT_UNSTABLE_QUIC_PROXY_PEERING") == "yes" { + if process.Config.Proxy.QUICProxyPeering { // the stateless reset key is important in case there's a crash // so peers can be told to close their side of the connections // instead of having to wait for a timeout; for this reason, we @@ -4707,7 +4706,7 @@ func (process *TeleportProcess) initProxyEndpoint(conn *Connector) error { var peerAddrString string var peerServer *peer.Server - var peerQUICServer *peer.QUICServer + var peerQUICServer *peerquic.Server if !process.Config.Proxy.DisableReverseTunnel && listeners.proxyPeer != nil { peerAddr, err := process.Config.Proxy.PublicPeerAddr() if err != nil { @@ -4716,9 +4715,9 @@ func (process *TeleportProcess) initProxyEndpoint(conn *Connector) error { peerAddrString = peerAddr.String() peerServer, err = peer.NewServer(peer.ServerConfig{ - Log: process.logger, - ClusterDialer: clusterdial.NewClusterDialer(tsrv), - CipherSuites: cfg.CipherSuites, + Log: process.logger, + Dialer: reversetunnelclient.NewPeerDialer(tsrv), + CipherSuites: cfg.CipherSuites, GetCertificate: func(*tls.ClientHelloInfo) (*tls.Certificate, error) { return conn.serverGetCertificate() }, @@ -4750,10 +4749,9 @@ func (process *TeleportProcess) initProxyEndpoint(conn *Connector) error { }) if peerQUICTransport != nil { - peerQUICServer, err := peer.NewQUICServer(peer.QUICServerConfig{ - Log: process.logger, - ClusterDialer: clusterdial.NewClusterDialer(tsrv), - CipherSuites: cfg.CipherSuites, + peerQUICServer, err := peerquic.NewServer(peerquic.ServerConfig{ + Log: process.logger, + Dialer: reversetunnelclient.NewPeerDialer(tsrv), GetCertificate: func(*tls.ClientHelloInfo) (*tls.Certificate, error) { return conn.serverGetCertificate() }, @@ -4771,11 +4769,11 @@ func (process *TeleportProcess) initProxyEndpoint(conn *Connector) error { process.RegisterCriticalFunc("proxy.peer.quic", func() error { if _, err := process.WaitForEvent(process.ExitContext(), ProxyReverseTunnelReady); err != nil { - logger.DebugContext(process.ExitContext(), "Process exiting: failed to start QUIC peer proxy service waiting for reverse tunnel server.") + logger.DebugContext(process.ExitContext(), "process exiting: failed to start QUIC peer proxy service waiting for reverse tunnel server") return nil } - logger.InfoContext(process.ExitContext(), "Starting QUIC peer proxy service.", "local_addr", logutils.StringerAttr(peerQUICTransport.Conn.LocalAddr())) + logger.InfoContext(process.ExitContext(), "starting QUIC peer proxy service", "local_addr", logutils.StringerAttr(peerQUICTransport.Conn.LocalAddr())) err := peerQUICServer.Serve(peerQUICTransport) if err != nil { return trace.Wrap(err) @@ -4797,8 +4795,8 @@ func (process *TeleportProcess) initProxyEndpoint(conn *Connector) error { logger.InfoContext(process.ExitContext(), "Enabling proxy group labels.", "group_id", cfg.Proxy.ProxyGroupID, "generation", cfg.Proxy.ProxyGroupGeneration) } if peerQUICTransport != nil { - staticLabels[types.ProxyPeerQUICLabel] = "x" - logger.InfoContext(process.ExitContext(), "Advertising proxy peering QUIC support.") + staticLabels[types.UnstableProxyPeerQUICLabel] = "yes" + logger.InfoContext(process.ExitContext(), "advertising proxy peering QUIC support") } sshProxy, err := regular.New( diff --git a/lib/service/servicecfg/proxy.go b/lib/service/servicecfg/proxy.go index c07ce5d47b0f4..93ab0767c69be 100644 --- a/lib/service/servicecfg/proxy.go +++ b/lib/service/servicecfg/proxy.go @@ -158,6 +158,11 @@ type ProxyConfig struct { // proxy built-in version server to retrieve target versions. This is part // of the automatic upgrades. AutomaticUpgradesChannels automaticupgrades.Channels + + // QUICProxyPeering will make it so that proxy peering will support inbound + // QUIC connections and will use QUIC to connect to peer proxies that + // advertise support for it. + QUICProxyPeering bool } // WebPublicAddr returns the address for the web endpoint on this proxy that