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 5ee3afc..1de0518 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -2,23 +2,82 @@ name: Rust on: push: - branches: [ "main" ] + branches: ["main"] pull_request: - branches: [ "main" ] + branches: ["main"] env: CARGO_TERM_COLOR: always +concurrency: + group: rust-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: - build: - name: Check + checks: + name: Checks + runs-on: ubuntu-latest + timeout-minutes: 15 + + 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 + - 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 }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 15 + strategy: + fail-fast: true + matrix: + os: [macos-latest, windows-latest] + + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: Check workspace + 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: false + fail-fast: true matrix: - os: [ubuntu-latest, macOS-latest, windows-latest] + include: + - os: macos-latest + target: aarch64-apple-ios + - os: ubuntu-latest + target: x86_64-unknown-netbsd steps: - - uses: actions/checkout@v3 - - name: Build - run: cargo build + - 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/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/Cargo.toml b/Cargo.toml index 2220896..79fda02 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,21 +10,22 @@ 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.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/README.md b/README.md index e18e76a..3e0e712 100644 --- a/README.md +++ b/README.md @@ -17,13 +17,18 @@ 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`: ```toml [dependencies] -nex = "0.26" +nex = "0.27" ``` ## Using Specific Sub-crates @@ -34,6 +39,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.27", features = ["async"] } +``` ## Privileges `nex-datalink` uses a raw socket which may require elevated privileges depending on your system's configuration. 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 = [] 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/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/docs/PRIVILEGED_TESTING.md b/docs/PRIVILEGED_TESTING.md new file mode 100644 index 0000000..367568c --- /dev/null +++ b/docs/PRIVILEGED_TESTING.md @@ -0,0 +1,58 @@ +# 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. + +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. | +| 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. diff --git a/examples/arp.rs b/examples/arp.rs index 2cc1b84..e131aed 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), @@ -66,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), @@ -80,23 +78,22 @@ 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 { - 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..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() @@ -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 @@ -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!(), }; @@ -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() @@ -133,38 +133,38 @@ 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 { - 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..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 { @@ -42,15 +42,10 @@ 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; + let version = Ipv4Packet::try_from_buf(&packet).unwrap().header.version; let fake_eth = EthernetPacket { header: EthernetHeader { destination: MacAddr::zero(), @@ -66,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>(()) @@ -96,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, @@ -114,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), @@ -130,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), @@ -171,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, @@ -189,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, @@ -206,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 => { @@ -270,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 fb55e85..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}; @@ -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 Ok(ipv4_packet) = Ipv4Packet::try_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 Ok(icmp_packet) = IcmpPacket::try_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"); } } } @@ -87,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(); @@ -98,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/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 fccbd61..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,25 +68,25 @@ 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), 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 Ok(ipv6) = Ipv6Packet::try_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 +95,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 Ok(udp) = UdpPacket::try_from_bytes(packet.clone()) + && !udp.payload.is_empty() + && let Ok(dns) = DnsPacket::try_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.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.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.dns_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.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..6aaf845 100644 --- a/examples/dump.rs +++ b/examples/dump.rs @@ -66,15 +66,10 @@ 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; + let version = Ipv4Packet::try_from_buf(packet).unwrap().header.version; let fake_eth = EthernetPacket { header: EthernetHeader { destination: MacAddr::zero(), @@ -90,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), @@ -119,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, @@ -137,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), @@ -153,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), @@ -194,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, @@ -212,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, @@ -229,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 => { @@ -293,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 431b280..bd6e682 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 @@ -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!(), }; @@ -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::try_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..1586b61 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 @@ -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!(), }; @@ -76,27 +78,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 Ok(ipv4_packet) = Ipv4Packet::try_from_buf(packet) + && ipv4_packet.header.next_level_protocol == nex_packet::ip::IpNextProtocol::Icmp + && let Ok(icmp_packet) = IcmpPacket::try_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"); } } } @@ -104,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 060bd24..8a3d8f2 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(); @@ -107,31 +112,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 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 + && 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..03e3d06 100644 --- a/examples/parse_frame.rs +++ b/examples/parse_frame.rs @@ -55,20 +55,15 @@ 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) { - 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 1bd4810..a47f9df 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"); @@ -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(); } } @@ -165,7 +168,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 +204,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::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 { - 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..b3ffb03 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 @@ -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"), }; @@ -93,7 +94,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 +127,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::try_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/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/.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 f69cd22..5a55543 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] @@ -48,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/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/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 e368316..48a5ed1 100644 --- a/fuzz/fuzz_targets/dns_name.rs +++ b/fuzz/fuzz_targets/dns_name.rs @@ -4,6 +4,5 @@ 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); }); 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/frame_parse.rs b/fuzz/fuzz_targets/frame_parse.rs index e304592..58f4b6a 100644 --- a/fuzz/fuzz_targets/frame_parse.rs +++ b/fuzz/fuzz_targets/frame_parse.rs @@ -2,10 +2,10 @@ 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 _ = FrameView::from_buf(data, ParseOption::default()); + let _ = Frame::try_from_buf_with_mode(data, ParseOption::default(), ParseMode::Strict); + let _ = FrameView::try_from_buf(data, ParseOption::default()); }); 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/ipv4_parse.rs b/fuzz/fuzz_targets/ipv4_parse.rs index 322b74d..7c912af 100644 --- a/fuzz/fuzz_targets/ipv4_parse.rs +++ b/fuzz/fuzz_targets/ipv4_parse.rs @@ -1,11 +1,10 @@ #![no_main] use libfuzzer_sys::fuzz_target; -use nex_packet::packet::Packet; use nex_packet::ipv4::Ipv4Packet; +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_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/ipv6_parse.rs b/fuzz/fuzz_targets/ipv6_parse.rs index a59f4a0..3acb9fd 100644 --- a/fuzz/fuzz_targets/ipv6_parse.rs +++ b/fuzz/fuzz_targets/ipv6_parse.rs @@ -1,11 +1,10 @@ #![no_main] use libfuzzer_sys::fuzz_target; -use nex_packet::packet::Packet; use nex_packet::ipv6::Ipv6Packet; +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/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/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/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-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-core/src/interface.rs b/nex-core/src/interface.rs index 92dd5c9..4b4a97a 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)] @@ -40,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, @@ -128,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, @@ -156,6 +184,7 @@ pub enum InterfaceType { MultiRateSymmetricDsl, HighPerformanceSerialBus, Wman, + Wwan, Wwanpp, Wwanpp2, Bridge, @@ -194,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"), @@ -248,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, @@ -275,9 +306,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, } @@ -310,9 +345,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, } @@ -329,34 +368,53 @@ 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, } impl Interface { #[cfg(feature = "gateway")] #[allow(clippy::should_implement_trait)] - pub fn default() -> Result { + pub fn default() -> Result { get_default_interface() } @@ -589,13 +647,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 +665,35 @@ 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, InterfaceType}; + + 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" + ); + } + + #[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-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; diff --git a/nex-datalink/Cargo.toml b/nex-datalink/Cargo.toml index cb61f1b..32d0ec1 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" @@ -17,7 +18,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" @@ -25,11 +26,15 @@ features = [ "Win32_Foundation", "Win32_Networking_WinSock", "Win32_System_IO", + "Win32_System_LibraryLoader", + "Win32_System_SystemInformation", "Win32_System_Threading", "Win32_System_WindowsProgramming", ] [features] +default = [] +async = ["dep:futures-core"] serde = ["dep:serde", "nex-core/serde"] pcap = ["dep:pcap"] diff --git a/nex-datalink/src/async_io/bpf.rs b/nex-datalink/src/async_io/bpf.rs index a8b39b6..9bc978b 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,70 @@ 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 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))), + }; + 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; @@ -136,7 +171,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(); @@ -157,6 +192,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 +220,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 +35,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 +48,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 +65,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 +91,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 +108,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 +129,107 @@ 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); + // 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()); + } + + if config.promiscuous { + // SAFETY: A zero bit pattern is valid for a packet membership request. + let mut request: linux::packet_mreq = unsafe { mem::zeroed() }; + request.mr_ifindex = network_interface.index as i32; + request.mr_type = linux::PACKET_MR_PROMISC as u16; + // SAFETY: The descriptor is open and `request` remains readable for + // the exact size supplied to setsockopt. + if unsafe { + libc::setsockopt( + fd.as_raw(), + linux::SOL_PACKET, + linux::PACKET_ADD_MEMBERSHIP, + (&request as *const linux::packet_mreq).cast(), + mem::size_of::() as libc::socklen_t, + ) + } == -1 + { + return Err(io::Error::last_os_error()); } - return Err(err); } - let epfd = unsafe { libc::epoll_create1(0) }; - if epfd == -1 { - let err = io::Error::last_os_error(); - unsafe { - nex_sys::close(fd); + 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; } - return Err(err); + 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 { + 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 71932a3..e0a863a 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,17 +11,17 @@ 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}; use futures_core::stream::Stream; -use crate::Config; +use crate::{Config, DatalinkError}; /// Trait for asynchronously sending raw packets. pub trait AsyncRawSender: Send { @@ -50,28 +50,36 @@ 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, configuration: Config, -) -> io::Result { - #[cfg(all(any(target_os = "linux", target_os = "android")))] +) -> 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(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) + 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..db258c2 100644 --- a/nex-datalink/src/async_io/wpcap.rs +++ b/nex-datalink/src/async_io/wpcap.rs @@ -5,56 +5,123 @@ 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; use std::mem; use std::pin::Pin; use std::slice; -use std::sync::{Arc, Mutex}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex, TryLockError}; use std::task::{Context, Poll, Waker}; -use std::thread; +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) { - unsafe { windows::PacketCloseAdapter(self.adapter) }; + 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 { (api.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 { + api: &'static windows::PacketApi, packet: windows::LPPACKET, } impl Drop for WinPcapPacket { fn drop(&mut self) { - unsafe { windows::PacketFreePacket(self.packet) }; + let api = self.api; + // SAFETY: `packet` is uniquely owned and came from + // PacketAllocatePacket, so it must be freed exactly once. + unsafe { (api.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 {} +/// 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)); } -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, @@ -62,18 +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, ); } + // 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 { @@ -95,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 @@ -122,122 +231,231 @@ 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]; + // SAFETY: PacketOpenAdapter reads the temporary NUL-terminated 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(|_| { 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 ret = unsafe { windows::PacketSetHwFilter(adapter, windows::NDIS_PACKET_TYPE_PROMISCUOUS) }; + // SAFETY: The adapter is open and the filter is an Npcap constant. + let ret = unsafe { + (api.PacketSetHwFilter)(adapter.adapter, windows::hw_filter_for(config.promiscuous)) + }; + if ret == 0 { + return Err(io::Error::last_os_error()); + } + + // SAFETY: The adapter is open and the size value is passed by value. + let ret = unsafe { (api.PacketSetBuff)(adapter.adapter, read_buffer_size as libc::c_int) }; if ret == 0 { - unsafe { windows::PacketCloseAdapter(adapter) }; return Err(io::Error::last_os_error()); } - let ret = unsafe { windows::PacketSetBuff(adapter, read_buffer_size as libc::c_int) }; + // SAFETY: The adapter is open and the threshold is valid. + let ret = unsafe { (api.PacketSetMinToCopy)(adapter.adapter, 1) }; if ret == 0 { - unsafe { windows::PacketCloseAdapter(adapter) }; return Err(io::Error::last_os_error()); } - let ret = unsafe { windows::PacketSetMinToCopy(adapter, 1) }; + // Use a bounded receive wait so dropping the channel can join the worker. + // SAFETY: The adapter is open and the timeout is passed by value. + let ret = unsafe { (api.PacketSetReadTimeout)(adapter.adapter, RECEIVE_TIMEOUT_MS) }; if ret == 0 { - unsafe { windows::PacketCloseAdapter(adapter) }; return Err(io::Error::last_os_error()); } - let write_packet = unsafe { windows::PacketAllocatePacket() }; + // SAFETY: PacketAllocatePacket takes no arguments and returns an owned + // packet pointer or null. + let write_packet = unsafe { (api.PacketAllocatePacket)() }; if write_packet.is_null() { - unsafe { windows::PacketCloseAdapter(adapter) }; return Err(io::Error::last_os_error()); } + let write_packet = WinPcapPacket { + api, + packet: write_packet, + }; + // SAFETY: The packet and backing vector remain live and immovable in the + // sender after this initialization. unsafe { - windows::PacketInitPacket( - write_packet, + (api.PacketInitPacket)( + write_packet.packet, write_buffer.as_mut_ptr() as windows::PVOID, config.write_buffer_size as windows::UINT, ); } - let adapter = Arc::new(WinPcapAdapter { adapter }); - let packets = Arc::new(Mutex::new(VecDeque::new())); - let waker: Arc>> = 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 read_buffer_size = read_buffer_size; + let send_waker = send_waker.clone(); + let stop = stop.clone(); thread::spawn(move || { let mut read_buffer = vec![0u8; read_buffer_size]; - let read_packet = unsafe { windows::PacketAllocatePacket() }; + // SAFETY: PacketAllocatePacket takes no arguments and returns an + // owned packet pointer or null. + 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( - read_packet, + (api.PacketInitPacket)( + 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 { (api.PacketReceivePacket)(adapter.adapter, read_packet.packet, 1) }; if ret == 0 { + let error = io::Error::last_os_error(); + // Release the adapter lock before backing off so a pending + // send is not starved, and avoid spinning on a persistent + // adapter failure. + drop(_operation); + wake(&send_waker); + push_error(&packets, error); + wake(&waker); + thread::sleep(RECEIVE_ERROR_BACKOFF); 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 { + 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 + // 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 { + malformed = Some("Npcap record length overflow"); + break; + }; + if header_len < mem::size_of::() || record_len > remaining { + malformed = Some("invalid Npcap BPF record lengths"); + 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(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); } } - }); - } + }) + }; let inner = Arc::new(Inner { adapter, packets, waker, + send_waker, + stop, + receive_thread: Mutex::new(Some(receive_thread)), }); let tx = AsyncWpcapSocketSender { inner: inner.clone(), write_buffer, - packet: WinPcapPacket { - packet: write_packet, - }, + packet: write_packet, }; let rx = AsyncWpcapSocketReceiver { inner }; Ok(AsyncChannel::Ethernet(Box::new(tx), Box::new(rx))) @@ -259,4 +477,80 @@ mod tests { let _ = poll_fn(|cx| tx.poll_send(cx, &packet)).await; }); } + + fn queue() -> 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/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/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/bpf.rs b/nex-datalink/src/bpf.rs index b4c88f3..701b457 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; @@ -18,7 +17,7 @@ static ETHERNET_NULL_HEADER_SIZE: usize = 4; /// The BPF-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, @@ -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, @@ -64,10 +63,31 @@ 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] -pub fn channel(network_interface: &Interface, config: Config) -> io::Result { +pub(crate) fn channel(network_interface: &Interface, config: Config) -> io::Result { #[cfg(any( target_os = "freebsd", target_os = "netbsd", @@ -77,6 +97,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()) @@ -92,6 +114,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(()) } @@ -122,7 +144,11 @@ pub fn channel(network_interface: &Interface, config: Config) -> io::Result io::Result io::Result Option> { - let len = num_packets * packet_size; - if len >= self.write_buffer.len() { + 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 { // If we're sending on the loopback device, discard the ethernet header. @@ -266,12 +285,20 @@ 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.fd, &mut self.fd_set as *mut libc::fd_set); + libc::FD_SET(self.fd.as_raw(), &mut self.fd_set as *mut libc::fd_set); libc::pselect( - self.fd.fd + 1, + self.fd.as_raw() + 1, ptr::null_mut(), &mut self.fd_set as *mut libc::fd_set, ptr::null_mut(), @@ -287,17 +314,17 @@ 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"))); - } else { - match unsafe { - libc::write( - self.fd.fd, - chunk.as_ptr().offset(offset as isize) as *const libc::c_void, - (chunk.len() - offset) as libc::size_t, - ) - } { - len if len == -1 => return Some(Err(io::Error::last_os_error())), - _ => (), - } + // 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(), + 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(())) @@ -313,10 +340,18 @@ 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.fd, &mut self.fd_set as *mut libc::fd_set); + libc::FD_SET(self.fd.as_raw(), &mut self.fd_set as *mut libc::fd_set); libc::pselect( - self.fd.fd + 1, + self.fd.as_raw() + 1, ptr::null_mut(), &mut self.fd_set as *mut libc::fd_set, ptr::null_mut(), @@ -328,18 +363,20 @@ 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 { + // SAFETY: `offset` is checked against `packet.len()` above; the + // descriptor is open and `write` retains no buffer pointer. match unsafe { libc::write( - self.fd.fd, - packet.as_ptr().offset(offset as isize) as *const libc::c_void, + self.fd.as_raw(), + 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(())), } } @@ -365,10 +402,12 @@ 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.fd, &mut self.fd_set as *mut libc::fd_set); + libc::FD_SET(self.fd.as_raw(), &mut self.fd_set as *mut libc::fd_set); libc::pselect( - self.fd.fd + 1, + self.fd.as_raw() + 1, &mut self.fd_set as *mut libc::fd_set, ptr::null_mut(), ptr::null_mut(), @@ -384,9 +423,11 @@ 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.fd, + self.fd.as_raw(), buffer.as_ptr() as *mut libc::c_void, buffer.len() as libc::size_t, ) @@ -394,20 +435,41 @@ 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", )); - let offset = (*packet).bh_hdrlen as isize + (*packet).bh_caplen as isize; - ptr = ptr.offset(bpf::BPF_WORDALIGN(offset)); } + // 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 = + validate_record_lengths(header_len, captured_len, remaining, header_size)?; + 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") + })?; } } } @@ -424,9 +486,44 @@ 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]) } } + +#[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); + } +} diff --git a/nex-datalink/src/lib.rs b/nex-datalink/src/lib.rs index ec604fc..5db12a5 100644 --- a/nex-datalink/src/lib.rs +++ b/nex-datalink/src/lib.rs @@ -1,30 +1,28 @@ //! Cross-platform datalink I/O primitives for sending and receiving raw packets. -#![deny(warnings)] - +use std::fmt; use std::io; use std::option::Option; use std::time::Duration; mod bindings; +#[cfg(feature = "async")] pub mod async_io; #[cfg(windows)] -#[path = "wpcap.rs"] -mod backend; +mod wpcap; #[cfg(windows)] -pub mod wpcap; +use wpcap as backend; -#[cfg(all(any(target_os = "linux", target_os = "android")))] -#[path = "linux.rs"] -mod backend; +#[cfg(any(target_os = "linux", target_os = "android"))] +mod linux; #[cfg(any(target_os = "linux", target_os = "android"))] -pub mod linux; +use linux as backend; -#[cfg(all(any( +#[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; +))] +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; @@ -54,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, @@ -70,23 +69,64 @@ pub enum Channel { /// Socket fanout type (Linux only). #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +#[non_exhaustive] 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)] +#[non_exhaustive] 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, } @@ -94,7 +134,28 @@ 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, +/// `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 { /// The size of buffer to use when writing packets. Defaults to 4096. pub write_buffer_size: usize, @@ -102,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. @@ -116,11 +178,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 { @@ -138,24 +243,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(()) } @@ -209,14 +314,19 @@ 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( 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. @@ -225,8 +335,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, @@ -236,8 +351,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>; } @@ -285,4 +402,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 c2f4595..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 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 { @@ -171,9 +177,11 @@ pub fn channel(network_interface: &Interface, config: Config) -> 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; let mut pollfd = libc::pollfd { - fd: self.socket.fd, + fd: self.socket.as_raw(), events: libc::POLLOUT, revents: 0, }; @@ -255,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, @@ -268,12 +279,17 @@ impl RawSender for RawSenderImpl { } else if ret == 0 { return Some(Err(io::Error::new(io::ErrorKind::TimedOut, "Timed out"))); } else if pollfd.revents & libc::POLLOUT != 0 { - if let Err(e) = nex_sys::send_to( - self.socket.fd, - chunk, - send_addr, - self.send_addr_len as libc::socklen_t, - ) { + // SAFETY: `send_addr` points to `self.send_addr`, which + // remains alive and has the supplied `sockaddr_ll` length. + let send_result = unsafe { + nex_sys::send_to( + self.socket.as_raw(), + chunk, + send_addr, + self.send_addr_len as libc::socklen_t, + ) + }; + if let Err(e) = send_result { return Some(Err(e)); } } else { @@ -293,7 +309,7 @@ impl RawSender for RawSenderImpl { #[inline] fn send(&mut self, packet: &[u8]) -> Option> { let mut pollfd = libc::pollfd { - fd: self.socket.fd, + fd: self.socket.as_raw(), events: libc::POLLOUT, revents: 0, }; @@ -302,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, @@ -316,12 +334,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(())), } @@ -342,9 +365,10 @@ 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.fd, + fd: self.socket.as_raw(), events: libc::POLLIN, revents: 0, }; @@ -353,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, @@ -367,7 +393,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-datalink/src/pcap.rs b/nex-datalink/src/pcap.rs index 3a30fc3..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. @@ -28,7 +29,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 +62,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 +76,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 +95,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 +116,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 +135,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 +149,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 +186,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) } @@ -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 d0938bc..e53fb87 100644 --- a/nex-datalink/src/wpcap.rs +++ b/nex-datalink/src/wpcap.rs @@ -4,57 +4,87 @@ 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}; +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); } } } +// 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 { + 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); } } } +// 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, /// 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, } } } @@ -64,117 +94,149 @@ 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 fn channel(network_interface: &Interface, config: Config) -> io::Result { - let mut read_buffer = Vec::new(); - read_buffer.resize(config.read_buffer_size, 0u8); +pub(crate) 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 mut write_buffer = Vec::new(); - write_buffer.resize(config.write_buffer_size, 0u8); + // Reject an out-of-range timeout before any OS handle is opened. + let read_timeout_ms = read_timeout_millis(config.read_timeout)?; + 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. 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(|_| { 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 { adapter }); + let adapter = Arc::new(WinPcapAdapter { + api, + adapter, + operation_lock: Mutex::new(()), + }); - let ret = unsafe { - windows::PacketSetHwFilter(adapter.adapter, windows::NDIS_PACKET_TYPE_PROMISCUOUS) - }; + 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 { (api.PacketSetHwFilter)(adapter.adapter, hw_filter) }; if ret == 0 { return Err(io::Error::last_os_error()); } // Set kernel buffer size - let ret = unsafe { - windows::PacketSetBuff(adapter.adapter, config.read_buffer_size as libc::c_int) - }; + // SAFETY: The adapter is open and PacketSetBuff retains no Rust pointers. + let ret = + 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 - let ret = unsafe { windows::PacketSetMinToCopy(adapter.adapter, 1) }; + // SAFETY: The adapter is open and the integer threshold is valid. + 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()); } - let read_packet = unsafe { windows::PacketAllocatePacket() }; + // SAFETY: PacketAllocatePacket takes no arguments and returns an owned + // packet pointer or null. + 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( - read_packet, + (api.PacketInitPacket)( + read_packet.packet, read_buffer.as_mut_ptr() as windows::PVOID, config.read_buffer_size as windows::UINT, ) } - let write_packet = unsafe { windows::PacketAllocatePacket() }; + // SAFETY: PacketAllocatePacket takes no arguments and returns an owned + // packet pointer or null. + let write_packet = unsafe { (api.PacketAllocatePacket)() }; if write_packet.is_null() { - unsafe { windows::PacketFreePacket(read_packet) }; 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( - write_packet, + (api.PacketInitPacket)( + write_packet.packet, write_buffer.as_mut_ptr() as windows::PVOID, config.write_buffer_size as windows::UINT, ) } + // SAFETY: `read_packet.packet` is live and initialized above. + let packet_capacity = unsafe { (*read_packet.packet).Length } as usize / 64; let sender = Box::new(RawSenderImpl { adapter: adapter.clone(), _write_buffer: write_buffer, - packet: WinPcapPacket { - packet: write_packet, - }, + packet: write_packet, }); let receiver = Box::new(RawReceiverImpl { - adapter: adapter, + adapter, _read_buffer: read_buffer, - packet: WinPcapPacket { - packet: read_packet, - }, + packet: read_packet, // Enough room for minimally sized packets without reallocating - packets: VecDeque::with_capacity(unsafe { (*read_packet).Length } as usize / 64), + 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, @@ -189,26 +251,51 @@ impl RawSender for RawSenderImpl { packet_size: usize, func: &mut dyn FnMut(&mut [u8]), ) -> Option> { - let len = num_packets * packet_size; - if len >= unsafe { (*self.packet.packet).Length } as usize { + 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 ret = unsafe { - windows::PacketSendPacket(self.adapter.adapter, self.packet.packet, 0) + 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 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 { (*self.packet.packet).Length = old_len; } @@ -229,42 +316,99 @@ 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, _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 +// 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 ret = unsafe { - windows::PacketReceivePacket(self.adapter.adapter, self.packet.packet, 0) - }; + 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 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()), - _ => 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)); + 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 }; + 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(|| { @@ -273,6 +417,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) @@ -280,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-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-packet/Cargo.toml b/nex-packet/Cargo.toml index b2bef75..0967b19 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" @@ -14,14 +15,19 @@ 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] -criterion = "0.5" +criterion = "0.8" +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/benches/packet_parse.rs b/nex-packet/benches/packet_parse.rs index 6a990b5..19c6307 100644 --- a/nex-packet/benches/packet_parse.rs +++ b/nex-packet/benches/packet_parse.rs @@ -1,8 +1,7 @@ use bytes::Bytes; use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; use nex_packet::{ - frame::{Frame, FrameView, ParseOption}, - packet::Packet, + frame::{Frame, FrameSlice, FrameView, ParseOption}, tcp::TcpPacket, udp::UdpPacket, }; @@ -32,32 +31,35 @@ 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("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::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 3ef8fb5..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 { @@ -481,7 +490,7 @@ impl<'a> 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,13 +499,18 @@ 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 } } 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/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 a61bca5..29d3309 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 { @@ -51,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 526cfbc..281cad0 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; @@ -13,6 +14,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 { @@ -24,7 +31,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, @@ -76,19 +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::() - + 3) - / 4) as u4; // includes padding self } @@ -97,15 +91,82 @@ 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, + }); + } + 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 + } + }; + 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()) } } @@ -113,6 +174,7 @@ impl Ipv4PacketBuilder { mod tests { use super::*; use crate::ip::IpNextProtocol; + use crate::ipv4::Ipv4OptionHeader; use bytes::Bytes; use std::net::Ipv4Addr; @@ -124,11 +186,66 @@ 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 ); assert_eq!(pkt.payload, payload); } + + #[test] + fn ipv4_builder_identification_is_caller_controlled() { + let default_packet = Ipv4PacketBuilder::new().build().expect("valid IPv4 packet"); + assert_eq!(default_packet.header.identification, 0); + + 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 9bf7545..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, @@ -12,6 +13,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 { @@ -79,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 @@ -111,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 d40a072..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; @@ -7,7 +8,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 @@ -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 c199b8c..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 @@ -24,8 +25,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 +39,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,28 +64,17 @@ 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 } 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 + 3) / 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 eed3c8b..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; @@ -32,19 +33,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 } @@ -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(); - self.packet.header.length = (total_len as u16).into(); + 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/checksum.rs b/nex-packet/src/checksum.rs index 9ae3c5e..218ed9b 100644 --- a/nex-packet/src/checksum.rs +++ b/nex-packet/src/checksum.rs @@ -3,20 +3,16 @@ 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)] +#[non_exhaustive] 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 { @@ -73,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 bcf2f36..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(mut bytes: Bytes) -> Option { - Self::from_buf(&mut 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,61 +807,68 @@ pub struct DnsResponsePacket { impl Packet for DnsResponsePacket { type Header = (); - fn from_buf(buf: &[u8]) -> Option { - if buf.len() < 12 { - return None; - } - - let mut pos = 0; - - let name_tag = u16::from_be_bytes([buf[pos], buf[pos + 1]]).into(); - pos += 2; + fn try_from_buf(buf: &[u8]) -> Result { + (|| -> Option { + if buf.len() < 12 { + return None; + } - let rtype = DnsType::new(u16::from_be_bytes([buf[pos], buf[pos + 1]])); - pos += 2; + let mut pos = 0; - let rclass = DnsClass::new(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 ttl = u32::from_be_bytes([buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3]]).into(); - pos += 4; + let rtype = DnsType::new(u16::from_be_bytes([buf[pos], buf[pos + 1]])); + pos += 2; - let data_len = u16::from_be_bytes([buf[pos], buf[pos + 1]]).into(); - pos += 2; + let rclass = DnsClass::new(u16::from_be_bytes([buf[pos], buf[pos + 1]])); + pos += 2; - let data_len_usize = data_len as usize; + let ttl = u32::from_be_bytes([buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3]]); + pos += 4; - if buf.len() < pos + data_len_usize { - return None; - } + let data_len = u16::from_be_bytes([buf[pos], buf[pos + 1]]); + pos += 2; - let data = buf[pos..pos + data_len_usize].to_vec(); - pos += data_len_usize; + let data_len_usize = data_len as 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(mut bytes: Bytes) -> Option { - Self::from_buf(&mut 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 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 +909,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 +921,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,14 +940,14 @@ impl DnsResponsePacket { rtype, rclass, ttl, - data_len: data_len.into(), + data_len, data, payload, }) } /// 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::from_bytes(&self.data).ok(), + 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 try_from_buf(buf: &[u8]) -> Result { + Self::try_from_buf(buf) + .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 { @@ -1047,12 +1106,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 +1186,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 +1197,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 @@ -1212,26 +1271,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 93b1c75..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, @@ -161,20 +162,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()); @@ -196,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()); @@ -236,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 { @@ -352,7 +373,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,22 +382,32 @@ 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 } } 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) { @@ -384,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) { @@ -395,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) { @@ -459,12 +500,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 @@ -479,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] @@ -517,7 +569,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..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,33 +57,40 @@ 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: quanta.into(), - 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 { 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 +99,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..ee9b349 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}, }; @@ -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 { @@ -67,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. /// @@ -78,31 +297,56 @@ 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() } } -/// 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, @@ -195,7 +439,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) }; @@ -240,7 +484,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) }; @@ -377,6 +621,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::*; @@ -463,40 +743,43 @@ mod tests { assert_eq!(packet.header.ethertype, EtherType::Ipv4); 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; - } + #[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())); } -} -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; - } + #[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/gre.rs b/nex-packet/src/gre.rs index de3f902..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: protocol_type.into(), - 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 { @@ -122,7 +129,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 +177,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 +276,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 +292,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..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 { @@ -242,7 +250,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 +285,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,13 +294,18 @@ 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 } } 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) { @@ -541,8 +569,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 +643,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 +692,7 @@ pub mod time_exceeded { pkt.payload[1], pkt.payload[2], pkt.payload[3], - ]) - .into(), + ]), payload: pkt.payload.slice(4..), }) } @@ -802,7 +829,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 +849,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..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()); @@ -327,7 +335,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,13 +344,18 @@ 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 } } 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 { @@ -817,12 +852,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 @@ -885,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 { @@ -1077,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 { @@ -1269,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 { @@ -1334,7 +1388,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 { @@ -1482,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, - }); + let payload = Bytes::copy_from_slice(&bytes[i..]); - i += option_len; - } - - 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 { @@ -1705,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); @@ -1784,10 +1852,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 { @@ -2227,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 { @@ -2377,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 b4d37ec..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, @@ -256,17 +257,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..f243f3b 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}; @@ -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 { @@ -241,10 +250,8 @@ impl Packet for Ipv4Packet { } } - // padding - while tmp_buf.len() % 4 != 0 { - 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(); @@ -254,8 +261,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. @@ -306,32 +313,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 { @@ -557,7 +578,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,13 +591,18 @@ 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] } } 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, @@ -676,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) { @@ -688,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) { @@ -700,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) { @@ -712,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) { @@ -724,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) { @@ -735,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) { @@ -746,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) { @@ -758,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) { @@ -771,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) { @@ -782,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) { @@ -793,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) { @@ -804,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], @@ -812,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) { @@ -820,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], @@ -828,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) { @@ -1030,7 +1121,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]); @@ -1089,7 +1180,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()); } @@ -1105,6 +1197,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..e37a105 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; @@ -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 { @@ -70,12 +78,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 +94,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); } } @@ -151,22 +160,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 { @@ -174,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 { @@ -187,6 +213,7 @@ fn parse_ipv6_from_bytes(bytes: Bytes, strict: bool) -> Result( bytes: &[u8], strict: bool, @@ -279,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; @@ -346,7 +369,7 @@ where next_header = nh; offset += 8; } - _ => unreachable!(), + _ => break, } } _ => break, @@ -389,7 +412,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,13 +421,18 @@ 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 } } 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 } } @@ -421,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(); @@ -440,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(); @@ -455,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()); @@ -505,6 +573,7 @@ impl<'a> MutableIpv6Packet<'a> { } #[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] pub enum ExtensionHeaderType { HopByHop, Destination, @@ -514,6 +583,7 @@ pub enum ExtensionHeaderType { } #[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] pub enum Ipv6ExtensionHeader { HopByHop { next: IpNextProtocol, @@ -556,16 +626,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 +649,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, @@ -665,6 +740,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; @@ -823,7 +923,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/lib.rs b/nex-packet/src/lib.rs index a8067c6..2547684 100644 --- a/nex-packet/src/lib.rs +++ b/nex-packet/src/lib.rs @@ -1,4 +1,9 @@ //! 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; pub mod builder; @@ -21,3 +26,5 @@ pub mod udp; pub mod util; pub mod vlan; pub mod vxlan; + +pub use builder::BuildError; diff --git a/nex-packet/src/packet.rs b/nex-packet/src/packet.rs index bf511ef..2aa245f 100644 --- a/nex-packet/src/packet.rs +++ b/nex-packet/src/packet.rs @@ -1,15 +1,32 @@ 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; - /// 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 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 raw bytes. (with ownership) - fn from_bytes(bytes: Bytes) -> Option; + /// 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; @@ -32,32 +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; @@ -88,16 +135,28 @@ 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::from_buf(self.packet()) + Self::Packet::try_from_buf(self.packet()).ok() } } /// 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

