diff --git a/.github/workflows/00-windows.yml b/.github/workflows/00-windows.yml index a7d39291..72415707 100644 --- a/.github/workflows/00-windows.yml +++ b/.github/workflows/00-windows.yml @@ -45,6 +45,21 @@ jobs: NPCAP_OEM_PASSWORD: ${{ secrets.NPCAP_OEM_PASSWORD }} NPCAP_OEM_USERNAME: ${{ secrets.NPCAP_OEM_USERNAME }} + windows-npcap-versions: + if: github.event_name != 'pull_request' + strategy: + fail-fast: false + matrix: + npcap: [ 'none', '1.10' ] + uses: './.github/workflows/01-build-and-test-windows.yml' + with: + os: 'windows-latest' + toolchain: 'stable' + npcap: ${{ matrix.npcap }} + secrets: + NPCAP_OEM_PASSWORD: ${{ secrets.NPCAP_OEM_PASSWORD }} + NPCAP_OEM_USERNAME: ${{ secrets.NPCAP_OEM_USERNAME }} + windows-lint-stable: uses: './.github/workflows/03-lint.yml' with: diff --git a/.github/workflows/01-build-and-test-windows.yml b/.github/workflows/01-build-and-test-windows.yml index 58521dda..3afb3ceb 100644 --- a/.github/workflows/01-build-and-test-windows.yml +++ b/.github/workflows/01-build-and-test-windows.yml @@ -11,6 +11,11 @@ on: required: false default: false type: boolean + npcap: + description: 'Npcap release to install, or none to leave the library out.' + required: false + default: '1.89' + type: string secrets: NPCAP_OEM_PASSWORD: required: true @@ -21,25 +26,20 @@ env: RUST_BACKTRACE: 1 CARGO_TERM_VERBOSE: true CARGO_TERM_COLOR: always - PCAP_CI_TEST_TARGETS: ${{ (github.event_name == 'pull_request') && '--lib' || '--all-targets' }} + PCAP_CI_TEST_TARGETS: ${{ (inputs.npcap == 'none' || github.event_name == 'pull_request') && '--lib' || '--all-targets' }} jobs: build-and-test: runs-on: ${{ inputs.os }} steps: - uses: actions/checkout@v7 - - run: | - Invoke-WebRequest -Uri "https://npcap.com/dist/npcap-sdk-1.16.zip" -OutFile "C:/npcap-sdk.zip" - Expand-Archive -LiteralPath C:/npcap-sdk.zip -DestinationPath C:/npcap-sdk - $arch = if ("${{ runner.arch }}" -eq "ARM64") { "ARM64" } else { "x64" } - echo "LIB=C:/npcap-sdk/Lib/$arch" >> $env:GITHUB_ENV # Secrets are not passed to workflows that are triggered by a pull request from a fork. # https://docs.github.com/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets - - if: github.event_name != 'pull_request' + - if: inputs.npcap != 'none' && github.event_name != 'pull_request' run: | $SecPassword = ConvertTo-SecureString "${{ secrets.NPCAP_OEM_PASSWORD }}" -AsPlainText -Force $CredObject = New-Object System.Management.Automation.PSCredential ("${{ secrets.NPCAP_OEM_USERNAME }}", $SecPassword) - Invoke-WebRequest -Uri "https://npcap.com/oem/dist/npcap-1.88-oem.exe" -OutFile C:/npcap-oem.exe -Credential $CredObject + Invoke-WebRequest -Uri "https://npcap.com/oem/dist/npcap-${{ inputs.npcap }}-oem.exe" -OutFile C:/npcap-oem.exe -Credential $CredObject C:/npcap-oem.exe /S - run: | rustup update --no-self-update ${{ inputs.toolchain }} diff --git a/.github/workflows/02-coverage.yml b/.github/workflows/02-coverage.yml index db6daefd..0994aa26 100644 --- a/.github/workflows/02-coverage.yml +++ b/.github/workflows/02-coverage.yml @@ -46,16 +46,11 @@ jobs: run: sudo apt-get install libpcap-dev - if: ${{ contains(inputs.os, 'macos') }} run: brew install libpcap - - if: ${{ contains(inputs.os, 'windows') }} - run: | - Invoke-WebRequest -Uri "https://npcap.com/dist/npcap-sdk-1.16.zip" -OutFile "C:/npcap-sdk.zip" - Expand-Archive -LiteralPath C:/npcap-sdk.zip -DestinationPath C:/npcap-sdk - echo "LIB=C:/npcap-sdk/Lib/x64" >> $env:GITHUB_ENV - if: ${{ contains(inputs.os, 'windows') && (github.event_name != 'pull_request') }} run: | $SecPassword = ConvertTo-SecureString "${{ secrets.NPCAP_OEM_PASSWORD }}" -AsPlainText -Force $CredObject = New-Object System.Management.Automation.PSCredential ("${{ secrets.NPCAP_OEM_USERNAME }}", $SecPassword) - Invoke-WebRequest -Uri "https://npcap.com/oem/dist/npcap-1.88-oem.exe" -OutFile C:/npcap-oem.exe -Credential $CredObject + Invoke-WebRequest -Uri "https://npcap.com/oem/dist/npcap-1.89-oem.exe" -OutFile C:/npcap-oem.exe -Credential $CredObject C:/npcap-oem.exe /S # No installation of actuall library since we cannot install OEM pcap on pull request branches # anyway. We'll just be running unit tests on Windows. No integration tests. diff --git a/CHANGELOG.md b/CHANGELOG.md index 73f5f08d..d2b2b086 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,10 @@ `Warning` carrying a `WarningCode` and the message that came with it. - `Error::PcapErrorCode`, carrying the `ErrorCode` libpcap failed with and the message it left behind. +- `Error` has a new `LibraryNotFound` variant on Windows, returned when `wpcap.dll` cannot be + loaded. +- `Error` has a new `EntrypointNotFound` variant on Windows, returned when `wpcap.dll` does not + export an entrypoint a call needs. - Sync link-layer types with libpcap 1.10.7 release. ### Changed @@ -56,11 +60,11 @@ path used to arrive as `Error::MalformedError` with the message thrown away. It now arrives as `Error::PcapError`. Device and link-layer type names are still rejected when malformed, though a rejected device name no longer takes the rest of the list with it. -- `Error` has a new `InvalidPath` variant on Windows, which exhaustive matches have to cover. - `windows-sys` updated from 0.36 to 0.61. `HANDLE` is a raw pointer there rather than an `isize`, which changes the signature of `Capture::get_event` on Windows. A raw pointer is not `Send`, so a type of your own that stores the returned `HANDLE` no longer derives `Send` and can no longer be moved to another thread without a wrapper of its own. `PacketStream` is unaffected. +- Windows binaries import nothing from `wpcap.dll` and pin no libpcap version at build time. ### Removed @@ -70,7 +74,7 @@ ### Fixed - `Capture::from_file`, `Capture::from_file_with_precision`, `Capture::savefile` and - `Capture::savefile_append` no longer convert the path with `Path::to_str`. On UN*X the path is + `Capture::savefile_append` no longer convert the path with `Path::to_str`. On UN\*X the path is handed to libpcap as bytes, so file names that are not valid UTF-8 now work. On Windows such a path returns the new `Error::InvalidPath`, where `savefile` used to panic and `from_file` used to report that a null pointer had been supplied as the file name. @@ -78,6 +82,7 @@ - `Device::list` and `Device::lookup` leave out an interface whose name is not valid UTF-8 instead of failing the whole enumeration with `Error::MalformedError`, and keep a description that is not valid UTF-8 lossily rather than rejecting it. +- `immediate_mode` now takes effect on a Windows build without `pcap_set_immediate_mode`. ## [2.5.0] - 2026-08-15 diff --git a/Cargo.toml b/Cargo.toml index e6dec337..03779f9c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ futures = { version = "0.3", optional = true } gat-std = { version = "0.1.1", optional = true } [target.'cfg(target_os = "windows")'.dependencies] -windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_Networking_WinSock"] } +windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_Networking_WinSock", "Win32_System_LibraryLoader", "Win32_System_SystemInformation"] } [dev-dependencies] etherparse = "0.21.0" diff --git a/README.md b/README.md index 431cc235..e2d0e6c0 100644 --- a/README.md +++ b/README.md @@ -30,9 +30,7 @@ This crate requires the libpcap (or Npcap on Windows) library. ### Windows -1. Install [Npcap](https://npcap.com/#download). -2. Download the [Npcap SDK](https://npcap.com/#download). -3. Add the SDK's `/Lib`, `/Lib/x64` or `/Lib/ARM64` folder to your `LIB` environment variable, matching the architecture you are building for. +Install [Npcap](https://npcap.com/#download). ### Linux @@ -69,12 +67,16 @@ If you are linking dynamically with libpcap, pcap will try to consult libpcap fo If `LIBPCAP_LIBDIR` is unset, the build will attempt to find the library via `pkg-config` instead. On most setups, this is the easiest way to get things working and may even eliminate the need for any custom build scripts in your software. +**These options do not apply on Windows.** No library is linked at build time, and `LIBPCAP_LIBDIR` is ignored. + #### Library Version If setting the library location does not work or you are linking statically, you may need to set the libpcap version manually. You can do this by setting the environment variable `LIBPCAP_VER` to the desired version (e.g. `env LIBPCAP_VER=1.5.0`). By default, if pcap fails to query libpcap/wpcap for its API version, it will assume the newest API so this should only be necessary if you are using an old version of libpcap. Note that `LIBPCAP_VER` is respected even if you haven't set `LIBPCAP_LIBDIR` and are using `pkg-config`. If it is unset, we'll find whatever available version as long as it's supported by the library. +On Windows there is no version to query. Each entrypoint is declared and resolved the first time it is called, so `LIBPCAP_VER` is required only when you deliberately wish to build against a reduced API. Calling an entrypoint that the installed `wpcap.dll` does not export, will return `Error::EntrypointNotFound` instead of failing the build. + ## Optional Features ### `capture-stream` diff --git a/build.rs b/build.rs index 73dd27bc..8b42bd3d 100644 --- a/build.rs +++ b/build.rs @@ -37,15 +37,14 @@ impl Version { ] } - fn max() -> Version { - #[cfg(not(windows))] - { - Version::new(1, 9, 1) - } - #[cfg(windows)] - { - Version::new(1, 0, 0) - } + fn fallback() -> Version { + Version::new(1, 9, 1) + } + + fn newest() -> Version { + Version::list() + .pop() + .expect("the version list is not empty") } fn docs_rs() -> Version { @@ -106,7 +105,7 @@ fn get_libpcap_version(libdirpath: Option) -> Result *mut c_char; @@ -175,7 +174,20 @@ fn main() { println!("cargo:rerun-if-env-changed=LIBPCAP_LIBDIR"); println!("cargo:rerun-if-env-changed=LIBPCAP_VER"); - // If user explicitly set LIBPCAP_LIBDIR, honour their wishes. This keeps + let windows_target = env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows"); + + // A Windows target imports no entrypoint from wpcap.dll. Declare the full + // list unless the caller has specified a version with LIBPCAP_VER. + if windows_target { + let version = match env::var("LIBPCAP_VER") { + Ok(pinned) => Version::parse(&pinned).expect("invalid LIBPCAP_VER"), + Err(_) => Version::newest(), + }; + emit_cfg_flags(version); + return; + } + + // If user explicitly set LIBPCAP_LIBDIR, honor their wishes. This keeps // existing build scripts running. If it's not set, try pkg-config. If // that's not set, try last ditch effort to build even though library wasn't // explicitly given. diff --git a/src/capture/activated/dead.rs b/src/capture/activated/dead.rs index c65db1e6..d8350f4e 100644 --- a/src/capture/activated/dead.rs +++ b/src/capture/activated/dead.rs @@ -13,7 +13,9 @@ use crate::capture::Precision; impl Capture { /// Creates a "fake" capture handle for the given link type. pub fn dead(linktype: Linktype) -> Result, Error> { - let handle = unsafe { raw::pcap_open_dead(linktype.0, 65535) }; + let library = raw::require_library()?; + + let handle = unsafe { raw::pcap_open_dead(&library, linktype.0, 65535) }; Ok(Capture::from( NonNull::::new(handle).ok_or(Error::InsufficientMemory)?, )) @@ -25,8 +27,17 @@ impl Capture { linktype: Linktype, precision: Precision, ) -> Result, Error> { + let library = raw::require_library()?; + + #[cfg(windows)] + if !raw::has_dead_precision() { + return Err(Error::EntrypointNotFound( + "pcap_open_dead_with_tstamp_precision", + )); + } + let handle = unsafe { - raw::pcap_open_dead_with_tstamp_precision(linktype.0, 65535, precision as u32) + raw::pcap_open_dead_with_tstamp_precision(&library, linktype.0, 65535, precision as u32) }; Ok(Capture::from( NonNull::::new(handle).ok_or(Error::InsufficientMemory)?, @@ -51,7 +62,7 @@ mod tests { let pcap = as_pcap_t(&mut dummy); let ctx = raw::pcap_open_dead_context(); - ctx.expect().return_once_st(move |_, _| pcap); + ctx.expect().return_once_st(move |_, _, _| pcap); let ctx = raw::pcap_close_context(); ctx.expect() @@ -70,10 +81,20 @@ mod tests { let mut dummy: isize = 777; let pcap = as_pcap_t(&mut dummy); + #[cfg(windows)] + let ctx = raw::has_dead_precision_context(); + #[cfg(windows)] + ctx.expect().return_once(|| true); + let ctx = raw::pcap_open_dead_with_tstamp_precision_context(); ctx.expect() - .with(predicate::always(), predicate::always(), predicate::eq(1)) - .return_once_st(move |_, _, _| pcap); + .with( + predicate::always(), + predicate::always(), + predicate::always(), + predicate::eq(1), + ) + .return_once_st(move |_, _, _, _| pcap); let ctx = raw::pcap_close_context(); ctx.expect() @@ -83,4 +104,16 @@ mod tests { let result = Capture::dead_with_precision(Linktype::ETHERNET, Precision::Nano); assert!(result.is_ok()); } + + #[test] + #[cfg(all(windows, libpcap_1_5_0))] + fn test_dead_precision_missing() { + let _m = RAWMTX.lock(); + + let ctx = raw::has_dead_precision_context(); + ctx.expect().return_once(|| false); + + let result = Capture::dead_with_precision(Linktype::ETHERNET, Precision::Nano); + assert!(matches!(result, Err(Error::EntrypointNotFound(_)))); + } } diff --git a/src/capture/activated/mod.rs b/src/capture/activated/mod.rs index 5162555e..f03b272c 100644 --- a/src/capture/activated/mod.rs +++ b/src/capture/activated/mod.rs @@ -155,6 +155,11 @@ impl Capture { /// which on most systems is not UTF-8: the name gets mangled and the file lands elsewhere. #[cfg(libpcap_1_7_2)] pub fn savefile_append>(&self, path: P) -> Result { + #[cfg(windows)] + if !raw::has_dump_append() { + return Err(Error::EntrypointNotFound("pcap_dump_open_append")); + } + let name = path_to_cstring(path.as_ref())?; let handle_opt = NonNull::::new(unsafe { raw::pcap_dump_open_append(self.handle.as_ptr(), name.as_ptr()) @@ -522,6 +527,11 @@ impl Savefile { // Prior to 1.9.0 when `pcap_dump_ftell64` was introduced, the offset was only reported as // a `long`. Where that is a 32-bit type, as it is on Windows, the call fails once the // savefile has grown past 2 GB. + #[cfg(windows)] + if !raw::has_dump_ftell64() { + return Err(Error::EntrypointNotFound("pcap_dump_ftell64")); + } + #[cfg(libpcap_1_9_0)] let offset = unsafe { raw::pcap_dump_ftell64(self.handle.as_ptr()) }; @@ -888,6 +898,11 @@ mod tests { let test_capture = test_capture::(pcap); let capture = test_capture.capture; + #[cfg(windows)] + let ctx = raw::has_dump_append_context(); + #[cfg(windows)] + ctx.expect().return_once(|| true); + let ctx = raw::pcap_dump_open_append_context(); ctx.expect() .withf_st(move |arg1, _| *arg1 == pcap) @@ -902,6 +917,24 @@ mod tests { assert!(result.is_ok()); } + #[test] + #[cfg(all(windows, libpcap_1_7_2))] + fn test_savefile_append_missing() { + let _m = RAWMTX.lock(); + + let mut value: isize = 777; + let pcap = as_pcap_t(&mut value); + + let test_capture = test_capture::(pcap); + let capture = test_capture.capture; + + let ctx = raw::has_dump_append_context(); + ctx.expect().return_once(|| false); + + let result = capture.savefile_append("path/to/nowhere"); + assert!(matches!(result, Err(Error::EntrypointNotFound(_)))); + } + #[test] fn test_savefile_error() { let _m = RAWMTX.lock(); @@ -934,6 +967,11 @@ mod tests { let test_capture = test_capture::(pcap); let capture = test_capture.capture; + #[cfg(windows)] + let has_ctx = raw::has_dump_append_context(); + #[cfg(windows)] + has_ctx.expect().return_once(|| true); + let ctx = raw::pcap_dump_open_append_context(); ctx.expect() .withf_st(move |arg1, _| *arg1 == pcap) @@ -979,6 +1017,11 @@ mod tests { fn test_savefile_ops() { let _m = RAWMTX.lock(); + #[cfg(windows)] + let has_ftell64 = raw::has_dump_ftell64_context(); + #[cfg(windows)] + has_ftell64.expect().times(..).return_const(true); + let mut value: isize = 888; let pcap_dumper = as_pcap_dumper_t(&mut value); diff --git a/src/capture/activated/offline.rs b/src/capture/activated/offline.rs index f4f7122c..e4ff89f7 100644 --- a/src/capture/activated/offline.rs +++ b/src/capture/activated/offline.rs @@ -23,8 +23,8 @@ impl Capture { /// which on most systems is not UTF-8: the name gets mangled and the file is not found. pub fn from_file>(path: P) -> Result, Error> { let path = path_to_cstring(path.as_ref())?; - Capture::new_raw(Some(path), |path, err| unsafe { - raw::pcap_open_offline(path, err) + Capture::new_raw(Some(path), |library, path, err| unsafe { + raw::pcap_open_offline(library, path, err) }) } @@ -39,9 +39,17 @@ impl Capture { path: P, precision: Precision, ) -> Result, Error> { + #[cfg(windows)] + if !raw::has_offline_precision() { + raw::require_library()?; + return Err(Error::EntrypointNotFound( + "pcap_open_offline_with_tstamp_precision", + )); + } + let path = path_to_cstring(path.as_ref())?; - Capture::new_raw(Some(path), |path, err| unsafe { - raw::pcap_open_offline_with_tstamp_precision(path, precision as _, err) + Capture::new_raw(Some(path), |library, path, err| unsafe { + raw::pcap_open_offline_with_tstamp_precision(library, path, precision as _, err) }) } @@ -53,7 +61,9 @@ impl Capture { #[cfg(not(windows))] pub unsafe fn from_raw_fd(fd: RawFd) -> Result, Error> { unsafe { open_raw_fd(fd, b'r') }.and_then(|file| { - Capture::new_raw(None, |_, err| unsafe { raw::pcap_fopen_offline(file, err) }) + Capture::new_raw(None, |library, _, err| unsafe { + raw::pcap_fopen_offline(library, file, err) + }) }) } @@ -69,8 +79,8 @@ impl Capture { precision: Precision, ) -> Result, Error> { unsafe { open_raw_fd(fd, b'r') }.and_then(|file| { - Capture::new_raw(None, |_, err| unsafe { - raw::pcap_fopen_offline_with_tstamp_precision(file, precision as _, err) + Capture::new_raw(None, |library, _, err| unsafe { + raw::pcap_fopen_offline_with_tstamp_precision(library, file, precision as _, err) }) }) } @@ -111,7 +121,7 @@ mod tests { let pcap = as_pcap_t(&mut dummy); let ctx = raw::pcap_open_offline_context(); - ctx.expect().return_once_st(move |_, _| pcap); + ctx.expect().return_once_st(move |_, _, _| pcap); let ctx = raw::pcap_close_context(); ctx.expect() @@ -130,10 +140,20 @@ mod tests { let mut dummy: isize = 777; let pcap = as_pcap_t(&mut dummy); + #[cfg(windows)] + let ctx = raw::has_offline_precision_context(); + #[cfg(windows)] + ctx.expect().return_once(|| true); + let ctx = raw::pcap_open_offline_with_tstamp_precision_context(); ctx.expect() - .with(predicate::always(), predicate::eq(1), predicate::always()) - .return_once_st(move |_, _, _| pcap); + .with( + predicate::always(), + predicate::always(), + predicate::eq(1), + predicate::always(), + ) + .return_once_st(move |_, _, _, _| pcap); let ctx = raw::pcap_close_context(); ctx.expect() @@ -144,6 +164,18 @@ mod tests { assert!(result.is_ok()); } + #[test] + #[cfg(all(windows, libpcap_1_5_0))] + fn test_from_file_precision_missing() { + let _m = RAWMTX.lock(); + + let ctx = raw::has_offline_precision_context(); + ctx.expect().return_once(|| false); + + let result = Capture::from_file_with_precision("path/to/nowhere", Precision::Nano); + assert!(matches!(result, Err(Error::EntrypointNotFound(_)))); + } + #[test] fn test_version() { let _m = RAWMTX.lock(); diff --git a/src/capture/inactive.rs b/src/capture/inactive.rs index 65078e84..0a5b4aef 100644 --- a/src/capture/inactive.rs +++ b/src/capture/inactive.rs @@ -35,8 +35,8 @@ impl Capture { pub fn from_device>(device: D) -> Result, Error> { let device: Device = device.into(); let name = CString::new(device.name)?; - Capture::new_raw(Some(name), |name, err| unsafe { - raw::pcap_create(name, err) + Capture::new_raw(Some(name), |library, name, err| unsafe { + raw::pcap_create(library, name, err) }) } @@ -61,6 +61,14 @@ impl Capture { }); } + // Now that the handle is active, `pcap_setmintocopy` will take the value that + // `immediate_mode` left behind. + #[cfg(windows)] + if let Some(size) = capture.min_to_copy { + capture + .check_err(unsafe { raw::pcap_setmintocopy(capture.handle.as_ptr(), size) == 0 })?; + } + Ok(capture) } @@ -77,6 +85,11 @@ impl Capture { /// If the capture device does not support the timestamp type, an error will be returned. #[cfg(libpcap_1_2_1)] pub fn tstamp_type(self, tstamp_type: TimestampType) -> Result, Error> { + #[cfg(windows)] + if !raw::has_tstamp_type() { + return Err(Error::EntrypointNotFound("pcap_set_tstamp_type")); + } + // libpcap leaves the error buffer alone here. All it reports is whether the device // claims to support the type, so there is no message to pass on. if unsafe { raw::pcap_set_tstamp_type(self.handle.as_ptr(), tstamp_type as _) } != 0 { @@ -94,7 +107,7 @@ impl Capture { /// Set immediate mode on or off. By default, this is off. /// - /// Note that in WinPcap immediate mode is set by passing a 0 argument to `min_to_copy`. + /// Note that in WinPcap, immediate mode is set by passing a 0 argument to `min_to_copy`. /// Immediate mode will be unset if `min_to_copy` is later called with a non-zero argument. /// Immediate mode is unset by resetting `min_to_copy` to the WinPcap default possibly changing /// a previously set value. When using `min_to_copy`, it is best to avoid `immediate_mode`. @@ -104,25 +117,27 @@ impl Capture { // immediate mode were more complicated, depended on the OS, and in some configurations had // to be set on an active capture. See // https://www.tcpdump.org/manpages/pcap_set_immediate_mode.3pcap.html. Since we do not - // expect pre-1.5.0 version on unix systems in the wild, we simply ignore those cases. + // expect pre-1.5.0 version on unix systems in the wild, those cases are simply ignored. + // Without `pcap_set_immediate_mode`, immediate mode is a `pcap_setmintocopy` of 0, + // which pcap takes only once the handle has been activated, so leave the value for + // `open`. + #[cfg(windows)] + if !raw::has_immediate_mode() || !cfg!(libpcap_1_5_0) { + let mut capture = self; + capture.min_to_copy = Some(if to { + 0 + } else { + raw::WINPCAP_MINTOCOPY_DEFAULT + }); + + return capture; + } + #[cfg(libpcap_1_5_0)] unsafe { raw::pcap_set_immediate_mode(self.handle.as_ptr(), to as _) }; - // In WinPcap we use `pcap_setmintocopy` as it does not have `pcap_set_immediate_mode`. - #[cfg(all(windows, not(libpcap_1_5_0)))] - unsafe { - raw::pcap_setmintocopy( - self.handle.as_ptr(), - if to { - 0 - } else { - raw::WINPCAP_MINTOCOPY_DEFAULT - }, - ) - }; - self } @@ -156,6 +171,11 @@ impl Capture { /// If the capture device does not support the timestamp precision, an error will be returned. #[cfg(libpcap_1_5_0)] pub fn precision(self, precision: Precision) -> Result, Error> { + #[cfg(windows)] + if !raw::has_tstamp_precision() { + return Err(Error::EntrypointNotFound("pcap_set_tstamp_precision")); + } + // libpcap leaves the error buffer alone here. All it reports is whether the device // claims to support the precision, so there is no message to pass on. if unsafe { raw::pcap_set_tstamp_precision(self.handle.as_ptr(), precision as _) } != 0 { @@ -241,7 +261,7 @@ mod tests { let pcap = as_pcap_t(&mut dummy); let ctx = raw::pcap_create_context(); - ctx.expect().return_once_st(move |_, _| pcap); + ctx.expect().return_once_st(move |_, _, _| pcap); let ctx = raw::pcap_close_context(); ctx.expect() @@ -257,7 +277,7 @@ mod tests { let _m = RAWMTX.lock(); let ctx = raw::pcap_create_context(); - ctx.expect().return_once_st(|_, _| std::ptr::null_mut()); + ctx.expect().return_once_st(|_, _, _| std::ptr::null_mut()); let result = Capture::from_device("some_device"); assert!(result.is_err()); @@ -384,6 +404,11 @@ mod tests { let test_capture = test_capture::(pcap); let capture = test_capture.capture; + #[cfg(windows)] + let has_ctx = raw::has_tstamp_type_context(); + #[cfg(windows)] + has_ctx.expect().times(2).returning(|| true); + let ctx = raw::pcap_set_tstamp_type_context(); ctx.expect() .withf_st(move |arg1, _| *arg1 == pcap) @@ -412,6 +437,27 @@ mod tests { ); } + #[test] + #[cfg(all(windows, libpcap_1_2_1))] + fn test_timestamp_type_missing() { + let _m = RAWMTX.lock(); + + let mut dummy: isize = 777; + let pcap = as_pcap_t(&mut dummy); + + let test_capture = test_capture::(pcap); + let capture = test_capture.capture; + + let ctx = raw::has_tstamp_type_context(); + ctx.expect().return_once(|| false); + + let result = capture.tstamp_type(TimestampType::Host); + assert!(matches!( + result, + Err(Error::EntrypointNotFound("pcap_set_tstamp_type")) + )); + } + #[test] fn test_promisc() { let _m = RAWMTX.lock(); @@ -430,36 +476,36 @@ mod tests { let _capture = capture.promisc(true); } - #[cfg(libpcap_1_5_0)] - struct ImmediateModeExpect(raw::__pcap_set_immediate_mode::Context); + #[cfg(all(libpcap_1_5_0, windows))] + struct ImmediateModeExpect( + raw::__pcap_set_immediate_mode::Context, + raw::__has_immediate_mode::Context, + ); - #[cfg(all(windows, not(libpcap_1_5_0)))] - struct ImmediateModeExpect(raw::__pcap_setmintocopy::Context); + #[cfg(all(libpcap_1_5_0, not(windows)))] + struct ImmediateModeExpect(raw::__pcap_set_immediate_mode::Context); - #[cfg(any(libpcap_1_5_0, windows))] + #[cfg(libpcap_1_5_0)] fn immediate_mode_expect(pcap: *mut raw::pcap_t) -> ImmediateModeExpect { - #[cfg(libpcap_1_5_0)] - { - let ctx = raw::pcap_set_immediate_mode_context(); - ctx.checkpoint(); - ctx.expect() - .withf_st(move |arg1, _| *arg1 == pcap) - .return_once(|_, _| 0); - ImmediateModeExpect(ctx) - } - #[cfg(all(windows, not(libpcap_1_5_0)))] + let ctx = raw::pcap_set_immediate_mode_context(); + ctx.checkpoint(); + ctx.expect() + .withf_st(move |arg1, _| *arg1 == pcap) + .return_once(|_, _| 0); + + #[cfg(windows)] { - let ctx = raw::pcap_setmintocopy_context(); - ctx.checkpoint(); - ctx.expect() - .withf_st(move |arg1, _| *arg1 == pcap) - .return_once(|_, _| 0); - ImmediateModeExpect(ctx) + let has_ctx = raw::has_immediate_mode_context(); + has_ctx.checkpoint(); + has_ctx.expect().return_once(|| true); + ImmediateModeExpect(ctx, has_ctx) } + #[cfg(not(windows))] + ImmediateModeExpect(ctx) } #[test] - #[cfg(any(libpcap_1_5_0, windows))] + #[cfg(libpcap_1_5_0)] fn test_immediate_mode() { let _m = RAWMTX.lock(); @@ -476,6 +522,96 @@ mod tests { let _capture = capture.immediate_mode(false); } + // A library without pcap_set_immediate_mode takes the value through pcap_setmintocopy + // instead, and only once the handle is active. + #[cfg(windows)] + fn winpcap_open_expect( + pcap: *mut raw::pcap_t, + size: i32, + ret: libc::c_int, + ) -> ( + raw::__pcap_activate::Context, + raw::__pcap_setmintocopy::Context, + ) { + let activate = raw::pcap_activate_context(); + activate.checkpoint(); + activate + .expect() + .withf_st(move |arg1| *arg1 == pcap) + .return_once(|_| 0); + + let mintocopy = raw::pcap_setmintocopy_context(); + mintocopy.checkpoint(); + mintocopy + .expect() + .withf_st(move |arg1, arg2| *arg1 == pcap && *arg2 == size) + .return_once(move |_, _| ret); + + (activate, mintocopy) + } + + #[test] + #[cfg(windows)] + fn test_immediate_mode_winpcap() { + let _m = RAWMTX.lock(); + + let mut dummy: isize = 777; + let pcap = as_pcap_t(&mut dummy); + + let test_capture = test_capture::(pcap); + let capture = test_capture.capture; + + let has_ctx = raw::has_immediate_mode_context(); + has_ctx.expect().times(2).returning(|| false); + + let capture = capture.immediate_mode(false).immediate_mode(true); + + let _ctx = winpcap_open_expect(pcap, 0, 0); + assert!(capture.open().is_ok()); + } + + #[test] + #[cfg(windows)] + fn test_immediate_mode_winpcap_off() { + let _m = RAWMTX.lock(); + + let mut dummy: isize = 777; + let pcap = as_pcap_t(&mut dummy); + + let test_capture = test_capture::(pcap); + let capture = test_capture.capture; + + let has_ctx = raw::has_immediate_mode_context(); + has_ctx.expect().return_once(|| false); + + let capture = capture.immediate_mode(false); + + let _ctx = winpcap_open_expect(pcap, raw::WINPCAP_MINTOCOPY_DEFAULT, 0); + assert!(capture.open().is_ok()); + } + + #[test] + #[cfg(windows)] + fn test_immediate_mode_winpcap_error() { + let _m = RAWMTX.lock(); + + let mut dummy: isize = 777; + let pcap = as_pcap_t(&mut dummy); + + let test_capture = test_capture::(pcap); + let capture = test_capture.capture; + + let has_ctx = raw::has_immediate_mode_context(); + has_ctx.expect().return_once(|| false); + + let capture = capture.immediate_mode(true); + + let _ctx = winpcap_open_expect(pcap, 0, -1); + let _err = geterr_expect(pcap); + + assert!(capture.open().is_err()); + } + #[test] #[cfg(all(libpcap_1_5_3, target_os = "macos"))] fn test_want_pktap() { @@ -542,6 +678,11 @@ mod tests { let test_capture = test_capture::(pcap); let capture = test_capture.capture; + #[cfg(windows)] + let has_ctx = raw::has_tstamp_precision_context(); + #[cfg(windows)] + has_ctx.expect().times(2).returning(|| true); + let ctx = raw::pcap_set_tstamp_precision_context(); ctx.expect() .withf_st(move |arg1, _| *arg1 == pcap) @@ -561,6 +702,27 @@ mod tests { ); } + #[test] + #[cfg(all(windows, libpcap_1_5_0))] + fn test_precision_missing() { + let _m = RAWMTX.lock(); + + let mut dummy: isize = 777; + let pcap = as_pcap_t(&mut dummy); + + let test_capture = test_capture::(pcap); + let capture = test_capture.capture; + + let ctx = raw::has_tstamp_precision_context(); + ctx.expect().return_once(|| false); + + let result = capture.precision(Precision::Nano); + assert!(matches!( + result, + Err(Error::EntrypointNotFound("pcap_set_tstamp_precision")) + )); + } + #[test] fn test_snaplen() { let _m = RAWMTX.lock(); diff --git a/src/capture/mod.rs b/src/capture/mod.rs index a2c0fa2b..112939ee 100644 --- a/src/capture/mod.rs +++ b/src/capture/mod.rs @@ -93,6 +93,8 @@ impl State for Dead {} pub struct Capture { nonblock: bool, warning: Option, + #[cfg(windows)] + min_to_copy: Option, handle: Arc, _marker: PhantomData, } @@ -128,6 +130,8 @@ impl From> for Capture { Capture { nonblock: false, warning: None, + #[cfg(windows)] + min_to_copy: None, handle: Arc::new(PcapHandle { handle }), _marker: PhantomData, } @@ -137,12 +141,14 @@ impl From> for Capture { impl Capture { fn new_raw(path: Option, func: F) -> Result, Error> where - F: FnOnce(*const libc::c_char, *mut libc::c_char) -> *mut raw::pcap_t, + F: FnOnce(&raw::Library, *const libc::c_char, *mut libc::c_char) -> *mut raw::pcap_t, { + let library = raw::require_library()?; + Error::with_errbuf(|err| { let handle = match path { - None => func(ptr::null(), err), - Some(path) => func(path.as_ptr(), err), + None => func(&library, ptr::null(), err), + Some(path) => func(&library, path.as_ptr(), err), }; Ok(Capture::from( NonNull::::new(handle).ok_or_else(|| unsafe { Error::new(err) })?, diff --git a/src/device.rs b/src/device.rs index 2838ab49..a92f57b4 100644 --- a/src/device.rs +++ b/src/device.rs @@ -195,9 +195,11 @@ impl Device { where F: FnOnce(*mut raw::pcap_if_t) -> Result, { + let library = raw::require_library()?; + let all_devs = Error::with_errbuf(|err| { let mut all_devs: *mut raw::pcap_if_t = ptr::null_mut(); - if unsafe { raw::pcap_findalldevs(&mut all_devs, err) } != 0 { + if unsafe { raw::pcap_findalldevs(&library, &mut all_devs, err) } != 0 { return Err(unsafe { Error::new(err) }); } Ok(all_devs) @@ -531,7 +533,7 @@ mod tests { let pcap = as_pcap_t(&mut dummy); let ctx = raw::pcap_create_context(); - ctx.expect().return_once_st(move |_, _| pcap); + ctx.expect().return_once_st(move |_, _, _| pcap); let ctx = raw::pcap_activate_context(); ctx.expect() @@ -554,7 +556,7 @@ mod tests { let _m = RAWMTX.lock(); let ctx = raw::pcap_findalldevs_context(); - ctx.expect().return_once_st(move |arg1, _| { + ctx.expect().return_once_st(move |_, arg1, _| { unsafe { *arg1 = std::ptr::null_mut() }; 0 }); @@ -573,7 +575,7 @@ mod tests { let ctx = raw::pcap_findalldevs_context(); ctx.checkpoint(); - ctx.expect().return_once_st(move |arg1, _| { + ctx.expect().return_once_st(move |_, arg1, _| { unsafe { *arg1 = devs_ptr }; 0 }); @@ -590,7 +592,7 @@ mod tests { let ctx = raw::pcap_findalldevs_context(); ctx.checkpoint(); - ctx.expect().return_once_st(move |_, _| -1); + ctx.expect().return_once_st(move |_, _, _| -1); let ctx = raw::pcap_freealldevs_context(); ctx.checkpoint(); @@ -604,7 +606,7 @@ mod tests { let _m = RAWMTX.lock(); let ctx = raw::pcap_findalldevs_context(); - ctx.expect().return_once_st(move |arg1, _| { + ctx.expect().return_once_st(move |_, arg1, _| { unsafe { *arg1 = std::ptr::null_mut() }; 0 }); @@ -626,7 +628,7 @@ mod tests { let ctx = raw::pcap_findalldevs_context(); ctx.checkpoint(); - ctx.expect().return_once_st(move |arg1, _| { + ctx.expect().return_once_st(move |_, arg1, _| { unsafe { *arg1 = devs_ptr }; 0 }); @@ -650,7 +652,7 @@ mod tests { let ctx = raw::pcap_findalldevs_context(); ctx.checkpoint(); - ctx.expect().return_once_st(move |_, _| -1); + ctx.expect().return_once_st(move |_, _, _| -1); let ctx = raw::pcap_freealldevs_context(); ctx.checkpoint(); @@ -673,7 +675,7 @@ mod tests { let devs_ptr = devs.as_mut_ptr(); let ctx = raw::pcap_findalldevs_context(); - ctx.expect().return_once_st(move |arg1, _| { + ctx.expect().return_once_st(move |_, arg1, _| { unsafe { *arg1 = devs_ptr }; 0 }); @@ -700,7 +702,7 @@ mod tests { let devs_ptr = devs.as_mut_ptr(); let ctx = raw::pcap_findalldevs_context(); - ctx.expect().return_once_st(move |arg1, _| { + ctx.expect().return_once_st(move |_, arg1, _| { unsafe { *arg1 = devs_ptr }; 0 }); @@ -720,7 +722,7 @@ mod tests { let devs_ptr = devs.as_mut_ptr(); let ctx = raw::pcap_findalldevs_context(); - ctx.expect().return_once_st(move |arg1, _| { + ctx.expect().return_once_st(move |_, arg1, _| { unsafe { *arg1 = devs_ptr }; 0 }); @@ -743,7 +745,7 @@ mod tests { let devs_ptr = devs.as_mut_ptr(); let ctx = raw::pcap_findalldevs_context(); - ctx.expect().return_once_st(move |arg1, _| { + ctx.expect().return_once_st(move |_, arg1, _| { unsafe { *arg1 = devs_ptr }; 0 }); diff --git a/src/lib.rs b/src/lib.rs index 5c120f9d..a8636c6c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -169,6 +169,7 @@ impl fmt::Display for ErrorCode { /// An error received from pcap #[derive(Debug, PartialEq, Eq)] +#[non_exhaustive] pub enum Error { /// The underlying library returned invalid UTF-8 MalformedError(std::str::Utf8Error), @@ -207,6 +208,12 @@ pub enum Error { InvalidPath, /// Errno error ErrnoError(errno::Errno), + #[cfg(windows)] + /// The capture library could not be loaded + LibraryNotFound, + #[cfg(windows)] + /// The capture library does not have an entrypoint the call needs + EntrypointNotFound(&'static str), /// Buffer size overflows capacity BufferOverflow, } @@ -292,7 +299,7 @@ fn path_to_cstring(path: &Path) -> Result { use std::os::unix::ffi::OsStrExt; path.as_os_str().as_bytes() }; - // libpcap has no entry points taking wide strings. It reads the path in the local code page, + // libpcap has no entrypoints taking wide strings. It reads the path in the local code page, // or in UTF-8 once pcap_init has been asked for that, so give it the UTF-8 form. A path that // is not valid UTF-8 holds an unpaired surrogate, which has no form libpcap would accept. #[cfg(windows)] @@ -331,6 +338,15 @@ impl fmt::Display for Error { #[cfg(windows)] InvalidPath => write!(f, "invalid path (not valid UTF-8)"), ErrnoError(ref e) => write!(f, "libpcap os errno: {e}"), + #[cfg(windows)] + LibraryNotFound => write!(f, "could not load wpcap.dll, Npcap may not be installed"), + #[cfg(windows)] + EntrypointNotFound(name) => { + write!( + f, + "the installed wpcap.dll does not export {name}, upgrade to a newer Npcap" + ) + } BufferOverflow => write!(f, "buffer size too large"), } } @@ -359,6 +375,10 @@ impl std::error::Error for Error { #[cfg(windows)] InvalidPath => "invalid path (not valid UTF-8)", ErrnoError(..) => "internal error, providing errno", + #[cfg(windows)] + LibraryNotFound => "could not load wpcap.dll", + #[cfg(windows)] + EntrypointNotFound(..) => "wpcap.dll is missing an entrypoint", BufferOverflow => "buffer size too large", } } @@ -425,8 +445,15 @@ pub enum CharEncoding { /// usually not UTF-8. #[cfg(libpcap_1_10_0)] pub fn init(encoding: CharEncoding) -> Result<(), Error> { + let library = raw::require_library()?; + + #[cfg(windows)] + if !raw::has_init() { + return Err(Error::EntrypointNotFound("pcap_init")); + } + Error::with_errbuf(|err| { - if unsafe { raw::pcap_init(encoding as _, err) } != 0 { + if unsafe { raw::pcap_init(&library, encoding as _, err) } != 0 { return Err(unsafe { Error::new(err) }); } Ok(()) @@ -568,6 +595,10 @@ mod tests { #[cfg(windows)] errors.push(Error::InvalidPath); errors.push(Error::ErrnoError(errno::Errno(125))); + #[cfg(windows)] + errors.push(Error::LibraryNotFound); + #[cfg(windows)] + errors.push(Error::EntrypointNotFound("pcap_set_immediate_mode")); errors.push(Error::BufferOverflow); for error in errors.iter() { @@ -605,10 +636,15 @@ mod tests { fn test_init() { let _m = RAWMTX.lock(); + #[cfg(windows)] + let has = raw::has_init_context(); + #[cfg(windows)] + has.expect().times(..).return_const(true); + let ctx = raw::pcap_init_context(); ctx.expect() - .withf_st(|arg1, _| *arg1 == raw::PCAP_CHAR_ENC_UTF_8) - .return_once(|_, _| 0); + .withf_st(|_, arg1, _| *arg1 == raw::PCAP_CHAR_ENC_UTF_8) + .return_once(|_, _, _| 0); let result = init(CharEncoding::Utf8); assert!(result.is_ok()); @@ -616,8 +652,8 @@ mod tests { let ctx = raw::pcap_init_context(); ctx.checkpoint(); ctx.expect() - .withf_st(|arg1, _| *arg1 == raw::PCAP_CHAR_ENC_LOCAL) - .return_once(|_, _| -1); + .withf_st(|_, arg1, _| *arg1 == raw::PCAP_CHAR_ENC_LOCAL) + .return_once(|_, _, _| -1); let result = init(CharEncoding::Local); assert!(result.is_err()); diff --git a/src/linktype.rs b/src/linktype.rs index 83f4ffe5..62db6e49 100644 --- a/src/linktype.rs +++ b/src/linktype.rs @@ -18,20 +18,26 @@ pub struct Linktype(pub i32); impl Linktype { /// Gets the name of the link type, such as EN10MB pub fn get_name(&self) -> Result { - unsafe { cstr_to_string(raw::pcap_datalink_val_to_name(self.0)) }? + let library = raw::require_library()?; + + unsafe { cstr_to_string(raw::pcap_datalink_val_to_name(&library, self.0)) }? .ok_or(Error::InvalidLinktype) } /// Gets the description of a link type. pub fn get_description(&self) -> Result { - unsafe { cstr_to_string(raw::pcap_datalink_val_to_description(self.0)) }? + let library = raw::require_library()?; + + unsafe { cstr_to_string(raw::pcap_datalink_val_to_description(&library, self.0)) }? .ok_or(Error::InvalidLinktype) } /// Gets the linktype from a name string pub fn from_name(name: &str) -> Result { + let library = raw::require_library()?; + let name = CString::new(name)?; - let val = unsafe { raw::pcap_datalink_name_to_val(name.as_ptr()) }; + let val = unsafe { raw::pcap_datalink_name_to_val(&library, name.as_ptr()) }; if val == -1 { return Err(Error::InvalidLinktype); } @@ -196,14 +202,14 @@ mod tests { let cstr = CString::new(name).unwrap(); let ctx = raw::pcap_datalink_val_to_name_context(); - ctx.expect().return_once(|_| cstr.into_raw()); + ctx.expect().return_once(|_, _| cstr.into_raw()); let linktype_name = Linktype::ARCNET_LINUX.get_name().unwrap(); assert_eq!(&linktype_name, name); let ctx = raw::pcap_datalink_val_to_name_context(); ctx.checkpoint(); - ctx.expect().return_once(|_| std::ptr::null()); + ctx.expect().return_once(|_, _| std::ptr::null()); let err = Linktype::ARCNET_LINUX.get_name().unwrap_err(); assert_eq!(err, Error::InvalidLinktype); @@ -216,7 +222,7 @@ mod tests { let cstr = CString::new(desc).unwrap(); let ctx = raw::pcap_datalink_val_to_description_context(); - ctx.expect().return_once(|_| cstr.into_raw()); + ctx.expect().return_once(|_, _| cstr.into_raw()); let linktype_name = Linktype::ARCNET_LINUX.get_description().unwrap(); assert_eq!(&linktype_name, desc); @@ -227,14 +233,14 @@ mod tests { let _m = RAWMTX.lock(); let ctx = raw::pcap_datalink_name_to_val_context(); - ctx.expect().return_once(|_| 7); + ctx.expect().return_once(|_, _| 7); let linktype = Linktype::from_name("git rekt scrub").unwrap(); assert_eq!(linktype, Linktype::ARCNET_BSD); let ctx = raw::pcap_datalink_name_to_val_context(); ctx.checkpoint(); - ctx.expect().return_once(|_| -1); + ctx.expect().return_once(|_, _| -1); let err = Linktype::from_name("git rekt scrub").unwrap_err(); assert_eq!(err, Error::InvalidLinktype); diff --git a/src/raw.rs b/src/raw.rs index c1355030..f4aff7a0 100644 --- a/src/raw.rs +++ b/src/raw.rs @@ -2,10 +2,15 @@ #![allow(dead_code)] #![allow(non_camel_case_types)] +use libc::FILE; use libc::{c_char, c_int, c_long, c_uchar, c_uint, c_ushort, sockaddr, timeval}; #[cfg(test)] use mockall::automock; +#[cfg(windows)] +use windows_sys::Win32::Foundation::HANDLE; + +use crate::Error; // The values have never changed; libpcap has only ever appended to them, so a version that // predates one of these never returns it. @@ -123,242 +128,682 @@ pub struct pcap_send_queue { pub buffer: *mut c_char, } +#[cfg(windows)] +pub const WINPCAP_MINTOCOPY_DEFAULT: c_int = 16000; + // This is not Option, pcap functions do not check if the handler is null so it is wrong to // pass them Option::::None. pub type pcap_handler = extern "C" fn(arg1: *mut c_uchar, arg2: *const pcap_pkthdr, arg3: *const c_uchar) -> (); -#[cfg_attr(test, automock)] -pub mod ffi { - use libc::FILE; +// Allows a binary that uses this crate to start on a system where Npcap is absent. +#[cfg(windows)] +mod loader { + use std::ffi::c_void; + use std::iter; + use std::ptr; + use std::sync::OnceLock; + use std::sync::atomic::{AtomicPtr, Ordering}; + + use windows_sys::Win32::Foundation::{HMODULE, MAX_PATH}; + use windows_sys::Win32::System::LibraryLoader::{ + GetProcAddress, LOAD_LIBRARY_FLAGS, LOAD_WITH_ALTERED_SEARCH_PATH, LoadLibraryExW, + }; + use windows_sys::Win32::System::SystemInformation::GetSystemDirectoryW; + + struct Module(HMODULE); + + // SAFETY: the handle belongs to the process rather than the thread that opened the + // library, and is never freed. + unsafe impl Send for Module {} + unsafe impl Sync for Module {} + + static LIBRARY: OnceLock> = OnceLock::new(); + + pub struct Entry { + name: &'static str, + address: AtomicPtr, + } - use super::*; + impl Entry { + const MISSING: *mut c_void = usize::MAX as *mut c_void; + + pub const fn new(name: &'static str) -> Entry { + Entry { + name, + address: AtomicPtr::new(ptr::null_mut()), + } + } + + fn resolve(&self) -> *mut c_void { + let cached = self.address.load(Ordering::Relaxed); + if !cached.is_null() { + return cached; + } + + let address = library() + .and_then(|module| unsafe { GetProcAddress(module.0, self.name.as_ptr()) }) + .map_or(Self::MISSING, |address| address as *const () as *mut c_void); + + // Both threads in a race resolve the same name to the same address, so the store + // requires no ordering. + self.address.store(address, Ordering::Relaxed); + address + } + + pub fn is_available(&self) -> bool { + self.resolve() != Self::MISSING + } + + pub fn address(&self) -> *mut c_void { + let address = self.resolve(); + assert!( + address != Self::MISSING, + "wpcap.dll does not export {}", + self.name.trim_end_matches('\0') + ); + + address + } + } - unsafe extern "C" { - // [OBSOLETE] pub fn pcap_lookupdev(arg1: *mut c_char) -> *mut c_char; - // pub fn pcap_lookupnet(arg1: *const c_char, arg2: *mut c_uint, arg3: *mut c_uint, - // arg4: *mut c_char) -> c_int; - pub fn pcap_create(arg1: *const c_char, arg2: *mut c_char) -> *mut pcap_t; - pub fn pcap_set_snaplen(arg1: *mut pcap_t, arg2: c_int) -> c_int; - pub fn pcap_set_promisc(arg1: *mut pcap_t, arg2: c_int) -> c_int; - // pub fn pcap_can_set_rfmon(arg1: *mut pcap_t) -> c_int; - pub fn pcap_set_timeout(arg1: *mut pcap_t, arg2: c_int) -> c_int; - pub fn pcap_set_buffer_size(arg1: *mut pcap_t, arg2: c_int) -> c_int; - pub fn pcap_activate(arg1: *mut pcap_t) -> c_int; - // pub fn pcap_open_live(arg1: *const c_char, arg2: c_int, arg3: c_int, arg4: c_int, - // arg5: *mut c_char) -> *mut pcap_t; - pub fn pcap_open_dead(arg1: c_int, arg2: c_int) -> *mut pcap_t; - pub fn pcap_open_offline(arg1: *const c_char, arg2: *mut c_char) -> *mut pcap_t; - pub fn pcap_close(arg1: *mut pcap_t); - pub fn pcap_loop( - arg1: *mut pcap_t, - arg2: c_int, - arg3: pcap_handler, - arg4: *mut c_uchar, - ) -> c_int; - pub fn pcap_dispatch( - arg1: *mut pcap_t, - arg2: c_int, - arg3: pcap_handler, - arg4: *mut c_uchar, - ) -> c_int; - // pub fn pcap_next(arg1: *mut pcap_t, arg2: *mut pcap_pkthdr) -> *const c_uchar; - pub fn pcap_next_ex( - arg1: *mut pcap_t, - arg2: *mut *mut pcap_pkthdr, - arg3: *mut *const c_uchar, - ) -> c_int; - pub fn pcap_breakloop(arg1: *mut pcap_t); - pub fn pcap_stats(arg1: *mut pcap_t, arg2: *mut pcap_stat) -> c_int; - pub fn pcap_setfilter(arg1: *mut pcap_t, arg2: *mut bpf_program) -> c_int; - pub fn pcap_setdirection(arg1: *mut pcap_t, arg2: pcap_direction_t) -> c_int; - // pub fn pcap_getnonblock(arg1: *mut pcap_t, arg2: *mut c_char) -> c_int; - pub fn pcap_setnonblock(arg1: *mut pcap_t, arg2: c_int, arg3: *mut c_char) -> c_int; - pub fn pcap_sendpacket(arg1: *mut pcap_t, arg2: *const c_uchar, arg3: c_int) -> c_int; - // pub fn pcap_statustostr(arg1: c_int) -> *const c_char; - // pub fn pcap_strerror(arg1: c_int) -> *const c_char; - pub fn pcap_geterr(arg1: *mut pcap_t) -> *mut c_char; - // pub fn pcap_perror(arg1: *mut pcap_t, arg2: *mut c_char); - pub fn pcap_compile( - arg1: *mut pcap_t, - arg2: *mut bpf_program, - arg3: *const c_char, - arg4: c_int, - arg5: c_uint, - ) -> c_int; - // pub fn pcap_compile_nopcap(arg1: c_int, arg2: c_int, arg3: *mut bpf_program, - // arg4: *const c_char, arg5: c_int, arg6: c_uint) -> c_int; - pub fn pcap_freecode(arg1: *mut bpf_program); - pub fn pcap_offline_filter( - arg1: *const bpf_program, - arg2: *const pcap_pkthdr, - arg3: *const c_uchar, - ) -> c_int; - pub fn pcap_datalink(arg1: *mut pcap_t) -> c_int; - // pub fn pcap_datalink_ext(arg1: *mut pcap_t) -> c_int; - pub fn pcap_list_datalinks(arg1: *mut pcap_t, arg2: *mut *mut c_int) -> c_int; - pub fn pcap_set_datalink(arg1: *mut pcap_t, arg2: c_int) -> c_int; - pub fn pcap_free_datalinks(arg1: *mut c_int); - pub fn pcap_datalink_name_to_val(arg1: *const c_char) -> c_int; - pub fn pcap_datalink_val_to_name(arg1: c_int) -> *const c_char; - pub fn pcap_datalink_val_to_description(arg1: c_int) -> *const c_char; - pub fn pcap_snapshot(arg1: *mut pcap_t) -> c_int; - // pub fn pcap_is_swapped(arg1: *mut pcap_t) -> c_int; - pub fn pcap_major_version(arg1: *mut pcap_t) -> c_int; - pub fn pcap_minor_version(arg1: *mut pcap_t) -> c_int; - // The one FILE * entry point that is not a macro on Windows. What it points at belongs - // to whichever C runtime libpcap was linked against, so only ever compare it to null. - pub fn pcap_file(arg1: *mut pcap_t) -> *mut FILE; - pub fn pcap_fileno(arg1: *mut pcap_t) -> c_int; - pub fn pcap_dump_open(arg1: *mut pcap_t, arg2: *const c_char) -> *mut pcap_dumper_t; - pub fn pcap_dump_ftell(arg1: *mut pcap_dumper_t) -> c_long; - pub fn pcap_dump_flush(arg1: *mut pcap_dumper_t) -> c_int; - pub fn pcap_dump_close(arg1: *mut pcap_dumper_t); - pub fn pcap_dump(arg1: *mut c_uchar, arg2: *const pcap_pkthdr, arg3: *const c_uchar); - pub fn pcap_findalldevs(arg1: *mut *mut pcap_if_t, arg2: *mut c_char) -> c_int; - pub fn pcap_freealldevs(arg1: *mut pcap_if_t); - // pub fn pcap_lib_version() -> *const c_char; - // pub fn bpf_image(arg1: *const bpf_insn, arg2: c_int) -> *mut c_char; - // pub fn bpf_dump(arg1: *const bpf_program, arg2: c_int); + pub fn is_available() -> bool { + library().is_some() } - #[cfg(libpcap_1_2_1)] - unsafe extern "C" { - // pub fn pcap_free_tstamp_types(arg1: *mut c_int) -> (); - // pub fn pcap_list_tstamp_types(arg1: *mut pcap_t, arg2: *mut *mut c_int) -> c_int; - // pub fn pcap_tstamp_type_name_to_val(arg1: *const c_char) -> c_int; - // pub fn pcap_tstamp_type_val_to_description(arg1: c_int) -> *const c_char; - // pub fn pcap_tstamp_type_val_to_name(arg1: c_int) -> *const c_char; - pub fn pcap_set_tstamp_type(arg1: *mut pcap_t, arg2: c_int) -> c_int; + fn library() -> Option<&'static Module> { + LIBRARY.get_or_init(open).as_ref() } - #[cfg(libpcap_1_5_0)] - unsafe extern "C" { - // pub fn pcap_get_tstamp_precision(arg1: *mut pcap_t) -> c_int; - pub fn pcap_open_dead_with_tstamp_precision( - arg1: c_int, - arg2: c_int, - arg3: c_uint, - ) -> *mut pcap_t; - pub fn pcap_open_offline_with_tstamp_precision( - arg1: *const c_char, - arg2: c_uint, - arg3: *mut c_char, - ) -> *mut pcap_t; - pub fn pcap_set_immediate_mode(arg1: *mut pcap_t, arg2: c_int) -> c_int; - pub fn pcap_set_tstamp_precision(arg1: *mut pcap_t, arg2: c_int) -> c_int; + // Npcap installs wpcap.dll into the Npcap subdirectory of the system directory, and places a + // copy in the system directory itself only when installed in WinPcap compatible mode. Try the + // ordinary search order first, so that an application shipping its own copy continues to use + // it, then fall back to the subdirectory, which is not searched by default. + fn open() -> Option { + load("wpcap.dll", 0).or_else(|| { + let path = format!("{}\\Npcap\\wpcap.dll", system_directory()?); + // wpcap.dll imports Packet.dll, so the directory containing the library must be + // searched as well. + load(&path, LOAD_WITH_ALTERED_SEARCH_PATH) + }) } - #[cfg(libpcap_1_7_2)] - unsafe extern "C" { - pub fn pcap_dump_open_append(arg1: *mut pcap_t, arg2: *const c_char) -> *mut pcap_dumper_t; + fn load(path: &str, flags: LOAD_LIBRARY_FLAGS) -> Option { + let path: Vec = path.encode_utf16().chain(iter::once(0)).collect(); + let module = unsafe { LoadLibraryExW(path.as_ptr(), ptr::null_mut(), flags) }; + + (!module.is_null()).then_some(Module(module)) } - #[cfg(libpcap_1_9_0)] - unsafe extern "C" { - // pcap_bufsize - // pcap_createsrcstr - pub fn pcap_dump_ftell64(arg1: *mut pcap_dumper_t) -> i64; - // pcap_findalldevs_ex - // pcap_get_required_select_timeout - // pcap_open - // pcap_parsesrcstr - // pcap_remoteact_accept - // pcap_remoteact_cleanup - // pcap_remoteact_close - // pcap_remoteact_list - // pcap_set_protocol_linux - // pcap_setsampling + fn system_directory() -> Option { + let mut buffer = [0u16; MAX_PATH as usize]; + // The return value is the number of characters written, the number the buffer should + // have held if it was too small, or zero if the call failed. + let len = unsafe { GetSystemDirectoryW(buffer.as_mut_ptr(), buffer.len() as u32) } as usize; + if len == 0 || len > buffer.len() { + return None; + } + + String::from_utf16(&buffer[..len]).ok() } - #[cfg(libpcap_1_9_1)] - unsafe extern "C" { - // pcap_datalink_val_to_description_or_dlt + #[cfg(test)] + mod tests { + use super::*; + use std::panic::{AssertUnwindSafe, catch_unwind}; + + // These read a real wpcap.dll. Return early where Npcap is not installed. + fn library_present() -> bool { + open().is_some() + } + + #[test] + fn test_entry() { + if !library_present() { + return; + } + + let present = Entry::new("pcap_lib_version\0"); + assert!(present.is_available()); + assert!(!present.address().is_null()); + // A second call, which returns the cached address. + assert!(present.is_available()); + + let missing = Entry::new("pcap_no_such_entrypoint\0"); + assert!(!missing.is_available()); + assert!(!missing.is_available()); + } + + #[test] + fn test_entry_address_missing() { + if !library_present() { + return; + } + + let panic = catch_unwind(AssertUnwindSafe(|| { + Entry::new("pcap_no_such_entrypoint\0").address() + })) + .unwrap_err(); + let message = panic + .downcast_ref::() + .map(String::as_str) + .or_else(|| panic.downcast_ref::<&str>().copied()) + .unwrap_or_default(); + assert!( + message.contains("wpcap.dll does not export pcap_no_such_entrypoint"), + "{message}" + ); + } + + #[test] + fn test_load() { + assert!(load("no-such-library.dll", 0).is_none()); + + if !library_present() { + return; + } + + // Where a default installation puts the library, reached only when the + // ordinary search order comes up empty. + if load("wpcap.dll", 0).is_none() { + let path = format!("{}\\Npcap\\wpcap.dll", system_directory().unwrap()); + assert!(load(&path, LOAD_WITH_ALTERED_SEARCH_PATH).is_some()); + } + + // Succeeds from either location. + assert!(open().is_some()); + } } +} - #[cfg(libpcap_1_10_0)] - unsafe extern "C" { - pub fn pcap_init(arg1: c_uint, arg2: *mut c_char) -> c_int; - // pcap_remoteact_accept_ex +pub struct Library(()); + +// Under cfg(test) the entrypoints are mocked, so there is no library to find. +#[cfg(any(not(windows), test))] +pub fn require_library() -> Result { + Ok(Library(())) +} + +#[cfg(all(windows, not(test)))] +pub fn require_library() -> Result { + if loader::is_available() { + Ok(Library(())) + } else { + Err(Error::LibraryNotFound) } } #[cfg(not(windows))] -#[cfg_attr(test, automock)] -pub mod ffi_unix { - use libc::FILE; +macro_rules! pcap_entry { + ($name:ident($($arg:ident: $argty:ty),*) $(-> $ret:ty)?) => {{ + unsafe extern "C" { + fn $name($($arg: $argty),*) $(-> $ret)?; + } + + unsafe { $name($($arg),*) } + }}; +} - use super::*; +#[cfg(windows)] +macro_rules! pcap_entry { + ($name:ident($($arg:ident: $argty:ty),*) $(-> $ret:ty)?) => {{ + static ENTRY: super::loader::Entry = + super::loader::Entry::new(concat!(stringify!($name), "\0")); + + let entry: unsafe extern "C" fn($($argty),*) $(-> $ret)? = + unsafe { std::mem::transmute(ENTRY.address()) }; + + unsafe { entry($($arg),*) } + }}; +} + +#[cfg(not(windows))] +macro_rules! pcap_ffi { + ( + $(#[$modattr:meta])* + pub mod $module:ident { + $(#[$linkattr:meta])* + unsafe extern "C" { + $( + $(#[$fnattr:meta])* + pub fn $name:ident($($arg:ident: $argty:ty),* $(,)?) $(-> $ret:ty)?; + )* + } + } + ) => { + $(#[$modattr])* + pub mod $module { + use super::*; + + $(#[$linkattr])* + unsafe extern "C" { + $( + $(#[$fnattr])* + pub fn $name($($arg: $argty),*) $(-> $ret)?; + )* + } + } + }; +} + +#[cfg(windows)] +macro_rules! pcap_ffi { + ( + $(#[$modattr:meta])* + pub mod $module:ident { + $(#[$linkattr:meta])* + unsafe extern "C" { + $( + $(#[$fnattr:meta])* + pub fn $name:ident($($arg:ident: $argty:ty),* $(,)?) $(-> $ret:ty)?; + )* + } + } + ) => { + $(#[$modattr])* + pub mod $module { + use super::*; + + $( + $(#[$fnattr])* + pub unsafe fn $name($($arg: $argty),*) $(-> $ret)? { + pcap_entry!($name($($arg: $argty),*) $(-> $ret)?) + } + )* + + // The names this module passes to GetProcAddress. Nothing verifies them at link + // time. + #[cfg(test)] + #[allow(clippy::vec_init_then_push)] + pub fn entrypoint_names() -> Vec<&'static str> { + let mut names = Vec::new(); + $( + $(#[$fnattr])* + names.push(concat!(stringify!($name), "\0")); + )* + names + } + } + }; +} + +macro_rules! pcap_ffi_library { + ( + $(#[$modattr:meta])* + pub mod $module:ident { + unsafe extern "C" { + $( + $(#[$fnattr:meta])* + pub fn $name:ident($($arg:ident: $argty:ty),* $(,)?) $(-> $ret:ty)?; + )* + } + } + ) => { + $(#[$modattr])* + pub mod $module { + use super::*; + + $( + $(#[$fnattr])* + pub unsafe fn $name(_library: &Library, $($arg: $argty),*) $(-> $ret)? { + pcap_entry!($name($($arg: $argty),*) $(-> $ret)?) + } + )* + + #[cfg(all(windows, test))] + #[allow(clippy::vec_init_then_push)] + pub fn entrypoint_names() -> Vec<&'static str> { + let mut names = Vec::new(); + $( + $(#[$fnattr])* + names.push(concat!(stringify!($name), "\0")); + )* + names + } + } + }; +} - #[link(name = "pcap")] - unsafe extern "C" { - // pub fn pcap_inject(arg1: *mut pcap_t, arg2: *const c_void, arg3: size_t) -> c_int; - pub fn pcap_set_rfmon(arg1: *mut pcap_t, arg2: c_int) -> c_int; - pub fn pcap_get_selectable_fd(arg1: *mut pcap_t) -> c_int; - // wpcap exports no FILE * entry points: libpcap may be linked against a different C - // runtime than its caller. On Windows pcap.h defines these names as macros that pull - // the OS handle out of the FILE * and call pcap_hopen_offline()/pcap_dump_hopen(). - pub fn pcap_fopen_offline(arg1: *mut FILE, arg2: *mut c_char) -> *mut pcap_t; - pub fn pcap_dump_fopen(arg1: *mut pcap_t, fp: *mut FILE) -> *mut pcap_dumper_t; - // wpcap does export this one, but the FILE * it hands back belongs to the C runtime - // wpcap was linked against, which is not necessarily the caller's, so it is no more - // usable on Windows than the entry points above. - pub fn pcap_dump_file(arg1: *mut pcap_dumper_t) -> *mut FILE; +pcap_ffi! { + #[cfg_attr(test, automock)] + pub mod ffi { + unsafe extern "C" { + // [OBSOLETE] pub fn pcap_lookupdev(arg1: *mut c_char) -> *mut c_char; + // pub fn pcap_lookupnet(arg1: *const c_char, arg2: *mut c_uint, arg3: *mut c_uint, + // arg4: *mut c_char) -> c_int; + pub fn pcap_set_snaplen(arg1: *mut pcap_t, arg2: c_int) -> c_int; + pub fn pcap_set_promisc(arg1: *mut pcap_t, arg2: c_int) -> c_int; + // pub fn pcap_can_set_rfmon(arg1: *mut pcap_t) -> c_int; + pub fn pcap_set_timeout(arg1: *mut pcap_t, arg2: c_int) -> c_int; + pub fn pcap_set_buffer_size(arg1: *mut pcap_t, arg2: c_int) -> c_int; + pub fn pcap_activate(arg1: *mut pcap_t) -> c_int; + // pub fn pcap_open_live(arg1: *const c_char, arg2: c_int, arg3: c_int, arg4: c_int, + // arg5: *mut c_char) -> *mut pcap_t; + pub fn pcap_close(arg1: *mut pcap_t); + pub fn pcap_loop( + arg1: *mut pcap_t, + arg2: c_int, + arg3: pcap_handler, + arg4: *mut c_uchar, + ) -> c_int; + pub fn pcap_dispatch( + arg1: *mut pcap_t, + arg2: c_int, + arg3: pcap_handler, + arg4: *mut c_uchar, + ) -> c_int; + // pub fn pcap_next(arg1: *mut pcap_t, arg2: *mut pcap_pkthdr) -> *const c_uchar; + pub fn pcap_next_ex( + arg1: *mut pcap_t, + arg2: *mut *mut pcap_pkthdr, + arg3: *mut *const c_uchar, + ) -> c_int; + pub fn pcap_breakloop(arg1: *mut pcap_t); + pub fn pcap_stats(arg1: *mut pcap_t, arg2: *mut pcap_stat) -> c_int; + pub fn pcap_setfilter(arg1: *mut pcap_t, arg2: *mut bpf_program) -> c_int; + pub fn pcap_setdirection(arg1: *mut pcap_t, arg2: pcap_direction_t) -> c_int; + // pub fn pcap_getnonblock(arg1: *mut pcap_t, arg2: *mut c_char) -> c_int; + pub fn pcap_setnonblock(arg1: *mut pcap_t, arg2: c_int, arg3: *mut c_char) -> c_int; + pub fn pcap_sendpacket(arg1: *mut pcap_t, arg2: *const c_uchar, arg3: c_int) -> c_int; + // pub fn pcap_statustostr(arg1: c_int) -> *const c_char; + // pub fn pcap_strerror(arg1: c_int) -> *const c_char; + pub fn pcap_geterr(arg1: *mut pcap_t) -> *mut c_char; + // pub fn pcap_perror(arg1: *mut pcap_t, arg2: *mut c_char); + pub fn pcap_compile( + arg1: *mut pcap_t, + arg2: *mut bpf_program, + arg3: *const c_char, + arg4: c_int, + arg5: c_uint, + ) -> c_int; + // pub fn pcap_compile_nopcap(arg1: c_int, arg2: c_int, arg3: *mut bpf_program, + // arg4: *const c_char, arg5: c_int, arg6: c_uint) -> c_int; + pub fn pcap_freecode(arg1: *mut bpf_program); + pub fn pcap_offline_filter( + arg1: *const bpf_program, + arg2: *const pcap_pkthdr, + arg3: *const c_uchar, + ) -> c_int; + pub fn pcap_datalink(arg1: *mut pcap_t) -> c_int; + // pub fn pcap_datalink_ext(arg1: *mut pcap_t) -> c_int; + pub fn pcap_list_datalinks(arg1: *mut pcap_t, arg2: *mut *mut c_int) -> c_int; + pub fn pcap_set_datalink(arg1: *mut pcap_t, arg2: c_int) -> c_int; + pub fn pcap_free_datalinks(arg1: *mut c_int); + pub fn pcap_snapshot(arg1: *mut pcap_t) -> c_int; + // pub fn pcap_is_swapped(arg1: *mut pcap_t) -> c_int; + pub fn pcap_major_version(arg1: *mut pcap_t) -> c_int; + pub fn pcap_minor_version(arg1: *mut pcap_t) -> c_int; + // The one FILE * entrypoint that is not a macro on Windows. What it points at belongs + // to the C runtime libpcap was linked against, which is not necessarily the + // caller's, so only ever compare it to null. + pub fn pcap_file(arg1: *mut pcap_t) -> *mut FILE; + pub fn pcap_fileno(arg1: *mut pcap_t) -> c_int; + pub fn pcap_dump_open(arg1: *mut pcap_t, arg2: *const c_char) -> *mut pcap_dumper_t; + // wpcap does export this one, but the FILE * it returns belongs to the C + // runtime wpcap was linked against, which is not necessarily the caller's, so it is + // no more usable on Windows than the entrypoints above. + #[cfg(not(windows))] + pub fn pcap_dump_file(arg1: *mut pcap_dumper_t) -> *mut FILE; + pub fn pcap_dump_ftell(arg1: *mut pcap_dumper_t) -> c_long; + pub fn pcap_dump_flush(arg1: *mut pcap_dumper_t) -> c_int; + pub fn pcap_dump_close(arg1: *mut pcap_dumper_t); + pub fn pcap_dump(arg1: *mut c_uchar, arg2: *const pcap_pkthdr, arg3: *const c_uchar); + pub fn pcap_freealldevs(arg1: *mut pcap_if_t); + // pub fn pcap_lib_version() -> *const c_char; + // pub fn bpf_image(arg1: *const bpf_insn, arg2: c_int) -> *mut c_char; + // pub fn bpf_dump(arg1: *const bpf_program, arg2: c_int); + + // pub fn pcap_free_tstamp_types(arg1: *mut c_int) -> (); + // pub fn pcap_list_tstamp_types(arg1: *mut pcap_t, arg2: *mut *mut c_int) -> c_int; + // pub fn pcap_tstamp_type_name_to_val(arg1: *const c_char) -> c_int; + // pub fn pcap_tstamp_type_val_to_description(arg1: c_int) -> *const c_char; + // pub fn pcap_tstamp_type_val_to_name(arg1: c_int) -> *const c_char; + #[cfg(libpcap_1_2_1)] + pub fn pcap_set_tstamp_type(arg1: *mut pcap_t, arg2: c_int) -> c_int; + + // pub fn pcap_get_tstamp_precision(arg1: *mut pcap_t) -> c_int; + #[cfg(libpcap_1_5_0)] + pub fn pcap_set_immediate_mode(arg1: *mut pcap_t, arg2: c_int) -> c_int; + #[cfg(libpcap_1_5_0)] + pub fn pcap_set_tstamp_precision(arg1: *mut pcap_t, arg2: c_int) -> c_int; + + #[cfg(libpcap_1_7_2)] + pub fn pcap_dump_open_append( + arg1: *mut pcap_t, + arg2: *const c_char, + ) -> *mut pcap_dumper_t; + + #[cfg(libpcap_1_9_0)] + pub fn pcap_dump_ftell64(arg1: *mut pcap_dumper_t) -> i64; + + // From libpcap 1.9.0, not bound: + // pcap_bufsize + // pcap_createsrcstr + // pcap_findalldevs_ex + // pcap_get_required_select_timeout + // pcap_open + // pcap_parsesrcstr + // pcap_remoteact_accept + // pcap_remoteact_cleanup + // pcap_remoteact_close + // pcap_remoteact_list + // pcap_set_protocol_linux + // pcap_setsampling + + // From libpcap 1.9.1, not bound: + // pcap_datalink_val_to_description_or_dlt + + // From libpcap 1.10.0, not bound: + // pcap_remoteact_accept_ex + } } +} - #[cfg(libpcap_1_5_0)] - #[link(name = "pcap")] - unsafe extern "C" { - pub fn pcap_fopen_offline_with_tstamp_precision( - arg1: *mut FILE, - arg2: c_uint, - arg3: *mut c_char, - ) -> *mut pcap_t; +#[cfg(not(windows))] +pcap_ffi! { + #[cfg_attr(test, automock)] + pub mod ffi_unix { + #[link(name = "pcap")] + unsafe extern "C" { + // pub fn pcap_inject(arg1: *mut pcap_t, arg2: *const c_void, arg3: size_t) -> c_int; + pub fn pcap_set_rfmon(arg1: *mut pcap_t, arg2: c_int) -> c_int; + pub fn pcap_get_selectable_fd(arg1: *mut pcap_t) -> c_int; + // wpcap exports no FILE * entrypoints: libpcap may be linked against a different C + // runtime than its caller. On Windows pcap.h defines these names as macros that pull + // the OS handle out of the FILE * and call pcap_hopen_offline()/pcap_dump_hopen(). + pub fn pcap_dump_fopen(arg1: *mut pcap_t, fp: *mut FILE) -> *mut pcap_dumper_t; + } } } #[cfg(target_os = "macos")] -#[cfg_attr(test, automock)] -pub mod ffi_macos { - use super::*; +pcap_ffi! { + #[cfg_attr(test, automock)] + pub mod ffi_macos { + unsafe extern "C" { + #[cfg(libpcap_1_5_3)] + pub fn pcap_set_want_pktap(arg1: *mut pcap_t, arg2: c_int) -> c_int; + } + } +} - #[cfg(libpcap_1_5_3)] - unsafe extern "C" { - pub fn pcap_set_want_pktap(arg1: *mut pcap_t, arg2: c_int) -> c_int; +#[cfg(windows)] +pcap_ffi! { + #[cfg_attr(test, automock)] + pub mod ffi_windows { + unsafe extern "C" { + pub fn pcap_setmintocopy(arg1: *mut pcap_t, arg2: c_int) -> c_int; + pub fn pcap_getevent(p: *mut pcap_t) -> HANDLE; + pub fn pcap_sendqueue_destroy(queue: *mut pcap_send_queue); + pub fn pcap_sendqueue_queue( + queue: *mut pcap_send_queue, + pkt_header: *const pcap_pkthdr, + pkt_data: *const c_uchar, + ) -> c_int; + pub fn pcap_sendqueue_transmit( + p: *mut pcap_t, + queue: *mut pcap_send_queue, + sync: c_int, + ) -> c_uint; + } + } +} + +pcap_ffi_library! { + #[cfg_attr(test, automock)] + pub mod ffi_library { + unsafe extern "C" { + pub fn pcap_create(arg1: *const c_char, arg2: *mut c_char) -> *mut pcap_t; + pub fn pcap_open_dead(arg1: c_int, arg2: c_int) -> *mut pcap_t; + pub fn pcap_open_offline(arg1: *const c_char, arg2: *mut c_char) -> *mut pcap_t; + pub fn pcap_datalink_name_to_val(arg1: *const c_char) -> c_int; + pub fn pcap_datalink_val_to_name(arg1: c_int) -> *const c_char; + pub fn pcap_datalink_val_to_description(arg1: c_int) -> *const c_char; + pub fn pcap_findalldevs(arg1: *mut *mut pcap_if_t, arg2: *mut c_char) -> c_int; + + // The FILE * entrypoint wpcap does not export; see ffi_unix above. + #[cfg(not(windows))] + pub fn pcap_fopen_offline(arg1: *mut FILE, arg2: *mut c_char) -> *mut pcap_t; + + #[cfg(windows)] + pub fn pcap_sendqueue_alloc(memsize: c_uint) -> *mut pcap_send_queue; + + #[cfg(libpcap_1_5_0)] + pub fn pcap_open_dead_with_tstamp_precision( + arg1: c_int, + arg2: c_int, + arg3: c_uint, + ) -> *mut pcap_t; + #[cfg(libpcap_1_5_0)] + pub fn pcap_open_offline_with_tstamp_precision( + arg1: *const c_char, + arg2: c_uint, + arg3: *mut c_char, + ) -> *mut pcap_t; + #[cfg(all(not(windows), libpcap_1_5_0))] + pub fn pcap_fopen_offline_with_tstamp_precision( + arg1: *mut FILE, + arg2: c_uint, + arg3: *mut c_char, + ) -> *mut pcap_t; + + #[cfg(libpcap_1_10_0)] + pub fn pcap_init(arg1: c_uint, arg2: *mut c_char) -> c_int; + } } } #[cfg(windows)] #[cfg_attr(test, automock)] -pub mod ffi_windows { - use windows_sys::Win32::Foundation::HANDLE; +pub mod optional { + use super::loader::Entry; - use super::*; + /// `pcap_set_tstamp_type`, added in libpcap 1.2.1. + pub fn has_tstamp_type() -> bool { + static ENTRY: Entry = Entry::new("pcap_set_tstamp_type\0"); + ENTRY.is_available() + } + + /// `pcap_open_dead_with_tstamp_precision`, added in libpcap 1.5.0. + pub fn has_dead_precision() -> bool { + static ENTRY: Entry = Entry::new("pcap_open_dead_with_tstamp_precision\0"); + ENTRY.is_available() + } + + /// `pcap_open_offline_with_tstamp_precision`, added in libpcap 1.5.0. + pub fn has_offline_precision() -> bool { + static ENTRY: Entry = Entry::new("pcap_open_offline_with_tstamp_precision\0"); + ENTRY.is_available() + } + + /// `pcap_set_immediate_mode`, added in libpcap 1.5.0. + pub fn has_immediate_mode() -> bool { + static ENTRY: Entry = Entry::new("pcap_set_immediate_mode\0"); + ENTRY.is_available() + } + + /// `pcap_set_tstamp_precision`, added in libpcap 1.5.0. + pub fn has_tstamp_precision() -> bool { + static ENTRY: Entry = Entry::new("pcap_set_tstamp_precision\0"); + ENTRY.is_available() + } + + /// `pcap_dump_open_append`, added in libpcap 1.7.2. + pub fn has_dump_append() -> bool { + static ENTRY: Entry = Entry::new("pcap_dump_open_append\0"); + ENTRY.is_available() + } + + /// `pcap_dump_ftell64`, added in libpcap 1.9.0. + pub fn has_dump_ftell64() -> bool { + static ENTRY: Entry = Entry::new("pcap_dump_ftell64\0"); + ENTRY.is_available() + } - pub const WINPCAP_MINTOCOPY_DEFAULT: c_int = 16000; - - #[link(name = "wpcap")] - unsafe extern "C" { - pub fn pcap_setmintocopy(arg1: *mut pcap_t, arg2: c_int) -> c_int; - pub fn pcap_getevent(p: *mut pcap_t) -> HANDLE; - pub fn pcap_sendqueue_alloc(memsize: c_uint) -> *mut pcap_send_queue; - pub fn pcap_sendqueue_destroy(queue: *mut pcap_send_queue); - pub fn pcap_sendqueue_queue( - queue: *mut pcap_send_queue, - pkt_header: *const pcap_pkthdr, - pkt_data: *const c_uchar, - ) -> c_int; - pub fn pcap_sendqueue_transmit( - p: *mut pcap_t, - queue: *mut pcap_send_queue, - sync: c_int, - ) -> c_uint; + /// `pcap_init`, added in libpcap 1.10.0. + pub fn has_init() -> bool { + static ENTRY: Entry = Entry::new("pcap_init\0"); + ENTRY.is_available() } } -// The conventional solution is to use `mockall_double`. However, automock's requirement for an -// inner module would require changing the imports in all the files using this module. This approach -// allows all the other modules to keep using the `raw` module as before. +#[cfg(test)] +#[cfg(windows)] +mod optional_tests { + use super::{loader, optional}; + + // A library without one of these is reported through optional rather than called. + const OPTIONAL: &[&str] = &[ + "pcap_set_tstamp_type\0", + "pcap_open_dead_with_tstamp_precision\0", + "pcap_open_offline_with_tstamp_precision\0", + "pcap_set_immediate_mode\0", + "pcap_set_tstamp_precision\0", + "pcap_dump_open_append\0", + "pcap_dump_ftell64\0", + "pcap_init\0", + ]; + + // wpcap.dll is resolved by name at runtime, so a name the library does not export becomes + // a panic at the call site rather than a link error. Resolve every one in advance. + #[test] + fn test_entrypoints_resolve() { + if !loader::is_available() { + return; + } + + let names = super::ffi::entrypoint_names() + .into_iter() + .chain(super::ffi_windows::entrypoint_names()) + .chain(super::ffi_library::entrypoint_names()) + .filter(|name| !OPTIONAL.contains(name)); + for name in names { + assert!( + loader::Entry::new(name).is_available(), + "wpcap.dll does not export {}", + name.trim_end_matches('\0') + ); + } + } + + #[test] + fn test_entrypoint_names() { + if !optional::has_init() { + return; + } + + assert!(optional::has_tstamp_type()); + assert!(optional::has_dead_precision()); + assert!(optional::has_offline_precision()); + assert!(optional::has_immediate_mode()); + assert!(optional::has_tstamp_precision()); + assert!(optional::has_dump_append()); + assert!(optional::has_dump_ftell64()); + assert!(optional::has_init()); + } +} + +// This approach allows all the other modules to keep using the `raw` module as before. #[cfg(not(test))] pub use ffi::*; @@ -374,6 +819,13 @@ pub use ffi_macos::*; #[cfg(windows)] pub use ffi_windows::*; +#[cfg(not(test))] +pub use ffi_library::*; + +#[cfg(not(test))] +#[cfg(windows)] +pub use optional::*; + #[cfg(test)] pub use mock_ffi::*; @@ -389,6 +841,13 @@ pub use mock_ffi_macos::*; #[cfg(windows)] pub use mock_ffi_windows::*; +#[cfg(test)] +pub use mock_ffi_library::*; + +#[cfg(test)] +#[cfg(windows)] +pub use mock_optional::*; + #[cfg(test)] pub mod testmod { use std::{ffi::CString, sync::Mutex}; diff --git a/src/sendqueue/windows.rs b/src/sendqueue/windows.rs index 5d59895e..5fa6efbd 100644 --- a/src/sendqueue/windows.rs +++ b/src/sendqueue/windows.rs @@ -61,7 +61,9 @@ impl SendQueue { /// Applications that need to precalculate exact buffer sizes can use [`packet_header_size()`](crate::packet_header_size()) /// to get the size of the header that is implicitly added along with each packet. pub fn new(memsize: u32) -> Result { - let squeue = unsafe { raw::pcap_sendqueue_alloc(memsize) }; + let library = raw::require_library()?; + + let squeue = unsafe { raw::pcap_sendqueue_alloc(&library, memsize) }; let squeue = NonNull::new(squeue).ok_or(Error::InsufficientMemory)?; Ok(Self(squeue)) diff --git a/tests/capture/activated/mod.rs b/tests/capture/activated/mod.rs index 8464f705..e0696cce 100644 --- a/tests/capture/activated/mod.rs +++ b/tests/capture/activated/mod.rs @@ -113,7 +113,14 @@ fn capture_dead_savefile_append() { drop(save); let cap = Capture::dead(Linktype(1)).unwrap(); - let mut save = cap.savefile_append(&tmpfile).unwrap(); + let appended = cap.savefile_append(&tmpfile); + + #[cfg(windows)] + if matches!(appended, Err(Error::EntrypointNotFound(_))) { + return; + } + + let mut save = appended.unwrap(); packets2.foreach(|p| save.write(p)); drop(save); @@ -134,7 +141,14 @@ fn capture_dead_savefile_offset() { let mut save = cap.savefile(&tmpfile).unwrap(); // The file header has been written, the packets have not. - let header_only = save.offset().unwrap(); + let offset = save.offset(); + + #[cfg(windows)] + if matches!(offset, Err(Error::EntrypointNotFound(_))) { + return; + } + + let header_only = offset.unwrap(); assert!(header_only > 0); packets.foreach(|p| save.write(p)); diff --git a/tests/charenc.rs b/tests/charenc.rs index bc082dbd..71d55f7e 100644 --- a/tests/charenc.rs +++ b/tests/charenc.rs @@ -21,16 +21,22 @@ const NAMES: &[&str] = &[ ]; #[cfg(not(windows))] -fn use_utf8_paths() {} +fn use_utf8_paths() -> bool { + true +} +// wpcap.dll may predate pcap_init, in which case the path stays in the local code page and +// there is nothing here to test. #[cfg(windows)] -fn use_utf8_paths() { - pcap::init(pcap::CharEncoding::Utf8).unwrap(); +fn use_utf8_paths() -> bool { + pcap::init(pcap::CharEncoding::Utf8).is_ok() } #[test] fn savefile_round_trip_non_ascii_paths() { - use_utf8_paths(); + if !use_utf8_paths() { + return; + } let dir = TempDir::new().unwrap();