gravitational/teleport · warning

VNet is already running

Error message

VNet is already running

What it means

errAlreadyRunning is a sentinel error in the VNet daemon package (lib/vnet/daemon/common_darwin.go:42). It is produced when the macOS VNet daemon receives a start request while an instance of the daemon is already running. The daemon maps the Objective-C error in the custom vnetErrorDomain with code VNEAlreadyRunningError to this Go sentinel, and the client translates it back when it sees that domain/code from C.StartVnet.

Source

Thrown at lib/vnet/daemon/common_darwin.go:42

import "C"

import (
	"errors"
	"unsafe"

	"github.com/gravitational/trace"

	"github.com/gravitational/teleport/lib/utils/darwinbundle"
)

var (
	// vnetErrorDomain is a custom error domain used for Objective-C errors that pertain to VNet.
	vnetErrorDomain = C.GoString(C.VNEErrorDomain)

	// errorCodeAlreadyRunning is returned within [vnetErrorDomain] errors to indicate that the daemon
	// received a message to start after it was already running.
	errorCodeAlreadyRunning = int(C.VNEAlreadyRunningError)
	errAlreadyRunning       = errors.New("VNet is already running")

	// errorCodeMissingCodeSigningIdentifiers is returned within [vnetErrorDomain] Obj-C errors and
	// transformed to [errMissingCodeSigningIdentifiers] in Go.
	errorCodeMissingCodeSigningIdentifiers = int(C.VNEMissingCodeSigningIdentifiersError)
	errMissingCodeSigningIdentifiers       = errors.New("either identifier or team identifier is missing in code signing information; is the binary signed?")
)

var (
	// nsCocoaErrorDomain is a generic error domain used in a lot of Apple's Cocoa frameworks.
	nsCocoaErrorDomain = "NSCocoaErrorDomain"

	// https://developer.apple.com/documentation/foundation/nsxpcconnectioninterrupted-swift.var
	errorCodeNSXPCConnectionInterrupted = int(C.NSXPCConnectionInterrupted)
	errXPCConnectionInterrupted         = errors.New("XPC connection interrupted")

	// https://developer.apple.com/documentation/foundation/nsxpcconnectioncodesigningrequirementfailure-swift.var
	errorCodeNSXPCConnectionCodeSigningRequirementFailure = int(C.NSXPCConnectionCodeSigningRequirementFailure)
	errXPCConnectionCodeSigningRequirementFailure         = errors.New("code signing requirement failed")

View on GitHub (pinned to 1283425b60)

Solutions

  1. Wait and retry: the client already waits 2*CheckUnprivilegedProcessInterval and calls the daemon again, so a transient race usually self-heals.
  2. Check if a daemon instance is already running (launchctl print / ps for the VNet daemon job) and stop it before starting a new one.
  3. If a stale daemon persists, reboot or explicitly bootout/kick the launchd job for the tsh daemon label.

Example fix

// before: starting VNet unconditionally
if err := client.RegisterAndCall(ctx, bundlePath, cfg); err != nil { return err }
// after: tolerate the already-running case in the caller
if err := client.RegisterAndCall(ctx, bundlePath, cfg); err != nil {
    if errors.Is(err, vnetdaemon.ErrAlreadyRunning) {
        return nil // VNet already active, nothing to do
    }
    return trace.Wrap(err)
}
Defensive patterns

Strategy: retry

Validate before calling

// check for an existing daemon job before starting
out, err := exec.Command("launchctl", "print", "system/"+daemonLabel).CombinedOutput()
if err == nil && !bytes.Contains(out, []byte("could not find service")) {
    // daemon already loaded; skip start or stop it first
}

Type guard

func isAlreadyRunning(err error) bool { return errors.Is(err, vnetdaemon.ErrAlreadyRunning) }

Try / catch

if err := client.RegisterAndCall(ctx, bundlePath, cfg); err != nil {
    if errors.Is(err, vnetdaemon.ErrAlreadyRunning) {
        // benign: daemon already active; optionally wait and retry once
        return nil
    }
    return trace.Wrap(err)
}

Prevention

When it happens

Trigger: Calling tsh VNet start (RegisterAndCall -> startByCalling) while a previous VNet daemon instance is still alive; or restarting VNet so quickly that the old daemon has not yet noticed the previous instance stopped and exited (startByCalling returns the error and RegisterAndCall waits 2*CheckUnprivilegedProcessInterval then retries once).

Common situations: Running `tsh vnet start` in a second terminal while VNet is already active; running VNet on two tsh clients under the same macOS user; stop-then-immediately-start races; stale daemon left running after a crash of the client process.

Related errors


AI-assisted analysis of gravitational/teleport@1283425b60 (2026-09-02). Data as JSON: /api/errors/4c2e26dec171b924. Report an issue: GitHub.