Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
255 changes: 113 additions & 142 deletions crates/prtip-core/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,20 @@ impl Iterator for PortRangeIterator {
}

/// State of a scanned port
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Archive, RkyvSerialize, RkyvDeserialize)]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Serialize,
Deserialize,
Archive,
RkyvSerialize,
RkyvDeserialize,
)]
#[rkyv(derive(Debug))]
pub enum PortState {
/// Port is open and accepting connections
Expand Down Expand Up @@ -477,29 +490,85 @@ impl fmt::Display for TimingTemplate {
}
}

/// Serializable representation of ScanResult for rkyv
#[derive(Debug, Clone, Archive, RkyvSerialize, RkyvDeserialize)]
/// rkyv-compatible serialization format for ScanResult
///
/// This type is optimized for zero-copy deserialization using rkyv.
/// It stores all data in a format that can be directly interpreted from
/// memory-mapped files without allocation.
///
/// # Manual Serialization for IpAddr
///
/// std::net::IpAddr does not implement rkyv traits, so we manually serialize
/// to bytes and convert between IpAddr and byte arrays in the From implementations.
///
/// # Alignment Requirements
///
/// This structure must maintain proper alignment for rkyv's zero-copy
/// deserialization. The fixed-size entry buffer (512 bytes) provides
/// adequate alignment for typical rkyv requirements.
#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
#[rkyv(derive(Debug))]
pub struct ScanResultRkyv {
target_ip: IpAddr,
port: u16,
state: PortState,
response_time_nanos: u128,
timestamp_nanos: i64,
banner: Option<String>,
service: Option<String>,
version: Option<String>,
raw_response: Option<Vec<u8>>,
/// Target IP address (16 bytes for IPv6 compatibility)
pub target_ip_bytes: [u8; 16],
/// Whether the IP is IPv4 (true) or IPv6 (false)
pub is_ipv4: bool,
/// Port number
pub port: u16,
/// Port state as u8 (Open=0, Closed=1, Filtered=2, Unknown=3)
pub state: u8,
/// Response time in nanoseconds (u64 to avoid truncation)
pub response_time_nanos: u64,
/// Timestamp in nanoseconds since Unix epoch
pub timestamp_nanos: i64,
/// Optional banner (max 128 bytes)
pub banner: Option<String>,
/// Optional service name (max 32 bytes)
pub service: Option<String>,
/// Optional service version (max 64 bytes)
pub version: Option<String>,
/// Optional raw response (limited to 256 bytes to fit in entry)
pub raw_response: Option<Vec<u8>>,
}

