Skip to content

[Enhancement] Migrate mhost-dns-proxy to Tauri 2 bundle.externalBin sidecar (drop current_exe() probe) #160

Description

@flyhigher139

Context

enable_dns_mode locates the privileged proxy binary via:

let proxy_path = std::env::current_exe()
    .ok()
    .and_then(|p| p.parent().map(|dir| dir.join(\"mhost-dns-proxy\")))
    .unwrap_or_else(|| PathBuf::from(\"mhost-dns-proxy\"));

Combined with a hand-rolled osascript elevation shell-script (platform.rs::enable_dns_mode), this has accumulated costs the 0.3.x branch is starting to trip over:

  • Implicit layout assumption: dev workflow requires a scripts/dev.sh wrapper ([Bug] DNS mode 在 pnpm tauri dev 下静默失效:mhost-dns-proxy binary 缺失导致所有查询卡死 #155) because pnpm tauri dev does not auto-build the sidecar; nothing tells the user this when pnpm tauri dev runs in stock form.
  • Bundle path discovery is fragile on macOS release builds: the .app contents are Tauri-determined; relying on current_exe().parent() works there but couples our code to whatever Tauri 2 currently does with main-bin placement. If Tauri changes the resolution (it has, between 1.x and 2.x), we break silently.
  • Cross-platform friction (改为支持本地 hosts 模式和本地 DNS 模式两种模式 #67): Windows / Linux non-macOS ports need the same privileged binary but cannot reuse the current osascript elevation. The cross-platform story gets cleaner if we go through Tauri's resource model.
  • Capability / security shape mismatch: launching a privileged binary via osascript is an out-of-band side-channel that the IPC layer cannot introspect or audit. A first-class Command::sidecar(...) invocation integrates with Tauri's permission model.

#156 (merged) made the existing path safe (pre-check + script quoting + transactional cleanup). This issue is the longer-term refactor that swaps the mechanism out.

Goal

Replace the manual osascript + current_exe().parent().join(...) flow with Tauri 2's first-class sidecar bundle mechanism. After this lands:

  • mhost-dns-proxy is declared in tauri.conf.json::bundle.externalBin with its target-triple suffix
  • The privileged launch happens via tauri::process::Command::new(\"mhost-dns-proxy\") from tauri-plugin-shell, with execute allowed by capabilities
  • scripts/dev.sh becomes unnecessary — pnpm tauri dev builds both the main bin and the sidecar up front
  • The privileged proxy no longer needs an embedded shell script at all; what used to be [ -x ] / kill -0 / trap becomes Rust control flow in the IPC handler
  • Windows / Linux tracking under 改为支持本地 hosts 模式和本地 DNS 模式两种模式 #67 can build on the same primitive

Approach sketch

  1. Cargo side — name the sidecar binary with target-triple suffix required by Tauri 2 sidecar lookup:
    • Add a build.rs or rename [[bin]] so output is mhost-dns-proxy-<target-triple> instead of mhost-dns-proxy
    • Or: configure tauri.conf.json::bundle.externalBin paths to point at the unsuffixed output and let Tauri rename (Tauri 2 actually accepts paths and computes the suffix automatically, but verify)
  2. tauri.conf.json:
    • bundle.externalBin: [\"bin/mhost-dns-proxy\"] (or absolute path)
    • Add tauri-plugin-shell allowlist — Command::new(\"mhost-dns-proxy\").execute() needs shell:allow-execute or per-binary capability
  3. Cargo.toml (root): add tauri-plugin-shell = \"2\" dep
  4. capabilities/default.json: add the sidecar to the permitted executables list
  5. Frontend / IPC: enable_dns_mode Rust command replaces osascript invocation with Command::new(\"mhost-dns-proxy\").args([\"--listen\", \"53\", \"--target\", \"1053\"]).spawn() (or .execute() depending on how we want sudo semantics)
  6. Privilege boundary:
    • macOS still needs root for bind :53. Tauri's sidecar runs as the app's uid, same as the main process — i.e. not root.
    • For bind :53 we still need a privilege step. Two options:
      a. Keep a tiny SMJobBless-style helper (only on macOS) and only that needs root
      b. Use setcap (Linux) / service (Windows) / osascript once at install time to install the SUID bit
    • Practical answer: keep osascript for the bind-53 step but launch just the binary via Command::sidecar(\"mhost-dns-proxy\"). Net result: one less custom shell script, but the bind-53 privilege elevation stays where it has to.
  7. Pre-checks: drop validate_proxy_binary (fix(dns): surface silent failures when mhost-dns-proxy binary is missing (#155) #156) in favour of Tauri-resolved path + Command::sidecar().status() smoke check on startup
  8. Cleanup: Exit-trap logic (fix(dns): surface silent failures when mhost-dns-proxy binary is missing (#155) #156 F4) becomes Rust Drop on the supervisor task holding the Child handle
  9. Tests: cleaner — no more build_enable_script + shell_single_quote + fragile sh execution. Spawning a Command::sidecar from a test harness is testable directly.
  10. scripts/dev.sh: can be retired entirely (or kept as a thin shim); pnpm tauri dev handles the sidecar build via bundle.externalBin

Open questions

  • Tauri 2 sidecar discovery on dev mode: does tauri build --debug build the sidecar automatically, or do we still need a manual cargo build step? If not, keep a stripped-down dev script.
  • macOS signing / notarization: sidecars must be signed with the same Developer ID as the main app; confirm whether Tauri 2's tauri-action handles this correctly out-of-the-box for cross-compiled targets (CI matrix).
  • Capabilities shape: do we need shell:allow-execute scoped to just mhost-dns-proxy, or a tighter per-binary capability?
  • Windows / Linux parity: this enables 改为支持本地 hosts 模式和本地 DNS 模式两种模式 #67 (cross-platform DNS mode). Should we scope this PR to macOS-only first (same as today) and add a follow-up for Windows / Linux privilege model?

Linked

Suggested acceptance criteria

  • pnpm tauri dev on a fresh clone without scripts/dev.sh: DNS mode still works
  • pnpm tauri build produces a signed bundle where mhost-dns-proxy is bundled as a sidecar (not bundled-inside or copy-pasted at runtime)
  • Capability manifest declares the sidecar by name; no shell:allow-execute with *
  • enable_dns_mode IPC handler still returns clear Err on sidecar missing (no silent failure)
  • disable_dns_mode cleanup is idempotent and reaches port 53 cleanup via Rust Drop, not via shell trap
  • macOS bundle passes codesign / notarization in CI on both aarch64-apple-darwin and x86_64-apple-darwin

Metadata

Metadata

Assignees

No one assigned

    Labels

    dns-modeDNS mode (本地 DNS server) 相关问题enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions