From fa73ab7b46ea1a015c629e1e796774a091f07914 Mon Sep 17 00:00:00 2001 From: Thomas Hallgren Date: Fri, 24 Nov 2023 09:00:49 +0100 Subject: [PATCH 1/3] The DNS search path on Windows is now restored when Telepresence quits The DNS search path that Telepresence uses to simulate the DNS lookup functionality in the connected cluster namespace was not removed by a `telepresence quit`, resulting in connectivity problems from the workstation. Telepresence will now remove the entries that it has added to the search list when it quits. Signed-off-by: Thomas Hallgren --- CHANGELOG.yml | 6 ++++++ pkg/client/rootd/dns/server_windows.go | 5 +++++ pkg/vif/device_windows.go | 18 ++++++++++++------ 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.yml b/CHANGELOG.yml index 7e29cf84d0..ace302eb87 100644 --- a/CHANGELOG.yml +++ b/CHANGELOG.yml @@ -33,6 +33,12 @@ items: - version: 2.17.1 date: (TBD) notes: + - type: bugfix + title: The DNS search path on Windows is now restored when Telepresence quits + body: The DNS search path that Telepresence uses to simulate the DNS lookup functionality in the connected + cluster namespace was not removed by a telepresence quit, resulting in connectivity problems + from the workstation. Telepresence will now remove the entries that it has added to the search list when + it quits. - type: bugfix title: Multiple services ports using the same target port would not get intercepted correctly. body: Intercepts didn't work when multiple service ports were using the same container port. Telepresence would diff --git a/pkg/client/rootd/dns/server_windows.go b/pkg/client/rootd/dns/server_windows.go index e44608e805..1dadf03238 100644 --- a/pkg/client/rootd/dns/server_windows.go +++ b/pkg/client/rootd/dns/server_windows.go @@ -31,6 +31,11 @@ func (s *Server) Worker(c context.Context, dev vif.Device, configureDNS func(net g := dgroup.NewGroup(c, dgroup.GroupConfig{}) g.Go("Server", func(c context.Context) error { // No need to close listener. It's closed by the dns server. + defer func() { + c, cancel := context.WithTimeout(context.WithoutCancel(c), 5*time.Second) + _ = dev.SetDNS(c, s.clusterDomain, s.config.RemoteIp, nil) + cancel() + }() s.processSearchPaths(g, s.updateRouterDNS, dev) return s.Run(c, make(chan struct{}), []net.PacketConn{listener}, nil, s.resolveInCluster) }) diff --git a/pkg/vif/device_windows.go b/pkg/vif/device_windows.go index da84479add..5bdf10dfc9 100644 --- a/pkg/vif/device_windows.go +++ b/pkg/vif/device_windows.go @@ -203,22 +203,26 @@ func (t *nativeDevice) setDNS(ctx context.Context, clusterDomain string, server if err != nil { return err } - // Windows does not use a dot suffix in the search path. - clusterDomain = strings.TrimSuffix(clusterDomain, ".") - // Put our new search path in front of other entries. Then include those // that don't end with our cluster domain (these are entries that aren't // managed by Telepresence). uniq := make(map[string]int, len(searchList)+len(gss)) i := 0 for _, gs := range searchList { + gs = strings.TrimSuffix(gs, ".") if _, ok := uniq[gs]; !ok { uniq[gs] = i i++ } } + clusterDomainDot := "." + clusterDomain + clusterDomain = strings.TrimSuffix(clusterDomainDot, ".") + ours := func(gs string) bool { + return strings.HasSuffix(gs, clusterDomain) || strings.HasSuffix(gs, clusterDomainDot) || gs == "tel2-search" + } + for _, gs := range gss { - if !strings.HasSuffix(gs, clusterDomain) { + if !ours(gs) { if _, ok := uniq[gs]; !ok { uniq[gs] = i i++ @@ -241,7 +245,7 @@ func psList(values []string) string { sb.WriteByte(',') } sb.WriteByte('"') - sb.WriteString(strings.TrimSuffix(gs, ".")) + sb.WriteString(gs) sb.WriteByte('"') } sb.WriteByte(')') @@ -311,7 +315,9 @@ func (t *nativeDevice) setRegistryGlobalSearchList(ctx context.Context, gss []st dlog.Errorf(ctx, `creating/opening registry value %s\%s failed: %v`, tcpParamKey, searchListKey, err) } else { defer rk.Close() - if err = rk.SetStringValue(searchListKey, strings.Join(gss, ",")); err != nil { + rv := strings.Join(gss, ",") + dlog.Debugf(ctx, `setting registry value %s\%s to %s`, tcpParamKey, searchListKey, rv) + if err = rk.SetStringValue(searchListKey, rv); err != nil { dlog.Errorf(ctx, `setting registry value %s\%s failed: %v`, tcpParamKey, searchListKey, err) } } From 8cf1fa274e85e336de31d118b7eb78d073d5b86c Mon Sep 17 00:00:00 2001 From: Thomas Hallgren Date: Fri, 24 Nov 2023 13:04:49 +0100 Subject: [PATCH 2/3] Fix concurrent map write fatal error. Occurred on macOS due to use of a static map that got shared among all reporters, but revealed a badly designed constructor for the reporter which is refactored in this commit. The fatal error that is fixed: fatal error: concurrent map writes goroutine 42 [running]: github.com/telepresenceio/telepresence/v2/pkg/client/scout.getInstallIDFromFilesystem({0x1061f0b68, 0xc0005edbc0}, 0xc0001f13b0, {0x1056c9d01, 0x3}) github.com/telepresenceio/telepresence/v2/pkg/client/scout/reporter.go:149 +0x7e5 github.com/telepresenceio/telepresence/v2/pkg/client/scout.NewReporterForInstallType.func1(0xc0001f13b0) github.com/telepresenceio/telepresence/v2/pkg/client/scout/reporter.go:186 +0x32 Signed-off-by: Thomas Hallgren --- CHANGELOG.yml | 17 +++-- integration_test/helm_test.go | 2 +- pkg/client/scout/os_metadata_darwin.go | 11 ++- pkg/client/scout/os_metadata_linux.go | 8 +-- pkg/client/scout/os_metadata_test.go | 3 +- pkg/client/scout/os_metadata_windows.go | 6 +- pkg/client/scout/reporter.go | 96 ++++++++++++------------- pkg/client/scout/reporter_test.go | 51 ++++++------- 8 files changed, 90 insertions(+), 104 deletions(-) diff --git a/CHANGELOG.yml b/CHANGELOG.yml index ace302eb87..1c2de66223 100644 --- a/CHANGELOG.yml +++ b/CHANGELOG.yml @@ -35,21 +35,30 @@ items: notes: - type: bugfix title: The DNS search path on Windows is now restored when Telepresence quits - body: The DNS search path that Telepresence uses to simulate the DNS lookup functionality in the connected + body: >- + The DNS search path that Telepresence uses to simulate the DNS lookup functionality in the connected cluster namespace was not removed by a telepresence quit, resulting in connectivity problems from the workstation. Telepresence will now remove the entries that it has added to the search list when it quits. + - type: bugfix + title: The user-daemon would sometimes get killed when used by multiple simultaneous CLI clients. + body: >- + The user-daemon would die with a fatal "fatal error: concurrent map writes" error in the + connector.log, effectively killing the ongoing connection. - type: bugfix title: Multiple services ports using the same target port would not get intercepted correctly. - body: Intercepts didn't work when multiple service ports were using the same container port. Telepresence would + body: >- + Intercepts didn't work when multiple service ports were using the same container port. Telepresence would think that one of the ports wasn't intercepted and therefore disable the intercept of the container port. - type: bugfix title: Root daemon refuses to disconnect. - body: The root daemon would sometimes hang forever when attempting to disconnect due to a deadlock in + body: >- + The root daemon would sometimes hang forever when attempting to disconnect due to a deadlock in the VIF-device. - type: bugfix title: Fix panic in user daemon when traffic-manager was unreachable - body: The user daemon would panic if the traffic-manager was unreachable. It will now instead report + body: >- + The user daemon would panic if the traffic-manager was unreachable. It will now instead report a proper error to the client. - type: change title: Removal of backward support for versions predating 2.6.0 diff --git a/integration_test/helm_test.go b/integration_test/helm_test.go index de319d7799..fce68d1886 100644 --- a/integration_test/helm_test.go +++ b/integration_test/helm_test.go @@ -118,7 +118,7 @@ func (s *helmSuite) Test_HelmMultipleInstalls() { s.Contains(stdout, "Connected to context") s.Eventually(func() bool { return itest.Run(ctx, "curl", "--silent", "--connect-timeout", "1", fmt.Sprintf("%s.%s", svc, s.appSpace2)) == nil - }, 7*time.Second, 1*time.Second) + }, 15*time.Second, 3*time.Second) }) s.Run("Can intercept", func() { diff --git a/pkg/client/scout/os_metadata_darwin.go b/pkg/client/scout/os_metadata_darwin.go index e07cd5c0e5..1148ae9bda 100644 --- a/pkg/client/scout/os_metadata_darwin.go +++ b/pkg/client/scout/os_metadata_darwin.go @@ -10,12 +10,10 @@ import ( "github.com/datawire/dlib/dlog" ) -func getOsMetadata(ctx context.Context) map[string]any { - osMeta := map[string]any{ - "os_version": "unknown", - "os_build_version": "unknown", - "os_name": "unknown", - } +func setOsMetadata(ctx context.Context, osMeta map[string]any) { + osMeta["os_version"] = "unknown" + osMeta["os_build_version"] = "unknown" + osMeta["os_name"] = "unknown" cmd := dexec.CommandContext(ctx, "sw_vers") cmd.DisableLogging = true if r, err := cmd.Output(); err != nil { @@ -36,5 +34,4 @@ func getOsMetadata(ctx context.Context) map[string]any { } } } - return osMeta } diff --git a/pkg/client/scout/os_metadata_linux.go b/pkg/client/scout/os_metadata_linux.go index d2b31d3b43..404bfc40df 100644 --- a/pkg/client/scout/os_metadata_linux.go +++ b/pkg/client/scout/os_metadata_linux.go @@ -28,8 +28,7 @@ func isWSL(ctx context.Context) bool { return strings.Contains(v, "WSL") || strings.Contains(v, "Windows") } -func getOsMetadata(ctx context.Context) map[string]any { - osMeta := map[string]any{} +func setOsMetadata(ctx context.Context, osMeta map[string]any) { osMeta["os_docker"] = isDocker(ctx) osMeta["os_wsl"] = isWSL(ctx) f, err := os.Open("/etc/os-release") @@ -38,7 +37,7 @@ func getOsMetadata(ctx context.Context) map[string]any { } if err != nil { dlog.Warnf(ctx, "Unable to open /etc/os-release or /usr/lib/os-release: %v", err) - return osMeta + return } scanner := bufio.NewScanner(f) osRelease := map[string]string{} @@ -49,7 +48,7 @@ func getOsMetadata(ctx context.Context) map[string]any { } if err := scanner.Err(); err != nil { dlog.Warnf(ctx, "Unable to scan contents of /etc/os-release: %v", err) - return osMeta + return } // Different Linuxes will report things in different ways, so this will scan the // contents of osRelease and look for each of the different keys that a value might be under @@ -65,5 +64,4 @@ func getOsMetadata(ctx context.Context) map[string]any { osMeta["os_name"] = getFromOSRelease("ID", "NAME") osMeta["os_version"] = getFromOSRelease("VERSION", "VERSION_ID") osMeta["os_build_version"] = getFromOSRelease("BUILD_ID") - return osMeta } diff --git a/pkg/client/scout/os_metadata_test.go b/pkg/client/scout/os_metadata_test.go index 28c4468f7a..d5b82c7178 100644 --- a/pkg/client/scout/os_metadata_test.go +++ b/pkg/client/scout/os_metadata_test.go @@ -8,7 +8,8 @@ import ( func TestOsMetadata(t *testing.T) { ctx := dlog.NewTestContext(t, false) - osMeta := getOsMetadata(ctx) + osMeta := make(map[string]any) + setOsMetadata(ctx, osMeta) for _, k := range []string{"os_version", "os_name"} { v := osMeta[k] if v == "" || v == "unknown" { diff --git a/pkg/client/scout/os_metadata_windows.go b/pkg/client/scout/os_metadata_windows.go index 4c18ceb35d..17d5e6d1f1 100644 --- a/pkg/client/scout/os_metadata_windows.go +++ b/pkg/client/scout/os_metadata_windows.go @@ -10,14 +10,13 @@ import ( "github.com/telepresenceio/telepresence/v2/pkg/proc" ) -func getOsMetadata(ctx context.Context) map[string]any { +func setOsMetadata(ctx context.Context, osMeta map[string]any) { cmd := proc.CommandContext(ctx, "wmic", "os", "get", "Caption,Version,BuildNumber", "/value") cmd.DisableLogging = true r, err := cmd.Output() - osMeta := map[string]any{} if err != nil { dlog.Warnf(ctx, "Error running wmic: %v", err) - return osMeta + return } scanner := bufio.NewScanner(bytes.NewReader(r)) for scanner.Scan() { @@ -42,5 +41,4 @@ func getOsMetadata(ctx context.Context) map[string]any { if err := scanner.Err(); err != nil { dlog.Warnf(ctx, "Unable to scan wmic output: %v", err) } - return osMeta } diff --git a/pkg/client/scout/reporter.go b/pkg/client/scout/reporter.go index 20df815ded..8218976436 100644 --- a/pkg/client/scout/reporter.go +++ b/pkg/client/scout/reporter.go @@ -69,9 +69,11 @@ var idFiles = map[InstallType]string{ //nolint:gochecknoglobals // constant Docker: "docker_id", } -// getInstallIDFromFilesystem returns the telepresence install ID, and also sets the reporter base -// metadata to include any conflicting install IDs written by old versions of the product. -func getInstallIDFromFilesystem(ctx context.Context, reporter *metriton.Reporter, installType InstallType) (string, error) { +// setInstallIDFromFilesystem sets the telepresence install ID in the given map, including any conflicting +// install IDs written by old versions of the product. +// +//nolint:gochecknoglobals // can be overridden for test purposes +var setInstallIDFromFilesystem = func(ctx context.Context, installType InstallType, md map[string]any) (string, error) { type filecacheEntry struct { Body string Err error @@ -146,7 +148,7 @@ func getInstallIDFromFilesystem(ctx context.Context, reporter *metriton.Reporter } } - reporter.BaseMetadata["new_install"] = len(allIDs) == 0 + md["new_install"] = len(allIDs) == 0 // We don't want to add the extra ids until we've decided if it's a new install or not // this is because we'd like a new install of type A to be reported even if there's already @@ -167,7 +169,7 @@ func getInstallIDFromFilesystem(ctx context.Context, reporter *metriton.Reporter for product, id := range allIDs { if id != retID { - reporter.BaseMetadata["install_id_"+product] = id + md["install_id_"+product] = id } } return retID, nil @@ -178,23 +180,28 @@ func getInstallIDFromFilesystem(ctx context.Context, reporter *metriton.Reporter const bufferSize = 40 func NewReporterForInstallType(ctx context.Context, mode string, installType InstallType, reportAnnotators []ReportAnnotator, reportMutators []ReportMutator) Reporter { - r := &reporter{ - reporter: &metriton.Reporter{ - Application: "telepresence2", - Version: client.Version(), - GetInstallID: func(r *metriton.Reporter) (string, error) { - id, err := getInstallIDFromFilesystem(ctx, r, installType) - if err != nil { - id = "00000000-0000-0000-0000-000000000000" - r.BaseMetadata["new_install"] = true - r.BaseMetadata["install_id_error"] = err.Error() - } - return id, nil - }, - }, - reportAnnotators: reportAnnotators, - reportMutators: reportMutators, + md := make(map[string]any, 12) + setOsMetadata(ctx, md) + installID, err := setInstallIDFromFilesystem(ctx, installType, md) + if err != nil { + installID = "00000000-0000-0000-0000-000000000000" + md["new_install"] = true + md["install_id_error"] = err.Error() } + // Fixed (growing) metadata passed with every report + md["mode"] = mode + md["trace_id"] = uuid.NewString() // It's sent as JSON so might as well convert it to a string once here. + md["goos"] = runtime.GOOS + md["goarch"] = runtime.GOARCH + + // Discover how Telepresence was installed based on the binary's location + installMethod, err := client.GetInstallMechanism() + if err != nil { + dlog.Errorf(ctx, "scout error getting executable: %s", err) + } + md["install_method"] = installMethod + setDefaultEnvironmentMetadata(md) + if env := client.GetEnv(ctx); env != nil && !env.ScoutDisable { // Some tests disable scout reporting by setting the host IP to 127.0.0.1. This spams // the logs with lots of "connection refused" messages and makes them hard to read. @@ -208,8 +215,21 @@ func NewReporterForInstallType(ctx context.Context, mode string, installType Ins } } } - r.initialize(ctx, mode, runtime.GOOS, runtime.GOARCH) - return r + + return &reporter{ + reporter: &metriton.Reporter{ + Application: "telepresence2", + Version: client.Version(), + GetInstallID: func(r *metriton.Reporter) (string, error) { + return installID, nil + }, + BaseMetadata: md, + }, + reportAnnotators: reportAnnotators, + reportMutators: reportMutators, + buffer: make(chan bufEntry, bufferSize), + done: make(chan struct{}), + } } // DefaultReportAnnotators are the default annotator functions that the NewReporter function will pass to NewReporterForInstallType. @@ -267,30 +287,6 @@ func SetMetadatum(ctx context.Context, key string, value any) { } } -// initialization broken out or constructor for the benefit of testing. -func (r *reporter) initialize(ctx context.Context, mode, goos, goarch string) { - r.buffer = make(chan bufEntry, bufferSize) - r.done = make(chan struct{}) - - // Fixed (growing) metadata passed with every report - baseMeta := getOsMetadata(ctx) - baseMeta["mode"] = mode - baseMeta["trace_id"] = uuid.NewString() // It's sent as JSON so might as well convert it to a string once here. - baseMeta["goos"] = goos - baseMeta["goarch"] = goarch - - // Discover how Telepresence was installed based on the binary's location - installMethod, err := client.GetInstallMechanism() - if err != nil { - dlog.Errorf(ctx, "scout error getting executable: %s", err) - } - baseMeta["install_method"] = installMethod - for k, v := range getDefaultEnvironmentMetadata() { - baseMeta[k] = v - } - r.reporter.BaseMetadata = baseMeta -} - func (r *reporter) InstallID() string { return r.reporter.InstallID() } @@ -399,9 +395,8 @@ func (r *reporter) doReport(ctx context.Context, be *bufEntry) { } } -// Returns a metadata map containing all the additional environment variables to be reported. -func getDefaultEnvironmentMetadata() map[string]string { - metadata := map[string]string{} +// setDefaultEnvironmentMetadata sets all the additional environment variables to be reported. +func setDefaultEnvironmentMetadata(metadata map[string]any) { for _, e := range os.Environ() { pair := strings.SplitN(e, "=", 2) if strings.HasPrefix(pair[0], EnvironmentMetadataPrefix) { @@ -409,5 +404,4 @@ func getDefaultEnvironmentMetadata() map[string]string { metadata[key] = pair[1] } } - return metadata } diff --git a/pkg/client/scout/reporter_test.go b/pkg/client/scout/reporter_test.go index b0ba6baaac..99034137b2 100644 --- a/pkg/client/scout/reporter_test.go +++ b/pkg/client/scout/reporter_test.go @@ -334,13 +334,9 @@ func TestInstallID(t *testing.T) { func TestReport(t *testing.T) { const ( - mockVersion = "v2.4.5-test" - mockApplication = "telepresence2" - mockInstallID = "00000000-1111-2222-3333-444444444444" - mockMode = "test-mode" - mockOS = "linux" - mockARCH = "amd64" - mockAction = "test-action" + mockInstallID = "00000000-1111-2222-3333-444444444444" + mockMode = "test-mode" + mockAction = "test-action" ) type testcase struct { InputEnv map[string]string @@ -353,8 +349,8 @@ func TestReport(t *testing.T) { ExpectedMetadata: map[string]any{ "action": mockAction, "mode": mockMode, - "goos": mockOS, - "goarch": mockARCH, + "goos": runtime.GOOS, + "goarch": runtime.GOARCH, }, }, "with-additional-scout-meta": { @@ -371,8 +367,8 @@ func TestReport(t *testing.T) { ExpectedMetadata: map[string]any{ "action": mockAction, "mode": mockMode, - "goos": mockOS, - "goarch": mockARCH, + "goos": runtime.GOOS, + "goarch": runtime.GOARCH, "extra_field_1": "extra value 1", "extra_field_2": "extra value 2", }, @@ -385,8 +381,8 @@ func TestReport(t *testing.T) { ExpectedMetadata: map[string]any{ "action": mockAction, "mode": mockMode, - "goos": mockOS, - "goarch": mockARCH, + "goos": runtime.GOOS, + "goarch": runtime.GOARCH, "extra_field_1": "extra value 1", "extra_field_2": "extra value 2", }, @@ -405,8 +401,8 @@ func TestReport(t *testing.T) { ExpectedMetadata: map[string]any{ "action": mockAction, "mode": mockMode, - "goos": mockOS, - "goarch": mockARCH, + "goos": runtime.GOOS, + "goarch": runtime.GOARCH, "extra_field_1": "extra value 1", }, }, @@ -420,8 +416,8 @@ func TestReport(t *testing.T) { ExpectedMetadata: map[string]any{ "action": mockAction, "mode": "overridden mode", - "goos": mockOS, - "goarch": mockARCH, + "goos": runtime.GOOS, + "goarch": runtime.GOARCH, }, }, "with-report-annotators": { @@ -445,8 +441,8 @@ func TestReport(t *testing.T) { ExpectedMetadata: map[string]any{ "action": "overridden action", "mode": "overridden mode", - "goos": mockOS, - "goarch": mockARCH, + "goos": runtime.GOOS, + "goarch": runtime.GOARCH, "extra_field": "extra value", // Not overridden by annotation "annotation": "annotated value", }, @@ -488,19 +484,12 @@ func TestReport(t *testing.T) { for k, v := range tcData.InputEnv { os.Setenv(k, v) } - scout := &reporter{ - buffer: make(chan bufEntry, 40), - reporter: &metriton.Reporter{ - Application: mockApplication, - Version: mockVersion, - GetInstallID: func(r *metriton.Reporter) (string, error) { - return mockInstallID, nil - }, - Endpoint: testServer.URL, - }, - reportAnnotators: tcData.ReportAnnotators, + + setInstallIDFromFilesystem = func(ctx context.Context, installType InstallType, md map[string]any) (string, error) { + return mockInstallID, nil } - scout.initialize(ctx, mockMode, mockOS, mockARCH) + scout := NewReporterForInstallType(ctx, mockMode, CLI, tcData.ReportAnnotators, nil).(*reporter) + scout.reporter.Endpoint = testServer.URL // Start scout report processing... sc, cancel := context.WithCancel(dcontext.WithSoftness(ctx)) From 5a376bb01b76baf28d24a4c84332ada310ff8b39 Mon Sep 17 00:00:00 2001 From: Thomas Hallgren Date: Tue, 28 Nov 2023 00:22:40 +0100 Subject: [PATCH 3/3] Add test to verify that DNS search path is restored on quit. This test is windows specific because: - On linux, the DNS search path is attached to the VIF, so when the VIF is gone, our DNS settings are gone with it. - On macOS, there's no DNS search path. Instead, we add domains that correspond to the namespaces. Signed-off-by: Thomas Hallgren --- .../restore_dns_search_windows_test.go | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 integration_test/restore_dns_search_windows_test.go diff --git a/integration_test/restore_dns_search_windows_test.go b/integration_test/restore_dns_search_windows_test.go new file mode 100644 index 0000000000..05a03fd88d --- /dev/null +++ b/integration_test/restore_dns_search_windows_test.go @@ -0,0 +1,40 @@ +package integration_test + +import ( + "os" + "strings" + + "golang.org/x/sys/windows/registry" + + "github.com/telepresenceio/telepresence/v2/integration_test/itest" +) + +func (s *notConnectedSuite) getGlobalSearchList() []string { + const ( + tcpParamKey = `System\CurrentControlSet\Services\Tcpip\Parameters` + searchListKey = `SearchList` + ) + rk, err := registry.OpenKey(registry.LOCAL_MACHINE, tcpParamKey, registry.QUERY_VALUE) + if os.IsNotExist(err) { + err = nil + } + s.Require().NoError(err) + defer rk.Close() + csv, _, err := rk.GetStringValue(searchListKey) + if os.IsNotExist(err) { + err = nil + } + s.Require().NoError(err) + return strings.Split(csv, ",") +} + +func (s *notConnectedSuite) Test_DNSSearchRestored() { + beforeConnect := s.getGlobalSearchList() + ctx := s.Context() + s.TelepresenceConnect(ctx) + afterConnect := s.getGlobalSearchList() + s.Assert().NotEqual(beforeConnect, afterConnect) + itest.TelepresenceQuitOk(ctx) + afterQuit := s.getGlobalSearchList() + s.Assert().Equal(beforeConnect, afterQuit) +}