Skip to content

Commit 6e8487c

Browse files
support comma-separated multi-range input + extend LCG iterator
Add full support for inputs like 1-100,200-300,5000-5100 by merging overlapping/adjacent ranges and feeding them into an extended LCG-based iterator. This keeps the original permutation guarantees while allowing arbitrary multi-range definitions. Also adds a serial mode for serial ordering. updates docs/tests.
1 parent 1b74455 commit 6e8487c

File tree

8 files changed

+386
-179
lines changed

8 files changed

+386
-179
lines changed

Cargo.lock

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ env_logger = "0.11.8"
2727
anstream = "=0.6.20"
2828
dirs = "6.0.0"
2929
gcd = "2.0.1"
30+
bit-set = "0.8"
3031
rand = "0.9.2"
3132
colorful = "0.3.2"
3233
ansi_term = "0.12.1"

src/input.rs

Lines changed: 48 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -27,35 +27,52 @@ pub enum ScriptsRequired {
2727
Custom,
2828
}
2929

30-
/// Represents the range of ports to be scanned.
30+
/// Represents the ranges of ports to be scanned Vec<(start:u16, end: u16)>
3131
#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
32-
pub struct PortRange {
33-
pub start: u16,
34-
pub end: u16,
35-
}
32+
pub struct PortRanges(pub Vec<(u16, u16)>);
3633

