From 4e68447560b19dc188392c16cad8234a0db2ab72 Mon Sep 17 00:00:00 2001 From: Nic Klaassen Date: Mon, 13 Jan 2025 15:59:26 -0800 Subject: [PATCH] [vnet] add windows tsh cli commands (#50935) * [vnet] add windows tsh cli commands This PR refactors the VNet tsh CLI commands and adds stubs for the commands that are going to be added for Windows VNet support. * fix linux build * update copyright year * use context.AfterFunc * fix typo --- tool/tsh/common/tsh.go | 27 ++++--- tool/tsh/common/vnet.go | 92 ++++++++++++++++++++++ tool/tsh/common/vnet_daemon_darwin.go | 4 +- tool/tsh/common/vnet_darwin.go | 50 ++++-------- tool/tsh/common/vnet_nodaemon.go | 15 +--- tool/tsh/common/vnet_other.go | 37 ++++----- tool/tsh/common/vnet_windows.go | 106 ++++++++++---------------- 7 files changed, 188 insertions(+), 143 deletions(-) create mode 100644 tool/tsh/common/vnet.go diff --git a/tool/tsh/common/tsh.go b/tool/tsh/common/tsh.go index 7677a6a842251..f9d4a038a9ae6 100644 --- a/tool/tsh/common/tsh.go +++ b/tool/tsh/common/tsh.go @@ -1258,9 +1258,12 @@ func Run(ctx context.Context, args []string, opts ...CliOption) error { workloadIdentityCmd := newSVIDCommands(app) - vnetCmd := newVnetCommand(app) - vnetAdminSetupCmd := newVnetAdminSetupCommand(app) - vnetDaemonCmd := newVnetDaemonCommand(app) + vnetCommand := newVnetCommand(app) + vnetAdminSetupCommand := newVnetAdminSetupCommand(app) + vnetDaemonCommand := newVnetDaemonCommand(app) + vnetInstallServiceCommand := newVnetInstallServiceCommand(app) + vnetUninstallServiceCommand := newVnetUninstallServiceCommand(app) + vnetServiceCommand := newVnetServiceCommand(app) gitCmd := newGitCommands(app) @@ -1638,12 +1641,18 @@ 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 vnetCommand.FullCommand(): + err = vnetCommand.run(&cf) + case vnetAdminSetupCommand.FullCommand(): + err = vnetAdminSetupCommand.run(&cf) + case vnetDaemonCommand.FullCommand(): + err = vnetDaemonCommand.run(&cf) + case vnetInstallServiceCommand.FullCommand(): + err = vnetInstallServiceCommand.run(&cf) + case vnetUninstallServiceCommand.FullCommand(): + err = vnetUninstallServiceCommand.run(&cf) + case vnetServiceCommand.FullCommand(): + err = vnetServiceCommand.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..8bcd80a57590f --- /dev/null +++ b/tool/tsh/common/vnet.go @@ -0,0 +1,92 @@ +// 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 common + +import ( + "context" + "fmt" + + "github.com/alecthomas/kingpin/v2" + "github.com/gravitational/trace" + + "github.com/gravitational/teleport/lib/vnet" +) + +type vnetCLICommand interface { + // FullCommand matches the signature of kingpin.CmdClause.FullCommand, which + // most commands should embed. + FullCommand() string + // run should be called iff FullCommand() matches the CLI parameters. + run(cf *CLIConf) error +} + +// vnetCommand implements the `tsh vnet` command to run VNet. +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) + } + fmt.Println("VNet is ready.") + context.AfterFunc(cf.Context, processManager.Close) + return trace.Wrap(processManager.Wait()) +} + +func newVnetAdminSetupCommand(app *kingpin.Application) vnetCLICommand { + return newPlatformVnetAdminSetupCommand(app) +} + +func newVnetDaemonCommand(app *kingpin.Application) vnetCLICommand { + return newPlatformVnetDaemonCommand(app) +} + +func newVnetInstallServiceCommand(app *kingpin.Application) vnetCLICommand { + return newPlatformVnetInstallServiceCommand(app) +} + +func newVnetUninstallServiceCommand(app *kingpin.Application) vnetCLICommand { + return newPlatformVnetUninstallServiceCommand(app) +} + +func newVnetServiceCommand(app *kingpin.Application) vnetCLICommand { + return newPlatformVnetServiceCommand(app) +} + +// vnetCommandNotSupported implements vnetCLICommand, it is returned when a specific +// command is not implemented for a certain platform or environment. +type vnetCommandNotSupported struct{} + +func (vnetCommandNotSupported) FullCommand() string { + return "" +} +func (vnetCommandNotSupported) run(*CLIConf) error { + panic("vnetCommandNotSupported.run should never be called, this is a bug") +} diff --git a/tool/tsh/common/vnet_daemon_darwin.go b/tool/tsh/common/vnet_daemon_darwin.go index 4154f400774bb..958248097487b 100644 --- a/tool/tsh/common/vnet_daemon_darwin.go +++ b/tool/tsh/common/vnet_daemon_darwin.go @@ -34,6 +34,8 @@ const ( vnetDaemonSubCommand = "vnet-daemon" ) +// vnetDaemonCommand implements the vnet-daemon subcommand to run the VNet MacOS +// daemon. type vnetDaemonCommand struct { *kingpin.CmdClause // Launch daemons added through SMAppService are launched from a static .plist file, hence @@ -41,7 +43,7 @@ type vnetDaemonCommand struct { // Instead, the daemon expects the arguments to be sent over XPC from an unprivileged process. } -func newVnetDaemonCommand(app *kingpin.Application) *vnetDaemonCommand { +func newPlatformVnetDaemonCommand(app *kingpin.Application) *vnetDaemonCommand { return &vnetDaemonCommand{ CmdClause: app.Command(vnetDaemonSubCommand, "Start the VNet daemon").Hidden(), } diff --git a/tool/tsh/common/vnet_darwin.go b/tool/tsh/common/vnet_darwin.go index 213a971f092b7..20c1f1b55d141 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,38 +28,6 @@ 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) - } - - 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 // vnetdaemon build tag. This is typically the case when running tsh in development where it's not // signed and bundled in tsh.app. @@ -83,7 +50,7 @@ type vnetAdminSetupCommand struct { euid int } -func newVnetAdminSetupCommand(app *kingpin.Application) *vnetAdminSetupCommand { +func newPlatformVnetAdminSetupCommand(app *kingpin.Application) *vnetAdminSetupCommand { cmd := &vnetAdminSetupCommand{ CmdClause: app.Command(teleport.VnetAdminSetupSubCommand, "Start the VNet admin subprocess.").Hidden(), } @@ -116,3 +83,18 @@ func (c *vnetAdminSetupCommand) run(cf *CLIConf) error { return trace.Wrap(vnet.RunAdminProcess(cf.Context, config)) } + +// 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{} +} + +// the vnet-service command is only supported on windows. +func newPlatformVnetServiceCommand(app *kingpin.Application) vnetCommandNotSupported { + return vnetCommandNotSupported{} +} diff --git a/tool/tsh/common/vnet_nodaemon.go b/tool/tsh/common/vnet_nodaemon.go index 2e6d516e214f8..d9142729d9f65 100644 --- a/tool/tsh/common/vnet_nodaemon.go +++ b/tool/tsh/common/vnet_nodaemon.go @@ -21,18 +21,9 @@ package common import ( "github.com/alecthomas/kingpin/v2" - "github.com/gravitational/trace" ) -func newVnetDaemonCommand(app *kingpin.Application) vnetDaemonNotSupported { - return vnetDaemonNotSupported{} -} - -type vnetDaemonNotSupported struct{} - -func (vnetDaemonNotSupported) FullCommand() string { - return "" -} -func (vnetDaemonNotSupported) run(*CLIConf) error { - return trace.NotImplemented("tsh was built without support for VNet daemon") +// The vnet-daemon command is only supported with the vnetdaemon tag on darwin. +func newPlatformVnetDaemonCommand(app *kingpin.Application) vnetCommandNotSupported { + return vnetCommandNotSupported{} } diff --git a/tool/tsh/common/vnet_other.go b/tool/tsh/common/vnet_other.go index dc705ee824567..86e0ee764725b 100644 --- a/tool/tsh/common/vnet_other.go +++ b/tool/tsh/common/vnet_other.go @@ -1,6 +1,3 @@ -//go:build !darwin && !windows -// +build !darwin,!windows - // Teleport // Copyright (C) 2024 Gravitational, Inc. // @@ -17,34 +14,30 @@ // 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 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{} -} +// Satisfy unused linter. +var _ = newVnetAppProvider -func newVnetAdminSetupCommand(app *kingpin.Application) vnetNotSupported { - return vnetNotSupported{} +func newPlatformVnetAdminSetupCommand(app *kingpin.Application) vnetCLICommand { + return vnetCommandNotSupported{} } -type vnetNotSupported struct{} - -func (vnetNotSupported) FullCommand() string { - return "" +func newPlatformVnetInstallServiceCommand(app *kingpin.Application) vnetCLICommand { + return vnetCommandNotSupported{} } -func (vnetNotSupported) run(*CLIConf) error { - return trace.Wrap(vnet.ErrVnetNotImplemented) + +func newPlatformVnetUninstallServiceCommand(app *kingpin.Application) vnetCLICommand { + return vnetCommandNotSupported{} } -var ( - // Satisfy unused linter. - _ = (*vnetAppProvider)(nil) - _ = newVnetAppProvider -) +func newPlatformVnetServiceCommand(app *kingpin.Application) vnetCLICommand { + return vnetCommandNotSupported{} +} diff --git a/tool/tsh/common/vnet_windows.go b/tool/tsh/common/vnet_windows.go index 59d90972f2971..67aa8722fd2dd 100644 --- a/tool/tsh/common/vnet_windows.go +++ b/tool/tsh/common/vnet_windows.go @@ -17,95 +17,71 @@ package common import ( - "fmt" - "os" - "github.com/alecthomas/kingpin/v2" "github.com/gravitational/trace" - - "github.com/gravitational/teleport" - "github.com/gravitational/teleport/api/types" - "github.com/gravitational/teleport/lib/vnet" - "github.com/gravitational/teleport/lib/vnet/daemon" + "golang.org/x/sys/windows/svc" ) -type vnetCommand struct { +var windowsServiceNotImplemented = &trace.NotImplementedError{Message: "VNet Windows service is not yet implemented"} + +type vnetInstallServiceCommand 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.").Hidden(), +func newPlatformVnetInstallServiceCommand(app *kingpin.Application) *vnetInstallServiceCommand { + cmd := &vnetInstallServiceCommand{ + CmdClause: app.Command("vnet-install-service", "Install the VNet Windows service.").Hidden(), } 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 (c *vnetInstallServiceCommand) run(cf *CLIConf) error { + // TODO(nklaassen): implement VNet Windows service installation. + return trace.Wrap(windowsServiceNotImplemented) +} - go func() { - <-cf.Context.Done() - processManager.Close() - }() +type vnetUninstallServiceCommand struct { + *kingpin.CmdClause +} - fmt.Println("VNet is ready.") +func newPlatformVnetUninstallServiceCommand(app *kingpin.Application) *vnetUninstallServiceCommand { + cmd := &vnetUninstallServiceCommand{ + CmdClause: app.Command("vnet-uninstall-service", "Uninstall (delete) the VNet Windows service.").Hidden(), + } + return cmd +} - return trace.Wrap(processManager.Wait()) +func (c *vnetUninstallServiceCommand) run(cf *CLIConf) error { + // TODO(nklaassen): implement VNet Windows service uninstallation. + return trace.Wrap(windowsServiceNotImplemented) } -// 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 { +// vnetServiceCommand is the command that runs the Windows service. +type vnetServiceCommand 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 newPlatformVnetServiceCommand(app *kingpin.Application) *vnetServiceCommand { + cmd := &vnetServiceCommand{ + CmdClause: app.Command("vnet-service", "Start the VNet 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 *vnetServiceCommand) run(_ *CLIConf) error { + if !isWindowsService() { + return trace.Errorf("not running as a Windows service, cannot run vnet-service command") } + // TODO(nklaassen): implement VNet Windows service. + return trace.Wrap(windowsServiceNotImplemented) +} - 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, - }, - } +func isWindowsService() bool { + isSvc, err := svc.IsWindowsService() + return err == nil && isSvc +} - return trace.Wrap(vnet.RunAdminProcess(cf.Context, config)) +// the admin-setup command is only supported on darwin. +func newPlatformVnetAdminSetupCommand(*kingpin.Application) vnetCommandNotSupported { + return vnetCommandNotSupported{} }