在 Rust 中构建自己的隧道:将本地站点暴露到 Web,拥有闪电般的性能

发布: (2025年12月7日 GMT+8 05:48)
4 min read
原文: Dev.to

Source: Dev.to

概览

Tauri 中的 Sidecar 模式允许应用使用用其他语言(Go、Python、C++ 等)编写的外部二进制文件或闭源工具。
在本示例中,cloudflared 可执行文件随应用一起打包。包含多个版本,以便 Tauri 的资源管理器在运行时能够为宿主架构选择正确的二进制文件。

当 Spot Serve 调用:

cloudflared tunnel --url http://localhost:8080

时,会依次执行以下步骤:

  1. 传输协商 – cloudflared 向 Cloudflare 的边缘网络(Argo)发起出站连接。它优先使用 QUIC(UDP)以获得更低延迟,如果 UDP 被阻塞则回退到基于 TCP 的 HTTP/2。
  2. 身份验证 – “Quick Tunnels”(TryCloudflare)无需预先认证;边缘会分配一个临时的 DNS 名称,位于 trycloudflare.com 域下。
  3. 隧道建立 – 创建持久会话;Cloudflare 的边缘充当入口点。
  4. 反向代理 – 边缘终止 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 进程。在生产应用中,这应当与全局窗口关闭或退出事件绑定,以避免僵尸进程。

代码仓库:

Back to Blog

相关文章

阅读更多 »