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 (f42df08).

Additional details and impacted files
@@           Coverage Diff            @@
##              dev    #2019    +/-   ##
========================================
  Coverage   83.45%   83.45%            
========================================
  Files         311      312     +1     
  Lines       54556    54568    +12     
  Branches    11491    11820   +329     
========================================
+ Hits        45529    45541    +12     
- Misses       7798     7835    +37     
+ Partials     1229     1192    -37     
Flag Coverage Δ
alpine320 75.90% <100.00%> (-0.01%) ⬇️
fedora42 75.46% <100.00%> (+<0.01%) ⬆️
macos-14 81.58% <100.00%> (+<0.01%) ⬆️
macos-15 81.57% <100.00%> (+<0.01%) ⬆️
mingw32 70.01% <ø> (ø)
mingw64 69.88% <ø> (ø)
npcap 85.25% <ø> (ø)
rhel94 75.46% <100.00%> (+<0.01%) ⬆️
ubuntu2004 59.49% <85.71%> (+<0.01%) ⬆️
ubuntu2004-zstd 59.58% <85.71%> (-0.01%) ⬇️
ubuntu2204 75.40% <100.00%> (-0.04%) ⬇️
ubuntu2204-icpx 57.84% <ø> (ø)
ubuntu2404 75.53% <100.00%> (+0.01%) ⬆️
ubuntu2404-arm64 75.57% <100.00%> (+<0.01%) ⬆️
unittest 83.45% <100.00%> (+<0.01%) ⬆️
windows-2022 85.25% <ø> (ø)
windows-2025 85.32% <ø> (ø)
winpcap 85.53% <ø> (ø)
xdp 53.00% <0.00%> (-0.02%) ⬇️

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
Comment on lines +91 to +106
OverlappedResult getOverlappedResult(const IWinDivertHandle* handle) override
{
auto winDivertHandle = dynamic_cast<const WinDivertHandle*>(handle);
if (winDivertHandle == nullptr)
{
throw std::runtime_error("Failed to get WinDivertHandle");
}

DWORD packetLen = 0;
if (GetOverlappedResult(winDivertHandle->get(), &m_Overlapped, &packetLen, FALSE))
{
return { OverlappedResult::Status::Success, static_cast<uint32_t>(packetLen), 0 };
}

return { OverlappedResult::Status::Failed, 0, static_cast<uint32_t>(GetLastError()) };
}
Copy link
Collaborator

@Dimi1010 Dimi1010 Nov 11, 2025

Choose a reason for hiding this comment

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

As far as I know, Win32 GetOverlappedResult does not modify anything in the result structure, so I think the whole operation can be const.

Also, wouldn't a reference param be better instead of pointer? We don't want nullptr handles being passed?

Copy link
Owner Author

Choose a reason for hiding this comment

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

GetOverlappedResult updates m_Overlapped so I guess it shouldn't be made const, no? 🤔

This method gets a pointer because we want to be able to cast it to WinDivertHandle

Copy link
Collaborator

Choose a reason for hiding this comment

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

GetOverlappedResult updates m_Overlapped so I guess it shouldn't be made const, no?

Sure, we can leave it non-const then.

This method gets a pointer because we want to be able to cast it to WinDivertHandle

We can also cast to WinDivertHandle if we take a reference, as those are technically a pointer under the hood.

OverlappedResult getOverlappedResult(const IWinDivertHandle& handle) override
{
	auto winDivertHandle = dynamic_cast<const WinDivertHandle*>(&handle);			
	if (winDivertHandle == nullptr)
	{
		throw std::runtime_error("Failed to get WinDivertHandle");
	}

	/* ... */
}

Also if we are just throwing an exception on cast failure can't we just use:

auto& winDivertHandle = dynamic_cast<const WinDivertHandle&>(handle);

This has a built-in std::bad_cast throw if the cast fails, which IMO is more descriptive than std::runtime_error?

Copy link
Owner Author

Choose a reason for hiding this comment

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