3734
#[cfg(not(tarpaulin_include))]
38-
fn parse_range(input: &str) -> Result<PortRange, String> {
39-
let range = input
40-
.split('-')
41-
.map(str::parse)
42-
.collect::<Result<Vec<u16>, std::num::ParseIntError>>();
43-
44-
if range.is_err() {
45-
return Err(String::from(
46-
"the range format must be 'start-end'. Example: 1-1000.",
47-
));
35+
/// Parse a single `start-end` token (e.g. "100-200") into `(start, end)`.
36+
/// Returns `None` when the token is malformed, cannot be parsed as `u16`,
37+
/// or `start > end`.
38+
fn parse_range(input: &str) -> Option<(u16, u16)> {
39+
let mut parts = input.trim().splitn(2, '-').map(str::trim);
40+
let a = parts.next()?;
41+
let b = parts.next()?;
42+
43+
let start = a.parse::<u16>().ok()?;
44+
let end = b.parse::<u16>().ok()?;
45+
46+
if start <= end {
47+
Some((start, end))
48+
} else {
49+
None
4850
}
49-
50-
match range.unwrap().as_slice() {
51-
[start, end] => Ok(PortRange {
52-
start: *start,
53-
end: *end,
54-
}),
55-
_ => Err(String::from(
56-
"the range format must be 'start-end'. Example: 1-1000.",
57-
)),
51+
}
52+
#[cfg(not(tarpaulin_include))]
53+
/// Parse a comma-separated list of `start-end` ranges into `PortRanges`.
54+
///
55+
/// Errors with a helpful message identifying the bad token.
56+
fn parse_ranges(input: &str) -> Result<PortRanges, String> {
57+
let s = input.trim();
58+
if s.is_empty() {
59+
return Err("empty input: expected one or more comma-separated 'start-end' pairs".into());
5860
}
61+
62+
let ranges_res: Result<Vec<(u16, u16)>, String> = s
63+
.split(',')
64+
.map(|token| {
65+
let t = token.trim();
66+
parse_range(t).ok_or_else(|| {
67+
format!(
68+
"invalid range token `{}` — expected `start-end` with 0 <= start <= end <= 65535",
69+
t
70+
)
71+
})
72+
})
73+
.collect();
74+
75+
ranges_res.map(PortRanges)
5976
}
6077

6178
#[derive(Parser, Debug, Clone)]
@@ -80,9 +97,9 @@ pub struct Opts {
8097
#[arg(short, long, value_delimiter = ',')]
8198
pub ports: Option<Vec<u16>>,
8299

83-
/// A range of ports with format start-end. Example: 1-1000.
84-
#[arg(short, long, conflicts_with = "ports", value_parser = parse_range)]
85-
pub range: Option<PortRange>,
100+
/// Ranges of ports with comma seperated start-end pairs. Example: 1-500,1000-2500,4000-7000
101+
#[arg(short, long, conflicts_with = "ports", value_parser = parse_ranges)]
102+
pub range: Option<PortRanges>,
86103

87104
/// Whether to ignore the configuration file or not.
88105
#[arg(short, long)]
@@ -169,10 +186,7 @@ impl Opts {
169186
let mut opts = Opts::parse();
170187

171188
if opts.ports.is_none() && opts.range.is_none() {
172-
opts.range = Some(PortRange {
173-
start: LOWEST_PORT_NUMBER,
174-
end: TOP_PORT_NUMBER,
175-
});
189+
opts.range = Some(PortRanges(vec![(LOWEST_PORT_NUMBER, TOP_PORT_NUMBER)]));
176190
}
177191

178192
opts
@@ -259,7 +273,7 @@ impl Default for Opts {
259273
pub struct Config {
260274
addresses: Option<Vec<String>>,
261275
ports: Option<Vec<u16>>,
262-
range: Option<PortRange>,
276+
range: Option<PortRanges>,
263277
greppable: Option<bool>,
264278
accessible: Option<bool>,
265279
batch_size: Option<usize>,
@@ -344,7 +358,7 @@ mod tests {
344358
use clap::{CommandFactory, Parser};
345359
use parameterized::parameterized;
346360

347-
use super::{Config, Opts, PortRange, ScanOrder, ScriptsRequired};
361+
use super::{Config, Opts, PortRanges, ScanOrder, ScriptsRequired};
348362

349363
impl Config {
350364
fn default() -> Self {
@@ -430,10 +444,7 @@ mod tests {
430444
fn opts_merge_optional_arguments() {
431445
let mut opts = Opts::default();
432446
let mut config = Config::default();
433-
config.range = Some(PortRange {
434-
start: 1,
435-
end: 1_000,
436-
});
447+
config.range = Some(PortRanges(vec![(1, 1_000)]));
437448
config.ulimit = Some(1_000);
438449
config.resolver = Some("1.1.1.1".to_owned());
439450

src/lib.rs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,14 @@
1111
//! use async_std::task::block_on;
1212
//! use std::{net::IpAddr, time::Duration};
1313
//!
14-
//! use rustscan::input::{PortRange, ScanOrder};
14+
//! use rustscan::input::{PortRanges, ScanOrder};
1515
//! use rustscan::port_strategy::PortStrategy;
1616
//! use rustscan::scanner::Scanner;
1717
//!
1818
//! fn main() {
1919
//! let addrs = vec!["127.0.0.1".parse::<IpAddr>().unwrap()];
20-
//! let range = PortRange {
21-
//! start: 1,
22-
//! end: 1_000,
23-
//! };
24-
//! let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random); // can be serial, random or manual https://github.com/RustScan/RustScan/blob/master/src/port_strategy/mod.rs
20+
//! let range = PortRanges(vec![(1, 1_000)]);
21+
//! let strategy = PortStrategy::pick(Some(range), None, ScanOrder::Random); // can be serial, random or manual https://github.com/RustScan/RustScan/blob/master/src/port_strategy/mod.rs
2522
//! let scanner = Scanner::new(
2623
//! &addrs, // the addresses to scan
2724
//! 10, // batch_size is how many ports at a time should be scanned

src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ fn main() {
8989
Duration::from_millis(opts.timeout.into()),
9090
opts.tries,
9191
opts.greppable,
92-
PortStrategy::pick(&opts.range, opts.ports, opts.scan_order),
92+
PortStrategy::pick(opts.range, opts.ports, opts.scan_order),
9393
opts.accessible,
9494
opts.exclude_ports.unwrap_or_default(),
9595
opts.udp,

src/port_strategy/mod.rs

Lines changed: 39 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Provides a means to hold configuration options specifically for port scanning.
22
mod range_iterator;
3-
use crate::input::{PortRange, ScanOrder};
3+
use crate::input::{PortRanges, ScanOrder};
44
use rand::rng;
55
use rand::seq::SliceRandom;
66
use range_iterator::RangeIterator;
@@ -17,20 +17,18 @@ pub enum PortStrategy {
1717
}
1818

1919
impl PortStrategy {
20-
pub fn pick(range: &Option<PortRange>, ports: Option<Vec<u16>>, order: ScanOrder) -> Self {
20+
pub fn pick(range: Option<PortRanges>, ports: Option<Vec<u16>>, order: ScanOrder) -> Self {
2121
match order {
2222
ScanOrder::Serial if ports.is_none() => {
23-
let range = range.as_ref().unwrap();
23+
let port_ranges = range.unwrap();
2424
PortStrategy::Serial(SerialRange {
25-
start: range.start,
26-
end: range.end,
25+
range: port_ranges.0,
2726
})
2827
}
2928
ScanOrder::Random if ports.is_none() => {
30-
let range = range.as_ref().unwrap();
29+
let port_ranges = range.unwrap();
3130
PortStrategy::Random(RandomRange {
32-
start: range.start,
33-
end: range.end,
31+
range: port_ranges.0,
3432
})
3533
}
3634
ScanOrder::Serial => PortStrategy::Manual(ports.unwrap()),
@@ -62,22 +60,20 @@ trait RangeOrder {
6260
/// ascending order.
6361
#[derive(Debug)]
6462
pub struct SerialRange {
65-
start: u16,
66-
end: u16,
63+
range: Vec<(u16, u16)>,
6764
}
6865

6966
impl RangeOrder for SerialRange {
7067
fn generate(&self) -> Vec<u16> {
71-
(self.start..=self.end).collect()
68+
RangeIterator::new_serial(&self.range).collect()
7269
}
7370
}
7471

7572
/// As the name implies RandomRange will always generate a vector with
7673
/// a random order. This vector is built following the LCG algorithm.
7774
#[derive(Debug)]
7875
pub struct RandomRange {
79-
start: u16,
80-
end: u16,
76+
range: Vec<(u16, u16)>,
8177
}
8278

8379
impl RangeOrder for RandomRange {
@@ -91,50 +87,64 @@ impl RangeOrder for RandomRange {
9187
// port numbers close to each other are pretty slim due to the way the
9288
// algorithm works.
9389
fn generate(&self) -> Vec<u16> {
94-
RangeIterator::new(self.start.into(), self.end.into()).collect()
90+
RangeIterator::new_random(&self.range).collect()
9591
}
9692
}
9793

9894
#[cfg(test)]
9995
mod tests {
10096
use super::PortStrategy;
101-
use crate::input::{PortRange, ScanOrder};
97+
use crate::input::{PortRanges, ScanOrder};
98+
use std::collections::HashSet;
10299

100+
fn expected_ports_from_ranges(input: &[(u16, u16)]) -> Vec<u16> {
101+
let mut s = HashSet::new();
102+
for &(start, end) in input {
103+
for p in start..=end {
104+
s.insert(p);
105+
}
106+
}
107+
let mut v: Vec<u16> = s.into_iter().collect();
108+
v.sort_unstable();
109+
v
110+
}
103111
#[test]
104112
fn serial_strategy_with_range() {
105-
let range = PortRange { start: 1, end: 100 };
106-
let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Serial);
113+
let ranges = PortRanges(vec![(1u16, 10u16), (20u16, 30u16), (100u16, 110u16)]);
114+
let strategy = PortStrategy::pick(Some(ranges.clone()), None, ScanOrder::Serial);
107115
let result = strategy.order();
108-
let expected_range = (1..=100).collect::<Vec<u16>>();
109-
assert_eq!(expected_range, result);
116+
let expected = expected_ports_from_ranges(&ranges.0);
117+
118+
assert_eq!(expected, result);
110119
}
111120
#[test]
112121
fn random_strategy_with_range() {
113-
let range = PortRange { start: 1, end: 100 };
114-
let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random);
122+
let ranges = PortRanges(vec![(1u16, 10u16), (20u16, 30u16), (100u16, 110u16)]);
123+
let strategy = PortStrategy::pick(Some(ranges.clone()), None, ScanOrder::Random);
115124
let mut result = strategy.order();
116-
let expected_range = (1..=100).collect::<Vec<u16>>();
117-
assert_ne!(expected_range, result);
125+
let expected = expected_ports_from_ranges(&ranges.0);
118126

127+
assert_ne!(expected, result);
119128
result.sort_unstable();
120-
assert_eq!(expected_range, result);
129+
130+
assert_eq!(expected, result);
121131
}
122132

123133
#[test]
124134
fn serial_strategy_with_ports() {
125-
let strategy = PortStrategy::pick(&None, Some(vec![80, 443]), ScanOrder::Serial);
135+
let strategy = PortStrategy::pick(None, Some(vec![80, 443]), ScanOrder::Serial);
126136
let result = strategy.order();
127137
assert_eq!(vec![80, 443], result);
128138
}
129139

130140
#[test]
131141
fn random_strategy_with_ports() {
132-
let strategy = PortStrategy::pick(&None, Some((1..10).collect()), ScanOrder::Random);
142+
let strategy = PortStrategy::pick(None, Some((1..10).collect()), ScanOrder::Random);
133143
let mut result = strategy.order();
134-
let expected_range = (1..10).collect::<Vec<u16>>();
135-
assert_ne!(expected_range, result);
144+
let expected = (1..10).collect::<Vec<u16>>();
145+
assert_ne!(expected, result);
136146

137147
result.sort_unstable();
138-
assert_eq!(expected_range, result);
148+
assert_eq!(expected, result);
139149
}
140150
}

0 commit comments

Comments
 (0)