Files
fast-xray/src/cli.rs
T

302 lines
9.5 KiB
Rust

//! 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>,
#[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<String>,
/// Read the vless:// node URL from a file (avoids shell escaping of `&`).
#[arg(long)]
pub(crate) node_file: Option<PathBuf>,
/// 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<usize>,
/// 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 <dir>/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<String>,
/// Read the vless:// node URL from a file (avoids shell escaping of `&`).
#[arg(long)]
pub(crate) node_file: Option<PathBuf>,
/// 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 <dir>/latency.csv (IP + latency) and
/// <dir>/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<String>,
/// Read the vless:// node URL from a file (avoids shell escaping of `&`).
#[arg(long)]
pub(crate) node_file: Option<PathBuf>,
/// 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 <dir>/speed.csv and <dir>/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<String>,
/// Read the vless:// node URL from a file (avoids shell escaping of `&`).
#[arg(long)]
pub(crate) node_file: Option<PathBuf>,
/// 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<f64>,
/// 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 <dir>/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<String>,
/// Read the vless:// node URL from a file (avoids shell escaping of `&`).
#[arg(long)]
pub(crate) node_file: Option<PathBuf>,
/// 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 <dir>/result.txt (importable nodes).
#[arg(short = 'o', long, default_value = "result")]
pub(crate) output: PathBuf,
}