We can also cast to WinDivertHandle if we take a reference, as those are technically a pointer under the hood.

I guess we can but it's not so nice to do dynamic_cast<const WinDivertHandle*>(&handle); 🤔

This has a built-in std::bad_cast throw if the cast fails, which IMO is more descriptive than std::runtime_error?

I think throwing a custom exception with a custom message is nicer, no?

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

Copy link
Collaborator

@Dimi1010 Dimi1010 Nov 13, 2025

Choose a reason for hiding this comment

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

Curious can't we write the entire script in powershell instead of having batch script that runs an embedded powershell script?
GH actions do support running a step directly in powershell.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Our other scripts are batch scripts and not ps1 scripts... maybe it's better to convert the powershell part to batch, what do you think?

Comment on lines +133 to +141
virtual uint32_t close(const IWinDivertHandle* handle) = 0;
virtual uint32_t recvEx(const IWinDivertHandle* handle, uint8_t* buffer, uint32_t bufferLen,
size_t addressesSize, IOverlappedWrapper* overlapped) = 0;
virtual std::vector<WinDivertAddress> recvExComplete() = 0;
virtual uint32_t sendEx(const IWinDivertHandle* handle, uint8_t* buffer, uint32_t bufferLen,
size_t addressesSize) = 0;
virtual std::unique_ptr<IOverlappedWrapper> createOverlapped() = 0;
virtual bool getParam(const IWinDivertHandle* handle, WinDivertParam param, uint64_t& value) = 0;
virtual bool setParam(const IWinDivertHandle* handle, WinDivertParam param, uint64_t value) = 0;
Copy link
Collaborator

Choose a reason for hiding this comment

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

ditto: Windivert handle pointer -> reference. The API is meaningless for nullptr handle.

Copy link
Collaborator

@Dimi1010 Dimi1010 Nov 13, 2025

Choose a reason for hiding this comment

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

Also, can't those functions be member functions of the IWinDivertHandle wrapper instead? That way we don't have to dynamic cast inside this class's implementors.

Copy link
Collaborator

@Dimi1010 Dimi1010 Nov 13, 2025

Choose a reason for hiding this comment

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

Expanding on this, I think restructuring IWinDivertImplementation and IWinDivertHandle would be beneficial for code performance and readability.

  • IWinDivertImplementation should just be a stateless factory for IWinDivertHandle objects.
    Maybe getNetworkInterfaces should be kept in there too, as that seems non-specific to the handles. The change would allow a single factory object to be shared between device instances.

  • IWinDivertHandle changes:

    • Operations directly applied to the handle should be transferred to the object (e.g. recvEx, sendEx, setParam, getParam, etc...). This would simplify the interface and remove the need to downcast inside IWinDivertImplementation, which is slow.
    • The current member IWinDivertImplementation::close should be removed. The handle would be closed by deleting the IWinDivertHandle object in RAII pattern.

Copy link
Owner Author

Choose a reason for hiding this comment

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

The idea was to mimic the WinDivert interface as much as possible - IWinDivertHandle is an abstraction of WinDivert's HANDLE, and IWinDivertImplementation should be a very thin wrapper around WinDivert interface. I did it on purpose so mocking WinDivert will be straightforward and most of the business logic will be inside WinDivertDevice

Copy link
Collaborator

Choose a reason for hiding this comment

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

I see.

I think we can still keep mostly the same API structure even with the proposed redesign, no?

Won't be complete 1 to 1, but the main change would be the handle first param would be omitted from the signature.

My main issue is that the current design uses dynamic cast on every operation, which is unnecessary overhead and makes the thin wrapper heavier than it needs to be.

@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 +290 to +293
find_package(WinDivert REQUIRED)
if(NOT WinDivert_FOUND)
message(FATAL_ERROR "WinDivert not found!")
endif()
Copy link
Collaborator

Choose a reason for hiding this comment

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

The required flag on find_package will make the process fail if WinDivert is missing. There is no need for the if not found branch.

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