From 0c7d27c6f9ce6557a3d8261ff8c1d8caa135c795 Mon Sep 17 00:00:00 2001 From: DoubleGate Date: Fri, 28 Aug 2026 20:44:30 -0400 Subject: [PATCH 1/2] chore(deps)!: take the major-version wave, fixing what it exposed Supersedes Dependabot #3 and #4. Those are the same wave from two directions: the "fuzz-deps" group turned out to rewrite the root workspace manifest, because `fuzz/` depends on the workspace by path. Neither was safe to merge as proposed. ## Two of the riskiest majors needed deleting, not migrating `etherparse` 0.15 -> 0.21 and `socket2` 0.5 -> 0.6 were the two large API rewrites in the group. Both crates are declared in three manifests each and imported by **zero** source files -- the same pattern as `quick-xml` and `tui-input` earlier on this branch. Removed. ## The real migration was rand `rand` 0.8 -> 0.9: 24 `thread_rng`, 17 `gen`/`gen::`, and 16 `gen_range` call sites across 10 files, renamed to `rng`, `random`, `random_range`. Every one was a deprecation rather than a removal, so `cargo build` stayed green throughout and only `-D warnings` surfaced them. ## sysinfo 0.38, not 0.39 0.39 declares `rust-version = "1.95"`, well above this workspace's 1.88 floor, and it is directly reachable from `prtip-core`. A local toolchain newer than the MSRV compiles it without complaint; only checking resolved metadata against the declared floor catches it. 0.38.4 is the newest line that fits and carries the same CPU API. `refresh_cpu()`/`global_cpu_info()` -> `refresh_cpu_usage()`/ `global_cpu_usage()`. Verified after: 0 of 502 resolved packages require more than 1.88. Also `colored` 3.1, `dirs` 6.0, `governor` 0.10, `ipnetwork` 0.21 (serde moved behind a feature; `ScanTarget` derives over `IpNetwork`, so it is now explicit), `mlua` 0.12, `nix` 0.31, `rlimit` 0.11, `thiserror` 2.0, `toml` 1.1, `windows` 0.62, `x509-parser` 0.18. `dirs`, `toml` and `ipnetwork` had crate-local pins outside the workspace table and were each being built twice. ## What the wave exposed The `ipnetwork` bump broke one test, and following it down found three real defects that had nothing to do with dependency versions. **Unbounded expansion (the serious one).** `expand_hosts()` was `network.iter().collect()` with no limit, so `prtip -sT -p 80 0.0.0.0/0` tried to allocate 4.3 billion addresses -- about 68 GB -- from eight characters of user input. Measured before and after: the old build printed its banner and hung until SIGKILL (exit 137); it now exits 1 immediately with Invalid target: 0.0.0.0/0 contains 4294967296 addresses, above the 16777216 limit for host expansion. Earlier releases were protected only by accident -- expanding a `/0` overflowed an integer and panicked fast. `ipnetwork` 0.21 removed the overflow and turned a loud failure into a silent hang. The bound is the check that panic was standing in for, and is exactly what the test's own note ("Future enhancement: should validate CIDR size before expansion") had been asking for. `expand_hosts()` now returns `Result`; 12 call sites updated. **`host_count()` returned 0 for a /31.** RFC 3021 makes both addresses of a point-to-point /31 usable, but the code subtracted network and broadcast unconditionally below /32. Latent while nothing trusted the number; it became `Scanner error: Result queue full (0/0)` the moment the scheduler sized its queue from it. **`host_count()` and `expand_hosts()` measured different things.** Usable hosts versus every address -- so a /30 counted 2 and scanned 4. Split into `address_count()` (what expansion yields, what queues size from) and `host_count()` (usable hosts, what a user is shown), with a test pinning `address_count()` to the actual expansion length across five prefixes. My own test caught this; the /31 fix alone would have left the /30 case broken. ## Two optimizations from the same reading - `scheduler.rs` computed `targets.iter().map(|t| t.expand_hosts().len()).sum()` -- materialising every address purely to count it. Now O(1) arithmetic. - `ScanTarget::first_host()` added. Four call sites in `decoy_scanner.rs` expanded an entire target to read `hosts[0]`, so a /8 allocated 268 MB to obtain one address. It is `iter().next()`, and works even on targets too large to expand. ## Test harness The CLI test helper selected the release binary with `release_path.exists()`, true for a *directory*. `docker run -v "$PWD/target/release/prtip:/prtip"` with no release build present makes Docker create the mount source as a root-owned directory, after which all 52 CLI integration tests failed with a baffling `PermissionDenied` rather than falling back to the working debug binary. Now `is_file()`. ## Verification cargo test --workspace 2,587 passed, 0 failed, 121 ignored (exit 0) cargo clippy --workspace --all-targets --locked -- -D warnings exit 0 cargo fmt --all -- --check exit 0 cargo audit exit 0 cargo deny check exit 0 advisories, licenses, bans, sources both generators --check exit 0 resolved MSRV 0 of 502 packages require > 1.88 2,581 -> 2,587; the +6 are regression tests for the defects above, including one asserting `/0` is refused and one that `first_host()` works on a target `expand_hosts()` rejects. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NBDCJvLbq7nuor7RtT57rD --- CHANGELOG.md | 75 ++++ Cargo.lock | 388 ++++++++++-------- Cargo.toml | 31 +- crates/prtip-cli/Cargo.toml | 2 +- crates/prtip-cli/tests/common/mod.rs | 13 +- crates/prtip-cli/tests/test_edge_cases.rs | 8 +- crates/prtip-core/Cargo.toml | 2 +- crates/prtip-core/src/resource_monitor.rs | 8 +- crates/prtip-core/src/retry.rs | 3 +- crates/prtip-core/src/types.rs | 184 ++++++++- crates/prtip-core/tests/integration.rs | 4 +- crates/prtip-network/Cargo.toml | 2 - crates/prtip-network/src/fragmentation.rs | 2 +- crates/prtip-network/src/ipv6_packet.rs | 2 +- crates/prtip-network/src/packet_builder.rs | 10 +- crates/prtip-scanner/Cargo.toml | 6 +- crates/prtip-scanner/src/decoy_scanner.rs | 66 ++- crates/prtip-scanner/src/idle/idle_scanner.rs | 6 +- crates/prtip-scanner/src/scheduler.rs | 14 +- crates/prtip-scanner/src/stealth_scanner.rs | 8 +- crates/prtip-scanner/src/syn_scanner.rs | 16 +- crates/prtip-scanner/src/tcp_connect.rs | 2 +- crates/prtip-scanner/src/timing.rs | 4 +- crates/prtip-scanner/src/udp_scanner.rs | 4 +- .../tests/common/error_injection.rs | 2 +- .../tests/integration_scanner.rs | 4 +- crates/prtip-tui/Cargo.toml | 2 +- 27 files changed, 570 insertions(+), 298 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8de820..b3df4a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,22 @@ private archived repository. Full detail: `docs/36-REPOSITORY-HISTORY.md`. ### Security +- **`ScanTarget::expand_hosts` is bounded and fallible.** It was + `network.iter().collect()` with no limit, so `prtip -sT -p 80 0.0.0.0/0` -- + eight characters of user input -- attempted to allocate 4.3 billion addresses, + roughly 68 GB. Verified before and after: the old build banner-printed and then + hung until SIGKILL (exit 137); it now exits 1 immediately with + `Invalid target: 0.0.0.0/0 contains 4294967296 addresses, above the 16777216 + limit for host expansion`. A self-inflicted denial of service on the machine + running the scan. + Earlier releases were saved from this only by accident -- expanding a `/0` + overflowed an integer and panicked fast. `ipnetwork` 0.21 removed the overflow, + converting a loud failure into a silent hang, which is how this surfaced. The + limit is the check that panic was standing in for, and is what the existing + test's own "Future enhancement: should validate CIDR size before expansion" + note had been asking for. + + All nine RustSec advisories affecting crates this project compiles are resolved, along with every unmaintained/unsound warning. `cargo audit` and `cargo deny check` both exit 0, for the workspace and for the `fuzz` crate, and @@ -91,6 +107,26 @@ along with every unmaintained/unsound warning. `cargo audit` and target. The exemption lives in `.cargo/audit.toml` (and `fuzz/.cargo/`), with the condition under which it must be removed. +### Fixed (target counting) + +- **`host_count()` reported 0 usable hosts for a `/31`.** RFC 3021 makes both + addresses of a point-to-point `/31` usable, but the code subtracted a network + and broadcast address unconditionally below `/32`. Harmless while nothing + trusted the number; it became `Scanner error: Result queue full (0/0)` the + moment the scheduler sized its queue from it. +- **`address_count()` split from `host_count()`.** They answer different + questions -- every address versus usable hosts -- and conflating them sized a + result queue at 2 for a `/30` that then scanned 4 addresses. The scheduler now + uses `address_count()`, which is pinned by test to equal exactly what + `expand_hosts()` yields; `host_count()` remains the figure to show a user. +- **Counting no longer allocates.** `scheduler.rs` computed + `targets.iter().map(|t| t.expand_hosts().len()).sum()` -- materialising every + address purely to count it. Now O(1) arithmetic. +- **`ScanTarget::first_host()` added.** Four call sites in `decoy_scanner.rs` + expanded an entire target to read `hosts[0]`, so a `/8` allocated 268 MB to + obtain one address. `first_host()` is `iter().next()`, and works even on + targets too large to expand. + ### Fixed (XML output) - **Service names were written unescaped into `-oX` output.** @@ -110,6 +146,45 @@ along with every unmaintained/unsound warning. `cargo audit` and valid. Nine tests cover it, including a negative control that walks every byte value and asserts the output contains no illegal code point. +### Changed (dependency majors) + +Supersedes Dependabot #3 and #4, which proposed the same wave from two +directions -- the "fuzz-deps" group turned out to rewrite the root workspace +manifest, because `fuzz/` depends on the workspace by path. + +- **`etherparse` and `socket2` removed.** Declared in three manifests each, + imported by **zero** source files -- the same pattern as `quick-xml` and + `tui-input`. The two riskiest majors in the group (`etherparse` 0.15 -> 0.21, + `socket2` 0.5 -> 0.6) needed deleting, not migrating. +- **`rand` 0.8 -> 0.9**, the only bump needing real code work: 24 `thread_rng`, + 17 `gen`/`gen::`, and 16 `gen_range` call sites across 10 files, renamed to + `rng`, `random`, and `random_range`. All were deprecations rather than + removals, so the build stayed green and only `-D warnings` caught them. +- **`sysinfo` 0.30 -> 0.38, not 0.39.** 0.39 requires Rust 1.95, well above this + workspace's 1.88 floor, and it is directly reachable from `prtip-core`. A local + toolchain newer than the MSRV builds it happily; only a metadata check against + the declared floor catches it. `refresh_cpu()` and `global_cpu_info()` became + `refresh_cpu_usage()` and `global_cpu_usage()`. +- **`ipnetwork` 0.20 -> 0.21** moved serde support behind a feature; `ScanTarget` + derives `Serialize`/`Deserialize` over `IpNetwork`, so `features = ["serde"]` + is now explicit. +- Also `colored` 3.1, `dirs` 6.0, `governor` 0.10, `mlua` 0.12, `nix` 0.31, + `rlimit` 0.11, `thiserror` 2.0, `toml` 1.1, `windows` 0.62, `x509-parser` 0.18 + -- all drop-in. +- **Crate-local pins folded into the workspace table.** `dirs`, `toml` and + `ipnetwork` were pinned separately inside member crates, so each was built + twice at two versions. +- Verified afterwards: 0 of 502 resolved packages require more than Rust 1.88. + +### Fixed (test harness) + +- **The CLI test helper treated a directory as a binary.** It selected the + release binary with `release_path.exists()`, which is true for a *directory*. + A `docker run -v "$PWD/target/release/prtip:/prtip"` with no release build + present makes Docker create the mount source as a root-owned directory, after + which all 52 CLI integration tests failed with a baffling `PermissionDenied` + instead of falling back to the perfectly good debug binary. Now `is_file()`. + ### Changed (supply chain) - **`.github/dependabot.yml` added.** The repository previously had no diff --git a/Cargo.lock b/Cargo.lock index 07a1143..f191a3e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -111,9 +111,9 @@ checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" [[package]] name = "asn1-rs" -version = "0.5.2" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6fd5ddaf0351dff5b8da21b2fb4ff8e08ddd02857f0bf69c47639106c0fff0" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" dependencies = [ "asn1-rs-derive", "asn1-rs-impl", @@ -121,31 +121,31 @@ dependencies = [ "nom", "num-traits", "rusticata-macros", - "thiserror 1.0.69", + "thiserror 2.0.20", "time", ] [[package]] name = "asn1-rs-derive" -version = "0.4.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "726535892e8eae7e70657b4c8ea93d26b8553afb1ce617caee529ef96d7dee6c" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", - "synstructure 0.12.6", + "syn 2.0.119", + "synstructure", ] [[package]] name = "asn1-rs-impl" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.119", ] [[package]] @@ -471,12 +471,11 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "colored" -version = "2.2.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "lazy_static", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -778,19 +777,6 @@ dependencies = [ "syn 3.0.4", ] -[[package]] -name = "dashmap" -version = "5.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" -dependencies = [ - "cfg-if", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core", -] - [[package]] name = "dashmap" version = "6.2.1" @@ -830,9 +816,9 @@ dependencies = [ [[package]] name = "der-parser" -version = "8.2.0" +version = "10.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbd676fbbab537128ef0278adb5576cf363cff6aa22a7b24effe97347cfab61e" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" dependencies = [ "asn1-rs", "displaydoc", @@ -902,23 +888,23 @@ dependencies = [ [[package]] name = "dirs" -version = "5.0.1" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ "dirs-sys", ] [[package]] name = "dirs-sys" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -995,15 +981,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "etherparse" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21696e6dfe1057a166a042c6d27b89a46aad2ee1003e6e1e03c49d54fd3270d7" -dependencies = [ - "arrayvec", -] - [[package]] name = "euclid" version = "0.22.14" @@ -1290,9 +1267,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 5.3.0", "wasip2", + "wasm-bindgen", ] [[package]] @@ -1314,22 +1293,25 @@ checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" [[package]] name = "governor" -version = "0.6.3" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68a7f542ee6b35af73b06abc0dad1c1bae89964e4e253bc4b587b91c9637867b" +checksum = "9efcab3c1958580ff1f25a2a41be1668f7603d849bb63af523b208a3cc1223b8" dependencies = [ "cfg-if", - "dashmap 5.5.3", - "futures", + "dashmap", + "futures-sink", "futures-timer", - "no-std-compat", + "futures-util", + "getrandom 0.3.4", + "hashbrown 0.16.1", "nonzero_ext", "parking_lot", "portable-atomic", "quanta", - "rand", + "rand 0.9.5", "smallvec", "spinning_top", + "web-time", ] [[package]] @@ -1476,7 +1458,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.2", + "windows-core", ] [[package]] @@ -1652,6 +1634,15 @@ dependencies = [ "serde", ] +[[package]] +name = "ipnetwork" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf370abdafd54d13e54a620e8c3e1145f28e46cc9d704bc6d94414559df41763" +dependencies = [ + "serde", +] + [[package]] name = "is-terminal" version = "0.4.17" @@ -1828,18 +1819,18 @@ dependencies = [ [[package]] name = "lua-src" -version = "550.0.0" +version = "550.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e836dc8ae16806c9bdcf42003a88da27d163433e3f9684c52f0301258004a4fb" +checksum = "75c110c2fa33f34e0de05448e1f3eb2e0631e7a69e2d8ae1586cffc9fc9f9949" dependencies = [ "cc", ] [[package]] name = "luajit-src" -version = "210.6.6+707c12b" +version = "210.7.2+b925b3e" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a86cc925d4053d0526ae7f5bc765dbd0d7a5d1a63d43974f4966cb349ca63295" +checksum = "920cf654b23d217c550ceea57c32cd2a413ea27b6d47ed77b5ee0cf655adefa6" dependencies = [ "cc", "which", @@ -1934,9 +1925,9 @@ dependencies = [ [[package]] name = "mlua" -version = "0.11.6" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccd36acfa49ce6ee56d1307a061dd302c564eee757e6e4cd67eb4f7204846fab" +checksum = "ad72ffa037cf5970c9860674f32f703fda25d86cf217475fe7a79c5f9961bcaa" dependencies = [ "bstr", "either", @@ -1945,14 +1936,13 @@ dependencies = [ "num-traits", "parking_lot", "rustc-hash", - "rustversion", ] [[package]] name = "mlua-sys" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f1c3a7fc7580227ece249fd90aa2fa3b39eb2b49d3aec5e103b3e85f2c3dfc8" +checksum = "92136787b906d4e55cfe96cd6c62e010bb1a56889d0d6cf83eb016dbad07576b" dependencies = [ "cc", "cfg-if", @@ -2001,34 +1991,29 @@ dependencies = [ [[package]] name = "nix" -version = "0.27.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ "bitflags 2.13.1", "cfg-if", + "cfg_aliases", "libc", + "memoffset", ] [[package]] name = "nix" -version = "0.29.0" +version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ "bitflags 2.13.1", "cfg-if", "cfg_aliases", "libc", - "memoffset", ] -[[package]] -name = "no-std-compat" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" - [[package]] name = "no-std-net" version = "0.6.0" @@ -2096,7 +2081,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand", + "rand 0.8.8", "smallvec", "zeroize", ] @@ -2166,11 +2151,30 @@ dependencies = [ "libc", ] +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "objc2-io-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" +dependencies = [ + "libc", + "objc2-core-foundation", +] + [[package]] name = "oid-registry" -version = "0.6.1" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bedf36ffb6ba96c2eb7144ef6270557b52e54b20c0a8e1eb2ff99a6c6959bff" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" dependencies = [ "asn1-rs", ] @@ -2418,7 +2422,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", - "rand", + "rand 0.8.8", ] [[package]] @@ -2516,7 +2520,7 @@ version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "682396b533413cc2e009fbb48aadf93619a149d3e57defba19ff50ce0201bd0d" dependencies = [ - "ipnetwork", + "ipnetwork 0.20.0", "pnet_base", "pnet_datalink", "pnet_packet", @@ -2539,7 +2543,7 @@ version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e79e70ec0be163102a332e1d2d5586d362ad76b01cec86f830241f2b6452a7b7" dependencies = [ - "ipnetwork", + "ipnetwork 0.20.0", "libc", "pnet_base", "pnet_sys", @@ -2696,7 +2700,7 @@ dependencies = [ "serde", "serde_json", "tempfile", - "thiserror 1.0.69", + "thiserror 2.0.20", "tokio", "toml", "tracing", @@ -2715,9 +2719,9 @@ dependencies = [ "filetime", "flate2", "indicatif", - "ipnetwork", + "ipnetwork 0.21.1", "parking_lot", - "rand", + "rand 0.9.5", "regex", "rkyv", "rlimit", @@ -2725,7 +2729,7 @@ dependencies = [ "serde_json", "sysinfo", "tempfile", - "thiserror 1.0.69", + "thiserror 2.0.20", "tokio", "toml", "tracing", @@ -2739,18 +2743,16 @@ dependencies = [ "anyhow", "bytes", "criterion", - "etherparse", "hwlocality", "libc", - "nix 0.27.1", + "nix 0.31.3", "parking_lot", "pnet", "pnet_datalink", "pnet_packet", "prtip-core", - "rand", - "socket2 0.5.10", - "thiserror 1.0.69", + "rand 0.9.5", + "thiserror 2.0.20", "tokio", "tracing", "windows", @@ -2764,12 +2766,11 @@ dependencies = [ "chrono", "criterion", "crossbeam", - "dashmap 6.2.1", - "etherparse", + "dashmap", "futures", "governor", "indicatif", - "ipnetwork", + "ipnetwork 0.21.1", "memmap2", "mlua", "native-tls", @@ -2780,16 +2781,15 @@ dependencies = [ "pnet_packet", "prtip-core", "prtip-network", - "rand", + "rand 0.9.5", "regex", "rkyv", "rustls", "serde", "serde_json", - "socket2 0.5.10", "sqlx", "tempfile", - "thiserror 1.0.69", + "thiserror 2.0.20", "tokio", "tokio-native-tls", "tokio-rustls", @@ -2808,12 +2808,12 @@ dependencies = [ "crossterm", "dirs", "futures", - "ipnetwork", + "ipnetwork 0.21.1", "parking_lot", "prtip-core", "ratatui", "tempfile", - "thiserror 1.0.69", + "thiserror 2.0.20", "tokio", "uuid", ] @@ -2890,8 +2890,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", ] [[package]] @@ -2901,7 +2911,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", ] [[package]] @@ -2913,6 +2933,15 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "ratatui" version = "0.30.2" @@ -3063,13 +3092,13 @@ dependencies = [ [[package]] name = "redox_users" -version = "0.4.6" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.17", "libredox", - "thiserror 1.0.69", + "thiserror 2.0.20", ] [[package]] @@ -3156,9 +3185,9 @@ dependencies = [ [[package]] name = "rlimit" -version = "0.10.2" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7043b63bd0cd1aaa628e476b80e6d4023a3b50eb32789f2728908107bd0c793a" +checksum = "f35ee2729c56bb610f6dba436bf78135f728b7373bdffae2ec815b2d3eb98cc3" dependencies = [ "libc", ] @@ -3176,7 +3205,7 @@ dependencies = [ "num-traits", "pkcs1", "pkcs8", - "rand_core", + "rand_core 0.6.4", "signature", "spki", "subtle", @@ -3366,11 +3395,11 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.9" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -3460,7 +3489,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -3496,16 +3525,6 @@ dependencies = [ "serde", ] -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "socket2" version = "0.6.5" @@ -3660,7 +3679,7 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", - "rand", + "rand 0.8.8", "rsa", "serde", "sha1", @@ -3699,7 +3718,7 @@ dependencies = [ "md-5", "memchr", "once_cell", - "rand", + "rand 0.8.8", "serde", "serde_json", "sha2", @@ -3825,18 +3844,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "synstructure" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", - "unicode-xid", -] - [[package]] name = "synstructure" version = "0.13.2" @@ -3850,16 +3857,15 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.30.13" +version = "0.38.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a5b4ddaee55fb2bea2bf0e5000747e5f5c0de765e5a5ff87f4cd106439f4bb3" +checksum = "92ab6a2f8bfe508deb3c6406578252e491d299cbbf3bc0529ecc3313aee4a52f" dependencies = [ - "cfg-if", - "core-foundation-sys", "libc", + "memchr", "ntapi", - "once_cell", - "rayon", + "objc2-core-foundation", + "objc2-io-kit", "windows", ] @@ -4086,7 +4092,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.5", + "socket2", "tokio-macros", "windows-sys 0.61.2", ] @@ -4135,44 +4141,42 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.23" +version = "1.1.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" dependencies = [ - "serde", + "indexmap", + "serde_core", "serde_spanned", "toml_datetime", - "toml_edit", + "toml_parser", + "toml_writer", + "winnow", ] [[package]] name = "toml_datetime" -version = "0.6.11" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ - "serde", + "serde_core", ] [[package]] -name = "toml_edit" -version = "0.22.27" +name = "toml_parser" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "toml_write", "winnow", ] [[package]] -name = "toml_write" -version = "0.1.2" +name = "toml_writer" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" [[package]] name = "tracing" @@ -4609,21 +4613,23 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.52.0" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" dependencies = [ - "windows-core 0.52.0", - "windows-targets 0.52.6", + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", ] [[package]] -name = "windows-core" -version = "0.52.0" +name = "windows-collections" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" dependencies = [ - "windows-targets 0.52.6", + "windows-core", ] [[package]] @@ -4639,6 +4645,17 @@ dependencies = [ "windows-strings", ] +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + [[package]] name = "windows-implement" version = "0.60.2" @@ -4667,6 +4684,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + [[package]] name = "windows-result" version = "0.4.1" @@ -4703,15 +4730,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.61.2" @@ -4752,6 +4770,15 @@ dependencies = [ "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -4844,12 +4871,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.7.15" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" -dependencies = [ - "memchr", -] +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" [[package]] name = "wit-bindgen" @@ -4865,9 +4889,9 @@ checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" [[package]] name = "x509-parser" -version = "0.15.1" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7069fba5b66b9193bd2c5d3d4ff12b839118f6bcbef5328efafafb5395cf63da" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" dependencies = [ "asn1-rs", "data-encoding", @@ -4876,7 +4900,7 @@ dependencies = [ "nom", "oid-registry", "rusticata-macros", - "thiserror 1.0.69", + "thiserror 2.0.20", "time", ] @@ -4900,7 +4924,7 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.119", - "synstructure 0.13.2", + "synstructure", ] [[package]] @@ -4941,7 +4965,7 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.119", - "synstructure 0.13.2", + "synstructure", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index b18f12c..528aba0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,9 +19,7 @@ tokio-util = "0.7" pnet = "0.35" pnet_datalink = "0.35" pnet_packet = "0.35" -socket2 = "0.5" pcap = "2.0" -etherparse = "0.15" # Concurrency crossbeam = "0.8" @@ -32,11 +30,11 @@ dashmap = "6.1" # Serialization serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -toml = "0.8" +toml = "1.1" # CLI clap = { version = "4.5", features = ["derive", "cargo"] } -colored = "2.1" +colored = "3.1" # Database rusqlite = { version = "0.31", features = ["bundled"] } @@ -57,7 +55,7 @@ tera = "1.19" printpdf = "0.6" # Home directory detection -dirs = "5.0" +dirs = "6.0" # Packet capture pcap-file = "2.0" @@ -68,24 +66,29 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] } # Error handling anyhow = "1.0" -thiserror = "1.0" +thiserror = "2.0" # Rate limiting -governor = "0.6" +governor = "0.10" # Utilities -ipnetwork = "0.20" -rand = "0.8" +# serde support moved behind a feature flag in 0.21; ScanTarget derives +# Serialize/Deserialize over IpNetwork, so it must be enabled explicitly. +ipnetwork = { version = "0.21", features = ["serde"] } +rand = "0.9" chrono = { version = "0.4", features = ["serde"] } uuid = { version = "1.7", features = ["v4", "serde"] } num_cpus = "1.16" -rlimit = "0.10" +rlimit = "0.11" indicatif = "0.18" regex = "1.11" -sysinfo = "0.30" +# 0.39 requires Rust 1.95, above this workspace's 1.88 MSRV. 0.38 is the +# newest line that fits the floor and carries the same CPU API +# (refresh_cpu_usage / global_cpu_usage). +sysinfo = "0.38" # Plugin system -mlua = { version = "0.11", features = ["lua54", "vendored", "send"] } +mlua = { version = "0.12", features = ["lua54", "vendored", "send"] } # TUI (Terminal User Interface) ratatui = "0.30" @@ -93,9 +96,9 @@ crossterm = { version = "0.29", features = ["event-stream"] } futures = "0.3" # Platform-specific (shared definitions) -nix = { version = "0.27", features = ["user", "process"] } +nix = { version = "0.31", features = ["user", "process"] } libc = "0.2" -windows = { version = "0.52", features = ["Win32_Security", "Win32_Foundation", "Win32_NetworkManagement_IpHelper", "Win32_UI_Shell"] } +windows = { version = "0.62", features = ["Win32_Security", "Win32_Foundation", "Win32_NetworkManagement_IpHelper", "Win32_UI_Shell"] } [workspace.package] version = "1.0.0" diff --git a/crates/prtip-cli/Cargo.toml b/crates/prtip-cli/Cargo.toml index 641aff4..d2bebac 100644 --- a/crates/prtip-cli/Cargo.toml +++ b/crates/prtip-cli/Cargo.toml @@ -65,7 +65,7 @@ console = "0.16" # Utilities num_cpus = { workspace = true } -dirs = "5.0" +dirs = { workspace = true } parking_lot = { workspace = true } # Platform-specific diff --git a/crates/prtip-cli/tests/common/mod.rs b/crates/prtip-cli/tests/common/mod.rs index a93371f..bea5d02 100644 --- a/crates/prtip-cli/tests/common/mod.rs +++ b/crates/prtip-cli/tests/common/mod.rs @@ -46,10 +46,17 @@ pub fn get_binary_path() -> PathBuf { release_path.push("release"); release_path.push(binary_name); - // Try release first (faster), then debug - if release_path.exists() { + // Try release first (faster), then debug. + // + // `is_file()` rather than `exists()`: a *directory* at either path is not a + // usable binary, and `exists()` returns true for one. That is not + // hypothetical -- `docker run -v "$PWD/target/release/prtip:/prtip"` with + // no release build present makes Docker create the mount source as a + // root-owned directory, after which every test here fails with a baffling + // `PermissionDenied` instead of falling back to the debug binary. + if release_path.is_file() { release_path - } else if debug_path.exists() { + } else if debug_path.is_file() { debug_path } else { panic!( diff --git a/crates/prtip-cli/tests/test_edge_cases.rs b/crates/prtip-cli/tests/test_edge_cases.rs index 6405191..14f8afe 100644 --- a/crates/prtip-cli/tests/test_edge_cases.rs +++ b/crates/prtip-cli/tests/test_edge_cases.rs @@ -141,8 +141,12 @@ fn test_cidr_slash_0_entire_internet() { stderr ); - // NOTE: This test documents current behavior (panic on overflow) - // Future enhancement: should validate CIDR size before expansion + // The "future enhancement" this note used to describe is now implemented: + // ScanTarget::expand_hosts validates the address count against + // MAX_EXPANDABLE_HOSTS and returns Error::InvalidTarget, so /0 is refused + // with a message rather than panicking on overflow (old behaviour) or + // hanging until the OOM killer intervenes (behaviour after ipnetwork 0.21 + // removed the overflow). } #[test] diff --git a/crates/prtip-core/Cargo.toml b/crates/prtip-core/Cargo.toml index 56f3ef4..f8611a1 100644 --- a/crates/prtip-core/Cargo.toml +++ b/crates/prtip-core/Cargo.toml @@ -29,7 +29,7 @@ tokio = { workspace = true } rand = { workspace = true } sysinfo = { workspace = true } flate2 = "1.0" -dirs = "5.0" +dirs = { workspace = true } rkyv = { workspace = true } [dev-dependencies] diff --git a/crates/prtip-core/src/resource_monitor.rs b/crates/prtip-core/src/resource_monitor.rs index bac6c63..8506542 100644 --- a/crates/prtip-core/src/resource_monitor.rs +++ b/crates/prtip-core/src/resource_monitor.rs @@ -155,10 +155,12 @@ impl ResourceMonitor { // Refresh system information self.system.refresh_memory(); - self.system.refresh_cpu(); + // sysinfo 0.39 split `refresh_cpu()` into targeted refreshes; only + // usage is read here, so the frequency probe is not paid for. + self.system.refresh_cpu_usage(); let available_memory = self.system.available_memory(); - let cpu_usage = self.system.global_cpu_info().cpu_usage(); + let cpu_usage = self.system.global_cpu_usage(); let memory_ok = available_memory >= self.config.memory_threshold; let cpu_ok = cpu_usage <= self.config.cpu_threshold; @@ -183,7 +185,7 @@ impl ResourceMonitor { /// Get CPU usage percentage (0-100) pub fn cpu_usage(&self) -> f32 { - self.system.global_cpu_info().cpu_usage() + self.system.global_cpu_usage() } /// Get the last checked status without refreshing diff --git a/crates/prtip-core/src/retry.rs b/crates/prtip-core/src/retry.rs index 0f55731..3e8f37d 100644 --- a/crates/prtip-core/src/retry.rs +++ b/crates/prtip-core/src/retry.rs @@ -229,7 +229,8 @@ where /// The jitter factor is randomly chosen between 0.75 and 1.25, giving a ±25% variation. fn add_jitter(delay: Duration) -> Duration { use rand::Rng; - let jitter_factor = rand::thread_rng().gen_range(0.75..=1.25); + // rand 0.9 renamed `thread_rng` to `rng` and `gen_range` to `random_range`. + let jitter_factor = rand::rng().random_range(0.75..=1.25); Duration::from_secs_f64(delay.as_secs_f64() * jitter_factor) } diff --git a/crates/prtip-core/src/types.rs b/crates/prtip-core/src/types.rs index af26cb5..6fdfc25 100644 --- a/crates/prtip-core/src/types.rs +++ b/crates/prtip-core/src/types.rs @@ -80,18 +80,16 @@ impl ScanTarget { } } - /// Get the number of hosts in this target - pub fn host_count(&self) -> u64 { + /// Total addresses in this target, including network and broadcast. + /// + /// This is what [`Self::expand_hosts`] yields, so it is the number to size + /// buffers and queues from. Distinct from [`Self::host_count`], which + /// reports *usable* hosts and is therefore smaller for most prefixes -- + /// conflating the two sized a result queue at 2 for a /30 that then scanned + /// 4 addresses. + pub fn address_count(&self) -> u64 { match self.network { - IpNetwork::V4(net) => { - let size = 2u64.pow((32 - net.prefix()) as u32); - // Subtract network and broadcast addresses for non-/32 - if net.prefix() < 32 { - size.saturating_sub(2) - } else { - size - } - } + IpNetwork::V4(net) => 2u64.pow((32 - net.prefix()) as u32), IpNetwork::V6(net) => { let prefix = net.prefix(); if prefix >= 64 { @@ -103,9 +101,77 @@ impl ScanTarget { } } - /// Expand into individual host IPs - pub fn expand_hosts(&self) -> Vec { - self.network.iter().collect() + /// Number of *usable* hosts in this target. + /// + /// Excludes the network and broadcast addresses where they exist, so this + /// is the figure to show a user. For anything that must match what is + /// actually scanned, use [`Self::address_count`]. + pub fn host_count(&self) -> u64 { + match self.network { + IpNetwork::V4(net) => { + let size = self.address_count(); + match net.prefix() { + // A /32 is a single host: no network or broadcast address. + 32 => size, + // A /31 likewise -- RFC 3021 makes both addresses usable on + // a point-to-point link. Subtracting 2 here returned 0 + // usable hosts, which surfaced as "Result queue full (0/0)". + 31 => size, + _ => size.saturating_sub(2), + } + } + IpNetwork::V6(_) => self.address_count(), + } + } + + /// First address in the target, without expanding it. + /// + /// `network.iter().next()` is O(1). Several callers need only the first + /// host -- to pick an IP version, or to key a backoff table -- and were + /// materialising the entire address list to index `[0]`, which meant a /8 + /// allocated 268 MB to read one address. + /// + /// Returns `None` only for a network that yields no addresses. + pub fn first_host(&self) -> Option { + self.network.iter().next() + } + + /// Largest target set this will materialise into a `Vec`. + /// + /// 16.7 million is a /8 -- larger than any legitimate single target, and + /// about 268 MB of `IpAddr` before any per-host scan state. Above this, + /// expansion is refused rather than attempted. + pub const MAX_EXPANDABLE_HOSTS: u64 = 1 << 24; + + /// Expand into individual host IPs. + /// + /// # Errors + /// + /// Returns [`Error::InvalidTarget`] if the target contains more than + /// [`Self::MAX_EXPANDABLE_HOSTS`] addresses. + /// + /// This is fallible on purpose. The address count comes from user input and + /// grows as `2^(32 - prefix)`, so an unbounded `iter().collect()` turns + /// `0.0.0.0/0` into an attempt to allocate 4.3 billion addresses -- roughly + /// 68 GB. That is a self-inflicted denial of service on the machine running + /// the scan, triggered by eight characters of input. + /// + /// Earlier versions were saved from this by accident: expanding a `/0` + /// overflowed an integer and panicked quickly. `ipnetwork` 0.21 removed the + /// overflow, which turned a fast, loud failure into a slow hang ending in + /// the OOM killer. The bound is the fix the panic was standing in for. + pub fn expand_hosts(&self) -> Result> { + let count = self.address_count(); + if count > Self::MAX_EXPANDABLE_HOSTS { + return Err(Error::InvalidTarget(format!( + "{} contains {} addresses, above the {} limit for host expansion. \ + Narrow the prefix (a /8 is the widest accepted) or split the scan.", + self.network, + count, + Self::MAX_EXPANDABLE_HOSTS + ))); + } + Ok(self.network.iter().collect()) } } @@ -944,6 +1010,96 @@ mod tests { assert!(PortRange::parse("").is_err()); } + #[test] + fn test_host_count_matches_expansion() { + // These two must never disagree: the scheduler sizes its result queue + // from host_count() and then scans expand_hosts(). They diverged for + // /31, where host_count() returned 0 while expansion produced 2. + for cidr in [ + "192.168.1.1/32", + "192.168.1.0/31", + "192.168.1.0/30", + "192.168.1.0/29", + "192.168.1.0/24", + ] { + let t = ScanTarget::parse(cidr).unwrap(); + let expanded = t.expand_hosts().unwrap().len() as u64; + assert_eq!( + t.address_count(), + expanded, + "{cidr}: address_count() must equal what expand_hosts() yields" + ); + assert!( + t.host_count() <= t.address_count(), + "{cidr}: usable hosts cannot exceed total addresses" + ); + } + } + + #[test] + fn test_host_count_slash_31_is_two() { + // RFC 3021: both addresses of a /31 are usable on a point-to-point + // link, so there is nothing to subtract. + let t = ScanTarget::parse("192.168.1.0/31").unwrap(); + assert_eq!(t.host_count(), 2); + assert_eq!(t.expand_hosts().unwrap().len(), 2); + } + + #[test] + fn test_expand_hosts_within_limit() { + let target = ScanTarget::parse("192.168.1.0/30").unwrap(); + let hosts = target + .expand_hosts() + .expect("a /30 is well within the limit"); + assert_eq!(hosts.len(), 4); + } + + #[test] + fn test_expand_hosts_refuses_entire_internet() { + // The regression this guards: `ipnetwork` 0.21 stopped overflowing on a + // /0, so `iter().collect()` went from a fast panic to an attempt to + // allocate 4.3 billion addresses -- a hang ending in the OOM killer, + // reachable from eight characters of user input. + let target = ScanTarget::parse("0.0.0.0/0").unwrap(); + let err = target + .expand_hosts() + .expect_err("/0 must be refused, not attempted"); + + let msg = err.to_string(); + // 2^32 -- every address, which is what expansion would allocate. Not + // 4294967294 (host_count), because the cap measures the allocation. + assert!( + msg.contains("4294967296"), + "should state the real size: {msg}" + ); + assert!(msg.contains("limit"), "should name the limit: {msg}"); + } + + #[test] + fn test_expand_hosts_limit_boundary() { + // A /8 is exactly at the cap and must still be permitted; a /7 is over. + let at_limit = ScanTarget::parse("10.0.0.0/8").unwrap(); + assert!(at_limit.host_count() <= ScanTarget::MAX_EXPANDABLE_HOSTS); + + let over_limit = ScanTarget::parse("10.0.0.0/7").unwrap(); + assert!(over_limit.host_count() > ScanTarget::MAX_EXPANDABLE_HOSTS); + assert!(over_limit.expand_hosts().is_err()); + } + + #[test] + fn test_first_host_does_not_require_expansion() { + // Must work for a target far too large to expand -- that is the point. + let huge = ScanTarget::parse("0.0.0.0/0").unwrap(); + assert!(huge.expand_hosts().is_err()); + assert!(huge.first_host().is_some()); + + let small = ScanTarget::parse("192.168.1.0/30").unwrap(); + assert_eq!( + small.first_host(), + small.expand_hosts().unwrap().first().copied() + ); + } + #[test] fn test_scan_target_single_ip() { let target = ScanTarget::parse("192.168.1.1").unwrap(); diff --git a/crates/prtip-core/tests/integration.rs b/crates/prtip-core/tests/integration.rs index 85f5d8d..408877e 100644 --- a/crates/prtip-core/tests/integration.rs +++ b/crates/prtip-core/tests/integration.rs @@ -40,7 +40,9 @@ fn test_scan_result_serialization() { #[test] fn test_scan_target_expansion() { let target = ScanTarget::parse("192.168.1.0/30").unwrap(); - let hosts = target.expand_hosts(); + let hosts = target + .expand_hosts() + .expect("target is small enough to expand"); assert_eq!(hosts.len(), 4); // /30 = 4 addresses } diff --git a/crates/prtip-network/Cargo.toml b/crates/prtip-network/Cargo.toml index 56fd9a1..08efc4c 100644 --- a/crates/prtip-network/Cargo.toml +++ b/crates/prtip-network/Cargo.toml @@ -21,8 +21,6 @@ tokio = { workspace = true } pnet = { workspace = true } pnet_datalink = { workspace = true } pnet_packet = { workspace = true } -socket2 = { workspace = true } -etherparse = { workspace = true } bytes = "1.9" thiserror = { workspace = true } anyhow = { workspace = true } diff --git a/crates/prtip-network/src/fragmentation.rs b/crates/prtip-network/src/fragmentation.rs index 7b9cae8..33b3763 100644 --- a/crates/prtip-network/src/fragmentation.rs +++ b/crates/prtip-network/src/fragmentation.rs @@ -131,7 +131,7 @@ pub fn fragment_tcp_packet(packet: &[u8], mtu: usize) -> Result>> { // Generate unique fragment ID use rand::Rng; - let fragment_id = rand::thread_rng().gen::(); + let fragment_id = rand::rng().random::(); let mut fragments = Vec::new(); let mut offset_bytes = 0; diff --git a/crates/prtip-network/src/ipv6_packet.rs b/crates/prtip-network/src/ipv6_packet.rs index c9d291f..4338c8f 100644 --- a/crates/prtip-network/src/ipv6_packet.rs +++ b/crates/prtip-network/src/ipv6_packet.rs @@ -308,7 +308,7 @@ impl Ipv6PacketBuilder { let mut fragments = Vec::new(); let mut offset = 0; use rand::Rng; - let fragment_id: u32 = rand::thread_rng().gen(); + let fragment_id: u32 = rand::rng().random(); while offset < self.payload.len() { let remaining = self.payload.len() - offset; diff --git a/crates/prtip-network/src/packet_builder.rs b/crates/prtip-network/src/packet_builder.rs index f90f8f2..436702b 100644 --- a/crates/prtip-network/src/packet_builder.rs +++ b/crates/prtip-network/src/packet_builder.rs @@ -187,7 +187,7 @@ impl TcpPacketBuilder { /// Create a new TCP packet builder with default values pub fn new() -> Self { use rand::Rng; - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); Self { src_mac: None, @@ -195,10 +195,10 @@ impl TcpPacketBuilder { src_ip: None, dst_ip: None, ttl: 64, // Standard Linux default - ip_id: rng.gen(), + ip_id: rng.random(), src_port: None, dst_port: None, - seq: rng.gen(), + seq: rng.random(), ack: 0, flags: TcpFlags::empty(), window: 65535, @@ -898,7 +898,7 @@ impl UdpPacketBuilder { /// Create a new UDP packet builder with default values pub fn new() -> Self { use rand::Rng; - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); Self { src_mac: None, @@ -906,7 +906,7 @@ impl UdpPacketBuilder { src_ip: None, dst_ip: None, ttl: 64, - ip_id: rng.gen(), + ip_id: rng.random(), src_port: None, dst_port: None, payload: Vec::new(), diff --git a/crates/prtip-scanner/Cargo.toml b/crates/prtip-scanner/Cargo.toml index 3296e21..28ee4ad 100644 --- a/crates/prtip-scanner/Cargo.toml +++ b/crates/prtip-scanner/Cargo.toml @@ -29,8 +29,6 @@ tokio = { workspace = true } # Networking pnet = { workspace = true } pnet_packet = { workspace = true } -socket2 = { workspace = true } -etherparse = { workspace = true } ipnetwork = { workspace = true } # TLS support @@ -38,7 +36,7 @@ tokio-native-tls = "0.3" native-tls = "0.2" rustls = "0.23" tokio-rustls = "0.26" -x509-parser = "0.15" +x509-parser = "0.18" # Database sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite", "chrono"] } @@ -62,7 +60,7 @@ tracing = { workspace = true } # Serialization serde = { workspace = true } serde_json = { workspace = true } -toml = "0.8" +toml = { workspace = true } # Time chrono = { workspace = true } diff --git a/crates/prtip-scanner/src/decoy_scanner.rs b/crates/prtip-scanner/src/decoy_scanner.rs index 585b3da..7203e08 100644 --- a/crates/prtip-scanner/src/decoy_scanner.rs +++ b/crates/prtip-scanner/src/decoy_scanner.rs @@ -277,17 +277,17 @@ impl DecoyScanner { /// Generate random IPv4 decoy IPs fn generate_random_decoys_ipv4(count: usize, exclude: &[Ipv4Addr]) -> Vec { - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); let mut decoys = Vec::with_capacity(count); let exclude_set: HashSet = exclude.iter().copied().collect(); // Generate random IPs avoiding reserved ranges while decoys.len() < count { let ip = Ipv4Addr::new( - rng.gen_range(1..224), // Avoid 0.x and 224+ (multicast) - rng.gen_range(0..=255), - rng.gen_range(0..=255), - rng.gen_range(1..255), // Avoid .0 and .255 + rng.random_range(1..224), // Avoid 0.x and 224+ (multicast) + rng.random_range(0..=255), + rng.random_range(0..=255), + rng.random_range(1..255), // Avoid .0 and .255 ); // Skip reserved ranges and duplicates @@ -317,7 +317,7 @@ impl DecoyScanner { ]; // First 4 u16 segments = 64 bits let mut decoys = Vec::with_capacity(count); - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); let mut attempts = 0; const MAX_ATTEMPTS: usize = 10000; // Prevent infinite loops @@ -326,10 +326,10 @@ impl DecoyScanner { // Generate random interface identifier (last 64 bits) let iid = [ - rng.gen::(), - rng.gen::(), - rng.gen::(), - rng.gen::(), + rng.random::(), + rng.random::(), + rng.random::(), + rng.random::(), ]; // Combine prefix + interface identifier @@ -429,7 +429,7 @@ impl DecoyScanner { if all_decoys.is_empty() { 0 } else { - rand::thread_rng().gen_range(0..=all_decoys.len()) + rand::rng().random_range(0..=all_decoys.len()) } } }; @@ -468,10 +468,10 @@ impl DecoyScanner { // Get real source IP (from network interface) let real_source = self.get_source_ip(&target)?; - // Get target IP for backoff check - let hosts = target.expand_hosts(); - let target_ip = if !hosts.is_empty() { - hosts[0] + // Get target IP for backoff check. Only the first host is needed, so + // this must not expand the whole target. + let target_ip = if let Some(first) = target.first_host() { + first } else { return Err(Error::Network("No hosts in target".to_string())); }; @@ -523,7 +523,7 @@ impl DecoyScanner { .config .network .source_port - .unwrap_or_else(|| rand::thread_rng().gen_range(10000..60000)); + .unwrap_or_else(|| rand::rng().random_range(10000..60000)); let key = ConnectionKey { src_ip: real_source, @@ -546,8 +546,7 @@ impl DecoyScanner { // Small random delay between decoys to appear more natural if i < send_order.len() - 1 { - let delay_us = - rand::thread_rng().gen_range(MIN_DECOY_DELAY_US..=MAX_DECOY_DELAY_US); + let delay_us = rand::rng().random_range(MIN_DECOY_DELAY_US..=MAX_DECOY_DELAY_US); time::sleep(Duration::from_micros(delay_us)).await; } } @@ -563,10 +562,9 @@ impl DecoyScanner { // For now, use a placeholder - should integrate with interface detection // In production, this would query routing table or use configured source IP - // Determine IP version from target - let hosts = target.expand_hosts(); - if !hosts.is_empty() { - match hosts[0] { + // Determine IP version from target (first host suffices) + if let Some(first) = target.first_host() { + match first { IpAddr::V4(_) => Ok(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 10))), // IPv4 placeholder IpAddr::V6(_) => Ok(IpAddr::V6(Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 1))), // IPv6 placeholder (link-local) } @@ -583,13 +581,12 @@ impl DecoyScanner { port: u16, source_ip: IpAddr, ) -> Result>> { - // Extract first host from target - let hosts = target.expand_hosts(); - if hosts.is_empty() { + // Extract first host from target (no expansion needed) + let Some(first_host) = target.first_host() else { return Err(Error::Network("No hosts in target".to_string())); - } + }; - let dest_ip = hosts[0]; + let dest_ip = first_host; // Ensure IP versions match if (source_ip.is_ipv4() && dest_ip.is_ipv6()) || (source_ip.is_ipv6() && dest_ip.is_ipv4()) @@ -604,14 +601,14 @@ impl DecoyScanner { .config .network .source_port - .unwrap_or_else(|| rand::thread_rng().gen_range(10000..60000)); + .unwrap_or_else(|| rand::rng().random_range(10000..60000)); // Build SYN packet (dual-stack support) let mut builder = TcpPacketBuilder::new() .source_port(src_port) .dest_port(port) .flags(TcpFlags::SYN) - .sequence(rand::thread_rng().gen()) + .sequence(rand::rng().random()) .window(65535); // Apply evasion features from Sprint 4.20 @@ -702,11 +699,8 @@ impl DecoyScanner { ) -> Result { use chrono::Utc; - // Get first host IP from target - let hosts = target.expand_hosts(); - let target_ip = if !hosts.is_empty() { - hosts[0] - } else { + // Get first host IP from target (no expansion needed) + let Some(target_ip) = target.first_host() else { return Err(Error::Network("No hosts in target".to_string())); }; @@ -906,9 +900,9 @@ impl DecoyScanner { /// Shuffle decoy order using Fisher-Yates (supports IPv4 and IPv6) fn shuffle_decoys(&self, decoys: &mut [IpAddr]) { - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); for i in (1..decoys.len()).rev() { - let j = rng.gen_range(0..=i); + let j = rng.random_range(0..=i); decoys.swap(i, j); } } diff --git a/crates/prtip-scanner/src/idle/idle_scanner.rs b/crates/prtip-scanner/src/idle/idle_scanner.rs index 8061d9e..38b2a81 100644 --- a/crates/prtip-scanner/src/idle/idle_scanner.rs +++ b/crates/prtip-scanner/src/idle/idle_scanner.rs @@ -426,16 +426,16 @@ async fn send_spoofed_syn(zombie_ip: IpAddr, target_ip: IpAddr, target_port: u16 } }; - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); // 1. Build TCP SYN packet let mut tcp_buffer = vec![0u8; 20]; // TCP header (no options) let mut tcp_packet = MutableTcpPacket::new(&mut tcp_buffer) .ok_or_else(|| Error::Scanner("Failed to create TCP packet".into()))?; - tcp_packet.set_source(rng.gen::()); // Random source port + tcp_packet.set_source(rng.random::()); // Random source port tcp_packet.set_destination(target_port); - tcp_packet.set_sequence(rng.gen::()); // Random ISN + tcp_packet.set_sequence(rng.random::()); // Random ISN tcp_packet.set_flags(TcpFlags::SYN); tcp_packet.set_window(65535); tcp_packet.set_data_offset(5); // 20 bytes / 4 = 5 diff --git a/crates/prtip-scanner/src/scheduler.rs b/crates/prtip-scanner/src/scheduler.rs index 9741b37..09eedcf 100644 --- a/crates/prtip-scanner/src/scheduler.rs +++ b/crates/prtip-scanner/src/scheduler.rs @@ -361,7 +361,7 @@ impl ScanScheduler { pcapng_writer: Option>>, ) -> Result> { // Expand target into individual IPs - let original_hosts = target.expand_hosts(); + let original_hosts = target.expand_hosts()?; debug!("Target expanded to {} hosts", original_hosts.len()); // Filter CDN IPs if enabled @@ -654,7 +654,7 @@ impl ScanScheduler { // Expand all targets to individual IPs let mut original_ips = Vec::new(); for target in &targets { - original_ips.extend(target.expand_hosts()); + original_ips.extend(target.expand_hosts()?); } // Filter CDN IPs if enabled @@ -830,7 +830,13 @@ impl ScanScheduler { let ports_vec: Vec = ports.iter().collect(); // Calculate estimated hosts for progress bar and buffer sizing - let estimated_hosts: usize = targets.iter().map(|t| t.expand_hosts().len()).sum(); + // host_count() is O(1) arithmetic; expand_hosts().len() allocated the + // entire address list purely to count it. + // address_count(), not host_count(): this sizes a queue for what + // expand_hosts() actually yields, which includes the network and + // broadcast addresses. host_count() reports usable hosts and would + // undersize the queue by 2 per target. + let estimated_hosts: usize = targets.iter().map(|t| t.address_count() as usize).sum(); // Create progress bar for real-time feedback let total_ports = (estimated_hosts * ports_vec.len()) as u64; @@ -873,7 +879,7 @@ impl ScanScheduler { let mut progress_tracker = ProgressTracker::new(scan_id, total_ports); for target in targets { - let original_hosts = target.expand_hosts(); + let original_hosts = target.expand_hosts()?; // Filter CDN IPs if enabled let hosts = if let Some(ref detector) = self.cdn_detector { diff --git a/crates/prtip-scanner/src/stealth_scanner.rs b/crates/prtip-scanner/src/stealth_scanner.rs index bdc1e03..31e325a 100644 --- a/crates/prtip-scanner/src/stealth_scanner.rs +++ b/crates/prtip-scanner/src/stealth_scanner.rs @@ -423,7 +423,7 @@ impl StealthScanner { .config .network .source_port - .unwrap_or_else(|| rand::thread_rng().gen_range(1024..65535)); + .unwrap_or_else(|| rand::rng().random_range(1024..65535)); // Send probe self.send_probe(target, port, src_port, scan_type, pcapng_writer.clone()) @@ -527,8 +527,8 @@ impl StealthScanner { pcapng_writer: Option>>, ) -> Result<()> { use rand::Rng; - let mut rng = rand::thread_rng(); - let sequence: u32 = rng.gen(); + let mut rng = rand::rng(); + let sequence: u32 = rng.random(); // Get appropriate local IP for target let local_ip = self.get_local_ip_for_target(target)?; @@ -991,7 +991,7 @@ impl StealthScanner { // Generate random source port let src_port = { use rand::Rng; - rand::thread_rng().gen_range(32768..=61000) + rand::rng().random_range(32768..=61000) }; // Build packet based on target IP version diff --git a/crates/prtip-scanner/src/syn_scanner.rs b/crates/prtip-scanner/src/syn_scanner.rs index 0e60814..ba19f0e 100644 --- a/crates/prtip-scanner/src/syn_scanner.rs +++ b/crates/prtip-scanner/src/syn_scanner.rs @@ -218,7 +218,7 @@ impl SynScanner { .config .network .source_port - .unwrap_or_else(|| rand::thread_rng().gen_range(1024..65535)); + .unwrap_or_else(|| rand::rng().random_range(1024..65535)); // Send initial SYN let sequence = self @@ -349,10 +349,10 @@ impl SynScanner { pcapng_writer: Option>>, ) -> Result { use rand::Rng; - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); // Generate sequence number (for stateless, could use SipHash) - let sequence: u32 = rng.gen(); + let sequence: u32 = rng.random(); // Get appropriate local IP for target let local_ip = self.get_local_ip_for_target(target)?; @@ -562,8 +562,8 @@ impl SynScanner { /// ``` fn build_syn_packet(&self, target: IpAddr, port: u16, src_port: u16) -> Result> { use rand::Rng; - let mut rng = rand::thread_rng(); - let sequence: u32 = rng.gen(); + let mut rng = rand::rng(); + let sequence: u32 = rng.random(); // Get appropriate local IP for target let local_ip = self.get_local_ip_for_target(target)?; @@ -980,7 +980,7 @@ impl SynScanner { batch_size: usize, ) -> Result>> { use rand::Rng; - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); let mut packets = Vec::with_capacity(batch_size.min(ports.len())); for &port in ports.iter().take(batch_size) { @@ -989,7 +989,7 @@ impl SynScanner { .config .network .source_port - .unwrap_or_else(|| rng.gen_range(1024..65535)); + .unwrap_or_else(|| rng.random_range(1024..65535)); // Build packet using existing method let packet = self.build_syn_packet(target, port, src_port)?; @@ -999,7 +999,7 @@ impl SynScanner { target_ip: target, target_port: port, source_port: src_port, - sequence: rng.gen(), // Sequence number embedded in packet + sequence: rng.random(), // Sequence number embedded in packet sent_time: Instant::now(), retries: 0, }; diff --git a/crates/prtip-scanner/src/tcp_connect.rs b/crates/prtip-scanner/src/tcp_connect.rs index bb76dbc..2714a17 100644 --- a/crates/prtip-scanner/src/tcp_connect.rs +++ b/crates/prtip-scanner/src/tcp_connect.rs @@ -519,7 +519,7 @@ impl TcpConnectScanner { ports: Vec, max_concurrent: usize, ) -> Result> { - let hosts = target.expand_hosts(); + let hosts = target.expand_hosts()?; let mut all_results = Vec::new(); for host in hosts { diff --git a/crates/prtip-scanner/src/timing.rs b/crates/prtip-scanner/src/timing.rs index 6370037..fd765c3 100644 --- a/crates/prtip-scanner/src/timing.rs +++ b/crates/prtip-scanner/src/timing.rs @@ -149,14 +149,14 @@ impl TimingConfig { } use rand::Rng; - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); // Jitter range: [duration * (1 - factor), duration * (1 + factor)] let millis = duration.as_millis() as f64; let min_millis = millis * (1.0 - self.jitter_factor); let max_millis = millis * (1.0 + self.jitter_factor); - let jittered_millis = rng.gen_range(min_millis..max_millis); + let jittered_millis = rng.random_range(min_millis..max_millis); Duration::from_millis(jittered_millis as u64) } } diff --git a/crates/prtip-scanner/src/udp_scanner.rs b/crates/prtip-scanner/src/udp_scanner.rs index 5b7f169..ca2ce18 100644 --- a/crates/prtip-scanner/src/udp_scanner.rs +++ b/crates/prtip-scanner/src/udp_scanner.rs @@ -411,7 +411,7 @@ impl UdpScanner { .config .network .source_port - .unwrap_or_else(|| rand::thread_rng().gen_range(1024..65535)); + .unwrap_or_else(|| rand::rng().random_range(1024..65535)); // Get protocol-specific payload if available let payload = get_udp_payload(port).unwrap_or_default(); @@ -924,7 +924,7 @@ impl UdpScanner { .config .network .source_port - .unwrap_or_else(|| rand::thread_rng().gen_range(1024..65535)); + .unwrap_or_else(|| rand::rng().random_range(1024..65535)); // Get protocol-specific payload let payload = get_udp_payload(port).unwrap_or_default(); diff --git a/crates/prtip-scanner/tests/common/error_injection.rs b/crates/prtip-scanner/tests/common/error_injection.rs index 8bc1658..6efccb7 100644 --- a/crates/prtip-scanner/tests/common/error_injection.rs +++ b/crates/prtip-scanner/tests/common/error_injection.rs @@ -136,7 +136,7 @@ impl ErrorInjector { } FailureMode::Probabilistic { rate } => { use rand::Rng; - if rand::thread_rng().gen::() < *rate { + if rand::rng().random::() < *rate { Err(io::Error::new(io::ErrorKind::ConnectionRefused, "probabilistic failure")) } else { Ok(()) diff --git a/crates/prtip-scanner/tests/integration_scanner.rs b/crates/prtip-scanner/tests/integration_scanner.rs index 2e68060..93db6ab 100644 --- a/crates/prtip-scanner/tests/integration_scanner.rs +++ b/crates/prtip-scanner/tests/integration_scanner.rs @@ -272,7 +272,9 @@ async fn test_storage_multiple_scans() { async fn test_target_expansion() { // Test CIDR expansion let target = ScanTarget::parse("192.168.1.0/30").unwrap(); - let hosts = target.expand_hosts(); + let hosts = target + .expand_hosts() + .expect("target is small enough to expand"); // /30 = 4 addresses - 2 (network/broadcast) = 2 usable assert_eq!(hosts.len(), 4); // pnet includes network/broadcast diff --git a/crates/prtip-tui/Cargo.toml b/crates/prtip-tui/Cargo.toml index df16bfa..e47e5ac 100644 --- a/crates/prtip-tui/Cargo.toml +++ b/crates/prtip-tui/Cargo.toml @@ -32,7 +32,7 @@ anyhow = { workspace = true } thiserror = { workspace = true } # Networking -ipnetwork = "0.20" +ipnetwork = { workspace = true } # Date/time handling chrono = { workspace = true } From 41e73fe582e114eb5ea76f2c0505f86f65ab928b Mon Sep 17 00:00:00 2001 From: DoubleGate Date: Fri, 28 Aug 2026 21:01:17 -0400 Subject: [PATCH 2/2] fix(windows): BOOL moved to windows::core in 0.62 `Test (windows-latest)` failed on this branch while the Linux suite was green: the `windows` 0.52 -> 0.62 bump moved `BOOL` out of `Win32::Foundation`. error[E0432]: unresolved import `windows::Win32::Foundation::BOOL` --> crates/prtip-network/src/privilege.rs:160:9 | no `BOOL` in `Win32::Foundation` Reproduced locally with `cargo check --target x86_64-pc-windows-gnu -p prtip-network` rather than waiting on CI; `prtip-network` now checks clean for that target. Both call sites -- `privilege.rs` and the privilege test -- import from `windows::core` instead. A whole-workspace Windows check still stops at `aws-lc-sys`, which needs a Windows C toolchain this machine does not have. That is a local cross-compilation limit, not a defect: CI builds natively on windows-latest with MSVC. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NBDCJvLbq7nuor7RtT57rD --- crates/prtip-network/src/privilege.rs | 4 +++- crates/prtip-network/tests/test_security_privilege.rs | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/prtip-network/src/privilege.rs b/crates/prtip-network/src/privilege.rs index 58bf86e..12e2d94 100644 --- a/crates/prtip-network/src/privilege.rs +++ b/crates/prtip-network/src/privilege.rs @@ -157,7 +157,9 @@ fn unix_drop_privileges(user: &str, group: &str) -> Result<()> { #[cfg(target_os = "windows")] fn windows_has_capability() -> Result { - use windows::Win32::Foundation::BOOL; + // `windows` 0.62 moved BOOL out of Win32::Foundation into the + // windows-core re-export. + use windows::core::BOOL; use windows::Win32::UI::Shell::IsUserAnAdmin; unsafe { diff --git a/crates/prtip-network/tests/test_security_privilege.rs b/crates/prtip-network/tests/test_security_privilege.rs index 616cc9d..692d118 100644 --- a/crates/prtip-network/tests/test_security_privilege.rs +++ b/crates/prtip-network/tests/test_security_privilege.rs @@ -350,7 +350,9 @@ fn test_security_capability_detection() { #[cfg(target_os = "windows")] { unsafe { - use windows::Win32::Foundation::BOOL; + // `windows` 0.62 moved BOOL out of Win32::Foundation into the + // windows-core re-export. + use windows::core::BOOL; let is_admin: BOOL = IsUserAnAdmin(); if is_admin.as_bool() { assert!(