diff --git a/lib/teleterm/vnet/service.go b/lib/teleterm/vnet/service.go index f3c4364c5040e..cbe18a8bab2ab 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)) +}