From 8a43724c911dc381e6970e4bdf16df1bbec0e9a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Cie=C5=9Blak?= Date: Tue, 17 Dec 2024 16:15:17 +0100 Subject: [PATCH] chore: restructure vnet for windows support (#49921) (#49982) * chore: restructure vnet for windows support In preparation for adding Windows support to VNet, I have restructured the package/file structure a bit in a way that I think makes more sense and narrows the scope of OS-specific code. I also unexported some identifiers that did not need to be exported, in an attempt to make it more clear what is actually called from outside the vnet package. This commit does not make any functional changes except for adding the hidden `tsh vnet` and `tsh vnet-admin-setup` commands on windows that will just return errors. * fix renames in comments * switch reexec to escalate * fix-imports Co-authored-by: Nic Klaassen --- lib/teleterm/vnet/service.go | 2 +- lib/vnet/admin_process.go | 157 +++++++++++++ lib/vnet/app_resolver.go | 54 ++--- ...on_darwin.go => escalate_daemon_darwin.go} | 5 +- lib/vnet/escalate_nodaemon_darwin.go | 113 ++++++++++ .../{setup_other.go => escalate_other.go} | 25 +-- lib/vnet/escalate_windows.go | 40 ++++ lib/vnet/{vnet.go => network_stack.go} | 167 +++++++------- lib/vnet/osconfig_other.go | 4 +- lib/vnet/osconfig_windows.go | 36 +++ ...{setup_test.go => process_manager_test.go} | 0 lib/vnet/{setup.go => run.go} | 212 ++++-------------- .../{setup_darwin.go => socket_darwin.go} | 116 ++-------- lib/vnet/socket_other.go | 45 ++++ lib/vnet/socket_windows.go | 45 ++++ lib/vnet/vnet_test.go | 16 +- .../{vnet_common.go => vnet_app_provider.go} | 5 +- tool/tsh/common/vnet_daemon_darwin.go | 23 +- tool/tsh/common/vnet_darwin.go | 38 ++-- .../tsh/common/vnet_nodaemon.go | 22 +- tool/tsh/common/vnet_nodaemon_darwin.go | 56 ----- tool/tsh/common/vnet_other.go | 8 +- tool/tsh/common/vnet_windows.go | 111 +++++++++ 23 files changed, 794 insertions(+), 506 deletions(-) create mode 100644 lib/vnet/admin_process.go rename lib/vnet/{setup_daemon_darwin.go => escalate_daemon_darwin.go} (82%) create mode 100644 lib/vnet/escalate_nodaemon_darwin.go rename lib/vnet/{setup_other.go => escalate_other.go} (68%) create mode 100644 lib/vnet/escalate_windows.go rename lib/vnet/{vnet.go => network_stack.go} (82%) create mode 100644 lib/vnet/osconfig_windows.go rename lib/vnet/{setup_test.go => process_manager_test.go} (100%) rename lib/vnet/{setup.go => run.go} (51%) rename lib/vnet/{setup_darwin.go => socket_darwin.go} (56%) create mode 100644 lib/vnet/socket_other.go create mode 100644 lib/vnet/socket_windows.go rename tool/tsh/common/{vnet_common.go => vnet_app_provider.go} (97%) rename lib/vnet/setup_nodaemon_darwin.go => tool/tsh/common/vnet_nodaemon.go (69%) delete mode 100644 tool/tsh/common/vnet_nodaemon_darwin.go create mode 100644 tool/tsh/common/vnet_windows.go diff --git a/lib/teleterm/vnet/service.go b/lib/teleterm/vnet/service.go index e8bf7cf3be27f..878bb05c37276 100644 --- a/lib/teleterm/vnet/service.go +++ b/lib/teleterm/vnet/service.go @@ -159,7 +159,7 @@ func (s *Service) Start(ctx context.Context, req *api.StartRequest) (*api.StartR } s.clusterConfigCache = vnet.NewClusterConfigCache(s.cfg.Clock) - processManager, err := vnet.SetupAndRun(ctx, &vnet.SetupAndRunConfig{ + processManager, err := vnet.Run(ctx, &vnet.RunConfig{ AppProvider: appProvider, ClusterConfigCache: s.clusterConfigCache, }) diff --git a/lib/vnet/admin_process.go b/lib/vnet/admin_process.go new file mode 100644 index 0000000000000..4c2411d729763 --- /dev/null +++ b/lib/vnet/admin_process.go @@ -0,0 +1,157 @@ +// 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" + "os" + "time" + + "github.com/gravitational/trace" + "golang.zx2c4.com/wireguard/tun" + + "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. +// +// 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 { + if err := config.CheckAndSetDefaults(); err != nil { + return trace.Wrap(err) + } + + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + tunName, err := createAndSendTUNDevice(ctx, config.SocketPath) + if err != nil { + return trace.Wrap(err) + } + + errCh := make(chan error) + go func() { + errCh <- trace.Wrap(osConfigurationLoop(ctx, tunName, config.IPv6Prefix, config.DNSAddr, config.HomePath, config.ClientCred)) + }() + + // Stay alive until we get an error on errCh, indicating that the osConfig loop exited. + // If the socket is deleted, indicating that the unprivileged process exited, cancel the context + // and then wait for the osConfig loop to exit and send an err on errCh. + ticker := time.NewTicker(daemon.CheckUnprivilegedProcessInterval) + defer ticker.Stop() + for { + select { + case <-ticker.C: + if _, err := os.Stat(config.SocketPath); err != nil { + log.DebugContext(ctx, "failed to stat socket path, assuming parent exited") + cancel() + return trace.Wrap(<-errCh) + } + case err := <-errCh: + return trace.Wrap(err) + } + } +} + +// 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. +func createAndSendTUNDevice(ctx context.Context, socketPath string) (string, error) { + tun, tunName, err := createTUNDevice(ctx) + if err != nil { + return "", trace.Wrap(err, "creating TUN device") + } + + defer func() { + // We can safely close the TUN device in the admin process after it has been sent on the socket. + if err := tun.Close(); err != nil { + log.WarnContext(ctx, "Failed to close TUN device.", "error", trace.Wrap(err)) + } + }() + + if err := sendTUNNameAndFd(socketPath, tunName, tun.File()); err != nil { + return "", trace.Wrap(err, "sending TUN over socket") + } + return tunName, nil +} + +func createTUNDevice(ctx context.Context) (tun.Device, string, error) { + log.DebugContext(ctx, "Creating TUN device.") + dev, err := tun.CreateTUN("utun", mtu) + if err != nil { + return nil, "", trace.Wrap(err, "creating TUN device") + } + name, err := dev.Name() + if err != nil { + return nil, "", trace.Wrap(err, "getting TUN device name") + } + return dev, name, nil +} + +// 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) + if err != nil { + return trace.Wrap(err, "creating OS configurator") + } + defer func() { + if err := osConfigurator.close(); err != nil { + log.ErrorContext(ctx, "Error while closing OS configurator", "error", err) + } + }() + + // Clean up any stale configuration left by a previous VNet instance that may have failed to clean up. + // This is necessary in case any stale /etc/resolver/ entries are still present, we need to + // be able to reach the proxy in order to fetch the vnet_config. + if err := osConfigurator.deconfigureOS(ctx); err != nil { + return trace.Wrap(err, "cleaning up OS configuration on startup") + } + + defer func() { + // 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) + } + }() + + if err := osConfigurator.updateOSConfiguration(ctx); err != nil { + return trace.Wrap(err, "applying initial OS configuration") + } + + // Re-configure the host OS every 10 seconds. This will pick up any newly logged-in clusters by + // reading profiles from TELEPORT_HOME. + const osConfigurationInterval = 10 * time.Second + ticker := time.NewTicker(osConfigurationInterval) + defer ticker.Stop() + for { + select { + case <-ticker.C: + if err := osConfigurator.updateOSConfiguration(ctx); err != nil { + return trace.Wrap(err, "updating OS configuration") + } + case <-ctx.Done(): + return ctx.Err() + } + } +} diff --git a/lib/vnet/app_resolver.go b/lib/vnet/app_resolver.go index d2fd0b3bc9d9a..3f87c42ec74d7 100644 --- a/lib/vnet/app_resolver.go +++ b/lib/vnet/app_resolver.go @@ -91,24 +91,24 @@ type DialOptions struct { InsecureSkipVerify bool } -// TCPAppResolver implements [TCPHandlerResolver] for Teleport TCP apps. -type TCPAppResolver struct { +// tcpAppResolver implements [tcpHandlerResolver] for Teleport TCP apps. +type tcpAppResolver struct { appProvider AppProvider clusterConfigCache *ClusterConfigCache log *slog.Logger clock clockwork.Clock } -// NewTCPAppResolver returns a new *TCPAppResolver which will resolve full-qualified domain names to -// TCPHandlers that will proxy TCP connection to Teleport TCP apps. +// newTCPAppResolver returns a new [*tcpAppResolver] which will resolve full-qualified domain names to +// [tcpHandler]s that will proxy TCP connection to Teleport TCP apps. // // It uses [appProvider] to list and retrieve cluster clients which are expected to be cached to avoid // repeated/unnecessary dials to the cluster. These clients are then used to list TCP apps that should be // handled. // // [appProvider] is also used to get app certificates used to dial the apps. -func NewTCPAppResolver(appProvider AppProvider, opts ...tcpAppResolverOption) (*TCPAppResolver, error) { - r := &TCPAppResolver{ +func newTCPAppResolver(appProvider AppProvider, opts ...tcpAppResolverOption) (*tcpAppResolver, error) { + r := &tcpAppResolver{ appProvider: appProvider, log: log.With(teleport.ComponentKey, "VNet.AppResolver"), } @@ -120,27 +120,27 @@ func NewTCPAppResolver(appProvider AppProvider, opts ...tcpAppResolverOption) (* return r, nil } -type tcpAppResolverOption func(*TCPAppResolver) +type tcpAppResolverOption func(*tcpAppResolver) // withClock is a functional option to override the default clock (for tests). func withClock(clock clockwork.Clock) tcpAppResolverOption { - return func(r *TCPAppResolver) { + return func(r *tcpAppResolver) { r.clock = clock } } // WithClusterConfigCache is a functional option to override the cluster config cache. func WithClusterConfigCache(clusterConfigCache *ClusterConfigCache) tcpAppResolverOption { - return func(r *TCPAppResolver) { + return func(r *tcpAppResolver) { r.clusterConfigCache = clusterConfigCache } } -// ResolveTCPHandler resolves a fully-qualified domain name to a [TCPHandlerSpec] for a Teleport TCP app that should +// resolveTCPHandler resolves a fully-qualified domain name to a [tcpHandlerSpec] for a Teleport TCP app that should // be used to handle all future TCP connections to [fqdn]. -// Avoid using [trace.Wrap] on [ErrNoTCPHandler] to prevent collecting a full stack trace on every unhandled +// Avoid using [trace.Wrap] on [errNoTCPHandler] to prevent collecting a full stack trace on every unhandled // query. -func (r *TCPAppResolver) ResolveTCPHandler(ctx context.Context, fqdn string) (*TCPHandlerSpec, error) { +func (r *tcpAppResolver) resolveTCPHandler(ctx context.Context, fqdn string) (*tcpHandlerSpec, error) { profileNames, err := r.appProvider.ListProfiles() if err != nil { return nil, trace.Wrap(err, "listing profiles") @@ -148,7 +148,7 @@ func (r *TCPAppResolver) ResolveTCPHandler(ctx context.Context, fqdn string) (*T for _, profileName := range profileNames { if fqdn == fullyQualify(profileName) { // This is a query for the proxy address, which we'll never want to handle. - return nil, ErrNoTCPHandler + return nil, errNoTCPHandler } clusterClient, err := r.clusterClientForAppFQDN(ctx, profileName, fqdn) @@ -172,12 +172,12 @@ func (r *TCPAppResolver) ResolveTCPHandler(ctx context.Context, fqdn string) (*T return r.resolveTCPHandlerForCluster(ctx, clusterClient, profileName, leafClusterName, fqdn) } // fqdn did not match any profile, forward the request upstream. - return nil, ErrNoTCPHandler + return nil, errNoTCPHandler } var errNoMatch = errors.New("cluster does not match queried FQDN") -func (r *TCPAppResolver) clusterClientForAppFQDN(ctx context.Context, profileName, fqdn string) (ClusterClient, error) { +func (r *tcpAppResolver) clusterClientForAppFQDN(ctx context.Context, profileName, fqdn string) (ClusterClient, error) { rootClient, err := r.appProvider.GetCachedClient(ctx, profileName, "") if err != nil { r.log.ErrorContext(ctx, "Failed to get root cluster client, apps in this cluster will not be resolved.", "profile", profileName, "error", err) @@ -236,15 +236,15 @@ func getLeafClusters(ctx context.Context, rootClient ClusterClient) ([]string, e } } -// resolveTCPHandlerForCluster takes a cluster client and resolves [fqdn] to a [TCPHandlerSpec] if a matching +// resolveTCPHandlerForCluster takes a cluster client and resolves [fqdn] to a [tcpHandlerSpec] if a matching // app is found in that cluster. -// Avoid using [trace.Wrap] on [ErrNoTCPHandler] to prevent collecting a full stack trace on every unhandled +// Avoid using [trace.Wrap] on [errNoTCPHandler] to prevent collecting a full stack trace on every unhandled // query. -func (r *TCPAppResolver) resolveTCPHandlerForCluster( +func (r *tcpAppResolver) resolveTCPHandlerForCluster( ctx context.Context, clusterClient ClusterClient, profileName, leafClusterName, fqdn string, -) (*TCPHandlerSpec, error) { +) (*tcpHandlerSpec, error) { log := r.log.With("profile", profileName, "leaf_cluster", leafClusterName, "fqdn", fqdn) // An app public_addr could technically be full-qualified or not, match either way. expr := fmt.Sprintf(`(resource.spec.public_addr == "%s" || resource.spec.public_addr == "%s") && hasPrefix(resource.spec.uri, "tcp://")`, @@ -258,11 +258,11 @@ func (r *TCPAppResolver) resolveTCPHandlerForCluster( // Don't return an unexpected error so we can try to find the app in different clusters or forward the // request upstream. log.InfoContext(ctx, "Failed to list application servers.", "error", err) - return nil, ErrNoTCPHandler + return nil, errNoTCPHandler } if len(resp.Resources) == 0 { // Didn't find any matching app, forward the request upstream. - return nil, ErrNoTCPHandler + return nil, errNoTCPHandler } app := resp.Resources[0].GetApp() appHandler, err := r.newTCPAppHandler(ctx, profileName, leafClusterName, app) @@ -275,9 +275,9 @@ func (r *TCPAppResolver) resolveTCPHandlerForCluster( return nil, trace.Wrap(err) } - return &TCPHandlerSpec{ - IPv4CIDRRange: clusterConfig.IPv4CIDRRange, - TCPHandler: appHandler, + return &tcpHandlerSpec{ + ipv4CIDRRange: clusterConfig.IPv4CIDRRange, + tcpHandler: appHandler, }, nil } @@ -293,7 +293,7 @@ type tcpAppHandler struct { mu sync.Mutex } -func (r *TCPAppResolver) newTCPAppHandler( +func (r *tcpAppResolver) newTCPAppHandler( ctx context.Context, profileName string, leafClusterName string, @@ -391,9 +391,9 @@ func (h *tcpAppHandler) getOrInitializeLocalProxy(ctx context.Context, localPort return newLP, nil } -// HandleTCPConnector handles an incoming TCP connection from VNet by passing it to the local alpn proxy, +// handleTCPConnector handles an incoming TCP connection from VNet by passing it to the local alpn proxy, // which is set up with middleware to automatically handler certificate renewal and re-logins. -func (h *tcpAppHandler) HandleTCPConnector(ctx context.Context, localPort uint16, connector func() (net.Conn, error)) error { +func (h *tcpAppHandler) handleTCPConnector(ctx context.Context, localPort uint16, connector func() (net.Conn, error)) error { lp, err := h.getOrInitializeLocalProxy(ctx, localPort) if err != nil { return trace.Wrap(err) diff --git a/lib/vnet/setup_daemon_darwin.go b/lib/vnet/escalate_daemon_darwin.go similarity index 82% rename from lib/vnet/setup_daemon_darwin.go rename to lib/vnet/escalate_daemon_darwin.go index dd6676b02b850..935c16afe9793 100644 --- a/lib/vnet/setup_daemon_darwin.go +++ b/lib/vnet/escalate_daemon_darwin.go @@ -27,10 +27,13 @@ import ( "github.com/gravitational/teleport/lib/vnet/daemon" ) +// execAdminProcess is called from the normal user process to register and call +// the daemon process which runs as root. func execAdminProcess(ctx context.Context, config daemon.Config) error { return trace.Wrap(daemon.RegisterAndCall(ctx, config)) } +// DaemonSubcommand runs the VNet daemon process. func DaemonSubcommand(ctx context.Context) error { - return trace.Wrap(daemon.Start(ctx, AdminSetup)) + return trace.Wrap(daemon.Start(ctx, RunAdminProcess)) } diff --git a/lib/vnet/escalate_nodaemon_darwin.go b/lib/vnet/escalate_nodaemon_darwin.go new file mode 100644 index 0000000000000..c26c01ae6a58a --- /dev/null +++ b/lib/vnet/escalate_nodaemon_darwin.go @@ -0,0 +1,113 @@ +// 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 !vnetdaemon +// +build !vnetdaemon + +package vnet + +import ( + "context" + "errors" + "fmt" + "os" + "os/exec" + "strings" + + "github.com/gravitational/trace" + + "github.com/gravitational/teleport" + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/vnet/daemon" +) + +// execAdminProcess is called from the normal user process to execute +// "tsh vnet-admin-setup" as root via an osascript wrapper. +func execAdminProcess(ctx context.Context, config daemon.Config) error { + executableName, err := os.Executable() + if err != nil { + return trace.Wrap(err, "getting executable path") + } + + if homePath := os.Getenv(types.HomeEnvVar); homePath == "" { + // Explicitly set TELEPORT_HOME if not already set. + os.Setenv(types.HomeEnvVar, config.HomePath) + } + + appleScript := fmt.Sprintf(` +set executableName to "%s" +set socketPath to "%s" +set ipv6Prefix to "%s" +set dnsAddr to "%s" +do shell script quoted form of executableName & `+ + `" %s -d --socket " & quoted form of socketPath & `+ + `" --ipv6-prefix " & quoted form of ipv6Prefix & `+ + `" --dns-addr " & quoted form of dnsAddr & `+ + `" --egid %d --euid %d" & `+ + `" >/var/log/vnet.log 2>&1" `+ + `with prompt "Teleport VNet wants to set up a virtual network device." with administrator privileges`, + executableName, config.SocketPath, config.IPv6Prefix, config.DNSAddr, teleport.VnetAdminSetupSubCommand, + os.Getegid(), os.Geteuid()) + + // The context we pass here has effect only on the password prompt being shown. Once osascript spawns the + // privileged process, canceling the context (and thus killing osascript) has no effect on the privileged + // process. + cmd := exec.CommandContext(ctx, "osascript", "-e", appleScript) + var stderr strings.Builder + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + var exitError *exec.ExitError + if errors.As(err, &exitError) { + stderr := stderr.String() + + // When the user closes the prompt for administrator privileges, the -128 error is returned. + // https://developer.apple.com/library/archive/documentation/AppleScript/Conceptual/AppleScriptLangGuide/reference/ASLR_error_codes.html#//apple_ref/doc/uid/TP40000983-CH220-SW2 + if strings.Contains(stderr, "-128") { + return trace.Errorf("password prompt closed by user") + } + + if errors.Is(ctx.Err(), context.Canceled) { + // osascript exiting due to canceled context. + return ctx.Err() + } + + stderrDesc := "" + if stderr != "" { + stderrDesc = fmt.Sprintf(", stderr: %s", stderr) + } + return trace.Wrap(exitError, "osascript exited%s", stderrDesc) + } + + return trace.Wrap(err) + } + + if ctx.Err() == nil { + // The admin subcommand is expected to run until VNet gets stopped (in other words, until ctx + // gets canceled). + // + // If it exits with no error _before_ ctx is canceled, then it most likely means that the socket + // was unexpectedly removed. When the socket gets removed, the admin subcommand assumes that the + // unprivileged process (executing this code here) has quit and thus it should quit as well. But + // we know that it's not the case, so in this scenario we return an error instead. + // + // If we don't return an error here, then other code won't be properly notified about the fact + // that the admin process has quit. + return trace.Errorf("admin subcommand exited prematurely with no error (likely because socket was removed)") + } + + return nil +} diff --git a/lib/vnet/setup_other.go b/lib/vnet/escalate_other.go similarity index 68% rename from lib/vnet/setup_other.go rename to lib/vnet/escalate_other.go index 11916d1bd94a7..76adfdf1a6606 100644 --- a/lib/vnet/setup_other.go +++ b/lib/vnet/escalate_other.go @@ -14,19 +14,16 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//go:build !darwin -// +build !darwin +//go:build !darwin && !windows +// +build !darwin,!windows package vnet import ( "context" - "net" - "os" "runtime" "github.com/gravitational/trace" - "golang.zx2c4.com/wireguard/tun" "github.com/gravitational/teleport/lib/vnet/daemon" ) @@ -36,22 +33,8 @@ var ( ErrVnetNotImplemented = &trace.NotImplementedError{Message: "VNet is not implemented on " + runtime.GOOS} ) -func createUnixSocket() (*net.UnixListener, string, error) { - return nil, "", trace.Wrap(ErrVnetNotImplemented) -} - -func sendTUNNameAndFd(socketPath, tunName string, tunFile *os.File) error { - return trace.Wrap(ErrVnetNotImplemented) -} - -func receiveTUNDevice(socket *net.UnixListener) (tun.Device, error) { - return nil, trace.Wrap(ErrVnetNotImplemented) -} - +// 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) } - -func DaemonSubcommand(ctx context.Context) error { - return trace.Wrap(ErrVnetNotImplemented) -} diff --git a/lib/vnet/escalate_windows.go b/lib/vnet/escalate_windows.go new file mode 100644 index 0000000000000..3b5d4464eefe8 --- /dev/null +++ b/lib/vnet/escalate_windows.go @@ -0,0 +1,40 @@ +// 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/vnet.go b/lib/vnet/network_stack.go similarity index 82% rename from lib/vnet/vnet.go rename to lib/vnet/network_stack.go index fb5b6710ac220..0479564033e19 100644 --- a/lib/vnet/vnet.go +++ b/lib/vnet/network_stack.go @@ -52,81 +52,83 @@ const ( defaultIPv4CIDRRange = "100.64.0.0/10" ) -// Config holds configuration parameters for the VNet. -type Config struct { - // TUNDevice is the OS TUN virtual network interface. - TUNDevice TUNDevice - // IPv6Prefix is the IPv6 ULA prefix to use for all assigned VNet IP addresses. - IPv6Prefix tcpip.Address - // DNSIPv6 is the IPv6 address on which to host the DNS server. It must be under IPv6Prefix. - DNSIPv6 tcpip.Address - // TCPHandlerResolver will be used to resolve all DNS queries that may be valid public addresses for +// networkStackConfig holds configuration parameters for the VNet network stack. +type networkStackConfig struct { + // tunDevice is the OS TUN virtual network interface. + tunDevice tunDevice + // ipv6Prefix is the IPv6 ULA prefix to use for all assigned VNet IP addresses. + ipv6Prefix tcpip.Address + // dnsIPv6 is the IPv6 address on which to host the DNS server. It must be under IPv6Prefix. + dnsIPv6 tcpip.Address + // tcpHandlerResolver will be used to resolve all DNS queries that may be valid public addresses for // Teleport apps. - TCPHandlerResolver TCPHandlerResolver + tcpHandlerResolver tcpHandlerResolver // upstreamNameserverSource, if set, overrides the default OS UpstreamNameserverSource which provides the // IP addresses that unmatched DNS queries should be forwarded to. It is used in tests. upstreamNameserverSource dns.UpstreamNameserverSource } -// CheckAndSetDefaults checks the config and sets defaults. -func (c *Config) CheckAndSetDefaults() error { - if c.TUNDevice == nil { - return trace.BadParameter("TUNdevice is required") +// checkAndSetDefaults checks the config and sets defaults. +func (c *networkStackConfig) checkAndSetDefaults() error { + if c.tunDevice == nil { + return trace.BadParameter("tunDevice is required") } - if c.IPv6Prefix.Len() != 16 || c.IPv6Prefix.AsSlice()[0] != 0xfd { - return trace.BadParameter("IPv6Prefix must be an IPv6 ULA address") + if c.ipv6Prefix.Len() != 16 || c.ipv6Prefix.AsSlice()[0] != 0xfd { + return trace.BadParameter("ipv6Prefix must be an IPv6 ULA address") } - if c.TCPHandlerResolver == nil { - return trace.BadParameter("TCPHandlerResolver is required") + if c.tcpHandlerResolver == nil { + return trace.BadParameter("tcpHandlerResolver is required") } return nil } -// TCPHandlerResolver describes a type that can resolve a fully-qualified domain name to a TCPHandlerSpec that -// defines the CIDR range to assign an IP to that handler from, and a handler for all future connections to -// that IP address. +// tcpHandlerResolver describes a type that can resolve a fully-qualified domain +// name to a [tcpHandlerSpec] that defines the CIDR range to assign an IP to +// that handler from, and a handler for all future connections to that IP +// address. // // Implementations beware - an FQDN always ends with a '.'. -type TCPHandlerResolver interface { - // ResolveTCPHandler decides if [fqdn] should match a TCP handler. +type tcpHandlerResolver interface { + // resolveTCPHandler decides if [fqdn] should match a TCP handler. // - // If [fqdn] matches a Teleport-managed TCP app it must return a TCPHandlerSpec defining the range to + // If [fqdn] matches a Teleport-managed TCP app it must return a + // [tcpHandlerSpec] defining the range to // assign an IP from, and a handler for future connections to any assigned IPs. // // If [fqdn] does not match it must return ErrNoTCPHandler. - ResolveTCPHandler(ctx context.Context, fqdn string) (*TCPHandlerSpec, error) + resolveTCPHandler(ctx context.Context, fqdn string) (*tcpHandlerSpec, error) } -// ErrNoTCPHandler should be returned by [TCPHandlerResolver]s when no handler matches the FQDN. -// Avoid using [trace.Wrap] on ErrNoTCPHandler where possible, this isn't an unexpected error that we would +// errNoTCPHandler should be returned by [tcpHandlerResolver]s when no handler matches the FQDN. +// Avoid using [trace.Wrap] on errNoTCPHandler where possible, this isn't an unexpected error that we would // expect to need to debug and [trace.Wrap] incurs overhead to collect a full stack trace. -var ErrNoTCPHandler = errors.New("no handler for address") - -// TCPHandlerSpec specifies a VNet TCP handler. -type TCPHandlerSpec struct { - // IPv4CIDRRange is the network that any V4 IP address should be assigned to this handler from. - IPv4CIDRRange string - // TCPHandler is the handler for TCP connections. - TCPHandler TCPHandler +var errNoTCPHandler = errors.New("no handler for address") + +// tcpHandlerSpec specifies a VNet TCP handler. +type tcpHandlerSpec struct { + // ipv4CIDRRange is the network that any V4 IP address should be assigned to this handler from. + ipv4CIDRRange string + // tcpHandler is the handler for TCP connections. + tcpHandler tcpHandler } -// TCPHandler defines the behavior for handling TCP connections from VNet. +// tcpHandler defines the behavior for handling TCP connections from VNet. // // Implementations should attempt to dial the target application and return any errors before calling // [connector] to complete the TCP handshake and get the TCP conn. This is so that clients will see that the // TCP connection was refused, instead of seeing a successful TCP dial that is immediately closed. -type TCPHandler interface { - HandleTCPConnector(ctx context.Context, localPort uint16, connector func() (net.Conn, error)) error +type tcpHandler interface { + handleTCPConnector(ctx context.Context, localPort uint16, connector func() (net.Conn, error)) error } -// UDPHandler defines the behavior for handling UDP connections from VNet. -type UDPHandler interface { +// udpHandler defines the behavior for handling UDP connections from VNet. +type udpHandler interface { HandleUDP(context.Context, net.Conn) error } -// TUNDevice abstracts a virtual network TUN device. -type TUNDevice interface { +// tunDevice abstracts a virtual network TUN device. +type tunDevice interface { // Write one or more packets to the device (without any additional headers). // On a successful write it returns the number of packets written. A nonzero // offset can be used to instruct the Device on where to begin writing from @@ -149,14 +151,14 @@ type TUNDevice interface { Close() error } -// NetworkStack holds configuration and state for the VNet. -type NetworkStack struct { +// networkStack implements the TCP and UDP networking stack for VNet. +type networkStack struct { // stack is the gVisor networking stack. stack *stack.Stack // tun is the OS TUN device. Incoming IP/L3 packets will be copied from here to [linkEndpoint], and // outgoing packets from [linkEndpoint] will be written here. - tun TUNDevice + tun tunDevice // linkEndpoint is the gVisor-side endpoint that emulates the OS TUN device. All incoming IP/L3 packets // from the OS TUN device will be injected as inbound packets to this endpoint to be processed by the @@ -170,7 +172,7 @@ type NetworkStack struct { // tcpHandlerResolver resolves app FQDNs to a TCP handler that will be used to handle all future TCP // connections to IP addresses that will be assigned to that FQDN. - tcpHandlerResolver TCPHandlerResolver + tcpHandlerResolver tcpHandlerResolver // resolveHandlerGroup is a [singleflight.Group] that will be used to avoid resolving the same FQDN // multiple times concurrently. Every call to [tcpHandlerResolver.ResolveTCPHandler] will be wrapped by // this. The key will be the FQDN. @@ -198,18 +200,18 @@ type state struct { // lookups based on an IPv6 address can use the 4-byte suffix. // tcpHandlers holds the map of IP addresses to assigned TCP handlers. - tcpHandlers map[ipv4]TCPHandler + tcpHandlers map[ipv4]tcpHandler // appIPs holds the map of app FQDNs to their assigned IP address, it like a reverse map of [tcpHandlers]. appIPs map[string]ipv4 // udpHandlers holds the map of IP addresses to assigned UDP handlers. - udpHandlers map[ipv4]UDPHandler + udpHandlers map[ipv4]udpHandler } func newState() state { return state{ - tcpHandlers: make(map[ipv4]TCPHandler), - udpHandlers: make(map[ipv4]UDPHandler), + tcpHandlers: make(map[ipv4]tcpHandler), + udpHandlers: make(map[ipv4]udpHandler), appIPs: make(map[string]ipv4), } } @@ -217,8 +219,8 @@ func newState() state { // newNetworkStack creates a new VNet network stack with the given configuration and root context. // It takes ownership of [cfg.TUNDevice] and will handle closing it before Run() returns. Call Run() // on the returned network stack to start the VNet. -func newNetworkStack(cfg *Config) (*NetworkStack, error) { - if err := cfg.CheckAndSetDefaults(); err != nil { +func newNetworkStack(cfg *networkStackConfig) (*networkStack, error) { + if err := cfg.checkAndSetDefaults(); err != nil { return nil, trace.Wrap(err) } slog := slog.With(teleport.ComponentKey, "VNet") @@ -232,12 +234,12 @@ func newNetworkStack(cfg *Config) (*NetworkStack, error) { return nil, trace.Wrap(err) } - ns := &NetworkStack{ - tun: cfg.TUNDevice, + ns := &networkStack{ + tun: cfg.tunDevice, stack: stack, linkEndpoint: linkEndpoint, - ipv6Prefix: cfg.IPv6Prefix, - tcpHandlerResolver: cfg.TCPHandlerResolver, + ipv6Prefix: cfg.ipv6Prefix, + tcpHandlerResolver: cfg.tcpHandlerResolver, destroyed: make(chan struct{}), state: newState(), slog: slog, @@ -249,7 +251,7 @@ func newNetworkStack(cfg *Config) (*NetworkStack, error) { udpForwarder := udp.NewForwarder(ns.stack, ns.handleUDP) ns.stack.SetTransportProtocolHandler(udp.ProtocolNumber, udpForwarder.HandlePacket) - if cfg.DNSIPv6 != (tcpip.Address{}) { + if cfg.dnsIPv6 != (tcpip.Address{}) { upstreamNameserverSource := cfg.upstreamNameserverSource if upstreamNameserverSource == nil { upstreamNameserverSource, err = dns.NewOSUpstreamNameserverSource() @@ -261,10 +263,10 @@ func newNetworkStack(cfg *Config) (*NetworkStack, error) { if err != nil { return nil, trace.Wrap(err) } - if err := ns.assignUDPHandler(cfg.DNSIPv6, dnsServer); err != nil { + if err := ns.assignUDPHandler(cfg.dnsIPv6, dnsServer); err != nil { return nil, trace.Wrap(err) } - slog.DebugContext(context.Background(), "Serving DNS on IPv6.", "dns_addr", cfg.DNSIPv6) + slog.DebugContext(context.Background(), "Serving DNS on IPv6.", "dns_addr", cfg.dnsIPv6) } return ns, nil @@ -311,9 +313,10 @@ func installVnetRoutes(stack *stack.Stack) error { return nil } -// Run starts the VNet. It blocks until [ctx] is canceled, at which point it closes the link endpoint, waits -// for all goroutines to terminate, and destroys the networking stack. -func (ns *NetworkStack) Run(ctx context.Context) error { +// run starts the VNet networking stack. It blocks until [ctx] is canceled, at +// which point it closes the link endpoint, waits for all goroutines to +// terminate, and destroys the networking stack. +func (ns *networkStack) run(ctx context.Context) error { ns.slog.InfoContext(ctx, "Running Teleport VNet.", "ipv6_prefix", ns.ipv6Prefix) ctx, cancel := context.WithCancel(ctx) @@ -357,7 +360,7 @@ func (ns *NetworkStack) Run(ctx context.Context) error { return trace.NewAggregateFromChannel(allErrors, context.Background()) } -func (ns *NetworkStack) handleTCP(req *tcp.ForwarderRequest) { +func (ns *networkStack) handleTCP(req *tcp.ForwarderRequest) { // Add 1 to the waitgroup because the networking stack runs this in its own goroutine. ns.wg.Add(1) defer ns.wg.Done() @@ -423,7 +426,7 @@ func (ns *NetworkStack) handleTCP(req *tcp.ForwarderRequest) { return conn, nil } - if err := handler.HandleTCPConnector(ctx, id.LocalPort, connector); err != nil { + if err := handler.handleTCPConnector(ctx, id.LocalPort, connector); err != nil { if errors.Is(err, context.Canceled) { slog.DebugContext(ctx, "TCP connection handler returned early due to canceled context.") } else { @@ -432,7 +435,7 @@ func (ns *NetworkStack) handleTCP(req *tcp.ForwarderRequest) { } } -func (ns *NetworkStack) getTCPHandler(addr tcpip.Address) (TCPHandler, bool) { +func (ns *networkStack) getTCPHandler(addr tcpip.Address) (tcpHandler, bool) { ns.state.mu.RLock() defer ns.state.mu.RUnlock() handler, ok := ns.state.tcpHandlers[ipv4Suffix(addr)] @@ -441,10 +444,10 @@ func (ns *NetworkStack) getTCPHandler(addr tcpip.Address) (TCPHandler, bool) { // assignTCPHandler assigns an IPv4 address to [handlerSpec] from its preferred CIDR range, and returns that // new assigned address. -func (ns *NetworkStack) assignTCPHandler(handlerSpec *TCPHandlerSpec, fqdn string) (ipv4, error) { - _, ipNet, err := net.ParseCIDR(handlerSpec.IPv4CIDRRange) +func (ns *networkStack) assignTCPHandler(handlerSpec *tcpHandlerSpec, fqdn string) (ipv4, error) { + _, ipNet, err := net.ParseCIDR(handlerSpec.ipv4CIDRRange) if err != nil { - return 0, trace.Wrap(err, "parsing CIDR %q", handlerSpec.IPv4CIDRRange) + return 0, trace.Wrap(err, "parsing CIDR %q", handlerSpec.ipv4CIDRRange) } ns.state.mu.Lock() @@ -458,7 +461,7 @@ func (ns *NetworkStack) assignTCPHandler(handlerSpec *TCPHandlerSpec, fqdn strin return 0, trace.Wrap(err, "assigning IP address") } - ns.state.tcpHandlers[ip] = handlerSpec.TCPHandler + ns.state.tcpHandlers[ip] = handlerSpec.tcpHandler ns.state.appIPs[fqdn] = ip if err := ns.addProtocolAddress(tcpip.AddrFrom4(ip.asArray())); err != nil { @@ -471,7 +474,7 @@ func (ns *NetworkStack) assignTCPHandler(handlerSpec *TCPHandlerSpec, fqdn strin return ip, nil } -func (ns *NetworkStack) handleUDP(req *udp.ForwarderRequest) { +func (ns *networkStack) handleUDP(req *udp.ForwarderRequest) { ns.wg.Add(1) go func() { defer ns.wg.Done() @@ -479,7 +482,7 @@ func (ns *NetworkStack) handleUDP(req *udp.ForwarderRequest) { }() } -func (ns *NetworkStack) handleUDPConcurrent(req *udp.ForwarderRequest) { +func (ns *networkStack) handleUDPConcurrent(req *udp.ForwarderRequest) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -529,7 +532,7 @@ func (ns *NetworkStack) handleUDPConcurrent(req *udp.ForwarderRequest) { } } -func (ns *NetworkStack) getUDPHandler(addr tcpip.Address) (UDPHandler, bool) { +func (ns *networkStack) getUDPHandler(addr tcpip.Address) (udpHandler, bool) { ipv4 := ipv4Suffix(addr) ns.state.mu.RLock() defer ns.state.mu.RUnlock() @@ -537,7 +540,7 @@ func (ns *NetworkStack) getUDPHandler(addr tcpip.Address) (UDPHandler, bool) { return handler, ok } -func (ns *NetworkStack) assignUDPHandler(addr tcpip.Address, handler UDPHandler) error { +func (ns *networkStack) assignUDPHandler(addr tcpip.Address, handler udpHandler) error { ipv4 := ipv4Suffix(addr) ns.state.mu.Lock() defer ns.state.mu.Unlock() @@ -552,7 +555,7 @@ func (ns *NetworkStack) assignUDPHandler(addr tcpip.Address, handler UDPHandler) } // ResolveA implements [dns.Resolver.ResolveA]. -func (ns *NetworkStack) ResolveA(ctx context.Context, fqdn string) (dns.Result, error) { +func (ns *networkStack) ResolveA(ctx context.Context, fqdn string) (dns.Result, error) { // Do the actual resolution within a [singleflight.Group] keyed by [fqdn] to avoid concurrent requests to // resolve an FQDN and then assign an address to it. resultAny, err, _ := ns.resolveHandlerGroup.Do(fqdn, func() (any, error) { @@ -564,9 +567,9 @@ func (ns *NetworkStack) ResolveA(ctx context.Context, fqdn string) (dns.Result, } // If fqdn is a Teleport-managed app, create a new handler for it. - handlerSpec, err := ns.tcpHandlerResolver.ResolveTCPHandler(ctx, fqdn) + handlerSpec, err := ns.tcpHandlerResolver.resolveTCPHandler(ctx, fqdn) if err != nil { - if errors.Is(err, ErrNoTCPHandler) { + if errors.Is(err, errNoTCPHandler) { // Did not find any known app, forward the DNS request upstream. return dns.Result{}, nil } @@ -591,7 +594,7 @@ func (ns *NetworkStack) ResolveA(ctx context.Context, fqdn string) (dns.Result, } // ResolveAAAA implements [dns.Resolver.ResolveAAAA]. -func (ns *NetworkStack) ResolveAAAA(ctx context.Context, fqdn string) (dns.Result, error) { +func (ns *networkStack) ResolveAAAA(ctx context.Context, fqdn string) (dns.Result, error) { result, err := ns.ResolveA(ctx, fqdn) if err != nil { return dns.Result{}, trace.Wrap(err) @@ -603,14 +606,14 @@ func (ns *NetworkStack) ResolveAAAA(ctx context.Context, fqdn string) (dns.Resul return result, nil } -func (ns *NetworkStack) appIPv4(fqdn string) (ipv4, bool) { +func (ns *networkStack) appIPv4(fqdn string) (ipv4, bool) { ns.state.mu.RLock() defer ns.state.mu.RUnlock() ipv4, ok := ns.state.appIPs[fqdn] return ipv4, ok } -func forwardBetweenTunAndNetstack(ctx context.Context, tun TUNDevice, linkEndpoint *channel.Endpoint) error { +func forwardBetweenTunAndNetstack(ctx context.Context, tun tunDevice, linkEndpoint *channel.Endpoint) error { slog.DebugContext(ctx, "Forwarding IP packets between OS and VNet.") g, ctx := errgroup.WithContext(ctx) g.Go(func() error { return forwardNetstackToTUN(ctx, linkEndpoint, tun) }) @@ -618,7 +621,7 @@ func forwardBetweenTunAndNetstack(ctx context.Context, tun TUNDevice, linkEndpoi return trace.Wrap(g.Wait()) } -func forwardNetstackToTUN(ctx context.Context, linkEndpoint *channel.Endpoint, tun TUNDevice) error { +func forwardNetstackToTUN(ctx context.Context, linkEndpoint *channel.Endpoint, tun tunDevice) error { bufs := [][]byte{make([]byte, device.MessageTransportHeaderSize+mtu)} for { packet := linkEndpoint.ReadContext(ctx) @@ -641,7 +644,7 @@ func forwardNetstackToTUN(ctx context.Context, linkEndpoint *channel.Endpoint, t // forwardTUNtoNetstack does not abort on ctx being canceled, but it does check the ctx error before // returning os.ErrClosed from tun.Read. -func forwardTUNtoNetstack(ctx context.Context, tun TUNDevice, linkEndpoint *channel.Endpoint) error { +func forwardTUNtoNetstack(ctx context.Context, tun tunDevice, linkEndpoint *channel.Endpoint) error { const readOffset = device.MessageTransportHeaderSize bufs := make([][]byte, tun.BatchSize()) for i := range bufs { @@ -671,7 +674,7 @@ func forwardTUNtoNetstack(ctx context.Context, tun TUNDevice, linkEndpoint *chan } } -func (ns *NetworkStack) addProtocolAddress(localAddress tcpip.Address) error { +func (ns *networkStack) addProtocolAddress(localAddress tcpip.Address) error { protocolAddress, err := protocolAddress(localAddress) if err != nil { return trace.Wrap(err) diff --git a/lib/vnet/osconfig_other.go b/lib/vnet/osconfig_other.go index 22780f8bc5f11..8fd543024abe3 100644 --- a/lib/vnet/osconfig_other.go +++ b/lib/vnet/osconfig_other.go @@ -14,8 +14,8 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//go:build !darwin -// +build !darwin +//go:build !darwin && !windows +// +build !darwin,!windows package vnet diff --git a/lib/vnet/osconfig_windows.go b/lib/vnet/osconfig_windows.go new file mode 100644 index 0000000000000..e1547ea69c108 --- /dev/null +++ b/lib/vnet/osconfig_windows.go @@ -0,0 +1,36 @@ +// 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/setup_test.go b/lib/vnet/process_manager_test.go similarity index 100% rename from lib/vnet/setup_test.go rename to lib/vnet/process_manager_test.go diff --git a/lib/vnet/setup.go b/lib/vnet/run.go similarity index 51% rename from lib/vnet/setup.go rename to lib/vnet/run.go index 446fa5d1022c6..6d7782e714438 100644 --- a/lib/vnet/setup.go +++ b/lib/vnet/run.go @@ -36,17 +36,42 @@ import ( var log = logutils.NewPackageLogger(teleport.ComponentKey, "vnet") -// SetupAndRun 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 [ProcessManager] which controls -// the lifecycle of both background tasks. +// 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. +// 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 SetupAndRun hands out the control to the -// process manager. If ctx gets canceled during SetupAndRun, the process manager gets closed along -// with its background tasks. -func SetupAndRun(ctx context.Context, config *SetupAndRunConfig) (*ProcessManager, error) { +// 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) } @@ -67,7 +92,7 @@ func SetupAndRun(ctx context.Context, config *SetupAndRunConfig) (*ProcessManage }() // Create the socket that's used to receive the TUN device from the admin process. - socket, socketPath, err := createUnixSocket() + socket, socketPath, err := createSocket() if err != nil { return nil, trace.Wrap(err) } @@ -125,54 +150,30 @@ func SetupAndRun(ctx context.Context, config *SetupAndRunConfig) (*ProcessManage } } - appResolver, err := NewTCPAppResolver(config.AppProvider, + appResolver, err := newTCPAppResolver(config.AppProvider, WithClusterConfigCache(config.ClusterConfigCache)) if err != nil { return nil, trace.Wrap(err) } - ns, err := newNetworkStack(&Config{ - TUNDevice: tun, - IPv6Prefix: ipv6Prefix, - DNSIPv6: dnsIPv6, - TCPHandlerResolver: appResolver, + ns, err := newNetworkStack(&networkStackConfig{ + tunDevice: tun, + ipv6Prefix: ipv6Prefix, + dnsIPv6: dnsIPv6, + tcpHandlerResolver: appResolver, }) if err != nil { return nil, trace.Wrap(err) } pm.AddCriticalBackgroundTask("network stack", func() error { - return trace.Wrap(ns.Run(processCtx)) + return trace.Wrap(ns.run(processCtx)) }) success = true return pm, nil } -// SetupAndRunConfig provides collaborators for the [SetupAndRun] function. -type SetupAndRunConfig 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 *SetupAndRunConfig) CheckAndSetDefaults() error { - if c.AppProvider == nil { - return trace.BadParameter("missing AppProvider") - } - - if c.HomePath == "" { - c.HomePath = profile.FullProfilePath(os.Getenv(types.HomeEnvVar)) - } - - return nil -} - func newProcessManager() (*ProcessManager, context.Context) { ctx, cancel := context.WithCancel(context.Background()) g, ctx := errgroup.WithContext(ctx) @@ -215,132 +216,3 @@ func (pm *ProcessManager) Wait() error { func (pm *ProcessManager) Close() { pm.cancel() } - -// AdminSetup must run as root. It creates and setups 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 AdminSetup(ctx context.Context, config daemon.Config) error { - if err := config.CheckAndSetDefaults(); err != nil { - return trace.Wrap(err) - } - - ctx, cancel := context.WithCancel(ctx) - defer cancel() - - tunName, err := createAndSendTUNDevice(ctx, config.SocketPath) - if err != nil { - return trace.Wrap(err) - } - - errCh := make(chan error) - go func() { - errCh <- trace.Wrap(osConfigurationLoop(ctx, tunName, config.IPv6Prefix, config.DNSAddr, config.HomePath, config.ClientCred)) - }() - - // Stay alive until we get an error on errCh, indicating that the osConfig loop exited. - // If the socket is deleted, indicating that the unprivileged process exited, cancel the context - // and then wait for the osConfig loop to exit and send an err on errCh. - ticker := time.NewTicker(daemon.CheckUnprivilegedProcessInterval) - defer ticker.Stop() - for { - select { - case <-ticker.C: - if _, err := os.Stat(config.SocketPath); err != nil { - log.DebugContext(ctx, "failed to stat socket path, assuming parent exited") - cancel() - return trace.Wrap(<-errCh) - } - case err := <-errCh: - return trace.Wrap(err) - } - } -} - -// 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. -func createAndSendTUNDevice(ctx context.Context, socketPath string) (string, error) { - tun, tunName, err := createTUNDevice(ctx) - if err != nil { - return "", trace.Wrap(err, "creating TUN device") - } - - defer func() { - // We can safely close the TUN device in the admin process after it has been sent on the socket. - if err := tun.Close(); err != nil { - log.WarnContext(ctx, "Failed to close TUN device.", "error", trace.Wrap(err)) - } - }() - - if err := sendTUNNameAndFd(socketPath, tunName, tun.File()); err != nil { - return "", trace.Wrap(err, "sending TUN over socket") - } - return tunName, nil -} - -// 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) - if err != nil { - return trace.Wrap(err, "creating OS configurator") - } - defer func() { - if err := osConfigurator.close(); err != nil { - log.ErrorContext(ctx, "Error while closing OS configurator", "error", err) - } - }() - - // Clean up any stale configuration left by a previous VNet instance that may have failed to clean up. - // This is necessary in case any stale /etc/resolver/ entries are still present, we need to - // be able to reach the proxy in order to fetch the vnet_config. - if err := osConfigurator.deconfigureOS(ctx); err != nil { - return trace.Wrap(err, "cleaning up OS configuration on startup") - } - - defer func() { - // 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) - } - }() - - if err := osConfigurator.updateOSConfiguration(ctx); err != nil { - return trace.Wrap(err, "applying initial OS configuration") - } - - // Re-configure the host OS every 10 seconds. This will pick up any newly logged-in clusters by - // reading profiles from TELEPORT_HOME. - const osConfigurationInterval = 10 * time.Second - ticker := time.NewTicker(osConfigurationInterval) - defer ticker.Stop() - for { - select { - case <-ticker.C: - if err := osConfigurator.updateOSConfiguration(ctx); err != nil { - return trace.Wrap(err, "updating OS configuration") - } - case <-ctx.Done(): - return ctx.Err() - } - } -} - -func createTUNDevice(ctx context.Context) (tun.Device, string, error) { - log.DebugContext(ctx, "Creating TUN device.") - dev, err := tun.CreateTUN("utun", mtu) - if err != nil { - return nil, "", trace.Wrap(err, "creating TUN device") - } - name, err := dev.Name() - if err != nil { - return nil, "", trace.Wrap(err, "getting TUN device name") - } - return dev, name, nil -} diff --git a/lib/vnet/setup_darwin.go b/lib/vnet/socket_darwin.go similarity index 56% rename from lib/vnet/setup_darwin.go rename to lib/vnet/socket_darwin.go index e967eb8f11b73..9597134221175 100644 --- a/lib/vnet/setup_darwin.go +++ b/lib/vnet/socket_darwin.go @@ -14,121 +14,25 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +//go:build darwin +// +build darwin + package vnet import ( - "context" - "errors" - "fmt" "net" "os" - "os/exec" "path/filepath" "runtime" - "strings" "time" "github.com/google/uuid" "github.com/gravitational/trace" "golang.org/x/sys/unix" "golang.zx2c4.com/wireguard/tun" - - "github.com/gravitational/teleport" - "github.com/gravitational/teleport/api/types" - "github.com/gravitational/teleport/lib/vnet/daemon" ) -// receiveTUNDevice is a blocking call which waits for the admin process to pass over the socket -// the name and fd of the TUN device. -func receiveTUNDevice(socket *net.UnixListener) (tun.Device, error) { - tunName, tunFd, err := recvTUNNameAndFd(socket) - if err != nil { - return nil, trace.Wrap(err, "receiving TUN name and file descriptor") - } - - tunDevice, err := tun.CreateTUNFromFile(os.NewFile(tunFd, tunName), 0) - return tunDevice, trace.Wrap(err, "creating TUN device from file descriptor") -} - -// execAdminSubcommand starts an osascript wrapper that starts tsh vnet-daemon as root. -// Used in execAdminProcess when vnetdaemon tag is not supplied. -func execAdminSubcommand(ctx context.Context, config daemon.Config) error { - executableName, err := os.Executable() - if err != nil { - return trace.Wrap(err, "getting executable path") - } - - if homePath := os.Getenv(types.HomeEnvVar); homePath == "" { - // Explicitly set TELEPORT_HOME if not already set. - os.Setenv(types.HomeEnvVar, config.HomePath) - } - - appleScript := fmt.Sprintf(` -set executableName to "%s" -set socketPath to "%s" -set ipv6Prefix to "%s" -set dnsAddr to "%s" -do shell script quoted form of executableName & `+ - `" %s -d --socket " & quoted form of socketPath & `+ - `" --ipv6-prefix " & quoted form of ipv6Prefix & `+ - `" --dns-addr " & quoted form of dnsAddr & `+ - `" --egid %d --euid %d" & `+ - `" >/var/log/vnet.log 2>&1" `+ - `with prompt "Teleport VNet wants to set up a virtual network device." with administrator privileges`, - executableName, config.SocketPath, config.IPv6Prefix, config.DNSAddr, teleport.VnetAdminSetupSubCommand, - os.Getegid(), os.Geteuid()) - - // The context we pass here has effect only on the password prompt being shown. Once osascript spawns the - // privileged process, canceling the context (and thus killing osascript) has no effect on the privileged - // process. - cmd := exec.CommandContext(ctx, "osascript", "-e", appleScript) - var stderr strings.Builder - cmd.Stderr = &stderr - - if err := cmd.Run(); err != nil { - var exitError *exec.ExitError - if errors.As(err, &exitError) { - stderr := stderr.String() - - // When the user closes the prompt for administrator privileges, the -128 error is returned. - // https://developer.apple.com/library/archive/documentation/AppleScript/Conceptual/AppleScriptLangGuide/reference/ASLR_error_codes.html#//apple_ref/doc/uid/TP40000983-CH220-SW2 - if strings.Contains(stderr, "-128") { - return trace.Errorf("password prompt closed by user") - } - - if errors.Is(ctx.Err(), context.Canceled) { - // osascript exiting due to canceled context. - return ctx.Err() - } - - stderrDesc := "" - if stderr != "" { - stderrDesc = fmt.Sprintf(", stderr: %s", stderr) - } - return trace.Wrap(exitError, "osascript exited%s", stderrDesc) - } - - return trace.Wrap(err) - } - - if ctx.Err() == nil { - // The admin subcommand is expected to run until VNet gets stopped (in other words, until ctx - // gets canceled). - // - // If it exits with no error _before_ ctx is canceled, then it most likely means that the socket - // was unexpectedly removed. When the socket gets removed, the admin subcommand assumes that the - // unprivileged process (executing this code here) has quit and thus it should quit as well. But - // we know that it's not the case, so in this scenario we return an error instead. - // - // If we don't return an error here, then other code won't be properly notified about the fact - // that the admin process has quit. - return trace.Errorf("admin subcommand exited prematurely with no error (likely because socket was removed)") - } - - return nil -} - -func createUnixSocket() (*net.UnixListener, string, error) { +func createSocket() (*net.UnixListener, string, error) { socketPath := filepath.Join(os.TempDir(), "vnet"+uuid.NewString()+".sock") socketAddr := &net.UnixAddr{Name: socketPath, Net: "unix"} l, err := net.ListenUnix(socketAddr.Net, socketAddr) @@ -165,6 +69,18 @@ func sendTUNNameAndFd(socketPath, tunName string, tunFile *os.File) error { return trace.Wrap(err, "writing to unix conn") } +// receiveTUNDevice is a blocking call which waits for the admin process to pass over the socket +// the name and fd of the TUN device. +func receiveTUNDevice(socket *net.UnixListener) (tun.Device, error) { + tunName, tunFd, err := recvTUNNameAndFd(socket) + if err != nil { + return nil, trace.Wrap(err, "receiving TUN name and file descriptor") + } + + tunDevice, err := tun.CreateTUNFromFile(os.NewFile(tunFd, tunName), 0) + return tunDevice, trace.Wrap(err, "creating TUN device from file descriptor") +} + // recvTUNNameAndFd receives the name of a TUN device and its open file descriptor over a unix socket, meant // for passing the TUN from the root process which must create it to the user process. func recvTUNNameAndFd(socket *net.UnixListener) (string, uintptr, error) { diff --git a/lib/vnet/socket_other.go b/lib/vnet/socket_other.go new file mode 100644 index 0000000000000..9b9ace5eaafdb --- /dev/null +++ b/lib/vnet/socket_other.go @@ -0,0 +1,45 @@ +// 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 ( + "os" + + "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) +} diff --git a/lib/vnet/socket_windows.go b/lib/vnet/socket_windows.go new file mode 100644 index 0000000000000..e76996edd3784 --- /dev/null +++ b/lib/vnet/socket_windows.go @@ -0,0 +1,45 @@ +// 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/vnet_test.go b/lib/vnet/vnet_test.go index 96259bbb51e26..314d16cdf9c1d 100644 --- a/lib/vnet/vnet_test.go +++ b/lib/vnet/vnet_test.go @@ -66,7 +66,7 @@ func TestMain(m *testing.M) { type testPack struct { vnetIPv6Prefix tcpip.Address dnsIPv6 tcpip.Address - ns *NetworkStack + ns *networkStack testStack *stack.Stack testLinkEndpoint *channel.Endpoint @@ -128,15 +128,15 @@ func newTestPack(t *testing.T, ctx context.Context, cfg testPackConfig) *testPac dnsIPv6 := ipv6WithSuffix(vnetIPv6Prefix, []byte{2}) - tcpHandlerResolver, err := NewTCPAppResolver(cfg.appProvider, withClock(cfg.clock)) + tcpHandlerResolver, err := newTCPAppResolver(cfg.appProvider, withClock(cfg.clock)) require.NoError(t, err) // Create the VNet and connect it to the other side of the TUN. - ns, err := newNetworkStack(&Config{ - TUNDevice: tun2, - IPv6Prefix: vnetIPv6Prefix, - DNSIPv6: dnsIPv6, - TCPHandlerResolver: tcpHandlerResolver, + ns, err := newNetworkStack(&networkStackConfig{ + tunDevice: tun2, + ipv6Prefix: vnetIPv6Prefix, + dnsIPv6: dnsIPv6, + tcpHandlerResolver: tcpHandlerResolver, upstreamNameserverSource: noUpstreamNameservers{}, }) require.NoError(t, err) @@ -144,7 +144,7 @@ func newTestPack(t *testing.T, ctx context.Context, cfg testPackConfig) *testPac utils.RunTestBackgroundTask(ctx, t, &utils.TestBackgroundTask{ Name: "VNet", Task: func(ctx context.Context) error { - if err := ns.Run(ctx); !errIsOK(err) { + if err := ns.run(ctx); !errIsOK(err) { return trace.Wrap(err) } return nil diff --git a/tool/tsh/common/vnet_common.go b/tool/tsh/common/vnet_app_provider.go similarity index 97% rename from tool/tsh/common/vnet_common.go rename to tool/tsh/common/vnet_app_provider.go index 8478a0a8a8b8d..13a4b663245bc 100644 --- a/tool/tsh/common/vnet_common.go +++ b/tool/tsh/common/vnet_app_provider.go @@ -33,8 +33,9 @@ import ( "github.com/gravitational/teleport/lib/vnet" ) -// vnetAppProvider implement [vnet.AppProvider] in order to provide the necessary methods to log in to apps -// and get clients able to list apps in all clusters in all current profiles. +// vnetAppProvider implements [vnet.AppProvider] in order to provide the +// necessary methods to log in to apps and get clients able to list apps in all +// clusters in all current profiles. type vnetAppProvider struct { cf *CLIConf clientStore *client.Store diff --git a/tool/tsh/common/vnet_daemon_darwin.go b/tool/tsh/common/vnet_daemon_darwin.go index 771ee9a22834c..4154f400774bb 100644 --- a/tool/tsh/common/vnet_daemon_darwin.go +++ b/tool/tsh/common/vnet_daemon_darwin.go @@ -22,12 +22,31 @@ package common import ( "log/slog" + "github.com/alecthomas/kingpin/v2" "github.com/gravitational/trace" "github.com/gravitational/teleport/lib/utils" "github.com/gravitational/teleport/lib/vnet" ) +const ( + // On darwin the command must match the command provided in the .plist file. + vnetDaemonSubCommand = "vnet-daemon" +) + +type vnetDaemonCommand struct { + *kingpin.CmdClause + // Launch daemons added through SMAppService are launched from a static .plist file, hence + // why this command does not accept any arguments. + // Instead, the daemon expects the arguments to be sent over XPC from an unprivileged process. +} + +func newVnetDaemonCommand(app *kingpin.Application) *vnetDaemonCommand { + return &vnetDaemonCommand{ + CmdClause: app.Command(vnetDaemonSubCommand, "Start the VNet daemon").Hidden(), + } +} + func (c *vnetDaemonCommand) run(cf *CLIConf) error { if cf.Debug { utils.InitLogger(utils.LoggingForDaemon, slog.LevelDebug) @@ -37,7 +56,3 @@ func (c *vnetDaemonCommand) run(cf *CLIConf) error { return trace.Wrap(vnet.DaemonSubcommand(cf.Context)) } - -func (c *vnetAdminSetupCommand) run(cf *CLIConf) error { - return trace.NotImplemented("tsh was built with support for VNet daemon, use %s instead", vnetDaemonSubCommand) -} diff --git a/tool/tsh/common/vnet_darwin.go b/tool/tsh/common/vnet_darwin.go index a5fcbc9cb2438..213a971f092b7 100644 --- a/tool/tsh/common/vnet_darwin.go +++ b/tool/tsh/common/vnet_darwin.go @@ -1,6 +1,3 @@ -//go:build darwin -// +build darwin - // Teleport // Copyright (C) 2024 Gravitational, Inc. // @@ -21,12 +18,15 @@ 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" ) type vnetCommand struct { @@ -46,7 +46,7 @@ func (c *vnetCommand) run(cf *CLIConf) error { return trace.Wrap(err) } - processManager, err := vnet.SetupAndRun(cf.Context, &vnet.SetupAndRunConfig{AppProvider: appProvider}) + processManager, err := vnet.Run(cf.Context, &vnet.RunConfig{AppProvider: appProvider}) if err != nil { return trace.Wrap(err) } @@ -95,18 +95,24 @@ func newVnetAdminSetupCommand(app *kingpin.Application) *vnetAdminSetupCommand { return cmd } -type vnetDaemonCommand struct { - *kingpin.CmdClause - // Launch daemons added through SMAppService are launched from a static .plist file, hence - // why this command does not accept any arguments. - // Instead, the daemon expects the arguments to be sent over XPC from an unprivileged process. -} +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 newVnetDaemonCommand(app *kingpin.Application) *vnetDaemonCommand { - return &vnetDaemonCommand{ - CmdClause: app.Command(vnetDaemonSubCommand, "Start the VNet daemon").Hidden(), + config := daemon.Config{ + SocketPath: c.socketPath, + IPv6Prefix: c.ipv6Prefix, + DNSAddr: c.dnsAddr, + HomePath: homePath, + ClientCred: daemon.ClientCred{ + Valid: true, + Egid: c.egid, + Euid: c.euid, + }, } -} -// The command must match the command provided in the .plist file. -const vnetDaemonSubCommand = "vnet-daemon" + return trace.Wrap(vnet.RunAdminProcess(cf.Context, config)) +} diff --git a/lib/vnet/setup_nodaemon_darwin.go b/tool/tsh/common/vnet_nodaemon.go similarity index 69% rename from lib/vnet/setup_nodaemon_darwin.go rename to tool/tsh/common/vnet_nodaemon.go index 5ab84b90d0661..2e6d516e214f8 100644 --- a/lib/vnet/setup_nodaemon_darwin.go +++ b/tool/tsh/common/vnet_nodaemon.go @@ -14,23 +14,25 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//go:build !vnetdaemon -// +build !vnetdaemon +//go:build !vnetdaemon || !darwin +// +build !vnetdaemon !darwin -package vnet +package common import ( - "context" - + "github.com/alecthomas/kingpin/v2" "github.com/gravitational/trace" - - "github.com/gravitational/teleport/lib/vnet/daemon" ) -func execAdminProcess(ctx context.Context, config daemon.Config) error { - return trace.Wrap(execAdminSubcommand(ctx, config)) +func newVnetDaemonCommand(app *kingpin.Application) vnetDaemonNotSupported { + return vnetDaemonNotSupported{} } -func DaemonSubcommand(ctx context.Context) error { +type vnetDaemonNotSupported struct{} + +func (vnetDaemonNotSupported) FullCommand() string { + return "" +} +func (vnetDaemonNotSupported) run(*CLIConf) error { return trace.NotImplemented("tsh was built without support for VNet daemon") } diff --git a/tool/tsh/common/vnet_nodaemon_darwin.go b/tool/tsh/common/vnet_nodaemon_darwin.go deleted file mode 100644 index b383c03197a4b..0000000000000 --- a/tool/tsh/common/vnet_nodaemon_darwin.go +++ /dev/null @@ -1,56 +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 !vnetdaemon -// +build !vnetdaemon - -package common - -import ( - "os" - - "github.com/gravitational/trace" - - "github.com/gravitational/teleport/api/types" - "github.com/gravitational/teleport/lib/vnet" - "github.com/gravitational/teleport/lib/vnet/daemon" -) - -func (c *vnetDaemonCommand) run(cf *CLIConf) error { - return trace.NotImplemented("tsh was built without support for VNet daemon") -} - -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{ - Valid: true, - Egid: c.egid, - Euid: c.euid, - }, - } - - return trace.Wrap(vnet.AdminSetup(cf.Context, config)) -} diff --git a/tool/tsh/common/vnet_other.go b/tool/tsh/common/vnet_other.go index 840c6da0ba568..dc705ee824567 100644 --- a/tool/tsh/common/vnet_other.go +++ b/tool/tsh/common/vnet_other.go @@ -1,5 +1,5 @@ -//go:build !darwin -// +build !darwin +//go:build !darwin && !windows +// +build !darwin,!windows // Teleport // Copyright (C) 2024 Gravitational, Inc. @@ -34,10 +34,6 @@ func newVnetAdminSetupCommand(app *kingpin.Application) vnetNotSupported { return vnetNotSupported{} } -func newVnetDaemonCommand(app *kingpin.Application) vnetNotSupported { - return vnetNotSupported{} -} - type vnetNotSupported struct{} func (vnetNotSupported) FullCommand() string { diff --git a/tool/tsh/common/vnet_windows.go b/tool/tsh/common/vnet_windows.go new file mode 100644 index 0000000000000..59d90972f2971 --- /dev/null +++ b/tool/tsh/common/vnet_windows.go @@ -0,0 +1,111 @@ +// Teleport +// Copyright (C) 2024 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package common + +import ( + "fmt" + "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" +) + +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.").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) + } + + go func() { + <-cf.Context.Done() + processManager.Close() + }() + + fmt.Println("VNet is ready.") + + return trace.Wrap(processManager.Wait()) +} + +// 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 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)) +}