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))
+}