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 is required for first registration; 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. Subsequent launches can omit it — the persisted session state reconnects automatically.

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. Calling logout stops the local node and clears local credentials, but tailnet removal still follows the control plane's ephemeral-node cleanup behavior. Use this for CI jobs, preview environments, disposable tests, and other nodes whose identity should not outlive the process. This affects registration with the control plane; use a fresh or cleared stateDir passed to Tailscale.init when you need to force a new ephemeral identity.

hostname sets the tailnet-visible hostname and the MagicDNS label, so the node becomes reachable at <hostname>.<tailnet>.ts.net. Leave unset to let the embedded runtime pick the OS default.

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 if already running (without a new authKey).

timeout bounds how long up waits for the node to reach a stable state after the native runtime starts. Increase it for slow mobile networks or self-hosted control planes.

Throws TailscaleUpException if no authKey is provided and no persisted session state exists, or if the node fails to reach a stable state before timeout (e.g. control plane unreachable).

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.');
  }
  final resolvedControlUrl = controlUrl ?? _defaultControlUrl;

  // Only count stable states that arrive AFTER start() returns. If up()
  // is called on an already-running node (with a new authKey), the old
  // engine's lingering `running` emission would otherwise satisfy the
  // "first stable state" check before the restart completes.
  final stable = Completer<void>();
  var startReturned = false;
  final sub = onStateChange.listen((state) {
    if (!startReturned) return;
    if (_isStableState(state) && !stable.isCompleted) {
      stable.complete();
    }
  });

  try {
    await _worker.start(
      hostname: hostname,
      authKey: authKey ?? '',
      ephemeral: ephemeral,
      controlUrl: resolvedControlUrl.toString(),
      stateDir: _stateDir,
    );
    _http = TailscaleHttpClient();
    startReturned = true;

    // No-op up() case: the engine is already at a stable state and
    // won't emit another event. Check once post-start so we don't
    // wait on a state change that will never come.
    final postStart = await status();
    if (_isStableState(postStart.state) && !stable.isCompleted) {
      stable.complete();
    }

    try {
      await stable.future.timeout(timeout);
    } on TimeoutException {
      final last = await status();
      throw TailscaleUpException(
        'Node did not reach a stable state within $timeout '
        '(last observed: ${last.state.name}). The control plane may '
        'be unreachable or the tailnet is experiencing issues.',
      );
    }
  } finally {
    await sub.cancel();
  }

  return status();
}