From 2cd14f06ae15c6b92f7e1d2a3e482bd89ab2026e Mon Sep 17 00:00:00 2001 From: syoul Date: Sat, 25 Apr 2026 23:51:35 +0200 Subject: [PATCH] fix(sidecar): use ephemeral ports for tcp/quic listen too mycelium defaults --tcp-listen-port and --quic-listen-port to 9651 when not provided. If anything else holds 9651 (a previous test instance, a Docker container from the level-1 procedure, another mycelium running on the host), the daemon comes up far enough to serve the loopback API for a few seconds before tearing itself down on the listen failure. Pick three distinct ephemeral ports (api, tcp, quic) per spawn. Trade-off: inbound peers need the actual port number, which we already log; the user can pin via a future SidecarConfig field. --- src-tauri/src/sidecar.rs | 40 +++++++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/src-tauri/src/sidecar.rs b/src-tauri/src/sidecar.rs index 45b14fd..89704a9 100644 --- a/src-tauri/src/sidecar.rs +++ b/src-tauri/src/sidecar.rs @@ -102,8 +102,14 @@ impl SidecarHandle { } let bin = locate_sidecar(app)?; - let port = portpicker::pick_unused_port() - .ok_or_else(|| AppError::Other("no free port available".into()))?; + // Three ports: HTTP API (loopback), TCP listen, QUIC (UDP) listen. + // mycelium defaults to 9651 for both peer-listen ports, which + // collides if another instance (or a leftover from a previous test) + // is already up. Always picking ephemeral ports avoids that at the + // cost of inbound peers needing the actual port number. + let api_port = pick_port()?; + let tcp_port = pick_port_skip(&[api_port])?; + let quic_port = pick_port_skip(&[api_port, tcp_port])?; let data_dir = app .path() @@ -115,7 +121,11 @@ impl SidecarHandle { let mut args = vec![ "--api-addr".to_string(), - format!("127.0.0.1:{port}"), + format!("127.0.0.1:{api_port}"), + "--tcp-listen-port".to_string(), + tcp_port.to_string(), + "--quic-listen-port".to_string(), + quic_port.to_string(), "--key-file".to_string(), key_path.display().to_string(), ]; @@ -137,7 +147,11 @@ impl SidecarHandle { } } - info!(?bin, port, "spawning mycelium sidecar via pkexec"); + info!( + ?bin, + api_port, tcp_port, quic_port, + "spawning mycelium sidecar via pkexec" + ); let mut cmd = elevation::elevated(&bin, &args); cmd.stdout(Stdio::piped()) @@ -150,7 +164,7 @@ impl SidecarHandle { // Stash before we await the health check, so a slow daemon // doesn't leave us with a zombie process if anything panics. - let api_url = format!("http://127.0.0.1:{port}"); + let api_url = format!("http://127.0.0.1:{api_port}"); *self.child.lock() = Some(child); *self.api_url.lock() = Some(api_url.clone()); *self.config_path.lock() = Some(config_path); @@ -257,6 +271,22 @@ impl SidecarHandle { } } +fn pick_port() -> AppResult { + portpicker::pick_unused_port().ok_or_else(|| AppError::Other("no free port available".into())) +} + +fn pick_port_skip(taken: &[u16]) -> AppResult { + for _ in 0..16 { + let p = pick_port()?; + if !taken.contains(&p) { + return Ok(p); + } + } + Err(AppError::Other( + "could not find a unique free port".into(), + )) +} + /// Resolve the bundled `mycelium-` binary in both `tauri dev` /// (cargo manifest) and bundled (resource_dir) modes. fn locate_sidecar(app: &AppHandle) -> AppResult {