impl From<&ScanResult> for ScanResultRkyv {
fn from(result: &ScanResult) -> Self {
// Convert IpAddr to bytes
let (target_ip_bytes, is_ipv4) = match result.target_ip {
IpAddr::V4(ipv4) => {
let mut bytes = [0u8; 16];
bytes[..4].copy_from_slice(&ipv4.octets());
(bytes, true)
}
IpAddr::V6(ipv6) => (ipv6.octets(), false),
};

// Convert PortState to u8
let state = match result.state {
PortState::Open => 0,
PortState::Closed => 1,
PortState::Filtered => 2,
PortState::Unknown => 3,
};

// Convert response time to u64 nanoseconds (avoid truncation issues)
// Note: u64 can represent up to ~584 years, which is more than sufficient
// for network response times. We clamp to u64::MAX to avoid overflow.
let response_time_nanos = result.response_time.as_nanos().min(u64::MAX as u128) as u64;

// Convert timestamp with proper error handling
let timestamp_nanos = result
.timestamp
.timestamp_nanos_opt()
.expect("timestamp out of range for nanosecond representation");

Self {
target_ip: result.target_ip,
target_ip_bytes,
is_ipv4,
port: result.port,
state: result.state,
response_time_nanos: result.response_time.as_nanos(),
timestamp_nanos: result.timestamp.timestamp_nanos_opt().unwrap_or(0),
state,
response_time_nanos,
timestamp_nanos,
banner: result.banner.clone(),
service: result.service.clone(),
version: result.version.clone(),
Expand All @@ -510,12 +579,36 @@ impl From<&ScanResult> for ScanResultRkyv {

impl From<ScanResultRkyv> for ScanResult {
fn from(rkyv: ScanResultRkyv) -> Self {
// Convert bytes back to IpAddr
let target_ip = if rkyv.is_ipv4 {
let mut octets = [0u8; 4];
octets.copy_from_slice(&rkyv.target_ip_bytes[..4]);
IpAddr::V4(std::net::Ipv4Addr::from(octets))
} else {
IpAddr::V6(std::net::Ipv6Addr::from(rkyv.target_ip_bytes))
};

// Convert u8 back to PortState
let state = match rkyv.state {
0 => PortState::Open,
1 => PortState::Closed,
2 => PortState::Filtered,
_ => PortState::Unknown,
};

// Convert u64 nanoseconds back to Duration
// Safe: u64::MAX nanoseconds fits within Duration's range
let response_time = Duration::from_nanos(rkyv.response_time_nanos);

// Convert i64 nanoseconds back to DateTime
let timestamp = DateTime::from_timestamp_nanos(rkyv.timestamp_nanos);

Self {
target_ip: rkyv.target_ip,
target_ip,
port: rkyv.port,
state: rkyv.state,
response_time: Duration::from_nanos(rkyv.response_time_nanos as u64),
timestamp: DateTime::from_timestamp_nanos(rkyv.timestamp_nanos),
state,
response_time,
timestamp,
banner: rkyv.banner,
service: rkyv.service,
version: rkyv.version,
Expand Down Expand Up @@ -641,128 +734,6 @@ impl fmt::Display for ScanResult {
}
}

/// rkyv-compatible serialization format for ScanResult
///
/// This type is optimized for zero-copy deserialization using rkyv.
/// It stores all data in a format that can be directly interpreted from
/// memory-mapped files without allocation.
///
/// # Alignment Requirements
///
/// This structure must maintain proper alignment for rkyv's zero-copy
/// deserialization. The fixed-size entry buffer (512 bytes) provides
/// adequate alignment for typical rkyv requirements.
#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
#[rkyv(derive(Debug))]
pub struct ScanResultRkyv {
/// Target IP address (16 bytes for IPv6 compatibility)
pub target_ip_bytes: [u8; 16],
/// Whether the IP is IPv4 (true) or IPv6 (false)
pub is_ipv4: bool,
/// Port number
pub port: u16,
/// Port state as u8 (Open=0, Closed=1, Filtered=2, Unknown=3)
pub state: u8,
/// Response time in nanoseconds (u64 to avoid truncation)
pub response_time_nanos: u64,
/// Timestamp in nanoseconds since Unix epoch
pub timestamp_nanos: i64,
/// Optional banner (max 128 bytes)
pub banner: Option<String>,
/// Optional service name (max 32 bytes)
pub service: Option<String>,
/// Optional service version (max 64 bytes)
pub version: Option<String>,
/// Optional raw response (limited to 256 bytes to fit in entry)
pub raw_response: Option<Vec<u8>>,
}

impl From<&ScanResult> for ScanResultRkyv {
fn from(result: &ScanResult) -> Self {
// Convert IpAddr to bytes
let (target_ip_bytes, is_ipv4) = match result.target_ip {
IpAddr::V4(ipv4) => {
let mut bytes = [0u8; 16];
bytes[..4].copy_from_slice(&ipv4.octets());
(bytes, true)
}
IpAddr::V6(ipv6) => (ipv6.octets(), false),
};

// Convert PortState to u8
let state = match result.state {
PortState::Open => 0,
PortState::Closed => 1,
PortState::Filtered => 2,
PortState::Unknown => 3,
};

// Convert response time to u64 nanoseconds (avoid truncation issues)
// Note: u64 can represent up to ~584 years, which is more than sufficient
// for network response times. We clamp to u64::MAX to avoid overflow.
let response_time_nanos = result.response_time.as_nanos().min(u64::MAX as u128) as u64;

// Convert timestamp with proper error handling
let timestamp_nanos = result
.timestamp
.timestamp_nanos_opt()
.expect("timestamp out of range for nanosecond representation");

Self {
target_ip_bytes,
is_ipv4,
port: result.port,
state,
response_time_nanos,
timestamp_nanos,
banner: result.banner.clone(),
service: result.service.clone(),
version: result.version.clone(),
raw_response: result.raw_response.clone(),
}
}
}

impl From<ScanResultRkyv> for ScanResult {
fn from(rkyv: ScanResultRkyv) -> Self {
// Convert bytes back to IpAddr
let target_ip = if rkyv.is_ipv4 {
let mut octets = [0u8; 4];
octets.copy_from_slice(&rkyv.target_ip_bytes[..4]);
IpAddr::V4(std::net::Ipv4Addr::from(octets))
} else {
IpAddr::V6(std::net::Ipv6Addr::from(rkyv.target_ip_bytes))
};

// Convert u8 back to PortState
let state = match rkyv.state {
0 => PortState::Open,
1 => PortState::Closed,
2 => PortState::Filtered,
_ => PortState::Unknown,
};

// Convert u64 nanoseconds back to Duration
// Safe: u64::MAX nanoseconds fits within Duration's range
let response_time = Duration::from_nanos(rkyv.response_time_nanos);

// Convert i64 nanoseconds back to DateTime
let timestamp = DateTime::from_timestamp_nanos(rkyv.timestamp_nanos);

Self {
target_ip,
port: rkyv.port,
state,
response_time,
timestamp,
banner: rkyv.banner,
service: rkyv.service,
version: rkyv.version,
raw_response: rkyv.raw_response,
}
}
}

/// Port filtering for exclusion/inclusion lists
///
/// Provides efficient port filtering using hash sets for O(1) lookups.
Expand Down
24 changes: 5 additions & 19 deletions crates/prtip-scanner/src/output/mmap_reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,33 +87,19 @@ impl MmapResultReader {
}

let offset = HEADER_SIZE + (index * self.entry_size);

// Read length prefix (8 bytes)
let len_bytes: [u8; 8] = self.mmap[offset..offset + LENGTH_PREFIX_SIZE].try_into().ok()?;
let len_bytes: [u8; 8] = self.mmap[offset..offset + LENGTH_PREFIX_SIZE]
.try_into()
.ok()?;
let len = u64::from_le_bytes(len_bytes) as usize;

if len == 0 || len + LENGTH_PREFIX_SIZE > self.entry_size {
return None;
}

// Read length prefix (u64 in little-endian)
let len = u64::from_le_bytes(
entry_bytes[..LENGTH_PREFIX_SIZE]
.try_into()
.expect("LENGTH_PREFIX_SIZE is 8 bytes"),
) as usize;

// Validate length
if len == 0 || len + LENGTH_PREFIX_SIZE > self.entry_size {
eprintln!(
"MmapResultReader: invalid entry length {} at index {}",
len, index
);
return None;
}

// Use zero-copy deserialization without unnecessary allocation
let data_bytes = &entry_bytes[LENGTH_PREFIX_SIZE..LENGTH_PREFIX_SIZE + len];
let data_bytes = &self.mmap[offset + LENGTH_PREFIX_SIZE..offset + LENGTH_PREFIX_SIZE + len];
match rkyv::from_bytes::<ScanResultRkyv, rkyv::rancor::Error>(data_bytes) {
Ok(rkyv_result) => Some(ScanResult::from(rkyv_result)),
Err(e) => {
Expand Down
2 changes: 1 addition & 1 deletion docs/archive/19-PHASE4-ENHANCEMENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1584,7 +1584,7 @@ Create comprehensive usage examples library, common scenarios guide, update all
- Insights: Performance comparison, use case recommendations

2. ~~**"Top Network Scanners Compared: Nmap, Masscan, ZMap, and More"** (findsec.org)~~
- ~~URL: https://findsec.org/index.php/blog/493-nmap-vs-masscan-zmap-rustscan-comparison~~ (Link no longer accessible)
- URL: (Link no longer accessible - findsec.org/index.php/blog/493-nmap-vs-masscan-zmap-rustscan-comparison)
- Insights: Feature matrix, speed benchmarks, tool selection guide

3. **"01/31/2025 – masscan vs nmap Scan"** (victsao.wordpress.com)
Expand Down