From 4e228f729f8ccad5428938665d3145a844a6bb0a Mon Sep 17 00:00:00 2001 From: Nic Klaassen Date: Thu, 9 Jan 2025 18:02:53 -0800 Subject: [PATCH 1/5] [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. --- tool/tsh/common/tsh.go | 27 ++++--- tool/tsh/common/vnet.go | 94 +++++++++++++++++++++++ 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 | 39 +++++----- tool/tsh/common/vnet_windows.go | 106 ++++++++++---------------- 7 files changed, 193 insertions(+), 142 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..6b67e4ff1a4f6 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 := newPlatformVnetDaemonCommand(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..15d234130c6da --- /dev/null +++ b/tool/tsh/common/vnet.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 common + +import ( + "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) + } + go func() { + <-cf.Context.Done() + processManager.Close() + }() + fmt.Println("VNet is ready.") + 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..86888f8d763b8 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,34 @@ // 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 commandNotSupported{} } -type vnetNotSupported struct{} +func newPlatformVnetDaemonCommand(app *kingpin.Application) vnetCLICommand { + return commandNotSupported{} +} -func (vnetNotSupported) FullCommand() string { - return "" +func newPlatformVnetInstallServiceCommand(app *kingpin.Application) vnetCLICommand { + return commandNotSupported{} } -func (vnetNotSupported) run(*CLIConf) error { - return trace.Wrap(vnet.ErrVnetNotImplemented) + +func newPlatformVnetUninstallServiceCommand(app *kingpin.Application) vnetCLICommand { + return commandNotSupported{} } -var ( - // Satisfy unused linter. - _ = (*vnetAppProvider)(nil) - _ = newVnetAppProvider -) +func newPlatformVnetServiceCommand(app *kingpin.Application) vnetCLICommand { + return commandNotSupported{} +} 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{} } From 241c8ec02694f7c619e7abd29bf4e53b0a236d99 Mon Sep 17 00:00:00 2001 From: Nic Klaassen Date: Mon, 13 Jan 2025 12:52:01 -0800 Subject: [PATCH 2/5] fix linux build --- tool/tsh/common/vnet_other.go | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/tool/tsh/common/vnet_other.go b/tool/tsh/common/vnet_other.go index 86888f8d763b8..86e0ee764725b 100644 --- a/tool/tsh/common/vnet_other.go +++ b/tool/tsh/common/vnet_other.go @@ -27,21 +27,17 @@ import ( var _ = newVnetAppProvider func newPlatformVnetAdminSetupCommand(app *kingpin.Application) vnetCLICommand { - return commandNotSupported{} -} - -func newPlatformVnetDaemonCommand(app *kingpin.Application) vnetCLICommand { - return commandNotSupported{} + return vnetCommandNotSupported{} } func newPlatformVnetInstallServiceCommand(app *kingpin.Application) vnetCLICommand { - return commandNotSupported{} + return vnetCommandNotSupported{} } func newPlatformVnetUninstallServiceCommand(app *kingpin.Application) vnetCLICommand { - return commandNotSupported{} + return vnetCommandNotSupported{} } func newPlatformVnetServiceCommand(app *kingpin.Application) vnetCLICommand { - return commandNotSupported{} + return vnetCommandNotSupported{} } From 3a055a0b8b78b020621dfc0c1c005218d7ca75dc Mon Sep 17 00:00:00 2001 From: Nic Klaassen Date: Mon, 13 Jan 2025 13:14:57 -0800 Subject: [PATCH 3/5] update copyright year --- tool/tsh/common/vnet.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tool/tsh/common/vnet.go b/tool/tsh/common/vnet.go index 15d234130c6da..19a0c26172789 100644 --- a/tool/tsh/common/vnet.go +++ b/tool/tsh/common/vnet.go @@ -1,5 +1,5 @@ // Teleport -// Copyright (C) 2024 Gravitational, Inc. +// 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 From f2a3c70561a36ca74fc505b752281a3ff7fc9ed5 Mon Sep 17 00:00:00 2001 From: Nic Klaassen Date: Mon, 13 Jan 2025 13:20:05 -0800 Subject: [PATCH 4/5] use context.AfterFunc --- tool/tsh/common/vnet.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tool/tsh/common/vnet.go b/tool/tsh/common/vnet.go index 19a0c26172789..8bcd80a57590f 100644 --- a/tool/tsh/common/vnet.go +++ b/tool/tsh/common/vnet.go @@ -17,6 +17,7 @@ package common import ( + "context" "fmt" "github.com/alecthomas/kingpin/v2" @@ -54,11 +55,8 @@ func (c *vnetCommand) run(cf *CLIConf) error { if err != nil { return trace.Wrap(err) } - go func() { - <-cf.Context.Done() - processManager.Close() - }() fmt.Println("VNet is ready.") + context.AfterFunc(cf.Context, processManager.Close) return trace.Wrap(processManager.Wait()) } From 9648bed29be59769a4c76b37124584431388b9cd Mon Sep 17 00:00:00 2001 From: Nic Klaassen Date: Mon, 13 Jan 2025 13:34:11 -0800 Subject: [PATCH 5/5] fix typo --- tool/tsh/common/tsh.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tool/tsh/common/tsh.go b/tool/tsh/common/tsh.go index 6b67e4ff1a4f6..f9d4a038a9ae6 100644 --- a/tool/tsh/common/tsh.go +++ b/tool/tsh/common/tsh.go @@ -1260,7 +1260,7 @@ func Run(ctx context.Context, args []string, opts ...CliOption) error { vnetCommand := newVnetCommand(app) vnetAdminSetupCommand := newVnetAdminSetupCommand(app) - vnetDaemonCommand := newPlatformVnetDaemonCommand(app) + vnetDaemonCommand := newVnetDaemonCommand(app) vnetInstallServiceCommand := newVnetInstallServiceCommand(app) vnetUninstallServiceCommand := newVnetUninstallServiceCommand(app) vnetServiceCommand := newVnetServiceCommand(app)