Created
June 6, 2025 18:22
-
-
Save jeffbryner/b296a635389614aa227572da9241a568 to your computer and use it in GitHub Desktop.
macOS: capture surrounding wifi networks in rust
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[package] | |
name = "wifiscanner" | |
version = "0.1.0" | |
edition = "2024" | |
[dependencies] | |
objc2-core-wlan = "0.3.1" | |
objc2 = "0.6.1" | |
objc2-foundation = "0.3.1" | |
objc2-core-location = "0.3.1" | |
serde = { version = "1.0", features = ["derive"] } | |
serde_json = "1.0" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="UTF-8"?> | |
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
<plist version="1.0"> | |
<dict> | |
<key>CFBundleExecutable</key> | |
<string>wifiscanner</string> <!-- Replace with your actual executable name --> | |
<key>CFBundleIdentifier</key> | |
<string>com.company.wifiscanner</string> <!-- Make this unique --> | |
<key>CFBundleName</key> | |
<string>WiFiScanner</string> <!-- Your application's name --> | |
<key>CFBundleVersion</key> | |
<string>1.0</string> | |
<key>CFBundlePackageType</key> | |
<string>APPL</string> | |
<key>LSMinimumSystemVersion</key> | |
<string>10.15</string> <!-- Set your target minimum macOS version --> | |
<key>NSLocationWhenInUseUsageDescription</key> | |
<string>This application needs access to your location to discover and display Wi-Fi network names (SSIDs) and BSSIDs.</string> | |
</dict> | |
</plist> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use objc2_core_wlan::CWWiFiClient; | |
use objc2_core_location::{CLLocationManager, CLAuthorizationStatus}; | |
use serde::Serialize; // For JSON serialization | |
use std::fs::File; | |
use std::io::Write; | |
use std::time::{SystemTime, UNIX_EPOCH}; | |
fn main() -> Result<(), String> { | |
let manager = unsafe { CLLocationManager::new() }; // Create an instance | |
let auth_status = unsafe { manager.authorizationStatus() }; // Call on the instance | |
println!("Authorization Status: {:?}", auth_status_to_string(auth_status)); | |
if auth_status == CLAuthorizationStatus::NotDetermined { | |
println!("Authorization not determined. Requesting 'Always' authorization..."); | |
unsafe { manager.requestAlwaysAuthorization() }; | |
// It's good practice to re-check the status after requesting, | |
// though the change might not be immediate or might require user interaction. | |
// For this example, we'll just print that we requested it. | |
// A more robust app might use a CLLocationManagerDelegate to get status updates. | |
let new_auth_status = unsafe { manager.authorizationStatus() }; | |
println!("Authorization Status after request: {}", auth_status_to_string(new_auth_status)); | |
} | |
// Define a struct for serializing network data | |
#[derive(Serialize)] | |
struct NetworkInfo { | |
ssid: String, | |
bssid: String, | |
} | |
let client = unsafe { CWWiFiClient::new() }; | |
let interface = match unsafe { client.interface() } { | |
Some(interface) => interface, | |
None => return Err("No WiFi interface found".into()), | |
}; | |
// Assuming scanForNetworksWithName_error(None) returns a Result as implied by original code. | |
// The Ok variant would contain Retained<NSSet<CWNetwork>>. | |
match unsafe { interface.scanForNetworksWithName_error(None) } { | |
Ok(networks_set) => { // networks_set is Retained<NSSet<CWNetwork>> | |
let count = unsafe { networks_set.count() }; | |
let mut discovered_networks: Vec<NetworkInfo> = Vec::new(); // To store network info | |
println!("Wi-Fi scan completed successfully! Found {} networks.", count); | |
// Get an enumerator for the set of networks. | |
// networks_set.objectEnumerator() returns Retained<NSEnumerator<CWNetwork>>. | |
let enumerator = unsafe { networks_set.objectEnumerator() }; | |
// Iterate through the networks. | |
// enumerator.nextObject() returns Option<Retained<CWNetwork>>. | |
while let Some(network_retained) = unsafe { enumerator.nextObject() } { | |
// network_retained is of type Retained<CWNetwork>. | |
// We can call CWNetwork methods on it (it derefs to &CWNetwork). | |
let ssid = unsafe { network_retained.ssid() } // Returns Option<Retained<NSString>> | |
.map(|s_retained| s_retained.to_string()) // s_retained is Retained<NSString> | |
.unwrap_or_else(|| "N/A".to_string()); | |
let bssid = unsafe { network_retained.bssid() } // Returns Option<Retained<NSString>> | |
.map(|s_retained| s_retained.to_string()) | |
.unwrap_or_else(|| "N/A".to_string()); | |
if ssid != "N/A" || bssid != "N/A" { // Only add if we have some data | |
discovered_networks.push(NetworkInfo { | |
ssid: ssid.clone(), // Clone since ssid is used in println! | |
bssid: bssid.clone(), // Clone since bssid is used in println! | |
}); | |
} | |
println!(" SSID: {}, BSSID: {}", ssid, bssid); | |
} | |
// Write to JSON file if any networks were found | |
if !discovered_networks.is_empty() { | |
let timestamp = SystemTime::now() | |
.duration_since(UNIX_EPOCH) | |
.map_err(|e| format!("SystemTime error: {}", e))? | |
.as_secs(); | |
let filename = format!("/tmp/wifi_scan_{}.json", timestamp); | |
let json_data = serde_json::to_string_pretty(&discovered_networks) | |
.map_err(|e| format!("Failed to serialize to JSON: {}", e))?; | |
write_to_file(&filename, &json_data)?; | |
println!("Network data written to {}", filename); | |
} | |
Ok(()) | |
} | |
Err(error) => Err(format!("Failed to scan for networks: {:?}", error)), | |
} | |
} | |
fn auth_status_to_string(status: CLAuthorizationStatus) -> String { | |
match status { | |
CLAuthorizationStatus::NotDetermined => "NotDetermined".to_string(), | |
CLAuthorizationStatus::Restricted => "Restricted".to_string(), | |
CLAuthorizationStatus::Denied => "Denied".to_string(), | |
CLAuthorizationStatus::AuthorizedAlways => "AuthorizedAlways".to_string(), | |
CLAuthorizationStatus::AuthorizedWhenInUse => "AuthorizedWhenInUse".to_string(), | |
// CLAuthorizationStatus::Authorized is deprecated but might appear on older systems | |
_ => format!("Unknown or Deprecated Status ({:?})", status.0), | |
} | |
} | |
fn write_to_file(filename: &str, content: &str) -> Result<(), String> { | |
let mut file = File::create(filename) | |
.map_err(|e| format!("Failed to create file {}: {}", filename, e))?; | |
file.write_all(content.as_bytes()) | |
.map_err(|e| format!("Failed to write to file {}: {}", filename, e)) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
A directory with your Info.plist and the cargo build --release binary | |
WiFiScanner.app | |
WiFiScanner.app/Contents | |
WiFiScanner.app/Contents/MacOS | |
WiFiScanner.app/Contents/MacOS/wifiscanner | |
WiFiScanner.app/Contents/Resources | |
WiFiScanner.app/Contents/Info.plist |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
https://www.google.com/maps/search/?api=1&query={latitude}%2C{longitude}&om=0