Here’s a small Rust scanner that tries SSH on an IP range, auto-accepts host keys (no prompt), and reports which hosts are reachable (even if auth fails with “Permission denied”). It shells out to your system’s ssh
with safe options:
StrictHostKeyChecking=no
→ auto-accept fingerprintsUserKnownHostsFile=/dev/null
→ don’t pollute your~/.ssh/known_hosts
BatchMode=yes
→ never wait for password prompts
Cargo.toml
[package] name = "ssh_find_hosts" version = "0.1.0" edition = "2021" [dependencies] rayon = "1.10"
src/main.rs
use rayon::prelude::*; use std::env; use std::process::{Command, Stdio}; #[derive(Debug, Clone)] struct Target { ip: String, user: String, port: u16, } #[derive(Debug)] enum Outcome { ReachableOk, // ssh command succeeded (you had access) ReachableAuthFailed, // permission denied / publickey Refused, // connection refused NoRoute, // no route to host Timeout, // connection timed out Unknown(String), // anything else (capture stderr) } fn try_ssh(t: &Target) -> Outcome { let dest = format!("{}@{}", t.user, t.ip); // Run a harmless remote command; '-q' silences banners. let output = Command::new("ssh") .args([ "-p", &t.port.to_string(), "-q", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", "-o", "BatchMode=yes", "-o", "ConnectTimeout=3", &dest, "true", ]) .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::piped()) .output(); let out = match output { Ok(o) => { let stderr = String::from_utf8_lossy(&o.stderr).to_lowercase(); if o.status.success() { Outcome::ReachableOk } else if stderr.contains("permission denied") || stderr.contains("publickey") || stderr.contains("authenticity of host") { // Host answered and we got to the auth step → reachable Outcome::ReachableAuthFailed } else if stderr.contains("connection refused") { Outcome::Refused } else if stderr.contains("no route to host") { Outcome::NoRoute } else if stderr.contains("operation timed out") || stderr.contains("connection timed out") { Outcome::Timeout } else { Outcome::Unknown(stderr.trim().to_string()) } } Err(e) => Outcome::Unknown(e.to_string()), }; out } fn main() { // Usage: ssh_find_hosts <prefix> <start> <end> <user> [port] // Example: ssh_find_hosts 192.168.1 100 150 kzorluoglu 22 let args: Vec<String> = env::args().collect(); if args.len() < 5 { eprintln!( "Usage: {} <prefix> <start> <end> <user> [port]\n\ Example: {} 192.168.1 100 150 kzorluoglu 22", args[0], args[0] ); std::process::exit(2); } let prefix = &args[1]; let start: u16 = args[2].parse().expect("start must be number"); let end: u16 = args[3].parse().expect("end must be number"); let user = &args[4]; let port: u16 = if args.len() > 5 { args[5].parse().expect("port must be number") } else { 22 }; let targets: Vec<Target> = (start..=end) .map(|i| Target { ip: format!("{}.{}", prefix, i), user: user.clone(), port, }) .collect(); #[derive(Default)] struct Buckets { ok: Vec<String>, auth_failed: Vec<String>, refused: Vec<String>, no_route: Vec<String>, timeout: Vec<String>, other: Vec<(String, String)>, } let mut buckets = Buckets::default(); targets.par_iter().for_each(|t| { let res = try_ssh(t); match res { Outcome::ReachableOk => println!("[OK] {}", t.ip), Outcome::ReachableAuthFailed => println!("[REACHABLE:AUTH-FAILED] {}", t.ip), Outcome::Refused => println!("[REFUSED] {}", t.ip), Outcome::NoRoute => println!("[NO-ROUTE] {}", t.ip), Outcome::Timeout => println!("[TIMEOUT] {}", t.ip), Outcome::Unknown(ref msg) => println!("[OTHER] {} :: {}", t.ip, msg), } }); // Second pass to collect neatly (serial, quick repeat but deterministic). let mut b = Buckets::default(); for t in targets { match try_ssh(&t) { Outcome::ReachableOk => b.ok.push(t.ip), Outcome::ReachableAuthFailed => b.auth_failed.push(t.ip), Outcome::Refused => b.refused.push(t.ip), Outcome::NoRoute => b.no_route.push(t.ip), Outcome::Timeout => b.timeout.push(t.ip), Outcome::Unknown(msg) => b.other.push((t.ip, msg)), } } println!("\n===== SUMMARY ====="); println!("Reachable (OK): {:?}", b.ok); println!("Reachable (auth failed): {:?}", b.auth_failed); println!("Connection refused: {:?}", b.refused); println!("No route: {:?}", b.no_route); println!("Timeout: {:?}", b.timeout); if !b.other.is_empty() { println!("Other:"); for (ip, msg) in b.other { println!(" {} :: {}", ip, msg); } } }
Build & run
cargo run --release -- 192.168.1 100 110 kzorluoglu 22
Views: 0