Disclaimer: This implementation is considered highly experimental. The
tailscale-rslibrary is currently unstable, unaudited, and primarily relies on DERP relays for communication. This guide documents a successful "Proof of Concept" for advanced private networking in Flutter Desktop.
This implementation demonstrates how to integrate the experimental tailscale-rs SDK into a Flutter Desktop (Windows) application. It leverages Infisical Universal Auth for secure secret management and flutter_rust_bridge (v2) for high-performance communication between Dart and Rust.
Core Achievements
- Encrypted Tunnel on Windows: Successfully initialized the Tailscale user-mode network stack within a Flutter application.
- Secure Secret Lifecycle: Automated Auth Key retrieval using Infisical's Universal Auth (form-urlencoded) to avoid hardcoding credentials.
- Cross-Platform Handshake: Verified end-to-end connectivity between a Windows Desktop app and a remote Android device (Termux/Python) via the Tailnet.
- Crypto Provider Fix: Resolved the
rustlsprocess-levelCryptoProviderrequirement on Windows by explicitly installing theringprovider.
- Frontend: Flutter (Windows)
- Bridge:
flutter_rust_bridge(v2) - Backend (Rust):
tailscale-rs: User-mode network stack.reqwest: For REST API communication with Infisical.sqlx: SQLite for local state and secret persistence.rustls: Managed cryptography provider usingring.
To avoid common CryptoProvider panics and ensure compatibility with Windows, use the following dependency configuration:
[dependencies]
flutter_rust_bridge = "=2.12.0"
tailscale = { version = "0.2" }
# Explicitly use ring for cryptography provider to avoid runtime panics
rustls = { version = "0.23", features = ["ring"] }
ring = "0.17"
# Infisical API communication
reqwest = { version = "0.11", features = ["json", "rustls-tls"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
# Local Storage
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] }
tokio = { version = "1.0", features = ["full"] }use crate::frb_generated::StreamSink;
use std::env;
use sqlx::sqlite::SqlitePool;
use tailscale::Device;
use serde::Deserialize;
use serde_json::json;
use std::net::Ipv4Addr;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
// Structures for Infisical API responses
#[derive(Deserialize)]
struct InfisicalAuthResponse {
#[serde(rename = "accessToken")]
access_token: String,
}
#[derive(Deserialize)]
struct InfisicalSecretResponse {
secret: SecretData,
}
#[derive(Deserialize)]
struct SecretData {
#[serde(rename = "secretValue")]
value: String,
}
#[flutter_rust_bridge::frb(init)]
pub fn init_app() {
flutter_rust_bridge::setup_default_user_utils();
}
/// Main logic called from Flutter to initialize the backend services
pub async fn start_backend_logic(path: String, client_id: String, client_secret: String) -> Result<String, String> {
// 1. Initialize Cryptography Provider
// MANDATORY for Windows to avoid 'Could not determine process-level CryptoProvider' panic
let _ = rustls::crypto::ring::default_provider().install_default();
// Enable the unstable tailscale-rs experiment (required by the SDK)
env::set_var("TS_RS_EXPERIMENT", "this_is_unstable_software");
let http = reqwest::Client::builder()
.user_agent("ActiveLoom/1.0")
.build()
.map_err(|e| e.to_string())?;
// 2. Authenticate with Infisical (Universal Auth)
// Must use x-www-form-urlencoded as per Infisical Universal Auth specs
let login_url = "https://app.infisical.com/api/v1/auth/universal-auth/login";
let params = [
("clientId", client_id.trim()),
("clientSecret", client_secret.trim()),
];
let auth_response = http.post(login_url)
.header("Content-Type", "application/x-www-form-urlencoded")
.form(¶ms)
.send()
.await
.map_err(|e| format!("Auth connection failed: {}", e))?;
if !auth_response.status().is_success() {
let err_status = auth_response.status();
let err_text = auth_response.text().await.unwrap_or_default();
return Err(format!("Infisical Auth rejected ({}): {}", err_status, err_text));
}
let auth_res: InfisicalAuthResponse = auth_response
.json()
.await
.map_err(|e| format!("Token parse error: {}", e))?;
// 3. Retrieve Secrets from Infisical (API v4)
let project_id = "YOUR_PROJECT_ID_HERE";
let secret_url = "https://us.infisical.com/api/v4/secrets/{YOUR_SECRET_NAME}";
let response = http.get(secret_url)
.query(&[
("projectId", project_id),
("environment", "dev"),
("type", "shared")
])
.bearer_auth(auth_res.access_token)
.send()
.await
.map_err(|e| format!("Secret retrieval network error: {}", e))?;
if !response.status().is_success() {
return Err("Failed to fetch secret from Infisical".into());
}
let body_text = response.text().await.map_err(|e| e.to_string())?;
let secret_data: InfisicalSecretResponse = serde_json::from_str(&body_text)
.map_err(|e| format!("Secret parse error: {}", e))?;
let tailscale_auth_key = secret_data.secret.value;
// 4. Local Persistence (SQLite)
let connection_url = format!("sqlite://{}", path.replace("\\", "/"));
let _pool = SqlitePool::connect(&connection_url).await.map_err(|e| e.to_string())?;
// 5. Initialize Tailscale SDK
let ts_config = tailscale::Config::default();
let dev = match Device::new(&ts_config, Some(tailscale_auth_key)).await {
Ok(d) => d,
Err(e) => return Err(format!("Tailscale SDK Critical Failure: {}", e)),
};
// 6. Connectivity Test (TCP Tunneling)
// Connect to a remote peer (e.g., Android Phone running a Python server)
let target_peer_ip = "100.x.y.z";
let target_addr: Ipv4Addr = target_peer_ip
.parse()
.map_err(|e| format!("Invalid IP: {}", e))?;
match dev.tcp_connect((target_addr, 8080).into()).await {
Ok(mut stream) => {
let http_request = "GET /test.txt HTTP/1.1\r\nHost: target\r\nConnection: close\r\n\r\n";
stream.write_all(http_request.as_bytes()).await.map_err(|e| e.to_string())?;
let mut response_buffer = String::new();
stream.read_to_string(&mut response_buffer).await.map_err(|e| e.to_string())?;
println!("Response through tunnel: {}", response_buffer);
Ok("Connectivity Test Passed!".into())
},
Err(e) => Err(format!("Tunnel established, but peer unreachable: {}", e)),
}
}import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:path_provider/path_provider.dart';
import 'package:activeloom/src/rust/api/simple.dart';
import 'package:activeloom/src/rust/frb_generated.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// Load environment variables (.env)
await dotenv.load(fileName: ".env");
// Initialize the Rust library
await RustLib.init();
// Prepare SQLite path for Windows (AppData)
final appDir = await getApplicationSupportDirectory();
final dbPath = "${appDir.path}\\database.db";
// Initialize Backend (Infisical -> SQLite -> Tailscale)
try {
final result = await startBackendLogic(
path: dbPath,
clientId: dotenv.get("INFISICAL_CLIENT_ID"),
clientSecret: dotenv.get("INFISICAL_CLIENT_SECRET"),
);
print("Rust Initialized: $result");
} catch (e) {
print("Error initializing Rust services: $e");
}
runApp(const MyApp());
}- Run as Administrator: Crucial for initializing the user-mode TUN network interfaces on Windows.
- Rustls Configuration: In recent rustls versions, you must manually install a CryptoProvider (like ring) to avoid immediate runtime panics during HTTPS or Tailscale handshakes.
- Experimental Status: tailscale-rs currently lacks NAT traversal (Direct Connections) and MagicDNS. Communication is routed via DERP relays, which may impact latency and throughput.
- Universal Auth: Ensure your Infisical Identity has the "Universal Auth" method enabled and is added as a member of the project with proper permissions.