From 94067da960caa7eac7afba9f61b0f8cddb616371 Mon Sep 17 00:00:00 2001 From: Nic Klaassen Date: Thu, 20 Feb 2025 12:15:23 -0800 Subject: [PATCH] [vnet] add vnet-(un)install-service commands for Windows (#52364) --- lib/devicetrust/native/device_windows.go | 2 +- lib/vnet/install_service_windows.go | 181 +++++++++++++++++++++++ tool/tsh/common/tsh.go | 6 + tool/tsh/common/vnet.go | 8 + tool/tsh/common/vnet_darwin.go | 12 +- tool/tsh/common/vnet_other.go | 8 + tool/tsh/common/vnet_windows.go | 32 +++- 7 files changed, 246 insertions(+), 3 deletions(-) create mode 100644 lib/vnet/install_service_windows.go diff --git a/lib/devicetrust/native/device_windows.go b/lib/devicetrust/native/device_windows.go index b0a871c18a994..a9b3858a46d92 100644 --- a/lib/devicetrust/native/device_windows.go +++ b/lib/devicetrust/native/device_windows.go @@ -230,7 +230,7 @@ func collectDeviceData(_ CollectDataMode) (*devicepb.DeviceCollectedData, error) return dcd, nil } -// activateCredentialInElevated child uses `runas` to trigger a child process +// activateCredentialInElevatedChild uses `runas` to trigger a child process // with elevated privileges. This is necessary because the process must have // elevated privileges in order to invoke the TPM 2.0 ActivateCredential // command. diff --git a/lib/vnet/install_service_windows.go b/lib/vnet/install_service_windows.go new file mode 100644 index 0000000000000..ff772ef25d0f3 --- /dev/null +++ b/lib/vnet/install_service_windows.go @@ -0,0 +1,181 @@ +// Teleport +// Copyright (C) 2025 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" + "errors" + "os" + "path/filepath" + "strings" + + "github.com/gravitational/trace" + "golang.org/x/sys/windows" + "golang.org/x/sys/windows/svc/mgr" +) + +// InstallService installs the VNet windows service. +// +// Windows services are installed by the service manager, which takes a path to +// the service executable. So that regular users are not able to overwrite the +// executable at that path, we use a path under %PROGRAMFILES%, which is not +// writable by regular users by default. +func InstallService(ctx context.Context) (err error) { + tshPath, err := os.Executable() + if err != nil { + return trace.Wrap(err, "getting current exe path") + } + if err := assertTshInProgramFiles(tshPath); err != nil { + return trace.Wrap(err, "checking if tsh.exe is installed under %%PROGRAMFILES%%") + } + if err := assertWintunInstalled(tshPath); err != nil { + return trace.Wrap(err, "checking if wintun.dll is installed next to %s", tshPath) + } + + svcMgr, err := mgr.Connect() + if err != nil { + return trace.Wrap(err, "connecting to Windows service manager") + } + svc, err := svcMgr.OpenService(serviceName) + if err != nil { + if !errors.Is(err, windows.ERROR_SERVICE_DOES_NOT_EXIST) { + return trace.Wrap(err, "unexpected error checking if Windows service %s exists", serviceName) + } + // The service has not been created yet and must be installed. + svc, err = svcMgr.CreateService( + serviceName, + tshPath, + mgr.Config{ + StartType: mgr.StartManual, + }, + ServiceCommand, + ) + if err != nil { + return trace.Wrap(err, "creating VNet Windows service") + } + } + if err := svc.Close(); err != nil { + return trace.Wrap(err, "closing VNet Windows service") + } + if err := grantServiceRights(); err != nil { + return trace.Wrap(err, "granting authenticated users permission to control the VNet Windows service") + } + return nil +} + +// UninstallService uninstalls the VNet windows service. +func UninstallService(ctx context.Context) (err error) { + svcMgr, err := mgr.Connect() + if err != nil { + return trace.Wrap(err, "connecting to Windows service manager") + } + svc, err := svcMgr.OpenService(serviceName) + if err != nil { + return trace.Wrap(err, "opening Windows service %s", serviceName) + } + if err := svc.Delete(); err != nil { + return trace.Wrap(err, "deleting Windows service %s", serviceName) + } + if err := svc.Close(); err != nil { + return trace.Wrap(err, "closing VNet Windows service") + } + return nil +} + +func grantServiceRights() error { + // Get the current security info for the service, requesting only the DACL + // (discretionary access control list). + si, err := windows.GetNamedSecurityInfo(serviceName, windows.SE_SERVICE, windows.DACL_SECURITY_INFORMATION) + if err != nil { + return trace.Wrap(err, "getting current service security information") + } + // Get the DACL from the security info. + dacl, _ /*defaulted*/, err := si.DACL() + if err != nil { + return trace.Wrap(err, "getting current service DACL") + } + // Build an explicit access entry allowing authenticated users to start, + // stop, and query the service. + ea := []windows.EXPLICIT_ACCESS{{ + AccessPermissions: windows.SERVICE_QUERY_STATUS | windows.SERVICE_START | windows.SERVICE_STOP, + AccessMode: windows.GRANT_ACCESS, + Trustee: windows.TRUSTEE{ + TrusteeForm: windows.TRUSTEE_IS_NAME, + TrusteeType: windows.TRUSTEE_IS_WELL_KNOWN_GROUP, + TrusteeValue: windows.TrusteeValueFromString("Authenticated Users"), + }, + }} + // Merge the new explicit access entry with the existing DACL. + dacl, err = windows.ACLFromEntries(ea, dacl) + if err != nil { + return trace.Wrap(err, "merging service DACL entries") + } + // Set the DACL on the service security info. + if err := windows.SetNamedSecurityInfo( + serviceName, + windows.SE_SERVICE, + windows.DACL_SECURITY_INFORMATION, + nil, // owner + nil, // group + dacl, // dacl + nil, // sacl + ); err != nil { + return trace.Wrap(err, "setting service DACL") + } + return nil +} + +// assertTshInProgramFiles asserts that tsh is a regular file installed under +// the program files directory (usually C:\Program Files\). +func assertTshInProgramFiles(tshPath string) error { + if err := assertRegularFile(tshPath); err != nil { + return trace.Wrap(err) + } + programFiles := os.Getenv("PROGRAMFILES") + if programFiles == "" { + return trace.Errorf("PROGRAMFILES env var is not set") + } + // Windows file paths are case-insensitive. + cleanedProgramFiles := strings.ToLower(filepath.Clean(programFiles)) + string(filepath.Separator) + cleanedTshPath := strings.ToLower(filepath.Clean(tshPath)) + if !strings.HasPrefix(cleanedTshPath, cleanedProgramFiles) { + return trace.BadParameter( + "tsh.exe is currently installed at %s, it must be installed under %s in order to install the VNet Windows service", + tshPath, programFiles) + } + return nil +} + +// asertWintunInstalled returns an error if wintun.dll is not a regular file +// installed in the same directory as tshPath. +func assertWintunInstalled(tshPath string) error { + dir := filepath.Dir(tshPath) + wintunPath := filepath.Join(dir, "wintun.dll") + return trace.Wrap(assertRegularFile(wintunPath)) +} + +func assertRegularFile(path string) error { + switch info, err := os.Lstat(path); { + case os.IsNotExist(err): + return trace.Wrap(err, "%s not found", path) + case err != nil: + return trace.Wrap(err, "unexpected error checking %s", path) + case !info.Mode().IsRegular(): + return trace.BadParameter("%s is not a regular file", path) + } + return nil +} diff --git a/tool/tsh/common/tsh.go b/tool/tsh/common/tsh.go index 0c6eed54b16f9..6c0e44ee427ee 100644 --- a/tool/tsh/common/tsh.go +++ b/tool/tsh/common/tsh.go @@ -1268,6 +1268,8 @@ func Run(ctx context.Context, args []string, opts ...CliOption) error { vnetAdminSetupCommand := newVnetAdminSetupCommand(app) vnetDaemonCommand := newVnetDaemonCommand(app) vnetServiceCommand := newVnetServiceCommand(app) + vnetInstallServiceCommand := newVnetInstallServiceCommand(app) + vnetUninstallServiceCommand := newVnetUninstallServiceCommand(app) gitCmd := newGitCommands(app) @@ -1652,6 +1654,10 @@ func Run(ctx context.Context, args []string, opts ...CliOption) error { err = vnetDaemonCommand.run(&cf) case vnetServiceCommand.FullCommand(): err = vnetServiceCommand.run(&cf) + case vnetInstallServiceCommand.FullCommand(): + err = vnetInstallServiceCommand.run(&cf) + case vnetUninstallServiceCommand.FullCommand(): + err = vnetUninstallServiceCommand.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 index 1725d32080701..7fad88402b35c 100644 --- a/tool/tsh/common/vnet.go +++ b/tool/tsh/common/vnet.go @@ -100,6 +100,14 @@ func newVnetServiceCommand(app *kingpin.Application) vnetCLICommand { return newPlatformVnetServiceCommand(app) } +func newVnetInstallServiceCommand(app *kingpin.Application) vnetCLICommand { + return newPlatformVnetInstallServiceCommand(app) +} + +func newVnetUninstallServiceCommand(app *kingpin.Application) vnetCLICommand { + return newPlatformVnetUninstallServiceCommand(app) +} + // vnetCommandNotSupported implements vnetCLICommand, it is returned when a specific // command is not implemented for a certain platform or environment. type vnetCommandNotSupported struct{} diff --git a/tool/tsh/common/vnet_darwin.go b/tool/tsh/common/vnet_darwin.go index 3ca4ef9f15be2..995e75c808e9c 100644 --- a/tool/tsh/common/vnet_darwin.go +++ b/tool/tsh/common/vnet_darwin.go @@ -85,11 +85,21 @@ func (c *vnetAdminSetupCommand) run(cf *CLIConf) error { return trace.Wrap(vnet.RunDarwinAdminProcess(cf.Context, config)) } -// the vnet-service command is only supported on windows. +// The vnet-service command is only supported on windows. func newPlatformVnetServiceCommand(app *kingpin.Application) vnetCommandNotSupported { return vnetCommandNotSupported{} } +// The vnet-install-service command is only supported on windows. +func newPlatformVnetInstallServiceCommand(app *kingpin.Application) vnetCommandNotSupported { + return vnetCommandNotSupported{} +} + +// The vnet-uninstall-service command is only supported on windows. +func newPlatformVnetUninstallServiceCommand(app *kingpin.Application) vnetCommandNotSupported { + return vnetCommandNotSupported{} +} + func runVnetDiagnostics(ctx context.Context, nsi vnet.NetworkStackInfo) error { fmt.Println("Running diagnostics.") routeConflictDiag, err := diag.NewRouteConflictDiag(&diag.RouteConflictConfig{ diff --git a/tool/tsh/common/vnet_other.go b/tool/tsh/common/vnet_other.go index d5c08d958837d..2c5434f57c96e 100644 --- a/tool/tsh/common/vnet_other.go +++ b/tool/tsh/common/vnet_other.go @@ -39,6 +39,14 @@ func newPlatformVnetServiceCommand(app *kingpin.Application) vnetCLICommand { return vnetCommandNotSupported{} } +func newPlatformVnetInstallServiceCommand(app *kingpin.Application) vnetCommandNotSupported { + return vnetCommandNotSupported{} +} + +func newPlatformVnetUninstallServiceCommand(app *kingpin.Application) vnetCommandNotSupported { + return vnetCommandNotSupported{} +} + //nolint:staticcheck // SA4023. runVnetDiagnostics on unsupported platforms always returns err. func runVnetDiagnostics(ctx context.Context, nsi vnet.NetworkStackInfo) error { return trace.NotImplemented("diagnostics are not implemented yet on this platform") diff --git a/tool/tsh/common/vnet_windows.go b/tool/tsh/common/vnet_windows.go index 6e2a30b3593af..ba23c936cbaeb 100644 --- a/tool/tsh/common/vnet_windows.go +++ b/tool/tsh/common/vnet_windows.go @@ -33,7 +33,7 @@ type vnetServiceCommand struct { func newPlatformVnetServiceCommand(app *kingpin.Application) *vnetServiceCommand { cmd := &vnetServiceCommand{ - CmdClause: app.Command(vnet.ServiceCommand, "Start the VNet service.").Hidden(), + CmdClause: app.Command(vnet.ServiceCommand, "Start the VNet Windows service.").Hidden(), } return cmd } @@ -53,6 +53,36 @@ func isWindowsService() bool { return err == nil && isSvc } +type vnetInstallServiceCommand struct { + *kingpin.CmdClause +} + +func newPlatformVnetInstallServiceCommand(app *kingpin.Application) *vnetInstallServiceCommand { + cmd := &vnetInstallServiceCommand{ + CmdClause: app.Command("vnet-install-service", "Install the VNet Windows service.").Hidden(), + } + return cmd +} + +func (c *vnetInstallServiceCommand) run(cf *CLIConf) error { + return trace.Wrap(vnet.InstallService(cf.Context), "installing Windows service") +} + +type vnetUninstallServiceCommand struct { + *kingpin.CmdClause +} + +func newPlatformVnetUninstallServiceCommand(app *kingpin.Application) *vnetUninstallServiceCommand { + cmd := &vnetUninstallServiceCommand{ + CmdClause: app.Command("vnet-uninstall-service", "Uninstall the VNet Windows service.").Hidden(), + } + return cmd +} + +func (c *vnetUninstallServiceCommand) run(cf *CLIConf) error { + return trace.Wrap(vnet.UninstallService(cf.Context), "uninstalling Windows service") +} + // the admin-setup command is only supported on darwin. func newPlatformVnetAdminSetupCommand(*kingpin.Application) vnetCommandNotSupported { return vnetCommandNotSupported{}