feat: add tunnel status checking with active interface verification
Add tunnel_status command to desktop client for querying active tunnel state. Add is_active method to tunnel_manager that calls status command on bundled backend. Add status command to tunnel-helper that checks WireGuard service state on Windows via sc query and interface state on macOS via wg show. Add windows_client_status function for IPC-based status queries with active field in TunnelResponse. Update App.tsx to query tunnel status on
This commit is contained in:
@@ -54,6 +54,7 @@ struct TunnelRequest {
|
||||
struct TunnelResponse {
|
||||
ok: bool,
|
||||
error: Option<String>,
|
||||
active: Option<bool>,
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
@@ -76,6 +77,12 @@ fn run() -> Result<(), String> {
|
||||
"service" => run_windows_service_dispatcher(),
|
||||
"install-service" => install_windows_service(),
|
||||
"uninstall-service" => uninstall_windows_service(),
|
||||
"status" => {
|
||||
let profile = parse_profile_arg(args)?;
|
||||
let active = windows_client_status(&profile)?;
|
||||
println!("{}", if active { "active" } else { "inactive" });
|
||||
Ok(())
|
||||
}
|
||||
"connect" | "disconnect" => {
|
||||
let profile = parse_profile_arg(args)?;
|
||||
windows_client_request(command.as_str(), &profile)
|
||||
@@ -88,6 +95,11 @@ fn run() -> Result<(), String> {
|
||||
{
|
||||
let profile = parse_profile_arg(args)?;
|
||||
match command.as_str() {
|
||||
"status" => {
|
||||
let active = tunnel_is_active(&profile)?;
|
||||
println!("{}", if active { "active" } else { "inactive" });
|
||||
Ok(())
|
||||
}
|
||||
"connect" => connect_direct(&profile),
|
||||
"disconnect" => disconnect_direct(&profile),
|
||||
_ => Err("unsupported command".into()),
|
||||
@@ -201,22 +213,37 @@ fn handle_service_client(mut stream: TcpStream) -> Result<(), String> {
|
||||
.map_err(|err| format!("Unable to decode IPC request: {err}"))?;
|
||||
let response = match request.action.as_str() {
|
||||
"connect" => match connect_direct(Path::new(&request.profile)) {
|
||||
Ok(()) => TunnelResponse { ok: true, error: None },
|
||||
Ok(()) => TunnelResponse { ok: true, error: None, active: None },
|
||||
Err(err) => TunnelResponse {
|
||||
ok: false,
|
||||
error: Some(err),
|
||||
active: None,
|
||||
},
|
||||
},
|
||||
"disconnect" => match disconnect_direct(Path::new(&request.profile)) {
|
||||
Ok(()) => TunnelResponse { ok: true, error: None },
|
||||
Ok(()) => TunnelResponse { ok: true, error: None, active: None },
|
||||
Err(err) => TunnelResponse {
|
||||
ok: false,
|
||||
error: Some(err),
|
||||
active: None,
|
||||
},
|
||||
},
|
||||
"status" => match tunnel_is_active(Path::new(&request.profile)) {
|
||||
Ok(active) => TunnelResponse {
|
||||
ok: true,
|
||||
error: None,
|
||||
active: Some(active),
|
||||
},
|
||||
Err(err) => TunnelResponse {
|
||||
ok: false,
|
||||
error: Some(err),
|
||||
active: None,
|
||||
},
|
||||
},
|
||||
_ => TunnelResponse {
|
||||
ok: false,
|
||||
error: Some("unsupported tunnel action".into()),
|
||||
active: None,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -314,6 +341,41 @@ fn windows_client_request(action: &str, profile: &Path) -> Result<(), String> {
|
||||
.unwrap_or_else(|| "NexaVPN tunnel service reported an unknown error".into()))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn windows_client_status(profile: &Path) -> Result<bool, String> {
|
||||
let mut stream = TcpStream::connect(IPC_BIND_ADDR).map_err(|_| {
|
||||
"NexaVPN background service is not available. Reinstall NexaVPN or install the tunnel service once as administrator."
|
||||
.to_string()
|
||||
})?;
|
||||
|
||||
let payload = serde_json::to_string(&TunnelRequest {
|
||||
action: "status".to_string(),
|
||||
profile: profile.display().to_string(),
|
||||
})
|
||||
.map_err(|err| format!("Unable to encode tunnel status request: {err}"))?;
|
||||
|
||||
stream
|
||||
.write_all(payload.as_bytes())
|
||||
.and_then(|_| stream.write_all(b"\n"))
|
||||
.map_err(|err| format!("Unable to send tunnel status request: {err}"))?;
|
||||
|
||||
let mut reader = BufReader::new(stream);
|
||||
let mut line = String::new();
|
||||
reader
|
||||
.read_line(&mut line)
|
||||
.map_err(|err| format!("Unable to read tunnel status response: {err}"))?;
|
||||
|
||||
let response = serde_json::from_str::<TunnelResponse>(&line)
|
||||
.map_err(|err| format!("Unable to decode tunnel status response: {err}"))?;
|
||||
if !response.ok {
|
||||
return Err(response
|
||||
.error
|
||||
.unwrap_or_else(|| "NexaVPN tunnel service status check failed".into()));
|
||||
}
|
||||
|
||||
Ok(response.active.unwrap_or(false))
|
||||
}
|
||||
|
||||
fn connect_direct(profile: &Path) -> Result<(), String> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
@@ -386,6 +448,47 @@ fn disconnect_direct(profile: &Path) -> Result<(), String> {
|
||||
Err("unsupported platform".into())
|
||||
}
|
||||
|
||||
fn tunnel_is_active(profile: &Path) -> Result<bool, String> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let tunnel_name = profile
|
||||
.file_stem()
|
||||
.and_then(|value| value.to_str())
|
||||
.ok_or_else(|| "invalid profile filename".to_string())?;
|
||||
let service_name = format!("WireGuardTunnel${}", tunnel_name);
|
||||
let output = Command::new("sc")
|
||||
.arg("query")
|
||||
.arg(&service_name)
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.output()
|
||||
.map_err(|err| format!("Unable to query tunnel status: {err}"))?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
return Ok(stdout.contains("RUNNING"));
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let tunnel_name = profile
|
||||
.file_stem()
|
||||
.and_then(|value| value.to_str())
|
||||
.ok_or_else(|| "invalid profile filename".to_string())?;
|
||||
let status = Command::new("wg")
|
||||
.arg("show")
|
||||
.arg(tunnel_name)
|
||||
.status()
|
||||
.map_err(|err| format!("Unable to query tunnel status: {err}"))?;
|
||||
return Ok(status.success());
|
||||
}
|
||||
|
||||
#[allow(unreachable_code)]
|
||||
Err("unsupported platform".into())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn find_windows_wireguard() -> Result<PathBuf, String> {
|
||||
let candidates = [
|
||||
|
||||
Reference in New Issue
Block a user