Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/workflows/00-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
16 changes: 8 additions & 8 deletions .github/workflows/01-build-and-test-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 }}
Expand Down
7 changes: 1 addition & 6 deletions .github/workflows/02-coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
9 changes: 7 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -70,14 +74,15 @@
### 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.
- `Savefile::write` no longer reads past the end of the packet data.
- `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

Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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`
Expand Down
34 changes: 23 additions & 11 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -106,7 +105,7 @@ fn get_libpcap_version(libdirpath: Option<PathBuf>) -> Result<Version, Box<dyn s
// "well-behaved." See https://github.com/nagisa/rust_libloading/issues/86 and
// https://github.com/nagisa/rust_libloading/blob/0.8.3/src/changelog.rs#L96-L151 for details.
let Ok(lib) = (unsafe { libloading::Library::new(libfile) }) else {
return Ok(Version::max());
return Ok(Version::fallback());
};

type PcapLibVersion = unsafe extern "C" fn() -> *mut c_char;
Expand Down Expand Up @@ -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.
Expand Down
43 changes: 38 additions & 5 deletions src/capture/activated/dead.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ use crate::capture::Precision;
impl Capture<Dead> {
/// Creates a "fake" capture handle for the given link type.
pub fn dead(linktype: Linktype) -> Result<Capture<Dead>, 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::<raw::pcap_t>::new(handle).ok_or(Error::InsufficientMemory)?,
))
Expand All @@ -25,8 +27,17 @@ impl Capture<Dead> {
linktype: Linktype,
precision: Precision,
) -> Result<Capture<Dead>, 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::<raw::pcap_t>::new(handle).ok_or(Error::InsufficientMemory)?,
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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(_))));
}
}
43 changes: 43 additions & 0 deletions src/capture/activated/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,11 @@ impl<T: Activated + ?Sized> Capture<T> {
/// 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<P: AsRef<Path>>(&self, path: P) -> Result<Savefile, Error> {
#[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::<raw::pcap_dumper_t>::new(unsafe {
raw::pcap_dump_open_append(self.handle.as_ptr(), name.as_ptr())
Expand Down Expand Up @@ -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()) };

Expand Down Expand Up @@ -888,6 +898,11 @@ mod tests {
let test_capture = test_capture::<Offline>(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)
Expand All @@ -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::<Offline>(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();
Expand Down Expand Up @@ -934,6 +967,11 @@ mod tests {
let test_capture = test_capture::<Offline>(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)
Expand Down Expand Up @@ -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);

Expand Down
Loading
Loading