diff --git a/crates/stackforge-core/src/layer/mod.rs b/crates/stackforge-core/src/layer/mod.rs index acfb849..d29d6ab 100644 --- a/crates/stackforge-core/src/layer/mod.rs +++ b/crates/stackforge-core/src/layer/mod.rs @@ -9,6 +9,7 @@ pub mod ethernet; pub mod field; pub mod ipv4; pub mod neighbor; +pub mod tcp; use std::ops::Range; @@ -19,6 +20,11 @@ pub use ethernet::{Dot3Builder, Dot3Layer, EthernetBuilder, EthernetLayer}; pub use field::{BytesField, Field, FieldDesc, FieldError, FieldType, FieldValue, MacAddress}; pub use ipv4::{Ipv4Builder, Ipv4Flags, Ipv4Layer, Ipv4Options, Ipv4Route}; pub use neighbor::{NeighborCache, NeighborResolver}; +pub use tcp::{ + TCP_FIELDS, TCP_MAX_HEADER_LEN, TCP_MIN_HEADER_LEN, TCP_SERVICES, TcpAoValue, TcpBuilder, + TcpFlags, TcpLayer, TcpOption, TcpOptionKind, TcpOptions, TcpOptionsBuilder, TcpSackBlock, + TcpTimestamp, service_name, service_port, tcp_checksum, tcp_checksum_ipv4, verify_tcp_checksum, +}; /// Identifies the type of network protocol layer. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -73,7 +79,7 @@ impl LayerKind { Self::Ipv4 => ipv4::IPV4_MIN_HEADER_LEN, Self::Ipv6 => 40, Self::Icmp | Self::Icmpv6 => 8, - Self::Tcp => 20, + Self::Tcp => tcp::TCP_MIN_HEADER_LEN, Self::Udp => 8, Self::Dns => 12, Self::Dot1Q => 4, @@ -264,6 +270,7 @@ impl LayerEnum { Self::Ethernet(l) => l.hashret(buf), Self::Arp(l) => l.hashret(buf), Self::Ipv4(l) => l.hashret(buf), + Self::Tcp(l) => l.hashret(buf), _ => vec![], } } @@ -334,31 +341,7 @@ impl Icmpv6Layer { } } -#[derive(Debug, Clone)] -pub struct TcpLayer { - pub index: LayerIndex, -} - -impl TcpLayer { - pub fn summary(&self, buf: &[u8]) -> String { - let slice = self.index.slice(buf); - if slice.len() >= 4 { - let src_port = u16::from_be_bytes([slice[0], slice[1]]); - let dst_port = u16::from_be_bytes([slice[2], slice[3]]); - format!("TCP {} > {}", src_port, dst_port) - } else { - "TCP".to_string() - } - } - pub fn header_len(&self, buf: &[u8]) -> usize { - let slice = self.index.slice(buf); - if slice.len() >= 13 { - ((slice[12] >> 4) as usize) * 4 - } else { - 20 - } - } -} +// TcpLayer is now imported from the tcp module #[derive(Debug, Clone)] pub struct UdpLayer { diff --git a/crates/stackforge-core/src/layer/tcp/builder.rs b/crates/stackforge-core/src/layer/tcp/builder.rs new file mode 100644 index 0000000..4e4c078 --- /dev/null +++ b/crates/stackforge-core/src/layer/tcp/builder.rs @@ -0,0 +1,831 @@ +//! TCP packet builder. +//! +//! Provides a fluent API for constructing TCP packets with automatic +//! field calculation (checksum, data offset). +//! +//! # Example +//! +//! ```rust +//! use stackforge_core::layer::tcp::{TcpBuilder, TcpFlags}; +//! use std::net::Ipv4Addr; +//! +//! // Build a SYN packet +//! let packet = TcpBuilder::new() +//! .src_port(12345) +//! .dst_port(80) +//! .seq(1000) +//! .syn() +//! .window(65535) +//! .mss(1460) +//! .wscale(7) +//! .sack_ok() +//! .build(); +//! ``` + +use std::net::{Ipv4Addr, Ipv6Addr}; + +use super::checksum::{tcp_checksum_ipv4, tcp_checksum_ipv6}; +use super::flags::TcpFlags; +use super::header::{TCP_MIN_HEADER_LEN, TcpLayer, offsets}; +use super::options::{TcpAoValue, TcpOption, TcpOptions, TcpOptionsBuilder, TcpSackBlock}; +use crate::layer::field::FieldError; + +/// Builder for TCP packets. +#[derive(Debug, Clone)] +pub struct TcpBuilder { + // Header fields + src_port: u16, + dst_port: u16, + seq: u32, + ack: u32, + data_offset: Option, + reserved: u8, + flags: TcpFlags, + window: u16, + checksum: Option, + urgent_ptr: u16, + + // Options + options: TcpOptions, + + // Payload + payload: Vec, + + // Build options + auto_checksum: bool, + auto_data_offset: bool, + + // IP addresses for checksum calculation + src_ip: Option, + dst_ip: Option, +} + +/// IP address enum for checksum calculation. +#[derive(Debug, Clone, Copy)] +pub enum IpAddr { + V4(Ipv4Addr), + V6(Ipv6Addr), +} + +impl From for IpAddr { + fn from(addr: Ipv4Addr) -> Self { + IpAddr::V4(addr) + } +} + +impl From for IpAddr { + fn from(addr: Ipv6Addr) -> Self { + IpAddr::V6(addr) + } +} + +impl Default for TcpBuilder { + fn default() -> Self { + Self { + src_port: 20, + dst_port: 80, + seq: 0, + ack: 0, + data_offset: None, + reserved: 0, + flags: TcpFlags::from_u16(0x02), // SYN + window: 8192, + checksum: None, + urgent_ptr: 0, + options: TcpOptions::new(), + payload: Vec::new(), + auto_checksum: true, + auto_data_offset: true, + src_ip: None, + dst_ip: None, + } + } +} + +impl TcpBuilder { + /// Create a new TCP builder with default values. + pub fn new() -> Self { + Self::default() + } + + /// Create a builder initialized from an existing packet. + pub fn from_bytes(data: &[u8]) -> Result { + let layer = TcpLayer::at_offset_dynamic(data, 0)?; + + let mut builder = Self::new(); + builder.src_port = layer.src_port(data)?; + builder.dst_port = layer.dst_port(data)?; + builder.seq = layer.seq(data)?; + builder.ack = layer.ack(data)?; + builder.data_offset = Some(layer.data_offset(data)?); + builder.reserved = layer.reserved(data)?; + builder.flags = layer.flags(data)?; + builder.window = layer.window(data)?; + builder.checksum = Some(layer.checksum(data)?); + builder.urgent_ptr = layer.urgent_ptr(data)?; + + // Parse options if present + if layer.options_len(data) > 0 { + builder.options = layer.options(data)?; + } + + // Copy payload + let header_len = layer.calculate_header_len(data); + if data.len() > header_len { + builder.payload = data[header_len..].to_vec(); + } + + // Disable auto-calculation since we're copying exact values + builder.auto_checksum = false; + builder.auto_data_offset = false; + + Ok(builder) + } + + // ========== Header Field Setters ========== + + /// Set the source port. + pub fn src_port(mut self, port: u16) -> Self { + self.src_port = port; + self + } + + /// Alias for src_port (Scapy compatibility). + pub fn sport(self, port: u16) -> Self { + self.src_port(port) + } + + /// Set the destination port. + pub fn dst_port(mut self, port: u16) -> Self { + self.dst_port = port; + self + } + + /// Alias for dst_port (Scapy compatibility). + pub fn dport(self, port: u16) -> Self { + self.dst_port(port) + } + + /// Set the sequence number. + pub fn seq(mut self, seq: u32) -> Self { + self.seq = seq; + self + } + + /// Set the acknowledgment number. + pub fn ack_num(mut self, ack: u32) -> Self { + self.ack = ack; + self + } + + /// Set the data offset (in 32-bit words). + pub fn data_offset(mut self, offset: u8) -> Self { + self.data_offset = Some(offset); + self.auto_data_offset = false; + self + } + + /// Alias for data_offset (Scapy compatibility). + pub fn dataofs(self, offset: u8) -> Self { + self.data_offset(offset) + } + + /// Set the reserved bits. + pub fn reserved(mut self, reserved: u8) -> Self { + self.reserved = reserved & 0x07; + self + } + + /// Set the flags. + pub fn flags(mut self, flags: TcpFlags) -> Self { + self.flags = flags; + self + } + + /// Set flags from a string like "S", "SA", "FA", etc. + pub fn flags_str(mut self, s: &str) -> Self { + self.flags = TcpFlags::from_str(s); + self + } + + /// Set the SYN flag. + pub fn syn(mut self) -> Self { + self.flags.syn = true; + self + } + + /// Set the ACK flag. + pub fn ack(mut self) -> Self { + self.flags.ack = true; + self + } + + /// Set the FIN flag. + pub fn fin(mut self) -> Self { + self.flags.fin = true; + self + } + + /// Set the RST flag. + pub fn rst(mut self) -> Self { + self.flags.rst = true; + self + } + + /// Set the PSH flag. + pub fn psh(mut self) -> Self { + self.flags.psh = true; + self + } + + /// Set the URG flag. + pub fn urg(mut self) -> Self { + self.flags.urg = true; + self + } + + /// Set the ECE flag. + pub fn ece(mut self) -> Self { + self.flags.ece = true; + self + } + + /// Set the CWR flag. + pub fn cwr(mut self) -> Self { + self.flags.cwr = true; + self + } + + /// Set the NS flag. + pub fn ns(mut self) -> Self { + self.flags.ns = true; + self + } + + /// Set SYN+ACK flags. + pub fn syn_ack(mut self) -> Self { + self.flags.syn = true; + self.flags.ack = true; + self + } + + /// Set FIN+ACK flags. + pub fn fin_ack(mut self) -> Self { + self.flags.fin = true; + self.flags.ack = true; + self + } + + /// Set PSH+ACK flags. + pub fn psh_ack(mut self) -> Self { + self.flags.psh = true; + self.flags.ack = true; + self + } + + /// Set the window size. + pub fn window(mut self, window: u16) -> Self { + self.window = window; + self + } + + /// Set the checksum manually. + pub fn checksum(mut self, checksum: u16) -> Self { + self.checksum = Some(checksum); + self.auto_checksum = false; + self + } + + /// Alias for checksum (Scapy compatibility). + pub fn chksum(self, checksum: u16) -> Self { + self.checksum(checksum) + } + + /// Set the urgent pointer. + pub fn urgent_ptr(mut self, urgptr: u16) -> Self { + self.urgent_ptr = urgptr; + self + } + + /// Alias for urgent_ptr (Scapy compatibility). + pub fn urgptr(self, urgptr: u16) -> Self { + self.urgent_ptr(urgptr) + } + + // ========== IP Address Setters (for checksum) ========== + + /// Set the source IP address (for checksum calculation). + pub fn src_ip>(mut self, ip: T) -> Self { + self.src_ip = Some(ip.into()); + self + } + + /// Set the destination IP address (for checksum calculation). + pub fn dst_ip>(mut self, ip: T) -> Self { + self.dst_ip = Some(ip.into()); + self + } + + /// Set both source and destination IPv4 addresses. + pub fn ipv4_addrs(mut self, src: Ipv4Addr, dst: Ipv4Addr) -> Self { + self.src_ip = Some(IpAddr::V4(src)); + self.dst_ip = Some(IpAddr::V4(dst)); + self + } + + /// Set both source and destination IPv6 addresses. + pub fn ipv6_addrs(mut self, src: Ipv6Addr, dst: Ipv6Addr) -> Self { + self.src_ip = Some(IpAddr::V6(src)); + self.dst_ip = Some(IpAddr::V6(dst)); + self + } + + // ========== Options ========== + + /// Set the options. + pub fn options(mut self, options: TcpOptions) -> Self { + self.options = options; + self + } + + /// Add a single option. + pub fn option(mut self, option: TcpOption) -> Self { + self.options.push(option); + self + } + + /// Add options using a builder function. + pub fn with_options(mut self, f: F) -> Self + where + F: FnOnce(TcpOptionsBuilder) -> TcpOptionsBuilder, + { + self.options = f(TcpOptionsBuilder::new()).build(); + self + } + + /// Add an MSS (Maximum Segment Size) option. + pub fn mss(mut self, mss: u16) -> Self { + self.options.push(TcpOption::Mss(mss)); + self + } + + /// Add a Window Scale option. + pub fn wscale(mut self, scale: u8) -> Self { + self.options.push(TcpOption::WScale(scale)); + self + } + + /// Add SACK Permitted option. + pub fn sack_ok(mut self) -> Self { + self.options.push(TcpOption::SackOk); + self + } + + /// Add a SACK option with blocks. + pub fn sack(mut self, blocks: Vec) -> Self { + self.options.push(TcpOption::Sack(blocks)); + self + } + + /// Add a Timestamp option. + pub fn timestamp(mut self, ts_val: u32, ts_ecr: u32) -> Self { + self.options.push(TcpOption::timestamp(ts_val, ts_ecr)); + self + } + + /// Add a NOP (padding) option. + pub fn nop(mut self) -> Self { + self.options.push(TcpOption::Nop); + self + } + + /// Add an EOL (End of Options) option. + pub fn eol(mut self) -> Self { + self.options.push(TcpOption::Eol); + self + } + + /// Add a TFO (TCP Fast Open) option. + pub fn tfo(mut self, cookie: Option>) -> Self { + self.options.push(TcpOption::Tfo { cookie }); + self + } + + /// Add an MD5 signature option. + pub fn md5(mut self, signature: [u8; 16]) -> Self { + self.options.push(TcpOption::Md5(signature)); + self + } + + /// Add an Authentication Option (TCP-AO). + pub fn ao(mut self, key_id: u8, rnext_key_id: u8, mac: Vec) -> Self { + self.options + .push(TcpOption::Ao(TcpAoValue::new(key_id, rnext_key_id, mac))); + self + } + + // ========== Payload ========== + + /// Set the payload data. + pub fn payload(mut self, payload: impl Into>) -> Self { + self.payload = payload.into(); + self + } + + /// Append data to the payload. + pub fn append_payload(mut self, data: &[u8]) -> Self { + self.payload.extend_from_slice(data); + self + } + + // ========== Build Options ========== + + /// Enable or disable automatic checksum calculation. + pub fn auto_checksum(mut self, enabled: bool) -> Self { + self.auto_checksum = enabled; + self + } + + /// Enable or disable automatic data offset calculation. + pub fn auto_data_offset(mut self, enabled: bool) -> Self { + self.auto_data_offset = enabled; + self + } + + // ========== Build Methods ========== + + /// Calculate the header size (including options, with padding). + pub fn header_size(&self) -> usize { + if let Some(doff) = self.data_offset { + (doff as usize) * 4 + } else { + let opts_len = self.options.padded_len(); + TCP_MIN_HEADER_LEN + opts_len + } + } + + /// Calculate the total packet size. + pub fn packet_size(&self) -> usize { + self.header_size() + self.payload.len() + } + + /// Build the TCP packet. + pub fn build(&self) -> Vec { + let total_size = self.packet_size(); + let mut buf = vec![0u8; total_size]; + self.build_into(&mut buf) + .expect("buffer is correctly sized"); + buf + } + + /// Build the TCP packet into an existing buffer. + pub fn build_into(&self, buf: &mut [u8]) -> Result { + let header_size = self.header_size(); + let total_size = self.packet_size(); + + if buf.len() < total_size { + return Err(FieldError::BufferTooShort { + offset: 0, + need: total_size, + have: buf.len(), + }); + } + + // Calculate data offset + let data_offset = if self.auto_data_offset { + (header_size / 4) as u8 + } else { + self.data_offset.unwrap_or(5) + }; + + // Source Port + buf[offsets::SRC_PORT] = (self.src_port >> 8) as u8; + buf[offsets::SRC_PORT + 1] = (self.src_port & 0xFF) as u8; + + // Destination Port + buf[offsets::DST_PORT] = (self.dst_port >> 8) as u8; + buf[offsets::DST_PORT + 1] = (self.dst_port & 0xFF) as u8; + + // Sequence Number + buf[offsets::SEQ..offsets::SEQ + 4].copy_from_slice(&self.seq.to_be_bytes()); + + // Acknowledgment Number + buf[offsets::ACK..offsets::ACK + 4].copy_from_slice(&self.ack.to_be_bytes()); + + // Data Offset + Reserved + NS flag + buf[offsets::DATA_OFFSET] = + ((data_offset & 0x0F) << 4) | ((self.reserved & 0x07) << 1) | self.flags.ns_bit(); + + // Flags (without NS) + buf[offsets::FLAGS] = self.flags.to_byte(); + + // Window + buf[offsets::WINDOW] = (self.window >> 8) as u8; + buf[offsets::WINDOW + 1] = (self.window & 0xFF) as u8; + + // Checksum (initially 0) + buf[offsets::CHECKSUM] = 0; + buf[offsets::CHECKSUM + 1] = 0; + + // Urgent Pointer + buf[offsets::URG_PTR] = (self.urgent_ptr >> 8) as u8; + buf[offsets::URG_PTR + 1] = (self.urgent_ptr & 0xFF) as u8; + + // Options + if !self.options.is_empty() { + let opts_bytes = self.options.to_bytes(); + let opts_end = offsets::OPTIONS + opts_bytes.len(); + if opts_end <= header_size { + buf[offsets::OPTIONS..opts_end].copy_from_slice(&opts_bytes); + } + } + + // Payload + if !self.payload.is_empty() { + buf[header_size..header_size + self.payload.len()].copy_from_slice(&self.payload); + } + + // Checksum (computed last if we have IP addresses) + let checksum = if self.auto_checksum { + match (self.src_ip, self.dst_ip) { + (Some(IpAddr::V4(src)), Some(IpAddr::V4(dst))) => { + tcp_checksum_ipv4(src, dst, &buf[..total_size]) + } + (Some(IpAddr::V6(src)), Some(IpAddr::V6(dst))) => { + tcp_checksum_ipv6(src, dst, &buf[..total_size]) + } + _ => 0, // Can't compute checksum without IP addresses + } + } else { + self.checksum.unwrap_or(0) + }; + buf[offsets::CHECKSUM] = (checksum >> 8) as u8; + buf[offsets::CHECKSUM + 1] = (checksum & 0xFF) as u8; + + Ok(total_size) + } + + /// Build only the header (no payload). + pub fn build_header(&self) -> Vec { + let header_size = self.header_size(); + let mut buf = vec![0u8; header_size]; + + // Create a copy without payload + let builder = Self { + payload: Vec::new(), + ..self.clone() + }; + builder + .build_into(&mut buf) + .expect("buffer is correctly sized"); + + buf + } +} + +// ========== Convenience Constructors ========== + +impl TcpBuilder { + /// Create a SYN packet builder. + pub fn syn_packet() -> Self { + Self::new().syn().ack_num(0) + } + + /// Create a SYN-ACK packet builder. + pub fn syn_ack_packet() -> Self { + Self::new().syn_ack() + } + + /// Create an ACK packet builder. + pub fn ack_packet() -> Self { + Self::new().flags(TcpFlags::A) + } + + /// Create a FIN-ACK packet builder. + pub fn fin_ack_packet() -> Self { + Self::new().fin_ack() + } + + /// Create a RST packet builder. + pub fn rst_packet() -> Self { + Self::new().flags(TcpFlags::R) + } + + /// Create a RST-ACK packet builder. + pub fn rst_ack_packet() -> Self { + Self::new().flags(TcpFlags::RA) + } + + /// Create a PSH-ACK packet builder (for data). + pub fn data_packet() -> Self { + Self::new().psh_ack() + } +} + +// ========== Random Values ========== + +#[cfg(feature = "rand")] +impl TcpBuilder { + /// Set a random sequence number. + pub fn random_seq(mut self) -> Self { + use rand::Rng; + self.seq = rand::rng().random(); + self + } + + /// Set a random source port (dynamic range: 49152-65535). + pub fn random_sport(mut self) -> Self { + use rand::Rng; + self.src_port = rand::rng().random_range(49152..=65535); + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_basic_build() { + let pkt = TcpBuilder::new() + .src_port(12345) + .dst_port(80) + .seq(1000) + .syn() + .window(65535) + .build(); + + assert_eq!(pkt.len(), 20); // Minimum header, no options + + let layer = TcpLayer::at_offset(0); + assert_eq!(layer.src_port(&pkt).unwrap(), 12345); + assert_eq!(layer.dst_port(&pkt).unwrap(), 80); + assert_eq!(layer.seq(&pkt).unwrap(), 1000); + assert_eq!(layer.window(&pkt).unwrap(), 65535); + + let flags = layer.flags(&pkt).unwrap(); + assert!(flags.syn); + assert!(!flags.ack); + } + + #[test] + fn test_with_options() { + let pkt = TcpBuilder::new() + .src_port(12345) + .dst_port(80) + .syn() + .mss(1460) + .wscale(7) + .sack_ok() + .nop() + .build(); + + let layer = TcpLayer::at_offset(0); + let opts = layer.options(&pkt).unwrap(); + + assert_eq!(opts.mss(), Some(1460)); + assert_eq!(opts.wscale(), Some(7)); + assert!(opts.sack_permitted()); + } + + #[test] + fn test_with_payload() { + let payload = b"Hello, TCP!"; + let pkt = TcpBuilder::new() + .src_port(12345) + .dst_port(80) + .psh_ack() + .payload(payload.to_vec()) + .build(); + + assert_eq!(pkt.len(), 20 + payload.len()); + + let layer = TcpLayer::at_offset(0); + let pkt_payload = layer.payload(&pkt); + assert_eq!(pkt_payload, payload); + } + + #[test] + fn test_with_checksum() { + let src_ip = Ipv4Addr::new(192, 168, 1, 1); + let dst_ip = Ipv4Addr::new(192, 168, 1, 2); + + let pkt = TcpBuilder::new() + .src_port(12345) + .dst_port(80) + .syn() + .ipv4_addrs(src_ip, dst_ip) + .build(); + + let layer = TcpLayer::at_offset(0); + let checksum = layer.checksum(&pkt).unwrap(); + assert_ne!(checksum, 0); + } + + #[test] + fn test_flags() { + let pkt = TcpBuilder::new().syn_ack().build(); + + let layer = TcpLayer::at_offset(0); + let flags = layer.flags(&pkt).unwrap(); + assert!(flags.syn); + assert!(flags.ack); + + let pkt = TcpBuilder::new().fin_ack().build(); + let flags = layer.flags(&pkt).unwrap(); + assert!(flags.fin); + assert!(flags.ack); + } + + #[test] + fn test_from_bytes() { + let original = TcpBuilder::new() + .src_port(12345) + .dst_port(443) + .seq(0xDEADBEEF) + .ack_num(0xCAFEBABE) + .syn_ack() + .window(32768) + .mss(1460) + .build(); + + let rebuilt = TcpBuilder::from_bytes(&original) + .unwrap() + .auto_checksum(false) + .build(); + + assert_eq!(original.len(), rebuilt.len()); + + let layer = TcpLayer::at_offset(0); + assert_eq!( + layer.src_port(&original).unwrap(), + layer.src_port(&rebuilt).unwrap() + ); + assert_eq!( + layer.dst_port(&original).unwrap(), + layer.dst_port(&rebuilt).unwrap() + ); + assert_eq!(layer.seq(&original).unwrap(), layer.seq(&rebuilt).unwrap()); + assert_eq!(layer.ack(&original).unwrap(), layer.ack(&rebuilt).unwrap()); + } + + #[test] + fn test_convenience_constructors() { + let syn = TcpBuilder::syn_packet().build(); + let layer = TcpLayer::at_offset(0); + let flags = layer.flags(&syn).unwrap(); + assert!(flags.syn); + assert!(!flags.ack); + + let syn_ack = TcpBuilder::syn_ack_packet().build(); + let flags = layer.flags(&syn_ack).unwrap(); + assert!(flags.syn); + assert!(flags.ack); + + let rst = TcpBuilder::rst_packet().build(); + let flags = layer.flags(&rst).unwrap(); + assert!(flags.rst); + } + + #[test] + fn test_timestamp_option() { + let pkt = TcpBuilder::new() + .syn() + .mss(1460) + .timestamp(12345, 0) + .build(); + + let layer = TcpLayer::at_offset(0); + let opts = layer.options(&pkt).unwrap(); + + let ts = opts.timestamp().unwrap(); + assert_eq!(ts.ts_val, 12345); + assert_eq!(ts.ts_ecr, 0); + } + + #[test] + fn test_tfo_option() { + let cookie = vec![0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]; + let pkt = TcpBuilder::new().syn().tfo(Some(cookie.clone())).build(); + + let layer = TcpLayer::at_offset(0); + let opts = layer.options(&pkt).unwrap(); + + assert_eq!(opts.tfo_cookie(), Some(cookie.as_slice())); + } + + #[test] + fn test_flags_str() { + let pkt = TcpBuilder::new().flags_str("SA").build(); + + let layer = TcpLayer::at_offset(0); + let flags = layer.flags(&pkt).unwrap(); + assert!(flags.syn); + assert!(flags.ack); + + let pkt = TcpBuilder::new().flags_str("FA").build(); + let flags = layer.flags(&pkt).unwrap(); + assert!(flags.fin); + assert!(flags.ack); + } +} diff --git a/crates/stackforge-core/src/layer/tcp/checksum.rs b/crates/stackforge-core/src/layer/tcp/checksum.rs new file mode 100644 index 0000000..b97100c --- /dev/null +++ b/crates/stackforge-core/src/layer/tcp/checksum.rs @@ -0,0 +1,387 @@ +//! TCP checksum calculation. +//! +//! TCP uses the Internet checksum (RFC 1071) computed over a pseudo-header +//! followed by the TCP header and payload. +//! +//! # IPv4 Pseudo-header +//! +//! ```text +//! +--------+--------+--------+--------+ +//! | Source Address | +//! +--------+--------+--------+--------+ +//! | Destination Address | +//! +--------+--------+--------+--------+ +//! | zero | PTCL | TCP Length | +//! +--------+--------+--------+--------+ +//! ``` +//! +//! # IPv6 Pseudo-header +//! +//! ```text +//! +--------+--------+--------+--------+ +//! | | +//! + + +//! | Source Address | +//! + + +//! | | +//! + + +//! | | +//! +--------+--------+--------+--------+ +//! | | +//! + + +//! | Destination Address | +//! + + +//! | | +//! + + +//! | | +//! +--------+--------+--------+--------+ +//! | Upper-Layer Packet Length | +//! +--------+--------+--------+--------+ +//! | zero |Next Hdr| +//! +--------+--------+--------+--------+ +//! ``` + +use std::net::{Ipv4Addr, Ipv6Addr}; + +use crate::layer::ipv4::checksum::{ + finalize_checksum, partial_checksum, pseudo_header_checksum, transport_checksum, +}; + +/// TCP protocol number for pseudo-header. +pub const TCP_PROTOCOL: u8 = 6; + +/// Compute TCP checksum with IPv4 pseudo-header. +/// +/// # Arguments +/// +/// * `src_ip` - Source IPv4 address +/// * `dst_ip` - Destination IPv4 address +/// * `tcp_data` - Complete TCP segment (header + payload) +/// +/// # Returns +/// +/// The computed checksum value. +pub fn tcp_checksum_ipv4(src_ip: Ipv4Addr, dst_ip: Ipv4Addr, tcp_data: &[u8]) -> u16 { + transport_checksum(&src_ip.octets(), &dst_ip.octets(), TCP_PROTOCOL, tcp_data) +} + +/// Compute TCP checksum with IPv6 pseudo-header. +/// +/// # Arguments +/// +/// * `src_ip` - Source IPv6 address +/// * `dst_ip` - Destination IPv6 address +/// * `tcp_data` - Complete TCP segment (header + payload) +/// +/// # Returns +/// +/// The computed checksum value. +pub fn tcp_checksum_ipv6(src_ip: Ipv6Addr, dst_ip: Ipv6Addr, tcp_data: &[u8]) -> u16 { + let tcp_len = tcp_data.len() as u32; + + // IPv6 pseudo-header checksum + let mut sum: u32 = 0; + + // Source address (16 bytes) + let src_octets = src_ip.octets(); + for chunk in src_octets.chunks(2) { + sum += u16::from_be_bytes([chunk[0], chunk[1]]) as u32; + } + + // Destination address (16 bytes) + let dst_octets = dst_ip.octets(); + for chunk in dst_octets.chunks(2) { + sum += u16::from_be_bytes([chunk[0], chunk[1]]) as u32; + } + + // Upper-layer packet length (4 bytes) + sum += ((tcp_len >> 16) & 0xFFFF) as u32; + sum += (tcp_len & 0xFFFF) as u32; + + // Zero + Next Header (4 bytes, but only last byte is non-zero) + sum += TCP_PROTOCOL as u32; + + // Add TCP data + sum = partial_checksum(tcp_data, sum); + + // Finalize + finalize_checksum(sum) +} + +/// Compute TCP checksum (generic version). +/// +/// Automatically handles IPv4 or IPv6 based on address type. +/// Uses raw byte slices for maximum flexibility. +/// +/// # Arguments +/// +/// * `src_ip` - Source IP address bytes (4 for IPv4, 16 for IPv6) +/// * `dst_ip` - Destination IP address bytes +/// * `tcp_data` - Complete TCP segment (header + payload) +/// +/// # Returns +/// +/// The computed checksum value, or None if address lengths are invalid. +pub fn tcp_checksum(src_ip: &[u8], dst_ip: &[u8], tcp_data: &[u8]) -> Option { + match (src_ip.len(), dst_ip.len()) { + (4, 4) => { + let src: [u8; 4] = src_ip.try_into().ok()?; + let dst: [u8; 4] = dst_ip.try_into().ok()?; + Some(transport_checksum(&src, &dst, TCP_PROTOCOL, tcp_data)) + } + (16, 16) => { + let src: [u8; 16] = src_ip.try_into().ok()?; + let dst: [u8; 16] = dst_ip.try_into().ok()?; + Some(tcp_checksum_ipv6( + Ipv6Addr::from(src), + Ipv6Addr::from(dst), + tcp_data, + )) + } + _ => None, + } +} + +/// Verify TCP checksum with IPv4 pseudo-header. +/// +/// # Arguments +/// +/// * `src_ip` - Source IPv4 address +/// * `dst_ip` - Destination IPv4 address +/// * `tcp_data` - Complete TCP segment (header + payload) with checksum +/// +/// # Returns +/// +/// `true` if the checksum is valid. +pub fn verify_tcp_checksum(src_ip: Ipv4Addr, dst_ip: Ipv4Addr, tcp_data: &[u8]) -> bool { + if tcp_data.len() < 20 { + return false; + } + + let checksum = tcp_checksum_ipv4(src_ip, dst_ip, tcp_data); + checksum == 0 || checksum == 0xFFFF +} + +/// Verify TCP checksum with IPv6 pseudo-header. +pub fn verify_tcp_checksum_ipv6(src_ip: Ipv6Addr, dst_ip: Ipv6Addr, tcp_data: &[u8]) -> bool { + if tcp_data.len() < 20 { + return false; + } + + let checksum = tcp_checksum_ipv6(src_ip, dst_ip, tcp_data); + checksum == 0 || checksum == 0xFFFF +} + +/// Build the IPv4 pseudo-header bytes for TCP. +/// +/// # Arguments +/// +/// * `src_ip` - Source IPv4 address +/// * `dst_ip` - Destination IPv4 address +/// * `tcp_len` - Length of TCP segment (header + payload) +/// +/// # Returns +/// +/// 12-byte pseudo-header. +pub fn ipv4_pseudo_header(src_ip: Ipv4Addr, dst_ip: Ipv4Addr, tcp_len: u16) -> [u8; 12] { + let mut header = [0u8; 12]; + + // Source IP (4 bytes) + header[0..4].copy_from_slice(&src_ip.octets()); + + // Destination IP (4 bytes) + header[4..8].copy_from_slice(&dst_ip.octets()); + + // Zero (1 byte) + header[8] = 0; + + // Protocol (1 byte) + header[9] = TCP_PROTOCOL; + + // TCP Length (2 bytes) + header[10..12].copy_from_slice(&tcp_len.to_be_bytes()); + + header +} + +/// Build the IPv6 pseudo-header bytes for TCP. +/// +/// # Arguments +/// +/// * `src_ip` - Source IPv6 address +/// * `dst_ip` - Destination IPv6 address +/// * `tcp_len` - Length of TCP segment (header + payload) +/// +/// # Returns +/// +/// 40-byte pseudo-header. +pub fn ipv6_pseudo_header(src_ip: Ipv6Addr, dst_ip: Ipv6Addr, tcp_len: u32) -> [u8; 40] { + let mut header = [0u8; 40]; + + // Source IP (16 bytes) + header[0..16].copy_from_slice(&src_ip.octets()); + + // Destination IP (16 bytes) + header[16..32].copy_from_slice(&dst_ip.octets()); + + // Upper-Layer Packet Length (4 bytes) + header[32..36].copy_from_slice(&tcp_len.to_be_bytes()); + + // Zero (3 bytes) + Next Header (1 byte) + header[36] = 0; + header[37] = 0; + header[38] = 0; + header[39] = TCP_PROTOCOL; + + header +} + +/// Compute checksum for partial data (for segmented computation). +/// +/// Useful when computing checksum across multiple buffers. +pub fn tcp_partial_checksum(src_ip: &[u8; 4], dst_ip: &[u8; 4], tcp_len: u16) -> u32 { + pseudo_header_checksum(src_ip, dst_ip, TCP_PROTOCOL, tcp_len) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_tcp_checksum_ipv4() { + let src_ip = Ipv4Addr::new(192, 168, 1, 1); + let dst_ip = Ipv4Addr::new(192, 168, 1, 2); + + // Minimal TCP header with zeroed checksum + let tcp_header = [ + 0x00, 0x50, // Source port: 80 + 0x1F, 0x90, // Dest port: 8080 + 0x00, 0x00, 0x00, 0x01, // Seq number + 0x00, 0x00, 0x00, 0x00, // Ack number + 0x50, 0x02, // Data offset + flags (SYN) + 0xFF, 0xFF, // Window + 0x00, 0x00, // Checksum (zeroed) + 0x00, 0x00, // Urgent pointer + ]; + + let checksum = tcp_checksum_ipv4(src_ip, dst_ip, &tcp_header); + assert_ne!(checksum, 0); + + // Insert checksum and verify + let mut tcp_with_checksum = tcp_header; + tcp_with_checksum[16] = (checksum >> 8) as u8; + tcp_with_checksum[17] = (checksum & 0xFF) as u8; + + assert!(verify_tcp_checksum(src_ip, dst_ip, &tcp_with_checksum)); + } + + #[test] + fn test_tcp_checksum_ipv6() { + let src_ip = Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1); + let dst_ip = Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 2); + + // Minimal TCP header with zeroed checksum + let tcp_header = [ + 0x00, 0x50, // Source port: 80 + 0x1F, 0x90, // Dest port: 8080 + 0x00, 0x00, 0x00, 0x01, // Seq number + 0x00, 0x00, 0x00, 0x00, // Ack number + 0x50, 0x02, // Data offset + flags (SYN) + 0xFF, 0xFF, // Window + 0x00, 0x00, // Checksum (zeroed) + 0x00, 0x00, // Urgent pointer + ]; + + let checksum = tcp_checksum_ipv6(src_ip, dst_ip, &tcp_header); + assert_ne!(checksum, 0); + + // Insert checksum and verify + let mut tcp_with_checksum = tcp_header; + tcp_with_checksum[16] = (checksum >> 8) as u8; + tcp_with_checksum[17] = (checksum & 0xFF) as u8; + + assert!(verify_tcp_checksum_ipv6(src_ip, dst_ip, &tcp_with_checksum)); + } + + #[test] + fn test_tcp_checksum_generic() { + let src_ipv4 = [192, 168, 1, 1]; + let dst_ipv4 = [192, 168, 1, 2]; + + let tcp_header = [ + 0x00, 0x50, // Source port: 80 + 0x1F, 0x90, // Dest port: 8080 + 0x00, 0x00, 0x00, 0x01, // Seq number + 0x00, 0x00, 0x00, 0x00, // Ack number + 0x50, 0x02, // Data offset + flags (SYN) + 0xFF, 0xFF, // Window + 0x00, 0x00, // Checksum (zeroed) + 0x00, 0x00, // Urgent pointer + ]; + + let checksum = tcp_checksum(&src_ipv4, &dst_ipv4, &tcp_header); + assert!(checksum.is_some()); + + let checksum_direct = tcp_checksum_ipv4( + Ipv4Addr::from(src_ipv4), + Ipv4Addr::from(dst_ipv4), + &tcp_header, + ); + assert_eq!(checksum.unwrap(), checksum_direct); + } + + #[test] + fn test_pseudo_header() { + let src_ip = Ipv4Addr::new(192, 168, 1, 1); + let dst_ip = Ipv4Addr::new(192, 168, 1, 2); + let tcp_len = 20u16; + + let header = ipv4_pseudo_header(src_ip, dst_ip, tcp_len); + + assert_eq!(&header[0..4], &[192, 168, 1, 1]); + assert_eq!(&header[4..8], &[192, 168, 1, 2]); + assert_eq!(header[8], 0); + assert_eq!(header[9], TCP_PROTOCOL); + assert_eq!(&header[10..12], &[0, 20]); + } + + #[test] + fn test_invalid_checksum() { + let src_ip = Ipv4Addr::new(192, 168, 1, 1); + let dst_ip = Ipv4Addr::new(192, 168, 1, 2); + + // TCP header with bad checksum + let tcp_header = [ + 0x00, 0x50, 0x1F, 0x90, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x50, 0x02, + 0xFF, 0xFF, 0xFF, 0xFF, // Bad checksum + 0x00, 0x00, + ]; + + assert!(!verify_tcp_checksum(src_ip, dst_ip, &tcp_header)); + } + + #[test] + fn test_checksum_with_payload() { + let src_ip = Ipv4Addr::new(10, 0, 0, 1); + let dst_ip = Ipv4Addr::new(10, 0, 0, 2); + + // TCP header + "Hello" payload + let mut tcp_segment = vec![ + 0x00, 0x50, // Source port: 80 + 0x00, 0x51, // Dest port: 81 + 0x00, 0x00, 0x00, 0x01, // Seq number + 0x00, 0x00, 0x00, 0x01, // Ack number + 0x50, 0x18, // Data offset + flags (PSH+ACK) + 0xFF, 0xFF, // Window + 0x00, 0x00, // Checksum (zeroed) + 0x00, 0x00, // Urgent pointer + ]; + tcp_segment.extend_from_slice(b"Hello"); + + let checksum = tcp_checksum_ipv4(src_ip, dst_ip, &tcp_segment); + tcp_segment[16] = (checksum >> 8) as u8; + tcp_segment[17] = (checksum & 0xFF) as u8; + + assert!(verify_tcp_checksum(src_ip, dst_ip, &tcp_segment)); + } +} diff --git a/crates/stackforge-core/src/layer/tcp/flags.rs b/crates/stackforge-core/src/layer/tcp/flags.rs new file mode 100644 index 0000000..a64cbef --- /dev/null +++ b/crates/stackforge-core/src/layer/tcp/flags.rs @@ -0,0 +1,588 @@ +//! TCP flags implementation. +//! +//! TCP flags are a 9-bit field in the TCP header (including NS, CWR, ECE). +//! This module provides a structured representation matching Scapy's FlagsField. + +use std::fmt; + +/// TCP flags structure (9 bits). +/// +/// Bit layout (from MSB to LSB in the 16-bit flags/reserved field): +/// - NS (Nonce Sum) - ECN nonce concealment +/// - CWR (Congestion Window Reduced) +/// - ECE (ECN-Echo) +/// - URG (Urgent) +/// - ACK (Acknowledgment) +/// - PSH (Push) +/// - RST (Reset) +/// - SYN (Synchronize) +/// - FIN (Finish) +/// +/// Scapy uses the string "FSRPAUECN" for flags (reversed order). +#[derive(Clone, Copy, PartialEq, Eq, Default, Hash)] +pub struct TcpFlags { + /// FIN - No more data from sender + pub fin: bool, + /// SYN - Synchronize sequence numbers + pub syn: bool, + /// RST - Reset the connection + pub rst: bool, + /// PSH - Push function + pub psh: bool, + /// ACK - Acknowledgment field significant + pub ack: bool, + /// URG - Urgent pointer field significant + pub urg: bool, + /// ECE - ECN-Echo (RFC 3168) + pub ece: bool, + /// CWR - Congestion Window Reduced (RFC 3168) + pub cwr: bool, + /// NS - ECN-nonce concealment protection (RFC 3540) + pub ns: bool, +} + +impl TcpFlags { + /// No flags set + pub const NONE: Self = Self { + fin: false, + syn: false, + rst: false, + psh: false, + ack: false, + urg: false, + ece: false, + cwr: false, + ns: false, + }; + + /// SYN flag only (connection initiation) + pub const S: Self = Self { + fin: false, + syn: true, + rst: false, + psh: false, + ack: false, + urg: false, + ece: false, + cwr: false, + ns: false, + }; + + /// SYN+ACK flags (connection acknowledgment) + pub const SA: Self = Self { + fin: false, + syn: true, + rst: false, + psh: false, + ack: true, + urg: false, + ece: false, + cwr: false, + ns: false, + }; + + /// ACK flag only + pub const A: Self = Self { + fin: false, + syn: false, + rst: false, + psh: false, + ack: true, + urg: false, + ece: false, + cwr: false, + ns: false, + }; + + /// FIN+ACK flags (connection termination) + pub const FA: Self = Self { + fin: true, + syn: false, + rst: false, + psh: false, + ack: true, + urg: false, + ece: false, + cwr: false, + ns: false, + }; + + /// RST flag only (connection reset) + pub const R: Self = Self { + fin: false, + syn: false, + rst: true, + psh: false, + ack: false, + urg: false, + ece: false, + cwr: false, + ns: false, + }; + + /// RST+ACK flags + pub const RA: Self = Self { + fin: false, + syn: false, + rst: true, + psh: false, + ack: true, + urg: false, + ece: false, + cwr: false, + ns: false, + }; + + /// PSH+ACK flags (data push) + pub const PA: Self = Self { + fin: false, + syn: false, + rst: false, + psh: true, + ack: true, + urg: false, + ece: false, + cwr: false, + ns: false, + }; + + /// Flag bit positions (in the 9-bit flags field) + pub const FIN_BIT: u16 = 0x001; + pub const SYN_BIT: u16 = 0x002; + pub const RST_BIT: u16 = 0x004; + pub const PSH_BIT: u16 = 0x008; + pub const ACK_BIT: u16 = 0x010; + pub const URG_BIT: u16 = 0x020; + pub const ECE_BIT: u16 = 0x040; + pub const CWR_BIT: u16 = 0x080; + pub const NS_BIT: u16 = 0x100; + + /// Create flags from a raw 16-bit value (data offset + reserved + flags). + /// + /// The flags are in the lower 9 bits (with NS in bit 8 of the high byte). + #[inline] + pub fn from_u16(value: u16) -> Self { + Self { + fin: (value & Self::FIN_BIT) != 0, + syn: (value & Self::SYN_BIT) != 0, + rst: (value & Self::RST_BIT) != 0, + psh: (value & Self::PSH_BIT) != 0, + ack: (value & Self::ACK_BIT) != 0, + urg: (value & Self::URG_BIT) != 0, + ece: (value & Self::ECE_BIT) != 0, + cwr: (value & Self::CWR_BIT) != 0, + ns: (value & Self::NS_BIT) != 0, + } + } + + /// Create flags from just the flags byte (lower 8 bits). + #[inline] + pub fn from_byte(byte: u8) -> Self { + Self::from_u16(byte as u16) + } + + /// Create flags from two bytes (data_offset_reserved + flags). + #[inline] + pub fn from_bytes(hi: u8, lo: u8) -> Self { + let ns = (hi & 0x01) != 0; + let mut flags = Self::from_byte(lo); + flags.ns = ns; + flags + } + + /// Convert to a raw 9-bit value. + #[inline] + pub fn to_u16(self) -> u16 { + let mut value = 0u16; + if self.fin { + value |= Self::FIN_BIT; + } + if self.syn { + value |= Self::SYN_BIT; + } + if self.rst { + value |= Self::RST_BIT; + } + if self.psh { + value |= Self::PSH_BIT; + } + if self.ack { + value |= Self::ACK_BIT; + } + if self.urg { + value |= Self::URG_BIT; + } + if self.ece { + value |= Self::ECE_BIT; + } + if self.cwr { + value |= Self::CWR_BIT; + } + if self.ns { + value |= Self::NS_BIT; + } + value + } + + /// Convert to the lower flags byte (without NS). + #[inline] + pub fn to_byte(self) -> u8 { + (self.to_u16() & 0xFF) as u8 + } + + /// Get the NS bit for the high byte. + #[inline] + pub fn ns_bit(self) -> u8 { + if self.ns { 0x01 } else { 0x00 } + } + + /// Create flags from a string like "S", "SA", "FA", "PA", "R", etc. + /// Uses Scapy's "FSRPAUECN" convention. + pub fn from_str(s: &str) -> Self { + let mut flags = Self::NONE; + for c in s.chars() { + match c { + 'F' | 'f' => flags.fin = true, + 'S' | 's' => flags.syn = true, + 'R' | 'r' => flags.rst = true, + 'P' | 'p' => flags.psh = true, + 'A' | 'a' => flags.ack = true, + 'U' | 'u' => flags.urg = true, + 'E' | 'e' => flags.ece = true, + 'C' | 'c' => flags.cwr = true, + 'N' | 'n' => flags.ns = true, + _ => {} // Ignore unknown characters + } + } + flags + } + + /// Check if this is a SYN packet (SYN set, ACK not set). + #[inline] + pub fn is_syn(&self) -> bool { + self.syn && !self.ack + } + + /// Check if this is a SYN-ACK packet. + #[inline] + pub fn is_syn_ack(&self) -> bool { + self.syn && self.ack + } + + /// Check if this is a pure ACK packet. + #[inline] + pub fn is_ack(&self) -> bool { + self.ack && !self.syn && !self.fin && !self.rst + } + + /// Check if this is a FIN packet. + #[inline] + pub fn is_fin(&self) -> bool { + self.fin + } + + /// Check if this is a RST packet. + #[inline] + pub fn is_rst(&self) -> bool { + self.rst + } + + /// Check if ECN is enabled (ECE or CWR set). + #[inline] + pub fn has_ecn(&self) -> bool { + self.ece || self.cwr + } + + /// Check if any flag is set. + #[inline] + pub fn is_empty(&self) -> bool { + !self.fin + && !self.syn + && !self.rst + && !self.psh + && !self.ack + && !self.urg + && !self.ece + && !self.cwr + && !self.ns + } + + /// Count how many flags are set. + #[inline] + pub fn count(&self) -> u8 { + let mut count = 0; + if self.fin { + count += 1; + } + if self.syn { + count += 1; + } + if self.rst { + count += 1; + } + if self.psh { + count += 1; + } + if self.ack { + count += 1; + } + if self.urg { + count += 1; + } + if self.ece { + count += 1; + } + if self.cwr { + count += 1; + } + if self.ns { + count += 1; + } + count + } +} + +impl fmt::Display for TcpFlags { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Scapy order: FSRPAUECN + let mut s = String::with_capacity(9); + if self.fin { + s.push('F'); + } + if self.syn { + s.push('S'); + } + if self.rst { + s.push('R'); + } + if self.psh { + s.push('P'); + } + if self.ack { + s.push('A'); + } + if self.urg { + s.push('U'); + } + if self.ece { + s.push('E'); + } + if self.cwr { + s.push('C'); + } + if self.ns { + s.push('N'); + } + + if s.is_empty() { + write!(f, "-") + } else { + write!(f, "{}", s) + } + } +} + +impl fmt::Debug for TcpFlags { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "TcpFlags({})", self) + } +} + +impl From for TcpFlags { + fn from(value: u16) -> Self { + Self::from_u16(value) + } +} + +impl From for TcpFlags { + fn from(value: u8) -> Self { + Self::from_byte(value) + } +} + +impl From for u16 { + fn from(flags: TcpFlags) -> Self { + flags.to_u16() + } +} + +impl From for u8 { + fn from(flags: TcpFlags) -> Self { + flags.to_byte() + } +} + +impl From<&str> for TcpFlags { + fn from(s: &str) -> Self { + Self::from_str(s) + } +} + +impl std::ops::BitOr for TcpFlags { + type Output = Self; + + fn bitor(self, rhs: Self) -> Self::Output { + Self { + fin: self.fin || rhs.fin, + syn: self.syn || rhs.syn, + rst: self.rst || rhs.rst, + psh: self.psh || rhs.psh, + ack: self.ack || rhs.ack, + urg: self.urg || rhs.urg, + ece: self.ece || rhs.ece, + cwr: self.cwr || rhs.cwr, + ns: self.ns || rhs.ns, + } + } +} + +impl std::ops::BitAnd for TcpFlags { + type Output = Self; + + fn bitand(self, rhs: Self) -> Self::Output { + Self { + fin: self.fin && rhs.fin, + syn: self.syn && rhs.syn, + rst: self.rst && rhs.rst, + psh: self.psh && rhs.psh, + ack: self.ack && rhs.ack, + urg: self.urg && rhs.urg, + ece: self.ece && rhs.ece, + cwr: self.cwr && rhs.cwr, + ns: self.ns && rhs.ns, + } + } +} + +impl std::ops::BitOrAssign for TcpFlags { + fn bitor_assign(&mut self, rhs: Self) { + *self = *self | rhs; + } +} + +impl std::ops::BitAndAssign for TcpFlags { + fn bitand_assign(&mut self, rhs: Self) { + *self = *self & rhs; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_from_u16() { + let flags = TcpFlags::from_u16(0x02); // SYN + assert!(flags.syn); + assert!(!flags.ack); + assert!(!flags.fin); + + let flags = TcpFlags::from_u16(0x12); // SYN+ACK + assert!(flags.syn); + assert!(flags.ack); + + let flags = TcpFlags::from_u16(0x10); // ACK + assert!(!flags.syn); + assert!(flags.ack); + + let flags = TcpFlags::from_u16(0x11); // FIN+ACK + assert!(flags.fin); + assert!(flags.ack); + + let flags = TcpFlags::from_u16(0x04); // RST + assert!(flags.rst); + + let flags = TcpFlags::from_u16(0x100); // NS + assert!(flags.ns); + } + + #[test] + fn test_to_u16() { + assert_eq!(TcpFlags::S.to_u16(), 0x02); + assert_eq!(TcpFlags::SA.to_u16(), 0x12); + assert_eq!(TcpFlags::A.to_u16(), 0x10); + assert_eq!(TcpFlags::FA.to_u16(), 0x11); + assert_eq!(TcpFlags::R.to_u16(), 0x04); + } + + #[test] + fn test_from_str() { + let flags = TcpFlags::from_str("S"); + assert!(flags.syn); + assert!(!flags.ack); + + let flags = TcpFlags::from_str("SA"); + assert!(flags.syn); + assert!(flags.ack); + + let flags = TcpFlags::from_str("FSRPAUECN"); + assert!(flags.fin); + assert!(flags.syn); + assert!(flags.rst); + assert!(flags.psh); + assert!(flags.ack); + assert!(flags.urg); + assert!(flags.ece); + assert!(flags.cwr); + assert!(flags.ns); + } + + #[test] + fn test_display() { + assert_eq!(TcpFlags::S.to_string(), "S"); + assert_eq!(TcpFlags::SA.to_string(), "SA"); + assert_eq!(TcpFlags::FA.to_string(), "FA"); + assert_eq!(TcpFlags::PA.to_string(), "PA"); + assert_eq!(TcpFlags::NONE.to_string(), "-"); + } + + #[test] + fn test_is_methods() { + assert!(TcpFlags::S.is_syn()); + assert!(!TcpFlags::SA.is_syn()); // SYN-ACK is not "just SYN" + assert!(TcpFlags::SA.is_syn_ack()); + assert!(TcpFlags::A.is_ack()); + assert!(TcpFlags::FA.is_fin()); + assert!(TcpFlags::R.is_rst()); + } + + #[test] + fn test_bit_ops() { + let flags = TcpFlags::S | TcpFlags::A; + assert!(flags.syn); + assert!(flags.ack); + + let flags = TcpFlags::SA & TcpFlags::S; + assert!(flags.syn); + assert!(!flags.ack); + } + + #[test] + fn test_from_bytes() { + // Data offset (5) + reserved (000) + NS (0) + flags (0x12 = SYN+ACK) + // Byte 12: 0101_0000 = 0x50 (data offset 5, NS=0) + // Byte 13: 0001_0010 = 0x12 (SYN+ACK) + let flags = TcpFlags::from_bytes(0x50, 0x12); + assert!(flags.syn); + assert!(flags.ack); + assert!(!flags.ns); + + // With NS bit set (bit 0 of byte 12) + let flags = TcpFlags::from_bytes(0x51, 0x12); + assert!(flags.syn); + assert!(flags.ack); + assert!(flags.ns); + } + + #[test] + fn test_constants() { + assert_eq!(TcpFlags::NONE.to_u16(), 0); + assert_eq!(TcpFlags::S.to_u16(), 0x002); + assert_eq!(TcpFlags::SA.to_u16(), 0x012); + assert_eq!(TcpFlags::A.to_u16(), 0x010); + assert_eq!(TcpFlags::FA.to_u16(), 0x011); + assert_eq!(TcpFlags::R.to_u16(), 0x004); + assert_eq!(TcpFlags::RA.to_u16(), 0x014); + assert_eq!(TcpFlags::PA.to_u16(), 0x018); + } +} diff --git a/crates/stackforge-core/src/layer/tcp/header.rs b/crates/stackforge-core/src/layer/tcp/header.rs new file mode 100644 index 0000000..e94ad01 --- /dev/null +++ b/crates/stackforge-core/src/layer/tcp/header.rs @@ -0,0 +1,758 @@ +//! TCP header layer implementation. +//! +//! Provides zero-copy access to TCP header fields per RFC 793. +//! +//! # TCP Header Format +//! +//! ```text +//! 0 1 2 3 +//! 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +//! | Source Port | Destination Port | +//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +//! | Sequence Number | +//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +//! | Acknowledgment Number | +//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +//! | Data | |N|C|E|U|A|P|R|S|F| | +//! | Offset| Res |S|W|C|R|C|S|S|Y|I| Window | +//! | | | |R|E|G|K|H|T|N|N| | +//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +//! | Checksum | Urgent Pointer | +//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +//! | Options | Padding | +//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +//! | data | +//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +//! ``` + +use crate::layer::field::{Field, FieldDesc, FieldError, FieldType, FieldValue}; +use crate::layer::{Layer, LayerIndex, LayerKind}; + +use super::flags::TcpFlags; +use super::options::{TcpOptions, parse_options}; +use super::services; + +/// Minimum TCP header length (no options). +pub const TCP_MIN_HEADER_LEN: usize = 20; + +/// Maximum TCP header length (with maximum options). +pub const TCP_MAX_HEADER_LEN: usize = 60; + +/// Field offsets within the TCP header. +pub mod offsets { + /// Source port (16 bits) + pub const SRC_PORT: usize = 0; + /// Destination port (16 bits) + pub const DST_PORT: usize = 2; + /// Sequence number (32 bits) + pub const SEQ: usize = 4; + /// Acknowledgment number (32 bits) + pub const ACK: usize = 8; + /// Data offset (4 bits) + Reserved (3 bits) + NS flag (1 bit) + pub const DATA_OFFSET: usize = 12; + /// Flags byte (8 bits: CWR, ECE, URG, ACK, PSH, RST, SYN, FIN) + pub const FLAGS: usize = 13; + /// Window size (16 bits) + pub const WINDOW: usize = 14; + /// Checksum (16 bits) + pub const CHECKSUM: usize = 16; + /// Urgent pointer (16 bits) + pub const URG_PTR: usize = 18; + /// Options start (if data offset > 5) + pub const OPTIONS: usize = 20; +} + +/// Field descriptors for dynamic access. +pub static FIELDS: &[FieldDesc] = &[ + FieldDesc::new("sport", offsets::SRC_PORT, 2, FieldType::U16), + FieldDesc::new("dport", offsets::DST_PORT, 2, FieldType::U16), + FieldDesc::new("seq", offsets::SEQ, 4, FieldType::U32), + FieldDesc::new("ack", offsets::ACK, 4, FieldType::U32), + FieldDesc::new("dataofs", offsets::DATA_OFFSET, 1, FieldType::U8), + FieldDesc::new("reserved", offsets::DATA_OFFSET, 1, FieldType::U8), + FieldDesc::new("flags", offsets::FLAGS, 1, FieldType::U8), + FieldDesc::new("window", offsets::WINDOW, 2, FieldType::U16), + FieldDesc::new("chksum", offsets::CHECKSUM, 2, FieldType::U16), + FieldDesc::new("urgptr", offsets::URG_PTR, 2, FieldType::U16), +]; + +/// A view into a TCP packet header. +#[derive(Debug, Clone)] +pub struct TcpLayer { + pub index: LayerIndex, +} + +impl TcpLayer { + /// Create a new TCP layer view with specified bounds. + #[inline] + pub const fn new(start: usize, end: usize) -> Self { + Self { + index: LayerIndex::new(LayerKind::Tcp, start, end), + } + } + + /// Create a layer at offset 0 with minimum header length. + #[inline] + pub const fn at_start() -> Self { + Self::new(0, TCP_MIN_HEADER_LEN) + } + + /// Create a layer at the specified offset with minimum header length. + #[inline] + pub const fn at_offset(offset: usize) -> Self { + Self::new(offset, offset + TCP_MIN_HEADER_LEN) + } + + /// Create a layer at offset, calculating actual header length from data offset. + pub fn at_offset_dynamic(buf: &[u8], offset: usize) -> Result { + if buf.len() < offset + TCP_MIN_HEADER_LEN { + return Err(FieldError::BufferTooShort { + offset, + need: TCP_MIN_HEADER_LEN, + have: buf.len().saturating_sub(offset), + }); + } + + let data_offset = ((buf[offset + offsets::DATA_OFFSET] >> 4) & 0x0F) as usize; + let header_len = data_offset * 4; + + if header_len < TCP_MIN_HEADER_LEN { + return Err(FieldError::InvalidValue(format!( + "Data offset {} is less than minimum (5)", + data_offset + ))); + } + + if buf.len() < offset + header_len { + return Err(FieldError::BufferTooShort { + offset, + need: header_len, + have: buf.len().saturating_sub(offset), + }); + } + + Ok(Self::new(offset, offset + header_len)) + } + + /// Validate that the buffer contains a valid TCP header at the offset. + pub fn validate(buf: &[u8], offset: usize) -> Result<(), FieldError> { + if buf.len() < offset + TCP_MIN_HEADER_LEN { + return Err(FieldError::BufferTooShort { + offset, + need: TCP_MIN_HEADER_LEN, + have: buf.len().saturating_sub(offset), + }); + } + + let data_offset = ((buf[offset + offsets::DATA_OFFSET] >> 4) & 0x0F) as usize; + if data_offset < 5 { + return Err(FieldError::InvalidValue(format!( + "Data offset {} is less than minimum (5)", + data_offset + ))); + } + + let header_len = data_offset * 4; + if buf.len() < offset + header_len { + return Err(FieldError::BufferTooShort { + offset, + need: header_len, + have: buf.len().saturating_sub(offset), + }); + } + + Ok(()) + } + + /// Calculate the actual header length from the buffer. + pub fn calculate_header_len(&self, buf: &[u8]) -> usize { + self.data_offset(buf) + .map(|doff| (doff as usize) * 4) + .unwrap_or(TCP_MIN_HEADER_LEN) + } + + /// Get the options length (header length - 20). + pub fn options_len(&self, buf: &[u8]) -> usize { + self.calculate_header_len(buf) + .saturating_sub(TCP_MIN_HEADER_LEN) + } + + // ========== Field Readers ========== + + /// Read the source port. + #[inline] + pub fn src_port(&self, buf: &[u8]) -> Result { + u16::read(buf, self.index.start + offsets::SRC_PORT) + } + + /// Alias for src_port (Scapy compatibility). + #[inline] + pub fn sport(&self, buf: &[u8]) -> Result { + self.src_port(buf) + } + + /// Read the destination port. + #[inline] + pub fn dst_port(&self, buf: &[u8]) -> Result { + u16::read(buf, self.index.start + offsets::DST_PORT) + } + + /// Alias for dst_port (Scapy compatibility). + #[inline] + pub fn dport(&self, buf: &[u8]) -> Result { + self.dst_port(buf) + } + + /// Read the sequence number. + #[inline] + pub fn seq(&self, buf: &[u8]) -> Result { + u32::read(buf, self.index.start + offsets::SEQ) + } + + /// Read the acknowledgment number. + #[inline] + pub fn ack(&self, buf: &[u8]) -> Result { + u32::read(buf, self.index.start + offsets::ACK) + } + + /// Read the data offset (in 32-bit words). + #[inline] + pub fn data_offset(&self, buf: &[u8]) -> Result { + let b = u8::read(buf, self.index.start + offsets::DATA_OFFSET)?; + Ok((b >> 4) & 0x0F) + } + + /// Alias for data_offset (Scapy compatibility). + #[inline] + pub fn dataofs(&self, buf: &[u8]) -> Result { + self.data_offset(buf) + } + + /// Read the reserved bits (should be 0). + #[inline] + pub fn reserved(&self, buf: &[u8]) -> Result { + let b = u8::read(buf, self.index.start + offsets::DATA_OFFSET)?; + Ok((b >> 1) & 0x07) + } + + /// Read the flags as raw byte. + #[inline] + pub fn flags_raw(&self, buf: &[u8]) -> Result { + u8::read(buf, self.index.start + offsets::FLAGS) + } + + /// Read the flags as a structured type. + #[inline] + pub fn flags(&self, buf: &[u8]) -> Result { + let hi = u8::read(buf, self.index.start + offsets::DATA_OFFSET)?; + let lo = u8::read(buf, self.index.start + offsets::FLAGS)?; + Ok(TcpFlags::from_bytes(hi, lo)) + } + + /// Read the window size. + #[inline] + pub fn window(&self, buf: &[u8]) -> Result { + u16::read(buf, self.index.start + offsets::WINDOW) + } + + /// Read the checksum. + #[inline] + pub fn checksum(&self, buf: &[u8]) -> Result { + u16::read(buf, self.index.start + offsets::CHECKSUM) + } + + /// Alias for checksum (Scapy compatibility). + #[inline] + pub fn chksum(&self, buf: &[u8]) -> Result { + self.checksum(buf) + } + + /// Read the urgent pointer. + #[inline] + pub fn urgent_ptr(&self, buf: &[u8]) -> Result { + u16::read(buf, self.index.start + offsets::URG_PTR) + } + + /// Alias for urgent_ptr (Scapy compatibility). + #[inline] + pub fn urgptr(&self, buf: &[u8]) -> Result { + self.urgent_ptr(buf) + } + + /// Read the options bytes (if any). + pub fn options_bytes<'a>(&self, buf: &'a [u8]) -> Result<&'a [u8], FieldError> { + let header_len = self.calculate_header_len(buf); + let opts_start = self.index.start + TCP_MIN_HEADER_LEN; + let opts_end = self.index.start + header_len; + + if buf.len() < opts_end { + return Err(FieldError::BufferTooShort { + offset: opts_start, + need: header_len - TCP_MIN_HEADER_LEN, + have: buf.len().saturating_sub(opts_start), + }); + } + + Ok(&buf[opts_start..opts_end]) + } + + /// Parse and return the options. + pub fn options(&self, buf: &[u8]) -> Result { + let opts_bytes = self.options_bytes(buf)?; + parse_options(opts_bytes) + } + + // ========== Field Writers ========== + + /// Set the source port. + #[inline] + pub fn set_src_port(&self, buf: &mut [u8], port: u16) -> Result<(), FieldError> { + port.write(buf, self.index.start + offsets::SRC_PORT) + } + + /// Alias for set_src_port (Scapy compatibility). + #[inline] + pub fn set_sport(&self, buf: &mut [u8], port: u16) -> Result<(), FieldError> { + self.set_src_port(buf, port) + } + + /// Set the destination port. + #[inline] + pub fn set_dst_port(&self, buf: &mut [u8], port: u16) -> Result<(), FieldError> { + port.write(buf, self.index.start + offsets::DST_PORT) + } + + /// Alias for set_dst_port (Scapy compatibility). + #[inline] + pub fn set_dport(&self, buf: &mut [u8], port: u16) -> Result<(), FieldError> { + self.set_dst_port(buf, port) + } + + /// Set the sequence number. + #[inline] + pub fn set_seq(&self, buf: &mut [u8], seq: u32) -> Result<(), FieldError> { + seq.write(buf, self.index.start + offsets::SEQ) + } + + /// Set the acknowledgment number. + #[inline] + pub fn set_ack(&self, buf: &mut [u8], ack: u32) -> Result<(), FieldError> { + ack.write(buf, self.index.start + offsets::ACK) + } + + /// Set the data offset (in 32-bit words). + #[inline] + pub fn set_data_offset(&self, buf: &mut [u8], offset: u8) -> Result<(), FieldError> { + let idx = self.index.start + offsets::DATA_OFFSET; + let current = u8::read(buf, idx)?; + let new_val = (current & 0x0F) | ((offset & 0x0F) << 4); + new_val.write(buf, idx) + } + + /// Alias for set_data_offset (Scapy compatibility). + #[inline] + pub fn set_dataofs(&self, buf: &mut [u8], offset: u8) -> Result<(), FieldError> { + self.set_data_offset(buf, offset) + } + + /// Set the reserved bits. + #[inline] + pub fn set_reserved(&self, buf: &mut [u8], reserved: u8) -> Result<(), FieldError> { + let idx = self.index.start + offsets::DATA_OFFSET; + let current = u8::read(buf, idx)?; + let new_val = (current & 0xF1) | ((reserved & 0x07) << 1); + new_val.write(buf, idx) + } + + /// Set the flags from raw byte. + #[inline] + pub fn set_flags_raw(&self, buf: &mut [u8], flags: u8) -> Result<(), FieldError> { + flags.write(buf, self.index.start + offsets::FLAGS) + } + + /// Set the flags from structured type. + #[inline] + pub fn set_flags(&self, buf: &mut [u8], flags: TcpFlags) -> Result<(), FieldError> { + // Set flags byte + flags + .to_byte() + .write(buf, self.index.start + offsets::FLAGS)?; + + // Set NS bit in data offset byte + let idx = self.index.start + offsets::DATA_OFFSET; + let current = u8::read(buf, idx)?; + let new_val = (current & 0xFE) | flags.ns_bit(); + new_val.write(buf, idx) + } + + /// Set the window size. + #[inline] + pub fn set_window(&self, buf: &mut [u8], window: u16) -> Result<(), FieldError> { + window.write(buf, self.index.start + offsets::WINDOW) + } + + /// Set the checksum. + #[inline] + pub fn set_checksum(&self, buf: &mut [u8], checksum: u16) -> Result<(), FieldError> { + checksum.write(buf, self.index.start + offsets::CHECKSUM) + } + + /// Alias for set_checksum (Scapy compatibility). + #[inline] + pub fn set_chksum(&self, buf: &mut [u8], checksum: u16) -> Result<(), FieldError> { + self.set_checksum(buf, checksum) + } + + /// Set the urgent pointer. + #[inline] + pub fn set_urgent_ptr(&self, buf: &mut [u8], urgptr: u16) -> Result<(), FieldError> { + urgptr.write(buf, self.index.start + offsets::URG_PTR) + } + + /// Alias for set_urgent_ptr (Scapy compatibility). + #[inline] + pub fn set_urgptr(&self, buf: &mut [u8], urgptr: u16) -> Result<(), FieldError> { + self.set_urgent_ptr(buf, urgptr) + } + + // ========== Dynamic Field Access ========== + + /// Get a field value by name. + pub fn get_field(&self, buf: &[u8], name: &str) -> Option> { + match name { + "sport" | "src_port" => Some(self.src_port(buf).map(FieldValue::U16)), + "dport" | "dst_port" => Some(self.dst_port(buf).map(FieldValue::U16)), + "seq" => Some(self.seq(buf).map(FieldValue::U32)), + "ack" => Some(self.ack(buf).map(FieldValue::U32)), + "dataofs" | "data_offset" => Some(self.data_offset(buf).map(FieldValue::U8)), + "reserved" => Some(self.reserved(buf).map(FieldValue::U8)), + "flags" => Some(self.flags_raw(buf).map(FieldValue::U8)), + "window" => Some(self.window(buf).map(FieldValue::U16)), + "chksum" | "checksum" => Some(self.checksum(buf).map(FieldValue::U16)), + "urgptr" | "urgent_ptr" => Some(self.urgent_ptr(buf).map(FieldValue::U16)), + _ => None, + } + } + + /// Set a field value by name. + pub fn set_field( + &self, + buf: &mut [u8], + name: &str, + value: FieldValue, + ) -> Option> { + match (name, value) { + ("sport" | "src_port", FieldValue::U16(v)) => Some(self.set_src_port(buf, v)), + ("dport" | "dst_port", FieldValue::U16(v)) => Some(self.set_dst_port(buf, v)), + ("seq", FieldValue::U32(v)) => Some(self.set_seq(buf, v)), + ("ack", FieldValue::U32(v)) => Some(self.set_ack(buf, v)), + ("dataofs" | "data_offset", FieldValue::U8(v)) => Some(self.set_data_offset(buf, v)), + ("reserved", FieldValue::U8(v)) => Some(self.set_reserved(buf, v)), + ("flags", FieldValue::U8(v)) => Some(self.set_flags_raw(buf, v)), + ("window", FieldValue::U16(v)) => Some(self.set_window(buf, v)), + ("chksum" | "checksum", FieldValue::U16(v)) => Some(self.set_checksum(buf, v)), + ("urgptr" | "urgent_ptr", FieldValue::U16(v)) => Some(self.set_urgent_ptr(buf, v)), + _ => None, + } + } + + /// Get list of field names. + pub fn field_names() -> &'static [&'static str] { + &[ + "sport", "dport", "seq", "ack", "dataofs", "reserved", "flags", "window", "chksum", + "urgptr", + ] + } + + // ========== Utility Methods ========== + + /// Get the payload length from IP total length. + /// Note: TCP doesn't have its own length field; it relies on IP layer. + pub fn payload_len(&self, buf: &[u8], ip_payload_len: usize) -> usize { + let header_len = self.calculate_header_len(buf); + ip_payload_len.saturating_sub(header_len) + } + + /// Get a slice of the payload data. + pub fn payload<'a>(&self, buf: &'a [u8]) -> &'a [u8] { + let header_len = self.calculate_header_len(buf); + let payload_start = self.index.start + header_len; + + if payload_start > buf.len() { + return &[]; + } + + &buf[payload_start..] + } + + /// Get the header bytes. + #[inline] + pub fn header_bytes<'a>(&self, buf: &'a [u8]) -> &'a [u8] { + let header_len = self.calculate_header_len(buf); + let end = (self.index.start + header_len).min(buf.len()); + &buf[self.index.start..end] + } + + /// Get the source port service name. + pub fn src_service(&self, buf: &[u8]) -> &'static str { + self.src_port(buf) + .map(services::service_name) + .unwrap_or("unknown") + } + + /// Get the destination port service name. + pub fn dst_service(&self, buf: &[u8]) -> &'static str { + self.dst_port(buf) + .map(services::service_name) + .unwrap_or("unknown") + } + + /// Compute hash for packet matching (like Scapy's hashret). + pub fn hashret(&self, buf: &[u8]) -> Vec { + let sport = self.src_port(buf).unwrap_or(0); + let dport = self.dst_port(buf).unwrap_or(0); + + // XOR the ports + let xored = sport ^ dport; + + // Return as bytes + xored.to_be_bytes().to_vec() + } + + /// Check if this packet answers another (for sr() matching). + pub fn answers(&self, buf: &[u8], other: &TcpLayer, other_buf: &[u8]) -> bool { + let self_flags = self.flags(buf).unwrap_or(TcpFlags::NONE); + let other_flags = other.flags(other_buf).unwrap_or(TcpFlags::NONE); + + // RST packets don't get answers + if other_flags.rst { + return false; + } + + // SYN packets without ACK don't answer anything + if self_flags.syn && !self_flags.ack { + return false; + } + + // SYN+ACK answers SYN + if self_flags.syn && self_flags.ack { + if !other_flags.syn { + return false; + } + } + + // Check ports + let self_sport = self.src_port(buf).unwrap_or(0); + let self_dport = self.dst_port(buf).unwrap_or(0); + let other_sport = other.src_port(other_buf).unwrap_or(0); + let other_dport = other.dst_port(other_buf).unwrap_or(0); + + if self_sport != other_dport || self_dport != other_sport { + return false; + } + + // Check sequence/ack numbers (with tolerance) + let self_seq = self.seq(buf).unwrap_or(0); + let self_ack = self.ack(buf).unwrap_or(0); + let other_seq = other.seq(other_buf).unwrap_or(0); + let other_ack = other.ack(other_buf).unwrap_or(0); + + // For SYN packets without ACK, don't check ack value + if !(other_flags.syn && !other_flags.ack) { + let diff = if other_ack > self_seq { + other_ack - self_seq + } else { + self_seq - other_ack + }; + if diff > 2 { + return false; + } + } + + // For RST packets without ACK, skip remaining checks + if self_flags.rst && !self_flags.ack { + return true; + } + + // Check ack vs seq with payload length tolerance + let other_payload_len = other.payload(other_buf).len() as u32; + let diff = if other_seq > self_ack { + other_seq - self_ack + } else { + self_ack - other_seq + }; + if diff > 2 + other_payload_len { + return false; + } + + true + } +} + +impl Layer for TcpLayer { + fn kind(&self) -> LayerKind { + LayerKind::Tcp + } + + fn summary(&self, buf: &[u8]) -> String { + let sport = self.src_port(buf).unwrap_or(0); + let dport = self.dst_port(buf).unwrap_or(0); + let flags = self.flags(buf).unwrap_or(TcpFlags::NONE); + + format!("TCP {} > {} {}", sport, dport, flags) + } + + fn header_len(&self, buf: &[u8]) -> usize { + self.calculate_header_len(buf) + } + + fn hashret(&self, buf: &[u8]) -> Vec { + self.hashret(buf) + } + + fn answers(&self, buf: &[u8], other: &Self, other_buf: &[u8]) -> bool { + self.answers(buf, other, other_buf) + } + + fn field_names(&self) -> &'static [&'static str] { + Self::field_names() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn sample_tcp_header() -> Vec { + vec![ + 0x00, 0x50, // Source port: 80 + 0x1F, 0x90, // Dest port: 8080 + 0x00, 0x00, 0x00, 0x01, // Seq: 1 + 0x00, 0x00, 0x00, 0x00, // Ack: 0 + 0x50, // Data offset: 5 (20 bytes), Reserved: 0, NS: 0 + 0x02, // Flags: SYN + 0xFF, 0xFF, // Window: 65535 + 0x00, 0x00, // Checksum (to be computed) + 0x00, 0x00, // Urgent pointer: 0 + ] + } + + #[test] + fn test_field_readers() { + let buf = sample_tcp_header(); + let layer = TcpLayer::at_offset(0); + + assert_eq!(layer.src_port(&buf).unwrap(), 80); + assert_eq!(layer.dst_port(&buf).unwrap(), 8080); + assert_eq!(layer.seq(&buf).unwrap(), 1); + assert_eq!(layer.ack(&buf).unwrap(), 0); + assert_eq!(layer.data_offset(&buf).unwrap(), 5); + assert_eq!(layer.window(&buf).unwrap(), 65535); + assert_eq!(layer.urgent_ptr(&buf).unwrap(), 0); + + let flags = layer.flags(&buf).unwrap(); + assert!(flags.syn); + assert!(!flags.ack); + } + + #[test] + fn test_field_writers() { + let mut buf = sample_tcp_header(); + let layer = TcpLayer::at_offset(0); + + layer.set_src_port(&mut buf, 12345).unwrap(); + assert_eq!(layer.src_port(&buf).unwrap(), 12345); + + layer.set_dst_port(&mut buf, 443).unwrap(); + assert_eq!(layer.dst_port(&buf).unwrap(), 443); + + layer.set_seq(&mut buf, 0x12345678).unwrap(); + assert_eq!(layer.seq(&buf).unwrap(), 0x12345678); + + layer.set_ack(&mut buf, 0xABCDEF00).unwrap(); + assert_eq!(layer.ack(&buf).unwrap(), 0xABCDEF00); + + layer.set_flags(&mut buf, TcpFlags::SA).unwrap(); + let flags = layer.flags(&buf).unwrap(); + assert!(flags.syn); + assert!(flags.ack); + } + + #[test] + fn test_flags() { + let mut buf = sample_tcp_header(); + let layer = TcpLayer::at_offset(0); + + // Test NS flag + let mut flags = TcpFlags::SA; + flags.ns = true; + layer.set_flags(&mut buf, flags).unwrap(); + + let read_flags = layer.flags(&buf).unwrap(); + assert!(read_flags.syn); + assert!(read_flags.ack); + assert!(read_flags.ns); + } + + #[test] + fn test_header_len() { + let buf = sample_tcp_header(); + let layer = TcpLayer::at_offset(0); + + assert_eq!(layer.calculate_header_len(&buf), 20); + + // Test with options (data offset = 6 = 24 bytes) + let mut buf_with_opts = sample_tcp_header(); + buf_with_opts[12] = 0x60; // Data offset = 6 + buf_with_opts.extend_from_slice(&[0, 0, 0, 0]); // 4 bytes of options/padding + + assert_eq!(layer.calculate_header_len(&buf_with_opts), 24); + } + + #[test] + fn test_validate() { + let buf = sample_tcp_header(); + assert!(TcpLayer::validate(&buf, 0).is_ok()); + + // Too short + let short = vec![0x00, 0x50]; + assert!(TcpLayer::validate(&short, 0).is_err()); + + // Invalid data offset + let mut bad_doff = sample_tcp_header(); + bad_doff[12] = 0x30; // Data offset = 3 (< minimum 5) + assert!(TcpLayer::validate(&bad_doff, 0).is_err()); + } + + #[test] + fn test_summary() { + let buf = sample_tcp_header(); + let layer = TcpLayer::at_offset(0); + + let summary = layer.summary(&buf); + assert!(summary.contains("80")); + assert!(summary.contains("8080")); + assert!(summary.contains("S")); // SYN flag + } + + #[test] + fn test_at_offset_dynamic() { + let buf = sample_tcp_header(); + let layer = TcpLayer::at_offset_dynamic(&buf, 0).unwrap(); + + assert_eq!(layer.index.start, 0); + assert_eq!(layer.index.end, 20); + } + + #[test] + fn test_payload() { + let mut buf = sample_tcp_header(); + buf.extend_from_slice(b"Hello, TCP!"); + + let layer = TcpLayer::at_offset(0); + let payload = layer.payload(&buf); + + assert_eq!(payload, b"Hello, TCP!"); + } +} diff --git a/crates/stackforge-core/src/layer/tcp/mod.rs b/crates/stackforge-core/src/layer/tcp/mod.rs new file mode 100644 index 0000000..0d618a4 --- /dev/null +++ b/crates/stackforge-core/src/layer/tcp/mod.rs @@ -0,0 +1,49 @@ +//! TCP (Transmission Control Protocol) layer module. +//! +//! This module implements the TCP protocol (RFC 793), providing packet parsing (via `TcpLayer`), +//! construction (via `TcpBuilder`), options handling, and checksum verification. +//! +//! # Features +//! +//! - Zero-copy parsing with lazy field access +//! - Complete TCP options support (MSS, Window Scale, SACK, Timestamps, etc.) +//! - TCP-AO (Authentication Option) support per RFC 5925 +//! - Checksum calculation with IPv4/IPv6 pseudo-header +//! - Service name resolution for well-known ports +//! - Builder pattern for packet construction +//! +//! # Example +//! +//! ```rust +//! use stackforge_core::layer::tcp::{TcpBuilder, TcpFlags}; +//! +//! // Build a SYN packet +//! let packet = TcpBuilder::new() +//! .src_port(12345) +//! .dst_port(80) +//! .seq(1000) +//! .syn() +//! .window(65535) +//! .mss(1460) +//! .build(); +//! ``` + +// Submodules +pub mod builder; +pub mod checksum; +pub mod flags; +pub mod header; +pub mod options; +pub mod services; + +// Re-export primary types for easier access +pub use builder::TcpBuilder; +pub use checksum::{tcp_checksum, tcp_checksum_ipv4, verify_tcp_checksum}; +pub use flags::TcpFlags; +pub use header::{ + FIELDS as TCP_FIELDS, TCP_MAX_HEADER_LEN, TCP_MIN_HEADER_LEN, TcpLayer, offsets as tcp_offsets, +}; +pub use options::{ + TcpAoValue, TcpOption, TcpOptionKind, TcpOptions, TcpOptionsBuilder, TcpSackBlock, TcpTimestamp, +}; +pub use services::{TCP_SERVICES, service_name, service_port}; diff --git a/crates/stackforge-core/src/layer/tcp/options.rs b/crates/stackforge-core/src/layer/tcp/options.rs new file mode 100644 index 0000000..e4a3106 --- /dev/null +++ b/crates/stackforge-core/src/layer/tcp/options.rs @@ -0,0 +1,1056 @@ +//! TCP options parsing and building. +//! +//! TCP options follow the header and are variable-length. +//! Maximum options length is 40 bytes (60 byte header - 20 byte minimum). +//! +//! # Option Format +//! +//! Single-byte options (EOL, NOP): +//! ```text +//! +--------+ +//! | Kind | +//! +--------+ +//! ``` +//! +//! Multi-byte options: +//! ```text +//! +--------+--------+--------... +//! | Kind | Length | Data +//! +--------+--------+--------... +//! ``` + +use crate::layer::field::FieldError; + +/// Maximum length of TCP options (in bytes). +pub const MAX_OPTIONS_LEN: usize = 40; + +/// TCP option kinds. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum TcpOptionKind { + /// End of Option List (RFC 793) + Eol = 0, + /// No Operation (RFC 793) + Nop = 1, + /// Maximum Segment Size (RFC 793) + Mss = 2, + /// Window Scale (RFC 7323) + WScale = 3, + /// SACK Permitted (RFC 2018) + SackOk = 4, + /// SACK (RFC 2018) + Sack = 5, + /// Timestamps (RFC 7323) + Timestamp = 8, + /// Alternate Checksum Request (RFC 1146) + AltChkSum = 14, + /// Alternate Checksum Data (RFC 1146) + AltChkSumOpt = 15, + /// MD5 Signature (RFC 2385) + Md5 = 19, + /// Mood (RFC 5841) - April Fools + Mood = 25, + /// User Timeout Option (RFC 5482) + Uto = 28, + /// Authentication Option (RFC 5925) + Ao = 29, + /// TCP Fast Open (RFC 7413) + Tfo = 34, + /// Unknown option + Unknown(u8), +} + +impl TcpOptionKind { + /// Create from raw option kind byte. + pub fn from_byte(b: u8) -> Self { + match b { + 0 => Self::Eol, + 1 => Self::Nop, + 2 => Self::Mss, + 3 => Self::WScale, + 4 => Self::SackOk, + 5 => Self::Sack, + 8 => Self::Timestamp, + 14 => Self::AltChkSum, + 15 => Self::AltChkSumOpt, + 19 => Self::Md5, + 25 => Self::Mood, + 28 => Self::Uto, + 29 => Self::Ao, + 34 => Self::Tfo, + x => Self::Unknown(x), + } + } + + /// Convert to raw option kind byte. + pub fn to_byte(self) -> u8 { + match self { + Self::Eol => 0, + Self::Nop => 1, + Self::Mss => 2, + Self::WScale => 3, + Self::SackOk => 4, + Self::Sack => 5, + Self::Timestamp => 8, + Self::AltChkSum => 14, + Self::AltChkSumOpt => 15, + Self::Md5 => 19, + Self::Mood => 25, + Self::Uto => 28, + Self::Ao => 29, + Self::Tfo => 34, + Self::Unknown(x) => x, + } + } + + /// Get the name of the option. + pub fn name(&self) -> &'static str { + match self { + Self::Eol => "EOL", + Self::Nop => "NOP", + Self::Mss => "MSS", + Self::WScale => "WScale", + Self::SackOk => "SAckOK", + Self::Sack => "SAck", + Self::Timestamp => "Timestamp", + Self::AltChkSum => "AltChkSum", + Self::AltChkSumOpt => "AltChkSumOpt", + Self::Md5 => "MD5", + Self::Mood => "Mood", + Self::Uto => "UTO", + Self::Ao => "AO", + Self::Tfo => "TFO", + Self::Unknown(_) => "Unknown", + } + } + + /// Check if this is a single-byte option (no length/data). + #[inline] + pub fn is_single_byte(&self) -> bool { + matches!(self, Self::Eol | Self::Nop) + } + + /// Get the expected fixed length for this option (if any). + /// Returns None for variable-length options. + pub fn expected_len(&self) -> Option { + match self { + Self::Eol | Self::Nop => Some(1), + Self::Mss => Some(4), + Self::WScale => Some(3), + Self::SackOk => Some(2), + Self::Timestamp => Some(10), + Self::AltChkSum => Some(4), // Variable but typical + Self::AltChkSumOpt => Some(2), + Self::Md5 => Some(18), + Self::Uto => Some(4), + Self::Tfo => Some(10), // Can vary (2-18) + Self::Sack | Self::Ao | Self::Mood | Self::Unknown(_) => None, + } + } +} + +impl std::fmt::Display for TcpOptionKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Unknown(x) => write!(f, "Unknown({})", x), + _ => write!(f, "{}", self.name()), + } + } +} + +/// TCP SACK block (pair of sequence numbers). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct TcpSackBlock { + /// Left edge of block + pub left: u32, + /// Right edge of block + pub right: u32, +} + +impl TcpSackBlock { + /// Create a new SACK block. + pub fn new(left: u32, right: u32) -> Self { + Self { left, right } + } +} + +/// TCP Timestamp option data. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct TcpTimestamp { + /// Timestamp value + pub ts_val: u32, + /// Timestamp echo reply + pub ts_ecr: u32, +} + +impl TcpTimestamp { + /// Create a new timestamp. + pub fn new(ts_val: u32, ts_ecr: u32) -> Self { + Self { ts_val, ts_ecr } + } +} + +/// TCP Authentication Option (AO) value per RFC 5925. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TcpAoValue { + /// Key ID + pub key_id: u8, + /// Receive next key ID + pub rnext_key_id: u8, + /// MAC (Message Authentication Code) + pub mac: Vec, +} + +impl TcpAoValue { + /// Create a new AO value. + pub fn new(key_id: u8, rnext_key_id: u8, mac: Vec) -> Self { + Self { + key_id, + rnext_key_id, + mac, + } + } +} + +/// A parsed TCP option. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TcpOption { + /// End of Option List (kind 0) + Eol, + + /// No Operation / Padding (kind 1) + Nop, + + /// Maximum Segment Size (kind 2) + Mss(u16), + + /// Window Scale (kind 3) + WScale(u8), + + /// SACK Permitted (kind 4) + SackOk, + + /// Selective Acknowledgment (kind 5) + Sack(Vec), + + /// Timestamps (kind 8) + Timestamp(TcpTimestamp), + + /// Alternate Checksum Request (kind 14) + AltChkSum { algorithm: u8, checksum: u16 }, + + /// Alternate Checksum Data (kind 15) + AltChkSumOpt, + + /// MD5 Signature (kind 19) + Md5([u8; 16]), + + /// Mood (kind 25) - RFC 5841 + Mood(String), + + /// User Timeout Option (kind 28) + Uto(u16), + + /// Authentication Option (kind 29) + Ao(TcpAoValue), + + /// TCP Fast Open (kind 34) + Tfo { + /// TFO cookie (optional) + cookie: Option>, + }, + + /// Unknown or unimplemented option + Unknown { kind: u8, data: Vec }, +} + +impl TcpOption { + /// Get the option kind. + pub fn kind(&self) -> TcpOptionKind { + match self { + Self::Eol => TcpOptionKind::Eol, + Self::Nop => TcpOptionKind::Nop, + Self::Mss(_) => TcpOptionKind::Mss, + Self::WScale(_) => TcpOptionKind::WScale, + Self::SackOk => TcpOptionKind::SackOk, + Self::Sack(_) => TcpOptionKind::Sack, + Self::Timestamp(_) => TcpOptionKind::Timestamp, + Self::AltChkSum { .. } => TcpOptionKind::AltChkSum, + Self::AltChkSumOpt => TcpOptionKind::AltChkSumOpt, + Self::Md5(_) => TcpOptionKind::Md5, + Self::Mood(_) => TcpOptionKind::Mood, + Self::Uto(_) => TcpOptionKind::Uto, + Self::Ao(_) => TcpOptionKind::Ao, + Self::Tfo { .. } => TcpOptionKind::Tfo, + Self::Unknown { kind, .. } => TcpOptionKind::Unknown(*kind), + } + } + + /// Get the serialized length of this option. + pub fn len(&self) -> usize { + match self { + Self::Eol => 1, + Self::Nop => 1, + Self::Mss(_) => 4, + Self::WScale(_) => 3, + Self::SackOk => 2, + Self::Sack(blocks) => 2 + blocks.len() * 8, + Self::Timestamp(_) => 10, + Self::AltChkSum { .. } => 4, + Self::AltChkSumOpt => 2, + Self::Md5(_) => 18, + Self::Mood(s) => 2 + s.len(), + Self::Uto(_) => 4, + Self::Ao(ao) => 4 + ao.mac.len(), + Self::Tfo { cookie: None } => 2, + Self::Tfo { cookie: Some(c) } => 2 + c.len(), + Self::Unknown { data, .. } => 2 + data.len(), + } + } + + /// Check if the option has data (for variants with data). + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Serialize the option to bytes. + pub fn to_bytes(&self) -> Vec { + match self { + Self::Eol => vec![0], + Self::Nop => vec![1], + + Self::Mss(mss) => { + let mut buf = vec![2, 4]; + buf.extend_from_slice(&mss.to_be_bytes()); + buf + } + + Self::WScale(scale) => vec![3, 3, *scale], + + Self::SackOk => vec![4, 2], + + Self::Sack(blocks) => { + let mut buf = vec![5, (2 + blocks.len() * 8) as u8]; + for block in blocks { + buf.extend_from_slice(&block.left.to_be_bytes()); + buf.extend_from_slice(&block.right.to_be_bytes()); + } + buf + } + + Self::Timestamp(ts) => { + let mut buf = vec![8, 10]; + buf.extend_from_slice(&ts.ts_val.to_be_bytes()); + buf.extend_from_slice(&ts.ts_ecr.to_be_bytes()); + buf + } + + Self::AltChkSum { + algorithm, + checksum, + } => { + let mut buf = vec![14, 4, *algorithm]; + buf.extend_from_slice(&checksum.to_be_bytes()); + buf + } + + Self::AltChkSumOpt => vec![15, 2], + + Self::Md5(sig) => { + let mut buf = vec![19, 18]; + buf.extend_from_slice(sig); + buf + } + + Self::Mood(mood) => { + let mut buf = vec![25, (2 + mood.len()) as u8]; + buf.extend_from_slice(mood.as_bytes()); + buf + } + + Self::Uto(timeout) => { + let mut buf = vec![28, 4]; + buf.extend_from_slice(&timeout.to_be_bytes()); + buf + } + + Self::Ao(ao) => { + let len = 4 + ao.mac.len(); + let mut buf = vec![29, len as u8, ao.key_id, ao.rnext_key_id]; + buf.extend_from_slice(&ao.mac); + buf + } + + Self::Tfo { cookie: None } => vec![34, 2], + + Self::Tfo { cookie: Some(c) } => { + let mut buf = vec![34, (2 + c.len()) as u8]; + buf.extend_from_slice(c); + buf + } + + Self::Unknown { kind, data } => { + let mut buf = vec![*kind, (2 + data.len()) as u8]; + buf.extend_from_slice(data); + buf + } + } + } + + /// Create an MSS option. + pub fn mss(mss: u16) -> Self { + Self::Mss(mss) + } + + /// Create a Window Scale option. + pub fn wscale(scale: u8) -> Self { + Self::WScale(scale) + } + + /// Create a Timestamp option. + pub fn timestamp(ts_val: u32, ts_ecr: u32) -> Self { + Self::Timestamp(TcpTimestamp::new(ts_val, ts_ecr)) + } + + /// Create a SACK option. + pub fn sack(blocks: Vec) -> Self { + Self::Sack(blocks) + } +} + +/// Collection of TCP options. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct TcpOptions { + pub options: Vec, +} + +impl TcpOptions { + /// Create empty options. + pub fn new() -> Self { + Self::default() + } + + /// Create from a list of options. + pub fn from_vec(options: Vec) -> Self { + Self { options } + } + + /// Check if there are no options. + pub fn is_empty(&self) -> bool { + self.options.is_empty() + } + + /// Get the number of options. + pub fn len(&self) -> usize { + self.options.len() + } + + /// Get the total serialized length. + pub fn byte_len(&self) -> usize { + self.options.iter().map(|o| o.len()).sum() + } + + /// Get the padded length (aligned to 4 bytes). + pub fn padded_len(&self) -> usize { + let len = self.byte_len(); + (len + 3) & !3 + } + + /// Add an option. + pub fn push(&mut self, option: TcpOption) { + self.options.push(option); + } + + /// Get an option by kind. + pub fn get(&self, kind: TcpOptionKind) -> Option<&TcpOption> { + self.options.iter().find(|o| o.kind() == kind) + } + + /// Get the MSS value if present. + pub fn mss(&self) -> Option { + self.options.iter().find_map(|o| match o { + TcpOption::Mss(mss) => Some(*mss), + _ => None, + }) + } + + /// Get the Window Scale value if present. + pub fn wscale(&self) -> Option { + self.options.iter().find_map(|o| match o { + TcpOption::WScale(scale) => Some(*scale), + _ => None, + }) + } + + /// Get the Timestamp if present. + pub fn timestamp(&self) -> Option { + self.options.iter().find_map(|o| match o { + TcpOption::Timestamp(ts) => Some(*ts), + _ => None, + }) + } + + /// Check if SACK is permitted. + pub fn sack_permitted(&self) -> bool { + self.options.iter().any(|o| matches!(o, TcpOption::SackOk)) + } + + /// Get the SACK blocks if present. + pub fn sack_blocks(&self) -> Option<&[TcpSackBlock]> { + self.options.iter().find_map(|o| match o { + TcpOption::Sack(blocks) => Some(blocks.as_slice()), + _ => None, + }) + } + + /// Get the Authentication Option if present. + pub fn ao(&self) -> Option<&TcpAoValue> { + self.options.iter().find_map(|o| match o { + TcpOption::Ao(ao) => Some(ao), + _ => None, + }) + } + + /// Get the TFO cookie if present. + pub fn tfo_cookie(&self) -> Option<&[u8]> { + self.options.iter().find_map(|o| match o { + TcpOption::Tfo { cookie: Some(c) } => Some(c.as_slice()), + _ => None, + }) + } + + /// Serialize all options to bytes (with padding). + pub fn to_bytes(&self) -> Vec { + let mut buf = Vec::new(); + for opt in &self.options { + buf.extend_from_slice(&opt.to_bytes()); + } + + // Pad to 4-byte boundary + let pad = (4 - (buf.len() % 4)) % 4; + buf.extend(std::iter::repeat(0u8).take(pad)); + + buf + } + + /// Serialize to bytes without padding. + pub fn to_bytes_unpadded(&self) -> Vec { + let mut buf = Vec::new(); + for opt in &self.options { + buf.extend_from_slice(&opt.to_bytes()); + } + buf + } +} + +impl IntoIterator for TcpOptions { + type Item = TcpOption; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.options.into_iter() + } +} + +impl<'a> IntoIterator for &'a TcpOptions { + type Item = &'a TcpOption; + type IntoIter = std::slice::Iter<'a, TcpOption>; + + fn into_iter(self) -> Self::IntoIter { + self.options.iter() + } +} + +/// Parse TCP options from bytes. +pub fn parse_options(data: &[u8]) -> Result { + let mut options = Vec::new(); + let mut offset = 0; + + while offset < data.len() { + let kind = data[offset]; + + match kind { + // End of Option List + 0 => { + options.push(TcpOption::Eol); + break; + } + + // NOP + 1 => { + options.push(TcpOption::Nop); + offset += 1; + } + + // Multi-byte options + _ => { + if offset + 1 >= data.len() { + return Err(FieldError::InvalidValue( + "option length field missing".to_string(), + )); + } + + let length = data[offset + 1] as usize; + if length < 2 { + return Err(FieldError::InvalidValue(format!( + "option length {} is less than minimum (2)", + length + ))); + } + + if offset + length > data.len() { + return Err(FieldError::BufferTooShort { + offset, + need: length, + have: data.len() - offset, + }); + } + + let opt_data = &data[offset..offset + length]; + let opt = parse_single_option(kind, opt_data)?; + options.push(opt); + + offset += length; + } + } + } + + Ok(TcpOptions { options }) +} + +/// Parse a single multi-byte option. +fn parse_single_option(kind: u8, data: &[u8]) -> Result { + let length = data[1] as usize; + let value = &data[2..length]; + + match kind { + // MSS + 2 => { + if length != 4 { + return Err(FieldError::InvalidValue(format!( + "MSS option length {} != 4", + length + ))); + } + let mss = u16::from_be_bytes([value[0], value[1]]); + Ok(TcpOption::Mss(mss)) + } + + // Window Scale + 3 => { + if length != 3 { + return Err(FieldError::InvalidValue(format!( + "WScale option length {} != 3", + length + ))); + } + Ok(TcpOption::WScale(value[0])) + } + + // SACK Permitted + 4 => { + if length != 2 { + return Err(FieldError::InvalidValue(format!( + "SAckOK option length {} != 2", + length + ))); + } + Ok(TcpOption::SackOk) + } + + // SACK + 5 => { + let block_count = value.len() / 8; + let mut blocks = Vec::with_capacity(block_count); + + for chunk in value.chunks_exact(8) { + let left = u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); + let right = u32::from_be_bytes([chunk[4], chunk[5], chunk[6], chunk[7]]); + blocks.push(TcpSackBlock::new(left, right)); + } + + Ok(TcpOption::Sack(blocks)) + } + + // Timestamps + 8 => { + if length != 10 { + return Err(FieldError::InvalidValue(format!( + "Timestamp option length {} != 10", + length + ))); + } + let ts_val = u32::from_be_bytes([value[0], value[1], value[2], value[3]]); + let ts_ecr = u32::from_be_bytes([value[4], value[5], value[6], value[7]]); + Ok(TcpOption::Timestamp(TcpTimestamp::new(ts_val, ts_ecr))) + } + + // Alternate Checksum Request + 14 => { + if length < 3 { + return Err(FieldError::InvalidValue(format!( + "AltChkSum option length {} < 3", + length + ))); + } + let algorithm = value[0]; + let checksum = if value.len() >= 3 { + u16::from_be_bytes([value[1], value[2]]) + } else { + 0 + }; + Ok(TcpOption::AltChkSum { + algorithm, + checksum, + }) + } + + // Alternate Checksum Data + 15 => Ok(TcpOption::AltChkSumOpt), + + // MD5 Signature + 19 => { + if length != 18 { + return Err(FieldError::InvalidValue(format!( + "MD5 option length {} != 18", + length + ))); + } + let mut sig = [0u8; 16]; + sig.copy_from_slice(value); + Ok(TcpOption::Md5(sig)) + } + + // Mood + 25 => { + let mood = String::from_utf8_lossy(value).to_string(); + Ok(TcpOption::Mood(mood)) + } + + // User Timeout Option + 28 => { + if length != 4 { + return Err(FieldError::InvalidValue(format!( + "UTO option length {} != 4", + length + ))); + } + let timeout = u16::from_be_bytes([value[0], value[1]]); + Ok(TcpOption::Uto(timeout)) + } + + // Authentication Option + 29 => { + if length < 4 { + return Err(FieldError::InvalidValue(format!( + "AO option length {} < 4", + length + ))); + } + let key_id = value[0]; + let rnext_key_id = value[1]; + let mac = value[2..].to_vec(); + Ok(TcpOption::Ao(TcpAoValue::new(key_id, rnext_key_id, mac))) + } + + // TCP Fast Open + 34 => { + let cookie = if value.is_empty() { + None + } else { + Some(value.to_vec()) + }; + Ok(TcpOption::Tfo { cookie }) + } + + // Unknown option + _ => Ok(TcpOption::Unknown { + kind, + data: value.to_vec(), + }), + } +} + +/// Builder for TCP options. +#[derive(Debug, Clone, Default)] +pub struct TcpOptionsBuilder { + options: Vec, +} + +impl TcpOptionsBuilder { + /// Create a new builder. + pub fn new() -> Self { + Self::default() + } + + /// Add a NOP (padding). + pub fn nop(mut self) -> Self { + self.options.push(TcpOption::Nop); + self + } + + /// Add an End of Option List marker. + pub fn eol(mut self) -> Self { + self.options.push(TcpOption::Eol); + self + } + + /// Add an MSS option. + pub fn mss(mut self, mss: u16) -> Self { + self.options.push(TcpOption::Mss(mss)); + self + } + + /// Add a Window Scale option. + pub fn wscale(mut self, scale: u8) -> Self { + self.options.push(TcpOption::WScale(scale)); + self + } + + /// Add SACK Permitted option. + pub fn sack_ok(mut self) -> Self { + self.options.push(TcpOption::SackOk); + self + } + + /// Add a SACK option. + pub fn sack(mut self, blocks: Vec) -> Self { + self.options.push(TcpOption::Sack(blocks)); + self + } + + /// Add a Timestamp option. + pub fn timestamp(mut self, ts_val: u32, ts_ecr: u32) -> Self { + self.options + .push(TcpOption::Timestamp(TcpTimestamp::new(ts_val, ts_ecr))); + self + } + + /// Add a TFO (TCP Fast Open) option with cookie. + pub fn tfo(mut self, cookie: Option>) -> Self { + self.options.push(TcpOption::Tfo { cookie }); + self + } + + /// Add an Authentication Option. + pub fn ao(mut self, key_id: u8, rnext_key_id: u8, mac: Vec) -> Self { + self.options + .push(TcpOption::Ao(TcpAoValue::new(key_id, rnext_key_id, mac))); + self + } + + /// Add an MD5 signature option. + pub fn md5(mut self, signature: [u8; 16]) -> Self { + self.options.push(TcpOption::Md5(signature)); + self + } + + /// Add a custom option. + pub fn option(mut self, option: TcpOption) -> Self { + self.options.push(option); + self + } + + /// Build the options. + pub fn build(self) -> TcpOptions { + TcpOptions { + options: self.options, + } + } +} + +/// Get the TCP-AO (Authentication Option) from a parsed options list. +pub fn get_tcp_ao(options: &TcpOptions) -> Option<&TcpAoValue> { + options.ao() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_nop_eol() { + let data = [1, 1, 1, 0]; // 3 NOPs + EOL + let opts = parse_options(&data).unwrap(); + + assert_eq!(opts.len(), 4); + assert!(matches!(opts.options[0], TcpOption::Nop)); + assert!(matches!(opts.options[1], TcpOption::Nop)); + assert!(matches!(opts.options[2], TcpOption::Nop)); + assert!(matches!(opts.options[3], TcpOption::Eol)); + } + + #[test] + fn test_parse_mss() { + let data = [ + 2, 4, // MSS option, length 4 + 0x05, 0xB4, // MSS = 1460 + ]; + + let opts = parse_options(&data).unwrap(); + assert_eq!(opts.len(), 1); + assert_eq!(opts.mss(), Some(1460)); + } + + #[test] + fn test_parse_wscale() { + let data = [ + 3, 3, // WScale option, length 3 + 7, // scale = 7 + ]; + + let opts = parse_options(&data).unwrap(); + assert_eq!(opts.len(), 1); + assert_eq!(opts.wscale(), Some(7)); + } + + #[test] + fn test_parse_timestamp() { + let data = [ + 8, 10, // Timestamp option, length 10 + 0x00, 0x00, 0x10, 0x00, // ts_val + 0x00, 0x00, 0x20, 0x00, // ts_ecr + ]; + + let opts = parse_options(&data).unwrap(); + assert_eq!(opts.len(), 1); + + let ts = opts.timestamp().unwrap(); + assert_eq!(ts.ts_val, 0x1000); + assert_eq!(ts.ts_ecr, 0x2000); + } + + #[test] + fn test_parse_sack() { + let data = [ + 5, 18, // SACK option, length 18 (2 blocks) + 0x00, 0x00, 0x10, 0x00, // left1 + 0x00, 0x00, 0x20, 0x00, // right1 + 0x00, 0x00, 0x30, 0x00, // left2 + 0x00, 0x00, 0x40, 0x00, // right2 + ]; + + let opts = parse_options(&data).unwrap(); + assert_eq!(opts.len(), 1); + + let blocks = opts.sack_blocks().unwrap(); + assert_eq!(blocks.len(), 2); + assert_eq!(blocks[0].left, 0x1000); + assert_eq!(blocks[0].right, 0x2000); + assert_eq!(blocks[1].left, 0x3000); + assert_eq!(blocks[1].right, 0x4000); + } + + #[test] + fn test_parse_sack_ok() { + let data = [4, 2]; // SACK Permitted + + let opts = parse_options(&data).unwrap(); + assert_eq!(opts.len(), 1); + assert!(opts.sack_permitted()); + } + + #[test] + fn test_parse_ao() { + let data = [ + 29, 6, // AO option, length 6 + 1, // key_id + 2, // rnext_key_id + 0xAB, 0xCD, // MAC (2 bytes) + ]; + + let opts = parse_options(&data).unwrap(); + assert_eq!(opts.len(), 1); + + let ao = opts.ao().unwrap(); + assert_eq!(ao.key_id, 1); + assert_eq!(ao.rnext_key_id, 2); + assert_eq!(ao.mac, vec![0xAB, 0xCD]); + } + + #[test] + fn test_serialize_options() { + let opts = TcpOptionsBuilder::new() + .mss(1460) + .wscale(7) + .sack_ok() + .timestamp(1000, 2000) + .build(); + + let bytes = opts.to_bytes(); + + // Should be padded to 4-byte boundary + assert_eq!(bytes.len() % 4, 0); + + // Parse back + let parsed = parse_options(&bytes).unwrap(); + assert_eq!(parsed.mss(), Some(1460)); + assert_eq!(parsed.wscale(), Some(7)); + assert!(parsed.sack_permitted()); + + let ts = parsed.timestamp().unwrap(); + assert_eq!(ts.ts_val, 1000); + assert_eq!(ts.ts_ecr, 2000); + } + + #[test] + fn test_option_kind_properties() { + assert!(TcpOptionKind::Eol.is_single_byte()); + assert!(TcpOptionKind::Nop.is_single_byte()); + assert!(!TcpOptionKind::Mss.is_single_byte()); + + assert_eq!(TcpOptionKind::Mss.expected_len(), Some(4)); + assert_eq!(TcpOptionKind::Timestamp.expected_len(), Some(10)); + assert_eq!(TcpOptionKind::Sack.expected_len(), None); + } + + #[test] + fn test_typical_syn_options() { + // Typical SYN packet options: MSS, SACK Permitted, Timestamp, NOP, Window Scale + let opts = TcpOptionsBuilder::new() + .mss(1460) + .sack_ok() + .timestamp(12345, 0) + .nop() + .wscale(7) + .build(); + + let bytes = opts.to_bytes(); + + // Parse back and verify + let parsed = parse_options(&bytes).unwrap(); + assert_eq!(parsed.mss(), Some(1460)); + assert!(parsed.sack_permitted()); + assert_eq!(parsed.wscale(), Some(7)); + + let ts = parsed.timestamp().unwrap(); + assert_eq!(ts.ts_val, 12345); + assert_eq!(ts.ts_ecr, 0); + } + + #[test] + fn test_tfo_option() { + let cookie = vec![0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]; + let opts = TcpOptionsBuilder::new().tfo(Some(cookie.clone())).build(); + + let bytes = opts.to_bytes(); + let parsed = parse_options(&bytes).unwrap(); + + assert_eq!(parsed.tfo_cookie(), Some(cookie.as_slice())); + } + + #[test] + fn test_md5_option() { + let sig = [1u8; 16]; + let opts = TcpOptionsBuilder::new().md5(sig).build(); + + let bytes = opts.to_bytes(); + let parsed = parse_options(&bytes).unwrap(); + + if let Some(TcpOption::Md5(parsed_sig)) = parsed.get(TcpOptionKind::Md5) { + assert_eq!(*parsed_sig, sig); + } else { + panic!("Expected MD5 option"); + } + } +} diff --git a/crates/stackforge-core/src/layer/tcp/services.rs b/crates/stackforge-core/src/layer/tcp/services.rs new file mode 100644 index 0000000..f2515e5 --- /dev/null +++ b/crates/stackforge-core/src/layer/tcp/services.rs @@ -0,0 +1,794 @@ +//! TCP services (port to name mapping). +//! +//! This module provides mappings between well-known TCP port numbers +//! and their service names, similar to Scapy's TCP_SERVICES. + +use std::collections::HashMap; +use std::sync::LazyLock; + +/// Well-known TCP services mapping (port -> name). +pub static TCP_SERVICES: LazyLock> = LazyLock::new(|| { + let mut m = HashMap::new(); + + // System ports (0-1023) + m.insert(1, "tcpmux"); + m.insert(5, "rje"); + m.insert(7, "echo"); + m.insert(9, "discard"); + m.insert(11, "systat"); + m.insert(13, "daytime"); + m.insert(17, "qotd"); + m.insert(18, "msp"); + m.insert(19, "chargen"); + m.insert(20, "ftp-data"); + m.insert(21, "ftp"); + m.insert(22, "ssh"); + m.insert(23, "telnet"); + m.insert(25, "smtp"); + m.insert(37, "time"); + m.insert(39, "rlp"); + m.insert(42, "nameserver"); + m.insert(43, "whois"); + m.insert(49, "tacacs"); + m.insert(50, "re-mail-ck"); + m.insert(53, "domain"); + m.insert(63, "whois++"); + m.insert(67, "bootps"); + m.insert(68, "bootpc"); + m.insert(69, "tftp"); + m.insert(70, "gopher"); + m.insert(71, "netrjs-1"); + m.insert(72, "netrjs-2"); + m.insert(73, "netrjs-3"); + m.insert(74, "netrjs-4"); + m.insert(79, "finger"); + m.insert(80, "http"); + m.insert(81, "hosts2-ns"); + m.insert(82, "xfer"); + m.insert(83, "mit-ml-dev"); + m.insert(84, "ctf"); + m.insert(85, "mit-ml-dev"); + m.insert(86, "mfcobol"); + m.insert(88, "kerberos"); + m.insert(89, "su-mit-tg"); + m.insert(90, "dnsix"); + m.insert(91, "mit-dov"); + m.insert(92, "npp"); + m.insert(93, "dcp"); + m.insert(94, "objcall"); + m.insert(95, "supdup"); + m.insert(96, "dixie"); + m.insert(97, "swift-rvf"); + m.insert(98, "tacnews"); + m.insert(99, "metagram"); + m.insert(101, "hostname"); + m.insert(102, "iso-tsap"); + m.insert(103, "gppitnp"); + m.insert(104, "acr-nema"); + m.insert(105, "csnet-ns"); + m.insert(106, "3com-tsmux"); + m.insert(107, "rtelnet"); + m.insert(108, "snagas"); + m.insert(109, "pop2"); + m.insert(110, "pop3"); + m.insert(111, "sunrpc"); + m.insert(112, "mcidas"); + m.insert(113, "auth"); + m.insert(115, "sftp"); + m.insert(117, "uucp-path"); + m.insert(118, "sqlserv"); + m.insert(119, "nntp"); + m.insert(120, "cfdptkt"); + m.insert(121, "erpc"); + m.insert(122, "smakynet"); + m.insert(123, "ntp"); + m.insert(124, "ansatrader"); + m.insert(125, "locus-map"); + m.insert(126, "unitary"); + m.insert(127, "locus-con"); + m.insert(128, "gss-xlicen"); + m.insert(129, "pwdgen"); + m.insert(130, "cisco-fna"); + m.insert(131, "cisco-tna"); + m.insert(132, "cisco-sys"); + m.insert(133, "statsrv"); + m.insert(134, "ingres-net"); + m.insert(135, "msrpc"); + m.insert(136, "profile"); + m.insert(137, "netbios-ns"); + m.insert(138, "netbios-dgm"); + m.insert(139, "netbios-ssn"); + m.insert(140, "emfis-data"); + m.insert(141, "emfis-cntl"); + m.insert(142, "bl-idm"); + m.insert(143, "imap"); + m.insert(144, "uma"); + m.insert(145, "uaac"); + m.insert(146, "iso-tp0"); + m.insert(147, "iso-ip"); + m.insert(148, "jargon"); + m.insert(149, "aed-512"); + m.insert(150, "sql-net"); + m.insert(151, "hems"); + m.insert(152, "bftp"); + m.insert(153, "sgmp"); + m.insert(154, "netsc-prod"); + m.insert(155, "netsc-dev"); + m.insert(156, "sqlsrv"); + m.insert(157, "knet-cmp"); + m.insert(158, "pcmail-srv"); + m.insert(159, "nss-routing"); + m.insert(160, "sgmp-traps"); + m.insert(161, "snmp"); + m.insert(162, "snmptrap"); + m.insert(163, "cmip-man"); + m.insert(164, "cmip-agent"); + m.insert(165, "xns-courier"); + m.insert(166, "s-net"); + m.insert(167, "namp"); + m.insert(168, "rsvd"); + m.insert(169, "send"); + m.insert(170, "print-srv"); + m.insert(171, "multiplex"); + m.insert(172, "cl/1"); + m.insert(173, "xyplex-mux"); + m.insert(174, "mailq"); + m.insert(175, "vmnet"); + m.insert(176, "genrad-mux"); + m.insert(177, "xdmcp"); + m.insert(178, "nextstep"); + m.insert(179, "bgp"); + m.insert(180, "ris"); + m.insert(181, "unify"); + m.insert(182, "audit"); + m.insert(183, "ocbinder"); + m.insert(184, "ocserver"); + m.insert(185, "remote-kis"); + m.insert(186, "kis"); + m.insert(187, "aci"); + m.insert(188, "mumps"); + m.insert(189, "qft"); + m.insert(190, "gacp"); + m.insert(191, "prospero"); + m.insert(192, "osu-nms"); + m.insert(193, "srmp"); + m.insert(194, "irc"); + m.insert(195, "dn6-nlm-aud"); + m.insert(196, "dn6-smm-red"); + m.insert(197, "dls"); + m.insert(198, "dls-mon"); + m.insert(199, "smux"); + m.insert(200, "src"); + m.insert(201, "at-rtmp"); + m.insert(202, "at-nbp"); + m.insert(203, "at-3"); + m.insert(204, "at-echo"); + m.insert(205, "at-5"); + m.insert(206, "at-zis"); + m.insert(207, "at-7"); + m.insert(208, "at-8"); + m.insert(209, "qmtp"); + m.insert(210, "z39.50"); + m.insert(211, "914c/g"); + m.insert(212, "anet"); + m.insert(213, "ipx"); + m.insert(214, "vmpwscs"); + m.insert(215, "softpc"); + m.insert(216, "CAIlic"); + m.insert(217, "dbase"); + m.insert(218, "mpp"); + m.insert(219, "uarps"); + m.insert(220, "imap3"); + m.insert(221, "fln-spx"); + m.insert(222, "rsh-spx"); + m.insert(223, "cdc"); + m.insert(224, "masqdialer"); + m.insert(242, "direct"); + m.insert(243, "sur-meas"); + m.insert(244, "dayna"); + m.insert(245, "link"); + m.insert(246, "dsp3270"); + m.insert(247, "subntbcst_tftp"); + m.insert(248, "bhfhs"); + m.insert(256, "rap"); + m.insert(257, "set"); + m.insert(259, "esro-gen"); + m.insert(260, "openport"); + m.insert(261, "nsiiops"); + m.insert(262, "arcisdms"); + m.insert(263, "hdap"); + m.insert(264, "bgmp"); + m.insert(280, "http-mgmt"); + m.insert(281, "personal-link"); + m.insert(282, "cableport-ax"); + m.insert(283, "rescap"); + m.insert(284, "corerjd"); + m.insert(286, "fxp-1"); + m.insert(287, "k-block"); + m.insert(308, "novastorbakcup"); + m.insert(309, "entrusttime"); + m.insert(310, "bhmds"); + m.insert(311, "asip-webadmin"); + m.insert(312, "vslmp"); + m.insert(313, "magenta-logic"); + m.insert(314, "opalis-robot"); + m.insert(315, "dpsi"); + m.insert(316, "decauth"); + m.insert(317, "zannet"); + m.insert(318, "pkix-timestamp"); + m.insert(319, "ptp-event"); + m.insert(320, "ptp-general"); + m.insert(321, "pip"); + m.insert(322, "rtsps"); + m.insert(333, "texar"); + m.insert(344, "pdap"); + m.insert(345, "pawserv"); + m.insert(346, "zserv"); + m.insert(347, "fatserv"); + m.insert(348, "csi-sgwp"); + m.insert(349, "mftp"); + m.insert(350, "matip-type-a"); + m.insert(351, "matip-type-b"); + m.insert(352, "dtag-ste-sb"); + m.insert(353, "ndsauth"); + m.insert(354, "bh611"); + m.insert(355, "datex-asn"); + m.insert(356, "cloanto-net-1"); + m.insert(357, "bhevent"); + m.insert(358, "shrinkwrap"); + m.insert(359, "nsrmp"); + m.insert(360, "scoi2odialog"); + m.insert(361, "semantix"); + m.insert(362, "srssend"); + m.insert(363, "rsvp_tunnel"); + m.insert(364, "aurora-cmgr"); + m.insert(365, "dtk"); + m.insert(366, "odmr"); + m.insert(367, "mortgageware"); + m.insert(368, "qbikgdp"); + m.insert(369, "rpc2portmap"); + m.insert(370, "codaauth2"); + m.insert(371, "clearcase"); + m.insert(372, "ulistserv"); + m.insert(373, "legent-1"); + m.insert(374, "legent-2"); + m.insert(375, "hassle"); + m.insert(376, "nip"); + m.insert(377, "tnETOS"); + m.insert(378, "dsETOS"); + m.insert(379, "is99c"); + m.insert(380, "is99s"); + m.insert(381, "hp-collector"); + m.insert(382, "hp-managed-node"); + m.insert(383, "hp-alarm-mgr"); + m.insert(384, "arns"); + m.insert(385, "ibm-app"); + m.insert(386, "asa"); + m.insert(387, "aurp"); + m.insert(388, "unidata-ldm"); + m.insert(389, "ldap"); + m.insert(390, "uis"); + m.insert(391, "synotics-relay"); + m.insert(392, "synotics-broker"); + m.insert(393, "dis"); + m.insert(394, "embl-ndt"); + m.insert(395, "netcp"); + m.insert(396, "netware-ip"); + m.insert(397, "mptn"); + m.insert(398, "kryptolan"); + m.insert(399, "iso-tsap-c2"); + m.insert(400, "work-sol"); + m.insert(401, "ups"); + m.insert(402, "genie"); + m.insert(403, "decap"); + m.insert(404, "nced"); + m.insert(405, "ncld"); + m.insert(406, "imsp"); + m.insert(407, "timbuktu"); + m.insert(408, "prm-sm"); + m.insert(409, "prm-nm"); + m.insert(410, "decladebug"); + m.insert(411, "rmt"); + m.insert(412, "synoptics-trap"); + m.insert(413, "smsp"); + m.insert(414, "infoseek"); + m.insert(415, "bnet"); + m.insert(416, "silverplatter"); + m.insert(417, "onmux"); + m.insert(418, "hyper-g"); + m.insert(419, "ariel1"); + m.insert(420, "smpte"); + m.insert(421, "ariel2"); + m.insert(422, "ariel3"); + m.insert(423, "opc-job-start"); + m.insert(424, "opc-job-track"); + m.insert(425, "icad-el"); + m.insert(426, "smartsdp"); + m.insert(427, "svrloc"); + m.insert(428, "ocs_cmu"); + m.insert(429, "ocs_amu"); + m.insert(430, "utmpsd"); + m.insert(431, "utmpcd"); + m.insert(432, "iasd"); + m.insert(433, "nnsp"); + m.insert(434, "mobileip-agent"); + m.insert(435, "mobilip-mn"); + m.insert(436, "dna-cml"); + m.insert(437, "comscm"); + m.insert(438, "dsfgw"); + m.insert(439, "dasp"); + m.insert(440, "sgcp"); + m.insert(441, "decvms-sysmgt"); + m.insert(442, "cvc_hostd"); + m.insert(443, "https"); + m.insert(444, "snpp"); + m.insert(445, "microsoft-ds"); + m.insert(446, "ddm-rdb"); + m.insert(447, "ddm-dfm"); + m.insert(448, "ddm-byte"); + m.insert(449, "as-servermap"); + m.insert(450, "tserver"); + m.insert(464, "kpasswd"); + m.insert(465, "smtps"); + m.insert(475, "cybercash"); + m.insert(487, "saft"); + m.insert(488, "gss-http"); + m.insert(489, "nest-protocol"); + m.insert(491, "go-login"); + m.insert(497, "retrospect"); + m.insert(500, "isakmp"); + m.insert(501, "stmf"); + m.insert(502, "asa-appl-proto"); + m.insert(504, "citadel"); + m.insert(510, "fcp"); + m.insert(512, "exec"); + m.insert(513, "login"); + m.insert(514, "shell"); + m.insert(515, "printer"); + m.insert(517, "talk"); + m.insert(518, "ntalk"); + m.insert(519, "utime"); + m.insert(520, "efs"); + m.insert(521, "ripng"); + m.insert(522, "ulp"); + m.insert(523, "ibm-db2"); + m.insert(524, "ncp"); + m.insert(525, "timed"); + m.insert(526, "tempo"); + m.insert(527, "stx"); + m.insert(528, "custix"); + m.insert(529, "irc-serv"); + m.insert(530, "courier"); + m.insert(531, "conference"); + m.insert(532, "netnews"); + m.insert(533, "netwall"); + m.insert(534, "mm-admin"); + m.insert(535, "iiop"); + m.insert(536, "opalis-rdv"); + m.insert(537, "nmsp"); + m.insert(538, "gdomap"); + m.insert(539, "apertus-ldp"); + m.insert(540, "uucp"); + m.insert(541, "uucp-rlogin"); + m.insert(542, "commerce"); + m.insert(543, "klogin"); + m.insert(544, "kshell"); + m.insert(545, "appleqtcsrvr"); + m.insert(546, "dhcpv6-client"); + m.insert(547, "dhcpv6-server"); + m.insert(548, "afp"); + m.insert(549, "idfp"); + m.insert(550, "new-rwho"); + m.insert(551, "cybercash"); + m.insert(552, "deviceshare"); + m.insert(553, "pirp"); + m.insert(554, "rtsp"); + m.insert(555, "dsf"); + m.insert(556, "remotefs"); + m.insert(557, "openvms-sysipc"); + m.insert(558, "sdnskmp"); + m.insert(559, "teedtap"); + m.insert(560, "rmonitor"); + m.insert(561, "monitor"); + m.insert(562, "chshell"); + m.insert(563, "nntps"); + m.insert(564, "9pfs"); + m.insert(565, "whoami"); + m.insert(566, "streettalk"); + m.insert(567, "banyan-rpc"); + m.insert(568, "ms-shuttle"); + m.insert(569, "ms-rome"); + m.insert(570, "meter"); + m.insert(571, "meter"); + m.insert(572, "sonar"); + m.insert(573, "banyan-vip"); + m.insert(574, "ftp-agent"); + m.insert(575, "vemmi"); + m.insert(576, "ipcd"); + m.insert(577, "vnas"); + m.insert(578, "ipdd"); + m.insert(579, "decbsrv"); + m.insert(580, "sntp-heartbeat"); + m.insert(581, "bdp"); + m.insert(582, "scc-security"); + m.insert(583, "philips-vc"); + m.insert(584, "keyserver"); + m.insert(585, "imap4-ssl"); + m.insert(586, "password-chg"); + m.insert(587, "submission"); + m.insert(588, "cal"); + m.insert(589, "eyelink"); + m.insert(590, "tns-cml"); + m.insert(591, "http-alt"); + m.insert(592, "eudora-set"); + m.insert(593, "http-rpc-epmap"); + m.insert(594, "tpip"); + m.insert(595, "cab-protocol"); + m.insert(596, "smsd"); + m.insert(597, "ptcnameservice"); + m.insert(598, "sco-websrvrmg3"); + m.insert(599, "acp"); + m.insert(600, "ipcserver"); + m.insert(606, "urm"); + m.insert(607, "nqs"); + m.insert(608, "sift-uft"); + m.insert(609, "npmp-trap"); + m.insert(610, "npmp-local"); + m.insert(611, "npmp-gui"); + m.insert(612, "hmmp-ind"); + m.insert(613, "hmmp-op"); + m.insert(614, "sshell"); + m.insert(615, "sco-inetmgr"); + m.insert(616, "sco-sysmgr"); + m.insert(617, "sco-dtmgr"); + m.insert(618, "dei-icda"); + m.insert(619, "digital-evm"); + m.insert(620, "sco-websrvrmgr"); + m.insert(621, "escp-ip"); + m.insert(622, "collaborator"); + m.insert(623, "asf-rmcp"); + m.insert(624, "cryptoadmin"); + m.insert(625, "dec_dlm"); + m.insert(626, "asia"); + m.insert(627, "passgo-tivoli"); + m.insert(628, "qmqp"); + m.insert(629, "3com-amp3"); + m.insert(630, "rda"); + m.insert(631, "ipp"); + m.insert(632, "bmpp"); + m.insert(633, "servstat"); + m.insert(634, "ginad"); + m.insert(635, "rlzdbase"); + m.insert(636, "ldaps"); + m.insert(637, "lanserver"); + m.insert(638, "mcns-sec"); + m.insert(639, "msdp"); + m.insert(640, "entrust-sps"); + m.insert(641, "repcmd"); + m.insert(642, "esro-emsdp"); + m.insert(643, "sanity"); + m.insert(644, "dwr"); + m.insert(645, "pssc"); + m.insert(646, "ldp"); + m.insert(647, "dhcp-failover"); + m.insert(648, "rrp"); + m.insert(649, "cadview-3d"); + m.insert(650, "obex"); + m.insert(651, "ieee-mms"); + m.insert(652, "hello-port"); + m.insert(653, "repscmd"); + m.insert(654, "aodv"); + m.insert(655, "tinc"); + m.insert(656, "spmp"); + m.insert(657, "rmc"); + m.insert(658, "tenfold"); + m.insert(660, "mac-srvr-admin"); + m.insert(661, "hap"); + m.insert(662, "pftp"); + m.insert(663, "purenoise"); + m.insert(664, "asf-secure-rmcp"); + m.insert(665, "sun-dr"); + m.insert(666, "mdqs"); + m.insert(667, "disclose"); + m.insert(668, "mecomm"); + m.insert(669, "meregister"); + m.insert(670, "vacdsm-sws"); + m.insert(671, "vacdsm-app"); + m.insert(672, "vpps-qua"); + m.insert(673, "cimplex"); + m.insert(674, "acap"); + m.insert(675, "dctp"); + m.insert(676, "vpps-via"); + m.insert(677, "vpp"); + m.insert(678, "ggf-ncp"); + m.insert(679, "mrm"); + m.insert(680, "entrust-aaas"); + m.insert(681, "entrust-aams"); + m.insert(682, "xfr"); + m.insert(683, "corba-iiop"); + m.insert(684, "corba-iiop-ssl"); + m.insert(685, "mdc-portmapper"); + m.insert(686, "hcp-wismar"); + m.insert(687, "asipregistry"); + m.insert(688, "realm-rusd"); + m.insert(689, "nmap"); + m.insert(690, "vatp"); + m.insert(691, "msexch-routing"); + m.insert(692, "hyperwave-isp"); + m.insert(693, "connendp"); + m.insert(694, "ha-cluster"); + m.insert(695, "ieee-mms-ssl"); + m.insert(696, "rushd"); + m.insert(697, "uuidgen"); + m.insert(698, "olsr"); + m.insert(699, "accessnetwork"); + m.insert(700, "epp"); + m.insert(701, "lmp"); + m.insert(702, "iris-beep"); + m.insert(704, "elcsd"); + m.insert(705, "agentx"); + m.insert(706, "silc"); + m.insert(707, "borland-dsj"); + m.insert(709, "entrust-kmsh"); + m.insert(710, "entrust-ash"); + m.insert(711, "cisco-tdp"); + m.insert(712, "tbrpf"); + m.insert(729, "netviewdm1"); + m.insert(730, "netviewdm2"); + m.insert(731, "netviewdm3"); + m.insert(741, "netgw"); + m.insert(742, "netrcs"); + m.insert(744, "flexlm"); + m.insert(747, "fujitsu-dev"); + m.insert(748, "ris-cm"); + m.insert(749, "kerberos-adm"); + m.insert(750, "rfile"); + m.insert(751, "pump"); + m.insert(752, "qrh"); + m.insert(753, "rrh"); + m.insert(754, "tell"); + m.insert(758, "nlogin"); + m.insert(759, "con"); + m.insert(760, "ns"); + m.insert(761, "rxe"); + m.insert(762, "quotad"); + m.insert(763, "cycleserv"); + m.insert(764, "omserv"); + m.insert(765, "webster"); + m.insert(767, "phonebook"); + m.insert(769, "vid"); + m.insert(770, "cadlock"); + m.insert(771, "rtip"); + m.insert(772, "cycleserv2"); + m.insert(773, "submit"); + m.insert(774, "rpasswd"); + m.insert(775, "entomb"); + m.insert(776, "wpages"); + m.insert(777, "multiling-http"); + m.insert(780, "wpgs"); + m.insert(800, "mdbs_daemon"); + m.insert(801, "device"); + m.insert(810, "fcp-udp"); + m.insert(828, "itm-mcell-s"); + m.insert(829, "pkix-3-ca-ra"); + m.insert(830, "netconf-ssh"); + m.insert(831, "netconf-beep"); + m.insert(832, "netconfsoaphttp"); + m.insert(833, "netconfsoapbeep"); + m.insert(847, "dhcp-failover2"); + m.insert(848, "gdoi"); + m.insert(860, "iscsi"); + m.insert(861, "owamp-control"); + m.insert(873, "rsync"); + m.insert(886, "iclcnet-locate"); + m.insert(887, "iclcnet_svinfo"); + m.insert(888, "accessbuilder"); + m.insert(900, "omginitialrefs"); + m.insert(901, "smpnameres"); + m.insert(902, "ideafarm-chat"); + m.insert(903, "ideafarm-catch"); + m.insert(910, "kink"); + m.insert(911, "xact-backup"); + m.insert(912, "apex-mesh"); + m.insert(913, "apex-edge"); + m.insert(989, "ftps-data"); + m.insert(990, "ftps"); + m.insert(991, "nas"); + m.insert(992, "telnets"); + m.insert(993, "imaps"); + m.insert(994, "ircs"); + m.insert(995, "pop3s"); + m.insert(996, "vsinet"); + m.insert(997, "maitrd"); + m.insert(998, "busboy"); + m.insert(999, "garcon"); + m.insert(1000, "cadlock2"); + m.insert(1001, "webpush"); + + // Common registered ports (1024-49151) + m.insert(1080, "socks"); + m.insert(1194, "openvpn"); + m.insert(1433, "ms-sql-s"); + m.insert(1434, "ms-sql-m"); + m.insert(1521, "oracle"); + m.insert(1723, "pptp"); + m.insert(1812, "radius"); + m.insert(1813, "radius-acct"); + m.insert(2049, "nfs"); + m.insert(2082, "cpanel"); + m.insert(2083, "cpanels"); + m.insert(2086, "whm"); + m.insert(2087, "whms"); + m.insert(2181, "zookeeper"); + m.insert(2222, "ssh-alt"); + m.insert(2375, "docker"); + m.insert(2376, "docker-s"); + m.insert(2379, "etcd-client"); + m.insert(2380, "etcd-server"); + m.insert(3000, "ppp"); + m.insert(3128, "squid"); + m.insert(3268, "ldap-gc"); + m.insert(3269, "ldaps-gc"); + m.insert(3306, "mysql"); + m.insert(3389, "ms-wbt-server"); + m.insert(3690, "svn"); + m.insert(4000, "terabase"); + m.insert(4369, "epmd"); + m.insert(4443, "pharos"); + m.insert(5000, "upnp"); + m.insert(5001, "commplex-link"); + m.insert(5060, "sip"); + m.insert(5061, "sips"); + m.insert(5222, "xmpp-client"); + m.insert(5223, "xmpp-client-ssl"); + m.insert(5269, "xmpp-server"); + m.insert(5432, "postgresql"); + m.insert(5672, "amqp"); + m.insert(5900, "vnc"); + m.insert(5984, "couchdb"); + m.insert(6000, "x11"); + m.insert(6379, "redis"); + m.insert(6443, "kubernetes-api"); + m.insert(6666, "irc-alt"); + m.insert(6667, "irc"); + m.insert(6668, "irc-alt"); + m.insert(6669, "irc-alt"); + m.insert(7000, "afs3-fileserver"); + m.insert(7001, "afs3-callback"); + m.insert(7002, "afs3-prserver"); + m.insert(7003, "afs3-vlserver"); + m.insert(7004, "afs3-kaserver"); + m.insert(7005, "afs3-volser"); + m.insert(7006, "afs3-errors"); + m.insert(7007, "afs3-bos"); + m.insert(7008, "afs3-update"); + m.insert(7009, "afs3-rmtsys"); + m.insert(7010, "ups-onlinet"); + m.insert(8000, "http-alt"); + m.insert(8008, "http-alt"); + m.insert(8080, "http-proxy"); + m.insert(8081, "blackice-icecap"); + m.insert(8088, "radan-http"); + m.insert(8443, "https-alt"); + m.insert(8888, "ddi-tcp-1"); + m.insert(9000, "cslistener"); + m.insert(9042, "cassandra"); + m.insert(9090, "websm"); + m.insert(9091, "xmltec-xmlmail"); + m.insert(9092, "kafka"); + m.insert(9200, "elasticsearch"); + m.insert(9300, "elasticsearch"); + m.insert(9418, "git"); + m.insert(9999, "abyss"); + m.insert(10000, "webmin"); + m.insert(11211, "memcached"); + m.insert(15672, "rabbitmq-mgmt"); + m.insert(25565, "minecraft"); + m.insert(27017, "mongodb"); + m.insert(27018, "mongodb"); + m.insert(27019, "mongodb"); + + m +}); + +/// Reverse mapping (name -> port). +pub static TCP_SERVICES_BY_NAME: LazyLock> = LazyLock::new(|| { + TCP_SERVICES + .iter() + .map(|(&port, &name)| (name, port)) + .collect() +}); + +/// Get the service name for a port. +/// +/// Returns "unknown" if the port is not in the database. +pub fn service_name(port: u16) -> &'static str { + TCP_SERVICES.get(&port).copied().unwrap_or("unknown") +} + +/// Get the port number for a service name. +/// +/// Returns None if the service name is not found. +pub fn service_port(name: &str) -> Option { + TCP_SERVICES_BY_NAME.get(name).copied() +} + +/// Check if a port is a well-known port (< 1024). +#[inline] +pub fn is_well_known_port(port: u16) -> bool { + port < 1024 +} + +/// Check if a port is a registered port (1024-49151). +#[inline] +pub fn is_registered_port(port: u16) -> bool { + (1024..49152).contains(&port) +} + +/// Check if a port is a dynamic/private port (49152-65535). +#[inline] +pub fn is_dynamic_port(port: u16) -> bool { + port >= 49152 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_well_known_services() { + assert_eq!(service_name(20), "ftp-data"); + assert_eq!(service_name(21), "ftp"); + assert_eq!(service_name(22), "ssh"); + assert_eq!(service_name(23), "telnet"); + assert_eq!(service_name(25), "smtp"); + assert_eq!(service_name(53), "domain"); + assert_eq!(service_name(80), "http"); + assert_eq!(service_name(110), "pop3"); + assert_eq!(service_name(143), "imap"); + assert_eq!(service_name(443), "https"); + assert_eq!(service_name(993), "imaps"); + assert_eq!(service_name(995), "pop3s"); + } + + #[test] + fn test_registered_services() { + assert_eq!(service_name(3306), "mysql"); + assert_eq!(service_name(5432), "postgresql"); + assert_eq!(service_name(6379), "redis"); + assert_eq!(service_name(8080), "http-proxy"); + assert_eq!(service_name(27017), "mongodb"); + } + + #[test] + fn test_unknown_port() { + assert_eq!(service_name(12345), "unknown"); + assert_eq!(service_name(65000), "unknown"); + } + + #[test] + fn test_service_port() { + assert_eq!(service_port("http"), Some(80)); + assert_eq!(service_port("https"), Some(443)); + assert_eq!(service_port("ssh"), Some(22)); + assert_eq!(service_port("mysql"), Some(3306)); + assert_eq!(service_port("nonexistent"), None); + } + + #[test] + fn test_port_categories() { + assert!(is_well_known_port(80)); + assert!(is_well_known_port(443)); + assert!(!is_well_known_port(8080)); + + assert!(!is_registered_port(80)); + assert!(is_registered_port(8080)); + assert!(is_registered_port(3306)); + assert!(!is_registered_port(50000)); + + assert!(!is_dynamic_port(80)); + assert!(!is_dynamic_port(8080)); + assert!(is_dynamic_port(50000)); + assert!(is_dynamic_port(65535)); + } +}