diff --git a/Cargo.lock b/Cargo.lock index 097e4a7..f71697f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -532,6 +532,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "object" version = "0.36.4" @@ -547,6 +557,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "parking_lot" version = "0.12.3" @@ -756,7 +772,6 @@ checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63" dependencies = [ "base64", "bytes", - "futures-channel", "futures-core", "futures-util", "http", @@ -925,6 +940,15 @@ dependencies = [ "serde", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1028,6 +1052,16 @@ dependencies = [ "syn 2.0.77", ] +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "tinyvec" version = "1.8.0" @@ -1128,9 +1162,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + [[package]] name = "tracing-core" version = "0.1.32" @@ -1138,6 +1184,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", ] [[package]] @@ -1152,6 +1224,8 @@ dependencies = [ "thiserror", "tokio", "tokio-stream", + "tracing", + "tracing-subscriber", "trust-dns-resolver", ] @@ -1244,6 +1318,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "want" version = "0.3.1" @@ -1338,9 +1418,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.26.3" +version = "0.26.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" +checksum = "841c67bff177718f1d4dfefde8d8f0e78f9b6589319ba88312f567fc5841a958" dependencies = [ "rustls-pki-types", ] diff --git a/Cargo.toml b/Cargo.toml index 50ab5a2..d033afe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] anyhow = "1.0.89" -reqwest = { version = "0.12.7", default-features = false, features = ["json", "blocking", "rustls-tls"] } +reqwest = { version = "0.12.7", default-features = false, features = ["json", "rustls-tls"] } serde = { version = "1.0.210", features = ["derive"] } thiserror = "1.0.63" trust-dns-resolver = "0.21" @@ -13,3 +13,5 @@ rayon = "1.10.0" tokio = { version = "1.40.0", features = ["full"] } futures = "0.3.30" tokio-stream = "0.1.16" +tracing = "0.1.40" +tracing-subscriber = "0.3.18" diff --git a/src/main.rs b/src/main.rs index e85f0a7..cb4b44f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ use std::{ env, time::{Duration, Instant}, }; +use tracing::info; mod error; pub use error::Error; @@ -13,8 +14,15 @@ mod subdomains; use model::Subdomain; mod common_ports; +const PORTS_CONCURRENCY: usize = 200; +const SUBDOMAINS_CONCURRENCY: usize = 100; +const CRT_SH_TIMEOUT: Duration = Duration::from_secs(10); +const PORT_TIMEOUT: Duration = Duration::from_secs(1); + #[tokio::main] async fn main() -> Result<(), anyhow::Error> { + tracing_subscriber::fmt::init(); + let args: Vec = env::args().collect(); if args.len() != 2 { @@ -23,24 +31,33 @@ async fn main() -> Result<(), anyhow::Error> { let target = args[1].as_str(); - let http_timeout = Duration::from_secs(10); - let http_client = Client::builder().timeout(http_timeout).build()?; + let http_client = Client::builder().timeout(CRT_SH_TIMEOUT).build()?; - let ports_concurrency = 200; - let subdomains_concurrency = 100; let scan_start = Instant::now(); let subdomains = subdomains::enumerate(&http_client, target).await?; + let domain_search = scan_start.elapsed(); + info!( + "Found {} subdomains in {:?}", + subdomains.len(), + domain_search + ); + // Concurrent stream method 1: Using buffer_unordered + collect let scan_result: Vec = stream::iter(subdomains.into_iter()) - .map(|subdomain| ports::scan_ports(ports_concurrency, subdomain)) - .buffer_unordered(subdomains_concurrency) + .map(|subdomain: Subdomain| tokio::spawn(ports::scan_ports(PORTS_CONCURRENCY, subdomain))) + .buffer_unordered(SUBDOMAINS_CONCURRENCY) + .map(|result| result.unwrap()) .collect() .await; - - let scan_duration = scan_start.elapsed(); - println!("Scan completed in {:?}", scan_duration); + + let complete_scan = scan_start.elapsed(); + info!("Scan completed in {:?}", complete_scan); + info!( + "Excess time: {:?}", + complete_scan - domain_search - PORT_TIMEOUT + ); for subdomain in scan_result { println!("{}:", &subdomain.domain); @@ -48,7 +65,7 @@ async fn main() -> Result<(), anyhow::Error> { println!(" {}: open", port.port); } - println!(""); + println!(); } Ok(()) diff --git a/src/model.rs b/src/model.rs index f6e9379..05537eb 100644 --- a/src/model.rs +++ b/src/model.rs @@ -3,6 +3,7 @@ use serde::Deserialize; #[derive(Debug, Clone)] pub struct Subdomain { pub domain: String, + #[allow(dead_code)] pub has_address: bool, pub open_ports: Vec, } diff --git a/src/ports.rs b/src/ports.rs index 07b2e64..563a94f 100644 --- a/src/ports.rs +++ b/src/ports.rs @@ -1,68 +1,58 @@ use crate::{ common_ports::MOST_COMMON_PORTS_100, model::{Port, Subdomain}, + PORT_TIMEOUT, +}; +use futures::{stream, StreamExt}; +use std::{ + net::{SocketAddr, ToSocketAddrs}, + time::Instant, }; -use futures::StreamExt; -use std::net::{SocketAddr, ToSocketAddrs}; -use std::time::Duration; use tokio::net::TcpStream; -use tokio::sync::mpsc; -pub async fn scan_ports(concurrency: usize, subdomain: Subdomain) -> Subdomain { - let mut ret = subdomain.clone(); - let socket_addresses: Vec = format!("{}:1024", subdomain.domain) +#[tracing::instrument(skip(subdomain), fields(subdomain = subdomain.domain))] +pub async fn scan_ports(concurrency: usize, mut subdomain: Subdomain) -> Subdomain { + tracing::debug!("Scanning"); + let now = Instant::now(); + + let socket_addresses: Vec = format!("{}:1024", &subdomain.domain) .to_socket_addrs() .expect("port scanner: Creating socket address") .collect(); - if socket_addresses.len() == 0 { + if socket_addresses.is_empty() { return subdomain; } let socket_address = socket_addresses[0]; - // Concurrent stream method 3: using channels - let (input_tx, input_rx) = mpsc::channel(concurrency); - let (output_tx, output_rx) = mpsc::channel(concurrency); - - tokio::spawn(async move { - for port in MOST_COMMON_PORTS_100 { - let _ = input_tx.send(*port).await; - } - }); + subdomain.open_ports = stream::iter( + // we clone to avoid some borrowing issues + MOST_COMMON_PORTS_100.to_owned(), + ) + .map(|port| scan_port(socket_address, port)) + .buffer_unordered(concurrency) + .filter(|p| futures::future::ready(p.is_open)) + .collect::>() + .await; - let input_rx_stream = tokio_stream::wrappers::ReceiverStream::new(input_rx); - input_rx_stream - .for_each_concurrent(concurrency, |port| { - let output_tx = output_tx.clone(); - async move { - let port = scan_port(socket_address, port).await; - if port.is_open { - let _ = output_tx.send(port).await; - } - } - }) - .await; - // close channel - drop(output_tx); + tracing::debug!("Scanned in {:?}", now.elapsed()); - let output_rx_stream = tokio_stream::wrappers::ReceiverStream::new(output_rx); - ret.open_ports = output_rx_stream.collect().await; - - ret + subdomain } +#[tracing::instrument(skip(socket_address))] async fn scan_port(mut socket_address: SocketAddr, port: u16) -> Port { - let timeout = Duration::from_secs(3); + tracing::trace!("Scanning"); socket_address.set_port(port); + let start = Instant::now(); let is_open = matches!( - tokio::time::timeout(timeout, TcpStream::connect(&socket_address)).await, + tokio::time::timeout(PORT_TIMEOUT, TcpStream::connect(&socket_address)).await, Ok(Ok(_)), ); - Port { - port: port, - is_open, - } + let time_taken = start.elapsed(); + tracing::trace!("Open {} in {:?}", port, time_taken); + Port { port, is_open } } diff --git a/src/subdomains.rs b/src/subdomains.rs index 71974fe..2231c0a 100644 --- a/src/subdomains.rs +++ b/src/subdomains.rs @@ -16,7 +16,7 @@ type DnsResolver = AsyncResolver Result, Error> { let entries: Vec = http_client - .get(&format!("https://crt.sh/?q=%25.{}&output=json", target)) + .get(format!("https://crt.sh/?q=%25.{}&output=json", target)) .send() .await? .json() @@ -31,14 +31,13 @@ pub async fn enumerate(http_client: &Client, target: &str) -> Result = entries .into_iter() - .map(|entry| { + .flat_map(|entry| { entry .name_value .split("\n") .map(|subdomain| subdomain.trim().to_string()) .collect::>() }) - .flatten() .filter(|subdomain: &String| subdomain != target) .filter(|subdomain: &String| !subdomain.contains("*")) .collect();