Skip to content

danieldevalois/experimental-tailscale-flutter

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 

Repository files navigation

Disclaimer: This implementation is considered highly experimental. The tailscale-rs library 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.

Overview

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 rustls process-level CryptoProvider requirement on Windows by explicitly installing the ring provider.

1. Architecture Overview

  • 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 using ring.

2. Prerequisites & Dependencies

Cargo.toml (Rust)

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"] }

3. Rust Implementation (simple.rs)

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(&params) 
        .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)),
    }
}

4. Flutter Frontend Implementation (main.dart)

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());
}

5. Key Technical Troubleshooting (Windows)**

  • 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.

About

Experimental Tailscale + Flutter Rust Bridge Integration

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors