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/putty_config_windows.go b/tool/tsh/common/putty_config_windows.go
index d1ea9eea0f419..9efc50b7665c6 100644
--- a/tool/tsh/common/putty_config_windows.go
+++ b/tool/tsh/common/putty_config_windows.go
@@ -389,7 +389,7 @@ func onPuttyConfig(cf *CLIConf) error {
if err := addPuTTYSession(proxyHost, hostname, port, login, ppkFilePath, certificateFilePath, localCommandString, cf.LeafClusterName); err != nil {
logger.ErrorContext(cf.Context, "Failed to add PuTTY session",
"user_host", userHostString,
- "error",err,
+ "error", err,
)
return trace.Wrap(err)
}
diff --git a/tool/tsh/common/tsh.go b/tool/tsh/common/tsh.go
index 44f6b42eb5311..32ddd00258412 100644
--- a/tool/tsh/common/tsh.go
+++ b/tool/tsh/common/tsh.go
@@ -1262,8 +1262,6 @@ func Run(ctx context.Context, args []string, opts ...CliOption) error {
vnetCommand := newVnetCommand(app)
vnetAdminSetupCommand := newVnetAdminSetupCommand(app)
vnetDaemonCommand := newVnetDaemonCommand(app)
- vnetInstallServiceCommand := newVnetInstallServiceCommand(app)
- vnetUninstallServiceCommand := newVnetUninstallServiceCommand(app)
vnetServiceCommand := newVnetServiceCommand(app)
gitCmd := newGitCommands(app)
@@ -1648,10 +1646,6 @@ func Run(ctx context.Context, args []string, opts ...CliOption) error {
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():
diff --git a/tool/tsh/common/vnet.go b/tool/tsh/common/vnet.go
index 8bcd80a57590f..d85f3536f94e5 100644
--- a/tool/tsh/common/vnet.go
+++ b/tool/tsh/common/vnet.go
@@ -51,7 +51,7 @@ func (c *vnetCommand) run(cf *CLIConf) error {
if err != nil {
return trace.Wrap(err)
}
- processManager, err := vnet.Run(cf.Context, &vnet.RunConfig{AppProvider: appProvider})
+ processManager, err := vnet.RunUserProcess(cf.Context, &vnet.UserProcessConfig{AppProvider: appProvider})
if err != nil {
return trace.Wrap(err)
}
@@ -68,14 +68,6 @@ 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)
}
diff --git a/tool/tsh/common/vnet_darwin.go b/tool/tsh/common/vnet_darwin.go
index 20c1f1b55d141..289efe0b035a1 100644
--- a/tool/tsh/common/vnet_darwin.go
+++ b/tool/tsh/common/vnet_darwin.go
@@ -68,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,
@@ -80,18 +79,7 @@ func (c *vnetAdminSetupCommand) run(cf *CLIConf) error {
Euid: c.euid,
},
}
-
- 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{}
+ return trace.Wrap(vnet.RunDarwinAdminProcess(cf.Context, config))
}
// the vnet-service command is only supported on windows.
diff --git a/tool/tsh/common/vnet_other.go b/tool/tsh/common/vnet_other.go
index 86e0ee764725b..d6ed0d03280c7 100644
--- a/tool/tsh/common/vnet_other.go
+++ b/tool/tsh/common/vnet_other.go
@@ -30,14 +30,6 @@ func newPlatformVnetAdminSetupCommand(app *kingpin.Application) vnetCLICommand {
return vnetCommandNotSupported{}
}
-func newPlatformVnetInstallServiceCommand(app *kingpin.Application) vnetCLICommand {
- return vnetCommandNotSupported{}
-}
-
-func newPlatformVnetUninstallServiceCommand(app *kingpin.Application) vnetCLICommand {
- return vnetCommandNotSupported{}
-}
-
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 67aa8722fd2dd..bb34adeb19e84 100644
--- a/tool/tsh/common/vnet_windows.go
+++ b/tool/tsh/common/vnet_windows.go
@@ -20,41 +20,9 @@ import (
"github.com/alecthomas/kingpin/v2"
"github.com/gravitational/trace"
"golang.org/x/sys/windows/svc"
-)
-
-var windowsServiceNotImplemented = &trace.NotImplementedError{Message: "VNet Windows service is not yet implemented"}
-
-type vnetInstallServiceCommand struct {
- *kingpin.CmdClause
-}
-
-func newPlatformVnetInstallServiceCommand(app *kingpin.Application) *vnetInstallServiceCommand {
- cmd := &vnetInstallServiceCommand{
- CmdClause: app.Command("vnet-install-service", "Install the VNet Windows service.").Hidden(),
- }
- return cmd
-}
-
-func (c *vnetInstallServiceCommand) run(cf *CLIConf) error {
- // TODO(nklaassen): implement VNet Windows service installation.
- return trace.Wrap(windowsServiceNotImplemented)
-}
-type vnetUninstallServiceCommand struct {
- *kingpin.CmdClause
-}
-
-func newPlatformVnetUninstallServiceCommand(app *kingpin.Application) *vnetUninstallServiceCommand {
- cmd := &vnetUninstallServiceCommand{
- CmdClause: app.Command("vnet-uninstall-service", "Uninstall (delete) the VNet Windows service.").Hidden(),
- }
- return cmd
-}
-
-func (c *vnetUninstallServiceCommand) run(cf *CLIConf) error {
- // TODO(nklaassen): implement VNet Windows service uninstallation.
- return trace.Wrap(windowsServiceNotImplemented)
-}
+ "github.com/gravitational/teleport/lib/vnet"
+)
// vnetServiceCommand is the command that runs the Windows service.
type vnetServiceCommand struct {
@@ -63,17 +31,19 @@ type vnetServiceCommand struct {
func newPlatformVnetServiceCommand(app *kingpin.Application) *vnetServiceCommand {
cmd := &vnetServiceCommand{
- CmdClause: app.Command("vnet-service", "Start the VNet service.").Hidden(),
+ CmdClause: app.Command(vnet.ServiceCommand, "Start the VNet service.").Hidden(),
}
return cmd
}
func (c *vnetServiceCommand) run(_ *CLIConf) error {
if !isWindowsService() {
- return trace.Errorf("not running as a Windows service, cannot run vnet-service command")
+ return trace.Errorf("not running as a Windows service, cannot run %s command", vnet.ServiceCommand)
+ }
+ if err := vnet.ServiceMain(); err != nil {
+ return trace.Wrap(err, "running VNet Windows service")
}
- // TODO(nklaassen): implement VNet Windows service.
- return trace.Wrap(windowsServiceNotImplemented)
+ return nil
}
func isWindowsService() bool {