From 863a7fbedfb8d443a528e7475edb5de541499ce9 Mon Sep 17 00:00:00 2001 From: GeckoEidechse <40122905+GeckoEidechse@users.noreply.github.com> Date: Sat, 3 Aug 2024 02:00:54 +0200 Subject: feat: Add initial CGNAT check logic (#969) Adds some logic that gets external IP and runs tracert to count the number of hops to said IP address. The goal is to have an automated way to check for CGNAT. --- src-tauri/src/main.rs | 1 + src-tauri/src/platform_specific/mod.rs | 10 +++++ src-tauri/src/platform_specific/windows.rs | 70 ++++++++++++++++++++++++++++++ src-vue/src/views/DeveloperView.vue | 12 +++++ 4 files changed, 93 insertions(+) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index ee7da27f..a9f484f5 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -136,6 +136,7 @@ fn main() { northstar::profile::delete_profile, northstar::profile::fetch_profiles, northstar::profile::validate_profile, + platform_specific::check_cgnat, platform_specific::get_host_os, platform_specific::get_local_northstar_proton_wrapper_version, platform_specific::install_northstar_proton_wrapper, diff --git a/src-tauri/src/platform_specific/mod.rs b/src-tauri/src/platform_specific/mod.rs index 6fdb1ed1..4e0514d4 100644 --- a/src-tauri/src/platform_specific/mod.rs +++ b/src-tauri/src/platform_specific/mod.rs @@ -38,3 +38,13 @@ pub async fn get_local_northstar_proton_wrapper_version() -> Result Result { + #[cfg(target_os = "linux")] + return Err("Not supported on Linux".to_string()); + + #[cfg(target_os = "windows")] + windows::check_cgnat().await +} diff --git a/src-tauri/src/platform_specific/windows.rs b/src-tauri/src/platform_specific/windows.rs index 678e5be5..fc6aab5d 100644 --- a/src-tauri/src/platform_specific/windows.rs +++ b/src-tauri/src/platform_specific/windows.rs @@ -1,5 +1,6 @@ /// Windows specific code use anyhow::{anyhow, Result}; +use std::net::Ipv4Addr; #[cfg(target_os = "windows")] use winreg::{enums::HKEY_LOCAL_MACHINE, RegKey}; @@ -32,3 +33,72 @@ pub fn origin_install_location_detection() -> Result { Err(anyhow!("No Origin / EA App install path found")) } + +/// Check whether the current device might be behind a CGNAT +pub async fn check_cgnat() -> Result { + // Use external service to grap IP + let url = "https://api.ipify.org"; + let response = reqwest::get(url).await.unwrap().text().await.unwrap(); + + // Check if valid IPv4 address and return early if not + if response.parse::().is_err() { + return Err(format!("Not valid IPv4 address: {}", response)); + } + + let hops_count = run_tracert(&response)?; + Ok(format!("Counted {} hops to {}", hops_count, response)) +} + +/// Count number of hops in tracert output +fn count_hops(output: &str) -> usize { + // Split the output into lines + let lines: Vec<&str> = output.lines().collect(); + + // Filter lines that appear to represent hops + let hop_lines: Vec<&str> = lines + .iter() + .filter(|&line| line.contains("ms") || line.contains("*")) // TODO check if it contains just the `ms` surrounded by whitespace, otherwise it might falsely pick up some domain names as well + .cloned() + .collect(); + + // Return the number of hops + hop_lines.len() +} + +/// Run `tracert` +fn run_tracert(target_ip: &str) -> Result { + // Ensure valid IPv4 address to avoid prevent command injection + assert!(target_ip.parse::().is_ok()); + + // Execute the `tracert` command + let output = match std::process::Command::new("tracert") + .arg("-4") // Force IPv4 + .arg("-d") // Prevent resolving intermediate IP addresses + .arg("-w") // Set timeout to 1 second + .arg("1000") + .arg("-h") // Set max hop count + .arg("5") + .arg(target_ip) + .output() + { + Ok(res) => res, + Err(err) => return Err(format!("Failed running tracert: {}", err)), + }; + + // Check if the command was successful + if output.status.success() { + // Convert the output to a string + let stdout = + std::str::from_utf8(&output.stdout).expect("Invalid UTF-8 sequence in command output"); + println!("{}", stdout); + + // Count the number of hops + let hop_count = count_hops(stdout); + Ok(hop_count) + } else { + let stderr = std::str::from_utf8(&output.stderr) + .expect("Invalid UTF-8 sequence in command error output"); + println!("{}", stderr); + Err(format!("Failed collecting tracert output: {}", stderr)) + } +} diff --git a/src-vue/src/views/DeveloperView.vue b/src-vue/src/views/DeveloperView.vue index a08c73f3..19214157 100644 --- a/src-vue/src/views/DeveloperView.vue +++ b/src-vue/src/views/DeveloperView.vue @@ -65,6 +65,9 @@

Repair:

+ + Run tracert and collect hop count + Get installed mods @@ -330,6 +333,15 @@ export default defineComponent({ .then((message) => { showNotification(`NSProton Version`, message as string); }) .catch((error) => { showNotification(`Error`, error, "error"); }) }, + async checkCgnat() { + await invoke("check_cgnat") + .then((message) => { + showNotification(message); + }) + .catch((error) => { + showErrorNotification(error); + }); + }, async copyReleaseNotesToClipboard() { navigator.clipboard.writeText(this.release_notes_text) .then(() => { -- cgit v1.2.3