up method
- String hostname = '',
- String? authKey,
- bool ephemeral = false,
- Uri? controlUrl,
- Duration timeout = const Duration(seconds: 30),
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();
}