Skip to content

Commit

Permalink
[vnet] Windows service
Browse files Browse the repository at this point in the history
  • Loading branch information
nklaassen committed Jan 14, 2025
1 parent 9648bed commit 18bf244
Show file tree
Hide file tree
Showing 27 changed files with 717 additions and 565 deletions.
2 changes: 1 addition & 1 deletion lib/teleterm/vnet/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ func (s *Service) Start(ctx context.Context, req *api.StartRequest) (*api.StartR
}

s.clusterConfigCache = vnet.NewClusterConfigCache(s.cfg.Clock)
processManager, err := vnet.Run(ctx, &vnet.RunConfig{
processManager, err := vnet.RunUserProcess(ctx, &vnet.UserProcessConfig{
AppProvider: appProvider,
ClusterConfigCache: s.clusterConfigCache,
})
Expand Down
140 changes: 9 additions & 131 deletions lib/vnet/admin_process.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Teleport
// Copyright (C) 2024 Gravitational, Inc.
// 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
Expand All @@ -18,140 +18,18 @@ package vnet

import (
"context"
"os"
"time"

"github.com/gravitational/trace"
"golang.zx2c4.com/wireguard/tun"

"github.com/gravitational/teleport/lib/vnet/daemon"
)

// RunAdminProcess must run as root. It creates and sets up a TUN device and passes
// the file descriptor for that device over the unix socket found at config.socketPath.
//
// It also handles host OS configuration that must run as root, and stays alive to keep the host configuration
// up to date. It will stay running until the socket at config.socketPath is deleted or until encountering an
// unrecoverable error.
//
// OS configuration is updated every [osConfigurationInterval]. During the update, it temporarily
// changes egid and euid of the process to that of the client connecting to the daemon.
func RunAdminProcess(ctx context.Context, config daemon.Config) error {
if err := config.CheckAndSetDefaults(); err != nil {
return trace.Wrap(err)
}

ctx, cancel := context.WithCancel(ctx)
defer cancel()

tunName, err := createAndSendTUNDevice(ctx, config.SocketPath)
if err != nil {
return trace.Wrap(err)
}

errCh := make(chan error)
go func() {
errCh <- trace.Wrap(osConfigurationLoop(ctx, tunName, config.IPv6Prefix, config.DNSAddr, config.HomePath, config.ClientCred))
}()

// Stay alive until we get an error on errCh, indicating that the osConfig loop exited.
// If the socket is deleted, indicating that the unprivileged process exited, cancel the context
// and then wait for the osConfig loop to exit and send an err on errCh.
ticker := time.NewTicker(daemon.CheckUnprivilegedProcessInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if _, err := os.Stat(config.SocketPath); err != nil {
log.DebugContext(ctx, "failed to stat socket path, assuming parent exited")
cancel()
return trace.Wrap(<-errCh)
}
case err := <-errCh:
return trace.Wrap(err)
}
}
// AdminProcessConfig holds configuration parameters for the VNet admin process.
type AdminProcessConfig struct {
PlatformAdminProcessConfig
}

// createAndSendTUNDevice creates a virtual network TUN device and sends the open file descriptor on
// [socketPath]. It returns the name of the TUN device or an error.
func createAndSendTUNDevice(ctx context.Context, socketPath string) (string, error) {
tun, tunName, err := createTUNDevice(ctx)
if err != nil {
return "", trace.Wrap(err, "creating TUN device")
}

defer func() {
// We can safely close the TUN device in the admin process after it has been sent on the socket.
if err := tun.Close(); err != nil {
log.WarnContext(ctx, "Failed to close TUN device.", "error", trace.Wrap(err))
}
}()

if err := sendTUNNameAndFd(socketPath, tunName, tun.File()); err != nil {
return "", trace.Wrap(err, "sending TUN over socket")
}
return tunName, nil
}

func createTUNDevice(ctx context.Context) (tun.Device, string, error) {
log.DebugContext(ctx, "Creating TUN device.")
dev, err := tun.CreateTUN("utun", mtu)
if err != nil {
return nil, "", trace.Wrap(err, "creating TUN device")
}
name, err := dev.Name()
if err != nil {
return nil, "", trace.Wrap(err, "getting TUN device name")
}
return dev, name, nil
}

// osConfigurationLoop will keep running until [ctx] is canceled or an unrecoverable error is encountered, in
// order to keep the host OS configuration up to date.
func osConfigurationLoop(ctx context.Context, tunName, ipv6Prefix, dnsAddr, homePath string, clientCred daemon.ClientCred) error {
osConfigurator, err := newOSConfigurator(tunName, ipv6Prefix, dnsAddr, homePath, clientCred)
if err != nil {
return trace.Wrap(err, "creating OS configurator")
}
defer func() {
if err := osConfigurator.close(); err != nil {
log.ErrorContext(ctx, "Error while closing OS configurator", "error", err)
}
}()

// Clean up any stale configuration left by a previous VNet instance that may have failed to clean up.
// This is necessary in case any stale /etc/resolver/<proxy address> entries are still present, we need to
// be able to reach the proxy in order to fetch the vnet_config.
if err := osConfigurator.deconfigureOS(ctx); err != nil {
return trace.Wrap(err, "cleaning up OS configuration on startup")
}

defer func() {
// Shutting down, deconfigure OS. Pass context.Background because [ctx] has likely been canceled
// already but we still need to clean up.
if err := osConfigurator.deconfigureOS(context.Background()); err != nil {
log.ErrorContext(ctx, "Error deconfiguring host OS before shutting down.", "error", err)
}
}()

if err := osConfigurator.updateOSConfiguration(ctx); err != nil {
return trace.Wrap(err, "applying initial OS configuration")
}

// Re-configure the host OS every 10 seconds. This will pick up any newly logged-in clusters by
// reading profiles from TELEPORT_HOME.
const osConfigurationInterval = 10 * time.Second
ticker := time.NewTicker(osConfigurationInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if err := osConfigurator.updateOSConfiguration(ctx); err != nil {
return trace.Wrap(err, "updating OS configuration")
}
case <-ctx.Done():
return ctx.Err()
}
}
// RunAdminProcess must run as root/administrator. It creates and sets up a TUN
// device and handles all OS configuration.
func RunAdminProcess(ctx context.Context, cfg AdminProcessConfig) error {
log.InfoContext(ctx, "Running VNet admin process", "cfg", cfg)
return trace.Wrap(platformRunAdminProcess(ctx, cfg))
}
119 changes: 119 additions & 0 deletions lib/vnet/admin_process_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// 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 <http://www.gnu.org/licenses/>.

package vnet

import (
"context"
"os"
"time"

"github.com/gravitational/trace"
"golang.zx2c4.com/wireguard/tun"

"github.com/gravitational/teleport/lib/vnet/daemon"
)

// PlatformAdminProcessConfig holds platform-specific parameters for the VNet
// admin process.
type PlatformAdminProcessConfig daemon.Config

func (c *PlatformAdminProcessConfig) checkAndSetDefaults() error {
return trace.Wrap((*daemon.Config)(c).CheckAndSetDefaults())
}

// platformRunAdminProcess must run as root. It creates and sets up a TUN device
// and passes the file descriptor for that device over the unix socket found at
// config.socketPath.
//
// It also handles host OS configuration that must run as root, and stays alive
// to keep the host configuration up to date. It will stay running until the
// socket at config.socketPath is deleted or until
// encountering an unrecoverable error.
//
// OS configuration is updated every [osConfigurationInterval]. During the
// update, it temporarily changes egid and euid of the process to that of the
// client connecting to the daemon.
func platformRunAdminProcess(ctx context.Context, config AdminProcessConfig) error {
if err := config.checkAndSetDefaults(); err != nil {
return trace.Wrap(err, "checking daemon process config")
}

ctx, cancel := context.WithCancel(ctx)
defer cancel()

tunName, err := createAndSendTUNDevice(ctx, config.SocketPath)
if err != nil {
return trace.Wrap(err)
}

errCh := make(chan error)
go func() {
errCh <- trace.Wrap(osConfigurationLoop(ctx, tunName, config))
}()

// Stay alive until we get an error on errCh, indicating that the osConfig loop exited.
// If the socket is deleted, indicating that the unprivileged process exited, cancel the context
// and then wait for the osConfig loop to exit and send an err on errCh.
ticker := time.NewTicker(daemon.CheckUnprivilegedProcessInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if _, err := os.Stat(config.SocketPath); err != nil {
log.DebugContext(ctx, "failed to stat socket path, assuming parent exited")
cancel()
return trace.Wrap(<-errCh)
}
case err := <-errCh:
return trace.Wrap(err)
}
}
}

// createAndSendTUNDevice creates a virtual network TUN device and sends the open file descriptor on
// [socketPath]. It returns the name of the TUN device or an error.
func createAndSendTUNDevice(ctx context.Context, socketPath string) (string, error) {
tun, tunName, err := createTUNDevice(ctx)
if err != nil {
return "", trace.Wrap(err, "creating TUN device")
}

defer func() {
// We can safely close the TUN device in the admin process after it has been sent on the socket.
if err := tun.Close(); err != nil {
log.WarnContext(ctx, "Failed to close TUN device.", "error", trace.Wrap(err))
}
}()

if err := sendTUNNameAndFd(socketPath, tunName, tun.File()); err != nil {
return "", trace.Wrap(err, "sending TUN over socket")
}
return tunName, nil
}

func createTUNDevice(ctx context.Context) (tun.Device, string, error) {
log.DebugContext(ctx, "Creating TUN device.")
dev, err := tun.CreateTUN("utun", mtu)
if err != nil {
return nil, "", trace.Wrap(err, "creating TUN device")
}
name, err := dev.Name()
if err != nil {
return nil, "", trace.Wrap(err, "getting TUN device name")
}
return dev, name, nil
}
47 changes: 47 additions & 0 deletions lib/vnet/admin_process_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// 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 <http://www.gnu.org/licenses/>.

package vnet

import (
"context"

"github.com/gravitational/trace"
"golang.zx2c4.com/wireguard/tun"
)

// PlatformAdminProcessConfig holds platform-specific parameters for the VNet
// admin process.
type PlatformAdminProcessConfig struct{}

// platformRunAdminProcess must run as administrator. It creates and sets up a TUN
// device, runs the VNet networking stack, and handles OS configuration.
func platformRunAdminProcess(ctx context.Context, cfg AdminProcessConfig) error {
device, err := tun.CreateTUN("TeleportVNet", mtu)
if err != nil {
return trace.Wrap(err, "creating TUN device")
}
defer device.Close()
tunName, err := device.Name()
if err != nil {
return trace.Wrap(err, "getting TUN device name")
}
log.InfoContext(ctx, "Created TUN interface", "tun", tunName)
// TODO(nklaassen): actually run VNet. For now, just stay alive until the
// context is canceled.
<-ctx.Done()
return trace.Wrap(ctx.Err())
}
8 changes: 5 additions & 3 deletions lib/vnet/escalate_daemon_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@ import (

// execAdminProcess is called from the normal user process to register and call
// the daemon process which runs as root.
func execAdminProcess(ctx context.Context, config daemon.Config) error {
return trace.Wrap(daemon.RegisterAndCall(ctx, config))
func execAdminProcess(ctx context.Context, config AdminProcessConfig) error {
return trace.Wrap(daemon.RegisterAndCall(ctx, daemon.Config(config)))
}

// DaemonSubcommand runs the VNet daemon process.
func DaemonSubcommand(ctx context.Context) error {
return trace.Wrap(daemon.Start(ctx, RunAdminProcess))
return trace.Wrap(daemon.Start(ctx, func(ctx context.Context, config daemon.Config) error {
return RunAdminProcess(ctx, AdminProcessConfig(config))
}))
}
3 changes: 1 addition & 2 deletions lib/vnet/escalate_nodaemon_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,11 @@ import (

"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/vnet/daemon"
)

// execAdminProcess is called from the normal user process to execute
// "tsh vnet-admin-setup" as root via an osascript wrapper.
func execAdminProcess(ctx context.Context, config daemon.Config) error {
func execAdminProcess(ctx context.Context, config AdminProcessConfig) error {
executableName, err := os.Executable()
if err != nil {
return trace.Wrap(err, "getting executable path")
Expand Down
Loading

0 comments on commit 18bf244

Please sign in to comment.