up method

  1. @override
Future<TailscaleStatus> up({
  1. String hostname = '',
  2. String? authKey,
  3. bool ephemeral = false,
  4. Uri? controlUrl,
  5. Duration timeout = const Duration(seconds: 30),
})
override

Brings the embedded Tailscale node up and connects to the control plane — Tailscale's coordination service at controlplane.tailscale.com, or a self-hosted Headscale if you set controlUrl. Registers the node on first launch, reconnects from persisted credentials on subsequent launches.

authKey can enroll a fresh persistent node without user interaction; get one from the tailnet admin panel at login.tailscale.com/admin/settings/keys (see tailscale.com/kb/1085/auth-keys). Reusable keys let you call up from multiple processes. It is optional in persistent mode: without a usable profile or key, upstream enters NodeState.needsLogin and returns an authorization URL for interactive enrollment. Subsequent launches can omit it because persisted session state reconnects.

Set ephemeral to register this process as a short-lived node. Ephemeral nodes are removed from the tailnet automatically after they go inactive by control-plane cleanup. A successful logout removes an ephemeral node from the tailnet immediately while preserving the lower-level StateStore container. Use this for CI jobs, preview environments, disposable tests, and other nodes whose identity should not outlive the process. Ephemeral mode retains no local identity: every new up after down (or process restart) needs a valid auth key, and a single-use key must be replaced. The configured stateDir is used only for admission/coordination and does not need to be cleared.

hostname sets the tailnet-visible hostname and the MagicDNS label, so the node becomes reachable at <hostname>.<tailnet>.ts.net. Leave it unset to use upstream tsnet's default: the lowercased host program name (or tsnet when the embedded runtime cannot resolve one).

Resolves on the first stable state: running, needsLogin, or needsMachineAuth. This intentionally differs from Go's tsnet.Server.Up, which blocks only on running — a Dart app that needs to drive an in-app auth flow should not have to re-enter up just to see the TailscaleStatus.authUrl. Inspect the returned TailscaleStatus.state to decide what to do next:

  • running — ready; http, tcp, etc. are usable.
  • needsLogin — open TailscaleStatus.authUrl in a browser / web view; the node finishes connecting after the user completes the flow.
  • needsMachineAuth — authenticated but awaiting admin approval on the control plane ( device approval).

Transitions delivered via onStateChange:

  • First launch: noState → starting → running
  • Reconnect with persisted creds: stopped → starting → running
  • If creds are expired: stopped → starting → needsLogin (with TailscaleStatus.authUrl populated)

No-op when a runtime is already active with the same hostname, effective control URL, and ephemeral mode. An auth key never replaces an active identity; call down before changing runtime configuration.

timeout bounds native startup and the stable-state wait. Once that deadline expires, the Future waits as long as required to establish fail-safe quarantine before returning, so total wall time can exceed timeout when native teardown is slow. A non-cancellable late native success is closed instead of becoming an unowned active node. Increase the timeout for slow mobile networks or self-hosted control planes.

Throws TailscaleUpException if the node fails to start or reach a stable state before timeout (e.g. control plane unreachable). When no auth key or usable profile exists, upstream normally returns needsLogin.

Implementation

@override
Future<TailscaleStatus> up({
  String hostname = '',
  String? authKey,
  bool ephemeral = false,
  Uri? controlUrl,
  Duration timeout = const Duration(seconds: 30),
}) async {
  _requireInitialized();
  if (timeout <= Duration.zero) {
    throw const TailscaleUsageException('up timeout must be positive.');
  }
  if (ephemeral && (authKey == null || authKey.isEmpty)) {
    throw const TailscaleUsageException(
      'ephemeral up requires a non-empty authKey.',
    );
  }
  final validatedHostname = validateRuntimeHostname(hostname);
  final canonicalControlUrl = canonicalizeControlUrl(controlUrl);
  if (_nativeStartInFlight) {
    throw const TailscaleUpException(
      'Another node start is already in progress.',
      code: TailscaleErrorCode.lifecycleBusy,
    );
  }

  // Reserve the complete public startup synchronously, including recovery,
  // native construction, stable-state observation, and Dart capability
  // setup. Teardown operations share this supervisor queue.
  _nativeStartInFlight = true;
  final startSettled = Completer<void>();
  _nativeStartPending = startSettled.future;
  final elapsed = Stopwatch()..start();
  try {
    return await _supervisorLifecycle.run(
      () => _runUp(
        hostname: validatedHostname,
        authKey: authKey ?? '',
        ephemeral: ephemeral,
        controlUrl: canonicalControlUrl,
        timeout: timeout,
        elapsed: elapsed,
      ),
    );
  } finally {
    _nativeStartInFlight = false;
    _nativeStartPending = null;
    startSettled.complete();
  }
}