From 6d899cbc1ccd0e100d3b4564d7002d220a40c259 Mon Sep 17 00:00:00 2001 From: Nic Klaassen Date: Fri, 13 Dec 2024 16:25:15 -0800 Subject: [PATCH 1/5] [vnet] install and run windows service This commit adds a Windows service for VNet. It adds support for automatically installing and running the service when the user runs `tsh vnet`, and adds a command to manually uninstall/delete the service. The service creates the TUN interface and establishes an IPC connection with the user process over a named pipe, but for now does not actually handle any networking, the rest will come in later PRs. If you want to test this out on a Windows machine/VM, you should be able to run `tsh vnet` and see that: 1. A service TeleportVNet is installed and runs `sc.exe query state=all | grep -A3 Teleport` 2. The service writes logs to `logs.txt` in the directory where `tsh` is installed (this is temporary until I find a better place for logs). 3. A TUN interface is created `netsh interface show interface` 4. The service stops and the interface is cleaned up when the user process exits. Unfortunately this PR does not include any unit tests. Most of the functionality here needs to be able to escalate to administrator with a UAC prompt and needs to run on Windows, this is exactly the kind of unit test that is very hard to write and would never actually be able to run in CI. But, any part of this that's broken would immediately break VNet on Windows, and this should be caught in any test plan. --- lib/teleterm/vnet/service.go | 2 +- ...min_process.go => admin_process_darwin.go} | 64 +--- lib/vnet/admin_process_windows.go | 94 +++++ lib/vnet/escalate_daemon_darwin.go | 8 +- lib/vnet/escalate_nodaemon_darwin.go | 3 +- lib/vnet/escalate_windows.go | 356 +++++++++++++++++- lib/vnet/network_stack.go | 3 + lib/vnet/osconfig.go | 93 +++-- lib/vnet/osconfig_darwin.go | 6 +- lib/vnet/osconfig_other.go | 34 -- lib/vnet/osconfig_windows.go | 5 + lib/vnet/process_manager.go | 79 ++++ lib/vnet/socket_other.go | 45 --- lib/vnet/socket_windows.go | 45 --- .../{escalate_other.go => unsupported_os.go} | 30 +- lib/vnet/{run.go => user_process_darwin.go} | 84 +---- lib/vnet/user_process_windows.go | 123 ++++++ tool/tsh/common/tsh.go | 14 +- tool/tsh/common/vnet.go | 71 ++++ tool/tsh/common/vnet_daemon_darwin.go | 1 - tool/tsh/common/vnet_darwin.go | 39 +- tool/tsh/common/vnet_nodaemon.go | 7 +- tool/tsh/common/vnet_other.go | 20 +- tool/tsh/common/vnet_windows.go | 117 +++--- 24 files changed, 915 insertions(+), 428 deletions(-) rename lib/vnet/{admin_process.go => admin_process_darwin.go} (61%) create mode 100644 lib/vnet/admin_process_windows.go delete mode 100644 lib/vnet/osconfig_other.go create mode 100644 lib/vnet/process_manager.go delete mode 100644 lib/vnet/socket_other.go delete mode 100644 lib/vnet/socket_windows.go rename lib/vnet/{escalate_other.go => unsupported_os.go} (63%) rename lib/vnet/{run.go => user_process_darwin.go} (68%) create mode 100644 lib/vnet/user_process_windows.go create mode 100644 tool/tsh/common/vnet.go diff --git a/lib/teleterm/vnet/service.go b/lib/teleterm/vnet/service.go index 713198a187558..391f69a4fb48e 100644 --- a/lib/teleterm/vnet/service.go +++ b/lib/teleterm/vnet/service.go @@ -160,7 +160,7 @@ func (s *Service) Start(ctx context.Context, req *api.StartRequest) (*api.StartR } s.clusterConfigCache = vnet.NewClusterConfigCache(s.cfg.Clock) - processManager, err := vnet.Run(ctx, &vnet.RunConfig{ + processManager, err := vnet.RunUserProcess(ctx, &vnet.UserProcessConfig{ AppProvider: appProvider, ClusterConfigCache: s.clusterConfigCache, }) diff --git a/lib/vnet/admin_process.go b/lib/vnet/admin_process_darwin.go similarity index 61% rename from lib/vnet/admin_process.go rename to lib/vnet/admin_process_darwin.go index 4c2411d729763..c80518e74a4b2 100644 --- a/lib/vnet/admin_process.go +++ b/lib/vnet/admin_process_darwin.go @@ -21,12 +21,17 @@ import ( "os" "time" + "github.com/gravitational/teleport/lib/vnet/daemon" "github.com/gravitational/trace" "golang.zx2c4.com/wireguard/tun" - - "github.com/gravitational/teleport/lib/vnet/daemon" ) +type AdminProcessConfig daemon.Config + +func (c *AdminProcessConfig) CheckAndSetDefaults() error { + return trace.Wrap((*daemon.Config)(c).CheckAndSetDefaults()) +} + // RunAdminProcess must run as root. It creates and sets up a TUN device and passes // the file descriptor for that device over the unix socket found at config.socketPath. // @@ -36,9 +41,9 @@ import ( // // OS configuration is updated every [osConfigurationInterval]. During the update, it temporarily // changes egid and euid of the process to that of the client connecting to the daemon. -func RunAdminProcess(ctx context.Context, config daemon.Config) error { +func RunAdminProcess(ctx context.Context, config AdminProcessConfig) error { if err := config.CheckAndSetDefaults(); err != nil { - return trace.Wrap(err) + return trace.Wrap(err, "checking daemon process config") } ctx, cancel := context.WithCancel(ctx) @@ -51,7 +56,7 @@ func RunAdminProcess(ctx context.Context, config daemon.Config) error { errCh := make(chan error) go func() { - errCh <- trace.Wrap(osConfigurationLoop(ctx, tunName, config.IPv6Prefix, config.DNSAddr, config.HomePath, config.ClientCred)) + errCh <- trace.Wrap(osConfigurationLoop(ctx, tunName, config)) }() // Stay alive until we get an error on errCh, indicating that the osConfig loop exited. @@ -106,52 +111,3 @@ func createTUNDevice(ctx context.Context) (tun.Device, string, error) { } return dev, name, nil } - -// osConfigurationLoop will keep running until [ctx] is canceled or an unrecoverable error is encountered, in -// order to keep the host OS configuration up to date. -func osConfigurationLoop(ctx context.Context, tunName, ipv6Prefix, dnsAddr, homePath string, clientCred daemon.ClientCred) error { - osConfigurator, err := newOSConfigurator(tunName, ipv6Prefix, dnsAddr, homePath, clientCred) - if err != nil { - return trace.Wrap(err, "creating OS configurator") - } - defer func() { - if err := osConfigurator.close(); err != nil { - log.ErrorContext(ctx, "Error while closing OS configurator", "error", err) - } - }() - - // Clean up any stale configuration left by a previous VNet instance that may have failed to clean up. - // This is necessary in case any stale /etc/resolver/ entries are still present, we need to - // be able to reach the proxy in order to fetch the vnet_config. - if err := osConfigurator.deconfigureOS(ctx); err != nil { - return trace.Wrap(err, "cleaning up OS configuration on startup") - } - - defer func() { - // Shutting down, deconfigure OS. Pass context.Background because [ctx] has likely been canceled - // already but we still need to clean up. - if err := osConfigurator.deconfigureOS(context.Background()); err != nil { - log.ErrorContext(ctx, "Error deconfiguring host OS before shutting down.", "error", err) - } - }() - - if err := osConfigurator.updateOSConfiguration(ctx); err != nil { - return trace.Wrap(err, "applying initial OS configuration") - } - - // Re-configure the host OS every 10 seconds. This will pick up any newly logged-in clusters by - // reading profiles from TELEPORT_HOME. - const osConfigurationInterval = 10 * time.Second - ticker := time.NewTicker(osConfigurationInterval) - defer ticker.Stop() - for { - select { - case <-ticker.C: - if err := osConfigurator.updateOSConfiguration(ctx); err != nil { - return trace.Wrap(err, "updating OS configuration") - } - case <-ctx.Done(): - return ctx.Err() - } - } -} diff --git a/lib/vnet/admin_process_windows.go b/lib/vnet/admin_process_windows.go new file mode 100644 index 0000000000000..02e210bf015a9 --- /dev/null +++ b/lib/vnet/admin_process_windows.go @@ -0,0 +1,94 @@ +// Teleport +// Copyright (C) 2024 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package vnet + +import ( + "context" + "time" + + "github.com/Microsoft/go-winio" + "github.com/gravitational/trace" + "golang.zx2c4.com/wireguard/tun" +) + +type AdminProcessConfig struct { + // NamedPipe is the name of a pipe used for IPC between the user process and + // the admin service. + NamedPipe string + + // TODO(nklaassen): delete these, the admin process will decide them, they + // don't need to be passed from the user process. Keeping them until I + // remove the references from osconfig.go. + IPv6Prefix string + DNSAddr string + HomePath string +} + +func (c *AdminProcessConfig) CheckAndSetDefaults() error { + if c.NamedPipe == "" { + return trace.BadParameter("missing pipe path") + } + return nil +} + +// RunAdminProcess must run as administrator. It creates and sets up a TUN +// device and runs the VNet networking stack. +// +// It also handles host OS configuration, OS configuration is updated every [osConfigurationInterval]. +// +// The admin process will stay running until the socket at config.socketPath is +// deleted or until encountering an unrecoverable error. +func RunAdminProcess(ctx context.Context, cfg AdminProcessConfig) error { + if err := cfg.CheckAndSetDefaults(); err != nil { + return trace.Wrap(err, "checking admin process config") + } + log.InfoContext(ctx, "Running VNet admin process", "cfg", cfg) + + dialTimeout := 200 * time.Millisecond + conn, err := winio.DialPipe(pipePath, &dialTimeout) + if err != nil { + return trace.Wrap(err, "dialing named pipe %s", pipePath) + } + conn.Close() + log.InfoContext(ctx, "Succesfully connected to user process over named pipe", "pipe", pipePath) + + device, err := tun.CreateTUN("TeleportVNet", mtu) + if err != nil { + return trace.Wrap(err, "creating TUN device") + } + defer device.Close() + tunName, err := device.Name() + if err != nil { + return trace.Wrap(err, "getting TUN device name") + } + log.InfoContext(ctx, "Created TUN interface", "tun", tunName) + + // TODO(nklaassen): actually run the networking stack and OS configuration. + // For now, stay alive as long as we can dial the pipe. + for { + select { + case <-time.After(time.Second): + conn, err := winio.DialPipe(pipePath, &dialTimeout) + if err != nil { + return trace.Wrap(err, "failed to dial pipe, assuming user process has terminated") + } + conn.Close() + case <-ctx.Done(): + return ctx.Err() + } + } +} diff --git a/lib/vnet/escalate_daemon_darwin.go b/lib/vnet/escalate_daemon_darwin.go index 935c16afe9793..780e8fba43882 100644 --- a/lib/vnet/escalate_daemon_darwin.go +++ b/lib/vnet/escalate_daemon_darwin.go @@ -29,11 +29,13 @@ import ( // execAdminProcess is called from the normal user process to register and call // the daemon process which runs as root. -func execAdminProcess(ctx context.Context, config daemon.Config) error { - return trace.Wrap(daemon.RegisterAndCall(ctx, config)) +func execAdminProcess(ctx context.Context, config AdminProcessConfig) error { + return trace.Wrap(daemon.RegisterAndCall(ctx, daemon.Config(config))) } // DaemonSubcommand runs the VNet daemon process. func DaemonSubcommand(ctx context.Context) error { - return trace.Wrap(daemon.Start(ctx, RunAdminProcess)) + return trace.Wrap(daemon.Start(ctx, func(ctx context.Context, config daemon.Config) error { + return RunAdminProcess(ctx, AdminProcessConfig(config)) + })) } diff --git a/lib/vnet/escalate_nodaemon_darwin.go b/lib/vnet/escalate_nodaemon_darwin.go index c26c01ae6a58a..6fc42da5d46be 100644 --- a/lib/vnet/escalate_nodaemon_darwin.go +++ b/lib/vnet/escalate_nodaemon_darwin.go @@ -31,12 +31,11 @@ import ( "github.com/gravitational/teleport" "github.com/gravitational/teleport/api/types" - "github.com/gravitational/teleport/lib/vnet/daemon" ) // execAdminProcess is called from the normal user process to execute // "tsh vnet-admin-setup" as root via an osascript wrapper. -func execAdminProcess(ctx context.Context, config daemon.Config) error { +func execAdminProcess(ctx context.Context, config AdminProcessConfig) error { executableName, err := os.Executable() if err != nil { return trace.Wrap(err, "getting executable path") diff --git a/lib/vnet/escalate_windows.go b/lib/vnet/escalate_windows.go index 3b5d4464eefe8..c38fe6a3f4d03 100644 --- a/lib/vnet/escalate_windows.go +++ b/lib/vnet/escalate_windows.go @@ -14,27 +14,357 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//go:build windows -// +build windows - package vnet import ( "context" + "log/slog" + "os" + "os/user" + "path/filepath" + "strings" + "syscall" + "time" + "github.com/alecthomas/kingpin/v2" + "github.com/google/safetext/shsprintf" "github.com/gravitational/trace" - - "github.com/gravitational/teleport/lib/vnet/daemon" + "golang.org/x/sys/windows" + "golang.org/x/sys/windows/svc" + "golang.org/x/sys/windows/svc/mgr" ) -var ( - // ErrVnetNotImplemented is an error indicating that VNet is not implemented on the host OS. - ErrVnetNotImplemented = &trace.NotImplementedError{Message: "VNet is not implemented on windows"} +const ( + serviceName = "TeleportVNet" + serviceDescription = "This service manages networking and OS configuration for Teleport VNet." + serviceAccessFlags = windows.SERVICE_START | windows.SERVICE_STOP | windows.SERVICE_QUERY_STATUS ) -// execAdminProcess is called from the normal user process to execute the admin -// subcommand as root. -func execAdminProcess(ctx context.Context, config daemon.Config) error { - // TODO(nklaassen): implement execAdminProcess on windows. - return trace.Wrap(ErrVnetNotImplemented) +// execAdminProcess is called from the normal user process start the VNet admin +// service, installing it first if necessary. +func execAdminProcess(ctx context.Context, cfg AdminProcessConfig) error { + service, err := startService(ctx, cfg) + if err != nil { + return trace.Wrap(err) + } + defer service.Close() + log.InfoContext(ctx, "Started Windows service", "service", serviceName) + for { + select { + case <-ctx.Done(): + log.InfoContext(ctx, "Context canceled, stopping Windows service") + if _, err := service.Control(svc.Stop); err != nil { + return trace.Wrap(err, "sending stop request to Windows service %s", serviceName) + } + return nil + case <-time.After(time.Second): + if status, err := service.Query(); err != nil { + return trace.Wrap(err, "querying admin service") + } else { + if status.State != svc.Running && status.State != svc.StartPending { + return trace.Errorf("service stopped running prematurely, status: %v", status) + } + } + } + } +} + +func startService(ctx context.Context, cfg AdminProcessConfig) (*mgr.Service, error) { + // Avoid [mgr.Connect] because it requests elevated permissions. + scManager, err := windows.OpenSCManager(nil /*machine*/, nil /*database*/, windows.SC_MANAGER_CONNECT) + if err != nil { + return nil, trace.Wrap(err, "opening Windows service manager") + } + defer windows.CloseServiceHandle(scManager) + serviceNamePtr, err := syscall.UTF16PtrFromString(serviceName) + if err != nil { + return nil, trace.Wrap(err, "converting service name to UTF16") + } + serviceHandle, err := windows.OpenService(scManager, serviceNamePtr, serviceAccessFlags) + if err != nil { + log.InfoContext(ctx, "Failed to open Windows service, trying to install the service", "error", err) + if err := escalateAndInstallService(); err != nil { + return nil, trace.Wrap(err, "installing Windows service") + } + if serviceHandle, err = waitForService(ctx, scManager, serviceNamePtr); err != nil { + return nil, trace.Wrap(err, "waiting for service immediately after installation") + } + } + service := &mgr.Service{ + Name: serviceName, + Handle: serviceHandle, + } + if err := service.Start("vnet-service", "--pipe", cfg.NamedPipe); err != nil { + return nil, trace.Wrap(err, "starting Windows service %s", serviceName) + } + return service, nil +} + +func escalateAndInstallService() error { + user, err := user.Current() + if err != nil { + return trace.Wrap(err, "getting current user") + } + return trace.Wrap(escalateAndRunSubcommand("vnet-install-service", "--userSID", user.Uid)) +} + +func escalateAndRunSubcommand(args ...string) error { + tshPath, err := os.Executable() + if err != nil { + return trace.Wrap(err, "getting executable path") + } + argPtrs, err := ptrsFromStrings( + "runas", + shsprintf.EscapeDefaultContext(tshPath), + escapeAndJoinArgs(args...), + ) + if err != nil { + return trace.Wrap(err) + } + if err := windows.ShellExecute( + 0, // parent window handle (default is no window) + argPtrs[0], // verb + argPtrs[1], // file + argPtrs[2], // args + nil, // cwd (default is current directory) + 1, // showCmd (1 is normal) + ); err != nil { + return trace.Wrap(err, "running subcommand as administrator via runas") + } + return nil +} + +func ptrsFromStrings(strs ...string) ([]*uint16, error) { + ptrs := make([]*uint16, len(strs)) + for i := range ptrs { + var err error + ptrs[i], err = syscall.UTF16PtrFromString(strs[i]) + if err != nil { + return nil, trace.Wrap(err, "converting string to UTF16") + } + } + return ptrs, nil +} + +func escapeAndJoinArgs(args ...string) string { + for i := range args { + args[i] = shsprintf.EscapeDefaultContext(args[i]) + } + return strings.Join(args, " ") +} + +func waitForService(ctx context.Context, scManager windows.Handle, serviceNamePtr *uint16) (windows.Handle, error) { + deadline := time.After(30 * time.Second) + for { + serviceHandle, err := windows.OpenService(scManager, serviceNamePtr, serviceAccessFlags) + if err == nil { + return serviceHandle, nil + } + select { + case <-ctx.Done(): + return 0, trace.Wrap(ctx.Err()) + case <-deadline: + return 0, trace.Errorf("timeout waiting for service to start") + case <-time.After(time.Second): + } + } +} + +// InstallService implements the vnet-install-service command, it must run as +// adminstrator and installs the TeleportVNet Windows service. +func InstallService(ctx context.Context, userSID string) error { + m, err := mgr.Connect() + if err != nil { + return trace.Wrap(err, "connecting to Windows service manager") + } + defer m.Disconnect() + service, err := installService(m) + if err != nil { + return trace.Wrap(err, "installing Windows service") + } + defer service.Close() + if err := configureServicePermissions(service, userSID); err != nil { + slog.ErrorContext(ctx, "Error configuring permissions for the Windows service, will attempt to delete the service", "error", err) + return trace.Wrap(service.Delete(), "deleting Windows service after failing to configure permissions") + } + return nil +} + +func installService(m *mgr.Mgr) (*mgr.Service, error) { + if _, err := m.OpenService(serviceName); err == nil { + return nil, trace.Errorf("Windows service %s is already installed", serviceName) + } + serviceCfg := mgr.Config{ + ServiceType: windows.SERVICE_WIN32_OWN_PROCESS, + StartType: mgr.StartManual, + ErrorControl: mgr.ErrorNormal, + DisplayName: serviceName, + Description: serviceDescription, + } + tshPath, err := os.Executable() + if err != nil { + return nil, trace.Wrap(err, "getting executable path") + } + service, err := m.CreateService(serviceName, tshPath, serviceCfg, "vnet-service") + if err != nil { + return nil, trace.Wrap(err, "creating Windows service") + } + return service, nil +} + +// configureServicePermissions sets the security descriptor DACL on the service +// such that the user who installed the service (identified by userSIDStr) is +// allowed to start, stop, and query the service. +func configureServicePermissions(service *mgr.Service, userSIDStr string) error { + userSID, err := windows.StringToSid(userSIDStr) + if err != nil { + return trace.Wrap(err, "parsing user SID from string") + } + securityDescriptor, err := windows.GetNamedSecurityInfo( + service.Name, windows.SE_SERVICE, windows.DACL_SECURITY_INFORMATION) + if err != nil { + return trace.Wrap(err, "getting current security descriptor for %s", service.Name) + } + currentDACL, _, err := securityDescriptor.DACL() + if err != nil { + return trace.Wrap(err, "getting DACL from security descriptor") + } + explicitAccess := []windows.EXPLICIT_ACCESS{{ + AccessPermissions: windows.ACCESS_MASK(serviceAccessFlags), + AccessMode: windows.GRANT_ACCESS, + Inheritance: windows.NO_INHERITANCE, + Trustee: windows.TRUSTEE{ + TrusteeForm: windows.TRUSTEE_IS_SID, + TrusteeType: windows.TRUSTEE_IS_USER, + TrusteeValue: windows.TrusteeValueFromSID(userSID), + }, + }} + newDACL, err := windows.ACLFromEntries(explicitAccess, currentDACL) + if err != nil { + return trace.Wrap(err, "preparing explicit access DACL") + } + if err := windows.SetNamedSecurityInfo( + service.Name, + windows.SE_SERVICE, + windows.DACL_SECURITY_INFORMATION, + nil, // don't change owner + nil, // don't change group + newDACL, // only set DACL + nil, // don't change SACL + ); err != nil { + return trace.Wrap(err, "setting security descriptor for %s", service.Name) + } + return nil +} + +// UninstallService implements the vnet-uninstall-service command to uninstall +// the TeleportVNet Windows service. If it does not have sufficient permissions, +// it tries to re-execute itself with administrator rights via a UAC prompt. +func UninstallService(ctx context.Context) error { + m, err := mgr.Connect() + if err != nil { + slog.ErrorContext(ctx, "Error connecting to service manager, attempting to escalate to administrator", + "error", err) + err := escalateAndRunSubcommand("vnet-uninstall-service") + return trace.Wrap(err, "escalating to administrator to uninstall service") + } + defer m.Disconnect() + service, err := m.OpenService(serviceName) + if err != nil { + return trace.Wrap(err, "unable to open service, it may not be installed") + } + defer service.Close() + if err := service.Delete(); err != nil { + return trace.Wrap(err, "deleting Windows service") + } + return nil +} + +// ServiceMain runs with Windows VNet service. +func ServiceMain() error { + cleanup, err := setupServiceLogger() + if err != nil { + return trace.Wrap(err) + } + defer cleanup() + if err := svc.Run(serviceName, &windowsService{}); err != nil { + return trace.Wrap(err, "running Windows service") + } + return nil +} + +type windowsService struct{} + +// Execute implements [svc.Handler]. +func (s *windowsService) Execute(args []string, requests <-chan svc.ChangeRequest, status chan<- svc.Status) (bool, uint32) { + const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown + status <- svc.Status{State: svc.StartPending, Accepts: cmdsAccepted} + status <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + errCh := make(chan error) + go func() { errCh <- s.run(ctx, args) }() + +loop: + for { + select { + case request := <-requests: + switch request.Cmd { + case svc.Interrogate: + status <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} + case svc.Stop, svc.Shutdown: + slog.InfoContext(ctx, "Shutting down service, received command", "cmd", request.Cmd) + break loop + } + case err := <-errCh: + slog.ErrorContext(ctx, "Running Windows VNet service", "error", err) + const exitCode = 1 + status <- svc.Status{State: svc.Stopped, Win32ExitCode: exitCode} + return false, exitCode + } + } + cancel() + status <- svc.Status{State: svc.StopPending} + <-errCh + const exitCode = 0 + status <- svc.Status{State: svc.Stopped, Win32ExitCode: exitCode} + return false, exitCode +} + +func (s *windowsService) run(ctx context.Context, args []string) error { + var pipePath string + app := kingpin.New(serviceName, "Teleport Windows Service") + serviceCmd := app.Command("vnet-service", "Start the VNet service.") + serviceCmd.Flag("pipe", "pipe path").Required().StringVar(&pipePath) + cmd, err := app.Parse(args[1:]) + if err != nil { + return trace.Wrap(err, "parsing arguments") + } + if cmd != serviceCmd.FullCommand() { + return trace.BadParameter("executed arguments did not match vnet-service") + } + cfg := AdminProcessConfig{ + NamedPipe: pipePath, + } + if err := RunAdminProcess(ctx, cfg); err != nil { + return trace.Wrap(err, "running admin process") + } + return nil +} + +func setupServiceLogger() (func(), error) { + exePath, err := os.Executable() + if err != nil { + return nil, trace.Wrap(err, "getting current executable path") + } + dir := filepath.Dir(exePath) + logFile, err := os.Create(filepath.Join(dir, "logs.txt")) + if err != nil { + return nil, trace.Wrap(err, "creating log file") + } + slog.SetDefault(slog.New(slog.NewTextHandler(logFile, &slog.HandlerOptions{ + Level: slog.LevelDebug, + }))) + return func() { logFile.Close() }, nil } diff --git a/lib/vnet/network_stack.go b/lib/vnet/network_stack.go index 0479564033e19..6c7dbfa2f8563 100644 --- a/lib/vnet/network_stack.go +++ b/lib/vnet/network_stack.go @@ -41,9 +41,12 @@ import ( "gvisor.dev/gvisor/pkg/waiter" "github.com/gravitational/teleport" + logutils "github.com/gravitational/teleport/lib/utils/log" "github.com/gravitational/teleport/lib/vnet/dns" ) +var log = logutils.NewPackageLogger(teleport.ComponentKey, "vnet") + const ( nicID = 1 mtu = 1500 diff --git a/lib/vnet/osconfig.go b/lib/vnet/osconfig.go index 0642ebd0980dd..b07528269eb92 100644 --- a/lib/vnet/osconfig.go +++ b/lib/vnet/osconfig.go @@ -19,6 +19,7 @@ package vnet import ( "context" "net" + "time" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" @@ -27,9 +28,58 @@ import ( "github.com/gravitational/teleport/api/utils" "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/client/clientcache" - "github.com/gravitational/teleport/lib/vnet/daemon" ) +// osConfigurationLoop will keep running until [ctx] is canceled or an unrecoverable error is encountered, in +// order to keep the host OS configuration up to date. +func osConfigurationLoop(ctx context.Context, tunName string, config AdminProcessConfig) error { + osConfigurator, err := newOSConfigurator(tunName, config) + if err != nil { + return trace.Wrap(err, "creating OS configurator") + } + defer func() { + if err := osConfigurator.close(); err != nil { + log.ErrorContext(ctx, "Error while closing OS configurator", "error", err) + } + }() + + // Clean up any stale configuration left by a previous VNet instance that + // may have failed to clean up. This is necessary in case any stale DNS + // configuration is still present, we need to be able to reach the proxy in + // order to fetch the vnet_config. + if err := osConfigurator.deconfigureOS(ctx); err != nil { + return trace.Wrap(err, "cleaning up OS configuration on startup") + } + + defer func() { + // Shutting down, deconfigure OS. Pass context.Background because [ctx] has likely been canceled + // already but we still need to clean up. + if err := osConfigurator.deconfigureOS(context.Background()); err != nil { + log.ErrorContext(ctx, "Error deconfiguring host OS before shutting down.", "error", err) + } + }() + + if err := osConfigurator.updateOSConfiguration(ctx); err != nil { + return trace.Wrap(err, "applying initial OS configuration") + } + + // Re-configure the host OS every 10 seconds. This will pick up any newly logged-in clusters by + // reading profiles from TELEPORT_HOME. + const osConfigurationInterval = 10 * time.Second + ticker := time.NewTicker(osConfigurationInterval) + defer ticker.Stop() + for { + select { + case <-ticker.C: + if err := osConfigurator.updateOSConfiguration(ctx); err != nil { + return trace.Wrap(err, "updating OS configuration") + } + case <-ctx.Done(): + return ctx.Err() + } + } +} + type osConfig struct { tunName string tunIPv4 string @@ -43,35 +93,24 @@ type osConfigurator struct { clientStore *client.Store clientCache *clientcache.Cache clusterConfigCache *ClusterConfigCache - // daemonClientCred are the credentials of the process that contacted the daemon. - daemonClientCred daemon.ClientCred - tunName string - tunIPv6 string - dnsAddr string - homePath string - tunIPv4 string + tunName string + tunIPv6 string + tunIPv4 string + config AdminProcessConfig } -func newOSConfigurator(tunName, ipv6Prefix, dnsAddr, homePath string, daemonClientCred daemon.ClientCred) (*osConfigurator, error) { - if homePath == "" { - // This runs as root so we need to be configured with the user's home path. - return nil, trace.BadParameter("homePath must be passed from unprivileged process") - } - +func newOSConfigurator(tunName string, config AdminProcessConfig) (*osConfigurator, error) { // ipv6Prefix always looks like "fdxx:xxxx:xxxx::" // Set the IPv6 address for the TUN to "fdxx:xxxx:xxxx::1", the first valid address in the range. - tunIPv6 := ipv6Prefix + "1" + tunIPv6 := config.IPv6Prefix + "1" configurator := &osConfigurator{ - tunName: tunName, - tunIPv6: tunIPv6, - dnsAddr: dnsAddr, - homePath: homePath, - clientStore: client.NewFSClientStore(homePath), - daemonClientCred: daemonClientCred, + clientStore: client.NewFSClientStore(config.HomePath), + clusterConfigCache: NewClusterConfigCache(clockwork.NewRealClock()), + tunName: tunName, + tunIPv6: tunIPv6, + config: config, } - configurator.clusterConfigCache = NewClusterConfigCache(clockwork.NewRealClock()) - clientCache, err := clientcache.New(clientcache.Config{ NewClientFunc: configurator.getClient, RetryWithReloginFunc: func(ctx context.Context, tc *client.TeleportClient, fn func() error, opts ...client.RetryWithReloginOption) error { @@ -92,7 +131,7 @@ func (c *osConfigurator) close() error { return trace.Wrap(c.clientCache.Clear()) } -// updateOSConfiguration reads tsh profiles out of [c.homePath]. For each profile, it reads the VNet +// updateOSConfiguration reads tsh profiles out of [c.config.HomePath]. For each profile, it reads the VNet // config of the root cluster and of each leaf cluster. Then it proceeds to update the OS based on // information from that config. // @@ -103,10 +142,10 @@ func (c *osConfigurator) updateOSConfiguration(ctx context.Context) error { var cidrRanges []string // Drop privileges to ensure that the user who spawned the daemon client has privileges necessary - // to access c.homePath that it sent when starting the daemon. + // to access [c.config.HomePath] that it sent when starting the daemon. // Otherwise a client could make the daemon read a profile out of any directory. if err := c.doWithDroppedRootPrivileges(ctx, func() error { - profileNames, err := profile.ListProfileNames(c.homePath) + profileNames, err := profile.ListProfileNames(c.config.HomePath) if err != nil { return trace.Wrap(err, "listing user profiles") } @@ -135,7 +174,7 @@ func (c *osConfigurator) updateOSConfiguration(ctx context.Context) error { tunName: c.tunName, tunIPv6: c.tunIPv6, tunIPv4: c.tunIPv4, - dnsAddr: c.dnsAddr, + dnsAddr: c.config.DNSAddr, dnsZones: dnsZones, cidrRanges: cidrRanges, }) diff --git a/lib/vnet/osconfig_darwin.go b/lib/vnet/osconfig_darwin.go index 27864c80bb400..bac21af47c3ff 100644 --- a/lib/vnet/osconfig_darwin.go +++ b/lib/vnet/osconfig_darwin.go @@ -158,12 +158,12 @@ func (c *osConfigurator) doWithDroppedRootPrivileges(ctx context.Context, fn fun rootEgid := os.Getegid() rootEuid := os.Geteuid() - log.InfoContext(ctx, "Temporarily dropping root privileges.", "egid", c.daemonClientCred.Egid, "euid", c.daemonClientCred.Euid) + log.InfoContext(ctx, "Temporarily dropping root privileges.", "egid", c.config.ClientCred.Egid, "euid", c.config.ClientCred.Euid) - if err := syscall.Setegid(c.daemonClientCred.Egid); err != nil { + if err := syscall.Setegid(c.config.ClientCred.Egid); err != nil { panic(trace.Wrap(err, "setting egid")) } - if err := syscall.Seteuid(c.daemonClientCred.Euid); err != nil { + if err := syscall.Seteuid(c.config.ClientCred.Euid); err != nil { panic(trace.Wrap(err, "setting euid")) } diff --git a/lib/vnet/osconfig_other.go b/lib/vnet/osconfig_other.go deleted file mode 100644 index 8fd543024abe3..0000000000000 --- a/lib/vnet/osconfig_other.go +++ /dev/null @@ -1,34 +0,0 @@ -// Teleport -// Copyright (C) 2024 Gravitational, Inc. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -//go:build !darwin && !windows -// +build !darwin,!windows - -package vnet - -import ( - "context" - - "github.com/gravitational/trace" -) - -func configureOS(ctx context.Context, cfg *osConfig) error { - return trace.Wrap(ErrVnetNotImplemented) -} - -func (c *osConfigurator) doWithDroppedRootPrivileges(ctx context.Context, fn func() error) (err error) { - return trace.Wrap(ErrVnetNotImplemented) -} diff --git a/lib/vnet/osconfig_windows.go b/lib/vnet/osconfig_windows.go index e1547ea69c108..d0291359984e6 100644 --- a/lib/vnet/osconfig_windows.go +++ b/lib/vnet/osconfig_windows.go @@ -25,6 +25,11 @@ import ( "github.com/gravitational/trace" ) +var ( + // ErrVnetNotImplemented is an error indicating that VNet is not implemented on the host OS. + ErrVnetNotImplemented = &trace.NotImplementedError{Message: "VNet is not implemented on windows"} +) + func configureOS(ctx context.Context, cfg *osConfig) error { // TODO(nklaassen): implement configureOS on Windows. return trace.Wrap(ErrVnetNotImplemented) diff --git a/lib/vnet/process_manager.go b/lib/vnet/process_manager.go new file mode 100644 index 0000000000000..2827ff1d9c180 --- /dev/null +++ b/lib/vnet/process_manager.go @@ -0,0 +1,79 @@ +// Teleport +// Copyright (C) 2024 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package vnet + +import ( + "context" + "fmt" + + "github.com/gravitational/trace" + "golang.org/x/sync/errgroup" +) + +func newProcessManager() (*ProcessManager, context.Context) { + ctx, cancel := context.WithCancel(context.Background()) + g, ctx := errgroup.WithContext(ctx) + return &ProcessManager{ + g: g, + cancel: cancel, + closed: make(chan struct{}), + }, ctx +} + +// ProcessManager handles background tasks needed to run VNet. +// Its semantics are similar to an error group with a context, but it cancels the context whenever +// any task returns prematurely, that is, a task exits while the context was not canceled. +type ProcessManager struct { + g *errgroup.Group + cancel context.CancelFunc + closed chan struct{} +} + +// AddCriticalBackgroundTask adds a function to the error group. [task] is expected to block until +// the context returned by [newProcessManager] gets canceled. The context gets canceled either by +// calling Close on [ProcessManager] or if any task returns. +func (pm *ProcessManager) AddCriticalBackgroundTask(name string, task func() error) { + pm.g.Go(func() error { + err := task() + if err == nil { + // Make sure to always return an error so that the errgroup context is canceled. + err = fmt.Errorf("critical task %q exited prematurely", name) + } + return trace.Wrap(err) + }) +} + +// Wait blocks and waits for the background tasks to finish, which typically happens when another +// goroutine calls Close on the process manager. +func (pm *ProcessManager) Wait() error { + err := pm.g.Wait() + select { + case <-pm.closed: + // Errors are expected after the process manager has been closed and the + // context has been canceled. + return nil + default: + } + return trace.Wrap(err) +} + +// Close stops any active background tasks by canceling the underlying context, +// and waits for all tasks to terminate. Close must not be called more than once. +func (pm *ProcessManager) Close() { + close(pm.closed) + pm.cancel() +} diff --git a/lib/vnet/socket_other.go b/lib/vnet/socket_other.go deleted file mode 100644 index 9b9ace5eaafdb..0000000000000 --- a/lib/vnet/socket_other.go +++ /dev/null @@ -1,45 +0,0 @@ -// Teleport -// Copyright (C) 2024 Gravitational, Inc. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -//go:build !darwin && !windows -// +build !darwin,!windows - -package vnet - -import ( - "os" - - "github.com/gravitational/trace" - "golang.zx2c4.com/wireguard/tun" -) - -func createSocket() (*noSocket, string, error) { - return nil, "", trace.Wrap(ErrVnetNotImplemented) -} - -func sendTUNNameAndFd(socketPath, tunName string, tunFile *os.File) error { - return trace.Wrap(ErrVnetNotImplemented) -} - -func receiveTUNDevice(_ *noSocket) (tun.Device, error) { - return nil, trace.Wrap(ErrVnetNotImplemented) -} - -type noSocket struct{} - -func (_ noSocket) Close() error { - return trace.Wrap(ErrVnetNotImplemented) -} diff --git a/lib/vnet/socket_windows.go b/lib/vnet/socket_windows.go deleted file mode 100644 index e76996edd3784..0000000000000 --- a/lib/vnet/socket_windows.go +++ /dev/null @@ -1,45 +0,0 @@ -// Teleport -// Copyright (C) 2024 Gravitational, Inc. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package vnet - -import ( - "os" - - "github.com/gravitational/trace" - "golang.zx2c4.com/wireguard/tun" -) - -func createSocket() (*noSocket, string, error) { - // TODO(nklaassen): implement createSocket on windows. - return nil, "", trace.Wrap(ErrVnetNotImplemented) -} - -func sendTUNNameAndFd(socketPath, tunName string, tunFile *os.File) error { - // TODO(nklaassen): implement sendTUNNameAndFd on windows. - return trace.Wrap(ErrVnetNotImplemented) -} - -func receiveTUNDevice(_ *noSocket) (tun.Device, error) { - // TODO(nklaassen): receiveTUNDevice on windows. - return nil, trace.Wrap(ErrVnetNotImplemented) -} - -type noSocket struct{} - -func (_ noSocket) Close() error { - return trace.Wrap(ErrVnetNotImplemented) -} diff --git a/lib/vnet/escalate_other.go b/lib/vnet/unsupported_os.go similarity index 63% rename from lib/vnet/escalate_other.go rename to lib/vnet/unsupported_os.go index 76adfdf1a6606..6592993d293eb 100644 --- a/lib/vnet/escalate_other.go +++ b/lib/vnet/unsupported_os.go @@ -24,8 +24,6 @@ import ( "runtime" "github.com/gravitational/trace" - - "github.com/gravitational/teleport/lib/vnet/daemon" ) var ( @@ -33,8 +31,30 @@ var ( ErrVnetNotImplemented = &trace.NotImplementedError{Message: "VNet is not implemented on " + runtime.GOOS} ) -// execAdminProcess is called from the normal user process to execute the admin -// subcommand as root. -func execAdminProcess(ctx context.Context, config daemon.Config) error { +type UserProcessConfig struct { + AppProvider any + ClusterConfigCache any +} + +func RunUserProcess(_ context.Context, _ *UserProcessConfig) (*ProcessManager, error) { + return nil, trace.Wrap(ErrVnetNotImplemented) +} + +type AdminProcessConfig struct { + SocketPath string + IPv6Prefix string + DNSAddr string + HomePath string +} + +func (_ AdminProcessConfig) CheckAndSetDefaults() error { + return trace.Wrap(ErrVnetNotImplemented) +} + +func configureOS(_ context.Context, _ *osConfig) error { + return trace.Wrap(ErrVnetNotImplemented) +} + +func (_ *osConfigurator) doWithDroppedRootPrivileges(_ context.Context, _ func() error) (err error) { return trace.Wrap(ErrVnetNotImplemented) } diff --git a/lib/vnet/run.go b/lib/vnet/user_process_darwin.go similarity index 68% rename from lib/vnet/run.go rename to lib/vnet/user_process_darwin.go index 6d7782e714438..117b5222c9c29 100644 --- a/lib/vnet/run.go +++ b/lib/vnet/user_process_darwin.go @@ -19,25 +19,18 @@ package vnet import ( "context" "errors" - "fmt" "os" "time" "github.com/gravitational/trace" - "golang.org/x/sync/errgroup" "golang.zx2c4.com/wireguard/tun" - "github.com/gravitational/teleport" "github.com/gravitational/teleport/api/profile" "github.com/gravitational/teleport/api/types" - logutils "github.com/gravitational/teleport/lib/utils/log" - "github.com/gravitational/teleport/lib/vnet/daemon" ) -var log = logutils.NewPackageLogger(teleport.ComponentKey, "vnet") - -// RunConfig provides the necessary configuration to run VNet. -type RunConfig struct { +// UserProcessConfig provides the necessary configuration to run VNet. +type UserProcessConfig struct { // AppProvider is a required field providing an interface implementation for [AppProvider]. AppProvider AppProvider // ClusterConfigCache is an optional field providing [ClusterConfigCache]. If empty, a new cache @@ -48,19 +41,17 @@ type RunConfig struct { HomePath string } -func (c *RunConfig) CheckAndSetDefaults() error { +func (c *UserProcessConfig) CheckAndSetDefaults() error { if c.AppProvider == nil { return trace.BadParameter("missing AppProvider") } - if c.HomePath == "" { c.HomePath = profile.FullProfilePath(os.Getenv(types.HomeEnvVar)) } - return nil } -// Run creates a network stack for VNet and runs it in the background. To do +// RunUserProcess creates a network stack for VNet and runs it in the background. To do // this, it also needs to launch an admin process in the background. It returns // a [ProcessManager] which controls the lifecycle of both background tasks. // @@ -68,10 +59,16 @@ func (c *RunConfig) CheckAndSetDefaults() error { // network stack, clean up any resources used by it and terminate the admin // process. // -// ctx is used to wait for setup steps that happen before Run hands out the -// control to the process manager. If ctx gets canceled during Run, the process +// ctx is used to wait for setup steps that happen before RunUserProcess hands out the +// control to the process manager. If ctx gets canceled during RunUserProcess, the process // manager gets closed along with its background tasks. -func Run(ctx context.Context, config *RunConfig) (*ProcessManager, error) { +func RunUserProcess(ctx context.Context, config *UserProcessConfig) (pm *ProcessManager, err error) { + defer func() { + if pm != nil && err != nil { + pm.Close() + } + }() + if err := config.CheckAndSetDefaults(); err != nil { return nil, trace.Wrap(err) } @@ -83,13 +80,6 @@ func Run(ctx context.Context, config *RunConfig) (*ProcessManager, error) { dnsIPv6 := ipv6WithSuffix(ipv6Prefix, []byte{2}) pm, processCtx := newProcessManager() - success := false - defer func() { - if !success { - // Closes the socket and background tasks. - pm.Close() - } - }() // Create the socket that's used to receive the TUN device from the admin process. socket, socketPath, err := createSocket() @@ -105,13 +95,13 @@ func Run(ctx context.Context, config *RunConfig) (*ProcessManager, error) { }) pm.AddCriticalBackgroundTask("admin process", func() error { - daemonConfig := daemon.Config{ + adminConfig := AdminProcessConfig{ SocketPath: socketPath, IPv6Prefix: ipv6Prefix.String(), DNSAddr: dnsIPv6.String(), HomePath: config.HomePath, } - return trace.Wrap(execAdminProcess(processCtx, daemonConfig)) + return trace.Wrap(execAdminProcess(processCtx, adminConfig)) }) recvTUNErr := make(chan error, 1) @@ -170,49 +160,5 @@ func Run(ctx context.Context, config *RunConfig) (*ProcessManager, error) { return trace.Wrap(ns.run(processCtx)) }) - success = true return pm, nil } - -func newProcessManager() (*ProcessManager, context.Context) { - ctx, cancel := context.WithCancel(context.Background()) - g, ctx := errgroup.WithContext(ctx) - - return &ProcessManager{ - g: g, - cancel: cancel, - }, ctx -} - -// ProcessManager handles background tasks needed to run VNet. -// Its semantics are similar to an error group with a context, but it cancels the context whenever -// any task returns prematurely, that is, a task exits while the context was not canceled. -type ProcessManager struct { - g *errgroup.Group - cancel context.CancelFunc -} - -// AddCriticalBackgroundTask adds a function to the error group. [task] is expected to block until -// the context returned by [newProcessManager] gets canceled. The context gets canceled either by -// calling Close on [ProcessManager] or if any task returns. -func (pm *ProcessManager) AddCriticalBackgroundTask(name string, task func() error) { - pm.g.Go(func() error { - err := task() - if err == nil { - // Make sure to always return an error so that the errgroup context is canceled. - err = fmt.Errorf("critical task %q exited prematurely", name) - } - return trace.Wrap(err) - }) -} - -// Wait blocks and waits for the background tasks to finish, which typically happens when another -// goroutine calls Close on the process manager. -func (pm *ProcessManager) Wait() error { - return trace.Wrap(pm.g.Wait()) -} - -// Close stops any active background tasks by canceling the underlying context. -func (pm *ProcessManager) Close() { - pm.cancel() -} diff --git a/lib/vnet/user_process_windows.go b/lib/vnet/user_process_windows.go new file mode 100644 index 0000000000000..490ffb9e8b401 --- /dev/null +++ b/lib/vnet/user_process_windows.go @@ -0,0 +1,123 @@ +// Teleport +// Copyright (C) 2024 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package vnet + +import ( + "context" + "log/slog" + "os" + + "github.com/Microsoft/go-winio" + "github.com/gravitational/teleport/api/profile" + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/trace" +) + +const ( + pipePath = `\\.\pipe\vnet` +) + +// UserProcessConfig provides the necessary configuration to run VNet. +type UserProcessConfig struct { + // AppProvider is a required field providing an interface implementation for [AppProvider]. + AppProvider AppProvider + // ClusterConfigCache is an optional field providing [ClusterConfigCache]. If empty, a new cache + // will be created. + ClusterConfigCache *ClusterConfigCache + // HomePath is the tsh home used for Teleport clients created by VNet. Resolved using the same + // rules as HomeDir in tsh. + HomePath string +} + +func (c *UserProcessConfig) CheckAndSetDefaults() error { + if c.AppProvider == nil { + return trace.BadParameter("missing AppProvider") + } + if c.HomePath == "" { + c.HomePath = profile.FullProfilePath(os.Getenv(types.HomeEnvVar)) + } + return nil +} + +// RunUserProcess launches a Windows service in the background that in turn +// calls [RunAdminProcess]. The user process exposes a gRPC interface on a named +// pipe that the admin process uses to query application names and get user +// certificates for apps. +// +// RunUserProcess returns a [ProcessManager] which controls the lifecycle of +// both the user and admin processes. +// +// The caller is expected to call Close on the process manager to clean up any +// resources and terminate the admin process, which will in turn stop the +// networking stack and deconfigure the host OS. +// +// ctx is used to wait for setup steps that happen before RunUserProcess hands out the +// control to the process manager. If ctx gets canceled during RunUserProcess, the process +// manager gets closed along with its background tasks. +func RunUserProcess(ctx context.Context, config *UserProcessConfig) (pm *ProcessManager, err error) { + defer func() { + if pm != nil && err != nil { + pm.Close() + } + }() + if err := config.CheckAndSetDefaults(); err != nil { + return nil, trace.Wrap(err) + } + ipv6Prefix, err := NewIPv6Prefix() + if err != nil { + return nil, trace.Wrap(err) + } + dnsIPv6 := ipv6WithSuffix(ipv6Prefix, []byte{2}) + // By default only the LocalSystem account, administrators, and the owner of + // the current process can access the pipe. The admin service runs as the + // LocalSystem account. We don't leak anything by letting processes owned + // by the same user as this process to connect to the pipe, they could read + // TELEPORT_HOME anyway. + pipe, err := winio.ListenPipe(pipePath, &winio.PipeConfig{}) + if err != nil { + return nil, trace.Wrap(err, "listening on named pipe") + } + pm, processCtx := newProcessManager() + pm.AddCriticalBackgroundTask("pipe closer", func() error { + <-processCtx.Done() + return trace.Wrap(pipe.Close()) + }) + pm.AddCriticalBackgroundTask("admin process", func() error { + adminConfig := AdminProcessConfig{ + NamedPipe: pipePath, + IPv6Prefix: ipv6Prefix.String(), + DNSAddr: dnsIPv6.String(), + HomePath: config.HomePath, + } + return trace.Wrap(execAdminProcess(processCtx, adminConfig)) + }) + pm.AddCriticalBackgroundTask("ipc service", func() error { + // TODO(nklaassen): wrap [config.AppProvider] with a gRPC service to expose + // the necessary methods to the admin process over [pipe]. + // For now just accept and drop any connections to prove the admin + // process can dial the pipe. The pipe will be closed when the process + // context completes and any blocked Accept call will return with an error. + slog.InfoContext(processCtx, "Listening on named pipe", "pipe", pipe.Addr().String()) + for { + _, err := pipe.Accept() + if err != nil { + return trace.Wrap(err) + } + } + }) + return pm, nil +} diff --git a/tool/tsh/common/tsh.go b/tool/tsh/common/tsh.go index 582cce0fcd08d..aff2df3db2351 100644 --- a/tool/tsh/common/tsh.go +++ b/tool/tsh/common/tsh.go @@ -1243,9 +1243,7 @@ func Run(ctx context.Context, args []string, opts ...CliOption) error { workloadIdentityCmd := newSVIDCommands(app) - vnetCmd := newVnetCommand(app) - vnetAdminSetupCmd := newVnetAdminSetupCommand(app) - vnetDaemonCmd := newVnetDaemonCommand(app) + vnetCommands := newVnetCommands(app) gitCmd := newGitCommands(app) @@ -1394,6 +1392,10 @@ func Run(ctx context.Context, args []string, opts ...CliOption) error { defer runtimetrace.Stop() } + if ok, err := vnetCommands.tryRun(&cf, command); ok || err != nil { + return trace.Wrap(err) + } + switch command { case ver.FullCommand(): err = onVersion(&cf) @@ -1620,12 +1622,6 @@ func Run(ctx context.Context, args []string, opts ...CliOption) error { err = onHeadlessApprove(&cf) case workloadIdentityCmd.issue.FullCommand(): err = workloadIdentityCmd.issue.run(&cf) - case vnetCmd.FullCommand(): - err = vnetCmd.run(&cf) - case vnetAdminSetupCmd.FullCommand(): - err = vnetAdminSetupCmd.run(&cf) - case vnetDaemonCmd.FullCommand(): - err = vnetDaemonCmd.run(&cf) case gitCmd.list.FullCommand(): err = gitCmd.list.run(&cf) case gitCmd.login.FullCommand(): diff --git a/tool/tsh/common/vnet.go b/tool/tsh/common/vnet.go new file mode 100644 index 0000000000000..d997c10a55cc7 --- /dev/null +++ b/tool/tsh/common/vnet.go @@ -0,0 +1,71 @@ +// Teleport +// Copyright (C) 2024 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package common + +import ( + "fmt" + + "github.com/alecthomas/kingpin/v2" + "github.com/gravitational/teleport/lib/vnet" + "github.com/gravitational/trace" +) + +type vnetCLICommand interface { + FullCommand() string + run(cf *CLIConf) error +} + +type vnetCommands struct { + subcommands []vnetCLICommand +} + +func (c *vnetCommands) tryRun(cf *CLIConf, command string) (bool, error) { + for _, subcommand := range c.subcommands { + if subcommand.FullCommand() == command { + return true, trace.Wrap(subcommand.run(cf)) + } + } + return false, nil +} + +type vnetCommand struct { + *kingpin.CmdClause +} + +func newVnetCommand(app *kingpin.Application) *vnetCommand { + cmd := &vnetCommand{ + CmdClause: app.Command("vnet", "Start Teleport VNet, a virtual network for TCP application access."), + } + return cmd +} + +func (c *vnetCommand) run(cf *CLIConf) error { + appProvider, err := newVnetAppProvider(cf) + if err != nil { + return trace.Wrap(err) + } + processManager, err := vnet.RunUserProcess(cf.Context, &vnet.UserProcessConfig{AppProvider: appProvider}) + if err != nil { + return trace.Wrap(err) + } + go func() { + <-cf.Context.Done() + processManager.Close() + }() + fmt.Println("VNet is ready.") + return trace.Wrap(processManager.Wait()) +} diff --git a/tool/tsh/common/vnet_daemon_darwin.go b/tool/tsh/common/vnet_daemon_darwin.go index 4154f400774bb..4ead961e59d3c 100644 --- a/tool/tsh/common/vnet_daemon_darwin.go +++ b/tool/tsh/common/vnet_daemon_darwin.go @@ -53,6 +53,5 @@ func (c *vnetDaemonCommand) run(cf *CLIConf) error { } else { utils.InitLogger(utils.LoggingForDaemon, slog.LevelInfo) } - return trace.Wrap(vnet.DaemonSubcommand(cf.Context)) } diff --git a/tool/tsh/common/vnet_darwin.go b/tool/tsh/common/vnet_darwin.go index 213a971f092b7..1ff8c555556d7 100644 --- a/tool/tsh/common/vnet_darwin.go +++ b/tool/tsh/common/vnet_darwin.go @@ -17,7 +17,6 @@ package common import ( - "fmt" "os" "github.com/alecthomas/kingpin/v2" @@ -29,36 +28,14 @@ import ( "github.com/gravitational/teleport/lib/vnet/daemon" ) -type vnetCommand struct { - *kingpin.CmdClause -} - -func newVnetCommand(app *kingpin.Application) *vnetCommand { - cmd := &vnetCommand{ - CmdClause: app.Command("vnet", "Start Teleport VNet, a virtual network for TCP application access."), - } - return cmd -} - -func (c *vnetCommand) run(cf *CLIConf) error { - appProvider, err := newVnetAppProvider(cf) - if err != nil { - return trace.Wrap(err) - } - - processManager, err := vnet.Run(cf.Context, &vnet.RunConfig{AppProvider: appProvider}) - if err != nil { - return trace.Wrap(err) +func newVnetCommands(app *kingpin.Application) *vnetCommands { + return &vnetCommands{ + subcommands: []vnetCLICommand{ + newVnetCommand(app), + newVnetAdminSetupCommand(app), + newVnetDaemonCommand(app), + }, } - - go func() { - <-cf.Context.Done() - processManager.Close() - }() - - fmt.Println("VNet is ready.") - - return trace.Wrap(processManager.Wait()) } // vnetAdminSetupCommand is the fallback command ran as root when tsh wasn't compiled with the @@ -102,7 +79,7 @@ func (c *vnetAdminSetupCommand) run(cf *CLIConf) error { return trace.BadParameter("%s must be set", types.HomeEnvVar) } - config := daemon.Config{ + config := vnet.AdminProcessConfig{ SocketPath: c.socketPath, IPv6Prefix: c.ipv6Prefix, DNSAddr: c.dnsAddr, diff --git a/tool/tsh/common/vnet_nodaemon.go b/tool/tsh/common/vnet_nodaemon.go index 2e6d516e214f8..1c3ee88952746 100644 --- a/tool/tsh/common/vnet_nodaemon.go +++ b/tool/tsh/common/vnet_nodaemon.go @@ -21,7 +21,6 @@ package common import ( "github.com/alecthomas/kingpin/v2" - "github.com/gravitational/trace" ) func newVnetDaemonCommand(app *kingpin.Application) vnetDaemonNotSupported { @@ -33,6 +32,8 @@ type vnetDaemonNotSupported struct{} func (vnetDaemonNotSupported) FullCommand() string { return "" } -func (vnetDaemonNotSupported) run(*CLIConf) error { - return trace.NotImplemented("tsh was built without support for VNet daemon") + +func (vnetDaemonNotSupported) run(_ *CLIConf) error { + // This can never be called because FullCommand returns an empty string. + return nil } diff --git a/tool/tsh/common/vnet_other.go b/tool/tsh/common/vnet_other.go index dc705ee824567..fbb29a27fa501 100644 --- a/tool/tsh/common/vnet_other.go +++ b/tool/tsh/common/vnet_other.go @@ -21,26 +21,10 @@ package common import ( "github.com/alecthomas/kingpin/v2" - "github.com/gravitational/trace" - - "github.com/gravitational/teleport/lib/vnet" ) -func newVnetCommand(app *kingpin.Application) vnetNotSupported { - return vnetNotSupported{} -} - -func newVnetAdminSetupCommand(app *kingpin.Application) vnetNotSupported { - return vnetNotSupported{} -} - -type vnetNotSupported struct{} - -func (vnetNotSupported) FullCommand() string { - return "" -} -func (vnetNotSupported) run(*CLIConf) error { - return trace.Wrap(vnet.ErrVnetNotImplemented) +func newVnetCommands(app *kingpin.Application) *vnetCommands { + return &vnetCommands{} } var ( diff --git a/tool/tsh/common/vnet_windows.go b/tool/tsh/common/vnet_windows.go index 59d90972f2971..c645849577866 100644 --- a/tool/tsh/common/vnet_windows.go +++ b/tool/tsh/common/vnet_windows.go @@ -17,95 +17,82 @@ package common import ( - "fmt" - "os" - "github.com/alecthomas/kingpin/v2" "github.com/gravitational/trace" + "golang.org/x/sys/windows/svc" - "github.com/gravitational/teleport" - "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/vnet" - "github.com/gravitational/teleport/lib/vnet/daemon" ) -type vnetCommand struct { - *kingpin.CmdClause +func isWindowsService() bool { + isSvc, err := svc.IsWindowsService() + return err == nil && isSvc } -func newVnetCommand(app *kingpin.Application) *vnetCommand { - cmd := &vnetCommand{ - CmdClause: app.Command("vnet", "Start Teleport VNet, a virtual network for TCP application access.").Hidden(), +func newVnetCommands(app *kingpin.Application) *vnetCommands { + if isWindowsService() { + return &vnetCommands{ + subcommands: []vnetCLICommand{ + newVnetServiceCommand(app), + }, + } + } + return &vnetCommands{ + subcommands: []vnetCLICommand{ + newVnetCommand(app), + newVnetInstallServiceCommand(app), + newVnetUninstallServiceCommand(app), + }, } - return cmd } -func (c *vnetCommand) run(cf *CLIConf) error { - appProvider, err := newVnetAppProvider(cf) - if err != nil { - return trace.Wrap(err) - } +type vnetInstallServiceCommand struct { + *kingpin.CmdClause + userSID string +} - processManager, err := vnet.Run(cf.Context, &vnet.RunConfig{AppProvider: appProvider}) - if err != nil { - return trace.Wrap(err) +func newVnetInstallServiceCommand(app *kingpin.Application) *vnetInstallServiceCommand { + cmd := &vnetInstallServiceCommand{ + CmdClause: app.Command("vnet-install-service", "Install the VNet Windows service.").Hidden(), } + cmd.Flag("userSID", "SID of the user that the service should be installed for.").Required().StringVar(&cmd.userSID) + return cmd +} - go func() { - <-cf.Context.Done() - processManager.Close() - }() - - fmt.Println("VNet is ready.") - - return trace.Wrap(processManager.Wait()) +func (c *vnetInstallServiceCommand) run(cf *CLIConf) error { + return trace.Wrap(vnet.InstallService(cf.Context, c.userSID)) } -// vnetAdminSetupCommand is the fallback command run as root when tsh isn't -// compiled with the vnetdaemon build tag. This is typically the case when -// running tsh in development where it's not signed and bundled in tsh.app. -// -// This command expects TELEPORT_HOME to be set to the tsh home of the user who wants to run VNet. -type vnetAdminSetupCommand struct { +type vnetUninstallServiceCommand struct { *kingpin.CmdClause - // socketPath is a path to a unix socket used for passing a TUN device from the admin process to - // the unprivileged process. - socketPath string - // ipv6Prefix is the IPv6 prefix for the VNet. - ipv6Prefix string - // dnsAddr is the IP address for the VNet DNS server. - dnsAddr string } -func newVnetAdminSetupCommand(app *kingpin.Application) *vnetAdminSetupCommand { - cmd := &vnetAdminSetupCommand{ - CmdClause: app.Command(teleport.VnetAdminSetupSubCommand, "Start the VNet admin subprocess.").Hidden(), +func newVnetUninstallServiceCommand(app *kingpin.Application) *vnetUninstallServiceCommand { + cmd := &vnetUninstallServiceCommand{ + CmdClause: app.Command("vnet-uninstall-service", "Uninstall (delete) the VNet Windows service.").Hidden(), } - cmd.Flag("socket", "socket path").StringVar(&cmd.socketPath) - cmd.Flag("ipv6-prefix", "IPv6 prefix for the VNet").StringVar(&cmd.ipv6Prefix) - cmd.Flag("dns-addr", "VNet DNS address").StringVar(&cmd.dnsAddr) return cmd } -func (c *vnetAdminSetupCommand) run(cf *CLIConf) error { - homePath := os.Getenv(types.HomeEnvVar) - if homePath == "" { - // This runs as root so we need to be configured with the user's home path. - return trace.BadParameter("%s must be set", types.HomeEnvVar) - } +func (c *vnetUninstallServiceCommand) run(cf *CLIConf) error { + return trace.Wrap(vnet.UninstallService(cf.Context)) +} - config := daemon.Config{ - SocketPath: c.socketPath, - IPv6Prefix: c.ipv6Prefix, - DNSAddr: c.dnsAddr, - HomePath: homePath, - ClientCred: daemon.ClientCred{ - // TODO(nklaassen): figure out how to pass some form of user - // identifier. For now Valid: true is a hack to make - // CheckAndSetDefaults pass. - Valid: true, - }, +// vnetServiceCommand is the command that runs the Windows service. +type vnetServiceCommand struct { + *kingpin.CmdClause +} + +func newVnetServiceCommand(app *kingpin.Application) *vnetServiceCommand { + cmd := &vnetServiceCommand{ + CmdClause: app.Command("vnet-service", "Start the VNet service.").Hidden(), } + return cmd +} - return trace.Wrap(vnet.RunAdminProcess(cf.Context, config)) +func (c *vnetServiceCommand) run(_ *CLIConf) error { + if err := vnet.ServiceMain(); err != nil { + return trace.Wrap(err) + } + return nil } From b09e5d449d63dd8bf5db95e9cbc76e59f48ddf23 Mon Sep 17 00:00:00 2001 From: Nic Klaassen Date: Fri, 20 Dec 2024 09:31:39 -0800 Subject: [PATCH 2/5] fix process manager test --- lib/vnet/process_manager.go | 24 +++++++++++++++--------- lib/vnet/process_manager_test.go | 4 +--- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/lib/vnet/process_manager.go b/lib/vnet/process_manager.go index 2827ff1d9c180..8e7ee6ed61cd3 100644 --- a/lib/vnet/process_manager.go +++ b/lib/vnet/process_manager.go @@ -19,6 +19,7 @@ package vnet import ( "context" "fmt" + "sync" "github.com/gravitational/trace" "golang.org/x/sync/errgroup" @@ -27,20 +28,25 @@ import ( func newProcessManager() (*ProcessManager, context.Context) { ctx, cancel := context.WithCancel(context.Background()) g, ctx := errgroup.WithContext(ctx) - return &ProcessManager{ + pm := &ProcessManager{ g: g, cancel: cancel, closed: make(chan struct{}), - }, ctx + } + pm.closeOnce = sync.OnceFunc(func() { + close(pm.closed) + }) + return pm, ctx } // ProcessManager handles background tasks needed to run VNet. // Its semantics are similar to an error group with a context, but it cancels the context whenever // any task returns prematurely, that is, a task exits while the context was not canceled. type ProcessManager struct { - g *errgroup.Group - cancel context.CancelFunc - closed chan struct{} + g *errgroup.Group + cancel context.CancelFunc + closed chan struct{} + closeOnce func() } // AddCriticalBackgroundTask adds a function to the error group. [task] is expected to block until @@ -63,17 +69,17 @@ func (pm *ProcessManager) Wait() error { err := pm.g.Wait() select { case <-pm.closed: - // Errors are expected after the process manager has been closed and the - // context has been canceled. + // Errors are expected after the process manager has been closed, + // usually due to context cancellation. return nil default: + return trace.Wrap(err) } - return trace.Wrap(err) } // Close stops any active background tasks by canceling the underlying context, // and waits for all tasks to terminate. Close must not be called more than once. func (pm *ProcessManager) Close() { - close(pm.closed) + pm.closeOnce() pm.cancel() } diff --git a/lib/vnet/process_manager_test.go b/lib/vnet/process_manager_test.go index 5309150e35b5e..1e95205a99117 100644 --- a/lib/vnet/process_manager_test.go +++ b/lib/vnet/process_manager_test.go @@ -70,8 +70,6 @@ func TestProcessManager_Close(t *testing.T) { }) pm.Close() - err := pm.Wait() - require.ErrorIs(t, err, context.Canceled) - require.ErrorIs(t, err, context.Cause(pmCtx)) + require.NoError(t, err) } From 019b3ca830c275685018d060730fb764efd4afbd Mon Sep 17 00:00:00 2001 From: Nic Klaassen Date: Fri, 20 Dec 2024 09:38:39 -0800 Subject: [PATCH 3/5] make fix-imports --- lib/devicetrust/native/device_windows.go | 2 +- lib/vnet/admin_process_darwin.go | 3 ++- lib/vnet/user_process_windows.go | 3 ++- tool/tsh/common/vnet.go | 3 ++- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/devicetrust/native/device_windows.go b/lib/devicetrust/native/device_windows.go index 575e238af5623..3cde66a79de00 100644 --- a/lib/devicetrust/native/device_windows.go +++ b/lib/devicetrust/native/device_windows.go @@ -30,13 +30,13 @@ import ( "time" "github.com/google/go-attestation/attest" - "github.com/gravitational/teleport" "github.com/gravitational/trace" "github.com/yusufpapurcu/wmi" "golang.org/x/sync/errgroup" "golang.org/x/sys/windows" "google.golang.org/protobuf/types/known/timestamppb" + "github.com/gravitational/teleport" devicepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/devicetrust/v1" "github.com/gravitational/teleport/lib/windowsexec" ) diff --git a/lib/vnet/admin_process_darwin.go b/lib/vnet/admin_process_darwin.go index c80518e74a4b2..0ec8b9cdf4ce2 100644 --- a/lib/vnet/admin_process_darwin.go +++ b/lib/vnet/admin_process_darwin.go @@ -21,9 +21,10 @@ import ( "os" "time" - "github.com/gravitational/teleport/lib/vnet/daemon" "github.com/gravitational/trace" "golang.zx2c4.com/wireguard/tun" + + "github.com/gravitational/teleport/lib/vnet/daemon" ) type AdminProcessConfig daemon.Config diff --git a/lib/vnet/user_process_windows.go b/lib/vnet/user_process_windows.go index 490ffb9e8b401..7e6a354d39b2f 100644 --- a/lib/vnet/user_process_windows.go +++ b/lib/vnet/user_process_windows.go @@ -22,9 +22,10 @@ import ( "os" "github.com/Microsoft/go-winio" + "github.com/gravitational/trace" + "github.com/gravitational/teleport/api/profile" "github.com/gravitational/teleport/api/types" - "github.com/gravitational/trace" ) const ( diff --git a/tool/tsh/common/vnet.go b/tool/tsh/common/vnet.go index d997c10a55cc7..8aceded6b2eac 100644 --- a/tool/tsh/common/vnet.go +++ b/tool/tsh/common/vnet.go @@ -20,8 +20,9 @@ import ( "fmt" "github.com/alecthomas/kingpin/v2" - "github.com/gravitational/teleport/lib/vnet" "github.com/gravitational/trace" + + "github.com/gravitational/teleport/lib/vnet" ) type vnetCLICommand interface { From 81067c2980a5f9e49b03b01bf9798169938f36a2 Mon Sep 17 00:00:00 2001 From: Nic Klaassen Date: Fri, 20 Dec 2024 09:49:00 -0800 Subject: [PATCH 4/5] fix lint --- lib/vnet/admin_process_windows.go | 12 +++++++++--- lib/vnet/escalate_windows.go | 2 +- lib/vnet/unsupported_os.go | 5 +++++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/lib/vnet/admin_process_windows.go b/lib/vnet/admin_process_windows.go index 02e210bf015a9..10e9a32fff0e2 100644 --- a/lib/vnet/admin_process_windows.go +++ b/lib/vnet/admin_process_windows.go @@ -64,7 +64,7 @@ func RunAdminProcess(ctx context.Context, cfg AdminProcessConfig) error { return trace.Wrap(err, "dialing named pipe %s", pipePath) } conn.Close() - log.InfoContext(ctx, "Succesfully connected to user process over named pipe", "pipe", pipePath) + log.InfoContext(ctx, "Successfully connected to user process over named pipe", "pipe", pipePath) device, err := tun.CreateTUN("TeleportVNet", mtu) if err != nil { @@ -77,8 +77,8 @@ func RunAdminProcess(ctx context.Context, cfg AdminProcessConfig) error { } log.InfoContext(ctx, "Created TUN interface", "tun", tunName) - // TODO(nklaassen): actually run the networking stack and OS configuration. - // For now, stay alive as long as we can dial the pipe. + // TODO(nklaassen): actually run VNet. For now, stay alive as long as we can + // dial the pipe. for { select { case <-time.After(time.Second): @@ -92,3 +92,9 @@ func RunAdminProcess(ctx context.Context, cfg AdminProcessConfig) error { } } } + +var ( + // Satisfy unused linter. + // TODO(nklaassen): run os configuration loop in admin process. + _ = osConfigurationLoop +) diff --git a/lib/vnet/escalate_windows.go b/lib/vnet/escalate_windows.go index c38fe6a3f4d03..d3cee1d585d4d 100644 --- a/lib/vnet/escalate_windows.go +++ b/lib/vnet/escalate_windows.go @@ -171,7 +171,7 @@ func waitForService(ctx context.Context, scManager windows.Handle, serviceNamePt } // InstallService implements the vnet-install-service command, it must run as -// adminstrator and installs the TeleportVNet Windows service. +// administrator and installs the TeleportVNet Windows service. func InstallService(ctx context.Context, userSID string) error { m, err := mgr.Connect() if err != nil { diff --git a/lib/vnet/unsupported_os.go b/lib/vnet/unsupported_os.go index 6592993d293eb..129edeb652757 100644 --- a/lib/vnet/unsupported_os.go +++ b/lib/vnet/unsupported_os.go @@ -58,3 +58,8 @@ func configureOS(_ context.Context, _ *osConfig) error { func (_ *osConfigurator) doWithDroppedRootPrivileges(_ context.Context, _ func() error) (err error) { return trace.Wrap(ErrVnetNotImplemented) } + +var ( + // Satisfy unused linter. + _ = osConfigurationLoop +) From e3ec4bec5aa94cc4e6724884b3830a06ddcf94d6 Mon Sep 17 00:00:00 2001 From: Nic Klaassen Date: Fri, 20 Dec 2024 11:54:43 -0800 Subject: [PATCH 5/5] fix tsh linux lint --- tool/tsh/common/vnet_other.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tool/tsh/common/vnet_other.go b/tool/tsh/common/vnet_other.go index fbb29a27fa501..2b10e48d947d5 100644 --- a/tool/tsh/common/vnet_other.go +++ b/tool/tsh/common/vnet_other.go @@ -29,6 +29,7 @@ func newVnetCommands(app *kingpin.Application) *vnetCommands { var ( // Satisfy unused linter. - _ = (*vnetAppProvider)(nil) _ = newVnetAppProvider + _ = newVnetCommand + _ = newVnetDaemonCommand )