Files
teleport/lib/srv/desktop/windows_server.go
T
Michael Wilson 49c64aabcb Move the authorizer into its own package. (#22776)
* Move the authorizer into its own package.

The Authorizer and associated functions and constants have been moved into
their own package `authz`. This will allow us to more easily create separate
services for gRPC functionality so that we're not pushing as much into the
`lib/auth` package.

Though this PR is large, the only actual code change here is making
`ContextForLocalUser` exported and changing the `permissions_test.go` so that
it doesn't rely on the `lib/auth` package.

* Use context functions.

* Test and linting fixes.

* Add in a temporary alias file to ensure e still compiles.

* Use functions instead of var aliases for performance reasons.

* Adding in godocs, cleaning up tests.

* Fix tests.

* Add a few more aliases.
2023-03-09 02:21:10 +00:00

1214 lines
43 KiB
Go

/*
Copyright 2021 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package desktop
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"net"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"github.com/go-ldap/ldap/v3"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
"github.com/sirupsen/logrus"
"github.com/gravitational/teleport"
apidefaults "github.com/gravitational/teleport/api/defaults"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/types/events"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/auth/windows"
"github.com/gravitational/teleport/lib/authz"
"github.com/gravitational/teleport/lib/defaults"
libevents "github.com/gravitational/teleport/lib/events"
"github.com/gravitational/teleport/lib/events/filesessions"
"github.com/gravitational/teleport/lib/limiter"
"github.com/gravitational/teleport/lib/modules"
"github.com/gravitational/teleport/lib/reversetunnel"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/session"
"github.com/gravitational/teleport/lib/srv"
"github.com/gravitational/teleport/lib/srv/desktop/rdp/rdpclient"
"github.com/gravitational/teleport/lib/srv/desktop/tdp"
"github.com/gravitational/teleport/lib/tlsca"
"github.com/gravitational/teleport/lib/utils"
)
const (
// dnsDialTimeout is the timeout for dialing the LDAP server
// when resolving Windows Desktop hostnames
dnsDialTimeout = 5 * time.Second
// ldapDialTimeout is the timeout for dialing the LDAP server
// when making an initial connection
ldapDialTimeout = 5 * time.Second
// ldapRequestTimeout is the timeout for making LDAP requests.
// It is larger than the dial timeout because LDAP queries in large
// Active Directory environments may take longer to complete.
ldapRequestTimeout = 20 * time.Second
// windowsDesktopServiceCertTTL is the TTL for certificates issued to the
// Windows Desktop Service in order to authenticate with the LDAP server.
// It is set longer than the Windows certificates for users because it is
// not used for interactive login and is only used when issuing certs for
// a restrictive service account.
windowsDesktopServiceCertTTL = 8 * time.Hour
// windowsDesktopServiceCertRetryInterval indicates how often to retry
// issuing an LDAP certificate if the operation fails.
windowsDesktopServiceCertRetryInterval = 10 * time.Minute
)
// ComputerAttributes are the attributes we fetch when discovering
// Windows hosts via LDAP
// see: https://docs.microsoft.com/en-us/windows/win32/adschema/c-computer#windows-server-2012-attributes
var ComputerAttributes = []string{
windows.AttrName,
windows.AttrCommonName,
windows.AttrDistinguishedName,
windows.AttrDNSHostName,
windows.AttrObjectGUID,
windows.AttrOS,
windows.AttrOSVersion,
windows.AttrPrimaryGroupID,
}
// WindowsService implements the RDP-based Windows desktop access service.
//
// This service accepts mTLS connections from the proxy, establishes RDP
// connections to Windows hosts and translates RDP into Teleport's desktop
// protocol.
type WindowsService struct {
cfg WindowsServiceConfig
middleware *auth.Middleware
ca *windows.CertificateStoreClient
lc *windows.LDAPClient
mu sync.Mutex // mu protects the fields that follow
ldapConfigured bool
ldapInitialized bool
ldapCertRenew *time.Timer
streamer libevents.Streamer
// lastDisoveryResults stores the results of the most recent LDAP search
// when desktop discovery is enabled.
// no synchronization is necessary because this is only read/written from
// the reconciler goroutine.
lastDiscoveryResults types.ResourcesWithLabelsMap
// Windows hosts discovered via LDAP likely won't resolve with the
// default DNS resolver, so we need a custom resolver that will
// query the domain controller.
dnsResolver *net.Resolver
// clusterName is the cached local cluster name, to avoid calling
// cfg.AccessPoint.GetClusterName multiple times.
clusterName string
// auditCache caches information from shared directory
// TDP messages that are needed for
// creating shared directory audit events.
auditCache sharedDirectoryAuditCache
closeCtx context.Context
close func()
}
// WindowsServiceConfig contains all necessary configuration values for a
// WindowsService.
type WindowsServiceConfig struct {
// Log is the logging sink for the service.
Log logrus.FieldLogger
// Clock provides current time.
Clock clockwork.Clock
DataDir string
// Authorizer is used to authorize requests.
Authorizer authz.Authorizer
// LockWatcher is used to monitor for new locks.
LockWatcher *services.LockWatcher
// Emitter emits audit log events.
Emitter events.Emitter
// TLS is the TLS server configuration.
TLS *tls.Config
// AccessPoint is the Auth API client (with caching).
AccessPoint auth.WindowsDesktopAccessPoint
// AuthClient is the Auth API client (without caching).
AuthClient auth.ClientI
// ConnLimiter limits the number of active connections per client IP.
ConnLimiter *limiter.ConnectionsLimiter
// Heartbeat contains configuration for service heartbeats.
Heartbeat HeartbeatConfig
// HostLabelsFn gets labels that should be applied to a Windows host.
HostLabelsFn func(host string) map[string]string
// ShowDesktopWallpaper determines whether desktop sessions will show a
// user-selected wallpaper vs a system-default, single-color wallpaper.
ShowDesktopWallpaper bool
// LDAPConfig contains parameters for connecting to an LDAP server.
// LDAP functionality is disabled if Addr is empty.
windows.LDAPConfig
// DiscoveryBaseDN is the base DN for searching for Windows Desktops.
// Desktop discovery is disabled if this field is empty.
DiscoveryBaseDN string
// DiscoveryLDAPFilters are additional LDAP filters for searching for
// Windows Desktops. If multiple filters are specified, they are ANDed
// together into a single search.
DiscoveryLDAPFilters []string
// DiscoveryLDAPAttributeLabels are optional LDAP attributes to convert
// into Teleport labels.
DiscoveryLDAPAttributeLabels []string
// Hostname of the windows desktop service
Hostname string
// ConnectedProxyGetter gets the proxies teleport is connected to.
ConnectedProxyGetter *reversetunnel.ConnectedProxyGetter
Labels map[string]string
}
// HeartbeatConfig contains the configuration for service heartbeats.
type HeartbeatConfig struct {
// HostUUID is the UUID of the host that this service runs on. Used as the
// name of the created API object.
HostUUID string
// PublicAddr is the public address of this service.
PublicAddr string
// OnHeartbeat is called after each heartbeat attempt.
OnHeartbeat func(error)
// StaticHosts is an optional list of AD-connected static Windows hosts to register.
StaticHosts []utils.NetAddr
// NonADHosts is an optional list of static Windows hosts to register, that are not part of Active Directory.
NonADHosts []utils.NetAddr
}
func (cfg *WindowsServiceConfig) checkAndSetDiscoveryDefaults() error {
switch {
case cfg.DiscoveryBaseDN == types.Wildcard:
cfg.DiscoveryBaseDN = cfg.DomainDN()
case len(cfg.DiscoveryBaseDN) > 0:
if _, err := ldap.ParseDN(cfg.DiscoveryBaseDN); err != nil {
return trace.BadParameter("WindowsServiceConfig contains an invalid base_dn: %v", err)
}
}
for _, filter := range cfg.DiscoveryLDAPFilters {
if _, err := ldap.CompileFilter(filter); err != nil {
return trace.BadParameter("WindowsServiceConfig contains an invalid LDAP filter %q: %v", filter, err)
}
}
return nil
}
func (cfg *WindowsServiceConfig) CheckAndSetDefaults() error {
if cfg.Log == nil {
cfg.Log = logrus.New().WithField(trace.Component, teleport.ComponentWindowsDesktop)
}
if cfg.Clock == nil {
cfg.Clock = clockwork.NewRealClock()
}
if cfg.Authorizer == nil {
return trace.BadParameter("WindowsServiceConfig is missing Authorizer")
}
if cfg.LockWatcher == nil {
return trace.BadParameter("WindowsServiceConfig is missing LockWatcher")
}
if cfg.Emitter == nil {
return trace.BadParameter("WindowsServiceConfig is missing Emitter")
}
if cfg.TLS == nil {
return trace.BadParameter("WindowsServiceConfig is missing TLS")
}
if cfg.AccessPoint == nil {
return trace.BadParameter("WindowsServiceConfig is missing AccessPoint")
}
if cfg.AuthClient == nil {
return trace.BadParameter("WindowsServiceConfig is missing AuthClient")
}
if cfg.ConnLimiter == nil {
return trace.BadParameter("WindowsServiceConfig is missing ConnLimiter")
}
if err := cfg.Heartbeat.CheckAndSetDefaults(); err != nil {
return trace.Wrap(err)
}
if cfg.LDAPConfig.Addr != "" {
if err := cfg.LDAPConfig.Check(); err != nil {
return trace.Wrap(err)
}
}
if err := cfg.checkAndSetDiscoveryDefaults(); err != nil {
return trace.Wrap(err)
}
if cfg.ConnectedProxyGetter == nil {
cfg.ConnectedProxyGetter = reversetunnel.NewConnectedProxyGetter()
}
return nil
}
func (cfg *HeartbeatConfig) CheckAndSetDefaults() error {
if cfg.HostUUID == "" {
return trace.BadParameter("HeartbeatConfig is missing HostUUID")
}
if cfg.PublicAddr == "" {
return trace.BadParameter("HeartbeatConfig is missing PublicAddr")
}
if cfg.OnHeartbeat == nil {
return trace.BadParameter("HeartbeatConfig is missing OnHeartbeat")
}
return nil
}
// NewWindowsService initializes a new WindowsService.
//
// To start serving connections, call Serve.
// When done serving connections, call Close.
func NewWindowsService(cfg WindowsServiceConfig) (*WindowsService, error) {
if err := cfg.CheckAndSetDefaults(); err != nil {
return nil, trace.Wrap(err)
}
// It's possible to provide a CA certificate for the LDAP server
// and to skip TLS valdiation, though this may be an error, so try
// to warn the user.
// (You may need this configuration in order to use certificates to
// authenticate with LDAP when the LDAP server name is not correct
// in the certificate).
if cfg.LDAPConfig.CA != nil && cfg.LDAPConfig.InsecureSkipVerify {
cfg.Log.Warn("LDAP configuration specifies both a CA certificate and insecure_skip_verify." +
"TLS connections to the LDAP server will not be verified. If this is intentional, disregard this warning.")
}
clusterName, err := cfg.AccessPoint.GetClusterName()
if err != nil {
return nil, trace.Wrap(err, "fetching cluster name")
}
var resolver *net.Resolver
if cfg.LDAPConfig.Addr != "" {
// Here we assume the LDAP server is an Active Directory Domain Controller,
// which means it should also be a DNS server that can resolve Windows hosts.
dnsServer, _, err := net.SplitHostPort(cfg.LDAPConfig.Addr)
if err != nil {
return nil, trace.Wrap(err)
}
dnsAddr := net.JoinHostPort(dnsServer, "53")
cfg.Log.Debugln("DNS lookups will be performed against", dnsAddr)
resolver = &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
// Ignore the address provided, and always explicitly dial
// the domain controller.
d := net.Dialer{Timeout: dnsDialTimeout}
return d.DialContext(ctx, network, dnsAddr)
},
}
}
clustername, err := cfg.AccessPoint.GetClusterName()
if err != nil {
return nil, trace.Wrap(err)
}
ctx, close := context.WithCancel(context.Background())
s := &WindowsService{
cfg: cfg,
middleware: &auth.Middleware{
ClusterName: clustername.GetClusterName(),
AcceptedUsage: []string{teleport.UsageWindowsDesktopOnly},
},
dnsResolver: resolver,
lc: &windows.LDAPClient{Cfg: cfg.LDAPConfig},
clusterName: clusterName.GetClusterName(),
closeCtx: ctx,
close: close,
auditCache: newSharedDirectoryAuditCache(),
}
s.ca = windows.NewCertificateStoreClient(windows.CertificateStoreConfig{
AccessPoint: s.cfg.AccessPoint,
LDAPConfig: s.cfg.LDAPConfig,
Log: s.cfg.Log,
ClusterName: s.clusterName,
LC: s.lc,
})
if s.cfg.LDAPConfig.Addr != "" {
s.ldapConfigured = true
// initialize LDAP - if this fails it will automatically schedule a retry.
// we don't want to return an error in this case, because failure to start
// the service brings down the entire Teleport process
if err := s.initializeLDAP(); err != nil {
s.cfg.Log.WithError(err).Error("initializing LDAP client, will retry")
}
}
ok := false
defer func() {
if !ok {
s.Close()
}
}()
recConfig, err := s.cfg.AccessPoint.GetSessionRecordingConfig(s.closeCtx)
if err != nil {
return nil, trace.Wrap(err)
}
streamer, err := s.newStreamer(s.closeCtx, recConfig)
if err != nil {
return nil, trace.Wrap(err)
}
s.streamer = streamer
if err := s.startServiceHeartbeat(); err != nil {
return nil, trace.Wrap(err)
}
if err := s.startStaticHostHeartbeats(); err != nil {
return nil, trace.Wrap(err)
}
if len(s.cfg.DiscoveryBaseDN) > 0 {
if err := s.startDesktopDiscovery(); err != nil {
return nil, trace.Wrap(err)
}
} else if len(s.cfg.Heartbeat.StaticHosts) == 0 && len(s.cfg.Heartbeat.NonADHosts) == 0 {
s.cfg.Log.Warnln("desktop discovery via LDAP is disabled, and no hosts are defined in the configuration; there will be no Windows desktops available to connect")
} else {
s.cfg.Log.Infoln("desktop discovery via LDAP is disabled, set 'base_dn' to enable")
}
ok = true
return s, nil
}
func (s *WindowsService) newStreamWriter(record bool, sessionID string) (libevents.StreamWriter, error) {
// AuditWriter doesn't always respect the RecordOuptut field,
// so ensure the session isn't recorded by using a discard stream.
// See https://github.com/gravitational/teleport/issues/16773
if !record {
return &libevents.DiscardStream{}, nil
}
return libevents.NewAuditWriter(libevents.AuditWriterConfig{
Component: teleport.ComponentWindowsDesktop,
Namespace: apidefaults.Namespace,
Context: s.closeCtx,
Clock: s.cfg.Clock,
ClusterName: s.clusterName,
SessionID: session.ID(sessionID),
Streamer: s.streamer,
ServerID: s.cfg.Heartbeat.HostUUID,
RecordOutput: true,
})
}
// newStreamer creates a streamer (sync or async) based on the cluster configuration.
// Synchronous streamers send events directly to the auth server, and blocks if the server
// cannot keep up. Asynchronous streamers buffers the events to disk and uploads them later.
func (s *WindowsService) newStreamer(ctx context.Context, recConfig types.SessionRecordingConfig) (libevents.Streamer, error) {
if services.IsRecordSync(recConfig.GetMode()) {
s.cfg.Log.Debugf("using sync streamer (for mode %v)", recConfig.GetMode())
return s.cfg.AuthClient, nil
}
s.cfg.Log.Debugf("using async streamer (for mode %v)", recConfig.GetMode())
uploadDir := filepath.Join(s.cfg.DataDir, teleport.LogsDir, teleport.ComponentUpload,
libevents.StreamingSessionsDir, apidefaults.Namespace)
fileStreamer, err := filesessions.NewStreamer(uploadDir)
if err != nil {
return nil, trace.Wrap(err)
}
return libevents.NewTeeStreamer(fileStreamer, s.cfg.Emitter), nil
}
func (s *WindowsService) tlsConfigForLDAP() (*tls.Config, error) {
// trim NETBIOS name from username
user := s.cfg.Username
if i := strings.LastIndex(s.cfg.Username, `\`); i != -1 {
user = user[i+1:]
}
if s.cfg.SID == "" {
s.cfg.Log.Warnf(`Your LDAP config is missing the SID of the user you're
using to sign in. This is set to become a strict requirement by May 2023,
please update your configuration file before then.`)
}
certDER, keyDER, err := s.generateCredentials(s.closeCtx, user, s.cfg.Domain, windowsDesktopServiceCertTTL, s.cfg.SID)
if err != nil {
return nil, trace.Wrap(err)
}
cert, err := x509.ParseCertificate(certDER)
if err != nil {
return nil, trace.Wrap(err, "parsing cert DER")
}
key, err := x509.ParsePKCS1PrivateKey(keyDER)
if err != nil {
return nil, trace.Wrap(err, "parsing key DER")
}
tc := &tls.Config{
Certificates: []tls.Certificate{
{
Certificate: [][]byte{cert.Raw},
PrivateKey: key,
},
},
InsecureSkipVerify: s.cfg.InsecureSkipVerify,
ServerName: s.cfg.ServerName,
}
if s.cfg.CA != nil {
pool := x509.NewCertPool()
pool.AddCert(s.cfg.CA)
tc.RootCAs = pool
}
return tc, nil
}
// initializeLDAP requests a TLS certificate from the auth server to be used for
// authenticating with the LDAP server. If the certificate is obtained, and
// authentication with the LDAP server succeeds, it schedules a renewal to take
// place before the certificate expires. If we are unable to obtain a certificate
// and authenticate with the LDAP server, then the operation will be automatically
// retried.
//
// This method is safe for concurrent calls.
func (s *WindowsService) initializeLDAP() error {
tc, err := s.tlsConfigForLDAP()
if trace.IsAccessDenied(err) && modules.GetModules().BuildType() == modules.BuildEnterprise {
s.cfg.Log.Warn("Could not generate certificate for LDAPS. Ensure that the auth server is licensed for desktop access.")
}
if err != nil {
s.mu.Lock()
s.ldapInitialized = false
// in the case where we're not licensed for desktop access, we retry less frequently,
// since this is likely not an intermittent error that will resolve itself quickly
s.scheduleNextLDAPCertRenewalLocked(windowsDesktopServiceCertRetryInterval * 3)
s.mu.Unlock()
return trace.Wrap(err)
}
conn, err := ldap.DialURL("ldaps://"+s.cfg.Addr,
ldap.DialWithTLSDialer(tc, &net.Dialer{Timeout: ldapDialTimeout}))
if err != nil {
s.mu.Lock()
s.ldapInitialized = false
s.scheduleNextLDAPCertRenewalLocked(windowsDesktopServiceCertRetryInterval)
s.mu.Unlock()
return trace.Wrap(err, "dial")
}
conn.SetTimeout(ldapRequestTimeout)
s.lc.SetClient(conn)
// Note: admin still needs to import our CA into the Group Policy following
// https://docs.vmware.com/en/VMware-Horizon-7/7.13/horizon-installation/GUID-7966AE16-D98F-430E-A916-391E8EAAFE18.html
//
// We can find the group policy object via LDAP, but it only contains an
// SMB file path with the actual policy. See
// https://en.wikipedia.org/wiki/Group_Policy
//
// In theory, we could update the policy file(s) over SMB following
// https://docs.microsoft.com/en-us/previous-versions/windows/desktop/policy/registry-policy-file-format,
// but I'm leaving this for later.
//
if err := s.ca.Update(s.closeCtx); err != nil {
return trace.Wrap(err)
}
s.mu.Lock()
s.ldapInitialized = true
s.scheduleNextLDAPCertRenewalLocked(windowsDesktopServiceCertTTL / 3)
s.mu.Unlock()
return nil
}
// scheduleNextLDAPCertRenewalLocked schedules a renewal of our LDAP credentials
// after some amount of time has elapsed. If an existing renewal is already
// scheduled, it is canceled and this new one takes its place.
//
// The lock on s.mu MUST be held.
func (s *WindowsService) scheduleNextLDAPCertRenewalLocked(after time.Duration) {
s.cfg.Log.Infof("next LDAP cert renewal scheduled in %v", after)
if s.ldapCertRenew != nil {
s.ldapCertRenew.Reset(after)
} else {
s.ldapCertRenew = time.AfterFunc(after, func() {
if err := s.initializeLDAP(); err != nil {
s.cfg.Log.WithError(err).Error("couldn't renew certificate for LDAP auth")
}
})
}
}
func (s *WindowsService) startServiceHeartbeat() error {
heartbeat, err := srv.NewHeartbeat(srv.HeartbeatConfig{
Context: s.closeCtx,
Component: teleport.ComponentWindowsDesktop,
Mode: srv.HeartbeatModeWindowsDesktopService,
Announcer: s.cfg.AccessPoint,
GetServerInfo: s.getServiceHeartbeatInfo,
KeepAlivePeriod: apidefaults.ServerKeepAliveTTL(),
AnnouncePeriod: apidefaults.ServerAnnounceTTL/2 + utils.RandomDuration(apidefaults.ServerAnnounceTTL/10),
CheckPeriod: defaults.HeartbeatCheckPeriod,
ServerTTL: apidefaults.ServerAnnounceTTL,
OnHeartbeat: s.cfg.Heartbeat.OnHeartbeat,
})
if err != nil {
return trace.Wrap(err)
}
go func() {
if err := heartbeat.Run(); err != nil {
s.cfg.Log.WithError(err).Error("Heartbeat ended with error")
}
}()
return nil
}
// startStaticHostHeartbeats spawns heartbeat routines for all static hosts in
// this service. We use heartbeats instead of registering once at startup to
// support expiration.
//
// When a WindowsService with a list of static hosts disappears, those hosts
// should eventually get cleaned up. But they should exist as long as the
// service itself is running.
func (s *WindowsService) startStaticHostHeartbeats() error {
for _, host := range s.cfg.Heartbeat.StaticHosts {
if err := s.startStaticHostHeartbeat(host, false); err != nil {
return err
}
}
for _, host := range s.cfg.Heartbeat.NonADHosts {
if err := s.startStaticHostHeartbeat(host, true); err != nil {
return trace.Wrap(err)
}
}
return nil
}
func (s *WindowsService) startStaticHostHeartbeat(host utils.NetAddr, nonAD bool) error {
heartbeat, err := srv.NewHeartbeat(srv.HeartbeatConfig{
Context: s.closeCtx,
Component: teleport.ComponentWindowsDesktop,
Mode: srv.HeartbeatModeWindowsDesktop,
Announcer: s.cfg.AccessPoint,
GetServerInfo: s.staticHostHeartbeatInfo(host, s.cfg.HostLabelsFn, nonAD),
KeepAlivePeriod: apidefaults.ServerKeepAliveTTL(),
AnnouncePeriod: apidefaults.ServerAnnounceTTL/2 + utils.RandomDuration(apidefaults.ServerAnnounceTTL/10),
CheckPeriod: 5 * time.Minute,
ServerTTL: apidefaults.ServerAnnounceTTL,
OnHeartbeat: s.cfg.Heartbeat.OnHeartbeat,
})
if err != nil {
return trace.Wrap(err)
}
go func() {
if err := heartbeat.Run(); err != nil {
s.cfg.Log.WithError(err).Error("Heartbeat ended with error")
}
}()
return nil
}
// Close instructs the server to stop accepting new connections and abort all
// established ones. Close does not wait for the connections to be finished.
func (s *WindowsService) Close() error {
s.mu.Lock()
defer s.mu.Unlock()
if s.ldapCertRenew != nil {
s.ldapCertRenew.Stop()
}
s.close()
s.lc.Close()
return nil
}
// Serve starts serving TLS connections for plainLis. plainLis should be a TCP
// listener and Serve will handle TLS internally.
func (s *WindowsService) Serve(plainLis net.Listener) error {
lis := tls.NewListener(plainLis, s.cfg.TLS)
defer lis.Close()
for {
select {
case <-s.closeCtx.Done():
return trace.Wrap(s.closeCtx.Err())
default:
}
conn, err := lis.Accept()
if err != nil {
if utils.IsOKNetworkError(err) || trace.IsConnectionProblem(err) {
return nil
}
return trace.Wrap(err)
}
proxyConn, ok := conn.(*tls.Conn)
if !ok {
return trace.ConnectionProblem(nil, "Got %T from TLS listener, expected *tls.Conn", conn)
}
go s.handleConnection(proxyConn)
}
}
func (s *WindowsService) readyForConnections() bool {
s.mu.Lock()
defer s.mu.Unlock()
// If LDAP was not configured, we assume all hosts are non-AD
// and the server can accept connections right away.
if !s.ldapConfigured {
return true
}
// If LDAP was configured, then we need to wait for it to be initialized
// before accepting connections.
return s.ldapInitialized
}
func (s *WindowsService) ldapReady() bool {
s.mu.Lock()
defer s.mu.Unlock()
return s.ldapInitialized
}
// handleConnection handles TLS connections from a Teleport proxy.
// It authenticates and authorizes the connection, and then begins
// translating the TDP messages from the proxy into native RDP.
func (s *WindowsService) handleConnection(proxyConn *tls.Conn) {
log := s.cfg.Log
tdpConn := tdp.NewConn(proxyConn)
defer tdpConn.Close()
// Inline function to enforce that we are centralizing TDP Error sending in this function.
sendTDPError := func(message string) {
if err := tdpConn.SendNotification(message, tdp.SeverityError); err != nil {
log.Errorf("Failed to send TDP error message %v", err)
}
}
// don't handle connections until the LDAP initialization retry loop has succeeded
// (it would fail anyway, but this presents a better error to the user)
if !s.readyForConnections() {
const msg = "This service cannot accept connections until LDAP initialization has completed."
log.Error(msg)
sendTDPError(msg)
return
}
// Check connection limits.
remoteAddr, _, err := net.SplitHostPort(proxyConn.RemoteAddr().String())
if err != nil {
log.WithError(err).Errorf("Could not parse client IP from %q", proxyConn.RemoteAddr().String())
sendTDPError("Internal error.")
return
}
log = log.WithField("client-ip", remoteAddr)
if err := s.cfg.ConnLimiter.AcquireConnection(remoteAddr); err != nil {
log.WithError(err).Warning("Connection limit exceeded, rejecting connection")
sendTDPError("Connection limit exceeded.")
return
}
defer s.cfg.ConnLimiter.ReleaseConnection(remoteAddr)
// Authenticate the client.
ctx, err := s.middleware.WrapContextWithUser(s.closeCtx, proxyConn)
if err != nil {
log.WithError(err).Warning("mTLS authentication failed for incoming connection")
sendTDPError("Connection authentication failed.")
return
}
log.Debug("Authenticated Windows desktop connection")
authContext, err := s.cfg.Authorizer.Authorize(ctx)
if err != nil {
log.WithError(err).Warning("authorization failed for Windows desktop connection")
sendTDPError("Connection authorization failed.")
return
}
// Fetch the target desktop info. Name of the desktop is passed via SNI.
desktopName := strings.TrimSuffix(proxyConn.ConnectionState().ServerName, SNISuffix)
log = log.WithField("desktop-name", desktopName)
desktops, err := s.cfg.AccessPoint.GetWindowsDesktops(ctx,
types.WindowsDesktopFilter{HostID: s.cfg.Heartbeat.HostUUID, Name: desktopName})
if err != nil {
log.WithError(err).Warning("Failed to fetch desktop by name")
sendTDPError("Teleport failed to find the requested desktop in its database.")
return
}
if len(desktops) == 0 {
log.Error("no windows desktops with HostID %s and Name %s", s.cfg.Heartbeat.HostUUID,
desktopName)
sendTDPError(fmt.Sprintf("Could not find desktop %v.", desktopName))
return
}
desktop := desktops[0]
log = log.WithField("desktop-addr", desktop.GetAddr())
log.Debug("Connecting to Windows desktop")
defer log.Debug("Windows desktop disconnected")
if err := s.connectRDP(ctx, log, tdpConn, desktop, authContext); err != nil {
log.Errorf("RDP connection failed: %v", err)
sendTDPError("RDP connection failed.")
return
}
}
func (s *WindowsService) connectRDP(ctx context.Context, log logrus.FieldLogger, tdpConn *tdp.Conn, desktop types.WindowsDesktop, authCtx *authz.Context) error {
identity := authCtx.Identity.GetIdentity()
netConfig, err := s.cfg.AccessPoint.GetClusterNetworkingConfig(ctx)
if err != nil {
return trace.Wrap(err)
}
authPref, err := s.cfg.AccessPoint.GetAuthPreference(ctx)
if err != nil {
return trace.Wrap(err)
}
sessionID := session.NewID()
// in order for the session to be recorded, the cluster's session recording mode must
// not be "off" and the user's roles must enable recording
recordSession := false
if authCtx.Checker.RecordDesktopSession() {
recConfig, err := s.cfg.AccessPoint.GetSessionRecordingConfig(ctx)
if err != nil {
return trace.Wrap(err)
}
recordSession = recConfig.GetMode() != types.RecordOff
} else {
log.Infof("desktop session %v will not be recorded, user %v's roles disable recording", string(sessionID), authCtx.User.GetName())
}
var windowsUser string
authorize := func(login string) error {
windowsUser = login // capture attempted login user
state := authCtx.GetAccessState(authPref)
return authCtx.Checker.CheckAccess(
desktop,
state,
services.NewWindowsLoginMatcher(login))
}
// Use a context that is canceled when we're done handling
// this connection. This ensures that the connection monitor
// will stop checking for idle activity when the connection
// is closed.
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// Create a session tracker so that other services, such as
// the session upload completer, can track the session's lifetime.
if err := s.trackSession(ctx, &identity, windowsUser, string(sessionID), desktop); err != nil {
return trace.Wrap(err)
}
sw, err := s.newStreamWriter(recordSession, string(sessionID))
if err != nil {
return trace.Wrap(err)
}
// Closing the stream writer is needed to flush all recorded data
// and trigger the upload. Do it in a goroutine since depending on
// the session size it can take a while, and we don't want to block
// the client.
defer func() {
go func() {
if err := sw.Close(context.Background()); err != nil {
log.WithError(err).Errorf("closing stream writer for desktop session %v", sessionID.String())
}
}()
}()
delay := timer()
tdpConn.OnSend = s.makeTDPSendHandler(ctx, sw, delay, &identity, string(sessionID), desktop.GetAddr(), tdpConn)
tdpConn.OnRecv = s.makeTDPReceiveHandler(ctx, sw, delay, &identity, string(sessionID), desktop.GetAddr(), tdpConn)
sessionStartTime := s.cfg.Clock.Now().UTC().Round(time.Millisecond)
rdpc, err := rdpclient.New(rdpclient.Config{
Log: log,
GenerateUserCert: func(ctx context.Context, username string, ttl time.Duration) (certDER, keyDER []byte, err error) {
return s.generateUserCert(ctx, username, ttl, desktop)
},
CertTTL: windows.CertTTL,
Addr: desktop.GetAddr(),
Conn: tdpConn,
AuthorizeFn: authorize,
AllowClipboard: authCtx.Checker.DesktopClipboard(),
AllowDirectorySharing: authCtx.Checker.DesktopDirectorySharing(),
ShowDesktopWallpaper: s.cfg.ShowDesktopWallpaper,
})
if err != nil {
s.onSessionStart(ctx, sw, &identity, sessionStartTime, windowsUser, string(sessionID), desktop, err)
return trace.Wrap(err)
}
monitorCfg := srv.MonitorConfig{
Context: ctx,
Conn: tdpConn,
Clock: s.cfg.Clock,
ClientIdleTimeout: authCtx.Checker.AdjustClientIdleTimeout(netConfig.GetClientIdleTimeout()),
DisconnectExpiredCert: srv.GetDisconnectExpiredCertFromIdentity(authCtx.Checker, authPref, &identity),
Entry: log,
Emitter: s.cfg.Emitter,
LockWatcher: s.cfg.LockWatcher,
LockingMode: authCtx.Checker.LockingMode(authPref.GetLockingMode()),
LockTargets: append(services.LockTargetsFromTLSIdentity(identity), types.LockTarget{WindowsDesktop: desktop.GetName()}),
Tracker: rdpc,
TeleportUser: identity.Username,
ServerID: s.cfg.Heartbeat.HostUUID,
IdleTimeoutMessage: netConfig.GetClientIdleTimeoutMessage(),
MessageWriter: &monitorErrorSender{
log: log,
tdpConn: tdpConn,
},
}
// UpdateClientActivity before starting monitor to
// be doubly sure that the client isn't disconnected
// due to an idle timeout before its had the chance to
// call StartAndWait()
rdpc.UpdateClientActivity()
if err := srv.StartMonitor(monitorCfg); err != nil {
// if we can't establish a connection monitor then we can't enforce RBAC.
// consider this a connection failure and return an error
// (in the happy path, rdpc remains open until Wait() completes)
s.onSessionStart(ctx, sw, &identity, sessionStartTime, windowsUser, string(sessionID), desktop, err)
return trace.Wrap(err)
}
s.onSessionStart(ctx, sw, &identity, sessionStartTime, windowsUser, string(sessionID), desktop, nil)
err = rdpc.Run(ctx)
s.onSessionEnd(ctx, sw, &identity, sessionStartTime, recordSession, windowsUser, string(sessionID), desktop)
return trace.Wrap(err)
}
func (s *WindowsService) makeTDPSendHandler(ctx context.Context, emitter events.Emitter, delay func() int64,
id *tlsca.Identity, sessionID, desktopAddr string, tdpConn *tdp.Conn) func(m tdp.Message, b []byte) {
return func(m tdp.Message, b []byte) {
switch b[0] {
case byte(tdp.TypePNG2Frame), byte(tdp.TypePNGFrame), byte(tdp.TypeError), byte(tdp.TypeNotification):
e := &events.DesktopRecording{
Metadata: events.Metadata{
Type: libevents.DesktopRecordingEvent,
Time: s.cfg.Clock.Now().UTC().Round(time.Millisecond),
},
Message: b,
DelayMilliseconds: delay(),
}
if e.Size() > libevents.MaxProtoMessageSizeBytes {
// Technically a PNG frame is unbounded and could be too big for a single protobuf.
// In practice though, Windows limits RDP bitmaps to 64x64 pixels, and we compress
// the PNGs before they get here, so most PNG frames are under 500 bytes. The largest
// ones are around 2000 bytes. Anything approaching the limit of a single protobuf
// is likely some sort of DoS attempt and not legitimate RDP traffic, so we don't log it.
s.cfg.Log.Warnf("refusing to record %d byte PNG frame, image too large", len(b))
} else if err := emitter.EmitAuditEvent(ctx, e); err != nil {
s.cfg.Log.WithError(err).Warning("could not emit desktop recording event")
}
case byte(tdp.TypeClipboardData):
if clip, ok := m.(tdp.ClipboardData); ok {
// the TDP send handler emits a clipboard receive event, because we
// received clipboard data from the remote desktop and are sending
// it on the TDP connection
s.onClipboardReceive(ctx, emitter, id, sessionID, desktopAddr, int32(len(clip)))
}
case byte(tdp.TypeSharedDirectoryAcknowledge):
if message, ok := m.(tdp.SharedDirectoryAcknowledge); ok {
s.onSharedDirectoryAcknowledge(ctx, emitter, id, sessionID, desktopAddr, message)
}
case byte(tdp.TypeSharedDirectoryReadRequest):
if message, ok := m.(tdp.SharedDirectoryReadRequest); ok {
s.onSharedDirectoryReadRequest(ctx, emitter, id, sessionID, desktopAddr, message, tdpConn)
}
case byte(tdp.TypeSharedDirectoryWriteRequest):
if message, ok := m.(tdp.SharedDirectoryWriteRequest); ok {
s.onSharedDirectoryWriteRequest(ctx, emitter, id, sessionID, desktopAddr, message, tdpConn)
}
}
}
}
func (s *WindowsService) makeTDPReceiveHandler(ctx context.Context, emitter events.Emitter, delay func() int64,
id *tlsca.Identity, sessionID, desktopAddr string, tdpConn *tdp.Conn) func(m tdp.Message) {
return func(m tdp.Message) {
switch msg := m.(type) {
case tdp.ClientScreenSpec, tdp.MouseButton, tdp.MouseMove:
b, err := m.Encode()
if err != nil {
s.cfg.Log.WithError(err).Warning("could not emit desktop recording event")
}
e := &events.DesktopRecording{
Metadata: events.Metadata{
Type: libevents.DesktopRecordingEvent,
Time: s.cfg.Clock.Now().UTC().Round(time.Millisecond),
},
Message: b,
DelayMilliseconds: delay(),
}
if e.Size() > libevents.MaxProtoMessageSizeBytes {
// screen spec, mouse button, and mouse move are fixed size messages,
// so they cannot exceed the maximum size
s.cfg.Log.Warnf("refusing to record %d byte %T message", len(b), m)
} else if err := emitter.EmitAuditEvent(ctx, e); err != nil {
s.cfg.Log.WithError(err).Warning("could not emit desktop recording event")
}
case tdp.ClipboardData:
// the TDP receive handler emits a clipboard send event, because we
// received clipboard data from the user (over TDP) and are sending
// it to the remote desktop
s.onClipboardSend(ctx, emitter, id, sessionID, desktopAddr, int32(len(msg)))
case tdp.SharedDirectoryAnnounce:
s.onSharedDirectoryAnnounce(ctx, emitter, id, sessionID, desktopAddr, m.(tdp.SharedDirectoryAnnounce), tdpConn)
case tdp.SharedDirectoryReadResponse:
s.onSharedDirectoryReadResponse(ctx, emitter, id, sessionID, desktopAddr, msg)
case tdp.SharedDirectoryWriteResponse:
s.onSharedDirectoryWriteResponse(ctx, emitter, id, sessionID, desktopAddr, msg)
}
}
}
func (s *WindowsService) getServiceHeartbeatInfo() (types.Resource, error) {
srv, err := types.NewWindowsDesktopServiceV3(
types.Metadata{
Name: s.cfg.Heartbeat.HostUUID,
Labels: s.cfg.Labels,
},
types.WindowsDesktopServiceSpecV3{
Addr: s.cfg.Heartbeat.PublicAddr,
TeleportVersion: teleport.Version,
Hostname: s.cfg.Hostname,
ProxyIDs: s.cfg.ConnectedProxyGetter.GetProxyIDs(),
})
if err != nil {
return nil, trace.Wrap(err)
}
srv.SetExpiry(s.cfg.Clock.Now().UTC().Add(apidefaults.ServerAnnounceTTL))
return srv, nil
}
// staticHostHeartbeatInfo generates the Windows Desktop resource
// for heartbeating statically defined hosts
func (s *WindowsService) staticHostHeartbeatInfo(netAddr utils.NetAddr,
getHostLabels func(string) map[string]string, nonAD bool) func() (types.Resource, error) {
return func() (types.Resource, error) {
addr := netAddr.String()
name, err := s.nameForStaticHost(addr)
if err != nil {
return nil, trace.Wrap(err)
}
// for static hosts, we match against the host's addr,
// as the name is a randomly generated UUID
labels := getHostLabels(addr)
labels[types.OriginLabel] = types.OriginConfigFile
labels[types.ADLabel] = strconv.FormatBool(!nonAD)
desktop, err := types.NewWindowsDesktopV3(
name,
labels,
types.WindowsDesktopSpecV3{
Addr: addr,
Domain: s.cfg.Domain,
HostID: s.cfg.Heartbeat.HostUUID,
NonAD: nonAD,
})
if err != nil {
return nil, trace.Wrap(err)
}
desktop.SetExpiry(s.cfg.Clock.Now().UTC().Add(apidefaults.ServerAnnounceTTL))
return desktop, nil
}
}
// nameForStaticHost attempts to find the name of an existing Windows desktop
// with the same address. If no matching address is found, a new name is
// generated.
//
// The list of WindowsDesktop objects should be read from the local cache. It
// should be reasonably fast to do this scan on every heartbeat. However, with
// a very large number of desktops in the cluster, this may use up a lot of CPU
// time.
func (s *WindowsService) nameForStaticHost(addr string) (string, error) {
desktops, err := s.cfg.AccessPoint.GetWindowsDesktops(s.closeCtx,
types.WindowsDesktopFilter{})
if err != nil {
return "", trace.Wrap(err)
}
for _, d := range desktops {
if d.GetAddr() == addr {
return d.GetName(), nil
}
}
host, _, err := utils.SplitHostPort(addr)
if err != nil {
host = addr
}
parts := strings.Split(s.cfg.Heartbeat.HostUUID, "-")
prefix := parts[len(parts)-1]
return prefix + "-static-" + strings.ReplaceAll(host, ".", "-"), nil
}
// timer returns a closure that on each call returns the
// number of milliseconds that have elapsed since the first call.
// it returns 0 on the very first call.
func timer() func() int64 {
var first time.Time
return func() int64 {
if first.IsZero() {
first = time.Now()
return 0
}
return int64(time.Since(first) / time.Millisecond)
}
}
// generateUserCert generates a keypair for the given Windows username,
// optionally querying LDAP for the user's Security Identifier.
func (s *WindowsService) generateUserCert(ctx context.Context, username string, ttl time.Duration, desktop types.WindowsDesktop) (certDER, keyDER []byte, err error) {
var activeDirectorySID string
if !desktop.NonAD() {
// Find the user's SID
s.cfg.Log.Debugf("querying LDAP for objectSid of Windows username: %v", username)
filters := []string{
fmt.Sprintf("(%s=%s)", windows.AttrObjectCategory, windows.CategoryPerson),
fmt.Sprintf("(%s=%s)", windows.AttrObjectClass, windows.ClassUser),
fmt.Sprintf("(%s=%s)", windows.AttrSAMAccountName, username),
}
entries, err := s.lc.ReadWithFilter(s.cfg.LDAPConfig.DomainDN(), windows.CombineLDAPFilters(filters), []string{windows.AttrObjectSid})
// if LDAP-based desktop discovery is not enabled, there may not be enough
// traffic to keep the connection open. Attempt to open a new LDAP connection
// in this case.
if trace.IsConnectionProblem(err) {
s.initializeLDAP() // ignore error, this is a best effort attempt
entries, err = s.lc.ReadWithFilter(s.cfg.LDAPConfig.DomainDN(), windows.CombineLDAPFilters(filters), []string{windows.AttrObjectSid})
}
if err != nil {
return nil, nil, trace.Wrap(err)
}
if len(entries) == 0 {
return nil, nil, trace.NotFound("LDAP failed to return objectSid for Windows username: %v", username)
} else if len(entries) > 1 {
s.cfg.Log.Warnf("LDAP unexpectedly returned multiple entries for objectSid for username: %v, taking the first", username)
}
activeDirectorySID, err = windows.ADSIDStringFromLDAPEntry(entries[0])
if err != nil {
return nil, nil, trace.Wrap(err)
}
s.cfg.Log.Debugf("Found objectSid %v for Windows username %v", activeDirectorySID, username)
}
return s.generateCredentials(ctx, username, desktop.GetDomain(), ttl, activeDirectorySID)
}
// generateCredentials generates a private key / certificate pair for the given
// Windows username. The certificate has certain special fields different from
// the regular Teleport user certificate, to meet the requirements of Active
// Directory. See:
// https://docs.microsoft.com/en-us/windows/security/identity-protection/smart-cards/smart-card-certificate-requirements-and-enumeration
func (s *WindowsService) generateCredentials(ctx context.Context, username, domain string, ttl time.Duration, activeDirectorySID string) (certDER, keyDER []byte, err error) {
return windows.GenerateWindowsDesktopCredentials(ctx, &windows.GenerateCredentialsRequest{
Username: username,
Domain: domain,
TTL: ttl,
ClusterName: s.clusterName,
ActiveDirectorySID: activeDirectorySID,
LDAPConfig: s.cfg.LDAPConfig,
AuthClient: s.cfg.AuthClient,
})
}
// trackSession creates a session tracker for the given sessionID and
// attributes, and starts a goroutine to continually extend the tracker
// expiration while the session is active. Once the given ctx is closed,
// the tracker will be marked as terminated.
func (s *WindowsService) trackSession(ctx context.Context, id *tlsca.Identity, windowsUser string, sessionID string, desktop types.WindowsDesktop) error {
trackerSpec := types.SessionTrackerSpecV1{
SessionID: sessionID,
Kind: string(types.WindowsDesktopSessionKind),
State: types.SessionState_SessionStateRunning,
Hostname: s.cfg.Hostname,
Address: desktop.GetAddr(),
DesktopName: desktop.GetName(),
ClusterName: s.clusterName,
Login: windowsUser,
Participants: []types.Participant{{
User: id.Username,
}},
HostUser: id.Username,
Created: s.cfg.Clock.Now(),
}
s.cfg.Log.Debugf("Creating tracker for session %v", sessionID)
tracker, err := srv.NewSessionTracker(ctx, trackerSpec, s.cfg.AuthClient)
if err != nil {
return trace.Wrap(err)
}
go func() {
if err := tracker.UpdateExpirationLoop(ctx, s.cfg.Clock); err != nil {
s.cfg.Log.WithError(err).Warnf("Failed to update session tracker expiration for session %v", sessionID)
}
}()
go func() {
<-ctx.Done()
if err := tracker.Close(s.closeCtx); err != nil {
s.cfg.Log.WithError(err).Debugf("Failed to close session tracker for session %v", sessionID)
}
}()
return nil
}
// monitorErrorSender implements the io.StringWriter
// interface in order to allow us to pass connection
// monitor disconnect messages back to the frontend
// over the tdp.Conn
type monitorErrorSender struct {
log logrus.FieldLogger
tdpConn *tdp.Conn
}
func (m *monitorErrorSender) WriteString(s string) (n int, err error) {
if err := m.tdpConn.SendNotification(s, tdp.SeverityError); err != nil {
errMsg := fmt.Sprintf("Failed to send TDP error message %v: %v", s, err)
m.log.Error(errMsg)
return 0, trace.Errorf(errMsg)
}
return len(s), nil
}