use std::io;
use std::time::Duration;
use pnet::datalink::{self, Channel, Config as PnetConfig};
use pnet::packet::ethernet::{EthernetPacket, EtherTypes};
use pnet::packet::ip::IpNextHeaderProtocols;
use pnet::packet::ipv4::Ipv4Packet;
use pnet::packet::tcp::TcpPacket;
use pnet::packet::Packet;
#[derive(Debug, Clone)]
pub struct CleartextCapture {
pub protocol: String,
pub src_ip: String,
pub dst_ip: String,
pub port: u16,
pub detail: String,
}
pub fn capture_cleartext(
iface_name: &str,
listen_secs: u64,
) -> Result<Vec<CleartextCapture>, io::Error> {
let interfaces = datalink::interfaces();
let iface = interfaces
.iter()
.find(|i| i.name == iface_name)
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, format!("Interface '{}' not found", iface_name)))?;
let config = PnetConfig {
promiscuous: true,
read_timeout: Some(Duration::from_millis(500)),
..Default::default()
};
let (_, mut rx) = match datalink::channel(iface, config)? {
Channel::Ethernet(tx, rx) => (tx, rx),
_ => return Err(io::Error::new(io::ErrorKind::Other, "Unsupported channel type")),
};
let mut captures = Vec::new();
let deadline = std::time::Instant::now() + Duration::from_secs(listen_secs);
eprintln!("[*] Passive: cleartext sniff on {} for {}s (promiscuous mode)", iface_name, listen_secs);
loop {
if std::time::Instant::now() >= deadline {
break;
}
match rx.next() {
Ok(frame) => {
if let Some(eth) = EthernetPacket::new(frame) {
if eth.get_ethertype() == EtherTypes::Ipv4 {
if let Some(ip) = Ipv4Packet::new(eth.payload()) {
if ip.get_next_level_protocol() == IpNextHeaderProtocols::Tcp {
if let Some(tcp) = TcpPacket::new(ip.payload()) {
let payload = tcp.payload();
if payload.is_empty() {
continue;
}
let dst_port = tcp.get_destination();
let src_ip = ip.get_source().to_string();
let dst_ip = ip.get_destination().to_string();
if let Some(c) = check_http_basic(payload, &src_ip, &dst_ip, dst_port) {
captures.push(c);
}
if let Some(c) = check_ftp(payload, &src_ip, &dst_ip, dst_port) {
captures.push(c);
}
if let Some(c) = check_smb_cleartext(payload, &src_ip, &dst_ip, dst_port) {
captures.push(c);
}
}
}
}
}
}
}
Err(e) if e.kind() == io::ErrorKind::TimedOut => continue,
Err(e) => {
eprintln!("[!] Packet capture error: {}", e);
break;
}
}
}
Ok(captures)
}
fn check_http_basic(payload: &[u8], src: &str, dst: &str, port: u16) -> Option<CleartextCapture> {
if port != 80 && port != 8080 && port != 8000 {
return None;
}
let text = std::str::from_utf8(payload).ok()?;
let lower = text.to_lowercase();
let pos = lower.find("authorization: basic ")?;
let rest = &text[pos + 21..];
let value: &str = rest.split_whitespace().next().unwrap_or("(truncated)");
let decoded = base64_decode(value.trim_end_matches('\r').trim_end_matches('\n'));
Some(CleartextCapture {
protocol: "HTTP-Basic".into(),
src_ip: src.into(),
dst_ip: dst.into(),
port,
detail: format!("Authorization: Basic {} → {}", value, decoded),
})
}
fn check_ftp(payload: &[u8], src: &str, dst: &str, port: u16) -> Option<CleartextCapture> {
if port != 21 {
return None;
}
let text = std::str::from_utf8(payload).ok()?;
let upper = text.to_uppercase();
if upper.starts_with("USER ") || upper.starts_with("PASS ") {
Some(CleartextCapture {
protocol: "FTP".into(),
src_ip: src.into(),
dst_ip: dst.into(),
port,
detail: text.trim_end().to_string(),
})
} else {
None
}
}
fn check_smb_cleartext(payload: &[u8], src: &str, dst: &str, port: u16) -> Option<CleartextCapture> {
if port != 445 && port != 139 {
return None;
}
if payload.len() > 4 && &payload[..4] == b"\xffSMB" {
if payload[4] == 0x73 {
return Some(CleartextCapture {
protocol: "SMBv1-SessionSetup".into(),
src_ip: src.into(),
dst_ip: dst.into(),
port,
detail: "SMBv1 Session Setup AndX detected (unencrypted authentication)".into(),
});
}
}
None
}
fn base64_decode(encoded: &str) -> String {
let alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut bits = 0u32;
let mut bit_count = 0u8;
let mut result = Vec::new();
for c in encoded.chars() {
if c == '=' {
break;
}
if let Some(val) = alphabet.find(c) {
bits = (bits << 6) | val as u32;
bit_count += 6;
if bit_count >= 8 {
bit_count -= 8;
result.push((bits >> bit_count) as u8 & 0xFF);
}
}
}
String::from_utf8_lossy(&result).into_owned()
}