From c4aa90e61160e9eda312a2123133d6c88d416d39 Mon Sep 17 00:00:00 2001 From: Nic Klaassen Date: Tue, 3 Jun 2025 11:06:16 -0700 Subject: [PATCH 01/26] first build --- lib/vnet/admin_process_linux.go | 127 ++++++++++++++++++ lib/vnet/diag/routeconflict_other.go | 2 + lib/vnet/dns/osnameservers_other.go | 2 +- ...ervers_darwin.go => osnameservers_unix.go} | 21 ++- lib/vnet/escalate_linux.go | 32 +++++ lib/vnet/osconfig_linux.go | 110 +++++++++++++++ lib/vnet/unsupported_os.go | 2 +- lib/vnet/user_process_linux.go | 105 +++++++++++++++ tool/tsh/common/vnet_linux.go | 64 +++++++++ tool/tsh/common/vnet_other.go | 2 +- .../teleterm/src/ui/Vnet/vnetContext.tsx | 3 +- 11 files changed, 462 insertions(+), 8 deletions(-) create mode 100644 lib/vnet/admin_process_linux.go rename lib/vnet/dns/{osnameservers_darwin.go => osnameservers_unix.go} (76%) create mode 100644 lib/vnet/escalate_linux.go create mode 100644 lib/vnet/osconfig_linux.go create mode 100644 lib/vnet/user_process_linux.go create mode 100644 tool/tsh/common/vnet_linux.go 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 5ca8360dabd60..379b7dee2f25d 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_other.go b/lib/vnet/dns/osnameservers_other.go index 3cc9d156f0d6a..b8db3e96e31e9 100644 --- a/lib/vnet/dns/osnameservers_other.go +++ b/lib/vnet/dns/osnameservers_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 dns diff --git a/lib/vnet/dns/osnameservers_darwin.go b/lib/vnet/dns/osnameservers_unix.go similarity index 76% rename from lib/vnet/dns/osnameservers_darwin.go rename to lib/vnet/dns/osnameservers_unix.go index 8f765ad4a9701..b45a9af3a98dd 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,15 +24,12 @@ 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 @@ -38,6 +37,20 @@ const ( // 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) { + // 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) 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/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/unsupported_os.go b/lib/vnet/unsupported_os.go index a003fa6c4f4c3..021714d62a5b7 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 f76d6565f89e0..fc031c8e3cc0d 100644 --- a/web/packages/teleterm/src/ui/Vnet/vnetContext.tsx +++ b/web/packages/teleterm/src/ui/Vnet/vnetContext.tsx @@ -175,7 +175,8 @@ export const VnetContextProvider: FC< () => mainProcessClient.getRuntimeSettings().platform, [mainProcessClient] ); - const isSupported = platform === 'darwin' || platform === 'win32'; + const isSupported = + platform === 'darwin' || platform === 'win32' || platform === 'linux'; const [checkInstallTimeRequirementsAttempt, checkInstallTimeRequirements] = useAsync( From 7807d29554e596498d3263d8a8f4d7898297c216 Mon Sep 17 00:00:00 2001 From: Pavel Evdokimov Date: Tue, 27 Jan 2026 14:27:29 +0000 Subject: [PATCH 02/26] add d-bus daemon for vnet --- go.mod | 2 +- lib/vnet/admin_process_linux.go | 46 +++--- lib/vnet/dbus_client_linux.go | 54 +++++++ lib/vnet/dbus_linux.go | 28 ++++ lib/vnet/dbus_service_linux.go | 213 +++++++++++++++++++++++++++ lib/vnet/escalate_linux.go | 156 ++++++++++++++++++-- tool/tsh/common/vnet_daemon_linux.go | 56 +++++++ tool/tsh/common/vnet_linux.go | 27 ++-- tool/tsh/common/vnet_nodaemon.go | 2 +- 9 files changed, 543 insertions(+), 41 deletions(-) create mode 100644 lib/vnet/dbus_client_linux.go create mode 100644 lib/vnet/dbus_linux.go create mode 100644 lib/vnet/dbus_service_linux.go create mode 100644 tool/tsh/common/vnet_daemon_linux.go diff --git a/go.mod b/go.mod index 6d4a67800153a..366e0d2b7f32a 100644 --- a/go.mod +++ b/go.mod @@ -135,6 +135,7 @@ require ( github.com/go-webauthn/webauthn v0.11.2 github.com/gobwas/ws v1.4.0 github.com/gocql/gocql v1.7.0 + github.com/godbus/dbus/v5 v5.2.2 github.com/gofrs/flock v0.13.0 github.com/gogo/protobuf v1.3.2 // replaced github.com/golang-jwt/jwt/v4 v4.5.2 @@ -427,7 +428,6 @@ require ( github.com/gobwas/pool v0.2.1 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect - github.com/godbus/dbus/v5 v5.2.2 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect diff --git a/lib/vnet/admin_process_linux.go b/lib/vnet/admin_process_linux.go index be21d25e6aa60..55a6b5def7458 100644 --- a/lib/vnet/admin_process_linux.go +++ b/lib/vnet/admin_process_linux.go @@ -19,6 +19,7 @@ package vnet import ( "context" "errors" + "os" "time" "github.com/gravitational/trace" @@ -73,12 +74,13 @@ func RunLinuxAdminProcess(ctx context.Context, config LinuxAdminProcessConfig) e return trace.Wrap(err, "reporting network stack info to client application") } - osConfigProvider, err := newRemoteOSConfigProvider( - clt, - tunName, - networkStackConfig.ipv6Prefix.String(), - networkStackConfig.dnsIPv6.String(), - ) + osConfigProvider, err := newOSConfigProvider(osConfigProviderConfig{ + clt: clt, + tunName: tunName, + ipv6Prefix: networkStackConfig.ipv6Prefix.String(), + dnsIPv6: networkStackConfig.dnsIPv6.String(), + addDNSAddress: networkStack.addDNSAddress, + }) if err != nil { return trace.Wrap(err, "creating OS config provider") } @@ -86,18 +88,21 @@ func RunLinuxAdminProcess(ctx context.Context, config LinuxAdminProcessConfig) e g, ctx := errgroup.WithContext(ctx) g.Go(func() error { + defer log.InfoContext(ctx, "Network stack terminated.") if err := networkStack.run(ctx); err != nil { return trace.Wrap(err, "running network stack") } return errors.New("network stack terminated") }) g.Go(func() error { + defer log.InfoContext(ctx, "OS configuration loop exited.") 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 { + defer log.InfoContext(ctx, "Ping loop exited.") tick := time.Tick(time.Second) for { select { @@ -110,18 +115,25 @@ func RunLinuxAdminProcess(ctx context.Context, config LinuxAdminProcessConfig) e } } }) - 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") + done := make(chan error) + go func() { + done <- g.Wait() + }() + + select { + case err := <-done: + return trace.Wrap(err, "running VNet admin process") + case <-ctx.Done(): } - name, err := dev.Name() - if err != nil { - return nil, "", trace.Wrap(err, "getting TUN device name") + + select { + case err := <-done: + // network stack exited cleanly within timeout + return trace.Wrap(err, "running VNet admin process") + case <-time.After(10 * time.Second): + log.ErrorContext(ctx, "VNet admin process did not exit within 10 seconds, forcing shutdown.") + os.Exit(1) + return nil } - return dev, name, nil } diff --git a/lib/vnet/dbus_client_linux.go b/lib/vnet/dbus_client_linux.go new file mode 100644 index 0000000000000..6e75befe09ed9 --- /dev/null +++ b/lib/vnet/dbus_client_linux.go @@ -0,0 +1,54 @@ +// Teleport +// Copyright (C) 2026 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package vnet + +import ( + "context" + + "github.com/godbus/dbus/v5" + "github.com/gravitational/trace" +) + +func startService(ctx context.Context, cfg LinuxAdminProcessConfig) error { + conn, err := dbus.ConnectSystemBus() + if err != nil { + return trace.NotFound("system D-Bus is unavailable: %v", err) + } + defer conn.Close() + + obj := conn.Object(vnetDBusServiceName, dbus.ObjectPath(vnetDBusObjectPath)) + call := obj.CallWithContext(ctx, vnetDBusStartMethod, 0, cfg.ClientApplicationServiceAddr, cfg.ServiceCredentialPath) + if call.Err != nil { + return trace.Wrap(call.Err, "calling D-Bus Start") + } + return nil +} + +func stopService(ctx context.Context) error { + conn, err := dbus.ConnectSystemBus() + if err != nil { + return trace.NotFound("system D-Bus is unavailable: %v", err) + } + defer conn.Close() + + obj := conn.Object(vnetDBusServiceName, dbus.ObjectPath(vnetDBusObjectPath)) + call := obj.CallWithContext(ctx, vnetDBusStopMethod, 0) + if call.Err != nil { + return trace.Wrap(call.Err, "calling D-Bus Stop") + } + return nil +} diff --git a/lib/vnet/dbus_linux.go b/lib/vnet/dbus_linux.go new file mode 100644 index 0000000000000..b36e5826bcbba --- /dev/null +++ b/lib/vnet/dbus_linux.go @@ -0,0 +1,28 @@ +// Teleport +// Copyright (C) 2026 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 + +const ( + vnetDBusServiceName = "org.teleport.vnet1" + vnetDBusObjectPath = "/org/teleport/vnet1" + vnetDBusInterface = "org.teleport.vnet1.Daemon" + vnetDBusStartMethod = vnetDBusInterface + ".Start" + vnetDBusStopMethod = vnetDBusInterface + ".Stop" + // vnetPolkitAction must match the action ID defined in the polkit policy file. + vnetPolkitAction = "org.teleport.vnet1.manage-daemon" + vnetSystemdUnitName = "teleport-vnet.service" +) diff --git a/lib/vnet/dbus_service_linux.go b/lib/vnet/dbus_service_linux.go new file mode 100644 index 0000000000000..1e09aa0822c3e --- /dev/null +++ b/lib/vnet/dbus_service_linux.go @@ -0,0 +1,213 @@ +// Teleport +// Copyright (C) 2026 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" + "sync" + + "github.com/godbus/dbus/v5" + "github.com/godbus/dbus/v5/introspect" + "github.com/gravitational/trace" +) + +// polkitAllowUserInteraction allows polkit to prompt the user +// for a password if it is required. +const polkitAllowUserInteraction = uint32(1) + +// introspectNode describes the exported D-Bus API. Update it if any method +// signature is changed or new methods are added. +var introspectNode = &introspect.Node{ + Name: vnetDBusObjectPath, + Interfaces: []introspect.Interface{ + introspect.IntrospectData, + { + Name: vnetDBusInterface, + Methods: []introspect.Method{ + { + Name: "Start", + Args: []introspect.Arg{ + {Name: "addr", Type: "s", Direction: "in"}, + {Name: "credPath", Type: "s", Direction: "in"}, + }, + }, + {Name: "Stop"}, + }, + }, + }, +} + +// RunLinuxDBusService runs the VNet D-Bus service that can start the VNet admin process. +func RunLinuxDBusService(ctx context.Context) error { + conn, err := dbus.ConnectSystemBus() + if err != nil { + return trace.Wrap(err, "connecting to system D-Bus") + } + defer conn.Close() + + serviceCtx, cancel := context.WithCancel(ctx) + defer cancel() + + daemon := &dbusDaemon{ + ctx: serviceCtx, + cancel: cancel, + conn: conn, + } + if err := conn.Export(daemon, dbus.ObjectPath(vnetDBusObjectPath), vnetDBusInterface); err != nil { + return trace.Wrap(err, "exporting D-Bus object") + } + if err := conn.Export( + introspect.NewIntrospectable(introspectNode), + dbus.ObjectPath(vnetDBusObjectPath), + "org.freedesktop.DBus.Introspectable", + ); err != nil { + return trace.Wrap(err, "exporting D-Bus introspection") + } + + reply, err := conn.RequestName(vnetDBusServiceName, dbus.NameFlagDoNotQueue) + if err != nil { + return trace.Wrap(err, "requesting D-Bus name") + } + if reply != dbus.RequestNameReplyPrimaryOwner { + return trace.Errorf("D-Bus name %s is already owned", vnetDBusServiceName) + } + log.InfoContext(serviceCtx, "Acquired D-Bus name", "name", vnetDBusServiceName) + + <-serviceCtx.Done() + return nil +} + +type dbusDaemon struct { + ctx context.Context + cancel context.CancelFunc + conn *dbus.Conn + + mu sync.Mutex + started bool +} + +// Start is a D-Bus method that starts the VNet admin process. +func (d *dbusDaemon) Start(addr, credPath string, sender dbus.Sender) *dbus.Error { + if err := d.authorize(sender); err != nil { + return dbus.MakeFailedError(trace.Wrap(err, "authorization failed")) + } + + uid, err := d.lookupSenderUID(sender) + if err != nil { + return dbus.MakeFailedError(trace.Wrap(err, "looking up D-Bus sender UID")) + } + + d.mu.Lock() + defer d.mu.Unlock() + if d.started { + return dbus.MakeFailedError(trace.Errorf("VNet admin process already started")) + } + d.started = true + + log.InfoContext(d.ctx, "Starting VNet admin process", "uid", uid) + + go func() { + err := RunLinuxAdminProcess(d.ctx, LinuxAdminProcessConfig{ + ClientApplicationServiceAddr: addr, + ServiceCredentialPath: credPath, + }) + // TODO: D-Bus supports signals, we might want to emit a signal when the admin process exits. + if err != nil { + log.ErrorContext(d.ctx, "VNet admin process exited with error", "error", err) + } + d.cancel() + }() + + return nil +} + +// Stop is a D-Bus method that stops the VNet admin process. +func (d *dbusDaemon) Stop(sender dbus.Sender) *dbus.Error { + if err := d.authorize(sender); err != nil { + return dbus.MakeFailedError(trace.Wrap(err, "authorization failed")) + } + uid, err := d.lookupSenderUID(sender) + if err != nil { + return dbus.MakeFailedError(trace.Wrap(err, "looking up D-Bus sender UID")) + } + // We intentionally do not reset started here to avoid a race with Start + // while the process is exiting. A new Start is allowed only after + // a new daemon instance is started. + // + // D-Bus activation can start the daemon on any method call. We allow + // Stop before Start so the service can exit immediately instead of idling + // waiting for a Start call that may never come. + log.InfoContext(d.ctx, "Stopping VNet admin process", "uid", uid) + d.cancel() + return nil +} + +func (d *dbusDaemon) authorize(sender dbus.Sender) error { + uid, err := d.lookupSenderUID(sender) + if err != nil { + return trace.Wrap(err, "looking up D-Bus sender UID") + } + if uid == 0 { + return nil + } + + subject := polkitSubject{ + Kind: "system-bus-name", + Details: map[string]dbus.Variant{ + "name": dbus.MakeVariant(string(sender)), + }, + } + var result struct { + Authorized bool + Challenge bool + Details map[string]string + } + if err := d.conn.Object("org.freedesktop.PolicyKit1", "/org/freedesktop/PolicyKit1/Authority"). + Call( + "org.freedesktop.PolicyKit1.Authority.CheckAuthorization", + 0, + subject, + vnetPolkitAction, + map[string]string{}, + polkitAllowUserInteraction, + "", + ).Store(&result); err != nil { + return trace.Wrap(err, "checking polkit authorization") + } + if !result.Authorized { + if result.Challenge { + return trace.Errorf("polkit authentication required") + } + return trace.Errorf("polkit authorization denied") + } + return nil +} + +func (d *dbusDaemon) lookupSenderUID(sender dbus.Sender) (uint32, error) { + var uid uint32 + if err := d.conn.Object("org.freedesktop.DBus", "/org/freedesktop/DBus"). + Call("org.freedesktop.DBus.GetConnectionUnixUser", 0, string(sender)). + Store(&uid); err != nil { + return 0, trace.Wrap(err, "querying D-Bus sender UID") + } + return uid, nil +} + +type polkitSubject struct { + Kind string + Details map[string]dbus.Variant +} diff --git a/lib/vnet/escalate_linux.go b/lib/vnet/escalate_linux.go index 234487efb0f29..f889e721621e3 100644 --- a/lib/vnet/escalate_linux.go +++ b/lib/vnet/escalate_linux.go @@ -1,32 +1,168 @@ +// Teleport +// Copyright (C) 2026 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" "os/exec" + "slices" + "time" + systemddbus "github.com/coreos/go-systemd/v22/dbus" + "github.com/godbus/dbus/v5" "github.com/gravitational/trace" + + "github.com/gravitational/teleport" +) + +const ( + terminateTimeout = 30 * time.Second ) func execAdminProcess(ctx context.Context, cfg LinuxAdminProcessConfig) error { + if err := checkDBusServiceAvailability(ctx); err != nil { + if os.Geteuid() == 0 { + log.WarnContext(ctx, "VNet daemon not available via D-Bus, running daemon as a child process", "error", err) + return trace.Wrap(runAdminSubcommand(ctx, cfg)) + } else { + return trace.Wrap(err) + } + } + + return trace.Wrap(runService(ctx, cfg)) +} + +func runService(ctx context.Context, cfg LinuxAdminProcessConfig) error { + err := startService(ctx, cfg) + if err != nil { + return trace.Wrap(err) + } + log.InfoContext(ctx, "Started systemd service", "service", vnetSystemdUnitName) + + conn, err := systemddbus.NewWithContext(ctx) + if err != nil { + return trace.NotFound("systemd D-Bus is unavailable: %v", err) + } + defer conn.Close() + + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + +loop: + for { + select { + case <-ctx.Done(): + log.InfoContext(ctx, "Context canceled, stopping systemd service") + err := stopService(ctx) + if err != nil { + return trace.Wrap(err) + } + break loop + case <-ticker.C: + state, err := systemdUnitActiveState(ctx, conn, vnetSystemdUnitName) + if err != nil { + return trace.Wrap(err, "querying systemd service %s", vnetSystemdUnitName) + } + if state != "active" && state != "activating" { + return trace.Errorf("service stopped running prematurely, status: %s", state) + } + } + } + + // Wait for the service to actually stop + deadline := time.After(terminateTimeout + 5*time.Second) + for { + select { + case <-deadline: + return trace.Errorf("systemd service %s failed to stop with %v", vnetSystemdUnitName, terminateTimeout) + case <-ticker.C: + state, err := systemdUnitActiveState(ctx, conn, vnetSystemdUnitName) + if err != nil { + return trace.Wrap(err, "querying systemd service %s", vnetSystemdUnitName) + } + if state == "inactive" { + return nil + } + } + } +} + +func systemdUnitActiveState(ctx context.Context, conn *systemddbus.Conn, unit string) (string, error) { + props, err := conn.GetUnitPropertiesContext(ctx, unit) + if err != nil { + return "", err + } + state, ok := props["ActiveState"].(string) + if !ok || state == "" { + return "", trace.Errorf("systemd ActiveState is missing for %s", unit) + } + return state, nil +} + +func checkDBusServiceAvailability(ctx context.Context) error { + conn, err := dbus.ConnectSystemBus() + if err != nil { + return trace.Wrap(err, "system D-Bus is unavailable") + } + defer conn.Close() + + // Check if the service is already running. + var hasOwner bool + err = conn.Object("org.freedesktop.DBus", "/org/freedesktop/DBus"). + CallWithContext(ctx, "org.freedesktop.DBus.NameHasOwner", 0, vnetDBusServiceName). + Store(&hasOwner) + if err != nil { + return trace.Wrap(err, "checking D-Bus service owner") + } + if hasOwner { + return nil + } + + // If it is not running, check that it can be activated via D-Bus. + var services []string + err = conn.Object("org.freedesktop.DBus", "/org/freedesktop/DBus"). + CallWithContext(ctx, "org.freedesktop.DBus.ListActivatableNames", 0). + Store(&services) + if err != nil { + return trace.Wrap(err, "listing activatable D-Bus names") + } + + if slices.Contains(services, vnetDBusServiceName) { + return nil + } else { + return trace.Errorf("D-Bus service %s is not available", vnetDBusServiceName) + } + // TODO: Maybe also check the systemd unit file exists. D-Bus can report a name + // as activatable even if the corresponding systemd unit is missing. +} + +func runAdminSubcommand(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", + cmd := exec.CommandContext(ctx, executableName, "-d", + teleport.VnetAdminSetupSubCommand, "--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") + return trace.Wrap(cmd.Run(), "running %s", teleport.VnetAdminSetupSubCommand) } diff --git a/tool/tsh/common/vnet_daemon_linux.go b/tool/tsh/common/vnet_daemon_linux.go new file mode 100644 index 0000000000000..1acad351dae83 --- /dev/null +++ b/tool/tsh/common/vnet_daemon_linux.go @@ -0,0 +1,56 @@ +// 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 linux + +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 linux the command must match the command provided in the systemd unit file. + vnetDaemonSubCommand = "vnet-daemon" +) + +// vnetDaemonCommand implements the vnet-daemon subcommand to run the VNet D-Bus daemon. +type vnetDaemonCommand struct { + *kingpin.CmdClause +} + +func newPlatformVnetDaemonCommand(app *kingpin.Application) *vnetDaemonCommand { + return &vnetDaemonCommand{ + CmdClause: app.Command(vnetDaemonSubCommand, "Start the VNet D-Bus daemon.").Hidden(), + } +} + +func (c *vnetDaemonCommand) run(cf *CLIConf) error { + level := slog.LevelInfo + if cf.Debug { + level = slog.LevelDebug + } + if _, err := utils.InitLogger(utils.LoggingForDaemon, level); err != nil { + return trace.Wrap(err, "initializing logger") + } + return trace.Wrap(vnet.RunLinuxDBusService(cf.Context)) +} diff --git a/tool/tsh/common/vnet_linux.go b/tool/tsh/common/vnet_linux.go index 76ae4f3679ffe..bece2f896ae13 100644 --- a/tool/tsh/common/vnet_linux.go +++ b/tool/tsh/common/vnet_linux.go @@ -22,33 +22,36 @@ import ( "github.com/alecthomas/kingpin/v2" "github.com/gravitational/trace" + "github.com/gravitational/teleport" 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 { +// vnetAdminSetupCommand is the fallback command ran as root when there is no +// available vnet daemon on system. +type vnetAdminSetupCommand struct { *kingpin.CmdClause cfg vnet.LinuxAdminProcessConfig } -func (c *vnetServiceCommand) run(clf *CLIConf) error { +func (c *vnetAdminSetupCommand) 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(), +func newPlatformVnetAdminSetupCommand(app *kingpin.Application) *vnetAdminSetupCommand { + cmd := &vnetAdminSetupCommand{ + CmdClause: app.Command(teleport.VnetAdminSetupSubCommand, "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) + 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-service command is only supported on windows. +func newPlatformVnetServiceCommand(app *kingpin.Application) vnetCommandNotSupported { + return vnetCommandNotSupported{} +} + // The vnet-install-service command is only supported on windows. func newPlatformVnetInstallServiceCommand(app *kingpin.Application) vnetCommandNotSupported { return vnetCommandNotSupported{} diff --git a/tool/tsh/common/vnet_nodaemon.go b/tool/tsh/common/vnet_nodaemon.go index c3e0917e046dd..ee536be15dfc3 100644 --- a/tool/tsh/common/vnet_nodaemon.go +++ b/tool/tsh/common/vnet_nodaemon.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 !vnetdaemon || !darwin +//go:build (!vnetdaemon || !darwin) && !linux package common From fea6ed076e663893bc9ea1c04e1514e39e14541d Mon Sep 17 00:00:00 2001 From: Pavel Evdokimov Date: Mon, 9 Feb 2026 14:46:00 +0000 Subject: [PATCH 03/26] set up dns server via systemd-resolved d-bus api --- lib/vnet/osconfig_linux.go | 180 +++++++++++++++++++++++++++++++------ 1 file changed, 154 insertions(+), 26 deletions(-) diff --git a/lib/vnet/osconfig_linux.go b/lib/vnet/osconfig_linux.go index b13573c958187..c316fea404a0a 100644 --- a/lib/vnet/osconfig_linux.go +++ b/lib/vnet/osconfig_linux.go @@ -18,10 +18,14 @@ package vnet import ( "context" + "net" "slices" + "syscall" - "github.com/gravitational/teleport/api/utils" + "github.com/godbus/dbus/v5" "github.com/gravitational/trace" + + "github.com/gravitational/teleport/api/utils" ) type platformOSConfigState struct { @@ -31,13 +35,12 @@ type platformOSConfigState struct { configuredNameserver bool configuredDNSZones []string broughtUpInterface bool + tunName string } // 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. + // TODO: use github.com/vishvananda/netlink to set up IPs and routes? 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" @@ -58,28 +61,10 @@ func platformConfigureOS(ctx context.Context, cfg *osConfig, state *platformOSCo } 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 err := configureDNS(ctx, cfg, state); err != nil { + return trace.Wrap(err, "configuring DNS") } - if state.configuredIPv6 && state.configuredNameserver && !state.broughtUpInterface { + if (state.configuredIPv4 || 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", @@ -95,7 +80,7 @@ func platformConfigureOS(ctx context.Context, cfg *osConfig, state *platformOSCo } log.InfoContext(ctx, "Setting an IPv4 route", "netmask", cidrRange) if err := runCommand(ctx, - "ip", "route", "add", cidrRange, "via", cfg.tunIPv4, "dev", cfg.tunName, + "ip", "route", "add", cidrRange, "dev", cfg.tunName, ); err != nil { return trace.Wrap(err) } @@ -108,3 +93,146 @@ func platformConfigureOS(ctx context.Context, cfg *osConfig, state *platformOSCo func shouldReconfiguredDNSZones(cfg *osConfig, state *platformOSConfigState) bool { return !utils.ContainSameUniqueElements(cfg.dnsZones, state.configuredDNSZones) } + +const ( + systemdResolvedService = "org.freedesktop.resolve1" + systemdResolvedObjectPath = "/org/freedesktop/resolve1" + systemdResolvedManager = "org.freedesktop.resolve1.Manager" + systemdResolvedSetLinkDNS = systemdResolvedManager + ".SetLinkDNS" + systemdResolvedSetDomains = systemdResolvedManager + ".SetLinkDomains" + systemdResolvedSetDefaultRoute = systemdResolvedManager + ".SetLinkDefaultRoute" +) + +type systemdResolvedDNSAddress struct { + Family int32 + Address []byte +} + +type systemdResolvedDomain struct { + Domain string + RoutingOnly bool +} + +func configureDNS(ctx context.Context, cfg *osConfig, state *platformOSConfigState) error { + // systemd-resolved stores DNS settings per network link. For VNet + // we configure DNS on the TUN link. The TUN is ephemeral, when + // the admin process exits and the TUN interface is deleted, + // systemd-resolved deletes the link and its DNS configuration. + // So we don't need additional DNS cleanup on restart + if cfg.tunName != "" { + state.tunName = cfg.tunName + } + if len(cfg.dnsAddrs) == 0 && len(cfg.dnsZones) > 0 { + return trace.BadParameter("empty nameserver with non-empty zones") + } + if len(cfg.dnsAddrs) > 0 && cfg.tunName == "" { + return trace.BadParameter("empty TUN interface name with non-empty nameserver") + } + + if err := checkSystemdResolvedAvailability(ctx); err != nil { + return err + } + conn, err := dbus.ConnectSystemBus() + if err != nil { + return trace.NotFound("system D-Bus is unavailable: %v", err) + } + defer conn.Close() + obj := conn.Object(systemdResolvedService, dbus.ObjectPath(systemdResolvedObjectPath)) + + if shouldReconfiguredDNSZones(cfg, state) { + iface, err := net.InterfaceByName(state.tunName) + if err != nil { + return trace.Wrap(err, "looking up interface %s", state.tunName) + } + log.InfoContext(ctx, "Configuring DNS zones", "zones", cfg.dnsZones) + domains := make([]systemdResolvedDomain, 0, len(cfg.dnsZones)) + for _, dnsZone := range cfg.dnsZones { + domains = append(domains, systemdResolvedDomain{ + Domain: dnsZone, + RoutingOnly: true, + }) + } + // Equivalent to: resolvectl domain ~ ~ ... + call := obj.CallWithContext(ctx, systemdResolvedSetDomains, 0, int32(iface.Index), domains) + if call.Err != nil { + return trace.Wrap(call.Err, "setting systemd-resolved link domains") + } + state.configuredDNSZones = cfg.dnsZones + } + + if len(cfg.dnsAddrs) > 0 && state.tunName != "" && !state.configuredNameserver { + iface, err := net.InterfaceByName(state.tunName) + if err != nil { + return trace.Wrap(err, "looking up interface %s", state.tunName) + } + addresses := make([]systemdResolvedDNSAddress, 0, len(cfg.dnsAddrs)) + for _, addr := range cfg.dnsAddrs { + address, err := systemdResolvedDNSAddressForIP(addr) + if err != nil { + return trace.Wrap(err, "parsing DNS nameserver %q", addr) + } + addresses = append(addresses, address) + } + log.InfoContext(ctx, "Configuring DNS nameserver", "nameservers", cfg.dnsAddrs) + // Equivalent to: resolvectl default-route false + call := obj.CallWithContext(ctx, systemdResolvedSetDefaultRoute, 0, int32(iface.Index), false) + if call.Err != nil { + return trace.Wrap(call.Err, "setting systemd-resolved link default route") + } + + // Equivalent to: resolvectl dns ... + call = obj.CallWithContext(ctx, systemdResolvedSetLinkDNS, 0, int32(iface.Index), addresses) + if call.Err != nil { + return trace.Wrap(call.Err, "setting systemd-resolved link DNS") + } + state.configuredNameserver = true + } + + return nil +} + +func systemdResolvedDNSAddressForIP(raw string) (systemdResolvedDNSAddress, error) { + ip := net.ParseIP(raw) + if ip == nil { + return systemdResolvedDNSAddress{}, trace.BadParameter("invalid IP address") + } + if ip4 := ip.To4(); ip4 != nil { + return systemdResolvedDNSAddress{ + Family: syscall.AF_INET, + Address: []byte(ip4), + }, nil + } + if ip16 := ip.To16(); ip16 != nil { + return systemdResolvedDNSAddress{ + Family: syscall.AF_INET6, + Address: []byte(ip16), + }, nil + } + return systemdResolvedDNSAddress{}, trace.BadParameter("unsupported IP address") +} + +func checkSystemdResolvedAvailability(ctx context.Context) error { + conn, err := dbus.ConnectSystemBus() + if err != nil { + return trace.Wrap(err, "system D-Bus is unavailable") + } + defer conn.Close() + + var hasOwner bool + err = conn.Object("org.freedesktop.DBus", "/org/freedesktop/DBus"). + CallWithContext(ctx, "org.freedesktop.DBus.NameHasOwner", 0, systemdResolvedService). + Store(&hasOwner) + if err != nil { + return trace.Wrap(err, "checking systemd-resolved D-Bus service owner") + } + if hasOwner { + return nil + } + + return trace.Errorf( + "systemd-resolved is not running (D-Bus service %s has no owner).\n"+ + "you can enable it with:\n"+ + " sudo systemctl enable --now systemd-resolved\n", + systemdResolvedService, + ) +} From c86be2e6a5ad4f7fd45f828c72929678cad72f2f Mon Sep 17 00:00:00 2001 From: Pavel Evdokimov Date: Fri, 13 Feb 2026 13:59:29 +0000 Subject: [PATCH 04/26] get DNS upstreams via systemd-resolved API --- ...ervers_unix.go => osnameservers_darwin.go} | 21 +---- lib/vnet/dns/osnameservers_linux.go | 92 +++++++++++++++++++ 2 files changed, 96 insertions(+), 17 deletions(-) rename lib/vnet/dns/{osnameservers_unix.go => osnameservers_darwin.go} (76%) create mode 100644 lib/vnet/dns/osnameservers_linux.go diff --git a/lib/vnet/dns/osnameservers_unix.go b/lib/vnet/dns/osnameservers_darwin.go similarity index 76% rename from lib/vnet/dns/osnameservers_unix.go rename to lib/vnet/dns/osnameservers_darwin.go index b45a9af3a98dd..8f765ad4a9701 100644 --- a/lib/vnet/dns/osnameservers_unix.go +++ b/lib/vnet/dns/osnameservers_darwin.go @@ -14,8 +14,6 @@ // 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 ( @@ -24,12 +22,15 @@ 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 @@ -37,20 +38,6 @@ import ( // 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) { - // 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) diff --git a/lib/vnet/dns/osnameservers_linux.go b/lib/vnet/dns/osnameservers_linux.go new file mode 100644 index 0000000000000..1e1aebd3c8243 --- /dev/null +++ b/lib/vnet/dns/osnameservers_linux.go @@ -0,0 +1,92 @@ +// Teleport +// Copyright (C) 2026 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 dns + +import ( + "context" + "log/slog" + "net/netip" + + "github.com/godbus/dbus/v5" + "github.com/gravitational/trace" +) + +const ( + systemdResolvedService = "org.freedesktop.resolve1" + systemdResolvedObjectPath = "/org/freedesktop/resolve1" + systemdResolvedManager = "org.freedesktop.resolve1.Manager" + + systemdResolvedDNSProperty = "DNS" +) + +type systemdResolvedDNS struct { + InterfaceIndex int32 + Family int32 + Address []byte +} + +// platformLoadUpstreamNameservers returns the list of DNS upstreams configured in systemd-resolved. +func platformLoadUpstreamNameservers(ctx context.Context) ([]string, error) { + conn, err := dbus.ConnectSystemBus() + if err != nil { + return nil, trace.NotFound("system D-Bus is unavailable: %v", err) + } + defer conn.Close() + + var hasOwner bool + err = conn.Object("org.freedesktop.DBus", "/org/freedesktop/DBus"). + CallWithContext(ctx, "org.freedesktop.DBus.NameHasOwner", 0, systemdResolvedService). + Store(&hasOwner) + if err != nil { + return nil, trace.Wrap(err, "checking systemd-resolved D-Bus service owner") + } + if !hasOwner { + return nil, trace.Errorf("systemd-resolved D-Bus service %s is not available", systemdResolvedService) + } + + obj := conn.Object(systemdResolvedService, dbus.ObjectPath(systemdResolvedObjectPath)) + call := obj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, systemdResolvedManager, systemdResolvedDNSProperty) + if call.Err != nil { + return nil, trace.Wrap(call.Err, "getting systemd-resolved property %s", systemdResolvedDNSProperty) + } + + var variant dbus.Variant + if err := call.Store(&variant); err != nil { + return nil, trace.Wrap(err, "decoding systemd-resolved property %s", systemdResolvedDNSProperty) + } + + var dns []systemdResolvedDNS + if err := dbus.Store([]any{variant.Value()}, &dns); err != nil { + return nil, trace.Wrap(err, "decoding systemd-resolved property %s", systemdResolvedDNSProperty) + } + + nameservers := make([]string, 0, len(dns)) + for _, entry := range dns { + addr, ok := netip.AddrFromSlice(entry.Address) + if !ok { + slog.DebugContext(ctx, "Skipping invalid DNS server address", "address_bytes", entry.Address) + continue + } + if addr.IsUnspecified() { + continue + } + nameservers = append(nameservers, withDNSPort(addr)) + } + + slog.DebugContext(ctx, "Loaded host upstream nameservers", "nameservers", nameservers) + return nameservers, nil +} From ec7bca28118c1e33dc19d8ce0ab88b38912314d9 Mon Sep 17 00:00:00 2001 From: Pavel Evdokimov Date: Fri, 13 Feb 2026 18:41:11 +0000 Subject: [PATCH 05/26] never forward DNS requests to self --- lib/vnet/dns/osnameservers.go | 2 +- lib/vnet/dns/osnameservers_darwin.go | 2 +- lib/vnet/dns/osnameservers_linux.go | 2 +- lib/vnet/dns/osnameservers_other.go | 2 +- lib/vnet/dns/osnameservers_windows.go | 2 +- lib/vnet/network_stack.go | 63 ++++++++++++++++++++++++++- 6 files changed, 67 insertions(+), 6 deletions(-) diff --git a/lib/vnet/dns/osnameservers.go b/lib/vnet/dns/osnameservers.go index f8678e6652879..e06fe0495ce46 100644 --- a/lib/vnet/dns/osnameservers.go +++ b/lib/vnet/dns/osnameservers.go @@ -56,6 +56,6 @@ func loadUpstreamNameservers(ctx context.Context) ([]string, error) { return platformLoadUpstreamNameservers(ctx) } -func withDNSPort(addr netip.Addr) string { +func WithDNSPort(addr netip.Addr) string { return netip.AddrPortFrom(addr, 53).String() } diff --git a/lib/vnet/dns/osnameservers_darwin.go b/lib/vnet/dns/osnameservers_darwin.go index 8f765ad4a9701..5a952712fa130 100644 --- a/lib/vnet/dns/osnameservers_darwin.go +++ b/lib/vnet/dns/osnameservers_darwin.go @@ -63,7 +63,7 @@ func platformLoadUpstreamNameservers(ctx context.Context) ([]string, error) { continue } - nameservers = append(nameservers, withDNSPort(ip)) + nameservers = append(nameservers, WithDNSPort(ip)) } slog.DebugContext(ctx, "Loaded host upstream nameservers.", "nameservers", nameservers, "config_file", confFilePath) diff --git a/lib/vnet/dns/osnameservers_linux.go b/lib/vnet/dns/osnameservers_linux.go index 1e1aebd3c8243..e29420778a844 100644 --- a/lib/vnet/dns/osnameservers_linux.go +++ b/lib/vnet/dns/osnameservers_linux.go @@ -84,7 +84,7 @@ func platformLoadUpstreamNameservers(ctx context.Context) ([]string, error) { if addr.IsUnspecified() { continue } - nameservers = append(nameservers, withDNSPort(addr)) + nameservers = append(nameservers, WithDNSPort(addr)) } slog.DebugContext(ctx, "Loaded host upstream nameservers", "nameservers", nameservers) diff --git a/lib/vnet/dns/osnameservers_other.go b/lib/vnet/dns/osnameservers_other.go index b8db3e96e31e9..961d740e5fcb5 100644 --- a/lib/vnet/dns/osnameservers_other.go +++ b/lib/vnet/dns/osnameservers_other.go @@ -30,7 +30,7 @@ var ( vnetNotImplemented = &trace.NotImplementedError{Message: "VNet is not implemented on " + runtime.GOOS} // Satisfy unused linter. - _ = withDNSPort + _ = WithDNSPort ) func platformLoadUpstreamNameservers(ctx context.Context) ([]string, error) { diff --git a/lib/vnet/dns/osnameservers_windows.go b/lib/vnet/dns/osnameservers_windows.go index 2db2063761bd9..1df944248b446 100644 --- a/lib/vnet/dns/osnameservers_windows.go +++ b/lib/vnet/dns/osnameservers_windows.go @@ -48,7 +48,7 @@ func platformLoadUpstreamNameservers(ctx context.Context) ([]string, error) { if ignoreUpstreamNameserver(ifaceNameserver) { continue } - nameservers = append(nameservers, withDNSPort(ifaceNameserver)) + nameservers = append(nameservers, WithDNSPort(ifaceNameserver)) } } slog.DebugContext(ctx, "Loaded host upstream nameservers", "nameservers", nameservers) diff --git a/lib/vnet/network_stack.go b/lib/vnet/network_stack.go index b2bf30d67d9fa..8c78213bc6868 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" @@ -161,6 +162,8 @@ type networkStack struct { // dnsServer is the VNet's local DNS server that can handle UDP DNS // requests. dnsServer *dns.Server + // upstreamFilter removes VNet DNS addresses from upstream nameserver lists. + upstreamFilter *filteredUpstreamSource // tcpHandlerResolver resolves 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. @@ -183,6 +186,48 @@ type networkStack struct { slog *slog.Logger } +type filteredUpstreamSource struct { + base dns.UpstreamNameserverSource + mu sync.RWMutex + exclude map[string]struct{} +} + +func newFilteredUpstreamSource(base dns.UpstreamNameserverSource) *filteredUpstreamSource { + return &filteredUpstreamSource{ + base: base, + exclude: make(map[string]struct{}), + } +} + +func (f *filteredUpstreamSource) AddExclude(addr string) { + if addr == "" { + return + } + f.mu.Lock() + defer f.mu.Unlock() + f.exclude[addr] = struct{}{} +} + +func (f *filteredUpstreamSource) UpstreamNameservers(ctx context.Context) ([]string, error) { + nameservers, err := f.base.UpstreamNameservers(ctx) + if err != nil { + return nil, err + } + f.mu.RLock() + defer f.mu.RUnlock() + if len(f.exclude) == 0 { + return nameservers, nil + } + filtered := nameservers[:0] + for _, nameserver := range nameservers { + if _, ok := f.exclude[nameserver]; ok { + continue + } + filtered = append(filtered, nameserver) + } + return filtered, nil +} + type state struct { // mu is a single mutex that protects the whole state struct. This could be optimized as necessary. mu sync.RWMutex @@ -244,7 +289,16 @@ func newNetworkStack(cfg *networkStackConfig) (*networkStack, error) { return nil, trace.Wrap(err) } } - dnsServer, err := dns.NewServer(ns, upstreamNameserverSource) + upstreamFilter := newFilteredUpstreamSource(upstreamNameserverSource) + ns.upstreamFilter = upstreamFilter + if cfg.dnsIPv6 != (tcpip.Address{}) { + addr, ok := netip.AddrFromSlice(cfg.dnsIPv6.AsSlice()) + if !ok { + return nil, trace.Errorf("error parsing IPv6 DNS address %v", cfg.dnsIPv6) + } + upstreamFilter.AddExclude(dns.WithDNSPort(addr)) + } + dnsServer, err := dns.NewServer(ns, upstreamFilter) if err != nil { return nil, trace.Wrap(err) } @@ -556,6 +610,13 @@ func (ns *networkStack) assignUDPHandler(addr tcpip.Address, handler udpHandler) // addDNSAddress adds a DNS handler at the given IP. func (ns *networkStack) addDNSAddress(ip net.IP) error { slog.DebugContext(context.Background(), "Serving DNS on IPv4.", "dns_addr", ip.String()) + addr, ok := netip.AddrFromSlice(ip) + if !ok { + return trace.Errorf("error parsing IPv4 DNS address %s", ip.String()) + } + if ns.upstreamFilter != nil { + ns.upstreamFilter.AddExclude(dns.WithDNSPort(addr)) + } return trace.Wrap(ns.assignUDPHandler(tcpip.AddrFromSlice(ip), ns.dnsServer), "adding UDP handler at %s", ip.String()) } From 7d06ab63956e60659dbd755b622d3c3498e5acf2 Mon Sep 17 00:00:00 2001 From: Pavel Evdokimov Date: Fri, 13 Feb 2026 21:32:43 +0000 Subject: [PATCH 06/26] refactor: move all systemd-resolved logic to a separate package --- lib/vnet/dns/osnameservers_linux.go | 43 ++------ lib/vnet/osconfig_linux.go | 97 +++-------------- lib/vnet/systemdresolved/dbus.go | 159 ++++++++++++++++++++++++++++ 3 files changed, 179 insertions(+), 120 deletions(-) create mode 100644 lib/vnet/systemdresolved/dbus.go diff --git a/lib/vnet/dns/osnameservers_linux.go b/lib/vnet/dns/osnameservers_linux.go index e29420778a844..c466dbf0e2087 100644 --- a/lib/vnet/dns/osnameservers_linux.go +++ b/lib/vnet/dns/osnameservers_linux.go @@ -23,22 +23,10 @@ import ( "github.com/godbus/dbus/v5" "github.com/gravitational/trace" -) - -const ( - systemdResolvedService = "org.freedesktop.resolve1" - systemdResolvedObjectPath = "/org/freedesktop/resolve1" - systemdResolvedManager = "org.freedesktop.resolve1.Manager" - systemdResolvedDNSProperty = "DNS" + "github.com/gravitational/teleport/lib/vnet/systemdresolved" ) -type systemdResolvedDNS struct { - InterfaceIndex int32 - Family int32 - Address []byte -} - // platformLoadUpstreamNameservers returns the list of DNS upstreams configured in systemd-resolved. func platformLoadUpstreamNameservers(ctx context.Context) ([]string, error) { conn, err := dbus.ConnectSystemBus() @@ -46,32 +34,13 @@ func platformLoadUpstreamNameservers(ctx context.Context) ([]string, error) { return nil, trace.NotFound("system D-Bus is unavailable: %v", err) } defer conn.Close() - - var hasOwner bool - err = conn.Object("org.freedesktop.DBus", "/org/freedesktop/DBus"). - CallWithContext(ctx, "org.freedesktop.DBus.NameHasOwner", 0, systemdResolvedService). - Store(&hasOwner) - if err != nil { - return nil, trace.Wrap(err, "checking systemd-resolved D-Bus service owner") - } - if !hasOwner { - return nil, trace.Errorf("systemd-resolved D-Bus service %s is not available", systemdResolvedService) - } - - obj := conn.Object(systemdResolvedService, dbus.ObjectPath(systemdResolvedObjectPath)) - call := obj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, systemdResolvedManager, systemdResolvedDNSProperty) - if call.Err != nil { - return nil, trace.Wrap(call.Err, "getting systemd-resolved property %s", systemdResolvedDNSProperty) + if err := systemdresolved.CheckAvailability(ctx, conn); err != nil { + return nil, err } - var variant dbus.Variant - if err := call.Store(&variant); err != nil { - return nil, trace.Wrap(err, "decoding systemd-resolved property %s", systemdResolvedDNSProperty) - } - - var dns []systemdResolvedDNS - if err := dbus.Store([]any{variant.Value()}, &dns); err != nil { - return nil, trace.Wrap(err, "decoding systemd-resolved property %s", systemdResolvedDNSProperty) + dns, err := systemdresolved.LoadConfiguredDNSServers(ctx, conn) + if err != nil { + return nil, err } nameservers := make([]string, 0, len(dns)) diff --git a/lib/vnet/osconfig_linux.go b/lib/vnet/osconfig_linux.go index c316fea404a0a..e9662dd0bad00 100644 --- a/lib/vnet/osconfig_linux.go +++ b/lib/vnet/osconfig_linux.go @@ -20,12 +20,12 @@ import ( "context" "net" "slices" - "syscall" "github.com/godbus/dbus/v5" "github.com/gravitational/trace" "github.com/gravitational/teleport/api/utils" + "github.com/gravitational/teleport/lib/vnet/systemdresolved" ) type platformOSConfigState struct { @@ -94,25 +94,6 @@ func shouldReconfiguredDNSZones(cfg *osConfig, state *platformOSConfigState) boo return !utils.ContainSameUniqueElements(cfg.dnsZones, state.configuredDNSZones) } -const ( - systemdResolvedService = "org.freedesktop.resolve1" - systemdResolvedObjectPath = "/org/freedesktop/resolve1" - systemdResolvedManager = "org.freedesktop.resolve1.Manager" - systemdResolvedSetLinkDNS = systemdResolvedManager + ".SetLinkDNS" - systemdResolvedSetDomains = systemdResolvedManager + ".SetLinkDomains" - systemdResolvedSetDefaultRoute = systemdResolvedManager + ".SetLinkDefaultRoute" -) - -type systemdResolvedDNSAddress struct { - Family int32 - Address []byte -} - -type systemdResolvedDomain struct { - Domain string - RoutingOnly bool -} - func configureDNS(ctx context.Context, cfg *osConfig, state *platformOSConfigState) error { // systemd-resolved stores DNS settings per network link. For VNet // we configure DNS on the TUN link. The TUN is ephemeral, when @@ -129,15 +110,14 @@ func configureDNS(ctx context.Context, cfg *osConfig, state *platformOSConfigSta return trace.BadParameter("empty TUN interface name with non-empty nameserver") } - if err := checkSystemdResolvedAvailability(ctx); err != nil { - return err - } conn, err := dbus.ConnectSystemBus() if err != nil { return trace.NotFound("system D-Bus is unavailable: %v", err) } defer conn.Close() - obj := conn.Object(systemdResolvedService, dbus.ObjectPath(systemdResolvedObjectPath)) + if err := systemdresolved.CheckAvailability(ctx, conn); err != nil { + return err + } if shouldReconfiguredDNSZones(cfg, state) { iface, err := net.InterfaceByName(state.tunName) @@ -145,17 +125,16 @@ func configureDNS(ctx context.Context, cfg *osConfig, state *platformOSConfigSta return trace.Wrap(err, "looking up interface %s", state.tunName) } log.InfoContext(ctx, "Configuring DNS zones", "zones", cfg.dnsZones) - domains := make([]systemdResolvedDomain, 0, len(cfg.dnsZones)) + domains := make([]systemdresolved.Domain, 0, len(cfg.dnsZones)) for _, dnsZone := range cfg.dnsZones { - domains = append(domains, systemdResolvedDomain{ + domains = append(domains, systemdresolved.Domain{ Domain: dnsZone, RoutingOnly: true, }) } // Equivalent to: resolvectl domain ~ ~ ... - call := obj.CallWithContext(ctx, systemdResolvedSetDomains, 0, int32(iface.Index), domains) - if call.Err != nil { - return trace.Wrap(call.Err, "setting systemd-resolved link domains") + if err := systemdresolved.SetLinkDomains(ctx, conn, int32(iface.Index), domains); err != nil { + return err } state.configuredDNSZones = cfg.dnsZones } @@ -165,9 +144,9 @@ func configureDNS(ctx context.Context, cfg *osConfig, state *platformOSConfigSta if err != nil { return trace.Wrap(err, "looking up interface %s", state.tunName) } - addresses := make([]systemdResolvedDNSAddress, 0, len(cfg.dnsAddrs)) + addresses := make([]systemdresolved.DNSAddress, 0, len(cfg.dnsAddrs)) for _, addr := range cfg.dnsAddrs { - address, err := systemdResolvedDNSAddressForIP(addr) + address, err := systemdresolved.DNSAddressForIP(addr) if err != nil { return trace.Wrap(err, "parsing DNS nameserver %q", addr) } @@ -175,64 +154,16 @@ func configureDNS(ctx context.Context, cfg *osConfig, state *platformOSConfigSta } log.InfoContext(ctx, "Configuring DNS nameserver", "nameservers", cfg.dnsAddrs) // Equivalent to: resolvectl default-route false - call := obj.CallWithContext(ctx, systemdResolvedSetDefaultRoute, 0, int32(iface.Index), false) - if call.Err != nil { - return trace.Wrap(call.Err, "setting systemd-resolved link default route") + if err := systemdresolved.SetLinkDefaultRoute(ctx, conn, int32(iface.Index), false); err != nil { + return err } // Equivalent to: resolvectl dns ... - call = obj.CallWithContext(ctx, systemdResolvedSetLinkDNS, 0, int32(iface.Index), addresses) - if call.Err != nil { - return trace.Wrap(call.Err, "setting systemd-resolved link DNS") + if err := systemdresolved.SetLinkDNS(ctx, conn, int32(iface.Index), addresses); err != nil { + return err } state.configuredNameserver = true } return nil } - -func systemdResolvedDNSAddressForIP(raw string) (systemdResolvedDNSAddress, error) { - ip := net.ParseIP(raw) - if ip == nil { - return systemdResolvedDNSAddress{}, trace.BadParameter("invalid IP address") - } - if ip4 := ip.To4(); ip4 != nil { - return systemdResolvedDNSAddress{ - Family: syscall.AF_INET, - Address: []byte(ip4), - }, nil - } - if ip16 := ip.To16(); ip16 != nil { - return systemdResolvedDNSAddress{ - Family: syscall.AF_INET6, - Address: []byte(ip16), - }, nil - } - return systemdResolvedDNSAddress{}, trace.BadParameter("unsupported IP address") -} - -func checkSystemdResolvedAvailability(ctx context.Context) error { - conn, err := dbus.ConnectSystemBus() - if err != nil { - return trace.Wrap(err, "system D-Bus is unavailable") - } - defer conn.Close() - - var hasOwner bool - err = conn.Object("org.freedesktop.DBus", "/org/freedesktop/DBus"). - CallWithContext(ctx, "org.freedesktop.DBus.NameHasOwner", 0, systemdResolvedService). - Store(&hasOwner) - if err != nil { - return trace.Wrap(err, "checking systemd-resolved D-Bus service owner") - } - if hasOwner { - return nil - } - - return trace.Errorf( - "systemd-resolved is not running (D-Bus service %s has no owner).\n"+ - "you can enable it with:\n"+ - " sudo systemctl enable --now systemd-resolved\n", - systemdResolvedService, - ) -} diff --git a/lib/vnet/systemdresolved/dbus.go b/lib/vnet/systemdresolved/dbus.go new file mode 100644 index 0000000000000..f273cd222fd46 --- /dev/null +++ b/lib/vnet/systemdresolved/dbus.go @@ -0,0 +1,159 @@ +// Teleport +// Copyright (C) 2026 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 systemdresolved + +import ( + "context" + "net" + "syscall" + + "github.com/godbus/dbus/v5" + "github.com/gravitational/trace" +) + +const ( + DBusService = "org.freedesktop.DBus" + DBusObjectPath = "/org/freedesktop/DBus" + DBusNameHasOwner = "org.freedesktop.DBus.NameHasOwner" + DBusPropertiesGet = "org.freedesktop.DBus.Properties.Get" + + Service = "org.freedesktop.resolve1" + ObjectPath = "/org/freedesktop/resolve1" + Manager = "org.freedesktop.resolve1.Manager" + + SetLinkDNSMethod = Manager + ".SetLinkDNS" + SetDomainsMethod = Manager + ".SetLinkDomains" + SetDefaultRouteMethod = Manager + ".SetLinkDefaultRoute" + + DNSProperty = "DNS" +) + +// DNSAddress is the systemd-resolved representation of a DNS address. +type DNSAddress struct { + Family int32 + Address []byte +} + +// Domain is the systemd-resolved representation of a DNS domain. +type Domain struct { + Domain string + RoutingOnly bool +} + +// DNS is a single DNS server entry from systemd-resolved's DNS property. +type DNS struct { + InterfaceIndex int32 + Family int32 + Address []byte +} + +// CheckAvailability returns an error if systemd-resolved is unavailable on D-Bus. +func CheckAvailability(ctx context.Context, conn *dbus.Conn) error { + var hasOwner bool + err := conn.Object(DBusService, dbus.ObjectPath(DBusObjectPath)). + CallWithContext(ctx, DBusNameHasOwner, 0, Service). + Store(&hasOwner) + if err != nil { + return trace.Wrap(err, "checking systemd-resolved D-Bus service owner") + } + if hasOwner { + return nil + } + return trace.Errorf( + "systemd-resolved is not running (D-Bus service %s has no owner).\n"+ + "you can enable it with:\n"+ + " sudo systemctl enable --now systemd-resolved\n", + Service, + ) +} + +// Object returns the systemd-resolved D-Bus object. +func Object(conn *dbus.Conn) dbus.BusObject { + return conn.Object(Service, dbus.ObjectPath(ObjectPath)) +} + +// LoadConfiguredDNSServers returns DNS servers currently configured in systemd-resolved. +func LoadConfiguredDNSServers(ctx context.Context, conn *dbus.Conn) ([]DNS, error) { + return loadDNSProperty(ctx, conn) +} + +// loadDNSProperty loads the systemd-resolved DNS property via D-Bus. +func loadDNSProperty(ctx context.Context, conn *dbus.Conn) ([]DNS, error) { + call := Object(conn).CallWithContext(ctx, DBusPropertiesGet, 0, Manager, DNSProperty) + if call.Err != nil { + return nil, trace.Wrap(call.Err, "getting systemd-resolved property %s", DNSProperty) + } + + var variant dbus.Variant + if err := call.Store(&variant); err != nil { + return nil, trace.Wrap(err, "decoding systemd-resolved property %s", DNSProperty) + } + + var dns []DNS + if err := dbus.Store([]any{variant.Value()}, &dns); err != nil { + return nil, trace.Wrap(err, "decoding systemd-resolved property %s", DNSProperty) + } + return dns, nil +} + +// SetLinkDomains configures per-link DNS search domains. +func SetLinkDomains(ctx context.Context, conn *dbus.Conn, ifaceIndex int32, domains []Domain) error { + call := Object(conn).CallWithContext(ctx, SetDomainsMethod, 0, ifaceIndex, domains) + if call.Err != nil { + return trace.Wrap(call.Err, "setting systemd-resolved link domains") + } + return nil +} + +// SetLinkDefaultRoute configures whether this link is the default DNS route. +func SetLinkDefaultRoute(ctx context.Context, conn *dbus.Conn, ifaceIndex int32, enabled bool) error { + call := Object(conn).CallWithContext(ctx, SetDefaultRouteMethod, 0, ifaceIndex, enabled) + if call.Err != nil { + return trace.Wrap(call.Err, "setting systemd-resolved link default route") + } + return nil +} + +// SetLinkDNS configures per-link DNS servers. +func SetLinkDNS(ctx context.Context, conn *dbus.Conn, ifaceIndex int32, addresses []DNSAddress) error { + call := Object(conn).CallWithContext(ctx, SetLinkDNSMethod, 0, ifaceIndex, addresses) + if call.Err != nil { + return trace.Wrap(call.Err, "setting systemd-resolved link DNS") + } + return nil +} + +// DNSAddressForIP converts an IP adress into a systemd-resolved DNSAddress. +func DNSAddressForIP(raw string) (DNSAddress, error) { + ip := net.ParseIP(raw) + if ip == nil { + return DNSAddress{}, trace.Errorf("invalid IP address") + } + if ip4 := ip.To4(); ip4 != nil { + return DNSAddress{ + Family: syscall.AF_INET, + Address: []byte(ip4), + }, nil + } + if ip16 := ip.To16(); ip16 != nil { + return DNSAddress{ + Family: syscall.AF_INET6, + Address: []byte(ip16), + }, nil + } + return DNSAddress{}, trace.Errorf("unsupported IP address") +} From 3e32eb4ae9f5658796b6aa52ad592f2facd0627e Mon Sep 17 00:00:00 2001 From: Pavel Evdokimov Date: Fri, 13 Feb 2026 21:56:50 +0000 Subject: [PATCH 07/26] refactor: move all polkit logic to a separate package --- lib/vnet/dbus_service_linux.go | 45 ++++++----------- lib/vnet/polkit/polkit.go | 91 ++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 31 deletions(-) create mode 100644 lib/vnet/polkit/polkit.go diff --git a/lib/vnet/dbus_service_linux.go b/lib/vnet/dbus_service_linux.go index 1e09aa0822c3e..436f47ddb8cfb 100644 --- a/lib/vnet/dbus_service_linux.go +++ b/lib/vnet/dbus_service_linux.go @@ -23,11 +23,9 @@ import ( "github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5/introspect" "github.com/gravitational/trace" -) -// polkitAllowUserInteraction allows polkit to prompt the user -// for a password if it is required. -const polkitAllowUserInteraction = uint32(1) + "github.com/gravitational/teleport/lib/vnet/polkit" +) // introspectNode describes the exported D-Bus API. Update it if any method // signature is changed or new methods are added. @@ -165,28 +163,18 @@ func (d *dbusDaemon) authorize(sender dbus.Sender) error { return nil } - subject := polkitSubject{ - Kind: "system-bus-name", - Details: map[string]dbus.Variant{ - "name": dbus.MakeVariant(string(sender)), - }, - } - var result struct { - Authorized bool - Challenge bool - Details map[string]string - } - if err := d.conn.Object("org.freedesktop.PolicyKit1", "/org/freedesktop/PolicyKit1/Authority"). - Call( - "org.freedesktop.PolicyKit1.Authority.CheckAuthorization", - 0, - subject, - vnetPolkitAction, - map[string]string{}, - polkitAllowUserInteraction, - "", - ).Store(&result); err != nil { - return trace.Wrap(err, "checking polkit authorization") + subject := polkit.NewSystemBusNameSubject(string(sender)) + result, err := polkit.CheckAuthorization( + d.ctx, + d.conn, + subject, + vnetPolkitAction, + map[string]string{}, + true, + "", + ) + if err != nil { + return err } if !result.Authorized { if result.Challenge { @@ -206,8 +194,3 @@ func (d *dbusDaemon) lookupSenderUID(sender dbus.Sender) (uint32, error) { } return uid, nil } - -type polkitSubject struct { - Kind string - Details map[string]dbus.Variant -} diff --git a/lib/vnet/polkit/polkit.go b/lib/vnet/polkit/polkit.go new file mode 100644 index 0000000000000..74a4f25986e75 --- /dev/null +++ b/lib/vnet/polkit/polkit.go @@ -0,0 +1,91 @@ +// Teleport +// Copyright (C) 2026 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 polkit + +import ( + "context" + + "github.com/godbus/dbus/v5" + "github.com/gravitational/trace" +) + +const ( + AuthorityServiceName = "org.freedesktop.PolicyKit1" + AuthorityObjectPath = "/org/freedesktop/PolicyKit1/Authority" + AuthorityInterface = "org.freedesktop.PolicyKit1.Authority" + + CheckAuthorizationMethod = AuthorityInterface + ".CheckAuthorization" + + CheckAuthorizationFlagNone = uint32(0x00000000) + CheckAuthorizationFlagAllowUserInteraction = uint32(0x00000001) + + SubjectKindSystemBusName = "system-bus-name" + SubjectDetailNameKey = "name" +) + +// Subject describes the entity being authorized. +type Subject struct { + // Kind is the subject kind, e.g. "system-bus-name". + Kind string + // Details are subject kind specific key/value pairs. + Details map[string]dbus.Variant +} + +// AuthorizationResult is the result of CheckAuthorization. +type AuthorizationResult struct { + // Authorized is true if the subject is authorized for the action. + Authorized bool + // Challenge is true if the subject could be authorized after authentication. + Challenge bool + // Details contains extra result information. + Details map[string]string +} + +// NewSystemBusNameSubject returns a polkit subject for a system bus name. +func NewSystemBusNameSubject(name string) Subject { + return Subject{ + Kind: SubjectKindSystemBusName, + Details: map[string]dbus.Variant{ + SubjectDetailNameKey: dbus.MakeVariant(name), + }, + } +} + +// CheckAuthorization calls polkit's CheckAuthorization method. +func CheckAuthorization( + ctx context.Context, + conn *dbus.Conn, + subject Subject, + actionID string, + details map[string]string, + allowUserInteraction bool, + cancellationID string, +) (AuthorizationResult, error) { + var flags uint32 + if allowUserInteraction { + flags = CheckAuthorizationFlagAllowUserInteraction + } else { + flags = CheckAuthorizationFlagNone + } + var result AuthorizationResult + if err := conn.Object(AuthorityServiceName, dbus.ObjectPath(AuthorityObjectPath)). + CallWithContext(ctx, CheckAuthorizationMethod, 0, subject, actionID, details, flags, cancellationID). + Store(&result); err != nil { + return AuthorizationResult{}, trace.Wrap(err, "checking polkit authorization") + } + return result, nil +} From 3d216128ecf362e01e0d228d7c3c21f5ad5b468b Mon Sep 17 00:00:00 2001 From: Pavel Evdokimov Date: Thu, 19 Feb 2026 20:41:52 +0000 Subject: [PATCH 08/26] filter systemd-resolved loopback upstreams --- lib/vnet/dns/osnameservers_linux.go | 2 +- lib/vnet/network_stack.go | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/vnet/dns/osnameservers_linux.go b/lib/vnet/dns/osnameservers_linux.go index c466dbf0e2087..f9b6a9d7d2bbb 100644 --- a/lib/vnet/dns/osnameservers_linux.go +++ b/lib/vnet/dns/osnameservers_linux.go @@ -50,7 +50,7 @@ func platformLoadUpstreamNameservers(ctx context.Context) ([]string, error) { slog.DebugContext(ctx, "Skipping invalid DNS server address", "address_bytes", entry.Address) continue } - if addr.IsUnspecified() { + if addr.IsUnspecified() || addr.IsLoopback() { continue } nameservers = append(nameservers, WithDNSPort(addr)) diff --git a/lib/vnet/network_stack.go b/lib/vnet/network_stack.go index 8c78213bc6868..ec39fb1b318bd 100644 --- a/lib/vnet/network_stack.go +++ b/lib/vnet/network_stack.go @@ -192,6 +192,8 @@ type filteredUpstreamSource struct { exclude map[string]struct{} } +// filteredUpstreamSource wraps an upstream source and excludes addresses added via AddExclude. +// It is mainly used to filter VNet's own DNS addresses. func newFilteredUpstreamSource(base dns.UpstreamNameserverSource) *filteredUpstreamSource { return &filteredUpstreamSource{ base: base, @@ -213,9 +215,11 @@ func (f *filteredUpstreamSource) UpstreamNameservers(ctx context.Context) ([]str if err != nil { return nil, err } + slog.DebugContext(ctx, "Loaded upstream nameservers (pre-filter)", "nameservers", nameservers) f.mu.RLock() defer f.mu.RUnlock() if len(f.exclude) == 0 { + slog.DebugContext(ctx, "Loaded upstream nameservers (post-filter)", "nameservers", nameservers) return nameservers, nil } filtered := nameservers[:0] @@ -225,6 +229,7 @@ func (f *filteredUpstreamSource) UpstreamNameservers(ctx context.Context) ([]str } filtered = append(filtered, nameserver) } + slog.DebugContext(ctx, "Loaded upstream nameservers (post-filter)", "nameservers", filtered) return filtered, nil } From d529c8c8749a9589631afddde151176bba1ee8d5 Mon Sep 17 00:00:00 2001 From: Pavel Evdokimov Date: Thu, 19 Feb 2026 21:02:12 +0000 Subject: [PATCH 09/26] add scope for link-local IPv6 adresses for upstreams --- lib/vnet/dns/osnameservers_linux.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/vnet/dns/osnameservers_linux.go b/lib/vnet/dns/osnameservers_linux.go index f9b6a9d7d2bbb..0c8fd6416db85 100644 --- a/lib/vnet/dns/osnameservers_linux.go +++ b/lib/vnet/dns/osnameservers_linux.go @@ -19,6 +19,7 @@ package dns import ( "context" "log/slog" + "net" "net/netip" "github.com/godbus/dbus/v5" @@ -53,6 +54,19 @@ func platformLoadUpstreamNameservers(ctx context.Context) ([]string, error) { if addr.IsUnspecified() || addr.IsLoopback() { continue } + // Link-local IPv6 addresses require a scope to be routable. + if addr.Is6() && addr.IsLinkLocalUnicast() { + if entry.InterfaceIndex == 0 { + slog.DebugContext(ctx, "Skipping link-local DNS server without interface index", "address", addr.String()) + continue + } + iface, err := net.InterfaceByIndex(int(entry.InterfaceIndex)) + if err != nil { + slog.DebugContext(ctx, "Skipping link-local DNS server with unknown interface", "address", addr.String(), "interface_index", entry.InterfaceIndex, "error", err) + continue + } + addr = addr.WithZone(iface.Name) + } nameservers = append(nameservers, WithDNSPort(addr)) } From e1bc30bd9f07cd278fe61b18940b4480fde74556 Mon Sep 17 00:00:00 2001 From: Pavel Evdokimov Date: Fri, 20 Feb 2026 17:01:33 +0000 Subject: [PATCH 10/26] comment updates, small fixes, and typo cleanup --- lib/vnet/admin_process_linux.go | 7 ++++++- lib/vnet/dbus_client_linux.go | 17 +++++++++++++++++ lib/vnet/dbus_linux.go | 18 ++++++++++++++++++ lib/vnet/dbus_service_linux.go | 15 +++++++++++---- lib/vnet/diag/routeconflict_other.go | 2 +- lib/vnet/dns/osnameservers.go | 3 ++- lib/vnet/dns/osnameservers_darwin.go | 2 +- lib/vnet/dns/osnameservers_linux.go | 2 +- lib/vnet/dns/osnameservers_other.go | 3 --- lib/vnet/dns/osnameservers_windows.go | 2 +- lib/vnet/escalate_linux.go | 2 +- lib/vnet/network_stack.go | 4 ++-- lib/vnet/osconfig_linux.go | 6 +++--- lib/vnet/systemdresolved/dbus.go | 4 ++-- 14 files changed, 66 insertions(+), 21 deletions(-) diff --git a/lib/vnet/admin_process_linux.go b/lib/vnet/admin_process_linux.go index 55a6b5def7458..09925be5cc66f 100644 --- a/lib/vnet/admin_process_linux.go +++ b/lib/vnet/admin_process_linux.go @@ -29,9 +29,14 @@ import ( vnetv1 "github.com/gravitational/teleport/gen/proto/go/teleport/lib/vnet/v1" ) +// LinuxAdminProcessConfig configures RunLinuxAdminProcess. type LinuxAdminProcessConfig struct { + // ClientApplicationServiceAddr is the address of the client application + // service the admin process connects to. ClientApplicationServiceAddr string - ServiceCredentialPath string + // ServiceCredentialPath is the path to IPC credentials used to authenticate + // with the client application service. + ServiceCredentialPath string } // RunLinuxAdminProcess must run as root. diff --git a/lib/vnet/dbus_client_linux.go b/lib/vnet/dbus_client_linux.go index 6e75befe09ed9..6e2fa2fbfafd6 100644 --- a/lib/vnet/dbus_client_linux.go +++ b/lib/vnet/dbus_client_linux.go @@ -23,6 +23,10 @@ import ( "github.com/gravitational/trace" ) +// startService is called from the normal user process to start +// the privileged VNet daemon. It connects to the system D-Bus +// and calls the corresponding Start method, exposed on the VNet +// D-Bus interface, passing the client service address and credential path. func startService(ctx context.Context, cfg LinuxAdminProcessConfig) error { conn, err := dbus.ConnectSystemBus() if err != nil { @@ -30,6 +34,15 @@ func startService(ctx context.Context, cfg LinuxAdminProcessConfig) error { } defer conn.Close() + // basically this corresponds to calling something like + // `busctl --system call org.teleport.vnet1 /org/teleport/vnet1 org.teleport.vnet1.Daemon Start ss "" ""` + // each D-Bus service owns a well-known name you refer to, then you specify an + // object path. object path is for granularity (a service can expose + // multiple objects, but it rarely used, so in our case it is the same as the name but + // slash-separated). then you call a method on a specific interface. the interface + // is implemented by some object, for vnet it’s the dbusDaemon struct. the D-Bus + // interface exposes the same methods as dbusDaemon, so we can call them over + // D-Bus. obj := conn.Object(vnetDBusServiceName, dbus.ObjectPath(vnetDBusObjectPath)) call := obj.CallWithContext(ctx, vnetDBusStartMethod, 0, cfg.ClientApplicationServiceAddr, cfg.ServiceCredentialPath) if call.Err != nil { @@ -38,6 +51,10 @@ func startService(ctx context.Context, cfg LinuxAdminProcessConfig) error { return nil } +// stopService is called from the normal user process to stop +// the privileged VNet daemon. It connects to the system D-Bus +// and calls the corresponding Stop method, exposed on the VNet +// D-Bus interface. func stopService(ctx context.Context) error { conn, err := dbus.ConnectSystemBus() if err != nil { diff --git a/lib/vnet/dbus_linux.go b/lib/vnet/dbus_linux.go index b36e5826bcbba..678bdf3301ea9 100644 --- a/lib/vnet/dbus_linux.go +++ b/lib/vnet/dbus_linux.go @@ -16,6 +16,24 @@ package vnet +// VNet's D-Bus daemon claims the org.teleport.vnet1 service name +// and exposes the org.teleport.vnet1.Daemon interface, which has Start +// and Stop methods. For it to work the system must include: +// - /usr/share/dbus-1/system.d/org.teleport.vnet1.conf to allow the daemon to +// claim the org.teleport.vnet1 name on the system bus. +// - /usr/share/dbus-1/system-services/org.teleport.vnet1.service to enable +// D-Bus activation of the daemon. +// - /usr/lib/systemd/system/teleport-vnet.service to manage the daemon +// lifecycle under systemd. +// - /usr/share/polkit-1/actions/org.teleport.vnet1.policy to define who can +// perform the org.teleport.vnet1.manage-daemon action (and therefore call +// Start and Stop methods). +// +// The daemon is managed by systemd. we don’t use systemd directly +// because `systemctl start` takes only unit names and doesn’t let us pass +// per start args. the closest options are template units or environment +// file, but those are clunky so we wrap the admin process in a D-Bus daemon +// and expose Start with explicit parameters. const ( vnetDBusServiceName = "org.teleport.vnet1" vnetDBusObjectPath = "/org/teleport/vnet1" diff --git a/lib/vnet/dbus_service_linux.go b/lib/vnet/dbus_service_linux.go index 436f47ddb8cfb..e8d12c885a5f5 100644 --- a/lib/vnet/dbus_service_linux.go +++ b/lib/vnet/dbus_service_linux.go @@ -49,7 +49,10 @@ var introspectNode = &introspect.Node{ }, } -// RunLinuxDBusService runs the VNet D-Bus service that can start the VNet admin process. +// RunLinuxDBusService runs the privileged VNet D-Bus daemon on the system bus. +// It claims the VNet service name and exports the VNet interface that +// exposes Start and Stop methods that normal client processes can call via +// system D-Bus. The daemon blocks until the context is canceled. func RunLinuxDBusService(ctx context.Context) error { conn, err := dbus.ConnectSystemBus() if err != nil { @@ -98,7 +101,9 @@ type dbusDaemon struct { started bool } -// Start is a D-Bus method that starts the VNet admin process. +// Start starts actual VNet admin process with passed address and credential path. +// It uses polkit to authorize the calling D-Bus sender. +// It returns an error if the admin process has already been started. func (d *dbusDaemon) Start(addr, credPath string, sender dbus.Sender) *dbus.Error { if err := d.authorize(sender); err != nil { return dbus.MakeFailedError(trace.Wrap(err, "authorization failed")) @@ -123,7 +128,7 @@ func (d *dbusDaemon) Start(addr, credPath string, sender dbus.Sender) *dbus.Erro ClientApplicationServiceAddr: addr, ServiceCredentialPath: credPath, }) - // TODO: D-Bus supports signals, we might want to emit a signal when the admin process exits. + // TODO(tangyatsu): D-Bus supports signals, we might want to emit a signal when the admin process exits. if err != nil { log.ErrorContext(d.ctx, "VNet admin process exited with error", "error", err) } @@ -133,7 +138,8 @@ func (d *dbusDaemon) Start(addr, credPath string, sender dbus.Sender) *dbus.Erro return nil } -// Stop is a D-Bus method that stops the VNet admin process. +// Stop stops actual VNet admin process by canceling the daemon context. +// It uses polkit to authorize the calling D-Bus sender. func (d *dbusDaemon) Stop(sender dbus.Sender) *dbus.Error { if err := d.authorize(sender); err != nil { return dbus.MakeFailedError(trace.Wrap(err, "authorization failed")) @@ -160,6 +166,7 @@ func (d *dbusDaemon) authorize(sender dbus.Sender) error { return trace.Wrap(err, "looking up D-Bus sender UID") } if uid == 0 { + // Always allow root to start the daemon. return nil } diff --git a/lib/vnet/diag/routeconflict_other.go b/lib/vnet/diag/routeconflict_other.go index 379b7dee2f25d..8bdfd68af76f4 100644 --- a/lib/vnet/diag/routeconflict_other.go +++ b/lib/vnet/diag/routeconflict_other.go @@ -25,7 +25,7 @@ import ( "github.com/gravitational/trace" ) -// TODO: linux diagnostics +// TODO(tangyatsu): 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 e06fe0495ce46..d312e7f1695ec 100644 --- a/lib/vnet/dns/osnameservers.go +++ b/lib/vnet/dns/osnameservers.go @@ -56,6 +56,7 @@ func loadUpstreamNameservers(ctx context.Context) ([]string, error) { return platformLoadUpstreamNameservers(ctx) } -func WithDNSPort(addr netip.Addr) string { +// AddrWithDNSPort returns addr with DNS port 53. +func AddrWithDNSPort(addr netip.Addr) string { return netip.AddrPortFrom(addr, 53).String() } diff --git a/lib/vnet/dns/osnameservers_darwin.go b/lib/vnet/dns/osnameservers_darwin.go index 5a952712fa130..f0471cbd69291 100644 --- a/lib/vnet/dns/osnameservers_darwin.go +++ b/lib/vnet/dns/osnameservers_darwin.go @@ -63,7 +63,7 @@ func platformLoadUpstreamNameservers(ctx context.Context) ([]string, error) { continue } - nameservers = append(nameservers, WithDNSPort(ip)) + nameservers = append(nameservers, AddrWithDNSPort(ip)) } slog.DebugContext(ctx, "Loaded host upstream nameservers.", "nameservers", nameservers, "config_file", confFilePath) diff --git a/lib/vnet/dns/osnameservers_linux.go b/lib/vnet/dns/osnameservers_linux.go index 0c8fd6416db85..69591db44d08d 100644 --- a/lib/vnet/dns/osnameservers_linux.go +++ b/lib/vnet/dns/osnameservers_linux.go @@ -67,7 +67,7 @@ func platformLoadUpstreamNameservers(ctx context.Context) ([]string, error) { } addr = addr.WithZone(iface.Name) } - nameservers = append(nameservers, WithDNSPort(addr)) + nameservers = append(nameservers, AddrWithDNSPort(addr)) } slog.DebugContext(ctx, "Loaded host upstream nameservers", "nameservers", nameservers) diff --git a/lib/vnet/dns/osnameservers_other.go b/lib/vnet/dns/osnameservers_other.go index 961d740e5fcb5..b1fd662fbe754 100644 --- a/lib/vnet/dns/osnameservers_other.go +++ b/lib/vnet/dns/osnameservers_other.go @@ -28,9 +28,6 @@ import ( var ( // vnetNotImplemented is an error indicating that VNet is not implemented on the host OS. vnetNotImplemented = &trace.NotImplementedError{Message: "VNet is not implemented on " + runtime.GOOS} - - // Satisfy unused linter. - _ = WithDNSPort ) func platformLoadUpstreamNameservers(ctx context.Context) ([]string, error) { diff --git a/lib/vnet/dns/osnameservers_windows.go b/lib/vnet/dns/osnameservers_windows.go index 1df944248b446..cda6d1eef5a03 100644 --- a/lib/vnet/dns/osnameservers_windows.go +++ b/lib/vnet/dns/osnameservers_windows.go @@ -48,7 +48,7 @@ func platformLoadUpstreamNameservers(ctx context.Context) ([]string, error) { if ignoreUpstreamNameserver(ifaceNameserver) { continue } - nameservers = append(nameservers, WithDNSPort(ifaceNameserver)) + nameservers = append(nameservers, AddrWithDNSPort(ifaceNameserver)) } } slog.DebugContext(ctx, "Loaded host upstream nameservers", "nameservers", nameservers) diff --git a/lib/vnet/escalate_linux.go b/lib/vnet/escalate_linux.go index f889e721621e3..a9521da4325f8 100644 --- a/lib/vnet/escalate_linux.go +++ b/lib/vnet/escalate_linux.go @@ -147,7 +147,7 @@ func checkDBusServiceAvailability(ctx context.Context) error { } else { return trace.Errorf("D-Bus service %s is not available", vnetDBusServiceName) } - // TODO: Maybe also check the systemd unit file exists. D-Bus can report a name + // TODO(tangyatsu): Maybe also check the systemd unit file exists. D-Bus can report a name // as activatable even if the corresponding systemd unit is missing. } diff --git a/lib/vnet/network_stack.go b/lib/vnet/network_stack.go index ec39fb1b318bd..5448f1f1a4d80 100644 --- a/lib/vnet/network_stack.go +++ b/lib/vnet/network_stack.go @@ -301,7 +301,7 @@ func newNetworkStack(cfg *networkStackConfig) (*networkStack, error) { if !ok { return nil, trace.Errorf("error parsing IPv6 DNS address %v", cfg.dnsIPv6) } - upstreamFilter.AddExclude(dns.WithDNSPort(addr)) + upstreamFilter.AddExclude(dns.AddrWithDNSPort(addr)) } dnsServer, err := dns.NewServer(ns, upstreamFilter) if err != nil { @@ -620,7 +620,7 @@ func (ns *networkStack) addDNSAddress(ip net.IP) error { return trace.Errorf("error parsing IPv4 DNS address %s", ip.String()) } if ns.upstreamFilter != nil { - ns.upstreamFilter.AddExclude(dns.WithDNSPort(addr)) + ns.upstreamFilter.AddExclude(dns.AddrWithDNSPort(addr)) } return trace.Wrap(ns.assignUDPHandler(tcpip.AddrFromSlice(ip), ns.dnsServer), "adding UDP handler at %s", ip.String()) diff --git a/lib/vnet/osconfig_linux.go b/lib/vnet/osconfig_linux.go index e9662dd0bad00..9ea5adbb7e98e 100644 --- a/lib/vnet/osconfig_linux.go +++ b/lib/vnet/osconfig_linux.go @@ -40,7 +40,7 @@ type platformOSConfigState struct { // platformConfigureOS configures the host OS according to cfg. func platformConfigureOS(ctx context.Context, cfg *osConfig, state *platformOSConfigState) error { - // TODO: use github.com/vishvananda/netlink to set up IPs and routes? + // TODO(tangyatsu): use github.com/vishvananda/netlink to set up IPs and routes? 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" @@ -90,7 +90,7 @@ func platformConfigureOS(ctx context.Context, cfg *osConfig, state *platformOSCo return nil } -func shouldReconfiguredDNSZones(cfg *osConfig, state *platformOSConfigState) bool { +func shouldReconfigureDNSZones(cfg *osConfig, state *platformOSConfigState) bool { return !utils.ContainSameUniqueElements(cfg.dnsZones, state.configuredDNSZones) } @@ -119,7 +119,7 @@ func configureDNS(ctx context.Context, cfg *osConfig, state *platformOSConfigSta return err } - if shouldReconfiguredDNSZones(cfg, state) { + if shouldReconfigureDNSZones(cfg, state) { iface, err := net.InterfaceByName(state.tunName) if err != nil { return trace.Wrap(err, "looking up interface %s", state.tunName) diff --git a/lib/vnet/systemdresolved/dbus.go b/lib/vnet/systemdresolved/dbus.go index f273cd222fd46..190cde197b4b0 100644 --- a/lib/vnet/systemdresolved/dbus.go +++ b/lib/vnet/systemdresolved/dbus.go @@ -110,7 +110,7 @@ func loadDNSProperty(ctx context.Context, conn *dbus.Conn) ([]DNS, error) { return dns, nil } -// SetLinkDomains configures per-link DNS search domains. +// SetLinkDomains configures per-link DNS domains. func SetLinkDomains(ctx context.Context, conn *dbus.Conn, ifaceIndex int32, domains []Domain) error { call := Object(conn).CallWithContext(ctx, SetDomainsMethod, 0, ifaceIndex, domains) if call.Err != nil { @@ -137,7 +137,7 @@ func SetLinkDNS(ctx context.Context, conn *dbus.Conn, ifaceIndex int32, addresse return nil } -// DNSAddressForIP converts an IP adress into a systemd-resolved DNSAddress. +// DNSAddressForIP converts an IP address into a systemd-resolved DNSAddress. func DNSAddressForIP(raw string) (DNSAddress, error) { ip := net.ParseIP(raw) if ip == nil { From eff094f01ca680da9e074e67734df276ea663f9f Mon Sep 17 00:00:00 2001 From: Pavel Evdokimov Date: Fri, 20 Feb 2026 17:06:25 +0000 Subject: [PATCH 11/26] return uid from D-Bus authorize func --- lib/vnet/dbus_service_linux.go | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/lib/vnet/dbus_service_linux.go b/lib/vnet/dbus_service_linux.go index e8d12c885a5f5..829fad4cdf686 100644 --- a/lib/vnet/dbus_service_linux.go +++ b/lib/vnet/dbus_service_linux.go @@ -105,13 +105,9 @@ type dbusDaemon struct { // It uses polkit to authorize the calling D-Bus sender. // It returns an error if the admin process has already been started. func (d *dbusDaemon) Start(addr, credPath string, sender dbus.Sender) *dbus.Error { - if err := d.authorize(sender); err != nil { - return dbus.MakeFailedError(trace.Wrap(err, "authorization failed")) - } - - uid, err := d.lookupSenderUID(sender) + uid, err := d.authorize(sender) if err != nil { - return dbus.MakeFailedError(trace.Wrap(err, "looking up D-Bus sender UID")) + return dbus.MakeFailedError(trace.Wrap(err, "authorization failed")) } d.mu.Lock() @@ -141,12 +137,9 @@ func (d *dbusDaemon) Start(addr, credPath string, sender dbus.Sender) *dbus.Erro // Stop stops actual VNet admin process by canceling the daemon context. // It uses polkit to authorize the calling D-Bus sender. func (d *dbusDaemon) Stop(sender dbus.Sender) *dbus.Error { - if err := d.authorize(sender); err != nil { - return dbus.MakeFailedError(trace.Wrap(err, "authorization failed")) - } - uid, err := d.lookupSenderUID(sender) + uid, err := d.authorize(sender) if err != nil { - return dbus.MakeFailedError(trace.Wrap(err, "looking up D-Bus sender UID")) + return dbus.MakeFailedError(trace.Wrap(err, "authorization failed")) } // We intentionally do not reset started here to avoid a race with Start // while the process is exiting. A new Start is allowed only after @@ -160,14 +153,16 @@ func (d *dbusDaemon) Stop(sender dbus.Sender) *dbus.Error { return nil } -func (d *dbusDaemon) authorize(sender dbus.Sender) error { +// authorize checks polkit authorization for the calling D-Bus sender and +// returns the sender UID. +func (d *dbusDaemon) authorize(sender dbus.Sender) (uint32, error) { uid, err := d.lookupSenderUID(sender) if err != nil { - return trace.Wrap(err, "looking up D-Bus sender UID") + return 0, trace.Wrap(err, "looking up D-Bus sender UID") } if uid == 0 { // Always allow root to start the daemon. - return nil + return uid, nil } subject := polkit.NewSystemBusNameSubject(string(sender)) @@ -181,15 +176,15 @@ func (d *dbusDaemon) authorize(sender dbus.Sender) error { "", ) if err != nil { - return err + return 0, err } if !result.Authorized { if result.Challenge { - return trace.Errorf("polkit authentication required") + return 0, trace.Errorf("polkit authentication required") } - return trace.Errorf("polkit authorization denied") + return 0, trace.Errorf("polkit authorization denied") } - return nil + return uid, nil } func (d *dbusDaemon) lookupSenderUID(sender dbus.Sender) (uint32, error) { From 06a8c22ac926d798861235e57e2308558874ac47 Mon Sep 17 00:00:00 2001 From: Pavel Evdokimov Date: Fri, 20 Feb 2026 22:16:12 +0000 Subject: [PATCH 12/26] add typed constants for systemd unit states --- lib/vnet/escalate_linux.go | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/lib/vnet/escalate_linux.go b/lib/vnet/escalate_linux.go index a9521da4325f8..761472d2af513 100644 --- a/lib/vnet/escalate_linux.go +++ b/lib/vnet/escalate_linux.go @@ -34,6 +34,22 @@ const ( terminateTimeout = 30 * time.Second ) +// systemdUnitActiveState is the ActiveState property of a systemd unit. +// The values and descriptions below are copied from the official +// org.freedesktop.systemd1 D-Bus interface documentation. +type systemdUnitState string + +const ( + // systemdUnitActive means started, bound, plugged in, …, depending on the unit type. + systemdUnitActive systemdUnitState = "active" + // systemdUnitInactive means stopped, unbound, unplugged, …, depending on the unit type. + systemdUnitInactive systemdUnitState = "inactive" + // systemdUnitFailed means similar to inactive, but the unit failed in some way (process returned error code on exit, crashed, an operation timed out, or after too many restarts). + systemdUnitFailed systemdUnitState = "failed" + // systemdUnitActivating means changing from inactive to active. + systemdUnitActivating systemdUnitState = "activating" +) + func execAdminProcess(ctx context.Context, cfg LinuxAdminProcessConfig) error { if err := checkDBusServiceAvailability(ctx); err != nil { if os.Geteuid() == 0 { @@ -74,11 +90,11 @@ loop: } break loop case <-ticker.C: - state, err := systemdUnitActiveState(ctx, conn, vnetSystemdUnitName) + state, err := getSystemdUnitState(ctx, conn, vnetSystemdUnitName) if err != nil { return trace.Wrap(err, "querying systemd service %s", vnetSystemdUnitName) } - if state != "active" && state != "activating" { + if state != systemdUnitActive && state != systemdUnitActivating { return trace.Errorf("service stopped running prematurely, status: %s", state) } } @@ -91,18 +107,18 @@ loop: case <-deadline: return trace.Errorf("systemd service %s failed to stop with %v", vnetSystemdUnitName, terminateTimeout) case <-ticker.C: - state, err := systemdUnitActiveState(ctx, conn, vnetSystemdUnitName) + state, err := getSystemdUnitState(ctx, conn, vnetSystemdUnitName) if err != nil { return trace.Wrap(err, "querying systemd service %s", vnetSystemdUnitName) } - if state == "inactive" { + if state == systemdUnitInactive || state == systemdUnitFailed { return nil } } } } -func systemdUnitActiveState(ctx context.Context, conn *systemddbus.Conn, unit string) (string, error) { +func getSystemdUnitState(ctx context.Context, conn *systemddbus.Conn, unit string) (systemdUnitState, error) { props, err := conn.GetUnitPropertiesContext(ctx, unit) if err != nil { return "", err @@ -111,7 +127,7 @@ func systemdUnitActiveState(ctx context.Context, conn *systemddbus.Conn, unit st if !ok || state == "" { return "", trace.Errorf("systemd ActiveState is missing for %s", unit) } - return state, nil + return systemdUnitState(state), nil } func checkDBusServiceAvailability(ctx context.Context) error { From ff7dee8acce924a3c8a6ba5fc7a3e7be0bacb89a Mon Sep 17 00:00:00 2001 From: Pavel Evdokimov Date: Fri, 20 Feb 2026 23:47:56 +0000 Subject: [PATCH 13/26] avoid using canceled context for stopping VNet daemon --- lib/vnet/escalate_linux.go | 38 +++++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/lib/vnet/escalate_linux.go b/lib/vnet/escalate_linux.go index 761472d2af513..61211a2f7a7b5 100644 --- a/lib/vnet/escalate_linux.go +++ b/lib/vnet/escalate_linux.go @@ -79,16 +79,21 @@ func runService(ctx context.Context, cfg LinuxAdminProcessConfig) error { ticker := time.NewTicker(time.Second) defer ticker.Stop() -loop: for { select { case <-ctx.Done(): - log.InfoContext(ctx, "Context canceled, stopping systemd service") - err := stopService(ctx) + stopCtx, stopCancel := context.WithTimeout(context.Background(), terminateTimeout) + defer stopCancel() + log.InfoContext(stopCtx, "Context canceled, stopping systemd service") + if err := stopService(stopCtx); err != nil { + return trace.Wrap(err, "sending stop request to systemd service %s", vnetSystemdUnitName) + } + err := waitForServiceStop(stopCtx, vnetSystemdUnitName) if err != nil { - return trace.Wrap(err) + return trace.Wrap(err, "systemd service %s failed to stop with %v", vnetSystemdUnitName, terminateTimeout) } - break loop + log.InfoContext(stopCtx, "Successfully stopped systemd service") + return nil case <-ticker.C: state, err := getSystemdUnitState(ctx, conn, vnetSystemdUnitName) if err != nil { @@ -99,17 +104,28 @@ loop: } } } +} + +func waitForServiceStop(ctx context.Context, unit string) error { + // Open a fresh connection here because the main loop's connection is bound to + // the original context and may be closed when that context is canceled. + conn, err := systemddbus.NewWithContext(ctx) + if err != nil { + return trace.NotFound("systemd D-Bus is unavailable: %v", err) + } + defer conn.Close() + + ticker := time.NewTicker(time.Second) + defer ticker.Stop() - // Wait for the service to actually stop - deadline := time.After(terminateTimeout + 5*time.Second) for { select { - case <-deadline: - return trace.Errorf("systemd service %s failed to stop with %v", vnetSystemdUnitName, terminateTimeout) + case <-ctx.Done(): + return ctx.Err() case <-ticker.C: - state, err := getSystemdUnitState(ctx, conn, vnetSystemdUnitName) + state, err := getSystemdUnitState(ctx, conn, unit) if err != nil { - return trace.Wrap(err, "querying systemd service %s", vnetSystemdUnitName) + return trace.Wrap(err, "querying systemd service %s", unit) } if state == systemdUnitInactive || state == systemdUnitFailed { return nil From d026ba843540dfa69b764a12f57c9ac27b3088bf Mon Sep 17 00:00:00 2001 From: Pavel Evdokimov Date: Wed, 11 Mar 2026 23:10:55 +0000 Subject: [PATCH 14/26] refactor: do not store ctx in dbusDaemon --- lib/vnet/dbus_service_linux.go | 124 +++++++++++++++++++++++---------- 1 file changed, 88 insertions(+), 36 deletions(-) diff --git a/lib/vnet/dbus_service_linux.go b/lib/vnet/dbus_service_linux.go index 829fad4cdf686..0d516632085ad 100644 --- a/lib/vnet/dbus_service_linux.go +++ b/lib/vnet/dbus_service_linux.go @@ -18,15 +18,21 @@ package vnet import ( "context" + "errors" "sync" + "sync/atomic" + "time" "github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5/introspect" "github.com/gravitational/trace" + "golang.org/x/sync/errgroup" "github.com/gravitational/teleport/lib/vnet/polkit" ) +const polkitAuthorizationTimeout = 30 * time.Second + // introspectNode describes the exported D-Bus API. Update it if any method // signature is changed or new methods are added. var introspectNode = &introspect.Node{ @@ -54,51 +60,93 @@ var introspectNode = &introspect.Node{ // exposes Start and Stop methods that normal client processes can call via // system D-Bus. The daemon blocks until the context is canceled. func RunLinuxDBusService(ctx context.Context) error { + daemon, err := newDBusDaemon() + if err != nil { + return trace.Wrap(err) + } + + stop := context.AfterFunc(ctx, daemon.Close) + defer stop() + + return trace.Wrap(daemon.Wait()) +} + +func newDBusDaemon() (_ *dbusDaemon, err error) { conn, err := dbus.ConnectSystemBus() if err != nil { - return trace.Wrap(err, "connecting to system D-Bus") + return nil, trace.Wrap(err, "connecting to system D-Bus") } - defer conn.Close() + defer func() { + if err != nil { + _ = conn.Close() + } + }() - serviceCtx, cancel := context.WithCancel(ctx) - defer cancel() + ctx, cancel := context.WithCancel(context.Background()) + g, ctx := errgroup.WithContext(ctx) daemon := &dbusDaemon{ - ctx: serviceCtx, - cancel: cancel, - conn: conn, + conn: conn, + g: g, + done: make(chan struct{}), + startAdminProcess: func(addr, credPath string) error { + return trace.Wrap(RunLinuxAdminProcess(ctx, LinuxAdminProcessConfig{ + ClientApplicationServiceAddr: addr, + ServiceCredentialPath: credPath, + })) + }, + cancelAdminProcess: cancel, } + if err := conn.Export(daemon, dbus.ObjectPath(vnetDBusObjectPath), vnetDBusInterface); err != nil { - return trace.Wrap(err, "exporting D-Bus object") + return nil, trace.Wrap(err, "exporting D-Bus object") } if err := conn.Export( introspect.NewIntrospectable(introspectNode), dbus.ObjectPath(vnetDBusObjectPath), "org.freedesktop.DBus.Introspectable", ); err != nil { - return trace.Wrap(err, "exporting D-Bus introspection") + return nil, trace.Wrap(err, "exporting D-Bus introspection") } reply, err := conn.RequestName(vnetDBusServiceName, dbus.NameFlagDoNotQueue) if err != nil { - return trace.Wrap(err, "requesting D-Bus name") + return nil, trace.Wrap(err, "requesting D-Bus name") } if reply != dbus.RequestNameReplyPrimaryOwner { - return trace.Errorf("D-Bus name %s is already owned", vnetDBusServiceName) + return nil, trace.Errorf("D-Bus name %s is already owned", vnetDBusServiceName) } - log.InfoContext(serviceCtx, "Acquired D-Bus name", "name", vnetDBusServiceName) - <-serviceCtx.Done() - return nil + log.InfoContext(context.Background(), "Acquired D-Bus name", "name", vnetDBusServiceName) + return daemon, nil } type dbusDaemon struct { - ctx context.Context - cancel context.CancelFunc - conn *dbus.Conn + conn *dbus.Conn + g *errgroup.Group + started atomic.Bool + done chan struct{} + closeOnce sync.Once + + startAdminProcess func(addr, credPath string) error + cancelAdminProcess context.CancelFunc +} - mu sync.Mutex - started bool +func (d *dbusDaemon) Close() { + d.closeOnce.Do(func() { + close(d.done) + d.cancelAdminProcess() + _ = d.conn.Close() + }) +} + +func (d *dbusDaemon) Wait() error { + <-d.done + err := d.g.Wait() + if err != nil && !errors.Is(err, context.Canceled) { + log.DebugContext(context.Background(), "D-Bus daemon background task exited with error after close", "error", err) + } + return nil } // Start starts actual VNet admin process with passed address and credential path. @@ -110,31 +158,32 @@ func (d *dbusDaemon) Start(addr, credPath string, sender dbus.Sender) *dbus.Erro return dbus.MakeFailedError(trace.Wrap(err, "authorization failed")) } - d.mu.Lock() - defer d.mu.Unlock() - if d.started { + select { + case <-d.done: + return dbus.MakeFailedError(trace.Errorf("VNet D-Bus daemon is shutting down")) + default: + } + + if !d.started.CompareAndSwap(false, true) { return dbus.MakeFailedError(trace.Errorf("VNet admin process already started")) } - d.started = true - log.InfoContext(d.ctx, "Starting VNet admin process", "uid", uid) + log.InfoContext(context.Background(), "Starting VNet admin process", "uid", uid) - go func() { - err := RunLinuxAdminProcess(d.ctx, LinuxAdminProcessConfig{ - ClientApplicationServiceAddr: addr, - ServiceCredentialPath: credPath, - }) + d.g.Go(func() error { + err := d.startAdminProcess(addr, credPath) // TODO(tangyatsu): D-Bus supports signals, we might want to emit a signal when the admin process exits. if err != nil { - log.ErrorContext(d.ctx, "VNet admin process exited with error", "error", err) + log.ErrorContext(context.Background(), "VNet admin process exited with error", "error", err) } - d.cancel() - }() + d.Close() + return trace.Wrap(err) + }) return nil } -// Stop stops actual VNet admin process by canceling the daemon context. +// Stop stops actual VNet admin process and exits the daemon. // It uses polkit to authorize the calling D-Bus sender. func (d *dbusDaemon) Stop(sender dbus.Sender) *dbus.Error { uid, err := d.authorize(sender) @@ -148,8 +197,8 @@ func (d *dbusDaemon) Stop(sender dbus.Sender) *dbus.Error { // D-Bus activation can start the daemon on any method call. We allow // Stop before Start so the service can exit immediately instead of idling // waiting for a Start call that may never come. - log.InfoContext(d.ctx, "Stopping VNet admin process", "uid", uid) - d.cancel() + log.InfoContext(context.Background(), "Stopping VNet admin process", "uid", uid) + d.Close() return nil } @@ -165,9 +214,12 @@ func (d *dbusDaemon) authorize(sender dbus.Sender) (uint32, error) { return uid, nil } + authCtx, cancel := context.WithTimeout(context.Background(), polkitAuthorizationTimeout) + defer cancel() + subject := polkit.NewSystemBusNameSubject(string(sender)) result, err := polkit.CheckAuthorization( - d.ctx, + authCtx, d.conn, subject, vnetPolkitAction, From dd0eebb8681f21464b22a41c4f6ca23ce50f18f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Cie=C5=9Blak?= Date: Thu, 12 Mar 2026 12:28:35 +0100 Subject: [PATCH 15/26] Improve dbusDaemon lifecycle handling --- lib/vnet/dbus_service_linux.go | 37 ++++++++++++++-------------------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/lib/vnet/dbus_service_linux.go b/lib/vnet/dbus_service_linux.go index 0d516632085ad..8707c8c66cd2a 100644 --- a/lib/vnet/dbus_service_linux.go +++ b/lib/vnet/dbus_service_linux.go @@ -18,7 +18,6 @@ package vnet import ( "context" - "errors" "sync" "sync/atomic" "time" @@ -26,7 +25,6 @@ import ( "github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5/introspect" "github.com/gravitational/trace" - "golang.org/x/sync/errgroup" "github.com/gravitational/teleport/lib/vnet/polkit" ) @@ -76,19 +74,17 @@ func newDBusDaemon() (_ *dbusDaemon, err error) { if err != nil { return nil, trace.Wrap(err, "connecting to system D-Bus") } + ctx, cancel := context.WithCancel(context.Background()) defer func() { if err != nil { + cancel() _ = conn.Close() } }() - ctx, cancel := context.WithCancel(context.Background()) - g, ctx := errgroup.WithContext(ctx) - daemon := &dbusDaemon{ conn: conn, - g: g, - done: make(chan struct{}), + done: make(chan error, 1), startAdminProcess: func(addr, credPath string) error { return trace.Wrap(RunLinuxAdminProcess(ctx, LinuxAdminProcessConfig{ ClientApplicationServiceAddr: addr, @@ -123,9 +119,9 @@ func newDBusDaemon() (_ *dbusDaemon, err error) { type dbusDaemon struct { conn *dbus.Conn - g *errgroup.Group started atomic.Bool - done chan struct{} + closing atomic.Bool + done chan error // buffered 1; receives the admin process error or nil closeOnce sync.Once startAdminProcess func(addr, credPath string) error @@ -133,20 +129,19 @@ type dbusDaemon struct { } func (d *dbusDaemon) Close() { + d.closing.Store(true) d.closeOnce.Do(func() { - close(d.done) d.cancelAdminProcess() _ = d.conn.Close() + // If no admin process goroutine was started, unblock Wait. + if !d.started.Load() { + d.done <- nil + } }) } func (d *dbusDaemon) Wait() error { - <-d.done - err := d.g.Wait() - if err != nil && !errors.Is(err, context.Canceled) { - log.DebugContext(context.Background(), "D-Bus daemon background task exited with error after close", "error", err) - } - return nil + return <-d.done } // Start starts actual VNet admin process with passed address and credential path. @@ -158,10 +153,8 @@ func (d *dbusDaemon) Start(addr, credPath string, sender dbus.Sender) *dbus.Erro return dbus.MakeFailedError(trace.Wrap(err, "authorization failed")) } - select { - case <-d.done: + if d.closing.Load() { return dbus.MakeFailedError(trace.Errorf("VNet D-Bus daemon is shutting down")) - default: } if !d.started.CompareAndSwap(false, true) { @@ -170,15 +163,15 @@ func (d *dbusDaemon) Start(addr, credPath string, sender dbus.Sender) *dbus.Erro log.InfoContext(context.Background(), "Starting VNet admin process", "uid", uid) - d.g.Go(func() error { + go func() { err := d.startAdminProcess(addr, credPath) // TODO(tangyatsu): D-Bus supports signals, we might want to emit a signal when the admin process exits. if err != nil { log.ErrorContext(context.Background(), "VNet admin process exited with error", "error", err) } + d.done <- err d.Close() - return trace.Wrap(err) - }) + }() return nil } From ed0c82d4526ff6470afb90763182a7ab607804ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Cie=C5=9Blak?= Date: Thu, 12 Mar 2026 12:57:32 +0100 Subject: [PATCH 16/26] Log errors from `dbusDaemon.Wait` --- lib/vnet/dbus_service_linux.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/vnet/dbus_service_linux.go b/lib/vnet/dbus_service_linux.go index 8707c8c66cd2a..cd033604dbb74 100644 --- a/lib/vnet/dbus_service_linux.go +++ b/lib/vnet/dbus_service_linux.go @@ -18,6 +18,7 @@ package vnet import ( "context" + "errors" "sync" "sync/atomic" "time" @@ -141,7 +142,11 @@ func (d *dbusDaemon) Close() { } func (d *dbusDaemon) Wait() error { - return <-d.done + err := <-d.done + if err != nil && !errors.Is(err, context.Canceled) { + log.DebugContext(context.Background(), "D-Bus daemon background task exited with error after close", "error", err) + } + return nil } // Start starts actual VNet admin process with passed address and credential path. From aea58055cc04d361aefbe501ca7e07414af7eef7 Mon Sep 17 00:00:00 2001 From: Pavel Evdokimov Date: Thu, 12 Mar 2026 16:01:39 +0000 Subject: [PATCH 17/26] refactor: reuse shared darwin/linux code for admin process --- lib/vnet/admin_process_darwin.go | 108 ++----------------------- lib/vnet/admin_process_linux.go | 99 ++--------------------- lib/vnet/admin_process_unix.go | 132 +++++++++++++++++++++++++++++++ 3 files changed, 142 insertions(+), 197 deletions(-) create mode 100644 lib/vnet/admin_process_unix.go diff --git a/lib/vnet/admin_process_darwin.go b/lib/vnet/admin_process_darwin.go index 9a7b789657a50..09c6152e909a1 100644 --- a/lib/vnet/admin_process_darwin.go +++ b/lib/vnet/admin_process_darwin.go @@ -18,18 +18,16 @@ package vnet import ( "context" - "errors" - "os" - "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" "github.com/gravitational/teleport/lib/vnet/daemon" ) +const ( + tunInterfaceName = "utun" +) + // RunDarwinAdminProcess must run as root. It creates and sets up a TUN device // and passes the file descriptor for that device over the unix socket found at // config.socketPath. @@ -54,101 +52,5 @@ func RunDarwinAdminProcess(ctx context.Context, config daemon.Config) error { } defer clt.close() - tun, tunName, err := createTUNDevice(ctx) - if err != nil { - return trace.Wrap(err) - } - defer tun.Close() - - 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 := newOSConfigProvider(osConfigProviderConfig{ - clt: clt, - tunName: tunName, - ipv6Prefix: networkStackConfig.ipv6Prefix.String(), - dnsIPv6: networkStackConfig.dnsIPv6.String(), - addDNSAddress: networkStack.addDNSAddress, - }) - if err != nil { - return trace.Wrap(err, "creating OS config provider") - } - osConfigurator := newOSConfigurator(osConfigProvider) - - g, ctx := errgroup.WithContext(ctx) - g.Go(func() error { - defer log.InfoContext(ctx, "Network stack terminated.") - if err := networkStack.run(ctx); err != nil { - return trace.Wrap(err, "running network stack") - } - return errors.New("network stack terminated") - }) - g.Go(func() error { - defer log.InfoContext(ctx, "OS configuration loop exited.") - 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 { - defer log.InfoContext(ctx, "Ping loop exited.") - 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() - } - } - }) - - done := make(chan error) - go func() { - done <- g.Wait() - }() - - select { - case err := <-done: - return trace.Wrap(err, "running VNet admin process") - case <-ctx.Done(): - } - - select { - case err := <-done: - // network stack exited cleanly within timeout - return trace.Wrap(err, "running VNet admin process") - case <-time.After(10 * time.Second): - log.ErrorContext(ctx, "VNet admin process did not exit within 10 seconds, forcing shutdown.") - os.Exit(1) - return 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 + return runUnixAdminProcess(ctx, clt, tunInterfaceName) } diff --git a/lib/vnet/admin_process_linux.go b/lib/vnet/admin_process_linux.go index 09925be5cc66f..5d0d0ff4ce755 100644 --- a/lib/vnet/admin_process_linux.go +++ b/lib/vnet/admin_process_linux.go @@ -18,15 +18,12 @@ package vnet import ( "context" - "errors" - "os" - "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" +const ( + tunInterfaceName = "TeleportVNet" ) // LinuxAdminProcessConfig configures RunLinuxAdminProcess. @@ -47,98 +44,12 @@ func RunLinuxAdminProcess(ctx context.Context, config LinuxAdminProcessConfig) e if err != nil { return trace.Wrap(err, "reading service IPC credentials") } + // TODO(tangyatsu): change to gRPC client over unix socket instead of TCP. 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 := newOSConfigProvider(osConfigProviderConfig{ - clt: clt, - tunName: tunName, - ipv6Prefix: networkStackConfig.ipv6Prefix.String(), - dnsIPv6: networkStackConfig.dnsIPv6.String(), - addDNSAddress: networkStack.addDNSAddress, - }) - if err != nil { - return trace.Wrap(err, "creating OS config provider") - } - osConfigurator := newOSConfigurator(osConfigProvider) - - g, ctx := errgroup.WithContext(ctx) - g.Go(func() error { - defer log.InfoContext(ctx, "Network stack terminated.") - if err := networkStack.run(ctx); err != nil { - return trace.Wrap(err, "running network stack") - } - return errors.New("network stack terminated") - }) - g.Go(func() error { - defer log.InfoContext(ctx, "OS configuration loop exited.") - 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 { - defer log.InfoContext(ctx, "Ping loop exited.") - 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() - } - } - }) - - done := make(chan error) - go func() { - done <- g.Wait() - }() - - select { - case err := <-done: - return trace.Wrap(err, "running VNet admin process") - case <-ctx.Done(): - } - - select { - case err := <-done: - // network stack exited cleanly within timeout - return trace.Wrap(err, "running VNet admin process") - case <-time.After(10 * time.Second): - log.ErrorContext(ctx, "VNet admin process did not exit within 10 seconds, forcing shutdown.") - os.Exit(1) - return nil - } + return runUnixAdminProcess(ctx, clt, tunInterfaceName) } diff --git a/lib/vnet/admin_process_unix.go b/lib/vnet/admin_process_unix.go new file mode 100644 index 0000000000000..15d19e866225c --- /dev/null +++ b/lib/vnet/admin_process_unix.go @@ -0,0 +1,132 @@ +// 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 . + +//go:build darwin || linux + +package vnet + +import ( + "context" + "errors" + "os" + "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" +) + +func runUnixAdminProcess(ctx context.Context, clt *clientApplicationServiceClient, tunInterfaceName string) error { + tun, tunName, err := createTUNDevice(ctx, tunInterfaceName) + if err != nil { + return trace.Wrap(err) + } + defer tun.Close() + + 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 := newOSConfigProvider(osConfigProviderConfig{ + clt: clt, + tunName: tunName, + ipv6Prefix: networkStackConfig.ipv6Prefix.String(), + dnsIPv6: networkStackConfig.dnsIPv6.String(), + addDNSAddress: networkStack.addDNSAddress, + }) + if err != nil { + return trace.Wrap(err, "creating OS config provider") + } + osConfigurator := newOSConfigurator(osConfigProvider) + + g, ctx := errgroup.WithContext(ctx) + g.Go(func() error { + defer log.InfoContext(ctx, "Network stack terminated.") + if err := networkStack.run(ctx); err != nil { + return trace.Wrap(err, "running network stack") + } + return errors.New("network stack terminated") + }) + g.Go(func() error { + defer log.InfoContext(ctx, "OS configuration loop exited.") + 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 { + defer log.InfoContext(ctx, "Ping loop exited.") + 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() + } + } + }) + + done := make(chan error) + go func() { + done <- g.Wait() + }() + + select { + case err := <-done: + return trace.Wrap(err, "running VNet admin process") + case <-ctx.Done(): + } + + select { + case err := <-done: + // network stack exited cleanly within timeout + return trace.Wrap(err, "running VNet admin process") + case <-time.After(10 * time.Second): + log.ErrorContext(ctx, "VNet admin process did not exit within 10 seconds, forcing shutdown.") + os.Exit(1) + return nil + } +} + +func createTUNDevice(ctx context.Context, interfaceName string) (tun.Device, string, error) { + log.DebugContext(ctx, "Creating TUN device.") + dev, err := tun.CreateTUN(interfaceName, 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 +} From bb18843111951bff246d24d395194ee515f955f9 Mon Sep 17 00:00:00 2001 From: Pavel Evdokimov Date: Thu, 12 Mar 2026 17:45:47 +0000 Subject: [PATCH 18/26] reintroduce isDiagSupported --- .../src/ui/Vnet/VnetConnectionItem.tsx | 23 +++++++++++-------- .../src/ui/Vnet/VnetSliderStep.story.tsx | 8 ++++++- .../teleterm/src/ui/Vnet/vnetContext.tsx | 11 +++++++++ 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/web/packages/teleterm/src/ui/Vnet/VnetConnectionItem.tsx b/web/packages/teleterm/src/ui/Vnet/VnetConnectionItem.tsx index 31d462562f00a..10bc05886472d 100644 --- a/web/packages/teleterm/src/ui/Vnet/VnetConnectionItem.tsx +++ b/web/packages/teleterm/src/ui/Vnet/VnetConnectionItem.tsx @@ -114,6 +114,7 @@ const VnetConnectionItemBase = forwardRef< diagnosticsAttempt, getDisabledDiagnosticsReason, showDiagWarningIndicator, + isDiagSupported, installTimeRequirementsCheck, } = useVnetContext(); const { close: closeConnectionsPanel } = useConnectionsContext(); @@ -268,16 +269,18 @@ const VnetConnectionItemBase = forwardRef< )} - { - e.stopPropagation(); - props.runDiagnosticsFromVnetPanel(); - }} - > - - + {isDiagSupported && ( + { + e.stopPropagation(); + props.runDiagnosticsFromVnetPanel(); + }} + > + + + )} )} diff --git a/web/packages/teleterm/src/ui/Vnet/VnetSliderStep.story.tsx b/web/packages/teleterm/src/ui/Vnet/VnetSliderStep.story.tsx index b68be7e1dc137..eadf19bbd109a 100644 --- a/web/packages/teleterm/src/ui/Vnet/VnetSliderStep.story.tsx +++ b/web/packages/teleterm/src/ui/Vnet/VnetSliderStep.story.tsx @@ -37,6 +37,7 @@ import { useVnetContext, VnetContextProvider } from './vnetContext'; import { VnetSliderStep as Component } from './VnetSliderStep'; type StoryProps = { + platform: 'darwin' | 'win32' | 'linux'; startVnet: 'success' | 'error' | 'processing'; autoStart: boolean; appDnsZones: string[]; @@ -55,6 +56,7 @@ type StoryProps = { }; const defaultArgs: StoryProps = { + platform: 'darwin', startVnet: 'success', autoStart: true, appDnsZones: ['teleport.example.com', 'company.test'], @@ -81,6 +83,10 @@ const meta: Meta = { }, ], argTypes: { + platform: { + control: { type: 'inline-radio' }, + options: ['darwin', 'win32', 'linux'], + }, startVnet: { control: { type: 'inline-radio' }, options: ['success', 'error', 'processing'], @@ -122,7 +128,7 @@ const meta: Meta = { export default meta; function VnetSliderStep(props: StoryProps) { - const appContext = new MockAppContext(); + const appContext = new MockAppContext({ platform: props.platform }); if (props.windowsVNetServiceNotFound) { appContext.vnet.checkInstallTimeRequirements = () => diff --git a/web/packages/teleterm/src/ui/Vnet/vnetContext.tsx b/web/packages/teleterm/src/ui/Vnet/vnetContext.tsx index fc031c8e3cc0d..1d9a8de4f3636 100644 --- a/web/packages/teleterm/src/ui/Vnet/vnetContext.tsx +++ b/web/packages/teleterm/src/ui/Vnet/vnetContext.tsx @@ -58,6 +58,10 @@ export type VnetContext = { * Describes whether the given OS can run VNet. */ isSupported: boolean; + /** + * Describes whether the given OS can run VNet diagnostics. + */ + isDiagSupported: boolean; status: VnetStatus; start: () => Promise<[void, Error]>; startAttempt: Attempt; @@ -177,6 +181,7 @@ export const VnetContextProvider: FC< ); const isSupported = platform === 'darwin' || platform === 'win32' || platform === 'linux'; + const isDiagSupported = platform === 'darwin' || platform === 'win32'; const [checkInstallTimeRequirementsAttempt, checkInstallTimeRequirements] = useAsync( @@ -485,6 +490,10 @@ export const VnetContextProvider: FC< useEffect( function periodicallyRunDiagnostics() { + if (!isDiagSupported) { + return; + } + if (status.value !== 'running') { return; } @@ -506,6 +515,7 @@ export const VnetContextProvider: FC< }; }, [ + isDiagSupported, diagnosticsIntervalMs, runDiagnosticsAndShowNotification, status.value, @@ -541,6 +551,7 @@ export const VnetContextProvider: FC< Date: Thu, 12 Mar 2026 21:09:03 +0000 Subject: [PATCH 19/26] refactor logging errors from dbusDaemon.Wait --- lib/vnet/dbus_service_linux.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/vnet/dbus_service_linux.go b/lib/vnet/dbus_service_linux.go index cd033604dbb74..d4952cc8a3f21 100644 --- a/lib/vnet/dbus_service_linux.go +++ b/lib/vnet/dbus_service_linux.go @@ -144,7 +144,7 @@ func (d *dbusDaemon) Close() { func (d *dbusDaemon) Wait() error { err := <-d.done if err != nil && !errors.Is(err, context.Canceled) { - log.DebugContext(context.Background(), "D-Bus daemon background task exited with error after close", "error", err) + return err } return nil } @@ -171,8 +171,10 @@ func (d *dbusDaemon) Start(addr, credPath string, sender dbus.Sender) *dbus.Erro go func() { err := d.startAdminProcess(addr, credPath) // TODO(tangyatsu): D-Bus supports signals, we might want to emit a signal when the admin process exits. - if err != nil { + if err != nil && !errors.Is(err, context.Canceled) { log.ErrorContext(context.Background(), "VNet admin process exited with error", "error", err) + } else { + log.InfoContext(context.Background(), "VNet admin process exited") } d.done <- err d.Close() From f192f39787d2dce77dae2d8696fd781d779c08fb Mon Sep 17 00:00:00 2001 From: Pavel Evdokimov Date: Thu, 12 Mar 2026 21:28:19 +0000 Subject: [PATCH 20/26] do not return error for missing TUN during deconfigure --- lib/vnet/osconfig_linux.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lib/vnet/osconfig_linux.go b/lib/vnet/osconfig_linux.go index 9ea5adbb7e98e..d02df9e491d02 100644 --- a/lib/vnet/osconfig_linux.go +++ b/lib/vnet/osconfig_linux.go @@ -18,8 +18,10 @@ package vnet import ( "context" + "errors" "net" "slices" + "strings" "github.com/godbus/dbus/v5" "github.com/gravitational/trace" @@ -109,6 +111,7 @@ func configureDNS(ctx context.Context, cfg *osConfig, state *platformOSConfigSta if len(cfg.dnsAddrs) > 0 && cfg.tunName == "" { return trace.BadParameter("empty TUN interface name with non-empty nameserver") } + deconfigure := len(cfg.dnsAddrs) == 0 && len(cfg.dnsZones) == 0 conn, err := dbus.ConnectSystemBus() if err != nil { @@ -122,6 +125,12 @@ func configureDNS(ctx context.Context, cfg *osConfig, state *platformOSConfigSta if shouldReconfigureDNSZones(cfg, state) { iface, err := net.InterfaceByName(state.tunName) if err != nil { + if deconfigure && isNoSuchNetworkInterfaceError(err) { + // During shutdown the TUN can disappear before deconfigure runs. + // In that case there is no link left to clear. + log.DebugContext(ctx, "Skipping DNS deconfiguration because TUN interface is unavailable.", "device", state.tunName) + return nil + } return trace.Wrap(err, "looking up interface %s", state.tunName) } log.InfoContext(ctx, "Configuring DNS zones", "zones", cfg.dnsZones) @@ -167,3 +176,11 @@ func configureDNS(ctx context.Context, cfg *osConfig, state *platformOSConfigSta return nil } + +func isNoSuchNetworkInterfaceError(err error) bool { + var opErr *net.OpError + if errors.As(err, &opErr) { + return strings.Contains(opErr.Err.Error(), "no such network interface") + } + return false +} From 2579159033a1d7a8c44c1d6cb52f180a5bec17b7 Mon Sep 17 00:00:00 2001 From: Pavel Evdokimov Date: Fri, 13 Mar 2026 13:54:25 +0000 Subject: [PATCH 21/26] use d.conn.Context() --- lib/vnet/dbus_service_linux.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/vnet/dbus_service_linux.go b/lib/vnet/dbus_service_linux.go index d4952cc8a3f21..033bf558ee077 100644 --- a/lib/vnet/dbus_service_linux.go +++ b/lib/vnet/dbus_service_linux.go @@ -214,7 +214,7 @@ func (d *dbusDaemon) authorize(sender dbus.Sender) (uint32, error) { return uid, nil } - authCtx, cancel := context.WithTimeout(context.Background(), polkitAuthorizationTimeout) + authCtx, cancel := context.WithTimeout(d.conn.Context(), polkitAuthorizationTimeout) defer cancel() subject := polkit.NewSystemBusNameSubject(string(sender)) From d8b7d0ce2fceae71657cb5f25e3838cc6e2c1f6b Mon Sep 17 00:00:00 2001 From: Pavel Evdokimov Date: Fri, 13 Mar 2026 14:00:14 +0000 Subject: [PATCH 22/26] avoid using bare slog logger --- lib/vnet/dns/dns.go | 6 ++---- lib/vnet/dns/dns_test.go | 4 ++-- lib/vnet/dns/osnameservers.go | 11 +++++++---- lib/vnet/dns/osnameservers_darwin.go | 2 +- lib/vnet/dns/osnameservers_darwin_test.go | 5 +++-- lib/vnet/dns/osnameservers_linux.go | 2 +- lib/vnet/dns/osnameservers_other.go | 4 +++- lib/vnet/dns/osnameservers_windows.go | 2 +- lib/vnet/network_stack.go | 24 +++++++++++++---------- 9 files changed, 34 insertions(+), 26 deletions(-) diff --git a/lib/vnet/dns/dns.go b/lib/vnet/dns/dns.go index fac646f231256..700b0274cc126 100644 --- a/lib/vnet/dns/dns.go +++ b/lib/vnet/dns/dns.go @@ -29,8 +29,6 @@ import ( "golang.org/x/net/dns/dnsmessage" "golang.org/x/sync/errgroup" "gvisor.dev/gvisor/pkg/tcpip" - - "github.com/gravitational/teleport" ) const ( @@ -88,7 +86,7 @@ type Server struct { // NewServer returns a DNS server that handles the details of the DNS protocol and asks [resolver] to answer // DNS questions. If [resolver] has no answer, requests will be forwarded to the upstream nameservers provided // by [upstreamNameserverSource]. -func NewServer(resolver Resolver, upstreamNameserverSource UpstreamNameserverSource) (*Server, error) { +func NewServer(resolver Resolver, upstreamNameserverSource UpstreamNameserverSource, logger *slog.Logger) (*Server, error) { return &Server{ resolver: resolver, upstreamNameserverSource: upstreamNameserverSource, @@ -98,7 +96,7 @@ func NewServer(resolver Resolver, upstreamNameserverSource UpstreamNameserverSou return &buf }, }, - slog: slog.With(teleport.ComponentKey, teleport.Component("vnet", "dns")), + slog: logger, }, nil } diff --git a/lib/vnet/dns/dns_test.go b/lib/vnet/dns/dns_test.go index b89418b5df646..80c30d4ae07e5 100644 --- a/lib/vnet/dns/dns_test.go +++ b/lib/vnet/dns/dns_test.go @@ -59,7 +59,7 @@ func TestServer(t *testing.T) { // Create two upstream nameservers that are able to resolve A and AAAA records for all names. var upstreamAddrs []string for i := range 2 { - upstreamServer, err := NewServer(staticResolver, noUpstreams) + upstreamServer, err := NewServer(staticResolver, noUpstreams, slog.Default()) require.NoError(t, err) conn, err := net.ListenUDP("udp", udpLocalhost) require.NoError(t, err) @@ -108,7 +108,7 @@ func TestServer(t *testing.T) { }, } upstreams := &stubUpstreamNamservers{nameservers: upstreamAddrs} - server, err := NewServer(resolver, upstreams) + server, err := NewServer(resolver, upstreams, slog.Default()) require.NoError(t, err) conn, err := net.ListenUDP("udp", udpLocalhost) diff --git a/lib/vnet/dns/osnameservers.go b/lib/vnet/dns/osnameservers.go index d312e7f1695ec..79e1c60a684ff 100644 --- a/lib/vnet/dns/osnameservers.go +++ b/lib/vnet/dns/osnameservers.go @@ -18,6 +18,7 @@ package dns import ( "context" + "log/slog" "net/netip" "time" @@ -31,10 +32,11 @@ import ( // these nameservers. type OSUpstreamNameserverSource struct { ttlCache *utils.FnCache + slog *slog.Logger } // NewOSUpstreamNameserverSource returns a new *OSUpstreamNameserverSource. -func NewOSUpstreamNameserverSource() (*OSUpstreamNameserverSource, error) { +func NewOSUpstreamNameserverSource(logger *slog.Logger) (*OSUpstreamNameserverSource, error) { ttlCache, err := utils.NewFnCache(utils.FnCacheConfig{ TTL: 10 * time.Second, }) @@ -43,17 +45,18 @@ func NewOSUpstreamNameserverSource() (*OSUpstreamNameserverSource, error) { } return &OSUpstreamNameserverSource{ ttlCache: ttlCache, + slog: logger, }, 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) { + return platformLoadUpstreamNameservers(ctx, s.slog) } // AddrWithDNSPort returns addr with DNS port 53. diff --git a/lib/vnet/dns/osnameservers_darwin.go b/lib/vnet/dns/osnameservers_darwin.go index f0471cbd69291..945c85bb017f9 100644 --- a/lib/vnet/dns/osnameservers_darwin.go +++ b/lib/vnet/dns/osnameservers_darwin.go @@ -37,7 +37,7 @@ const ( // 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, slog *slog.Logger) ([]string, error) { f, err := os.Open(confFilePath) if err != nil { return nil, trace.Wrap(err, "opening %s", confFilePath) diff --git a/lib/vnet/dns/osnameservers_darwin_test.go b/lib/vnet/dns/osnameservers_darwin_test.go index c33df777bef67..b19ca51293e26 100644 --- a/lib/vnet/dns/osnameservers_darwin_test.go +++ b/lib/vnet/dns/osnameservers_darwin_test.go @@ -20,6 +20,7 @@ package dns import ( "context" + "log/slog" "net" "testing" @@ -39,9 +40,9 @@ func TestOSUpstreamNameservers(t *testing.T) { t.Cleanup(cancel) resolver := &stubResolver{} - upstreams, err := NewOSUpstreamNameserverSource() + upstreams, err := NewOSUpstreamNameserverSource(slog.Default()) require.NoError(t, err) - server, err := NewServer(resolver, upstreams) + server, err := NewServer(resolver, upstreams, slog.Default()) require.NoError(t, err) conn, err := net.ListenUDP("udp", udpLocalhost) diff --git a/lib/vnet/dns/osnameservers_linux.go b/lib/vnet/dns/osnameservers_linux.go index 69591db44d08d..fe7c3ef478af8 100644 --- a/lib/vnet/dns/osnameservers_linux.go +++ b/lib/vnet/dns/osnameservers_linux.go @@ -29,7 +29,7 @@ import ( ) // platformLoadUpstreamNameservers returns the list of DNS upstreams configured in systemd-resolved. -func platformLoadUpstreamNameservers(ctx context.Context) ([]string, error) { +func platformLoadUpstreamNameservers(ctx context.Context, slog *slog.Logger) ([]string, error) { conn, err := dbus.ConnectSystemBus() if err != nil { return nil, trace.NotFound("system D-Bus is unavailable: %v", err) diff --git a/lib/vnet/dns/osnameservers_other.go b/lib/vnet/dns/osnameservers_other.go index b1fd662fbe754..7cf5417d53750 100644 --- a/lib/vnet/dns/osnameservers_other.go +++ b/lib/vnet/dns/osnameservers_other.go @@ -20,6 +20,7 @@ package dns import ( "context" + "log/slog" "runtime" "github.com/gravitational/trace" @@ -30,6 +31,7 @@ var ( vnetNotImplemented = &trace.NotImplementedError{Message: "VNet is not implemented on " + runtime.GOOS} ) -func platformLoadUpstreamNameservers(ctx context.Context) ([]string, error) { +func platformLoadUpstreamNameservers(ctx context.Context, slog *slog.Logger) ([]string, error) { + _ = slog return nil, trace.Wrap(vnetNotImplemented) } diff --git a/lib/vnet/dns/osnameservers_windows.go b/lib/vnet/dns/osnameservers_windows.go index cda6d1eef5a03..7040ec77392ec 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, slog *slog.Logger) ([]string, error) { interfaces, err := winipcfg.GetIPInterfaceTable(windows.AF_INET) if err != nil { return nil, trace.Wrap(err, "looking up local network interfaces") diff --git a/lib/vnet/network_stack.go b/lib/vnet/network_stack.go index 5448f1f1a4d80..9554e0fefcc98 100644 --- a/lib/vnet/network_stack.go +++ b/lib/vnet/network_stack.go @@ -50,6 +50,7 @@ var log = logutils.NewPackageLogger(teleport.ComponentKey, logComponent) const ( logComponent = "vnet" + dnsLogComponent = "dns" nicID = 1 mtu = 1500 tcpReceiveBufferSize = 0 // 0 means a default will be used. @@ -190,14 +191,16 @@ type filteredUpstreamSource struct { base dns.UpstreamNameserverSource mu sync.RWMutex exclude map[string]struct{} + slog *slog.Logger } // filteredUpstreamSource wraps an upstream source and excludes addresses added via AddExclude. // It is mainly used to filter VNet's own DNS addresses. -func newFilteredUpstreamSource(base dns.UpstreamNameserverSource) *filteredUpstreamSource { +func newFilteredUpstreamSource(base dns.UpstreamNameserverSource, slog *slog.Logger) *filteredUpstreamSource { return &filteredUpstreamSource{ base: base, exclude: make(map[string]struct{}), + slog: slog, } } @@ -215,11 +218,11 @@ func (f *filteredUpstreamSource) UpstreamNameservers(ctx context.Context) ([]str if err != nil { return nil, err } - slog.DebugContext(ctx, "Loaded upstream nameservers (pre-filter)", "nameservers", nameservers) + f.slog.DebugContext(ctx, "Loaded upstream nameservers (pre-filter)", "nameservers", nameservers) f.mu.RLock() defer f.mu.RUnlock() if len(f.exclude) == 0 { - slog.DebugContext(ctx, "Loaded upstream nameservers (post-filter)", "nameservers", nameservers) + f.slog.DebugContext(ctx, "Loaded upstream nameservers (post-filter)", "nameservers", nameservers) return nameservers, nil } filtered := nameservers[:0] @@ -229,7 +232,7 @@ func (f *filteredUpstreamSource) UpstreamNameservers(ctx context.Context) ([]str } filtered = append(filtered, nameserver) } - slog.DebugContext(ctx, "Loaded upstream nameservers (post-filter)", "nameservers", filtered) + f.slog.DebugContext(ctx, "Loaded upstream nameservers (post-filter)", "nameservers", filtered) return filtered, nil } @@ -265,7 +268,8 @@ func newNetworkStack(cfg *networkStackConfig) (*networkStack, error) { if err := cfg.checkAndSetDefaults(); err != nil { return nil, trace.Wrap(err) } - slog := slog.With(teleport.ComponentKey, logComponent) + logger := slog.With(teleport.ComponentKey, logComponent) + dnsLogger := slog.With(teleport.ComponentKey, teleport.Component(logComponent, dnsLogComponent)) stack, linkEndpoint, err := createStack() if err != nil { @@ -284,17 +288,17 @@ func newNetworkStack(cfg *networkStackConfig) (*networkStack, error) { tcpHandlerResolver: cfg.tcpHandlerResolver, destroyed: make(chan struct{}), state: newState(), - slog: slog, + slog: logger, } upstreamNameserverSource := cfg.upstreamNameserverSource if upstreamNameserverSource == nil { - upstreamNameserverSource, err = dns.NewOSUpstreamNameserverSource() + upstreamNameserverSource, err = dns.NewOSUpstreamNameserverSource(dnsLogger) if err != nil { return nil, trace.Wrap(err) } } - upstreamFilter := newFilteredUpstreamSource(upstreamNameserverSource) + upstreamFilter := newFilteredUpstreamSource(upstreamNameserverSource, dnsLogger) ns.upstreamFilter = upstreamFilter if cfg.dnsIPv6 != (tcpip.Address{}) { addr, ok := netip.AddrFromSlice(cfg.dnsIPv6.AsSlice()) @@ -303,7 +307,7 @@ func newNetworkStack(cfg *networkStackConfig) (*networkStack, error) { } upstreamFilter.AddExclude(dns.AddrWithDNSPort(addr)) } - dnsServer, err := dns.NewServer(ns, upstreamFilter) + dnsServer, err := dns.NewServer(ns, upstreamFilter, dnsLogger) if err != nil { return nil, trace.Wrap(err) } @@ -319,7 +323,7 @@ func newNetworkStack(cfg *networkStackConfig) (*networkStack, error) { 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) + logger.DebugContext(context.Background(), "Serving DNS on IPv6.", "dns_addr", cfg.dnsIPv6) } return ns, nil From 9a1753c5715fa22987866d9291702762d6b1263b Mon Sep 17 00:00:00 2001 From: Pavel Evdokimov Date: Fri, 13 Mar 2026 14:27:47 +0000 Subject: [PATCH 23/26] avoid mutating cached upstream nameserver slices --- lib/vnet/network_stack.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/vnet/network_stack.go b/lib/vnet/network_stack.go index 9554e0fefcc98..207af902b47d1 100644 --- a/lib/vnet/network_stack.go +++ b/lib/vnet/network_stack.go @@ -225,7 +225,7 @@ func (f *filteredUpstreamSource) UpstreamNameservers(ctx context.Context) ([]str f.slog.DebugContext(ctx, "Loaded upstream nameservers (post-filter)", "nameservers", nameservers) return nameservers, nil } - filtered := nameservers[:0] + filtered := make([]string, 0, len(nameservers)) for _, nameserver := range nameservers { if _, ok := f.exclude[nameserver]; ok { continue From 214f13535f62944df7ebfaf88361ed55e25eb4ed Mon Sep 17 00:00:00 2001 From: Pavel Evdokimov Date: Fri, 13 Mar 2026 23:00:59 +0000 Subject: [PATCH 24/26] fix race between dbus Start and Stop --- lib/vnet/dbus_service_linux.go | 49 +++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/lib/vnet/dbus_service_linux.go b/lib/vnet/dbus_service_linux.go index 033bf558ee077..93c00b316054f 100644 --- a/lib/vnet/dbus_service_linux.go +++ b/lib/vnet/dbus_service_linux.go @@ -20,7 +20,6 @@ import ( "context" "errors" "sync" - "sync/atomic" "time" "github.com/godbus/dbus/v5" @@ -119,26 +118,33 @@ func newDBusDaemon() (_ *dbusDaemon, err error) { } type dbusDaemon struct { - conn *dbus.Conn - started atomic.Bool - closing atomic.Bool - done chan error // buffered 1; receives the admin process error or nil - closeOnce sync.Once + mu sync.Mutex + conn *dbus.Conn + started bool + closing bool + done chan error // buffered 1; receives the admin process error or nil startAdminProcess func(addr, credPath string) error cancelAdminProcess context.CancelFunc } func (d *dbusDaemon) Close() { - d.closing.Store(true) - d.closeOnce.Do(func() { - d.cancelAdminProcess() - _ = d.conn.Close() - // If no admin process goroutine was started, unblock Wait. - if !d.started.Load() { - d.done <- nil - } - }) + d.mu.Lock() + if d.closing { + d.mu.Unlock() + return + } + d.closing = true + started := d.started + d.mu.Unlock() + + d.cancelAdminProcess() + _ = d.conn.Close() + + // If no admin process goroutine was started, unblock Wait. + if !started { + d.done <- nil + } } func (d *dbusDaemon) Wait() error { @@ -158,13 +164,18 @@ func (d *dbusDaemon) Start(addr, credPath string, sender dbus.Sender) *dbus.Erro return dbus.MakeFailedError(trace.Wrap(err, "authorization failed")) } - if d.closing.Load() { + d.mu.Lock() + if d.closing { + d.mu.Unlock() return dbus.MakeFailedError(trace.Errorf("VNet D-Bus daemon is shutting down")) } - if !d.started.CompareAndSwap(false, true) { + if d.started { + d.mu.Unlock() return dbus.MakeFailedError(trace.Errorf("VNet admin process already started")) } + d.started = true + d.mu.Unlock() log.InfoContext(context.Background(), "Starting VNet admin process", "uid", uid) @@ -232,9 +243,9 @@ func (d *dbusDaemon) authorize(sender dbus.Sender) (uint32, error) { } if !result.Authorized { if result.Challenge { - return 0, trace.Errorf("polkit authentication required") + return 0, trace.AccessDenied("polkit authentication required") } - return 0, trace.Errorf("polkit authorization denied") + return 0, trace.AccessDenied("polkit authorization denied") } return uid, nil } From 9dbf3f846f4dd95c0151b852ef25d0be970a0b5d Mon Sep 17 00:00:00 2001 From: Pavel Evdokimov Date: Fri, 13 Mar 2026 23:33:42 +0000 Subject: [PATCH 25/26] small fixes --- lib/vnet/admin_process_unix.go | 1 + lib/vnet/polkit/polkit.go | 4 +--- lib/vnet/systemdresolved/dbus.go | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/vnet/admin_process_unix.go b/lib/vnet/admin_process_unix.go index 15d19e866225c..2173be88e7ed5 100644 --- a/lib/vnet/admin_process_unix.go +++ b/lib/vnet/admin_process_unix.go @@ -126,6 +126,7 @@ func createTUNDevice(ctx context.Context, interfaceName string) (tun.Device, str } name, err := dev.Name() if err != nil { + dev.Close() return nil, "", trace.Wrap(err, "getting TUN device name") } return dev, name, nil diff --git a/lib/vnet/polkit/polkit.go b/lib/vnet/polkit/polkit.go index 74a4f25986e75..bd88174ad6825 100644 --- a/lib/vnet/polkit/polkit.go +++ b/lib/vnet/polkit/polkit.go @@ -75,11 +75,9 @@ func CheckAuthorization( allowUserInteraction bool, cancellationID string, ) (AuthorizationResult, error) { - var flags uint32 + flags := CheckAuthorizationFlagNone if allowUserInteraction { flags = CheckAuthorizationFlagAllowUserInteraction - } else { - flags = CheckAuthorizationFlagNone } var result AuthorizationResult if err := conn.Object(AuthorityServiceName, dbus.ObjectPath(AuthorityObjectPath)). diff --git a/lib/vnet/systemdresolved/dbus.go b/lib/vnet/systemdresolved/dbus.go index 190cde197b4b0..72c0e996aa007 100644 --- a/lib/vnet/systemdresolved/dbus.go +++ b/lib/vnet/systemdresolved/dbus.go @@ -141,7 +141,7 @@ func SetLinkDNS(ctx context.Context, conn *dbus.Conn, ifaceIndex int32, addresse func DNSAddressForIP(raw string) (DNSAddress, error) { ip := net.ParseIP(raw) if ip == nil { - return DNSAddress{}, trace.Errorf("invalid IP address") + return DNSAddress{}, trace.BadParameter("invalid IP address: %s", raw) } if ip4 := ip.To4(); ip4 != nil { return DNSAddress{ From 2cdddd381dc8c8820fe6f63565330307b36492ae Mon Sep 17 00:00:00 2001 From: Pavel Evdokimov Date: Mon, 16 Mar 2026 18:43:58 +0000 Subject: [PATCH 26/26] some refactoring --- lib/vnet/dbus_service_linux.go | 6 +----- lib/vnet/dns/osnameservers_other.go | 1 - 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/lib/vnet/dbus_service_linux.go b/lib/vnet/dbus_service_linux.go index 93c00b316054f..cda2e3195eddb 100644 --- a/lib/vnet/dbus_service_linux.go +++ b/lib/vnet/dbus_service_linux.go @@ -165,18 +165,14 @@ func (d *dbusDaemon) Start(addr, credPath string, sender dbus.Sender) *dbus.Erro } d.mu.Lock() + defer d.mu.Unlock() if d.closing { - d.mu.Unlock() return dbus.MakeFailedError(trace.Errorf("VNet D-Bus daemon is shutting down")) } - if d.started { - d.mu.Unlock() return dbus.MakeFailedError(trace.Errorf("VNet admin process already started")) } d.started = true - d.mu.Unlock() - log.InfoContext(context.Background(), "Starting VNet admin process", "uid", uid) go func() { diff --git a/lib/vnet/dns/osnameservers_other.go b/lib/vnet/dns/osnameservers_other.go index 7cf5417d53750..9dfdc28c8b9ad 100644 --- a/lib/vnet/dns/osnameservers_other.go +++ b/lib/vnet/dns/osnameservers_other.go @@ -32,6 +32,5 @@ var ( ) func platformLoadUpstreamNameservers(ctx context.Context, slog *slog.Logger) ([]string, error) { - _ = slog return nil, trace.Wrap(vnetNotImplemented) }