Build your own tunnel in Rust: Expose local sites to the Web with blazing performance
Source: Dev.to
Overview
The Sidecar pattern in Tauri lets applications use external binaries written in other languages (Go, Python, C++, etc.) or closed‑source tools.
In this example the cloudflared executable is bundled with the app. Multiple versions are included so Tauri’s resource manager can select the correct binary for the host architecture at runtime.
When the Spot Serve invokes:
cloudflared tunnel --url http://localhost:8080
the following steps occur:
- Transport Negotiation – cloudflared opens an outbound connection to Cloudflare’s edge network (Argo). It prefers QUIC (UDP) for lower latency, falling back to HTTP/2 over TCP if UDP is blocked.
- Authentication – “Quick Tunnels” (TryCloudflare) require no pre‑authentication; the edge assigns a temporary DNS name under
trycloudflare.com. - Tunnel Establishment – a persistent session is created; Cloudflare’s edge acts as the ingress point.
- Reverse Proxying – the edge terminates TLS, inspects the
Hostheader, and routes the request through the tunnel to the local server. The response follows the reverse path.
This mechanism bypasses inbound firewall rules because the connection is strictly outbound.
Defining the Sidecar Configuration
In tauri.conf.json the external binary is registered so the bundler includes it in the final artifact.
{
"tauri": {
"bundle": {
"externalBin": [
"binaries/cloudflared"
]
},
"allowlist": {
"shell": {
"all": false,
"execute": true,
"sidecar": true
}
}
}
}
Place the appropriate binaries (e.g., cloudflared-x86_64-unknown-linux-gnu) in the src-tauri/binaries directory.
Spawning the Tunnel
The core logic lives in a Rust command module. A TunnelState struct holds the active child process, shared via tauri::State.
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>,
}
impl Default for TunnelState {
fn default() -> Self {
Self {
child_process: Mutex::new(None),
}
}
}
start_tunnel command
#[command]
pub fn start_tunnel(
app_handle: tauri::AppHandle,
port: u16,
state: tauri::State<TunnelState>,
) -> Result<(), Box<dyn std::error::Error>> {
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());
}
// cloudflared tunnel --url http://localhost:
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))?;
// Keep the child handle for later termination
*state_guard = Some(child);
// Listen to stdout/stderr for the public URL
tauri::async_runtime::spawn(async move {
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::Stdout(line) | CommandEvent::Stderr(line) => {
println!("[cloudflared]: {}", line);
if let Some(mat) = url_regex.find(&line) {
let public_url = mat.as_str().to_string();
println!("Tunnel Established: {}", public_url);
app_handle.emit_all("tunnel-url", public_url).unwrap();
}
}
CommandEvent::Terminated(payload) => {
println!("Tunnel terminated: {:?}", payload);
break;
}
_ => {}
}
}
});
Ok(())
}
The command spawns the sidecar, captures its output, extracts the TryCloudflare URL, and emits it to the frontend.
Stopping the Tunnel
#[command]
pub fn stop_tunnel(state: tauri::State<TunnelState>) -> Result<(), Box<dyn std::error::Error>> {
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(())
}
Calling stop_tunnel terminates the cloudflared process. In a production app this should also be tied to global window‑close or exit events to avoid zombie processes.
Repository: