From f68a61f7609d282325b67c863aab921e22fb237d Mon Sep 17 00:00:00 2001 From: xah30 Date: Fri, 29 May 2026 19:45:59 +0300 Subject: [PATCH] fix(tunnel,aura-gui): macOS TUN auto-assign + admin-access check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs found in the GUI's first end-to-end test: ## #41 was incomplete — `Some("")` is not the same as `None` for tun-rs The agent's earlier #41 fix passed `""` to `Configuration::tun_name()` expecting the tun crate to treat empty as "let the kernel auto-assign". It doesn't. Looking at tun-0.8.9/src/platform/macos/device.rs: if !tun_name.starts_with("utun") { return Err(Error::InvalidName); } An empty string fails `starts_with("utun")` so the create errors out before the kernel is ever consulted. The auto-assign branch ONLY triggers when `config.tun_name` is `None` — which requires us to skip the `.tun_name()` call entirely, not pass a sentinel value. Fix: split the builder chain so `.tun_name()` is only called when the sanitized name is non-empty. The kernel now correctly auto-picks the next free `utunN` for the standard provisioned `tun_name = "aura0"` config. User-visible symptom this resolves: the GUI's Connect button consistently died with `failed to create TUN device 'aura0'` followed by an InvalidName chain, even though aura was running as root. ## check_admin_access tested the wrong command shape `check_admin_access` ran `sudo -n --help` and inferred the sudoers entry was installed iff that succeeded. But our sudoers entry is scoped to ` client *` — ` --help` does NOT match, so even when the entry was correctly installed and Connect was already working, the yellow "One-time setup needed" banner stayed up forever. Switched to `sudo -n -l ` which lists matching sudoers entries for the binary path itself. Returns 0 iff ANY entry covers it without a password — works regardless of the per-command scope. ## Verification - `cargo test -p aura-tunnel --lib tun` — all 3 sanitize / create tests pass - Rebuilt `target/release/aura` and `/Applications/Aura.app` against the fixes - Confirmed via `sudo -n -l /Users/xah30/AuraVPN/target/release/aura` that the installed sudoers entry is detectable by the new check 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 --- aura-gui/src-tauri/src/lib.rs | 26 ++++++++++++++++---------- crates/aura-tunnel/src/tun.rs | 20 ++++++++++++++++---- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/aura-gui/src-tauri/src/lib.rs b/aura-gui/src-tauri/src/lib.rs index fea2509..08e49b3 100644 --- a/aura-gui/src-tauri/src/lib.rs +++ b/aura-gui/src-tauri/src/lib.rs @@ -221,24 +221,30 @@ fn get_aura_binary_path(state: tauri::State<'_, Arc>) -> String { state.aura_binary.lock().display().to_string() } -/// `true` if `sudo -n --help` runs without prompting (i.e. the NOPASSWD sudoers entry is -/// installed). The UI uses this to gate the "Install admin access" button so the user only sees -/// it when it's actually needed. +/// `true` if the NOPASSWD sudoers entry for ` client *` is installed and works. +/// +/// We use `sudo -n -l ` which lists the sudoers entries matching the binary path and +/// returns 0 iff at least one entry covers it without a password. This is correct even when our +/// sudoers fragment is scoped to ` client *` (and so wouldn't match ` --help` — +/// that's why the earlier `sudo -n --help` check kept saying "not installed" while in +/// reality the entry was there and Connect was working). #[tauri::command] fn check_admin_access(state: tauri::State<'_, Arc>) -> bool { let bin = state.aura_binary.lock().clone(); #[cfg(unix)] { - match std::process::Command::new("/usr/bin/sudo") + let output = std::process::Command::new("/usr/bin/sudo") .arg("-n") + .arg("-l") .arg(bin) - .arg("--help") .stdin(std::process::Stdio::null()) - .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::null()) - .status() - { - Ok(s) => s.success(), + .output(); + match output { + Ok(out) => { + // sudo -n -l prints the matching entry's command path on stdout when allowed + // (e.g. "/usr/local/bin/aura"); on refusal it exits non-zero and prints to stderr. + out.status.success() + } Err(_) => false, } } diff --git a/crates/aura-tunnel/src/tun.rs b/crates/aura-tunnel/src/tun.rs index 9a0e51c..eb30802 100644 --- a/crates/aura-tunnel/src/tun.rs +++ b/crates/aura-tunnel/src/tun.rs @@ -95,9 +95,16 @@ impl AuraTun { .mask(); // macOS: the kernel utun driver enforces `^utun[0-9]+$` and rejects anything else with - // `invalid device tun name`. Pass the requested name through `sanitize_macos_tun_name` - // which returns `""` for non-conforming names; the tun crate treats `""` as - // "let the kernel pick the next free utunN". + // `invalid device tun name`. Earlier v3.4 attempt passed `""` to `.tun_name()` thinking + // tun-rs would treat empty as "kernel auto-assign" — it does NOT. Looking at + // tun-0.8.9/src/platform/macos/device.rs: + // + // if !tun_name.starts_with("utun") { return Err(Error::InvalidName); } + // + // An empty string fails the `starts_with` check and the create errors out. The fix is + // to skip the `.tun_name()` call ENTIRELY for non-conforming names — that leaves + // `Configuration::tun_name` as `None`, which the tun crate handles by passing id=0 to + // the kernel (auto-assign next free utunN). #[cfg(target_os = "macos")] let requested_name = sanitize_macos_tun_name(name); #[cfg(not(target_os = "macos"))] @@ -105,12 +112,17 @@ impl AuraTun { let mut config = tun::Configuration::default(); config - .tun_name(requested_name) .address(ip) .netmask(netmask) .mtu(mtu) .layer(tun::Layer::L3) .up(); + // Only set tun_name when it's a value the kernel will accept. On macOS that means a + // valid `utunN` string; otherwise we leave it unset (None) so the tun crate's auto- + // assign branch kicks in. On Linux/Windows the requested name is always honoured. + if !requested_name.is_empty() { + config.tun_name(requested_name); + } let inner = tun::create_as_async(&config) .with_context(|| format!("failed to create TUN device '{name}'"))?;