feat: init
This commit is contained in:
+11
@@ -0,0 +1,11 @@
|
||||
# Rust build output
|
||||
/target/
|
||||
|
||||
# Backup files
|
||||
**/*.rs.bk
|
||||
|
||||
# IDE/editor
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
result/
|
||||
Generated
+1643
File diff suppressed because it is too large
Load Diff
+23
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "fast-xray"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
rust-version = "1.85"
|
||||
description = "Fast Cloudflare IP optimizer for CDN-fronted Xray/VLESS nodes."
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
console = "0.15"
|
||||
futures = "0.3"
|
||||
indicatif = "0.17"
|
||||
ipnet = "2"
|
||||
rand = "0.8"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
|
||||
tokio = { version = "1", features = ["macros", "net", "rt-multi-thread", "time"] }
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
lto = "thin"
|
||||
codegen-units = 1
|
||||
strip = true
|
||||
@@ -0,0 +1,118 @@
|
||||
//! Fetch Cloudflare's published CDN IP ranges from the web.
|
||||
//!
|
||||
//! Cloudflare publishes its anycast ranges as plain-text CIDR lists, one per
|
||||
//! line. This module fetches them and returns parsed, typed CIDR lists for
|
||||
//! other stages (sampling, probing) to consume. It does not write files or
|
||||
//! shell out.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use ipnet::{IpNet, Ipv4Net, Ipv6Net};
|
||||
|
||||
pub const CF_IPV4_URL: &str = "https://www.cloudflare.com/ips-v4";
|
||||
pub const CF_IPV6_URL: &str = "https://www.cloudflare.com/ips-v6";
|
||||
|
||||
const USER_AGENT: &str = "cfopt/0.1";
|
||||
|
||||
/// Which address families to fetch.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[allow(dead_code)] // V4 / V6 reserved for a future --family CLI flag.
|
||||
pub enum Family {
|
||||
V4,
|
||||
V6,
|
||||
Both,
|
||||
}
|
||||
|
||||
/// Parsed Cloudflare CDN ranges, split by address family.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct CfRanges {
|
||||
pub v4: Vec<Ipv4Net>,
|
||||
pub v6: Vec<Ipv6Net>,
|
||||
}
|
||||
|
||||
impl CfRanges {
|
||||
#[allow(dead_code)] // paired with len(); used by later stages.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.v4.is_empty() && self.v6.is_empty()
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // used by later stages.
|
||||
pub fn len(&self) -> usize {
|
||||
self.v4.len() + self.v6.len()
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch Cloudflare ranges for the requested family and return them parsed.
|
||||
pub async fn fetch_ranges(family: Family) -> Result<CfRanges> {
|
||||
let client = reqwest::Client::builder()
|
||||
.user_agent(USER_AGENT)
|
||||
.timeout(std::time::Duration::from_secs(15))
|
||||
.build()
|
||||
.context("build http client")?;
|
||||
|
||||
let mut ranges = CfRanges::default();
|
||||
if matches!(family, Family::V4 | Family::Both) {
|
||||
let text = fetch_text(&client, CF_IPV4_URL).await?;
|
||||
ranges.v4 = parse_v4_lines(&text);
|
||||
}
|
||||
if matches!(family, Family::V6 | Family::Both) {
|
||||
let text = fetch_text(&client, CF_IPV6_URL).await?;
|
||||
ranges.v6 = parse_v6_lines(&text);
|
||||
}
|
||||
Ok(ranges)
|
||||
}
|
||||
|
||||
async fn fetch_text(client: &reqwest::Client, url: &str) -> Result<String> {
|
||||
client
|
||||
.get(url)
|
||||
.send()
|
||||
.await
|
||||
.with_context(|| format!("request {url}"))?
|
||||
.error_for_status()
|
||||
.with_context(|| format!("bad status from {url}"))?
|
||||
.text()
|
||||
.await
|
||||
.with_context(|| format!("read body from {url}"))
|
||||
}
|
||||
|
||||
/// Parse a plain-text CIDR list, keeping only IPv4 networks.
|
||||
pub fn parse_v4_lines(text: &str) -> Vec<Ipv4Net> {
|
||||
parse_cidr_lines(text)
|
||||
.into_iter()
|
||||
.filter_map(|net| match net {
|
||||
IpNet::V4(v4) => Some(v4),
|
||||
IpNet::V6(_) => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Parse a plain-text CIDR list, keeping only IPv6 networks.
|
||||
pub fn parse_v6_lines(text: &str) -> Vec<Ipv6Net> {
|
||||
parse_cidr_lines(text)
|
||||
.into_iter()
|
||||
.filter_map(|net| match net {
|
||||
IpNet::V6(v6) => Some(v6),
|
||||
IpNet::V4(_) => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Parse a plain-text CIDR list, skipping blanks and anything unparseable.
|
||||
fn parse_cidr_lines(text: &str) -> Vec<IpNet> {
|
||||
text.lines()
|
||||
.filter_map(|line| line.split_whitespace().next())
|
||||
.filter_map(|token| token.parse::<IpNet>().ok())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn splits_v4_and_v6() {
|
||||
let text = "173.245.48.0/20\n# comment\n\n103.21.244.0/22 trailing\n2400:cb00::/32\n";
|
||||
assert_eq!(parse_v4_lines(text).len(), 2);
|
||||
assert_eq!(parse_v6_lines(text).len(), 1);
|
||||
assert_eq!(parse_v4_lines(text)[0].to_string(), "173.245.48.0/20");
|
||||
}
|
||||
}
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
//! cfopt — Cloudflare IP optimizer for CDN-fronted VLESS nodes.
|
||||
//!
|
||||
//! Subcommands mirror the optimization pipeline. Implemented so far:
|
||||
//! ping — collect the fastest reachable Cloudflare IPs over TCP.
|
||||
|
||||
mod cloudflare;
|
||||
mod ping;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use console::style;
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
|
||||
use cloudflare::Family;
|
||||
use ping::{PingResult, Progress};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "fast-xray", about = "Fast Cloudflare IP optimizer for CDN-fronted Xray/VLESS nodes.")]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Command,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Command {
|
||||
/// Probe Cloudflare IPs over TCP 443 and collect the fastest reachable ones.
|
||||
Ping(PingArgs),
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
struct PingArgs {
|
||||
/// Target number of valid (reachable) IPs to collect.
|
||||
#[arg(short = 'n', long, default_value_t = 50)]
|
||||
count: usize,
|
||||
|
||||
/// TCP connect timeout in seconds; slower than this counts as unreachable.
|
||||
#[arg(short = 't', long, default_value_t = 3.0)]
|
||||
timeout: f64,
|
||||
|
||||
/// Concurrent probes.
|
||||
#[arg(short = 'c', long, default_value_t = 100)]
|
||||
concurrency: usize,
|
||||
|
||||
/// Also test IPv6 ranges (off by default).
|
||||
#[arg(short = '6', long = "ipv6")]
|
||||
ipv6: bool,
|
||||
|
||||
/// TCP port to probe.
|
||||
#[arg(short = 'p', long, default_value_t = 443)]
|
||||
port: u16,
|
||||
|
||||
/// Random IPs sampled per segment in the availability pre-scan.
|
||||
#[arg(long, default_value_t = 5)]
|
||||
probe_sample: usize,
|
||||
|
||||
/// Safety cap: stop after probing this many IPs. Defaults to count * 100.
|
||||
#[arg(long)]
|
||||
max_probe: Option<usize>,
|
||||
|
||||
/// Write the resulting IPs (one per line, fastest first) to this file.
|
||||
#[arg(short = 'o', long, default_value = "result/ip.txt")]
|
||||
output: PathBuf,
|
||||
|
||||
/// Print the full result table (IP + latency) at the end.
|
||||
#[arg(short = 'v', long)]
|
||||
verbose: bool,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
match cli.command {
|
||||
Command::Ping(args) => run_ping(args).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_ping(args: PingArgs) -> Result<()> {
|
||||
let family = if args.ipv6 { Family::Both } else { Family::V4 };
|
||||
|
||||
let spinner = ProgressBar::new_spinner();
|
||||
spinner.enable_steady_tick(Duration::from_millis(90));
|
||||
spinner.set_message("Fetching Cloudflare ranges…");
|
||||
let ranges = cloudflare::fetch_ranges(family).await?;
|
||||
spinner.finish_with_message(format!(
|
||||
"{} Cloudflare ranges: {} v4, {} v6",
|
||||
style("✓").green().bold(),
|
||||
ranges.v4.len(),
|
||||
ranges.v6.len()
|
||||
));
|
||||
|
||||
let cfg = ping::PingConfig {
|
||||
count: args.count,
|
||||
timeout: Duration::from_secs_f64(args.timeout),
|
||||
concurrency: args.concurrency.max(1),
|
||||
ipv6: args.ipv6,
|
||||
port: args.port,
|
||||
probe_sample: args.probe_sample,
|
||||
max_probe: args.max_probe.unwrap_or(args.count.saturating_mul(100)),
|
||||
};
|
||||
|
||||
let bar = ProgressBar::new(cfg.count as u64);
|
||||
bar.set_style(
|
||||
ProgressStyle::with_template(
|
||||
"{spinner:.cyan} [{bar:32.green/dim}] {pos}/{len} valid {msg}",
|
||||
)
|
||||
.unwrap()
|
||||
.progress_chars("=>-"),
|
||||
);
|
||||
bar.enable_steady_tick(Duration::from_millis(90));
|
||||
|
||||
let target = cfg.count;
|
||||
let results = ping::run(&ranges, &cfg, |progress| match progress {
|
||||
Progress::Probing { ip, ok, valid, probed } => {
|
||||
bar.set_position(valid.min(target) as u64);
|
||||
let mark = if ok { style("✓").green() } else { style("·").dim() };
|
||||
bar.set_message(format!("probed {probed} {mark} {ip}"));
|
||||
}
|
||||
Progress::PreScan { live, segments, valid } => {
|
||||
bar.println(format!(
|
||||
" pre-scan: {} live / {} segments, {} already valid",
|
||||
style(live).cyan(),
|
||||
segments,
|
||||
style(valid).green(),
|
||||
));
|
||||
bar.set_position(valid.min(target) as u64);
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
bar.finish_and_clear();
|
||||
|
||||
if results.is_empty() {
|
||||
eprintln!("{} no reachable IP found", style("✗").red().bold());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if args.verbose {
|
||||
print_table(&results);
|
||||
}
|
||||
write_output(&results, &args.output)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Pretty, colored result table on stderr (human-facing).
|
||||
fn print_table(results: &[PingResult]) {
|
||||
eprintln!();
|
||||
eprintln!(
|
||||
"{}",
|
||||
style(format!(" Fastest {} IPs", results.len())).bold().underlined()
|
||||
);
|
||||
eprintln!(" {:>3} {:<39} {:>9}", "#", "IP", "Latency");
|
||||
for (i, r) in results.iter().enumerate() {
|
||||
let ms = r.latency.as_secs_f64() * 1000.0;
|
||||
let latency = format!("{ms:>6.1} ms");
|
||||
let colored = if ms < 150.0 {
|
||||
style(latency).green()
|
||||
} else if ms < 250.0 {
|
||||
style(latency).yellow()
|
||||
} else {
|
||||
style(latency).red()
|
||||
};
|
||||
eprintln!(" {:>3} {:<39} {}", i + 1, r.ip.to_string(), colored);
|
||||
}
|
||||
eprintln!();
|
||||
}
|
||||
|
||||
/// Write IPs only (one per line, fastest first) to `path`.
|
||||
fn write_output(results: &[PingResult], path: &Path) -> Result<()> {
|
||||
let mut body = String::with_capacity(results.len() * 16);
|
||||
for r in results {
|
||||
body.push_str(&r.ip.to_string());
|
||||
body.push('\n');
|
||||
}
|
||||
if let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) {
|
||||
std::fs::create_dir_all(parent)
|
||||
.with_context(|| format!("create dir for {}", path.display()))?;
|
||||
}
|
||||
std::fs::write(path, &body).with_context(|| format!("write {}", path.display()))?;
|
||||
eprintln!(
|
||||
"{} saved {} IPs -> {}",
|
||||
style("✓").green().bold(),
|
||||
results.len(),
|
||||
style(path.display()).cyan()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
//! Stage 1 (`ping`): TCP-probe Cloudflare IPs and collect the fastest
|
||||
//! reachable ones.
|
||||
//!
|
||||
//! We do NOT use ICMP: optimization cares about whether TCP 443 (the port the
|
||||
//! real VLESS+TLS connection uses) is reachable and fast. ICMP only tells us
|
||||
//! the host answers pings — a different thing, often throttled or blocked —
|
||||
//! and would need raw sockets or an external `ping` binary.
|
||||
//!
|
||||
//! Flow:
|
||||
//! Phase A — availability pre-scan: sample a few IPs per segment, keep only
|
||||
//! segments that answer (CF segment usability is very uneven).
|
||||
//! Phase B — collection: sample uniformly across live segments and probe in
|
||||
//! concurrent batches until `count` valid IPs are gathered (or the
|
||||
//! `max_probe` budget is hit), then return the lowest-latency ones.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use anyhow::Result;
|
||||
use futures::stream::{self, StreamExt};
|
||||
use ipnet::IpNet;
|
||||
use rand::Rng;
|
||||
use rand::seq::SliceRandom;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::timeout;
|
||||
|
||||
use crate::cloudflare::CfRanges;
|
||||
|
||||
/// One reachable IP and its TCP-connect latency.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PingResult {
|
||||
pub ip: IpAddr,
|
||||
pub latency: Duration,
|
||||
}
|
||||
|
||||
/// Progress events emitted during a run so the caller can render UI. Keeps this
|
||||
/// module free of any console/progress-bar dependency.
|
||||
pub enum Progress {
|
||||
/// A single IP just finished probing (emitted for every probe, live feed).
|
||||
Probing { ip: IpAddr, ok: bool, valid: usize, probed: usize },
|
||||
/// Availability pre-scan finished.
|
||||
PreScan { live: usize, segments: usize, valid: usize },
|
||||
}
|
||||
|
||||
/// Parameters for the ping stage.
|
||||
pub struct PingConfig {
|
||||
/// Target number of valid IPs to collect.
|
||||
pub count: usize,
|
||||
/// Per-probe TCP connect timeout; slower than this is treated as down.
|
||||
pub timeout: Duration,
|
||||
/// Number of probes in flight at once.
|
||||
pub concurrency: usize,
|
||||
/// Whether IPv6 segments are included.
|
||||
pub ipv6: bool,
|
||||
/// TCP port to probe.
|
||||
pub port: u16,
|
||||
/// IPs sampled per segment during the availability pre-scan.
|
||||
pub probe_sample: usize,
|
||||
/// Hard cap on how many IPs we probe before giving up.
|
||||
pub max_probe: usize,
|
||||
}
|
||||
|
||||
/// Run the ping stage and return collected valid IPs, lowest latency first.
|
||||
///
|
||||
/// `on_progress` is invoked between probe batches with [`Progress`] events.
|
||||
pub async fn run(
|
||||
ranges: &CfRanges,
|
||||
cfg: &PingConfig,
|
||||
mut on_progress: impl FnMut(Progress),
|
||||
) -> Result<Vec<PingResult>> {
|
||||
let mut nets: Vec<IpNet> = ranges.v4.iter().map(|n| IpNet::V4(*n)).collect();
|
||||
if cfg.ipv6 {
|
||||
nets.extend(ranges.v6.iter().map(|n| IpNet::V6(*n)));
|
||||
}
|
||||
if nets.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let mut seen: HashSet<IpAddr> = HashSet::new();
|
||||
let mut collected: Vec<PingResult> = Vec::new();
|
||||
|
||||
// Phase A: availability pre-scan, learn which segments are live.
|
||||
let mut a_samples: Vec<(usize, IpAddr)> = Vec::new();
|
||||
{
|
||||
let mut rng = rand::thread_rng();
|
||||
for (idx, net) in nets.iter().enumerate() {
|
||||
for _ in 0..cfg.probe_sample {
|
||||
let ip = random_host(net, &mut rng);
|
||||
if seen.insert(ip) {
|
||||
a_samples.push((idx, ip));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let a_ips: Vec<IpAddr> = a_samples.iter().map(|(_, ip)| *ip).collect();
|
||||
let mut probed = 0usize;
|
||||
let mut a_hits: HashMap<IpAddr, Duration> = HashMap::new();
|
||||
probe_each(a_ips, cfg.port, cfg.timeout, cfg.concurrency, |ip, lat| {
|
||||
probed += 1;
|
||||
if let Some(d) = lat {
|
||||
a_hits.insert(ip, d);
|
||||
}
|
||||
on_progress(Progress::Probing { ip, ok: lat.is_some(), valid: a_hits.len(), probed });
|
||||
})
|
||||
.await;
|
||||
|
||||
let mut live_idx: HashSet<usize> = HashSet::new();
|
||||
for (idx, ip) in &a_samples {
|
||||
if let Some(d) = a_hits.get(ip) {
|
||||
live_idx.insert(*idx);
|
||||
collected.push(PingResult { ip: *ip, latency: *d });
|
||||
}
|
||||
}
|
||||
let live_nets: Vec<IpNet> = nets
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(idx, _)| live_idx.contains(idx))
|
||||
.map(|(_, net)| *net)
|
||||
.collect();
|
||||
|
||||
on_progress(Progress::PreScan {
|
||||
live: live_nets.len(),
|
||||
segments: nets.len(),
|
||||
valid: collected.len(),
|
||||
});
|
||||
|
||||
if live_nets.is_empty() {
|
||||
return Ok(finalize(collected, cfg.count));
|
||||
}
|
||||
|
||||
// Phase B: collect until we have `count` valid IPs (or run out of budget).
|
||||
while collected.len() < cfg.count && probed < cfg.max_probe {
|
||||
let want = cfg.count - collected.len();
|
||||
let budget = cfg.max_probe - probed;
|
||||
let batch_size = (want * 2).max(cfg.concurrency).min(budget);
|
||||
|
||||
let batch = {
|
||||
let mut rng = rand::thread_rng();
|
||||
let mut cand: Vec<IpAddr> = Vec::with_capacity(batch_size);
|
||||
let mut tries = 0;
|
||||
let max_tries = batch_size.saturating_mul(20).max(1);
|
||||
while cand.len() < batch_size && tries < max_tries {
|
||||
if let Some(net) = live_nets.choose(&mut rng) {
|
||||
let ip = random_host(net, &mut rng);
|
||||
if seen.insert(ip) {
|
||||
cand.push(ip);
|
||||
}
|
||||
}
|
||||
tries += 1;
|
||||
}
|
||||
cand
|
||||
};
|
||||
if batch.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
probe_each(batch, cfg.port, cfg.timeout, cfg.concurrency, |ip, lat| {
|
||||
probed += 1;
|
||||
let ok = lat.is_some();
|
||||
if let Some(d) = lat {
|
||||
collected.push(PingResult { ip, latency: d });
|
||||
}
|
||||
on_progress(Progress::Probing { ip, ok, valid: collected.len(), probed });
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(finalize(collected, cfg.count))
|
||||
}
|
||||
|
||||
/// Sort by latency ascending and keep the best `count`.
|
||||
fn finalize(mut results: Vec<PingResult>, count: usize) -> Vec<PingResult> {
|
||||
results.sort_by_key(|r| r.latency);
|
||||
results.truncate(count);
|
||||
results
|
||||
}
|
||||
|
||||
/// Probe a batch of IPs with bounded concurrency, invoking `on_each` as soon
|
||||
/// as every individual probe finishes (drives the live progress feed).
|
||||
async fn probe_each(
|
||||
ips: Vec<IpAddr>,
|
||||
port: u16,
|
||||
to: Duration,
|
||||
concurrency: usize,
|
||||
mut on_each: impl FnMut(IpAddr, Option<Duration>),
|
||||
) {
|
||||
let mut probes = stream::iter(
|
||||
ips.into_iter().map(|ip| async move { (ip, probe_one(ip, port, to).await) }),
|
||||
)
|
||||
.buffer_unordered(concurrency.max(1));
|
||||
while let Some((ip, lat)) = probes.next().await {
|
||||
on_each(ip, lat);
|
||||
}
|
||||
}
|
||||
|
||||
/// One TCP-connect probe. Returns the connect latency, or None if it failed
|
||||
/// or timed out.
|
||||
async fn probe_one(ip: IpAddr, port: u16, to: Duration) -> Option<Duration> {
|
||||
let addr = SocketAddr::new(ip, port);
|
||||
let started = Instant::now();
|
||||
match timeout(to, TcpStream::connect(addr)).await {
|
||||
Ok(Ok(_stream)) => Some(started.elapsed()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Pick a uniformly random usable host inside a CIDR segment.
|
||||
fn random_host(net: &IpNet, rng: &mut impl Rng) -> IpAddr {
|
||||
match net {
|
||||
IpNet::V4(n) => {
|
||||
let first = u32::from(n.network());
|
||||
let last = u32::from(n.broadcast());
|
||||
let (lo, hi) = if last - first >= 2 { (first + 1, last - 1) } else { (first, last) };
|
||||
IpAddr::V4(Ipv4Addr::from(rng.gen_range(lo..=hi)))
|
||||
}
|
||||
IpNet::V6(n) => {
|
||||
let first = u128::from(n.network());
|
||||
let last = u128::from(n.broadcast());
|
||||
let (lo, hi) = if last - first >= 2 { (first + 1, last - 1) } else { (first, last) };
|
||||
IpAddr::V6(Ipv6Addr::from(rng.gen_range(lo..=hi)))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user