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{}
}