diff --git a/lib/srv/desktop/discovery.go b/lib/srv/desktop/discovery.go index 7c9d8a76a0e58..60003d41b7c00 100644 --- a/lib/srv/desktop/discovery.go +++ b/lib/srv/desktop/discovery.go @@ -97,10 +97,15 @@ func (s *WindowsService) ldapSearchFilter() string { // getDesktopsFromLDAP discovers Windows hosts via LDAP func (s *WindowsService) getDesktopsFromLDAP() map[string]types.WindowsDesktop { - if !s.ldapReady() { - s.cfg.Logger.WarnContext(context.Background(), "skipping desktop discovery: LDAP not yet initialized") + // Check whether we've ever successfully initialized our LDAP client. + s.mu.Lock() + if !s.ldapInitialized { + s.cfg.Logger.DebugContext(context.Background(), "LDAP not ready, skipping discovery and attempting to reconnect") + s.mu.Unlock() + s.initializeLDAP() return nil } + s.mu.Unlock() filter := s.ldapSearchFilter() s.cfg.Logger.DebugContext(context.Background(), "searching for desktops", "filter", filter) @@ -248,7 +253,11 @@ func (s *WindowsService) lookupDesktop(ctx context.Context, hostname string) ([] // ldapEntryToWindowsDesktop generates the Windows Desktop resource // from an LDAP search result -func (s *WindowsService) ldapEntryToWindowsDesktop(ctx context.Context, entry *ldap.Entry, getHostLabels func(string) map[string]string) (types.WindowsDesktop, error) { +func (s *WindowsService) ldapEntryToWindowsDesktop( + ctx context.Context, + entry *ldap.Entry, + getHostLabels func(string) map[string]string, +) (types.WindowsDesktop, error) { hostname := entry.GetAttributeValue(windows.AttrDNSHostName) if hostname == "" { attrs := make([]string, len(entry.Attributes)) diff --git a/lib/srv/desktop/windows_server.go b/lib/srv/desktop/windows_server.go index 708a72ebd9c68..63ac573b46142 100644 --- a/lib/srv/desktop/windows_server.go +++ b/lib/srv/desktop/windows_server.go @@ -419,10 +419,60 @@ func NewWindowsService(cfg WindowsServiceConfig) (*WindowsService, error) { s.cfg.Logger.InfoContext(ctx, "desktop discovery via LDAP is disabled, set 'base_dn' to enable") } + // if LDAP-based discovery is not enabled, but we have configured LDAP + // then it's important that we periodically try to use the LDAP connection + // to detect connection closure + if s.ldapConfigured && len(s.cfg.DiscoveryBaseDN) == 0 { + s.startLDAPConnectionCheck(ctx) + } + ok = true return s, nil } +// startLDAPConnectionCheck starts a background process that +// periodically reads from the LDAP connection in order to detect +// connection closure, and reconnects if necessary. +// This is useful when LDAP-based discovery is disabled, because without +// discovery the connection goes idle and may be closed by the server. +func (s *WindowsService) startLDAPConnectionCheck(ctx context.Context) { + s.cfg.Logger.DebugContext(ctx, "starting LDAP connection checker") + go func() { + t := s.cfg.Clock.NewTicker(5 * time.Minute) + defer t.Stop() + + for { + select { + case <-t.Chan(): + // First check if we have successfully initialized the LDAP client. + // If not, then do that now and return. + // (This mimics the check that is performed when LDAP discovery is enabled.) + s.mu.Lock() + if !s.ldapInitialized { + s.cfg.Logger.DebugContext(context.Background(), "LDAP not ready, attempting to reconnect") + s.mu.Unlock() + s.initializeLDAP() + return + } + s.mu.Unlock() + + // If we have initialized the LDAP client, then try to use it to make sure we're still connected + // by attempting to read CAs in the NTAuth store (we know we have permissions to do so). + ntAuthDN := "CN=NTAuthCertificates,CN=Public Key Services,CN=Services,CN=Configuration," + s.cfg.LDAPConfig.DomainDN() + _, err := s.lc.Read(ntAuthDN, "certificationAuthority", []string{"cACertificate"}) + if trace.IsConnectionProblem(err) { + s.cfg.Logger.DebugContext(ctx, "detected broken LDAP connection, will reconnect") + if err := s.initializeLDAP(); err != nil { + s.cfg.Logger.WarnContext(ctx, "failed to reconnect to LDAP", "error", err) + } + } + case <-ctx.Done(): + return + } + } + }() +} + func (s *WindowsService) newSessionRecorder(recConfig types.SessionRecordingConfig, sessionID string) (libevents.SessionPreparerRecorder, error) { return recorder.New(recorder.Config{ SessionID: session.ID(sessionID), @@ -686,12 +736,6 @@ func (s *WindowsService) readyForConnections() bool { 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.