Skip to content

Conversation

@seladb
Copy link
Owner

@seladb seladb commented Nov 9, 2025

This PR integrates WinDivert as another packet capture engine in PcapPlusPlus, enabling packet capture/injection on Windows via the WinDivert driver.

What is WinDivert

WinDivert is an open-source Windows library (kernel + user-mode) that allows applications to intercept, modify, drop or inject network packets traversing the Windows network stack. It is designed for use cases such as packet sniffing, firewalling, NAT-/VPN-tunneling, loopback traffic inspection, etc.

Key features include:

  • Capturing both inbound and outbound packets (and loopback) on Windows 7/8/10/11.
  • Support for IPv4 and IPv6, and a simple filtering language.
  • User-mode API (via windivert.h / WinDivert.dll) that interacts with a kernel-mode driver.

Project Links

Testing

This PR includes basic tests for the WinDivertDevice. However, it also adds a lightweight abstraction over the WinDivert API using internal interfaces. It enables testing WinDivertDevice logic without the real driver by providing mock implementations. These mock tests aren't implemented in this PR, but can be added later.

@codecov
Copy link

codecov bot commented Nov 9, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 83.45%. Comparing base (a789e12) to head (0ab7d08).
⚠️ Report is 4 commits behind head on dev.

Additional details and impacted files
@@            Coverage Diff             @@
##              dev    #2019      +/-   ##
==========================================
- Coverage   83.46%   83.45%   -0.01%     
==========================================
  Files         311      312       +1     
  Lines       54556    54590      +34     
  Branches    11491    11500       +9     
==========================================
+ Hits        45534    45560      +26     
- Misses       7795     7832      +37     
+ Partials     1227     1198      -29     
Flag Coverage Δ
alpine320 75.88% <100.00%> (-0.02%) ⬇️
fedora42 75.42% <100.00%> (-0.04%) ⬇️
macos-14 81.57% <100.00%> (-0.01%) ⬇️
macos-15 81.56% <100.00%> (-0.02%) ⬇️
mingw32 69.98% <ø> (-0.05%) ⬇️
mingw64 69.84% <ø> (-0.04%) ⬇️
npcap 85.25% <ø> (-0.01%) ⬇️
rhel94 75.45% <100.00%> (-0.03%) ⬇️
ubuntu2004 59.47% <85.71%> (-0.03%) ⬇️
ubuntu2004-zstd 59.57% <85.71%> (-0.02%) ⬇️
ubuntu2204 75.39% <100.00%> (-0.04%) ⬇️
ubuntu2204-icpx 57.83% <ø> (-0.01%) ⬇️
ubuntu2404 75.48% <100.00%> (-0.04%) ⬇️
ubuntu2404-arm64 75.56% <100.00%> (-0.02%) ⬇️
unittest 83.45% <100.00%> (-0.01%) ⬇️
windows-2022 85.25% <ø> (-0.01%) ⬇️
windows-2025 85.32% <ø> (-0.01%) ⬇️
winpcap 85.52% <ø> (-0.01%) ⬇️
xdp 52.97% <0.00%> (-0.04%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@seladb seladb changed the title Add support for WinDivert packet capture engine [DRAFT] Add support for WinDivert packet capture engine Nov 10, 2025
PTF_ASSERT_TRUE(sendURLRequest("www.google.com"));
// let the capture work for couple of seconds
totalSleepTime = incSleep(capturedPackets, 2, 7);
totalSleepTime = incSleep(capturedPackets, 2, 20);
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test also failed in CI so I made it more robust

Comment on lines +75 to 83
parser.add_argument(
"--include-tests",
"-t",
type=str,
nargs="+",
default=[],
help="Pcap++ tests to include",
)
parser.add_argument(
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change was needed to support running only WinDivert tests in the windivert job in CI

@seladb seladb marked this pull request as ready for review November 14, 2025 07:36
@seladb seladb changed the title [DRAFT] Add support for WinDivert packet capture engine Add support for WinDivert packet capture engine Nov 14, 2025
Comment on lines +432 to +434
/// @brief Replace the underlying implementation (intended for testing/mocking).
/// @param[in] implementation An implementation of the WinDivert backend APIs.
void setImplementation(std::unique_ptr<internal::IWinDivertImplementation> implementation);
Copy link
Collaborator

@Dimi1010 Dimi1010 Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, this should use constructor dependency injection and disallow switching of implementation after a device has been constructed. It simplifies the lifecycle.

Curently, this setter directly swaps the implementation. If that happens while the device is already open, it might cause a desync between implementation and handle? It's not a big deal currently, but it's still an inconsistency.

/// - Enumerating relevant Windows network interfaces.
/// Keeping these responsibilities here keeps WinDivertDevice decoupled from concrete
/// system/driver calls and enables unit testing and alternative implementations.
class IWinDivertImplementation
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: "Implementation" seems a bit generic descriptor? Maybe IWinDivertDriver or IWinDivertAPI? I prefer driver, since its not supposed to be the pubic API we provide.

/// @typedef ReceivePacketCallback
/// @brief Callback invoked with a batch of received packets when using the callback receive API.
/// The callback is called from the receiving loop until stopReceive() is invoked or an error/timeout occurs.
using ReceivePacketCallback = std::function<void(const WinDivertRawPacketVector& packetVec)>;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious, do we want to add a context second parameter as future extension point? It can be an empty struct or it can hold a pointer to the device? Adding it initially would allow adding more context data in the future if needed without breaking the callback signature.

The other devices have a "Device" callback, but I find that a bit rigid if you want to add data only relevant to the callback context.

struct WinDivertReceiveCallbackCtx
{
   WinDivertDevice* device = nullptr; // Maybe?
};

std::function<void(const WinDivertRawPacketVector& packetVec, WinDivertReceiveCallbackCtx& ctx)>`
``

Comment on lines +34 to +35
WinDivertOverlappedWrapper(const WinDivertOverlappedWrapper&) = delete;
WinDivertOverlappedWrapper& operator=(const WinDivertOverlappedWrapper&) = delete;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: If we are holding the overlapped struct as value inside the object, perhaps mark the move ctors explicitly deleted too? We don't want the object to be moved either.

They won't get implicitly declared anyway, so its fine either way code wise.

remainingBytes -= packetLength;
}

callback(receivedPackets);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should probably check if the callback is empty. Calling an empty std::function is UB.

The check can be either here (if for some reason receiving packets and doing nothing with them is valid) or at the start. I think the start would be better?

return { SendResult::Status::Failed, 0, "Batch size has to be a positive number" };
}

uint8_t buffer[WINDIVERT_BUFFER_LEN];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: std::array?

endif()

# Normalize user-provided root path
if(DEFINED WINDIVERT_ROOT)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make this WinDivert_ROOT to match the rest of the variables in the module?
As far as I know, it is expected for the variables to match precisely with the package name.

Suggested change
if(DEFINED WINDIVERT_ROOT)
if(DEFINED WinDivert_ROOT)

# Normalize user-provided root path
if(DEFINED WINDIVERT_ROOT)
file(TO_CMAKE_PATH "${WINDIVERT_ROOT}" _WinDivert_ROOT_HINT)
elseif(DEFINED ENV{WINDIVERT_ROOT})
Copy link
Collaborator

@Dimi1010 Dimi1010 Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto: var name.

Unsure about this one tho. Env vars are case sensitive on unix, and case-insensitive on windows. But probably better to match exact anyway.

Suggested change
elseif(DEFINED ENV{WINDIVERT_ROOT})
elseif(DEFINED ENV{WinDivert_ROOT})

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants