diff --git a/lib/srv/app/azure/handler_test.go b/lib/srv/app/azure/handler_test.go index cead5740df5f5..f8fec00bdbfb7 100644 --- a/lib/srv/app/azure/handler_test.go +++ b/lib/srv/app/azure/handler_test.go @@ -47,9 +47,7 @@ func TestForwarder_getToken(t *testing.T) { checkErr require.ErrorAssertionFunc } - var tests []testCase - - tests = []testCase{ + tests := []testCase{ { name: "base case", getTokenContext: context.Background(), diff --git a/lib/vnet/admin_process_linux.go b/lib/vnet/admin_process_linux.go new file mode 100644 index 0000000000000..be21d25e6aa60 --- /dev/null +++ b/lib/vnet/admin_process_linux.go @@ -0,0 +1,127 @@ +// Teleport +// Copyright (C) 2024 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package vnet + +import ( + "context" + "errors" + "time" + + "github.com/gravitational/trace" + "golang.org/x/sync/errgroup" + "golang.zx2c4.com/wireguard/tun" + + vnetv1 "github.com/gravitational/teleport/gen/proto/go/teleport/lib/vnet/v1" +) + +type LinuxAdminProcessConfig struct { + ClientApplicationServiceAddr string + ServiceCredentialPath string +} + +// RunLinuxAdminProcess must run as root. +func RunLinuxAdminProcess(ctx context.Context, config LinuxAdminProcessConfig) error { + log.InfoContext(ctx, "Running VNet admin process") + + serviceCreds, err := readCredentials(config.ServiceCredentialPath) + if err != nil { + return trace.Wrap(err, "reading service IPC credentials") + } + clt, err := newClientApplicationServiceClient(ctx, serviceCreds, config.ClientApplicationServiceAddr) + if err != nil { + return trace.Wrap(err, "creating user process client") + } + defer clt.close() + + tun, err := tun.CreateTUN("TeleportVNet", mtu) + if err != nil { + return trace.Wrap(err, "creating TUN device") + } + defer tun.Close() + tunName, err := tun.Name() + if err != nil { + return trace.Wrap(err, "getting TUN device name") + } + + networkStackConfig, err := newNetworkStackConfig(ctx, tun, clt) + if err != nil { + return trace.Wrap(err, "creating network stack config") + } + networkStack, err := newNetworkStack(networkStackConfig) + if err != nil { + return trace.Wrap(err, "creating network stack") + } + + if err := clt.ReportNetworkStackInfo(ctx, &vnetv1.NetworkStackInfo{ + InterfaceName: tunName, + Ipv6Prefix: networkStackConfig.ipv6Prefix.String(), + }); err != nil { + return trace.Wrap(err, "reporting network stack info to client application") + } + + osConfigProvider, err := newRemoteOSConfigProvider( + clt, + tunName, + networkStackConfig.ipv6Prefix.String(), + networkStackConfig.dnsIPv6.String(), + ) + if err != nil { + return trace.Wrap(err, "creating OS config provider") + } + osConfigurator := newOSConfigurator(osConfigProvider) + + g, ctx := errgroup.WithContext(ctx) + g.Go(func() error { + if err := networkStack.run(ctx); err != nil { + return trace.Wrap(err, "running network stack") + } + return errors.New("network stack terminated") + }) + g.Go(func() error { + if err := osConfigurator.runOSConfigurationLoop(ctx); err != nil { + return trace.Wrap(err, "running OS configuration loop") + } + return errors.New("OS configuration loop terminated") + }) + g.Go(func() error { + tick := time.Tick(time.Second) + for { + select { + case <-tick: + if err := clt.Ping(ctx); err != nil { + return trace.Wrap(err, "failed to ping client application, it may have exited, shutting down") + } + case <-ctx.Done(): + return ctx.Err() + } + } + }) + return trace.Wrap(g.Wait(), "running VNet admin process") +} + +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/diag/routeconflict_other.go b/lib/vnet/diag/routeconflict_other.go index ce0acdaca1553..bb8cf0a42d952 100644 --- a/lib/vnet/diag/routeconflict_other.go +++ b/lib/vnet/diag/routeconflict_other.go @@ -25,6 +25,8 @@ import ( "github.com/gravitational/trace" ) +// TODO: linux diagnostics + func (n *NetInterfaces) interfaceApp(ctx context.Context, ifaceName string) (string, error) { return "", trace.NotImplemented("InterfaceApp is not implemented") } diff --git a/lib/vnet/dns/osnameservers.go b/lib/vnet/dns/osnameservers.go index f8678e6652879..a53a487e9b62f 100644 --- a/lib/vnet/dns/osnameservers.go +++ b/lib/vnet/dns/osnameservers.go @@ -24,17 +24,19 @@ import ( "github.com/gravitational/trace" "github.com/gravitational/teleport/lib/utils" + "github.com/gravitational/teleport/lib/utils/slices" ) // OSUpstreamNameserverSource provides the list of upstream DNS nameservers // configured in the OS. The VNet DNS resolver will forward unhandles queries to // these nameservers. type OSUpstreamNameserverSource struct { - ttlCache *utils.FnCache + ttlCache *utils.FnCache + localAddr netip.Addr } // NewOSUpstreamNameserverSource returns a new *OSUpstreamNameserverSource. -func NewOSUpstreamNameserverSource() (*OSUpstreamNameserverSource, error) { +func NewOSUpstreamNameserverSource(localAddr netip.Addr) (*OSUpstreamNameserverSource, error) { ttlCache, err := utils.NewFnCache(utils.FnCacheConfig{ TTL: 10 * time.Second, }) @@ -42,18 +44,28 @@ func NewOSUpstreamNameserverSource() (*OSUpstreamNameserverSource, error) { return nil, trace.Wrap(err) } return &OSUpstreamNameserverSource{ - ttlCache: ttlCache, + ttlCache: ttlCache, + localAddr: localAddr, }, nil } // UpstreamNameservers returns a cached view of the host OS's current default // nameservers. func (s *OSUpstreamNameserverSource) UpstreamNameservers(ctx context.Context) ([]string, error) { - return utils.FnCacheGet(ctx, s.ttlCache, 0, loadUpstreamNameservers) + return utils.FnCacheGet(ctx, s.ttlCache, 0, s.loadUpstreamNameservers) } -func loadUpstreamNameservers(ctx context.Context) ([]string, error) { - return platformLoadUpstreamNameservers(ctx) +func (s *OSUpstreamNameserverSource) loadUpstreamNameservers(ctx context.Context) ([]string, error) { + allNameservers, err := platformLoadUpstreamNameservers(ctx) + if err != nil { + return nil, trace.Wrap(err) + } + return slices.FilterMapUnique(allNameservers, func(addr netip.Addr) (string, bool) { + if addr.Compare(s.localAddr) == 0 { + return "", false + } + return withDNSPort(addr), true + }), nil } func withDNSPort(addr netip.Addr) string { diff --git a/lib/vnet/dns/osnameservers_other.go b/lib/vnet/dns/osnameservers_other.go index 3cc9d156f0d6a..17af50dd62d2d 100644 --- a/lib/vnet/dns/osnameservers_other.go +++ b/lib/vnet/dns/osnameservers_other.go @@ -14,12 +14,13 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//go:build !darwin && !windows +//go:build !darwin && !windows && !linux package dns import ( "context" + "net/netip" "runtime" "github.com/gravitational/trace" @@ -33,6 +34,6 @@ var ( _ = withDNSPort ) -func platformLoadUpstreamNameservers(ctx context.Context) ([]string, error) { +func platformLoadUpstreamNameservers(ctx context.Context) ([]netip.Addr, error) { return nil, trace.Wrap(vnetNotImplemented) } diff --git a/lib/vnet/dns/osnameservers_darwin.go b/lib/vnet/dns/osnameservers_unix.go similarity index 71% rename from lib/vnet/dns/osnameservers_darwin.go rename to lib/vnet/dns/osnameservers_unix.go index 8f765ad4a9701..92ddd7316e7d4 100644 --- a/lib/vnet/dns/osnameservers_darwin.go +++ b/lib/vnet/dns/osnameservers_unix.go @@ -14,6 +14,8 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +//go:build linux || darwin + package dns import ( @@ -22,29 +24,40 @@ import ( "log/slog" "net/netip" "os" + "runtime" "strings" "github.com/gravitational/trace" ) -const ( - confFilePath = "/etc/resolv.conf" -) - // platformLoadUpstreamNameservers reads the OS DNS nameservers found in // /etc/resolv.conf. The comments in that file make it clear it is not actually // consulted for DNS hostname resolution, but MacOS seems to keep it up to date // with the current default nameservers as configured for the OS, and it is the // easiest place to read them. Eventually we should probably use a better // method, but for now this works. -func platformLoadUpstreamNameservers(ctx context.Context) ([]string, error) { +func platformLoadUpstreamNameservers(ctx context.Context) ([]netip.Addr, error) { + // TODO: this is very hacky and just happens to work on the EC2 I've been + // testing on, figure out a good way to find upstream nameservers on Linux + // or a better way of resolving queries VNet can't handle (names in custom + // DNS zones that don't match a teleport app may resolve to some other + // company internal app outside of VNet/Teleport). + var confFilePath string + switch runtime.GOOS { + case "darwin": + confFilePath = "/etc/resolv.conf" + case "linux": + confFilePath = "/run/systemd/resolve/resolv.conf" + default: + return nil, trace.NotImplemented("unsupported os %s", runtime.GOOS) + } f, err := os.Open(confFilePath) if err != nil { return nil, trace.Wrap(err, "opening %s", confFilePath) } defer f.Close() - var nameservers []string + var nameservers []netip.Addr scanner := bufio.NewScanner(f) for scanner.Scan() { line := scanner.Text() @@ -63,7 +76,7 @@ func platformLoadUpstreamNameservers(ctx context.Context) ([]string, error) { continue } - nameservers = append(nameservers, withDNSPort(ip)) + nameservers = append(nameservers, ip) } slog.DebugContext(ctx, "Loaded host upstream nameservers.", "nameservers", nameservers, "config_file", confFilePath) diff --git a/lib/vnet/dns/osnameservers_windows.go b/lib/vnet/dns/osnameservers_windows.go index 2db2063761bd9..605c670ca248c 100644 --- a/lib/vnet/dns/osnameservers_windows.go +++ b/lib/vnet/dns/osnameservers_windows.go @@ -30,7 +30,7 @@ import ( // platformLoadUpstreamNameservers attempts to find the default DNS nameservers // that VNet should forward unmatched queries to. To do this, it finds the // nameservers configured for each interface and sorts by the interface metric. -func platformLoadUpstreamNameservers(ctx context.Context) ([]string, error) { +func platformLoadUpstreamNameservers(ctx context.Context) ([]netip.Addr, error) { interfaces, err := winipcfg.GetIPInterfaceTable(windows.AF_INET) if err != nil { return nil, trace.Wrap(err, "looking up local network interfaces") @@ -38,7 +38,7 @@ func platformLoadUpstreamNameservers(ctx context.Context) ([]string, error) { sort.Slice(interfaces, func(i, j int) bool { return interfaces[i].Metric < interfaces[j].Metric }) - var nameservers []string + var nameservers []netip.Addr for _, iface := range interfaces { ifaceNameservers, err := iface.InterfaceLUID.DNS() if err != nil { @@ -48,7 +48,7 @@ func platformLoadUpstreamNameservers(ctx context.Context) ([]string, error) { if ignoreUpstreamNameserver(ifaceNameserver) { continue } - nameservers = append(nameservers, withDNSPort(ifaceNameserver)) + nameservers = append(nameservers, ifaceNameserver) } } slog.DebugContext(ctx, "Loaded host upstream nameservers", "nameservers", nameservers) diff --git a/lib/vnet/escalate_linux.go b/lib/vnet/escalate_linux.go new file mode 100644 index 0000000000000..234487efb0f29 --- /dev/null +++ b/lib/vnet/escalate_linux.go @@ -0,0 +1,32 @@ +package vnet + +import ( + "context" + "os" + "os/exec" + + "github.com/gravitational/trace" +) + +func execAdminProcess(ctx context.Context, cfg LinuxAdminProcessConfig) error { + executableName, err := os.Executable() + if err != nil { + return trace.Wrap(err, "getting executable path") + } + + // TODO: find a proper way to start the service without just running sudo + // and hoping there's no password requirement... + // + // Also need to figure out how we want to set up a service that runs as root + // that can be started from Connect, maybe some systemd service but that + // doesn't solve how we allow the service to be started. + cmd := exec.CommandContext(ctx, "sudo", executableName, "-d", + "vnet-service", + "--addr", cfg.ClientApplicationServiceAddr, + "--cred-path", cfg.ServiceCredentialPath, + ) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + log.DebugContext(ctx, "Escalating to root with sudo") + return trace.Wrap(cmd.Run(), "escalating to root with sudo") +} diff --git a/lib/vnet/network_stack.go b/lib/vnet/network_stack.go index 25256b4a4eb59..321ee6f38b7d0 100644 --- a/lib/vnet/network_stack.go +++ b/lib/vnet/network_stack.go @@ -21,6 +21,7 @@ import ( "errors" "log/slog" "net" + "net/netip" "os" "sync" @@ -242,7 +243,11 @@ func newNetworkStack(cfg *networkStackConfig) (*networkStack, error) { if cfg.dnsIPv6 != (tcpip.Address{}) { upstreamNameserverSource := cfg.upstreamNameserverSource if upstreamNameserverSource == nil { - upstreamNameserverSource, err = dns.NewOSUpstreamNameserverSource() + localAddr, ok := netip.AddrFromSlice(cfg.dnsIPv6.AsSlice()) + if !ok { + return nil, trace.Errorf("failed to config tcpip.Addr to netip.Addr: %s", cfg.dnsIPv6.String()) + } + upstreamNameserverSource, err = dns.NewOSUpstreamNameserverSource(localAddr) if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/vnet/osconfig_linux.go b/lib/vnet/osconfig_linux.go new file mode 100644 index 0000000000000..b13573c958187 --- /dev/null +++ b/lib/vnet/osconfig_linux.go @@ -0,0 +1,110 @@ +// 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" + "slices" + + "github.com/gravitational/teleport/api/utils" + "github.com/gravitational/trace" +) + +type platformOSConfigState struct { + configuredIPv6 bool + configuredIPv4 bool + configuredCidrRanges []string + configuredNameserver bool + configuredDNSZones []string + broughtUpInterface bool +} + +// platformConfigureOS configures the host OS according to cfg. +func platformConfigureOS(ctx context.Context, cfg *osConfig, state *platformOSConfigState) error { + // TODO: we should probably use proper APIs (dbus?) to set up IPs, routes, + // DNS etc and add checks that the host is actually using systemd-resolved + // before trying to run or supporting other DNS setups. + if cfg.tunIPv6 != "" && !state.configuredIPv6 { + log.InfoContext(ctx, "Setting IPv6 address for the TUN device.", "device", cfg.tunName, "address", cfg.tunIPv6) + addrWithPrefix := cfg.tunIPv6 + "/64" + if err := runCommand(ctx, + "ip", "addr", "add", addrWithPrefix, "dev", cfg.tunName, + ); err != nil { + return trace.Wrap(err) + } + state.configuredIPv6 = true + } + if cfg.tunIPv4 != "" && !state.configuredIPv4 { + log.InfoContext(ctx, "Setting IPv4 address for the TUN device.", + "device", cfg.tunName, "address", cfg.tunIPv4) + if err := runCommand(ctx, + "ip", "addr", "add", cfg.tunIPv4, "dev", cfg.tunName, + ); err != nil { + return trace.Wrap(err) + } + state.configuredIPv4 = true + } + if cfg.dnsAddr != "" && !state.configuredNameserver { + log.InfoContext(ctx, "Configuring DNS nameserver", "nameserver", cfg.dnsAddr) + if err := runCommand(ctx, + "resolvectl", "dns", cfg.tunName, cfg.dnsAddr, + ); err != nil { + return trace.Wrap(err) + } + state.configuredNameserver = true + } + if shouldReconfiguredDNSZones(cfg, state) { + log.InfoContext(ctx, "Configuring DNS zones", "zones", cfg.dnsZones) + domains := make([]string, 0, len(cfg.dnsZones)) + for _, dnsZone := range cfg.dnsZones { + domains = append(domains, "~"+dnsZone) + } + args := append([]string{"domain", cfg.tunName}, domains...) + if err := runCommand(ctx, "resolvectl", args...); err != nil { + return trace.Wrap(err) + } + state.configuredDNSZones = cfg.dnsZones + } + if state.configuredIPv6 && state.configuredNameserver && !state.broughtUpInterface { + log.InfoContext(ctx, "Bringing up the VNet interface", "device", cfg.tunName) + if err := runCommand(ctx, + "ip", "link", "set", cfg.tunName, "up", + ); err != nil { + return trace.Wrap(err) + } + state.broughtUpInterface = true + } + if cfg.tunIPv4 != "" && state.configuredIPv4 && state.broughtUpInterface { + for _, cidrRange := range cfg.cidrRanges { + if slices.Contains(state.configuredCidrRanges, cidrRange) { + continue + } + log.InfoContext(ctx, "Setting an IPv4 route", "netmask", cidrRange) + if err := runCommand(ctx, + "ip", "route", "add", cidrRange, "via", cfg.tunIPv4, "dev", cfg.tunName, + ); err != nil { + return trace.Wrap(err) + } + state.configuredCidrRanges = append(state.configuredCidrRanges, cidrRange) + } + } + return nil +} + +func shouldReconfiguredDNSZones(cfg *osConfig, state *platformOSConfigState) bool { + return !utils.ContainSameUniqueElements(cfg.dnsZones, state.configuredDNSZones) +} diff --git a/lib/vnet/osconfig_windows.go b/lib/vnet/osconfig_windows.go index 0b10495434e1a..fdb2a18ee2166 100644 --- a/lib/vnet/osconfig_windows.go +++ b/lib/vnet/osconfig_windows.go @@ -113,7 +113,7 @@ func platformConfigureOS(ctx context.Context, cfg *osConfig, state *platformOSCo state.configuredV6Address = true } - if shouldUpdateDNSConfig(cfg, state) { + if shouldReconfiguredDNSZones(cfg, state) { if err := configureDNS(ctx, cfg.dnsZones, cfg.dnsAddr); err != nil { return trace.Wrap(err, "configuring DNS") } @@ -134,16 +134,6 @@ func addrMaskForCIDR(cidr string) (string, string, error) { return ipNet.IP.String(), net.IP(ipNet.Mask).String(), nil } -func shouldUpdateDNSConfig(cfg *osConfig, state *platformOSConfigState) bool { - // Always reconfigure if there should be no zones, to make sure we clear - // any leftover state when starting up. - if len(cfg.dnsZones) == 0 { - return true - } - // Otherwise, reconfigure if anything has changed. - return !utils.ContainSameUniqueElements(cfg.dnsZones, state.configuredDNSZones) -} - func configureDNS(ctx context.Context, zones []string, nameserver string) (err error) { if len(nameserver) == 0 && len(zones) > 0 { return trace.BadParameter("empty nameserver with non-empty zones") @@ -222,3 +212,13 @@ func deleteRegistryKey(key string) error { keyHandle.Close() return trace.Wrap(deleteErr, "failed to delete DNS registry key %s", key) } + +func shouldReconfiguredDNSZones(cfg *osConfig, state *platformOSConfigState) bool { + // Always reconfigure if there should be no zones, to make sure we clear + // any leftover state when starting up. + if len(cfg.dnsZones) == 0 { + return true + } + // Otherwise, reconfigure if anything has changed. + return !utils.ContainSameUniqueElements(cfg.dnsZones, state.configuredDNSZones) +} diff --git a/lib/vnet/unsupported_os.go b/lib/vnet/unsupported_os.go index b21576076ff8a..c431dcd5bf659 100644 --- a/lib/vnet/unsupported_os.go +++ b/lib/vnet/unsupported_os.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//go:build !darwin && !windows +//go:build !darwin && !windows && !linux package vnet diff --git a/lib/vnet/user_process_linux.go b/lib/vnet/user_process_linux.go new file mode 100644 index 0000000000000..97aca85e07b08 --- /dev/null +++ b/lib/vnet/user_process_linux.go @@ -0,0 +1,105 @@ +// 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" + "net" + "os" + + "github.com/gravitational/trace" + "google.golang.org/grpc" + grpccredentials "google.golang.org/grpc/credentials" + + "github.com/gravitational/teleport/api/utils/grpc/interceptors" + vnetv1 "github.com/gravitational/teleport/gen/proto/go/teleport/lib/vnet/v1" +) + +// runPlatformUserProcess launches a daemon in the background that will handle +// all networking and OS configuration. The user process exposes a gRPC +// interface that the daemon uses to query application names and get user +// certificates for apps. If successful it sets p.processManager and +// p.networkStackInfo. +func (p *UserProcess) runPlatformUserProcess(processCtx context.Context) error { + ipcCreds, err := newIPCCredentials() + if err != nil { + return trace.Wrap(err, "creating credentials for IPC") + } + serverTLSConfig, err := ipcCreds.server.serverTLSConfig() + if err != nil { + return trace.Wrap(err, "generating gRPC server TLS config") + } + + credDir, err := os.MkdirTemp("", "vnet_service_certs") + if err != nil { + return trace.Wrap(err, "creating temp dir for service certs") + } + // Write credentials with 0200 so that only root can read them and no user + // processes should be able to connect to the service. + if err := ipcCreds.client.write(credDir, 0200); err != nil { + return trace.Wrap(err, "writing service IPC credentials") + } + + listener, err := net.Listen("tcp", "localhost:0") + if err != nil { + return trace.Wrap(err, "listening on tcp socket") + } + // grpcServer.Serve takes ownership of (and closes) the listener. + grpcServer := grpc.NewServer( + grpc.Creds(grpccredentials.NewTLS(serverTLSConfig)), + grpc.UnaryInterceptor(interceptors.GRPCServerUnaryErrorInterceptor), + grpc.StreamInterceptor(interceptors.GRPCServerStreamErrorInterceptor), + ) + vnetv1.RegisterClientApplicationServiceServer(grpcServer, p.clientApplicationService) + + p.processManager.AddCriticalBackgroundTask("admin process", func() error { + defer func() { + // Delete service credentials after the service terminates. + if ipcCreds.client.remove(credDir); err != nil { + log.ErrorContext(processCtx, "Failed to remove service credential files", "error", err) + } + if err := os.RemoveAll(credDir); err != nil { + log.ErrorContext(processCtx, "Failed to remove service credential directory", "error", err) + } + }() + return trace.Wrap(execAdminProcess(processCtx, LinuxAdminProcessConfig{ + ServiceCredentialPath: credDir, + ClientApplicationServiceAddr: listener.Addr().String(), + })) + }) + p.processManager.AddCriticalBackgroundTask("gRPC service", func() error { + log.InfoContext(processCtx, "Starting gRPC service", + "addr", listener.Addr().String()) + return trace.Wrap(grpcServer.Serve(listener), + "serving VNet user process gRPC service") + }) + p.processManager.AddCriticalBackgroundTask("gRPC server closer", func() error { + // grpcServer.Serve does not stop on its own when processCtx is done, so + // this task waits for processCtx and then explicitly stops grpcServer. + <-processCtx.Done() + grpcServer.GracefulStop() + return nil + }) + + select { + case nsi := <-p.clientApplicationService.networkStackInfo: + p.networkStackInfo = nsi + return nil + case <-processCtx.Done(): + return trace.Wrap(p.processManager.Wait(), "process manager exited before network stack info was received") + } +} diff --git a/tool/tsh/common/vnet_linux.go b/tool/tsh/common/vnet_linux.go new file mode 100644 index 0000000000000..76ae4f3679ffe --- /dev/null +++ b/tool/tsh/common/vnet_linux.go @@ -0,0 +1,64 @@ +// Teleport +// Copyright (C) 2025 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package common + +import ( + "context" + + "github.com/alecthomas/kingpin/v2" + "github.com/gravitational/trace" + + vnetv1 "github.com/gravitational/teleport/gen/proto/go/teleport/lib/vnet/v1" + "github.com/gravitational/teleport/lib/vnet" +) + +func newPlatformVnetAdminSetupCommand(app *kingpin.Application) vnetCommandNotSupported { + return vnetCommandNotSupported{} +} + +// vnetServiceCommand runs the VNet service. +type vnetServiceCommand struct { + *kingpin.CmdClause + cfg vnet.LinuxAdminProcessConfig +} + +func (c *vnetServiceCommand) run(clf *CLIConf) error { + return trace.Wrap(vnet.RunLinuxAdminProcess(clf.Context, c.cfg)) +} + +func newPlatformVnetServiceCommand(app *kingpin.Application) *vnetServiceCommand { + cmd := &vnetServiceCommand{ + CmdClause: app.Command("vnet-service", "Start the VNet admin subprocess.").Hidden(), + } + cmd.Flag("addr", "client application service address").Required().StringVar(&cmd.cfg.ClientApplicationServiceAddr) + cmd.Flag("cred-path", "path to TLS credentials for connecting to client application").Required().StringVar(&cmd.cfg.ServiceCredentialPath) + return cmd +} + +// The vnet-install-service command is only supported on windows. +func newPlatformVnetInstallServiceCommand(app *kingpin.Application) vnetCommandNotSupported { + return vnetCommandNotSupported{} +} + +// The vnet-uninstall-service command is only supported on windows. +func newPlatformVnetUninstallServiceCommand(app *kingpin.Application) vnetCommandNotSupported { + return vnetCommandNotSupported{} +} + +func runVnetDiagnostics(ctx context.Context, nsi *vnetv1.NetworkStackInfo) error { + return trace.NotImplemented("diagnostics not implemented") +} diff --git a/tool/tsh/common/vnet_other.go b/tool/tsh/common/vnet_other.go index 60198a9ca9d84..c44d35cdf7c7e 100644 --- a/tool/tsh/common/vnet_other.go +++ b/tool/tsh/common/vnet_other.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//go:build !darwin && !windows +//go:build !darwin && !windows && !linux package common diff --git a/web/packages/teleterm/src/ui/Vnet/vnetContext.tsx b/web/packages/teleterm/src/ui/Vnet/vnetContext.tsx index 4deba14e0d802..bee54b316961e 100644 --- a/web/packages/teleterm/src/ui/Vnet/vnetContext.tsx +++ b/web/packages/teleterm/src/ui/Vnet/vnetContext.tsx @@ -145,7 +145,7 @@ export const VnetContextProvider: FC< () => mainProcessClient.getRuntimeSettings().platform, [mainProcessClient] ); - const isSupported = platform === 'darwin' || platform === 'win32'; + const isSupported = platform === 'darwin' || platform === 'win32' || platform === 'linux'; const isDiagSupported = platform === 'darwin'; const [startAttempt, start] = useAsync(