Skip to content

Commit

Permalink
[vnet] add vnet-(un)install-service commands for Windows (#52364)
Browse files Browse the repository at this point in the history
  • Loading branch information
nklaassen authored Feb 20, 2025
1 parent 9051675 commit 94067da
Show file tree
Hide file tree
Showing 7 changed files with 246 additions and 3 deletions.
2 changes: 1 addition & 1 deletion lib/devicetrust/native/device_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
181 changes: 181 additions & 0 deletions lib/vnet/install_service_windows.go
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

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
}
6 changes: 6 additions & 0 deletions tool/tsh/common/tsh.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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():
Expand Down
8 changes: 8 additions & 0 deletions tool/tsh/common/vnet.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand Down
12 changes: 11 additions & 1 deletion tool/tsh/common/vnet_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
8 changes: 8 additions & 0 deletions tool/tsh/common/vnet_other.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
32 changes: 31 additions & 1 deletion tool/tsh/common/vnet_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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{}
Expand Down

0 comments on commit 94067da

Please sign in to comment.