, } @@ -105,9 +164,16 @@ impl<'a, P: Packet> MutablePacket<'a> for GenericMutablePacket<'a, P> { type Packet = P; fn new(buffer: &'a mut [u8]) -> Option { - P::from_buf(buffer)?; + 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, }) } @@ -121,45 +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, _) = (&mut *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) = (&mut *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 running additional validation. + /// Construct a mutable packet without requiring a valid packet layout. + /// + /// 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::from_buf(self.packet()) { - Some(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()); } } diff --git a/nex-packet/src/parse.rs b/nex-packet/src/parse.rs index 3697b9f..6ff3c70 100644 --- a/nex-packet/src/parse.rs +++ b/nex-packet/src/parse.rs @@ -1,7 +1,50 @@ -//! 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)` | +//! | `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`] 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] + 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] @@ -90,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 f8c1de9..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]); @@ -288,11 +289,16 @@ 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) } } + /// 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 { - if self.kind == TcpOptionKind::WSCALE && self.data.len() > 0 { + 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. @@ -321,6 +337,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 { @@ -400,15 +427,10 @@ 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) { + 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]); @@ -416,11 +438,16 @@ 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) } } + /// 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]); @@ -429,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 { - if self.kind == TcpOptionKind::WSCALE && self.data.len() > 0 { + 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. @@ -466,11 +503,19 @@ 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 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 { @@ -846,7 +891,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,13 +902,18 @@ 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 } } 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, @@ -1001,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(); @@ -1047,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(); @@ -1057,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()); @@ -1227,8 +1327,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 +1404,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..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(mut bytes: &[u8]) -> Option { - Self::try_from_buf(&mut 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()); @@ -126,7 +134,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 +145,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)] } } @@ -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 9079ee1..f837976 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); @@ -99,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) } @@ -130,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) } @@ -143,11 +139,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 { @@ -166,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] @@ -204,20 +220,43 @@ 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 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)); } } + + #[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); + } } diff --git a/nex-packet/src/vlan.rs b/nex-packet/src/vlan.rs index 4f92898..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(mut bytes: Bytes) -> Option { - Self::from_buf(&mut 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 { @@ -121,7 +129,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 +206,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,12 +215,18 @@ 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 } } 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) as u8) << 4); + 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 { 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()); + } +} diff --git a/nex-socket/Cargo.toml b/nex-socket/Cargo.toml index e4e37d2..aa3ffdd 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" @@ -13,12 +14,12 @@ license = "MIT" [dependencies] nex-core = { workspace = true } nex-packet = { workspace = true } -socket2 = { version = "0.5", features = ["all"] } -tokio = { version = "1", features = ["time", "sync", "net", "rt"] } +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" @@ -29,3 +30,7 @@ features = [ "Win32_System_Threading", "Win32_System_WindowsProgramming", ] + +[features] +default = [] +async = ["dep:tokio"] diff --git a/nex-socket/src/icmp/async_impl.rs b/nex-socket/src/icmp/async_impl.rs index 9a34863..7c7fcdc 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)), @@ -38,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)?; @@ -69,12 +71,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 79a9558..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, @@ -41,7 +43,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, @@ -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/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/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/lib.rs b/nex-socket/src/lib.rs index a033a53..6e20647 100644 --- a/nex-socket/src/lib.rs +++ b/nex-socket/src/lib.rs @@ -2,15 +2,64 @@ //! //! `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; 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] pub enum SocketFamily { IPV4, IPV6, @@ -44,7 +93,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..9e3a0fb 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,25 +70,9 @@ impl AsyncTcpSocket { socket.set_send_buffer_size(size)?; } if let Some(tos) = config.tos { - socket.set_tos(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)?; + socket.set_tos_v4(tos)?; } + crate::apply_tclass_v6(&socket, config.tclass_v6)?; if let Some(only_v6) = config.only_v6 { socket.set_only_v6(only_v6)?; } @@ -141,7 +125,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 +141,9 @@ impl AsyncTcpSocket { return Err(err); } - return Ok(stream); - } - Err(e) => { - return Err(e); + Ok(stream) } + Err(e) => Err(e), } } @@ -195,7 +177,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, @@ -266,12 +249,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. @@ -281,12 +264,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. @@ -331,12 +314,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. @@ -345,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) @@ -364,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() @@ -407,7 +382,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..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, @@ -24,7 +25,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, @@ -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/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/tcp/sync_impl.rs b/nex-socket/src/tcp/sync_impl.rs index f43676d..7b44e86 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,25 +80,9 @@ impl TcpSocket { socket.set_send_buffer_size(size)?; } if let Some(tos) = config.tos { - socket.set_tos(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)?; + socket.set_tos_v4(tos)?; } + crate::apply_tclass_v6(&socket, config.tclass_v6)?; if let Some(only_v6) = config.only_v6 { socket.set_only_v6(only_v6)?; } @@ -183,7 +167,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 +231,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,11 +243,13 @@ 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, - SOL_SOCKET as i32, - SO_ERROR as i32, + SOL_SOCKET, + SO_ERROR, &mut so_error as *mut _ as *mut _, &mut optlen, ) @@ -284,7 +273,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 +299,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, @@ -375,12 +371,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. @@ -390,12 +386,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. @@ -440,12 +436,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. @@ -454,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) @@ -473,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() @@ -516,7 +504,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. @@ -566,6 +554,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..820de86 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. @@ -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,25 +66,9 @@ impl AsyncUdpSocket { socket.set_send_buffer_size(size)?; } if let Some(tos) = config.tos { - socket.set_tos(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)?; + socket.set_tos_v4(tos)?; } + crate::apply_tclass_v6(&socket, config.tclass_v6)?; if let Some(only_v6) = config.only_v6 { socket.set_only_v6(only_v6)?; } @@ -104,11 +88,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 +113,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()) @@ -211,6 +203,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/config.rs b/nex-socket/src/udp/config.rs index 8e9a250..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, @@ -23,7 +24,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, @@ -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 81c9a56..39957be 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; @@ -21,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(), @@ -42,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(), @@ -117,6 +122,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-socket/src/udp/sync_impl.rs b/nex-socket/src/udp/sync_impl.rs index 7db7869..f28f5e0 100644 --- a/nex-socket/src/udp/sync_impl.rs +++ b/nex-socket/src/udp/sync_impl.rs @@ -1,8 +1,10 @@ +#![allow(clippy::useless_conversion)] + 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)] @@ -70,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)?; @@ -88,25 +90,9 @@ impl UdpSocket { socket.set_send_buffer_size(size)?; } if let Some(tos) = config.tos { - socket.set_tos(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)?; + socket.set_tos_v4(tos)?; } + crate::apply_tclass_v6(&socket, config.tclass_v6)?; if let Some(only_v6) = config.only_v6 { socket.set_only_v6(only_v6)?; } @@ -177,6 +163,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) => { @@ -188,50 +176,47 @@ 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", - )); - } + 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", + )); + } + // 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), + IpAddr::V6(_) => None, + }) { + #[cfg(target_os = "netbsd")] + { + pktinfo.ipi_addr.s_addr = u32::from_ne_bytes(src.octets()); } - 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, - }) { + #[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(|_| { - 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(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 packet_info_meta.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 +231,40 @@ 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) = 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", + )); } + // 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, + 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 packet_info_meta.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)) @@ -313,7 +288,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, @@ -461,11 +437,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<()> { @@ -528,6 +504,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) } @@ -545,11 +541,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( @@ -557,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) @@ -575,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() @@ -629,7 +617,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,8 +660,51 @@ 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()); } + + #[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); +} 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-sys/src/lib.rs b/nex-sys/src/lib.rs index 882aee9..3f25ef0 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,72 @@ 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. +#[derive(Debug)] 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 +92,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 +123,58 @@ pub fn recv_from( Ok(len as usize) } } + +#[cfg(test)] +mod tests { + #[cfg(not(target_os = "windows"))] + 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); + } + + #[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); + } +} 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) } } diff --git a/nex/Cargo.toml b/nex/Cargo.toml index 10ab2c8..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" @@ -24,6 +25,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 +77,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"