Files
teleport/lib/reversetunnel/remotesite.go
T
rosstimothy 2cced62805 Fix violations of non-constant format strings linter (#51812)
Depends on https://github.com/gravitational/teleport.e/pull/6006.
Now that we are compliant, the ignore rule was removed from the
golangci-lint config to prevent future regressions.
2025-02-04 16:43:35 +00:00

1011 lines
31 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 reversetunnel
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"log/slog"
"net"
"sync"
"time"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
"golang.org/x/crypto/ssh"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/client"
"github.com/gravitational/teleport/api/constants"
"github.com/gravitational/teleport/api/types"
apiutils "github.com/gravitational/teleport/api/utils"
"github.com/gravitational/teleport/api/utils/retryutils"
"github.com/gravitational/teleport/api/utils/sshutils"
"github.com/gravitational/teleport/lib/auth/authclient"
"github.com/gravitational/teleport/lib/reversetunnelclient"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/services/readonly"
"github.com/gravitational/teleport/lib/srv/forward"
"github.com/gravitational/teleport/lib/teleagent"
"github.com/gravitational/teleport/lib/utils"
logutils "github.com/gravitational/teleport/lib/utils/log"
)
// remoteSite is a remote site that established the inbound connection to
// the local reverse tunnel server, and now it can provide access to the
// cluster behind it.
type remoteSite struct {
sync.RWMutex
logger *slog.Logger
domainName string
connections []*remoteConn
lastUsed int
srv *server
// connInfo represents the connection to the remote cluster.
connInfo types.TunnelConnection
// lastConnInfo is the last connInfo.
lastConnInfo types.TunnelConnection
ctx context.Context
cancel context.CancelFunc
clock clockwork.Clock
// certificateCache caches host certificates for the forwarding server.
certificateCache *certificateCache
// localClient provides access to the Auth Server API of the cluster
// within which reversetunnelclient.Server is running.
localClient authclient.ClientI
// remoteClient provides access to the Auth Server API of the remote cluster that
// this site belongs to.
remoteClient authclient.ClientI
// localAccessPoint provides access to a cached subset of the Auth Server API of
// the local cluster.
localAccessPoint authclient.ProxyAccessPoint
// remoteAccessPoint provides access to a cached subset of the Auth Server API of
// the remote cluster this site belongs to.
remoteAccessPoint authclient.RemoteProxyAccessPoint
// nodeWatcher provides access the node set for the remote site
nodeWatcher *services.GenericWatcher[types.Server, readonly.Server]
// remoteCA is the last remote certificate authority recorded by the client.
// It is used to detect CA rotation status changes. If the rotation
// state has been changed, the tunnel will reconnect to re-create the client
// with new settings.
remoteCA types.CertAuthority
// offlineThreshold is how long to wait for a keep alive message before
// marking a reverse tunnel connection as invalid.
offlineThreshold time.Duration
// proxySyncInterval defines the interval at which discovery requests are
// sent to keep agents in sync
proxySyncInterval time.Duration
}
func (s *remoteSite) getRemoteClient() (authclient.ClientI, bool, error) {
// check if all cert authorities are initiated and if everything is OK
ca, err := s.srv.localAccessPoint.GetCertAuthority(s.ctx, types.CertAuthID{Type: types.HostCA, DomainName: s.domainName}, false)
if err != nil {
return nil, false, trace.Wrap(err)
}
if len(ca.GetTrustedTLSKeyPairs()) == 0 {
return nil, false, trace.BadParameter("no TLS keys found")
}
// The fact that cluster has keys to remote CA means that the key exchange
// has completed.
s.logger.DebugContext(s.ctx, "Using TLS client to remote cluster")
tlsConfig := utils.TLSConfig(s.srv.ClientTLSCipherSuites)
// encode the name of this cluster to identify this cluster,
// connecting to the remote one (it is used to find the right certificate
// authority to verify)
tlsConfig.ServerName = apiutils.EncodeClusterName(s.srv.ClusterName)
tlsConfig.GetClientCertificate = func(*tls.CertificateRequestInfo) (*tls.Certificate, error) {
tlsCert, err := s.srv.GetClientTLSCertificate()
if err != nil {
return nil, trace.Wrap(err)
}
return tlsCert, nil
}
tlsConfig.InsecureSkipVerify = true
tlsConfig.VerifyConnection = utils.VerifyConnectionWithRoots(func() (*x509.CertPool, error) {
pool, _, err := authclient.ClientCertPool(s.ctx, s.srv.localAccessPoint, s.domainName, types.HostCA)
if err != nil {
return nil, trace.Wrap(err)
}
return pool, nil
})
clt, err := authclient.NewClient(client.Config{
Dialer: client.ContextDialerFunc(s.authServerContextDialer),
Credentials: []client.Credentials{
client.LoadTLS(tlsConfig),
},
CircuitBreakerConfig: s.srv.CircuitBreakerConfig,
})
if err != nil {
return nil, false, trace.Wrap(err)
}
return clt, false, nil
}
func (s *remoteSite) authServerContextDialer(ctx context.Context, network, address string) (net.Conn, error) {
conn, err := s.DialAuthServer(reversetunnelclient.DialParams{})
return conn, err
}
// GetTunnelsCount always returns 0 for local cluster
func (s *remoteSite) GetTunnelsCount() int {
return s.connectionCount()
}
func (s *remoteSite) CachingAccessPoint() (authclient.RemoteProxyAccessPoint, error) {
return s.remoteAccessPoint, nil
}
// NodeWatcher returns the services.NodeWatcher for the remote cluster.
func (s *remoteSite) NodeWatcher() (*services.GenericWatcher[types.Server, readonly.Server], error) {
return s.nodeWatcher, nil
}
// GitServerWatcher returns the Git server watcher for the remote cluster.
func (s *remoteSite) GitServerWatcher() (*services.GenericWatcher[types.Server, readonly.Server], error) {
return nil, trace.NotImplemented("GitServerWatcher not implemented for remoteSite")
}
func (s *remoteSite) GetClient() (authclient.ClientI, error) {
return s.remoteClient, nil
}
func (s *remoteSite) String() string {
return fmt.Sprintf("remoteSite(%v)", s.domainName)
}
func (s *remoteSite) connectionCount() int {
s.RLock()
defer s.RUnlock()
return len(s.connections)
}
func (s *remoteSite) HasValidConnections() bool {
s.RLock()
defer s.RUnlock()
for _, conn := range s.connections {
if !conn.isInvalid() {
return true
}
}
return false
}
// Close closes remote cluster connections
func (s *remoteSite) Close() error {
s.Lock()
defer s.Unlock()
s.cancel()
var errors []error
for i := range s.connections {
if err := s.connections[i].Close(); err != nil {
errors = append(errors, err)
}
}
s.connections = []*remoteConn{}
if s.remoteAccessPoint != nil {
if err := s.remoteAccessPoint.Close(); err != nil {
errors = append(errors, err)
}
}
return trace.NewAggregate(errors...)
}
// IsClosed reports whether this remoteSite has been closed.
func (s *remoteSite) IsClosed() bool {
return s.ctx.Err() != nil
}
// nextConn returns next connection that is ready
// and has not been marked as invalid
// it will close connections marked as invalid
func (s *remoteSite) nextConn() (*remoteConn, error) {
s.Lock()
defer s.Unlock()
s.removeInvalidConns()
for i := 0; i < len(s.connections); i++ {
s.lastUsed = (s.lastUsed + 1) % len(s.connections)
remoteConn := s.connections[s.lastUsed]
// connection could have been initiated, but agent
// on the other side is not ready yet.
// Proxy assumes that connection is ready to serve when
// it has received a first heartbeat, otherwise
// it could attempt to use it before the agent
// had a chance to start handling connection requests,
// what could lead to proxy marking the connection
// as invalid without a good reason.
if remoteConn.isReady() {
return remoteConn, nil
}
}
return nil, trace.NotFound("%v is offline: no active tunnels to %v found", s.GetName(), s.srv.ClusterName)
}
// removeInvalidConns removes connections marked as invalid,
// it should be called only under write lock
func (s *remoteSite) removeInvalidConns() {
// for first pass, do nothing if no connections are marked
count := 0
for _, conn := range s.connections {
if conn.isInvalid() {
count++
}
}
if count == 0 {
return
}
s.lastUsed = 0
conns := make([]*remoteConn, 0, len(s.connections)-count)
for i := range s.connections {
if !s.connections[i].isInvalid() {
conns = append(conns, s.connections[i])
} else {
go func(conn *remoteConn) {
if err := conn.Close(); err != nil {
s.logger.WarnContext(s.ctx, "Failed to close invalid connection", "error", err)
}
}(s.connections[i])
}
}
s.connections = conns
}
// addConn helper adds a new active remote cluster connection to the list
// of such connections
func (s *remoteSite) addConn(conn net.Conn, sconn ssh.Conn) (*remoteConn, error) {
s.Lock()
defer s.Unlock()
rconn := newRemoteConn(&connConfig{
conn: conn,
sconn: sconn,
tunnelType: string(types.ProxyTunnel),
proxyName: s.connInfo.GetProxyName(),
clusterName: s.domainName,
offlineThreshold: s.offlineThreshold,
})
s.connections = append(s.connections, rconn)
s.lastUsed = 0
return rconn, nil
}
func (s *remoteSite) adviseReconnect(ctx context.Context) {
wg := &sync.WaitGroup{}
s.RLock()
for _, conn := range s.connections {
s.logger.DebugContext(ctx, "Sending reconnect to server", "server_id", conn.nodeID)
wg.Add(1)
go func(conn *remoteConn) {
if err := conn.adviseReconnect(); err != nil {
s.logger.WarnContext(ctx, "Failed to send reconnection advisory", "error", err)
}
wg.Done()
}(conn)
}
s.RUnlock()
wait := make(chan struct{})
go func() {
wg.Wait()
close(wait)
}()
select {
case <-ctx.Done():
case <-wait:
}
}
func (s *remoteSite) GetStatus() string {
connInfo, err := s.getLastConnInfo()
if err != nil {
return teleport.RemoteClusterStatusOffline
}
return services.TunnelConnectionStatus(s.clock, connInfo, s.offlineThreshold)
}
func (s *remoteSite) copyConnInfo() types.TunnelConnection {
s.RLock()
defer s.RUnlock()
return s.connInfo.Clone()
}
func (s *remoteSite) setLastConnInfo(connInfo types.TunnelConnection) {
s.Lock()
defer s.Unlock()
s.lastConnInfo = connInfo.Clone()
}
func (s *remoteSite) getLastConnInfo() (types.TunnelConnection, error) {
s.RLock()
defer s.RUnlock()
if s.lastConnInfo == nil {
return nil, trace.NotFound("no last connection found")
}
return s.lastConnInfo.Clone(), nil
}
func (s *remoteSite) registerHeartbeat(t time.Time) {
connInfo := s.copyConnInfo()
connInfo.SetLastHeartbeat(t)
connInfo.SetExpiry(s.clock.Now().Add(s.offlineThreshold))
s.setLastConnInfo(connInfo)
err := s.localAccessPoint.UpsertTunnelConnection(connInfo)
if err != nil {
s.logger.WarnContext(s.ctx, "Failed to register heartbeat", "error", err)
}
}
// deleteConnectionRecord deletes connection record to let know peer proxies
// that this node lost the connection and needs to be discovered
func (s *remoteSite) deleteConnectionRecord() {
if err := s.localAccessPoint.DeleteTunnelConnection(s.connInfo.GetClusterName(), s.connInfo.GetName()); err != nil {
s.logger.WarnContext(s.ctx, "Failed to delete tunnel connection", "error", err)
}
}
// fanOutProxies is a non-blocking call that puts the new proxies
// list so that remote connection can notify the remote agent
// about the list update
func (s *remoteSite) fanOutProxies(proxies []types.Server) {
s.Lock()
defer s.Unlock()
for _, conn := range s.connections {
conn.updateProxies(proxies)
}
}
// handleHeartbeat receives heartbeat messages from the connected agent
// if the agent has missed several heartbeats in a row, Proxy marks
// the connection as invalid.
func (s *remoteSite) handleHeartbeat(ctx context.Context, conn *remoteConn, ch ssh.Channel, reqC <-chan *ssh.Request) {
logger := s.logger.With(
"server_id", conn.nodeID,
"addr", logutils.StringerAttr(conn.conn.RemoteAddr()),
)
sshutils.DiscardChannelData(ch)
if ch != nil {
defer func() {
if err := ch.Close(); err != nil {
logger.WarnContext(ctx, "Failed to close heartbeat channel", "error", err)
}
}()
}
firstHeartbeat := true
proxyResyncTicker := s.clock.NewTicker(s.proxySyncInterval)
defer func() {
proxyResyncTicker.Stop()
logger.InfoContext(ctx, "Cluster connection closed")
if err := conn.Close(); err != nil && !utils.IsUseOfClosedNetworkError(err) {
logger.WarnContext(ctx, "Failed to close remote connection for remote site", "error", err)
}
if err := s.srv.onSiteTunnelClose(s); err != nil {
logger.WarnContext(ctx, "Failed to close remote site", "error", err)
}
}()
offlineThresholdTimer := time.NewTimer(s.offlineThreshold)
defer offlineThresholdTimer.Stop()
for {
select {
case <-s.ctx.Done():
logger.InfoContext(ctx, "closing")
return
case <-proxyResyncTicker.Chan():
var req discoveryRequest
proxies, err := s.srv.proxyWatcher.CurrentResources(s.srv.ctx)
if err != nil {
logger.WarnContext(ctx, "Failed to get proxy set", "error", err)
}
req.SetProxies(proxies)
if err := conn.sendDiscoveryRequest(ctx, req); err != nil {
logger.DebugContext(ctx, "Marking connection invalid on error", "error", err)
conn.markInvalid(err)
return
}
case proxies := <-conn.newProxiesC:
var req discoveryRequest
req.SetProxies(proxies)
if err := conn.sendDiscoveryRequest(ctx, req); err != nil {
logger.DebugContext(ctx, "Marking connection invalid on error", "error", err)
conn.markInvalid(err)
return
}
case req := <-reqC:
if req == nil {
logger.InfoContext(ctx, "Cluster agent disconnected")
conn.markInvalid(trace.ConnectionProblem(nil, "agent disconnected"))
if !s.HasValidConnections() {
logger.DebugContext(ctx, "Deleting connection record")
s.deleteConnectionRecord()
}
return
}
if firstHeartbeat {
// as soon as the agent connects and sends a first heartbeat
// send it the list of current proxies back
proxies, err := s.srv.proxyWatcher.CurrentResources(ctx)
if err != nil {
logger.WarnContext(ctx, "Failed to get proxy set", "error", err)
}
if len(proxies) > 0 {
conn.updateProxies(proxies)
}
firstHeartbeat = false
}
var timeSent time.Time
var roundtrip time.Duration
if req.Payload != nil {
if err := timeSent.UnmarshalText(req.Payload); err == nil {
roundtrip = s.srv.Clock.Now().Sub(timeSent)
}
}
pinglog := logger
if roundtrip != 0 {
pinglog = pinglog.With("latency", roundtrip)
}
pinglog.DebugContext(ctx, "Received ping request", "remote_addr", logutils.StringerAttr(conn.conn.RemoteAddr()))
tm := s.clock.Now().UTC()
conn.setLastHeartbeat(tm)
go s.registerHeartbeat(tm)
case t := <-offlineThresholdTimer.C:
conn.markInvalid(trace.ConnectionProblem(nil, "no heartbeats for %v", s.offlineThreshold))
// terminate and remove the connection after missing more than missedHeartBeatThreshold heartbeats if
// the connection isn't still servicing any sessions
hb := conn.getLastHeartbeat()
if t.After(hb.Add(s.offlineThreshold * missedHeartBeatThreshold)) {
count := conn.activeSessions()
if count == 0 {
logger.ErrorContext(ctx, "Closing unhealthy and idle connection", "last_heartbeat", hb)
return
}
logger.WarnContext(ctx, "Deferring closure of unhealthy connection due to active connections", "active_conn_count", count)
}
offlineThresholdTimer.Reset(s.offlineThreshold)
continue
}
if !offlineThresholdTimer.Stop() {
<-offlineThresholdTimer.C
}
offlineThresholdTimer.Reset(s.offlineThreshold)
}
}
func (s *remoteSite) GetName() string {
return s.domainName
}
func (s *remoteSite) GetLastConnected() time.Time {
connInfo, err := s.getLastConnInfo()
if err != nil {
return time.Time{}
}
return connInfo.GetLastHeartbeat()
}
func (s *remoteSite) compareAndSwapCertAuthority(ca types.CertAuthority) error {
s.Lock()
defer s.Unlock()
if s.remoteCA == nil {
s.remoteCA = ca
return nil
}
rotation := s.remoteCA.GetRotation()
if rotation.Matches(ca.GetRotation()) {
s.remoteCA = ca
return nil
}
s.remoteCA = ca
return trace.CompareFailed("remote certificate authority rotation has been updated")
}
func (s *remoteSite) updateCertAuthorities(retry retryutils.Retry, remoteWatcher *services.CertAuthorityWatcher, remoteVersion string) {
defer remoteWatcher.Close()
for {
err := s.watchCertAuthorities(remoteWatcher, remoteVersion)
if err != nil {
switch {
case trace.IsNotFound(err):
s.logger.DebugContext(s.ctx, "Remote cluster does not support cert authorities rotation yet")
case trace.IsCompareFailed(err):
s.logger.InfoContext(s.ctx, "Remote cluster has updated certificate authorities, going to force reconnect")
if err := s.srv.onSiteTunnelClose(&alwaysClose{RemoteSite: s}); err != nil {
s.logger.WarnContext(s.ctx, "Failed to close remote site", "error", err)
}
return
case trace.IsConnectionProblem(err):
s.logger.DebugContext(s.ctx, "Remote cluster is offline")
default:
s.logger.WarnContext(s.ctx, "Could not perform cert authorities update", "error", err)
}
}
startedWaiting := s.clock.Now()
select {
case t := <-retry.After():
s.logger.DebugContext(s.ctx, "Initiating new cert authority watch after applying backoff", "backoff_duration", t.Sub(startedWaiting))
retry.Inc()
case <-s.ctx.Done():
return
}
}
}
func (s *remoteSite) watchCertAuthorities(remoteWatcher *services.CertAuthorityWatcher, remoteVersion string) error {
filter := types.CertAuthorityFilter{
types.HostCA: s.srv.ClusterName,
types.UserCA: s.srv.ClusterName,
types.DatabaseCA: s.srv.ClusterName,
types.OpenSSHCA: s.srv.ClusterName,
}
localWatch, err := s.srv.CertAuthorityWatcher.Subscribe(s.ctx, filter)
if err != nil {
return trace.Wrap(err)
}
defer func() {
if err := localWatch.Close(); err != nil {
s.logger.WarnContext(s.ctx, "Failed to close local ca watcher subscription", "error", err)
}
}()
remoteWatch, err := remoteWatcher.Subscribe(
s.ctx,
types.CertAuthorityFilter{
types.HostCA: s.domainName,
},
)
if err != nil {
return trace.Wrap(err)
}
defer func() {
if err := remoteWatch.Close(); err != nil {
s.logger.WarnContext(s.ctx, "Failed to close remote ca watcher subscription", "error", err)
}
}()
localCAs := make(map[types.CertAuthType]types.CertAuthority, len(filter))
for caType, clusterName := range filter {
caID := types.CertAuthID{
Type: caType,
DomainName: clusterName,
}
ca, err := s.localAccessPoint.GetCertAuthority(s.ctx, caID, false)
if err != nil {
return trace.Wrap(err, "failed to get local cert authority")
}
if err := s.remoteClient.RotateExternalCertAuthority(s.ctx, ca); err != nil {
return trace.Wrap(err, "failed to push local cert authority")
}
s.logger.DebugContext(s.ctx, "Pushed local cert authority", "cert_authority", logutils.StringerAttr(caID))
localCAs[caType] = ca
}
remoteCA, err := s.remoteAccessPoint.GetCertAuthority(s.ctx, types.CertAuthID{
Type: types.HostCA,
DomainName: s.domainName,
}, false)
if err != nil {
return trace.Wrap(err, "failed to get remote cert authority")
}
if remoteCA.GetName() != s.domainName || remoteCA.GetType() != types.HostCA {
return trace.BadParameter("received wrong CA, expected remote host CA, got %v", remoteCA.GetID())
}
maybeUpsertRemoteCA := func(remoteCA types.CertAuthority) error {
oldRemoteCA, err := s.localAccessPoint.GetCertAuthority(s.ctx, types.CertAuthID{
Type: types.HostCA,
DomainName: remoteCA.GetClusterName(),
}, false)
if err != nil && !trace.IsNotFound(err) {
return trace.Wrap(err)
}
// if CA is changed or does not exist, update backend
if err != nil || !services.CertAuthoritiesEquivalent(oldRemoteCA, remoteCA) {
s.logger.DebugContext(s.ctx, "Ingesting remote cert authority", "cert_authority", logutils.StringerAttr(remoteCA.GetID()))
if err := s.localClient.UpsertCertAuthority(s.ctx, remoteCA); err != nil {
return trace.Wrap(err)
}
}
// keep track of when the remoteSite needs to reconnect
if err := s.compareAndSwapCertAuthority(remoteCA); err != nil {
return trace.Wrap(err)
}
return nil
}
if err := maybeUpsertRemoteCA(remoteCA); err != nil {
return trace.Wrap(err)
}
s.logger.DebugContext(s.ctx, "Watching for cert authority changes")
for {
select {
case <-s.ctx.Done():
return trace.Wrap(s.ctx.Err())
case <-localWatch.Done():
s.logger.WarnContext(s.ctx, "Local CertAuthority watcher subscription has closed")
return fmt.Errorf("local ca watcher for cluster %s has closed", s.srv.ClusterName)
case <-remoteWatch.Done():
s.logger.WarnContext(s.ctx, "Remote CertAuthority watcher subscription has closed")
return fmt.Errorf("remote ca watcher for cluster %s has closed", s.domainName)
case evt := <-localWatch.Events():
switch evt.Type {
case types.OpPut:
newCA, ok := evt.Resource.(types.CertAuthority)
if !ok {
continue
}
previousCA, ok := localCAs[newCA.GetType()]
if ok && services.CertAuthoritiesEquivalent(previousCA, newCA) {
continue
}
// clone to prevent a race with watcher filtering, as
// RotateExternalCertAuthority (client side) will end up calling
// CheckAndSetDefaults
// TODO(espadolini): figure out who should be responsible for validating the CA *once*
newCA = newCA.Clone()
if err := s.remoteClient.RotateExternalCertAuthority(s.ctx, newCA); err != nil {
s.logger.WarnContext(s.ctx, "Failed to rotate external ca", "error", err)
return trace.Wrap(err)
}
localCAs[newCA.GetType()] = newCA
}
case evt := <-remoteWatch.Events():
switch evt.Type {
case types.OpPut:
remoteCA, ok := evt.Resource.(types.CertAuthority)
if !ok {
continue
}
// the CA might not be trusted but the watcher's fanout logic is
// local, so this is ok
if err := maybeUpsertRemoteCA(remoteCA); err != nil {
return trace.Wrap(err)
}
}
}
}
}
func (s *remoteSite) updateLocks(retry retryutils.Retry) {
s.logger.DebugContext(s.ctx, "Watching for remote lock changes")
for {
startedWaiting := s.clock.Now()
select {
case t := <-retry.After():
s.logger.DebugContext(s.ctx, "Initiating new lock watch after applying backoff", "backoff_duration", t.Sub(startedWaiting))
retry.Inc()
case <-s.ctx.Done():
return
}
if err := s.watchLocks(); err != nil {
switch {
case trace.IsNotImplemented(err):
s.logger.DebugContext(s.ctx, "Remote cluster does not support locks yet", "cluster", s.domainName)
case trace.IsConnectionProblem(err):
s.logger.DebugContext(s.ctx, "Remote cluster is offline", "cluster", s.domainName)
default:
s.logger.WarnContext(s.ctx, "Could not update remote locks", "error", err)
}
}
}
}
func (s *remoteSite) watchLocks() error {
watcher, err := s.srv.LockWatcher.Subscribe(s.ctx)
if err != nil {
s.logger.ErrorContext(s.ctx, "Failed to subscribe to LockWatcher", "error", err)
return err
}
defer func() {
if err := watcher.Close(); err != nil {
s.logger.WarnContext(s.ctx, "Failed to close lock watcher subscription", "error", err)
}
}()
for {
select {
case <-watcher.Done():
s.logger.WarnContext(s.ctx, "Lock watcher subscription has closed", "error", watcher.Error())
return trace.Wrap(watcher.Error())
case <-s.ctx.Done():
return trace.Wrap(s.ctx.Err())
case evt := <-watcher.Events():
switch evt.Type {
case types.OpPut, types.OpDelete:
locks := s.srv.LockWatcher.GetCurrent()
if err := s.remoteClient.ReplaceRemoteLocks(s.ctx, s.srv.ClusterName, locks); err != nil {
return trace.Wrap(err)
}
}
}
}
}
func (s *remoteSite) DialAuthServer(params reversetunnelclient.DialParams) (net.Conn, error) {
conn, err := s.connThroughTunnel(&sshutils.DialReq{
Address: constants.RemoteAuthServer,
ClientSrcAddr: stringOrEmpty(params.From),
ClientDstAddr: stringOrEmpty(params.OriginalClientDstAddr),
})
return conn, err
}
// Dial is used to connect a requesting client (say, tsh) to an SSH server
// located in a remote connected site, the connection goes through the
// reverse proxy tunnel.
func (s *remoteSite) Dial(params reversetunnelclient.DialParams) (net.Conn, error) {
localRecCfg, err := s.localAccessPoint.GetSessionRecordingConfig(s.ctx)
if err != nil {
return nil, trace.Wrap(err)
}
if shouldDialAndForward(params, localRecCfg) {
return s.dialAndForward(params)
}
if params.ConnType == types.NodeTunnel {
// If the remote cluster is recording at the proxy we need to respect
// that and forward and record the session. We will be connecting
// to the node without connecting through the remote proxy, so the
// session won't have a chance to get recorded at the remote proxy.
remoteRecCfg, err := s.remoteAccessPoint.GetSessionRecordingConfig(s.ctx)
if err != nil {
return nil, trace.Wrap(err)
}
if services.IsRecordAtProxy(remoteRecCfg.GetMode()) {
return s.dialAndForward(params)
}
}
// Attempt to perform a direct TCP dial.
return s.DialTCP(params)
}
func (s *remoteSite) DialTCP(params reversetunnelclient.DialParams) (net.Conn, error) {
s.logger.DebugContext(s.ctx, "Initiating dial request",
"source_addr", logutils.StringerAttr(params.From),
"target_addr", logutils.StringerAttr(params.To),
)
conn, err := s.connThroughTunnel(&sshutils.DialReq{
Address: params.To.String(),
ServerID: params.ServerID,
ConnType: params.ConnType,
ClientSrcAddr: stringOrEmpty(params.From),
ClientDstAddr: stringOrEmpty(params.OriginalClientDstAddr),
IsAgentlessNode: params.IsAgentlessNode,
})
if err != nil {
return nil, trace.Wrap(err)
}
return conn, nil
}
func (s *remoteSite) dialAndForward(params reversetunnelclient.DialParams) (_ net.Conn, retErr error) {
if params.GetUserAgent == nil && !params.IsAgentlessNode {
return nil, trace.BadParameter("user agent getter is required for teleport nodes")
}
s.logger.DebugContext(s.ctx, "Initiating dial and forward request",
"source_addr", logutils.StringerAttr(params.From),
"target_addr", logutils.StringerAttr(params.To),
)
// request user agent connection if a SSH user agent is set
var userAgent teleagent.Agent
if params.GetUserAgent != nil {
ua, err := params.GetUserAgent()
if err != nil {
return nil, trace.Wrap(err)
}
userAgent = ua
defer func() {
if retErr != nil {
retErr = trace.NewAggregate(retErr, userAgent.Close())
}
}()
}
// Get a host certificate for the forwarding node from the cache.
hostCertificate, err := s.certificateCache.getHostCertificate(s.ctx, params.Address, params.Principals)
if err != nil {
return nil, trace.Wrap(err)
}
targetConn, err := s.connThroughTunnel(&sshutils.DialReq{
Address: params.To.String(),
ServerID: params.ServerID,
ConnType: params.ConnType,
ClientSrcAddr: stringOrEmpty(params.From),
ClientDstAddr: stringOrEmpty(params.OriginalClientDstAddr),
IsAgentlessNode: params.IsAgentlessNode,
})
if err != nil {
return nil, trace.Wrap(err)
}
// Create a forwarding server that serves a single SSH connection on it. This
// server does not need to close, it will close and release all resources
// once conn is closed.
serverConfig := forward.ServerConfig{
LocalAuthClient: s.localClient,
TargetClusterAccessPoint: s.remoteAccessPoint,
UserAgent: userAgent,
IsAgentlessNode: params.IsAgentlessNode,
AgentlessSigner: params.AgentlessSigner,
TargetConn: targetConn,
SrcAddr: params.From,
DstAddr: params.To,
HostCertificate: hostCertificate,
Ciphers: s.srv.Config.Ciphers,
KEXAlgorithms: s.srv.Config.KEXAlgorithms,
MACAlgorithms: s.srv.Config.MACAlgorithms,
DataDir: s.srv.Config.DataDir,
Address: params.Address,
UseTunnel: UseTunnel(s.logger, targetConn),
FIPS: s.srv.FIPS,
HostUUID: s.srv.ID,
Emitter: s.srv.Config.Emitter,
ParentContext: s.srv.Context,
LockWatcher: s.srv.LockWatcher,
TargetID: params.ServerID,
TargetAddr: params.To.String(),
TargetHostname: params.Address,
TargetServer: params.TargetServer,
Clock: s.clock,
}
// Ensure the hostname is set correctly if we have details of the target
if params.TargetServer != nil {
serverConfig.TargetHostname = params.TargetServer.GetHostname()
}
remoteServer, err := forward.New(serverConfig)
if err != nil {
return nil, trace.Wrap(err)
}
go remoteServer.Serve()
// Return a connection to the forwarding server.
conn, err := remoteServer.Dial()
if err != nil {
return nil, trace.Wrap(err)
}
return conn, nil
}
// UseTunnel makes a channel request asking for the type of connection. If
// the other side does not respond (older cluster) or takes to long to
// respond, be on the safe side and assume it's not a tunnel connection.
func UseTunnel(logger *slog.Logger, c *sshutils.ChConn) bool {
responseCh := make(chan bool, 1)
go func() {
ok, err := c.SendRequest(sshutils.ConnectionTypeRequest, true, nil)
if err != nil {
responseCh <- false
return
}
responseCh <- ok
}()
select {
case response := <-responseCh:
return response
case <-time.After(1 * time.Second):
logger.DebugContext(context.Background(), "Timed out waiting for response: returning false")
return false
}
}
func (s *remoteSite) connThroughTunnel(req *sshutils.DialReq) (*sshutils.ChConn, error) {
s.logger.DebugContext(s.ctx, "Requesting connection in remote cluster.",
"target_address", req.Address,
"target_server_id", req.ServerID,
)
// Loop through existing remote connections and try and establish a
// connection over the "reverse tunnel".
var conn *sshutils.ChConn
var err error
for i := 0; i < s.connectionCount(); i++ {
conn, err = s.chanTransportConn(req)
if err == nil {
return conn, nil
}
s.logger.WarnContext(s.ctx, "Request for connection to remote site failed", "error", err)
}
// Didn't connect and no error? This means we didn't have any connected
// tunnels to try.
if err == nil {
// Return the appropriate message if the user is trying to connect to a
// cluster or a node.
if req.Address != constants.RemoteAuthServer {
return nil, trace.ConnectionProblem(nil, "node %v is offline", req.Address)
}
return nil, trace.ConnectionProblem(nil, "cluster %v is offline", s.GetName())
}
return nil, err
}
func (s *remoteSite) chanTransportConn(req *sshutils.DialReq) (*sshutils.ChConn, error) {
rconn, err := s.nextConn()
if err != nil {
return nil, trace.Wrap(err)
}
conn, markInvalid, err := sshutils.ConnectProxyTransport(rconn.sconn, req, false)
if err != nil {
if markInvalid {
rconn.markInvalid(err)
}
return nil, trace.Wrap(err)
}
return conn, nil
}