1594 字
8 分钟
Rust 编写高效端口扫描器原型
最近在学习 Rust 相关开发,打算写一个端口扫描器原型练练手。
NOTE为什么是原型呢?因为这里只会介绍一下核心的内容,以个人思路为主,而且市面上已经有非常好用的端口扫描器工具了。
端口扫描器设计
扫描方式
主要还是围绕 SYN 半连接扫描和 TCP 全连接扫描的选择。
SYN半连接扫描- 原理: 发送
SYN包 -> 收到SYN/ACK(表示端口开放) -> 发送RST(中断连接)。 - 优点: 速度极快,隐蔽性较高(不完成三次握手,日志较少)。
- 缺点:
- 需要构建原始数据包 (
Raw Socket)。 - Rust 标准库不支持
Raw Socket,需依赖pnet或socket2等第三方库。 - 跨平台兼容性较差(Windows 上通常需要驱动支持)。
- 需要手动处理数据包过滤、重传和丢包逻辑,实现复杂度高,易漏报。
- 需要构建原始数据包 (
- 原理: 发送
TCP全连接扫描- 原理: 完成完整的三次握手 (
SYN->SYN/ACK->ACK),然后调用close()。 - 优点:
- 实现简单,利用操作系统提供的
connect系统调用。 - 不需要
Root/Administrator权限。 - Rust 拥有强大的异步运行时 (
Tokio/async-std),可以轻松支撑数万并发。
- 实现简单,利用操作系统提供的
- 缺点: 理论上比 SYN 扫描慢(多一次 RTT),且更容易被目标主机日志记录。
- 原理: 完成完整的三次握手 (
NOTE因为有 Rust 的异步并发支持,这里选择用
TCP全连接扫描。
程序概念设计
并发设计
为了提高扫描速度,这里选择 tokio 库进行异步并发。
#[tokio::main]async fn main() { // 初始化配置、日志等 // 启动扫描任务 scanner().await;}
async fn scanner() { // 扫描逻辑入口 println!("Starting the high-performance scanner…");}任务队列设计
因为是异步并发,为了在有限的内存资源下处理大量的扫描任务,这里稍微设计一下整体的架构。
我们先造一个任务池,叫做 task_list,这个 task_list 里有 1000 个空位。然后我们创建一个任务生成器 A,用来创建任务对象。大体流程就是:
A为每个IP + Port创建一个对象,创建 1000 个,塞进task_list里- 池子开始并发运行
- 突然,905 个任务运行完了,此处就空了出来,
task_list立刻通知 A 再造一个对象 - A 创造对象,并将对象塞进
task_list的第 905 空位里
TIP这里用
FuturesUnordered创建任务池,谁先完成,谁就先被取出。
let mut tasks = FuturesUnordered::new();let batch_size = 1000; // 池子大小
// 初始化,先把池子灌满for _ in 0..batch_size { task.push([这里是任务对象])}
// 然后开始捞结果while let Some(result) = tasks.next().await { [这里收集结果]; [把下一个 push 进去];}搓代码
Main 函数
先搓出来 main 函数
use std::net::{IpAddr, SocketAddr};use std::time::Duration;
#[tokio::main]async fn main() { // 初始化数据 // 这里用 unwrap 是因为 IP 是硬编码的,如果这都挂了,说明写代码的人脑子进水了 // 如果实际工具你要是敢用 unwrap,掐死你! let host = "127.0.0.1".parse::<IpAddr>().unwrap(); let posts: Vec<u16> = (1..=1000).collect(); // 测试端口
// 初始化扫描器 // 扫描器应该是一个单独的对象 // 结构体属性有这些 // - address(IpAddr): 这是要目标地址 // - posts(Vec<u16>): 这是要处理的目标端口,这里同理,也应该是引用 // - batch_size(usize): 因为我们设计了一个任务池,这里要给一个池子大小 // - timeout: 扫描器进行扫描的超时时间 let scanner = Scanner::new( host, posts, 1000, // 池子大小设置为 1000 Duration::from_millis(1500), // 超时设置为 1.5 );
// 我们现在拿到了一个扫描器对象,这个对象应该自己有一些异步方法 // 我们创建一个 run 方法,这个 run 方法会返回所有开放的 IP:Port,也就是 Vec<SocketAddr> let results = scanner.run().await;
// 现在我们拿到了 Vec<SocketAddr>,直接遍历取出就行 for socket in results { println!("{}", socket); }}
struct Scanner { host: IpAddr, ports: Vec<u16>, batch_size: usize, timeout: Duration,}
impl Scanner { fn new(host: IpAddr, ports: Vec<u16>, batch_size: usize, timeout: Duration) -> Self { Self { host, ports, batch_size, timeout, } }
async fn run(&self) -> Vec<SocketAddr> { let result: Vec<SocketAddr> = Vec::new();
result }}Run 方法
async fn run(&self) -> Vec<SocketAddr> { let mut result: Vec<SocketAddr> = Vec::new(); // 创建一个空的缓冲区,放最后的结果 let mut tasks = FuturesUnordered::new(); // 创建任务池 let mut port_iterator = self.ports.iter(); // 创建一个 ports 迭代器
// 进行预填充,先放 1000 个任务对象 for _ in 0..self.batch_size { // 迭代器的 next() 方法,返回的是一个 Some(T) if let Some(&port) = port_iterator.next() { let timeout = self.timeout; // 超时时间 // 拿到 port 后,构建 socket 对象(ScoketAddr) let socket = SocketAddr::new(self.host, port); // 将动作封装成一个动作包(scan_worker),push进 task 里 tasks.push(Scanner::scan_worker(socket, timeout)); } else { // 如果端口还没填满 batch_size 就没了,直接跳出 break; } }
// 执行,跑完一个,造一个,放进去一个 // tasks.next().await 会等待任意一个 future 完成 // scan_result 是 Option<SocketAddr> 类型 while let Some(scan_result) = tasks.next().await { // 把 open_socket 给拿出来 if let Some(open_socket) = scan_result { // 这就是结果了,直接 push 进 result 里 result.push(open_socket); }
// 完成了一个,task 就有了一个空位,从迭代器里生成一个放进去 if let Some(&port) = port_iterator.next() { let socket = SocketAddr::new(self.host, port); let timeout = self.timeout; tasks.push(Scanner::scan_worker(socket, timeout)); } }
result }scan_worker 方法
async fn scan_worker(socket: SocketAddr, timeout: Duration) -> Option<SocketAddr> { if Scanner::socket_scan(socket, timeout).await { Some(socket) } else { None } }socket_scan 方法
// 端口扫描的核心组件 async fn socket_scan(socket: SocketAddr, timeout: Duration) -> bool { // tokio::time::timeout 接收一个 Duration 和一个 Future // 它会把 Future 包一层,如果超时了就返回 Err(Elapsed) match tokio::time::timeout(timeout, TcpStream::connect(socket)).await { Ok(Ok(_)) => true, _ => false, } }完整代码
use futures::stream::FuturesUnordered;use futures::StreamExt;use std::net::{IpAddr, SocketAddr};use std::time::Duration;use tokio::net::TcpStream;
#[tokio::main]async fn main() { let host = "127.0.0.1".parse::<IpAddr>().unwrap(); let posts: Vec<u16> = (1..=65535).collect();
let scanner = Scanner::new(host, posts, 1000, Duration::from_millis(1500));
let results = scanner.run().await;
for socket in results { println!("{}", socket); }}
struct Scanner { host: IpAddr, ports: Vec<u16>, batch_size: usize, timeout: Duration,}
impl Scanner { fn new(host: IpAddr, ports: Vec<u16>, batch_size: usize, timeout: Duration) -> Self { Self { host, ports, batch_size, timeout, } }
async fn scan_worker(socket: SocketAddr, timeout: Duration) -> Option<SocketAddr> { if Scanner::socket_scan(socket, timeout).await { Some(socket) } else { None } }
async fn run(&self) -> Vec<SocketAddr> { let mut result: Vec<SocketAddr> = Vec::new(); let mut tasks = FuturesUnordered::new(); let mut port_iterator = self.ports.iter();
for _ in 0..self.batch_size { if let Some(&port) = port_iterator.next() { let timeout = self.timeout;
let socket = SocketAddr::new(self.host, port);
tasks.push(Scanner::scan_worker(socket, timeout)); } else { break; } }
while let Some(scan_result) = tasks.next().await { if let Some(open_socket) = scan_result { result.push(open_socket); }
if let Some(&port) = port_iterator.next() { let socket = SocketAddr::new(self.host, port); let timeout = self.timeout; tasks.push(Scanner::scan_worker(socket, timeout)); } }
result }
async fn socket_scan(socket: SocketAddr, timeout: Duration) -> bool { match tokio::time::timeout(timeout, TcpStream::connect(socket)).await { Ok(Ok(_)) => true, _ => false, } }}部分信息可能已经过时