Skip to content

Commit

Permalink
[vnet] install and run windows service
Browse files Browse the repository at this point in the history
This commit adds a Windows service for VNet. It adds support for
automatically installing and running the service when the user runs
`tsh vnet`, and adds a command to manually uninstall/delete the service.

The service creates the TUN interface and establishes an IPC connection
with the user process over a named pipe, but for now does not actually
handle any networking, the rest will come in later PRs.

If you want to test this out on a Windows machine/VM, you should be able
to run `tsh vnet` and see that:
1. A service TeleportVNet is installed and runs
   `sc.exe query state=all | grep -A3 Teleport`
2. The service writes logs to `logs.txt` in the directory where `tsh` is
   installed (this is temporary until I find a better place for logs).
3. A TUN interface is created `netsh interface show interface`
4. The service stops and the interface is cleaned up when the user
   process exits.

Unfortunately this PR does not include any unit tests. Most of the
functionality here needs to be able to escalate to administrator with a
UAC prompt and needs to run on Windows, this is exactly the kind of unit
test that is very hard to write and would never actually be able to run
in CI.
But, any part of this that's broken would immediately break VNet on
Windows, and this should be caught in any test plan.
  • Loading branch information
nklaassen committed Jan 10, 2025
1 parent 4e228f7 commit 36bdd77
Show file tree
Hide file tree
Showing 18 changed files with 770 additions and 187 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
61 changes: 9 additions & 52 deletions lib/vnet/admin_process.go → lib/vnet/admin_process_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ import (
"github.com/gravitational/teleport/lib/vnet/daemon"
)

type AdminProcessConfig daemon.Config

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

// 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.
//
Expand All @@ -36,9 +42,9 @@ import (
//
// 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 {
func RunAdminProcess(ctx context.Context, config AdminProcessConfig) error {
if err := config.CheckAndSetDefaults(); err != nil {
return trace.Wrap(err)
return trace.Wrap(err, "checking daemon process config")
}

ctx, cancel := context.WithCancel(ctx)
Expand All @@ -51,7 +57,7 @@ func RunAdminProcess(ctx context.Context, config daemon.Config) error {

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

// Stay alive until we get an error on errCh, indicating that the osConfig loop exited.
Expand Down Expand Up @@ -106,52 +112,3 @@ func createTUNDevice(ctx context.Context) (tun.Device, string, error) {
}
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()
}
}
}
66 changes: 66 additions & 0 deletions lib/vnet/admin_process_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// 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"
)

type AdminProcessConfig struct {
// TODO(nklaassen): delete these, the admin process will decide them, they
// don't need to be passed from the user process. Keeping them until I
// remove the references from osconfig.go.
IPv6Prefix string
DNSAddr string
HomePath string
}

// RunAdminProcess must run as administrator. It creates and sets up a TUN
// device and runs the VNet networking stack.
//
// It also handles host OS configuration, OS configuration is updated every [osConfigurationInterval].
//
// The admin process will stay running until the socket at config.socketPath is
// deleted or until encountering an unrecoverable error.
func RunAdminProcess(ctx context.Context, cfg AdminProcessConfig) error {
log.InfoContext(ctx, "Running VNet admin process", "cfg", cfg)

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

var (
// Satisfy unused linter.
// TODO(nklaassen): run os configuration loop in admin process.
_ = osConfigurationLoop
)
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 36bdd77

Please sign in to comment.