//! Command-line interface definitions (clap). use std::path::PathBuf; use clap::{Args, Parser, Subcommand}; /// With a node but no subcommand, the top-level args run the full auto pipeline /// (ping → latency → speed → export). With no arguments at all (e.g. a /// double-clicked binary), the local web UI launches. #[derive(Parser)] #[command( name = "fast-xray", about = "Fast Cloudflare IP optimizer for CDN-fronted Xray/VLESS nodes.", args_conflicts_with_subcommands = true )] pub(crate) struct Cli { #[command(subcommand)] pub(crate) command: Option, #[command(flatten)] pub(crate) auto: AutoArgs, } #[derive(Subcommand)] pub(crate) enum Command { /// Probe Cloudflare IPs over TCP 443 and collect the fastest reachable ones. Ping(PingArgs), /// Measure real proxy latency for candidate IPs through the node. Latency(LatencyArgs), /// Download through the node to measure real speed. Speed(SpeedArgs), /// Build importable vless:// nodes from a valid-IP list and a node. Export(ExportArgs), /// Find one IP fast enough to hit a target speed, then stop. Easy(EasyArgs), /// Launch the local web UI (zero-install front end for `easy`). Web(WebArgs), } #[derive(Args)] pub(crate) struct AutoArgs { /// Input vless:// node URL (or use --node-file). pub(crate) node: Option, /// Read the vless:// node URL from a file (avoids shell escaping of `&`). #[arg(long)] pub(crate) node_file: Option, /// Stage 1: how many valid IPs the ping stage collects. #[arg(short = 'n', long, default_value_t = 50)] pub(crate) count: usize, /// Stage 2: keep the fastest N after latency (feeds the speed stage). /// 0 = keep all that pass. #[arg(long, default_value_t = 0)] pub(crate) lat_top: usize, /// Stage 3: final node count written to result.txt. 0 = keep all. #[arg(long, default_value_t = 0)] pub(crate) speed_top: usize, /// Stage 3: concurrent downloads during the speed test (1 = most accurate). #[arg(long, default_value_t = 1)] pub(crate) speed_concurrency: usize, /// Also test IPv6 ranges (off by default). #[arg(short = '6', long = "ipv6")] pub(crate) ipv6: bool, /// Stage 1: drop IPs whose TCP ping exceeds this (ms). #[arg(long, default_value_t = 300.0)] pub(crate) ping_max: f64, /// Stage 2: drop IPs whose real latency exceeds this (ms). 0 = off. #[arg(long, default_value_t = 0.0)] pub(crate) lat_max: f64, /// Quality gate: drop IPs slower than this (MB/s). 0 = off. #[arg(long, default_value_t = 0.0)] pub(crate) min_speed: f64, /// Output directory for all intermediate files and result.txt. #[arg(short = 'o', long, default_value = "result")] pub(crate) output: PathBuf, /// Print each stage's result table. #[arg(short = 'v', long)] pub(crate) verbose: bool, } #[derive(Args)] pub(crate) struct PingArgs { /// Target number of valid (reachable) IPs to collect. #[arg(short = 'n', long, default_value_t = 50)] pub(crate) count: usize, /// TCP connect timeout in seconds; slower than this counts as unreachable. #[arg(short = 't', long, default_value_t = 3.0)] pub(crate) timeout: f64, /// Concurrent probes. #[arg(short = 'c', long, default_value_t = 100)] pub(crate) concurrency: usize, /// Also test IPv6 ranges (off by default). #[arg(short = '6', long = "ipv6")] pub(crate) ipv6: bool, /// TCP port to probe. #[arg(short = 'p', long, default_value_t = 443)] pub(crate) port: u16, /// Safety cap: stop after probing this many IPs. Defaults to count * 100. #[arg(long)] pub(crate) max_probe: Option, /// TCP probes per IP. >1 enables packet-loss filtering. #[arg(long, default_value_t = 1)] pub(crate) times: usize, /// Reject IPs with average latency below this (ms). #[arg(long = "min", default_value_t = 10.0)] pub(crate) min_latency: f64, /// Reject IPs with average latency above this (ms). #[arg(long = "max", default_value_t = 300.0)] pub(crate) max_latency: f64, /// Reject IPs with loss rate above this (0.0-1.0). Default 0 = no loss. #[arg(long, default_value_t = 0.0)] pub(crate) max_loss: f64, /// Output directory. Writes /ip.txt (plain IPs, fastest first). #[arg(short = 'o', long, default_value = "result")] pub(crate) output: PathBuf, /// Print the full result table (IP + latency) at the end. #[arg(short = 'v', long)] pub(crate) verbose: bool, } #[derive(Args)] pub(crate) struct LatencyArgs { /// Input vless:// node URL (or use --node-file). pub(crate) node: Option, /// Read the vless:// node URL from a file (avoids shell escaping of `&`). #[arg(long)] pub(crate) node_file: Option, /// IP list to test (one per line), e.g. the ping stage output. #[arg(short = 'i', long, default_value = "result/ip.txt")] pub(crate) input: PathBuf, /// Concurrent tunnels. #[arg(short = 'c', long, default_value_t = 50)] pub(crate) concurrency: usize, /// Per-IP timeout in seconds for the whole tunnel + request. #[arg(short = 't', long, default_value_t = 5.0)] pub(crate) timeout: f64, /// Reject IPs whose real latency exceeds this (ms). 0 = timeout only. #[arg(long = "max", default_value_t = 0.0)] pub(crate) max_latency: f64, /// Keep only the fastest N results. 0 = keep all that pass. #[arg(short = 'p', long, default_value_t = 0)] pub(crate) top: usize, /// Output directory. Writes /latency.csv (IP + latency) and /// /latency.txt (plain IPs, fastest first). #[arg(short = 'o', long, default_value = "result")] pub(crate) output: PathBuf, /// Print the full result table at the end. #[arg(short = 'v', long)] pub(crate) verbose: bool, } #[derive(Args)] pub(crate) struct SpeedArgs { /// Input vless:// node URL (or use --node-file). pub(crate) node: Option, /// Read the vless:// node URL from a file (avoids shell escaping of `&`). #[arg(long)] pub(crate) node_file: Option, /// Input CSV from the latency stage (IP + latency). #[arg(short = 'i', long, default_value = "result/latency.csv")] pub(crate) input: PathBuf, /// Concurrent downloads. Default 1 for accurate throughput. #[arg(short = 'c', long, default_value_t = 1)] pub(crate) concurrency: usize, /// Per-IP download time cap in seconds. #[arg(short = 't', long, default_value_t = 10.0)] pub(crate) timeout: f64, /// Bytes to download per IP before stopping. #[arg(long, default_value_t = 10_000_000)] pub(crate) bytes: u64, /// Reject IPs slower than this (MB/s). 0 = keep all. #[arg(long = "min", default_value_t = 0.0)] pub(crate) min_speed: f64, /// Keep only the best N results. 0 means keep all. #[arg(short = 'p', long, default_value_t = 0)] pub(crate) top: usize, /// Output directory. Writes /speed.csv and /speed.txt. #[arg(short = 'o', long, default_value = "result")] pub(crate) output: PathBuf, /// Print the full result table at the end. #[arg(short = 'v', long)] pub(crate) verbose: bool, } #[derive(Args)] pub(crate) struct EasyArgs { /// Input vless:// node URL (or use --node-file). pub(crate) node: Option, /// Read the vless:// node URL from a file (avoids shell escaping of `&`). #[arg(long)] pub(crate) node_file: Option, /// Target download speed in MB/s; returns the first IP that reaches it. /// Omit to default to 80% of the measured direct (no-proxy) speed. #[arg(long)] pub(crate) speed: Option, /// Give up after this many valid IPs (those that pass both ping and /// latency). Reaching it without a fast-enough IP means "not found". #[arg(long, default_value_t = 100)] pub(crate) max: usize, /// Also search IPv6 ranges (off by default). #[arg(short = '6', long = "ipv6")] pub(crate) ipv6: bool, /// Output directory. Writes /result.txt with the chosen node. #[arg(short = 'o', long, default_value = "result")] pub(crate) output: PathBuf, } impl EasyArgs { pub(crate) fn chuan() -> Self { Self { node: Some(crate::easy::CHUAN_NODE.to_string()), node_file: None, speed: None, max: 100, ipv6: false, output: PathBuf::from("result"), } } } #[derive(Args)] pub(crate) struct WebArgs { /// Address to bind. Use 0.0.0.0 to expose on the LAN or behind a reverse proxy. #[arg(long, default_value = "127.0.0.1")] pub(crate) host: String, /// Port to listen on. #[arg(short = 'p', long, default_value_t = 8080)] pub(crate) port: u16, /// Don't open the browser automatically on startup. #[arg(long)] pub(crate) no_open: bool, } impl Default for WebArgs { fn default() -> Self { Self { host: "127.0.0.1".to_string(), port: 8080, no_open: false } } } #[derive(Args)] pub(crate) struct ExportArgs { /// Input vless:// node URL (or use --node-file). pub(crate) node: Option, /// Read the vless:// node URL from a file (avoids shell escaping of `&`). #[arg(long)] pub(crate) node_file: Option, /// Valid IP list: plain IPs, or a CSV whose first column is the IP. #[arg(short = 'i', long, default_value = "result/ip.txt")] pub(crate) input: PathBuf, /// Output directory. Writes /result.txt (importable nodes). #[arg(short = 'o', long, default_value = "result")] pub(crate) output: PathBuf, }