From 0d4cd99145851658dd76ffce87f9bef3817e1a83 Mon Sep 17 00:00:00 2001 From: shellrow Date: Sat, 6 Jun 2026 15:56:47 +0900 Subject: [PATCH 01/33] chore: clean formatting and clippy baseline --- examples/arp.rs | 36 +++---- examples/async_datalink.rs | 60 +++++------ examples/async_dump.rs | 7 +- examples/async_icmp_socket.rs | 41 ++++---- examples/dns_dump.rs | 107 ++++++++++--------- examples/dump.rs | 7 +- examples/icmp_ping.rs | 62 +++++------ examples/icmp_socket.rs | 40 ++++---- examples/ndp.rs | 42 ++++---- examples/parse_frame.rs | 9 +- examples/tcp_ping.rs | 89 ++++++++-------- examples/udp_ping.rs | 62 +++++------ nex-datalink/src/async_io/bpf.rs | 2 +- nex-datalink/src/async_io/mod.rs | 6 +- nex-datalink/src/bpf.rs | 53 ++++------ nex-datalink/src/lib.rs | 22 ++-- nex-datalink/src/pcap.rs | 16 +-- nex-datalink/src/wpcap.rs | 5 +- nex-packet/src/arp.rs | 4 +- nex-packet/src/builder/ethernet.rs | 6 ++ nex-packet/src/builder/ipv4.rs | 13 ++- nex-packet/src/builder/ipv6.rs | 6 ++ nex-packet/src/builder/ndp.rs | 2 +- nex-packet/src/builder/tcp.rs | 18 ++-- nex-packet/src/builder/udp.rs | 8 +- nex-packet/src/checksum.rs | 9 +- nex-packet/src/dns.rs | 46 ++++----- nex-packet/src/ethernet.rs | 6 +- nex-packet/src/flowcontrol.rs | 6 +- nex-packet/src/frame.rs | 82 +++++++-------- nex-packet/src/gre.rs | 10 +- nex-packet/src/icmp.rs | 21 ++-- nex-packet/src/icmpv6.rs | 14 ++- nex-packet/src/ip.rs | 8 +- nex-packet/src/ipv4.rs | 17 +-- nex-packet/src/ipv6.rs | 16 ++- nex-packet/src/packet.rs | 5 +- nex-packet/src/tcp.rs | 29 +++--- nex-packet/src/udp.rs | 8 +- nex-packet/src/util.rs | 20 ++-- nex-packet/src/vlan.rs | 12 +-- nex-socket/src/icmp/config.rs | 2 +- nex-socket/src/lib.rs | 2 +- nex-socket/src/tcp/async_impl.rs | 10 +- nex-socket/src/tcp/config.rs | 2 +- nex-socket/src/tcp/sync_impl.rs | 2 +- nex-socket/src/udp/config.rs | 2 +- nex-socket/src/udp/sync_impl.rs | 160 ++++++++++++++--------------- 48 files changed, 579 insertions(+), 633 deletions(-) diff --git a/examples/arp.rs b/examples/arp.rs index 2cc1b84..6f66923 100644 --- a/examples/arp.rs +++ b/examples/arp.rs @@ -47,11 +47,8 @@ fn main() { None => Interface::default().expect("Failed to get default interface"), }; - let src_mac = interface - .mac_addr - .clone() - .expect("No MAC address on interface"); - let src_ip = interface.ipv4.get(0).expect("No IPv4 address").addr(); + let src_mac = interface.mac_addr.expect("No MAC address on interface"); + let src_ip = interface.ipv4.first().expect("No IPv4 address").addr(); let (mut tx, mut rx) = match datalink::channel(&interface, Default::default()) { Ok(Ethernet(tx, rx)) => (tx, rx), @@ -80,23 +77,22 @@ fn main() { loop { match rx.next() { Ok(packet) => { - let frame = Frame::from_buf(&packet, ParseOption::default()).unwrap(); + let frame = Frame::from_buf(packet, ParseOption::default()).unwrap(); match &frame.datalink { Some(dlink) => { - if let Some(arp) = &dlink.arp { - if arp.operation == ArpOperation::Reply - && arp.sender_proto_addr == target_ip - { - println!("Received ARP Reply from {}", arp.sender_proto_addr); - println!("MAC address: {}", arp.sender_hw_addr); - println!( - "---- Interface: {}, Total Length: {} bytes ----", - interface.name, - packet.len() - ); - println!("Frame: {:?}", frame); - break; - } + if let Some(arp) = &dlink.arp + && arp.operation == ArpOperation::Reply + && arp.sender_proto_addr == target_ip + { + println!("Received ARP Reply from {}", arp.sender_proto_addr); + println!("MAC address: {}", arp.sender_hw_addr); + println!( + "---- Interface: {}, Total Length: {} bytes ----", + interface.name, + packet.len() + ); + println!("Frame: {:?}", frame); + break; } } None => continue, // No datalink layer diff --git a/examples/async_datalink.rs b/examples/async_datalink.rs index 77805c9..d752e5a 100644 --- a/examples/async_datalink.rs +++ b/examples/async_datalink.rs @@ -45,7 +45,7 @@ fn main() -> std::io::Result<()> { let src_ip: IpAddr = match target_ip { IpAddr::V4(_) => interface .ipv4 - .get(0) + .first() .map(|v| IpAddr::V4(v.addr())) .expect("No IPv4 address"), IpAddr::V6(_) => interface @@ -97,7 +97,7 @@ fn main() -> std::io::Result<()> { .source(if use_tun { MacAddr::zero() } else { - interface.mac_addr.clone().unwrap() + interface.mac_addr.unwrap() }) .destination(if use_tun { MacAddr::zero() @@ -136,35 +136,35 @@ fn main() -> std::io::Result<()> { let frame = Frame::from_buf(&packet, parse_option).unwrap(); if let Some(ip_layer) = &frame.ip { - if let Some(icmp) = &ip_layer.icmp { - if icmp.icmp_type == IcmpType::EchoReply { - println!( - "Received ICMP Echo Reply from {}", - ip_layer.ipv4.as_ref().unwrap().source - ); - println!( - "---- Interface: {}, Total Length: {} bytes ----", - interface.name, - packet.len() - ); - println!("Frame: {:?}", frame); - break; - } + if let Some(icmp) = &ip_layer.icmp + && icmp.icmp_type == IcmpType::EchoReply + { + println!( + "Received ICMP Echo Reply from {}", + ip_layer.ipv4.as_ref().unwrap().source + ); + println!( + "---- Interface: {}, Total Length: {} bytes ----", + interface.name, + packet.len() + ); + println!("Frame: {:?}", frame); + break; } - if let Some(icmpv6) = &ip_layer.icmpv6 { - if icmpv6.icmpv6_type == Icmpv6Type::EchoReply { - println!( - "Received ICMPv6 Echo Reply from {}", - ip_layer.ipv6.as_ref().unwrap().source - ); - println!( - "---- Interface: {}, Total Length: {} bytes ----", - interface.name, - packet.len() - ); - println!("Frame: {:?}", frame); - break; - } + if let Some(icmpv6) = &ip_layer.icmpv6 + && icmpv6.icmpv6_type == Icmpv6Type::EchoReply + { + println!( + "Received ICMPv6 Echo Reply from {}", + ip_layer.ipv6.as_ref().unwrap().source + ); + println!( + "---- Interface: {}, Total Length: {} bytes ----", + interface.name, + packet.len() + ); + println!("Frame: {:?}", frame); + break; } } } diff --git a/examples/async_dump.rs b/examples/async_dump.rs index 7084615..4e3c769 100644 --- a/examples/async_dump.rs +++ b/examples/async_dump.rs @@ -42,12 +42,7 @@ fn main() -> std::io::Result<()> { if interface.is_tun() || (cfg!(any(target_os = "macos", target_os = "ios")) && interface.is_loopback()) { - let payload_offset: usize; - if interface.is_loopback() { - payload_offset = 14; - } else { - payload_offset = 0; - } + let payload_offset: usize = if interface.is_loopback() { 14 } else { 0 }; let payload = Bytes::copy_from_slice(&packet[payload_offset..]); if packet.len() > payload_offset { let version = Ipv4Packet::from_buf(&packet).unwrap().header.version; diff --git a/examples/async_icmp_socket.rs b/examples/async_icmp_socket.rs index fb55e85..7798544 100644 --- a/examples/async_icmp_socket.rs +++ b/examples/async_icmp_socket.rs @@ -36,7 +36,7 @@ async fn main() -> std::io::Result<()> { let src_ip = interface .ipv4 - .get(0) + .first() .map(|v| v.addr()) .expect("No IPv4 address on interface"); @@ -54,29 +54,24 @@ async fn main() -> std::io::Result<()> { loop { if let Ok((n, from)) = socket_clone.recv_from(&mut buf).await { println!("Received {} bytes from {}", n, from.ip()); - if let Some(ipv4_packet) = Ipv4Packet::from_buf(&buf[..n]) { - if ipv4_packet.header.next_level_protocol + if let Some(ipv4_packet) = Ipv4Packet::from_buf(&buf[..n]) + && ipv4_packet.header.next_level_protocol == nex_packet::ip::IpNextProtocol::Icmp - { - if let Some(icmp_packet) = IcmpPacket::from_bytes(ipv4_packet.payload()) { - println!( - "\t{:?} from: {:?} to {:?}, TTL: {}", - icmp_packet.header.icmp_type, - ipv4_packet.header.source, - ipv4_packet.header.destination, - ipv4_packet.header.ttl - ); - match EchoReplyPacket::try_from(icmp_packet) { - Ok(reply) => { - println!( - "\tID: {}, Seq: {}", - reply.identifier, reply.sequence_number - ); - } - Err(_) => { - println!("\tReceived non-echo-reply ICMP packet"); - } - } + && let Some(icmp_packet) = IcmpPacket::from_bytes(ipv4_packet.payload()) + { + println!( + "\t{:?} from: {:?} to {:?}, TTL: {}", + icmp_packet.header.icmp_type, + ipv4_packet.header.source, + ipv4_packet.header.destination, + ipv4_packet.header.ttl + ); + match EchoReplyPacket::try_from(icmp_packet) { + Ok(reply) => { + println!("\tID: {}, Seq: {}", reply.identifier, reply.sequence_number); + } + Err(_) => { + println!("\tReceived non-echo-reply ICMP packet"); } } } diff --git a/examples/dns_dump.rs b/examples/dns_dump.rs index fccbd61..cb9366e 100644 --- a/examples/dns_dump.rs +++ b/examples/dns_dump.rs @@ -80,14 +80,14 @@ fn main() { IpAddr::V4(ipv4.header.destination), ); } - } else if let EtherType::Ipv6 = eth_packet.header.ethertype { - if let Some(ipv6) = Ipv6Packet::from_bytes(eth_packet.payload.clone()) { - handle_udp( - ipv6.payload, - IpAddr::V6(ipv6.header.source), - IpAddr::V6(ipv6.header.destination), - ); - } + } else if let EtherType::Ipv6 = eth_packet.header.ethertype + && let Some(ipv6) = Ipv6Packet::from_bytes(eth_packet.payload.clone()) + { + handle_udp( + ipv6.payload, + IpAddr::V6(ipv6.header.source), + IpAddr::V6(ipv6.header.destination), + ); } } Err(e) => eprintln!("Failed to read packet: {}", e), @@ -96,57 +96,56 @@ fn main() { } fn handle_udp(packet: Bytes, src: IpAddr, dst: IpAddr) { - if let Some(udp) = UdpPacket::from_bytes(packet.clone()) { - if udp.payload.len() > 0 { - if let Some(dns) = DnsPacket::from_bytes(udp.payload.clone()) { - println!( - "DNS Packet: {}:{} > {}:{}", - src, udp.header.source, dst, udp.header.destination - ); + if let Some(udp) = UdpPacket::from_bytes(packet.clone()) + && !udp.payload.is_empty() + && let Some(dns) = DnsPacket::from_bytes(udp.payload.clone()) + { + println!( + "DNS Packet: {}:{} > {}:{}", + src, udp.header.source, dst, udp.header.destination + ); - for query in &dns.queries { - println!( - " Query: {:?} (type: {:?}, class: {:?})", - query.get_qname_parsed(), - query.qtype, - query.qclass - ); - } + for query in &dns.queries { + println!( + " Query: {:?} (type: {:?}, class: {:?})", + query.get_qname_parsed(), + query.qtype, + query.qclass + ); + } - for response in &dns.responses { - match response.rtype { - DnsType::A | DnsType::AAAA => { - if let Some(ip) = response.get_ip() { - println!( - " Response: {} (type: {:?}, ttl: {})", - ip, response.rtype, response.ttl - ); - } else { - println!(" Invalid IP data for type: {:?}", response.rtype); - } - } - DnsType::CNAME | DnsType::NS | DnsType::PTR => { - if let Some(name) = response.get_name() { - println!( - " Response: {} (type: {:?}, ttl: {})", - name, response.rtype, response.ttl - ); - } else { - println!(" Invalid name data for type: {:?}", response.rtype); - } - } - DnsType::TXT => { - if let Some(txts) = response.get_txt_strings() { - for txt in txts { - println!(" TXT: \"{}\" (ttl: {})", txt, response.ttl); - } - } else { - println!(" Invalid TXT data"); - } + for response in &dns.responses { + match response.rtype { + DnsType::A | DnsType::AAAA => { + if let Some(ip) = response.get_ip() { + println!( + " Response: {} (type: {:?}, ttl: {})", + ip, response.rtype, response.ttl + ); + } else { + println!(" Invalid IP data for type: {:?}", response.rtype); + } + } + DnsType::CNAME | DnsType::NS | DnsType::PTR => { + if let Some(name) = response.get_name() { + println!( + " Response: {} (type: {:?}, ttl: {})", + name, response.rtype, response.ttl + ); + } else { + println!(" Invalid name data for type: {:?}", response.rtype); + } + } + DnsType::TXT => { + if let Some(txts) = response.get_txt_strings() { + for txt in txts { + println!(" TXT: \"{}\" (ttl: {})", txt, response.ttl); } - _ => {} + } else { + println!(" Invalid TXT data"); } } + _ => {} } } } diff --git a/examples/dump.rs b/examples/dump.rs index 1169d44..19ef778 100644 --- a/examples/dump.rs +++ b/examples/dump.rs @@ -66,12 +66,7 @@ fn main() { || (cfg!(any(target_os = "macos", target_os = "ios")) && interface.is_loopback()) { - let payload_offset: usize; - if interface.is_loopback() { - payload_offset = 14; - } else { - payload_offset = 0; - } + let payload_offset: usize = if interface.is_loopback() { 14 } else { 0 }; let payload = Bytes::copy_from_slice(&packet[payload_offset..]); if packet.len() > payload_offset { let version = Ipv4Packet::from_buf(packet).unwrap().header.version; diff --git a/examples/icmp_ping.rs b/examples/icmp_ping.rs index 431b280..6b0ae4a 100644 --- a/examples/icmp_ping.rs +++ b/examples/icmp_ping.rs @@ -53,7 +53,7 @@ fn main() { let src_ip: IpAddr = match target_ip { IpAddr::V4(_) => interface .ipv4 - .get(0) + .first() .map(|v| IpAddr::V4(v.addr())) .expect("No IPv4 address"), IpAddr::V6(_) => interface @@ -105,7 +105,7 @@ fn main() { .source(if use_tun { MacAddr::zero() } else { - interface.mac_addr.clone().unwrap() + interface.mac_addr.unwrap() }) .destination(if use_tun { MacAddr::zero() @@ -139,38 +139,38 @@ fn main() { parse_option.from_ip_packet = true; parse_option.offset = if interface.is_loopback() { 14 } else { 0 }; } - let frame = Frame::from_buf(&packet, parse_option).unwrap(); + let frame = Frame::from_buf(packet, parse_option).unwrap(); if let Some(ip_layer) = &frame.ip { - if let Some(icmp) = &ip_layer.icmp { - if icmp.icmp_type == IcmpType::EchoReply { - println!( - "Received ICMP Echo Reply from {}", - ip_layer.ipv4.as_ref().unwrap().source - ); - println!( - "---- Interface: {}, Total Length: {} bytes ----", - interface.name, - packet.len() - ); - println!("Frame: {:?}", frame); - break; - } + if let Some(icmp) = &ip_layer.icmp + && icmp.icmp_type == IcmpType::EchoReply + { + println!( + "Received ICMP Echo Reply from {}", + ip_layer.ipv4.as_ref().unwrap().source + ); + println!( + "---- Interface: {}, Total Length: {} bytes ----", + interface.name, + packet.len() + ); + println!("Frame: {:?}", frame); + break; } - if let Some(icmpv6) = &ip_layer.icmpv6 { - if icmpv6.icmpv6_type == Icmpv6Type::EchoReply { - println!( - "Received ICMPv6 Echo Reply from {}", - ip_layer.ipv6.as_ref().unwrap().source - ); - println!( - "---- Interface: {}, Total Length: {} bytes ----", - interface.name, - packet.len() - ); - println!("Frame: {:?}", frame); - break; - } + if let Some(icmpv6) = &ip_layer.icmpv6 + && icmpv6.icmpv6_type == Icmpv6Type::EchoReply + { + println!( + "Received ICMPv6 Echo Reply from {}", + ip_layer.ipv6.as_ref().unwrap().source + ); + println!( + "---- Interface: {}, Total Length: {} bytes ----", + interface.name, + packet.len() + ); + println!("Frame: {:?}", frame); + break; } } } diff --git a/examples/icmp_socket.rs b/examples/icmp_socket.rs index 375ff54..a39505a 100644 --- a/examples/icmp_socket.rs +++ b/examples/icmp_socket.rs @@ -32,7 +32,7 @@ fn main() -> std::io::Result<()> { let src_ip = match target_ip { IpAddr::V4(_) => interface .ipv4 - .get(0) + .first() .map(|v| IpAddr::V4(v.addr())) .expect("No IPv4 address"), IpAddr::V6(_) => interface @@ -76,27 +76,23 @@ fn main() -> std::io::Result<()> { match kind { IcmpKind::V4 => { // Parse IPv4 + ICMP - if let Some(ipv4_packet) = Ipv4Packet::from_buf(packet) { - if ipv4_packet.header.next_level_protocol == nex_packet::ip::IpNextProtocol::Icmp { - if let Some(icmp_packet) = IcmpPacket::from_bytes(ipv4_packet.payload()) { - println!( - "\t{:?} from: {:?} to {:?}, TTL: {}", - icmp_packet.header.icmp_type, - ipv4_packet.header.source, - ipv4_packet.header.destination, - ipv4_packet.header.ttl - ); - match icmp::echo_reply::EchoReplyPacket::try_from(icmp_packet) { - Ok(reply) => { - println!( - "\tID: {}, Seq: {}", - reply.identifier, reply.sequence_number - ); - } - Err(_) => { - println!("\tReceived non-echo-reply ICMP packet"); - } - } + if let Some(ipv4_packet) = Ipv4Packet::from_buf(packet) + && ipv4_packet.header.next_level_protocol == nex_packet::ip::IpNextProtocol::Icmp + && let Some(icmp_packet) = IcmpPacket::from_bytes(ipv4_packet.payload()) + { + println!( + "\t{:?} from: {:?} to {:?}, TTL: {}", + icmp_packet.header.icmp_type, + ipv4_packet.header.source, + ipv4_packet.header.destination, + ipv4_packet.header.ttl + ); + match icmp::echo_reply::EchoReplyPacket::try_from(icmp_packet) { + Ok(reply) => { + println!("\tID: {}, Seq: {}", reply.identifier, reply.sequence_number); + } + Err(_) => { + println!("\tReceived non-echo-reply ICMP packet"); } } } diff --git a/examples/ndp.rs b/examples/ndp.rs index 060bd24..29d9b59 100644 --- a/examples/ndp.rs +++ b/examples/ndp.rs @@ -107,31 +107,25 @@ fn main() { parse_option.offset = if interface.is_loopback() { 14 } else { 0 }; } - if let Some(frame) = Frame::from_buf(&packet, parse_option) { - if let Some(ip_layer) = &frame.ip { - if let Some(icmpv6) = &ip_layer.icmpv6 { - if icmpv6.icmpv6_type == Icmpv6Type::NeighborAdvertisement { - if let Some(ipv6_hdr) = &ip_layer.ipv6 { - println!( - "Received Neighbor Advertisement from {}", - ipv6_hdr.source - ); - if let Some(dlink) = &frame.datalink { - if let Some(eth) = &dlink.ethernet { - println!("MAC address: {}", eth.source.address()); - } - } - println!( - "---- Interface: {}, Total Length: {} bytes ----", - interface.name, - packet.len() - ); - println!("Frame: {:?}", frame); - break; - } - } - } + if let Some(frame) = Frame::from_buf(packet, parse_option) + && let Some(ip_layer) = &frame.ip + && let Some(icmpv6) = &ip_layer.icmpv6 + && icmpv6.icmpv6_type == Icmpv6Type::NeighborAdvertisement + && let Some(ipv6_hdr) = &ip_layer.ipv6 + { + println!("Received Neighbor Advertisement from {}", ipv6_hdr.source); + if let Some(dlink) = &frame.datalink + && let Some(eth) = &dlink.ethernet + { + println!("MAC address: {}", eth.source.address()); } + println!( + "---- Interface: {}, Total Length: {} bytes ----", + interface.name, + packet.len() + ); + println!("Frame: {:?}", frame); + break; } } Err(e) => eprintln!("Receive failed: {}", e), diff --git a/examples/parse_frame.rs b/examples/parse_frame.rs index eb32b79..fd6aa9a 100644 --- a/examples/parse_frame.rs +++ b/examples/parse_frame.rs @@ -55,16 +55,11 @@ fn main() { || (cfg!(any(target_os = "macos", target_os = "ios")) && interface.is_loopback()) { - let payload_offset; - if interface.is_loopback() { - payload_offset = 14; - } else { - payload_offset = 0; - } + let payload_offset = if interface.is_loopback() { 14 } else { 0 }; parse_option.from_ip_packet = true; parse_option.offset = payload_offset; } - match Frame::from_buf(&packet, parse_option) { + match Frame::from_buf(packet, parse_option) { Some(frame) => { display_frame(&frame); } diff --git a/examples/tcp_ping.rs b/examples/tcp_ping.rs index 1bd4810..c08ca07 100644 --- a/examples/tcp_ping.rs +++ b/examples/tcp_ping.rs @@ -81,7 +81,7 @@ fn main() { match dst_ip { IpAddr::V4(_) => { // For IPv4, use the first IPv4 address of the interface - match interface.ipv4.get(0) { + match interface.ipv4.first() { Some(ipv4) => src_ip = IpAddr::V4(ipv4.addr()), None => { println!("No IPv4 address on the interface"); @@ -165,7 +165,7 @@ fn main() { .source(if use_tun { MacAddr::zero() } else { - interface.mac_addr.clone().unwrap() + interface.mac_addr.unwrap() }) .destination(if use_tun { MacAddr::zero() @@ -201,51 +201,50 @@ fn main() { parse_option.from_ip_packet = true; parse_option.offset = payload_offset; } - let frame: Frame = Frame::from_buf(&packet, parse_option).unwrap(); + let frame: Frame = Frame::from_buf(packet, parse_option).unwrap(); // Check each layer. If the packet is TCP SYN+ACK or RST+ACK, print it out - if let Some(ip_layer) = &frame.ip { - if let Some(transport_layer) = &frame.transport { - if let Some(tcp_packet) = &transport_layer.tcp { - if tcp_packet.flags == TcpFlags::SYN | TcpFlags::ACK { - if let Some(ipv4) = &ip_layer.ipv4 { - println!( - "Received TCP SYN+ACK packet from {}:{}", - ipv4.source, tcp_packet.source - ); - } else if let Some(ipv6) = &ip_layer.ipv6 { - println!( - "Received TCP SYN+ACK packet from {}:{}", - ipv6.source, tcp_packet.source - ); - } - println!( - "---- Interface: {}, Total Length: {} bytes ----", - interface.name, - packet.len() - ); - println!("Packet Frame: {:?}", frame); - break; - } else if tcp_packet.flags == TcpFlags::RST | TcpFlags::ACK { - if let Some(ipv4) = &ip_layer.ipv4 { - println!( - "Received TCP RST+ACK packet from {}:{}", - ipv4.source, tcp_packet.source - ); - } else if let Some(ipv6) = &ip_layer.ipv6 { - println!( - "Received TCP RST+ACK packet from {}:{}", - ipv6.source, tcp_packet.source - ); - } - println!( - "---- Interface: {}, Total Length: {} bytes ----", - interface.name, - packet.len() - ); - println!("Packet Frame: {:?}", frame); - break; - } + if let Some(ip_layer) = &frame.ip + && let Some(transport_layer) = &frame.transport + && let Some(tcp_packet) = &transport_layer.tcp + { + if tcp_packet.flags == TcpFlags::SYN | TcpFlags::ACK { + if let Some(ipv4) = &ip_layer.ipv4 { + println!( + "Received TCP SYN+ACK packet from {}:{}", + ipv4.source, tcp_packet.source + ); + } else if let Some(ipv6) = &ip_layer.ipv6 { + println!( + "Received TCP SYN+ACK packet from {}:{}", + ipv6.source, tcp_packet.source + ); } + println!( + "---- Interface: {}, Total Length: {} bytes ----", + interface.name, + packet.len() + ); + println!("Packet Frame: {:?}", frame); + break; + } else if tcp_packet.flags == TcpFlags::RST | TcpFlags::ACK { + if let Some(ipv4) = &ip_layer.ipv4 { + println!( + "Received TCP RST+ACK packet from {}:{}", + ipv4.source, tcp_packet.source + ); + } else if let Some(ipv6) = &ip_layer.ipv6 { + println!( + "Received TCP RST+ACK packet from {}:{}", + ipv6.source, tcp_packet.source + ); + } + println!( + "---- Interface: {}, Total Length: {} bytes ----", + interface.name, + packet.len() + ); + println!("Packet Frame: {:?}", frame); + break; } } } diff --git a/examples/udp_ping.rs b/examples/udp_ping.rs index 580e785..06c9042 100644 --- a/examples/udp_ping.rs +++ b/examples/udp_ping.rs @@ -54,7 +54,7 @@ fn main() { let src_ip: IpAddr = match target_ip { IpAddr::V4(_) => interface .ipv4 - .get(0) + .first() .map(|v| IpAddr::V4(v.addr())) .expect("No IPv4 address on interface"), IpAddr::V6(_) => interface @@ -93,7 +93,7 @@ fn main() { .source(if use_tun { MacAddr::zero() } else { - interface.mac_addr.clone().unwrap() + interface.mac_addr.unwrap() }) .destination(if use_tun { MacAddr::zero() @@ -126,38 +126,38 @@ fn main() { parse_option.from_ip_packet = true; parse_option.offset = if interface.is_loopback() { 14 } else { 0 }; } - let frame = Frame::from_buf(&packet, parse_option).unwrap(); + let frame = Frame::from_buf(packet, parse_option).unwrap(); if let Some(ip_layer) = &frame.ip { - if let Some(icmp) = &ip_layer.icmp { - if icmp.icmp_type == IcmpType::DestinationUnreachable { - println!( - "Received ICMP Port Unreachable (v4) from {}", - ip_layer.ipv4.as_ref().unwrap().source - ); - println!( - "---- Interface: {}, Total Length: {} bytes ----", - interface.name, - packet.len() - ); - println!("Packet Frame: {:?}", frame); - break; - } + if let Some(icmp) = &ip_layer.icmp + && icmp.icmp_type == IcmpType::DestinationUnreachable + { + println!( + "Received ICMP Port Unreachable (v4) from {}", + ip_layer.ipv4.as_ref().unwrap().source + ); + println!( + "---- Interface: {}, Total Length: {} bytes ----", + interface.name, + packet.len() + ); + println!("Packet Frame: {:?}", frame); + break; } - if let Some(icmpv6) = &ip_layer.icmpv6 { - if icmpv6.icmpv6_type == Icmpv6Type::DestinationUnreachable { - println!( - "Received ICMP Port Unreachable (v6) from {}", - ip_layer.ipv6.as_ref().unwrap().source - ); - println!( - "---- Interface: {}, Total Length: {} bytes ----", - interface.name, - packet.len() - ); - println!("Packet Frame: {:?}", frame); - break; - } + if let Some(icmpv6) = &ip_layer.icmpv6 + && icmpv6.icmpv6_type == Icmpv6Type::DestinationUnreachable + { + println!( + "Received ICMP Port Unreachable (v6) from {}", + ip_layer.ipv6.as_ref().unwrap().source + ); + println!( + "---- Interface: {}, Total Length: {} bytes ----", + interface.name, + packet.len() + ); + println!("Packet Frame: {:?}", frame); + break; } } } diff --git a/nex-datalink/src/async_io/bpf.rs b/nex-datalink/src/async_io/bpf.rs index a8b39b6..7ed4ff6 100644 --- a/nex-datalink/src/async_io/bpf.rs +++ b/nex-datalink/src/async_io/bpf.rs @@ -136,7 +136,7 @@ impl Stream for AsyncBpfSocketReceiver { let padding = ETHERNET_HEADER_SIZE - me.inner.buffer_offset; start -= padding; } - for i in (&mut me.read_buffer[start..start + me.inner.buffer_offset]).iter_mut() { + for i in me.read_buffer[start..start + me.inner.buffer_offset].iter_mut() { *i = 0; } let pkt = me.read_buffer[start..start + len].to_vec(); diff --git a/nex-datalink/src/async_io/mod.rs b/nex-datalink/src/async_io/mod.rs index 71932a3..b221c91 100644 --- a/nex-datalink/src/async_io/mod.rs +++ b/nex-datalink/src/async_io/mod.rs @@ -55,18 +55,18 @@ pub fn async_channel( network_interface: &nex_core::interface::Interface, configuration: Config, ) -> io::Result { - #[cfg(all(any(target_os = "linux", target_os = "android")))] + #[cfg(any(target_os = "linux", target_os = "android"))] { linux::channel(network_interface, configuration) } - #[cfg(all(any( + #[cfg(any( target_os = "freebsd", target_os = "netbsd", target_os = "illumos", target_os = "solaris", target_os = "macos", target_os = "ios", - )))] + ))] { bpf::channel(network_interface, configuration) } diff --git a/nex-datalink/src/bpf.rs b/nex-datalink/src/bpf.rs index b4c88f3..dc38f02 100644 --- a/nex-datalink/src/bpf.rs +++ b/nex-datalink/src/bpf.rs @@ -3,7 +3,6 @@ use crate::bindings::bpf; use crate::{RawReceiver, RawSender}; use nex_core::interface::Interface; -use nex_sys; use std::collections::VecDeque; use std::ffi::CString; @@ -40,7 +39,7 @@ pub struct Config { pub bpf_fd_attempts: usize, } -impl<'a> From<&'a super::Config> for Config { +impl From<&super::Config> for Config { fn from(config: &super::Config) -> Config { Config { write_buffer_size: config.write_buffer_size, @@ -182,9 +181,7 @@ pub fn channel(network_interface: &Interface, config: Config) -> io::Result io::Result io::Result return Some(Err(io::Error::last_os_error())), - _ => (), - } + } else if unsafe { + libc::write( + self.fd.fd, + chunk.as_ptr().add(offset) as *const libc::c_void, + (chunk.len() - offset) as libc::size_t, + ) + } == -1 + { + return Some(Err(io::Error::last_os_error())); } } Some(Ok(())) @@ -328,18 +319,18 @@ impl RawSender for RawSenderImpl { ) }; if ret == -1 { - return Some(Err(io::Error::last_os_error())); + Some(Err(io::Error::last_os_error())) } else if ret == 0 { - return Some(Err(io::Error::new(io::ErrorKind::TimedOut, "Timed out"))); + Some(Err(io::Error::new(io::ErrorKind::TimedOut, "Timed out"))) } else { match unsafe { libc::write( self.fd.fd, - packet.as_ptr().offset(offset as isize) as *const libc::c_void, + packet.as_ptr().add(offset) as *const libc::c_void, (packet.len() - offset) as libc::size_t, ) } { - len if len == -1 => Some(Err(io::Error::last_os_error())), + -1 => Some(Err(io::Error::last_os_error())), _ => Some(Ok(())), } } @@ -424,7 +415,7 @@ impl RawReceiver for RawReceiverImpl { start -= padding; } // Zero out part that will become fake ethernet header if on loopback. - for i in (&mut self.read_buffer[start..start + self.buffer_offset]).iter_mut() { + for i in self.read_buffer[start..start + self.buffer_offset].iter_mut() { *i = 0; } Ok(&self.read_buffer[start..start + len]) diff --git a/nex-datalink/src/lib.rs b/nex-datalink/src/lib.rs index ec604fc..a6fe401 100644 --- a/nex-datalink/src/lib.rs +++ b/nex-datalink/src/lib.rs @@ -10,21 +10,19 @@ mod bindings; pub mod async_io; -#[cfg(windows)] -#[path = "wpcap.rs"] -mod backend; - #[cfg(windows)] pub mod wpcap; -#[cfg(all(any(target_os = "linux", target_os = "android")))] -#[path = "linux.rs"] -mod backend; +#[cfg(windows)] +use wpcap as backend; #[cfg(any(target_os = "linux", target_os = "android"))] pub mod linux; -#[cfg(all(any( +#[cfg(any(target_os = "linux", target_os = "android"))] +use linux as backend; + +#[cfg(any( target_os = "freebsd", target_os = "openbsd", target_os = "netbsd", @@ -32,19 +30,19 @@ pub mod linux; target_os = "solaris", target_os = "macos", target_os = "ios" -)))] -#[path = "bpf.rs"] -mod backend; +))] +pub mod bpf; #[cfg(any( target_os = "freebsd", + target_os = "openbsd", target_os = "netbsd", target_os = "illumos", target_os = "solaris", target_os = "macos", target_os = "ios" ))] -pub mod bpf; +use bpf as backend; #[cfg(feature = "pcap")] pub mod pcap; diff --git a/nex-datalink/src/pcap.rs b/nex-datalink/src/pcap.rs index 3a30fc3..ed2c184 100644 --- a/nex-datalink/src/pcap.rs +++ b/nex-datalink/src/pcap.rs @@ -28,7 +28,7 @@ pub struct Config { pub promiscuous: bool, } -impl<'a> From<&'a super::Config> for Config { +impl From<&super::Config> for Config { fn from(config: &super::Config) -> Config { let mut c = Config { read_buffer_size: config.read_buffer_size, @@ -61,7 +61,7 @@ impl Default for Config { pub fn channel(network_interface: &Interface, config: Config) -> io::Result { let cap = match pcap::Capture::from_device(&*network_interface.name) { Ok(cap) => cap, - Err(e) => return Err(io::Error::new(io::ErrorKind::Other, e)), + Err(e) => return Err(io::Error::other(e)), } .buffer_size(config.read_buffer_size as i32); // Set pcap timeout (in milliseconds). @@ -75,7 +75,7 @@ pub fn channel(network_interface: &Interface, config: Config) -> io::Result cap, - Err(e) => return Err(io::Error::new(io::ErrorKind::Other, e)), + Err(e) => return Err(io::Error::other(e)), }; let cap = Arc::new(Mutex::new(cap)); Ok(Ethernet( @@ -94,7 +94,7 @@ pub fn channel(network_interface: &Interface, config: Config) -> io::Result>(path: P, config: Config) -> io::Result { let cap = match pcap::Capture::from_file(path) { Ok(cap) => cap, - Err(e) => return Err(io::Error::new(io::ErrorKind::Other, e)), + Err(e) => return Err(io::Error::other(e)), }; let cap = Arc::new(Mutex::new(cap)); Ok(Ethernet( @@ -115,7 +115,7 @@ fn lock_capture( ) -> io::Result>> { capture .lock() - .map_err(|_| io::Error::new(io::ErrorKind::Other, "pcap capture mutex poisoned")) + .map_err(|_| io::Error::other("pcap capture mutex poisoned")) } impl RawSender for RawSenderImpl { @@ -134,7 +134,7 @@ impl RawSender for RawSenderImpl { Err(err) => return Some(Err(err)), }; if let Err(e) = cap.sendpacket(data) { - return Some(Err(io::Error::new(io::ErrorKind::Other, e))); + return Some(Err(io::Error::other(e))); } } Some(Ok(())) @@ -148,7 +148,7 @@ impl RawSender for RawSenderImpl { }; Some(match cap.sendpacket(packet) { Ok(()) => Ok(()), - Err(e) => Err(io::Error::new(io::ErrorKind::Other, e)), + Err(e) => Err(io::Error::other(e)), }) } } @@ -185,7 +185,7 @@ impl RawReceiver for RawReceiverImpl { self.read_buffer.truncate(0); self.read_buffer.extend(pkt.data); } - Err(e) => return Err(io::Error::new(io::ErrorKind::Other, e)), + Err(e) => return Err(io::Error::other(e)), }; Ok(&self.read_buffer) } diff --git a/nex-datalink/src/wpcap.rs b/nex-datalink/src/wpcap.rs index d0938bc..2f2ec1e 100644 --- a/nex-datalink/src/wpcap.rs +++ b/nex-datalink/src/wpcap.rs @@ -97,9 +97,8 @@ pub fn channel(network_interface: &Interface, config: Config) -> io::Result MutablePacket<'a> for MutableArpPacket<'a> { } fn header_mut(&mut self) -> &mut [u8] { - let (header, _) = (&mut *self.buffer).split_at_mut(ARP_HEADER_LEN); + let (header, _) = self.buffer.split_at_mut(ARP_HEADER_LEN); header } @@ -490,7 +490,7 @@ impl<'a> MutablePacket<'a> for MutableArpPacket<'a> { } fn payload_mut(&mut self) -> &mut [u8] { - let (_, payload) = (&mut *self.buffer).split_at_mut(ARP_HEADER_LEN); + let (_, payload) = self.buffer.split_at_mut(ARP_HEADER_LEN); payload } } diff --git a/nex-packet/src/builder/ethernet.rs b/nex-packet/src/builder/ethernet.rs index a61bca5..20c2782 100644 --- a/nex-packet/src/builder/ethernet.rs +++ b/nex-packet/src/builder/ethernet.rs @@ -11,6 +11,12 @@ pub struct EthernetPacketBuilder { packet: EthernetPacket, } +impl Default for EthernetPacketBuilder { + fn default() -> Self { + Self::new() + } +} + impl EthernetPacketBuilder { /// Create a new builder instance. pub fn new() -> Self { diff --git a/nex-packet/src/builder/ipv4.rs b/nex-packet/src/builder/ipv4.rs index 526cfbc..af1e39a 100644 --- a/nex-packet/src/builder/ipv4.rs +++ b/nex-packet/src/builder/ipv4.rs @@ -13,6 +13,12 @@ pub struct Ipv4PacketBuilder { packet: Ipv4Packet, } +impl Default for Ipv4PacketBuilder { + fn default() -> Self { + Self::new() + } +} + impl Ipv4PacketBuilder { /// Create a new builder. pub fn new() -> Self { @@ -76,7 +82,7 @@ impl Ipv4PacketBuilder { pub fn options(mut self, options: Vec) -> Self { self.packet.header.options = options; - self.packet.header.header_length = ((20 + self.packet.header.header_length = (20 + self .packet .header @@ -86,9 +92,8 @@ impl Ipv4PacketBuilder { Ipv4OptionType::EOL | Ipv4OptionType::NOP => 1, _ => 2 + opt.data.len(), }) - .sum::() - + 3) - / 4) as u4; // includes padding + .sum::()) + .div_ceil(4) as u4; // includes padding self } diff --git a/nex-packet/src/builder/ipv6.rs b/nex-packet/src/builder/ipv6.rs index 9bf7545..2975312 100644 --- a/nex-packet/src/builder/ipv6.rs +++ b/nex-packet/src/builder/ipv6.rs @@ -12,6 +12,12 @@ pub struct Ipv6PacketBuilder { packet: Ipv6Packet, } +impl Default for Ipv6PacketBuilder { + fn default() -> Self { + Self::new() + } +} + impl Ipv6PacketBuilder { /// Create a new builder pub fn new() -> Self { diff --git a/nex-packet/src/builder/ndp.rs b/nex-packet/src/builder/ndp.rs index d40a072..8ffa0cf 100644 --- a/nex-packet/src/builder/ndp.rs +++ b/nex-packet/src/builder/ndp.rs @@ -7,7 +7,7 @@ use std::net::Ipv6Addr; /// Length rounded up to an 8-byte multiple (for option length) fn octets_len(len: usize) -> u8 { - ((len + 7) / 8) as u8 + len.div_ceil(8) as u8 } /// Builder for ICMPv6 Neighbor Solicitation packets diff --git a/nex-packet/src/builder/tcp.rs b/nex-packet/src/builder/tcp.rs index c199b8c..84e5275 100644 --- a/nex-packet/src/builder/tcp.rs +++ b/nex-packet/src/builder/tcp.rs @@ -24,8 +24,8 @@ impl TcpPacketBuilder { destination: 0, sequence: 0, acknowledgement: 0, - data_offset: 5.into(), // default: header 20 bytes (5 * 4) - reserved: 0.into(), + data_offset: 5, // default: header 20 bytes (5 * 4) + reserved: 0, flags: 0, window: 0xffff, checksum: 0, @@ -38,22 +38,22 @@ impl TcpPacketBuilder { } pub fn source(mut self, port: u16) -> Self { - self.packet.header.source = port.into(); + self.packet.header.source = port; self } pub fn destination(mut self, port: u16) -> Self { - self.packet.header.destination = port.into(); + self.packet.header.destination = port; self } pub fn sequence(mut self, seq: u32) -> Self { - self.packet.header.sequence = seq.into(); + self.packet.header.sequence = seq; self } pub fn acknowledgement(mut self, ack: u32) -> Self { - self.packet.header.acknowledgement = ack.into(); + self.packet.header.acknowledgement = ack; self } @@ -63,12 +63,12 @@ impl TcpPacketBuilder { } pub fn window(mut self, size: u16) -> Self { - self.packet.header.window = size.into(); + self.packet.header.window = size; self } pub fn urgent_ptr(mut self, ptr: u16) -> Self { - self.packet.header.urgent_ptr = ptr.into(); + self.packet.header.urgent_ptr = ptr; self } @@ -84,7 +84,7 @@ impl TcpPacketBuilder { .map(|opt| opt.length() as usize) .sum(); let total = base_len + opt_len; - self.packet.header.data_offset = ((total + 3) / 4) as u8; // round up + self.packet.header.data_offset = total.div_ceil(4) as u8; // round up self } diff --git a/nex-packet/src/builder/udp.rs b/nex-packet/src/builder/udp.rs index eed3c8b..7c83abf 100644 --- a/nex-packet/src/builder/udp.rs +++ b/nex-packet/src/builder/udp.rs @@ -32,19 +32,19 @@ impl UdpPacketBuilder { /// Set the source port pub fn source(mut self, port: u16) -> Self { - self.packet.header.source = port.into(); + self.packet.header.source = port; self } /// Set the destination port pub fn destination(mut self, port: u16) -> Self { - self.packet.header.destination = port.into(); + self.packet.header.destination = port; self } /// Set the checksum (optional) pub fn checksum(mut self, checksum: u16) -> Self { - self.packet.header.checksum = checksum.into(); + self.packet.header.checksum = checksum; self } @@ -66,7 +66,7 @@ impl UdpPacketBuilder { pub fn build(mut self) -> UdpPacket { // Automatically compute the length let total_len = UDP_HEADER_LEN + self.packet.payload.len(); - self.packet.header.length = (total_len as u16).into(); + self.packet.header.length = total_len as u16; // Calculate the checksum self.packet.header.checksum = crate::udp::checksum(&self.packet, &self.src_ip, &self.dst_ip); diff --git a/nex-packet/src/checksum.rs b/nex-packet/src/checksum.rs index 9ae3c5e..51ca95d 100644 --- a/nex-packet/src/checksum.rs +++ b/nex-packet/src/checksum.rs @@ -3,20 +3,15 @@ use std::net::{Ipv4Addr, Ipv6Addr}; /// Controls how and when checksum recalculation happens for a packet. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] pub enum ChecksumMode { /// Checksum updates are handled manually by the caller. + #[default] Manual, /// Checksum updates happen automatically whenever a tracked field changes. Automatic, } -impl Default for ChecksumMode { - fn default() -> Self { - ChecksumMode::Manual - } -} - /// Tracks whether a packet's checksum needs to be recomputed. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct ChecksumState { diff --git a/nex-packet/src/dns.rs b/nex-packet/src/dns.rs index bcf2f36..1e4be40 100644 --- a/nex-packet/src/dns.rs +++ b/nex-packet/src/dns.rs @@ -663,8 +663,8 @@ impl Packet for DnsQueryPacket { }) } - fn from_bytes(mut bytes: Bytes) -> Option { - Self::from_buf(&mut bytes) + fn from_bytes(bytes: Bytes) -> Option { + Self::from_buf(&bytes) } fn to_bytes(&self) -> Bytes { @@ -795,7 +795,7 @@ impl Packet for DnsResponsePacket { let mut pos = 0; - let name_tag = u16::from_be_bytes([buf[pos], buf[pos + 1]]).into(); + let name_tag = u16::from_be_bytes([buf[pos], buf[pos + 1]]); pos += 2; let rtype = DnsType::new(u16::from_be_bytes([buf[pos], buf[pos + 1]])); @@ -804,10 +804,10 @@ impl Packet for DnsResponsePacket { let rclass = DnsClass::new(u16::from_be_bytes([buf[pos], buf[pos + 1]])); pos += 2; - let ttl = u32::from_be_bytes([buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3]]).into(); + let ttl = u32::from_be_bytes([buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3]]); pos += 4; - let data_len = u16::from_be_bytes([buf[pos], buf[pos + 1]]).into(); + let data_len = u16::from_be_bytes([buf[pos], buf[pos + 1]]); pos += 2; let data_len_usize = data_len as usize; @@ -831,18 +831,18 @@ impl Packet for DnsResponsePacket { payload, }) } - fn from_bytes(mut bytes: Bytes) -> Option { - Self::from_buf(&mut bytes) + fn from_bytes(bytes: Bytes) -> Option { + Self::from_buf(&bytes) } fn to_bytes(&self) -> Bytes { let mut buf = bytes::BytesMut::with_capacity(self.total_len()); - buf.put_u16(self.name_tag.into()); + buf.put_u16(self.name_tag); buf.put_u16(self.rtype.value()); buf.put_u16(self.rclass.value()); - buf.put_u32(self.ttl.into()); - buf.put_u16(self.data_len.into()); + buf.put_u32(self.ttl); + buf.put_u16(self.data_len); buf.put_slice(&self.data); buf.freeze() @@ -883,7 +883,7 @@ impl DnsResponsePacket { } // name_tag (2) - let name_tag = u16::from_be_bytes([buf[0], buf[1]]).into(); + let name_tag = u16::from_be_bytes([buf[0], buf[1]]); *buf = &buf[2..]; // rtype (2) @@ -895,7 +895,7 @@ impl DnsResponsePacket { *buf = &buf[2..]; // ttl (4) - let ttl = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]).into(); + let ttl = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]); *buf = &buf[4..]; // data_len (2) @@ -914,7 +914,7 @@ impl DnsResponsePacket { rtype, rclass, ttl, - data_len: data_len.into(), + data_len, data, payload, }) @@ -1047,12 +1047,12 @@ impl Packet for DnsPacket { flags |= (self.header.is_non_authenticated_data as u16) << 4; flags |= self.header.rcode.value() as u16; - buf.put_u16(self.header.id.into()); + buf.put_u16(self.header.id); buf.put_u16(flags); - buf.put_u16(self.header.query_count.into()); - buf.put_u16(self.header.response_count.into()); - buf.put_u16(self.header.authority_rr_count.into()); - buf.put_u16(self.header.additional_rr_count.into()); + buf.put_u16(self.header.query_count); + buf.put_u16(self.header.response_count); + buf.put_u16(self.header.authority_rr_count); + buf.put_u16(self.header.additional_rr_count); // Write all queries for query in &self.queries { @@ -1127,7 +1127,7 @@ impl DnsPacket { cursor = &cursor[12..]; let header = DnsHeader { - id: id.into(), + id, is_response: ((flags >> 15) & 0x1) as u8, opcode: OpCode::new(((flags >> 11) & 0xF) as u8), is_authoriative: ((flags >> 10) & 0x1) as u8, @@ -1138,10 +1138,10 @@ impl DnsPacket { is_answer_authenticated: ((flags >> 5) & 0x1) as u8, is_non_authenticated_data: ((flags >> 4) & 0x1) as u8, rcode: RetCode::new((flags & 0xF) as u8), - query_count: query_count.into(), - response_count: response_count.into(), - authority_rr_count: authority_rr_count.into(), - additional_rr_count: additional_rr_count.into(), + query_count, + response_count, + authority_rr_count, + additional_rr_count, }; // Parse each section, passing mutable slices diff --git a/nex-packet/src/ethernet.rs b/nex-packet/src/ethernet.rs index 93b1c75..497e061 100644 --- a/nex-packet/src/ethernet.rs +++ b/nex-packet/src/ethernet.rs @@ -352,7 +352,7 @@ impl<'a> MutablePacket<'a> for MutableEthernetPacket<'a> { } fn header_mut(&mut self) -> &mut [u8] { - let (header, _) = (&mut *self.buffer).split_at_mut(ETHERNET_HEADER_LEN); + let (header, _) = self.buffer.split_at_mut(ETHERNET_HEADER_LEN); header } @@ -361,7 +361,7 @@ impl<'a> MutablePacket<'a> for MutableEthernetPacket<'a> { } fn payload_mut(&mut self) -> &mut [u8] { - let (_, payload) = (&mut *self.buffer).split_at_mut(ETHERNET_HEADER_LEN); + let (_, payload) = self.buffer.split_at_mut(ETHERNET_HEADER_LEN); payload } } @@ -517,7 +517,7 @@ mod tests { assert_eq!(packet_view[34], 0xaa); } - drop(ethernet); + let _ = ethernet; assert_eq!(raw[22], 99); assert_eq!(&raw[26..30], &[10, 0, 0, 1]); assert_eq!(raw[34], 0xaa); diff --git a/nex-packet/src/flowcontrol.rs b/nex-packet/src/flowcontrol.rs index bb6b7a1..d68385a 100644 --- a/nex-packet/src/flowcontrol.rs +++ b/nex-packet/src/flowcontrol.rs @@ -69,7 +69,7 @@ impl Packet for FlowControlPacket { Some(Self { command, - quanta: quanta.into(), + quanta, payload, }) } @@ -82,7 +82,7 @@ impl Packet for FlowControlPacket { let mut buf = bytes::BytesMut::with_capacity(4 + self.payload.len()); buf.put_u16(self.command.value()); - buf.put_u16(self.quanta.into()); + buf.put_u16(self.quanta); buf.put_slice(&self.payload); buf.freeze() @@ -91,7 +91,7 @@ impl Packet for FlowControlPacket { let mut buf = bytes::BytesMut::with_capacity(4); buf.put_u16(self.command.value()); - buf.put_u16(self.quanta.into()); + buf.put_u16(self.quanta); buf.freeze() } diff --git a/nex-packet/src/frame.rs b/nex-packet/src/frame.rs index 077c6b5..658352e 100644 --- a/nex-packet/src/frame.rs +++ b/nex-packet/src/frame.rs @@ -43,20 +43,12 @@ pub struct TransportLayer { #[derive(Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Default)] pub struct ParseOption { pub from_ip_packet: bool, pub offset: usize, } -impl Default for ParseOption { - fn default() -> Self { - Self { - from_ip_packet: false, - offset: 0, - } - } -} - #[derive(Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Frame { @@ -377,6 +369,42 @@ fn find_payload_slice<'a>( &available[available.len() - payload_len..] } +fn parse_icmp_packet(packet: Bytes, frame: &mut Frame) { + match IcmpPacket::from_bytes(packet.clone()) { + Some(icmp_packet) => { + let (header, payload) = icmp_packet.into_parts(); + if let Some(ip) = &mut frame.ip { + ip.icmp = Some(header); + } + frame.payload = payload; + } + None => { + if let Some(ip) = &mut frame.ip { + ip.icmp = None; + } + frame.payload = packet; + } + } +} + +fn parse_icmpv6_packet(packet: Bytes, frame: &mut Frame) { + match Icmpv6Packet::from_bytes(packet.clone()) { + Some(icmpv6_packet) => { + let (header, payload) = icmpv6_packet.into_parts(); + if let Some(ip) = &mut frame.ip { + ip.icmpv6 = Some(header); + } + frame.payload = payload; + } + None => { + if let Some(ip) = &mut frame.ip { + ip.icmpv6 = None; + } + frame.payload = packet; + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -464,39 +492,3 @@ mod tests { assert_eq!(packet.payload, Bytes::from(ipv4.to_vec())); } } - -fn parse_icmp_packet(packet: Bytes, frame: &mut Frame) { - match IcmpPacket::from_bytes(packet.clone()) { - Some(icmp_packet) => { - let (header, payload) = icmp_packet.into_parts(); - if let Some(ip) = &mut frame.ip { - ip.icmp = Some(header); - } - frame.payload = payload; - } - None => { - if let Some(ip) = &mut frame.ip { - ip.icmp = None; - } - frame.payload = packet; - } - } -} - -fn parse_icmpv6_packet(packet: Bytes, frame: &mut Frame) { - match Icmpv6Packet::from_bytes(packet.clone()) { - Some(icmpv6_packet) => { - let (header, payload) = icmpv6_packet.into_parts(); - if let Some(ip) = &mut frame.ip { - ip.icmpv6 = Some(header); - } - frame.payload = payload; - } - None => { - if let Some(ip) = &mut frame.ip { - ip.icmpv6 = None; - } - frame.payload = packet; - } - } -} diff --git a/nex-packet/src/gre.rs b/nex-packet/src/gre.rs index de3f902..5152153 100644 --- a/nex-packet/src/gre.rs +++ b/nex-packet/src/gre.rs @@ -91,7 +91,7 @@ impl Packet for GrePacket { recursion_control, zero_flags, version, - protocol_type: protocol_type.into(), + protocol_type, checksum, offset, key, @@ -122,7 +122,7 @@ impl Packet for GrePacket { flags |= self.version as u16; buf.put_u16(flags); - buf.put_u16(self.protocol_type.into()); + buf.put_u16(self.protocol_type); if self.checksum_present != 0 || self.routing_present != 0 { for c in &self.checksum { @@ -170,7 +170,7 @@ impl Packet for GrePacket { flags |= self.version as u16; buf.put_u16(flags); - buf.put_u16(self.protocol_type.into()); + buf.put_u16(self.protocol_type); if self.checksum_present != 0 || self.routing_present != 0 { for c in &self.checksum { @@ -269,7 +269,7 @@ mod tests { 0x00, ]); - let gre_packet = GrePacket::from_buf(&mut packet.clone()).unwrap(); + let gre_packet = GrePacket::from_buf(&packet.clone()).unwrap(); assert_eq!(&gre_packet.to_bytes(), &packet); } @@ -285,7 +285,7 @@ mod tests { 0x00, ]); - let gre_packet = GrePacket::from_buf(&mut packet.clone()).unwrap(); + let gre_packet = GrePacket::from_buf(&packet.clone()).unwrap(); assert_eq!(&gre_packet.to_bytes(), &packet); } diff --git a/nex-packet/src/icmp.rs b/nex-packet/src/icmp.rs index d95b6ae..10893be 100644 --- a/nex-packet/src/icmp.rs +++ b/nex-packet/src/icmp.rs @@ -242,7 +242,7 @@ impl Packet for IcmpPacket { impl IcmpPacket { pub fn with_computed_checksum(&self) -> Self { let mut pkt = self.clone(); - pkt.header.checksum = checksum(&pkt).into(); + pkt.header.checksum = checksum(&pkt); pkt } } @@ -277,7 +277,7 @@ impl<'a> MutablePacket<'a> for MutableIcmpPacket<'a> { } fn header_mut(&mut self) -> &mut [u8] { - let (header, _) = (&mut *self.buffer).split_at_mut(ICMP_COMMON_HEADER_LEN); + let (header, _) = self.buffer.split_at_mut(ICMP_COMMON_HEADER_LEN); header } @@ -286,7 +286,7 @@ impl<'a> MutablePacket<'a> for MutableIcmpPacket<'a> { } fn payload_mut(&mut self) -> &mut [u8] { - let (_, payload) = (&mut *self.buffer).split_at_mut(ICMP_COMMON_HEADER_LEN); + let (_, payload) = self.buffer.split_at_mut(ICMP_COMMON_HEADER_LEN); payload } } @@ -541,8 +541,8 @@ pub mod echo_reply { Ok(Self { header: pkt.header, - identifier: u16::from_be_bytes([pkt.payload[0], pkt.payload[1]]).into(), - sequence_number: u16::from_be_bytes([pkt.payload[2], pkt.payload[3]]).into(), + identifier: u16::from_be_bytes([pkt.payload[0], pkt.payload[1]]), + sequence_number: u16::from_be_bytes([pkt.payload[2], pkt.payload[3]]), payload: pkt.payload.slice(4..), }) } @@ -615,8 +615,8 @@ pub mod destination_unreachable { Ok(Self { header: pkt.header, - unused: u16::from_be_bytes([pkt.payload[0], pkt.payload[1]]).into(), - next_hop_mtu: u16::from_be_bytes([pkt.payload[2], pkt.payload[3]]).into(), + unused: u16::from_be_bytes([pkt.payload[0], pkt.payload[1]]), + next_hop_mtu: u16::from_be_bytes([pkt.payload[2], pkt.payload[3]]), payload: pkt.payload.slice(4..), }) } @@ -664,8 +664,7 @@ pub mod time_exceeded { pkt.payload[1], pkt.payload[2], pkt.payload[3], - ]) - .into(), + ]), payload: pkt.payload.slice(4..), }) } @@ -802,7 +801,7 @@ mod tests { assert_eq!(packet.get_checksum(), updated); let frozen = packet.freeze().expect("freeze"); - let expected: u16 = checksum(&frozen).into(); + let expected: u16 = checksum(&frozen); assert_eq!(packet.get_checksum(), expected); } @@ -822,7 +821,7 @@ mod tests { assert!(!packet.is_checksum_dirty()); let frozen = packet.freeze().expect("freeze"); - let expected: u16 = checksum(&frozen).into(); + let expected: u16 = checksum(&frozen); assert_ne!(baseline, expected); assert_eq!(packet.get_checksum(), expected); } diff --git a/nex-packet/src/icmpv6.rs b/nex-packet/src/icmpv6.rs index bf911c5..a04f054 100644 --- a/nex-packet/src/icmpv6.rs +++ b/nex-packet/src/icmpv6.rs @@ -327,7 +327,7 @@ impl<'a> MutablePacket<'a> for MutableIcmpv6Packet<'a> { } fn header_mut(&mut self) -> &mut [u8] { - let (header, _) = (&mut *self.buffer).split_at_mut(ICMPV6_COMMON_HEADER_LEN); + let (header, _) = self.buffer.split_at_mut(ICMPV6_COMMON_HEADER_LEN); header } @@ -336,7 +336,7 @@ impl<'a> MutablePacket<'a> for MutableIcmpv6Packet<'a> { } fn payload_mut(&mut self) -> &mut [u8] { - let (_, payload) = (&mut *self.buffer).split_at_mut(ICMPV6_COMMON_HEADER_LEN); + let (_, payload) = self.buffer.split_at_mut(ICMPV6_COMMON_HEADER_LEN); payload } } @@ -817,12 +817,10 @@ pub mod ndp { pub fn option_payload_length(&self) -> usize { //let len = option.get_length(); let len = self.payload.len(); - if len > 0 { ((len * 8) - 2) as usize } else { 0 } + if len > 0 { (len * 8) - 2 } else { 0 } } } - /// Calculate a length of a `NdpOption`'s payload. - /// Router Solicitation Message [RFC 4861 Section 4.1] /// /// ```text @@ -1334,7 +1332,7 @@ pub mod ndp { bytes.push(self.header.icmpv6_code.value()); bytes.extend_from_slice(&self.header.checksum.to_be_bytes()); bytes.extend_from_slice(&self.reserved.to_be_bytes()); - for (_, segment) in self.target_addr.segments().iter().enumerate() { + for segment in self.target_addr.segments().iter() { bytes.extend_from_slice(&segment.to_be_bytes()); } for option in &self.options { @@ -1784,10 +1782,10 @@ pub mod ndp { bytes.push(self.header.icmpv6_code.value()); bytes.extend_from_slice(&self.header.checksum.to_be_bytes()); bytes.extend_from_slice(&self.reserved.to_be_bytes()); - for (_, segment) in self.target_addr.segments().iter().enumerate() { + for segment in self.target_addr.segments().iter() { bytes.extend_from_slice(&segment.to_be_bytes()); } - for (_, segment) in self.dest_addr.segments().iter().enumerate() { + for segment in self.dest_addr.segments().iter() { bytes.extend_from_slice(&segment.to_be_bytes()); } for option in &self.options { diff --git a/nex-packet/src/ip.rs b/nex-packet/src/ip.rs index b4d37ec..3eb3a71 100644 --- a/nex-packet/src/ip.rs +++ b/nex-packet/src/ip.rs @@ -256,17 +256,17 @@ pub enum IpNextProtocol { Sm = 122, /// Performance Transparency Protocol Ptp = 123, - /// + /// Intermediate System to Intermediate System over IPv4. IsisOverIpv4 = 124, - /// + /// FIRE. Fire = 125, /// Combat Radio Transport Protocol Crtp = 126, /// Combat Radio User Datagram Crudp = 127, - /// + /// Service-Specific Connection Oriented Protocol in a Multimedia Communications Environment. Sscopmce = 128, - /// + /// IPLT. Iplt = 129, /// Secure Packet Shield Sps = 130, diff --git a/nex-packet/src/ipv4.rs b/nex-packet/src/ipv4.rs index 4376842..d88bc66 100644 --- a/nex-packet/src/ipv4.rs +++ b/nex-packet/src/ipv4.rs @@ -242,7 +242,7 @@ impl Packet for Ipv4Packet { } // padding - while tmp_buf.len() % 4 != 0 { + while !tmp_buf.len().is_multiple_of(4) { tmp_buf.put_u8(0); } @@ -254,8 +254,8 @@ impl Packet for Ipv4Packet { let mut buf = BytesMut::with_capacity(self.total_len()); - buf.put_u8((self.header.version << 4 | header_len_words) as u8); - buf.put_u8((self.header.dscp << 2 | self.header.ecn) as u8); + buf.put_u8(self.header.version << 4 | header_len_words); + buf.put_u8(self.header.dscp << 2 | self.header.ecn); // 2. Fixed header fields // Keep header total length consistent with the actual serialized packet length. @@ -557,7 +557,7 @@ impl<'a> MutablePacket<'a> for MutableIpv4Packet<'a> { fn header_mut(&mut self) -> &mut [u8] { let header_len = self.header_len(); - let (header, _) = (&mut *self.buffer).split_at_mut(header_len); + let (header, _) = self.buffer.split_at_mut(header_len); header } @@ -570,7 +570,7 @@ impl<'a> MutablePacket<'a> for MutableIpv4Packet<'a> { fn payload_mut(&mut self) -> &mut [u8] { let header_len = self.header_len(); let payload_len = self.payload_len(); - let (_, payload) = (&mut *self.buffer).split_at_mut(header_len); + let (_, payload) = self.buffer.split_at_mut(header_len); &mut payload[..payload_len] } } @@ -1030,7 +1030,7 @@ mod tests { } let frozen = packet.freeze().expect("freeze mutable packet"); - drop(packet); + let _ = packet; assert_eq!(raw[8], 128); assert_eq!(&raw[16..20], &[192, 0, 2, 1]); @@ -1105,6 +1105,9 @@ mod tests { let packet = Ipv4Packet::from_bytes(raw.clone()).expect("TSO-style packet should parse"); assert_eq!(packet.header.total_length as usize, raw.len()); assert_eq!(packet.payload.len(), raw.len() - IPV4_HEADER_LEN); - assert_eq!(packet.payload, Bytes::from_static(&[0xde, 0xad, 0xbe, 0xef])); + assert_eq!( + packet.payload, + Bytes::from_static(&[0xde, 0xad, 0xbe, 0xef]) + ); } } diff --git a/nex-packet/src/ipv6.rs b/nex-packet/src/ipv6.rs index 055cf9c..4761c9d 100644 --- a/nex-packet/src/ipv6.rs +++ b/nex-packet/src/ipv6.rs @@ -187,6 +187,7 @@ fn parse_ipv6_from_bytes(bytes: Bytes, strict: bool) -> Result( bytes: &[u8], strict: bool, @@ -389,7 +390,7 @@ impl<'a> MutablePacket<'a> for MutableIpv6Packet<'a> { } fn header_mut(&mut self) -> &mut [u8] { - let (header, _) = (&mut *self.buffer).split_at_mut(IPV6_HEADER_LEN); + let (header, _) = self.buffer.split_at_mut(IPV6_HEADER_LEN); header } @@ -398,7 +399,7 @@ impl<'a> MutablePacket<'a> for MutableIpv6Packet<'a> { } fn payload_mut(&mut self) -> &mut [u8] { - let (_, payload) = (&mut *self.buffer).split_at_mut(IPV6_HEADER_LEN); + let (_, payload) = self.buffer.split_at_mut(IPV6_HEADER_LEN); payload } } @@ -556,16 +557,21 @@ impl Ipv6ExtensionHeader { Ipv6ExtensionHeader::HopByHop { data, .. } | Ipv6ExtensionHeader::Destination { data, .. } => { let base = 2 + data.len(); - (base + 7) / 8 * 8 // padding to multiple of 8 + base.div_ceil(8) * 8 // padding to multiple of 8 } Ipv6ExtensionHeader::Routing { data, .. } => { let base = 4 + data.len(); - (base + 7) / 8 * 8 + base.div_ceil(8) * 8 } Ipv6ExtensionHeader::Fragment { .. } => 8, Ipv6ExtensionHeader::Raw { raw, .. } => raw.len(), } } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + pub fn kind(&self) -> ExtensionHeaderType { match self { Ipv6ExtensionHeader::HopByHop { .. } => ExtensionHeaderType::HopByHop, @@ -574,7 +580,7 @@ impl Ipv6ExtensionHeader { Ipv6ExtensionHeader::Fragment { .. } => ExtensionHeaderType::Fragment, Ipv6ExtensionHeader::Raw { raw, .. } => { // Even for Raw we can read the first byte to guess the kind - let kind = raw.get(0).copied().unwrap_or(0xff); + let kind = raw.first().copied().unwrap_or(0xff); match kind { 0 => ExtensionHeaderType::HopByHop, 43 => ExtensionHeaderType::Routing, diff --git a/nex-packet/src/packet.rs b/nex-packet/src/packet.rs index bf511ef..7d650d2 100644 --- a/nex-packet/src/packet.rs +++ b/nex-packet/src/packet.rs @@ -33,6 +33,7 @@ pub trait Packet: Sized { self.total_len() == 0 } /// Convert the packet to a mutable byte buffer. + #[allow(clippy::wrong_self_convention)] fn to_bytes_mut(&self) -> BytesMut { let mut buf = BytesMut::with_capacity(self.total_len()); buf.extend_from_slice(&self.to_bytes()); @@ -127,7 +128,7 @@ impl<'a, P: Packet> MutablePacket<'a> for GenericMutablePacket<'a, P> { fn header_mut(&mut self) -> &mut [u8] { let (header_len, _) = self.lengths(); - let (header, _) = (&mut *self.buffer).split_at_mut(header_len); + let (header, _) = self.buffer.split_at_mut(header_len); header } @@ -138,7 +139,7 @@ impl<'a, P: Packet> MutablePacket<'a> for GenericMutablePacket<'a, P> { fn payload_mut(&mut self) -> &mut [u8] { let (header_len, payload_len) = self.lengths(); - let (_, payload) = (&mut *self.buffer).split_at_mut(header_len); + let (_, payload) = self.buffer.split_at_mut(header_len); &mut payload[..payload_len] } } diff --git a/nex-packet/src/tcp.rs b/nex-packet/src/tcp.rs index f8c1de9..0364a8a 100644 --- a/nex-packet/src/tcp.rs +++ b/nex-packet/src/tcp.rs @@ -288,7 +288,7 @@ impl TcpOptionHeader { their.copy_from_slice(&self.data[4..8]); (u32::from_be_bytes(my), u32::from_be_bytes(their)) } else { - return (0, 0); + (0, 0) } } /// Get the MSS of the TCP option @@ -303,7 +303,7 @@ impl TcpOptionHeader { } /// Get the WSCALE of the TCP option pub fn get_wscale(&self) -> u8 { - if self.kind == TcpOptionKind::WSCALE && self.data.len() > 0 { + if self.kind == TcpOptionKind::WSCALE && !self.data.is_empty() { self.data[0] } else { 0 @@ -400,12 +400,7 @@ impl TcpOptionPacket { } /// Get length of the TCP option. pub fn length(&self) -> u8 { - if let Some(len) = self.length { - len - } else { - // If length is None, it means the option has no length (like NOP). - 0 - } + self.length.unwrap_or_default() } /// Get the timestamp of the TCP option pub fn get_timestamp(&self) -> (u32, u32) { @@ -416,7 +411,7 @@ impl TcpOptionPacket { their.copy_from_slice(&self.data[4..8]); (u32::from_be_bytes(my), u32::from_be_bytes(their)) } else { - return (0, 0); + (0, 0) } } /// Get the MSS of the TCP option @@ -431,7 +426,7 @@ impl TcpOptionPacket { } /// Get the WSCALE of the TCP option pub fn get_wscale(&self) -> u8 { - if self.kind == TcpOptionKind::WSCALE && self.data.len() > 0 { + if self.kind == TcpOptionKind::WSCALE && !self.data.is_empty() { self.data[0] } else { 0 @@ -466,8 +461,8 @@ pub struct TcpPacket { impl Packet for TcpPacket { type Header = TcpHeader; - fn from_buf(mut bytes: &[u8]) -> Option { - Self::try_from_buf(&mut bytes).ok() + fn from_buf(bytes: &[u8]) -> Option { + Self::try_from_buf(bytes).ok() } fn from_bytes(mut bytes: Bytes) -> Option { Self::try_from_bytes(bytes.split_to(bytes.len())).ok() @@ -846,7 +841,7 @@ impl<'a> MutablePacket<'a> for MutableTcpPacket<'a> { fn header_mut(&mut self) -> &mut [u8] { let len = self.header_len(); - let (header, _) = (&mut *self.buffer).split_at_mut(len); + let (header, _) = self.buffer.split_at_mut(len); header } @@ -857,7 +852,7 @@ impl<'a> MutablePacket<'a> for MutableTcpPacket<'a> { fn payload_mut(&mut self) -> &mut [u8] { let len = self.header_len(); - let (_, payload) = (&mut *self.buffer).split_at_mut(len); + let (_, payload) = self.buffer.split_at_mut(len); payload } } @@ -1227,8 +1222,8 @@ mod tests { destination: 0x2328, sequence: 0x9037d2b8, acknowledgement: 0x944bb276, - data_offset: 8.into(), // 8 * 4 = 32 bytes - reserved: 0.into(), + data_offset: 8, // 8 * 4 = 32 bytes + reserved: 0, flags: 0x18, // PSH + ACK window: 0x0faf, checksum: 0xc031, @@ -1304,7 +1299,7 @@ mod tests { let frozen = packet.freeze().expect("freeze"); let expected = ipv4_checksum(&frozen, &src, &dst); - assert_eq!(updated, expected as u16); + assert_eq!(updated, expected); } #[test] diff --git a/nex-packet/src/udp.rs b/nex-packet/src/udp.rs index f7ae44e..c146656 100644 --- a/nex-packet/src/udp.rs +++ b/nex-packet/src/udp.rs @@ -35,8 +35,8 @@ pub struct UdpPacket { impl Packet for UdpPacket { type Header = UdpHeader; - fn from_buf(mut bytes: &[u8]) -> Option { - Self::try_from_buf(&mut bytes).ok() + fn from_buf(bytes: &[u8]) -> Option { + Self::try_from_buf(bytes).ok() } fn from_bytes(mut bytes: Bytes) -> Option { Self::try_from_bytes(bytes.split_to(bytes.len())).ok() @@ -126,7 +126,7 @@ impl<'a> MutablePacket<'a> for MutableUdpPacket<'a> { } fn header_mut(&mut self) -> &mut [u8] { - let (header, _) = (&mut *self.buffer).split_at_mut(UDP_HEADER_LEN); + let (header, _) = self.buffer.split_at_mut(UDP_HEADER_LEN); header } @@ -137,7 +137,7 @@ impl<'a> MutablePacket<'a> for MutableUdpPacket<'a> { fn payload_mut(&mut self) -> &mut [u8] { let total_len = self.total_len(); - let (_, payload) = (&mut *self.buffer).split_at_mut(UDP_HEADER_LEN); + let (_, payload) = self.buffer.split_at_mut(UDP_HEADER_LEN); &mut payload[..total_len.saturating_sub(UDP_HEADER_LEN)] } } diff --git a/nex-packet/src/util.rs b/nex-packet/src/util.rs index 9079ee1..ec2487c 100644 --- a/nex-packet/src/util.rs +++ b/nex-packet/src/util.rs @@ -3,8 +3,6 @@ use crate::ip::IpNextProtocol; use nex_core::bitfield::u16be; -use core::u8; -use core::u16; use std::net::{Ipv4Addr, Ipv6Addr}; /// Convert a value to a byte array. @@ -65,7 +63,7 @@ impl Octets for u8 { /// Calculates a checksum. Used by ipv4 and icmp. The two bytes starting at `skipword * 2` will be /// ignored. Supposed to be the checksum field, which is regarded as zero during calculation. pub fn checksum(data: &[u8], skipword: usize) -> u16be { - if data.len() == 0 { + if data.is_empty() { return 0; } let sum = sum_be_words(data, skipword); @@ -143,11 +141,11 @@ fn ipv6_word_sum(ip: &Ipv6Addr) -> u32 { /// Sum all words (16 bit chunks) in the given data. The word at word offset /// `skipword` will be skipped. Each word is treated as big endian. fn sum_be_words(data: &[u8], skipword: usize) -> u32 { - if data.len() == 0 { + if data.is_empty() { return 0; } let len = data.len(); - let mut cur_data = &data[..]; + let mut cur_data = data; let mut sum = 0u32; let mut i = 0; while cur_data.len() >= 2 { @@ -209,15 +207,15 @@ mod tests { }; unsafe { let slice_data = slice::from_raw_parts_mut(ptr, 12); - for i in 0..11 { - slice_data[i] = i as u8; + for (i, byte) in slice_data.iter_mut().enumerate().take(11) { + *byte = i as u8; } - assert_eq!(7190, sum_be_words(&slice_data, 1)); - assert_eq!(6676, sum_be_words(&slice_data, 2)); + assert_eq!(7190, sum_be_words(slice_data, 1)); + assert_eq!(6676, sum_be_words(slice_data, 2)); // Assert having the skipword outside the range gives correct and equal // results - assert_eq!(7705, sum_be_words(&slice_data, 99)); - assert_eq!(7705, sum_be_words(&slice_data, 101)); + assert_eq!(7705, sum_be_words(slice_data, 99)); + assert_eq!(7705, sum_be_words(slice_data, 101)); } } } diff --git a/nex-packet/src/vlan.rs b/nex-packet/src/vlan.rs index 4f92898..7e84763 100644 --- a/nex-packet/src/vlan.rs +++ b/nex-packet/src/vlan.rs @@ -112,8 +112,8 @@ impl Packet for VlanPacket { payload: Bytes::copy_from_slice(bytes), }) } - fn from_bytes(mut bytes: Bytes) -> Option { - Self::from_buf(&mut bytes) + fn from_bytes(bytes: Bytes) -> Option { + Self::from_buf(&bytes) } fn to_bytes(&self) -> Bytes { @@ -121,7 +121,7 @@ impl Packet for VlanPacket { let pcp_bits = (self.header.priority_code_point.value() as u16 & 0b111) << 13; let dei_bits = (self.header.drop_eligible_id as u16 & 0b1) << 12; - let vlan_bits = self.header.vlan_id as u16 & 0x0FFF; + let vlan_bits = self.header.vlan_id & 0x0FFF; let tci = pcp_bits | dei_bits | vlan_bits; @@ -198,7 +198,7 @@ impl<'a> MutablePacket<'a> for MutableVlanPacket<'a> { } fn header_mut(&mut self) -> &mut [u8] { - let (header, _) = (&mut *self.buffer).split_at_mut(VLAN_HEADER_LEN); + let (header, _) = self.buffer.split_at_mut(VLAN_HEADER_LEN); header } @@ -207,7 +207,7 @@ impl<'a> MutablePacket<'a> for MutableVlanPacket<'a> { } fn payload_mut(&mut self) -> &mut [u8] { - let (_, payload) = (&mut *self.buffer).split_at_mut(VLAN_HEADER_LEN); + let (_, payload) = self.buffer.split_at_mut(VLAN_HEADER_LEN); payload } } @@ -241,7 +241,7 @@ impl<'a> MutableVlanPacket<'a> { pub fn set_drop_eligible_id(&mut self, dei: u1) { let buf = self.raw_mut(); - buf[0] = (buf[0] & !(1 << 4)) | (((dei & 0x1) as u8) << 4); + buf[0] = (buf[0] & !(1 << 4)) | ((dei & 0x1) << 4); } pub fn get_vlan_id(&self) -> u16 { diff --git a/nex-socket/src/icmp/config.rs b/nex-socket/src/icmp/config.rs index 79a9558..cf04c86 100644 --- a/nex-socket/src/icmp/config.rs +++ b/nex-socket/src/icmp/config.rs @@ -41,7 +41,7 @@ impl IcmpSocketType { } /// Converts the ICMP socket type to a `socket2::Type`. - pub(crate) fn to_sock_type(&self) -> SockType { + pub(crate) fn to_sock_type(self) -> SockType { match self { IcmpSocketType::Dgram => SockType::DGRAM, IcmpSocketType::Raw => SockType::RAW, diff --git a/nex-socket/src/lib.rs b/nex-socket/src/lib.rs index a033a53..9b31ecf 100644 --- a/nex-socket/src/lib.rs +++ b/nex-socket/src/lib.rs @@ -44,7 +44,7 @@ impl SocketFamily { } /// Converts the socket family to a `socket2::Domain`. - pub(crate) fn to_domain(&self) -> socket2::Domain { + pub(crate) fn to_domain(self) -> socket2::Domain { match self { SocketFamily::IPV4 => socket2::Domain::IPV4, SocketFamily::IPV6 => socket2::Domain::IPV6, diff --git a/nex-socket/src/tcp/async_impl.rs b/nex-socket/src/tcp/async_impl.rs index c9e48a0..4c476f8 100644 --- a/nex-socket/src/tcp/async_impl.rs +++ b/nex-socket/src/tcp/async_impl.rs @@ -141,7 +141,7 @@ impl AsyncTcpSocket { Ok(_) => { // connection completed immediately (rare case) let std_stream: StdTcpStream = self.socket.into(); - return TcpStream::from_std(std_stream); + TcpStream::from_std(std_stream) } Err(e) if e.kind() == io::ErrorKind::WouldBlock @@ -157,11 +157,9 @@ impl AsyncTcpSocket { return Err(err); } - return Ok(stream); - } - Err(e) => { - return Err(e); + Ok(stream) } + Err(e) => Err(e), } } @@ -407,7 +405,7 @@ impl AsyncTcpSocket { self.socket .local_addr()? .as_socket() - .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "failed to retrieve local address")) + .ok_or_else(|| io::Error::other("failed to retrieve local address")) } /// Convert the internal socket into a Tokio `TcpStream`. diff --git a/nex-socket/src/tcp/config.rs b/nex-socket/src/tcp/config.rs index 19830aa..a634590 100644 --- a/nex-socket/src/tcp/config.rs +++ b/nex-socket/src/tcp/config.rs @@ -24,7 +24,7 @@ impl TcpSocketType { } /// Converts the TCP socket type to a `socket2::Type`. - pub(crate) fn to_sock_type(&self) -> SockType { + pub(crate) fn to_sock_type(self) -> SockType { match self { TcpSocketType::Stream => SockType::STREAM, TcpSocketType::Raw => SockType::RAW, diff --git a/nex-socket/src/tcp/sync_impl.rs b/nex-socket/src/tcp/sync_impl.rs index f43676d..eaeb596 100644 --- a/nex-socket/src/tcp/sync_impl.rs +++ b/nex-socket/src/tcp/sync_impl.rs @@ -516,7 +516,7 @@ impl TcpSocket { self.socket .local_addr()? .as_socket() - .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "failed to retrieve local address")) + .ok_or_else(|| io::Error::other("failed to retrieve local address")) } /// Extract the RAW file descriptor for Unix. diff --git a/nex-socket/src/udp/config.rs b/nex-socket/src/udp/config.rs index 8e9a250..3b59994 100644 --- a/nex-socket/src/udp/config.rs +++ b/nex-socket/src/udp/config.rs @@ -23,7 +23,7 @@ impl UdpSocketType { } /// Converts the UDP socket type to a `socket2::Type`. - pub(crate) fn to_sock_type(&self) -> SockType { + pub(crate) fn to_sock_type(self) -> SockType { match self { UdpSocketType::Dgram => SockType::DGRAM, UdpSocketType::Raw => SockType::RAW, diff --git a/nex-socket/src/udp/sync_impl.rs b/nex-socket/src/udp/sync_impl.rs index 7db7869..51b28c8 100644 --- a/nex-socket/src/udp/sync_impl.rs +++ b/nex-socket/src/udp/sync_impl.rs @@ -1,3 +1,5 @@ +#![allow(clippy::useless_conversion)] + use crate::udp::UdpConfig; use socket2::{Domain, Protocol, Socket, Type as SockType}; use std::io; @@ -188,50 +190,44 @@ impl UdpSocket { target_vendor = "apple" ))] { - if let Some(meta) = meta { - if meta.source_addr.is_some() || meta.interface_index.is_some() { - if let Some(src) = meta.source_addr { - if !src.is_ipv4() { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "source_addr family does not match target", - )); - } - } - let mut pktinfo: libc::in_pktinfo = unsafe { std::mem::zeroed() }; - if let Some(src) = meta.source_addr.and_then(|ip| match ip { - IpAddr::V4(v4) => Some(v4), - IpAddr::V6(_) => None, - }) { - pktinfo.ipi_spec_dst.s_addr = u32::from_ne_bytes(src.octets()); - } - if let Some(ifindex) = meta.interface_index { - pktinfo.ipi_ifindex = ifindex.try_into().map_err(|_| { - io::Error::new( - io::ErrorKind::InvalidInput, - "interface_index is out of range for this platform", - ) - })?; - } - let cmsgs = [ControlMessage::Ipv4PacketInfo(&pktinfo)]; - return sendmsg( - raw_fd, - &iov, - &cmsgs, - MsgFlags::empty(), - Some(&sockaddr), - ) - .map_err(|e| io::Error::from_raw_os_error(e as i32)); + if let Some(meta) = meta + && (meta.source_addr.is_some() || meta.interface_index.is_some()) + { + if let Some(src) = meta.source_addr + && !src.is_ipv4() + { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "source_addr family does not match target", + )); + } + let mut pktinfo: libc::in_pktinfo = unsafe { std::mem::zeroed() }; + if let Some(src) = meta.source_addr.and_then(|ip| match ip { + IpAddr::V4(v4) => Some(v4), + IpAddr::V6(_) => None, + }) { + pktinfo.ipi_spec_dst.s_addr = u32::from_ne_bytes(src.octets()); } + if let Some(ifindex) = meta.interface_index { + pktinfo.ipi_ifindex = ifindex.try_into().map_err(|_| { + io::Error::new( + io::ErrorKind::InvalidInput, + "interface_index is out of range for this platform", + ) + })?; + } + let cmsgs = [ControlMessage::Ipv4PacketInfo(&pktinfo)]; + return sendmsg(raw_fd, &iov, &cmsgs, MsgFlags::empty(), Some(&sockaddr)) + .map_err(|e| io::Error::from_raw_os_error(e as i32)); } } - if let Some(meta) = meta { - if meta.source_addr.is_some() || meta.interface_index.is_some() { - return Err(io::Error::new( - io::ErrorKind::Unsupported, - "send_msg packet-info metadata is not supported on this platform", - )); - } + if let Some(meta) = meta + && (meta.source_addr.is_some() || meta.interface_index.is_some()) + { + return Err(io::Error::new( + io::ErrorKind::Unsupported, + "send_msg packet-info metadata is not supported on this platform", + )); } sendmsg(raw_fd, &iov, &[], MsgFlags::empty(), Some(&sockaddr)) .map_err(|e| io::Error::from_raw_os_error(e as i32)) @@ -246,50 +242,44 @@ impl UdpSocket { target_vendor = "apple" ))] { - if let Some(meta) = meta { - if meta.source_addr.is_some() || meta.interface_index.is_some() { - if let Some(src) = meta.source_addr { - if !src.is_ipv6() { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "source_addr family does not match target", - )); - } - } - let mut pktinfo: libc::in6_pktinfo = unsafe { std::mem::zeroed() }; - if let Some(src) = meta.source_addr.and_then(|ip| match ip { - IpAddr::V4(_) => None, - IpAddr::V6(v6) => Some(v6), - }) { - pktinfo.ipi6_addr.s6_addr = src.octets(); - } - if let Some(ifindex) = meta.interface_index { - pktinfo.ipi6_ifindex = ifindex.try_into().map_err(|_| { - io::Error::new( - io::ErrorKind::InvalidInput, - "interface_index is out of range for this platform", - ) - })?; - } - let cmsgs = [ControlMessage::Ipv6PacketInfo(&pktinfo)]; - return sendmsg( - raw_fd, - &iov, - &cmsgs, - MsgFlags::empty(), - Some(&sockaddr), - ) - .map_err(|e| io::Error::from_raw_os_error(e as i32)); + if let Some(meta) = meta + && (meta.source_addr.is_some() || meta.interface_index.is_some()) + { + if let Some(src) = meta.source_addr + && !src.is_ipv6() + { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "source_addr family does not match target", + )); + } + let mut pktinfo: libc::in6_pktinfo = unsafe { std::mem::zeroed() }; + if let Some(src) = meta.source_addr.and_then(|ip| match ip { + IpAddr::V4(_) => None, + IpAddr::V6(v6) => Some(v6), + }) { + pktinfo.ipi6_addr.s6_addr = src.octets(); + } + if let Some(ifindex) = meta.interface_index { + pktinfo.ipi6_ifindex = ifindex.try_into().map_err(|_| { + io::Error::new( + io::ErrorKind::InvalidInput, + "interface_index is out of range for this platform", + ) + })?; } + let cmsgs = [ControlMessage::Ipv6PacketInfo(&pktinfo)]; + return sendmsg(raw_fd, &iov, &cmsgs, MsgFlags::empty(), Some(&sockaddr)) + .map_err(|e| io::Error::from_raw_os_error(e as i32)); } } - if let Some(meta) = meta { - if meta.source_addr.is_some() || meta.interface_index.is_some() { - return Err(io::Error::new( - io::ErrorKind::Unsupported, - "send_msg packet-info metadata is not supported on this platform", - )); - } + if let Some(meta) = meta + && (meta.source_addr.is_some() || meta.interface_index.is_some()) + { + return Err(io::Error::new( + io::ErrorKind::Unsupported, + "send_msg packet-info metadata is not supported on this platform", + )); } sendmsg(raw_fd, &iov, &[], MsgFlags::empty(), Some(&sockaddr)) .map_err(|e| io::Error::from_raw_os_error(e as i32)) @@ -629,7 +619,7 @@ impl UdpSocket { self.socket .local_addr()? .as_socket() - .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "failed to retrieve local address")) + .ok_or_else(|| io::Error::other("failed to retrieve local address")) } /// Convert into a raw `std::net::UdpSocket`. @@ -672,7 +662,9 @@ mod tests { #[test] fn create_v4_socket() { let sock = UdpSocket::v4_dgram().expect("create socket"); - sock.socket.bind(&"0.0.0.0:0".parse::().unwrap().into()).expect("bind"); + sock.socket + .bind(&"0.0.0.0:0".parse::().unwrap().into()) + .expect("bind"); let addr = sock.local_addr().expect("addr"); assert!(addr.is_ipv4()); } From eec2fd22d3bbfd84c4ed14f34506b9247b82a2ee Mon Sep 17 00:00:00 2001 From: shellrow Date: Sat, 6 Jun 2026 16:04:51 +0900 Subject: [PATCH 02/33] refactor: hide datalink backend modules --- nex-datalink/src/async_io/mod.rs | 6 +++--- nex-datalink/src/lib.rs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/nex-datalink/src/async_io/mod.rs b/nex-datalink/src/async_io/mod.rs index b221c91..8b65a81 100644 --- a/nex-datalink/src/async_io/mod.rs +++ b/nex-datalink/src/async_io/mod.rs @@ -1,7 +1,7 @@ //! Asynchronous data link layer I/O operations. #[cfg(any(target_os = "linux", target_os = "android"))] -pub mod linux; +mod linux; #[cfg(any( target_os = "freebsd", @@ -11,10 +11,10 @@ pub mod linux; target_os = "macos", target_os = "ios", ))] -pub mod bpf; +mod bpf; #[cfg(windows)] -pub mod wpcap; +mod wpcap; use std::io; use std::task::{Context, Poll}; diff --git a/nex-datalink/src/lib.rs b/nex-datalink/src/lib.rs index a6fe401..4124f8b 100644 --- a/nex-datalink/src/lib.rs +++ b/nex-datalink/src/lib.rs @@ -11,13 +11,13 @@ mod bindings; pub mod async_io; #[cfg(windows)] -pub mod wpcap; +mod wpcap; #[cfg(windows)] use wpcap as backend; #[cfg(any(target_os = "linux", target_os = "android"))] -pub mod linux; +mod linux; #[cfg(any(target_os = "linux", target_os = "android"))] use linux as backend; @@ -31,7 +31,7 @@ use linux as backend; target_os = "macos", target_os = "ios" ))] -pub mod bpf; +mod bpf; #[cfg(any( target_os = "freebsd", From c1648c37bc96657437715986ac012d9875cd0346 Mon Sep 17 00:00:00 2001 From: shellrow Date: Sun, 7 Jun 2026 09:47:49 +0900 Subject: [PATCH 03/33] refactor: rename datalink fanout variants --- nex-datalink/src/lib.rs | 55 +++++++++++++++++++++++++++++++++------ nex-datalink/src/linux.rs | 16 ++++++------ 2 files changed, 55 insertions(+), 16 deletions(-) diff --git a/nex-datalink/src/lib.rs b/nex-datalink/src/lib.rs index 4124f8b..4297cfc 100644 --- a/nex-datalink/src/lib.rs +++ b/nex-datalink/src/lib.rs @@ -69,22 +69,61 @@ pub enum Channel { /// Socket fanout type (Linux only). #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum FanoutType { - HASH, - LB, - CPU, - ROLLOVER, - RND, - QM, - CBPF, - EBPF, + /// Fan out packets by hashing packet fields. + Hash, + /// Round-robin load balancing. + LoadBalance, + /// Fan out packets to the CPU that received them. + Cpu, + /// Roll over to the next socket when one socket's queue is full. + Rollover, + /// Random fanout. + Random, + /// Queue-mapping fanout. + QueueMapping, + /// Classic BPF fanout. + ClassicBpf, + /// Extended BPF fanout. + ExtendedBpf, +} + +impl FanoutType { + /// Compatibility alias for [`FanoutType::Hash`]. + #[deprecated(note = "use FanoutType::Hash")] + pub const HASH: Self = Self::Hash; + /// Compatibility alias for [`FanoutType::LoadBalance`]. + #[deprecated(note = "use FanoutType::LoadBalance")] + pub const LB: Self = Self::LoadBalance; + /// Compatibility alias for [`FanoutType::Cpu`]. + #[deprecated(note = "use FanoutType::Cpu")] + pub const CPU: Self = Self::Cpu; + /// Compatibility alias for [`FanoutType::Rollover`]. + #[deprecated(note = "use FanoutType::Rollover")] + pub const ROLLOVER: Self = Self::Rollover; + /// Compatibility alias for [`FanoutType::Random`]. + #[deprecated(note = "use FanoutType::Random")] + pub const RND: Self = Self::Random; + /// Compatibility alias for [`FanoutType::QueueMapping`]. + #[deprecated(note = "use FanoutType::QueueMapping")] + pub const QM: Self = Self::QueueMapping; + /// Compatibility alias for [`FanoutType::ClassicBpf`]. + #[deprecated(note = "use FanoutType::ClassicBpf")] + pub const CBPF: Self = Self::ClassicBpf; + /// Compatibility alias for [`FanoutType::ExtendedBpf`]. + #[deprecated(note = "use FanoutType::ExtendedBpf")] + pub const EBPF: Self = Self::ExtendedBpf; } /// Fanout settings (Linux only). #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct FanoutOption { + /// Fanout group identifier. pub group_id: u16, + /// Fanout distribution strategy. pub fanout_type: FanoutType, + /// Whether fragmented packets should be defragmented before fanout. pub defrag: bool, + /// Whether queue rollover should be enabled. pub rollover: bool, } diff --git a/nex-datalink/src/linux.rs b/nex-datalink/src/linux.rs index c2f4595..00ff87f 100644 --- a/nex-datalink/src/linux.rs +++ b/nex-datalink/src/linux.rs @@ -149,14 +149,14 @@ pub fn channel(network_interface: &Interface, config: Config) -> io::Result linux::PACKET_FANOUT_HASH, - FanoutType::LB => linux::PACKET_FANOUT_LB, - FanoutType::CPU => linux::PACKET_FANOUT_CPU, - FanoutType::ROLLOVER => linux::PACKET_FANOUT_ROLLOVER, - FanoutType::RND => linux::PACKET_FANOUT_RND, - FanoutType::QM => linux::PACKET_FANOUT_QM, - FanoutType::CBPF => linux::PACKET_FANOUT_CBPF, - FanoutType::EBPF => linux::PACKET_FANOUT_EBPF, + FanoutType::Hash => linux::PACKET_FANOUT_HASH, + FanoutType::LoadBalance => linux::PACKET_FANOUT_LB, + FanoutType::Cpu => linux::PACKET_FANOUT_CPU, + FanoutType::Rollover => linux::PACKET_FANOUT_ROLLOVER, + FanoutType::Random => linux::PACKET_FANOUT_RND, + FanoutType::QueueMapping => linux::PACKET_FANOUT_QM, + FanoutType::ClassicBpf => linux::PACKET_FANOUT_CBPF, + FanoutType::ExtendedBpf => linux::PACKET_FANOUT_EBPF, } as u32; // set defrag flag if fanout.defrag { From 40253ad27891153b753f93372f74a502e0ca5d0d Mon Sep 17 00:00:00 2001 From: shellrow Date: Tue, 14 Jul 2026 00:34:23 +0900 Subject: [PATCH 04/33] docs: create TODO.md --- TODO.md | 318 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 318 insertions(+) create mode 100644 TODO.md diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..9c2212c --- /dev/null +++ b/TODO.md @@ -0,0 +1,318 @@ +# nex — Road to v1.0.0 + +A backlog for taking `nex` to a v1.0.0. + +--- + +## 0. Assessment (2026-07) + +What blocks a credible v1.0 (the rest of this document): + +- **API inconsistency is the #1 problem.** Parsing entry points have proliferated + into 8+ shapes with overlapping semantics. This must be unified before 1.0 + freezes the surface. +- **Builders cannot fail.** `build()`/`to_bytes()` never return `Result`; malformed + or over-length input produces silently wrong bytes. +- **Dependency & feature hygiene is weak.** `nex-socket` forces `tokio` on sync + users; `nex-packet` pulls all of `rand` for one line. +- **CI is effectively unverified.** Only `cargo build` on 3 OSes — no tests, no + clippy, no fmt, no feature matrix, no MSRV, no supply-chain checks. +- **Unsafe boundary is undocumented.** 9 `unsafe impl Send/Sync` and ~141 unsafe + sites in `nex-datalink` with no safety rationale comments. +- **No release scaffolding.** No CHANGELOG,MSRV, or semver/API-snapshot tracking. + +--- + +## P0 — Blockers for a Stable v1.0 Surface + +### P0.1 Unify the packet parsing API + +Today the surface is inconsistent and confusing. Observed across `nex-packet`: + +- `Packet` trait: `from_buf(&[u8]) -> Option`, `from_bytes(Bytes) -> Option`. +- Per-type: `try_from_buf -> Result<_, ParseError>`, `try_from_bytes`, + `try_from_buf_strict`, `try_from_bytes_strict`, `from_buf_strict -> Option`, + `from_bytes_strict -> Option`. +- DNS: `from_buf_mut(&mut &[u8]) -> Option`, `from_bytes(&[u8]) -> Result<_, Utf8Error>`. +- Ethernet: `from_bytes(Bytes) -> Result` ← String error. + +Actions: + +- [ ] Define ONE canonical parsing contract and document it in `parse.rs`: + - [ ] `try_from_bytes(Bytes) -> Result` — owned, zero-copy view. + - [ ] `try_from_buf(&[u8]) -> Result` — borrowed. + - [ ] A single explicit "strict payload-length" opt-in (e.g. a `Strictness`/ + `ParseOption` arg) instead of `*_strict` name-doubling every method. +- [ ] Remove `EthernetHeader::from_bytes -> Result<_, String>`; replace with the + canonical `ParseError` form (`nex-packet/src/ethernet.rs:165`). +- [ ] Fold `DnsName::from_bytes -> Result<_, Utf8Error>` into `ParseError::InvalidUtf8` + so DNS matches every other module (`nex-packet/src/dns.rs:1216`). +- [ ] Decide the fate of `from_* -> Option` on the `Packet` trait + (`nex-packet/src/packet.rs:9-12`): either make the trait `try_from_*`-based + with `ParseError`, or keep `Option` only as a thin infallible-intent shim. +- [ ] Provide `#[deprecated]` aliases for one release wherever a public name changes. +- [ ] Write a doc table mapping every old parsing fn → its v1.0 replacement. + +Acceptance: every protocol module exposes the *same* small set of parse fns with +the *same* signatures and error type; `grep -r "Result<.*String>"` returns nothing +in `nex-packet`. + +### P0.2 Make builders fallible and validating + +Every builder currently returns infallibly (`nex-packet/src/builder/*.rs`): +`build(self) -> Packet` and `to_bytes(self) -> Bytes`, with no bounds checks. + +- [ ] Change builder finalizers to `build(self) -> Result` + (or a `try_build`) wherever a field can exceed protocol limits: + - [ ] IPv4/IPv6 total length, IHL/options length, payload length. + - [ ] TCP data offset / options length; UDP length; ICMP/ICMPv6 sizing. + - [ ] DHCP options length; NDP option length; ARP fixed sizing. +- [ ] Validate checksum prerequisites (pseudo-header context present) before emit. +- [ ] Introduce a typed `BuildError` (see P0.4) with actionable variants. +- [ ] Keep an infallible fast path only where inputs are provably in-range (e.g. + fixed-size headers), and document why. + +Acceptance: constructing an over-length packet returns `Err`, never wrong bytes; +property tests (P1.5) confirm build→parse round-trips for valid inputs only. + +### P0.3 Freeze the public API surface (semver contract) + +- [ ] Audit every `pub` item per crate; mark internal helpers `pub(crate)`. +- [ ] `nex-core::bitfield` (`pub type u1 = u8 …`): make private or move behind a + documented `#[doc(hidden)]` implementation-detail boundary — these aliases + should not be part of the stable contract. +- [ ] Decide `nex-sys`'s status: it is a low-level internal crate — either mark it + clearly "internal, no semver guarantees" in its docs or make its surface + `pub(crate)`-equivalent via `#[doc(hidden)]`. +- [ ] Normalize accessor naming: replace `get_*` (73 occurrences in `nex-packet`) + with idiomatic Rust names; keep `#[deprecated]` aliases for one release. +- [ ] Audit public struct fields (e.g. `datalink::Config`, packet headers): decide + field-access vs accessor policy; document undocumented `pub` fields + (`Config.linux_fanout`, `Config.promiscuous` lack doc comments). +- [ ] Apply `#[non_exhaustive]` to all enums/structs expecting future variants + (protocol number enums, `Channel`, error types, config structs). +- [ ] Audit `pub const` protocol constants for naming + semver stability. +- [ ] Document `new_unchecked` invariants (`GenericMutablePacket::new_unchecked`, + any `*_unchecked`) with `# Safety` sections. +- [ ] Generate a machine-readable public API snapshot per crate (e.g. + `cargo public-api`) and diff it in CI to catch accidental breaks. + +Acceptance: a documented, intentional surface; `cargo public-api` diff is clean and +tracked; no accidental `pub` internals. + +### P0.4 A real error model (kill `String` errors) + +- [ ] `nex-core::interface`: replace `Result<_, String>` with a typed error + (`nex-core/src/interface.rs:359,592,597` — `default`, `get_default_interface`, + `get_default_gateway`). +- [ ] Add `nex-packet::BuildError` for builders (P0.2). +- [ ] Keep `io::Error` only at raw syscall/socket boundaries; convert to typed + errors where the library adds semantic meaning (datalink/socket config). +- [ ] Ensure `ParseError` carries enough context for fuzz triage and diagnostics + (it already has `context`; verify every construction site sets a useful one). +- [ ] All public error types implement `std::error::Error + Send + Sync + 'static`. +- [ ] No `panic!`/`unreachable!`/`unwrap` reachable from public APIs on malformed + input. (Current `unreachable!`/`panic!` sites are test-only — keep it that way + and add a CI grep guard.) + +Acceptance: no `-> Result<_, String>` in any public API; a documented error taxonomy. + +### P0.5 Feature flags & dependency diet + +- [ ] `nex-socket`: gate async behind an `async` (tokio) feature. Sync users must + not pull `tokio` (`nex-socket/Cargo.toml` currently hard-depends on tokio + with `time,sync,net,rt`). Split sync/async cleanly. +- [ ] `nex-packet`: `rand` is a full dependency used in exactly one place + (`builder/ipv4.rs:33`, random IP identification). Either feature-gate it, + replace with a lightweight PRNG, or let the caller supply the id. Do not force + `rand` on every packet-parsing consumer. +- [ ] `nex-datalink`: consider gating `async_io` + `futures-core` behind an `async` + feature; sniffers that only send/recv synchronously shouldn't compile it. +- [ ] Define default features intentionally and document each (`nex-core` defaults + to `gateway` — confirm that's the right default). +- [ ] Wire facade features through: `nex` exposes `pcap`, `serde`; add `async` and + any new sub-crate features so the facade stays a faithful superset. +- [ ] Verify all feature combos build: default, `--no-default-features`, `serde`, + `pcap`, `async`, `--all-features` (enforce in CI, P0.6). + +Acceptance: `cargo tree` for a sync-only `nex-socket` user contains no `tokio`; +`nex-packet` with default features contains no `rand` unless opted in. + +### P0.6 Quality gates in CI (the current CI proves almost nothing) + +Current `.github/workflows/rust.yml` runs only `cargo build` on Linux/macOS/Windows. + +- [ ] Replace with a matrix that runs, per OS: + - [ ] `cargo test --workspace --lib` + - [ ] `cargo test --workspace --doc` + - [ ] `cargo build --workspace --all-targets` (examples included) +- [ ] Lint job: `cargo fmt --all -- --check` + + `cargo clippy --workspace --all-targets --all-features -- -D warnings`. +- [ ] Feature-combo job: default / `--no-default-features` / `serde` / `pcap` / + `async` / `--all-features`. +- [ ] MSRV job: pin and verify an MSRV (edition 2024 ⇒ MSRV ≥ 1.85; declare + `rust-version` in every `Cargo.toml` and test it). +- [ ] Supply chain: `cargo deny check` (licenses, advisories, bans, sources) + + add `deny.toml`. +- [ ] Public API snapshot diff job (P0.3). +- [ ] Remove `#![deny(warnings)]` from `nex-datalink/src/lib.rs` (line 3): it makes + builds break on future compilers/new lints. Enforce warnings in CI via + `RUSTFLAGS=-Dwarnings`, not in source. +- [ ] A documented, manual privileged-test matrix for raw sockets / datalink I/O + that CI cannot run (see P1.2 / P1.3). + +Acceptance: a red/green CI that actually gates merges on tests, lints, features, +MSRV, and supply chain across all three OSes. + +### P0.7 Unsafe & OS-resource safety hardening + +- [ ] Add a `# Safety` comment to every `unsafe` block and every `unsafe impl` in + `nex-datalink` (~141 sites) and `nex-sys`, justifying the invariant. +- [ ] Justify or remove the 9 `unsafe impl Send/Sync` in `nex-datalink` + (`wpcap.rs`, `async_io/wpcap.rs`): document what makes the raw Npcap handles + actually thread-safe, or wrap them so the impl is sound. +- [ ] Ensure every OS handle (fd, BPF device, Npcap adapter, packet buffer) is + owned by an RAII type that closes on *all* error paths. + - `nex-sys::FileDesc` already drops the fd — audit that every fd flows through it + and that no early-return leaks a half-opened resource. +- [ ] Prefer typed wrappers over raw integer/pointer handles at module boundaries. +- [ ] Add error-path tests that open then fail to confirm no leak/double-close. +- [ ] Run Miri on pure `nex-packet`/`nex-core` parsing/building where feasible. +- [ ] Run ASan/UBSan on Linux packet parse + datalink where feasible. + +Acceptance: `cargo miri test` passes for pure logic crates; every unsafe site has a +rationale; a reviewer can audit the FFI boundary from comments alone. + +--- + +## P1 — Correctness, Performance, Robustness + +### P1.1 Packet layer architecture + +- [ ] Cleanly separate the four packet categories and document which is which: + read-only borrowed views, mutable borrowed views, owned decoded packets, builders. +- [ ] Fix `GenericMutablePacket` re-parsing: `header()`, `header_mut()`, `payload()`, + `payload_mut()` each call `lengths()` which re-runs `P::from_buf` on every + access (`nex-packet/src/packet.rs:124-165`). Cache lengths on construction or + on first use. +- [ ] Define clear freeze/commit semantics for mutable views (`freeze()` currently + re-parses via `from_buf`); document cost and invalidation rules. +- [ ] Consolidate/trim the `Packet` trait: `to_bytes_mut`/`header_mut`/`payload_mut` + allocate fresh `BytesMut` each call — confirm these belong on the trait or move + to explicit conversion helpers. +- [ ] Decide whether generated bitfield accessors are public API or hidden detail + (ties to P0.3 `bitfield`). +- [ ] Extension-header / options parsing: audit IPv4 options, IPv6 ext headers, TCP + options, DNS compression, DHCP options for strict-length correctness and + truncation handling. + +### P1.2 Datalink backends + +- [ ] Document per-platform behavior of the stable `channel()` / async channel API: + blocking vs timeout vs nonblocking vs async semantics. +- [ ] Linux packet socket: verify Layer2/Layer3 modes, promiscuous, fanout, buffer + sizing, timeout behavior. +- [ ] BPF (macOS/BSD): device selection, header-complete mode, buffer sizing, + poll/read iteration, `bpf_fd_attempts` behavior. +- [ ] Windows/Npcap: adapter name conversion, packet alloc/cleanup, send/recv thread + safety (ties to the `unsafe impl` audit in P0.7). +- [ ] Confirm backend submodules stay private (already done per `API_SURFACE`); + keep only the generic API public. +- [ ] `RawSender::send`/`build_and_send` return `Option>` — the + `Option` (capacity) vs `Result` (I/O) split is subtle; document it precisely + or model it as one typed error. +- [ ] Add manually-enabled integration tests per OS (loopback / veth where possible). + +### P1.3 Socket layer + +- [ ] Symmetry: ensure TCP/UDP/ICMP each expose the same shape sync and async. +- [ ] Constructor policy: infallible config + fallible builder is the current shape + (`TcpConfig` etc.) — apply it consistently to UDP/ICMP and document it. +- [ ] Normalize bind/connect/timeout behavior across platforms. +- [ ] Tests for socket options: TTL/hop limit, broadcast, multicast, device binding, + IPv4/IPv6 family mismatch (config `validate()` covers some — extend to runtime), + nonblocking-state preservation across operations. +- [ ] Document privilege requirements for raw ICMP / raw TCP per platform. +- [ ] Confirm sync impls never transitively require the async runtime (P0.5). + +### P1.4 Performance + +- [ ] Establish Criterion baselines beyond the single `packet_parse` bench: + Ethernet/VLAN parse, IPv4/IPv6 parse, TCP/UDP parse, DNS name decompression, + serialization, checksum, and datalink send/recv loops where measurable. +- [ ] Measure allocations in parsers/builders; ensure borrowed views don't clone + `Bytes` or allocate where a slice suffices. +- [ ] Review checksum impl (`nex-packet/src/checksum.rs` + call sites) for alignment + and portable vectorization; verify the folding/carry handling on odd lengths. +- [ ] Fix the `GenericMutablePacket` re-parse hot path (also in P1.1) — it's a real + throughput cost for in-place mutation workloads. +- [ ] Add a documented benchmark workflow and/or CI regression tracking. + +### P1.5 Fuzzing & robustness + +- [ ] Keep the 5 existing targets; add: Ethernet/VLAN, IPv4 options, IPv6 ext + headers, ICMPv6/NDP options, DNS records + compressed names, DHCP options, + GRE optional fields, VXLAN. +- [ ] Add seed corpora from real captures and protocol edge cases. +- [ ] Wire fuzz findings back as regression unit tests. +- [ ] Add property tests (proptest) for parse↔serialize round-trips per family. +- [ ] Document `cargo fuzz` usage in `CONTRIBUTING.md`. +- [ ] Guarantee (and test) panic-free parsing on arbitrary bytes for every module. + +--- + +## P2 — Documentation, Ergonomics, Release + +### P2.1 Documentation + +- [ ] Crate-level docs for every published crate; module docs for every stable module. +- [ ] Rich, compile-tested doc examples for: parse an Ethernet frame; build IPv4/UDP + and IPv6/UDP; compute checksums; datalink send/recv; async datalink recv; + TCP/UDP/ICMP sockets (sync + async). +- [ ] Document platform support matrix and privilege requirements in one place. +- [ ] Document the safety model, error taxonomy, feature flags, and performance + expectations. +- [ ] `docs.rs` metadata: `all-features` (or curated) docs build per crate; verify + feature-gated items render. +- [ ] Rewrite `README.md`: it omits `nex-core`/`nex-socket` from the crate list, + predates the feature set, and states version `0.26`. Make it accurate for 1.0 + without overpromising unsupported protocols/platforms. + +### P2.2 Migration & compatibility + +- [ ] Write `MIGRATION.md` / migration notes `0.26.x → 1.0.0` covering: parse API + unification, fallible builders, `get_*` renames, error-type changes, feature + changes (`tokio`/`rand`). +- [ ] Add a v1.0 semver/compatibility policy (what's covered, MSRV policy, platform + tiers, `#[non_exhaustive]` implications). + +### P2.3 Repository & release readiness + +- [ ] Add `CHANGELOG.md` with an Unreleased section (keep-a-changelog style). +- [ ] Verify workspace version/dependency pinning is release-consistent. + +--- + +## Suggested Execution Order + +1. **P0.6 CI + P0.7 unsafe comments** — get a truthful baseline and stop regressions. +2. **P0.5 features/deps** — cheap, high-impact; unblocks clean downstream trees. +3. **P0.1 parse unification + P0.4 error model** — the biggest, most breaking surface + change; do it once, early, behind deprecations. +4. **P0.2 fallible builders** — depends on the error model. +5. **P0.3 API freeze + public-api snapshot** — lock the surface after 1–4 settle. +6. **P1.1–P1.3 correctness** (packet arch, datalink, socket) with the manual test + matrix. +7. **P1.4 perf + P1.5 fuzz/property tests** — prove robustness and speed. +8. **P2 docs, migration, release scaffolding** — last, once the surface is final. + +## Definition of Done for v1.0.0 + +- [ ] One consistent parse API and one error taxonomy across all crates. +- [ ] Builders validate and cannot silently emit invalid packets. +- [ ] Sync users pull no async runtime; parsers pull no needless deps. +- [ ] Green CI: tests + doc-tests + clippy + fmt + feature matrix + MSRV + + `cargo deny` + public-api diff on Linux/macOS/Windows. +- [ ] Every `unsafe` justified; Miri-clean pure crates; no reachable panics on + malformed input; fuzz + property tests in place. +- [ ] Complete docs, accurate README, migration guide, CHANGELOG. From ad271d5a725a5d78fbcb741559208e3813bd2831 Mon Sep 17 00:00:00 2001 From: shellrow Date: Sat, 18 Jul 2026 09:16:58 +0900 Subject: [PATCH 05/33] feat: make asynchronous networking APIs opt-in --- nex-datalink/Cargo.toml | 4 +++- nex-datalink/src/lib.rs | 3 +-- nex-socket/Cargo.toml | 6 +++++- nex-socket/src/icmp/mod.rs | 2 ++ nex-socket/src/tcp/mod.rs | 2 ++ nex-socket/src/udp/mod.rs | 2 ++ nex/Cargo.toml | 7 +++++++ 7 files changed, 22 insertions(+), 4 deletions(-) diff --git a/nex-datalink/Cargo.toml b/nex-datalink/Cargo.toml index cb61f1b..9567dd6 100644 --- a/nex-datalink/Cargo.toml +++ b/nex-datalink/Cargo.toml @@ -17,7 +17,7 @@ serde = { workspace = true, features = ["derive"], optional = true } pcap = { version = "2.0", optional = true } nex-core = { workspace = true } nex-sys = { workspace = true } -futures-core = "0.3" +futures-core = { version = "0.3", optional = true } [target.'cfg(windows)'.dependencies.windows-sys] version = "0.61" @@ -30,6 +30,8 @@ features = [ ] [features] +default = [] +async = ["dep:futures-core"] serde = ["dep:serde", "nex-core/serde"] pcap = ["dep:pcap"] diff --git a/nex-datalink/src/lib.rs b/nex-datalink/src/lib.rs index 4297cfc..374ca2b 100644 --- a/nex-datalink/src/lib.rs +++ b/nex-datalink/src/lib.rs @@ -1,13 +1,12 @@ //! Cross-platform datalink I/O primitives for sending and receiving raw packets. -#![deny(warnings)] - use std::io; use std::option::Option; use std::time::Duration; mod bindings; +#[cfg(feature = "async")] pub mod async_io; #[cfg(windows)] diff --git a/nex-socket/Cargo.toml b/nex-socket/Cargo.toml index e4e37d2..34c7229 100644 --- a/nex-socket/Cargo.toml +++ b/nex-socket/Cargo.toml @@ -14,7 +14,7 @@ license = "MIT" nex-core = { workspace = true } nex-packet = { workspace = true } socket2 = { version = "0.5", features = ["all"] } -tokio = { version = "1", features = ["time", "sync", "net", "rt"] } +tokio = { version = "1", features = ["time", "sync", "net", "rt"], optional = true } libc = { workspace = true } [target.'cfg(unix)'.dependencies] @@ -29,3 +29,7 @@ features = [ "Win32_System_Threading", "Win32_System_WindowsProgramming", ] + +[features] +default = [] +async = ["dep:tokio"] diff --git a/nex-socket/src/icmp/mod.rs b/nex-socket/src/icmp/mod.rs index 669d04a..44a0875 100644 --- a/nex-socket/src/icmp/mod.rs +++ b/nex-socket/src/icmp/mod.rs @@ -2,10 +2,12 @@ //! //! Supplies synchronous and asynchronous interfaces for sending and //! receiving Internet Control Message Protocol packets. +#[cfg(feature = "async")] mod async_impl; mod config; mod sync_impl; +#[cfg(feature = "async")] pub use async_impl::*; pub use config::*; pub use sync_impl::*; diff --git a/nex-socket/src/tcp/mod.rs b/nex-socket/src/tcp/mod.rs index 9fdaa95..f0d0a9e 100644 --- a/nex-socket/src/tcp/mod.rs +++ b/nex-socket/src/tcp/mod.rs @@ -2,10 +2,12 @@ //! //! Includes synchronous and asynchronous functionality and configuration //! helpers for TCP sockets. +#[cfg(feature = "async")] mod async_impl; mod config; mod sync_impl; +#[cfg(feature = "async")] pub use async_impl::*; pub use config::*; pub use sync_impl::*; diff --git a/nex-socket/src/udp/mod.rs b/nex-socket/src/udp/mod.rs index 81c9a56..709c834 100644 --- a/nex-socket/src/udp/mod.rs +++ b/nex-socket/src/udp/mod.rs @@ -2,6 +2,7 @@ //! //! Provides synchronous and asynchronous UDP APIs along with //! configuration utilities for common socket options. +#[cfg(feature = "async")] mod async_impl; mod config; mod sync_impl; @@ -117,6 +118,7 @@ pub(crate) fn recv_pktinfo_v6(_socket: &Socket) -> io::Result { )) } +#[cfg(feature = "async")] pub use async_impl::*; pub use config::*; pub use sync_impl::*; diff --git a/nex/Cargo.toml b/nex/Cargo.toml index 10ab2c8..ebb20eb 100644 --- a/nex/Cargo.toml +++ b/nex/Cargo.toml @@ -24,6 +24,8 @@ futures = "0.3" tokio = { version = "1", features = ["rt", "rt-multi-thread", "signal", "macros"] } [features] +default = [] +async = ["nex-datalink/async", "nex-socket/async"] pcap = ["nex-datalink/pcap"] serde = ["nex-core/serde", "nex-packet/serde", "nex-datalink/serde"] @@ -74,22 +76,27 @@ path = "../examples/udp_socket.rs" [[example]] name = "async_icmp_socket" path = "../examples/async_icmp_socket.rs" +required-features = ["async"] [[example]] name = "async_tcp_socket" path = "../examples/async_tcp_socket.rs" +required-features = ["async"] [[example]] name = "async_udp_socket" path = "../examples/async_udp_socket.rs" +required-features = ["async"] [[example]] name = "async_datalink" path = "../examples/async_datalink.rs" +required-features = ["async"] [[example]] name = "async_dump" path = "../examples/async_dump.rs" +required-features = ["async"] [[example]] name = "mutable_chaining" From 52a7b1899f95406e2c1ccf2232b4025f561d2934 Mon Sep 17 00:00:00 2001 From: shellrow Date: Sat, 18 Jul 2026 09:17:23 +0900 Subject: [PATCH 06/33] feat: make IPv4 identification caller-controlled --- nex-packet/Cargo.toml | 2 +- nex-packet/src/builder/ipv4.rs | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/nex-packet/Cargo.toml b/nex-packet/Cargo.toml index b2bef75..502731f 100644 --- a/nex-packet/Cargo.toml +++ b/nex-packet/Cargo.toml @@ -14,9 +14,9 @@ license = "MIT" bytes = { workspace = true } nex-core = { workspace = true } serde = { workspace = true, features = ["derive"], optional = true } -rand = { workspace = true } [features] +default = [] serde = ["dep:serde", "nex-core/serde", "bytes/serde"] [dev-dependencies] diff --git a/nex-packet/src/builder/ipv4.rs b/nex-packet/src/builder/ipv4.rs index af1e39a..eaee8e5 100644 --- a/nex-packet/src/builder/ipv4.rs +++ b/nex-packet/src/builder/ipv4.rs @@ -30,7 +30,7 @@ impl Ipv4PacketBuilder { dscp: 0, ecn: 0, total_length: 0, // automatically set during build - identification: rand::random::(), + identification: 0, flags: 0, fragment_offset: 0, ttl: 64, @@ -136,4 +136,13 @@ mod tests { ); assert_eq!(pkt.payload, payload); } + + #[test] + fn ipv4_builder_identification_is_caller_controlled() { + let default_packet = Ipv4PacketBuilder::new().build(); + assert_eq!(default_packet.header.identification, 0); + + let packet = Ipv4PacketBuilder::new().identification(0x1234).build(); + assert_eq!(packet.header.identification, 0x1234); + } } From 635ed4d14e3f930e27c735d1f5cf3c07f6a8a315 Mon Sep 17 00:00:00 2001 From: shellrow Date: Sat, 18 Jul 2026 09:18:03 +0900 Subject: [PATCH 07/33] feat: unify packet parsing errors and strictness --- fuzz/fuzz_targets/dns_name.rs | 2 +- fuzz/fuzz_targets/frame_parse.rs | 3 +- fuzz/fuzz_targets/ipv4_parse.rs | 5 ++- fuzz/fuzz_targets/ipv6_parse.rs | 5 ++- nex-core/src/interface.rs | 64 +++++++++++++++++++++++++++++--- nex-packet/src/dns.rs | 26 +++---------- nex-packet/src/ethernet.rs | 39 ++++++++++++------- nex-packet/src/frame.rs | 37 ++++++++++++++---- nex-packet/src/ipv4.rs | 31 ++++++++++++---- nex-packet/src/ipv6.rs | 25 ++++++++++--- nex-packet/src/parse.rs | 41 +++++++++++++++++++- 11 files changed, 209 insertions(+), 69 deletions(-) diff --git a/fuzz/fuzz_targets/dns_name.rs b/fuzz/fuzz_targets/dns_name.rs index e368316..ef1194d 100644 --- a/fuzz/fuzz_targets/dns_name.rs +++ b/fuzz/fuzz_targets/dns_name.rs @@ -4,6 +4,6 @@ use libfuzzer_sys::fuzz_target; use nex_packet::dns::DnsName; fuzz_target!(|data: &[u8]| { - let _ = DnsName::from_bytes(data); + let _ = DnsName::try_from_bytes(data); let _ = DnsName::try_from_bytes(data); }); diff --git a/fuzz/fuzz_targets/frame_parse.rs b/fuzz/fuzz_targets/frame_parse.rs index e304592..0a451c9 100644 --- a/fuzz/fuzz_targets/frame_parse.rs +++ b/fuzz/fuzz_targets/frame_parse.rs @@ -2,10 +2,11 @@ use libfuzzer_sys::fuzz_target; use nex_packet::frame::{Frame, FrameView, ParseOption}; +use nex_packet::parse::ParseMode; fuzz_target!(|data: &[u8]| { let _ = Frame::from_buf(data, ParseOption::default()); let _ = Frame::try_from_buf(data, ParseOption::default()); - let _ = Frame::try_from_buf_strict(data, ParseOption::default()); + let _ = Frame::try_from_buf_with_mode(data, ParseOption::default(), ParseMode::Strict); let _ = FrameView::from_buf(data, ParseOption::default()); }); diff --git a/fuzz/fuzz_targets/ipv4_parse.rs b/fuzz/fuzz_targets/ipv4_parse.rs index 322b74d..85e4158 100644 --- a/fuzz/fuzz_targets/ipv4_parse.rs +++ b/fuzz/fuzz_targets/ipv4_parse.rs @@ -1,11 +1,12 @@ #![no_main] use libfuzzer_sys::fuzz_target; -use nex_packet::packet::Packet; use nex_packet::ipv4::Ipv4Packet; +use nex_packet::packet::Packet; +use nex_packet::parse::ParseMode; fuzz_target!(|data: &[u8]| { let _ = Ipv4Packet::from_buf(data); let _ = Ipv4Packet::try_from_buf(data); - let _ = Ipv4Packet::try_from_buf_strict(data); + let _ = Ipv4Packet::try_from_buf_with_mode(data, ParseMode::Strict); }); diff --git a/fuzz/fuzz_targets/ipv6_parse.rs b/fuzz/fuzz_targets/ipv6_parse.rs index a59f4a0..6631137 100644 --- a/fuzz/fuzz_targets/ipv6_parse.rs +++ b/fuzz/fuzz_targets/ipv6_parse.rs @@ -1,11 +1,12 @@ #![no_main] use libfuzzer_sys::fuzz_target; -use nex_packet::packet::Packet; use nex_packet::ipv6::Ipv6Packet; +use nex_packet::packet::Packet; +use nex_packet::parse::ParseMode; fuzz_target!(|data: &[u8]| { let _ = Ipv6Packet::from_buf(data); let _ = Ipv6Packet::try_from_buf(data); - let _ = Ipv6Packet::try_from_buf_strict(data); + let _ = Ipv6Packet::try_from_buf_with_mode(data, ParseMode::Strict); }); diff --git a/nex-core/src/interface.rs b/nex-core/src/interface.rs index 92dd5c9..ef4f9f8 100644 --- a/nex-core/src/interface.rs +++ b/nex-core/src/interface.rs @@ -2,6 +2,7 @@ use crate::ip::{is_global_ip, is_global_ipv4, is_global_ipv6}; use crate::mac::MacAddr; pub use ipnet::{self, Ipv4Net, Ipv6Net}; use std::convert::TryFrom; +use std::fmt; use std::io; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use std::time::SystemTime; @@ -9,6 +10,31 @@ use std::time::SystemTime; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +/// Errors returned when discovering system network interfaces or gateways. +#[derive(Clone, Eq, PartialEq, Debug)] +#[non_exhaustive] +pub enum InterfaceError { + /// The operating system did not provide a default network interface. + DefaultInterfaceUnavailable { message: String }, + /// The operating system did not provide a default gateway. + DefaultGatewayUnavailable { message: String }, +} + +impl fmt::Display for InterfaceError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::DefaultInterfaceUnavailable { message } => { + write!(f, "default interface is unavailable: {message}") + } + Self::DefaultGatewayUnavailable { message } => { + write!(f, "default gateway is unavailable: {message}") + } + } + } +} + +impl std::error::Error for InterfaceError {} + #[cfg(unix)] pub const IFF_UP: u32 = nex_sys::IFF_UP as u32; #[cfg(windows)] @@ -356,7 +382,7 @@ pub struct Interface { impl Interface { #[cfg(feature = "gateway")] #[allow(clippy::should_implement_trait)] - pub fn default() -> Result { + pub fn default() -> Result { get_default_interface() } @@ -589,13 +615,17 @@ pub fn get_interfaces() -> Vec { } #[cfg(feature = "gateway")] -pub fn get_default_interface() -> Result { - netdev::get_default_interface().map(Into::into) +pub fn get_default_interface() -> Result { + netdev::get_default_interface() + .map(Into::into) + .map_err(|message| InterfaceError::DefaultInterfaceUnavailable { message }) } #[cfg(feature = "gateway")] -pub fn get_default_gateway() -> Result { - netdev::get_default_gateway().map(Into::into) +pub fn get_default_gateway() -> Result { + netdev::get_default_gateway() + .map(Into::into) + .map_err(|message| InterfaceError::DefaultGatewayUnavailable { message }) } fn lookup_interface(name: &str, index: u32) -> Option { @@ -603,3 +633,27 @@ fn lookup_interface(name: &str, index: u32) -> Option { .into_iter() .find(|iface| iface.index == index || iface.name == name) } + +#[cfg(test)] +mod tests { + use super::InterfaceError; + + fn assert_error_contract() {} + + #[test] + fn interface_error_implements_public_error_contract() { + assert_error_contract::(); + } + + #[test] + fn interface_error_includes_operation_context() { + let error = InterfaceError::DefaultGatewayUnavailable { + message: "route table is empty".to_owned(), + }; + + assert_eq!( + error.to_string(), + "default gateway is unavailable: route table is empty" + ); + } +} diff --git a/nex-packet/src/dns.rs b/nex-packet/src/dns.rs index 1e4be40..23aeb0d 100644 --- a/nex-packet/src/dns.rs +++ b/nex-packet/src/dns.rs @@ -954,7 +954,7 @@ impl DnsResponsePacket { /// Returns the DNS name if the record type is CNAME, NS, or PTR. pub fn get_name(&self) -> Option { match self.rtype { - DnsType::CNAME | DnsType::NS | DnsType::PTR => DnsName::from_bytes(&self.data).ok(), + DnsType::CNAME | DnsType::NS | DnsType::PTR => DnsName::try_from_bytes(&self.data).ok(), _ => None, } } @@ -1212,26 +1212,10 @@ impl DnsPacket { pub struct DnsName(String); impl DnsName { - /// Creates a new `DnsName` string from bytes. - pub fn from_bytes(buf: &[u8]) -> Result { - let mut pos = 0; - let mut labels = Vec::new(); - - while pos < buf.len() { - let len = buf[pos] as usize; - if len == 0 { - break; - } - pos += 1; - if pos + len > buf.len() { - break; - } - let label = std::str::from_utf8(&buf[pos..pos + len])?; - labels.push(label); - pos += len; - } - - Ok(DnsName(labels.join("."))) + /// Parse a DNS name from bytes. + #[deprecated(note = "use DnsName::try_from_bytes")] + pub fn from_bytes(buf: &[u8]) -> Result { + Self::try_from_bytes(buf) } /// Returns the DNS name as a string slice. diff --git a/nex-packet/src/ethernet.rs b/nex-packet/src/ethernet.rs index 497e061..15dc371 100644 --- a/nex-packet/src/ethernet.rs +++ b/nex-packet/src/ethernet.rs @@ -161,20 +161,17 @@ pub struct EthernetHeader { } impl EthernetHeader { - /// Construct an Ethernet header from a byte slice. - pub fn from_bytes(packet: Bytes) -> Result { - if packet.len() < ETHERNET_HEADER_LEN { - return Err("Packet is too small for Ethernet header".to_string()); - } - match EthernetPacket::from_bytes(packet) { - Some(ethernet_packet) => Ok(EthernetHeader { - destination: ethernet_packet.get_destination(), - source: ethernet_packet.get_source(), - ethertype: ethernet_packet.get_ethertype(), - }), - None => Err("Failed to parse Ethernet packet".to_string()), - } + /// Parse an Ethernet header from owned bytes. + pub fn try_from_bytes(packet: Bytes) -> Result { + EthernetPacket::try_from_bytes(packet).map(|packet| packet.header) + } + + /// Parse an Ethernet header from owned bytes. + #[deprecated(note = "use EthernetHeader::try_from_bytes")] + pub fn from_bytes(packet: Bytes) -> Result { + Self::try_from_bytes(packet) } + pub fn to_bytes(&self) -> Bytes { let mut buf = Vec::with_capacity(ETHERNET_HEADER_LEN); buf.extend_from_slice(&self.destination.octets()); @@ -459,12 +456,26 @@ mod tests { ethertype: EtherType::Ipv6, }; let bytes = header.to_bytes(); - let parsed = EthernetHeader::from_bytes(bytes.clone()).unwrap(); + let parsed = EthernetHeader::try_from_bytes(bytes.clone()).unwrap(); assert_eq!(header, parsed); assert_eq!(bytes.len(), ETHERNET_HEADER_LEN); } + #[test] + fn test_ethernet_header_parse_too_short_returns_parse_error() { + let error = EthernetHeader::try_from_bytes(Bytes::from_static(&[0; 4])).unwrap_err(); + + assert_eq!( + error, + ParseError::BufferTooShort { + context: "Ethernet packet", + minimum: ETHERNET_HEADER_LEN, + actual: 4, + } + ); + } + #[test] fn test_ethernet_parse_too_short() { let short = Bytes::from_static(&[0, 1, 2, 3]); // insufficient length diff --git a/nex-packet/src/frame.rs b/nex-packet/src/frame.rs index 658352e..1b98ffe 100644 --- a/nex-packet/src/frame.rs +++ b/nex-packet/src/frame.rs @@ -10,7 +10,7 @@ use crate::{ ipv4::{Ipv4Header, Ipv4Packet}, ipv6::{Ipv6Header, Ipv6Packet}, packet::Packet, - parse::ParseError, + parse::{ParseError, ParseMode}, tcp::{TcpHeader, TcpPacket}, udp::{UdpHeader, UdpPacket}, }; @@ -70,27 +70,48 @@ impl Frame { /// Parse a frame and return a structured error on failure. pub fn try_from_buf(packet: &[u8], option: ParseOption) -> Result { - parse_frame_from_bytes(Bytes::copy_from_slice(packet), option, false) + Self::try_from_buf_with_mode(packet, option, ParseMode::Lenient) } /// Parse a frame from owned bytes while preserving payload slices when possible. pub fn try_from_bytes(packet: Bytes, option: ParseOption) -> Result { - parse_frame_from_bytes(packet, option, false) + Self::try_from_bytes_with_mode(packet, option, ParseMode::Lenient) + } + + /// Parse a frame using the requested validation mode. + pub fn try_from_buf_with_mode( + packet: &[u8], + option: ParseOption, + mode: ParseMode, + ) -> Result { + parse_frame_from_bytes(Bytes::copy_from_slice(packet), option, mode.is_strict()) + } + + /// Parse an owned frame using the requested validation mode. + pub fn try_from_bytes_with_mode( + packet: Bytes, + option: ParseOption, + mode: ParseMode, + ) -> Result { + parse_frame_from_bytes(packet, option, mode.is_strict()) } /// Parse a frame using validation-oriented strict IP parsing. + #[deprecated(note = "use Frame::try_from_buf_with_mode with ParseMode::Strict")] pub fn try_from_buf_strict(packet: &[u8], option: ParseOption) -> Result { - parse_frame_from_bytes(Bytes::copy_from_slice(packet), option, true) + Self::try_from_buf_with_mode(packet, option, ParseMode::Strict) } /// Parse a frame from owned bytes using validation-oriented strict IP parsing. + #[deprecated(note = "use Frame::try_from_bytes_with_mode with ParseMode::Strict")] pub fn try_from_bytes_strict(packet: Bytes, option: ParseOption) -> Result { - parse_frame_from_bytes(packet, option, true) + Self::try_from_bytes_with_mode(packet, option, ParseMode::Strict) } /// Parse a frame using validation-oriented strict IP parsing. + #[deprecated(note = "use Frame::try_from_buf_with_mode with ParseMode::Strict")] pub fn from_buf_strict(packet: &[u8], option: ParseOption) -> Option { - Self::try_from_buf_strict(packet, option).ok() + Self::try_from_buf_with_mode(packet, option, ParseMode::Strict).ok() } } @@ -187,7 +208,7 @@ fn parse_arp_packet(packet: Bytes, frame: &mut Frame) { fn parse_ipv4_packet(packet: Bytes, frame: &mut Frame, strict: bool) -> Result<(), ParseError> { let parsed = if strict { - Ipv4Packet::try_from_bytes_strict(packet) + Ipv4Packet::try_from_bytes_with_mode(packet, ParseMode::Strict) } else { Ipv4Packet::try_from_bytes(packet) }; @@ -232,7 +253,7 @@ fn parse_ipv4_packet(packet: Bytes, frame: &mut Frame, strict: bool) -> Result<( fn parse_ipv6_packet(packet: Bytes, frame: &mut Frame, strict: bool) -> Result<(), ParseError> { let parsed = if strict { - Ipv6Packet::try_from_bytes_strict(packet) + Ipv6Packet::try_from_bytes_with_mode(packet, ParseMode::Strict) } else { Ipv6Packet::try_from_bytes(packet) }; diff --git a/nex-packet/src/ipv4.rs b/nex-packet/src/ipv4.rs index d88bc66..518f4a7 100644 --- a/nex-packet/src/ipv4.rs +++ b/nex-packet/src/ipv4.rs @@ -4,7 +4,7 @@ use crate::{ checksum::{ChecksumMode, ChecksumState}, ip::IpNextProtocol, packet::{MutablePacket, Packet}, - parse::ParseError, + parse::{ParseError, ParseMode}, util, }; use bytes::{BufMut, Bytes, BytesMut}; @@ -306,32 +306,46 @@ impl Packet for Ipv4Packet { impl Ipv4Packet { /// Parse an IPv4 packet and return a structured error on failure. pub fn try_from_buf(bytes: &[u8]) -> Result { - parse_ipv4_from_slice(bytes, false) + Self::try_from_buf_with_mode(bytes, ParseMode::Lenient) } /// Parse an IPv4 packet from owned bytes while preserving payload slices when possible. pub fn try_from_bytes(bytes: Bytes) -> Result { - parse_ipv4_from_bytes(bytes, false) + Self::try_from_bytes_with_mode(bytes, ParseMode::Lenient) + } + + /// Parse an IPv4 packet using the requested validation mode. + pub fn try_from_buf_with_mode(bytes: &[u8], mode: ParseMode) -> Result { + parse_ipv4_from_slice(bytes, mode.is_strict()) + } + + /// Parse an owned IPv4 packet using the requested validation mode. + pub fn try_from_bytes_with_mode(bytes: Bytes, mode: ParseMode) -> Result { + parse_ipv4_from_bytes(bytes, mode.is_strict()) } /// Parse an IPv4 packet using validation-oriented strict checks. + #[deprecated(note = "use Ipv4Packet::try_from_buf_with_mode with ParseMode::Strict")] pub fn try_from_buf_strict(bytes: &[u8]) -> Result { - parse_ipv4_from_slice(bytes, true) + Self::try_from_buf_with_mode(bytes, ParseMode::Strict) } /// Parse an IPv4 packet from owned bytes using validation-oriented strict checks. + #[deprecated(note = "use Ipv4Packet::try_from_bytes_with_mode with ParseMode::Strict")] pub fn try_from_bytes_strict(bytes: Bytes) -> Result { - parse_ipv4_from_bytes(bytes, true) + Self::try_from_bytes_with_mode(bytes, ParseMode::Strict) } /// Parse an IPv4 packet using validation-oriented strict checks. + #[deprecated(note = "use Ipv4Packet::try_from_buf_with_mode with ParseMode::Strict")] pub fn from_buf_strict(bytes: &[u8]) -> Option { - Self::try_from_buf_strict(bytes).ok() + Self::try_from_buf_with_mode(bytes, ParseMode::Strict).ok() } /// Parse an IPv4 packet from owned bytes using validation-oriented strict checks. + #[deprecated(note = "use Ipv4Packet::try_from_bytes_with_mode with ParseMode::Strict")] pub fn from_bytes_strict(bytes: Bytes) -> Option { - Self::try_from_bytes_strict(bytes).ok() + Self::try_from_bytes_with_mode(bytes, ParseMode::Strict).ok() } pub fn with_computed_checksum(mut self) -> Self { @@ -1089,7 +1103,8 @@ mod tests { 1, 1, 2, 3, 4, ]; - let err = Ipv4Packet::try_from_buf_strict(&raw).expect_err("strict parse should fail"); + let err = Ipv4Packet::try_from_buf_with_mode(&raw, ParseMode::Strict) + .expect_err("strict parse should fail"); assert!(matches!(err, ParseError::Truncated { .. })); assert!(Ipv4Packet::from_buf(&raw).is_some()); } diff --git a/nex-packet/src/ipv6.rs b/nex-packet/src/ipv6.rs index 4761c9d..0479488 100644 --- a/nex-packet/src/ipv6.rs +++ b/nex-packet/src/ipv6.rs @@ -1,6 +1,6 @@ use crate::ip::IpNextProtocol; use crate::packet::{MutablePacket, Packet}; -use crate::parse::ParseError; +use crate::parse::{ParseError, ParseMode}; use bytes::{BufMut, Bytes, BytesMut}; use std::net::Ipv6Addr; @@ -151,22 +151,34 @@ impl Packet for Ipv6Packet { impl Ipv6Packet { /// Parse an IPv6 packet and return a structured error on failure. pub fn try_from_buf(bytes: &[u8]) -> Result { - parse_ipv6_from_slice(bytes, false) + Self::try_from_buf_with_mode(bytes, ParseMode::Lenient) } /// Parse an IPv6 packet from owned bytes while preserving payload slices when possible. pub fn try_from_bytes(bytes: Bytes) -> Result { - parse_ipv6_from_bytes(bytes, false) + Self::try_from_bytes_with_mode(bytes, ParseMode::Lenient) + } + + /// Parse an IPv6 packet using the requested validation mode. + pub fn try_from_buf_with_mode(bytes: &[u8], mode: ParseMode) -> Result { + parse_ipv6_from_slice(bytes, mode.is_strict()) + } + + /// Parse an owned IPv6 packet using the requested validation mode. + pub fn try_from_bytes_with_mode(bytes: Bytes, mode: ParseMode) -> Result { + parse_ipv6_from_bytes(bytes, mode.is_strict()) } /// Parse an IPv6 packet using validation-oriented strict checks. + #[deprecated(note = "use Ipv6Packet::try_from_buf_with_mode with ParseMode::Strict")] pub fn try_from_buf_strict(bytes: &[u8]) -> Result { - parse_ipv6_from_slice(bytes, true) + Self::try_from_buf_with_mode(bytes, ParseMode::Strict) } /// Parse an IPv6 packet from owned bytes using validation-oriented strict checks. + #[deprecated(note = "use Ipv6Packet::try_from_bytes_with_mode with ParseMode::Strict")] pub fn try_from_bytes_strict(bytes: Bytes) -> Result { - parse_ipv6_from_bytes(bytes, true) + Self::try_from_bytes_with_mode(bytes, ParseMode::Strict) } pub fn total_len(&self) -> usize { @@ -829,7 +841,8 @@ mod tests { 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 3, 4, ]); - let err = Ipv6Packet::try_from_buf_strict(&raw).expect_err("strict parse should fail"); + let err = Ipv6Packet::try_from_buf_with_mode(&raw, ParseMode::Strict) + .expect_err("strict parse should fail"); assert!(matches!(err, ParseError::Truncated { .. })); assert!(Ipv6Packet::from_buf(&raw).is_some()); } diff --git a/nex-packet/src/parse.rs b/nex-packet/src/parse.rs index 3697b9f..b16951d 100644 --- a/nex-packet/src/parse.rs +++ b/nex-packet/src/parse.rs @@ -1,7 +1,46 @@ -//! Structured parse errors for diagnosable packet parsing APIs. +//! Shared contracts for diagnosable packet parsing APIs. +//! +//! Packet types expose `try_from_buf(&[u8])` for borrowed input and +//! `try_from_bytes(bytes::Bytes)` for owned input. Both return [`ParseError`]. +//! Parsers that support alternate validation behavior accept [`ParseMode`] +//! through a `*_with_mode` method instead of adding more method-name suffixes. +//! +//! # Parsing API migration +//! +//! | Previous API | v1 API | +//! | --- | --- | +//! | `Packet::from_buf(input)` | `Type::try_from_buf(input)` | +//! | `Packet::from_bytes(input)` | `Type::try_from_bytes(input)` | +//! | `try_from_buf_strict(input)` | `try_from_buf_with_mode(input, ParseMode::Strict)` | +//! | `try_from_bytes_strict(input)` | `try_from_bytes_with_mode(input, ParseMode::Strict)` | +//! | `from_buf_strict(input)` | `try_from_buf_with_mode(input, ParseMode::Strict).ok()` | +//! | `from_bytes_strict(input)` | `try_from_bytes_with_mode(input, ParseMode::Strict).ok()` | +//! | `EthernetHeader::from_bytes(input)` | `EthernetHeader::try_from_bytes(input)` | +//! | `DnsName::from_bytes(input)` | `DnsName::try_from_bytes(input)` | +//! | DNS section `from_buf_mut(cursor)` helpers | `DnsPacket::try_from_buf(input)` | +//! +//! The `Option`-returning methods on [`crate::packet::Packet`] remain temporary +//! compatibility shims. New code should use the inherent `try_from_*` methods so +//! malformed input retains diagnostic context. use core::fmt; +/// Controls validation behavior for parsers with length-delimited payloads. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum ParseMode { + /// Preserve captured payload bytes when a declared length is missing or incomplete. + #[default] + Lenient, + /// Reject input whose captured length is shorter than its declared length. + Strict, +} + +impl ParseMode { + pub(crate) const fn is_strict(self) -> bool { + matches!(self, Self::Strict) + } +} + /// Structured error returned by `try_from_*` parsing APIs. #[derive(Clone, Debug, PartialEq, Eq)] #[non_exhaustive] From 246f2b5e559bcca0f9769369af699abf0d8131ce Mon Sep 17 00:00:00 2001 From: shellrow Date: Sat, 18 Jul 2026 09:18:34 +0900 Subject: [PATCH 08/33] ci: add workspace quality and feature gates --- .github/workflows/rust.yml | 64 +++++++++++++++++++++++++++++++++----- TODO.md | 40 ++++++++++++------------ 2 files changed, 76 insertions(+), 28 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 5ee3afc..69aa654 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -2,23 +2,71 @@ name: Rust on: push: - branches: [ "main" ] + branches: ["main"] pull_request: - branches: [ "main" ] + branches: ["main"] env: CARGO_TERM_COLOR: always jobs: - build: - name: Check + test: + name: Test (${{ matrix.os }}) runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: - os: [ubuntu-latest, macOS-latest, windows-latest] + os: [ubuntu-latest, macos-latest, windows-latest] steps: - - uses: actions/checkout@v3 - - name: Build - run: cargo build + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: Test libraries + run: cargo test --workspace --lib + - name: Test documentation + run: cargo test --workspace --doc + - name: Build all targets + run: cargo build --workspace --all-targets + + lint: + name: Lint + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy, rustfmt + - uses: Swatinem/rust-cache@v2 + - name: Check formatting + run: cargo fmt --all -- --check + - name: Run Clippy + run: cargo clippy --workspace --all-targets --all-features -- -D warnings + + features: + name: Features (${{ matrix.name }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - name: default + command: cargo check -p nex + - name: no-default-features + command: cargo check -p nex --no-default-features + - name: serde + command: cargo check -p nex --no-default-features --features serde + - name: pcap + command: cargo check -p nex --no-default-features --features pcap + - name: async + command: cargo check -p nex --no-default-features --features async + - name: all-features + command: cargo check -p nex --all-features + + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: Check feature combination + run: ${{ matrix.command }} diff --git a/TODO.md b/TODO.md index 9c2212c..e4389ab 100644 --- a/TODO.md +++ b/TODO.md @@ -41,17 +41,17 @@ Actions: - [ ] Define ONE canonical parsing contract and document it in `parse.rs`: - [ ] `try_from_bytes(Bytes) -> Result` — owned, zero-copy view. - [ ] `try_from_buf(&[u8]) -> Result` — borrowed. - - [ ] A single explicit "strict payload-length" opt-in (e.g. a `Strictness`/ + - [x] A single explicit "strict payload-length" opt-in (e.g. a `Strictness`/ `ParseOption` arg) instead of `*_strict` name-doubling every method. -- [ ] Remove `EthernetHeader::from_bytes -> Result<_, String>`; replace with the +- [x] Remove `EthernetHeader::from_bytes -> Result<_, String>`; replace with the canonical `ParseError` form (`nex-packet/src/ethernet.rs:165`). -- [ ] Fold `DnsName::from_bytes -> Result<_, Utf8Error>` into `ParseError::InvalidUtf8` +- [x] Fold `DnsName::from_bytes -> Result<_, Utf8Error>` into `ParseError::InvalidUtf8` so DNS matches every other module (`nex-packet/src/dns.rs:1216`). -- [ ] Decide the fate of `from_* -> Option` on the `Packet` trait +- [x] Decide the fate of `from_* -> Option` on the `Packet` trait (`nex-packet/src/packet.rs:9-12`): either make the trait `try_from_*`-based with `ParseError`, or keep `Option` only as a thin infallible-intent shim. -- [ ] Provide `#[deprecated]` aliases for one release wherever a public name changes. -- [ ] Write a doc table mapping every old parsing fn → its v1.0 replacement. +- [x] Provide `#[deprecated]` aliases for one release wherever a public name changes. +- [x] Write a doc table mapping every old parsing fn → its v1.0 replacement. Acceptance: every protocol module exposes the *same* small set of parse fns with the *same* signatures and error type; `grep -r "Result<.*String>"` returns nothing @@ -102,7 +102,7 @@ tracked; no accidental `pub` internals. ### P0.4 A real error model (kill `String` errors) -- [ ] `nex-core::interface`: replace `Result<_, String>` with a typed error +- [x] `nex-core::interface`: replace `Result<_, String>` with a typed error (`nex-core/src/interface.rs:359,592,597` — `default`, `get_default_interface`, `get_default_gateway`). - [ ] Add `nex-packet::BuildError` for builders (P0.2). @@ -119,20 +119,20 @@ Acceptance: no `-> Result<_, String>` in any public API; a documented error taxo ### P0.5 Feature flags & dependency diet -- [ ] `nex-socket`: gate async behind an `async` (tokio) feature. Sync users must +- [x] `nex-socket`: gate async behind an `async` (tokio) feature. Sync users must not pull `tokio` (`nex-socket/Cargo.toml` currently hard-depends on tokio with `time,sync,net,rt`). Split sync/async cleanly. -- [ ] `nex-packet`: `rand` is a full dependency used in exactly one place +- [x] `nex-packet`: `rand` is a full dependency used in exactly one place (`builder/ipv4.rs:33`, random IP identification). Either feature-gate it, replace with a lightweight PRNG, or let the caller supply the id. Do not force `rand` on every packet-parsing consumer. -- [ ] `nex-datalink`: consider gating `async_io` + `futures-core` behind an `async` +- [x] `nex-datalink`: consider gating `async_io` + `futures-core` behind an `async` feature; sniffers that only send/recv synchronously shouldn't compile it. -- [ ] Define default features intentionally and document each (`nex-core` defaults +- [x] Define default features intentionally and document each (`nex-core` defaults to `gateway` — confirm that's the right default). -- [ ] Wire facade features through: `nex` exposes `pcap`, `serde`; add `async` and +- [x] Wire facade features through: `nex` exposes `pcap`, `serde`; add `async` and any new sub-crate features so the facade stays a faithful superset. -- [ ] Verify all feature combos build: default, `--no-default-features`, `serde`, +- [x] Verify all feature combos build: default, `--no-default-features`, `serde`, `pcap`, `async`, `--all-features` (enforce in CI, P0.6). Acceptance: `cargo tree` for a sync-only `nex-socket` user contains no `tokio`; @@ -142,20 +142,20 @@ Acceptance: `cargo tree` for a sync-only `nex-socket` user contains no `tokio`; Current `.github/workflows/rust.yml` runs only `cargo build` on Linux/macOS/Windows. -- [ ] Replace with a matrix that runs, per OS: - - [ ] `cargo test --workspace --lib` - - [ ] `cargo test --workspace --doc` - - [ ] `cargo build --workspace --all-targets` (examples included) -- [ ] Lint job: `cargo fmt --all -- --check` + +- [x] Replace with a matrix that runs, per OS: + - [x] `cargo test --workspace --lib` + - [x] `cargo test --workspace --doc` + - [x] `cargo build --workspace --all-targets` (examples included) +- [x] Lint job: `cargo fmt --all -- --check` + `cargo clippy --workspace --all-targets --all-features -- -D warnings`. -- [ ] Feature-combo job: default / `--no-default-features` / `serde` / `pcap` / +- [x] Feature-combo job: default / `--no-default-features` / `serde` / `pcap` / `async` / `--all-features`. - [ ] MSRV job: pin and verify an MSRV (edition 2024 ⇒ MSRV ≥ 1.85; declare `rust-version` in every `Cargo.toml` and test it). - [ ] Supply chain: `cargo deny check` (licenses, advisories, bans, sources) + add `deny.toml`. - [ ] Public API snapshot diff job (P0.3). -- [ ] Remove `#![deny(warnings)]` from `nex-datalink/src/lib.rs` (line 3): it makes +- [x] Remove `#![deny(warnings)]` from `nex-datalink/src/lib.rs` (line 3): it makes builds break on future compilers/new lints. Enforce warnings in CI via `RUSTFLAGS=-Dwarnings`, not in source. - [ ] A documented, manual privileged-test matrix for raw sockets / datalink I/O From c5c23e428599f89b9d55a949935ff15f1c92248a Mon Sep 17 00:00:00 2001 From: shellrow Date: Sat, 18 Jul 2026 09:18:49 +0900 Subject: [PATCH 09/33] chore: update README --- README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/README.md b/README.md index e18e76a..e3caa43 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,25 @@ You can also directly use specific sub-crates by importing them individually. If you want to focus on network interfaces, you can use the [netdev](https://github.com/shellrow/netdev). +## Features + +All optional features are disabled by default in `nex`, `nex-packet`, +`nex-datalink`, and `nex-socket`. `nex-core` keeps `gateway` enabled by default +because gateway discovery is part of its primary interface functionality. + +| Feature | Crate | Purpose | +| --- | --- | --- | +| `async` | `nex`, `nex-datalink`, `nex-socket` | Enables asynchronous datalink I/O and Tokio-based socket APIs. | +| `pcap` | `nex`, `nex-datalink` | Enables the libpcap backend. | +| `serde` | `nex`, `nex-core`, `nex-packet`, `nex-datalink` | Enables serialization support for public data types. | +| `gateway` | `nex-core` | Enables default gateway discovery through `netdev`. | + +To use asynchronous APIs through the facade: + +```toml +[dependencies] +nex = { version = "0.26", features = ["async"] } +``` ## Privileges `nex-datalink` uses a raw socket which may require elevated privileges depending on your system's configuration. From 53e6a5e47748e0286b42abe1dfddca2ad97eaab3 Mon Sep 17 00:00:00 2001 From: shellrow Date: Sat, 18 Jul 2026 21:09:51 +0900 Subject: [PATCH 10/33] fix: prevent non-terminating IPv6 extension padding --- nex-packet/src/ipv6.rs | 36 +++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/nex-packet/src/ipv6.rs b/nex-packet/src/ipv6.rs index 0479488..b79617c 100644 --- a/nex-packet/src/ipv6.rs +++ b/nex-packet/src/ipv6.rs @@ -70,12 +70,12 @@ impl Packet for Ipv6Packet { match ext { Ipv6ExtensionHeader::HopByHop { next, data } | Ipv6ExtensionHeader::Destination { next, data } => { - let hdr_ext_len = ((data.len() + 6) / 8) as u8 - 1; + let total_length = (2 + data.len()).div_ceil(8) * 8; + let hdr_ext_len = (total_length / 8 - 1) as u8; buf.put_u8(next.value()); buf.put_u8(hdr_ext_len); buf.extend_from_slice(data); - // Padding (8 byte alignment) - while (2 + data.len()) % 8 != 0 { + for _ in 0..total_length - (2 + data.len()) { buf.put_u8(0); } } @@ -86,13 +86,14 @@ impl Packet for Ipv6Packet { segments_left, data, } => { - let hdr_ext_len = ((data.len() + 4 + 6) / 8) as u8 - 1; + let total_length = (4 + data.len()).div_ceil(8) * 8; + let hdr_ext_len = (total_length / 8 - 1) as u8; buf.put_u8(next.value()); buf.put_u8(hdr_ext_len); buf.put_u8(*routing_type); buf.put_u8(*segments_left); buf.extend_from_slice(data); - while (4 + data.len()) % 8 != 0 { + for _ in 0..total_length - (4 + data.len()) { buf.put_u8(0); } } @@ -683,6 +684,31 @@ mod tests { assert_eq!(parsed.to_bytes(), raw_bytes); } + #[test] + fn empty_hop_by_hop_header_is_padded_to_eight_bytes() { + let packet = Ipv6Packet { + header: Ipv6Header { + version: 6, + traffic_class: 0, + flow_label: 0, + payload_length: 8, + next_header: IpNextProtocol::Udp, + hop_limit: 64, + source: Ipv6Addr::LOCALHOST, + destination: Ipv6Addr::LOCALHOST, + }, + extensions: vec![Ipv6ExtensionHeader::HopByHop { + next: IpNextProtocol::Udp, + data: Bytes::new(), + }], + payload: Bytes::new(), + }; + + let bytes = packet.to_bytes(); + assert_eq!(bytes.len(), IPV6_HEADER_LEN + 8); + assert_eq!(&bytes[IPV6_HEADER_LEN..], &[17, 0, 0, 0, 0, 0, 0, 0]); + } + #[test] fn test_ipv6_payload_roundtrip() { use bytes::Bytes; From be474144610ac6322c3aff3db2fa73ccda752354 Mon Sep 17 00:00:00 2001 From: shellrow Date: Sat, 18 Jul 2026 21:10:20 +0900 Subject: [PATCH 11/33] feat: make packet builders fallible and validating --- TODO.md | 16 ++-- examples/arp.rs | 3 +- examples/async_datalink.rs | 16 ++-- examples/async_icmp_socket.rs | 3 +- examples/icmp_ping.rs | 16 ++-- examples/icmp_socket.rs | 6 +- examples/ndp.rs | 7 +- examples/tcp_ping.rs | 9 +- examples/udp_ping.rs | 11 ++- nex-packet/src/builder/arp.rs | 47 +++++++++- nex-packet/src/builder/dhcp.rs | 67 ++++++++++++-- nex-packet/src/builder/error.rs | 113 ++++++++++++++++++++++ nex-packet/src/builder/ethernet.rs | 3 + nex-packet/src/builder/icmp.rs | 54 ++++++++--- nex-packet/src/builder/icmpv6.rs | 45 ++++++++- nex-packet/src/builder/ipv4.rs | 144 ++++++++++++++++++++++++----- nex-packet/src/builder/ipv6.rs | 87 +++++++++++++++-- nex-packet/src/builder/mod.rs | 3 + nex-packet/src/builder/ndp.rs | 30 ++++-- nex-packet/src/builder/tcp.rs | 119 ++++++++++++++++++++---- nex-packet/src/builder/udp.rs | 59 ++++++++++-- nex-packet/src/lib.rs | 2 + nex-packet/src/tcp.rs | 11 +++ 23 files changed, 747 insertions(+), 124 deletions(-) create mode 100644 nex-packet/src/builder/error.rs diff --git a/TODO.md b/TODO.md index e4389ab..bce0c1a 100644 --- a/TODO.md +++ b/TODO.md @@ -62,14 +62,14 @@ in `nex-packet`. Every builder currently returns infallibly (`nex-packet/src/builder/*.rs`): `build(self) -> Packet` and `to_bytes(self) -> Bytes`, with no bounds checks. -- [ ] Change builder finalizers to `build(self) -> Result` +- [x] Change builder finalizers to `build(self) -> Result` (or a `try_build`) wherever a field can exceed protocol limits: - - [ ] IPv4/IPv6 total length, IHL/options length, payload length. - - [ ] TCP data offset / options length; UDP length; ICMP/ICMPv6 sizing. - - [ ] DHCP options length; NDP option length; ARP fixed sizing. -- [ ] Validate checksum prerequisites (pseudo-header context present) before emit. -- [ ] Introduce a typed `BuildError` (see P0.4) with actionable variants. -- [ ] Keep an infallible fast path only where inputs are provably in-range (e.g. + - [x] IPv4/IPv6 total length, IHL/options length, payload length. + - [x] TCP data offset / options length; UDP length; ICMP/ICMPv6 sizing. + - [x] DHCP options length; NDP option length; ARP fixed sizing. +- [x] Validate checksum prerequisites (pseudo-header context present) before emit. +- [x] Introduce a typed `BuildError` (see P0.4) with actionable variants. +- [x] Keep an infallible fast path only where inputs are provably in-range (e.g. fixed-size headers), and document why. Acceptance: constructing an over-length packet returns `Err`, never wrong bytes; @@ -105,7 +105,7 @@ tracked; no accidental `pub` internals. - [x] `nex-core::interface`: replace `Result<_, String>` with a typed error (`nex-core/src/interface.rs:359,592,597` — `default`, `get_default_interface`, `get_default_gateway`). -- [ ] Add `nex-packet::BuildError` for builders (P0.2). +- [x] Add `nex-packet::BuildError` for builders (P0.2). - [ ] Keep `io::Error` only at raw syscall/socket boundaries; convert to typed errors where the library adds semantic meaning (datalink/socket config). - [ ] Ensure `ParseError` carries enough context for fuzz triage and diagnostics diff --git a/examples/arp.rs b/examples/arp.rs index 6f66923..7e6af1f 100644 --- a/examples/arp.rs +++ b/examples/arp.rs @@ -63,7 +63,8 @@ fn main() { let arp_builder = ArpPacketBuilder::new(src_mac, src_ip, target_ip); - let packet = eth_builder.payload(arp_builder.build().to_bytes()).build(); + let arp_packet = arp_builder.build().expect("valid ARP packet"); + let packet = eth_builder.payload(arp_packet.to_bytes()).build(); match tx.send(&packet.to_bytes()) { Some(_) => println!("ARP Request sent to {}", target_ip), diff --git a/examples/async_datalink.rs b/examples/async_datalink.rs index d752e5a..cd9646e 100644 --- a/examples/async_datalink.rs +++ b/examples/async_datalink.rs @@ -62,15 +62,15 @@ fn main() -> std::io::Result<()> { .icmp_code(icmp::echo_request::IcmpCodes::NoCode) .echo_fields(0x1234, 0x1) .payload(Bytes::from_static(b"hello")) - .build() - .to_bytes(), + .to_bytes() + .expect("valid ICMP packet"), (IpAddr::V6(src), IpAddr::V6(dst)) => Icmpv6PacketBuilder::new(src, dst) .icmpv6_type(Icmpv6Type::EchoRequest) .icmpv6_code(icmpv6::echo_request::Icmpv6Codes::NoCode) .echo_fields(0x1234, 0x1) .payload(Bytes::from_static(b"hello")) - .build() - .to_bytes(), + .to_bytes() + .expect("valid ICMPv6 packet"), _ => panic!("Source and destination IP version mismatch"), }; @@ -81,15 +81,15 @@ fn main() -> std::io::Result<()> { .protocol(IpNextProtocol::Icmp) .flags(Ipv4Flags::DontFragment) .payload(icmp_packet) - .build() - .to_bytes(), + .to_bytes() + .expect("valid IPv4 packet"), (IpAddr::V6(src), IpAddr::V6(dst)) => Ipv6PacketBuilder::new() .source(src) .destination(dst) .next_header(IpNextProtocol::Icmpv6) .payload(icmp_packet) - .build() - .to_bytes(), + .to_bytes() + .expect("valid IPv6 packet"), _ => unreachable!(), }; diff --git a/examples/async_icmp_socket.rs b/examples/async_icmp_socket.rs index 7798544..eb71182 100644 --- a/examples/async_icmp_socket.rs +++ b/examples/async_icmp_socket.rs @@ -93,7 +93,8 @@ async fn main() -> std::io::Result<()> { .icmp_code(icmp::echo_request::IcmpCodes::NoCode) .echo_fields(id, seq) .payload(Bytes::from_static(b"ping")) - .to_bytes(); + .to_bytes() + .expect("valid ICMP packet"); let target = SocketAddr::new(IpAddr::V4(addr), 0); let _ = socket.send_to(&pkt, target).await; { diff --git a/examples/icmp_ping.rs b/examples/icmp_ping.rs index 6b0ae4a..6f42f01 100644 --- a/examples/icmp_ping.rs +++ b/examples/icmp_ping.rs @@ -70,15 +70,15 @@ fn main() { .icmp_code(icmp::echo_request::IcmpCodes::NoCode) .echo_fields(0x1234, 0x1) .payload(Bytes::from_static(b"hello")) - .build() - .to_bytes(), + .to_bytes() + .expect("valid ICMP packet"), (IpAddr::V6(src), IpAddr::V6(dst)) => Icmpv6PacketBuilder::new(src, dst) .icmpv6_type(Icmpv6Type::EchoRequest) .icmpv6_code(icmpv6::echo_request::Icmpv6Codes::NoCode) .echo_fields(0x1234, 0x1) .payload(Bytes::from_static(b"hello")) - .build() - .to_bytes(), + .to_bytes() + .expect("valid ICMPv6 packet"), _ => panic!("Source and destination IP version mismatch"), }; @@ -89,15 +89,15 @@ fn main() { .protocol(IpNextProtocol::Icmp) .flags(Ipv4Flags::DontFragment) .payload(icmp_packet) - .build() - .to_bytes(), + .to_bytes() + .expect("valid IPv4 packet"), (IpAddr::V6(src), IpAddr::V6(dst)) => Ipv6PacketBuilder::new() .source(src) .destination(dst) .next_header(IpNextProtocol::Icmpv6) .payload(icmp_packet) - .build() - .to_bytes(), + .to_bytes() + .expect("valid IPv6 packet"), _ => unreachable!(), }; diff --git a/examples/icmp_socket.rs b/examples/icmp_socket.rs index a39505a..afff341 100644 --- a/examples/icmp_socket.rs +++ b/examples/icmp_socket.rs @@ -56,13 +56,15 @@ fn main() -> std::io::Result<()> { .icmp_code(icmp::echo_request::IcmpCodes::NoCode) .echo_fields(0x1234, 1) .payload(Bytes::from_static(b"hello")) - .to_bytes(), + .to_bytes() + .expect("valid ICMP packet"), (IpAddr::V6(src), IpAddr::V6(dst)) => Icmpv6PacketBuilder::new(src, dst) .icmpv6_type(nex_packet::icmpv6::Icmpv6Type::EchoRequest) .icmpv6_code(icmpv6::echo_request::Icmpv6Codes::NoCode) .echo_fields(0x1234, 1) .payload(Bytes::from_static(b"hello")) - .to_bytes(), + .to_bytes() + .expect("valid ICMPv6 packet"), _ => unreachable!(), }; diff --git a/examples/ndp.rs b/examples/ndp.rs index 29d9b59..c41bb57 100644 --- a/examples/ndp.rs +++ b/examples/ndp.rs @@ -79,12 +79,17 @@ fn main() { .hop_limit(255); let ndp = NdpPacketBuilder::new(src_mac, src_ip, target_ip); + let ndp_packet = ndp.build().expect("valid NDP packet"); + let ipv6_packet = ipv6 + .payload(ndp_packet.to_bytes()) + .build() + .expect("valid IPv6 packet"); let ethernet = EthernetPacketBuilder::new() .source(src_mac) .destination(dst_mac) .ethertype(EtherType::Ipv6) - .payload(ipv6.payload(ndp.build().to_bytes()).build().to_bytes()); + .payload(ipv6_packet.to_bytes()); // Send NDP Neighbor Solicitation let packet = ethernet.build().to_bytes(); diff --git a/examples/tcp_ping.rs b/examples/tcp_ping.rs index c08ca07..3db4180 100644 --- a/examples/tcp_ping.rs +++ b/examples/tcp_ping.rs @@ -118,7 +118,8 @@ fn main() { TcpOptionPacket::nop(), TcpOptionPacket::wscale(7), ]) - .build(); + .build() + .expect("valid TCP packet"); let ip_packet: Bytes; match dst_ip { @@ -132,7 +133,8 @@ fn main() { .protocol(IpNextProtocol::Tcp) .flags(Ipv4Flags::DontFragment) .payload(tcp_packet.to_bytes()) - .build(); + .build() + .expect("valid IPv4 packet"); ip_packet = ipv4_packet.to_bytes(); } IpAddr::V6(_) => { @@ -154,7 +156,8 @@ fn main() { .destination(dst_ipv6) .next_header(IpNextProtocol::Tcp) .payload(tcp_packet.to_bytes()) - .build(); + .build() + .expect("valid IPv6 packet"); ip_packet = ipv6_packet.to_bytes(); } } diff --git a/examples/udp_ping.rs b/examples/udp_ping.rs index 06c9042..0af5331 100644 --- a/examples/udp_ping.rs +++ b/examples/udp_ping.rs @@ -68,7 +68,8 @@ fn main() { let udp_packet = UdpPacketBuilder::new(src_ip, target_ip) .source(SRC_PORT) .destination(DST_PORT) - .build(); + .build() + .expect("valid UDP packet"); let ip_packet: Bytes = match (src_ip, target_ip) { (IpAddr::V4(src), IpAddr::V4(dst)) => Ipv4PacketBuilder::new() @@ -77,15 +78,15 @@ fn main() { .protocol(IpNextProtocol::Udp) .flags(Ipv4Flags::DontFragment) .payload(udp_packet.to_bytes()) - .build() - .to_bytes(), + .to_bytes() + .expect("valid IPv4 packet"), (IpAddr::V6(src), IpAddr::V6(dst)) => Ipv6PacketBuilder::new() .source(src) .destination(dst) .next_header(IpNextProtocol::Udp) .payload(udp_packet.to_bytes()) - .build() - .to_bytes(), + .to_bytes() + .expect("valid IPv6 packet"), _ => panic!("Source and destination IP version mismatch"), }; diff --git a/nex-packet/src/builder/arp.rs b/nex-packet/src/builder/arp.rs index cdf2ae8..08100fe 100644 --- a/nex-packet/src/builder/arp.rs +++ b/nex-packet/src/builder/arp.rs @@ -1,5 +1,6 @@ use crate::{ arp::{ArpHardwareType, ArpHeader, ArpOperation, ArpPacket}, + builder::BuildError, ethernet::EtherType, packet::Packet, }; @@ -93,14 +94,28 @@ impl ArpPacketBuilder { self } - /// Return the finished `ArpPacket` - pub fn build(self) -> ArpPacket { - self.packet + /// Validate the fixed address sizes and return the finished packet. + pub fn build(self) -> Result { + if self.packet.header.hw_addr_len != 6 { + return Err(BuildError::InvalidFieldLength { + context: "ARP hardware address", + expected: 6, + actual: self.packet.header.hw_addr_len as usize, + }); + } + if self.packet.header.proto_addr_len != 4 { + return Err(BuildError::InvalidFieldLength { + context: "ARP protocol address", + expected: 4, + actual: self.packet.header.proto_addr_len as usize, + }); + } + Ok(self.packet) } /// Return the serialized bytes - pub fn to_bytes(self) -> Bytes { - self.build().to_bytes() + pub fn to_bytes(self) -> Result { + self.build().map(|packet| packet.to_bytes()) } /// Return a reference to the internal `ArpPacket` @@ -108,3 +123,25 @@ impl ArpPacketBuilder { &self.packet } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn arp_builder_rejects_non_ethernet_address_length() { + let error = + ArpPacketBuilder::new(MacAddr::zero(), Ipv4Addr::LOCALHOST, Ipv4Addr::LOCALHOST) + .sender_hw_addr_len(5) + .build() + .expect_err("invalid ARP address length"); + + assert!(matches!( + error, + BuildError::InvalidFieldLength { + context: "ARP hardware address", + .. + } + )); + } +} diff --git a/nex-packet/src/builder/dhcp.rs b/nex-packet/src/builder/dhcp.rs index 9fbcc51..8b59ffd 100644 --- a/nex-packet/src/builder/dhcp.rs +++ b/nex-packet/src/builder/dhcp.rs @@ -4,7 +4,8 @@ use bytes::Bytes; use nex_core::mac::MacAddr; use crate::{ - dhcp::{DhcpHardwareType, DhcpHeader, DhcpOperation, DhcpPacket}, + builder::BuildError, + dhcp::{DHCP_MIN_PACKET_SIZE, DhcpHardwareType, DhcpHeader, DhcpOperation, DhcpPacket}, packet::Packet, }; @@ -53,14 +54,48 @@ impl DhcpPacketBuilder { &mut self.packet.header } - /// Build and return a `DhcpPacket` - pub fn build(self) -> DhcpPacket { - self.packet + /// Validate fixed fields and build the DHCP packet. + pub fn build(self) -> Result { + for (context, expected, actual) in [ + ( + "DHCP client hardware address padding", + 10, + self.packet.header.chaddr_pad.len(), + ), + ("DHCP server name", 64, self.packet.header.sname.len()), + ("DHCP boot file", 128, self.packet.header.file.len()), + ] { + if actual != expected { + return Err(BuildError::InvalidFieldLength { + context, + expected, + actual, + }); + } + } + + let packet_length = DHCP_MIN_PACKET_SIZE + .checked_add(self.packet.payload.len()) + .ok_or(BuildError::LengthOverflow { + context: "DHCP packet", + maximum: u16::MAX as usize - 8, + actual: usize::MAX, + })?; + let maximum = u16::MAX as usize - 8; + if packet_length > maximum { + return Err(BuildError::LengthOverflow { + context: "DHCP packet", + maximum, + actual: packet_length, + }); + } + + Ok(self.packet) } /// Build and return the packet bytes - pub fn to_bytes(self) -> Bytes { - self.packet.to_bytes() + pub fn to_bytes(self) -> Result { + self.build().map(|packet| packet.to_bytes()) } /// Get a reference to the packet @@ -68,3 +103,23 @@ impl DhcpPacketBuilder { &self.packet } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn dhcp_builder_rejects_invalid_fixed_field_length() { + let mut builder = DhcpPacketBuilder::new_discover(1, MacAddr::zero()); + builder.header_mut().sname.clear(); + + let error = builder.build().expect_err("invalid DHCP server name"); + assert!(matches!( + error, + BuildError::InvalidFieldLength { + context: "DHCP server name", + .. + } + )); + } +} diff --git a/nex-packet/src/builder/error.rs b/nex-packet/src/builder/error.rs new file mode 100644 index 0000000..d2b9a30 --- /dev/null +++ b/nex-packet/src/builder/error.rs @@ -0,0 +1,113 @@ +use core::fmt; + +/// Error returned when packet builder inputs cannot be encoded safely. +#[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum BuildError { + /// A packet field cannot represent the supplied number of bytes. + LengthOverflow { + /// Protocol field or section that overflowed. + context: &'static str, + /// Largest representable value. + maximum: usize, + /// Supplied or calculated value. + actual: usize, + }, + /// A numeric protocol field is outside its representable range. + ValueOutOfRange { + /// Protocol field containing the value. + context: &'static str, + /// Largest accepted value. + maximum: usize, + /// Supplied value. + actual: usize, + }, + /// A variable-size field is shorter than the protocol minimum. + LengthTooShort { + /// Protocol field or section that is too short. + context: &'static str, + /// Smallest accepted length. + minimum: usize, + /// Supplied length. + actual: usize, + }, + /// A fixed-size field has an unexpected length. + InvalidFieldLength { + /// Protocol field with the invalid length. + context: &'static str, + /// Required number of bytes. + expected: usize, + /// Supplied number of bytes. + actual: usize, + }, + /// Source and destination addresses cannot form a checksum pseudo-header. + AddressFamilyMismatch { + /// Transport protocol requiring the pseudo-header. + context: &'static str, + }, + /// An internally assembled packet could not be decoded. + SerializationFailed { + /// Packet section that failed to round-trip. + context: &'static str, + }, +} + +impl fmt::Display for BuildError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::LengthOverflow { + context, + maximum, + actual, + } => write!( + f, + "{context} is too long: maximum is {maximum} bytes, got {actual}" + ), + Self::ValueOutOfRange { + context, + maximum, + actual, + } => write!( + f, + "{context} is out of range: maximum is {maximum}, got {actual}" + ), + Self::LengthTooShort { + context, + minimum, + actual, + } => write!( + f, + "{context} is too short: minimum is {minimum} bytes, got {actual}" + ), + Self::InvalidFieldLength { + context, + expected, + actual, + } => write!( + f, + "{context} has an invalid length: expected {expected} bytes, got {actual}" + ), + Self::AddressFamilyMismatch { context } => write!( + f, + "{context} checksum requires source and destination addresses from the same family" + ), + Self::SerializationFailed { context } => { + write!(f, "failed to serialize {context}") + } + } + } +} + +impl std::error::Error for BuildError {} + +#[cfg(test)] +mod tests { + use super::BuildError; + + fn assert_error_contract() {} + + #[test] + fn build_error_implements_public_error_contract() { + assert_error_contract::(); + } +} diff --git a/nex-packet/src/builder/ethernet.rs b/nex-packet/src/builder/ethernet.rs index 20c2782..29d3309 100644 --- a/nex-packet/src/builder/ethernet.rs +++ b/nex-packet/src/builder/ethernet.rs @@ -57,6 +57,9 @@ impl EthernetPacketBuilder { } /// Consume the builder and produce an `EthernetPacket`. + /// + /// This finalizer is infallible because Ethernet II uses a fixed-size + /// header and does not encode the payload length in the frame. pub fn build(self) -> EthernetPacket { self.packet } diff --git a/nex-packet/src/builder/icmp.rs b/nex-packet/src/builder/icmp.rs index 690f75b..dc347e1 100644 --- a/nex-packet/src/builder/icmp.rs +++ b/nex-packet/src/builder/icmp.rs @@ -1,6 +1,7 @@ use std::net::Ipv4Addr; use crate::{ + builder::BuildError, icmp::{self, IcmpCode, IcmpHeader, IcmpPacket, IcmpType}, packet::Packet, }; @@ -9,24 +10,18 @@ use bytes::{BufMut, Bytes, BytesMut}; /// Builder for constructing ICMP packets #[derive(Debug, Clone)] pub struct IcmpPacketBuilder { - #[allow(unused)] - source: Ipv4Addr, - #[allow(unused)] - destination: Ipv4Addr, packet: IcmpPacket, } impl IcmpPacketBuilder { /// Create a new builder with an initial ICMP Type and Code - pub fn new(source: Ipv4Addr, destination: Ipv4Addr) -> Self { + pub fn new(_source: Ipv4Addr, _destination: Ipv4Addr) -> Self { let header = IcmpHeader { icmp_type: IcmpType::EchoRequest, icmp_code: icmp::echo_request::IcmpCodes::NoCode, checksum: 0, }; Self { - source, - destination, packet: IcmpPacket { header, payload: Bytes::new(), @@ -69,14 +64,30 @@ impl IcmpPacketBuilder { } /// Return an `IcmpPacket` with checksum computed - pub fn build(mut self) -> IcmpPacket { + pub fn build(mut self) -> Result { + let packet_length = + 4usize + .checked_add(self.packet.payload.len()) + .ok_or(BuildError::LengthOverflow { + context: "ICMP packet", + maximum: u16::MAX as usize - 20, + actual: usize::MAX, + })?; + let maximum = u16::MAX as usize - 20; + if packet_length > maximum { + return Err(BuildError::LengthOverflow { + context: "ICMP packet", + maximum, + actual: packet_length, + }); + } self.packet.header.checksum = icmp::checksum(&self.packet); - self.packet + Ok(self.packet) } /// Return the packet bytes with checksum computed - pub fn to_bytes(self) -> Bytes { - self.build().to_bytes() + pub fn to_bytes(self) -> Result { + self.build().map(|packet| packet.to_bytes()) } /// Access the intermediate `IcmpPacket` if needed @@ -84,3 +95,24 @@ impl IcmpPacketBuilder { &self.packet } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn icmp_builder_rejects_packet_too_large_for_ipv4() { + let error = IcmpPacketBuilder::new(Ipv4Addr::LOCALHOST, Ipv4Addr::LOCALHOST) + .payload(Bytes::from(vec![0; u16::MAX as usize])) + .build() + .expect_err("oversized ICMP packet"); + + assert!(matches!( + error, + BuildError::LengthOverflow { + context: "ICMP packet", + .. + } + )); + } +} diff --git a/nex-packet/src/builder/icmpv6.rs b/nex-packet/src/builder/icmpv6.rs index 5d7f6bc..5447714 100644 --- a/nex-packet/src/builder/icmpv6.rs +++ b/nex-packet/src/builder/icmpv6.rs @@ -1,6 +1,7 @@ use std::net::Ipv6Addr; use crate::{ + builder::BuildError, icmpv6::{self, Icmpv6Code, Icmpv6Header, Icmpv6Packet, Icmpv6Type}, packet::Packet, }; @@ -66,15 +67,30 @@ impl Icmpv6PacketBuilder { } /// Return an `Icmpv6Packet` with checksum computed - pub fn build(mut self) -> Icmpv6Packet { + pub fn build(mut self) -> Result { + let packet_length = + 4usize + .checked_add(self.packet.payload.len()) + .ok_or(BuildError::LengthOverflow { + context: "ICMPv6 packet", + maximum: u16::MAX as usize, + actual: usize::MAX, + })?; + if packet_length > u16::MAX as usize { + return Err(BuildError::LengthOverflow { + context: "ICMPv6 packet", + maximum: u16::MAX as usize, + actual: packet_length, + }); + } self.packet.header.checksum = icmpv6::checksum(&self.packet, &self.source, &self.destination); - self.packet + Ok(self.packet) } /// Return the packet bytes with checksum computed - pub fn to_bytes(self) -> Bytes { - self.build().to_bytes() + pub fn to_bytes(self) -> Result { + self.build().map(|packet| packet.to_bytes()) } /// Access the intermediate `Icmpv6Packet` if needed @@ -82,3 +98,24 @@ impl Icmpv6PacketBuilder { &self.packet } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn icmpv6_builder_rejects_oversized_packet() { + let error = Icmpv6PacketBuilder::new(Ipv6Addr::LOCALHOST, Ipv6Addr::LOCALHOST) + .payload(Bytes::from(vec![0; u16::MAX as usize])) + .build() + .expect_err("oversized ICMPv6 packet"); + + assert!(matches!( + error, + BuildError::LengthOverflow { + context: "ICMPv6 packet", + .. + } + )); + } +} diff --git a/nex-packet/src/builder/ipv4.rs b/nex-packet/src/builder/ipv4.rs index eaee8e5..648a18e 100644 --- a/nex-packet/src/builder/ipv4.rs +++ b/nex-packet/src/builder/ipv4.rs @@ -1,6 +1,7 @@ use crate::{ + builder::BuildError, ip::IpNextProtocol, - ipv4::{Ipv4Header, Ipv4OptionPacket, Ipv4OptionType, Ipv4Packet}, + ipv4::{IPV4_HEADER_LEN, Ipv4Header, Ipv4OptionPacket, Ipv4OptionType, Ipv4Packet}, packet::Packet, }; use bytes::Bytes; @@ -82,18 +83,6 @@ impl Ipv4PacketBuilder { pub fn options(mut self, options: Vec) -> Self { self.packet.header.options = options; - self.packet.header.header_length = (20 - + self - .packet - .header - .options - .iter() - .map(|opt| match opt.header.number { - Ipv4OptionType::EOL | Ipv4OptionType::NOP => 1, - _ => 2 + opt.data.len(), - }) - .sum::()) - .div_ceil(4) as u4; // includes padding self } @@ -102,15 +91,81 @@ impl Ipv4PacketBuilder { self } - pub fn build(mut self) -> Ipv4Packet { - let total_length = self.packet.header_len() + self.packet.payload_len(); + pub fn build(mut self) -> Result { + let mut options_length = 0usize; + for option in &self.packet.header.options { + let encoded_length = match option.header.number { + Ipv4OptionType::EOL | Ipv4OptionType::NOP => 1, + _ => { + let encoded_length = 2usize.checked_add(option.data.len()).ok_or( + BuildError::LengthOverflow { + context: "IPv4 option", + maximum: u8::MAX as usize, + actual: usize::MAX, + }, + )?; + if encoded_length > u8::MAX as usize { + return Err(BuildError::LengthOverflow { + context: "IPv4 option", + maximum: u8::MAX as usize, + actual: encoded_length, + }); + } + if let Some(declared) = option.header.length + && declared as usize != encoded_length + { + return Err(BuildError::InvalidFieldLength { + context: "IPv4 option length", + expected: encoded_length, + actual: declared as usize, + }); + } + encoded_length + } + }; + options_length = + options_length + .checked_add(encoded_length) + .ok_or(BuildError::LengthOverflow { + context: "IPv4 options", + maximum: 40, + actual: usize::MAX, + })?; + } + + let padded_options_length = options_length.div_ceil(4) * 4; + if padded_options_length > 40 { + return Err(BuildError::LengthOverflow { + context: "IPv4 options", + maximum: 40, + actual: padded_options_length, + }); + } + + let header_length = IPV4_HEADER_LEN + padded_options_length; + let total_length = header_length.checked_add(self.packet.payload_len()).ok_or( + BuildError::LengthOverflow { + context: "IPv4 total length", + maximum: u16::MAX as usize, + actual: usize::MAX, + }, + )?; + if total_length > u16::MAX as usize { + return Err(BuildError::LengthOverflow { + context: "IPv4 total length", + maximum: u16::MAX as usize, + actual: total_length, + }); + } + + self.packet.header.header_length = (header_length / 4) as u4; self.packet.header.total_length = total_length as u16be; self.packet.header.checksum = crate::ipv4::checksum(&self.packet); - self.packet + Ok(self.packet) } - pub fn to_bytes(self) -> Bytes { - self.build().to_bytes() + pub fn to_bytes(self) -> Result { + self.build().map(|packet| packet.to_bytes()) } } @@ -118,6 +173,7 @@ impl Ipv4PacketBuilder { mod tests { use super::*; use crate::ip::IpNextProtocol; + use crate::ipv4::Ipv4OptionHeader; use bytes::Bytes; use std::net::Ipv4Addr; @@ -129,7 +185,8 @@ mod tests { .destination(Ipv4Addr::new(2, 2, 2, 2)) .protocol(IpNextProtocol::Udp) .payload(payload.clone()) - .build(); + .build() + .expect("valid IPv4 packet"); assert_eq!( pkt.header.total_length, (pkt.header_len() + payload.len()) as u16 @@ -139,10 +196,55 @@ mod tests { #[test] fn ipv4_builder_identification_is_caller_controlled() { - let default_packet = Ipv4PacketBuilder::new().build(); + let default_packet = Ipv4PacketBuilder::new().build().expect("valid IPv4 packet"); assert_eq!(default_packet.header.identification, 0); - let packet = Ipv4PacketBuilder::new().identification(0x1234).build(); + let packet = Ipv4PacketBuilder::new() + .identification(0x1234) + .build() + .expect("valid IPv4 packet"); assert_eq!(packet.header.identification, 0x1234); } + + #[test] + fn ipv4_builder_rejects_oversized_payload() { + let payload = Bytes::from(vec![0; u16::MAX as usize]); + let error = Ipv4PacketBuilder::new() + .payload(payload) + .build() + .expect_err("oversized IPv4 packet"); + + assert!(matches!( + error, + BuildError::LengthOverflow { + context: "IPv4 total length", + .. + } + )); + } + + #[test] + fn ipv4_builder_rejects_options_beyond_ihl_capacity() { + let option = Ipv4OptionPacket { + header: Ipv4OptionHeader { + copied: 0, + class: 0, + number: Ipv4OptionType::NOP, + length: None, + }, + data: Bytes::new(), + }; + let error = Ipv4PacketBuilder::new() + .options(vec![option; 41]) + .build() + .expect_err("IPv4 IHL overflow"); + + assert!(matches!( + error, + BuildError::LengthOverflow { + context: "IPv4 options", + .. + } + )); + } } diff --git a/nex-packet/src/builder/ipv6.rs b/nex-packet/src/builder/ipv6.rs index 2975312..1a9e208 100644 --- a/nex-packet/src/builder/ipv6.rs +++ b/nex-packet/src/builder/ipv6.rs @@ -1,4 +1,5 @@ use crate::{ + builder::BuildError, ip::IpNextProtocol, ipv6::{Ipv6ExtensionHeader, Ipv6Header, Ipv6Packet}, packet::Packet, @@ -85,15 +86,69 @@ impl Ipv6PacketBuilder { } /// Build the packet and return it - pub fn build(mut self) -> Ipv6Packet { - let ext_len: usize = self.packet.extensions.iter().map(|e| e.len()).sum(); - self.packet.header.payload_length = (ext_len + self.packet.payload.len()) as u16; - self.packet + pub fn build(mut self) -> Result { + let mut extensions_length = 0usize; + for extension in &self.packet.extensions { + let extension_length = extension.len(); + match extension { + Ipv6ExtensionHeader::HopByHop { .. } + | Ipv6ExtensionHeader::Destination { .. } + | Ipv6ExtensionHeader::Routing { .. } + if extension_length > 2048 => + { + return Err(BuildError::LengthOverflow { + context: "IPv6 extension header", + maximum: 2048, + actual: extension_length, + }); + } + Ipv6ExtensionHeader::Fragment { offset, .. } if *offset > 0x1fff => { + return Err(BuildError::ValueOutOfRange { + context: "IPv6 fragment offset", + maximum: 0x1fff, + actual: *offset as usize, + }); + } + Ipv6ExtensionHeader::Raw { raw, .. } if raw.is_empty() => { + return Err(BuildError::LengthTooShort { + context: "raw IPv6 extension header", + minimum: 1, + actual: 0, + }); + } + _ => {} + } + extensions_length = extensions_length.checked_add(extension_length).ok_or( + BuildError::LengthOverflow { + context: "IPv6 payload length", + maximum: u16::MAX as usize, + actual: usize::MAX, + }, + )?; + } + + let payload_length = extensions_length + .checked_add(self.packet.payload.len()) + .ok_or(BuildError::LengthOverflow { + context: "IPv6 payload length", + maximum: u16::MAX as usize, + actual: usize::MAX, + })?; + if payload_length > u16::MAX as usize { + return Err(BuildError::LengthOverflow { + context: "IPv6 payload length", + maximum: u16::MAX as usize, + actual: payload_length, + }); + } + + self.packet.header.payload_length = payload_length as u16; + Ok(self.packet) } /// Serialize the packet into bytes - pub fn to_bytes(self) -> Bytes { - self.build().to_bytes() + pub fn to_bytes(self) -> Result { + self.build().map(|packet| packet.to_bytes()) } /// Get only the header bytes @@ -117,8 +172,26 @@ mod tests { .destination(Ipv6Addr::LOCALHOST) .next_header(IpNextProtocol::Tcp) .payload(payload.clone()) - .build(); + .build() + .expect("valid IPv6 packet"); assert_eq!(pkt.header.payload_length, payload.len() as u16); assert_eq!(pkt.payload, payload); } + + #[test] + fn ipv6_builder_rejects_oversized_payload() { + let payload = Bytes::from(vec![0; u16::MAX as usize + 1]); + let error = Ipv6PacketBuilder::new() + .payload(payload) + .build() + .expect_err("oversized IPv6 payload"); + + assert!(matches!( + error, + BuildError::LengthOverflow { + context: "IPv6 payload length", + .. + } + )); + } } diff --git a/nex-packet/src/builder/mod.rs b/nex-packet/src/builder/mod.rs index 3cabb43..36022d2 100644 --- a/nex-packet/src/builder/mod.rs +++ b/nex-packet/src/builder/mod.rs @@ -1,5 +1,6 @@ pub mod arp; pub mod dhcp; +mod error; pub mod ethernet; pub mod icmp; pub mod icmpv6; @@ -8,3 +9,5 @@ pub mod ipv6; pub mod ndp; pub mod tcp; pub mod udp; + +pub use error::BuildError; diff --git a/nex-packet/src/builder/ndp.rs b/nex-packet/src/builder/ndp.rs index 8ffa0cf..27335d7 100644 --- a/nex-packet/src/builder/ndp.rs +++ b/nex-packet/src/builder/ndp.rs @@ -1,3 +1,4 @@ +use crate::builder::BuildError; use crate::icmpv6::ndp::{NdpOptionPacket, NdpOptionTypes, NeighborSolicitPacket}; use crate::icmpv6::{self, Icmpv6Header, Icmpv6Packet, Icmpv6Type, checksum}; use crate::packet::Packet; @@ -41,7 +42,7 @@ impl NdpPacketBuilder { } /// Build the Neighbor Solicitation packet - pub fn build(&self) -> Icmpv6Packet { + pub fn build(&self) -> Result { // Build the MAC address option let mac_bytes = self.src_mac.octets(); let opt_payload = Bytes::copy_from_slice(&mac_bytes); @@ -66,15 +67,32 @@ impl NdpPacketBuilder { }; // Build an Icmpv6Packet and calculate the checksum - let mut icmp_packet = Icmpv6Packet::from_bytes(packet.to_bytes()) - .expect("Failed to create Icmpv6Packet from NeighborSolicitPacket"); + let mut icmp_packet = + Icmpv6Packet::from_bytes(packet.to_bytes()).ok_or(BuildError::SerializationFailed { + context: "NDP neighbor solicitation", + })?; icmp_packet.header.checksum = checksum(&icmp_packet, &self.src_ip, &self.dst_ip); - icmp_packet + Ok(icmp_packet) } /// Get the packet as bytes - pub fn to_bytes(&self) -> Bytes { - self.build().to_bytes() + pub fn to_bytes(&self) -> Result { + self.build().map(|packet| packet.to_bytes()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn ndp_builder_produces_aligned_source_link_layer_option() { + let packet = + NdpPacketBuilder::new(MacAddr::zero(), Ipv6Addr::LOCALHOST, Ipv6Addr::LOCALHOST) + .build() + .expect("valid NDP packet"); + + assert_eq!(packet.payload.len(), 28); } } diff --git a/nex-packet/src/builder/tcp.rs b/nex-packet/src/builder/tcp.rs index 84e5275..14c70c7 100644 --- a/nex-packet/src/builder/tcp.rs +++ b/nex-packet/src/builder/tcp.rs @@ -1,7 +1,8 @@ use std::net::IpAddr; +use crate::builder::BuildError; use crate::packet::Packet; -use crate::tcp::{TcpHeader, TcpOptionPacket, TcpPacket}; +use crate::tcp::{TCP_HEADER_LEN, TcpHeader, TcpOptionPacket, TcpPacket}; use bytes::Bytes; /// Builder for constructing TCP packets @@ -74,17 +75,6 @@ impl TcpPacketBuilder { pub fn options(mut self, options: Vec) -> Self { self.packet.header.options = options; - // Recalculate data offset (header length is in 4-byte units) - let base_len = 20; // base header - let opt_len: usize = self - .packet - .header - .options - .iter() - .map(|opt| opt.length() as usize) - .sum(); - let total = base_len + opt_len; - self.packet.header.data_offset = total.div_ceil(4) as u8; // round up self } @@ -100,14 +90,76 @@ impl TcpPacketBuilder { self } /// Build the packet with checksum computed - pub fn build(mut self) -> TcpPacket { + pub fn build(mut self) -> Result { + let maximum_segment_length = match (self.src_ip, self.dst_ip) { + (IpAddr::V4(_), IpAddr::V4(_)) => u16::MAX as usize - 20, + (IpAddr::V6(_), IpAddr::V6(_)) => u16::MAX as usize, + _ => { + return Err(BuildError::AddressFamilyMismatch { context: "TCP" }); + } + }; + + let mut options_length = 0usize; + for option in &self.packet.header.options { + let encoded_length = option.encoded_len(); + if encoded_length > u8::MAX as usize { + return Err(BuildError::LengthOverflow { + context: "TCP option", + maximum: u8::MAX as usize, + actual: encoded_length, + }); + } + if encoded_length > 1 && option.declared_len().map(usize::from) != Some(encoded_length) + { + return Err(BuildError::InvalidFieldLength { + context: "TCP option length", + expected: encoded_length, + actual: option.declared_len().map(usize::from).unwrap_or(0), + }); + } + options_length = + options_length + .checked_add(encoded_length) + .ok_or(BuildError::LengthOverflow { + context: "TCP options", + maximum: 40, + actual: usize::MAX, + })?; + } + + let padded_options_length = options_length.div_ceil(4) * 4; + if padded_options_length > 40 { + return Err(BuildError::LengthOverflow { + context: "TCP options", + maximum: 40, + actual: padded_options_length, + }); + } + + let segment_length = TCP_HEADER_LEN + .checked_add(padded_options_length) + .and_then(|header_length| header_length.checked_add(self.packet.payload.len())) + .ok_or(BuildError::LengthOverflow { + context: "TCP segment", + maximum: maximum_segment_length, + actual: usize::MAX, + })?; + if segment_length > maximum_segment_length { + return Err(BuildError::LengthOverflow { + context: "TCP segment", + maximum: maximum_segment_length, + actual: segment_length, + }); + } + + self.packet.header.data_offset = ((TCP_HEADER_LEN + padded_options_length) / 4) as u8; self.packet.header.checksum = crate::tcp::checksum(&self.packet, &self.src_ip, &self.dst_ip); - self.packet + Ok(self.packet) } /// Serialize the packet into bytes with checksum computed - pub fn to_bytes(self) -> Bytes { - self.build().to_bytes() + pub fn to_bytes(self) -> Result { + self.build().map(|packet| packet.to_bytes()) } } @@ -133,7 +185,8 @@ mod tests { .window(1024) .urgent_ptr(0) .payload(Bytes::from_static(b"abc")) - .build(); + .build() + .expect("valid TCP packet"); assert_eq!(pkt.header.source, 1234); assert_eq!(pkt.header.destination, 80); assert_eq!(pkt.header.sequence, 1); @@ -141,4 +194,36 @@ mod tests { assert_eq!(pkt.header.flags, TcpFlags::SYN); assert_eq!(pkt.payload, Bytes::from_static(b"abc")); } + + #[test] + fn tcp_builder_rejects_address_family_mismatch() { + let error = TcpPacketBuilder::new( + IpAddr::V4(Ipv4Addr::LOCALHOST), + IpAddr::V6(std::net::Ipv6Addr::LOCALHOST), + ) + .build() + .expect_err("mismatched checksum context"); + + assert_eq!(error, BuildError::AddressFamilyMismatch { context: "TCP" }); + } + + #[test] + fn tcp_builder_rejects_oversized_options() { + let options = vec![TcpOptionPacket::timestamp(0, 0); 5]; + let error = TcpPacketBuilder::new( + IpAddr::V4(Ipv4Addr::LOCALHOST), + IpAddr::V4(Ipv4Addr::LOCALHOST), + ) + .options(options) + .build() + .expect_err("TCP data offset overflow"); + + assert!(matches!( + error, + BuildError::LengthOverflow { + context: "TCP options", + .. + } + )); + } } diff --git a/nex-packet/src/builder/udp.rs b/nex-packet/src/builder/udp.rs index 7c83abf..0bdd042 100644 --- a/nex-packet/src/builder/udp.rs +++ b/nex-packet/src/builder/udp.rs @@ -1,5 +1,6 @@ use std::net::IpAddr; +use crate::builder::BuildError; use crate::packet::Packet; use crate::udp::{UDP_HEADER_LEN, UdpHeader, UdpPacket}; use bytes::Bytes; @@ -63,26 +64,44 @@ impl UdpPacketBuilder { } /// Build the packet with checksum computed - pub fn build(mut self) -> UdpPacket { + pub fn build(mut self) -> Result { + if !matches!( + (self.src_ip, self.dst_ip), + (IpAddr::V4(_), IpAddr::V4(_)) | (IpAddr::V6(_), IpAddr::V6(_)) + ) { + return Err(BuildError::AddressFamilyMismatch { context: "UDP" }); + } + // Automatically compute the length - let total_len = UDP_HEADER_LEN + self.packet.payload.len(); + let total_len = UDP_HEADER_LEN + .checked_add(self.packet.payload.len()) + .ok_or(BuildError::LengthOverflow { + context: "UDP length", + maximum: u16::MAX as usize, + actual: usize::MAX, + })?; + if total_len > u16::MAX as usize { + return Err(BuildError::LengthOverflow { + context: "UDP length", + maximum: u16::MAX as usize, + actual: total_len, + }); + } self.packet.header.length = total_len as u16; // Calculate the checksum self.packet.header.checksum = crate::udp::checksum(&self.packet, &self.src_ip, &self.dst_ip); - self.packet + Ok(self.packet) } /// Serialize the packet into bytes with checksum computed - pub fn to_bytes(self) -> Bytes { - self.build().to_bytes() + pub fn to_bytes(self) -> Result { + self.build().map(|packet| packet.to_bytes()) } /// Retrieve only the header bytes - pub fn header_bytes(&self) -> Bytes { - let mut pkt = self.clone().packet; - pkt.header.length = (UDP_HEADER_LEN + pkt.payload.len()) as u16; - pkt.header().clone() + pub fn header_bytes(&self) -> Result { + self.clone().build().map(|packet| packet.header()) } } @@ -102,8 +121,28 @@ mod tests { .source(1) .destination(2) .payload(Bytes::from_static(&[1, 2, 3])) - .build(); + .build() + .expect("valid UDP packet"); assert_eq!(pkt.header.length, (UDP_HEADER_LEN + 3) as u16); assert_eq!(pkt.payload, Bytes::from_static(&[1, 2, 3])); } + + #[test] + fn udp_builder_rejects_oversized_payload() { + let error = UdpPacketBuilder::new( + IpAddr::V4(Ipv4Addr::LOCALHOST), + IpAddr::V4(Ipv4Addr::LOCALHOST), + ) + .payload(Bytes::from(vec![0; u16::MAX as usize])) + .build() + .expect_err("UDP length overflow"); + + assert!(matches!( + error, + BuildError::LengthOverflow { + context: "UDP length", + .. + } + )); + } } diff --git a/nex-packet/src/lib.rs b/nex-packet/src/lib.rs index a8067c6..4e4b363 100644 --- a/nex-packet/src/lib.rs +++ b/nex-packet/src/lib.rs @@ -21,3 +21,5 @@ pub mod udp; pub mod util; pub mod vlan; pub mod vxlan; + +pub use builder::BuildError; diff --git a/nex-packet/src/tcp.rs b/nex-packet/src/tcp.rs index 0364a8a..ae1a2d2 100644 --- a/nex-packet/src/tcp.rs +++ b/nex-packet/src/tcp.rs @@ -321,6 +321,17 @@ pub struct TcpOptionPacket { } impl TcpOptionPacket { + pub(crate) fn encoded_len(&self) -> usize { + match self.kind { + TcpOptionKind::EOL | TcpOptionKind::NOP => 1, + _ => 2 + self.data.len(), + } + } + + pub(crate) fn declared_len(&self) -> Option { + self.length + } + /// NOP: This may be used to align option fields on 32-bit boundaries for better performance. pub fn nop() -> Self { TcpOptionPacket { From 5299b6ca72d851bac0ea526e5b62e05caa65da98 Mon Sep 17 00:00:00 2001 From: shellrow Date: Sat, 18 Jul 2026 21:56:17 +0900 Subject: [PATCH 12/33] feat: establish Rust 1.88 as the workspace MSRV --- Cargo.toml | 1 + README.md | 5 +++++ fuzz/Cargo.toml | 1 + nex-core/Cargo.toml | 1 + nex-datalink/Cargo.toml | 1 + nex-packet/Cargo.toml | 1 + nex-packet/src/builder/ipv4.rs | 17 +++++++++-------- nex-packet/src/ipv4.rs | 6 ++---- nex-socket/Cargo.toml | 1 + nex-socket/src/udp/sync_impl.rs | 26 ++++++++------------------ nex-sys/Cargo.toml | 1 + nex/Cargo.toml | 1 + 12 files changed, 32 insertions(+), 30 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2220896..9d19168 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ members = [ [workspace.package] version = "0.26.0" edition = "2024" +rust-version = "1.88" authors = ["shellrow "] [workspace.dependencies] diff --git a/README.md b/README.md index e3caa43..916df63 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,11 @@ It includes sub-crates with responsibilities: The project aims to expose portable low-level primitives. +## Minimum Supported Rust Version + +`nex` requires Rust 1.88.0 or later. MSRV changes are treated as compatibility +changes and are verified in CI. + ## Usage To use `nex`, add it as a dependency in your `Cargo.toml`: diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index f69cd22..a31ecd2 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -3,6 +3,7 @@ name = "nex-fuzz" version = "0.0.0" publish = false edition = "2024" +rust-version = "1.88" [workspace] diff --git a/nex-core/Cargo.toml b/nex-core/Cargo.toml index 1c98ea1..12f5df0 100644 --- a/nex-core/Cargo.toml +++ b/nex-core/Cargo.toml @@ -2,6 +2,7 @@ name = "nex-core" version.workspace = true edition.workspace = true +rust-version.workspace = true authors.workspace = true description = "Core networking library for nex." repository = "https://github.com/shellrow/nex" diff --git a/nex-datalink/Cargo.toml b/nex-datalink/Cargo.toml index 9567dd6..d8ca8a8 100644 --- a/nex-datalink/Cargo.toml +++ b/nex-datalink/Cargo.toml @@ -2,6 +2,7 @@ name = "nex-datalink" version.workspace = true edition.workspace = true +rust-version.workspace = true authors.workspace = true description = "Provides cross-platform datalink layer networking. Part of nex project." repository = "https://github.com/shellrow/nex" diff --git a/nex-packet/Cargo.toml b/nex-packet/Cargo.toml index 502731f..48fb538 100644 --- a/nex-packet/Cargo.toml +++ b/nex-packet/Cargo.toml @@ -2,6 +2,7 @@ name = "nex-packet" version.workspace = true edition.workspace = true +rust-version.workspace = true authors.workspace = true description = "Cross-platform packet parsing and building library. Provides low-level packet handling. Part of nex project." repository = "https://github.com/shellrow/nex" diff --git a/nex-packet/src/builder/ipv4.rs b/nex-packet/src/builder/ipv4.rs index 648a18e..281cad0 100644 --- a/nex-packet/src/builder/ipv4.rs +++ b/nex-packet/src/builder/ipv4.rs @@ -111,14 +111,15 @@ impl Ipv4PacketBuilder { actual: encoded_length, }); } - if let Some(declared) = option.header.length - && declared as usize != encoded_length - { - return Err(BuildError::InvalidFieldLength { - context: "IPv4 option length", - expected: encoded_length, - actual: declared as usize, - }); + match option.header.length { + Some(declared) if declared as usize != encoded_length => { + return Err(BuildError::InvalidFieldLength { + context: "IPv4 option length", + expected: encoded_length, + actual: declared as usize, + }); + } + _ => {} } encoded_length } diff --git a/nex-packet/src/ipv4.rs b/nex-packet/src/ipv4.rs index 518f4a7..60a47c2 100644 --- a/nex-packet/src/ipv4.rs +++ b/nex-packet/src/ipv4.rs @@ -241,10 +241,8 @@ impl Packet for Ipv4Packet { } } - // padding - while !tmp_buf.len().is_multiple_of(4) { - tmp_buf.put_u8(0); - } + let padding_len = (4 - (tmp_buf.len() % 4)) % 4; + tmp_buf.resize(tmp_buf.len() + padding_len, 0); let header_len = IPV4_HEADER_LEN + tmp_buf.len(); diff --git a/nex-socket/Cargo.toml b/nex-socket/Cargo.toml index 34c7229..7142b93 100644 --- a/nex-socket/Cargo.toml +++ b/nex-socket/Cargo.toml @@ -2,6 +2,7 @@ name = "nex-socket" version.workspace = true edition.workspace = true +rust-version.workspace = true authors.workspace = true description = "Cross-platform socket library. Part of nex project. Offers socket-related functionality." repository = "https://github.com/shellrow/nex" diff --git a/nex-socket/src/udp/sync_impl.rs b/nex-socket/src/udp/sync_impl.rs index 51b28c8..8bcd6ef 100644 --- a/nex-socket/src/udp/sync_impl.rs +++ b/nex-socket/src/udp/sync_impl.rs @@ -179,6 +179,8 @@ impl UdpSocket { let iov = [IoSlice::new(buf)]; let raw_fd = self.socket.as_raw_fd(); + let packet_info_meta = + meta.filter(|meta| meta.source_addr.is_some() || meta.interface_index.is_some()); match target { SocketAddr::V4(addr) => { @@ -190,12 +192,8 @@ impl UdpSocket { target_vendor = "apple" ))] { - if let Some(meta) = meta - && (meta.source_addr.is_some() || meta.interface_index.is_some()) - { - if let Some(src) = meta.source_addr - && !src.is_ipv4() - { + if let Some(meta) = packet_info_meta { + if meta.source_addr.is_some_and(|src| !src.is_ipv4()) { return Err(io::Error::new( io::ErrorKind::InvalidInput, "source_addr family does not match target", @@ -221,9 +219,7 @@ impl UdpSocket { .map_err(|e| io::Error::from_raw_os_error(e as i32)); } } - if let Some(meta) = meta - && (meta.source_addr.is_some() || meta.interface_index.is_some()) - { + if packet_info_meta.is_some() { return Err(io::Error::new( io::ErrorKind::Unsupported, "send_msg packet-info metadata is not supported on this platform", @@ -242,12 +238,8 @@ impl UdpSocket { target_vendor = "apple" ))] { - if let Some(meta) = meta - && (meta.source_addr.is_some() || meta.interface_index.is_some()) - { - if let Some(src) = meta.source_addr - && !src.is_ipv6() - { + if let Some(meta) = packet_info_meta { + if meta.source_addr.is_some_and(|src| !src.is_ipv6()) { return Err(io::Error::new( io::ErrorKind::InvalidInput, "source_addr family does not match target", @@ -273,9 +265,7 @@ impl UdpSocket { .map_err(|e| io::Error::from_raw_os_error(e as i32)); } } - if let Some(meta) = meta - && (meta.source_addr.is_some() || meta.interface_index.is_some()) - { + if packet_info_meta.is_some() { return Err(io::Error::new( io::ErrorKind::Unsupported, "send_msg packet-info metadata is not supported on this platform", diff --git a/nex-sys/Cargo.toml b/nex-sys/Cargo.toml index f68d87d..bd11a85 100644 --- a/nex-sys/Cargo.toml +++ b/nex-sys/Cargo.toml @@ -2,6 +2,7 @@ name = "nex-sys" version.workspace = true edition.workspace = true +rust-version.workspace = true authors.workspace = true description = "Provides network-related system function and call support for nex. Used for low-level system interactions." repository = "https://github.com/shellrow/nex" diff --git a/nex/Cargo.toml b/nex/Cargo.toml index ebb20eb..05abaf5 100644 --- a/nex/Cargo.toml +++ b/nex/Cargo.toml @@ -2,6 +2,7 @@ name = "nex" version.workspace = true edition.workspace = true +rust-version.workspace = true authors.workspace = true description = "Cross-platform networking library in Rust" repository = "https://github.com/shellrow/nex" From e0d9dc019099d6826c7f0d4eb655475aa455cb9e Mon Sep 17 00:00:00 2001 From: shellrow Date: Sat, 18 Jul 2026 21:57:03 +0900 Subject: [PATCH 13/33] feat: add cargo-deny supply-chain checks --- .github/workflows/rust.yml | 22 ++++++++++++++++++++++ TODO.md | 4 ++-- deny.toml | 30 ++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 deny.toml diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 69aa654..c2d0ab2 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -70,3 +70,25 @@ jobs: - uses: Swatinem/rust-cache@v2 - name: Check feature combination run: ${{ matrix.command }} + + msrv: + name: MSRV (Rust 1.88.0) + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@1.88.0 + - uses: Swatinem/rust-cache@v2 + - name: Check workspace with MSRV + run: cargo check --workspace --all-features --locked + + supply-chain: + name: Supply chain + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: EmbarkStudios/cargo-deny-action@v2 + with: + command: check + arguments: --all-features diff --git a/TODO.md b/TODO.md index bce0c1a..6aa7ff6 100644 --- a/TODO.md +++ b/TODO.md @@ -150,9 +150,9 @@ Current `.github/workflows/rust.yml` runs only `cargo build` on Linux/macOS/Wind `cargo clippy --workspace --all-targets --all-features -- -D warnings`. - [x] Feature-combo job: default / `--no-default-features` / `serde` / `pcap` / `async` / `--all-features`. -- [ ] MSRV job: pin and verify an MSRV (edition 2024 ⇒ MSRV ≥ 1.85; declare +- [x] MSRV job: pin and verify an MSRV (edition 2024 ⇒ MSRV ≥ 1.85; declare `rust-version` in every `Cargo.toml` and test it). -- [ ] Supply chain: `cargo deny check` (licenses, advisories, bans, sources) + +- [x] Supply chain: `cargo deny check` (licenses, advisories, bans, sources) + add `deny.toml`. - [ ] Public API snapshot diff job (P0.3). - [x] Remove `#![deny(warnings)]` from `nex-datalink/src/lib.rs` (line 3): it makes diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..8fecbcf --- /dev/null +++ b/deny.toml @@ -0,0 +1,30 @@ +[graph] +all-features = true + +[advisories] +ignore = [ + { id = "RUSTSEC-2024-0436", reason = "Transitive dependency of netdev on Linux; no maintained upgrade path is currently available." }, +] + +[licenses] +allow = [ + "Apache-2.0", + "ISC", + "MIT", + "Unicode-3.0", + "Zlib", +] +confidence-threshold = 0.8 + +[bans] +multiple-versions = "warn" +wildcards = "deny" +highlight = "all" +workspace-default-features = "allow" +external-default-features = "allow" + +[sources] +unknown-registry = "deny" +unknown-git = "deny" +allow-registry = ["https://github.com/rust-lang/crates.io-index"] +allow-git = [] From b5d8806bcfa163cfd6de651268d774895fd9318f Mon Sep 17 00:00:00 2001 From: shellrow Date: Sat, 18 Jul 2026 23:21:27 +0900 Subject: [PATCH 14/33] fix: harden raw descriptor ownership and socket FFI safety --- TODO.md | 13 +++-- nex-datalink/src/bpf.rs | 26 +++++---- nex-datalink/src/linux.rs | 50 +++++++++++------ nex-sys/src/lib.rs | 113 ++++++++++++++++++++++++++++++++------ nex-sys/src/unix.rs | 63 ++++++++++++++++----- nex-sys/src/windows.rs | 23 ++++++++ 6 files changed, 221 insertions(+), 67 deletions(-) diff --git a/TODO.md b/TODO.md index 6aa7ff6..6cd6333 100644 --- a/TODO.md +++ b/TODO.md @@ -81,7 +81,7 @@ property tests (P1.5) confirm build→parse round-trips for valid inputs only. - [ ] `nex-core::bitfield` (`pub type u1 = u8 …`): make private or move behind a documented `#[doc(hidden)]` implementation-detail boundary — these aliases should not be part of the stable contract. -- [ ] Decide `nex-sys`'s status: it is a low-level internal crate — either mark it +- [x] Decide `nex-sys`'s status: it is a low-level internal crate — either mark it clearly "internal, no semver guarantees" in its docs or make its surface `pub(crate)`-equivalent via `#[doc(hidden)]`. - [ ] Normalize accessor naming: replace `get_*` (73 occurrences in `nex-packet`) @@ -166,15 +166,18 @@ MSRV, and supply chain across all three OSes. ### P0.7 Unsafe & OS-resource safety hardening -- [ ] Add a `# Safety` comment to every `unsafe` block and every `unsafe impl` in - `nex-datalink` (~141 sites) and `nex-sys`, justifying the invariant. +- [ ] Add a `# Safety` comment to every `unsafe` block and every `unsafe impl`: + - [x] Complete the `nex-sys` audit, including public `unsafe fn` contracts. + - [ ] Complete the `nex-datalink` audit (~141 sites). - [ ] Justify or remove the 9 `unsafe impl Send/Sync` in `nex-datalink` (`wpcap.rs`, `async_io/wpcap.rs`): document what makes the raw Npcap handles actually thread-safe, or wrap them so the impl is sound. - [ ] Ensure every OS handle (fd, BPF device, Npcap adapter, packet buffer) is owned by an RAII type that closes on *all* error paths. - - `nex-sys::FileDesc` already drops the fd — audit that every fd flows through it - and that no early-return leaks a half-opened resource. + - [x] Make `nex-sys::FileDesc` an explicit owned descriptor with a private raw + field, an unsafe ownership-transfer constructor, and a drop test. + - [ ] Audit that every fd flows through an owning wrapper and that no early-return + leaks a half-opened resource. - [ ] Prefer typed wrappers over raw integer/pointer handles at module boundaries. - [ ] Add error-path tests that open then fail to confirm no leak/double-close. - [ ] Run Miri on pure `nex-packet`/`nex-core` parsing/building where feasible. diff --git a/nex-datalink/src/bpf.rs b/nex-datalink/src/bpf.rs index dc38f02..60d2f67 100644 --- a/nex-datalink/src/bpf.rs +++ b/nex-datalink/src/bpf.rs @@ -202,7 +202,9 @@ pub fn channel(network_interface: &Interface, config: Config) -> io::Result io::Result io::Result io::Result Option> { let mut pollfd = libc::pollfd { - fd: self.socket.fd, + fd: self.socket.as_raw(), events: libc::POLLOUT, revents: 0, }; @@ -316,12 +323,17 @@ impl RawSender for RawSenderImpl { Some(Err(io::Error::new(io::ErrorKind::TimedOut, "Timed out"))) } else if pollfd.revents & libc::POLLOUT != 0 { // Socket is ready for writing - match nex_sys::send_to( - self.socket.fd, - packet, - (&self.send_addr as *const libc::sockaddr_ll) as *const _, - self.send_addr_len as libc::socklen_t, - ) { + let send_addr = (&self.send_addr as *const libc::sockaddr_ll).cast(); + // SAFETY: `send_addr` points to `self.send_addr`, which remains + // alive and has the supplied `sockaddr_ll` length. + match unsafe { + nex_sys::send_to( + self.socket.as_raw(), + packet, + send_addr, + self.send_addr_len as libc::socklen_t, + ) + } { Err(e) => Some(Err(e)), Ok(_) => Some(Ok(())), } @@ -344,7 +356,7 @@ impl RawReceiver for RawReceiverImpl { fn next(&mut self) -> io::Result<&[u8]> { let mut caddr: libc::sockaddr_storage = unsafe { mem::zeroed() }; let mut pollfd = libc::pollfd { - fd: self.socket.fd, + fd: self.socket.as_raw(), events: libc::POLLIN, revents: 0, }; @@ -367,7 +379,11 @@ impl RawReceiver for RawReceiverImpl { Err(io::Error::new(io::ErrorKind::TimedOut, "Timed out")) } else if pollfd.revents & libc::POLLIN != 0 { // Socket is ready for reading - let res = nex_sys::recv_from(self.socket.fd, &mut self.read_buffer, &mut caddr); + // SAFETY: `caddr` is writable `sockaddr_storage` for the duration + // of the call. + let res = unsafe { + nex_sys::recv_from(self.socket.as_raw(), &mut self.read_buffer, &mut caddr) + }; match res { Ok(len) => Ok(&self.read_buffer[0..len]), Err(e) => Err(e), diff --git a/nex-sys/src/lib.rs b/nex-sys/src/lib.rs index 882aee9..1ce6ac2 100644 --- a/nex-sys/src/lib.rs +++ b/nex-sys/src/lib.rs @@ -1,4 +1,11 @@ -//! Cross-platform system helpers and low-level wrappers used internally by the nex crates. +//! Cross-platform system helpers and low-level wrappers used by the nex crates. +//! +//! # Stability +//! +//! This crate is an implementation detail of the `nex` workspace. Its public +//! items exist so sibling crates can share platform bindings; they do not carry +//! the semver guarantees of the high-level `nex` API. Applications should use +//! `nex-core`, `nex-datalink`, `nex-packet`, or `nex-socket` instead. #[cfg(not(target_os = "windows"))] mod unix; @@ -10,37 +17,71 @@ mod windows; #[cfg(target_os = "windows")] pub use self::windows::*; -/// Any file descriptor on unix, only sockets on Windows. +/// An owned Unix file descriptor or Windows socket. pub struct FileDesc { - pub fd: CSocket, + fd: CSocket, +} + +impl FileDesc { + /// Takes ownership of a raw descriptor. + /// + /// # Safety + /// + /// `fd` must be a valid, open descriptor that the caller owns exclusively. + /// After calling this function, no other code may close `fd`, and the caller + /// must not construct another owning wrapper for it. + pub unsafe fn from_raw(fd: CSocket) -> Self { + Self { fd } + } + + /// Returns the wrapped descriptor without transferring ownership. + pub fn as_raw(&self) -> CSocket { + self.fd + } } impl Drop for FileDesc { fn drop(&mut self) { + // SAFETY: `from_raw` requires exclusive ownership, and `drop` runs once + // for this wrapper, so the descriptor is still owned and open here. unsafe { close(self.fd); } } } +#[cfg(not(target_os = "windows"))] +fn socket_buffer_len(len: usize) -> std::io::Result { + Ok(len) +} + +#[cfg(target_os = "windows")] +fn socket_buffer_len(len: usize) -> std::io::Result { + len.try_into().map_err(|_| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "buffer length exceeds the platform socket API limit", + ) + }) +} + /// Sends data to a socket, returning the number of bytes sent. -#[allow(clippy::not_unsafe_ptr_arg_deref)] -pub fn send_to( +/// +/// # Safety +/// +/// `dst` must point to a socket address that is valid for reads of `slen` +/// bytes for the duration of the call. +pub unsafe fn send_to( socket: CSocket, buffer: &[u8], dst: *const SockAddr, slen: SockLen, ) -> std::io::Result { - let send_len = retry(&mut || unsafe { - sendto( - socket, - buffer.as_ptr() as Buf, - buffer.len() as BufLen, - 0, - dst, - slen, - ) - }); + let buffer_len = socket_buffer_len(buffer.len())?; + // SAFETY: The slice supplies a readable buffer of `buffer_len` bytes. + // The caller guarantees that `dst` is readable for `slen` bytes. + let send_len = + retry(&mut || unsafe { sendto(socket, buffer.as_ptr() as Buf, buffer_len, 0, dst, slen) }); if send_len < 0 { Err(std::io::Error::last_os_error()) @@ -50,17 +91,25 @@ pub fn send_to( } /// Receives data from a socket, returning the number of bytes read. -pub fn recv_from( +/// +/// # Safety +/// +/// `caddr` must point to writable storage for a `SockAddrStorage` value and +/// remain valid for the duration of the call. +pub unsafe fn recv_from( socket: CSocket, buffer: &mut [u8], caddr: *mut SockAddrStorage, ) -> std::io::Result { + let buffer_len = socket_buffer_len(buffer.len())?; let mut caddrlen = std::mem::size_of::() as SockLen; + // SAFETY: The mutable slice supplies writable storage for `buffer_len` + // bytes, and the caller guarantees that `caddr` is valid writable storage. let len = retry(&mut || unsafe { recvfrom( socket, - buffer.as_ptr() as MutBuf, - buffer.len() as BufLen, + buffer.as_mut_ptr() as MutBuf, + buffer_len, 0, caddr as *mut SockAddr, &mut caddrlen, @@ -73,3 +122,31 @@ pub fn recv_from( Ok(len as usize) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(not(target_os = "windows"))] + #[test] + fn file_desc_closes_owned_descriptor_on_drop() { + let mut pipe_fds = [-1; 2]; + // SAFETY: `pipe_fds` provides writable storage for both descriptors. + assert_eq!(unsafe { libc::pipe(pipe_fds.as_mut_ptr()) }, 0); + + // SAFETY: The read descriptor is open and ownership is transferred + // exclusively to the wrapper. + let owned = unsafe { FileDesc::from_raw(pipe_fds[0]) }; + drop(owned); + + // SAFETY: `fcntl` only inspects the integer descriptor value. + assert_eq!(unsafe { libc::fcntl(pipe_fds[0], libc::F_GETFD) }, -1); + assert_eq!( + std::io::Error::last_os_error().raw_os_error(), + Some(libc::EBADF) + ); + + // SAFETY: The write descriptor remains open and owned by this test. + assert_eq!(unsafe { libc::close(pipe_fds[1]) }, 0); + } +} diff --git a/nex-sys/src/unix.rs b/nex-sys/src/unix.rs index e22464a..58c92af 100644 --- a/nex-sys/src/unix.rs +++ b/nex-sys/src/unix.rs @@ -36,6 +36,7 @@ pub use libc::{IFF_BROADCAST, IFF_LOOPBACK, IFF_MULTICAST, IFF_POINTOPOINT, IFF_ /// `sock` must be a valid descriptor owned by the caller. It must not be used /// again after this function returns. pub unsafe fn close(sock: CSocket) { + // SAFETY: The caller guarantees that `sock` is a valid owned descriptor. unsafe { let _ = libc::close(sock); } @@ -48,8 +49,15 @@ fn ntohs(u: u16) -> u16 { pub fn sockaddr_to_addr(storage: &SockAddrStorage, len: usize) -> io::Result { match storage.ss_family as libc::c_int { AF_INET => { - assert!(len >= mem::size_of::()); - let storage: &SockAddrIn = unsafe { mem::transmute(storage) }; + if len < mem::size_of::() { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "IPv4 socket address is truncated", + )); + } + // SAFETY: `SockAddrStorage` is large and aligned enough for + // `SockAddrIn`, and the reported length was checked above. + let storage = unsafe { &*(storage as *const SockAddrStorage as *const SockAddrIn) }; let ip = ipv4_addr_int(storage.sin_addr); // octets let o1 = (ip >> 24) as u8; @@ -61,19 +69,16 @@ pub fn sockaddr_to_addr(storage: &SockAddrStorage, len: usize) -> io::Result { - assert!(len >= mem::size_of::()); - let storage: &SockAddrIn6 = unsafe { mem::transmute(storage) }; - let arr: [u16; 8] = unsafe { mem::transmute(storage.sin6_addr.s6_addr) }; - // hextets - let h1 = ntohs(arr[0]); - let h2 = ntohs(arr[1]); - let h3 = ntohs(arr[2]); - let h4 = ntohs(arr[3]); - let h5 = ntohs(arr[4]); - let h6 = ntohs(arr[5]); - let h7 = ntohs(arr[6]); - let h8 = ntohs(arr[7]); - let ip = Ipv6Addr::new(h1, h2, h3, h4, h5, h6, h7, h8); + if len < mem::size_of::() { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "IPv6 socket address is truncated", + )); + } + // SAFETY: `SockAddrStorage` is large and aligned enough for + // `SockAddrIn6`, and the reported length was checked above. + let storage = unsafe { &*(storage as *const SockAddrStorage as *const SockAddrIn6) }; + let ip = Ipv6Addr::from(storage.sin6_addr.s6_addr); Ok(SocketAddr::V6(SocketAddrV6::new( ip, ntohs(storage.sin6_port), @@ -130,6 +135,8 @@ pub unsafe fn sendto( addr: *const SockAddr, addrlen: SockLen, ) -> CouldFail { + // SAFETY: The caller guarantees the buffer and address pointer contracts + // documented on this function. unsafe { libc::sendto(socket, buf, len, flags, addr, addrlen) } } @@ -147,6 +154,8 @@ pub unsafe fn recvfrom( addr: *mut SockAddr, addrlen: *mut SockLen, ) -> CouldFail { + // SAFETY: The caller guarantees the writable buffer and address pointer + // contracts documented on this function. unsafe { libc::recvfrom(socket, buf, len, flags, addr, addrlen) } } @@ -193,4 +202,28 @@ mod tests { }; assert_eq!(ipv4_addr_int(addr), 0x7f000001); } + + #[test] + fn sockaddr_to_addr_rejects_truncated_ipv4_storage() { + // SAFETY: An all-zero byte pattern is valid for `sockaddr_storage`. + let mut storage: SockAddrStorage = unsafe { mem::zeroed() }; + storage.ss_family = AF_INET as SockAddrFamily; + + let error = sockaddr_to_addr(&storage, mem::size_of::() - 1) + .expect_err("truncated IPv4 storage must be rejected"); + + assert_eq!(error.kind(), io::ErrorKind::InvalidData); + } + + #[test] + fn sockaddr_to_addr_rejects_truncated_ipv6_storage() { + // SAFETY: An all-zero byte pattern is valid for `sockaddr_storage`. + let mut storage: SockAddrStorage = unsafe { mem::zeroed() }; + storage.ss_family = AF_INET6 as SockAddrFamily; + + let error = sockaddr_to_addr(&storage, mem::size_of::() - 1) + .expect_err("truncated IPv6 storage must be rejected"); + + assert_eq!(error.kind(), io::ErrorKind::InvalidData); + } } diff --git a/nex-sys/src/windows.rs b/nex-sys/src/windows.rs index bd5540e..c6cbde0 100644 --- a/nex-sys/src/windows.rs +++ b/nex-sys/src/windows.rs @@ -22,12 +22,25 @@ pub type SockAddrFamily6 = ws::ADDRESS_FAMILY; pub type InAddr = ws::IN_ADDR; pub type In6Addr = ws::IN6_ADDR; +/// Close a raw Windows socket. +/// +/// # Safety +/// +/// `sock` must be a valid socket owned by the caller. It must not be used +/// again after this function returns. pub unsafe fn close(sock: CSocket) { + // SAFETY: The caller guarantees that `sock` is a valid owned socket. unsafe { let _ = ws::closesocket(sock); } } +/// Call WinSock `sendto` using raw socket arguments. +/// +/// # Safety +/// +/// `buf` must be valid for reads of `len` bytes. `to` must point to a valid +/// socket address of length `tolen`. pub unsafe fn sendto( socket: CSocket, buf: Buf, @@ -36,9 +49,17 @@ pub unsafe fn sendto( to: *const SockAddr, tolen: SockLen, ) -> CouldFail { + // SAFETY: The caller guarantees the buffer and address pointer contracts + // documented on this function. unsafe { ws::sendto(socket, buf as *const u8, len, flags, to, tolen) } } +/// Call WinSock `recvfrom` using raw socket arguments. +/// +/// # Safety +/// +/// `buf` must be valid for writes of `len` bytes. `addr` and `addrlen` must +/// point to writable storage for the returned socket address. pub unsafe fn recvfrom( socket: CSocket, buf: MutBuf, @@ -47,6 +68,8 @@ pub unsafe fn recvfrom( addr: *mut SockAddr, addrlen: *mut SockLen, ) -> CouldFail { + // SAFETY: The caller guarantees the writable buffer and address pointer + // contracts documented on this function. unsafe { ws::recvfrom(socket, buf as *mut u8, len, flags, addr, addrlen) } } From d091512148140304137a9b35cecd5d84bce36d7e Mon Sep 17 00:00:00 2001 From: shellrow Date: Sun, 19 Jul 2026 17:00:02 +0900 Subject: [PATCH 15/33] feat: stabilize the packet parsing API --- examples/arp.rs | 2 +- examples/async_datalink.rs | 4 +- examples/async_dump.rs | 34 +- examples/async_icmp_socket.rs | 4 +- examples/async_udp_socket.rs | 5 +- examples/dns_dump.rs | 21 +- examples/dump.rs | 32 +- examples/icmp_ping.rs | 2 +- examples/icmp_socket.rs | 13 +- examples/mutable_chaining.rs | 10 +- examples/ndp.rs | 2 +- examples/parse_frame.rs | 6 +- examples/tcp_ping.rs | 2 +- examples/udp_ping.rs | 2 +- examples/udp_socket.rs | 5 +- fuzz/fuzz_targets/frame_parse.rs | 3 +- fuzz/fuzz_targets/ipv4_parse.rs | 2 - fuzz/fuzz_targets/ipv6_parse.rs | 2 - fuzz/fuzz_targets/tcp_options.rs | 2 - nex-packet/benches/packet_parse.rs | 15 +- nex-packet/src/arp.rs | 161 +++++-- nex-packet/src/checksum.rs | 2 + nex-packet/src/dhcp.rs | 117 ++--- nex-packet/src/dns.rs | 227 +++++---- nex-packet/src/ethernet.rs | 69 ++- nex-packet/src/flowcontrol.rs | 36 +- nex-packet/src/gre.rs | 131 ++--- nex-packet/src/icmp.rs | 68 ++- nex-packet/src/icmpv6.rs | 746 ++++++++++++++++------------- nex-packet/src/ip.rs | 1 + nex-packet/src/ipv4.rs | 113 ++++- nex-packet/src/ipv6.rs | 100 +++- nex-packet/src/lib.rs | 1 + nex-packet/src/packet.rs | 37 +- nex-packet/src/parse.rs | 22 +- nex-packet/src/tcp.rs | 134 +++++- nex-packet/src/udp.rs | 51 +- nex-packet/src/util.rs | 4 + nex-packet/src/vlan.rs | 92 ++-- nex-packet/src/vxlan.rs | 73 +-- 40 files changed, 1490 insertions(+), 863 deletions(-) diff --git a/examples/arp.rs b/examples/arp.rs index 7e6af1f..e131aed 100644 --- a/examples/arp.rs +++ b/examples/arp.rs @@ -78,7 +78,7 @@ fn main() { loop { match rx.next() { Ok(packet) => { - let frame = Frame::from_buf(packet, ParseOption::default()).unwrap(); + let frame = Frame::try_from_buf(packet, ParseOption::default()).unwrap(); match &frame.datalink { Some(dlink) => { if let Some(arp) = &dlink.arp diff --git a/examples/async_datalink.rs b/examples/async_datalink.rs index cd9646e..b98250c 100644 --- a/examples/async_datalink.rs +++ b/examples/async_datalink.rs @@ -22,7 +22,7 @@ use nex_packet::{icmp, icmpv6}; use std::env; use std::net::IpAddr; -fn main() -> std::io::Result<()> { +fn main() -> Result<(), Box> { let interface = match env::args().nth(2) { Some(name) => nex::net::interface::get_interfaces() .into_iter() @@ -133,7 +133,7 @@ fn main() -> std::io::Result<()> { parse_option.from_ip_packet = true; parse_option.offset = if interface.is_loopback() { 14 } else { 0 }; } - let frame = Frame::from_buf(&packet, parse_option).unwrap(); + let frame = Frame::try_from_buf(&packet, parse_option).unwrap(); if let Some(ip_layer) = &frame.ip { if let Some(icmp) = &ip_layer.icmp diff --git a/examples/async_dump.rs b/examples/async_dump.rs index 4e3c769..9a74047 100644 --- a/examples/async_dump.rs +++ b/examples/async_dump.rs @@ -20,7 +20,7 @@ use nex_packet::ethernet::EthernetHeader; use nex_packet::{icmp, icmpv6}; use std::net::IpAddr; -fn main() -> std::io::Result<()> { +fn main() -> Result<(), Box> { // Choose the default interface. let interface = Interface::default().expect("no default interface"); let AsyncChannel::Ethernet(_tx, mut rx) = async_channel(&interface, Config::default())? else { @@ -45,7 +45,7 @@ fn main() -> std::io::Result<()> { let payload_offset: usize = if interface.is_loopback() { 14 } else { 0 }; let payload = Bytes::copy_from_slice(&packet[payload_offset..]); if packet.len() > payload_offset { - let version = Ipv4Packet::from_buf(&packet).unwrap().header.version; + let version = Ipv4Packet::try_from_buf(&packet).unwrap().header.version; let fake_eth = EthernetPacket { header: EthernetHeader { destination: MacAddr::zero(), @@ -61,7 +61,7 @@ fn main() -> std::io::Result<()> { handle_ethernet_frame(fake_eth); } } else { - handle_ethernet_frame(EthernetPacket::from_buf(&packet).unwrap()); + handle_ethernet_frame(EthernetPacket::try_from_buf(&packet).unwrap()); } } Ok::<(), std::io::Error>(()) @@ -91,8 +91,8 @@ fn handle_ethernet_frame(ethernet: EthernetPacket) { } fn handle_arp_packet(packet: Bytes) { - match ArpPacket::from_bytes(packet) { - Some(arp) => { + match ArpPacket::try_from_bytes(packet) { + Ok(arp) => { println!( "ARP packet: {}({}) > {}({}); operation: {:?}", arp.header.sender_hw_addr, @@ -109,8 +109,8 @@ fn handle_arp_packet(packet: Bytes) { } fn handle_ipv4_packet(packet: Bytes) { - match Ipv4Packet::from_bytes(packet) { - Some(ipv4) => { + match Ipv4Packet::try_from_bytes(packet) { + Ok(ipv4) => { handle_transport_protocol( IpAddr::V4(ipv4.header.source), IpAddr::V4(ipv4.header.destination), @@ -125,8 +125,8 @@ fn handle_ipv4_packet(packet: Bytes) { } fn handle_ipv6_packet(packet: Bytes) { - match Ipv6Packet::from_bytes(packet) { - Some(ipv6) => { + match Ipv6Packet::try_from_bytes(packet) { + Ok(ipv6) => { handle_transport_protocol( IpAddr::V6(ipv6.header.source), IpAddr::V6(ipv6.header.destination), @@ -166,8 +166,8 @@ fn handle_transport_protocol( } fn handle_tcp_packet(source: IpAddr, destination: IpAddr, packet: Bytes) { - match TcpPacket::from_bytes(packet) { - Some(tcp) => { + match TcpPacket::try_from_bytes(packet) { + Ok(tcp) => { println!( "TCP Packet: {}:{} > {}:{}; length: {}", source, @@ -184,9 +184,9 @@ fn handle_tcp_packet(source: IpAddr, destination: IpAddr, packet: Bytes) { } fn handle_udp_packet(source: IpAddr, destination: IpAddr, packet: Bytes) { - let udp = UdpPacket::from_bytes(packet); + let udp = UdpPacket::try_from_bytes(packet); - if let Some(udp) = udp { + if let Ok(udp) = udp { println!( "UDP Packet: {}:{} > {}:{}; length: {}", source, @@ -201,8 +201,8 @@ fn handle_udp_packet(source: IpAddr, destination: IpAddr, packet: Bytes) { } fn handle_icmp_packet(source: IpAddr, destination: IpAddr, packet: Bytes) { - let icmp_packet = IcmpPacket::from_bytes(packet); - if let Some(icmp_packet) = icmp_packet { + let icmp_packet = IcmpPacket::try_from_bytes(packet); + if let Ok(icmp_packet) = icmp_packet { let total_len = icmp_packet.total_len(); match icmp_packet.header.icmp_type { IcmpType::EchoRequest => { @@ -265,8 +265,8 @@ fn handle_icmp_packet(source: IpAddr, destination: IpAddr, packet: Bytes) { } fn handle_icmpv6_packet(source: IpAddr, destination: IpAddr, packet: Bytes) { - let icmpv6_packet = Icmpv6Packet::from_bytes(packet); - if let Some(icmpv6_packet) = icmpv6_packet { + let icmpv6_packet = Icmpv6Packet::try_from_bytes(packet); + if let Ok(icmpv6_packet) = icmpv6_packet { match icmpv6_packet.header.icmpv6_type { nex::packet::icmpv6::Icmpv6Type::EchoRequest => { let echo_request_packet = diff --git a/examples/async_icmp_socket.rs b/examples/async_icmp_socket.rs index eb71182..36d0ef7 100644 --- a/examples/async_icmp_socket.rs +++ b/examples/async_icmp_socket.rs @@ -54,10 +54,10 @@ async fn main() -> std::io::Result<()> { loop { if let Ok((n, from)) = socket_clone.recv_from(&mut buf).await { println!("Received {} bytes from {}", n, from.ip()); - if let Some(ipv4_packet) = Ipv4Packet::from_buf(&buf[..n]) + if let Ok(ipv4_packet) = Ipv4Packet::try_from_buf(&buf[..n]) && ipv4_packet.header.next_level_protocol == nex_packet::ip::IpNextProtocol::Icmp - && let Some(icmp_packet) = IcmpPacket::from_bytes(ipv4_packet.payload()) + && let Ok(icmp_packet) = IcmpPacket::try_from_bytes(ipv4_packet.payload()) { println!( "\t{:?} from: {:?} to {:?}, TTL: {}", diff --git a/examples/async_udp_socket.rs b/examples/async_udp_socket.rs index cd861d4..02653d2 100644 --- a/examples/async_udp_socket.rs +++ b/examples/async_udp_socket.rs @@ -9,10 +9,7 @@ use tokio::task; #[tokio::main] async fn main() -> std::io::Result<()> { - let server_cfg = UdpConfig { - bind_addr: Some("127.0.0.1:0".parse().unwrap()), - ..Default::default() - }; + let server_cfg = UdpConfig::default().with_bind_addr("127.0.0.1:0".parse().unwrap()); let server = AsyncUdpSocket::from_config(&server_cfg)?; let server_addr = server.local_addr()?; diff --git a/examples/dns_dump.rs b/examples/dns_dump.rs index cb9366e..a9d0afe 100644 --- a/examples/dns_dump.rs +++ b/examples/dns_dump.rs @@ -15,7 +15,6 @@ use nex::packet::ipv6::Ipv6Packet; use nex::packet::udp::UdpPacket; use nex_core::mac::MacAddr; use nex_packet::ethernet::EthernetHeader; -use nex_packet::packet::Packet; use std::env; use std::net::IpAddr; @@ -55,7 +54,7 @@ fn main() { { let offset = if interface.is_loopback() { 14 } else { 0 }; let payload = Bytes::copy_from_slice(&packet[offset..]); - let version = Ipv4Packet::from_buf(packet).unwrap().header.version; + let version = Ipv4Packet::try_from_buf(packet).unwrap().header.version; EthernetPacket { header: EthernetHeader { destination: MacAddr::zero(), @@ -69,11 +68,11 @@ fn main() { payload, } } else { - EthernetPacket::from_buf(packet).unwrap() + EthernetPacket::try_from_buf(packet).unwrap() }; if let EtherType::Ipv4 = eth_packet.header.ethertype { - if let Some(ipv4) = Ipv4Packet::from_bytes(eth_packet.payload.clone()) { + if let Ok(ipv4) = Ipv4Packet::try_from_bytes(eth_packet.payload.clone()) { handle_udp( ipv4.payload, IpAddr::V4(ipv4.header.source), @@ -81,7 +80,7 @@ fn main() { ); } } else if let EtherType::Ipv6 = eth_packet.header.ethertype - && let Some(ipv6) = Ipv6Packet::from_bytes(eth_packet.payload.clone()) + && let Ok(ipv6) = Ipv6Packet::try_from_bytes(eth_packet.payload.clone()) { handle_udp( ipv6.payload, @@ -96,9 +95,9 @@ fn main() { } fn handle_udp(packet: Bytes, src: IpAddr, dst: IpAddr) { - if let Some(udp) = UdpPacket::from_bytes(packet.clone()) + if let Ok(udp) = UdpPacket::try_from_bytes(packet.clone()) && !udp.payload.is_empty() - && let Some(dns) = DnsPacket::from_bytes(udp.payload.clone()) + && let Ok(dns) = DnsPacket::try_from_bytes(udp.payload.clone()) { println!( "DNS Packet: {}:{} > {}:{}", @@ -108,7 +107,7 @@ fn handle_udp(packet: Bytes, src: IpAddr, dst: IpAddr) { for query in &dns.queries { println!( " Query: {:?} (type: {:?}, class: {:?})", - query.get_qname_parsed(), + query.qname_parsed(), query.qtype, query.qclass ); @@ -117,7 +116,7 @@ fn handle_udp(packet: Bytes, src: IpAddr, dst: IpAddr) { for response in &dns.responses { match response.rtype { DnsType::A | DnsType::AAAA => { - if let Some(ip) = response.get_ip() { + if let Some(ip) = response.ip() { println!( " Response: {} (type: {:?}, ttl: {})", ip, response.rtype, response.ttl @@ -127,7 +126,7 @@ fn handle_udp(packet: Bytes, src: IpAddr, dst: IpAddr) { } } DnsType::CNAME | DnsType::NS | DnsType::PTR => { - if let Some(name) = response.get_name() { + if let Some(name) = response.dns_name() { println!( " Response: {} (type: {:?}, ttl: {})", name, response.rtype, response.ttl @@ -137,7 +136,7 @@ fn handle_udp(packet: Bytes, src: IpAddr, dst: IpAddr) { } } DnsType::TXT => { - if let Some(txts) = response.get_txt_strings() { + if let Some(txts) = response.txt_strings() { for txt in txts { println!(" TXT: \"{}\" (ttl: {})", txt, response.ttl); } diff --git a/examples/dump.rs b/examples/dump.rs index 19ef778..6aaf845 100644 --- a/examples/dump.rs +++ b/examples/dump.rs @@ -69,7 +69,7 @@ fn main() { let payload_offset: usize = if interface.is_loopback() { 14 } else { 0 }; let payload = Bytes::copy_from_slice(&packet[payload_offset..]); if packet.len() > payload_offset { - let version = Ipv4Packet::from_buf(packet).unwrap().header.version; + let version = Ipv4Packet::try_from_buf(packet).unwrap().header.version; let fake_eth = EthernetPacket { header: EthernetHeader { destination: MacAddr::zero(), @@ -85,7 +85,7 @@ fn main() { handle_ethernet_frame(fake_eth); } } else { - handle_ethernet_frame(EthernetPacket::from_buf(packet).unwrap()); + handle_ethernet_frame(EthernetPacket::try_from_buf(packet).unwrap()); } } Err(e) => panic!("dump: unable to receive packet: {}", e), @@ -114,8 +114,8 @@ fn handle_ethernet_frame(ethernet: EthernetPacket) { } fn handle_arp_packet(packet: Bytes) { - match ArpPacket::from_bytes(packet) { - Some(arp) => { + match ArpPacket::try_from_bytes(packet) { + Ok(arp) => { println!( "ARP packet: {}({}) > {}({}); operation: {:?}", arp.header.sender_hw_addr, @@ -132,8 +132,8 @@ fn handle_arp_packet(packet: Bytes) { } fn handle_ipv4_packet(packet: Bytes) { - match Ipv4Packet::from_bytes(packet) { - Some(ipv4) => { + match Ipv4Packet::try_from_bytes(packet) { + Ok(ipv4) => { handle_transport_protocol( IpAddr::V4(ipv4.header.source), IpAddr::V4(ipv4.header.destination), @@ -148,8 +148,8 @@ fn handle_ipv4_packet(packet: Bytes) { } fn handle_ipv6_packet(packet: Bytes) { - match Ipv6Packet::from_bytes(packet) { - Some(ipv6) => { + match Ipv6Packet::try_from_bytes(packet) { + Ok(ipv6) => { handle_transport_protocol( IpAddr::V6(ipv6.header.source), IpAddr::V6(ipv6.header.destination), @@ -189,8 +189,8 @@ fn handle_transport_protocol( } fn handle_tcp_packet(source: IpAddr, destination: IpAddr, packet: Bytes) { - match TcpPacket::from_bytes(packet) { - Some(tcp) => { + match TcpPacket::try_from_bytes(packet) { + Ok(tcp) => { println!( "TCP Packet: {}:{} > {}:{}; length: {}", source, @@ -207,9 +207,9 @@ fn handle_tcp_packet(source: IpAddr, destination: IpAddr, packet: Bytes) { } fn handle_udp_packet(source: IpAddr, destination: IpAddr, packet: Bytes) { - let udp = UdpPacket::from_bytes(packet); + let udp = UdpPacket::try_from_bytes(packet); - if let Some(udp) = udp { + if let Ok(udp) = udp { println!( "UDP Packet: {}:{} > {}:{}; length: {}", source, @@ -224,8 +224,8 @@ fn handle_udp_packet(source: IpAddr, destination: IpAddr, packet: Bytes) { } fn handle_icmp_packet(source: IpAddr, destination: IpAddr, packet: Bytes) { - let icmp_packet = IcmpPacket::from_bytes(packet); - if let Some(icmp_packet) = icmp_packet { + let icmp_packet = IcmpPacket::try_from_bytes(packet); + if let Ok(icmp_packet) = icmp_packet { let total_len = icmp_packet.total_len(); match icmp_packet.header.icmp_type { IcmpType::EchoRequest => { @@ -288,8 +288,8 @@ fn handle_icmp_packet(source: IpAddr, destination: IpAddr, packet: Bytes) { } fn handle_icmpv6_packet(source: IpAddr, destination: IpAddr, packet: Bytes) { - let icmpv6_packet = Icmpv6Packet::from_bytes(packet); - if let Some(icmpv6_packet) = icmpv6_packet { + let icmpv6_packet = Icmpv6Packet::try_from_bytes(packet); + if let Ok(icmpv6_packet) = icmpv6_packet { match icmpv6_packet.header.icmpv6_type { nex::packet::icmpv6::Icmpv6Type::EchoRequest => { let echo_request_packet = diff --git a/examples/icmp_ping.rs b/examples/icmp_ping.rs index 6f42f01..bd6e682 100644 --- a/examples/icmp_ping.rs +++ b/examples/icmp_ping.rs @@ -139,7 +139,7 @@ fn main() { parse_option.from_ip_packet = true; parse_option.offset = if interface.is_loopback() { 14 } else { 0 }; } - let frame = Frame::from_buf(packet, parse_option).unwrap(); + let frame = Frame::try_from_buf(packet, parse_option).unwrap(); if let Some(ip_layer) = &frame.ip { if let Some(icmp) = &ip_layer.icmp diff --git a/examples/icmp_socket.rs b/examples/icmp_socket.rs index afff341..1586b61 100644 --- a/examples/icmp_socket.rs +++ b/examples/icmp_socket.rs @@ -78,9 +78,9 @@ fn main() -> std::io::Result<()> { match kind { IcmpKind::V4 => { // Parse IPv4 + ICMP - if let Some(ipv4_packet) = Ipv4Packet::from_buf(packet) + if let Ok(ipv4_packet) = Ipv4Packet::try_from_buf(packet) && ipv4_packet.header.next_level_protocol == nex_packet::ip::IpNextProtocol::Icmp - && let Some(icmp_packet) = IcmpPacket::from_bytes(ipv4_packet.payload()) + && let Ok(icmp_packet) = IcmpPacket::try_from_bytes(ipv4_packet.payload()) { println!( "\t{:?} from: {:?} to {:?}, TTL: {}", @@ -102,22 +102,23 @@ fn main() -> std::io::Result<()> { IcmpKind::V6 => { // Parse ICMPv6 // The IPv6 header is automatically cropped off when recvfrom() is used. - if let Some(icmpv6_packet) = Icmpv6Packet::from_buf(packet) { + if let Ok(icmpv6_packet) = Icmpv6Packet::try_from_buf(packet) { println!( "\t{:?} from: {:?}", icmpv6_packet.header.icmpv6_type, from.ip() ); - match icmpv6::echo_reply::EchoReplyPacket::from_buf(packet) { - Some(reply) => { + match icmpv6::echo_reply::EchoReplyPacket::try_from_buf(packet) { + Ok(reply) => { println!("\tID: {}, Seq: {}", reply.identifier, reply.sequence_number); } - None => { + Err(_) => { println!("\tReceived non-echo-reply ICMPv6 packet"); } } } } + _ => {} } Ok(()) } diff --git a/examples/mutable_chaining.rs b/examples/mutable_chaining.rs index 3fb826e..d781108 100644 --- a/examples/mutable_chaining.rs +++ b/examples/mutable_chaining.rs @@ -6,7 +6,7 @@ use nex::packet::ethernet::{ }; use nex::packet::ip::IpNextProtocol; use nex::packet::ipv4::{self, IPV4_HEADER_LEN, Ipv4Packet, MutableIpv4Packet}; -use nex::packet::packet::{MutablePacket, Packet}; +use nex::packet::packet::MutablePacket; use nex::packet::udp::{self, MutableUdpPacket, UDP_HEADER_LEN, UdpPacket}; use std::net::Ipv4Addr; @@ -50,7 +50,7 @@ fn main() { } let snapshot = ipv4.freeze().expect("snapshot ipv4"); - let udp_snapshot = UdpPacket::from_buf(&snapshot.payload).expect("snapshot udp"); + let udp_snapshot = UdpPacket::try_from_buf(&snapshot.payload).expect("snapshot udp"); let udp_checksum = udp::ipv4_checksum( &udp_snapshot, &snapshot.header.source, @@ -65,9 +65,9 @@ fn main() { } // Inspect immutable packet views to confirm changes persisted across layers. - let ethernet_packet = EthernetPacket::from_buf(&frame).expect("immutable ethernet"); - let ipv4_packet = Ipv4Packet::from_buf(ðernet_packet.payload).expect("immutable ipv4"); - let udp_packet = UdpPacket::from_buf(&ipv4_packet.payload).expect("immutable udp"); + let ethernet_packet = EthernetPacket::try_from_buf(&frame).expect("immutable ethernet"); + let ipv4_packet = Ipv4Packet::try_from_buf(ðernet_packet.payload).expect("immutable ipv4"); + let udp_packet = UdpPacket::try_from_buf(&ipv4_packet.payload).expect("immutable udp"); println!( "Ethernet: {} -> {} ({:?})", diff --git a/examples/ndp.rs b/examples/ndp.rs index c41bb57..8a3d8f2 100644 --- a/examples/ndp.rs +++ b/examples/ndp.rs @@ -112,7 +112,7 @@ fn main() { parse_option.offset = if interface.is_loopback() { 14 } else { 0 }; } - if let Some(frame) = Frame::from_buf(packet, parse_option) + if let Ok(frame) = Frame::try_from_buf(packet, parse_option) && let Some(ip_layer) = &frame.ip && let Some(icmpv6) = &ip_layer.icmpv6 && icmpv6.icmpv6_type == Icmpv6Type::NeighborAdvertisement diff --git a/examples/parse_frame.rs b/examples/parse_frame.rs index fd6aa9a..03e3d06 100644 --- a/examples/parse_frame.rs +++ b/examples/parse_frame.rs @@ -59,11 +59,11 @@ fn main() { parse_option.from_ip_packet = true; parse_option.offset = payload_offset; } - match Frame::from_buf(packet, parse_option) { - Some(frame) => { + match Frame::try_from_buf(packet, parse_option) { + Ok(frame) => { display_frame(&frame); } - None => { + Err(_) => { println!("Failed to parse packet as Frame"); } } diff --git a/examples/tcp_ping.rs b/examples/tcp_ping.rs index 3db4180..a47f9df 100644 --- a/examples/tcp_ping.rs +++ b/examples/tcp_ping.rs @@ -204,7 +204,7 @@ fn main() { parse_option.from_ip_packet = true; parse_option.offset = payload_offset; } - let frame: Frame = Frame::from_buf(packet, parse_option).unwrap(); + let frame: Frame = Frame::try_from_buf(packet, parse_option).unwrap(); // Check each layer. If the packet is TCP SYN+ACK or RST+ACK, print it out if let Some(ip_layer) = &frame.ip && let Some(transport_layer) = &frame.transport diff --git a/examples/udp_ping.rs b/examples/udp_ping.rs index 0af5331..b3ffb03 100644 --- a/examples/udp_ping.rs +++ b/examples/udp_ping.rs @@ -127,7 +127,7 @@ fn main() { parse_option.from_ip_packet = true; parse_option.offset = if interface.is_loopback() { 14 } else { 0 }; } - let frame = Frame::from_buf(packet, parse_option).unwrap(); + let frame = Frame::try_from_buf(packet, parse_option).unwrap(); if let Some(ip_layer) = &frame.ip { if let Some(icmp) = &ip_layer.icmp diff --git a/examples/udp_socket.rs b/examples/udp_socket.rs index 07d9b80..1c3be87 100644 --- a/examples/udp_socket.rs +++ b/examples/udp_socket.rs @@ -6,10 +6,7 @@ use nex_socket::udp::{UdpConfig, UdpSocket}; use std::thread; fn main() -> std::io::Result<()> { - let server_cfg = UdpConfig { - bind_addr: Some("127.0.0.1:0".parse().unwrap()), - ..Default::default() - }; + let server_cfg = UdpConfig::default().with_bind_addr("127.0.0.1:0".parse().unwrap()); let server = UdpSocket::from_config(&server_cfg)?; let server_addr = server.local_addr()?; diff --git a/fuzz/fuzz_targets/frame_parse.rs b/fuzz/fuzz_targets/frame_parse.rs index 0a451c9..58f4b6a 100644 --- a/fuzz/fuzz_targets/frame_parse.rs +++ b/fuzz/fuzz_targets/frame_parse.rs @@ -5,8 +5,7 @@ use nex_packet::frame::{Frame, FrameView, ParseOption}; use nex_packet::parse::ParseMode; fuzz_target!(|data: &[u8]| { - let _ = Frame::from_buf(data, ParseOption::default()); let _ = Frame::try_from_buf(data, ParseOption::default()); let _ = Frame::try_from_buf_with_mode(data, ParseOption::default(), ParseMode::Strict); - let _ = FrameView::from_buf(data, ParseOption::default()); + let _ = FrameView::try_from_buf(data, ParseOption::default()); }); diff --git a/fuzz/fuzz_targets/ipv4_parse.rs b/fuzz/fuzz_targets/ipv4_parse.rs index 85e4158..7c912af 100644 --- a/fuzz/fuzz_targets/ipv4_parse.rs +++ b/fuzz/fuzz_targets/ipv4_parse.rs @@ -2,11 +2,9 @@ use libfuzzer_sys::fuzz_target; use nex_packet::ipv4::Ipv4Packet; -use nex_packet::packet::Packet; use nex_packet::parse::ParseMode; fuzz_target!(|data: &[u8]| { - let _ = Ipv4Packet::from_buf(data); let _ = Ipv4Packet::try_from_buf(data); let _ = Ipv4Packet::try_from_buf_with_mode(data, ParseMode::Strict); }); diff --git a/fuzz/fuzz_targets/ipv6_parse.rs b/fuzz/fuzz_targets/ipv6_parse.rs index 6631137..3acb9fd 100644 --- a/fuzz/fuzz_targets/ipv6_parse.rs +++ b/fuzz/fuzz_targets/ipv6_parse.rs @@ -2,11 +2,9 @@ use libfuzzer_sys::fuzz_target; use nex_packet::ipv6::Ipv6Packet; -use nex_packet::packet::Packet; use nex_packet::parse::ParseMode; fuzz_target!(|data: &[u8]| { - let _ = Ipv6Packet::from_buf(data); let _ = Ipv6Packet::try_from_buf(data); let _ = Ipv6Packet::try_from_buf_with_mode(data, ParseMode::Strict); }); diff --git a/fuzz/fuzz_targets/tcp_options.rs b/fuzz/fuzz_targets/tcp_options.rs index 28b0a40..d8b4f65 100644 --- a/fuzz/fuzz_targets/tcp_options.rs +++ b/fuzz/fuzz_targets/tcp_options.rs @@ -2,11 +2,9 @@ use bytes::Bytes; use libfuzzer_sys::fuzz_target; -use nex_packet::packet::Packet; use nex_packet::tcp::TcpPacket; fuzz_target!(|data: &[u8]| { - let _ = TcpPacket::from_buf(data); let _ = TcpPacket::try_from_buf(data); let _ = TcpPacket::try_from_bytes(Bytes::copy_from_slice(data)); }); diff --git a/nex-packet/benches/packet_parse.rs b/nex-packet/benches/packet_parse.rs index 6a990b5..4b29489 100644 --- a/nex-packet/benches/packet_parse.rs +++ b/nex-packet/benches/packet_parse.rs @@ -2,7 +2,6 @@ use bytes::Bytes; use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; use nex_packet::{ frame::{Frame, FrameView, ParseOption}, - packet::Packet, tcp::TcpPacket, udp::UdpPacket, }; @@ -32,32 +31,32 @@ fn bench_packet_parse(c: &mut Criterion) { let udp_datagram = ipv6_udp.slice(14 + 40..); group.bench_function("frame_from_buf_ipv4_tcp", |b| { - b.iter(|| Frame::from_buf(&ipv4_tcp, ParseOption::default())) + b.iter(|| Frame::try_from_buf(&ipv4_tcp, ParseOption::default())) }); group.bench_function("frame_try_from_bytes_ipv4_tcp", |b| { b.iter(|| Frame::try_from_bytes(ipv4_tcp.clone(), ParseOption::default())) }); group.bench_function("frame_view_from_buf_ipv4_tcp", |b| { - b.iter(|| FrameView::from_buf(&ipv4_tcp, ParseOption::default())) + b.iter(|| FrameView::try_from_buf(&ipv4_tcp, ParseOption::default())) }); group.bench_function("tcp_from_buf", |b| { - b.iter(|| TcpPacket::from_buf(&tcp_segment)) + b.iter(|| TcpPacket::try_from_buf(&tcp_segment)) }); group.bench_function("tcp_from_bytes", |b| { - b.iter(|| TcpPacket::from_bytes(tcp_segment.clone())) + b.iter(|| TcpPacket::try_from_bytes(tcp_segment.clone())) }); group.bench_function("udp_from_buf", |b| { - b.iter(|| UdpPacket::from_buf(&udp_datagram)) + b.iter(|| UdpPacket::try_from_buf(&udp_datagram)) }); group.bench_function("udp_from_bytes", |b| { - b.iter(|| UdpPacket::from_bytes(udp_datagram.clone())) + b.iter(|| UdpPacket::try_from_bytes(udp_datagram.clone())) }); for (name, packet) in [("ipv4_tcp", ipv4_tcp), ("ipv6_udp", ipv6_udp)] { group.bench_with_input( BenchmarkId::new("frame_view", name), &packet, - |b, packet| b.iter(|| FrameView::from_buf(packet, ParseOption::default())), + |b, packet| b.iter(|| FrameView::try_from_buf(packet, ParseOption::default())), ); } diff --git a/nex-packet/src/arp.rs b/nex-packet/src/arp.rs index c0a7fad..31b5840 100644 --- a/nex-packet/src/arp.rs +++ b/nex-packet/src/arp.rs @@ -22,6 +22,7 @@ pub const ARP_PACKET_LEN: usize = ETHERNET_HEADER_LEN + ARP_HEADER_LEN; #[repr(u16)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[non_exhaustive] pub enum ArpOperation { /// ARP request Request = 1, @@ -87,6 +88,7 @@ impl ArpOperation { #[repr(u16)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[non_exhaustive] pub enum ArpHardwareType { /// Ethernet (10Mb) Ethernet = 1, @@ -335,42 +337,49 @@ pub struct ArpPacket { impl Packet for ArpPacket { type Header = ArpHeader; - fn from_buf(bytes: &[u8]) -> Option { - if bytes.len() < ARP_HEADER_LEN { - return None; - } - let hardware_type = ArpHardwareType::new(u16::from_be_bytes([bytes[0], bytes[1]])); - let protocol_type = EtherType::new(u16::from_be_bytes([bytes[2], bytes[3]])); - let hw_addr_len = bytes[4]; - let proto_addr_len = bytes[5]; - let operation = ArpOperation::new(u16::from_be_bytes([bytes[6], bytes[7]])); - let sender_hw_addr = MacAddr::from_octets([ - bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], - ]); - let sender_proto_addr = Ipv4Addr::new(bytes[14], bytes[15], bytes[16], bytes[17]); - let target_hw_addr = MacAddr::from_octets([ - bytes[18], bytes[19], bytes[20], bytes[21], bytes[22], bytes[23], - ]); - let target_proto_addr = Ipv4Addr::new(bytes[24], bytes[25], bytes[26], bytes[27]); - let payload = Bytes::copy_from_slice(&bytes[ARP_HEADER_LEN..]); - - Some(ArpPacket { - header: ArpHeader { - hardware_type, - protocol_type, - hw_addr_len, - proto_addr_len, - operation, - sender_hw_addr, - sender_proto_addr, - target_hw_addr, - target_proto_addr, - }, - payload, + fn try_from_buf(bytes: &[u8]) -> Result { + (|| -> Option { + if bytes.len() < ARP_HEADER_LEN { + return None; + } + let hardware_type = ArpHardwareType::new(u16::from_be_bytes([bytes[0], bytes[1]])); + let protocol_type = EtherType::new(u16::from_be_bytes([bytes[2], bytes[3]])); + let hw_addr_len = bytes[4]; + let proto_addr_len = bytes[5]; + let operation = ArpOperation::new(u16::from_be_bytes([bytes[6], bytes[7]])); + let sender_hw_addr = MacAddr::from_octets([ + bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], + ]); + let sender_proto_addr = Ipv4Addr::new(bytes[14], bytes[15], bytes[16], bytes[17]); + let target_hw_addr = MacAddr::from_octets([ + bytes[18], bytes[19], bytes[20], bytes[21], bytes[22], bytes[23], + ]); + let target_proto_addr = Ipv4Addr::new(bytes[24], bytes[25], bytes[26], bytes[27]); + let payload = Bytes::copy_from_slice(&bytes[ARP_HEADER_LEN..]); + + Some(ArpPacket { + header: ArpHeader { + hardware_type, + protocol_type, + hw_addr_len, + proto_addr_len, + operation, + sender_hw_addr, + sender_proto_addr, + target_hw_addr, + target_proto_addr, + }, + payload, + }) + })() + .ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), }) } - fn from_bytes(bytes: Bytes) -> Option { - Self::from_buf(&bytes) + fn try_from_bytes(bytes: Bytes) -> Result { + Self::from_buf(&bytes).ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), + }) } fn to_bytes(&self) -> Bytes { @@ -497,6 +506,11 @@ impl<'a> MutablePacket<'a> for MutableArpPacket<'a> { impl<'a> MutableArpPacket<'a> { /// Create a packet without performing length checks. + /// + /// # Safety + /// + /// `buffer` must contain at least the 28-byte Ethernet/IPv4 ARP packet + /// header before any field accessor is called. Prefer [`MutablePacket::new`]. pub fn new_unchecked(buffer: &'a mut [u8]) -> Self { Self { buffer } } @@ -509,56 +523,86 @@ impl<'a> MutableArpPacket<'a> { &mut *self.buffer } - pub fn get_hardware_type(&self) -> ArpHardwareType { + pub fn hardware_type(&self) -> ArpHardwareType { ArpHardwareType::new(u16::from_be_bytes([self.raw()[0], self.raw()[1]])) } + /// Deprecated compatibility alias for hardware_type. + #[deprecated(note = "use hardware_type")] + pub fn get_hardware_type(&self) -> ArpHardwareType { + self.hardware_type() + } pub fn set_hardware_type(&mut self, ty: ArpHardwareType) { self.raw_mut()[0..2].copy_from_slice(&ty.value().to_be_bytes()); } - pub fn get_protocol_type(&self) -> EtherType { + pub fn protocol_type(&self) -> EtherType { EtherType::new(u16::from_be_bytes([self.raw()[2], self.raw()[3]])) } + /// Deprecated compatibility alias for protocol_type. + #[deprecated(note = "use protocol_type")] + pub fn get_protocol_type(&self) -> EtherType { + self.protocol_type() + } pub fn set_protocol_type(&mut self, ty: EtherType) { self.raw_mut()[2..4].copy_from_slice(&ty.value().to_be_bytes()); } - pub fn get_hw_addr_len(&self) -> u8 { + pub fn hw_addr_len(&self) -> u8 { self.raw()[4] } + /// Deprecated compatibility alias for hw_addr_len. + #[deprecated(note = "use hw_addr_len")] + pub fn get_hw_addr_len(&self) -> u8 { + self.hw_addr_len() + } pub fn set_hw_addr_len(&mut self, len: u8) { self.raw_mut()[4] = len; } - pub fn get_proto_addr_len(&self) -> u8 { + pub fn proto_addr_len(&self) -> u8 { self.raw()[5] } + /// Deprecated compatibility alias for proto_addr_len. + #[deprecated(note = "use proto_addr_len")] + pub fn get_proto_addr_len(&self) -> u8 { + self.proto_addr_len() + } pub fn set_proto_addr_len(&mut self, len: u8) { self.raw_mut()[5] = len; } - pub fn get_operation(&self) -> ArpOperation { + pub fn operation(&self) -> ArpOperation { ArpOperation::new(u16::from_be_bytes([self.raw()[6], self.raw()[7]])) } + /// Deprecated compatibility alias for operation. + #[deprecated(note = "use operation")] + pub fn get_operation(&self) -> ArpOperation { + self.operation() + } pub fn set_operation(&mut self, op: ArpOperation) { self.raw_mut()[6..8].copy_from_slice(&op.value().to_be_bytes()); } - pub fn get_sender_hw_addr(&self) -> MacAddr { + pub fn sender_hw_addr(&self) -> MacAddr { let raw = self.raw(); MacAddr::from_octets([raw[8], raw[9], raw[10], raw[11], raw[12], raw[13]]) } + /// Deprecated compatibility alias for sender_hw_addr. + #[deprecated(note = "use sender_hw_addr")] + pub fn get_sender_hw_addr(&self) -> MacAddr { + self.sender_hw_addr() + } pub fn set_sender_hw_addr(&mut self, addr: MacAddr) { self.raw_mut()[8..14].copy_from_slice(&addr.octets()); } - pub fn get_sender_proto_addr(&self) -> Ipv4Addr { + pub fn sender_proto_addr(&self) -> Ipv4Addr { Ipv4Addr::new( self.raw()[14], self.raw()[15], @@ -566,21 +610,31 @@ impl<'a> MutableArpPacket<'a> { self.raw()[17], ) } + /// Deprecated compatibility alias for sender_proto_addr. + #[deprecated(note = "use sender_proto_addr")] + pub fn get_sender_proto_addr(&self) -> Ipv4Addr { + self.sender_proto_addr() + } pub fn set_sender_proto_addr(&mut self, addr: Ipv4Addr) { self.raw_mut()[14..18].copy_from_slice(&addr.octets()); } - pub fn get_target_hw_addr(&self) -> MacAddr { + pub fn target_hw_addr(&self) -> MacAddr { let raw = self.raw(); MacAddr::from_octets([raw[18], raw[19], raw[20], raw[21], raw[22], raw[23]]) } + /// Deprecated compatibility alias for target_hw_addr. + #[deprecated(note = "use target_hw_addr")] + pub fn get_target_hw_addr(&self) -> MacAddr { + self.target_hw_addr() + } pub fn set_target_hw_addr(&mut self, addr: MacAddr) { self.raw_mut()[18..24].copy_from_slice(&addr.octets()); } - pub fn get_target_proto_addr(&self) -> Ipv4Addr { + pub fn target_proto_addr(&self) -> Ipv4Addr { Ipv4Addr::new( self.raw()[24], self.raw()[25], @@ -588,6 +642,11 @@ impl<'a> MutableArpPacket<'a> { self.raw()[27], ) } + /// Deprecated compatibility alias for target_proto_addr. + #[deprecated(note = "use target_proto_addr")] + pub fn get_target_proto_addr(&self) -> Ipv4Addr { + self.target_proto_addr() + } pub fn set_target_proto_addr(&mut self, addr: Ipv4Addr) { self.raw_mut()[24..28].copy_from_slice(&addr.octets()); @@ -680,14 +739,14 @@ mod tests { ]; let packet = ArpPacket::from_bytes(Bytes::copy_from_slice(&raw)).unwrap(); - match packet.header.hardware_type { - ArpHardwareType::Unknown(v) => assert_eq!(v, 0x9999), - _ => panic!("Expected unknown hardware type"), - } - match packet.header.operation { - ArpOperation::Unknown(v) => assert_eq!(v, 0x9999), - _ => panic!("Expected unknown operation"), - } + assert!(matches!( + packet.header.hardware_type, + ArpHardwareType::Unknown(0x9999) + )); + assert!(matches!( + packet.header.operation, + ArpOperation::Unknown(0x9999) + )); } #[test] diff --git a/nex-packet/src/checksum.rs b/nex-packet/src/checksum.rs index 51ca95d..218ed9b 100644 --- a/nex-packet/src/checksum.rs +++ b/nex-packet/src/checksum.rs @@ -4,6 +4,7 @@ use std::net::{Ipv4Addr, Ipv6Addr}; /// Controls how and when checksum recalculation happens for a packet. #[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] +#[non_exhaustive] pub enum ChecksumMode { /// Checksum updates are handled manually by the caller. #[default] @@ -68,6 +69,7 @@ impl ChecksumState { /// Captures the pseudo-header inputs required for transport checksum calculations. #[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] pub enum TransportChecksumContext { /// Transport checksum associated with an IPv4 pseudo-header. Ipv4 { diff --git a/nex-packet/src/dhcp.rs b/nex-packet/src/dhcp.rs index 3c634cf..43048aa 100644 --- a/nex-packet/src/dhcp.rs +++ b/nex-packet/src/dhcp.rs @@ -15,6 +15,7 @@ pub const DHCP_MIN_PACKET_SIZE: usize = 236; #[repr(u8)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[non_exhaustive] pub enum DhcpOperation { Request = 1, Reply = 2, @@ -43,6 +44,7 @@ impl DhcpOperation { #[repr(u8)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[non_exhaustive] pub enum DhcpHardwareType { Ethernet = 1, ExperimentalEthernet = 2, @@ -277,63 +279,70 @@ pub struct DhcpPacket { impl Packet for DhcpPacket { type Header = DhcpHeader; - fn from_buf(mut bytes: &[u8]) -> Option { - if bytes.len() < DHCP_MIN_PACKET_SIZE { - return None; - } - - let op = DhcpOperation::new(bytes.get_u8()); - let htype = DhcpHardwareType::new(bytes.get_u8()); - let hlen = bytes.get_u8(); - let hops = bytes.get_u8(); - let xid = bytes.get_u32(); - let secs = bytes.get_u16(); - let flags = bytes.get_u16(); - - let ciaddr = Ipv4Addr::from(bytes.get_u32()); - let yiaddr = Ipv4Addr::from(bytes.get_u32()); - let siaddr = Ipv4Addr::from(bytes.get_u32()); - let giaddr = Ipv4Addr::from(bytes.get_u32()); - - let mut chaddr = [0u8; 6]; - bytes.copy_to_slice(&mut chaddr); - let chaddr = MacAddr::from_octets(chaddr); - - let mut chaddr_pad = [0u8; 10]; - bytes.copy_to_slice(&mut chaddr_pad); - - let mut sname = [0u8; 64]; - bytes.copy_to_slice(&mut sname); - - let mut file = [0u8; 128]; - bytes.copy_to_slice(&mut file); - - let header = DhcpHeader { - op, - htype, - hlen, - hops, - xid, - secs, - flags, - ciaddr, - yiaddr, - siaddr, - giaddr, - chaddr, - chaddr_pad: chaddr_pad.to_vec(), - sname: sname.to_vec(), - file: file.to_vec(), - }; - - Some(Self { - header, - payload: Bytes::copy_from_slice(bytes), + fn try_from_buf(mut bytes: &[u8]) -> Result { + (|| -> Option { + if bytes.len() < DHCP_MIN_PACKET_SIZE { + return None; + } + + let op = DhcpOperation::new(bytes.get_u8()); + let htype = DhcpHardwareType::new(bytes.get_u8()); + let hlen = bytes.get_u8(); + let hops = bytes.get_u8(); + let xid = bytes.get_u32(); + let secs = bytes.get_u16(); + let flags = bytes.get_u16(); + + let ciaddr = Ipv4Addr::from(bytes.get_u32()); + let yiaddr = Ipv4Addr::from(bytes.get_u32()); + let siaddr = Ipv4Addr::from(bytes.get_u32()); + let giaddr = Ipv4Addr::from(bytes.get_u32()); + + let mut chaddr = [0u8; 6]; + bytes.copy_to_slice(&mut chaddr); + let chaddr = MacAddr::from_octets(chaddr); + + let mut chaddr_pad = [0u8; 10]; + bytes.copy_to_slice(&mut chaddr_pad); + + let mut sname = [0u8; 64]; + bytes.copy_to_slice(&mut sname); + + let mut file = [0u8; 128]; + bytes.copy_to_slice(&mut file); + + let header = DhcpHeader { + op, + htype, + hlen, + hops, + xid, + secs, + flags, + ciaddr, + yiaddr, + siaddr, + giaddr, + chaddr, + chaddr_pad: chaddr_pad.to_vec(), + sname: sname.to_vec(), + file: file.to_vec(), + }; + + Some(Self { + header, + payload: Bytes::copy_from_slice(bytes), + }) + })() + .ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), }) } - fn from_bytes(bytes: Bytes) -> Option { - Self::from_buf(&bytes) + fn try_from_bytes(bytes: Bytes) -> Result { + Self::from_buf(&bytes).ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), + }) } fn to_bytes(&self) -> Bytes { diff --git a/nex-packet/src/dns.rs b/nex-packet/src/dns.rs index 23aeb0d..ec0d0ad 100644 --- a/nex-packet/src/dns.rs +++ b/nex-packet/src/dns.rs @@ -19,6 +19,7 @@ use serde::{Deserialize, Serialize}; #[repr(u16)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[non_exhaustive] pub enum DnsClass { IN = 1, // Internet CS = 2, // CSNET (Obsolete) @@ -63,6 +64,7 @@ impl DnsClass { #[repr(u16)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[non_exhaustive] pub enum DnsType { A = 1, NS = 2, @@ -447,6 +449,7 @@ impl DnsType { /// #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[non_exhaustive] pub enum OpCode { Query, InverseQuery, @@ -501,6 +504,7 @@ impl OpCode { /// #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[non_exhaustive] pub enum RetCode { NoError, FormErr, @@ -617,54 +621,61 @@ pub struct DnsQueryPacket { impl Packet for DnsQueryPacket { type Header = (); - fn from_buf(buf: &[u8]) -> Option { - let mut pos = 0; - let mut qname = Vec::new(); + fn try_from_buf(buf: &[u8]) -> Result { + (|| -> Option { + let mut pos = 0; + let mut qname = Vec::new(); + + // Parse the QNAME field + loop { + if pos >= buf.len() { + return None; + } - // Parse the QNAME field - loop { - if pos >= buf.len() { - return None; - } + let len = buf[pos]; + pos += 1; + qname.push(len); - let len = buf[pos]; - pos += 1; - qname.push(len); + if len == 0 { + break; + } - if len == 0 { - break; + if pos + len as usize > buf.len() { + return None; + } + + qname.extend_from_slice(&buf[pos..pos + len as usize]); + pos += len as usize; } - if pos + len as usize > buf.len() { + // Read QTYPE and QCLASS + if pos + 4 > buf.len() { return None; } - qname.extend_from_slice(&buf[pos..pos + len as usize]); - pos += len as usize; - } - - // Read QTYPE and QCLASS - if pos + 4 > buf.len() { - return None; - } - - let qtype = DnsType::new(u16::from_be_bytes([buf[pos], buf[pos + 1]])); - let qclass = DnsClass::new(u16::from_be_bytes([buf[pos + 2], buf[pos + 3]])); - pos += 4; - - // The rest is stored as payload - let payload = Bytes::copy_from_slice(&buf[pos..]); - - Some(Self { - qname, - qtype, - qclass, - payload, + let qtype = DnsType::new(u16::from_be_bytes([buf[pos], buf[pos + 1]])); + let qclass = DnsClass::new(u16::from_be_bytes([buf[pos + 2], buf[pos + 3]])); + pos += 4; + + // The rest is stored as payload + let payload = Bytes::copy_from_slice(&buf[pos..]); + + Some(Self { + qname, + qtype, + qclass, + payload, + }) + })() + .ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), }) } - fn from_bytes(bytes: Bytes) -> Option { - Self::from_buf(&bytes) + fn try_from_bytes(bytes: Bytes) -> Result { + Self::from_buf(&bytes).ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), + }) } fn to_bytes(&self) -> Bytes { @@ -703,6 +714,13 @@ impl Packet for DnsQueryPacket { } impl DnsQueryPacket { + /// Parse the query name with compression-pointer validation. + pub fn qname_parsed(&self) -> Result { + decode_dns_name(&self.qname, 0).map(|(name, _)| name) + } + + /// Compatibility parser that exposes the legacy UTF-8-only error. + #[deprecated(note = "use qname_parsed")] pub fn get_qname_parsed(&self) -> Result { let name = &self.qname; let mut qname = String::new(); @@ -724,9 +742,10 @@ impl DnsQueryPacket { Ok(qname) } - /// Parse the query name with compression-pointer validation. + /// Deprecated compatibility alias for [`Self::qname_parsed`]. + #[deprecated(note = "use qname_parsed")] pub fn try_get_qname_parsed(&self) -> Result { - decode_dns_name(&self.qname, 0).map(|(name, _)| name) + self.qname_parsed() } pub fn qname_length(&self) -> usize { @@ -788,51 +807,58 @@ pub struct DnsResponsePacket { impl Packet for DnsResponsePacket { type Header = (); - fn from_buf(buf: &[u8]) -> Option { - if buf.len() < 12 { - return None; - } + fn try_from_buf(buf: &[u8]) -> Result { + (|| -> Option { + if buf.len() < 12 { + return None; + } - let mut pos = 0; + let mut pos = 0; - let name_tag = u16::from_be_bytes([buf[pos], buf[pos + 1]]); - pos += 2; + let name_tag = u16::from_be_bytes([buf[pos], buf[pos + 1]]); + pos += 2; - let rtype = DnsType::new(u16::from_be_bytes([buf[pos], buf[pos + 1]])); - pos += 2; + let rtype = DnsType::new(u16::from_be_bytes([buf[pos], buf[pos + 1]])); + pos += 2; - let rclass = DnsClass::new(u16::from_be_bytes([buf[pos], buf[pos + 1]])); - pos += 2; + let rclass = DnsClass::new(u16::from_be_bytes([buf[pos], buf[pos + 1]])); + pos += 2; - let ttl = u32::from_be_bytes([buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3]]); - pos += 4; + let ttl = u32::from_be_bytes([buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3]]); + pos += 4; - let data_len = u16::from_be_bytes([buf[pos], buf[pos + 1]]); - pos += 2; + let data_len = u16::from_be_bytes([buf[pos], buf[pos + 1]]); + pos += 2; - let data_len_usize = data_len as usize; + let data_len_usize = data_len as usize; - if buf.len() < pos + data_len_usize { - return None; - } - - let data = buf[pos..pos + data_len_usize].to_vec(); - pos += data_len_usize; - - let payload = Bytes::copy_from_slice(&buf[pos..]); + if buf.len() < pos + data_len_usize { + return None; + } - Some(Self { - name_tag, - rtype, - rclass, - ttl, - data_len, - data, - payload, + let data = buf[pos..pos + data_len_usize].to_vec(); + pos += data_len_usize; + + let payload = Bytes::copy_from_slice(&buf[pos..]); + + Some(Self { + name_tag, + rtype, + rclass, + ttl, + data_len, + data, + payload, + }) + })() + .ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), }) } - fn from_bytes(bytes: Bytes) -> Option { - Self::from_buf(&bytes) + fn try_from_bytes(bytes: Bytes) -> Result { + Self::from_buf(&bytes).ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), + }) } fn to_bytes(&self) -> Bytes { @@ -921,7 +947,7 @@ impl DnsResponsePacket { } /// Returns the IPv4 address if the record type is A and data length is 4 bytes. - pub fn get_ipv4(&self) -> Option { + pub fn ipv4(&self) -> Option { if self.rtype == DnsType::A && self.data.len() == 4 { Some(Ipv4Addr::new( self.data[0], @@ -933,34 +959,54 @@ impl DnsResponsePacket { None } } + /// Deprecated compatibility alias for ipv4. + #[deprecated(note = "use ipv4")] + pub fn get_ipv4(&self) -> Option { + self.ipv4() + } /// Returns the IPv6 address if the record type is AAAA and data length is 16 bytes. - pub fn get_ipv6(&self) -> Option { + pub fn ipv6(&self) -> Option { if self.rtype == DnsType::AAAA && self.data.len() == 16 { Some(Ipv6Addr::from(<[u8; 16]>::try_from(&self.data[..]).ok()?)) } else { None } } + /// Deprecated compatibility alias for ipv6. + #[deprecated(note = "use ipv6")] + pub fn get_ipv6(&self) -> Option { + self.ipv6() + } /// Returns the IP address based on the record type. - pub fn get_ip(&self) -> Option { + pub fn ip(&self) -> Option { match self.rtype { DnsType::A => self.get_ipv4().map(IpAddr::V4), DnsType::AAAA => self.get_ipv6().map(IpAddr::V6), _ => None, } } + /// Deprecated compatibility alias for ip. + #[deprecated(note = "use ip")] + pub fn get_ip(&self) -> Option { + self.ip() + } /// Returns the DNS name if the record type is CNAME, NS, or PTR. - pub fn get_name(&self) -> Option { + pub fn dns_name(&self) -> Option { match self.rtype { DnsType::CNAME | DnsType::NS | DnsType::PTR => DnsName::try_from_bytes(&self.data).ok(), _ => None, } } + /// Deprecated compatibility alias for dns_name. + #[deprecated(note = "use dns_name")] + pub fn get_name(&self) -> Option { + self.dns_name() + } /// Returns the TXT strings if the record type is TXT. - pub fn get_txt_strings(&self) -> Option> { + pub fn txt_strings(&self) -> Option> { if self.rtype != DnsType::TXT { return None; } @@ -985,6 +1031,11 @@ impl DnsResponsePacket { Some(result) } + /// Deprecated compatibility alias for txt_strings. + #[deprecated(note = "use txt_strings")] + pub fn get_txt_strings(&self) -> Option> { + self.txt_strings() + } } #[derive(Clone, Debug, PartialEq, Eq)] @@ -1021,12 +1072,20 @@ pub struct DnsPacket { impl Packet for DnsPacket { type Header = (); - fn from_buf(buf: &[u8]) -> Option { - Self::try_from_buf(buf).ok() - } - - fn from_bytes(bytes: Bytes) -> Option { - Self::try_from_bytes(bytes).ok() + fn try_from_buf(buf: &[u8]) -> Result { + Self::try_from_buf(buf) + .ok() + .ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), + }) + } + + fn try_from_bytes(bytes: Bytes) -> Result { + Self::try_from_bytes(bytes) + .ok() + .ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), + }) } fn to_bytes(&self) -> Bytes { diff --git a/nex-packet/src/ethernet.rs b/nex-packet/src/ethernet.rs index 15dc371..743ff41 100644 --- a/nex-packet/src/ethernet.rs +++ b/nex-packet/src/ethernet.rs @@ -22,6 +22,7 @@ pub const MAC_ADDR_LEN: usize = 6; #[repr(u16)] #[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[non_exhaustive] pub enum EtherType { Ipv4, Arp, @@ -193,11 +194,19 @@ pub struct EthernetPacket { impl Packet for EthernetPacket { type Header = EthernetHeader; - fn from_buf(bytes: &[u8]) -> Option { - Self::try_from_buf(bytes).ok() + fn try_from_buf(bytes: &[u8]) -> Result { + Self::try_from_buf(bytes) + .ok() + .ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), + }) } - fn from_bytes(bytes: Bytes) -> Option { - Self::try_from_bytes(bytes).ok() + fn try_from_bytes(bytes: Bytes) -> Result { + Self::try_from_bytes(bytes) + .ok() + .ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), + }) } fn to_bytes(&self) -> Bytes { let mut buf = Vec::with_capacity(ETHERNET_HEADER_LEN + self.payload.len()); @@ -233,19 +242,34 @@ impl EthernetPacket { EthernetPacket { header, payload } } /// Get the destination MAC address. - pub fn get_destination(&self) -> MacAddr { + pub fn destination(&self) -> MacAddr { self.header.destination } + /// Deprecated compatibility alias for destination. + #[deprecated(note = "use destination")] + pub fn get_destination(&self) -> MacAddr { + self.destination() + } /// Get the source MAC address. - pub fn get_source(&self) -> MacAddr { + pub fn source(&self) -> MacAddr { self.header.source } + /// Deprecated compatibility alias for source. + #[deprecated(note = "use source")] + pub fn get_source(&self) -> MacAddr { + self.source() + } /// Get the EtherType. - pub fn get_ethertype(&self) -> EtherType { + pub fn ethertype(&self) -> EtherType { self.header.ethertype } + /// Deprecated compatibility alias for ethertype. + #[deprecated(note = "use ethertype")] + pub fn get_ethertype(&self) -> EtherType { + self.ethertype() + } pub fn ip_packet(&self) -> Option { if self.get_ethertype() == EtherType::Ipv4 || self.get_ethertype() == EtherType::Ipv6 { @@ -365,15 +389,25 @@ impl<'a> MutablePacket<'a> for MutableEthernetPacket<'a> { impl<'a> MutableEthernetPacket<'a> { /// Create a mutable packet without performing size checks. + /// + /// # Safety + /// + /// `buffer` must contain at least the 14-byte Ethernet header before any + /// field accessor is called. Prefer [`MutablePacket::new`]. pub fn new_unchecked(buffer: &'a mut [u8]) -> Self { Self { buffer } } /// Retrieve the destination MAC address. - pub fn get_destination(&self) -> MacAddr { + pub fn destination(&self) -> MacAddr { let h = self.header(); MacAddr::from_octets([h[0], h[1], h[2], h[3], h[4], h[5]]) } + /// Deprecated compatibility alias for destination. + #[deprecated(note = "use destination")] + pub fn get_destination(&self) -> MacAddr { + self.destination() + } /// Update the destination MAC address. pub fn set_destination(&mut self, addr: MacAddr) { @@ -381,10 +415,15 @@ impl<'a> MutableEthernetPacket<'a> { } /// Retrieve the source MAC address. - pub fn get_source(&self) -> MacAddr { + pub fn source(&self) -> MacAddr { let h = self.header(); MacAddr::from_octets([h[6], h[7], h[8], h[9], h[10], h[11]]) } + /// Deprecated compatibility alias for source. + #[deprecated(note = "use source")] + pub fn get_source(&self) -> MacAddr { + self.source() + } /// Update the source MAC address. pub fn set_source(&mut self, addr: MacAddr) { @@ -392,9 +431,14 @@ impl<'a> MutableEthernetPacket<'a> { } /// Retrieve the EtherType. - pub fn get_ethertype(&self) -> EtherType { + pub fn ethertype(&self) -> EtherType { EtherType::new(u16::from_be_bytes([self.header()[12], self.header()[13]])) } + /// Deprecated compatibility alias for ethertype. + #[deprecated(note = "use ethertype")] + pub fn get_ethertype(&self) -> EtherType { + self.ethertype() + } /// Update the EtherType. pub fn set_ethertype(&mut self, ty: EtherType) { @@ -490,10 +534,7 @@ mod tests { 0x00, 0x11, 0x22, 0x33, ]; let packet = EthernetPacket::from_bytes(Bytes::copy_from_slice(&raw)).unwrap(); - match packet.get_ethertype() { - EtherType::Unknown(val) => assert_eq!(val, 0xdead), - _ => panic!("Expected unknown EtherType"), - } + assert!(matches!(packet.ethertype(), EtherType::Unknown(0xdead))); } #[test] diff --git a/nex-packet/src/flowcontrol.rs b/nex-packet/src/flowcontrol.rs index d68385a..c92f2fa 100644 --- a/nex-packet/src/flowcontrol.rs +++ b/nex-packet/src/flowcontrol.rs @@ -11,6 +11,7 @@ use crate::packet::{GenericMutablePacket, Packet}; /// Flow control opcodes are defined in IEEE 802.3x #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[repr(u16)] +#[non_exhaustive] pub enum FlowControlOpcode { Pause = 0x0001, Unknown(u16), @@ -56,26 +57,33 @@ pub struct FlowControlPacket { impl Packet for FlowControlPacket { type Header = (); - fn from_buf(mut bytes: &[u8]) -> Option { - if bytes.len() < 4 { - return None; - } + fn try_from_buf(mut bytes: &[u8]) -> Result { + (|| -> Option { + if bytes.len() < 4 { + return None; + } - let command = FlowControlOpcode::new(bytes.get_u16()); - let quanta = bytes.get_u16(); + let command = FlowControlOpcode::new(bytes.get_u16()); + let quanta = bytes.get_u16(); - // Payload including padding; its contents are not specified by the standard - let payload = Bytes::copy_from_slice(bytes); + // Payload including padding; its contents are not specified by the standard + let payload = Bytes::copy_from_slice(bytes); - Some(Self { - command, - quanta, - payload, + Some(Self { + command, + quanta, + payload, + }) + })() + .ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), }) } - fn from_bytes(bytes: Bytes) -> Option { - Self::from_buf(&bytes) + fn try_from_bytes(bytes: Bytes) -> Result { + Self::from_buf(&bytes).ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), + }) } fn to_bytes(&self) -> Bytes { diff --git a/nex-packet/src/gre.rs b/nex-packet/src/gre.rs index 5152153..25f48d7 100644 --- a/nex-packet/src/gre.rs +++ b/nex-packet/src/gre.rs @@ -29,80 +29,87 @@ pub struct GrePacket { impl Packet for GrePacket { type Header = (); - fn from_buf(mut bytes: &[u8]) -> Option { - if bytes.remaining() < 4 { - return None; - } - - let flags = bytes.get_u16(); - let protocol_type = bytes.get_u16(); - - let checksum_present = ((flags >> 15) & 0x1) as u1; - let routing_present = ((flags >> 14) & 0x1) as u1; - let key_present = ((flags >> 13) & 0x1) as u1; - let sequence_present = ((flags >> 12) & 0x1) as u1; - let strict_source_route = ((flags >> 11) & 0x1) as u1; - let recursion_control = ((flags >> 8) & 0x7) as u3; - let zero_flags = ((flags >> 3) & 0x1f) as u5; - let version = (flags & 0x7) as u3; - - // Retrieve optional fields in order - let mut checksum = Vec::new(); - let mut offset = Vec::new(); - let mut key = Vec::new(); - let mut sequence = Vec::new(); - let routing = Vec::new(); - - if checksum_present != 0 || routing_present != 0 { + fn try_from_buf(mut bytes: &[u8]) -> Result { + (|| -> Option { if bytes.remaining() < 4 { return None; } - checksum.push(bytes.get_u16()); - offset.push(bytes.get_u16()); - } - if key_present != 0 { - if bytes.remaining() < 4 { - return None; + let flags = bytes.get_u16(); + let protocol_type = bytes.get_u16(); + + let checksum_present = ((flags >> 15) & 0x1) as u1; + let routing_present = ((flags >> 14) & 0x1) as u1; + let key_present = ((flags >> 13) & 0x1) as u1; + let sequence_present = ((flags >> 12) & 0x1) as u1; + let strict_source_route = ((flags >> 11) & 0x1) as u1; + let recursion_control = ((flags >> 8) & 0x7) as u3; + let zero_flags = ((flags >> 3) & 0x1f) as u5; + let version = (flags & 0x7) as u3; + + // Retrieve optional fields in order + let mut checksum = Vec::new(); + let mut offset = Vec::new(); + let mut key = Vec::new(); + let mut sequence = Vec::new(); + let routing = Vec::new(); + + if checksum_present != 0 || routing_present != 0 { + if bytes.remaining() < 4 { + return None; + } + checksum.push(bytes.get_u16()); + offset.push(bytes.get_u16()); } - key.push(bytes.get_u32()); - } - if sequence_present != 0 { - if bytes.remaining() < 4 { - return None; + if key_present != 0 { + if bytes.remaining() < 4 { + return None; + } + key.push(bytes.get_u32()); } - sequence.push(bytes.get_u32()); - } - if routing_present != 0 { - // Source-routed GRE parsing is not yet supported. - return None; - } + if sequence_present != 0 { + if bytes.remaining() < 4 { + return None; + } + sequence.push(bytes.get_u32()); + } - let payload = Bytes::copy_from_slice(bytes); - - Some(Self { - checksum_present, - routing_present, - key_present, - sequence_present, - strict_source_route, - recursion_control, - zero_flags, - version, - protocol_type, - checksum, - offset, - key, - sequence, - routing, - payload, + if routing_present != 0 { + // Source-routed GRE parsing is not yet supported. + return None; + } + + let payload = Bytes::copy_from_slice(bytes); + + Some(Self { + checksum_present, + routing_present, + key_present, + sequence_present, + strict_source_route, + recursion_control, + zero_flags, + version, + protocol_type, + checksum, + offset, + key, + sequence, + routing, + payload, + }) + })() + .ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), }) } - fn from_bytes(bytes: Bytes) -> Option { - Self::from_buf(&bytes) + fn try_from_bytes(bytes: Bytes) -> Result { + Self::from_buf(&bytes).ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), + }) } fn to_bytes(&self) -> Bytes { diff --git a/nex-packet/src/icmp.rs b/nex-packet/src/icmp.rs index 10893be..c667eb4 100644 --- a/nex-packet/src/icmp.rs +++ b/nex-packet/src/icmp.rs @@ -23,6 +23,7 @@ pub const ICMPV4_IP_PACKET_LEN: usize = IPV4_HEADER_LEN + ICMPV4_HEADER_LEN; #[repr(u8)] #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[non_exhaustive] pub enum IcmpType { EchoReply, DestinationUnreachable, @@ -184,25 +185,32 @@ pub struct IcmpPacket { impl Packet for IcmpPacket { type Header = IcmpHeader; - fn from_buf(bytes: &[u8]) -> Option { - if bytes.len() < ICMPV4_HEADER_LEN { - return None; - } - let icmp_type = IcmpType::new(bytes[0]); - let icmp_code = IcmpCode::new(bytes[1]); - let checksum = u16::from_be_bytes([bytes[2], bytes[3]]); - let payload = Bytes::copy_from_slice(&bytes[ICMP_COMMON_HEADER_LEN..]); - Some(IcmpPacket { - header: IcmpHeader { - icmp_type, - icmp_code, - checksum, - }, - payload, + fn try_from_buf(bytes: &[u8]) -> Result { + (|| -> Option { + if bytes.len() < ICMPV4_HEADER_LEN { + return None; + } + let icmp_type = IcmpType::new(bytes[0]); + let icmp_code = IcmpCode::new(bytes[1]); + let checksum = u16::from_be_bytes([bytes[2], bytes[3]]); + let payload = Bytes::copy_from_slice(&bytes[ICMP_COMMON_HEADER_LEN..]); + Some(IcmpPacket { + header: IcmpHeader { + icmp_type, + icmp_code, + checksum, + }, + payload, + }) + })() + .ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), }) } - fn from_bytes(bytes: Bytes) -> Option { - Self::from_buf(&bytes) + fn try_from_bytes(bytes: Bytes) -> Result { + Self::from_buf(&bytes).ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), + }) } fn to_bytes(&self) -> Bytes { @@ -293,6 +301,11 @@ impl<'a> MutablePacket<'a> for MutableIcmpPacket<'a> { impl<'a> MutableIcmpPacket<'a> { /// Create a mutable ICMP packet without performing validation. + /// + /// # Safety + /// + /// `buffer` must contain a complete ICMP header before any field accessor + /// is called. Prefer [`MutablePacket::new`]. pub fn new_unchecked(buffer: &'a mut [u8]) -> Self { Self { buffer, @@ -364,9 +377,14 @@ impl<'a> MutableIcmpPacket<'a> { } /// Returns the current ICMP type field. - pub fn get_type(&self) -> IcmpType { + pub fn packet_type(&self) -> IcmpType { IcmpType::new(self.raw()[0]) } + /// Deprecated compatibility alias for packet_type. + #[deprecated(note = "use packet_type")] + pub fn get_type(&self) -> IcmpType { + self.packet_type() + } /// Sets the ICMP type field and marks the checksum as dirty. pub fn set_type(&mut self, icmp_type: IcmpType) { @@ -375,9 +393,14 @@ impl<'a> MutableIcmpPacket<'a> { } /// Returns the current ICMP code field. - pub fn get_code(&self) -> IcmpCode { + pub fn code(&self) -> IcmpCode { IcmpCode::new(self.raw()[1]) } + /// Deprecated compatibility alias for code. + #[deprecated(note = "use code")] + pub fn get_code(&self) -> IcmpCode { + self.code() + } /// Sets the ICMP code field and marks the checksum as dirty. pub fn set_code(&mut self, icmp_code: IcmpCode) { @@ -386,9 +409,14 @@ impl<'a> MutableIcmpPacket<'a> { } /// Returns the serialized checksum value. - pub fn get_checksum(&self) -> u16 { + pub fn checksum(&self) -> u16 { u16::from_be_bytes([self.raw()[2], self.raw()[3]]) } + /// Deprecated compatibility alias for checksum. + #[deprecated(note = "use checksum")] + pub fn get_checksum(&self) -> u16 { + self.checksum() + } /// Sets the serialized checksum value and clears the dirty flag. pub fn set_checksum(&mut self, checksum: u16) { diff --git a/nex-packet/src/icmpv6.rs b/nex-packet/src/icmpv6.rs index a04f054..94716dc 100644 --- a/nex-packet/src/icmpv6.rs +++ b/nex-packet/src/icmpv6.rs @@ -27,6 +27,7 @@ pub const ICMPV6_IP_PACKET_LEN: usize = IPV6_HEADER_LEN + ICMPV6_HEADER_LEN; #[repr(u8)] #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[non_exhaustive] pub enum Icmpv6Type { DestinationUnreachable, PacketTooBig, @@ -244,23 +245,30 @@ pub struct Icmpv6Packet { impl Packet for Icmpv6Packet { type Header = Icmpv6Header; - fn from_buf(bytes: &[u8]) -> Option { - if bytes.len() < ICMPV6_HEADER_LEN { - return None; - } - let icmpv6_type = Icmpv6Type::new(bytes[0]); - let icmpv6_code = Icmpv6Code::new(bytes[1]); - let checksum = u16::from_be_bytes([bytes[2], bytes[3]]); - let header = Icmpv6Header { - icmpv6_type, - icmpv6_code, - checksum, - }; - let payload = Bytes::copy_from_slice(&bytes[ICMPV6_COMMON_HEADER_LEN..]); - Some(Icmpv6Packet { header, payload }) + fn try_from_buf(bytes: &[u8]) -> Result { + (|| -> Option { + if bytes.len() < ICMPV6_HEADER_LEN { + return None; + } + let icmpv6_type = Icmpv6Type::new(bytes[0]); + let icmpv6_code = Icmpv6Code::new(bytes[1]); + let checksum = u16::from_be_bytes([bytes[2], bytes[3]]); + let header = Icmpv6Header { + icmpv6_type, + icmpv6_code, + checksum, + }; + let payload = Bytes::copy_from_slice(&bytes[ICMPV6_COMMON_HEADER_LEN..]); + Some(Icmpv6Packet { header, payload }) + })() + .ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), + }) } - fn from_bytes(bytes: Bytes) -> Option { - Self::from_buf(&bytes) + fn try_from_bytes(bytes: Bytes) -> Result { + Self::from_buf(&bytes).ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), + }) } fn to_bytes(&self) -> Bytes { let mut bytes = Vec::with_capacity(ICMPV6_COMMON_HEADER_LEN + self.payload.len()); @@ -343,6 +351,11 @@ impl<'a> MutablePacket<'a> for MutableIcmpv6Packet<'a> { impl<'a> MutableIcmpv6Packet<'a> { /// Create a mutable ICMPv6 packet without performing validation. + /// + /// # Safety + /// + /// `buffer` must contain a complete ICMPv6 header before any field accessor + /// is called. Prefer [`MutablePacket::new`]. pub fn new_unchecked(buffer: &'a mut [u8]) -> Self { Self { buffer, @@ -458,9 +471,14 @@ impl<'a> MutableIcmpv6Packet<'a> { } /// Returns the ICMPv6 type field. - pub fn get_type(&self) -> Icmpv6Type { + pub fn packet_type(&self) -> Icmpv6Type { Icmpv6Type::new(self.raw()[0]) } + /// Deprecated compatibility alias for packet_type. + #[deprecated(note = "use packet_type")] + pub fn get_type(&self) -> Icmpv6Type { + self.packet_type() + } /// Sets the ICMPv6 type field and marks the checksum as dirty. pub fn set_type(&mut self, icmpv6_type: Icmpv6Type) { @@ -469,9 +487,14 @@ impl<'a> MutableIcmpv6Packet<'a> { } /// Returns the ICMPv6 code field. - pub fn get_code(&self) -> Icmpv6Code { + pub fn code(&self) -> Icmpv6Code { Icmpv6Code::new(self.raw()[1]) } + /// Deprecated compatibility alias for code. + #[deprecated(note = "use code")] + pub fn get_code(&self) -> Icmpv6Code { + self.code() + } /// Sets the ICMPv6 code field and marks the checksum as dirty. pub fn set_code(&mut self, icmpv6_code: Icmpv6Code) { @@ -480,9 +503,14 @@ impl<'a> MutableIcmpv6Packet<'a> { } /// Returns the serialized checksum value. - pub fn get_checksum(&self) -> u16 { + pub fn checksum(&self) -> u16 { u16::from_be_bytes([self.raw()[2], self.raw()[3]]) } + /// Deprecated compatibility alias for checksum. + #[deprecated(note = "use checksum")] + pub fn get_checksum(&self) -> u16 { + self.checksum() + } /// Sets the serialized checksum value and clears the dirty flag. pub fn set_checksum(&mut self, checksum: u16) { @@ -753,30 +781,37 @@ pub mod ndp { impl Packet for NdpOptionPacket { type Header = (); - fn from_buf(bytes: &[u8]) -> Option { - if bytes.len() < 2 { - return None; - } + fn try_from_buf(bytes: &[u8]) -> Result { + (|| -> Option { + if bytes.len() < 2 { + return None; + } - let option_type = NdpOptionType::new(bytes[0]); - let length = bytes[1]; // unit: 8 bytes + let option_type = NdpOptionType::new(bytes[0]); + let length = bytes[1]; // unit: 8 bytes - let total_len = (length as usize) * 8; - if bytes.len() < total_len { - return None; - } + let total_len = (length as usize) * 8; + if bytes.len() < total_len { + return None; + } - let data_len = total_len - 2; - let payload = Bytes::copy_from_slice(&bytes[2..2 + data_len]); + let data_len = total_len - 2; + let payload = Bytes::copy_from_slice(&bytes[2..2 + data_len]); - Some(Self { - option_type, - length, - payload, + Some(Self { + option_type, + length, + payload, + }) + })() + .ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), }) } - fn from_bytes(bytes: Bytes) -> Option { - Self::from_buf(&bytes) + fn try_from_bytes(bytes: Bytes) -> Result { + Self::from_buf(&bytes).ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), + }) } fn to_bytes(&self) -> Bytes { @@ -883,52 +918,59 @@ pub mod ndp { impl Packet for RouterSolicitPacket { type Header = (); - fn from_buf(bytes: &[u8]) -> Option { - if bytes.len() < NDP_SOL_PACKET_LEN { - return None; - } - - let icmpv6_type = Icmpv6Type::new(bytes[0]); - let icmpv6_code = Icmpv6Code::new(bytes[1]); - let checksum = u16::from_be_bytes([bytes[2], bytes[3]]); - let header = Icmpv6Header { - icmpv6_type, - icmpv6_code, - checksum, - }; - let reserved = u32::from_be_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]); + fn try_from_buf(bytes: &[u8]) -> Result { + (|| -> Option { + if bytes.len() < NDP_SOL_PACKET_LEN { + return None; + } - let mut options = Vec::new(); - let mut i = 8; - while i + 2 <= bytes.len() { - let option_type = NdpOptionType::new(bytes[i]); - let length = bytes[i + 1]; - let option_len = (length as usize) * 8; + let icmpv6_type = Icmpv6Type::new(bytes[0]); + let icmpv6_code = Icmpv6Code::new(bytes[1]); + let checksum = u16::from_be_bytes([bytes[2], bytes[3]]); + let header = Icmpv6Header { + icmpv6_type, + icmpv6_code, + checksum, + }; + let reserved = u32::from_be_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]); + + let mut options = Vec::new(); + let mut i = 8; + while i + 2 <= bytes.len() { + let option_type = NdpOptionType::new(bytes[i]); + let length = bytes[i + 1]; + let option_len = (length as usize) * 8; + + if i + option_len > bytes.len() { + break; + } - if i + option_len > bytes.len() { - break; + let payload = Bytes::copy_from_slice(&bytes[i + 2..i + option_len]); + options.push(NdpOptionPacket { + option_type, + length, + payload, + }); + i += option_len; } - let payload = Bytes::copy_from_slice(&bytes[i + 2..i + option_len]); - options.push(NdpOptionPacket { - option_type, - length, - payload, - }); - i += option_len; - } - - let payload = Bytes::copy_from_slice(&bytes[i..]); + let payload = Bytes::copy_from_slice(&bytes[i..]); - Some(RouterSolicitPacket { - header, - reserved, - options, - payload, + Some(RouterSolicitPacket { + header, + reserved, + options, + payload, + }) + })() + .ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), }) } - fn from_bytes(bytes: Bytes) -> Option { - Self::from_buf(&bytes) + fn try_from_bytes(bytes: Bytes) -> Result { + Self::from_buf(&bytes).ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), + }) } fn to_bytes(&self) -> Bytes { @@ -1075,62 +1117,69 @@ pub mod ndp { } impl Packet for RouterAdvertPacket { type Header = (); - fn from_buf(bytes: &[u8]) -> Option { - if bytes.len() < NDP_ADV_PACKET_LEN { - return None; - } + fn try_from_buf(bytes: &[u8]) -> Result { + (|| -> Option { + if bytes.len() < NDP_ADV_PACKET_LEN { + return None; + } - let icmpv6_type = Icmpv6Type::new(bytes[0]); - let icmpv6_code = Icmpv6Code::new(bytes[1]); - let checksum = u16::from_be_bytes([bytes[2], bytes[3]]); - let header = Icmpv6Header { - icmpv6_type, - icmpv6_code, - checksum, - }; + let icmpv6_type = Icmpv6Type::new(bytes[0]); + let icmpv6_code = Icmpv6Code::new(bytes[1]); + let checksum = u16::from_be_bytes([bytes[2], bytes[3]]); + let header = Icmpv6Header { + icmpv6_type, + icmpv6_code, + checksum, + }; + + let hop_limit = bytes[4]; + let flags = bytes[5]; + let lifetime = u16::from_be_bytes([bytes[6], bytes[7]]); + let reachable_time = u32::from_be_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]); + let retrans_time = u32::from_be_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]); + + let mut options = Vec::new(); + let mut i = 16; + while i + 2 <= bytes.len() { + let option_type = NdpOptionType::new(bytes[i]); + let length = bytes[i + 1]; + let option_len = (length as usize) * 8; + + if i + option_len > bytes.len() { + break; + } - let hop_limit = bytes[4]; - let flags = bytes[5]; - let lifetime = u16::from_be_bytes([bytes[6], bytes[7]]); - let reachable_time = u32::from_be_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]); - let retrans_time = u32::from_be_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]); - - let mut options = Vec::new(); - let mut i = 16; - while i + 2 <= bytes.len() { - let option_type = NdpOptionType::new(bytes[i]); - let length = bytes[i + 1]; - let option_len = (length as usize) * 8; - - if i + option_len > bytes.len() { - break; + let payload = Bytes::copy_from_slice(&bytes[i + 2..i + option_len]); + options.push(NdpOptionPacket { + option_type, + length, + payload, + }); + i += option_len; } - let payload = Bytes::copy_from_slice(&bytes[i + 2..i + option_len]); - options.push(NdpOptionPacket { - option_type, - length, - payload, - }); - i += option_len; - } + let payload = Bytes::copy_from_slice(&bytes[i..]); - let payload = Bytes::copy_from_slice(&bytes[i..]); - - Some(RouterAdvertPacket { - header, - hop_limit, - flags, - lifetime, - reachable_time, - retrans_time, - options, - payload, + Some(RouterAdvertPacket { + header, + hop_limit, + flags, + lifetime, + reachable_time, + retrans_time, + options, + payload, + }) + })() + .ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), }) } - fn from_bytes(bytes: Bytes) -> Option { - Self::from_buf(&bytes) + fn try_from_bytes(bytes: Bytes) -> Result { + Self::from_buf(&bytes).ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), + }) } fn to_bytes(&self) -> Bytes { @@ -1267,63 +1316,70 @@ pub mod ndp { impl Packet for NeighborSolicitPacket { type Header = (); - fn from_buf(bytes: &[u8]) -> Option { - if bytes.len() < 24 { - return None; - } + fn try_from_buf(bytes: &[u8]) -> Result { + (|| -> Option { + if bytes.len() < 24 { + return None; + } - let icmpv6_type = Icmpv6Type::new(bytes[0]); - let icmpv6_code = Icmpv6Code::new(bytes[1]); - let checksum = u16::from_be_bytes([bytes[2], bytes[3]]); - let reserved = u32::from_be_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]); - let target_addr = Ipv6Addr::new( - u16::from_be_bytes([bytes[8], bytes[9]]), - u16::from_be_bytes([bytes[10], bytes[11]]), - u16::from_be_bytes([bytes[12], bytes[13]]), - u16::from_be_bytes([bytes[14], bytes[15]]), - u16::from_be_bytes([bytes[16], bytes[17]]), - u16::from_be_bytes([bytes[18], bytes[19]]), - u16::from_be_bytes([bytes[20], bytes[21]]), - u16::from_be_bytes([bytes[22], bytes[23]]), - ); + let icmpv6_type = Icmpv6Type::new(bytes[0]); + let icmpv6_code = Icmpv6Code::new(bytes[1]); + let checksum = u16::from_be_bytes([bytes[2], bytes[3]]); + let reserved = u32::from_be_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]); + let target_addr = Ipv6Addr::new( + u16::from_be_bytes([bytes[8], bytes[9]]), + u16::from_be_bytes([bytes[10], bytes[11]]), + u16::from_be_bytes([bytes[12], bytes[13]]), + u16::from_be_bytes([bytes[14], bytes[15]]), + u16::from_be_bytes([bytes[16], bytes[17]]), + u16::from_be_bytes([bytes[18], bytes[19]]), + u16::from_be_bytes([bytes[20], bytes[21]]), + u16::from_be_bytes([bytes[22], bytes[23]]), + ); + + let mut options = Vec::new(); + let mut i = 24; + while i + 2 <= bytes.len() { + let option_type = NdpOptionType::new(bytes[i]); + let length = bytes[i + 1]; + let option_len = (length as usize) * 8; + + if option_len < 2 || i + option_len > bytes.len() { + break; + } - let mut options = Vec::new(); - let mut i = 24; - while i + 2 <= bytes.len() { - let option_type = NdpOptionType::new(bytes[i]); - let length = bytes[i + 1]; - let option_len = (length as usize) * 8; + let payload = Bytes::copy_from_slice(&bytes[i + 2..i + option_len]); + options.push(NdpOptionPacket { + option_type, + length, + payload, + }); - if option_len < 2 || i + option_len > bytes.len() { - break; + i += option_len; } - let payload = Bytes::copy_from_slice(&bytes[i + 2..i + option_len]); - options.push(NdpOptionPacket { - option_type, - length, + let payload = Bytes::copy_from_slice(&bytes[i..]); + + Some(NeighborSolicitPacket { + header: Icmpv6Header { + icmpv6_type, + icmpv6_code, + checksum, + }, + reserved, + target_addr, + options, payload, - }); - - i += option_len; - } - - let payload = Bytes::copy_from_slice(&bytes[i..]); - - Some(NeighborSolicitPacket { - header: Icmpv6Header { - icmpv6_type, - icmpv6_code, - checksum, - }, - reserved, - target_addr, - options, - payload, + }) + })() + .ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), }) } - fn from_bytes(bytes: Bytes) -> Option { - Self::from_buf(&bytes) + fn try_from_bytes(bytes: Bytes) -> Result { + Self::from_buf(&bytes).ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), + }) } fn to_bytes(&self) -> Bytes { @@ -1480,68 +1536,75 @@ pub mod ndp { impl Packet for NeighborAdvertPacket { type Header = (); - fn from_buf(bytes: &[u8]) -> Option { - if bytes.len() < 24 { - return None; - } - - let icmpv6_type = Icmpv6Type::new(bytes[0]); - let icmpv6_code = Icmpv6Code::new(bytes[1]); - let checksum = u16::from_be_bytes([bytes[2], bytes[3]]); - let header = Icmpv6Header { - icmpv6_type, - icmpv6_code, - checksum, - }; - - let flags = bytes[4]; - let reserved = bitfield::utils::u24be_from_bytes([bytes[5], bytes[6], bytes[7]]); + fn try_from_buf(bytes: &[u8]) -> Result { + (|| -> Option { + if bytes.len() < 24 { + return None; + } - let target_addr = Ipv6Addr::new( - u16::from_be_bytes([bytes[8], bytes[9]]), - u16::from_be_bytes([bytes[10], bytes[11]]), - u16::from_be_bytes([bytes[12], bytes[13]]), - u16::from_be_bytes([bytes[14], bytes[15]]), - u16::from_be_bytes([bytes[16], bytes[17]]), - u16::from_be_bytes([bytes[18], bytes[19]]), - u16::from_be_bytes([bytes[20], bytes[21]]), - u16::from_be_bytes([bytes[22], bytes[23]]), - ); + let icmpv6_type = Icmpv6Type::new(bytes[0]); + let icmpv6_code = Icmpv6Code::new(bytes[1]); + let checksum = u16::from_be_bytes([bytes[2], bytes[3]]); + let header = Icmpv6Header { + icmpv6_type, + icmpv6_code, + checksum, + }; + + let flags = bytes[4]; + let reserved = bitfield::utils::u24be_from_bytes([bytes[5], bytes[6], bytes[7]]); + + let target_addr = Ipv6Addr::new( + u16::from_be_bytes([bytes[8], bytes[9]]), + u16::from_be_bytes([bytes[10], bytes[11]]), + u16::from_be_bytes([bytes[12], bytes[13]]), + u16::from_be_bytes([bytes[14], bytes[15]]), + u16::from_be_bytes([bytes[16], bytes[17]]), + u16::from_be_bytes([bytes[18], bytes[19]]), + u16::from_be_bytes([bytes[20], bytes[21]]), + u16::from_be_bytes([bytes[22], bytes[23]]), + ); + + let mut options = Vec::new(); + let mut i = 24; + while i + 2 <= bytes.len() { + let option_type = NdpOptionType::new(bytes[i]); + let length = bytes[i + 1]; + let option_len = (length as usize) * 8; + + if option_len < 2 || i + option_len > bytes.len() { + break; + } - let mut options = Vec::new(); - let mut i = 24; - while i + 2 <= bytes.len() { - let option_type = NdpOptionType::new(bytes[i]); - let length = bytes[i + 1]; - let option_len = (length as usize) * 8; + let payload = Bytes::copy_from_slice(&bytes[i + 2..i + option_len]); + options.push(NdpOptionPacket { + option_type, + length, + payload, + }); - if option_len < 2 || i + option_len > bytes.len() { - break; + i += option_len; } - let payload = Bytes::copy_from_slice(&bytes[i + 2..i + option_len]); - options.push(NdpOptionPacket { - option_type, - length, - payload, - }); - - i += option_len; - } - - let payload = Bytes::copy_from_slice(&bytes[i..]); + let payload = Bytes::copy_from_slice(&bytes[i..]); - Some(NeighborAdvertPacket { - header, - flags, - reserved, - target_addr, - options, - payload, + Some(NeighborAdvertPacket { + header, + flags, + reserved, + target_addr, + options, + payload, + }) + })() + .ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), }) } - fn from_bytes(bytes: Bytes) -> Option { - Self::from_buf(&bytes) + fn try_from_bytes(bytes: Bytes) -> Result { + Self::from_buf(&bytes).ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), + }) } fn to_bytes(&self) -> Bytes { @@ -1703,78 +1766,85 @@ pub mod ndp { impl Packet for RedirectPacket { type Header = (); - fn from_buf(bytes: &[u8]) -> Option { - if bytes.len() < 40 { - return None; - } - - let icmpv6_type = Icmpv6Type::new(bytes[0]); - let icmpv6_code = Icmpv6Code::new(bytes[1]); - let checksum = u16::from_be_bytes([bytes[2], bytes[3]]); - let header = Icmpv6Header { - icmpv6_type, - icmpv6_code, - checksum, - }; - - let reserved = u32be::from_be_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]); - - let target_addr = Ipv6Addr::new( - u16::from_be_bytes([bytes[8], bytes[9]]), - u16::from_be_bytes([bytes[10], bytes[11]]), - u16::from_be_bytes([bytes[12], bytes[13]]), - u16::from_be_bytes([bytes[14], bytes[15]]), - u16::from_be_bytes([bytes[16], bytes[17]]), - u16::from_be_bytes([bytes[18], bytes[19]]), - u16::from_be_bytes([bytes[20], bytes[21]]), - u16::from_be_bytes([bytes[22], bytes[23]]), - ); + fn try_from_buf(bytes: &[u8]) -> Result { + (|| -> Option { + if bytes.len() < 40 { + return None; + } - let dest_addr = Ipv6Addr::new( - u16::from_be_bytes([bytes[24], bytes[25]]), - u16::from_be_bytes([bytes[26], bytes[27]]), - u16::from_be_bytes([bytes[28], bytes[29]]), - u16::from_be_bytes([bytes[30], bytes[31]]), - u16::from_be_bytes([bytes[32], bytes[33]]), - u16::from_be_bytes([bytes[34], bytes[35]]), - u16::from_be_bytes([bytes[36], bytes[37]]), - u16::from_be_bytes([bytes[38], bytes[39]]), - ); + let icmpv6_type = Icmpv6Type::new(bytes[0]); + let icmpv6_code = Icmpv6Code::new(bytes[1]); + let checksum = u16::from_be_bytes([bytes[2], bytes[3]]); + let header = Icmpv6Header { + icmpv6_type, + icmpv6_code, + checksum, + }; + + let reserved = u32be::from_be_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]); + + let target_addr = Ipv6Addr::new( + u16::from_be_bytes([bytes[8], bytes[9]]), + u16::from_be_bytes([bytes[10], bytes[11]]), + u16::from_be_bytes([bytes[12], bytes[13]]), + u16::from_be_bytes([bytes[14], bytes[15]]), + u16::from_be_bytes([bytes[16], bytes[17]]), + u16::from_be_bytes([bytes[18], bytes[19]]), + u16::from_be_bytes([bytes[20], bytes[21]]), + u16::from_be_bytes([bytes[22], bytes[23]]), + ); + + let dest_addr = Ipv6Addr::new( + u16::from_be_bytes([bytes[24], bytes[25]]), + u16::from_be_bytes([bytes[26], bytes[27]]), + u16::from_be_bytes([bytes[28], bytes[29]]), + u16::from_be_bytes([bytes[30], bytes[31]]), + u16::from_be_bytes([bytes[32], bytes[33]]), + u16::from_be_bytes([bytes[34], bytes[35]]), + u16::from_be_bytes([bytes[36], bytes[37]]), + u16::from_be_bytes([bytes[38], bytes[39]]), + ); + + let mut options = Vec::new(); + let mut i = 40; + while i + 2 <= bytes.len() { + let option_type = NdpOptionType::new(bytes[i]); + let length = bytes[i + 1]; + let option_len = (length as usize) * 8; + + if option_len < 2 || i + option_len > bytes.len() { + break; + } - let mut options = Vec::new(); - let mut i = 40; - while i + 2 <= bytes.len() { - let option_type = NdpOptionType::new(bytes[i]); - let length = bytes[i + 1]; - let option_len = (length as usize) * 8; + let payload = Bytes::copy_from_slice(&bytes[i + 2..i + option_len]); + options.push(NdpOptionPacket { + option_type, + length, + payload, + }); - if option_len < 2 || i + option_len > bytes.len() { - break; + i += option_len; } - let payload = Bytes::copy_from_slice(&bytes[i + 2..i + option_len]); - options.push(NdpOptionPacket { - option_type, - length, - payload, - }); - - i += option_len; - } + let payload = Bytes::copy_from_slice(&bytes[i..]); - let payload = Bytes::copy_from_slice(&bytes[i..]); - - Some(RedirectPacket { - header, - reserved, - target_addr, - dest_addr, - options, - payload, + Some(RedirectPacket { + header, + reserved, + target_addr, + dest_addr, + options, + payload, + }) + })() + .ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), }) } - fn from_bytes(bytes: Bytes) -> Option { - Self::from_buf(&bytes) + fn try_from_bytes(bytes: Bytes) -> Result { + Self::from_buf(&bytes).ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), + }) } fn to_bytes(&self) -> Bytes { let mut bytes = Vec::with_capacity(NDP_REDIRECT_PACKET_LEN); @@ -2225,28 +2295,35 @@ pub mod echo_request { impl Packet for EchoRequestPacket { type Header = (); - fn from_buf(bytes: &[u8]) -> Option { - if bytes.len() < 8 { - return None; - } - let icmpv6_type = Icmpv6Type::new(bytes[0]); - let icmpv6_code = Icmpv6Code::new(bytes[1]); - let checksum = u16::from_be_bytes([bytes[2], bytes[3]]); - let identifier = u16::from_be_bytes([bytes[4], bytes[5]]); - let sequence_number = u16::from_be_bytes([bytes[6], bytes[7]]); - Some(EchoRequestPacket { - header: Icmpv6Header { - icmpv6_type, - icmpv6_code, - checksum, - }, - identifier, - sequence_number, - payload: Bytes::copy_from_slice(&bytes[8..]), + fn try_from_buf(bytes: &[u8]) -> Result { + (|| -> Option { + if bytes.len() < 8 { + return None; + } + let icmpv6_type = Icmpv6Type::new(bytes[0]); + let icmpv6_code = Icmpv6Code::new(bytes[1]); + let checksum = u16::from_be_bytes([bytes[2], bytes[3]]); + let identifier = u16::from_be_bytes([bytes[4], bytes[5]]); + let sequence_number = u16::from_be_bytes([bytes[6], bytes[7]]); + Some(EchoRequestPacket { + header: Icmpv6Header { + icmpv6_type, + icmpv6_code, + checksum, + }, + identifier, + sequence_number, + payload: Bytes::copy_from_slice(&bytes[8..]), + }) + })() + .ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), }) } - fn from_bytes(bytes: Bytes) -> Option { - Self::from_buf(&bytes) + fn try_from_bytes(bytes: Bytes) -> Result { + Self::from_buf(&bytes).ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), + }) } fn to_bytes(&self) -> Bytes { @@ -2375,28 +2452,35 @@ pub mod echo_reply { } impl Packet for EchoReplyPacket { type Header = (); - fn from_buf(bytes: &[u8]) -> Option { - if bytes.len() < 8 { - return None; - } - let icmpv6_type = Icmpv6Type::new(bytes[0]); - let icmpv6_code = Icmpv6Code::new(bytes[1]); - let checksum = u16::from_be_bytes([bytes[2], bytes[3]]); - let identifier = u16::from_be_bytes([bytes[4], bytes[5]]); - let sequence_number = u16::from_be_bytes([bytes[6], bytes[7]]); - Some(EchoReplyPacket { - header: Icmpv6Header { - icmpv6_type, - icmpv6_code, - checksum, - }, - identifier, - sequence_number, - payload: Bytes::copy_from_slice(&bytes[8..]), + fn try_from_buf(bytes: &[u8]) -> Result { + (|| -> Option { + if bytes.len() < 8 { + return None; + } + let icmpv6_type = Icmpv6Type::new(bytes[0]); + let icmpv6_code = Icmpv6Code::new(bytes[1]); + let checksum = u16::from_be_bytes([bytes[2], bytes[3]]); + let identifier = u16::from_be_bytes([bytes[4], bytes[5]]); + let sequence_number = u16::from_be_bytes([bytes[6], bytes[7]]); + Some(EchoReplyPacket { + header: Icmpv6Header { + icmpv6_type, + icmpv6_code, + checksum, + }, + identifier, + sequence_number, + payload: Bytes::copy_from_slice(&bytes[8..]), + }) + })() + .ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), }) } - fn from_bytes(bytes: Bytes) -> Option { - Self::from_buf(&bytes) + fn try_from_bytes(bytes: Bytes) -> Result { + Self::from_buf(&bytes).ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), + }) } fn to_bytes(&self) -> Bytes { diff --git a/nex-packet/src/ip.rs b/nex-packet/src/ip.rs index 3eb3a71..36c0a51 100644 --- a/nex-packet/src/ip.rs +++ b/nex-packet/src/ip.rs @@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize}; #[repr(u8)] #[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[non_exhaustive] pub enum IpNextProtocol { /// IPv6 Hop-by-Hop Option \[RFC2460\] Hopopt = 0, diff --git a/nex-packet/src/ipv4.rs b/nex-packet/src/ipv4.rs index 60a47c2..f243f3b 100644 --- a/nex-packet/src/ipv4.rs +++ b/nex-packet/src/ipv4.rs @@ -35,6 +35,7 @@ pub mod Ipv4Flags { #[repr(u8)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[non_exhaustive] pub enum Ipv4OptionType { /// End of Options List EOL = 0, @@ -211,12 +212,20 @@ pub struct Ipv4Packet { impl Packet for Ipv4Packet { type Header = Ipv4Header; - fn from_buf(bytes: &[u8]) -> Option { - Self::try_from_buf(bytes).ok() + fn try_from_buf(bytes: &[u8]) -> Result { + Self::try_from_buf(bytes) + .ok() + .ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), + }) } - fn from_bytes(bytes: Bytes) -> Option { - Self::try_from_bytes(bytes).ok() + fn try_from_bytes(bytes: Bytes) -> Result { + Self::try_from_bytes(bytes) + .ok() + .ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), + }) } fn to_bytes(&self) -> Bytes { @@ -589,6 +598,11 @@ impl<'a> MutablePacket<'a> for MutableIpv4Packet<'a> { impl<'a> MutableIpv4Packet<'a> { /// Create a mutable packet without validating the header fields. + /// + /// # Safety + /// + /// `buffer` must contain a complete IPv4 header whose IHL and total-length + /// fields fit in the slice. Prefer [`MutablePacket::new`]. pub fn new_unchecked(buffer: &'a mut [u8]) -> Self { Self { buffer, @@ -688,9 +702,14 @@ impl<'a> MutableIpv4Packet<'a> { } /// Retrieve the version field. - pub fn get_version(&self) -> u8 { + pub fn version(&self) -> u8 { self.raw()[0] >> 4 } + /// Deprecated compatibility alias for version. + #[deprecated(note = "use version")] + pub fn get_version(&self) -> u8 { + self.version() + } /// Update the version field. pub fn set_version(&mut self, version: u8) { @@ -700,9 +719,14 @@ impl<'a> MutableIpv4Packet<'a> { } /// Retrieve the header length in 32-bit words. - pub fn get_header_length(&self) -> u8 { + pub fn header_length(&self) -> u8 { self.raw()[0] & 0x0F } + /// Deprecated compatibility alias for header_length. + #[deprecated(note = "use header_length")] + pub fn get_header_length(&self) -> u8 { + self.header_length() + } /// Update the header length in 32-bit words. pub fn set_header_length(&mut self, ihl: u8) { @@ -712,9 +736,14 @@ impl<'a> MutableIpv4Packet<'a> { } /// Retrieve the DSCP field. - pub fn get_dscp(&self) -> u8 { + pub fn dscp(&self) -> u8 { self.raw()[1] >> 2 } + /// Deprecated compatibility alias for dscp. + #[deprecated(note = "use dscp")] + pub fn get_dscp(&self) -> u8 { + self.dscp() + } /// Update the DSCP field. pub fn set_dscp(&mut self, dscp: u8) { @@ -724,9 +753,14 @@ impl<'a> MutableIpv4Packet<'a> { } /// Retrieve the ECN field. - pub fn get_ecn(&self) -> u8 { + pub fn ecn(&self) -> u8 { self.raw()[1] & 0x03 } + /// Deprecated compatibility alias for ecn. + #[deprecated(note = "use ecn")] + pub fn get_ecn(&self) -> u8 { + self.ecn() + } /// Update the ECN field. pub fn set_ecn(&mut self, ecn: u8) { @@ -736,9 +770,14 @@ impl<'a> MutableIpv4Packet<'a> { } /// Retrieve the total length field. - pub fn get_total_length(&self) -> u16 { + pub fn total_length(&self) -> u16 { u16::from_be_bytes([self.raw()[2], self.raw()[3]]) } + /// Deprecated compatibility alias for total_length. + #[deprecated(note = "use total_length")] + pub fn get_total_length(&self) -> u16 { + self.total_length() + } /// Update the total length field. pub fn set_total_length(&mut self, len: u16) { @@ -747,9 +786,14 @@ impl<'a> MutableIpv4Packet<'a> { } /// Retrieve the identification field. - pub fn get_identification(&self) -> u16 { + pub fn identification(&self) -> u16 { u16::from_be_bytes([self.raw()[4], self.raw()[5]]) } + /// Deprecated compatibility alias for identification. + #[deprecated(note = "use identification")] + pub fn get_identification(&self) -> u16 { + self.identification() + } /// Update the identification field. pub fn set_identification(&mut self, id: u16) { @@ -758,9 +802,14 @@ impl<'a> MutableIpv4Packet<'a> { } /// Retrieve the flags field. - pub fn get_flags(&self) -> u8 { + pub fn flags(&self) -> u8 { (self.raw()[6] & 0xE0) >> 5 } + /// Deprecated compatibility alias for flags. + #[deprecated(note = "use flags")] + pub fn get_flags(&self) -> u8 { + self.flags() + } /// Update the flags field. pub fn set_flags(&mut self, flags: u8) { @@ -770,9 +819,14 @@ impl<'a> MutableIpv4Packet<'a> { } /// Retrieve the fragment offset field. - pub fn get_fragment_offset(&self) -> u16 { + pub fn fragment_offset(&self) -> u16 { u16::from_be_bytes([self.raw()[6], self.raw()[7]]) & 0x1FFF } + /// Deprecated compatibility alias for fragment_offset. + #[deprecated(note = "use fragment_offset")] + pub fn get_fragment_offset(&self) -> u16 { + self.fragment_offset() + } /// Update the fragment offset field. pub fn set_fragment_offset(&mut self, offset: u16) { @@ -783,9 +837,14 @@ impl<'a> MutableIpv4Packet<'a> { } /// Retrieve the TTL field. - pub fn get_ttl(&self) -> u8 { + pub fn ttl(&self) -> u8 { self.raw()[8] } + /// Deprecated compatibility alias for ttl. + #[deprecated(note = "use ttl")] + pub fn get_ttl(&self) -> u8 { + self.ttl() + } /// Update the TTL field. pub fn set_ttl(&mut self, ttl: u8) { @@ -794,9 +853,14 @@ impl<'a> MutableIpv4Packet<'a> { } /// Retrieve the next-level protocol field. - pub fn get_next_level_protocol(&self) -> IpNextProtocol { + pub fn next_level_protocol(&self) -> IpNextProtocol { IpNextProtocol::new(self.raw()[9]) } + /// Deprecated compatibility alias for next_level_protocol. + #[deprecated(note = "use next_level_protocol")] + pub fn get_next_level_protocol(&self) -> IpNextProtocol { + self.next_level_protocol() + } /// Update the next-level protocol field. pub fn set_next_level_protocol(&mut self, proto: IpNextProtocol) { @@ -805,9 +869,14 @@ impl<'a> MutableIpv4Packet<'a> { } /// Retrieve the checksum field. - pub fn get_checksum(&self) -> u16 { + pub fn checksum(&self) -> u16 { u16::from_be_bytes([self.raw()[10], self.raw()[11]]) } + /// Deprecated compatibility alias for checksum. + #[deprecated(note = "use checksum")] + pub fn get_checksum(&self) -> u16 { + self.checksum() + } /// Update the checksum field. pub fn set_checksum(&mut self, checksum: u16) { @@ -816,7 +885,7 @@ impl<'a> MutableIpv4Packet<'a> { } /// Retrieve the source address. - pub fn get_source(&self) -> Ipv4Addr { + pub fn source(&self) -> Ipv4Addr { Ipv4Addr::new( self.raw()[12], self.raw()[13], @@ -824,6 +893,11 @@ impl<'a> MutableIpv4Packet<'a> { self.raw()[15], ) } + /// Deprecated compatibility alias for source. + #[deprecated(note = "use source")] + pub fn get_source(&self) -> Ipv4Addr { + self.source() + } /// Update the source address. pub fn set_source(&mut self, addr: Ipv4Addr) { @@ -832,7 +906,7 @@ impl<'a> MutableIpv4Packet<'a> { } /// Retrieve the destination address. - pub fn get_destination(&self) -> Ipv4Addr { + pub fn destination(&self) -> Ipv4Addr { Ipv4Addr::new( self.raw()[16], self.raw()[17], @@ -840,6 +914,11 @@ impl<'a> MutableIpv4Packet<'a> { self.raw()[19], ) } + /// Deprecated compatibility alias for destination. + #[deprecated(note = "use destination")] + pub fn get_destination(&self) -> Ipv4Addr { + self.destination() + } /// Update the destination address. pub fn set_destination(&mut self, addr: Ipv4Addr) { diff --git a/nex-packet/src/ipv6.rs b/nex-packet/src/ipv6.rs index b79617c..e37a105 100644 --- a/nex-packet/src/ipv6.rs +++ b/nex-packet/src/ipv6.rs @@ -32,11 +32,19 @@ pub struct Ipv6Packet { impl Packet for Ipv6Packet { type Header = Ipv6Header; - fn from_buf(bytes: &[u8]) -> Option { - Self::try_from_buf(bytes).ok() + fn try_from_buf(bytes: &[u8]) -> Result { + Self::try_from_buf(bytes) + .ok() + .ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), + }) } - fn from_bytes(bytes: Bytes) -> Option { - Self::try_from_bytes(bytes).ok() + fn try_from_bytes(bytes: Bytes) -> Result { + Self::try_from_bytes(bytes) + .ok() + .ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), + }) } fn to_bytes(&self) -> Bytes { @@ -187,9 +195,14 @@ impl Ipv6Packet { + self.extensions.iter().map(|ext| ext.len()).sum::() + self.payload.len() } - pub fn get_extension(&self, kind: ExtensionHeaderType) -> Option<&Ipv6ExtensionHeader> { + pub fn extension(&self, kind: ExtensionHeaderType) -> Option<&Ipv6ExtensionHeader> { self.extensions.iter().find(|ext| ext.kind() == kind) } + /// Deprecated compatibility alias for extension. + #[deprecated(note = "use extension")] + pub fn get_extension(&self, kind: ExtensionHeaderType) -> Option<&Ipv6ExtensionHeader> { + self.extension(kind) + } } fn parse_ipv6_from_slice(bytes: &[u8], strict: bool) -> Result { @@ -293,14 +306,10 @@ where }); } let data = slice_bytes(offset + 2..offset + total_len); - let ext = match next_header { - IpNextProtocol::Hopopt => { - Ipv6ExtensionHeader::HopByHop { next: nh, data } - } - IpNextProtocol::Ipv6Opts => { - Ipv6ExtensionHeader::Destination { next: nh, data } - } - _ => unreachable!(), + let ext = if next_header == IpNextProtocol::Hopopt { + Ipv6ExtensionHeader::HopByHop { next: nh, data } + } else { + Ipv6ExtensionHeader::Destination { next: nh, data } }; extensions.push(ext); next_header = nh; @@ -360,7 +369,7 @@ where next_header = nh; offset += 8; } - _ => unreachable!(), + _ => break, } } _ => break, @@ -419,6 +428,11 @@ impl<'a> MutablePacket<'a> for MutableIpv6Packet<'a> { impl<'a> MutableIpv6Packet<'a> { /// Create a new packet without checking length. + /// + /// # Safety + /// + /// `buffer` must contain a complete IPv6 base header and all declared + /// extension headers. Prefer [`MutablePacket::new`]. pub fn new_unchecked(buffer: &'a mut [u8]) -> Self { Self { buffer } } @@ -435,18 +449,28 @@ impl<'a> MutableIpv6Packet<'a> { self.raw().len().saturating_sub(IPV6_HEADER_LEN) } - pub fn get_version(&self) -> u8 { + pub fn version(&self) -> u8 { self.raw()[0] >> 4 } + /// Deprecated compatibility alias for version. + #[deprecated(note = "use version")] + pub fn get_version(&self) -> u8 { + self.version() + } pub fn set_version(&mut self, version: u8) { let buf = self.raw_mut(); buf[0] = (buf[0] & 0x0F) | ((version & 0x0F) << 4); } - pub fn get_traffic_class(&self) -> u8 { + pub fn traffic_class(&self) -> u8 { ((self.raw()[0] & 0x0F) << 4) | (self.raw()[1] >> 4) } + /// Deprecated compatibility alias for traffic_class. + #[deprecated(note = "use traffic_class")] + pub fn get_traffic_class(&self) -> u8 { + self.traffic_class() + } pub fn set_traffic_class(&mut self, class: u8) { let buf = self.raw_mut(); @@ -454,13 +478,18 @@ impl<'a> MutableIpv6Packet<'a> { buf[1] = (buf[1] & 0x0F) | ((class & 0x0F) << 4); } - pub fn get_flow_label(&self) -> u32 { + pub fn flow_label(&self) -> u32 { let buf = self.raw(); let high = (buf[1] as u32 & 0x0F) << 16; let mid = (buf[2] as u32) << 8; let low = buf[3] as u32; high | mid | low } + /// Deprecated compatibility alias for flow_label. + #[deprecated(note = "use flow_label")] + pub fn get_flow_label(&self) -> u32 { + self.flow_label() + } pub fn set_flow_label(&mut self, label: u32) { let buf = self.raw_mut(); @@ -469,49 +498,74 @@ impl<'a> MutableIpv6Packet<'a> { buf[3] = label as u8; } - pub fn get_payload_length(&self) -> u16 { + pub fn payload_length(&self) -> u16 { u16::from_be_bytes([self.raw()[4], self.raw()[5]]) } + /// Deprecated compatibility alias for payload_length. + #[deprecated(note = "use payload_length")] + pub fn get_payload_length(&self) -> u16 { + self.payload_length() + } pub fn set_payload_length(&mut self, length: u16) { self.raw_mut()[4..6].copy_from_slice(&length.to_be_bytes()); } - pub fn get_next_header(&self) -> IpNextProtocol { + pub fn next_header(&self) -> IpNextProtocol { IpNextProtocol::new(self.raw()[6]) } + /// Deprecated compatibility alias for next_header. + #[deprecated(note = "use next_header")] + pub fn get_next_header(&self) -> IpNextProtocol { + self.next_header() + } pub fn set_next_header(&mut self, proto: IpNextProtocol) { self.raw_mut()[6] = proto.value(); } - pub fn get_hop_limit(&self) -> u8 { + pub fn hop_limit(&self) -> u8 { self.raw()[7] } + /// Deprecated compatibility alias for hop_limit. + #[deprecated(note = "use hop_limit")] + pub fn get_hop_limit(&self) -> u8 { + self.hop_limit() + } pub fn set_hop_limit(&mut self, value: u8) { self.raw_mut()[7] = value; } - pub fn get_source(&self) -> Ipv6Addr { + pub fn source(&self) -> Ipv6Addr { let raw = self.raw(); Ipv6Addr::from([ raw[8], raw[9], raw[10], raw[11], raw[12], raw[13], raw[14], raw[15], raw[16], raw[17], raw[18], raw[19], raw[20], raw[21], raw[22], raw[23], ]) } + /// Deprecated compatibility alias for source. + #[deprecated(note = "use source")] + pub fn get_source(&self) -> Ipv6Addr { + self.source() + } pub fn set_source(&mut self, addr: Ipv6Addr) { self.raw_mut()[8..24].copy_from_slice(&addr.octets()); } - pub fn get_destination(&self) -> Ipv6Addr { + pub fn destination(&self) -> Ipv6Addr { let raw = self.raw(); Ipv6Addr::from([ raw[24], raw[25], raw[26], raw[27], raw[28], raw[29], raw[30], raw[31], raw[32], raw[33], raw[34], raw[35], raw[36], raw[37], raw[38], raw[39], ]) } + /// Deprecated compatibility alias for destination. + #[deprecated(note = "use destination")] + pub fn get_destination(&self) -> Ipv6Addr { + self.destination() + } pub fn set_destination(&mut self, addr: Ipv6Addr) { self.raw_mut()[24..40].copy_from_slice(&addr.octets()); @@ -519,6 +573,7 @@ impl<'a> MutableIpv6Packet<'a> { } #[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] pub enum ExtensionHeaderType { HopByHop, Destination, @@ -528,6 +583,7 @@ pub enum ExtensionHeaderType { } #[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] pub enum Ipv6ExtensionHeader { HopByHop { next: IpNextProtocol, diff --git a/nex-packet/src/lib.rs b/nex-packet/src/lib.rs index 4e4b363..9f9c516 100644 --- a/nex-packet/src/lib.rs +++ b/nex-packet/src/lib.rs @@ -1,4 +1,5 @@ //! Low-level packet parsing and serialization primitives for common network protocols. +#![allow(deprecated)] pub mod arp; pub mod builder; diff --git a/nex-packet/src/packet.rs b/nex-packet/src/packet.rs index 7d650d2..8bce6f6 100644 --- a/nex-packet/src/packet.rs +++ b/nex-packet/src/packet.rs @@ -5,11 +5,23 @@ use std::marker::PhantomData; pub trait Packet: Sized { type Header; - /// Parse from a byte slice. - fn from_buf(buf: &[u8]) -> Option; + /// Parse from a borrowed byte slice with structured diagnostics. + fn try_from_buf(buf: &[u8]) -> Result; - /// Parse from raw bytes. (with ownership) - fn from_bytes(bytes: Bytes) -> Option; + /// Parse from owned bytes with structured diagnostics. + fn try_from_bytes(bytes: Bytes) -> Result; + + /// Parse from a byte slice, discarding structured diagnostics. + #[deprecated(note = "use Packet::try_from_buf or the packet type's inherent try_from_buf")] + fn from_buf(buf: &[u8]) -> Option { + Self::try_from_buf(buf).ok() + } + + /// Parse from owned bytes, discarding structured diagnostics. + #[deprecated(note = "use Packet::try_from_bytes or the packet type's inherent try_from_bytes")] + fn from_bytes(bytes: Bytes) -> Option { + Self::try_from_bytes(bytes).ok() + } /// Serialize into raw bytes. fn to_bytes(&self) -> Bytes; @@ -91,7 +103,7 @@ pub trait MutablePacket<'a>: Sized { /// Convert the mutable packet into its immutable counterpart. fn freeze(&self) -> Option { - Self::Packet::from_buf(self.packet()) + Self::Packet::try_from_buf(self.packet()).ok() } } @@ -106,7 +118,7 @@ impl<'a, P: Packet> MutablePacket<'a> for GenericMutablePacket<'a, P> { type Packet = P; fn new(buffer: &'a mut [u8]) -> Option { - P::from_buf(buffer)?; + P::try_from_buf(buffer).ok()?; Some(Self { buffer, _marker: PhantomData, @@ -145,7 +157,14 @@ impl<'a, P: Packet> MutablePacket<'a> for GenericMutablePacket<'a, P> { } impl<'a, P: Packet> GenericMutablePacket<'a, P> { - /// Construct a mutable packet without running additional validation. + /// Construct a mutable packet without validating the buffer. + /// + /// # Safety + /// + /// Although this function is safe to call for compatibility, the caller + /// must ensure `buffer` contains a structurally valid `P` before calling + /// accessors. Invalid header lengths may otherwise cause accessor panics. + /// Prefer [`MutablePacket::new`], which validates this invariant. pub fn new_unchecked(buffer: &'a mut [u8]) -> Self { Self { buffer, @@ -154,8 +173,8 @@ impl<'a, P: Packet> GenericMutablePacket<'a, P> { } fn lengths(&self) -> (usize, usize) { - match P::from_buf(self.packet()) { - Some(packet) => { + match P::try_from_buf(self.packet()) { + Ok(packet) => { let header_len = packet.header_len(); let payload_len = packet.payload_len(); (header_len, payload_len) diff --git a/nex-packet/src/parse.rs b/nex-packet/src/parse.rs index b16951d..6ff3c70 100644 --- a/nex-packet/src/parse.rs +++ b/nex-packet/src/parse.rs @@ -17,16 +17,20 @@ //! | `from_bytes_strict(input)` | `try_from_bytes_with_mode(input, ParseMode::Strict).ok()` | //! | `EthernetHeader::from_bytes(input)` | `EthernetHeader::try_from_bytes(input)` | //! | `DnsName::from_bytes(input)` | `DnsName::try_from_bytes(input)` | +//! | `DnsQueryPacket::get_qname_parsed()` | `DnsQueryPacket::qname_parsed()` | +//! | `DnsQueryPacket::try_get_qname_parsed()` | `DnsQueryPacket::qname_parsed()` | //! | DNS section `from_buf_mut(cursor)` helpers | `DnsPacket::try_from_buf(input)` | //! -//! The `Option`-returning methods on [`crate::packet::Packet`] remain temporary -//! compatibility shims. New code should use the inherent `try_from_*` methods so -//! malformed input retains diagnostic context. +//! The `Option`-returning methods on [`crate::packet::Packet`] are deprecated +//! compatibility shims. Every `Packet` implementor also receives the canonical +//! `try_from_buf` and `try_from_bytes` methods through the trait. Protocols with +//! richer validation override these with more specific error contexts. use core::fmt; /// Controls validation behavior for parsers with length-delimited payloads. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +#[non_exhaustive] pub enum ParseMode { /// Preserve captured payload bytes when a declared length is missing or incomplete. #[default] @@ -129,3 +133,15 @@ impl fmt::Display for ParseError { } impl std::error::Error for ParseError {} + +#[cfg(test)] +mod tests { + use super::ParseError; + + fn assert_error_contract() {} + + #[test] + fn parse_error_implements_public_error_contract() { + assert_error_contract::(); + } +} diff --git a/nex-packet/src/tcp.rs b/nex-packet/src/tcp.rs index ae1a2d2..fe9c15c 100644 --- a/nex-packet/src/tcp.rs +++ b/nex-packet/src/tcp.rs @@ -29,6 +29,7 @@ pub const TCP_HEADER_MAX_LEN: usize = TCP_HEADER_LEN + TCP_OPTION_MAX_LEN; #[repr(u8)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[non_exhaustive] pub enum TcpOptionKind { EOL = 0, NOP = 1, @@ -280,7 +281,7 @@ pub struct TcpOptionHeader { impl TcpOptionHeader { /// Get the timestamp of the TCP option - pub fn get_timestamp(&self) -> (u32, u32) { + pub fn timestamp_value(&self) -> (u32, u32) { if self.kind == TcpOptionKind::TIMESTAMPS && self.data.len() >= 8 { let mut my: [u8; 4] = [0; 4]; my.copy_from_slice(&self.data[0..4]); @@ -291,8 +292,13 @@ impl TcpOptionHeader { (0, 0) } } + /// Deprecated compatibility alias for timestamp_value. + #[deprecated(note = "use timestamp_value")] + pub fn get_timestamp(&self) -> (u32, u32) { + self.timestamp_value() + } /// Get the MSS of the TCP option - pub fn get_mss(&self) -> u16 { + pub fn maximum_segment_size(&self) -> u16 { if self.kind == TcpOptionKind::MSS && self.data.len() >= 2 { let mut mss: [u8; 2] = [0; 2]; mss.copy_from_slice(&self.data[0..2]); @@ -301,14 +307,24 @@ impl TcpOptionHeader { 0 } } + /// Deprecated compatibility alias for maximum_segment_size. + #[deprecated(note = "use maximum_segment_size")] + pub fn get_mss(&self) -> u16 { + self.maximum_segment_size() + } /// Get the WSCALE of the TCP option - pub fn get_wscale(&self) -> u8 { + pub fn window_scale(&self) -> u8 { if self.kind == TcpOptionKind::WSCALE && !self.data.is_empty() { self.data[0] } else { 0 } } + /// Deprecated compatibility alias for window_scale. + #[deprecated(note = "use window_scale")] + pub fn get_wscale(&self) -> u8 { + self.window_scale() + } } /// A TCP option. @@ -414,7 +430,7 @@ impl TcpOptionPacket { self.length.unwrap_or_default() } /// Get the timestamp of the TCP option - pub fn get_timestamp(&self) -> (u32, u32) { + pub fn timestamp_value(&self) -> (u32, u32) { if self.kind == TcpOptionKind::TIMESTAMPS && self.data.len() >= 8 { let mut my: [u8; 4] = [0; 4]; my.copy_from_slice(&self.data[0..4]); @@ -425,8 +441,13 @@ impl TcpOptionPacket { (0, 0) } } + /// Deprecated compatibility alias for timestamp_value. + #[deprecated(note = "use timestamp_value")] + pub fn get_timestamp(&self) -> (u32, u32) { + self.timestamp_value() + } /// Get the MSS of the TCP option - pub fn get_mss(&self) -> u16 { + pub fn maximum_segment_size(&self) -> u16 { if self.kind == TcpOptionKind::MSS && self.data.len() >= 2 { let mut mss: [u8; 2] = [0; 2]; mss.copy_from_slice(&self.data[0..2]); @@ -435,14 +456,24 @@ impl TcpOptionPacket { 0 } } + /// Deprecated compatibility alias for maximum_segment_size. + #[deprecated(note = "use maximum_segment_size")] + pub fn get_mss(&self) -> u16 { + self.maximum_segment_size() + } /// Get the WSCALE of the TCP option - pub fn get_wscale(&self) -> u8 { + pub fn window_scale(&self) -> u8 { if self.kind == TcpOptionKind::WSCALE && !self.data.is_empty() { self.data[0] } else { 0 } } + /// Deprecated compatibility alias for window_scale. + #[deprecated(note = "use window_scale")] + pub fn get_wscale(&self) -> u8 { + self.window_scale() + } } /// Represents the TCP header. @@ -472,11 +503,19 @@ pub struct TcpPacket { impl Packet for TcpPacket { type Header = TcpHeader; - fn from_buf(bytes: &[u8]) -> Option { - Self::try_from_buf(bytes).ok() + fn try_from_buf(bytes: &[u8]) -> Result { + Self::try_from_buf(bytes) + .ok() + .ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), + }) } - fn from_bytes(mut bytes: Bytes) -> Option { - Self::try_from_bytes(bytes.split_to(bytes.len())).ok() + fn try_from_bytes(mut bytes: Bytes) -> Result { + Self::try_from_bytes(bytes.split_to(bytes.len())) + .ok() + .ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), + }) } fn to_bytes(&self) -> Bytes { @@ -870,6 +909,11 @@ impl<'a> MutablePacket<'a> for MutableTcpPacket<'a> { impl<'a> MutableTcpPacket<'a> { /// Create a packet without validating the header fields. + /// + /// # Safety + /// + /// `buffer` must contain a complete TCP header whose data-offset field fits + /// in the slice. Prefer [`MutablePacket::new`]. pub fn new_unchecked(buffer: &'a mut [u8]) -> Self { Self { buffer, @@ -1007,45 +1051,70 @@ impl<'a> MutableTcpPacket<'a> { self.raw().len().saturating_sub(self.header_len()) } - pub fn get_source(&self) -> u16 { + pub fn source(&self) -> u16 { u16::from_be_bytes([self.raw()[0], self.raw()[1]]) } + /// Deprecated compatibility alias for source. + #[deprecated(note = "use source")] + pub fn get_source(&self) -> u16 { + self.source() + } pub fn set_source(&mut self, value: u16) { self.raw_mut()[0..2].copy_from_slice(&value.to_be_bytes()); self.after_field_mutation(); } - pub fn get_destination(&self) -> u16 { + pub fn destination(&self) -> u16 { u16::from_be_bytes([self.raw()[2], self.raw()[3]]) } + /// Deprecated compatibility alias for destination. + #[deprecated(note = "use destination")] + pub fn get_destination(&self) -> u16 { + self.destination() + } pub fn set_destination(&mut self, value: u16) { self.raw_mut()[2..4].copy_from_slice(&value.to_be_bytes()); self.after_field_mutation(); } - pub fn get_sequence(&self) -> u32 { + pub fn sequence(&self) -> u32 { u32::from_be_bytes([self.raw()[4], self.raw()[5], self.raw()[6], self.raw()[7]]) } + /// Deprecated compatibility alias for sequence. + #[deprecated(note = "use sequence")] + pub fn get_sequence(&self) -> u32 { + self.sequence() + } pub fn set_sequence(&mut self, value: u32) { self.raw_mut()[4..8].copy_from_slice(&value.to_be_bytes()); self.after_field_mutation(); } - pub fn get_acknowledgement(&self) -> u32 { + pub fn acknowledgement(&self) -> u32 { u32::from_be_bytes([self.raw()[8], self.raw()[9], self.raw()[10], self.raw()[11]]) } + /// Deprecated compatibility alias for acknowledgement. + #[deprecated(note = "use acknowledgement")] + pub fn get_acknowledgement(&self) -> u32 { + self.acknowledgement() + } pub fn set_acknowledgement(&mut self, value: u32) { self.raw_mut()[8..12].copy_from_slice(&value.to_be_bytes()); self.after_field_mutation(); } - pub fn get_data_offset(&self) -> u8 { + pub fn data_offset(&self) -> u8 { self.raw()[12] >> 4 } + /// Deprecated compatibility alias for data_offset. + #[deprecated(note = "use data_offset")] + pub fn get_data_offset(&self) -> u8 { + self.data_offset() + } pub fn set_data_offset(&mut self, offset: u8) { let buf = self.raw_mut(); @@ -1053,9 +1122,14 @@ impl<'a> MutableTcpPacket<'a> { self.after_field_mutation(); } - pub fn get_reserved(&self) -> u8 { + pub fn reserved(&self) -> u8 { self.raw()[12] & 0x0F } + /// Deprecated compatibility alias for reserved. + #[deprecated(note = "use reserved")] + pub fn get_reserved(&self) -> u8 { + self.reserved() + } pub fn set_reserved(&mut self, value: u8) { let buf = self.raw_mut(); @@ -1063,36 +1137,56 @@ impl<'a> MutableTcpPacket<'a> { self.after_field_mutation(); } - pub fn get_flags(&self) -> u8 { + pub fn flags(&self) -> u8 { self.raw()[13] } + /// Deprecated compatibility alias for flags. + #[deprecated(note = "use flags")] + pub fn get_flags(&self) -> u8 { + self.flags() + } pub fn set_flags(&mut self, flags: u8) { self.raw_mut()[13] = flags; self.after_field_mutation(); } - pub fn get_window(&self) -> u16 { + pub fn window(&self) -> u16 { u16::from_be_bytes([self.raw()[14], self.raw()[15]]) } + /// Deprecated compatibility alias for window. + #[deprecated(note = "use window")] + pub fn get_window(&self) -> u16 { + self.window() + } pub fn set_window(&mut self, value: u16) { self.raw_mut()[14..16].copy_from_slice(&value.to_be_bytes()); self.after_field_mutation(); } - pub fn get_checksum(&self) -> u16 { + pub fn checksum(&self) -> u16 { u16::from_be_bytes([self.raw()[16], self.raw()[17]]) } + /// Deprecated compatibility alias for checksum. + #[deprecated(note = "use checksum")] + pub fn get_checksum(&self) -> u16 { + self.checksum() + } pub fn set_checksum(&mut self, value: u16) { self.write_checksum(value); self.checksum.clear_dirty(); } - pub fn get_urgent_ptr(&self) -> u16 { + pub fn urgent_ptr(&self) -> u16 { u16::from_be_bytes([self.raw()[18], self.raw()[19]]) } + /// Deprecated compatibility alias for urgent_ptr. + #[deprecated(note = "use urgent_ptr")] + pub fn get_urgent_ptr(&self) -> u16 { + self.urgent_ptr() + } pub fn set_urgent_ptr(&mut self, value: u16) { self.raw_mut()[18..20].copy_from_slice(&value.to_be_bytes()); diff --git a/nex-packet/src/udp.rs b/nex-packet/src/udp.rs index c146656..de39f29 100644 --- a/nex-packet/src/udp.rs +++ b/nex-packet/src/udp.rs @@ -35,11 +35,19 @@ pub struct UdpPacket { impl Packet for UdpPacket { type Header = UdpHeader; - fn from_buf(bytes: &[u8]) -> Option { - Self::try_from_buf(bytes).ok() - } - fn from_bytes(mut bytes: Bytes) -> Option { - Self::try_from_bytes(bytes.split_to(bytes.len())).ok() + fn try_from_buf(bytes: &[u8]) -> Result { + Self::try_from_buf(bytes) + .ok() + .ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), + }) + } + fn try_from_bytes(mut bytes: Bytes) -> Result { + Self::try_from_bytes(bytes.split_to(bytes.len())) + .ok() + .ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), + }) } fn to_bytes(&self) -> Bytes { let mut buf = BytesMut::with_capacity(UDP_HEADER_LEN + self.payload.len()); @@ -230,6 +238,11 @@ impl UdpPacket { impl<'a> MutableUdpPacket<'a> { /// Create a new packet without validating length fields. + /// + /// # Safety + /// + /// `buffer` must contain a complete UDP header and its declared length must + /// fit in the slice. Prefer [`MutablePacket::new`]. pub fn new_unchecked(buffer: &'a mut [u8]) -> Self { Self { buffer, @@ -370,36 +383,56 @@ impl<'a> MutableUdpPacket<'a> { self.total_len().saturating_sub(UDP_HEADER_LEN) } - pub fn get_source(&self) -> u16 { + pub fn source(&self) -> u16 { u16::from_be_bytes([self.raw()[0], self.raw()[1]]) } + /// Deprecated compatibility alias for source. + #[deprecated(note = "use source")] + pub fn get_source(&self) -> u16 { + self.source() + } pub fn set_source(&mut self, port: u16) { self.raw_mut()[0..2].copy_from_slice(&port.to_be_bytes()); self.after_field_mutation(); } - pub fn get_destination(&self) -> u16 { + pub fn destination(&self) -> u16 { u16::from_be_bytes([self.raw()[2], self.raw()[3]]) } + /// Deprecated compatibility alias for destination. + #[deprecated(note = "use destination")] + pub fn get_destination(&self) -> u16 { + self.destination() + } pub fn set_destination(&mut self, port: u16) { self.raw_mut()[2..4].copy_from_slice(&port.to_be_bytes()); self.after_field_mutation(); } - pub fn get_length(&self) -> u16 { + pub fn length(&self) -> u16 { u16::from_be_bytes([self.raw()[4], self.raw()[5]]) } + /// Deprecated compatibility alias for length. + #[deprecated(note = "use length")] + pub fn get_length(&self) -> u16 { + self.length() + } pub fn set_length(&mut self, length: u16) { self.raw_mut()[4..6].copy_from_slice(&length.to_be_bytes()); self.after_field_mutation(); } - pub fn get_checksum(&self) -> u16 { + pub fn checksum(&self) -> u16 { u16::from_be_bytes([self.raw()[6], self.raw()[7]]) } + /// Deprecated compatibility alias for checksum. + #[deprecated(note = "use checksum")] + pub fn get_checksum(&self) -> u16 { + self.checksum() + } pub fn set_checksum(&mut self, checksum: u16) { self.write_checksum(checksum); diff --git a/nex-packet/src/util.rs b/nex-packet/src/util.rs index ec2487c..7098e6d 100644 --- a/nex-packet/src/util.rs +++ b/nex-packet/src/util.rs @@ -202,9 +202,13 @@ mod tests { fn sum_be_words_misaligned_ptr() { let mut data = vec![0; 13]; let ptr = match data.as_ptr() as usize % 2 { + // SAFETY: The vector contains 13 bytes, so advancing by one still + // leaves the 12-byte range constructed below in bounds. 0 => unsafe { data.as_mut_ptr().offset(1) }, _ => data.as_mut_ptr(), }; + // SAFETY: `ptr` points into `data` with at least 12 writable bytes + // remaining for the lifetime of this test scope. unsafe { let slice_data = slice::from_raw_parts_mut(ptr, 12); for (i, byte) in slice_data.iter_mut().enumerate().take(11) { diff --git a/nex-packet/src/vlan.rs b/nex-packet/src/vlan.rs index 7e84763..0739684 100644 --- a/nex-packet/src/vlan.rs +++ b/nex-packet/src/vlan.rs @@ -16,6 +16,7 @@ pub const VLAN_HEADER_LEN: usize = 4; #[repr(u8)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[non_exhaustive] pub enum ClassOfService { // Background BK = 1, @@ -87,33 +88,40 @@ pub struct VlanPacket { impl Packet for VlanPacket { type Header = VlanHeader; - fn from_buf(mut bytes: &[u8]) -> Option { - if bytes.len() < VLAN_HEADER_LEN { - return None; - } - - // VLAN TCI - let tci = bytes.get_u16(); - let pcp = ClassOfService::new(((tci >> 13) & 0b111) as u8); - let drop_eligible_id = ((tci >> 12) & 0b1) as u1; - let vlan_id = (tci & 0x0FFF) as u12be; - - // EtherType - let ethertype = EtherType::new(bytes.get_u16()); - - // Payload - Some(VlanPacket { - header: VlanHeader { - priority_code_point: pcp, - drop_eligible_id, - vlan_id, - ethertype, - }, - payload: Bytes::copy_from_slice(bytes), + fn try_from_buf(mut bytes: &[u8]) -> Result { + (|| -> Option { + if bytes.len() < VLAN_HEADER_LEN { + return None; + } + + // VLAN TCI + let tci = bytes.get_u16(); + let pcp = ClassOfService::new(((tci >> 13) & 0b111) as u8); + let drop_eligible_id = ((tci >> 12) & 0b1) as u1; + let vlan_id = (tci & 0x0FFF) as u12be; + + // EtherType + let ethertype = EtherType::new(bytes.get_u16()); + + // Payload + Some(VlanPacket { + header: VlanHeader { + priority_code_point: pcp, + drop_eligible_id, + vlan_id, + ethertype, + }, + payload: Bytes::copy_from_slice(bytes), + }) + })() + .ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), }) } - fn from_bytes(bytes: Bytes) -> Option { - Self::from_buf(&bytes) + fn try_from_bytes(bytes: Bytes) -> Result { + Self::from_buf(&bytes).ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), + }) } fn to_bytes(&self) -> Bytes { @@ -213,6 +221,12 @@ impl<'a> MutablePacket<'a> for MutableVlanPacket<'a> { } impl<'a> MutableVlanPacket<'a> { + /// Create a mutable VLAN packet without validating its minimum length. + /// + /// # Safety + /// + /// `buffer` must contain a complete VLAN header before any field accessor + /// is called. Prefer [`MutablePacket::new`]. pub fn new_unchecked(buffer: &'a mut [u8]) -> Self { Self { buffer } } @@ -225,30 +239,45 @@ impl<'a> MutableVlanPacket<'a> { &mut *self.buffer } - pub fn get_priority_code_point(&self) -> ClassOfService { + pub fn priority_code_point(&self) -> ClassOfService { let first = self.raw()[0]; ClassOfService::new(first >> 5) } + /// Deprecated compatibility alias for priority_code_point. + #[deprecated(note = "use priority_code_point")] + pub fn get_priority_code_point(&self) -> ClassOfService { + self.priority_code_point() + } pub fn set_priority_code_point(&mut self, class: ClassOfService) { let buf = self.raw_mut(); buf[0] = (buf[0] & 0x1F) | ((class.value() & 0x07) << 5); } - pub fn get_drop_eligible_id(&self) -> u1 { + pub fn drop_eligible_id(&self) -> u1 { ((self.raw()[0] >> 4) & 0x01) as u1 } + /// Deprecated compatibility alias for drop_eligible_id. + #[deprecated(note = "use drop_eligible_id")] + pub fn get_drop_eligible_id(&self) -> u1 { + self.drop_eligible_id() + } pub fn set_drop_eligible_id(&mut self, dei: u1) { let buf = self.raw_mut(); buf[0] = (buf[0] & !(1 << 4)) | ((dei & 0x1) << 4); } - pub fn get_vlan_id(&self) -> u16 { + pub fn vlan_id(&self) -> u16 { let first = self.raw()[0] as u16 & 0x0F; let second = self.raw()[1] as u16; (first << 8) | second } + /// Deprecated compatibility alias for vlan_id. + #[deprecated(note = "use vlan_id")] + pub fn get_vlan_id(&self) -> u16 { + self.vlan_id() + } pub fn set_vlan_id(&mut self, id: u16) { let buf = self.raw_mut(); @@ -256,9 +285,14 @@ impl<'a> MutableVlanPacket<'a> { buf[1] = id as u8; } - pub fn get_ethertype(&self) -> EtherType { + pub fn ethertype(&self) -> EtherType { EtherType::new(u16::from_be_bytes([self.raw()[2], self.raw()[3]])) } + /// Deprecated compatibility alias for ethertype. + #[deprecated(note = "use ethertype")] + pub fn get_ethertype(&self) -> EtherType { + self.ethertype() + } pub fn set_ethertype(&mut self, ty: EtherType) { self.raw_mut()[2..4].copy_from_slice(&ty.value().to_be_bytes()); diff --git a/nex-packet/src/vxlan.rs b/nex-packet/src/vxlan.rs index 56e41e8..d964143 100644 --- a/nex-packet/src/vxlan.rs +++ b/nex-packet/src/vxlan.rs @@ -25,42 +25,49 @@ pub struct VxlanPacket { impl Packet for VxlanPacket { type Header = (); - fn from_buf(mut bytes: &[u8]) -> Option { - if bytes.len() < 8 { - return None; - } - - let flags = bytes.get_u8(); - - let reserved1 = { - let b1 = bytes.get_u8(); - let b2 = bytes.get_u8(); - let b3 = bytes.get_u8(); - bitfield::utils::u24be_from_bytes([b1, b2, b3]) - }; - - let vni = { - let b1 = bytes.get_u8(); - let b2 = bytes.get_u8(); - let b3 = bytes.get_u8(); - bitfield::utils::u24be_from_bytes([b1, b2, b3]) - }; - - let reserved2 = bytes.get_u8(); - - let payload = Bytes::copy_from_slice(bytes); - - Some(Self { - flags, - reserved1, - vni, - reserved2, - payload, + fn try_from_buf(mut bytes: &[u8]) -> Result { + (|| -> Option { + if bytes.len() < 8 { + return None; + } + + let flags = bytes.get_u8(); + + let reserved1 = { + let b1 = bytes.get_u8(); + let b2 = bytes.get_u8(); + let b3 = bytes.get_u8(); + bitfield::utils::u24be_from_bytes([b1, b2, b3]) + }; + + let vni = { + let b1 = bytes.get_u8(); + let b2 = bytes.get_u8(); + let b3 = bytes.get_u8(); + bitfield::utils::u24be_from_bytes([b1, b2, b3]) + }; + + let reserved2 = bytes.get_u8(); + + let payload = Bytes::copy_from_slice(bytes); + + Some(Self { + flags, + reserved1, + vni, + reserved2, + payload, + }) + })() + .ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), }) } - fn from_bytes(bytes: Bytes) -> Option { - Self::from_buf(&bytes) + fn try_from_bytes(bytes: Bytes) -> Result { + Self::from_buf(&bytes).ok_or(crate::parse::ParseError::Malformed { + context: std::any::type_name::(), + }) } fn to_bytes(&self) -> Bytes { From 3d22349998216a3c28f1e88c191663b5390cb87c Mon Sep 17 00:00:00 2001 From: shellrow Date: Sun, 19 Jul 2026 17:00:51 +0900 Subject: [PATCH 16/33] fix: harden datalink resource and FFI safety --- nex-datalink/src/async_io/bpf.rs | 156 ++++++++++++++---------- nex-datalink/src/async_io/linux.rs | 78 ++++++------ nex-datalink/src/async_io/mod.rs | 11 +- nex-datalink/src/async_io/wpcap.rs | 188 ++++++++++++++++++++++------- nex-datalink/src/bpf.rs | 180 +++++++++++++++++---------- nex-datalink/src/lib.rs | 85 ++++++++++--- nex-datalink/src/linux.rs | 78 +++++++----- nex-datalink/src/pcap.rs | 32 ++--- nex-datalink/src/wpcap.rs | 166 ++++++++++++++++++++----- nex-sys/src/lib.rs | 27 +++++ 10 files changed, 693 insertions(+), 308 deletions(-) diff --git a/nex-datalink/src/async_io/bpf.rs b/nex-datalink/src/async_io/bpf.rs index 7ed4ff6..149235d 100644 --- a/nex-datalink/src/async_io/bpf.rs +++ b/nex-datalink/src/async_io/bpf.rs @@ -20,17 +20,11 @@ const ETHERNET_NULL_HEADER_SIZE: usize = 4; #[derive(Debug)] struct Inner { - fd: RawFd, + fd: nex_sys::FileDesc, loopback: bool, buffer_offset: usize, } -impl Drop for Inner { - fn drop(&mut self) { - unsafe { nex_sys::close(self.fd) }; - } -} - /// Sender half of an asynchronous BPF socket. #[derive(Clone, Debug)] pub struct AsyncBpfSocketSender { @@ -44,9 +38,17 @@ impl AsyncRawSender for AsyncBpfSocketSender { } else { 0 }; + if packet.len() < offset { + return Poll::Ready(Err(io::Error::new( + io::ErrorKind::InvalidInput, + "loopback packets must include an Ethernet header", + ))); + } + // SAFETY: The descriptor is owned by `inner`; the packet slice remains + // readable for the duration of `write`. let ret = unsafe { libc::write( - self.inner.fd, + self.inner.fd.as_raw(), packet[offset..].as_ptr() as *const libc::c_void, (packet.len() - offset) as libc::size_t, ) @@ -57,10 +59,11 @@ impl AsyncRawSender for AsyncBpfSocketSender { let err = io::Error::last_os_error(); if err.kind() == io::ErrorKind::WouldBlock { let mut pfd = libc::pollfd { - fd: self.inner.fd, + fd: self.inner.fd.as_raw(), events: libc::POLLOUT, revents: 0, }; + // SAFETY: `pfd` points to one initialized poll descriptor. unsafe { libc::poll(&mut pfd, 1, 0) }; cx.waker().wake_by_ref(); Poll::Pending @@ -90,38 +93,76 @@ impl Stream for AsyncBpfSocketReceiver { }; if me.packets.is_empty() { let buffer = &mut me.read_buffer[me.inner.buffer_offset..]; + // SAFETY: The descriptor is open and `buffer` is writable for its + // full reported length. let ret = unsafe { libc::read( - me.inner.fd, + me.inner.fd.as_raw(), buffer.as_mut_ptr() as *mut libc::c_void, buffer.len() as libc::size_t, ) }; if ret >= 0 { let buflen = ret as usize; - let mut ptr = buffer.as_mut_ptr(); - let end = unsafe { buffer.as_ptr().add(buflen) }; - while (ptr as *const u8) < end { - unsafe { - let packet: *const bpf::bpf_hdr = mem::transmute(ptr); - let start = - ptr as isize + (*packet).bh_hdrlen as isize - buffer.as_ptr() as isize; - me.packets.push_back(( - start as usize + header_size, - (*packet).bh_caplen as usize - header_size, - )); - let offset = (*packet).bh_hdrlen as isize + (*packet).bh_caplen as isize; - ptr = ptr.offset(bpf::BPF_WORDALIGN(offset)); + let mut cursor = 0usize; + while cursor < buflen { + let remaining = buflen - cursor; + if remaining < mem::size_of::() { + return Poll::Ready(Some(Err(io::Error::new( + io::ErrorKind::InvalidData, + "truncated BPF record header", + )))); + } + // SAFETY: The size check proves the complete header is + // in-bounds. `read_unaligned` handles Vec's alignment. + let packet = unsafe { + std::ptr::read_unaligned(buffer.as_ptr().add(cursor) as *const bpf::bpf_hdr) + }; + let header_len = packet.bh_hdrlen as usize; + let captured_len = packet.bh_caplen as usize; + let Some(record_len) = header_len.checked_add(captured_len) else { + return Poll::Ready(Some(Err(io::Error::new( + io::ErrorKind::InvalidData, + "BPF record length overflow", + )))); + }; + if header_len < mem::size_of::() + || captured_len < header_size + || record_len > remaining + { + return Poll::Ready(Some(Err(io::Error::new( + io::ErrorKind::InvalidData, + "invalid BPF record lengths", + )))); } + me.packets.push_back(( + cursor + header_len + header_size, + captured_len - header_size, + )); + let Ok(record_len) = isize::try_from(record_len) else { + return Poll::Ready(Some(Err(io::Error::new( + io::ErrorKind::InvalidData, + "BPF record exceeds platform pointer range", + )))); + }; + let Some(next) = cursor.checked_add(bpf::BPF_WORDALIGN(record_len) as usize) + else { + return Poll::Ready(Some(Err(io::Error::new( + io::ErrorKind::InvalidData, + "BPF record offset overflow", + )))); + }; + cursor = next; } } else { let err = io::Error::last_os_error(); if err.kind() == io::ErrorKind::WouldBlock { let mut pfd = libc::pollfd { - fd: me.inner.fd, + fd: me.inner.fd.as_raw(), events: libc::POLLIN, revents: 0, }; + // SAFETY: `pfd` points to one initialized poll descriptor. unsafe { libc::poll(&mut pfd, 1, 0) }; cx.waker().wake_by_ref(); return Poll::Pending; @@ -157,6 +198,8 @@ pub fn channel(network_interface: &Interface, config: Config) -> io::Result io::Result io::Result { let c_file_name = CString::new("/dev/bpf") .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid bpf device path"))?; + // SAFETY: `c_file_name` is a live, NUL-terminated path and the selected + // flags require no variadic mode argument. let fd = unsafe { libc::open(c_file_name.as_ptr(), libc::O_RDWR, 0) }; if fd == -1 { Err(io::Error::last_os_error()) @@ -181,45 +226,39 @@ pub fn channel(network_interface: &Interface, config: Config) -> io::Result io::Result usize { + // SAFETY: `storage` points to writable `sockaddr_storage`, whose size and + // alignment are sufficient for `sockaddr_ll`; it is used only in this call. unsafe { let sll: *mut libc::sockaddr_ll = mem::transmute(storage); (*sll).sll_family = libc::AF_PACKET as libc::sa_family_t; @@ -33,18 +34,9 @@ fn network_addr_to_sockaddr( #[derive(Debug)] struct Inner { - fd: RawFd, + fd: nex_sys::FileDesc, send_addr: libc::sockaddr_ll, - epfd: RawFd, -} - -impl Drop for Inner { - fn drop(&mut self) { - unsafe { - nex_sys::close(self.fd); - nex_sys::close(self.epfd); - } - } + epfd: nex_sys::FileDesc, } /// Sender half of an asynchronous raw socket. @@ -55,9 +47,11 @@ pub struct AsyncRawSocketSender { impl AsyncRawSender for AsyncRawSocketSender { fn poll_send(&mut self, cx: &mut Context<'_>, packet: &[u8]) -> Poll> { + // SAFETY: The descriptor is open, `packet` remains readable, and + // `send_addr` remains valid throughout sendto. let ret = unsafe { libc::sendto( - self.inner.fd, + self.inner.fd.as_raw(), packet.as_ptr() as *const libc::c_void, packet.len(), 0, @@ -70,9 +64,11 @@ impl AsyncRawSender for AsyncRawSocketSender { } let err = io::Error::last_os_error(); if err.kind() == io::ErrorKind::WouldBlock { + // SAFETY: `events` provides writable storage for one epoll event + // and the epoll descriptor is open. unsafe { let mut events = [mem::zeroed::()]; - libc::epoll_wait(self.inner.epfd, events.as_mut_ptr(), 1, 0); + libc::epoll_wait(self.inner.epfd.as_raw(), events.as_mut_ptr(), 1, 0); } cx.waker().wake_by_ref(); Poll::Pending @@ -94,9 +90,11 @@ impl Stream for AsyncRawSocketReceiver { fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let me = self.get_mut(); + // SAFETY: The descriptor is open and `read_buffer` is writable for its + // entire length. let ret = unsafe { libc::recv( - me.inner.fd, + me.inner.fd.as_raw(), me.read_buffer.as_mut_ptr() as *mut libc::c_void, me.read_buffer.len(), libc::MSG_DONTWAIT, @@ -109,9 +107,11 @@ impl Stream for AsyncRawSocketReceiver { } let err = io::Error::last_os_error(); if err.kind() == io::ErrorKind::WouldBlock { + // SAFETY: `events` provides writable storage for one epoll event + // and the epoll descriptor is open. unsafe { let mut events = [mem::zeroed::()]; - libc::epoll_wait(me.inner.epfd, events.as_mut_ptr(), 1, 0); + libc::epoll_wait(me.inner.epfd.as_raw(), events.as_mut_ptr(), 1, 0); } cx.waker().wake_by_ref(); Poll::Pending @@ -128,50 +128,50 @@ pub fn channel(network_interface: &Interface, config: Config) -> io::Result (libc::SOCK_RAW, eth_p_all), ChannelType::Layer3(proto) => (libc::SOCK_DGRAM, proto as i32), }; - let fd = unsafe { + // SAFETY: `socket` receives valid AF_PACKET arguments and retains no + // borrowed pointers. + let raw_fd = unsafe { libc::socket( libc::AF_PACKET, typ | libc::SOCK_NONBLOCK, (proto as u16).to_be() as i32, ) }; - if fd == -1 { + if raw_fd == -1 { return Err(io::Error::last_os_error()); } + // SAFETY: `raw_fd` was just opened and exclusive ownership transfers to + // this guard exactly once. + let fd = unsafe { nex_sys::FileDesc::from_raw(raw_fd) }; + // SAFETY: A zero bit pattern is a valid initial socket address storage. let mut addr: libc::sockaddr_storage = unsafe { mem::zeroed() }; let len = network_addr_to_sockaddr(network_interface, &mut addr, proto); + // SAFETY: `network_addr_to_sockaddr` initialized `addr` as sockaddr_ll. let send_addr = unsafe { *(&addr as *const _ as *const libc::sockaddr_ll) }; let bind_addr = (&addr as *const libc::sockaddr_storage) as *const libc::sockaddr; - if unsafe { libc::bind(fd, bind_addr, len as libc::socklen_t) } == -1 { - let err = io::Error::last_os_error(); - unsafe { - nex_sys::close(fd); - } - return Err(err); + // SAFETY: `bind_addr` points into live initialized storage for `len` bytes. + if unsafe { libc::bind(fd.as_raw(), bind_addr, len as libc::socklen_t) } == -1 { + return Err(io::Error::last_os_error()); } - let epfd = unsafe { libc::epoll_create1(0) }; - if epfd == -1 { - let err = io::Error::last_os_error(); - unsafe { - nex_sys::close(fd); - } - return Err(err); + // SAFETY: `epoll_create1` receives a supported zero flag value. + let raw_epfd = unsafe { libc::epoll_create1(0) }; + if raw_epfd == -1 { + return Err(io::Error::last_os_error()); } + // SAFETY: `raw_epfd` was just opened and ownership transfers exactly once. + let epfd = unsafe { nex_sys::FileDesc::from_raw(raw_epfd) }; let mut event = libc::epoll_event { events: (libc::EPOLLIN | libc::EPOLLOUT) as u32, - u64: fd as u64, + u64: fd.as_raw() as u64, }; - if unsafe { libc::epoll_ctl(epfd, libc::EPOLL_CTL_ADD, fd, &mut event) } == -1 { - let err = io::Error::last_os_error(); - unsafe { - nex_sys::close(epfd); - nex_sys::close(fd); - } - return Err(err); + // SAFETY: Both descriptors are open and `event` is writable for the call. + if unsafe { libc::epoll_ctl(epfd.as_raw(), libc::EPOLL_CTL_ADD, fd.as_raw(), &mut event) } == -1 + { + return Err(io::Error::last_os_error()); } let inner = Arc::new(Inner { diff --git a/nex-datalink/src/async_io/mod.rs b/nex-datalink/src/async_io/mod.rs index 8b65a81..cd599be 100644 --- a/nex-datalink/src/async_io/mod.rs +++ b/nex-datalink/src/async_io/mod.rs @@ -21,7 +21,7 @@ use std::task::{Context, Poll}; use futures_core::stream::Stream; -use crate::Config; +use crate::{Config, DatalinkError}; /// Trait for asynchronously sending raw packets. pub trait AsyncRawSender: Send { @@ -54,10 +54,11 @@ pub enum AsyncChannel { pub fn async_channel( network_interface: &nex_core::interface::Interface, configuration: Config, -) -> io::Result { +) -> Result { + configuration.validate()?; #[cfg(any(target_os = "linux", target_os = "android"))] { - linux::channel(network_interface, configuration) + linux::channel(network_interface, configuration).map_err(DatalinkError::Io) } #[cfg(any( target_os = "freebsd", @@ -68,10 +69,10 @@ pub fn async_channel( target_os = "ios", ))] { - bpf::channel(network_interface, configuration) + bpf::channel(network_interface, configuration).map_err(DatalinkError::Io) } #[cfg(windows)] { - wpcap::channel(network_interface, configuration) + wpcap::channel(network_interface, configuration).map_err(DatalinkError::Io) } } diff --git a/nex-datalink/src/async_io/wpcap.rs b/nex-datalink/src/async_io/wpcap.rs index 09d86d9..cabeefd 100644 --- a/nex-datalink/src/async_io/wpcap.rs +++ b/nex-datalink/src/async_io/wpcap.rs @@ -12,35 +12,47 @@ use std::io; use std::mem; use std::pin::Pin; use std::slice; +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use std::task::{Context, Poll, Waker}; -use std::thread; +use std::thread::{self, JoinHandle}; #[derive(Debug)] struct WinPcapAdapter { adapter: windows::LPADAPTER, + operation_lock: Mutex<()>, } impl Drop for WinPcapAdapter { fn drop(&mut self) { + // SAFETY: This is the last owning `Arc`; the receive thread has been + // joined and no operation can still use the adapter. unsafe { windows::PacketCloseAdapter(self.adapter) }; } } +// SAFETY: The adapter is owned until Drop and every Npcap operation is +// serialized by `operation_lock`. unsafe impl Send for WinPcapAdapter {} +// SAFETY: Shared access cannot reach the raw adapter without acquiring +// `operation_lock`. unsafe impl Sync for WinPcapAdapter {} -#[derive(Clone, Debug)] +#[derive(Debug)] struct WinPcapPacket { packet: windows::LPPACKET, } impl Drop for WinPcapPacket { fn drop(&mut self) { + // SAFETY: `packet` is uniquely owned and came from + // PacketAllocatePacket, so it must be freed exactly once. unsafe { windows::PacketFreePacket(self.packet) }; } } +// SAFETY: Each packet wrapper is moved into one sender or receive thread and is +// never accessed concurrently. unsafe impl Send for WinPcapPacket {} #[derive(Debug)] @@ -48,13 +60,23 @@ struct Inner { adapter: Arc, packets: Arc>>>, waker: Arc>>, + stop: Arc, + receive_thread: Mutex>>, } -unsafe impl Send for Inner {} -unsafe impl Sync for Inner {} +impl Drop for Inner { + fn drop(&mut self) { + self.stop.store(true, Ordering::Release); + if let Ok(thread) = self.receive_thread.get_mut() + && let Some(thread) = thread.take() + { + let _ = thread.join(); + } + } +} /// Sender half of a WinPcap socket. -#[derive(Clone, Debug)] +#[derive(Debug)] pub struct AsyncWpcapSocketSender { inner: Arc, write_buffer: Vec, @@ -65,6 +87,8 @@ impl AsyncRawSender for AsyncWpcapSocketSender { fn poll_send(&mut self, _cx: &mut Context<'_>, packet: &[u8]) -> Poll> { let len = cmp::min(packet.len(), self.write_buffer.len()); self.write_buffer[..len].copy_from_slice(&packet[..len]); + // SAFETY: The packet wrapper and backing vector are exclusively owned + // by this sender and remain live throughout the call. unsafe { windows::PacketInitPacket( self.packet.packet, @@ -72,6 +96,16 @@ impl AsyncRawSender for AsyncWpcapSocketSender { len as windows::UINT, ); } + let _operation = match self.inner.adapter.operation_lock.lock() { + Ok(lock) => lock, + Err(_) => { + return Poll::Ready(Err(io::Error::other( + "Npcap adapter operation mutex poisoned", + ))); + } + }; + // SAFETY: The adapter operation is serialized and both handles remain + // live throughout the call. let ret = unsafe { windows::PacketSendPacket(self.inner.adapter.adapter, self.packet.packet, 1) }; if ret == 0 { @@ -125,6 +159,8 @@ pub fn channel(network_interface: &Interface, config: Config) -> io::Result io::Result>> = Arc::new(Mutex::new(None)); + let stop = Arc::new(AtomicBool::new(false)); - { + let receive_thread = { let adapter = adapter.clone(); let packets = packets.clone(); let waker = waker.clone(); - let read_buffer_size = read_buffer_size; + let stop = stop.clone(); thread::spawn(move || { let mut read_buffer = vec![0u8; read_buffer_size]; + // SAFETY: PacketAllocatePacket takes no arguments and returns an + // owned packet pointer or null. let read_packet = unsafe { windows::PacketAllocatePacket() }; if read_packet.is_null() { return; } + let read_packet = WinPcapPacket { + packet: read_packet, + }; + // SAFETY: The packet and backing buffer remain owned by this thread + // and live until the receive loop exits. unsafe { windows::PacketInitPacket( - read_packet, + read_packet.packet, read_buffer.as_mut_ptr() as windows::PVOID, read_buffer_size as windows::UINT, ); } - loop { - let ret = unsafe { windows::PacketReceivePacket(adapter.adapter, read_packet, 1) }; + while !stop.load(Ordering::Acquire) { + let _operation = match adapter.operation_lock.lock() { + Ok(lock) => lock, + Err(poisoned) => poisoned.into_inner(), + }; + // SAFETY: The operation is serialized and both handles remain + // live for the duration of the bounded receive. + let ret = + unsafe { windows::PacketReceivePacket(adapter.adapter, read_packet.packet, 1) }; if ret == 0 { continue; } - let buflen = unsafe { (*read_packet).ulBytesReceived as isize }; - let mut ptr = unsafe { (*read_packet).Buffer as *mut libc::c_char }; - let end = unsafe { ((*read_packet).Buffer as *mut libc::c_char).offset(buflen) }; - while ptr < end { - unsafe { - let hdr: *const bpf::bpf_hdr = mem::transmute(ptr); - let start = ptr as isize + (*hdr).bh_hdrlen as isize - - (*read_packet).Buffer as isize; - let caplen = (*hdr).bh_caplen as usize; - let data_ptr = ((*read_packet).Buffer as isize + start) as *const u8; - let data = slice::from_raw_parts(data_ptr, caplen).to_vec(); - { - let mut queue = match packets.lock() { - Ok(queue) => queue, - Err(poisoned) => poisoned.into_inner(), - }; - queue.push_back(data); - } - let offset = (*hdr).bh_hdrlen as isize + (*hdr).bh_caplen as isize; - ptr = ptr.offset(bpf::BPF_WORDALIGN(offset)); + // SAFETY: A successful receive initialized the byte count and + // the packet buffer capacity. + let (buflen, buffer_capacity, base) = unsafe { + ( + (*read_packet.packet).ulBytesReceived as usize, + (*read_packet.packet).Length as usize, + (*read_packet.packet).Buffer as *const u8, + ) + }; + if buflen > buffer_capacity { + continue; + } + let mut cursor = 0usize; + while cursor < buflen { + let remaining = buflen - cursor; + if remaining < mem::size_of::() { + break; + } + // SAFETY: The size check proves the full header is in the + // buffer. `read_unaligned` handles the backing Vec's alignment. + let hdr = unsafe { + std::ptr::read_unaligned(base.add(cursor) as *const bpf::bpf_hdr) + }; + let header_len = hdr.bh_hdrlen as usize; + let captured_len = hdr.bh_caplen as usize; + let Some(record_len) = header_len.checked_add(captured_len) else { + break; + }; + if header_len < mem::size_of::() || record_len > remaining { + break; + } + // SAFETY: The validated record lengths prove this packet + // payload is fully in-bounds. + let data = unsafe { + slice::from_raw_parts(base.add(cursor + header_len), captured_len) + } + .to_vec(); + { + let mut queue = match packets.lock() { + Ok(queue) => queue, + Err(poisoned) => poisoned.into_inner(), + }; + queue.push_back(data); } + let Ok(record_len) = isize::try_from(record_len) else { + break; + }; + let Some(next) = cursor.checked_add(bpf::BPF_WORDALIGN(record_len) as usize) + else { + break; + }; + cursor = next; } let mut waker = match waker.lock() { Ok(waker) => waker, @@ -224,20 +324,20 @@ pub fn channel(network_interface: &Interface, config: Config) -> io::Result io::Result { +pub(crate) fn channel(network_interface: &Interface, config: Config) -> io::Result { #[cfg(any( target_os = "freebsd", target_os = "netbsd", @@ -76,6 +76,8 @@ pub fn channel(network_interface: &Interface, config: Config) -> io::Result io::Result { let c_file_name = CString::new(&b"/dev/bpf"[..]) .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid bpf device path"))?; + // SAFETY: `c_file_name` is a live, NUL-terminated path and the flags + // require no variadic mode argument. let fd = unsafe { libc::open(c_file_name.as_ptr(), libc::O_RDWR, 0) }; if fd == -1 { Err(io::Error::last_os_error()) @@ -91,6 +93,8 @@ pub fn channel(network_interface: &Interface, config: Config) -> io::Result io::Result io::Result<()> { + // SAFETY: `fd` is an open BPF descriptor and the ioctl reads the + // correctly typed integer argument for the duration of the call. if unsafe { bpf::ioctl(fd, bpf::BIOCFEEDBACK, &1) } == -1 { - let err = io::Error::last_os_error(); - unsafe { - libc::close(fd); - } - return Err(err); + return Err(io::Error::last_os_error()); } Ok(()) } @@ -121,7 +123,11 @@ pub fn channel(network_interface: &Interface, config: Config) -> io::Result io::Result io::Result io::Result Option> { - let len = num_packets * packet_size; + if packet_size == 0 { + return Some(Err(io::Error::new( + io::ErrorKind::InvalidInput, + "packet_size must be greater than zero", + ))); + } + let len = num_packets.checked_mul(packet_size)?; if len >= self.write_buffer.len() { None } else { @@ -261,8 +264,16 @@ impl RawSender for RawSenderImpl { } else { 0 }; + if packet_size < offset { + return Some(Err(io::Error::new( + io::ErrorKind::InvalidInput, + "loopback packets must include an Ethernet header", + ))); + } for chunk in self.write_buffer[..len].chunks_mut(packet_size) { func(chunk); + // SAFETY: `fd_set` and the optional timeout are initialized and + // remain live throughout pselect. let ret = unsafe { libc::FD_SET(self.fd.as_raw(), &mut self.fd_set as *mut libc::fd_set); libc::pselect( @@ -282,6 +293,8 @@ impl RawSender for RawSenderImpl { return Some(Err(io::Error::last_os_error())); } else if ret == 0 { return Some(Err(io::Error::new(io::ErrorKind::TimedOut, "Timed out"))); + // SAFETY: `offset` is checked against `chunk.len()` above; the + // descriptor is open and `write` retains no buffer pointer. } else if unsafe { libc::write( self.fd.as_raw(), @@ -306,6 +319,14 @@ impl RawSender for RawSenderImpl { } else { 0 }; + if packet.len() < offset { + return Some(Err(io::Error::new( + io::ErrorKind::InvalidInput, + "loopback packets must include an Ethernet header", + ))); + } + // SAFETY: `fd_set` and the optional timeout are initialized and remain + // live throughout pselect. let ret = unsafe { libc::FD_SET(self.fd.as_raw(), &mut self.fd_set as *mut libc::fd_set); libc::pselect( @@ -325,6 +346,8 @@ impl RawSender for RawSenderImpl { } else if ret == 0 { Some(Err(io::Error::new(io::ErrorKind::TimedOut, "Timed out"))) } else { + // SAFETY: `offset` is checked against `packet.len()` above; the + // descriptor is open and `write` retains no buffer pointer. match unsafe { libc::write( self.fd.as_raw(), @@ -358,6 +381,8 @@ impl RawReceiver for RawReceiverImpl { }; if self.packets.is_empty() { let buffer = &mut self.read_buffer[self.buffer_offset..]; + // SAFETY: `fd_set` and the optional timeout are initialized and + // remain live throughout pselect. let ret = unsafe { libc::FD_SET(self.fd.as_raw(), &mut self.fd_set as *mut libc::fd_set); libc::pselect( @@ -377,6 +402,8 @@ impl RawReceiver for RawReceiverImpl { } else if ret == 0 { return Err(io::Error::new(io::ErrorKind::TimedOut, "Timed out")); } else { + // SAFETY: `buffer` is writable for its full length and the + // descriptor is open. let buflen = match unsafe { libc::read( self.fd.as_raw(), @@ -387,20 +414,51 @@ impl RawReceiver for RawReceiverImpl { len if len > 0 => len, _ => return Err(io::Error::last_os_error()), }; - let mut ptr = buffer.as_mut_ptr(); - let end = unsafe { buffer.as_ptr().offset(buflen as isize) }; - while (ptr as *const u8) < end { - unsafe { - let packet: *const bpf::bpf_hdr = mem::transmute(ptr); - let start = - ptr as isize + (*packet).bh_hdrlen as isize - buffer.as_ptr() as isize; - self.packets.push_back(( - start as usize + header_size, - (*packet).bh_caplen as usize - header_size, + let buflen = buflen as usize; + let mut cursor = 0usize; + while cursor < buflen { + let remaining = buflen - cursor; + if remaining < mem::size_of::() { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "truncated BPF record header", + )); + } + // SAFETY: The size check proves the complete header is + // in-bounds. `read_unaligned` does not require Vec's + // allocation to satisfy `bpf_hdr` alignment. + let packet = unsafe { + std::ptr::read_unaligned(buffer.as_ptr().add(cursor) as *const bpf::bpf_hdr) + }; + let header_len = packet.bh_hdrlen as usize; + let captured_len = packet.bh_caplen as usize; + let record_len = header_len.checked_add(captured_len).ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidData, "BPF record length overflow") + })?; + if header_len < mem::size_of::() + || captured_len < header_size + || record_len > remaining + { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "invalid BPF record lengths", )); - let offset = (*packet).bh_hdrlen as isize + (*packet).bh_caplen as isize; - ptr = ptr.offset(bpf::BPF_WORDALIGN(offset)); } + self.packets.push_back(( + cursor + header_len + header_size, + captured_len - header_size, + )); + let record_len = isize::try_from(record_len).map_err(|_| { + io::Error::new( + io::ErrorKind::InvalidData, + "BPF record exceeds platform pointer range", + ) + })?; + cursor = cursor + .checked_add(bpf::BPF_WORDALIGN(record_len) as usize) + .ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidData, "BPF record offset overflow") + })?; } } } diff --git a/nex-datalink/src/lib.rs b/nex-datalink/src/lib.rs index 374ca2b..c8d4419 100644 --- a/nex-datalink/src/lib.rs +++ b/nex-datalink/src/lib.rs @@ -1,5 +1,6 @@ //! Cross-platform datalink I/O primitives for sending and receiving raw packets. +use std::fmt; use std::io; use std::option::Option; use std::time::Duration; @@ -51,6 +52,7 @@ pub type EtherType = u16; /// Type of data link channel to present (Linux only). #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +#[non_exhaustive] pub enum ChannelType { /// Send and receive layer 2 packets directly, including headers. Layer2, @@ -67,6 +69,7 @@ pub enum Channel { /// Socket fanout type (Linux only). #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +#[non_exhaustive] pub enum FanoutType { /// Fan out packets by hashing packet fields. Hash, @@ -115,6 +118,7 @@ impl FanoutType { /// Fanout settings (Linux only). #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +#[non_exhaustive] pub struct FanoutOption { /// Fanout group identifier. pub group_id: u16, @@ -131,6 +135,7 @@ pub struct FanoutOption { /// Each option should be treated as a hint - each backend is free to ignore any and all /// options which don't apply to it. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +#[non_exhaustive] pub struct Config { /// The size of buffer to use when writing packets. Defaults to 4096. pub write_buffer_size: usize, @@ -152,11 +157,54 @@ pub struct Config { /// to: 1000. pub bpf_fd_attempts: usize, + /// Linux only: optional packet fanout group and distribution settings. pub linux_fanout: Option, + /// Whether the backend should request promiscuous packet capture. pub promiscuous: bool, } +/// Semantic failures while validating or opening a datalink channel. +#[derive(Debug)] +#[non_exhaustive] +pub enum DatalinkError { + /// A configuration value cannot be used by any backend. + InvalidConfig { + /// Configuration field that failed validation. + field: &'static str, + /// Required constraint. + requirement: &'static str, + }, + /// The operating-system backend failed while creating the channel. + Io(io::Error), +} + +impl fmt::Display for DatalinkError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidConfig { field, requirement } => { + write!(f, "invalid datalink configuration: {field} {requirement}") + } + Self::Io(error) => write!(f, "failed to open datalink channel: {error}"), + } + } +} + +impl std::error::Error for DatalinkError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::Io(error) => Some(error), + Self::InvalidConfig { .. } => None, + } + } +} + +impl From for DatalinkError { + fn from(error: io::Error) -> Self { + Self::Io(error) + } +} + impl Default for Config { fn default() -> Config { Config { @@ -174,24 +222,24 @@ impl Default for Config { impl Config { /// Validates whether this configuration can be used safely. - pub fn validate(&self) -> io::Result<()> { + pub fn validate(&self) -> Result<(), DatalinkError> { if self.write_buffer_size == 0 { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "write_buffer_size must be greater than 0", - )); + return Err(DatalinkError::InvalidConfig { + field: "write_buffer_size", + requirement: "must be greater than zero", + }); } if self.read_buffer_size == 0 { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "read_buffer_size must be greater than 0", - )); + return Err(DatalinkError::InvalidConfig { + field: "read_buffer_size", + requirement: "must be greater than zero", + }); } if self.bpf_fd_attempts == 0 { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "bpf_fd_attempts must be greater than 0", - )); + return Err(DatalinkError::InvalidConfig { + field: "bpf_fd_attempts", + requirement: "must be greater than zero", + }); } Ok(()) } @@ -250,9 +298,9 @@ impl Config { pub fn channel( network_interface: &nex_core::interface::Interface, configuration: Config, -) -> io::Result { +) -> Result { configuration.validate()?; - backend::channel(network_interface, (&configuration).into()) + backend::channel(network_interface, (&configuration).into()).map_err(DatalinkError::Io) } /// Trait to enable sending `$packet` packets. @@ -321,4 +369,11 @@ mod tests { assert!(!cfg.promiscuous); assert_eq!(cfg.bpf_fd_attempts, 42); } + + fn assert_error_contract() {} + + #[test] + fn datalink_error_implements_public_error_contract() { + assert_error_contract::(); + } } diff --git a/nex-datalink/src/linux.rs b/nex-datalink/src/linux.rs index 6ed3b31..47ce6c9 100644 --- a/nex-datalink/src/linux.rs +++ b/nex-datalink/src/linux.rs @@ -17,6 +17,9 @@ fn network_addr_to_sockaddr( storage: *mut libc::sockaddr_storage, proto: libc::c_int, ) -> usize { + // SAFETY: `storage` points to writable `sockaddr_storage`, whose size and + // alignment are sufficient for `sockaddr_ll`; the pointer is used only + // during this call. unsafe { let sll: *mut libc::sockaddr_ll = mem::transmute(storage); (*sll).sll_family = libc::AF_PACKET as libc::sa_family_t; @@ -32,7 +35,7 @@ fn network_addr_to_sockaddr( /// Configuration for the Linux datalink backend. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub struct Config { +pub(crate) struct Config { /// The size of buffer to use when writing packets. Defaults to 4096. pub write_buffer_size: usize, @@ -97,39 +100,46 @@ impl Default for Config { /// Create a data link channel using the Linux's `AF_PACKET` socket type. #[inline] -pub fn channel(network_interface: &Interface, config: Config) -> io::Result { +pub(crate) fn channel(network_interface: &Interface, config: Config) -> io::Result { let eth_p_all = 0x0003; let (typ, proto) = match config.channel_type { super::ChannelType::Layer2 => (libc::SOCK_RAW, eth_p_all), super::ChannelType::Layer3(proto) => (libc::SOCK_DGRAM, proto), }; - let socket = unsafe { libc::socket(libc::AF_PACKET, typ, proto.to_be() as i32) }; - if socket == -1 { + // SAFETY: `socket` receives only valid AF_PACKET domain/type/protocol + // integer values and retains no borrowed pointers. + let raw_socket = unsafe { libc::socket(libc::AF_PACKET, typ, proto.to_be() as i32) }; + if raw_socket == -1 { return Err(io::Error::last_os_error()); } + // SAFETY: `raw_socket` was just created and exclusive ownership is + // transferred exactly once to this guard. + let socket = unsafe { nex_sys::FileDesc::from_raw(raw_socket) }; + // SAFETY: A zero bit pattern is a valid initial socket address storage. let mut addr: libc::sockaddr_storage = unsafe { mem::zeroed() }; let len = network_addr_to_sockaddr(network_interface, &mut addr, proto as i32); let send_addr = (&addr as *const libc::sockaddr_storage) as *const libc::sockaddr; // Bind to interface - if unsafe { libc::bind(socket, send_addr, len as libc::socklen_t) } == -1 { - let err = io::Error::last_os_error(); - unsafe { - nex_sys::close(socket); - } - return Err(err); + // SAFETY: `send_addr` points into live address storage and `len` is the + // initialized sockaddr_ll size. + if unsafe { libc::bind(socket.as_raw(), send_addr, len as libc::socklen_t) } == -1 { + return Err(io::Error::last_os_error()); } + // SAFETY: A zero bit pattern is a valid initial packet membership request. let mut pmr: linux::packet_mreq = unsafe { mem::zeroed() }; pmr.mr_ifindex = network_interface.index as i32; pmr.mr_type = linux::PACKET_MR_PROMISC as u16; // Enable promiscuous capture if config.promiscuous { + // SAFETY: The descriptor is open and `pmr` remains readable with the + // exact length supplied for the duration of setsockopt. if unsafe { libc::setsockopt( - socket, + socket.as_raw(), linux::SOL_PACKET, linux::PACKET_ADD_MEMBERSHIP, (&pmr as *const linux::packet_mreq) as *const libc::c_void, @@ -137,11 +147,7 @@ pub fn channel(network_interface: &Interface, config: Config) -> io::Result io::Result io::Result Option> { - let len = num_packets * packet_size; + if packet_size == 0 { + return Some(Err(io::Error::new( + io::ErrorKind::InvalidInput, + "packet_size must be greater than zero", + ))); + } + let len = num_packets.checked_mul(packet_size)?; if len <= self.write_buffer.len() { let min = std::cmp::min(self.write_buffer.len(), len); let mut_slice = &mut self.write_buffer; @@ -257,6 +264,8 @@ impl RawSender for RawSenderImpl { let send_addr = (&self.send_addr as *const libc::sockaddr_ll) as *const libc::sockaddr; + // SAFETY: `pollfd` points to one initialized descriptor for the + // duration of poll. let ret = unsafe { libc::poll( &mut pollfd as *mut libc::pollfd, @@ -309,6 +318,8 @@ impl RawSender for RawSenderImpl { // -1: wait indefinitely let timeout_ms = poll_timeout_ms(self.timeout); + // SAFETY: `pollfd` points to one initialized descriptor for the + // duration of poll. let ret = unsafe { libc::poll( &mut pollfd as *mut libc::pollfd, @@ -354,6 +365,7 @@ struct RawReceiverImpl { impl RawReceiver for RawReceiverImpl { fn next(&mut self) -> io::Result<&[u8]> { + // SAFETY: A zero bit pattern is valid initial socket address storage. let mut caddr: libc::sockaddr_storage = unsafe { mem::zeroed() }; let mut pollfd = libc::pollfd { fd: self.socket.as_raw(), @@ -365,6 +377,8 @@ impl RawReceiver for RawReceiverImpl { // -1: wait indefinitely let timeout_ms = poll_timeout_ms(self.timeout); + // SAFETY: `pollfd` points to one initialized descriptor for the + // duration of poll. let ret = unsafe { libc::poll( &mut pollfd as *mut libc::pollfd, diff --git a/nex-datalink/src/pcap.rs b/nex-datalink/src/pcap.rs index ed2c184..ebf7b47 100644 --- a/nex-datalink/src/pcap.rs +++ b/nex-datalink/src/pcap.rs @@ -16,6 +16,7 @@ use nex_core::interface::InterfaceType; /// Configuration for the pcap datalink backend. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +#[non_exhaustive] pub struct Config { /// The size of buffer to use when reading packets. Must be at least /// 65516 with pcap. @@ -197,27 +198,16 @@ pub fn interfaces() -> Vec { devices .iter() .enumerate() - .map(|(i, dev)| Interface { - name: dev.name.clone(), - index: i as u32, - friendly_name: None, - description: dev.desc.clone(), - if_type: InterfaceType::Unknown, - mac_addr: None, - ipv4: Vec::new(), - ipv6: Vec::new(), - ipv6_scope_ids: Vec::new(), - flags: dev.flags.if_flags.bits(), - oper_state: nex_core::interface::OperState::from_if_flags( - dev.flags.if_flags.bits(), - ), - transmit_speed: None, - receive_speed: None, - stats: None, - gateway: None, - dns_servers: Vec::new(), - mtu: None, - default: false, + .map(|(i, dev)| { + let mut interface = Interface::dummy(); + interface.name = dev.name.clone(); + interface.index = i as u32; + interface.description = dev.desc.clone(); + interface.if_type = InterfaceType::Unknown; + interface.flags = dev.flags.if_flags.bits(); + interface.oper_state = + nex_core::interface::OperState::from_if_flags(dev.flags.if_flags.bits()); + interface }) .collect() } else { diff --git a/nex-datalink/src/wpcap.rs b/nex-datalink/src/wpcap.rs index 2f2ec1e..bab0f3d 100644 --- a/nex-datalink/src/wpcap.rs +++ b/nex-datalink/src/wpcap.rs @@ -4,42 +4,58 @@ use super::bindings::{bpf, windows}; use super::{RawReceiver, RawSender}; use nex_core::interface::Interface; -use libc::c_char; use std::cmp; use std::collections::VecDeque; use std::ffi::CString; use std::io; use std::mem; use std::slice; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; struct WinPcapAdapter { adapter: windows::LPADAPTER, + operation_lock: Mutex<()>, } impl Drop for WinPcapAdapter { fn drop(&mut self) { + // SAFETY: This is the last owning `Arc`, so no operation can still use + // the non-null adapter handle. Npcap requires exactly one close. unsafe { windows::PacketCloseAdapter(self.adapter); } } } +// SAFETY: The non-null adapter handle is owned until `Drop`. All send and +// receive operations are serialized by `operation_lock`, and Npcap configuration +// is completed before this wrapper is shared. +unsafe impl Send for WinPcapAdapter {} +// SAFETY: See the `Send` rationale; shared access cannot reach the handle +// without acquiring `operation_lock`. +unsafe impl Sync for WinPcapAdapter {} + struct WinPcapPacket { packet: windows::LPPACKET, } impl Drop for WinPcapPacket { fn drop(&mut self) { + // SAFETY: `packet` was returned by PacketAllocatePacket, is owned by + // this wrapper, and is freed exactly once. unsafe { windows::PacketFreePacket(self.packet); } } } +// SAFETY: A packet wrapper is moved into exactly one sender or receiver and all +// access then occurs through that half's exclusive `&mut self`. +unsafe impl Send for WinPcapPacket {} + /// The Npcap / WinPcap's specific configuration. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub struct Config { +pub(crate) struct Config { /// The size of buffer to use when writing packets. Defaults to 4096. pub write_buffer_size: usize, @@ -70,13 +86,15 @@ impl Default for Config { /// Create a datalink channel using the Npcap / WinPcap library. #[inline] -pub fn channel(network_interface: &Interface, config: Config) -> io::Result { +pub(crate) fn channel(network_interface: &Interface, config: Config) -> io::Result { let mut read_buffer = Vec::new(); read_buffer.resize(config.read_buffer_size, 0u8); let mut write_buffer = Vec::new(); write_buffer.resize(config.write_buffer_size, 0u8); + // SAFETY: PacketOpenAdapter reads the temporary NUL-terminated interface + // name during the call and returns an owned adapter handle. let adapter = unsafe { let npf_if_name: String = windows::to_npf_name(&network_interface.name); let net_if_str = CString::new(npf_if_name.as_bytes()).map_err(|_| { @@ -87,8 +105,12 @@ pub fn channel(network_interface: &Interface, config: Config) -> io::Result io::Result io::Result Option> { - let len = num_packets * packet_size; + if packet_size == 0 { + return Some(Err(io::Error::new( + io::ErrorKind::InvalidInput, + "packet_size must be greater than zero", + ))); + } + let len = num_packets.checked_mul(packet_size)?; + // SAFETY: The packet pointer is owned by `self` and initialized. if len >= unsafe { (*self.packet.packet).Length } as usize { None } else { + // SAFETY: The packet pointer is owned by `self` and initialized. let min = unsafe { cmp::min((*self.packet.packet).Length as usize, len) }; + // SAFETY: Npcap initialized Buffer for at least Length bytes, and + // `min` is capped to that length. let slice: &mut [u8] = unsafe { slice::from_raw_parts_mut((*self.packet.packet).Buffer as *mut u8, min) }; for chunk in slice.chunks_mut(packet_size) { func(chunk); // Make sure the right length of packet is sent + // SAFETY: The packet pointer is owned by `self`. let old_len = unsafe { (*self.packet.packet).Length }; + // SAFETY: `packet_size` is bounded by the packet buffer length. unsafe { (*self.packet.packet).Length = packet_size as u32; } + let _operation = match self.adapter.operation_lock.lock() { + Ok(lock) => lock, + Err(_) => { + return Some(Err(io::Error::other( + "Npcap adapter operation mutex poisoned", + ))); + } + }; + // SAFETY: The adapter operation is serialized, and both owned + // handles remain live for the duration of the call. let ret = unsafe { windows::PacketSendPacket(self.adapter.adapter, self.packet.packet, 0) }; + // SAFETY: The packet is still exclusively owned by `self`. unsafe { (*self.packet.packet).Length = old_len; } @@ -228,8 +286,9 @@ impl RawSender for RawSenderImpl { } } +// SAFETY: The raw packet is uniquely owned by this sender and adapter access is +// serialized by `WinPcapAdapter::operation_lock`. unsafe impl Send for RawSenderImpl {} -unsafe impl Sync for RawSenderImpl {} struct RawReceiverImpl { adapter: Arc, @@ -238,32 +297,77 @@ struct RawReceiverImpl { packets: VecDeque<(usize, usize)>, } +// SAFETY: The raw packet is uniquely owned by this receiver and adapter access +// is serialized by `WinPcapAdapter::operation_lock`. unsafe impl Send for RawReceiverImpl {} -unsafe impl Sync for RawReceiverImpl {} impl RawReceiver for RawReceiverImpl { fn next(&mut self) -> io::Result<&[u8]> { // NOTE Most of the logic here is identical to FreeBSD/OS X while self.packets.is_empty() { + let _operation = self + .adapter + .operation_lock + .lock() + .map_err(|_| io::Error::other("Npcap adapter operation mutex poisoned"))?; + // SAFETY: The adapter operation is serialized, and both owned + // handles remain live for the duration of the call. let ret = unsafe { windows::PacketReceivePacket(self.adapter.adapter, self.packet.packet, 0) }; let buflen = match ret { 0 => return Err(io::Error::last_os_error()), - _ => unsafe { (*self.packet.packet).ulBytesReceived as isize }, + // SAFETY: A successful receive initialized the byte count. + _ => unsafe { (*self.packet.packet).ulBytesReceived as usize }, }; - let mut ptr = unsafe { (*self.packet.packet).Buffer as *mut c_char }; - let end = unsafe { ((*self.packet.packet).Buffer as *mut c_char).offset(buflen) }; - while ptr < end { - unsafe { - let packet: *const bpf::bpf_hdr = mem::transmute(ptr); - let start = ptr as isize + (*packet).bh_hdrlen as isize - - (*self.packet.packet).Buffer as isize; - self.packets - .push_back((start as usize, (*packet).bh_caplen as usize)); - let offset = (*packet).bh_hdrlen as isize + (*packet).bh_caplen as isize; - ptr = ptr.offset(bpf::BPF_WORDALIGN(offset)); + // SAFETY: The packet remains live and was initialized with a buffer + // whose capacity is recorded in Length. + let buffer_capacity = unsafe { (*self.packet.packet).Length as usize }; + if buflen > buffer_capacity { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "Npcap reported bytes beyond the receive buffer", + )); + } + // SAFETY: The packet remains live and its buffer was initialized by + // the successful receive. + let base = unsafe { (*self.packet.packet).Buffer as *const u8 }; + let mut cursor = 0usize; + while cursor < buflen { + let remaining = buflen - cursor; + if remaining < mem::size_of::() { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "truncated Npcap BPF record header", + )); + } + // SAFETY: The complete header is in-bounds due to the size + // check. `read_unaligned` handles the backing Vec's alignment. + let packet = + unsafe { std::ptr::read_unaligned(base.add(cursor) as *const bpf::bpf_hdr) }; + let header_len = packet.bh_hdrlen as usize; + let captured_len = packet.bh_caplen as usize; + let record_len = header_len.checked_add(captured_len).ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidData, "Npcap record length overflow") + })?; + if header_len < mem::size_of::() || record_len > remaining { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "invalid Npcap BPF record lengths", + )); } + self.packets.push_back((cursor + header_len, captured_len)); + let record_len = isize::try_from(record_len).map_err(|_| { + io::Error::new( + io::ErrorKind::InvalidData, + "Npcap record exceeds platform pointer range", + ) + })?; + cursor = cursor + .checked_add(bpf::BPF_WORDALIGN(record_len) as usize) + .ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidData, "Npcap record offset overflow") + })?; } } let (start, len) = self.packets.pop_front().ok_or_else(|| { @@ -272,6 +376,8 @@ impl RawReceiver for RawReceiverImpl { "packet queue unexpectedly empty", ) })?; + // SAFETY: `start` and `len` came from a validated BPF record within the + // current packet buffer, which remains owned by `self`. let slice = unsafe { let data = (*self.packet.packet).Buffer as usize + start; slice::from_raw_parts(data as *const u8, len) diff --git a/nex-sys/src/lib.rs b/nex-sys/src/lib.rs index 1ce6ac2..2606ce7 100644 --- a/nex-sys/src/lib.rs +++ b/nex-sys/src/lib.rs @@ -18,6 +18,7 @@ mod windows; pub use self::windows::*; /// An owned Unix file descriptor or Windows socket. +#[derive(Debug)] pub struct FileDesc { fd: CSocket, } @@ -149,4 +150,30 @@ mod tests { // SAFETY: The write descriptor remains open and owned by this test. assert_eq!(unsafe { libc::close(pipe_fds[1]) }, 0); } + + #[cfg(not(target_os = "windows"))] + #[test] + fn file_desc_closes_owned_descriptor_on_early_error() { + fn fail_after_ownership_transfer(fd: CSocket) -> std::io::Result<()> { + // SAFETY: The caller transfers one live, exclusively owned + // descriptor to this function. + let _owned = unsafe { FileDesc::from_raw(fd) }; + Err(std::io::Error::other("injected setup failure")) + } + + let mut pipe_fds = [-1; 2]; + // SAFETY: `pipe_fds` provides writable storage for both descriptors. + assert_eq!(unsafe { libc::pipe(pipe_fds.as_mut_ptr()) }, 0); + assert!(fail_after_ownership_transfer(pipe_fds[0]).is_err()); + + // SAFETY: `fcntl` only inspects the descriptor number. + assert_eq!(unsafe { libc::fcntl(pipe_fds[0], libc::F_GETFD) }, -1); + assert_eq!( + std::io::Error::last_os_error().raw_os_error(), + Some(libc::EBADF) + ); + + // SAFETY: The write descriptor remains open and owned by this test. + assert_eq!(unsafe { libc::close(pipe_fds[1]) }, 0); + } } From 7c13525bc170b03479c9073270f3db2d6d482192 Mon Sep 17 00:00:00 2001 From: shellrow Date: Sun, 19 Jul 2026 17:01:34 +0900 Subject: [PATCH 17/33] feat: stabilize core network interface types --- nex-core/src/interface.rs | 29 +++++++++++++++++++++++++++++ nex-core/src/lib.rs | 5 +++++ 2 files changed, 34 insertions(+) diff --git a/nex-core/src/interface.rs b/nex-core/src/interface.rs index ef4f9f8..f196331 100644 --- a/nex-core/src/interface.rs +++ b/nex-core/src/interface.rs @@ -66,6 +66,7 @@ pub const IFF_RUNNING: u32 = libc::IFF_RUNNING as u32; /// Operational state of a network interface. #[derive(Clone, Copy, Eq, PartialEq, Hash, Debug)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[non_exhaustive] pub enum OperState { Unknown, NotPresent, @@ -154,6 +155,7 @@ impl From for OperState { /// Cross-platform classification of a network interface. #[derive(Clone, Copy, Eq, PartialEq, Hash, Debug)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[non_exhaustive] pub enum InterfaceType { Unknown, Ethernet, @@ -301,9 +303,13 @@ impl TryFrom for InterfaceType { /// Address information for a related network device. #[derive(Clone, Eq, PartialEq, Hash, Debug)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[non_exhaustive] pub struct NetworkDevice { + /// Link-layer address of the related device. pub mac_addr: MacAddr, + /// IPv4 addresses assigned to the device. pub ipv4: Vec, + /// IPv6 addresses assigned to the device. pub ipv6: Vec, } @@ -336,9 +342,13 @@ impl From for NetworkDevice { /// Interface traffic statistics at a given point in time. #[derive(Clone, Eq, PartialEq, Hash, Debug)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[non_exhaustive] pub struct InterfaceStats { + /// Total received bytes. pub rx_bytes: u64, + /// Total transmitted bytes. pub tx_bytes: u64, + /// Time at which the counters were sampled. pub timestamp: Option, } @@ -355,27 +365,46 @@ impl From for InterfaceStats { /// A network interface. #[derive(Clone, Eq, PartialEq, Hash, Debug)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[non_exhaustive] pub struct Interface { + /// Operating-system interface index. pub index: u32, + /// Operating-system interface name. pub name: String, + /// User-facing interface name when supplied by the platform. pub friendly_name: Option, + /// Platform-provided interface description. pub description: Option, + /// Classified interface hardware or transport type. pub if_type: InterfaceType, + /// Link-layer address, when one is available. pub mac_addr: Option, + /// Assigned IPv4 networks. pub ipv4: Vec, + /// Assigned IPv6 networks. pub ipv6: Vec, + /// Scope identifiers corresponding to scoped IPv6 addresses. pub ipv6_scope_ids: Vec, + /// Raw platform interface flags. pub flags: u32, + /// Current operational state. pub oper_state: OperState, + /// Reported transmit speed in bits per second. pub transmit_speed: Option, + /// Reported receive speed in bits per second. pub receive_speed: Option, + /// Most recently sampled traffic statistics. pub stats: Option, #[cfg(feature = "gateway")] + /// Default gateway reachable through this interface. pub gateway: Option, #[cfg(feature = "gateway")] + /// DNS servers associated with this interface. pub dns_servers: Vec, + /// Maximum transmission unit in bytes. pub mtu: Option, #[cfg(feature = "gateway")] + /// Whether this is the platform's default interface. pub default: bool, } diff --git a/nex-core/src/lib.rs b/nex-core/src/lib.rs index a8935dd..560c2e1 100644 --- a/nex-core/src/lib.rs +++ b/nex-core/src/lib.rs @@ -1,6 +1,11 @@ //! Core network types and helpers shared across the `nex` crates. //! Includes interface, MAC/IP, and bitfield utilities used by low-level networking code. +/// Implementation-detail integer aliases used by generated packet accessors. +/// +/// These aliases are public only so packet crates can share the generated +/// representation. They are not part of the stable application-facing API. +#[doc(hidden)] pub mod bitfield; pub mod interface; pub mod ip; From c0f195a29415ff3c47d3e9146047cec68ddebfba Mon Sep 17 00:00:00 2001 From: shellrow Date: Sun, 19 Jul 2026 17:02:36 +0900 Subject: [PATCH 18/33] feat: stabilize socket configuration APIs --- nex-socket/src/icmp/async_impl.rs | 4 ++++ nex-socket/src/icmp/config.rs | 3 +++ nex-socket/src/lib.rs | 1 + nex-socket/src/tcp/async_impl.rs | 3 ++- nex-socket/src/tcp/config.rs | 2 ++ nex-socket/src/tcp/sync_impl.rs | 20 +++++++++++++++++--- nex-socket/src/udp/async_impl.rs | 8 ++++++++ nex-socket/src/udp/config.rs | 2 ++ nex-socket/src/udp/mod.rs | 4 ++++ nex-socket/src/udp/sync_impl.rs | 7 ++++++- 10 files changed, 49 insertions(+), 5 deletions(-) diff --git a/nex-socket/src/icmp/async_impl.rs b/nex-socket/src/icmp/async_impl.rs index 9a34863..4030b70 100644 --- a/nex-socket/src/icmp/async_impl.rs +++ b/nex-socket/src/icmp/async_impl.rs @@ -69,12 +69,16 @@ impl AsyncIcmpSocket { // Convert socket2::Socket into std::net::UdpSocket #[cfg(windows)] + // SAFETY: `into_raw_socket` transfers the valid socket exactly once to + // `StdUdpSocket`. let std_socket = unsafe { use std::os::windows::io::{FromRawSocket, IntoRawSocket}; StdUdpSocket::from_raw_socket(socket.into_raw_socket()) }; #[cfg(unix)] + // SAFETY: `into_raw_fd` transfers the valid descriptor exactly once to + // `StdUdpSocket`. let std_socket = unsafe { use std::os::fd::{FromRawFd, IntoRawFd}; diff --git a/nex-socket/src/icmp/config.rs b/nex-socket/src/icmp/config.rs index cf04c86..9acb566 100644 --- a/nex-socket/src/icmp/config.rs +++ b/nex-socket/src/icmp/config.rs @@ -5,6 +5,7 @@ use crate::SocketFamily; /// ICMP protocol version. #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] pub enum IcmpKind { V4, V6, @@ -12,6 +13,7 @@ pub enum IcmpKind { /// ICMP socket type, either DGRAM or RAW. #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] pub enum IcmpSocketType { Dgram, Raw, @@ -51,6 +53,7 @@ impl IcmpSocketType { /// Configuration for an ICMP socket. #[derive(Debug, Clone)] +#[non_exhaustive] pub struct IcmpConfig { /// The socket family. pub socket_family: SocketFamily, diff --git a/nex-socket/src/lib.rs b/nex-socket/src/lib.rs index 9b31ecf..339558f 100644 --- a/nex-socket/src/lib.rs +++ b/nex-socket/src/lib.rs @@ -11,6 +11,7 @@ use std::net::{IpAddr, SocketAddr}; /// Represents the socket address family (IPv4 or IPv6) #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] pub enum SocketFamily { IPV4, IPV6, diff --git a/nex-socket/src/tcp/async_impl.rs b/nex-socket/src/tcp/async_impl.rs index 4c476f8..c79e03b 100644 --- a/nex-socket/src/tcp/async_impl.rs +++ b/nex-socket/src/tcp/async_impl.rs @@ -193,7 +193,8 @@ impl AsyncTcpSocket { /// Receive a raw TCP packet. Requires `SockType::RAW`. pub fn recv_from(&self, buf: &mut [u8]) -> io::Result<(usize, SocketAddr)> { - // Safety: `MaybeUninit` has the same memory layout as `u8`. + // SAFETY: `MaybeUninit` has the same layout as `u8`, and the slice + // preserves the original buffer's length and lifetime. let buf_maybe = unsafe { std::slice::from_raw_parts_mut( buf.as_mut_ptr() as *mut std::mem::MaybeUninit, diff --git a/nex-socket/src/tcp/config.rs b/nex-socket/src/tcp/config.rs index a634590..8266ecd 100644 --- a/nex-socket/src/tcp/config.rs +++ b/nex-socket/src/tcp/config.rs @@ -7,6 +7,7 @@ use crate::SocketFamily; /// TCP socket type, either STREAM or RAW. #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] pub enum TcpSocketType { Stream, Raw, @@ -34,6 +35,7 @@ impl TcpSocketType { /// Configuration options for a TCP socket. #[derive(Debug, Clone)] +#[non_exhaustive] pub struct TcpConfig { /// The socket family, either IPv4 or IPv6. pub socket_family: SocketFamily, diff --git a/nex-socket/src/tcp/sync_impl.rs b/nex-socket/src/tcp/sync_impl.rs index eaeb596..a50b9f2 100644 --- a/nex-socket/src/tcp/sync_impl.rs +++ b/nex-socket/src/tcp/sync_impl.rs @@ -183,7 +183,8 @@ impl TcpSocket { // Wait for the connection using poll use std::os::unix::io::BorrowedFd; - // Safety: raw_fd is valid for the lifetime of this scope + // SAFETY: `raw_fd` belongs to `socket` and remains valid for this scope; + // BorrowedFd does not take ownership. let mut fds = [PollFd::new( unsafe { BorrowedFd::borrow_raw(raw_fd) }, PollFlags::POLLOUT, @@ -246,6 +247,8 @@ impl TcpSocket { }]; let timeout_ms = timeout.as_millis().clamp(0, i32::MAX as u128) as i32; + // SAFETY: `fds` is writable for the supplied element count and remains + // live throughout WSAPoll. let result = unsafe { WSAPoll(fds.as_mut_ptr(), fds.len() as u32, timeout_ms) }; if result == SOCKET_ERROR { return Err(io::Error::last_os_error()); @@ -256,6 +259,8 @@ impl TcpSocket { // Check for errors via `SO_ERROR` let mut so_error: i32 = 0; let mut optlen = size_of::() as i32; + // SAFETY: `so_error` and `optlen` are writable for the duration of + // getsockopt and `sock` is open. let ret = unsafe { getsockopt( sock, @@ -284,7 +289,13 @@ impl TcpSocket { /// Accept an incoming connection. pub fn accept(&self) -> io::Result<(TcpStream, SocketAddr)> { let (stream, addr) = self.socket.accept()?; - Ok((stream.into(), addr.as_socket().unwrap())) + let address = addr.as_socket().ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidData, + "accepted peer did not provide an IP socket address", + ) + })?; + Ok((stream.into(), address)) } /// Convert the socket into a `TcpStream`. @@ -304,7 +315,8 @@ impl TcpSocket { /// Receive a raw packet (for RAW TCP use). pub fn recv_from(&self, buf: &mut [u8]) -> io::Result<(usize, SocketAddr)> { - // Safety: `MaybeUninit` is layout-compatible with `u8`. + // SAFETY: `MaybeUninit` is layout-compatible with `u8`, and the + // slice preserves the original buffer's length and lifetime. let buf_maybe = unsafe { std::slice::from_raw_parts_mut( buf.as_mut_ptr() as *mut std::mem::MaybeUninit, @@ -566,6 +578,8 @@ mod tests { #[cfg(unix)] fn socket_is_nonblocking(socket: &Socket) -> bool { + // SAFETY: The descriptor belongs to the borrowed live socket and F_GETFL + // neither takes ownership nor retains pointers. let flags = unsafe { fcntl(socket.as_raw_fd(), F_GETFL) }; assert!(flags >= 0, "F_GETFL failed: {}", io::Error::last_os_error()); (flags & O_NONBLOCK) != 0 diff --git a/nex-socket/src/udp/async_impl.rs b/nex-socket/src/udp/async_impl.rs index a7b5a83..c76937e 100644 --- a/nex-socket/src/udp/async_impl.rs +++ b/nex-socket/src/udp/async_impl.rs @@ -104,11 +104,15 @@ impl AsyncUdpSocket { } #[cfg(windows)] + // SAFETY: `into_raw_socket` transfers the valid socket exactly once to + // `StdUdpSocket`. let std_socket = unsafe { use std::os::windows::io::{FromRawSocket, IntoRawSocket}; StdUdpSocket::from_raw_socket(socket.into_raw_socket()) }; #[cfg(unix)] + // SAFETY: `into_raw_fd` transfers the valid descriptor exactly once to + // `StdUdpSocket`. let std_socket = unsafe { use std::os::fd::{FromRawFd, IntoRawFd}; StdUdpSocket::from_raw_fd(socket.into_raw_fd()) @@ -125,11 +129,15 @@ impl AsyncUdpSocket { socket.set_nonblocking(true)?; #[cfg(windows)] + // SAFETY: `into_raw_socket` transfers the valid socket exactly once to + // `StdUdpSocket`. let std_socket = unsafe { use std::os::windows::io::{FromRawSocket, IntoRawSocket}; StdUdpSocket::from_raw_socket(socket.into_raw_socket()) }; #[cfg(unix)] + // SAFETY: `into_raw_fd` transfers the valid descriptor exactly once to + // `StdUdpSocket`. let std_socket = unsafe { use std::os::fd::{FromRawFd, IntoRawFd}; StdUdpSocket::from_raw_fd(socket.into_raw_fd()) diff --git a/nex-socket/src/udp/config.rs b/nex-socket/src/udp/config.rs index 3b59994..0b7786d 100644 --- a/nex-socket/src/udp/config.rs +++ b/nex-socket/src/udp/config.rs @@ -6,6 +6,7 @@ use crate::SocketFamily; /// UDP socket type, either DGRAM or RAW. #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] pub enum UdpSocketType { Dgram, Raw, @@ -33,6 +34,7 @@ impl UdpSocketType { /// Configuration options for a UDP socket. #[derive(Debug, Clone)] +#[non_exhaustive] pub struct UdpConfig { /// The socket family. pub socket_family: SocketFamily, diff --git a/nex-socket/src/udp/mod.rs b/nex-socket/src/udp/mod.rs index 709c834..39957be 100644 --- a/nex-socket/src/udp/mod.rs +++ b/nex-socket/src/udp/mod.rs @@ -22,6 +22,8 @@ fn set_bool_sockopt( ) -> io::Result<()> { use std::os::fd::AsRawFd; let value: libc::c_int = if on { 1 } else { 0 }; + // SAFETY: The socket descriptor is open and `value` is readable with the + // exact size supplied for the duration of setsockopt. let ret = unsafe { libc::setsockopt( socket.as_raw_fd(), @@ -43,6 +45,8 @@ fn get_bool_sockopt(socket: &Socket, level: libc::c_int, optname: libc::c_int) - use std::os::fd::AsRawFd; let mut value: libc::c_int = 0; let mut len = std::mem::size_of::() as libc::socklen_t; + // SAFETY: The socket descriptor is open; `value` and `len` are writable for + // the duration of getsockopt. let ret = unsafe { libc::getsockopt( socket.as_raw_fd(), diff --git a/nex-socket/src/udp/sync_impl.rs b/nex-socket/src/udp/sync_impl.rs index 8bcd6ef..6d94413 100644 --- a/nex-socket/src/udp/sync_impl.rs +++ b/nex-socket/src/udp/sync_impl.rs @@ -199,6 +199,8 @@ impl UdpSocket { "source_addr family does not match target", )); } + // SAFETY: A zero bit pattern is a valid initial + // `in_pktinfo` value. let mut pktinfo: libc::in_pktinfo = unsafe { std::mem::zeroed() }; if let Some(src) = meta.source_addr.and_then(|ip| match ip { IpAddr::V4(v4) => Some(v4), @@ -245,6 +247,8 @@ impl UdpSocket { "source_addr family does not match target", )); } + // SAFETY: A zero bit pattern is a valid initial + // `in6_pktinfo` value. let mut pktinfo: libc::in6_pktinfo = unsafe { std::mem::zeroed() }; if let Some(src) = meta.source_addr.and_then(|ip| match ip { IpAddr::V4(_) => None, @@ -293,7 +297,8 @@ impl UdpSocket { /// Receive data. pub fn recv_from(&self, buf: &mut [u8]) -> io::Result<(usize, SocketAddr)> { - // Safety: `MaybeUninit` has the same layout as `u8`. + // SAFETY: `MaybeUninit` has the same layout as `u8`, and the slice + // preserves the original buffer's length and lifetime. let buf_maybe = unsafe { std::slice::from_raw_parts_mut( buf.as_mut_ptr() as *mut std::mem::MaybeUninit, From a977e0c056ddd0f33a14323a0d4a4792bd48e42e Mon Sep 17 00:00:00 2001 From: shellrow Date: Sun, 19 Jul 2026 17:04:31 +0900 Subject: [PATCH 19/33] docs: update docs --- TODO.md | 52 +++++++++++++++++++------------------- docs/PRIVILEGED_TESTING.md | 46 +++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 26 deletions(-) create mode 100644 docs/PRIVILEGED_TESTING.md diff --git a/TODO.md b/TODO.md index 6cd6333..7ef744a 100644 --- a/TODO.md +++ b/TODO.md @@ -38,9 +38,9 @@ Today the surface is inconsistent and confusing. Observed across `nex-packet`: Actions: -- [ ] Define ONE canonical parsing contract and document it in `parse.rs`: - - [ ] `try_from_bytes(Bytes) -> Result` — owned, zero-copy view. - - [ ] `try_from_buf(&[u8]) -> Result` — borrowed. +- [x] Define ONE canonical parsing contract and document it in `parse.rs`: + - [x] `try_from_bytes(Bytes) -> Result` — owned, zero-copy view. + - [x] `try_from_buf(&[u8]) -> Result` — borrowed. - [x] A single explicit "strict payload-length" opt-in (e.g. a `Strictness`/ `ParseOption` arg) instead of `*_strict` name-doubling every method. - [x] Remove `EthernetHeader::from_bytes -> Result<_, String>`; replace with the @@ -77,24 +77,24 @@ property tests (P1.5) confirm build→parse round-trips for valid inputs only. ### P0.3 Freeze the public API surface (semver contract) -- [ ] Audit every `pub` item per crate; mark internal helpers `pub(crate)`. -- [ ] `nex-core::bitfield` (`pub type u1 = u8 …`): make private or move behind a +- [x] Audit every `pub` item per crate; mark internal helpers `pub(crate)`. +- [x] `nex-core::bitfield` (`pub type u1 = u8 …`): make private or move behind a documented `#[doc(hidden)]` implementation-detail boundary — these aliases should not be part of the stable contract. - [x] Decide `nex-sys`'s status: it is a low-level internal crate — either mark it clearly "internal, no semver guarantees" in its docs or make its surface `pub(crate)`-equivalent via `#[doc(hidden)]`. -- [ ] Normalize accessor naming: replace `get_*` (73 occurrences in `nex-packet`) +- [x] Normalize accessor naming: replace `get_*` (73 occurrences in `nex-packet`) with idiomatic Rust names; keep `#[deprecated]` aliases for one release. -- [ ] Audit public struct fields (e.g. `datalink::Config`, packet headers): decide +- [x] Audit public struct fields (e.g. `datalink::Config`, packet headers): decide field-access vs accessor policy; document undocumented `pub` fields (`Config.linux_fanout`, `Config.promiscuous` lack doc comments). -- [ ] Apply `#[non_exhaustive]` to all enums/structs expecting future variants +- [x] Apply `#[non_exhaustive]` to all enums/structs expecting future variants (protocol number enums, `Channel`, error types, config structs). -- [ ] Audit `pub const` protocol constants for naming + semver stability. -- [ ] Document `new_unchecked` invariants (`GenericMutablePacket::new_unchecked`, +- [x] Audit `pub const` protocol constants for naming + semver stability. +- [x] Document `new_unchecked` invariants (`GenericMutablePacket::new_unchecked`, any `*_unchecked`) with `# Safety` sections. -- [ ] Generate a machine-readable public API snapshot per crate (e.g. +- [x] Generate a machine-readable public API snapshot per crate (e.g. `cargo public-api`) and diff it in CI to catch accidental breaks. Acceptance: a documented, intentional surface; `cargo public-api` diff is clean and @@ -106,12 +106,12 @@ tracked; no accidental `pub` internals. (`nex-core/src/interface.rs:359,592,597` — `default`, `get_default_interface`, `get_default_gateway`). - [x] Add `nex-packet::BuildError` for builders (P0.2). -- [ ] Keep `io::Error` only at raw syscall/socket boundaries; convert to typed +- [x] Keep `io::Error` only at raw syscall/socket boundaries; convert to typed errors where the library adds semantic meaning (datalink/socket config). -- [ ] Ensure `ParseError` carries enough context for fuzz triage and diagnostics +- [x] Ensure `ParseError` carries enough context for fuzz triage and diagnostics (it already has `context`; verify every construction site sets a useful one). -- [ ] All public error types implement `std::error::Error + Send + Sync + 'static`. -- [ ] No `panic!`/`unreachable!`/`unwrap` reachable from public APIs on malformed +- [x] All public error types implement `std::error::Error + Send + Sync + 'static`. +- [x] No `panic!`/`unreachable!`/`unwrap` reachable from public APIs on malformed input. (Current `unreachable!`/`panic!` sites are test-only — keep it that way and add a CI grep guard.) @@ -154,11 +154,11 @@ Current `.github/workflows/rust.yml` runs only `cargo build` on Linux/macOS/Wind `rust-version` in every `Cargo.toml` and test it). - [x] Supply chain: `cargo deny check` (licenses, advisories, bans, sources) + add `deny.toml`. -- [ ] Public API snapshot diff job (P0.3). +- [x] Public API snapshot diff job (P0.3). - [x] Remove `#![deny(warnings)]` from `nex-datalink/src/lib.rs` (line 3): it makes builds break on future compilers/new lints. Enforce warnings in CI via `RUSTFLAGS=-Dwarnings`, not in source. -- [ ] A documented, manual privileged-test matrix for raw sockets / datalink I/O +- [x] A documented, manual privileged-test matrix for raw sockets / datalink I/O that CI cannot run (see P1.2 / P1.3). Acceptance: a red/green CI that actually gates merges on tests, lints, features, @@ -166,22 +166,22 @@ MSRV, and supply chain across all three OSes. ### P0.7 Unsafe & OS-resource safety hardening -- [ ] Add a `# Safety` comment to every `unsafe` block and every `unsafe impl`: +- [x] Add a `# Safety` comment to every `unsafe` block and every `unsafe impl`: - [x] Complete the `nex-sys` audit, including public `unsafe fn` contracts. - - [ ] Complete the `nex-datalink` audit (~141 sites). -- [ ] Justify or remove the 9 `unsafe impl Send/Sync` in `nex-datalink` + - [x] Complete the `nex-datalink` audit (~141 sites). +- [x] Justify or remove the 9 `unsafe impl Send/Sync` in `nex-datalink` (`wpcap.rs`, `async_io/wpcap.rs`): document what makes the raw Npcap handles actually thread-safe, or wrap them so the impl is sound. -- [ ] Ensure every OS handle (fd, BPF device, Npcap adapter, packet buffer) is +- [x] Ensure every OS handle (fd, BPF device, Npcap adapter, packet buffer) is owned by an RAII type that closes on *all* error paths. - [x] Make `nex-sys::FileDesc` an explicit owned descriptor with a private raw field, an unsafe ownership-transfer constructor, and a drop test. - - [ ] Audit that every fd flows through an owning wrapper and that no early-return + - [x] Audit that every fd flows through an owning wrapper and that no early-return leaks a half-opened resource. -- [ ] Prefer typed wrappers over raw integer/pointer handles at module boundaries. -- [ ] Add error-path tests that open then fail to confirm no leak/double-close. -- [ ] Run Miri on pure `nex-packet`/`nex-core` parsing/building where feasible. -- [ ] Run ASan/UBSan on Linux packet parse + datalink where feasible. +- [x] Prefer typed wrappers over raw integer/pointer handles at module boundaries. +- [x] Add error-path tests that open then fail to confirm no leak/double-close. +- [x] Run Miri on pure `nex-packet`/`nex-core` parsing/building where feasible. +- [x] Run ASan/UBSan on Linux packet parse + datalink where feasible. Acceptance: `cargo miri test` passes for pure logic crates; every unsafe site has a rationale; a reviewer can audit the FFI boundary from comments alone. diff --git a/docs/PRIVILEGED_TESTING.md b/docs/PRIVILEGED_TESTING.md new file mode 100644 index 0000000..6c596b8 --- /dev/null +++ b/docs/PRIVILEGED_TESTING.md @@ -0,0 +1,46 @@ +# Manual Privileged Test Matrix + +Run this matrix before a release on dedicated hosts or disposable virtual +machines. Do not run it on an untrusted shared network. Record the OS version, +Rust version, interface name, command, and result in the release checklist. + +| Platform | Prerequisites | Synchronous checks | Asynchronous checks | Expected result | +| --- | --- | --- | --- | --- | +| Linux | Root or `CAP_NET_RAW`; loopback plus a veth pair | `dump`, `arp`, `icmp_ping`, `tcp_ping`, `udp_ping`; Layer2 and Layer3 channels; promiscuous off/on; fanout with two receivers | `async_datalink`, `async_dump`, async ICMP/TCP/UDP socket examples | Packets transmit and receive, timeouts are honored, and all processes exit without leaked descriptors. | +| macOS | Root; one Ethernet or Wi-Fi interface plus loopback; available `/dev/bpf*` devices | `dump`, `arp`, `icmp_ping`, `tcp_ping`, `udp_ping`; loopback and non-loopback BPF; repeated open/close beyond one BPF descriptor | `async_datalink`, `async_dump`, async socket examples | BPF headers are decoded, loopback header translation is correct, and descriptors return to the pre-test count. | +| FreeBSD/OpenBSD/NetBSD | Root; BPF enabled; loopback plus one test interface | Same BPF examples as macOS, including immediate mode and configured timeouts | `async_datalink` and `async_dump` | Device selection and BPF alignment work without truncated frames or descriptor leaks. | +| Windows | Administrator; current Npcap in WinPcap-compatible mode | `dump`, `arp`, `icmp_ping`, `tcp_ping`, `udp_ping`; repeated channel creation; concurrent sender and receiver | `async_datalink`, `async_dump`, async socket examples; repeatedly create and drop channels | Adapter names resolve, send/receive operations remain serialized, worker threads stop on drop, and Npcap handles are freed once. | + +## Error-path leak check + +For each backend, capture the process handle count before the test, repeatedly +open a channel with a deliberately invalid interface or configuration after the +first resource allocation point, and capture the count again. Linux and BSD +hosts should inspect `/proc//fd` or `lsof -p ` as available. Windows +hosts should use Process Explorer or `Get-Process`. + +The count must return to baseline after every iteration. Run at least 1,000 +iterations. Any monotonically increasing handle count, double-close diagnostic, +sanitizer report, or worker thread surviving channel drop is a release blocker. + +## Sanitizers and Miri + +Run pure logic under Miri: + +```sh +cargo +nightly miri test -p nex-core --lib +cargo +nightly miri test -p nex-packet --lib +``` + +Run Linux parsing and datalink tests under AddressSanitizer and LeakSanitizer on +a nightly toolchain: + +```sh +RUSTFLAGS="-Zsanitizer=address" cargo +nightly test -p nex-packet -p nex-datalink --lib --target x86_64-unknown-linux-gnu +RUSTFLAGS="-Zsanitizer=leak" cargo +nightly test -p nex-packet -p nex-datalink --lib --target x86_64-unknown-linux-gnu +``` + +Rust does not currently expose LLVM UndefinedBehaviorSanitizer through +`-Zsanitizer`; Miri supplies the pure-Rust undefined-behavior check instead. +Privileged datalink examples must also be exercised under each available +sanitizer on the Linux test host. From d4b26fa5a9634334e6567705acf58a9e20eb6e1a Mon Sep 17 00:00:00 2001 From: shellrow Date: Sun, 19 Jul 2026 21:44:00 +0900 Subject: [PATCH 20/33] perf: add allocation-free views and cache mutable layouts --- docs/PACKET_MODEL.md | 48 +++++ nex-packet/benches/packet_parse.rs | 5 +- nex-packet/src/frame.rs | 272 ++++++++++++++++++++++++++++- nex-packet/src/lib.rs | 4 + nex-packet/src/packet.rs | 244 +++++++++++++++++++++----- 5 files changed, 532 insertions(+), 41 deletions(-) create mode 100644 docs/PACKET_MODEL.md diff --git a/docs/PACKET_MODEL.md b/docs/PACKET_MODEL.md new file mode 100644 index 0000000..dfb0971 --- /dev/null +++ b/docs/PACKET_MODEL.md @@ -0,0 +1,48 @@ +# Packet Model + +`nex-packet` separates packet handling into four categories: + +| Category | Ownership | Mutation | Typical types | +| --- | --- | --- | --- | +| Borrowed view | Borrows caller bytes | No | `FrameSlice<'a>`, `FrameView<'a>` | +| Mutable borrowed view | Exclusively borrows caller bytes | In place | `MutableIpv4Packet<'a>`, `GenericMutablePacket<'a, P>` | +| Owned decoded packet | Owns serialized `Bytes` and decoded fields | By replacing owned fields | `Ipv4Packet`, `TcpPacket`, `DnsPacket` | +| Builder | Owns construction state | Validated setters/build | Types under `builder` | + +Use allocation-free `FrameSlice` for layer boundaries on a hot path. +`FrameView` retains decoded header compatibility and may allocate for variable +options. Use a mutable view when editing an existing buffer, an owned packet +when data must outlive the input, and a builder when constructing new wire +data. + +## Mutable layout and freeze + +Protocol-specific mutable views validate their minimum layout on construction. +`GenericMutablePacket` additionally caches header and payload boundaries after +one parse. Ordinary field and payload edits do not cause another parse. +Changing a structural field through `packet_mut()` does not update the cached +boundaries; call `refresh_layout()` before requesting slices under the new +layout. + +`freeze()` is the commit point. It parses the current bytes again and returns +an owned packet only when the complete layout is valid. The parse is deliberate: +it prevents stale or inconsistent mutable bytes from becoming an owned packet. + +The allocation-returning `Packet::to_bytes_mut`, `header_mut`, and +`payload_mut` compatibility methods are deprecated. Their explicit +`copy_*_to_bytes_mut` replacements make the allocation visible at call sites. + +## Generated representation details + +The `nex_core::bitfield` aliases are `#[doc(hidden)]` implementation details +shared by packet field representations. They are public for cross-crate code +generation only and carry no independent semantic contract. + +## Variable-length protocol data + +IPv4 options, IPv6 extension headers, TCP options, DNS compression, and DHCP +options are length-delimited and reject truncated structural fields. Parsers +that distinguish capture truncation expose `ParseMode`: lenient mode preserves +available captured payload where documented, while strict mode rejects data +shorter than its declared protocol length. Fuzz and property tests exercise +both arbitrary input and valid parse/serialize round trips. diff --git a/nex-packet/benches/packet_parse.rs b/nex-packet/benches/packet_parse.rs index 4b29489..19c6307 100644 --- a/nex-packet/benches/packet_parse.rs +++ b/nex-packet/benches/packet_parse.rs @@ -1,7 +1,7 @@ use bytes::Bytes; use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; use nex_packet::{ - frame::{Frame, FrameView, ParseOption}, + frame::{Frame, FrameSlice, FrameView, ParseOption}, tcp::TcpPacket, udp::UdpPacket, }; @@ -39,6 +39,9 @@ fn bench_packet_parse(c: &mut Criterion) { group.bench_function("frame_view_from_buf_ipv4_tcp", |b| { b.iter(|| FrameView::try_from_buf(&ipv4_tcp, ParseOption::default())) }); + group.bench_function("frame_slice_from_buf_ipv4_tcp", |b| { + b.iter(|| FrameSlice::try_from_buf(&ipv4_tcp, ParseOption::default())) + }); group.bench_function("tcp_from_buf", |b| { b.iter(|| TcpPacket::try_from_buf(&tcp_segment)) }); diff --git a/nex-packet/src/frame.rs b/nex-packet/src/frame.rs index 1b98ffe..ee9b349 100644 --- a/nex-packet/src/frame.rs +++ b/nex-packet/src/frame.rs @@ -59,6 +59,233 @@ pub struct Frame { pub packet_len: usize, } +/// Allocation-free borrowed slices for each decoded frame layer. +/// +/// This view identifies protocol boundaries without constructing owned packet +/// headers. Use the protocol-specific parser for decoded fields. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct FrameSlice<'a> { + /// Complete captured input. + pub packet: &'a [u8], + /// Link-layer header, when the input includes one. + pub datalink: Option<&'a [u8]>, + /// IP or ARP header, including variable-length options/extensions. + pub network: Option<&'a [u8]>, + /// TCP, UDP, ICMP, or ICMPv6 header. + pub transport: Option<&'a [u8]>, + /// Bytes after the last recognized header. + pub payload: &'a [u8], + /// Decoded Ethernet type when available. + pub ethertype: Option, + /// Decoded IP next-header value when available. + pub ip_protocol: Option, +} + +impl<'a> FrameSlice<'a> { + /// Parse layer boundaries without copying or allocating packet bytes. + pub fn try_from_buf(packet: &'a [u8], option: ParseOption) -> Result { + let (datalink, ethertype, network_bytes) = if option.from_ip_packet { + let network_bytes = packet + .get(option.offset..) + .ok_or(ParseError::InvalidLength { + context: "frame IP offset", + value: option.offset, + })?; + let ethertype = match network_bytes.first().map(|byte| byte >> 4) { + Some(4) => EtherType::Ipv4, + Some(6) => EtherType::Ipv6, + _ => { + return Err(ParseError::Malformed { + context: "frame IP version", + }); + } + }; + (None, ethertype, network_bytes) + } else { + if packet.len() < 14 { + return Err(ParseError::BufferTooShort { + context: "Ethernet frame", + minimum: 14, + actual: packet.len(), + }); + } + ( + Some(&packet[..14]), + EtherType::new(u16::from_be_bytes([packet[12], packet[13]])), + &packet[14..], + ) + }; + + let mut view = Self { + packet, + datalink, + network: None, + transport: None, + payload: network_bytes, + ethertype: Some(ethertype), + ip_protocol: None, + }; + + match ethertype { + EtherType::Ipv4 => view.parse_ipv4(network_bytes)?, + EtherType::Ipv6 => view.parse_ipv6(network_bytes)?, + EtherType::Arp if network_bytes.len() >= 28 => { + view.network = Some(&network_bytes[..28]); + view.payload = &network_bytes[28..]; + } + _ => {} + } + Ok(view) + } + + fn parse_ipv4(&mut self, bytes: &'a [u8]) -> Result<(), ParseError> { + if bytes.len() < 20 { + return Err(ParseError::BufferTooShort { + context: "IPv4 frame", + minimum: 20, + actual: bytes.len(), + }); + } + if bytes[0] >> 4 != 4 { + return Err(ParseError::Malformed { + context: "IPv4 frame version", + }); + } + let header_len = (bytes[0] as usize & 0x0f) * 4; + if header_len < 20 || header_len > bytes.len() { + return Err(ParseError::InvalidLength { + context: "IPv4 frame header", + value: header_len, + }); + } + let declared = u16::from_be_bytes([bytes[2], bytes[3]]) as usize; + let packet_len = if declared == 0 { + bytes.len() + } else { + declared.min(bytes.len()) + }; + if packet_len < header_len { + return Err(ParseError::InvalidLength { + context: "IPv4 frame total length", + value: declared, + }); + } + self.network = Some(&bytes[..header_len]); + let protocol = IpNextProtocol::new(bytes[9]); + self.ip_protocol = Some(protocol); + self.parse_transport(protocol, &bytes[header_len..packet_len]) + } + + fn parse_ipv6(&mut self, bytes: &'a [u8]) -> Result<(), ParseError> { + if bytes.len() < 40 { + return Err(ParseError::BufferTooShort { + context: "IPv6 frame", + minimum: 40, + actual: bytes.len(), + }); + } + if bytes[0] >> 4 != 6 { + return Err(ParseError::Malformed { + context: "IPv6 frame version", + }); + } + let declared_payload = u16::from_be_bytes([bytes[4], bytes[5]]) as usize; + let packet_len = 40usize + .checked_add(declared_payload) + .map(|length| length.min(bytes.len())) + .ok_or(ParseError::InvalidLength { + context: "IPv6 frame payload length", + value: declared_payload, + })?; + let mut next = bytes[6]; + let mut cursor = 40usize; + while cursor < packet_len { + let extension_len = match next { + 0 | 43 | 60 => { + if cursor + 2 > packet_len { + return Err(ParseError::Truncated { + context: "IPv6 extension header", + expected: cursor + 2, + actual: packet_len, + }); + } + (bytes[cursor + 1] as usize + 1) * 8 + } + 44 => 8, + 51 => { + if cursor + 2 > packet_len { + return Err(ParseError::Truncated { + context: "IPv6 authentication header", + expected: cursor + 2, + actual: packet_len, + }); + } + (bytes[cursor + 1] as usize + 2) * 4 + } + _ => break, + }; + let end = cursor + .checked_add(extension_len) + .filter(|end| *end <= packet_len) + .ok_or(ParseError::Truncated { + context: "IPv6 extension header", + expected: cursor.saturating_add(extension_len), + actual: packet_len, + })?; + next = bytes[cursor]; + cursor = end; + } + self.network = Some(&bytes[..cursor]); + let protocol = IpNextProtocol::new(next); + self.ip_protocol = Some(protocol); + self.parse_transport(protocol, &bytes[cursor..packet_len]) + } + + fn parse_transport( + &mut self, + protocol: IpNextProtocol, + bytes: &'a [u8], + ) -> Result<(), ParseError> { + let header_len = match protocol { + IpNextProtocol::Tcp => { + if bytes.len() < 20 { + self.payload = bytes; + return Ok(()); + } + let length = (bytes[12] as usize >> 4) * 4; + if length < 20 || length > bytes.len() { + return Err(ParseError::InvalidLength { + context: "TCP frame header", + value: length, + }); + } + length + } + IpNextProtocol::Udp => { + if bytes.len() < 8 { + self.payload = bytes; + return Ok(()); + } + 8 + } + IpNextProtocol::Icmp | IpNextProtocol::Icmpv6 => { + if bytes.len() < 4 { + self.payload = bytes; + return Ok(()); + } + 4 + } + _ => { + self.payload = bytes; + return Ok(()); + } + }; + self.transport = Some(&bytes[..header_len]); + self.payload = &bytes[header_len..]; + Ok(()) + } +} + impl Frame { /// Parse a frame from a raw buffer. /// @@ -115,7 +342,11 @@ impl Frame { } } -/// Borrowed frame view for zero-copy packet inspection on hot paths. +/// Borrowed-payload frame view with decoded owned headers. +/// +/// This compatibility type constructs decoded header values and may allocate +/// for packet options. Use [`FrameSlice`] when allocation-free layer slicing is +/// required. #[derive(Clone, Debug, PartialEq, Eq)] pub struct FrameView<'a> { pub datalink: Option, @@ -512,4 +743,43 @@ mod tests { assert_eq!(packet.header.ethertype, EtherType::Ipv4); assert_eq!(packet.payload, Bytes::from(ipv4.to_vec())); } + + #[test] + fn frame_slice_borrows_ipv4_tcp_layers() { + let bytes = [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0x08, 0x00, 0x45, 0, 0, 44, 0, 0, 0, 0, 64, 6, 0, + 0, 192, 0, 2, 1, 198, 51, 100, 2, 0, 80, 0x04, 0xd2, 0, 0, 0, 0, 0, 0, 0, 0, 0x50, + 0x18, 0, 0, 0, 0, 0, 0, b'd', b'a', b't', b'a', + ]; + + let view = FrameSlice::try_from_buf(&bytes, ParseOption::default()).expect("frame slice"); + assert_eq!(view.datalink, Some(&bytes[..14])); + assert_eq!(view.network, Some(&bytes[14..34])); + assert_eq!(view.transport, Some(&bytes[34..54])); + assert_eq!(view.payload, b"data"); + assert_eq!(view.ip_protocol, Some(IpNextProtocol::Tcp)); + assert!(std::ptr::eq(view.payload.as_ptr(), bytes[54..].as_ptr())); + } + + #[test] + fn frame_slice_walks_ipv6_extension_headers() { + let mut bytes = vec![0u8; 14 + 40 + 8 + 8 + 3]; + bytes[12..14].copy_from_slice(&0x86ddu16.to_be_bytes()); + bytes[14] = 0x60; + bytes[18..20].copy_from_slice(&19u16.to_be_bytes()); + bytes[20] = 0; + bytes[21] = 64; + bytes[54] = 17; + bytes[55] = 0; + bytes[62..64].copy_from_slice(&1234u16.to_be_bytes()); + bytes[64..66].copy_from_slice(&53u16.to_be_bytes()); + bytes[66..68].copy_from_slice(&11u16.to_be_bytes()); + bytes[70..].copy_from_slice(b"dns"); + + let view = FrameSlice::try_from_buf(&bytes, ParseOption::default()).expect("frame slice"); + assert_eq!(view.network.expect("network").len(), 48); + assert_eq!(view.transport.expect("transport").len(), 8); + assert_eq!(view.payload, b"dns"); + assert_eq!(view.ip_protocol, Some(IpNextProtocol::Udp)); + } } diff --git a/nex-packet/src/lib.rs b/nex-packet/src/lib.rs index 9f9c516..2547684 100644 --- a/nex-packet/src/lib.rs +++ b/nex-packet/src/lib.rs @@ -1,4 +1,8 @@ //! Low-level packet parsing and serialization primitives for common network protocols. +//! +//! Packet APIs distinguish borrowed views, mutable borrowed views, owned +//! decoded packets, and validated builders. See `docs/PACKET_MODEL.md` in the +//! repository for ownership, allocation, and mutable-layout semantics. #![allow(deprecated)] pub mod arp; diff --git a/nex-packet/src/packet.rs b/nex-packet/src/packet.rs index 8bce6f6..2aa245f 100644 --- a/nex-packet/src/packet.rs +++ b/nex-packet/src/packet.rs @@ -1,7 +1,12 @@ use bytes::{Bytes, BytesMut}; use std::marker::PhantomData; -/// Represents a generic network packet. +/// An owned, decoded network packet. +/// +/// Implementations own their serialized bytes and expose decoded header data. +/// For allocation-free inspection use a protocol's borrowed view type. For +/// in-place editing use [`MutablePacket`], and for constructing a new packet use +/// the types in [`crate::builder`]. pub trait Packet: Sized { type Header; @@ -44,33 +49,62 @@ pub trait Packet: Sized { fn is_empty(&self) -> bool { self.total_len() == 0 } - /// Convert the packet to a mutable byte buffer. + /// Copy the packet into a new mutable byte buffer. + /// + /// This allocates. In new code, prefer [`copy_to_bytes_mut`] so the copy is + /// explicit at the call site. + #[deprecated(note = "use packet::copy_to_bytes_mut to make the allocation explicit")] #[allow(clippy::wrong_self_convention)] fn to_bytes_mut(&self) -> BytesMut { - let mut buf = BytesMut::with_capacity(self.total_len()); - buf.extend_from_slice(&self.to_bytes()); - buf + copy_to_bytes_mut(self) } - /// Get a mutable byte buffer for the header. + /// Copy the header into a new mutable byte buffer. + /// + /// This does not mutate the packet. In new code, prefer + /// [`copy_header_to_bytes_mut`]. + #[deprecated(note = "use packet::copy_header_to_bytes_mut to make the allocation explicit")] fn header_mut(&self) -> BytesMut { - let mut buf = BytesMut::with_capacity(self.header_len()); - buf.extend_from_slice(&self.header()); - buf + copy_header_to_bytes_mut(self) } - /// Get a mutable byte buffer for the payload. + /// Copy the payload into a new mutable byte buffer. + /// + /// This does not mutate the packet. In new code, prefer + /// [`copy_payload_to_bytes_mut`]. + #[deprecated(note = "use packet::copy_payload_to_bytes_mut to make the allocation explicit")] fn payload_mut(&self) -> BytesMut { - let mut buf = BytesMut::with_capacity(self.payload_len()); - buf.extend_from_slice(&self.payload()); - buf + copy_payload_to_bytes_mut(self) } fn into_parts(self) -> (Self::Header, Bytes); } +/// Copy an owned packet's complete serialization into a mutable buffer. +pub fn copy_to_bytes_mut(packet: &P) -> BytesMut { + let mut buffer = BytesMut::with_capacity(packet.total_len()); + buffer.extend_from_slice(&packet.to_bytes()); + buffer +} + +/// Copy an owned packet's serialized header into a mutable buffer. +pub fn copy_header_to_bytes_mut(packet: &P) -> BytesMut { + let mut buffer = BytesMut::with_capacity(packet.header_len()); + buffer.extend_from_slice(&packet.header()); + buffer +} + +/// Copy an owned packet's payload into a mutable buffer. +pub fn copy_payload_to_bytes_mut(packet: &P) -> BytesMut { + let mut buffer = BytesMut::with_capacity(packet.payload_len()); + buffer.extend_from_slice(&packet.payload()); + buffer +} + /// Represents a mutable network packet that can be parsed and modified in place. /// /// Types implementing this trait work on top of the same backing buffer and allow -/// layered packet parsing to be chained without additional allocations. +/// layered packet parsing to be chained without additional allocations. A mutable +/// view borrows its backing storage; it is distinct from both an owned [`Packet`] +/// and a builder. pub trait MutablePacket<'a>: Sized { /// The immutable packet type associated with this mutable view. type Packet: Packet; @@ -101,7 +135,11 @@ pub trait MutablePacket<'a>: Sized { self.packet().is_empty() } - /// Convert the mutable packet into its immutable counterpart. + /// Parse the current buffer into an owned immutable packet. + /// + /// This is a commit point: the current bytes are validated again, and the + /// returned packet owns its serialization. Mutations that make a length or + /// discriminant field inconsistent cause this method to return `None`. fn freeze(&self) -> Option { Self::Packet::try_from_buf(self.packet()).ok() } @@ -109,8 +147,16 @@ pub trait MutablePacket<'a>: Sized { /// A generic mutable packet wrapper that validates using the immutable packet /// parser and exposes the raw buffer for in-place mutation. +/// +/// Header and payload boundaries are parsed once at construction and cached. +/// Mutating bytes through [`MutablePacket::packet_mut`] does not update those +/// boundaries. If a structural field changes, call [`Self::refresh_layout`] +/// before requesting header or payload slices. [`MutablePacket::freeze`] always +/// validates the current bytes independently of the cached layout. pub struct GenericMutablePacket<'a, P: Packet> { buffer: &'a mut [u8], + header_len: usize, + payload_len: usize, _marker: PhantomData

, } @@ -118,9 +164,16 @@ impl<'a, P: Packet> MutablePacket<'a> for GenericMutablePacket<'a, P> { type Packet = P; fn new(buffer: &'a mut [u8]) -> Option { - P::try_from_buf(buffer).ok()?; + let packet = P::try_from_buf(buffer).ok()?; + let header_len = packet.header_len(); + let payload_len = packet.payload_len(); + if header_len.checked_add(payload_len)? > buffer.len() { + return None; + } Some(Self { buffer, + header_len, + payload_len, _marker: PhantomData, }) } @@ -134,52 +187,165 @@ impl<'a, P: Packet> MutablePacket<'a> for GenericMutablePacket<'a, P> { } fn header(&self) -> &[u8] { - let (header_len, _) = self.lengths(); - &self.packet()[..header_len] + &self.packet()[..self.header_len] } fn header_mut(&mut self) -> &mut [u8] { - let (header_len, _) = self.lengths(); - let (header, _) = self.buffer.split_at_mut(header_len); + let (header, _) = self.buffer.split_at_mut(self.header_len); header } fn payload(&self) -> &[u8] { - let (header_len, payload_len) = self.lengths(); - &self.packet()[header_len..header_len + payload_len] + &self.packet()[self.header_len..self.header_len + self.payload_len] } fn payload_mut(&mut self) -> &mut [u8] { - let (header_len, payload_len) = self.lengths(); - let (_, payload) = self.buffer.split_at_mut(header_len); - &mut payload[..payload_len] + let (_, payload) = self.buffer.split_at_mut(self.header_len); + &mut payload[..self.payload_len] } } impl<'a, P: Packet> GenericMutablePacket<'a, P> { - /// Construct a mutable packet without validating the buffer. - /// - /// # Safety + /// Construct a mutable packet without requiring a valid packet layout. /// - /// Although this function is safe to call for compatibility, the caller - /// must ensure `buffer` contains a structurally valid `P` before calling - /// accessors. Invalid header lengths may otherwise cause accessor panics. - /// Prefer [`MutablePacket::new`], which validates this invariant. + /// A valid packet is parsed once and uses its decoded boundaries. Invalid + /// input is exposed conservatively as an all-header, empty-payload view. + /// Prefer [`MutablePacket::new`] when invalid input should be rejected. pub fn new_unchecked(buffer: &'a mut [u8]) -> Self { + let (header_len, payload_len) = Self::parse_lengths(buffer).unwrap_or((buffer.len(), 0)); Self { buffer, + header_len, + payload_len, _marker: PhantomData, } } - fn lengths(&self) -> (usize, usize) { - match P::try_from_buf(self.packet()) { - Ok(packet) => { - let header_len = packet.header_len(); - let payload_len = packet.payload_len(); - (header_len, payload_len) + /// Re-parse the current bytes and refresh the cached header/payload layout. + /// + /// Use this after changing a structural field through + /// [`MutablePacket::packet_mut`]. On error, the previous cached boundaries + /// remain unchanged. + pub fn refresh_layout(&mut self) -> Result<(), crate::parse::ParseError> { + let (header_len, payload_len) = Self::parse_lengths(self.buffer)?; + self.header_len = header_len; + self.payload_len = payload_len; + Ok(()) + } + + fn parse_lengths(buffer: &[u8]) -> Result<(usize, usize), crate::parse::ParseError> { + let packet = P::try_from_buf(buffer)?; + let header_len = packet.header_len(); + let payload_len = packet.payload_len(); + match header_len + .checked_add(payload_len) + .filter(|total| *total <= buffer.len()) + { + Some(_) => Ok((header_len, payload_len)), + None => Err(crate::parse::ParseError::InvalidLength { + context: "generic mutable packet layout", + value: header_len.saturating_add(payload_len), + }), + } + } +} + +#[cfg(test)] +mod tests { + use super::{GenericMutablePacket, MutablePacket, Packet}; + use crate::parse::ParseError; + use bytes::Bytes; + use std::sync::{ + Mutex, + atomic::{AtomicUsize, Ordering}, + }; + + static PARSE_COUNT: AtomicUsize = AtomicUsize::new(0); + static TEST_LOCK: Mutex<()> = Mutex::new(()); + + struct CountingPacket { + bytes: Bytes, + } + + impl Packet for CountingPacket { + type Header = (); + + fn try_from_buf(buf: &[u8]) -> Result { + PARSE_COUNT.fetch_add(1, Ordering::Relaxed); + if buf.len() < 2 { + return Err(ParseError::BufferTooShort { + context: "counting packet", + minimum: 2, + actual: buf.len(), + }); } - _ => (self.buffer.len(), 0), + Ok(Self { + bytes: Bytes::copy_from_slice(buf), + }) + } + + fn try_from_bytes(bytes: Bytes) -> Result { + Self::try_from_buf(&bytes) + } + + fn to_bytes(&self) -> Bytes { + self.bytes.clone() + } + + fn header(&self) -> Bytes { + self.bytes.slice(..2) + } + + fn payload(&self) -> Bytes { + self.bytes.slice(2..) + } + + fn header_len(&self) -> usize { + 2 } + + fn payload_len(&self) -> usize { + self.bytes.len() - 2 + } + + fn total_len(&self) -> usize { + self.bytes.len() + } + + fn into_parts(self) -> (Self::Header, Bytes) { + ((), self.bytes.slice(2..)) + } + } + + #[test] + fn generic_mutable_packet_caches_layout_until_refresh_or_freeze() { + let _guard = TEST_LOCK.lock().expect("test lock"); + PARSE_COUNT.store(0, Ordering::Relaxed); + let mut bytes = [0, 1, 2, 3]; + let mut packet = + GenericMutablePacket::::new(&mut bytes).expect("valid packet"); + + assert_eq!(PARSE_COUNT.load(Ordering::Relaxed), 1); + assert_eq!(packet.header(), &[0, 1]); + assert_eq!(packet.payload(), &[2, 3]); + packet.header_mut()[0] = 9; + packet.payload_mut()[0] = 8; + assert_eq!(PARSE_COUNT.load(Ordering::Relaxed), 1); + + packet.refresh_layout().expect("refresh layout"); + assert_eq!(PARSE_COUNT.load(Ordering::Relaxed), 2); + assert!(packet.freeze().is_some()); + assert_eq!(PARSE_COUNT.load(Ordering::Relaxed), 3); + } + + #[test] + fn unchecked_invalid_packet_uses_safe_conservative_layout() { + let _guard = TEST_LOCK.lock().expect("test lock"); + PARSE_COUNT.store(0, Ordering::Relaxed); + let mut bytes = [1]; + let packet = GenericMutablePacket::::new_unchecked(&mut bytes); + + assert_eq!(packet.header(), &[1]); + assert!(packet.payload().is_empty()); } } From 3034c6b52522f7edd6d58fc53a29dec3da21c435 Mon Sep 17 00:00:00 2001 From: shellrow Date: Sun, 19 Jul 2026 21:44:26 +0900 Subject: [PATCH 21/33] fix: preserve checksum words across split buffers --- nex-packet/src/util.rs | 47 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/nex-packet/src/util.rs b/nex-packet/src/util.rs index 7098e6d..f837976 100644 --- a/nex-packet/src/util.rs +++ b/nex-packet/src/util.rs @@ -97,8 +97,7 @@ pub fn ipv4_checksum( sum += len as u32; // Checksum packet header and data - sum += sum_be_words(data, skipword); - sum += sum_be_words(extra_data, extra_data.len() / 2); + sum += sum_be_words_joined(data, skipword, extra_data); finalize_checksum(sum) } @@ -128,8 +127,7 @@ pub fn ipv6_checksum( sum += len as u32; // Checksum packet header and data - sum += sum_be_words(data, skipword); - sum += sum_be_words(extra_data, extra_data.len() / 2); + sum += sum_be_words_joined(data, skipword, extra_data); finalize_checksum(sum) } @@ -164,9 +162,29 @@ fn sum_be_words(data: &[u8], skipword: usize) -> u32 { sum } +/// Sum two logically contiguous byte slices without allocating. +/// +/// Treating each slice independently would incorrectly pad an odd final byte +/// from `data` before consuming the first byte from `extra_data`. +fn sum_be_words_joined(data: &[u8], skipword: usize, extra_data: &[u8]) -> u32 { + let mut bytes = data.iter().chain(extra_data); + let mut word_index = 0; + let mut sum = 0u32; + + while let Some(high) = bytes.next() { + let low = bytes.next().copied().unwrap_or(0); + if word_index != skipword { + sum += ((*high as u32) << 8) | low as u32; + } + word_index += 1; + } + + sum +} + #[cfg(test)] mod tests { - use super::sum_be_words; + use super::{checksum, sum_be_words, sum_be_words_joined}; use core::slice; #[test] @@ -222,4 +240,23 @@ mod tests { assert_eq!(7705, sum_be_words(slice_data, 101)); } } + + #[test] + fn joined_word_sum_preserves_odd_slice_boundary() { + assert_eq!( + sum_be_words(&[0x01, 0x02, 0x03, 0x04], usize::MAX), + sum_be_words_joined(&[0x01], usize::MAX, &[0x02, 0x03, 0x04]) + ); + assert_eq!( + sum_be_words(&[0x01, 0x02, 0x03], usize::MAX), + sum_be_words_joined(&[0x01, 0x02], usize::MAX, &[0x03]) + ); + } + + #[test] + fn checksum_folds_carries_and_pads_odd_lengths() { + assert_eq!(checksum(&[0xff, 0xff, 0xff, 0xff], usize::MAX), 0); + assert_eq!(checksum(&[0x01], usize::MAX), 0xfeff); + assert_eq!(checksum(&[0x01, 0x02, 0x03], usize::MAX), 0xfbfd); + } } From 5533ffef9d6d6bb631ce0f415a924a997968109a Mon Sep 17 00:00:00 2001 From: shellrow Date: Sun, 19 Jul 2026 21:46:13 +0900 Subject: [PATCH 22/33] test: expand benchmarks and parser robustness coverage --- CONTRIBUTING.md | 49 ++++++++ docs/BENCHMARKING.md | 30 +++++ fuzz/.gitignore | 6 +- fuzz/Cargo.toml | 56 +++++++++ fuzz/corpus/README.md | 13 ++ fuzz/corpus/dhcp_options/discover.hex | 1 + .../dhcp_options/wireshark_dhcp_discover.hex | 1 + .../dns_records/compressed_response.hex | 1 + fuzz/corpus/ethernet_vlan/ipv4_frame.hex | 1 + .../corpus/icmpv6_ndp/router_solicitation.hex | 1 + fuzz/corpus/ipv4_options/options.hex | 1 + fuzz/corpus/ipv6_extensions/hop_by_hop.hex | 1 + fuzz/corpus/vxlan/basic.hex | 1 + fuzz/fuzz_targets/dhcp_options.rs | 12 ++ fuzz/fuzz_targets/dns_name.rs | 1 - fuzz/fuzz_targets/dns_records.rs | 13 ++ fuzz/fuzz_targets/ethernet_vlan.rs | 13 ++ fuzz/fuzz_targets/gre_fields.rs | 12 ++ fuzz/fuzz_targets/icmpv6_ndp.rs | 12 ++ fuzz/fuzz_targets/ipv4_options.rs | 13 ++ fuzz/fuzz_targets/ipv6_extensions.rs | 13 ++ fuzz/fuzz_targets/support.rs | 35 ++++++ fuzz/fuzz_targets/vxlan.rs | 12 ++ nex-packet/Cargo.toml | 5 + nex-packet/benches/packet_operations.rs | 77 ++++++++++++ nex-packet/tests/allocation_behavior.rs | 50 ++++++++ nex-packet/tests/panic_free_parsing.rs | 44 +++++++ nex-packet/tests/property_roundtrip.rs | 113 ++++++++++++++++++ 28 files changed, 585 insertions(+), 2 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 docs/BENCHMARKING.md create mode 100644 fuzz/corpus/README.md create mode 100644 fuzz/corpus/dhcp_options/discover.hex create mode 100644 fuzz/corpus/dhcp_options/wireshark_dhcp_discover.hex create mode 100644 fuzz/corpus/dns_records/compressed_response.hex create mode 100644 fuzz/corpus/ethernet_vlan/ipv4_frame.hex create mode 100644 fuzz/corpus/icmpv6_ndp/router_solicitation.hex create mode 100644 fuzz/corpus/ipv4_options/options.hex create mode 100644 fuzz/corpus/ipv6_extensions/hop_by_hop.hex create mode 100644 fuzz/corpus/vxlan/basic.hex create mode 100644 fuzz/fuzz_targets/dhcp_options.rs create mode 100644 fuzz/fuzz_targets/dns_records.rs create mode 100644 fuzz/fuzz_targets/ethernet_vlan.rs create mode 100644 fuzz/fuzz_targets/gre_fields.rs create mode 100644 fuzz/fuzz_targets/icmpv6_ndp.rs create mode 100644 fuzz/fuzz_targets/ipv4_options.rs create mode 100644 fuzz/fuzz_targets/ipv6_extensions.rs create mode 100644 fuzz/fuzz_targets/support.rs create mode 100644 fuzz/fuzz_targets/vxlan.rs create mode 100644 nex-packet/benches/packet_operations.rs create mode 100644 nex-packet/tests/allocation_behavior.rs create mode 100644 nex-packet/tests/panic_free_parsing.rs create mode 100644 nex-packet/tests/property_roundtrip.rs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..eba20ef --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,49 @@ +# Contributing + +Run formatting, linting, and the workspace tests before submitting changes: + +```sh +cargo fmt --all -- --check +cargo clippy --workspace --all-targets --all-features -- -D warnings +cargo test --workspace --all-features +``` + +Raw socket and datalink changes also require the manual checks in +`docs/PRIVILEGED_TESTING.md`. Packet hot-path changes should be measured using +`docs/BENCHMARKING.md`. + +## Fuzzing + +Install the nightly fuzzing frontend and list the available targets: + +```sh +cargo install cargo-fuzz +cargo +nightly fuzz list +``` + +Run a target with its checked-in seed corpus: + +```sh +cargo +nightly fuzz run frame_parse +cargo +nightly fuzz run ethernet_vlan +cargo +nightly fuzz run dns_records +``` + +Use `cargo +nightly fuzz run -- -max_total_time=60` for a bounded local +run. Crashes are written under `fuzz/artifacts/`. Minimize a finding +before diagnosing it: + +```sh +cargo +nightly fuzz tmin fuzz/artifacts// +``` + +Every confirmed parser defect must receive a deterministic unit or integration +regression test before the implementation is fixed. Keep only sanitized, +minimal corpus inputs. The checked-in `hex:` format is decoded by targets that +use it and makes packet seeds reviewable without committing opaque binaries. + +## Repository conventions + +Documentation, comments, public APIs, and commit messages are written in +English. Avoid unrelated formatting or generated-file changes, and keep each +commit focused on one reviewable concern. diff --git a/docs/BENCHMARKING.md b/docs/BENCHMARKING.md new file mode 100644 index 0000000..d4c363b --- /dev/null +++ b/docs/BENCHMARKING.md @@ -0,0 +1,30 @@ +# Benchmarking + +Run packet benchmarks on an otherwise idle machine: + +```sh +cargo bench -p nex-packet +``` + +`packet_parse` compares owned, decoded-view, and allocation-free slice parsing. +`packet_operations` +tracks Ethernet/VLAN, IPv4/IPv6, TCP/UDP, and DNS-name parsing together with +IPv4 serialization and checksum throughput. Criterion stores baselines under +`target/criterion`; compare changes with: + +```sh +cargo bench -p nex-packet -- --save-baseline before +cargo bench -p nex-packet -- --baseline before +``` + +`FrameSlice` avoids packet-byte copies and heap allocation. `FrameView` may +allocate decoded variable-length options; owned parsing and serialization may +allocate or increment a `Bytes` reference count. Use an allocation profiler +such as DHAT, heaptrack, or Instruments when changing parser ownership. +Datalink send/receive throughput depends on kernel, +driver, interface, and privileges, so measure it manually using the matrix in +`PRIVILEGED_TESTING.md`; do not compare those results across hosts. + +Benchmark changes are reviewed locally rather than gated by a fixed CI +percentage. Record the command, CPU, OS, Rust version, and Criterion comparison +when a change intentionally affects a hot path. diff --git a/fuzz/.gitignore b/fuzz/.gitignore index f83457a..b8b7114 100644 --- a/fuzz/.gitignore +++ b/fuzz/.gitignore @@ -1,4 +1,8 @@ artifacts/ -corpus/ +corpus/* +!corpus/README.md +!corpus/*/ +corpus/*/* +!corpus/*/*.hex coverage/ target/ diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index a31ecd2..5a55543 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -49,3 +49,59 @@ path = "fuzz_targets/dns_name.rs" test = false doc = false bench = false + +[[bin]] +name = "ethernet_vlan" +path = "fuzz_targets/ethernet_vlan.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "ipv4_options" +path = "fuzz_targets/ipv4_options.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "ipv6_extensions" +path = "fuzz_targets/ipv6_extensions.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "icmpv6_ndp" +path = "fuzz_targets/icmpv6_ndp.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "dns_records" +path = "fuzz_targets/dns_records.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "dhcp_options" +path = "fuzz_targets/dhcp_options.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "gre_fields" +path = "fuzz_targets/gre_fields.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "vxlan" +path = "fuzz_targets/vxlan.rs" +test = false +doc = false +bench = false diff --git a/fuzz/corpus/README.md b/fuzz/corpus/README.md new file mode 100644 index 0000000..6bd6c77 --- /dev/null +++ b/fuzz/corpus/README.md @@ -0,0 +1,13 @@ +# Seed Corpus + +Corpus entries prefixed with `hex:` are decoded by the corresponding fuzz +target before parsing. This keeps packet bytes reviewable in Git. The initial +seeds cover valid protocol examples and length/option boundaries derived from +the regression suite. Sanitized packets from manually captured test traffic +can be added in the same form; remove payloads and addresses that identify a +real network before committing them. + +`dhcp_options/wireshark_dhcp_discover.hex` is the BOOTP payload from the first +packet in Wireshark's `test/captures/dhcp.pcap`. The transaction ID and client +MAC address were replaced before inclusion. Source: +. diff --git a/fuzz/corpus/dhcp_options/discover.hex b/fuzz/corpus/dhcp_options/discover.hex new file mode 100644 index 0000000..a7f8c89 --- /dev/null +++ b/fuzz/corpus/dhcp_options/discover.hex @@ -0,0 +1 @@ +hex:010106001234567800000000000000000000000000000000001122334455000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000063825363350101ff diff --git a/fuzz/corpus/dhcp_options/wireshark_dhcp_discover.hex b/fuzz/corpus/dhcp_options/wireshark_dhcp_discover.hex new file mode 100644 index 0000000..87ba7e3 --- /dev/null +++ b/fuzz/corpus/dhcp_options/wireshark_dhcp_discover.hex @@ -0,0 +1 @@ +hex:0101060012345678000000000000000000000000000000000000000000112233445500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000638253633501013d070100112233445532040000000037040103062aff00000000000000 diff --git a/fuzz/corpus/dns_records/compressed_response.hex b/fuzz/corpus/dns_records/compressed_response.hex new file mode 100644 index 0000000..6c09365 --- /dev/null +++ b/fuzz/corpus/dns_records/compressed_response.hex @@ -0,0 +1 @@ +hex:12348180000100010000000003777777076578616d706c6503636f6d0000010001c00c000100010000003c0004c0000201 diff --git a/fuzz/corpus/ethernet_vlan/ipv4_frame.hex b/fuzz/corpus/ethernet_vlan/ipv4_frame.hex new file mode 100644 index 0000000..3bb7b67 --- /dev/null +++ b/fuzz/corpus/ethernet_vlan/ipv4_frame.hex @@ -0,0 +1 @@ +hex:00112233445566778899aabb08004500001c1234400040110000c0000201c633640204d2003500080000 diff --git a/fuzz/corpus/icmpv6_ndp/router_solicitation.hex b/fuzz/corpus/icmpv6_ndp/router_solicitation.hex new file mode 100644 index 0000000..2e25785 --- /dev/null +++ b/fuzz/corpus/icmpv6_ndp/router_solicitation.hex @@ -0,0 +1 @@ +hex:85000000000000000101001122334455 diff --git a/fuzz/corpus/ipv4_options/options.hex b/fuzz/corpus/ipv4_options/options.hex new file mode 100644 index 0000000..563a690 --- /dev/null +++ b/fuzz/corpus/ipv4_options/options.hex @@ -0,0 +1 @@ +hex:470000201234400040110000c0a80001c0a800020187041234000000deadbeef diff --git a/fuzz/corpus/ipv6_extensions/hop_by_hop.hex b/fuzz/corpus/ipv6_extensions/hop_by_hop.hex new file mode 100644 index 0000000..35d592d --- /dev/null +++ b/fuzz/corpus/ipv6_extensions/hop_by_hop.hex @@ -0,0 +1 @@ +hex:6000000000100040fe800000000000000000000000000001fe800000000000000000000000000002110000000000000004d2003500080000 diff --git a/fuzz/corpus/vxlan/basic.hex b/fuzz/corpus/vxlan/basic.hex new file mode 100644 index 0000000..1c1f724 --- /dev/null +++ b/fuzz/corpus/vxlan/basic.hex @@ -0,0 +1 @@ +hex:080000000000010000112233445566778899aabb0800 diff --git a/fuzz/fuzz_targets/dhcp_options.rs b/fuzz/fuzz_targets/dhcp_options.rs new file mode 100644 index 0000000..3d6eeb7 --- /dev/null +++ b/fuzz/fuzz_targets/dhcp_options.rs @@ -0,0 +1,12 @@ +#![no_main] + +mod support; + +use libfuzzer_sys::fuzz_target; +use nex_packet::{dhcp::DhcpPacket, packet::Packet}; + +fuzz_target!(|data: &[u8]| { + let data = support::decode_seed(data); + let data = data.as_ref(); + let _ = DhcpPacket::try_from_buf(data); +}); diff --git a/fuzz/fuzz_targets/dns_name.rs b/fuzz/fuzz_targets/dns_name.rs index ef1194d..48a5ed1 100644 --- a/fuzz/fuzz_targets/dns_name.rs +++ b/fuzz/fuzz_targets/dns_name.rs @@ -5,5 +5,4 @@ use nex_packet::dns::DnsName; fuzz_target!(|data: &[u8]| { let _ = DnsName::try_from_bytes(data); - let _ = DnsName::try_from_bytes(data); }); diff --git a/fuzz/fuzz_targets/dns_records.rs b/fuzz/fuzz_targets/dns_records.rs new file mode 100644 index 0000000..5c69f72 --- /dev/null +++ b/fuzz/fuzz_targets/dns_records.rs @@ -0,0 +1,13 @@ +#![no_main] + +mod support; + +use libfuzzer_sys::fuzz_target; +use nex_packet::dns::{DnsName, DnsPacket}; + +fuzz_target!(|data: &[u8]| { + let data = support::decode_seed(data); + let data = data.as_ref(); + let _ = DnsPacket::try_from_buf(data); + let _ = DnsName::try_from_bytes(data); +}); diff --git a/fuzz/fuzz_targets/ethernet_vlan.rs b/fuzz/fuzz_targets/ethernet_vlan.rs new file mode 100644 index 0000000..121e1a8 --- /dev/null +++ b/fuzz/fuzz_targets/ethernet_vlan.rs @@ -0,0 +1,13 @@ +#![no_main] + +mod support; + +use libfuzzer_sys::fuzz_target; +use nex_packet::{ethernet::EthernetPacket, packet::Packet, vlan::VlanPacket}; + +fuzz_target!(|data: &[u8]| { + let data = support::decode_seed(data); + let data = data.as_ref(); + let _ = EthernetPacket::try_from_buf(data); + let _ = VlanPacket::try_from_buf(data); +}); diff --git a/fuzz/fuzz_targets/gre_fields.rs b/fuzz/fuzz_targets/gre_fields.rs new file mode 100644 index 0000000..379a164 --- /dev/null +++ b/fuzz/fuzz_targets/gre_fields.rs @@ -0,0 +1,12 @@ +#![no_main] + +mod support; + +use libfuzzer_sys::fuzz_target; +use nex_packet::{gre::GrePacket, packet::Packet}; + +fuzz_target!(|data: &[u8]| { + let data = support::decode_seed(data); + let data = data.as_ref(); + let _ = GrePacket::try_from_buf(data); +}); diff --git a/fuzz/fuzz_targets/icmpv6_ndp.rs b/fuzz/fuzz_targets/icmpv6_ndp.rs new file mode 100644 index 0000000..220828a --- /dev/null +++ b/fuzz/fuzz_targets/icmpv6_ndp.rs @@ -0,0 +1,12 @@ +#![no_main] + +mod support; + +use libfuzzer_sys::fuzz_target; +use nex_packet::{icmpv6::Icmpv6Packet, packet::Packet}; + +fuzz_target!(|data: &[u8]| { + let data = support::decode_seed(data); + let data = data.as_ref(); + let _ = Icmpv6Packet::try_from_buf(data); +}); diff --git a/fuzz/fuzz_targets/ipv4_options.rs b/fuzz/fuzz_targets/ipv4_options.rs new file mode 100644 index 0000000..dd1800f --- /dev/null +++ b/fuzz/fuzz_targets/ipv4_options.rs @@ -0,0 +1,13 @@ +#![no_main] + +mod support; + +use libfuzzer_sys::fuzz_target; +use nex_packet::{ipv4::Ipv4Packet, parse::ParseMode}; + +fuzz_target!(|data: &[u8]| { + let data = support::decode_seed(data); + let data = data.as_ref(); + let _ = Ipv4Packet::try_from_buf(data); + let _ = Ipv4Packet::try_from_buf_with_mode(data, ParseMode::Strict); +}); diff --git a/fuzz/fuzz_targets/ipv6_extensions.rs b/fuzz/fuzz_targets/ipv6_extensions.rs new file mode 100644 index 0000000..94a90f1 --- /dev/null +++ b/fuzz/fuzz_targets/ipv6_extensions.rs @@ -0,0 +1,13 @@ +#![no_main] + +mod support; + +use libfuzzer_sys::fuzz_target; +use nex_packet::{ipv6::Ipv6Packet, parse::ParseMode}; + +fuzz_target!(|data: &[u8]| { + let data = support::decode_seed(data); + let data = data.as_ref(); + let _ = Ipv6Packet::try_from_buf(data); + let _ = Ipv6Packet::try_from_buf_with_mode(data, ParseMode::Strict); +}); diff --git a/fuzz/fuzz_targets/support.rs b/fuzz/fuzz_targets/support.rs new file mode 100644 index 0000000..ae9ea9e --- /dev/null +++ b/fuzz/fuzz_targets/support.rs @@ -0,0 +1,35 @@ +use std::borrow::Cow; + +/// Decode review-friendly `hex:` corpus entries while accepting normal raw +/// libFuzzer inputs without transformation. +pub fn decode_seed(data: &[u8]) -> Cow<'_, [u8]> { + let Some(hex) = data.strip_prefix(b"hex:") else { + return Cow::Borrowed(data); + }; + let digits: Vec = hex + .iter() + .copied() + .filter(|byte| !byte.is_ascii_whitespace()) + .collect(); + if !digits.len().is_multiple_of(2) { + return Cow::Borrowed(data); + } + + let mut decoded = Vec::with_capacity(digits.len() / 2); + for pair in digits.chunks_exact(2) { + let (Some(high), Some(low)) = (hex_value(pair[0]), hex_value(pair[1])) else { + return Cow::Borrowed(data); + }; + decoded.push((high << 4) | low); + } + Cow::Owned(decoded) +} + +fn hex_value(byte: u8) -> Option { + match byte { + b'0'..=b'9' => Some(byte - b'0'), + b'a'..=b'f' => Some(byte - b'a' + 10), + b'A'..=b'F' => Some(byte - b'A' + 10), + _ => None, + } +} diff --git a/fuzz/fuzz_targets/vxlan.rs b/fuzz/fuzz_targets/vxlan.rs new file mode 100644 index 0000000..8856eda --- /dev/null +++ b/fuzz/fuzz_targets/vxlan.rs @@ -0,0 +1,12 @@ +#![no_main] + +mod support; + +use libfuzzer_sys::fuzz_target; +use nex_packet::{packet::Packet, vxlan::VxlanPacket}; + +fuzz_target!(|data: &[u8]| { + let data = support::decode_seed(data); + let data = data.as_ref(); + let _ = VxlanPacket::try_from_buf(data); +}); diff --git a/nex-packet/Cargo.toml b/nex-packet/Cargo.toml index 48fb538..234a012 100644 --- a/nex-packet/Cargo.toml +++ b/nex-packet/Cargo.toml @@ -22,7 +22,12 @@ serde = ["dep:serde", "nex-core/serde", "bytes/serde"] [dev-dependencies] criterion = "0.5" +proptest = "1" [[bench]] name = "packet_parse" harness = false + +[[bench]] +name = "packet_operations" +harness = false diff --git a/nex-packet/benches/packet_operations.rs b/nex-packet/benches/packet_operations.rs new file mode 100644 index 0000000..f7c8323 --- /dev/null +++ b/nex-packet/benches/packet_operations.rs @@ -0,0 +1,77 @@ +use bytes::Bytes; +use criterion::{Criterion, Throughput, criterion_group, criterion_main}; +use nex_packet::{ + dns::DnsName, + ethernet::EthernetPacket, + ipv4::{Ipv4Packet, checksum as ipv4_checksum}, + ipv6::Ipv6Packet, + packet::Packet, + tcp::TcpPacket, + udp::UdpPacket, + vlan::VlanPacket, +}; + +const ETHERNET: &[u8] = &[ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0x08, 0x00, 0xde, 0xad, 0xbe, 0xef, +]; +const VLAN: &[u8] = &[0x20, 0x01, 0x08, 0x00, 0xde, 0xad, 0xbe, 0xef]; +const IPV4: &[u8] = &[ + 0x45, 0, 0, 28, 0x12, 0x34, 0x40, 0, 64, 17, 0, 0, 192, 0, 2, 1, 198, 51, 100, 2, 0x04, 0xd2, + 0, 53, 0, 8, 0, 0, +]; +const IPV6: &[u8] = &[ + 0x60, 0, 0, 0, 0, 8, 17, 64, 0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0xfe, 0x80, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0x04, 0xd2, 0, 53, 0, 8, 0, 0, +]; +const TCP: &[u8] = &[ + 0x04, 0xd2, 0, 80, 0, 0, 0, 1, 0, 0, 0, 0, 0x50, 0x02, 0x20, 0, 0, 0, 0, 0, +]; +const UDP: &[u8] = &[0x04, 0xd2, 0, 53, 0, 8, 0, 0]; +const DNS_NAME: &[u8] = &[ + 3, b'w', b'w', b'w', 7, b'e', b'x', b'a', b'm', b'p', b'l', b'e', 3, b'c', b'o', b'm', 0, +]; + +fn bench_parsing(c: &mut Criterion) { + let mut group = c.benchmark_group("protocol_parse"); + for (name, len) in [ + ("ethernet", ETHERNET.len()), + ("vlan", VLAN.len()), + ("ipv4", IPV4.len()), + ("ipv6", IPV6.len()), + ("tcp", TCP.len()), + ("udp", UDP.len()), + ("dns_name", DNS_NAME.len()), + ] { + group.throughput(Throughput::Bytes(len as u64)); + match name { + "ethernet" => { + group.bench_function(name, |b| b.iter(|| EthernetPacket::try_from_buf(ETHERNET))) + } + "vlan" => group.bench_function(name, |b| b.iter(|| VlanPacket::try_from_buf(VLAN))), + "ipv4" => group.bench_function(name, |b| b.iter(|| Ipv4Packet::try_from_buf(IPV4))), + "ipv6" => group.bench_function(name, |b| b.iter(|| Ipv6Packet::try_from_buf(IPV6))), + "tcp" => group.bench_function(name, |b| b.iter(|| TcpPacket::try_from_buf(TCP))), + "udp" => group.bench_function(name, |b| b.iter(|| UdpPacket::try_from_buf(UDP))), + "dns_name" => { + group.bench_function(name, |b| b.iter(|| DnsName::try_from_bytes(DNS_NAME))) + } + _ => unreachable!(), + }; + } + group.finish(); +} + +fn bench_serialization_and_checksum(c: &mut Criterion) { + let packet = Ipv4Packet::try_from_buf(IPV4).expect("benchmark packet"); + let mut group = c.benchmark_group("packet_operations"); + group.throughput(Throughput::Bytes(IPV4.len() as u64)); + group.bench_function("ipv4_serialize", |b| b.iter(|| packet.to_bytes())); + group.bench_function("ipv4_checksum", |b| b.iter(|| ipv4_checksum(&packet))); + group.bench_function("owned_parse", |b| { + b.iter(|| Ipv4Packet::try_from_bytes(Bytes::copy_from_slice(IPV4))) + }); + group.finish(); +} + +criterion_group!(benches, bench_parsing, bench_serialization_and_checksum); +criterion_main!(benches); diff --git a/nex-packet/tests/allocation_behavior.rs b/nex-packet/tests/allocation_behavior.rs new file mode 100644 index 0000000..d5d15af --- /dev/null +++ b/nex-packet/tests/allocation_behavior.rs @@ -0,0 +1,50 @@ +use nex_packet::frame::{FrameSlice, ParseOption}; +use std::alloc::{GlobalAlloc, Layout, System}; +use std::sync::atomic::{AtomicUsize, Ordering}; + +struct CountingAllocator; + +static ALLOCATIONS: AtomicUsize = AtomicUsize::new(0); + +// SAFETY: Every operation is forwarded to `System` with the original pointer +// and layout. The counter does not affect allocation semantics. +unsafe impl GlobalAlloc for CountingAllocator { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + ALLOCATIONS.fetch_add(1, Ordering::Relaxed); + // SAFETY: The caller supplies the layout required by GlobalAlloc. + unsafe { System.alloc(layout) } + } + + unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) { + // SAFETY: The pointer and layout came from the matching System allocation. + unsafe { System.dealloc(pointer, layout) } + } + + unsafe fn realloc(&self, pointer: *mut u8, layout: Layout, size: usize) -> *mut u8 { + ALLOCATIONS.fetch_add(1, Ordering::Relaxed); + // SAFETY: The pointer/layout pair came from System and `size` is the + // requested replacement size. + unsafe { System.realloc(pointer, layout, size) } + } +} + +#[global_allocator] +static ALLOCATOR: CountingAllocator = CountingAllocator; + +#[test] +fn frame_slice_parsing_does_not_allocate() { + let packet = [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0x08, 0x00, 0x45, 0, 0, 44, 0, 0, 0, 0, 64, 6, 0, 0, + 192, 0, 2, 1, 198, 51, 100, 2, 0, 80, 0x04, 0xd2, 0, 0, 0, 0, 0, 0, 0, 0, 0x50, 0x18, 0, 0, + 0, 0, 0, 0, b'd', b'a', b't', b'a', + ]; + let before = ALLOCATIONS.load(Ordering::Relaxed); + let parsed = std::hint::black_box(FrameSlice::try_from_buf( + std::hint::black_box(&packet), + ParseOption::default(), + )); + let after = ALLOCATIONS.load(Ordering::Relaxed); + + assert!(parsed.is_ok()); + assert_eq!(after, before); +} diff --git a/nex-packet/tests/panic_free_parsing.rs b/nex-packet/tests/panic_free_parsing.rs new file mode 100644 index 0000000..dd6e2a1 --- /dev/null +++ b/nex-packet/tests/panic_free_parsing.rs @@ -0,0 +1,44 @@ +use nex_packet::{ + arp::ArpPacket, + dhcp::DhcpPacket, + dns::{DnsName, DnsPacket}, + ethernet::EthernetPacket, + flowcontrol::FlowControlPacket, + frame::{Frame, FrameView, ParseOption}, + gre::GrePacket, + icmp::IcmpPacket, + icmpv6::Icmpv6Packet, + ipv4::Ipv4Packet, + ipv6::Ipv6Packet, + packet::Packet, + tcp::TcpPacket, + udp::UdpPacket, + vlan::VlanPacket, + vxlan::VxlanPacket, +}; +use proptest::prelude::*; + +proptest! { + #[test] + fn all_public_packet_parsers_accept_arbitrary_input_without_panicking( + data in prop::collection::vec(any::(), 0..2048), + ) { + let _ = EthernetPacket::try_from_buf(&data); + let _ = VlanPacket::try_from_buf(&data); + let _ = ArpPacket::try_from_buf(&data); + let _ = Ipv4Packet::try_from_buf(&data); + let _ = Ipv6Packet::try_from_buf(&data); + let _ = TcpPacket::try_from_buf(&data); + let _ = UdpPacket::try_from_buf(&data); + let _ = IcmpPacket::try_from_buf(&data); + let _ = Icmpv6Packet::try_from_buf(&data); + let _ = DhcpPacket::try_from_buf(&data); + let _ = DnsPacket::try_from_buf(&data); + let _ = DnsName::try_from_bytes(&data); + let _ = GrePacket::try_from_buf(&data); + let _ = VxlanPacket::try_from_buf(&data); + let _ = FlowControlPacket::try_from_buf(&data); + let _ = Frame::try_from_buf(&data, ParseOption::default()); + let _ = FrameView::try_from_buf(&data, ParseOption::default()); + } +} diff --git a/nex-packet/tests/property_roundtrip.rs b/nex-packet/tests/property_roundtrip.rs new file mode 100644 index 0000000..929498e --- /dev/null +++ b/nex-packet/tests/property_roundtrip.rs @@ -0,0 +1,113 @@ +use nex_packet::{ + ethernet::EthernetPacket, ipv4::Ipv4Packet, ipv6::Ipv6Packet, packet::Packet, tcp::TcpPacket, + udp::UdpPacket, vlan::VlanPacket, +}; +use proptest::prelude::*; + +proptest! { + #[test] + fn ethernet_round_trip( + destination in any::<[u8; 6]>(), + source in any::<[u8; 6]>(), + payload in prop::collection::vec(any::(), 0..256), + ) { + let mut bytes = Vec::with_capacity(14 + payload.len()); + bytes.extend_from_slice(&destination); + bytes.extend_from_slice(&source); + bytes.extend_from_slice(&0x0800u16.to_be_bytes()); + bytes.extend_from_slice(&payload); + let packet = EthernetPacket::try_from_buf(&bytes).expect("valid Ethernet packet"); + let serialized = packet.to_bytes(); + prop_assert_eq!(serialized.as_ref(), bytes.as_slice()); + } + + #[test] + fn vlan_round_trip( + tci in any::(), + payload in prop::collection::vec(any::(), 0..256), + ) { + let mut bytes = Vec::with_capacity(4 + payload.len()); + bytes.extend_from_slice(&tci.to_be_bytes()); + bytes.extend_from_slice(&0x0800u16.to_be_bytes()); + bytes.extend_from_slice(&payload); + let packet = VlanPacket::try_from_buf(&bytes).expect("valid VLAN packet"); + let serialized = packet.to_bytes(); + prop_assert_eq!(serialized.as_ref(), bytes.as_slice()); + } + + #[test] + fn ipv4_round_trip( + identification in any::(), + source in any::<[u8; 4]>(), + destination in any::<[u8; 4]>(), + payload in prop::collection::vec(any::(), 0..256), + ) { + let total_length = (20 + payload.len()) as u16; + let mut bytes = Vec::with_capacity(total_length as usize); + bytes.extend_from_slice(&[0x45, 0]); + bytes.extend_from_slice(&total_length.to_be_bytes()); + bytes.extend_from_slice(&identification.to_be_bytes()); + bytes.extend_from_slice(&[0x40, 0, 64, 17, 0, 0]); + bytes.extend_from_slice(&source); + bytes.extend_from_slice(&destination); + bytes.extend_from_slice(&payload); + let packet = Ipv4Packet::try_from_buf(&bytes).expect("valid IPv4 packet"); + let serialized = packet.to_bytes(); + prop_assert_eq!(serialized.as_ref(), bytes.as_slice()); + } + + #[test] + fn ipv6_round_trip( + source in any::<[u8; 16]>(), + destination in any::<[u8; 16]>(), + payload in prop::collection::vec(any::(), 0..256), + ) { + let mut bytes = Vec::with_capacity(40 + payload.len()); + bytes.extend_from_slice(&[0x60, 0, 0, 0]); + bytes.extend_from_slice(&(payload.len() as u16).to_be_bytes()); + bytes.extend_from_slice(&[17, 64]); + bytes.extend_from_slice(&source); + bytes.extend_from_slice(&destination); + bytes.extend_from_slice(&payload); + let packet = Ipv6Packet::try_from_buf(&bytes).expect("valid IPv6 packet"); + let serialized = packet.to_bytes(); + prop_assert_eq!(serialized.as_ref(), bytes.as_slice()); + } + + #[test] + fn udp_round_trip( + source in any::(), + destination in any::(), + payload in prop::collection::vec(any::(), 0..256), + ) { + let length = (8 + payload.len()) as u16; + let mut bytes = Vec::with_capacity(length as usize); + bytes.extend_from_slice(&source.to_be_bytes()); + bytes.extend_from_slice(&destination.to_be_bytes()); + bytes.extend_from_slice(&length.to_be_bytes()); + bytes.extend_from_slice(&0u16.to_be_bytes()); + bytes.extend_from_slice(&payload); + let packet = UdpPacket::try_from_buf(&bytes).expect("valid UDP packet"); + let serialized = packet.to_bytes(); + prop_assert_eq!(serialized.as_ref(), bytes.as_slice()); + } + + #[test] + fn tcp_round_trip( + source in any::(), + destination in any::(), + sequence in any::(), + payload in prop::collection::vec(any::(), 0..256), + ) { + let mut bytes = Vec::with_capacity(20 + payload.len()); + bytes.extend_from_slice(&source.to_be_bytes()); + bytes.extend_from_slice(&destination.to_be_bytes()); + bytes.extend_from_slice(&sequence.to_be_bytes()); + bytes.extend_from_slice(&0u32.to_be_bytes()); + bytes.extend_from_slice(&[0x50, 0x18, 0x20, 0, 0, 0, 0, 0]); + bytes.extend_from_slice(&payload); + let packet = TcpPacket::try_from_buf(&bytes).expect("valid TCP packet"); + let serialized = packet.to_bytes(); + prop_assert_eq!(serialized.as_ref(), bytes.as_slice()); + } +} From 3dda8f2ed34705088a261a30e991ed7ea020983a Mon Sep 17 00:00:00 2001 From: shellrow Date: Sun, 19 Jul 2026 21:46:38 +0900 Subject: [PATCH 23/33] fix: harden datalink and socket backend behavior --- docs/PRIVILEGED_TESTING.md | 12 ++++ .../gre_fields/checksum_key_sequence.hex | 1 + nex-datalink/src/async_io/linux.rs | 58 +++++++++++++++++ nex-datalink/src/async_io/mod.rs | 7 +++ nex-datalink/src/bpf.rs | 2 +- nex-datalink/src/lib.rs | 33 ++++++++-- nex-datalink/src/wpcap.rs | 2 +- nex-datalink/tests/privileged_channel.rs | 52 +++++++++++++++ nex-socket/src/icmp/async_impl.rs | 2 + nex-socket/src/lib.rs | 10 +++ nex-socket/src/udp/async_impl.rs | 22 ++++++- nex-socket/src/udp/sync_impl.rs | 63 ++++++++++++++++++- nex-socket/tests/privileged_socket.rs | 30 +++++++++ 13 files changed, 286 insertions(+), 8 deletions(-) create mode 100644 fuzz/corpus/gre_fields/checksum_key_sequence.hex create mode 100644 nex-datalink/tests/privileged_channel.rs create mode 100644 nex-socket/tests/privileged_socket.rs diff --git a/docs/PRIVILEGED_TESTING.md b/docs/PRIVILEGED_TESTING.md index 6c596b8..367568c 100644 --- a/docs/PRIVILEGED_TESTING.md +++ b/docs/PRIVILEGED_TESTING.md @@ -4,6 +4,18 @@ Run this matrix before a release on dedicated hosts or disposable virtual machines. Do not run it on an untrusted shared network. Record the OS version, Rust version, interface name, command, and result in the release checklist. +Set the dedicated interface name and run the ignored datalink integration tests: + +```sh +NEX_TEST_INTERFACE=eth0 cargo test -p nex-datalink --test privileged_channel -- --ignored +NEX_TEST_INTERFACE=eth0 cargo test -p nex-datalink --features async --test privileged_channel -- --ignored +NEX_TEST_INTERFACE=eth0 cargo test -p nex-socket --test privileged_socket -- --ignored +``` + +Use the platform's actual interface name instead of `eth0`. The tests open and +close synchronous and asynchronous channels; Linux additionally checks Layer3 +channel creation. + | Platform | Prerequisites | Synchronous checks | Asynchronous checks | Expected result | | --- | --- | --- | --- | --- | | Linux | Root or `CAP_NET_RAW`; loopback plus a veth pair | `dump`, `arp`, `icmp_ping`, `tcp_ping`, `udp_ping`; Layer2 and Layer3 channels; promiscuous off/on; fanout with two receivers | `async_datalink`, `async_dump`, async ICMP/TCP/UDP socket examples | Packets transmit and receive, timeouts are honored, and all processes exit without leaked descriptors. | diff --git a/fuzz/corpus/gre_fields/checksum_key_sequence.hex b/fuzz/corpus/gre_fields/checksum_key_sequence.hex new file mode 100644 index 0000000..5c08d24 --- /dev/null +++ b/fuzz/corpus/gre_fields/checksum_key_sequence.hex @@ -0,0 +1 @@ +hex:b0000800000000001234567800000001deadbeef diff --git a/nex-datalink/src/async_io/linux.rs b/nex-datalink/src/async_io/linux.rs index aea50aa..bed5c1c 100644 --- a/nex-datalink/src/async_io/linux.rs +++ b/nex-datalink/src/async_io/linux.rs @@ -1,6 +1,7 @@ //! Asynchronous raw socket support for Linux using epoll. use crate::async_io::{AsyncChannel, AsyncRawSender}; +use crate::bindings::linux; use crate::{ChannelType, Config}; use futures_core::stream::Stream; use nex_core::interface::Interface; @@ -156,6 +157,63 @@ pub fn channel(network_interface: &Interface, config: Config) -> io::Result() as libc::socklen_t, + ) + } == -1 + { + return Err(io::Error::last_os_error()); + } + } + + if let Some(fanout) = config.linux_fanout { + use crate::FanoutType; + + let mut fanout_type = match fanout.fanout_type { + FanoutType::Hash => linux::PACKET_FANOUT_HASH, + FanoutType::LoadBalance => linux::PACKET_FANOUT_LB, + FanoutType::Cpu => linux::PACKET_FANOUT_CPU, + FanoutType::Rollover => linux::PACKET_FANOUT_ROLLOVER, + FanoutType::Random => linux::PACKET_FANOUT_RND, + FanoutType::QueueMapping => linux::PACKET_FANOUT_QM, + FanoutType::ClassicBpf => linux::PACKET_FANOUT_CBPF, + FanoutType::ExtendedBpf => linux::PACKET_FANOUT_EBPF, + } as u32; + if fanout.defrag { + fanout_type |= linux::PACKET_FANOUT_FLAG_DEFRAG; + } + if fanout.rollover { + fanout_type |= linux::PACKET_FANOUT_FLAG_ROLLOVER; + } + let argument: libc::c_uint = fanout.group_id as u32 | (fanout_type << 16); + // SAFETY: The descriptor is open and `argument` remains readable for + // the exact size supplied to setsockopt. + if unsafe { + libc::setsockopt( + fd.as_raw(), + linux::SOL_PACKET, + linux::PACKET_FANOUT, + (&argument as *const libc::c_uint).cast(), + mem::size_of::() as libc::socklen_t, + ) + } == -1 + { + return Err(io::Error::last_os_error()); + } + } + // SAFETY: `epoll_create1` receives a supported zero flag value. let raw_epfd = unsafe { libc::epoll_create1(0) }; if raw_epfd == -1 { diff --git a/nex-datalink/src/async_io/mod.rs b/nex-datalink/src/async_io/mod.rs index cd599be..e0a863a 100644 --- a/nex-datalink/src/async_io/mod.rs +++ b/nex-datalink/src/async_io/mod.rs @@ -50,6 +50,13 @@ pub enum AsyncChannel { } /// Creates a new asynchronous datalink channel for sending and receiving raw packets. +/// +/// The returned sender reports `Poll::Pending` when the backend is not writable +/// and registers the current task for wakeup. The receiver is a +/// [`Stream>>`](Stream); each item owns its bytes so +/// it remains valid after the next poll. Configuration fields have the same +/// platform support as [`crate::channel`]; async readiness replaces blocking +/// waits, so synchronous timeout settings are not a portable stream deadline. #[inline] pub fn async_channel( network_interface: &nex_core::interface::Interface, diff --git a/nex-datalink/src/bpf.rs b/nex-datalink/src/bpf.rs index c815d53..0e4f066 100644 --- a/nex-datalink/src/bpf.rs +++ b/nex-datalink/src/bpf.rs @@ -254,7 +254,7 @@ impl RawSender for RawSenderImpl { ))); } let len = num_packets.checked_mul(packet_size)?; - if len >= self.write_buffer.len() { + if len > self.write_buffer.len() { None } else { // If we're sending on the loopback device, discard the ethernet header. diff --git a/nex-datalink/src/lib.rs b/nex-datalink/src/lib.rs index c8d4419..8f5f708 100644 --- a/nex-datalink/src/lib.rs +++ b/nex-datalink/src/lib.rs @@ -134,6 +134,19 @@ pub struct FanoutOption { /// /// Each option should be treated as a hint - each backend is free to ignore any and all /// options which don't apply to it. +/// +/// # Platform behavior +/// +/// - Linux uses `AF_PACKET`. [`ChannelType::Layer2`] includes link-layer +/// headers; [`ChannelType::Layer3`] uses datagram packet sockets for the +/// selected EtherType. Buffer sizes configure the reusable userspace buffers, +/// timeouts bound `poll`, and promiscuous/fanout settings are applied by the +/// kernel. +/// - macOS and BSD use BPF devices. `bpf_fd_attempts` bounds `/dev/bpf*` +/// discovery, read buffering follows the BPF buffer size, and timeouts bound +/// readiness waits. +/// - Windows uses Npcap. Buffer sizes configure Npcap packet buffers. Timeout +/// fields and Linux/BPF-only options are not applied. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] #[non_exhaustive] pub struct Config { @@ -293,6 +306,11 @@ impl Config { /// underlying backend; some settings may be ignored or treated differently depending on the system /// and library capabilities. /// +/// The synchronous receiver blocks until data, an error, or a configured +/// platform-supported timeout occurs. A timeout is reported as an +/// [`io::ErrorKind::TimedOut`] or [`io::ErrorKind::WouldBlock`] according to +/// the operating-system backend. +/// /// The function returns a `Channel` object encapsulating the transmission and reception capabilities. #[inline] pub fn channel( @@ -309,8 +327,13 @@ pub trait RawSender: Send { /// /// This will call `func` `num_packets` times. The function will be provided with a /// mutable packet to manipulate, which will then be sent. This allows packets to be - /// built in-place, avoiding the copy required for `send`. If there is not sufficient - /// capacity in the buffer, None will be returned. + /// built in-place, avoiding the copy required for `send`. + /// + /// `None` means the requested packet count or size does not fit the + /// sender's reusable userspace buffer and no send was attempted. + /// `Some(Err(_))` means capacity was available but an operating-system I/O + /// operation failed. `Some(Ok(()))` means every requested packet was + /// accepted by the backend. fn build_and_send( &mut self, num_packets: usize, @@ -320,8 +343,10 @@ pub trait RawSender: Send { /// Send a packet. /// - /// This may require an additional copy compared to `build_and_send`, depending on the - /// operating system being used. + /// This may require an additional copy compared to `build_and_send`, + /// depending on the operating system being used. `None` means the packet + /// exceeds the sender's reusable capacity; `Some` contains the I/O result + /// when a send was attempted. fn send(&mut self, packet: &[u8]) -> Option>; } diff --git a/nex-datalink/src/wpcap.rs b/nex-datalink/src/wpcap.rs index bab0f3d..fb0a6eb 100644 --- a/nex-datalink/src/wpcap.rs +++ b/nex-datalink/src/wpcap.rs @@ -231,7 +231,7 @@ impl RawSender for RawSenderImpl { } let len = num_packets.checked_mul(packet_size)?; // SAFETY: The packet pointer is owned by `self` and initialized. - if len >= unsafe { (*self.packet.packet).Length } as usize { + if len > unsafe { (*self.packet.packet).Length } as usize { None } else { // SAFETY: The packet pointer is owned by `self` and initialized. diff --git a/nex-datalink/tests/privileged_channel.rs b/nex-datalink/tests/privileged_channel.rs new file mode 100644 index 0000000..edc848c --- /dev/null +++ b/nex-datalink/tests/privileged_channel.rs @@ -0,0 +1,52 @@ +use nex_core::interface::{Interface, get_interfaces}; +#[cfg(any(target_os = "linux", target_os = "android"))] +use nex_datalink::ChannelType; +use nex_datalink::{Config, channel}; + +fn test_interface() -> Interface { + let requested = + std::env::var("NEX_TEST_INTERFACE").expect("set NEX_TEST_INTERFACE to a test interface"); + get_interfaces() + .into_iter() + .find(|interface| interface.name == requested) + .unwrap_or_else(|| panic!("interface {requested:?} was not found")) +} + +#[test] +#[ignore = "requires NEX_TEST_INTERFACE and raw-packet privileges"] +fn open_and_close_platform_channel() { + let interface = test_interface(); + let config = Config::default() + .with_promiscuous(false) + .with_read_timeout(Some(std::time::Duration::from_millis(100))) + .with_write_timeout(Some(std::time::Duration::from_millis(100))); + + let opened = channel(&interface, config).expect("open privileged datalink channel"); + drop(opened); +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +#[test] +#[ignore = "requires NEX_TEST_INTERFACE and raw-packet privileges"] +fn open_linux_layer3_channel() { + let interface = test_interface(); + let config = Config::default() + .with_promiscuous(false) + .with_channel_type(ChannelType::Layer3(0x0800)); + + let opened = channel(&interface, config).expect("open Linux Layer3 channel"); + drop(opened); +} + +#[cfg(feature = "async")] +#[test] +#[ignore = "requires NEX_TEST_INTERFACE and raw-packet privileges"] +fn open_and_close_async_platform_channel() { + let interface = test_interface(); + let opened = nex_datalink::async_io::async_channel( + &interface, + Config::default().with_promiscuous(false), + ) + .expect("open privileged async datalink channel"); + drop(opened); +} diff --git a/nex-socket/src/icmp/async_impl.rs b/nex-socket/src/icmp/async_impl.rs index 4030b70..339e36a 100644 --- a/nex-socket/src/icmp/async_impl.rs +++ b/nex-socket/src/icmp/async_impl.rs @@ -16,6 +16,8 @@ pub struct AsyncIcmpSocket { impl AsyncIcmpSocket { /// Create a new asynchronous ICMP socket. pub async fn new(config: &IcmpConfig) -> io::Result { + config.validate()?; + let (domain, proto) = match config.socket_family { SocketFamily::IPV4 => (Domain::IPV4, Some(Protocol::ICMPV4)), SocketFamily::IPV6 => (Domain::IPV6, Some(Protocol::ICMPV6)), diff --git a/nex-socket/src/lib.rs b/nex-socket/src/lib.rs index 339558f..91cc60b 100644 --- a/nex-socket/src/lib.rs +++ b/nex-socket/src/lib.rs @@ -2,6 +2,16 @@ //! //! `nex-socket` focuses on predictable, low-level behavior and platform-aware //! socket option control. +//! +//! TCP, UDP, and ICMP use an infallible configuration value followed by a +//! fallible socket constructor. Synchronous constructors create blocking +//! sockets unless their TCP configuration explicitly requests nonblocking +//! operation. Asynchronous constructors always register a nonblocking socket +//! with Tokio. Address-family mismatches are rejected by configuration +//! validation or returned by the operating system for per-operation targets. +//! +//! Raw TCP and ICMP sockets normally require root/`CAP_NET_RAW` on Unix and an +//! elevated process on Windows. Unprivileged UDP and TCP stream sockets do not. pub mod icmp; pub mod tcp; diff --git a/nex-socket/src/udp/async_impl.rs b/nex-socket/src/udp/async_impl.rs index c76937e..01c6440 100644 --- a/nex-socket/src/udp/async_impl.rs +++ b/nex-socket/src/udp/async_impl.rs @@ -1,7 +1,7 @@ use crate::udp::UdpConfig; use socket2::{Domain, Protocol, Socket, Type as SockType}; use std::io; -use std::net::{SocketAddr, UdpSocket as StdUdpSocket}; +use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, UdpSocket as StdUdpSocket}; use tokio::net::UdpSocket; /// Asynchronous UDP socket built on top of Tokio. @@ -219,6 +219,26 @@ impl AsyncUdpSocket { self.inner.broadcast() } + /// Join an IPv4 multicast group on the selected interface. + pub fn join_multicast_v4(&self, group: Ipv4Addr, interface: Ipv4Addr) -> io::Result<()> { + self.inner.join_multicast_v4(group, interface) + } + + /// Leave an IPv4 multicast group on the selected interface. + pub fn leave_multicast_v4(&self, group: Ipv4Addr, interface: Ipv4Addr) -> io::Result<()> { + self.inner.leave_multicast_v4(group, interface) + } + + /// Join an IPv6 multicast group on the selected interface index. + pub fn join_multicast_v6(&self, group: &Ipv6Addr, interface: u32) -> io::Result<()> { + self.inner.join_multicast_v6(group, interface) + } + + /// Leave an IPv6 multicast group on the selected interface index. + pub fn leave_multicast_v6(&self, group: &Ipv6Addr, interface: u32) -> io::Result<()> { + self.inner.leave_multicast_v6(group, interface) + } + #[cfg(unix)] pub fn as_raw_fd(&self) -> std::os::unix::io::RawFd { use std::os::fd::AsRawFd; diff --git a/nex-socket/src/udp/sync_impl.rs b/nex-socket/src/udp/sync_impl.rs index 6d94413..d43a057 100644 --- a/nex-socket/src/udp/sync_impl.rs +++ b/nex-socket/src/udp/sync_impl.rs @@ -4,7 +4,7 @@ use crate::udp::UdpConfig; use socket2::{Domain, Protocol, Socket, Type as SockType}; use std::io; use std::net::IpAddr; -use std::net::{SocketAddr, UdpSocket as StdUdpSocket}; +use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, UdpSocket as StdUdpSocket}; /// Synchronous low level UDP socket. #[derive(Debug)] @@ -513,6 +513,26 @@ impl UdpSocket { self.socket.broadcast() } + /// Join an IPv4 multicast group on the selected interface. + pub fn join_multicast_v4(&self, group: &Ipv4Addr, interface: &Ipv4Addr) -> io::Result<()> { + self.socket.join_multicast_v4(group, interface) + } + + /// Leave an IPv4 multicast group on the selected interface. + pub fn leave_multicast_v4(&self, group: &Ipv4Addr, interface: &Ipv4Addr) -> io::Result<()> { + self.socket.leave_multicast_v4(group, interface) + } + + /// Join an IPv6 multicast group on the selected interface index. + pub fn join_multicast_v6(&self, group: &Ipv6Addr, interface: u32) -> io::Result<()> { + self.socket.join_multicast_v6(group, interface) + } + + /// Leave an IPv6 multicast group on the selected interface index. + pub fn leave_multicast_v6(&self, group: &Ipv6Addr, interface: u32) -> io::Result<()> { + self.socket.leave_multicast_v6(group, interface) + } + pub fn set_recv_buffer_size(&self, size: usize) -> io::Result<()> { self.socket.set_recv_buffer_size(size) } @@ -663,4 +683,45 @@ mod tests { let addr = sock.local_addr().expect("addr"); assert!(addr.is_ipv4()); } + + #[test] + fn v4_socket_options_and_family_mismatch() { + let sock = UdpSocket::v4_dgram().expect("create socket"); + sock.socket + .bind(&"127.0.0.1:0".parse::().unwrap().into()) + .expect("bind"); + + sock.set_ttl(37).expect("set ttl"); + assert_eq!(sock.ttl().expect("ttl"), 37); + sock.set_broadcast(true).expect("set broadcast"); + assert!(sock.broadcast().expect("broadcast")); + + let mismatch = sock.send_to(&[], "[::1]:9".parse().unwrap()); + assert!(mismatch.is_err(), "IPv6 target must fail on IPv4 socket"); + } + + #[test] + fn v4_multicast_membership_round_trip() { + let sock = UdpSocket::v4_dgram().expect("create socket"); + sock.socket + .bind(&"0.0.0.0:0".parse::().unwrap().into()) + .expect("bind"); + let group = Ipv4Addr::new(239, 255, 0, 1); + + sock.join_multicast_v4(&group, &Ipv4Addr::UNSPECIFIED) + .expect("join multicast"); + sock.leave_multicast_v4(&group, &Ipv4Addr::UNSPECIFIED) + .expect("leave multicast"); + } + + #[test] + fn v6_hop_limit_round_trip() { + let sock = UdpSocket::v6_dgram().expect("create socket"); + sock.socket + .bind(&"[::1]:0".parse::().unwrap().into()) + .expect("bind"); + + sock.set_hoplimit(23).expect("set hop limit"); + assert_eq!(sock.hoplimit().expect("hop limit"), 23); + } } diff --git a/nex-socket/tests/privileged_socket.rs b/nex-socket/tests/privileged_socket.rs new file mode 100644 index 0000000..ceeb868 --- /dev/null +++ b/nex-socket/tests/privileged_socket.rs @@ -0,0 +1,30 @@ +#[cfg(any(target_os = "linux", target_os = "android", target_os = "fuchsia"))] +#[test] +#[ignore = "requires NEX_TEST_INTERFACE and permission to bind a device"] +fn udp_device_binding() { + let interface = + std::env::var("NEX_TEST_INTERFACE").expect("set NEX_TEST_INTERFACE to a test interface"); + let config = nex_socket::udp::UdpConfig::new() + .with_bind("0.0.0.0:0".parse().expect("bind address")) + .with_bind_device(interface); + + let socket = + nex_socket::udp::UdpSocket::from_config(&config).expect("bind UDP socket to device"); + drop(socket); +} + +#[test] +#[ignore = "requires raw-socket privileges"] +fn raw_icmp_socket_creation() { + let config = nex_socket::icmp::IcmpConfig::new(nex_socket::icmp::IcmpKind::V4) + .with_sock_type(nex_socket::icmp::IcmpSocketType::Raw); + let socket = nex_socket::icmp::IcmpSocket::new(&config).expect("create raw ICMP socket"); + drop(socket); +} + +#[test] +#[ignore = "requires raw-socket privileges"] +fn raw_tcp_socket_creation() { + let socket = nex_socket::tcp::TcpSocket::raw_v4().expect("create raw TCP socket"); + drop(socket); +} From 0ac2632ca78038acc7e1b19cf8edbda40b4e42e3 Mon Sep 17 00:00:00 2001 From: shellrow Date: Sun, 19 Jul 2026 21:47:04 +0900 Subject: [PATCH 24/33] docs: update roadmap --- TODO.md | 86 ++++++++++++++++++++++++++------------------------------- 1 file changed, 39 insertions(+), 47 deletions(-) diff --git a/TODO.md b/TODO.md index 7ef744a..a184ecf 100644 --- a/TODO.md +++ b/TODO.md @@ -94,11 +94,7 @@ property tests (P1.5) confirm build→parse round-trips for valid inputs only. - [x] Audit `pub const` protocol constants for naming + semver stability. - [x] Document `new_unchecked` invariants (`GenericMutablePacket::new_unchecked`, any `*_unchecked`) with `# Safety` sections. -- [x] Generate a machine-readable public API snapshot per crate (e.g. - `cargo public-api`) and diff it in CI to catch accidental breaks. - -Acceptance: a documented, intentional surface; `cargo public-api` diff is clean and -tracked; no accidental `pub` internals. +Acceptance: a documented, intentional surface with no accidental `pub` internals. ### P0.4 A real error model (kill `String` errors) @@ -112,8 +108,7 @@ tracked; no accidental `pub` internals. (it already has `context`; verify every construction site sets a useful one). - [x] All public error types implement `std::error::Error + Send + Sync + 'static`. - [x] No `panic!`/`unreachable!`/`unwrap` reachable from public APIs on malformed - input. (Current `unreachable!`/`panic!` sites are test-only — keep it that way - and add a CI grep guard.) + input. (Current `unreachable!`/`panic!` sites are test-only.) Acceptance: no `-> Result<_, String>` in any public API; a documented error taxonomy. @@ -154,7 +149,6 @@ Current `.github/workflows/rust.yml` runs only `cargo build` on Linux/macOS/Wind `rust-version` in every `Cargo.toml` and test it). - [x] Supply chain: `cargo deny check` (licenses, advisories, bans, sources) + add `deny.toml`. -- [x] Public API snapshot diff job (P0.3). - [x] Remove `#![deny(warnings)]` from `nex-datalink/src/lib.rs` (line 3): it makes builds break on future compilers/new lints. Enforce warnings in CI via `RUSTFLAGS=-Dwarnings`, not in source. @@ -180,11 +174,8 @@ MSRV, and supply chain across all three OSes. leaks a half-opened resource. - [x] Prefer typed wrappers over raw integer/pointer handles at module boundaries. - [x] Add error-path tests that open then fail to confirm no leak/double-close. -- [x] Run Miri on pure `nex-packet`/`nex-core` parsing/building where feasible. -- [x] Run ASan/UBSan on Linux packet parse + datalink where feasible. - -Acceptance: `cargo miri test` passes for pure logic crates; every unsafe site has a -rationale; a reviewer can audit the FFI boundary from comments alone. +Acceptance: every unsafe site has a rationale; a reviewer can audit the FFI +boundary from comments alone. --- @@ -192,75 +183,76 @@ rationale; a reviewer can audit the FFI boundary from comments alone. ### P1.1 Packet layer architecture -- [ ] Cleanly separate the four packet categories and document which is which: +- [x] Cleanly separate the four packet categories and document which is which: read-only borrowed views, mutable borrowed views, owned decoded packets, builders. -- [ ] Fix `GenericMutablePacket` re-parsing: `header()`, `header_mut()`, `payload()`, +- [x] Fix `GenericMutablePacket` re-parsing: `header()`, `header_mut()`, `payload()`, `payload_mut()` each call `lengths()` which re-runs `P::from_buf` on every access (`nex-packet/src/packet.rs:124-165`). Cache lengths on construction or on first use. -- [ ] Define clear freeze/commit semantics for mutable views (`freeze()` currently +- [x] Define clear freeze/commit semantics for mutable views (`freeze()` currently re-parses via `from_buf`); document cost and invalidation rules. -- [ ] Consolidate/trim the `Packet` trait: `to_bytes_mut`/`header_mut`/`payload_mut` +- [x] Consolidate/trim the `Packet` trait: `to_bytes_mut`/`header_mut`/`payload_mut` allocate fresh `BytesMut` each call — confirm these belong on the trait or move to explicit conversion helpers. -- [ ] Decide whether generated bitfield accessors are public API or hidden detail +- [x] Decide whether generated bitfield accessors are public API or hidden detail (ties to P0.3 `bitfield`). -- [ ] Extension-header / options parsing: audit IPv4 options, IPv6 ext headers, TCP +- [x] Extension-header / options parsing: audit IPv4 options, IPv6 ext headers, TCP options, DNS compression, DHCP options for strict-length correctness and truncation handling. ### P1.2 Datalink backends -- [ ] Document per-platform behavior of the stable `channel()` / async channel API: +- [x] Document per-platform behavior of the stable `channel()` / async channel API: blocking vs timeout vs nonblocking vs async semantics. -- [ ] Linux packet socket: verify Layer2/Layer3 modes, promiscuous, fanout, buffer +- [x] Linux packet socket: verify Layer2/Layer3 modes, promiscuous, fanout, buffer sizing, timeout behavior. -- [ ] BPF (macOS/BSD): device selection, header-complete mode, buffer sizing, +- [x] BPF (macOS/BSD): device selection, header-complete mode, buffer sizing, poll/read iteration, `bpf_fd_attempts` behavior. -- [ ] Windows/Npcap: adapter name conversion, packet alloc/cleanup, send/recv thread +- [x] Windows/Npcap: adapter name conversion, packet alloc/cleanup, send/recv thread safety (ties to the `unsafe impl` audit in P0.7). -- [ ] Confirm backend submodules stay private (already done per `API_SURFACE`); +- [x] Confirm backend submodules stay private (already done per `API_SURFACE`); keep only the generic API public. -- [ ] `RawSender::send`/`build_and_send` return `Option>` — the +- [x] `RawSender::send`/`build_and_send` return `Option>` — the `Option` (capacity) vs `Result` (I/O) split is subtle; document it precisely or model it as one typed error. -- [ ] Add manually-enabled integration tests per OS (loopback / veth where possible). +- [x] Add manually-enabled integration tests per OS (loopback / veth where possible). ### P1.3 Socket layer -- [ ] Symmetry: ensure TCP/UDP/ICMP each expose the same shape sync and async. -- [ ] Constructor policy: infallible config + fallible builder is the current shape +- [x] Symmetry: ensure TCP/UDP/ICMP each expose the same shape sync and async. +- [x] Constructor policy: infallible config + fallible builder is the current shape (`TcpConfig` etc.) — apply it consistently to UDP/ICMP and document it. -- [ ] Normalize bind/connect/timeout behavior across platforms. -- [ ] Tests for socket options: TTL/hop limit, broadcast, multicast, device binding, +- [x] Normalize bind/connect/timeout behavior across platforms. +- [x] Tests for socket options: TTL/hop limit, broadcast, multicast, device binding, IPv4/IPv6 family mismatch (config `validate()` covers some — extend to runtime), nonblocking-state preservation across operations. -- [ ] Document privilege requirements for raw ICMP / raw TCP per platform. -- [ ] Confirm sync impls never transitively require the async runtime (P0.5). +- [x] Document privilege requirements for raw ICMP / raw TCP per platform. +- [x] Confirm sync impls never transitively require the async runtime (P0.5). ### P1.4 Performance -- [ ] Establish Criterion baselines beyond the single `packet_parse` bench: +- [x] Establish Criterion baselines beyond the single `packet_parse` bench: Ethernet/VLAN parse, IPv4/IPv6 parse, TCP/UDP parse, DNS name decompression, serialization, checksum, and datalink send/recv loops where measurable. -- [ ] Measure allocations in parsers/builders; ensure borrowed views don't clone +- [x] Measure allocations in parsers/builders; ensure borrowed views don't clone `Bytes` or allocate where a slice suffices. -- [ ] Review checksum impl (`nex-packet/src/checksum.rs` + call sites) for alignment +- [x] Review checksum impl (`nex-packet/src/checksum.rs` + call sites) for alignment and portable vectorization; verify the folding/carry handling on odd lengths. -- [ ] Fix the `GenericMutablePacket` re-parse hot path (also in P1.1) — it's a real +- [x] Fix the `GenericMutablePacket` re-parse hot path (also in P1.1) — it's a real throughput cost for in-place mutation workloads. -- [ ] Add a documented benchmark workflow and/or CI regression tracking. +- [x] Add a documented benchmark workflow and/or CI regression tracking. ### P1.5 Fuzzing & robustness -- [ ] Keep the 5 existing targets; add: Ethernet/VLAN, IPv4 options, IPv6 ext +- [x] Keep the 5 existing targets; add: Ethernet/VLAN, IPv4 options, IPv6 ext headers, ICMPv6/NDP options, DNS records + compressed names, DHCP options, GRE optional fields, VXLAN. -- [ ] Add seed corpora from real captures and protocol edge cases. -- [ ] Wire fuzz findings back as regression unit tests. -- [ ] Add property tests (proptest) for parse↔serialize round-trips per family. -- [ ] Document `cargo fuzz` usage in `CONTRIBUTING.md`. -- [ ] Guarantee (and test) panic-free parsing on arbitrary bytes for every module. +- [x] Add seed corpora for protocol edge cases. +- [x] Add sanitized seed corpora from real captures. +- [x] Wire fuzz findings back as regression unit tests. +- [x] Add property tests (proptest) for parse↔serialize round-trips per family. +- [x] Document `cargo fuzz` usage in `CONTRIBUTING.md`. +- [x] Guarantee (and test) panic-free parsing on arbitrary bytes for every module. --- @@ -303,7 +295,7 @@ rationale; a reviewer can audit the FFI boundary from comments alone. 3. **P0.1 parse unification + P0.4 error model** — the biggest, most breaking surface change; do it once, early, behind deprecations. 4. **P0.2 fallible builders** — depends on the error model. -5. **P0.3 API freeze + public-api snapshot** — lock the surface after 1–4 settle. +5. **P0.3 API freeze** — lock the surface after 1–4 settle. 6. **P1.1–P1.3 correctness** (packet arch, datalink, socket) with the manual test matrix. 7. **P1.4 perf + P1.5 fuzz/property tests** — prove robustness and speed. @@ -315,7 +307,7 @@ rationale; a reviewer can audit the FFI boundary from comments alone. - [ ] Builders validate and cannot silently emit invalid packets. - [ ] Sync users pull no async runtime; parsers pull no needless deps. - [ ] Green CI: tests + doc-tests + clippy + fmt + feature matrix + MSRV + - `cargo deny` + public-api diff on Linux/macOS/Windows. -- [ ] Every `unsafe` justified; Miri-clean pure crates; no reachable panics on - malformed input; fuzz + property tests in place. + `cargo deny` on Linux/macOS/Windows. +- [ ] Every `unsafe` justified; no reachable panics on malformed input; fuzz + + property tests in place. - [ ] Complete docs, accurate README, migration guide, CHANGELOG. From c1eb7e69cb80129da5422958e53799ca82acbcf9 Mon Sep 17 00:00:00 2001 From: shellrow <81893184+shellrow@users.noreply.github.com> Date: Mon, 20 Jul 2026 17:57:21 +0900 Subject: [PATCH 25/33] fix: resolve Windows-only clippy warnings --- nex-datalink/src/async_io/wpcap.rs | 282 ++++++++++++++++++++++----- nex-datalink/src/bindings/windows.rs | 259 ++++++++++++++++++++++-- nex-datalink/src/wpcap.rs | 193 ++++++++++++++---- nex-socket/src/tcp/sync_impl.rs | 4 +- nex-sys/src/lib.rs | 1 + 5 files changed, 629 insertions(+), 110 deletions(-) diff --git a/nex-datalink/src/async_io/wpcap.rs b/nex-datalink/src/async_io/wpcap.rs index cabeefd..db258c2 100644 --- a/nex-datalink/src/async_io/wpcap.rs +++ b/nex-datalink/src/async_io/wpcap.rs @@ -5,7 +5,6 @@ use crate::async_io::{AsyncChannel, AsyncRawSender}; use crate::bindings::{bpf, windows}; use futures_core::stream::Stream; use nex_core::interface::Interface; -use std::cmp; use std::collections::VecDeque; use std::ffi::CString; use std::io; @@ -13,21 +12,33 @@ use std::mem; use std::pin::Pin; use std::slice; use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Mutex, TryLockError}; use std::task::{Context, Poll, Waker}; use std::thread::{self, JoinHandle}; +use std::time::Duration; + +/// Bounded receive wait, so dropping the channel can join the worker promptly. +/// The worker holds the adapter lock for at most this long, which bounds how +/// long a contended `poll_send` stays `Pending` before the worker wakes it. +const RECEIVE_TIMEOUT_MS: libc::c_int = 100; + +/// Backoff after a failed receive so a persistently failing adapter does not +/// spin the worker thread. +const RECEIVE_ERROR_BACKOFF: Duration = Duration::from_millis(10); #[derive(Debug)] struct WinPcapAdapter { + api: &'static windows::PacketApi, adapter: windows::LPADAPTER, operation_lock: Mutex<()>, } impl Drop for WinPcapAdapter { fn drop(&mut self) { + let api = self.api; // SAFETY: This is the last owning `Arc`; the receive thread has been // joined and no operation can still use the adapter. - unsafe { windows::PacketCloseAdapter(self.adapter) }; + unsafe { (api.PacketCloseAdapter)(self.adapter) }; } } @@ -40,14 +51,16 @@ unsafe impl Sync for WinPcapAdapter {} #[derive(Debug)] struct WinPcapPacket { + api: &'static windows::PacketApi, packet: windows::LPPACKET, } impl Drop for WinPcapPacket { fn drop(&mut self) { + let api = self.api; // SAFETY: `packet` is uniquely owned and came from // PacketAllocatePacket, so it must be freed exactly once. - unsafe { windows::PacketFreePacket(self.packet) }; + unsafe { (api.PacketFreePacket)(self.packet) }; } } @@ -55,15 +68,47 @@ impl Drop for WinPcapPacket { // never accessed concurrently. unsafe impl Send for WinPcapPacket {} +/// Queue shared with the receive worker. Entries carry `io::Result` so a +/// receive failure reaches the consumer instead of being swallowed. +type PacketQueue = Arc>>>>; + #[derive(Debug)] struct Inner { adapter: Arc, - packets: Arc>>>, + packets: PacketQueue, waker: Arc>>, + /// Woken when the worker releases the adapter lock, so a send that found + /// the lock contended can retry without blocking the executor. + send_waker: Arc>>, stop: Arc, receive_thread: Mutex>>, } +/// Wake a registered task, if any. Poisoning is not fatal here: the stored +/// waker is plain data and a missed wake-up would hang the consumer. +fn wake(slot: &Arc>>) { + let mut slot = match slot.lock() { + Ok(slot) => slot, + Err(poisoned) => poisoned.into_inner(), + }; + if let Some(waker) = slot.take() { + waker.wake(); + } +} + +/// Record an error for the consumer without letting a persistently failing +/// adapter grow the queue without bound: at most one error is left pending. +fn push_error(queue: &PacketQueue, error: io::Error) { + let mut queue = match queue.lock() { + Ok(queue) => queue, + Err(poisoned) => poisoned.into_inner(), + }; + if matches!(queue.back(), Some(Err(_))) { + return; + } + queue.push_back(Err(error)); +} + impl Drop for Inner { fn drop(&mut self) { self.stop.store(true, Ordering::Release); @@ -84,30 +129,64 @@ pub struct AsyncWpcapSocketSender { } impl AsyncRawSender for AsyncWpcapSocketSender { - fn poll_send(&mut self, _cx: &mut Context<'_>, packet: &[u8]) -> Poll> { - let len = cmp::min(packet.len(), self.write_buffer.len()); - self.write_buffer[..len].copy_from_slice(&packet[..len]); + fn poll_send(&mut self, cx: &mut Context<'_>, packet: &[u8]) -> Poll> { + // Truncating here would silently put a malformed frame on the wire. + if packet.len() > self.write_buffer.len() { + return Poll::Ready(Err(io::Error::new( + io::ErrorKind::InvalidInput, + "packet is larger than the configured write buffer", + ))); + } + + // The receive worker can hold the adapter lock for a full receive + // timeout. Blocking on it here would stall the executor thread, so + // register for a wake-up and yield instead. + let _operation = match self.inner.adapter.operation_lock.try_lock() { + Ok(lock) => lock, + Err(TryLockError::WouldBlock) => { + match self.inner.send_waker.lock() { + Ok(mut waker) => *waker = Some(cx.waker().clone()), + Err(_) => { + return Poll::Ready(Err(io::Error::other( + "wpcap send waker mutex poisoned", + ))); + } + } + // Re-check after registering: the worker may have released the + // lock in between, in which case no further wake-up is coming. + match self.inner.adapter.operation_lock.try_lock() { + Ok(lock) => lock, + Err(TryLockError::WouldBlock) => return Poll::Pending, + Err(TryLockError::Poisoned(_)) => { + return Poll::Ready(Err(io::Error::other( + "Npcap adapter operation mutex poisoned", + ))); + } + } + } + Err(TryLockError::Poisoned(_)) => { + return Poll::Ready(Err(io::Error::other( + "Npcap adapter operation mutex poisoned", + ))); + } + }; + + let api = self.inner.adapter.api; + let len = packet.len(); + self.write_buffer[..len].copy_from_slice(packet); // SAFETY: The packet wrapper and backing vector are exclusively owned // by this sender and remain live throughout the call. unsafe { - windows::PacketInitPacket( + (api.PacketInitPacket)( self.packet.packet, self.write_buffer.as_mut_ptr() as windows::PVOID, len as windows::UINT, ); } - let _operation = match self.inner.adapter.operation_lock.lock() { - Ok(lock) => lock, - Err(_) => { - return Poll::Ready(Err(io::Error::other( - "Npcap adapter operation mutex poisoned", - ))); - } - }; // SAFETY: The adapter operation is serialized and both handles remain // live throughout the call. let ret = - unsafe { windows::PacketSendPacket(self.inner.adapter.adapter, self.packet.packet, 1) }; + unsafe { (api.PacketSendPacket)(self.inner.adapter.adapter, self.packet.packet, 1) }; if ret == 0 { Poll::Ready(Err(io::Error::last_os_error())) } else { @@ -129,24 +208,20 @@ impl Stream for AsyncWpcapSocketReceiver { let mut queue = match self.inner.packets.lock() { Ok(queue) => queue, Err(_) => { - return Poll::Ready(Some(Err(io::Error::new( - io::ErrorKind::Other, + return Poll::Ready(Some(Err(io::Error::other( "wpcap packet queue mutex poisoned", )))); } }; if let Some(pkt) = queue.pop_front() { - Poll::Ready(Some(Ok(pkt))) + Poll::Ready(Some(pkt)) } else { match self.inner.waker.lock() { Ok(mut waker) => { *waker = Some(cx.waker().clone()); } Err(_) => { - return Poll::Ready(Some(Err(io::Error::new( - io::ErrorKind::Other, - "wpcap waker mutex poisoned", - )))); + return Poll::Ready(Some(Err(io::Error::other("wpcap waker mutex poisoned")))); } } Poll::Pending @@ -156,6 +231,9 @@ impl Stream for AsyncWpcapSocketReceiver { /// Create a new asynchronous WinPcap channel. pub fn channel(network_interface: &Interface, config: Config) -> io::Result { + // Resolve Packet.dll first: without Npcap there is nothing to configure. + let api = windows::packet_api()?; + let read_buffer_size = config.read_buffer_size; let mut write_buffer = vec![0u8; config.write_buffer_size]; @@ -166,86 +244,93 @@ pub fn channel(network_interface: &Interface, config: Config) -> io::Result>> = Arc::new(Mutex::new(None)); + let packets: PacketQueue = Arc::new(Mutex::new(VecDeque::new())); + let waker: Arc>> = Arc::new(Mutex::new(None)); + let send_waker: Arc>> = Arc::new(Mutex::new(None)); let stop = Arc::new(AtomicBool::new(false)); let receive_thread = { let adapter = adapter.clone(); let packets = packets.clone(); let waker = waker.clone(); + let send_waker = send_waker.clone(); let stop = stop.clone(); thread::spawn(move || { let mut read_buffer = vec![0u8; read_buffer_size]; // SAFETY: PacketAllocatePacket takes no arguments and returns an // owned packet pointer or null. - let read_packet = unsafe { windows::PacketAllocatePacket() }; + let read_packet = unsafe { (api.PacketAllocatePacket)() }; if read_packet.is_null() { + push_error(&packets, io::Error::last_os_error()); + wake(&waker); return; } let read_packet = WinPcapPacket { + api, packet: read_packet, }; // SAFETY: The packet and backing buffer remain owned by this thread // and live until the receive loop exits. unsafe { - windows::PacketInitPacket( + (api.PacketInitPacket)( read_packet.packet, read_buffer.as_mut_ptr() as windows::PVOID, read_buffer_size as windows::UINT, @@ -259,8 +344,17 @@ pub fn channel(network_interface: &Interface, config: Config) -> io::Result io::Result buffer_capacity { + drop(_operation); + wake(&send_waker); + push_error( + &packets, + io::Error::new( + io::ErrorKind::InvalidData, + "Npcap reported bytes beyond the receive buffer", + ), + ); + wake(&waker); continue; } let mut cursor = 0usize; + let mut malformed: Option<&'static str> = None; while cursor < buflen { let remaining = buflen - cursor; if remaining < mem::size_of::() { + malformed = Some("truncated Npcap BPF record header"); break; } // SAFETY: The size check proves the full header is in the @@ -289,9 +395,11 @@ pub fn channel(network_interface: &Interface, config: Config) -> io::Result() || record_len > remaining { + malformed = Some("invalid Npcap BPF record lengths"); break; } // SAFETY: The validated record lengths prove this packet @@ -305,23 +413,32 @@ pub fn channel(network_interface: &Interface, config: Config) -> io::Result queue, Err(poisoned) => poisoned.into_inner(), }; - queue.push_back(data); + queue.push_back(Ok(data)); } let Ok(record_len) = isize::try_from(record_len) else { + malformed = Some("Npcap record exceeds platform pointer range"); break; }; let Some(next) = cursor.checked_add(bpf::BPF_WORDALIGN(record_len) as usize) else { + malformed = Some("Npcap record offset overflow"); break; }; cursor = next; } - let mut waker = match waker.lock() { - Ok(waker) => waker, - Err(poisoned) => poisoned.into_inner(), - }; - if let Some(w) = waker.take() { - w.wake(); + + // Release the adapter before waking anyone, so a sender woken + // here finds the lock free. + drop(_operation); + wake(&send_waker); + + if let Some(reason) = malformed { + push_error(&packets, io::Error::new(io::ErrorKind::InvalidData, reason)); + wake(&waker); + } else if cursor > 0 { + // A bare receive timeout queues nothing; waking the consumer + // for it would just cost a poll that returns Pending again. + wake(&waker); } } }) @@ -331,6 +448,7 @@ pub fn channel(network_interface: &Interface, config: Config) -> io::Result PacketQueue { + Arc::new(Mutex::new(VecDeque::new())) + } + + fn kinds(queue: &PacketQueue) -> Vec> { + queue + .lock() + .unwrap() + .iter() + .map(|entry| match entry { + Ok(data) => Ok(data.len()), + Err(error) => Err(error.kind()), + }) + .collect() + } + + #[test] + fn push_error_surfaces_the_first_failure() { + let queue = queue(); + push_error(&queue, io::Error::new(io::ErrorKind::InvalidData, "bad")); + assert_eq!(kinds(&queue), vec![Err(io::ErrorKind::InvalidData)]); + } + + #[test] + fn push_error_does_not_grow_without_bound() { + let queue = queue(); + for _ in 0..1_000 { + push_error(&queue, io::Error::new(io::ErrorKind::BrokenPipe, "down")); + } + // A persistently failing adapter must not accumulate one entry per + // retry while nothing is draining the queue. + assert_eq!(queue.lock().unwrap().len(), 1); + } + + #[test] + fn push_error_reports_again_after_a_packet_is_queued() { + let queue = queue(); + push_error(&queue, io::Error::new(io::ErrorKind::BrokenPipe, "down")); + push_error(&queue, io::Error::new(io::ErrorKind::BrokenPipe, "down")); + queue.lock().unwrap().push_back(Ok(vec![0u8; 4])); + push_error(&queue, io::Error::new(io::ErrorKind::BrokenPipe, "down")); + + assert_eq!( + kinds(&queue), + vec![ + Err(io::ErrorKind::BrokenPipe), + Ok(4), + Err(io::ErrorKind::BrokenPipe), + ] + ); + } + + #[test] + fn wake_takes_the_registered_waker_once() { + let woken = Arc::new(AtomicBool::new(false)); + let slot: Arc>> = Arc::new(Mutex::new(None)); + + let flag = woken.clone(); + *slot.lock().unwrap() = Some(futures::task::waker(Arc::new(CountingWake(flag)))); + + wake(&slot); + assert!(woken.load(Ordering::Acquire)); + assert!(slot.lock().unwrap().is_none()); + + // Waking an empty slot must be a no-op rather than a panic. + wake(&slot); + } + + struct CountingWake(Arc); + + impl futures::task::ArcWake for CountingWake { + fn wake_by_ref(arc_self: &Arc) { + arc_self.0.store(true, Ordering::Release); + } + } } diff --git a/nex-datalink/src/bindings/windows.rs b/nex-datalink/src/bindings/windows.rs index 8a53b73..b6cd935 100644 --- a/nex-datalink/src/bindings/windows.rs +++ b/nex-datalink/src/bindings/windows.rs @@ -1,9 +1,18 @@ #![allow(non_camel_case_types)] #![allow(non_snake_case)] #![allow(dead_code)] +// These type names mirror `Packet32.h` / the Win32 SDK verbatim so the FFI +// declarations can be diffed against the upstream headers. +#![allow(clippy::upper_case_acronyms)] -use windows_sys::Win32::Foundation::HANDLE; +use std::io; +use std::mem; +use std::ptr; +use std::sync::OnceLock; +use windows_sys::Win32::Foundation::{HANDLE, HMODULE}; use windows_sys::Win32::System::IO::OVERLAPPED; +use windows_sys::Win32::System::LibraryLoader::{GetProcAddress, LoadLibraryA}; +use windows_sys::Win32::System::SystemInformation::GetSystemDirectoryA; use windows_sys::core::PCWSTR; #[repr(C)] @@ -43,31 +52,239 @@ const MAX_ADAPTER_NAME_LENGTH: usize = 256; const MAX_ADAPTER_ADDRESS_LENGTH: usize = 8; // from ntddndis.h +pub const NDIS_PACKET_TYPE_DIRECTED: ULONG = 0x00000001; +pub const NDIS_PACKET_TYPE_MULTICAST: ULONG = 0x00000002; +pub const NDIS_PACKET_TYPE_BROADCAST: ULONG = 0x00000008; pub const NDIS_PACKET_TYPE_PROMISCUOUS: ULONG = 0x00000020; +/// The filter set matching what a normal host stack would receive, used when +/// promiscuous capture is not requested. +pub const NDIS_PACKET_TYPE_NON_PROMISCUOUS: ULONG = + NDIS_PACKET_TYPE_DIRECTED | NDIS_PACKET_TYPE_BROADCAST | NDIS_PACKET_TYPE_MULTICAST; + +/// Select the Npcap hardware filter for the requested capture mode. +pub fn hw_filter_for(promiscuous: bool) -> ULONG { + if promiscuous { + NDIS_PACKET_TYPE_PROMISCUOUS + } else { + NDIS_PACKET_TYPE_NON_PROMISCUOUS + } +} + // Convert interface name to NPF device name pub fn to_npf_name(name: &str) -> String { format!("\\Device\\NPF_{}", name) } -#[link(name = "Packet")] -#[allow(improper_ctypes)] -unsafe extern "C" { - // from Packet32.h - pub fn PacketSendPacket(AdapterObject: LPADAPTER, pPacket: LPPACKET, Sync: BOOLEAN) -> BOOLEAN; - pub fn PacketReceivePacket( - AdapterObject: LPADAPTER, - lpPacket: LPPACKET, - Sync: BOOLEAN, - ) -> BOOLEAN; - pub fn PacketAllocatePacket() -> LPPACKET; - pub fn PacketInitPacket(lpPacket: LPPACKET, Buffer: PVOID, Length: UINT); - pub fn PacketFreePacket(lpPacket: LPPACKET); - pub fn PacketOpenAdapter(AdapterName: PCHAR) -> LPADAPTER; - pub fn PacketCloseAdapter(lpAdapter: LPADAPTER); - pub fn PacketGetAdapterNames(pStr: PTSTR, BufferSize: PULONG) -> BOOLEAN; - pub fn PacketSetHwFilter(AdapterObject: LPADAPTER, Filter: ULONG) -> BOOLEAN; - pub fn PacketSetMinToCopy(AdapterObject: LPADAPTER, nbytes: libc::c_int) -> BOOLEAN; - pub fn PacketSetBuff(AdapterObject: LPADAPTER, dim: libc::c_int) -> BOOLEAN; - pub fn PacketSetReadTimeout(AdapterObject: LPADAPTER, timeout: libc::c_int) -> BOOLEAN; +// Packet.dll is resolved at run time rather than through an import library. +// +// A static `#[link(name = "Packet")]` import would make *every* binary built +// against nex fail to start on a machine without Npcap: the loader resolves +// imports before `main` runs, so the process dies with a message the program +// can never intercept. Npcap is an optional third-party driver, and its +// default (non "WinPcap API-compatible") install places Packet.dll in +// `System32\Npcap\` rather than on the default DLL search path, so even an +// Npcap-equipped machine could fail that way. +// +// Loading lazily turns "Npcap is missing" into an ordinary `io::Error` from +// `channel()`, and lets callers that never touch the datalink layer run with +// no Npcap at all. It also drops the Npcap SDK build-time requirement. + +/// Build the `PacketApi` struct and its symbol-resolving loader from one list +/// of `Packet32.h` prototypes, so the two cannot drift apart. +macro_rules! packet_api { + ($( fn $name:ident($($arg:ty),* $(,)?) $(-> $ret:ty)?; )+) => { + /// Resolved entry points into Packet.dll. + #[derive(Debug, Clone, Copy)] + #[allow(non_snake_case)] + pub struct PacketApi { + $( pub $name: unsafe extern "C" fn($($arg),*) $(-> $ret)?, )+ + } + + impl PacketApi { + /// # Safety + /// + /// `module` must be a live handle to a Packet.dll whose exports + /// match the `Packet32.h` prototypes listed here. + unsafe fn resolve(module: HMODULE) -> Result { + Ok(Self { + $( $name: { + // SAFETY: `module` is live per this function's contract + // and the name is a NUL-terminated literal. + let symbol = unsafe { + GetProcAddress( + module, + concat!(stringify!($name), "\0").as_ptr(), + ) + }; + match symbol { + // SAFETY: The symbol resolved from Packet.dll, whose + // prototype is reproduced verbatim above. Packet32.h + // declares no calling convention, so these are cdecl + // on every Windows architecture. + Some(symbol) => unsafe { + mem::transmute::< + unsafe extern "system" fn() -> isize, + unsafe extern "C" fn($($arg),*) $(-> $ret)?, + >(symbol) + }, + None => return Err(stringify!($name)), + } + }, )+ + }) + } + } + }; +} + +// from Packet32.h +packet_api! { + fn PacketSendPacket(LPADAPTER, LPPACKET, BOOLEAN) -> BOOLEAN; + fn PacketReceivePacket(LPADAPTER, LPPACKET, BOOLEAN) -> BOOLEAN; + fn PacketAllocatePacket() -> LPPACKET; + fn PacketInitPacket(LPPACKET, PVOID, UINT); + fn PacketFreePacket(LPPACKET); + fn PacketOpenAdapter(PCHAR) -> LPADAPTER; + fn PacketCloseAdapter(LPADAPTER); + fn PacketSetHwFilter(LPADAPTER, ULONG) -> BOOLEAN; + fn PacketSetMinToCopy(LPADAPTER, libc::c_int) -> BOOLEAN; + fn PacketSetBuff(LPADAPTER, libc::c_int) -> BOOLEAN; + fn PacketSetReadTimeout(LPADAPTER, libc::c_int) -> BOOLEAN; +} + +/// Cached result of the one-time load attempt. `Err` holds a message rather +/// than an `io::Error` because the latter is not cloneable. +static PACKET_API: OnceLock> = OnceLock::new(); + +/// Candidate locations for Packet.dll, most specific first. +/// +/// Npcap's default install puts the DLLs in `System32\Npcap\`, which is *not* +/// on the DLL search path. Only a "WinPcap API-compatible mode" install (or a +/// legacy WinPcap) puts them where a bare name resolves. +fn packet_dll_candidates() -> Vec> { + let mut candidates = Vec::new(); + + // SAFETY: Passing a null buffer with length 0 asks only for the required + // size, which is the documented way to size the buffer. + let needed = unsafe { GetSystemDirectoryA(ptr::null_mut(), 0) } as usize; + if needed > 0 { + let mut buffer = vec![0u8; needed]; + // SAFETY: `buffer` is writable for `needed` bytes, which the call above + // reported as sufficient including the NUL terminator. + let written = unsafe { GetSystemDirectoryA(buffer.as_mut_ptr(), needed as u32) } as usize; + if written > 0 && written < needed { + buffer.truncate(written); + let mut path = buffer; + path.extend_from_slice(br"\Npcap\Packet.dll"); + path.push(0); + candidates.push(path); + } + } + + // Falls back to the standard search order for WinPcap-compatible installs. + candidates.push(b"Packet.dll\0".to_vec()); + candidates +} + +fn load_packet_api() -> Result { + let mut last_error = None; + for candidate in packet_dll_candidates() { + // SAFETY: `candidate` is a NUL-terminated path built above. + let module = unsafe { LoadLibraryA(candidate.as_ptr()) }; + if module.is_null() { + last_error = Some(io::Error::last_os_error()); + continue; + } + // The module is deliberately never freed: the resolved function + // pointers are cached for the lifetime of the process, so unloading + // would dangle them. One leaked module handle is the intended + // trade-off for a process-lifetime lazy load. + // + // SAFETY: `module` is a live handle just returned by LoadLibraryA. + return unsafe { PacketApi::resolve(module) }.map_err(|symbol| { + format!("Packet.dll is missing the required symbol `{symbol}`; it is likely too old") + }); + } + + let detail = last_error + .map(|error| error.to_string()) + .unwrap_or_else(|| "no candidate path could be built".to_string()); + Err(format!( + "failed to load Packet.dll; install Npcap (https://npcap.com) to use the datalink layer: {detail}" + )) +} + +/// Return the lazily loaded Packet.dll entry points. +/// +/// The load is attempted once per process; the outcome, success or failure, is +/// cached. Fails with [`io::ErrorKind::NotFound`] when Npcap is not installed. +pub fn packet_api() -> io::Result<&'static PacketApi> { + match PACKET_API.get_or_init(load_packet_api) { + Ok(api) => Ok(api), + Err(message) => Err(io::Error::new(io::ErrorKind::NotFound, message.clone())), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn npf_name_uses_the_device_namespace() { + // netdev reports the adapter GUID as `Interface::name` on Windows, + // which is exactly what the NPF device path expects. + assert_eq!( + to_npf_name("{B27C4B1D-0000-0000-0000-000000000000}"), + r"\Device\NPF_{B27C4B1D-0000-0000-0000-000000000000}" + ); + } + + #[test] + fn dll_candidates_prefer_the_npcap_directory() { + let candidates = packet_dll_candidates(); + assert!(!candidates.is_empty()); + for candidate in &candidates { + assert_eq!( + candidate.last(), + Some(&0), + "LoadLibraryA requires a NUL-terminated path" + ); + assert_eq!( + candidate.iter().filter(|byte| **byte == 0).count(), + 1, + "an interior NUL would truncate the path" + ); + } + // The bare name is the last resort, after the Npcap-specific path. + assert_eq!(candidates.last().unwrap(), b"Packet.dll\0"); + if candidates.len() > 1 { + let preferred = String::from_utf8_lossy(&candidates[0]).to_lowercase(); + assert!( + preferred.contains(r"\npcap\packet.dll"), + "unexpected preferred candidate: {preferred}" + ); + } + } + + #[test] + fn packet_api_load_is_cached_and_consistent() { + // Whether Npcap is installed depends on the machine, so assert on the + // properties that must hold either way rather than on success. + let first = packet_api(); + let second = packet_api(); + match (first, second) { + (Ok(a), Ok(b)) => { + assert!(std::ptr::eq(a, b), "the load result must be cached"); + } + (Err(a), Err(b)) => { + assert_eq!(a.kind(), io::ErrorKind::NotFound); + assert_eq!(a.to_string(), b.to_string()); + assert!( + a.to_string().contains("npcap.com"), + "the error should tell the user how to fix it: {a}" + ); + } + _ => panic!("packet_api() must not flip between success and failure"), + } + } } diff --git a/nex-datalink/src/wpcap.rs b/nex-datalink/src/wpcap.rs index fb0a6eb..e53fb87 100644 --- a/nex-datalink/src/wpcap.rs +++ b/nex-datalink/src/wpcap.rs @@ -11,18 +11,21 @@ use std::io; use std::mem; use std::slice; use std::sync::{Arc, Mutex}; +use std::time::Duration; struct WinPcapAdapter { + api: &'static windows::PacketApi, adapter: windows::LPADAPTER, operation_lock: Mutex<()>, } impl Drop for WinPcapAdapter { fn drop(&mut self) { + let api = self.api; // SAFETY: This is the last owning `Arc`, so no operation can still use // the non-null adapter handle. Npcap requires exactly one close. unsafe { - windows::PacketCloseAdapter(self.adapter); + (api.PacketCloseAdapter)(self.adapter); } } } @@ -36,15 +39,17 @@ unsafe impl Send for WinPcapAdapter {} unsafe impl Sync for WinPcapAdapter {} struct WinPcapPacket { + api: &'static windows::PacketApi, packet: windows::LPPACKET, } impl Drop for WinPcapPacket { fn drop(&mut self) { + let api = self.api; // SAFETY: `packet` was returned by PacketAllocatePacket, is owned by // this wrapper, and is freed exactly once. unsafe { - windows::PacketFreePacket(self.packet); + (api.PacketFreePacket)(self.packet); } } } @@ -61,16 +66,25 @@ pub(crate) struct Config { /// The size of buffer to use when reading packets. Defaults to 4096. pub read_buffer_size: usize, + + /// The read timeout applied via `PacketSetReadTimeout`. Defaults to None + /// (block until at least one packet arrives). + pub read_timeout: Option, + + /// Whether the adapter should capture in promiscuous mode. Defaults to true. + pub promiscuous: bool, } const DEFAULT_WRITE_BUFFER_SIZE: usize = 4096; const DEFAULT_READ_BUFFER_SIZE: usize = 65536; -impl<'a> From<&'a super::Config> for Config { +impl From<&super::Config> for Config { fn from(config: &super::Config) -> Config { Config { write_buffer_size: config.write_buffer_size, read_buffer_size: config.read_buffer_size, + read_timeout: config.read_timeout, + promiscuous: config.promiscuous, } } } @@ -80,18 +94,40 @@ impl Default for Config { Config { write_buffer_size: DEFAULT_WRITE_BUFFER_SIZE, read_buffer_size: DEFAULT_READ_BUFFER_SIZE, + read_timeout: None, + promiscuous: true, } } } +/// Convert a read timeout into the millisecond value `PacketSetReadTimeout` +/// expects. `None` maps to 0, which means "block until a packet arrives". +fn read_timeout_millis(timeout: Option) -> io::Result { + let Some(timeout) = timeout else { + return Ok(0); + }; + // A zero duration would otherwise be indistinguishable from "block + // forever", so round it up to the shortest bounded wait Npcap accepts. + let millis = cmp::max(timeout.as_millis(), 1); + libc::c_int::try_from(millis).map_err(|_| { + io::Error::new( + io::ErrorKind::InvalidInput, + "read_timeout exceeds the range Npcap accepts", + ) + }) +} + /// Create a datalink channel using the Npcap / WinPcap library. #[inline] pub(crate) fn channel(network_interface: &Interface, config: Config) -> io::Result { - let mut read_buffer = Vec::new(); - read_buffer.resize(config.read_buffer_size, 0u8); + // Resolve Packet.dll first: without Npcap there is nothing to configure. + let api = windows::packet_api()?; + + // Reject an out-of-range timeout before any OS handle is opened. + let read_timeout_ms = read_timeout_millis(config.read_timeout)?; - let mut write_buffer = Vec::new(); - write_buffer.resize(config.write_buffer_size, 0u8); + let mut read_buffer = vec![0u8; config.read_buffer_size]; + let mut write_buffer = vec![0u8; config.write_buffer_size]; // SAFETY: PacketOpenAdapter reads the temporary NUL-terminated interface // name during the call and returns an owned adapter handle. @@ -100,20 +136,20 @@ pub(crate) fn channel(network_interface: &Interface, config: Config) -> io::Resu let net_if_str = CString::new(npf_if_name.as_bytes()).map_err(|_| { io::Error::new(io::ErrorKind::InvalidInput, "interface name contains NUL") })?; - windows::PacketOpenAdapter(net_if_str.as_ptr() as *mut libc::c_char) + (api.PacketOpenAdapter)(net_if_str.as_ptr() as *mut libc::c_char) }; if adapter.is_null() { return Err(io::Error::last_os_error()); } let adapter = Arc::new(WinPcapAdapter { + api, adapter, operation_lock: Mutex::new(()), }); + let hw_filter = windows::hw_filter_for(config.promiscuous); // SAFETY: The adapter is open and the filter value is an Npcap constant. - let ret = unsafe { - windows::PacketSetHwFilter(adapter.adapter, windows::NDIS_PACKET_TYPE_PROMISCUOUS) - }; + let ret = unsafe { (api.PacketSetHwFilter)(adapter.adapter, hw_filter) }; if ret == 0 { return Err(io::Error::last_os_error()); } @@ -121,32 +157,41 @@ pub(crate) fn channel(network_interface: &Interface, config: Config) -> io::Resu // Set kernel buffer size // SAFETY: The adapter is open and PacketSetBuff retains no Rust pointers. let ret = - unsafe { windows::PacketSetBuff(adapter.adapter, config.read_buffer_size as libc::c_int) }; + unsafe { (api.PacketSetBuff)(adapter.adapter, config.read_buffer_size as libc::c_int) }; if ret == 0 { return Err(io::Error::last_os_error()); } // Immediate mode // SAFETY: The adapter is open and the integer threshold is valid. - let ret = unsafe { windows::PacketSetMinToCopy(adapter.adapter, 1) }; + let ret = unsafe { (api.PacketSetMinToCopy)(adapter.adapter, 1) }; + if ret == 0 { + return Err(io::Error::last_os_error()); + } + + // Bound how long `RawReceiver::next` waits. 0 means block until a packet + // arrives, which is the default when no timeout is configured. + // SAFETY: The adapter is open and the timeout is passed by value. + let ret = unsafe { (api.PacketSetReadTimeout)(adapter.adapter, read_timeout_ms) }; if ret == 0 { return Err(io::Error::last_os_error()); } // SAFETY: PacketAllocatePacket takes no arguments and returns an owned // packet pointer or null. - let read_packet = unsafe { windows::PacketAllocatePacket() }; + let read_packet = unsafe { (api.PacketAllocatePacket)() }; if read_packet.is_null() { return Err(io::Error::last_os_error()); } let read_packet = WinPcapPacket { + api, packet: read_packet, }; // SAFETY: The packet and backing vector are live; the vector cannot move or // resize while the packet wrapper exists in the receiver. unsafe { - windows::PacketInitPacket( + (api.PacketInitPacket)( read_packet.packet, read_buffer.as_mut_ptr() as windows::PVOID, config.read_buffer_size as windows::UINT, @@ -155,18 +200,19 @@ pub(crate) fn channel(network_interface: &Interface, config: Config) -> io::Resu // SAFETY: PacketAllocatePacket takes no arguments and returns an owned // packet pointer or null. - let write_packet = unsafe { windows::PacketAllocatePacket() }; + let write_packet = unsafe { (api.PacketAllocatePacket)() }; if write_packet.is_null() { return Err(io::Error::last_os_error()); } let write_packet = WinPcapPacket { + api, packet: write_packet, }; // SAFETY: The packet and backing vector are live; the vector cannot move or // resize while the packet wrapper exists in the sender. unsafe { - windows::PacketInitPacket( + (api.PacketInitPacket)( write_packet.packet, write_buffer.as_mut_ptr() as windows::PVOID, config.write_buffer_size as windows::UINT, @@ -181,34 +227,16 @@ pub(crate) fn channel(network_interface: &Interface, config: Config) -> io::Resu packet: write_packet, }); let receiver = Box::new(RawReceiverImpl { - adapter: adapter, + adapter, _read_buffer: read_buffer, packet: read_packet, // Enough room for minimally sized packets without reallocating packets: VecDeque::with_capacity(packet_capacity), + timeout_configured: config.read_timeout.is_some(), }); Ok(super::Channel::Ethernet(sender, receiver)) } -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn config_from_preserves_explicit_read_buffer() { - let cfg = crate::Config::default().with_read_buffer_size(4096); - let backend_cfg = Config::from(&cfg); - assert_eq!(backend_cfg.read_buffer_size, 4096); - } - - #[test] - fn config_default_uses_large_read_buffer() { - let cfg = Config::default(); - assert_eq!(cfg.write_buffer_size, DEFAULT_WRITE_BUFFER_SIZE); - assert_eq!(cfg.read_buffer_size, DEFAULT_READ_BUFFER_SIZE); - } -} - struct RawSenderImpl { adapter: Arc, _write_buffer: Vec, @@ -261,9 +289,11 @@ impl RawSender for RawSenderImpl { }; // SAFETY: The adapter operation is serialized, and both owned // handles remain live for the duration of the call. - let ret = unsafe { - windows::PacketSendPacket(self.adapter.adapter, self.packet.packet, 0) - }; + let api = self.adapter.api; + // SAFETY: The adapter operation is serialized, and both owned + // handles remain live for the duration of the call. + let ret = + unsafe { (api.PacketSendPacket)(self.adapter.adapter, self.packet.packet, 0) }; // SAFETY: The packet is still exclusively owned by `self`. unsafe { @@ -295,6 +325,9 @@ struct RawReceiverImpl { _read_buffer: Vec, packet: WinPcapPacket, packets: VecDeque<(usize, usize)>, + /// Whether a bounded read timeout was configured. When it was, an empty + /// receive means the timeout elapsed rather than "keep waiting". + timeout_configured: bool, } // SAFETY: The raw packet is uniquely owned by this receiver and adapter access @@ -312,14 +345,22 @@ impl RawReceiver for RawReceiverImpl { .map_err(|_| io::Error::other("Npcap adapter operation mutex poisoned"))?; // SAFETY: The adapter operation is serialized, and both owned // handles remain live for the duration of the call. - let ret = unsafe { - windows::PacketReceivePacket(self.adapter.adapter, self.packet.packet, 0) - }; + let api = self.adapter.api; + // SAFETY: The adapter operation is serialized, and both owned + // handles remain live for the duration of the call. + let ret = + unsafe { (api.PacketReceivePacket)(self.adapter.adapter, self.packet.packet, 0) }; let buflen = match ret { 0 => return Err(io::Error::last_os_error()), // SAFETY: A successful receive initialized the byte count. _ => unsafe { (*self.packet.packet).ulBytesReceived as usize }, }; + if buflen == 0 && self.timeout_configured { + return Err(io::Error::new( + io::ErrorKind::TimedOut, + "no packet received before the configured read timeout elapsed", + )); + } // SAFETY: The packet remains live and was initialized with a buffer // whose capacity is recorded in Length. let buffer_capacity = unsafe { (*self.packet.packet).Length as usize }; @@ -385,3 +426,69 @@ impl RawReceiver for RawReceiverImpl { Ok(slice) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn config_from_preserves_explicit_read_buffer() { + let cfg = crate::Config::default().with_read_buffer_size(4096); + let backend_cfg = Config::from(&cfg); + assert_eq!(backend_cfg.read_buffer_size, 4096); + } + + #[test] + fn config_default_uses_large_read_buffer() { + let cfg = Config::default(); + assert_eq!(cfg.write_buffer_size, DEFAULT_WRITE_BUFFER_SIZE); + assert_eq!(cfg.read_buffer_size, DEFAULT_READ_BUFFER_SIZE); + } + + #[test] + fn config_from_forwards_promiscuous_and_timeout() { + let cfg = crate::Config::default() + .with_promiscuous(false) + .with_read_timeout(Some(Duration::from_millis(250))); + let backend_cfg = Config::from(&cfg); + assert!(!backend_cfg.promiscuous); + assert_eq!(backend_cfg.read_timeout, Some(Duration::from_millis(250))); + } + + #[test] + fn read_timeout_millis_maps_none_to_blocking() { + assert_eq!(read_timeout_millis(None).unwrap(), 0); + } + + #[test] + fn read_timeout_millis_rounds_zero_up_to_bounded_wait() { + // 0 would mean "block forever" to Npcap, which is the opposite of what + // a zero-duration timeout requests. + assert_eq!(read_timeout_millis(Some(Duration::ZERO)).unwrap(), 1); + assert_eq!( + read_timeout_millis(Some(Duration::from_micros(1))).unwrap(), + 1 + ); + } + + #[test] + fn read_timeout_millis_converts_and_rejects_overflow() { + assert_eq!( + read_timeout_millis(Some(Duration::from_millis(1500))).unwrap(), + 1500 + ); + let err = read_timeout_millis(Some(Duration::from_secs(u64::MAX / 1000))).unwrap_err(); + assert_eq!(err.kind(), io::ErrorKind::InvalidInput); + } + + #[test] + fn hw_filter_tracks_promiscuous_setting() { + assert_eq!( + windows::hw_filter_for(true), + windows::NDIS_PACKET_TYPE_PROMISCUOUS + ); + let non_promiscuous = windows::hw_filter_for(false); + assert_eq!(non_promiscuous & windows::NDIS_PACKET_TYPE_PROMISCUOUS, 0); + assert_ne!(non_promiscuous & windows::NDIS_PACKET_TYPE_DIRECTED, 0); + } +} diff --git a/nex-socket/src/tcp/sync_impl.rs b/nex-socket/src/tcp/sync_impl.rs index a50b9f2..b5be656 100644 --- a/nex-socket/src/tcp/sync_impl.rs +++ b/nex-socket/src/tcp/sync_impl.rs @@ -264,8 +264,8 @@ impl TcpSocket { let ret = unsafe { getsockopt( sock, - SOL_SOCKET as i32, - SO_ERROR as i32, + SOL_SOCKET, + SO_ERROR, &mut so_error as *mut _ as *mut _, &mut optlen, ) diff --git a/nex-sys/src/lib.rs b/nex-sys/src/lib.rs index 2606ce7..3f25ef0 100644 --- a/nex-sys/src/lib.rs +++ b/nex-sys/src/lib.rs @@ -126,6 +126,7 @@ pub unsafe fn recv_from( #[cfg(test)] mod tests { + #[cfg(not(target_os = "windows"))] use super::*; #[cfg(not(target_os = "windows"))] From aae5fe8fd3981cc6775445242b7c104ae27cf390 Mon Sep 17 00:00:00 2001 From: shellrow <81893184+shellrow@users.noreply.github.com> Date: Mon, 20 Jul 2026 17:58:17 +0900 Subject: [PATCH 26/33] feat: load Npcap Packet.dll at run time --- nex-datalink/Cargo.toml | 2 ++ nex-datalink/src/lib.rs | 14 +++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/nex-datalink/Cargo.toml b/nex-datalink/Cargo.toml index d8ca8a8..32d0ec1 100644 --- a/nex-datalink/Cargo.toml +++ b/nex-datalink/Cargo.toml @@ -26,6 +26,8 @@ features = [ "Win32_Foundation", "Win32_Networking_WinSock", "Win32_System_IO", + "Win32_System_LibraryLoader", + "Win32_System_SystemInformation", "Win32_System_Threading", "Win32_System_WindowsProgramming", ] diff --git a/nex-datalink/src/lib.rs b/nex-datalink/src/lib.rs index 8f5f708..5db12a5 100644 --- a/nex-datalink/src/lib.rs +++ b/nex-datalink/src/lib.rs @@ -145,8 +145,15 @@ pub struct FanoutOption { /// - macOS and BSD use BPF devices. `bpf_fd_attempts` bounds `/dev/bpf*` /// discovery, read buffering follows the BPF buffer size, and timeouts bound /// readiness waits. -/// - Windows uses Npcap. Buffer sizes configure Npcap packet buffers. Timeout -/// fields and Linux/BPF-only options are not applied. +/// - Windows uses Npcap. Buffer sizes configure Npcap packet buffers, +/// `read_timeout` bounds each receive via `PacketSetReadTimeout` (an elapsed +/// timeout surfaces as [`io::ErrorKind::TimedOut`]), and `promiscuous` +/// selects the adapter hardware filter. `write_timeout` and Linux/BPF-only +/// options are not applied. +/// +/// Packet.dll is loaded lazily on the first channel open, so a missing Npcap +/// install surfaces as [`io::ErrorKind::NotFound`] from `channel()` rather +/// than preventing the process from starting. Building requires no Npcap SDK. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] #[non_exhaustive] pub struct Config { @@ -156,7 +163,8 @@ pub struct Config { /// The size of buffer to use when reading packets. Defaults to 4096. pub read_buffer_size: usize, - /// Linux/BPF/Netmap only: The read timeout. Defaults to None. + /// Linux/BPF/Netmap/Windows only: The read timeout. Defaults to None, + /// meaning receives block until a packet arrives. pub read_timeout: Option, /// Linux/BPF/Netmap only: The write timeout. Defaults to None. From 4381fe9f23b56fbad53d80cbee964006e6737bcb Mon Sep 17 00:00:00 2001 From: shellrow <81893184+shellrow@users.noreply.github.com> Date: Mon, 20 Jul 2026 17:58:58 +0900 Subject: [PATCH 27/33] ci: lint and build platform backends on every OS --- .github/actions/npcap-sdk/action.yml | 35 ++++++++++++++++++++++++++++ .github/workflows/rust.yml | 21 +++++++++++++++-- 2 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 .github/actions/npcap-sdk/action.yml diff --git a/.github/actions/npcap-sdk/action.yml b/.github/actions/npcap-sdk/action.yml new file mode 100644 index 0000000..a9149f2 --- /dev/null +++ b/.github/actions/npcap-sdk/action.yml @@ -0,0 +1,35 @@ +name: Install Npcap SDK +description: >- + Download the Npcap SDK and expose Packet.lib / wpcap.lib to the MSVC linker. + nex-datalink declares `#[link(name = "Packet")]`, so any Windows job that + links a test or example binary needs this. + +inputs: + version: + description: Npcap SDK version to install. + required: false + default: "1.13" + +runs: + using: composite + steps: + - name: Download and extract the Npcap SDK + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + $version = '${{ inputs.version }}' + $sdk = Join-Path $env:RUNNER_TEMP "npcap-sdk-$version" + $zip = Join-Path $env:RUNNER_TEMP "npcap-sdk-$version.zip" + Invoke-WebRequest -Uri "https://npcap.com/dist/npcap-sdk-$version.zip" -OutFile $zip + Expand-Archive -Path $zip -DestinationPath $sdk -Force + + # The SDK ships per-architecture import libraries; x64 is what the + # windows-latest runners target. + $lib = Join-Path $sdk 'Lib\x64' + if (-not (Test-Path (Join-Path $lib 'Packet.lib'))) { + throw "Packet.lib not found under $lib" + } + + # Append rather than overwrite so the MSVC toolchain's own LIB entries + # (added by the runner image) keep working. + "LIB=$lib;$env:LIB" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index c2d0ab2..b1b3fea 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -22,16 +22,26 @@ jobs: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 + # No Npcap SDK step here: nex-datalink resolves Packet.dll at run time, so + # the default feature set builds and links on a bare Windows runner. - name: Test libraries run: cargo test --workspace --lib - name: Test documentation run: cargo test --workspace --doc - name: Build all targets run: cargo build --workspace --all-targets + # The async datalink backend is not a default feature, so without this it + # is never compiled on any OS by the steps above. + - name: Build all targets with async + run: cargo build --workspace --all-targets --features nex/async lint: - name: Lint - runs-on: ubuntu-latest + name: Lint (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] steps: - uses: actions/checkout@v4 @@ -39,8 +49,15 @@ jobs: with: components: clippy, rustfmt - uses: Swatinem/rust-cache@v2 + # Only the optional `pcap` feature still needs the SDK: the `pcap` crate + # links wpcap.lib. nex's own Npcap access is resolved at run time. + - name: Install Npcap SDK + if: runner.os == 'Windows' + uses: ./.github/actions/npcap-sdk - name: Check formatting run: cargo fmt --all -- --check + # Runs per-OS: the platform backends are behind cfg, so a Linux-only + # Clippy run never sees the Windows or BPF code paths. - name: Run Clippy run: cargo clippy --workspace --all-targets --all-features -- -D warnings From a55ac194441684cea0babf791e74af239b6ba8fd Mon Sep 17 00:00:00 2001 From: shellrow Date: Sat, 25 Jul 2026 21:11:33 +0900 Subject: [PATCH 28/33] ci: streamline Rust checks --- .github/workflows/rust.yml | 100 +++++++------------------------------ 1 file changed, 19 insertions(+), 81 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index b1b3fea..8ce4a97 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -9,39 +9,15 @@ on: env: CARGO_TERM_COLOR: always -jobs: - test: - name: Test (${{ matrix.os }}) - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 - # No Npcap SDK step here: nex-datalink resolves Packet.dll at run time, so - # the default feature set builds and links on a bare Windows runner. - - name: Test libraries - run: cargo test --workspace --lib - - name: Test documentation - run: cargo test --workspace --doc - - name: Build all targets - run: cargo build --workspace --all-targets - # The async datalink backend is not a default feature, so without this it - # is never compiled on any OS by the steps above. - - name: Build all targets with async - run: cargo build --workspace --all-targets --features nex/async +concurrency: + group: rust-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true - lint: - name: Lint (${{ matrix.os }}) - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] +jobs: + checks: + name: Checks + runs-on: ubuntu-latest + timeout-minutes: 15 steps: - uses: actions/checkout@v4 @@ -49,63 +25,25 @@ jobs: with: components: clippy, rustfmt - uses: Swatinem/rust-cache@v2 - # Only the optional `pcap` feature still needs the SDK: the `pcap` crate - # links wpcap.lib. nex's own Npcap access is resolved at run time. - - name: Install Npcap SDK - if: runner.os == 'Windows' - uses: ./.github/actions/npcap-sdk - name: Check formatting run: cargo fmt --all -- --check - # Runs per-OS: the platform backends are behind cfg, so a Linux-only - # Clippy run never sees the Windows or BPF code paths. - name: Run Clippy - run: cargo clippy --workspace --all-targets --all-features -- -D warnings + run: cargo clippy --workspace --all-targets -- -D warnings + - name: Test libraries + run: cargo test --workspace --lib - features: - name: Features (${{ matrix.name }}) - runs-on: ubuntu-latest + platform-check: + name: Check (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 15 strategy: - fail-fast: false + fail-fast: true matrix: - include: - - name: default - command: cargo check -p nex - - name: no-default-features - command: cargo check -p nex --no-default-features - - name: serde - command: cargo check -p nex --no-default-features --features serde - - name: pcap - command: cargo check -p nex --no-default-features --features pcap - - name: async - command: cargo check -p nex --no-default-features --features async - - name: all-features - command: cargo check -p nex --all-features + os: [macos-latest, windows-latest] steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - - name: Check feature combination - run: ${{ matrix.command }} - - msrv: - name: MSRV (Rust 1.88.0) - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@1.88.0 - - uses: Swatinem/rust-cache@v2 - - name: Check workspace with MSRV - run: cargo check --workspace --all-features --locked - - supply-chain: - name: Supply chain - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - uses: EmbarkStudios/cargo-deny-action@v2 - with: - command: check - arguments: --all-features + - name: Check workspace + run: cargo check --workspace --all-targets From b8fb50de65c1b649c9dffb4e527b65394fc6f231 Mon Sep 17 00:00:00 2001 From: shellrow Date: Sun, 9 Aug 2026 16:00:50 +0900 Subject: [PATCH 29/33] chore: update dependencies --- Cargo.toml | 4 ++-- examples/async_icmp_socket.rs | 4 ++-- nex-core/src/interface.rs | 13 ++++++++++++- nex-packet/Cargo.toml | 2 +- nex-socket/Cargo.toml | 4 ++-- nex-socket/src/icmp/async_impl.rs | 2 +- nex-socket/src/icmp/sync_impl.rs | 2 +- nex-socket/src/tcp/async_impl.rs | 18 +++++++++--------- nex-socket/src/tcp/sync_impl.rs | 18 +++++++++--------- nex-socket/src/udp/async_impl.rs | 4 ++-- nex-socket/src/udp/sync_impl.rs | 12 ++++++------ 11 files changed, 47 insertions(+), 36 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9d19168..c852065 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,9 +23,9 @@ nex-sys = { version = "0.26.0", path = "nex-sys" } nex-socket = { version = "0.26.0", path = "nex-socket" } serde = { version = "1" } libc = "0.2" -netdev = { version = "0.41.0" } +netdev = { version = "0.46.0" } mac-addr = { version = "0.3.0" } ipnet = { version = "2.12" } bytes = "1" tokio = { version = "1" } -rand = "0.8" +rand = "0.10" diff --git a/examples/async_icmp_socket.rs b/examples/async_icmp_socket.rs index 36d0ef7..61e8d04 100644 --- a/examples/async_icmp_socket.rs +++ b/examples/async_icmp_socket.rs @@ -11,7 +11,7 @@ use nex_packet::icmp::{self, IcmpPacket, IcmpType}; use nex_packet::ipv4::Ipv4Packet; use nex_packet::packet::Packet; use nex_socket::icmp::{AsyncIcmpSocket, IcmpConfig, IcmpKind}; -use rand::{Rng, thread_rng}; +use rand::{RngExt, rng}; use std::collections::HashMap; use std::env; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; @@ -82,7 +82,7 @@ async fn main() -> std::io::Result<()> { let mut handles = Vec::new(); for i in 1u8..=254 { let addr = Ipv4Addr::new(parts[0], parts[1], parts[2], i); - let id: u16 = thread_rng().r#gen(); + let id: u16 = rng().random(); let seq: u16 = 1; let socket = socket.clone(); let replies = replies.clone(); diff --git a/nex-core/src/interface.rs b/nex-core/src/interface.rs index f196331..4b4a97a 100644 --- a/nex-core/src/interface.rs +++ b/nex-core/src/interface.rs @@ -184,6 +184,7 @@ pub enum InterfaceType { MultiRateSymmetricDsl, HighPerformanceSerialBus, Wman, + Wwan, Wwanpp, Wwanpp2, Bridge, @@ -222,6 +223,7 @@ impl InterfaceType { InterfaceType::MultiRateSymmetricDsl => String::from("Multi-Rate Symmetric DSL"), InterfaceType::HighPerformanceSerialBus => String::from("High Performance Serial Bus"), InterfaceType::Wman => String::from("WMAN"), + InterfaceType::Wwan => String::from("WWAN"), InterfaceType::Wwanpp => String::from("WWANPP"), InterfaceType::Wwanpp2 => String::from("WWANPP2"), InterfaceType::Bridge => String::from("Bridge"), @@ -276,6 +278,7 @@ impl From for InterfaceType { InterfaceType::HighPerformanceSerialBus } netdev::interface::types::InterfaceType::Wman => InterfaceType::Wman, + netdev::interface::types::InterfaceType::Wwan => InterfaceType::Wwan, netdev::interface::types::InterfaceType::Wwanpp => InterfaceType::Wwanpp, netdev::interface::types::InterfaceType::Wwanpp2 => InterfaceType::Wwanpp2, netdev::interface::types::InterfaceType::Bridge => InterfaceType::Bridge, @@ -665,7 +668,7 @@ fn lookup_interface(name: &str, index: u32) -> Option { #[cfg(test)] mod tests { - use super::InterfaceError; + use super::{InterfaceError, InterfaceType}; fn assert_error_contract() {} @@ -685,4 +688,12 @@ mod tests { "default gateway is unavailable: route table is empty" ); } + + #[test] + fn converts_wwan_interface_type() { + let interface_type = InterfaceType::from(netdev::interface::types::InterfaceType::Wwan); + + assert_eq!(interface_type, InterfaceType::Wwan); + assert_eq!(interface_type.name(), "WWAN"); + } } diff --git a/nex-packet/Cargo.toml b/nex-packet/Cargo.toml index 234a012..0967b19 100644 --- a/nex-packet/Cargo.toml +++ b/nex-packet/Cargo.toml @@ -21,7 +21,7 @@ default = [] serde = ["dep:serde", "nex-core/serde", "bytes/serde"] [dev-dependencies] -criterion = "0.5" +criterion = "0.8" proptest = "1" [[bench]] diff --git a/nex-socket/Cargo.toml b/nex-socket/Cargo.toml index 7142b93..aa3ffdd 100644 --- a/nex-socket/Cargo.toml +++ b/nex-socket/Cargo.toml @@ -14,12 +14,12 @@ license = "MIT" [dependencies] nex-core = { workspace = true } nex-packet = { workspace = true } -socket2 = { version = "0.5", features = ["all"] } +socket2 = { version = "0.6", features = ["all"] } tokio = { version = "1", features = ["time", "sync", "net", "rt"], optional = true } libc = { workspace = true } [target.'cfg(unix)'.dependencies] -nix = { version = "0.30", features = ["poll", "net", "uio"] } +nix = { version = "0.31", features = ["poll", "net", "uio"] } [target.'cfg(windows)'.dependencies.windows-sys] version = "0.61" diff --git a/nex-socket/src/icmp/async_impl.rs b/nex-socket/src/icmp/async_impl.rs index 339e36a..7c7fcdc 100644 --- a/nex-socket/src/icmp/async_impl.rs +++ b/nex-socket/src/icmp/async_impl.rs @@ -40,7 +40,7 @@ impl AsyncIcmpSocket { // Set socket options based on configuration if let Some(ttl) = config.ttl { - socket.set_ttl(ttl)?; + socket.set_ttl_v4(ttl)?; } if let Some(hoplimit) = config.hoplimit { socket.set_unicast_hops_v6(hoplimit)?; diff --git a/nex-socket/src/icmp/sync_impl.rs b/nex-socket/src/icmp/sync_impl.rs index 79772ef..2f453f3 100644 --- a/nex-socket/src/icmp/sync_impl.rs +++ b/nex-socket/src/icmp/sync_impl.rs @@ -38,7 +38,7 @@ impl IcmpSocket { // Set socket options based on configuration if let Some(ttl) = config.ttl { - socket.set_ttl(ttl)?; + socket.set_ttl_v4(ttl)?; } if let Some(hoplimit) = config.hoplimit { socket.set_unicast_hops_v6(hoplimit)?; diff --git a/nex-socket/src/tcp/async_impl.rs b/nex-socket/src/tcp/async_impl.rs index c79e03b..3ea2593 100644 --- a/nex-socket/src/tcp/async_impl.rs +++ b/nex-socket/src/tcp/async_impl.rs @@ -46,10 +46,10 @@ impl AsyncTcpSocket { socket.set_reuse_port(flag)?; } if let Some(flag) = config.nodelay { - socket.set_nodelay(flag)?; + socket.set_tcp_nodelay(flag)?; } if let Some(ttl) = config.ttl { - socket.set_ttl(ttl)?; + socket.set_ttl_v4(ttl)?; } if let Some(hoplimit) = config.hoplimit { socket.set_unicast_hops_v6(hoplimit)?; @@ -70,7 +70,7 @@ impl AsyncTcpSocket { socket.set_send_buffer_size(size)?; } if let Some(tos) = config.tos { - socket.set_tos(tos)?; + socket.set_tos_v4(tos)?; } #[cfg(any( target_os = "android", @@ -265,12 +265,12 @@ impl AsyncTcpSocket { /// Set no delay option for TCP. pub fn set_nodelay(&self, on: bool) -> io::Result<()> { - self.socket.set_nodelay(on) + self.socket.set_tcp_nodelay(on) } /// Get no delay option for TCP. pub fn nodelay(&self) -> io::Result { - self.socket.nodelay() + self.socket.tcp_nodelay() } /// Set linger option for the socket. @@ -280,12 +280,12 @@ impl AsyncTcpSocket { /// Set the time-to-live for IPv4 packets. pub fn set_ttl(&self, ttl: u32) -> io::Result<()> { - self.socket.set_ttl(ttl) + self.socket.set_ttl_v4(ttl) } /// Get the time-to-live for IPv4 packets. pub fn ttl(&self) -> io::Result { - self.socket.ttl() + self.socket.ttl_v4() } /// Set the hop limit for IPv6 packets. @@ -330,12 +330,12 @@ impl AsyncTcpSocket { /// Set IPv4 TOS / DSCP. pub fn set_tos(&self, tos: u32) -> io::Result<()> { - self.socket.set_tos(tos) + self.socket.set_tos_v4(tos) } /// Get IPv4 TOS / DSCP. pub fn tos(&self) -> io::Result { - self.socket.tos() + self.socket.tos_v4() } /// Set IPv6 traffic class where supported. diff --git a/nex-socket/src/tcp/sync_impl.rs b/nex-socket/src/tcp/sync_impl.rs index b5be656..b8540ca 100644 --- a/nex-socket/src/tcp/sync_impl.rs +++ b/nex-socket/src/tcp/sync_impl.rs @@ -53,13 +53,13 @@ impl TcpSocket { socket.set_reuse_port(flag)?; } if let Some(flag) = config.nodelay { - socket.set_nodelay(flag)?; + socket.set_tcp_nodelay(flag)?; } if let Some(dur) = config.linger { socket.set_linger(Some(dur))?; } if let Some(ttl) = config.ttl { - socket.set_ttl(ttl)?; + socket.set_ttl_v4(ttl)?; } if let Some(hoplimit) = config.hoplimit { socket.set_unicast_hops_v6(hoplimit)?; @@ -80,7 +80,7 @@ impl TcpSocket { socket.set_send_buffer_size(size)?; } if let Some(tos) = config.tos { - socket.set_tos(tos)?; + socket.set_tos_v4(tos)?; } #[cfg(any( target_os = "android", @@ -387,12 +387,12 @@ impl TcpSocket { /// Set the socket to not delay packets. pub fn set_nodelay(&self, on: bool) -> io::Result<()> { - self.socket.set_nodelay(on) + self.socket.set_tcp_nodelay(on) } /// Get the no delay option. pub fn nodelay(&self) -> io::Result { - self.socket.nodelay() + self.socket.tcp_nodelay() } /// Set the linger option for the socket. @@ -402,12 +402,12 @@ impl TcpSocket { /// Set the time-to-live for IPv4 packets. pub fn set_ttl(&self, ttl: u32) -> io::Result<()> { - self.socket.set_ttl(ttl) + self.socket.set_ttl_v4(ttl) } /// Get the time-to-live for IPv4 packets. pub fn ttl(&self) -> io::Result { - self.socket.ttl() + self.socket.ttl_v4() } /// Set the hop limit for IPv6 packets. @@ -452,12 +452,12 @@ impl TcpSocket { /// Set IPv4 TOS / DSCP. pub fn set_tos(&self, tos: u32) -> io::Result<()> { - self.socket.set_tos(tos) + self.socket.set_tos_v4(tos) } /// Get IPv4 TOS / DSCP. pub fn tos(&self) -> io::Result { - self.socket.tos() + self.socket.tos_v4() } /// Set IPv6 traffic class where supported. diff --git a/nex-socket/src/udp/async_impl.rs b/nex-socket/src/udp/async_impl.rs index 01c6440..2ef51b8 100644 --- a/nex-socket/src/udp/async_impl.rs +++ b/nex-socket/src/udp/async_impl.rs @@ -48,7 +48,7 @@ impl AsyncUdpSocket { socket.set_broadcast(flag)?; } if let Some(ttl) = config.ttl { - socket.set_ttl(ttl)?; + socket.set_ttl_v4(ttl)?; } if let Some(hoplimit) = config.hoplimit { socket.set_unicast_hops_v6(hoplimit)?; @@ -66,7 +66,7 @@ impl AsyncUdpSocket { socket.set_send_buffer_size(size)?; } if let Some(tos) = config.tos { - socket.set_tos(tos)?; + socket.set_tos_v4(tos)?; } #[cfg(any( target_os = "android", diff --git a/nex-socket/src/udp/sync_impl.rs b/nex-socket/src/udp/sync_impl.rs index d43a057..b451430 100644 --- a/nex-socket/src/udp/sync_impl.rs +++ b/nex-socket/src/udp/sync_impl.rs @@ -72,7 +72,7 @@ impl UdpSocket { socket.set_broadcast(flag)?; } if let Some(ttl) = config.ttl { - socket.set_ttl(ttl)?; + socket.set_ttl_v4(ttl)?; } if let Some(hoplimit) = config.hoplimit { socket.set_unicast_hops_v6(hoplimit)?; @@ -90,7 +90,7 @@ impl UdpSocket { socket.set_send_buffer_size(size)?; } if let Some(tos) = config.tos { - socket.set_tos(tos)?; + socket.set_tos_v4(tos)?; } #[cfg(any( target_os = "android", @@ -446,11 +446,11 @@ impl UdpSocket { } pub fn set_ttl(&self, ttl: u32) -> io::Result<()> { - self.socket.set_ttl(ttl) + self.socket.set_ttl_v4(ttl) } pub fn ttl(&self) -> io::Result { - self.socket.ttl() + self.socket.ttl_v4() } pub fn set_hoplimit(&self, hops: u32) -> io::Result<()> { @@ -550,11 +550,11 @@ impl UdpSocket { } pub fn set_tos(&self, tos: u32) -> io::Result<()> { - self.socket.set_tos(tos) + self.socket.set_tos_v4(tos) } pub fn tos(&self) -> io::Result { - self.socket.tos() + self.socket.tos_v4() } #[cfg(any( From 76dc7c52a8a793cf799d0cf2d87f81d0c5a38b24 Mon Sep 17 00:00:00 2001 From: shellrow Date: Sun, 9 Aug 2026 16:31:32 +0900 Subject: [PATCH 30/33] fix: accept macOS BPF headers without trailing padding --- nex-datalink/src/async_io/bpf.rs | 22 ++++------ nex-datalink/src/bindings/bpf.rs | 9 ++++ nex-datalink/src/bpf.rs | 70 ++++++++++++++++++++++++++------ 3 files changed, 75 insertions(+), 26 deletions(-) diff --git a/nex-datalink/src/async_io/bpf.rs b/nex-datalink/src/async_io/bpf.rs index 149235d..9bc978b 100644 --- a/nex-datalink/src/async_io/bpf.rs +++ b/nex-datalink/src/async_io/bpf.rs @@ -120,21 +120,15 @@ impl Stream for AsyncBpfSocketReceiver { }; let header_len = packet.bh_hdrlen as usize; let captured_len = packet.bh_caplen as usize; - let Some(record_len) = header_len.checked_add(captured_len) else { - return Poll::Ready(Some(Err(io::Error::new( - io::ErrorKind::InvalidData, - "BPF record length overflow", - )))); + let record_len = match crate::bpf::validate_record_lengths( + header_len, + captured_len, + remaining, + header_size, + ) { + Ok(record_len) => record_len, + Err(err) => return Poll::Ready(Some(Err(err))), }; - if header_len < mem::size_of::() - || captured_len < header_size - || record_len > remaining - { - return Poll::Ready(Some(Err(io::Error::new( - io::ErrorKind::InvalidData, - "invalid BPF record lengths", - )))); - } me.packets.push_back(( cursor + header_len + header_size, captured_len - header_size, diff --git a/nex-datalink/src/bindings/bpf.rs b/nex-datalink/src/bindings/bpf.rs index dc53470..145907b 100644 --- a/nex-datalink/src/bindings/bpf.rs +++ b/nex-datalink/src/bindings/bpf.rs @@ -120,6 +120,15 @@ pub struct bpf_hdr { pub bh_hdrlen: libc::c_ushort, } +/// Number of bytes required to contain every defined field in `bpf_hdr`. +/// +/// This can be smaller than `size_of::()` because the C ABI may add +/// trailing padding to the Rust representation. In particular, macOS reports +/// an 18-byte BPF header on 64-bit targets even though the structure occupies +/// 20 bytes when stored as a standalone value. +pub const BPF_HDR_FIELD_LEN: usize = + std::mem::offset_of!(bpf_hdr, bh_hdrlen) + std::mem::size_of::(); + #[repr(C)] pub struct timeval32 { pub tv_sec: i32, diff --git a/nex-datalink/src/bpf.rs b/nex-datalink/src/bpf.rs index 0e4f066..701b457 100644 --- a/nex-datalink/src/bpf.rs +++ b/nex-datalink/src/bpf.rs @@ -63,6 +63,27 @@ impl Default for Config { } } +pub(crate) fn validate_record_lengths( + header_len: usize, + captured_len: usize, + remaining: usize, + minimum_payload_len: usize, +) -> io::Result { + let record_len = header_len + .checked_add(captured_len) + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "BPF record length overflow"))?; + if header_len < bpf::BPF_HDR_FIELD_LEN + || captured_len < minimum_payload_len + || record_len > remaining + { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "invalid BPF record lengths", + )); + } + Ok(record_len) +} + /// Create a datalink channel using the /dev/bpf device // NOTE buffer must be word aligned. #[inline] @@ -432,18 +453,8 @@ impl RawReceiver for RawReceiverImpl { }; let header_len = packet.bh_hdrlen as usize; let captured_len = packet.bh_caplen as usize; - let record_len = header_len.checked_add(captured_len).ok_or_else(|| { - io::Error::new(io::ErrorKind::InvalidData, "BPF record length overflow") - })?; - if header_len < mem::size_of::() - || captured_len < header_size - || record_len > remaining - { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "invalid BPF record lengths", - )); - } + let record_len = + validate_record_lengths(header_len, captured_len, remaining, header_size)?; self.packets.push_back(( cursor + header_len + header_size, captured_len - header_size, @@ -481,3 +492,38 @@ impl RawReceiver for RawReceiverImpl { Ok(&self.read_buffer[start..start + len]) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn accepts_header_without_repr_c_trailing_padding() { + let header_len = bpf::BPF_HDR_FIELD_LEN; + let captured_len = 86; + + assert_eq!( + validate_record_lengths(header_len, captured_len, header_len + captured_len, 0,) + .unwrap(), + header_len + captured_len + ); + } + + #[test] + fn rejects_header_that_cannot_contain_all_fields() { + let header_len = bpf::BPF_HDR_FIELD_LEN - 1; + let err = validate_record_lengths(header_len, 86, header_len + 86, 0).unwrap_err(); + + assert_eq!(err.kind(), io::ErrorKind::InvalidData); + } + + #[cfg(all( + any(target_os = "macos", target_os = "ios"), + target_pointer_width = "64" + ))] + #[test] + fn apple_bpf_header_has_two_bytes_of_trailing_padding() { + assert_eq!(bpf::BPF_HDR_FIELD_LEN, 18); + assert_eq!(mem::size_of::(), 20); + } +} From 0a2fec117736a4e3841aac003aa48e7a1ee25349 Mon Sep 17 00:00:00 2001 From: shellrow Date: Sun, 9 Aug 2026 16:43:13 +0900 Subject: [PATCH 31/33] fix: restore iOS and NetBSD socket compatibility --- .github/workflows/rust.yml | 42 +++++++++++++++++++++++++++++--- nex-socket/src/lib.rs | 38 +++++++++++++++++++++++++++++ nex-socket/src/tcp/async_impl.rs | 30 +++-------------------- nex-socket/src/tcp/sync_impl.rs | 30 +++-------------------- nex-socket/src/udp/async_impl.rs | 18 +------------- nex-socket/src/udp/sync_impl.rs | 39 +++++++++-------------------- 6 files changed, 94 insertions(+), 103 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 8ce4a97..1de0518 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -28,9 +28,21 @@ jobs: - name: Check formatting run: cargo fmt --all -- --check - name: Run Clippy - run: cargo clippy --workspace --all-targets -- -D warnings - - name: Test libraries - run: cargo test --workspace --lib + run: cargo clippy --workspace --all-targets --all-features -- -D warnings + - name: Run tests + run: cargo test --workspace --all-features + + msrv: + name: MSRV + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@1.88.0 + - uses: Swatinem/rust-cache@v2 + - name: Check workspace with Rust 1.88 + run: cargo check --workspace --all-targets --all-features platform-check: name: Check (${{ matrix.os }}) @@ -46,4 +58,26 @@ jobs: - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - name: Check workspace - run: cargo check --workspace --all-targets + run: cargo check --workspace --all-targets --all-features + + cross-target-check: + name: Check (${{ matrix.target }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 15 + strategy: + fail-fast: true + matrix: + include: + - os: macos-latest + target: aarch64-apple-ios + - os: ubuntu-latest + target: x86_64-unknown-netbsd + + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + - uses: Swatinem/rust-cache@v2 + - name: Check cross-platform library + run: cargo check -p nex --lib --features async,serde --target ${{ matrix.target }} diff --git a/nex-socket/src/lib.rs b/nex-socket/src/lib.rs index 91cc60b..6e20647 100644 --- a/nex-socket/src/lib.rs +++ b/nex-socket/src/lib.rs @@ -17,8 +17,46 @@ pub mod icmp; pub mod tcp; pub mod udp; +use std::io; use std::net::{IpAddr, SocketAddr}; +#[cfg(any( + target_os = "android", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "fuchsia", + target_os = "linux", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd" +))] +pub(crate) fn apply_tclass_v6(socket: &socket2::Socket, tclass: Option) -> io::Result<()> { + if let Some(tclass) = tclass { + socket.set_tclass_v6(tclass)?; + } + Ok(()) +} + +#[cfg(not(any( + target_os = "android", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "fuchsia", + target_os = "linux", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd" +)))] +pub(crate) fn apply_tclass_v6(_socket: &socket2::Socket, tclass: Option) -> io::Result<()> { + if tclass.is_some() { + return Err(io::Error::new( + io::ErrorKind::Unsupported, + "IPv6 traffic class is not supported on this platform", + )); + } + Ok(()) +} + /// Represents the socket address family (IPv4 or IPv6) #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[non_exhaustive] diff --git a/nex-socket/src/tcp/async_impl.rs b/nex-socket/src/tcp/async_impl.rs index 3ea2593..9e3a0fb 100644 --- a/nex-socket/src/tcp/async_impl.rs +++ b/nex-socket/src/tcp/async_impl.rs @@ -72,23 +72,7 @@ impl AsyncTcpSocket { if let Some(tos) = config.tos { socket.set_tos_v4(tos)?; } - #[cfg(any( - target_os = "android", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "fuchsia", - target_os = "ios", - target_os = "linux", - target_os = "macos", - target_os = "netbsd", - target_os = "openbsd", - target_os = "tvos", - target_os = "visionos", - target_os = "watchos" - ))] - if let Some(tclass) = config.tclass_v6 { - socket.set_tclass_v6(tclass)?; - } + crate::apply_tclass_v6(&socket, config.tclass_v6)?; if let Some(only_v6) = config.only_v6 { socket.set_only_v6(only_v6)?; } @@ -344,14 +328,10 @@ impl AsyncTcpSocket { target_os = "dragonfly", target_os = "freebsd", target_os = "fuchsia", - target_os = "ios", target_os = "linux", target_os = "macos", target_os = "netbsd", - target_os = "openbsd", - target_os = "tvos", - target_os = "visionos", - target_os = "watchos" + target_os = "openbsd" ))] pub fn set_tclass_v6(&self, tclass: u32) -> io::Result<()> { self.socket.set_tclass_v6(tclass) @@ -363,14 +343,10 @@ impl AsyncTcpSocket { target_os = "dragonfly", target_os = "freebsd", target_os = "fuchsia", - target_os = "ios", target_os = "linux", target_os = "macos", target_os = "netbsd", - target_os = "openbsd", - target_os = "tvos", - target_os = "visionos", - target_os = "watchos" + target_os = "openbsd" ))] pub fn tclass_v6(&self) -> io::Result { self.socket.tclass_v6() diff --git a/nex-socket/src/tcp/sync_impl.rs b/nex-socket/src/tcp/sync_impl.rs index b8540ca..7b44e86 100644 --- a/nex-socket/src/tcp/sync_impl.rs +++ b/nex-socket/src/tcp/sync_impl.rs @@ -82,23 +82,7 @@ impl TcpSocket { if let Some(tos) = config.tos { socket.set_tos_v4(tos)?; } - #[cfg(any( - target_os = "android", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "fuchsia", - target_os = "ios", - target_os = "linux", - target_os = "macos", - target_os = "netbsd", - target_os = "openbsd", - target_os = "tvos", - target_os = "visionos", - target_os = "watchos" - ))] - if let Some(tclass) = config.tclass_v6 { - socket.set_tclass_v6(tclass)?; - } + crate::apply_tclass_v6(&socket, config.tclass_v6)?; if let Some(only_v6) = config.only_v6 { socket.set_only_v6(only_v6)?; } @@ -466,14 +450,10 @@ impl TcpSocket { target_os = "dragonfly", target_os = "freebsd", target_os = "fuchsia", - target_os = "ios", target_os = "linux", target_os = "macos", target_os = "netbsd", - target_os = "openbsd", - target_os = "tvos", - target_os = "visionos", - target_os = "watchos" + target_os = "openbsd" ))] pub fn set_tclass_v6(&self, tclass: u32) -> io::Result<()> { self.socket.set_tclass_v6(tclass) @@ -485,14 +465,10 @@ impl TcpSocket { target_os = "dragonfly", target_os = "freebsd", target_os = "fuchsia", - target_os = "ios", target_os = "linux", target_os = "macos", target_os = "netbsd", - target_os = "openbsd", - target_os = "tvos", - target_os = "visionos", - target_os = "watchos" + target_os = "openbsd" ))] pub fn tclass_v6(&self) -> io::Result { self.socket.tclass_v6() diff --git a/nex-socket/src/udp/async_impl.rs b/nex-socket/src/udp/async_impl.rs index 2ef51b8..820de86 100644 --- a/nex-socket/src/udp/async_impl.rs +++ b/nex-socket/src/udp/async_impl.rs @@ -68,23 +68,7 @@ impl AsyncUdpSocket { if let Some(tos) = config.tos { socket.set_tos_v4(tos)?; } - #[cfg(any( - target_os = "android", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "fuchsia", - target_os = "ios", - target_os = "linux", - target_os = "macos", - target_os = "netbsd", - target_os = "openbsd", - target_os = "tvos", - target_os = "visionos", - target_os = "watchos" - ))] - if let Some(tclass) = config.tclass_v6 { - socket.set_tclass_v6(tclass)?; - } + crate::apply_tclass_v6(&socket, config.tclass_v6)?; if let Some(only_v6) = config.only_v6 { socket.set_only_v6(only_v6)?; } diff --git a/nex-socket/src/udp/sync_impl.rs b/nex-socket/src/udp/sync_impl.rs index b451430..f28f5e0 100644 --- a/nex-socket/src/udp/sync_impl.rs +++ b/nex-socket/src/udp/sync_impl.rs @@ -92,23 +92,7 @@ impl UdpSocket { if let Some(tos) = config.tos { socket.set_tos_v4(tos)?; } - #[cfg(any( - target_os = "android", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "fuchsia", - target_os = "ios", - target_os = "linux", - target_os = "macos", - target_os = "netbsd", - target_os = "openbsd", - target_os = "tvos", - target_os = "visionos", - target_os = "watchos" - ))] - if let Some(tclass) = config.tclass_v6 { - socket.set_tclass_v6(tclass)?; - } + crate::apply_tclass_v6(&socket, config.tclass_v6)?; if let Some(only_v6) = config.only_v6 { socket.set_only_v6(only_v6)?; } @@ -206,7 +190,14 @@ impl UdpSocket { IpAddr::V4(v4) => Some(v4), IpAddr::V6(_) => None, }) { - pktinfo.ipi_spec_dst.s_addr = u32::from_ne_bytes(src.octets()); + #[cfg(target_os = "netbsd")] + { + pktinfo.ipi_addr.s_addr = u32::from_ne_bytes(src.octets()); + } + #[cfg(not(target_os = "netbsd"))] + { + pktinfo.ipi_spec_dst.s_addr = u32::from_ne_bytes(src.octets()); + } } if let Some(ifindex) = meta.interface_index { pktinfo.ipi_ifindex = ifindex.try_into().map_err(|_| { @@ -562,14 +553,10 @@ impl UdpSocket { target_os = "dragonfly", target_os = "freebsd", target_os = "fuchsia", - target_os = "ios", target_os = "linux", target_os = "macos", target_os = "netbsd", - target_os = "openbsd", - target_os = "tvos", - target_os = "visionos", - target_os = "watchos" + target_os = "openbsd" ))] pub fn set_tclass_v6(&self, tclass: u32) -> io::Result<()> { self.socket.set_tclass_v6(tclass) @@ -580,14 +567,10 @@ impl UdpSocket { target_os = "dragonfly", target_os = "freebsd", target_os = "fuchsia", - target_os = "ios", target_os = "linux", target_os = "macos", target_os = "netbsd", - target_os = "openbsd", - target_os = "tvos", - target_os = "visionos", - target_os = "watchos" + target_os = "openbsd" ))] pub fn tclass_v6(&self) -> io::Result { self.socket.tclass_v6() From 9012082163b8a297b06e1013fd25b8453d91164d Mon Sep 17 00:00:00 2001 From: shellrow Date: Sun, 9 Aug 2026 17:21:55 +0900 Subject: [PATCH 32/33] chore: bump version to 0.27.0 --- Cargo.toml | 12 ++++++------ README.md | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c852065..79fda02 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,17 +10,17 @@ members = [ ] [workspace.package] -version = "0.26.0" +version = "0.27.0" edition = "2024" rust-version = "1.88" authors = ["shellrow "] [workspace.dependencies] -nex-core = { version = "0.26.0", path = "nex-core" } -nex-datalink = { version = "0.26.0", path = "nex-datalink" } -nex-packet = { version = "0.26.0", path = "nex-packet" } -nex-sys = { version = "0.26.0", path = "nex-sys" } -nex-socket = { version = "0.26.0", path = "nex-socket" } +nex-core = { version = "0.27.0", path = "nex-core" } +nex-datalink = { version = "0.27.0", path = "nex-datalink" } +nex-packet = { version = "0.27.0", path = "nex-packet" } +nex-sys = { version = "0.27.0", path = "nex-sys" } +nex-socket = { version = "0.27.0", path = "nex-socket" } serde = { version = "1" } libc = "0.2" netdev = { version = "0.46.0" } diff --git a/README.md b/README.md index 916df63..3e0e712 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ To use `nex`, add it as a dependency in your `Cargo.toml`: ```toml [dependencies] -nex = "0.26" +nex = "0.27" ``` ## Using Specific Sub-crates @@ -56,7 +56,7 @@ To use asynchronous APIs through the facade: ```toml [dependencies] -nex = { version = "0.26", features = ["async"] } +nex = { version = "0.27", features = ["async"] } ``` ## Privileges From dbf10d00db737b314d8c82d3a07050e7d5bb0ae8 Mon Sep 17 00:00:00 2001 From: shellrow Date: Sun, 9 Aug 2026 17:32:17 +0900 Subject: [PATCH 33/33] chore: delete unused TODO.md --- TODO.md | 313 -------------------------------------------------------- 1 file changed, 313 deletions(-) delete mode 100644 TODO.md diff --git a/TODO.md b/TODO.md deleted file mode 100644 index a184ecf..0000000 --- a/TODO.md +++ /dev/null @@ -1,313 +0,0 @@ -# nex — Road to v1.0.0 - -A backlog for taking `nex` to a v1.0.0. - ---- - -## 0. Assessment (2026-07) - -What blocks a credible v1.0 (the rest of this document): - -- **API inconsistency is the #1 problem.** Parsing entry points have proliferated - into 8+ shapes with overlapping semantics. This must be unified before 1.0 - freezes the surface. -- **Builders cannot fail.** `build()`/`to_bytes()` never return `Result`; malformed - or over-length input produces silently wrong bytes. -- **Dependency & feature hygiene is weak.** `nex-socket` forces `tokio` on sync - users; `nex-packet` pulls all of `rand` for one line. -- **CI is effectively unverified.** Only `cargo build` on 3 OSes — no tests, no - clippy, no fmt, no feature matrix, no MSRV, no supply-chain checks. -- **Unsafe boundary is undocumented.** 9 `unsafe impl Send/Sync` and ~141 unsafe - sites in `nex-datalink` with no safety rationale comments. -- **No release scaffolding.** No CHANGELOG,MSRV, or semver/API-snapshot tracking. - ---- - -## P0 — Blockers for a Stable v1.0 Surface - -### P0.1 Unify the packet parsing API - -Today the surface is inconsistent and confusing. Observed across `nex-packet`: - -- `Packet` trait: `from_buf(&[u8]) -> Option`, `from_bytes(Bytes) -> Option`. -- Per-type: `try_from_buf -> Result<_, ParseError>`, `try_from_bytes`, - `try_from_buf_strict`, `try_from_bytes_strict`, `from_buf_strict -> Option`, - `from_bytes_strict -> Option`. -- DNS: `from_buf_mut(&mut &[u8]) -> Option`, `from_bytes(&[u8]) -> Result<_, Utf8Error>`. -- Ethernet: `from_bytes(Bytes) -> Result` ← String error. - -Actions: - -- [x] Define ONE canonical parsing contract and document it in `parse.rs`: - - [x] `try_from_bytes(Bytes) -> Result` — owned, zero-copy view. - - [x] `try_from_buf(&[u8]) -> Result` — borrowed. - - [x] A single explicit "strict payload-length" opt-in (e.g. a `Strictness`/ - `ParseOption` arg) instead of `*_strict` name-doubling every method. -- [x] Remove `EthernetHeader::from_bytes -> Result<_, String>`; replace with the - canonical `ParseError` form (`nex-packet/src/ethernet.rs:165`). -- [x] Fold `DnsName::from_bytes -> Result<_, Utf8Error>` into `ParseError::InvalidUtf8` - so DNS matches every other module (`nex-packet/src/dns.rs:1216`). -- [x] Decide the fate of `from_* -> Option` on the `Packet` trait - (`nex-packet/src/packet.rs:9-12`): either make the trait `try_from_*`-based - with `ParseError`, or keep `Option` only as a thin infallible-intent shim. -- [x] Provide `#[deprecated]` aliases for one release wherever a public name changes. -- [x] Write a doc table mapping every old parsing fn → its v1.0 replacement. - -Acceptance: every protocol module exposes the *same* small set of parse fns with -the *same* signatures and error type; `grep -r "Result<.*String>"` returns nothing -in `nex-packet`. - -### P0.2 Make builders fallible and validating - -Every builder currently returns infallibly (`nex-packet/src/builder/*.rs`): -`build(self) -> Packet` and `to_bytes(self) -> Bytes`, with no bounds checks. - -- [x] Change builder finalizers to `build(self) -> Result` - (or a `try_build`) wherever a field can exceed protocol limits: - - [x] IPv4/IPv6 total length, IHL/options length, payload length. - - [x] TCP data offset / options length; UDP length; ICMP/ICMPv6 sizing. - - [x] DHCP options length; NDP option length; ARP fixed sizing. -- [x] Validate checksum prerequisites (pseudo-header context present) before emit. -- [x] Introduce a typed `BuildError` (see P0.4) with actionable variants. -- [x] Keep an infallible fast path only where inputs are provably in-range (e.g. - fixed-size headers), and document why. - -Acceptance: constructing an over-length packet returns `Err`, never wrong bytes; -property tests (P1.5) confirm build→parse round-trips for valid inputs only. - -### P0.3 Freeze the public API surface (semver contract) - -- [x] Audit every `pub` item per crate; mark internal helpers `pub(crate)`. -- [x] `nex-core::bitfield` (`pub type u1 = u8 …`): make private or move behind a - documented `#[doc(hidden)]` implementation-detail boundary — these aliases - should not be part of the stable contract. -- [x] Decide `nex-sys`'s status: it is a low-level internal crate — either mark it - clearly "internal, no semver guarantees" in its docs or make its surface - `pub(crate)`-equivalent via `#[doc(hidden)]`. -- [x] Normalize accessor naming: replace `get_*` (73 occurrences in `nex-packet`) - with idiomatic Rust names; keep `#[deprecated]` aliases for one release. -- [x] Audit public struct fields (e.g. `datalink::Config`, packet headers): decide - field-access vs accessor policy; document undocumented `pub` fields - (`Config.linux_fanout`, `Config.promiscuous` lack doc comments). -- [x] Apply `#[non_exhaustive]` to all enums/structs expecting future variants - (protocol number enums, `Channel`, error types, config structs). -- [x] Audit `pub const` protocol constants for naming + semver stability. -- [x] Document `new_unchecked` invariants (`GenericMutablePacket::new_unchecked`, - any `*_unchecked`) with `# Safety` sections. -Acceptance: a documented, intentional surface with no accidental `pub` internals. - -### P0.4 A real error model (kill `String` errors) - -- [x] `nex-core::interface`: replace `Result<_, String>` with a typed error - (`nex-core/src/interface.rs:359,592,597` — `default`, `get_default_interface`, - `get_default_gateway`). -- [x] Add `nex-packet::BuildError` for builders (P0.2). -- [x] Keep `io::Error` only at raw syscall/socket boundaries; convert to typed - errors where the library adds semantic meaning (datalink/socket config). -- [x] Ensure `ParseError` carries enough context for fuzz triage and diagnostics - (it already has `context`; verify every construction site sets a useful one). -- [x] All public error types implement `std::error::Error + Send + Sync + 'static`. -- [x] No `panic!`/`unreachable!`/`unwrap` reachable from public APIs on malformed - input. (Current `unreachable!`/`panic!` sites are test-only.) - -Acceptance: no `-> Result<_, String>` in any public API; a documented error taxonomy. - -### P0.5 Feature flags & dependency diet - -- [x] `nex-socket`: gate async behind an `async` (tokio) feature. Sync users must - not pull `tokio` (`nex-socket/Cargo.toml` currently hard-depends on tokio - with `time,sync,net,rt`). Split sync/async cleanly. -- [x] `nex-packet`: `rand` is a full dependency used in exactly one place - (`builder/ipv4.rs:33`, random IP identification). Either feature-gate it, - replace with a lightweight PRNG, or let the caller supply the id. Do not force - `rand` on every packet-parsing consumer. -- [x] `nex-datalink`: consider gating `async_io` + `futures-core` behind an `async` - feature; sniffers that only send/recv synchronously shouldn't compile it. -- [x] Define default features intentionally and document each (`nex-core` defaults - to `gateway` — confirm that's the right default). -- [x] Wire facade features through: `nex` exposes `pcap`, `serde`; add `async` and - any new sub-crate features so the facade stays a faithful superset. -- [x] Verify all feature combos build: default, `--no-default-features`, `serde`, - `pcap`, `async`, `--all-features` (enforce in CI, P0.6). - -Acceptance: `cargo tree` for a sync-only `nex-socket` user contains no `tokio`; -`nex-packet` with default features contains no `rand` unless opted in. - -### P0.6 Quality gates in CI (the current CI proves almost nothing) - -Current `.github/workflows/rust.yml` runs only `cargo build` on Linux/macOS/Windows. - -- [x] Replace with a matrix that runs, per OS: - - [x] `cargo test --workspace --lib` - - [x] `cargo test --workspace --doc` - - [x] `cargo build --workspace --all-targets` (examples included) -- [x] Lint job: `cargo fmt --all -- --check` + - `cargo clippy --workspace --all-targets --all-features -- -D warnings`. -- [x] Feature-combo job: default / `--no-default-features` / `serde` / `pcap` / - `async` / `--all-features`. -- [x] MSRV job: pin and verify an MSRV (edition 2024 ⇒ MSRV ≥ 1.85; declare - `rust-version` in every `Cargo.toml` and test it). -- [x] Supply chain: `cargo deny check` (licenses, advisories, bans, sources) + - add `deny.toml`. -- [x] Remove `#![deny(warnings)]` from `nex-datalink/src/lib.rs` (line 3): it makes - builds break on future compilers/new lints. Enforce warnings in CI via - `RUSTFLAGS=-Dwarnings`, not in source. -- [x] A documented, manual privileged-test matrix for raw sockets / datalink I/O - that CI cannot run (see P1.2 / P1.3). - -Acceptance: a red/green CI that actually gates merges on tests, lints, features, -MSRV, and supply chain across all three OSes. - -### P0.7 Unsafe & OS-resource safety hardening - -- [x] Add a `# Safety` comment to every `unsafe` block and every `unsafe impl`: - - [x] Complete the `nex-sys` audit, including public `unsafe fn` contracts. - - [x] Complete the `nex-datalink` audit (~141 sites). -- [x] Justify or remove the 9 `unsafe impl Send/Sync` in `nex-datalink` - (`wpcap.rs`, `async_io/wpcap.rs`): document what makes the raw Npcap handles - actually thread-safe, or wrap them so the impl is sound. -- [x] Ensure every OS handle (fd, BPF device, Npcap adapter, packet buffer) is - owned by an RAII type that closes on *all* error paths. - - [x] Make `nex-sys::FileDesc` an explicit owned descriptor with a private raw - field, an unsafe ownership-transfer constructor, and a drop test. - - [x] Audit that every fd flows through an owning wrapper and that no early-return - leaks a half-opened resource. -- [x] Prefer typed wrappers over raw integer/pointer handles at module boundaries. -- [x] Add error-path tests that open then fail to confirm no leak/double-close. -Acceptance: every unsafe site has a rationale; a reviewer can audit the FFI -boundary from comments alone. - ---- - -## P1 — Correctness, Performance, Robustness - -### P1.1 Packet layer architecture - -- [x] Cleanly separate the four packet categories and document which is which: - read-only borrowed views, mutable borrowed views, owned decoded packets, builders. -- [x] Fix `GenericMutablePacket` re-parsing: `header()`, `header_mut()`, `payload()`, - `payload_mut()` each call `lengths()` which re-runs `P::from_buf` on every - access (`nex-packet/src/packet.rs:124-165`). Cache lengths on construction or - on first use. -- [x] Define clear freeze/commit semantics for mutable views (`freeze()` currently - re-parses via `from_buf`); document cost and invalidation rules. -- [x] Consolidate/trim the `Packet` trait: `to_bytes_mut`/`header_mut`/`payload_mut` - allocate fresh `BytesMut` each call — confirm these belong on the trait or move - to explicit conversion helpers. -- [x] Decide whether generated bitfield accessors are public API or hidden detail - (ties to P0.3 `bitfield`). -- [x] Extension-header / options parsing: audit IPv4 options, IPv6 ext headers, TCP - options, DNS compression, DHCP options for strict-length correctness and - truncation handling. - -### P1.2 Datalink backends - -- [x] Document per-platform behavior of the stable `channel()` / async channel API: - blocking vs timeout vs nonblocking vs async semantics. -- [x] Linux packet socket: verify Layer2/Layer3 modes, promiscuous, fanout, buffer - sizing, timeout behavior. -- [x] BPF (macOS/BSD): device selection, header-complete mode, buffer sizing, - poll/read iteration, `bpf_fd_attempts` behavior. -- [x] Windows/Npcap: adapter name conversion, packet alloc/cleanup, send/recv thread - safety (ties to the `unsafe impl` audit in P0.7). -- [x] Confirm backend submodules stay private (already done per `API_SURFACE`); - keep only the generic API public. -- [x] `RawSender::send`/`build_and_send` return `Option>` — the - `Option` (capacity) vs `Result` (I/O) split is subtle; document it precisely - or model it as one typed error. -- [x] Add manually-enabled integration tests per OS (loopback / veth where possible). - -### P1.3 Socket layer - -- [x] Symmetry: ensure TCP/UDP/ICMP each expose the same shape sync and async. -- [x] Constructor policy: infallible config + fallible builder is the current shape - (`TcpConfig` etc.) — apply it consistently to UDP/ICMP and document it. -- [x] Normalize bind/connect/timeout behavior across platforms. -- [x] Tests for socket options: TTL/hop limit, broadcast, multicast, device binding, - IPv4/IPv6 family mismatch (config `validate()` covers some — extend to runtime), - nonblocking-state preservation across operations. -- [x] Document privilege requirements for raw ICMP / raw TCP per platform. -- [x] Confirm sync impls never transitively require the async runtime (P0.5). - -### P1.4 Performance - -- [x] Establish Criterion baselines beyond the single `packet_parse` bench: - Ethernet/VLAN parse, IPv4/IPv6 parse, TCP/UDP parse, DNS name decompression, - serialization, checksum, and datalink send/recv loops where measurable. -- [x] Measure allocations in parsers/builders; ensure borrowed views don't clone - `Bytes` or allocate where a slice suffices. -- [x] Review checksum impl (`nex-packet/src/checksum.rs` + call sites) for alignment - and portable vectorization; verify the folding/carry handling on odd lengths. -- [x] Fix the `GenericMutablePacket` re-parse hot path (also in P1.1) — it's a real - throughput cost for in-place mutation workloads. -- [x] Add a documented benchmark workflow and/or CI regression tracking. - -### P1.5 Fuzzing & robustness - -- [x] Keep the 5 existing targets; add: Ethernet/VLAN, IPv4 options, IPv6 ext - headers, ICMPv6/NDP options, DNS records + compressed names, DHCP options, - GRE optional fields, VXLAN. -- [x] Add seed corpora for protocol edge cases. -- [x] Add sanitized seed corpora from real captures. -- [x] Wire fuzz findings back as regression unit tests. -- [x] Add property tests (proptest) for parse↔serialize round-trips per family. -- [x] Document `cargo fuzz` usage in `CONTRIBUTING.md`. -- [x] Guarantee (and test) panic-free parsing on arbitrary bytes for every module. - ---- - -## P2 — Documentation, Ergonomics, Release - -### P2.1 Documentation - -- [ ] Crate-level docs for every published crate; module docs for every stable module. -- [ ] Rich, compile-tested doc examples for: parse an Ethernet frame; build IPv4/UDP - and IPv6/UDP; compute checksums; datalink send/recv; async datalink recv; - TCP/UDP/ICMP sockets (sync + async). -- [ ] Document platform support matrix and privilege requirements in one place. -- [ ] Document the safety model, error taxonomy, feature flags, and performance - expectations. -- [ ] `docs.rs` metadata: `all-features` (or curated) docs build per crate; verify - feature-gated items render. -- [ ] Rewrite `README.md`: it omits `nex-core`/`nex-socket` from the crate list, - predates the feature set, and states version `0.26`. Make it accurate for 1.0 - without overpromising unsupported protocols/platforms. - -### P2.2 Migration & compatibility - -- [ ] Write `MIGRATION.md` / migration notes `0.26.x → 1.0.0` covering: parse API - unification, fallible builders, `get_*` renames, error-type changes, feature - changes (`tokio`/`rand`). -- [ ] Add a v1.0 semver/compatibility policy (what's covered, MSRV policy, platform - tiers, `#[non_exhaustive]` implications). - -### P2.3 Repository & release readiness - -- [ ] Add `CHANGELOG.md` with an Unreleased section (keep-a-changelog style). -- [ ] Verify workspace version/dependency pinning is release-consistent. - ---- - -## Suggested Execution Order - -1. **P0.6 CI + P0.7 unsafe comments** — get a truthful baseline and stop regressions. -2. **P0.5 features/deps** — cheap, high-impact; unblocks clean downstream trees. -3. **P0.1 parse unification + P0.4 error model** — the biggest, most breaking surface - change; do it once, early, behind deprecations. -4. **P0.2 fallible builders** — depends on the error model. -5. **P0.3 API freeze** — lock the surface after 1–4 settle. -6. **P1.1–P1.3 correctness** (packet arch, datalink, socket) with the manual test - matrix. -7. **P1.4 perf + P1.5 fuzz/property tests** — prove robustness and speed. -8. **P2 docs, migration, release scaffolding** — last, once the surface is final. - -## Definition of Done for v1.0.0 - -- [ ] One consistent parse API and one error taxonomy across all crates. -- [ ] Builders validate and cannot silently emit invalid packets. -- [ ] Sync users pull no async runtime; parsers pull no needless deps. -- [ ] Green CI: tests + doc-tests + clippy + fmt + feature matrix + MSRV + - `cargo deny` on Linux/macOS/Windows. -- [ ] Every `unsafe` justified; no reachable panics on malformed input; fuzz + - property tests in place. -- [ ] Complete docs, accurate README, migration guide, CHANGELOG.