rsubdomain 1.2.14

A high-performance subdomain brute-force tool written in Rust
Documentation
use clap::Parser;
use log::info;
use log::LevelFilter;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};

use rsubdomain::{
    device, export_scan_data, flush_raw_record_output, logger, print_aggregated_domains, print_summary_stats,
    print_verification_result, resolve_resolver_input, resolve_target_domain_input,
    BruteForceProgress, BruteForceProgressPhase, ProgressCallback,
    CdnAnalysisOptions, Opts, OutputFormat, SpeedTester, SubdomainBruteConfig,
    SubdomainBruteEngine, SubdomainScanData,
};

#[tokio::main]
async fn main() {
    let opts = Opts::parse();
    let log_level = if opts.slient {
        LevelFilter::Off
    } else {
        LevelFilter::Info
    };
    let _ = logger::init_logger(log_level);

    if opts.list_network {
        device::print_network_devices();
        return;
    }

    if opts.network_test {
        if let Err(error) = run_network_speed_test(&opts.target_ip).await {
            eprintln!("网速测试失败: {}", error);
        }
        return;
    }

    if let Err(error) = run_subdomain_brute(opts).await {
        eprintln!("域名暴破失败: {}", error);
    }
}

async fn run_network_speed_test(target_ip: &str) -> Result<(), Box<dyn std::error::Error>> {
    let tester = SpeedTester::new_with_target(target_ip).await?;
    let result = tester.run_speed_test(10).await;
    tester.display_result(&result);
    Ok(())
}

async fn run_subdomain_brute(opts: Opts) -> Result<(), Box<dyn std::error::Error>> {
    let domain_input = resolve_target_domain_input(&opts)?;
    let resolver_input = resolve_resolver_input(&opts)?;
    let progress_callback = build_progress_callback(!opts.slient);

    if !opts.slient {
        info!(
            "目标域名: {} 个(原始输入 {},排除 {})",
            domain_input.domains.len(),
            domain_input.input_count,
            domain_input.excluded_count
        );
        info!(
            "DNS解析器: {} 个{}",
            resolver_input.resolvers.len(),
            if resolver_input.input_count == 0 {
                ",使用内置默认解析器".to_string()
            } else {
                String::new()
            }
        );
        info!(
            "运行控制: retry={}, dns-timeout={}s, wait={}s, verify-timeout={}s, verify-concurrency={}, bandwidth={}, transport={:?}, cdn-detect={}, cdn-collapse={}",
            opts.retry,
            opts.dns_timeout,
            opts.wait_seconds,
            opts.verify_timeout,
            opts.verify_concurrency,
            opts.bandwidth,
            opts.transport,
            resolve_cdn_detect(&opts),
            resolve_cdn_collapse(&opts)
        );
    }

    let engine = SubdomainBruteEngine::new(SubdomainBruteConfig {
        domains: domain_input.domains,
        resolvers: resolver_input.resolvers,
        dictionary_file: opts.file.clone(),
        dictionary: None,
        skip_wildcard: opts.skip_wildcard,
        bandwidth_limit: Some(opts.bandwidth.clone()),
        verify_mode: opts.verify,
        max_retries: opts.retry,
        dns_timeout_seconds: opts.dns_timeout,
        max_wait_seconds: opts.wait_seconds,
        verify_timeout_seconds: opts.verify_timeout,
        verify_concurrency: opts.verify_concurrency,
        resolve_records: opts.resolve_records,
        cdn_detect: resolve_cdn_detect(&opts),
        cdn_collapse: resolve_cdn_collapse(&opts),
        query_types: opts.query_types.clone(),
        silent: opts.slient,
        raw_records: opts.raw_records,
        device: opts.device.clone(),
        transport: opts.transport,
        progress_callback,
    })
    .await?;

    let results = engine.run_brute_force().await?;
    flush_raw_record_output();
    let scan_data = SubdomainScanData::from_results_with_options(
        &results,
        CdnAnalysisOptions {
            detect: resolve_cdn_detect(&opts),
            collapse: resolve_cdn_collapse(&opts),
        },
    );

    if !opts.slient && !opts.raw_records {
        print_aggregated_domains(&scan_data.aggregated_domains);
    }

    if opts.verify {
        for result in &scan_data.verification_results {
            print_verification_result(result);
        }
    }

    if opts.summary {
        print_summary_stats(&scan_data.summary);
    }

    if let Some(output_path) = opts.output {
        let format = opts.format.parse::<OutputFormat>().unwrap_or_else(|error| {
            eprintln!("输出格式解析错误: {}, 使用默认JSON格式", error);
            OutputFormat::Json
        });

        export_scan_data(&scan_data, &output_path, &format)?;
    }

    Ok(())
}

fn build_progress_callback(enabled: bool) -> Option<ProgressCallback> {
    if !enabled {
        return None;
    }

    #[derive(Debug)]
    struct ProgressLogState {
        last_phase: Option<BruteForceProgressPhase>,
        last_logged_at: Instant,
        last_sent_queries: usize,
    }

    let state = Arc::new(Mutex::new(ProgressLogState {
        last_phase: None,
        last_logged_at: Instant::now() - Duration::from_secs(5),
        last_sent_queries: 0,
    }));

    Some(Arc::new(move |progress: BruteForceProgress| {
        let mut state = match state.lock() {
            Ok(guard) => guard,
            Err(_) => return,
        };

        let now = Instant::now();
        let phase_changed = state.last_phase != Some(progress.phase);
        let sending_finished = progress.sent_queries == progress.total_queries;
        let should_log = match progress.phase {
            BruteForceProgressPhase::SendingQueries => {
                phase_changed
                    || progress.sent_queries == 1
                    || sending_finished
                    || (now.duration_since(state.last_logged_at) >= Duration::from_secs(1)
                        && progress.sent_queries > state.last_sent_queries)
            }
            BruteForceProgressPhase::WaitingForResponses => {
                phase_changed || now.duration_since(state.last_logged_at) >= Duration::from_secs(1)
            }
            BruteForceProgressPhase::Completed => true,
        };

        if !should_log {
            return;
        }

        state.last_phase = Some(progress.phase);
        state.last_logged_at = now;
        state.last_sent_queries = progress.sent_queries;

        match progress.phase {
            BruteForceProgressPhase::SendingQueries => {
                let percentage = if progress.total_queries == 0 {
                    100.0
                } else {
                    (progress.sent_queries as f64 / progress.total_queries as f64) * 100.0
                };
                info!(
                    "发送进度: {}/{} ({:.1}%), 已发现 {}",
                    progress.sent_queries,
                    progress.total_queries,
                    percentage,
                    progress.discovered_domains
                );
            }
            BruteForceProgressPhase::WaitingForResponses => {
                info!(
                    "等待响应: 已发送 {}, 已发现 {}",
                    progress.sent_queries, progress.discovered_domains
                );
            }
            BruteForceProgressPhase::Completed => {
                info!(
                    "扫描完成: 已发送 {}, 已发现 {}",
                    progress.sent_queries, progress.discovered_domains
                );
            }
        }
    }))
}

fn resolve_cdn_detect(opts: &Opts) -> bool {
    if opts.no_cdn_detect {
        return false;
    }

    if opts.cdn_detect {
        return true;
    }

    true
}

fn resolve_cdn_collapse(opts: &Opts) -> bool {
    if opts.no_cdn_collapse {
        return false;
    }

    if opts.cdn_collapse {
        return true;
    }

    true
}