Skip to content

Commit

Permalink
feat: Add initial CGNAT check logic (#969)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
GeckoEidechse authored Aug 3, 2024
1 parent 1ac4198 commit 863a7fb
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 0 deletions.
1 change: 1 addition & 0 deletions src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
10 changes: 10 additions & 0 deletions src-tauri/src/platform_specific/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,13 @@ pub async fn get_local_northstar_proton_wrapper_version() -> Result<String, Stri
#[cfg(target_os = "windows")]
Err("Not supported on Windows".to_string())
}

/// Check whether the current device might be behind a CGNAT
#[tauri::command]
pub async fn check_cgnat() -> Result<String, String> {
#[cfg(target_os = "linux")]
return Err("Not supported on Linux".to_string());

#[cfg(target_os = "windows")]
windows::check_cgnat().await
}
70 changes: 70 additions & 0 deletions src-tauri/src/platform_specific/windows.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -32,3 +33,72 @@ pub fn origin_install_location_detection() -> Result<String, anyhow::Error> {

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<String, String> {
// 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::<Ipv4Addr>().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<usize, String> {
// Ensure valid IPv4 address to avoid prevent command injection
assert!(target_ip.parse::<Ipv4Addr>().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))
}
}
12 changes: 12 additions & 0 deletions src-vue/src/views/DeveloperView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@

<h3>Repair:</h3>

<el-button type="primary" @click="checkCgnat">
Run tracert and collect hop count
</el-button>

<el-button type="primary" @click="getInstalledMods">
Get installed mods
Expand Down Expand Up @@ -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<string>("check_cgnat")
.then((message) => {
showNotification(message);
})
.catch((error) => {
showErrorNotification(error);
});
},
async copyReleaseNotesToClipboard() {
navigator.clipboard.writeText(this.release_notes_text)
.then(() => {
Expand Down

0 comments on commit 863a7fb

Please sign in to comment.