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_hostsBatchMode=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: 3