diff --git a/lib/teleterm/vnet/service.go b/lib/teleterm/vnet/service.go index 713198a187558..391f69a4fb48e 100644 --- a/lib/teleterm/vnet/service.go +++ b/lib/teleterm/vnet/service.go @@ -160,7 +160,7 @@ func (s *Service) Start(ctx context.Context, req *api.StartRequest) (*api.StartR } s.clusterConfigCache = vnet.NewClusterConfigCache(s.cfg.Clock) - processManager, err := vnet.Run(ctx, &vnet.RunConfig{ + processManager, err := vnet.RunUserProcess(ctx, &vnet.UserProcessConfig{ AppProvider: appProvider, ClusterConfigCache: s.clusterConfigCache, }) diff --git a/lib/vnet/admin_process.go b/lib/vnet/admin_process_darwin.go similarity index 85% rename from lib/vnet/admin_process.go rename to lib/vnet/admin_process_darwin.go index 4c2411d729763..f9ee788327842 100644 --- a/lib/vnet/admin_process.go +++ b/lib/vnet/admin_process_darwin.go @@ -27,18 +27,17 @@ import ( "github.com/gravitational/teleport/lib/vnet/daemon" ) -// RunAdminProcess must run as root. It creates and sets up a TUN device and passes -// the file descriptor for that device over the unix socket found at config.socketPath. +// RunDarwinAdminProcess must run as root. It creates and sets up a TUN device +// and passes the file descriptor for that device over the unix socket found at +// config.socketPath. // -// It also handles host OS configuration that must run as root, and stays alive to keep the host configuration -// up to date. It will stay running until the socket at config.socketPath is deleted or until encountering an -// unrecoverable error. -// -// OS configuration is updated every [osConfigurationInterval]. During the update, it temporarily -// changes egid and euid of the process to that of the client connecting to the daemon. -func RunAdminProcess(ctx context.Context, config daemon.Config) error { +// It also handles host OS configuration that must run as root, and stays alive +// to keep the host configuration up to date. It will stay running until the +// socket at config.socketPath is deleted, ctx is canceled, or until +// encountering an unrecoverable error. +func RunDarwinAdminProcess(ctx context.Context, config daemon.Config) error { if err := config.CheckAndSetDefaults(); err != nil { - return trace.Wrap(err) + return trace.Wrap(err, "checking daemon process config") } ctx, cancel := context.WithCancel(ctx) @@ -74,7 +73,7 @@ func RunAdminProcess(ctx context.Context, config daemon.Config) error { } // createAndSendTUNDevice creates a virtual network TUN device and sends the open file descriptor on -// [socketPath]. It returns the name of the TUN device or an error. +// socketPath. It returns the name of the TUN device or an error. func createAndSendTUNDevice(ctx context.Context, socketPath string) (string, error) { tun, tunName, err := createTUNDevice(ctx) if err != nil { @@ -107,7 +106,7 @@ func createTUNDevice(ctx context.Context) (tun.Device, string, error) { return dev, name, nil } -// osConfigurationLoop will keep running until [ctx] is canceled or an unrecoverable error is encountered, in +// osConfigurationLoop will keep running until ctx is canceled or an unrecoverable error is encountered, in // order to keep the host OS configuration up to date. func osConfigurationLoop(ctx context.Context, tunName, ipv6Prefix, dnsAddr, homePath string, clientCred daemon.ClientCred) error { osConfigurator, err := newOSConfigurator(tunName, ipv6Prefix, dnsAddr, homePath, clientCred) @@ -128,7 +127,7 @@ func osConfigurationLoop(ctx context.Context, tunName, ipv6Prefix, dnsAddr, home } defer func() { - // Shutting down, deconfigure OS. Pass context.Background because [ctx] has likely been canceled + // Shutting down, deconfigure OS. Pass context.Background because ctx has likely been canceled // already but we still need to clean up. if err := osConfigurator.deconfigureOS(context.Background()); err != nil { log.ErrorContext(ctx, "Error deconfiguring host OS before shutting down.", "error", err) diff --git a/lib/vnet/socket_other.go b/lib/vnet/admin_process_windows.go similarity index 51% rename from lib/vnet/socket_other.go rename to lib/vnet/admin_process_windows.go index 9b9ace5eaafdb..1c30c38eb36d1 100644 --- a/lib/vnet/socket_other.go +++ b/lib/vnet/admin_process_windows.go @@ -14,32 +14,32 @@ // 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 vnet import ( - "os" + "context" "github.com/gravitational/trace" "golang.zx2c4.com/wireguard/tun" ) -func createSocket() (*noSocket, string, error) { - return nil, "", trace.Wrap(ErrVnetNotImplemented) -} - -func sendTUNNameAndFd(socketPath, tunName string, tunFile *os.File) error { - return trace.Wrap(ErrVnetNotImplemented) -} - -func receiveTUNDevice(_ *noSocket) (tun.Device, error) { - return nil, trace.Wrap(ErrVnetNotImplemented) -} - -type noSocket struct{} - -func (_ noSocket) Close() error { - return trace.Wrap(ErrVnetNotImplemented) +// runWindowsAdminProcess must run as administrator. It creates and sets up a TUN +// device, runs the VNet networking stack, and handles OS configuration. It will +// continue to run until [ctx] is canceled or encountering an unrecoverable +// error. +func runWindowsAdminProcess(ctx context.Context) error { + device, err := tun.CreateTUN("TeleportVNet", mtu) + if err != nil { + return trace.Wrap(err, "creating TUN device") + } + defer device.Close() + tunName, err := device.Name() + if err != nil { + return trace.Wrap(err, "getting TUN device name") + } + log.InfoContext(ctx, "Created TUN interface", "tun", tunName) + // TODO(nklaassen): actually run VNet. For now, just stay alive until the + // context is canceled. + <-ctx.Done() + return trace.Wrap(ctx.Err()) } diff --git a/lib/vnet/escalate_daemon_darwin.go b/lib/vnet/escalate_daemon_darwin.go index 935c16afe9793..6e9572fef59b6 100644 --- a/lib/vnet/escalate_daemon_darwin.go +++ b/lib/vnet/escalate_daemon_darwin.go @@ -35,5 +35,5 @@ func execAdminProcess(ctx context.Context, config daemon.Config) error { // DaemonSubcommand runs the VNet daemon process. func DaemonSubcommand(ctx context.Context) error { - return trace.Wrap(daemon.Start(ctx, RunAdminProcess)) + return trace.Wrap(daemon.Start(ctx, RunDarwinAdminProcess)) } diff --git a/lib/vnet/escalate_other.go b/lib/vnet/escalate_other.go deleted file mode 100644 index 76adfdf1a6606..0000000000000 --- a/lib/vnet/escalate_other.go +++ /dev/null @@ -1,40 +0,0 @@ -// 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 . - -//go:build !darwin && !windows -// +build !darwin,!windows - -package vnet - -import ( - "context" - "runtime" - - "github.com/gravitational/trace" - - "github.com/gravitational/teleport/lib/vnet/daemon" -) - -var ( - // ErrVnetNotImplemented is an error indicating that VNet is not implemented on the host OS. - ErrVnetNotImplemented = &trace.NotImplementedError{Message: "VNet is not implemented on " + runtime.GOOS} -) - -// execAdminProcess is called from the normal user process to execute the admin -// subcommand as root. -func execAdminProcess(ctx context.Context, config daemon.Config) error { - return trace.Wrap(ErrVnetNotImplemented) -} diff --git a/lib/vnet/escalate_windows.go b/lib/vnet/escalate_windows.go deleted file mode 100644 index 3b5d4464eefe8..0000000000000 --- a/lib/vnet/escalate_windows.go +++ /dev/null @@ -1,40 +0,0 @@ -// 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 . - -//go:build windows -// +build windows - -package vnet - -import ( - "context" - - "github.com/gravitational/trace" - - "github.com/gravitational/teleport/lib/vnet/daemon" -) - -var ( - // ErrVnetNotImplemented is an error indicating that VNet is not implemented on the host OS. - ErrVnetNotImplemented = &trace.NotImplementedError{Message: "VNet is not implemented on windows"} -) - -// execAdminProcess is called from the normal user process to execute the admin -// subcommand as root. -func execAdminProcess(ctx context.Context, config daemon.Config) error { - // TODO(nklaassen): implement execAdminProcess on windows. - return trace.Wrap(ErrVnetNotImplemented) -} diff --git a/lib/vnet/network_stack.go b/lib/vnet/network_stack.go index 0479564033e19..6c7dbfa2f8563 100644 --- a/lib/vnet/network_stack.go +++ b/lib/vnet/network_stack.go @@ -41,9 +41,12 @@ import ( "gvisor.dev/gvisor/pkg/waiter" "github.com/gravitational/teleport" + logutils "github.com/gravitational/teleport/lib/utils/log" "github.com/gravitational/teleport/lib/vnet/dns" ) +var log = logutils.NewPackageLogger(teleport.ComponentKey, "vnet") + const ( nicID = 1 mtu = 1500 diff --git a/lib/vnet/osconfig.go b/lib/vnet/osconfig.go index 0642ebd0980dd..05ac1dcb3a688 100644 --- a/lib/vnet/osconfig.go +++ b/lib/vnet/osconfig.go @@ -14,6 +14,11 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +// TODO(nklaassen): refactor OS configuration so this file isn't +// platform-specific. +//go:build darwin +// +build darwin + package vnet import ( diff --git a/lib/vnet/osconfig_windows.go b/lib/vnet/osconfig_windows.go deleted file mode 100644 index e1547ea69c108..0000000000000 --- a/lib/vnet/osconfig_windows.go +++ /dev/null @@ -1,36 +0,0 @@ -// 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 . - -//go:build windows -// +build windows - -package vnet - -import ( - "context" - - "github.com/gravitational/trace" -) - -func configureOS(ctx context.Context, cfg *osConfig) error { - // TODO(nklaassen): implement configureOS on Windows. - return trace.Wrap(ErrVnetNotImplemented) -} - -func (c *osConfigurator) doWithDroppedRootPrivileges(ctx context.Context, fn func() error) (err error) { - // TODO(nklaassen): implement doWithDroppedPrivileges on Windows. - return trace.Wrap(ErrVnetNotImplemented) -} diff --git a/lib/vnet/process_manager.go b/lib/vnet/process_manager.go new file mode 100644 index 0000000000000..08c7d35f6028e --- /dev/null +++ b/lib/vnet/process_manager.go @@ -0,0 +1,90 @@ +// 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 vnet + +import ( + "context" + "errors" + "fmt" + "sync" + + "github.com/gravitational/trace" + "golang.org/x/sync/errgroup" +) + +func newProcessManager() (*ProcessManager, context.Context) { + ctx, cancel := context.WithCancel(context.Background()) + g, ctx := errgroup.WithContext(ctx) + pm := &ProcessManager{ + g: g, + cancel: cancel, + closed: make(chan struct{}), + } + pm.closeOnce = sync.OnceFunc(func() { + close(pm.closed) + }) + return pm, ctx +} + +// ProcessManager handles background tasks needed to run VNet. +// Its semantics are similar to an error group with a context, but it cancels the context whenever +// any task returns prematurely, that is, a task exits while the context was not canceled. +type ProcessManager struct { + g *errgroup.Group + cancel context.CancelFunc + closed chan struct{} + closeOnce func() +} + +// AddCriticalBackgroundTask adds a function to the error group. [task] is expected to block until +// the context returned by [newProcessManager] gets canceled. The context gets canceled either by +// calling Close on [ProcessManager] or if any task returns. +func (pm *ProcessManager) AddCriticalBackgroundTask(name string, task func() error) { + pm.g.Go(func() error { + err := task() + if err == nil { + // Make sure to always return an error so that the errgroup context is canceled. + err = fmt.Errorf("critical task %q exited prematurely", name) + } + return trace.Wrap(err) + }) +} + +// Wait blocks and waits for the background tasks to finish, which typically happens when another +// goroutine calls Close on the process manager. +func (pm *ProcessManager) Wait() error { + err := pm.g.Wait() + select { + case <-pm.closed: + // Errors are expected after the process manager has been closed, + // usually due to context cancellation, but other error types may be + // returned. Log unexpected errors at debug level but return nil. + if err != nil && !errors.Is(err, context.Canceled) { + log.DebugContext(context.Background(), "ProcessManager exited with error after being closed", "error", err) + } + return nil + default: + return trace.Wrap(err) + } +} + +// Close stops any active background tasks by canceling the underlying context, +// and waits for all tasks to terminate. +func (pm *ProcessManager) Close() { + pm.closeOnce() + pm.cancel() +} diff --git a/lib/vnet/process_manager_test.go b/lib/vnet/process_manager_test.go index 5309150e35b5e..1e95205a99117 100644 --- a/lib/vnet/process_manager_test.go +++ b/lib/vnet/process_manager_test.go @@ -70,8 +70,6 @@ func TestProcessManager_Close(t *testing.T) { }) pm.Close() - err := pm.Wait() - require.ErrorIs(t, err, context.Canceled) - require.ErrorIs(t, err, context.Cause(pmCtx)) + require.NoError(t, err) } diff --git a/lib/vnet/service_windows.go b/lib/vnet/service_windows.go new file mode 100644 index 0000000000000..1387d6cca4407 --- /dev/null +++ b/lib/vnet/service_windows.go @@ -0,0 +1,189 @@ +// 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" + "log/slog" + "os" + "path/filepath" + "syscall" + "time" + + "github.com/gravitational/trace" + "golang.org/x/sys/windows" + "golang.org/x/sys/windows/svc" + "golang.org/x/sys/windows/svc/mgr" +) + +const ( + ServiceCommand = "vnet-service" + serviceName = "TeleportVNet" + serviceDescription = "This service manages networking and OS configuration for Teleport VNet." + serviceAccessFlags = windows.SERVICE_START | windows.SERVICE_STOP | windows.SERVICE_QUERY_STATUS +) + +// runService is called from the normal user process to run the VNet Windows in +// the background and wait for it to exit. It will terminate the service and +// return immediately if [ctx] is canceled. +func runService(ctx context.Context) error { + service, err := startService(ctx) + if err != nil { + return trace.Wrap(err) + } + defer service.Close() + log.InfoContext(ctx, "Started Windows service", "service", service.Name) + ticker := time.Tick(time.Second) + for { + select { + case <-ctx.Done(): + log.InfoContext(ctx, "Context canceled, stopping Windows service") + if _, err := service.Control(svc.Stop); err != nil { + return trace.Wrap(err, "sending stop request to Windows service %s", service.Name) + } + return nil + case <-ticker: + status, err := service.Query() + if err != nil { + return trace.Wrap(err, "querying admin service") + } + if status.State != svc.Running && status.State != svc.StartPending { + return trace.Errorf("service stopped running prematurely, status: %+v", status) + } + } + } +} + +// startService starts the Windows VNet admin service in the background. +func startService(ctx context.Context) (*mgr.Service, error) { + // Avoid [mgr.Connect] because it requests elevated permissions. + scManager, err := windows.OpenSCManager(nil /*machine*/, nil /*database*/, windows.SC_MANAGER_CONNECT) + if err != nil { + return nil, trace.Wrap(err, "opening Windows service manager") + } + defer windows.CloseServiceHandle(scManager) + serviceNamePtr, err := syscall.UTF16PtrFromString(serviceName) + if err != nil { + return nil, trace.Wrap(err, "converting service name to UTF16") + } + serviceHandle, err := windows.OpenService(scManager, serviceNamePtr, serviceAccessFlags) + if err != nil { + return nil, trace.Wrap(err, "opening Windows service %v", serviceName) + } + service := &mgr.Service{ + Name: serviceName, + Handle: serviceHandle, + } + if err := service.Start(ServiceCommand); err != nil { + return nil, trace.Wrap(err, "starting Windows service %s", serviceName) + } + return service, nil +} + +// ServiceMain runs the Windows VNet admin service. +func ServiceMain() error { + if err := setupServiceLogger(); err != nil { + return trace.Wrap(err, "setting up logger for service") + } + if err := svc.Run(serviceName, &windowsService{}); err != nil { + return trace.Wrap(err, "running Windows service") + } + return nil +} + +// windowsService implements [svc.Handler]. +type windowsService struct{} + +// Execute implements [svc.Handler.Execute], the GoDoc is copied below. +// +// Execute will be called by the package code at the start of the service, and +// the service will exit once Execute completes. Inside Execute you must read +// service change requests from [requests] and act accordingly. You must keep +// service control manager up to date about state of your service by writing +// into [status] as required. args contains service name followed by argument +// strings passed to the service. +// You can provide service exit code in exitCode return parameter, with 0 being +// "no error". You can also indicate if exit code, if any, is service specific +// or not by using svcSpecificEC parameter. +func (s *windowsService) Execute(args []string, requests <-chan svc.ChangeRequest, status chan<- svc.Status) (svcSpecificEC bool, exitCode uint32) { + const cmdsAccepted = svc.AcceptStop // Interrogate is always accepted and there is no const for it. + status <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + errCh := make(chan error) + go func() { errCh <- s.run(ctx, args) }() + +loop: + for { + select { + case request := <-requests: + switch request.Cmd { + case svc.Interrogate: + state := svc.Running + if ctx.Err() != nil { + state = svc.StopPending + } + status <- svc.Status{State: state, Accepts: cmdsAccepted} + case svc.Stop: + slog.InfoContext(ctx, "Received stop command, shutting down service") + cancel() + status <- svc.Status{State: svc.StopPending} + } + case err := <-errCh: + slog.ErrorContext(ctx, "Windows VNet service terminated", "error", err) + if err != nil { + exitCode = 1 + } + break loop + } + } + status <- svc.Status{State: svc.Stopped, Win32ExitCode: exitCode} + return false, exitCode +} + +func (s *windowsService) run(ctx context.Context, args []string) error { + if err := runWindowsAdminProcess(ctx); err != nil { + return trace.Wrap(err, "running admin process") + } + return nil +} + +func setupServiceLogger() error { + logFile, err := serviceLogFile() + if err != nil { + return trace.Wrap(err, "creating log file for service") + } + slog.SetDefault(slog.New(slog.NewTextHandler(logFile, &slog.HandlerOptions{ + Level: slog.LevelDebug, + }))) + return nil +} + +func serviceLogFile() (*os.File, error) { + // TODO(nklaassen): find a better path for Windows service logs. + exePath, err := os.Executable() + if err != nil { + return nil, trace.Wrap(err, "getting current executable path") + } + dir := filepath.Dir(exePath) + logFile, err := os.Create(filepath.Join(dir, "logs.txt")) + if err != nil { + return nil, trace.Wrap(err, "creating log file") + } + return logFile, nil +} diff --git a/lib/vnet/socket_windows.go b/lib/vnet/socket_windows.go deleted file mode 100644 index e76996edd3784..0000000000000 --- a/lib/vnet/socket_windows.go +++ /dev/null @@ -1,45 +0,0 @@ -// 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 vnet - -import ( - "os" - - "github.com/gravitational/trace" - "golang.zx2c4.com/wireguard/tun" -) - -func createSocket() (*noSocket, string, error) { - // TODO(nklaassen): implement createSocket on windows. - return nil, "", trace.Wrap(ErrVnetNotImplemented) -} - -func sendTUNNameAndFd(socketPath, tunName string, tunFile *os.File) error { - // TODO(nklaassen): implement sendTUNNameAndFd on windows. - return trace.Wrap(ErrVnetNotImplemented) -} - -func receiveTUNDevice(_ *noSocket) (tun.Device, error) { - // TODO(nklaassen): receiveTUNDevice on windows. - return nil, trace.Wrap(ErrVnetNotImplemented) -} - -type noSocket struct{} - -func (_ noSocket) Close() error { - return trace.Wrap(ErrVnetNotImplemented) -} diff --git a/lib/vnet/osconfig_other.go b/lib/vnet/unsupported_os.go similarity index 70% rename from lib/vnet/osconfig_other.go rename to lib/vnet/unsupported_os.go index 8fd543024abe3..a807101f88801 100644 --- a/lib/vnet/osconfig_other.go +++ b/lib/vnet/unsupported_os.go @@ -21,14 +21,14 @@ package vnet import ( "context" + "runtime" "github.com/gravitational/trace" ) -func configureOS(ctx context.Context, cfg *osConfig) error { - return trace.Wrap(ErrVnetNotImplemented) -} +// ErrVnetNotImplemented is an error indicating that VNet is not implemented on the host OS. +var ErrVnetNotImplemented = &trace.NotImplementedError{Message: "VNet is not implemented on " + runtime.GOOS} -func (c *osConfigurator) doWithDroppedRootPrivileges(ctx context.Context, fn func() error) (err error) { - return trace.Wrap(ErrVnetNotImplemented) +func runPlatformUserProcess(_ context.Context, _ *UserProcessConfig) (*ProcessManager, error) { + return nil, trace.Wrap(ErrVnetNotImplemented) } diff --git a/lib/vnet/user_process.go b/lib/vnet/user_process.go new file mode 100644 index 0000000000000..820c70504a753 --- /dev/null +++ b/lib/vnet/user_process.go @@ -0,0 +1,66 @@ +// 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" + "os" + + "github.com/gravitational/trace" + + "github.com/gravitational/teleport/api/profile" + "github.com/gravitational/teleport/api/types" +) + +// UserProcessConfig provides the necessary configuration to run VNet. +type UserProcessConfig struct { + // AppProvider is a required field providing an interface implementation for [AppProvider]. + AppProvider AppProvider + // ClusterConfigCache is an optional field providing [ClusterConfigCache]. If empty, a new cache + // will be created. + ClusterConfigCache *ClusterConfigCache + // HomePath is the tsh home used for Teleport clients created by VNet. Resolved using the same + // rules as HomeDir in tsh. + HomePath string +} + +func (c *UserProcessConfig) checkAndSetDefaults() error { + if c.AppProvider == nil { + return trace.BadParameter("missing AppProvider") + } + if c.HomePath == "" { + c.HomePath = profile.FullProfilePath(os.Getenv(types.HomeEnvVar)) + } + return nil +} + +// RunUserProcess is called by all VNet client applications (tsh, Connect) to +// start and run all VNet tasks. It returns a [ProcessManager] which controls +// the lifecycle of all tasks and background processes. +// +// ctx is used for setup steps that happen before RunUserProcess passes control +// to the process manager. Canceling ctx after RunUserProcess returns will _not_ +// cancel the background tasks. If [RunUserProcess] returns without error, the +// caller is expected to call Close on the process manager to clean up any +// resources, terminate all processes, and remove any OS configuration used for +// actively running VNet. +func RunUserProcess(ctx context.Context, cfg *UserProcessConfig) (pm *ProcessManager, err error) { + if err := cfg.checkAndSetDefaults(); err != nil { + return nil, trace.Wrap(err) + } + return runPlatformUserProcess(ctx, cfg) +} diff --git a/lib/vnet/run.go b/lib/vnet/user_process_darwin.go similarity index 53% rename from lib/vnet/run.go rename to lib/vnet/user_process_darwin.go index 6d7782e714438..a1b07da1b38ff 100644 --- a/lib/vnet/run.go +++ b/lib/vnet/user_process_darwin.go @@ -19,62 +19,25 @@ package vnet import ( "context" "errors" - "fmt" - "os" "time" "github.com/gravitational/trace" - "golang.org/x/sync/errgroup" "golang.zx2c4.com/wireguard/tun" - "github.com/gravitational/teleport" - "github.com/gravitational/teleport/api/profile" - "github.com/gravitational/teleport/api/types" - logutils "github.com/gravitational/teleport/lib/utils/log" "github.com/gravitational/teleport/lib/vnet/daemon" ) -var log = logutils.NewPackageLogger(teleport.ComponentKey, "vnet") - -// RunConfig provides the necessary configuration to run VNet. -type RunConfig struct { - // AppProvider is a required field providing an interface implementation for [AppProvider]. - AppProvider AppProvider - // ClusterConfigCache is an optional field providing [ClusterConfigCache]. If empty, a new cache - // will be created. - ClusterConfigCache *ClusterConfigCache - // HomePath is the tsh home used for Teleport clients created by VNet. Resolved using the same - // rules as HomeDir in tsh. - HomePath string -} - -func (c *RunConfig) CheckAndSetDefaults() error { - if c.AppProvider == nil { - return trace.BadParameter("missing AppProvider") - } - - if c.HomePath == "" { - c.HomePath = profile.FullProfilePath(os.Getenv(types.HomeEnvVar)) - } - - return nil -} - -// Run creates a network stack for VNet and runs it in the background. To do -// this, it also needs to launch an admin process in the background. It returns -// a [ProcessManager] which controls the lifecycle of both background tasks. -// -// The caller is expected to call Close on the process manager to close the -// network stack, clean up any resources used by it and terminate the admin -// process. -// -// ctx is used to wait for setup steps that happen before Run hands out the -// control to the process manager. If ctx gets canceled during Run, the process -// manager gets closed along with its background tasks. -func Run(ctx context.Context, config *RunConfig) (*ProcessManager, error) { - if err := config.CheckAndSetDefaults(); err != nil { - return nil, trace.Wrap(err) - } +// runPlatformUserProcess creates a network stack for VNet and runs it in the +// background. To do this, it also needs to launch an admin process in the +// background. It returns a [ProcessManager] which controls the lifecycle of +// both background tasks. +func runPlatformUserProcess(ctx context.Context, config *UserProcessConfig) (pm *ProcessManager, err error) { + // Make sure to close the process manager if returning a non-nil error. + defer func() { + if pm != nil && err != nil { + pm.Close() + } + }() ipv6Prefix, err := NewIPv6Prefix() if err != nil { @@ -82,21 +45,14 @@ func Run(ctx context.Context, config *RunConfig) (*ProcessManager, error) { } dnsIPv6 := ipv6WithSuffix(ipv6Prefix, []byte{2}) - pm, processCtx := newProcessManager() - success := false - defer func() { - if !success { - // Closes the socket and background tasks. - pm.Close() - } - }() - // Create the socket that's used to receive the TUN device from the admin process. socket, socketPath, err := createSocket() if err != nil { return nil, trace.Wrap(err) } log.DebugContext(ctx, "Created unix socket for admin process", "socket", socketPath) + + pm, processCtx := newProcessManager() pm.AddCriticalBackgroundTask("socket closer", func() error { // Keep the socket open until the process context is canceled. // Closing the socket signals the admin process to terminate. @@ -170,49 +126,5 @@ func Run(ctx context.Context, config *RunConfig) (*ProcessManager, error) { return trace.Wrap(ns.run(processCtx)) }) - success = true return pm, nil } - -func newProcessManager() (*ProcessManager, context.Context) { - ctx, cancel := context.WithCancel(context.Background()) - g, ctx := errgroup.WithContext(ctx) - - return &ProcessManager{ - g: g, - cancel: cancel, - }, ctx -} - -// ProcessManager handles background tasks needed to run VNet. -// Its semantics are similar to an error group with a context, but it cancels the context whenever -// any task returns prematurely, that is, a task exits while the context was not canceled. -type ProcessManager struct { - g *errgroup.Group - cancel context.CancelFunc -} - -// AddCriticalBackgroundTask adds a function to the error group. [task] is expected to block until -// the context returned by [newProcessManager] gets canceled. The context gets canceled either by -// calling Close on [ProcessManager] or if any task returns. -func (pm *ProcessManager) AddCriticalBackgroundTask(name string, task func() error) { - pm.g.Go(func() error { - err := task() - if err == nil { - // Make sure to always return an error so that the errgroup context is canceled. - err = fmt.Errorf("critical task %q exited prematurely", name) - } - return trace.Wrap(err) - }) -} - -// Wait blocks and waits for the background tasks to finish, which typically happens when another -// goroutine calls Close on the process manager. -func (pm *ProcessManager) Wait() error { - return trace.Wrap(pm.g.Wait()) -} - -// Close stops any active background tasks by canceling the underlying context. -func (pm *ProcessManager) Close() { - pm.cancel() -} diff --git a/lib/vnet/user_process_windows.go b/lib/vnet/user_process_windows.go new file mode 100644 index 0000000000000..9fccd0bb528b3 --- /dev/null +++ b/lib/vnet/user_process_windows.go @@ -0,0 +1,44 @@ +// 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 vnet + +import ( + "context" + + "github.com/gravitational/trace" +) + +// runPlatformUserProcess launches a Windows service in the background that will +// handle all networking and OS configuration. The user process exposes a gRPC +// interface that the admin process uses to query application names and get user +// certificates for apps. It returns a [ProcessManager] which controls the +// lifecycle of both the user and admin processes. +func runPlatformUserProcess(ctx context.Context, config *UserProcessConfig) (pm *ProcessManager, err error) { + // Make sure to close the process manager if returning a non-nil error. + defer func() { + if pm != nil && err != nil { + pm.Close() + } + }() + + pm, processCtx := newProcessManager() + pm.AddCriticalBackgroundTask("VNet Windows service", func() error { + return trace.Wrap(runService(processCtx), "running VNet Windows service in the background") + }) + // TODO(nklaassen): run user process gRPC service. + return pm, nil +} diff --git a/tool/tsh/common/tsh.go b/tool/tsh/common/tsh.go index 88da7ea84f47e..338ddb17750a6 100644 --- a/tool/tsh/common/tsh.go +++ b/tool/tsh/common/tsh.go @@ -1263,9 +1263,10 @@ 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) + vnetServiceCommand := newVnetServiceCommand(app) gitCmd := newGitCommands(app) @@ -1640,12 +1641,14 @@ 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 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..d85f3536f94e5 --- /dev/null +++ b/tool/tsh/common/vnet.go @@ -0,0 +1,84 @@ +// 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.RunUserProcess(cf.Context, &vnet.UserProcessConfig{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 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..289efe0b035a1 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(), } @@ -101,7 +68,6 @@ func (c *vnetAdminSetupCommand) run(cf *CLIConf) error { // 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) } - config := daemon.Config{ SocketPath: c.socketPath, IPv6Prefix: c.ipv6Prefix, @@ -113,6 +79,10 @@ func (c *vnetAdminSetupCommand) run(cf *CLIConf) error { Euid: c.euid, }, } + return trace.Wrap(vnet.RunDarwinAdminProcess(cf.Context, config)) +} - return trace.Wrap(vnet.RunAdminProcess(cf.Context, config)) +// 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..d6ed0d03280c7 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,22 @@ // 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 newPlatformVnetServiceCommand(app *kingpin.Application) vnetCLICommand { + return vnetCommandNotSupported{} } -func (vnetNotSupported) run(*CLIConf) error { - return trace.Wrap(vnet.ErrVnetNotImplemented) -} - -var ( - // Satisfy unused linter. - _ = (*vnetAppProvider)(nil) - _ = newVnetAppProvider -) diff --git a/tool/tsh/common/vnet_windows.go b/tool/tsh/common/vnet_windows.go index 59d90972f2971..bb34adeb19e84 100644 --- a/tool/tsh/common/vnet_windows.go +++ b/tool/tsh/common/vnet_windows.go @@ -17,95 +17,41 @@ package common import ( - "fmt" - "os" - "github.com/alecthomas/kingpin/v2" "github.com/gravitational/trace" + "golang.org/x/sys/windows/svc" - "github.com/gravitational/teleport" - "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/vnet" - "github.com/gravitational/teleport/lib/vnet/daemon" ) -type vnetCommand struct { +// vnetServiceCommand is the command that runs the Windows service. +type vnetServiceCommand 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 newPlatformVnetServiceCommand(app *kingpin.Application) *vnetServiceCommand { + cmd := &vnetServiceCommand{ + CmdClause: app.Command(vnet.ServiceCommand, "Start the VNet service.").Hidden(), } return cmd } -func (c *vnetCommand) run(cf *CLIConf) error { - appProvider, err := newVnetAppProvider(cf) - if err != nil { - return trace.Wrap(err) +func (c *vnetServiceCommand) run(_ *CLIConf) error { + if !isWindowsService() { + return trace.Errorf("not running as a Windows service, cannot run %s command", vnet.ServiceCommand) } - - processManager, err := vnet.Run(cf.Context, &vnet.RunConfig{AppProvider: appProvider}) - if err != nil { - return trace.Wrap(err) + if err := vnet.ServiceMain(); err != nil { + return trace.Wrap(err, "running VNet Windows service") } - - go func() { - <-cf.Context.Done() - processManager.Close() - }() - - fmt.Println("VNet is ready.") - - return trace.Wrap(processManager.Wait()) + return nil } -// 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 { - *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 isWindowsService() bool { + isSvc, err := svc.IsWindowsService() + return err == nil && isSvc } -func newVnetAdminSetupCommand(app *kingpin.Application) *vnetAdminSetupCommand { - cmd := &vnetAdminSetupCommand{ - CmdClause: app.Command(teleport.VnetAdminSetupSubCommand, "Start the VNet admin subprocess.").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) - } - - 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, - }, - } - - return trace.Wrap(vnet.RunAdminProcess(cf.Context, config)) +// the admin-setup command is only supported on darwin. +func newPlatformVnetAdminSetupCommand(*kingpin.Application) vnetCommandNotSupported { + return vnetCommandNotSupported{} }