在 Rust 中构建自己的隧道:将本地站点暴露到 Web,拥有闪电般的性能
Source: Dev.to
概览
Tauri 中的 Sidecar 模式允许应用使用用其他语言(Go、Python、C++ 等)编写的外部二进制文件或闭源工具。
在本示例中,cloudflared 可执行文件随应用一起打包。包含多个版本,以便 Tauri 的资源管理器在运行时能够为宿主架构选择正确的二进制文件。
当 Spot Serve 调用:
cloudflared tunnel --url http://localhost:8080
时,会依次执行以下步骤:
- 传输协商 – cloudflared 向 Cloudflare 的边缘网络(Argo)发起出站连接。它优先使用 QUIC(UDP)以获得更低延迟,如果 UDP 被阻塞则回退到基于 TCP 的 HTTP/2。
- 身份验证 – “Quick Tunnels”(TryCloudflare)无需预先认证;边缘会分配一个临时的 DNS 名称,位于
trycloudflare.com域下。 - 隧道建立 – 创建持久会话;Cloudflare 的边缘充当入口点。
- 反向代理 – 边缘终止 TLS,检查
Host头部,并通过隧道将请求路由到本地服务器。响应沿相反路径返回。
该机制通过仅使用出站连接来绕过入站防火墙规则。
定义 Sidecar 配置
在 tauri.conf.json 中注册外部二进制文件,以便打包工具将其包含在最终产物中。
{
"tauri": {
"bundle": {
"externalBin": [
"binaries/cloudflared"
]
},
"allowlist": {
"shell": {
"all": false,
"execute": true,
"sidecar": true
}
}
}
}
将相应的二进制文件(例如 cloudflared-x86_64-unknown-linux-gnu)放入 src-tauri/binaries 目录。
启动隧道
核心逻辑位于 Rust 命令模块中。TunnelState 结构体保存活动子进程,并通过 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]
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(())
}
该命令启动 sidecar,捕获其输出,提取 TryCloudflare URL,并将其发送到前端。
停止隧道
#[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(())
}
调用 stop_tunnel 会终止 cloudflared 进程。在生产应用中,这应当与全局窗口关闭或退出事件绑定,以避免僵尸进程。
代码仓库: