Skip to content

Commit

Permalink
cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
nklaassen committed Jan 7, 2025
1 parent e3ec4be commit b97bcdd
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 144 deletions.
42 changes: 4 additions & 38 deletions lib/vnet/admin_process_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,12 @@ package vnet

import (
"context"
"time"

"github.com/Microsoft/go-winio"
"github.com/gravitational/trace"
"golang.zx2c4.com/wireguard/tun"
)

type AdminProcessConfig struct {
// NamedPipe is the name of a pipe used for IPC between the user process and
// the admin service.
NamedPipe string

// 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.
Expand All @@ -38,13 +32,6 @@ type AdminProcessConfig struct {
HomePath string
}

func (c *AdminProcessConfig) CheckAndSetDefaults() error {
if c.NamedPipe == "" {
return trace.BadParameter("missing pipe path")
}
return nil
}

// RunAdminProcess must run as administrator. It creates and sets up a TUN
// device and runs the VNet networking stack.
//
Expand All @@ -53,19 +40,8 @@ func (c *AdminProcessConfig) CheckAndSetDefaults() error {
// 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 {
if err := cfg.CheckAndSetDefaults(); err != nil {
return trace.Wrap(err, "checking admin process config")
}
log.InfoContext(ctx, "Running VNet admin process", "cfg", cfg)

dialTimeout := 200 * time.Millisecond
conn, err := winio.DialPipe(pipePath, &dialTimeout)
if err != nil {
return trace.Wrap(err, "dialing named pipe %s", pipePath)
}
conn.Close()
log.InfoContext(ctx, "Successfully connected to user process over named pipe", "pipe", pipePath)

device, err := tun.CreateTUN("TeleportVNet", mtu)
if err != nil {
return trace.Wrap(err, "creating TUN device")
Expand All @@ -77,20 +53,10 @@ func RunAdminProcess(ctx context.Context, cfg AdminProcessConfig) error {
}
log.InfoContext(ctx, "Created TUN interface", "tun", tunName)

// TODO(nklaassen): actually run VNet. For now, stay alive as long as we can
// dial the pipe.
for {
select {
case <-time.After(time.Second):
conn, err := winio.DialPipe(pipePath, &dialTimeout)
if err != nil {
return trace.Wrap(err, "failed to dial pipe, assuming user process has terminated")
}
conn.Close()
case <-ctx.Done():
return ctx.Err()
}
}
// TODO(nklaassen): actually run VNet. For now, just stay alive until the
// context is canceled.
<-ctx.Done()
return trace.Wrap(ctx.Err())
}

var (
Expand Down
112 changes: 58 additions & 54 deletions lib/vnet/escalate_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ func execAdminProcess(ctx context.Context, cfg AdminProcessConfig) error {
}
}

// startService starts the Windows VNet admin service in the background, and
// installs it first if necessary.
func startService(ctx context.Context, cfg AdminProcessConfig) (*mgr.Service, error) {
// Avoid [mgr.Connect] because it requests elevated permissions.
scManager, err := windows.OpenSCManager(nil /*machine*/, nil /*database*/, windows.SC_MANAGER_CONNECT)
Expand All @@ -86,15 +88,15 @@ func startService(ctx context.Context, cfg AdminProcessConfig) (*mgr.Service, er
if err := escalateAndInstallService(); err != nil {
return nil, trace.Wrap(err, "installing Windows service")
}
if serviceHandle, err = waitForService(ctx, scManager, serviceNamePtr); err != nil {
if serviceHandle, err = waitForServiceToInstall(ctx, scManager, serviceNamePtr); err != nil {
return nil, trace.Wrap(err, "waiting for service immediately after installation")
}
}
service := &mgr.Service{
Name: serviceName,
Handle: serviceHandle,
}
if err := service.Start("vnet-service", "--pipe", cfg.NamedPipe); err != nil {
if err := service.Start("vnet-service"); err != nil {
return nil, trace.Wrap(err, "starting Windows service %s", serviceName)
}
return service, nil
Expand All @@ -105,56 +107,52 @@ func escalateAndInstallService() error {
if err != nil {
return trace.Wrap(err, "getting current user")
}
return trace.Wrap(escalateAndRunSubcommand("vnet-install-service", "--userSID", user.Uid))
if err := escalateAndRunSubcommand("vnet-install-service", "--userSID", user.Uid); err != nil {
return trace.Wrap(err, "installing VNet admin service")
}
return nil
}

func escalateAndRunSubcommand(args ...string) error {
tshPath, err := os.Executable()
if err != nil {
return trace.Wrap(err, "getting executable path")
}
argPtrs, err := ptrsFromStrings(
"runas",
shsprintf.EscapeDefaultContext(tshPath),
escapeAndJoinArgs(args...),
)
verbPtr, err := syscall.UTF16PtrFromString("runas")
if err != nil {
return trace.Wrap(err)
return trace.Wrap(err, "converting string to UTF16")
}
pathPtr, err := syscall.UTF16PtrFromString(shsprintf.EscapeDefaultContext(tshPath))
if err != nil {
return trace.Wrap(err, "converting string to UTF16")
}
argsPtr, err := syscall.UTF16PtrFromString(escapeAndJoinArgs(args...))
if err != nil {
return trace.Wrap(err, "converting string to UTF16")
}
if err := windows.ShellExecute(
0, // parent window handle (default is no window)
argPtrs[0], // verb
argPtrs[1], // file
argPtrs[2], // args
nil, // cwd (default is current directory)
1, // showCmd (1 is normal)
0, // parent window handle (default is no window)
verbPtr,
pathPtr,
argsPtr,
nil, // cwd (default is current directory)
1, // showCmd (1 is normal)
); err != nil {
return trace.Wrap(err, "running subcommand as administrator via runas")
}
return nil
}

func ptrsFromStrings(strs ...string) ([]*uint16, error) {
ptrs := make([]*uint16, len(strs))
for i := range ptrs {
var err error
ptrs[i], err = syscall.UTF16PtrFromString(strs[i])
if err != nil {
return nil, trace.Wrap(err, "converting string to UTF16")
}
}
return ptrs, nil
}

func escapeAndJoinArgs(args ...string) string {
for i := range args {
args[i] = shsprintf.EscapeDefaultContext(args[i])
}
return strings.Join(args, " ")
}

func waitForService(ctx context.Context, scManager windows.Handle, serviceNamePtr *uint16) (windows.Handle, error) {
deadline := time.After(30 * time.Second)
func waitForServiceToInstall(ctx context.Context, scManager windows.Handle, serviceNamePtr *uint16) (windows.Handle, error) {
timeout := time.Minute
deadline := time.After(timeout)
for {
serviceHandle, err := windows.OpenService(scManager, serviceNamePtr, serviceAccessFlags)
if err == nil {
Expand All @@ -164,8 +162,9 @@ func waitForService(ctx context.Context, scManager windows.Handle, serviceNamePt
case <-ctx.Done():
return 0, trace.Wrap(ctx.Err())
case <-deadline:
return 0, trace.Errorf("timeout waiting for service to start")
return 0, trace.Errorf("timed waiting for service to be installed after %v", timeout)
case <-time.After(time.Second):
log.InfoContext(ctx, "Waiting for admin service to be installed")
}
}
}
Expand All @@ -184,8 +183,11 @@ func InstallService(ctx context.Context, userSID string) error {
}
defer service.Close()
if err := configureServicePermissions(service, userSID); err != nil {
slog.ErrorContext(ctx, "Error configuring permissions for the Windows service, will attempt to delete the service", "error", err)
return trace.Wrap(service.Delete(), "deleting Windows service after failing to configure permissions")
log.ErrorContext(ctx, "Error configuring permissions for the Windows service, will attempt to delete the service", "error", err)
if deleteErr := service.Delete(); deleteErr != nil {
log.ErrorContext(ctx, "Failed to delete Windows service after failing to configure permissions", "error", err)
}
return trace.Wrap(err, "configuring windows service permissions")
}
return nil
}
Expand Down Expand Up @@ -257,14 +259,13 @@ func configureServicePermissions(service *mgr.Service, userSIDStr string) error
return nil
}

// UninstallService implements the vnet-uninstall-service command to uninstall
// UninstallService implements the `tsh vnet-uninstall-service` command to uninstall
// the TeleportVNet Windows service. If it does not have sufficient permissions,
// it tries to re-execute itself with administrator rights via a UAC prompt.
func UninstallService(ctx context.Context) error {
m, err := mgr.Connect()
if err != nil {
slog.ErrorContext(ctx, "Error connecting to service manager, attempting to escalate to administrator",
"error", err)
log.ErrorContext(ctx, "Error connecting to service manager, attempting to escalate to administrator", "error", err)
err := escalateAndRunSubcommand("vnet-uninstall-service")
return trace.Wrap(err, "escalating to administrator to uninstall service")
}
Expand All @@ -280,13 +281,11 @@ func UninstallService(ctx context.Context) error {
return nil
}

// ServiceMain runs with Windows VNet service.
// ServiceMain runs the Windows VNet admin service.
func ServiceMain() error {
cleanup, err := setupServiceLogger()
if err != nil {
return trace.Wrap(err)
if err := setupServiceLogger(); err != nil {
return trace.Wrap(err, "setting up logger for service")
}
defer cleanup()
if err := svc.Run(serviceName, &windowsService{}); err != nil {
return trace.Wrap(err, "running Windows service")
}
Expand Down Expand Up @@ -326,34 +325,42 @@ loop:
}
cancel()
status <- svc.Status{State: svc.StopPending}
<-errCh
// Ignoring the error because the stop was explicitly requested.
_ := <-errCh
const exitCode = 0
status <- svc.Status{State: svc.Stopped, Win32ExitCode: exitCode}
return false, exitCode
}

func (s *windowsService) run(ctx context.Context, args []string) error {
var pipePath string
app := kingpin.New(serviceName, "Teleport Windows Service")
serviceCmd := app.Command("vnet-service", "Start the VNet service.")
serviceCmd.Flag("pipe", "pipe path").Required().StringVar(&pipePath)
cmd, err := app.Parse(args[1:])
if err != nil {
return trace.Wrap(err, "parsing arguments")
return trace.Wrap(err, "parsing runtime service arguments")
}
if cmd != serviceCmd.FullCommand() {
return trace.BadParameter("executed arguments did not match vnet-service")
}
cfg := AdminProcessConfig{
NamedPipe: pipePath,
return trace.BadParameter("Windows service runtime arguments did not match vnet-service")
}
if err := RunAdminProcess(ctx, cfg); err != nil {
if err := RunAdminProcess(ctx, AdminProcessConfig{}); err != nil {
return trace.Wrap(err, "running admin process")
}
return nil
}

func setupServiceLogger() (func(), error) {
func setupServiceLogger() error {
logFile, err := serviceLogFile()
if err != nil {
return trace.Wrap(err, "creating log file for service")
}
slog.SetDefault(slog.New(slog.NewTextHandler(logFile, &slog.HandlerOptions{
Level: slog.LevelDebug,
})))
return nil
}

func serviceLogFile() (*os.File, error) {
// TODO(nklaassen): find a better path for Windows service logs.
exePath, err := os.Executable()
if err != nil {
return nil, trace.Wrap(err, "getting current executable path")
Expand All @@ -363,8 +370,5 @@ func setupServiceLogger() (func(), error) {
if err != nil {
return nil, trace.Wrap(err, "creating log file")
}
slog.SetDefault(slog.New(slog.NewTextHandler(logFile, &slog.HandlerOptions{
Level: slog.LevelDebug,
})))
return func() { logFile.Close() }, nil
return logFile, nil
}
56 changes: 4 additions & 52 deletions lib/vnet/user_process_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,14 @@ package vnet

import (
"context"
"log/slog"
"os"

"github.com/Microsoft/go-winio"
"github.com/gravitational/trace"

"github.com/gravitational/teleport/api/profile"
"github.com/gravitational/teleport/api/types"
)

const (
pipePath = `\\.\pipe\vnet`
)

// UserProcessConfig provides the necessary configuration to run VNet.
type UserProcessConfig struct {
// AppProvider is a required field providing an interface implementation for [AppProvider].
Expand All @@ -44,7 +38,7 @@ type UserProcessConfig struct {
HomePath string
}

func (c *UserProcessConfig) CheckAndSetDefaults() error {
func (c *UserProcessConfig) checkAndSetDefaults() error {
if c.AppProvider == nil {
return trace.BadParameter("missing AppProvider")
}
Expand All @@ -70,55 +64,13 @@ func (c *UserProcessConfig) CheckAndSetDefaults() error {
// control to the process manager. If ctx gets canceled during RunUserProcess, the process
// manager gets closed along with its background tasks.
func RunUserProcess(ctx context.Context, config *UserProcessConfig) (pm *ProcessManager, err error) {
defer func() {
if pm != nil && err != nil {
pm.Close()
}
}()
if err := config.CheckAndSetDefaults(); err != nil {
return nil, trace.Wrap(err)
}
ipv6Prefix, err := NewIPv6Prefix()
if err != nil {
if err := config.checkAndSetDefaults(); err != nil {
return nil, trace.Wrap(err)
}
dnsIPv6 := ipv6WithSuffix(ipv6Prefix, []byte{2})
// By default only the LocalSystem account, administrators, and the owner of
// the current process can access the pipe. The admin service runs as the
// LocalSystem account. We don't leak anything by letting processes owned
// by the same user as this process to connect to the pipe, they could read
// TELEPORT_HOME anyway.
pipe, err := winio.ListenPipe(pipePath, &winio.PipeConfig{})
if err != nil {
return nil, trace.Wrap(err, "listening on named pipe")
}
pm, processCtx := newProcessManager()
pm.AddCriticalBackgroundTask("pipe closer", func() error {
<-processCtx.Done()
return trace.Wrap(pipe.Close())
})
pm.AddCriticalBackgroundTask("admin process", func() error {
adminConfig := AdminProcessConfig{
NamedPipe: pipePath,
IPv6Prefix: ipv6Prefix.String(),
DNSAddr: dnsIPv6.String(),
HomePath: config.HomePath,
}
return trace.Wrap(execAdminProcess(processCtx, adminConfig))
})
pm.AddCriticalBackgroundTask("ipc service", func() error {
// TODO(nklaassen): wrap [config.AppProvider] with a gRPC service to expose
// the necessary methods to the admin process over [pipe].
// For now just accept and drop any connections to prove the admin
// process can dial the pipe. The pipe will be closed when the process
// context completes and any blocked Accept call will return with an error.
slog.InfoContext(processCtx, "Listening on named pipe", "pipe", pipe.Addr().String())
for {
_, err := pipe.Accept()
if err != nil {
return trace.Wrap(err)
}
}
return trace.Wrap(execAdminProcess(processCtx, AdminProcessConfig{}), "running admin process in the background")
})
// TODO(nklaassen): run user process gRPC service.
return pm, nil
}
Loading

0 comments on commit b97bcdd

Please sign in to comment.