init static method
- required String stateDir,
- required String appId,
- TailscaleLogLevel logLevel = TailscaleLogLevel.silent,
- bool noLogsNoSupport = false,
Configures the Tailscale library. Call this once at app startup, alongside other library initializers.
appId is the embedding application's stable identifier, such as its
reverse-DNS bundle or application ID. Tailscale reserves the dedicated
<appId>.tailscale Keybay namespace for the StateStore encryption key.
It must remain unchanged for the lifetime of stateDir. Constructing
this binding does not access Keybay; secure-state lifecycle operations
resolve it lazily.
stateDir is the app-owned base directory for persistent package state.
The logical Tailscale StateStore is one authenticated encrypted file under
its owner-only tailscale/ subtree. One random 32-byte DEK is held by
Keybay and retained in memory for the runtime lifetime. Missing custody,
unsafe paths or permissions, tampering, and pre-launch SQLite/FileStore
layouts fail closed; legacy identities are not migrated. After the first
successful persistent up, later launches can reconnect without an auth
key.
This encrypts StateStore data, not the entire subtree. Upstream logs, log configuration, and TLS/certificate sidecars can remain outside that encryption boundary. Owner-only permissions and backup exclusion are therefore still required.
Pick a durable application-support directory, not a user documents
directory, and exclude it from cloud backup. Mark the directory excluded:
NSURLIsExcludedFromBackupKey on iOS; dataExtractionRules /
fullBackupContent rules on Android. Persistent Android nodes require API
31+; persistent Linux nodes require desktop secret-tool and an available,
unlocked Secret Service. Older Android and headless Linux can use explicit
ephemeral mode, which uses an in-memory StateStore and never accesses
Keybay.
Upstream tsnet uploads diagnostic logs to Tailscale by default,
independently of logLevel and even when using a Headscale control
server. Set noLogsNoSupport to opt this process out before its first
runtime starts. This also opts out of support that depends on those logs.
The choice is immutable for the process, matching the other initialization
settings. Local owner-only log/config sidecars may still be created.
Implementation
static void init({
required String stateDir,
required String appId,
TailscaleLogLevel logLevel = TailscaleLogLevel.silent,
bool noLogsNoSupport = false,
}) {
if (stateDir.trim().isEmpty) {
throw const TailscaleUsageException('stateDir must not be empty.');
}
final custody = KeybayStateCustodyBinding(hostAppId: appId);
try {
ensurePosixFdTransportAvailable();
} catch (error) {
throw TailscaleUsageException(
'POSIX fd transport is not available on this platform.',
cause: error,
);
}
final stateDirPtr = stateDir.toNativeUtf8();
final keybayNamespacePtr = custody.keybayNamespace.toNativeUtf8();
final resultPtr = native.duneConfigure(
stateDirPtr,
keybayNamespacePtr,
logLevel.nativeValue,
noLogsNoSupport ? 1 : 0,
);
// Ephemeral scratch must live in a platform-writable temporary location.
// Dart resolves the app's real one (Go's os.TempDir() fallback is not
// app-writable on Android); native ignores empty and repeated values.
final scratchParentPtr = Directory.systemTemp.path.toNativeUtf8();
try {
native.duneSetEphemeralScratchParent(scratchParentPtr);
} finally {
calloc.free(scratchParentPtr);
}
try {
final decoded = jsonDecode(resultPtr.toDartString());
if (decoded is! Map<String, dynamic>) {
throw const TailscaleConfigurationException(
'Native runtime returned an invalid initialization response.',
);
}
final error = decoded['error'] as String?;
if (error != null) {
throw TailscaleConfigurationException(error);
}
final canonicalStateDir = decoded['stateDir'] as String?;
if (canonicalStateDir == null || canonicalStateDir.isEmpty) {
throw const TailscaleConfigurationException(
'Native runtime did not return a canonical state directory.',
);
}
final candidate = _TailscaleInitialization(
canonicalStateBaseDir: canonicalStateDir,
logLevel: logLevel,
noLogsNoSupport: noLogsNoSupport,
keybay: custody,
);
final configured = _initialization;
if (configured == null) {
_initialization = candidate;
} else if (!configured.hasSameIdentity(candidate)) {
throw const TailscaleConfigurationException(
'Native and Dart Tailscale initialization identities diverged.',
);
}
} on TailscaleConfigurationException {
rethrow;
} catch (error) {
throw TailscaleConfigurationException(
'Failed to configure the native Tailscale runtime.',
cause: error,
);
} finally {
native.duneFree(resultPtr);
calloc.free(stateDirPtr);
calloc.free(keybayNamespacePtr);
}
}