Skip to content

Instantly share code, notes, and snippets.

@jeffbryner
Created June 6, 2025 18:22
Show Gist options
  • Save jeffbryner/b296a635389614aa227572da9241a568 to your computer and use it in GitHub Desktop.
Save jeffbryner/b296a635389614aa227572da9241a568 to your computer and use it in GitHub Desktop.
macOS: capture surrounding wifi networks in rust
[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"
<?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>
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))
}
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
@jeffbryner
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment