The Sidecar pattern is supported by Tauri to allow applications to leverage external binaries written in other languages (Go, Python, C++) or to utilize closed-source tools.
The cloudflared executable is included in the application's distribution resources. To support cross-platform deployment, multiple versions of the binary are bundled, and Tauri's resource manager resolves the appropriate binary at runtime based on the host architecture.
When the Spot Serve invokes cloudflared tunnel --url http://localhost:8080, the following sequence occurs:
Transport Negotiation:
cloudflaredinitiates an outbound connection to Cloudflare's edge network (Argo). By default, it attempts to use the QUIC protocol (based on UDP) for its performance benefits (reduced latency, congestion control). If QUIC fails (e.g., due to strict UDP firewalls), it falls back to HTTP/2 over TCP.Authentication: For "Quick Tunnels" (TryCloudflare), no pre-authentication is required. The edge network assigns a temporary, random DNS record under
trycloudflare.com.Tunnel Establishment: A persistent session is created. Cloudflare's edge acts as the ingress point.
Reverse Proxying:
- Ingress: A user makes an HTTPS request to https://random-name.trycloudflare.com.
- Routing: Cloudflare terminates the TLS at the edge, inspects the Host header, and routes the request into the established tunnel connection.
-
Egress:
cloudflaredreceives the request via the tunnel protocol (Capsule), reconstructs it, and forwards it tolocalhost:8080. - Response: The local server's response follows the reverse path.
This mechanism completely bypasses the need for inbound firewall rules, as the connection is strictly outbound.
Code Walkthrough
Defining the Sidecar Configuration
In the tauri.conf.json file, the external binary is registered. This tells the bundler to include the binary in the final build artifact.
{
"tauri": {
"bundle": {
"externalBin": [
"binaries/cloudflared"
]
},
"allowlist": {
"shell": {
"all": false,
"execute": true,
"sidecar": true
}
}
}
}
This configuration necessitates that the binaries (e.g., cloudflared-x86_64-unknown-linux-gnu) exist in the src-tauri/binaries directory.
The core logic resides in a Rust command module. We must handle the asynchronous spawning of the process and the non-blocking reading of its output streams.
First, we define a structure to hold the active child process. Since Tauri commands are stateless functions, we use tauri::State to manage persistence across calls.
use std::sync::Mutex;
use tauri::command;
use tauri::api::process::{Command, CommandEvent};
pub struct TunnelState {
// Mutex to safely share the process handle across threads
pub child_process: Mutex<Option<tauri::api::process::CommandChild>>,
}
impl Default for TunnelState {
fn default() -> Self {
Self {
child_process: Mutex::new(None),
}
}
}
Spawning the Tunnel
The start_tunnel function orchestrates the sidecar. Note the use of Command::new_sidecar which handles the path resolution automatically.
#[command]
pub fn start_tunnel(
app_handle: tauri::AppHandle,
port: u16,
state: tauri::State<TunnelState>,
) -> Result<(), String> {
let mut state_guard = state.child_process.lock().map_err(|_| "Failed to lock state")?;
if state_guard.is_some() {
return Err("Tunnel is already running".into());
}
// Configure the sidecar command: cloudflared tunnel --url http://localhost:PORT
let (mut rx, child) = Command::new_sidecar("cloudflared")
.map_err(|e| format!("Failed to create sidecar command: {}", e))?
.args(&["tunnel", "--url", &format!("http://localhost:{}", port)])
.spawn()
.map_err(|e| format!("Failed to spawn sidecar: {}", e))?;
// Store the child handle for later termination
*state_guard = Some(child);
// Spawn an async task to listen to stdout/stderr events
tauri::async_runtime::spawn(async move {
// Regex to find the TryCloudflare URL
// Pattern matches: https://[random-subdomain].trycloudflare.com
let url_regex = regex::Regex::new(r"https://[a-zA-Z0-9-]+\.trycloudflare\.com").unwrap();
while let Some(event) = rx.recv().await {
match event {
CommandEvent::Stderr(line) | CommandEvent::Stdout(line) => {
// Log the raw output for debugging
println!("[cloudflared]: {}", line);
// Check for the URL
if let Some(mat) = url_regex.find(&line) {
let public_url = mat.as_str().to_string();
println!("Tunnel Established: {}", public_url);
// Emit the URL back to the frontend
app_handle.emit_all("tunnel-url", public_url).unwrap();
}
}
CommandEvent::Terminated(payload) => {
println!("Tunnel terminated: {:?}", payload);
break;
}
_ => {}
}
}
});
Ok(())
}
Stopping the tunnel is as critical as starting it. If the user closes the app, the cloudflared process must die.
#[command]
pub fn stop_tunnel(state: tauri::State<TunnelState>) -> Result<(), String> {
let mut state_guard = state.child_process.lock().map_err(|_| "Failed to lock state")?;
if let Some(child) = state_guard.take() {
child.kill().map_err(|e| format!("Failed to kill process: {}", e))?;
}
Ok(())
}
Crucially, in a production app, this logic should also be hooked into the application's global WindowEvent::Destroyed or exit handler to prevent zombie processes.
Repository: https://github.com/explicit-logic/spot-serve-gui

Top comments (0)