Skip to content

Conversation

Dimi1010
Copy link
Collaborator

@Dimi1010 Dimi1010 commented Sep 10, 2025

This PR updates BpfFilterWrapper to allow matching with a packet while the filter is marked as const.

It also extends the wrapper with C++11 move constructors and a new constructor to create an instance with a set filter.

It additionally adds const qualified match methods to GenericFilter.

Copy link

codecov bot commented Sep 10, 2025

Codecov Report

❌ Patch coverage is 86.06557% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.49%. Comparing base (96484fb) to head (e6d7d21).

Files with missing lines Patch % Lines
Pcap++/src/PcapFilter.cpp 82.82% 10 Missing and 7 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##              dev    #1957      +/-   ##
==========================================
- Coverage   83.51%   83.49%   -0.02%     
==========================================
  Files         311      311              
  Lines       54897    54960      +63     
  Branches    12213    11938     -275     
==========================================
+ Hits        45845    45891      +46     
+ Misses       7788     7774      -14     
- Partials     1264     1295      +31     
Flag Coverage Δ
alpine320 75.92% <71.79%> (-0.04%) ⬇️
fedora42 76.04% <69.13%> (-0.07%) ⬇️
macos-13 81.63% <78.26%> (-0.03%) ⬇️
macos-14 81.63% <78.26%> (-0.02%) ⬇️
macos-15 81.64% <78.26%> (-0.02%) ⬇️
mingw32 70.28% <66.66%> (-0.04%) ⬇️
mingw64 70.27% <66.66%> (+0.06%) ⬆️
npcap ?
rhel94 75.77% <70.51%> (-0.05%) ⬇️
ubuntu2004 60.21% <68.83%> (-0.06%) ⬇️
ubuntu2004-zstd 60.34% <68.83%> (-0.03%) ⬇️
ubuntu2204 75.70% <70.51%> (-0.07%) ⬇️
ubuntu2204-icpx 60.78% <74.78%> (+0.02%) ⬆️
ubuntu2404 75.94% <71.79%> (-0.06%) ⬇️
ubuntu2404-arm64 75.59% <71.79%> (-0.04%) ⬇️
unittest 83.49% <86.06%> (-0.02%) ⬇️
windows-2022 85.49% <83.33%> (+0.13%) ⬆️
windows-2025 85.51% <82.41%> (+0.10%) ⬆️
winpcap 85.51% <82.41%> (-0.10%) ⬇️
xdp 53.50% <0.00%> (-0.05%) ⬇️

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.

@Dimi1010 Dimi1010 marked this pull request as ready for review September 14, 2025 20:11
@Dimi1010 Dimi1010 requested a review from seladb as a code owner September 14, 2025 20:11
Copy link
Owner

@seladb seladb left a comment

Choose a reason for hiding this comment

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

I left a few comments, but in general, many changes make this PR hard to review. Maybe we can break it up to multiple PRs?

/// @brief Match a raw packet against the filter.
/// @param rawPacket The raw packet to match against.
/// @return True if the filter matches (or if it's empty). False otherwise.
bool matches(RawPacket const& rawPacket) const;
Copy link
Owner

Choose a reason for hiding this comment

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

This is an overload of matchPacketWithFilter, no? Why not keep the same name?
Also - maybe add const qualifier to the existing methods?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I suppose it can be done, but I made a new method for two reasons.

For one, matchPacketWithFilter() takes a pointer param, matches() takes a const reference parameter.

The second one is that IMO matchPacketWithFilter is a bit overly verbose for a method to a filter object? The object itself is already a filter, so there isn't much else to match with? The flow would then be filter -> matches -> rawPacket, which IMO would read cleaner. I figured that since the parameter signature is changing, might as well do both changes at once.

Copy link
Owner

Choose a reason for hiding this comment

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

Having methods with different names that do more or less the same thing is confusing. We can deprecate the old methods and generate matches methods instead, but I think it'd be easier to just stick with the same name

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'm fine with deprecating the old ones. I think matches makes for a more user friendly API since it is more concise.

While we are on the topic. I think the IPcapDevice::matchPacketWithFilter should probably be deprecated too. It is a static method that is somewhat redundant. It requires the user to pass a filter object and a packet. If the user already has that, he can just call filter.match***(packet) instead, which IMO would be more readable than IPcapDevice::matchPacketWithFilter(filter, packet).

Copy link
Owner

Choose a reason for hiding this comment

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

I'm fine with deprecating the old ones. I think matches makes for a more user friendly API since it is more concise.

I guess we can deprecate them? 🤔

While we are on the topic. I think the IPcapDevice::matchPacketWithFilter should probably be deprecated too. It is a static method that is somewhat redundant. It requires the user to pass a filter object and a packet. If the user already has that, he can just call filter.match***(packet) instead, which IMO would be more readable than IPcapDevice::matchPacketWithFilter(filter, packet).

I agree, I think we can deprecate it. To be honest, I don't remember why it was added, there was probably a reason but I don't recall what it was

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@seladb Do you want to deprecate them here or in a new PR so it is cleaner? IMO, a clean PR afterwards would be better for finding the deprecations later.

Copy link
Owner

Choose a reason for hiding this comment

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

It's ok to open a new PR for the deprecations 👍

Comment on lines 178 to 187
bool GeneralFilter::cacheFilterInternal() const
{
std::string filterStr;
parseToString(filterStr);
if (!m_BpfWrapper.setFilter(filterStr))
return false;

m_CachedFilter = true;
return true;
}
Copy link
Owner

Choose a reason for hiding this comment

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

Why do we need this method? BpfFilterWrapper::setFilter() already prevents resetting the filter if it hasn't changed

Copy link
Collaborator Author

@Dimi1010 Dimi1010 Sep 15, 2025

Choose a reason for hiding this comment

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

The new code prevents the need to recompute the filter string, which was previously done on every call, to check if the filter needs to be reset. Now it is just a bool check.

Copy link
Owner

Choose a reason for hiding this comment

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

I don't think recomputing the filter string is a big deal since it is not done in real time

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Wdym not done in real time?

Copy link
Owner

Choose a reason for hiding this comment

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

Setting a filter (usually) isn't time-critical, meaning that spending a few more cycles on parsing the string shouldn't be an issue. As opposed to parsing a packet which is done in real-time so should be as fast as possible

Copy link
Owner

Choose a reason for hiding this comment

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

Got it. I dug deeper into it, and the complexity is about invalidating the cache when the filter changes - we need to call invalidateCache() in every setter, and we might miss one (for example, you missed calling it in setDirection() 🙂 ).
I'm not sure how to solve it though...

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed the invalidate miss. Thanks! b06fcef

I am not sure there is a better way to do it. With this optimization the derived classes need to signal the base implementation to regenerate the cache somehow, since they add new public mutators that the base doesn't know about.

Copy link
Owner

Choose a reason for hiding this comment

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

I asked ChatGPT, and it suggested using a Property<T> Template Wrapper:

#include <iostream>
#include <functional>

template<typename T>
class Property {
    T value;
    std::function<void()> beforeSetter;
public:
    Property(std::function<void()> beforeSetterFunc = nullptr)
        : beforeSetter(beforeSetterFunc) {}

    T get() const {
        return value;
    }

    void set(const T& val) {
        if (beforeSetter) beforeSetter();
        value = val;
    }

    // Optional: Implicit conversion to T
    operator T() const {
        return value;
    }
};

class Base {
protected:
    void beforeSetter() {
        std::cout << "[Base] beforeSetter called\n";
    }

public:
    Property<int> x;

    Base() : x([this]() { beforeSetter(); }) {}

    // Optional convenience
    void setX(int val) { x.set(val); }
    int getX() const { return x.get(); }
};

class DerivedA : public Base {
protected:
    void beforeSetter() {
        std::cout << "[DerivedA] beforeSetter called\n";
    }

public:
    Property<std::string> name;

    DerivedA() : name([this]() { beforeSetter(); }) {}
    
    void setName(const std::string& n) { name.set(n); }
    std::string getName() const { return name.get(); }
};

Similarly, it suggested a PropertyVector<T> for vectors:

#include <vector>
#include <algorithm>

template<typename T>
class PropertyVector {
    std::vector<T> value;
    std::function<void()> beforeSetter;
public:
    PropertyVector(std::function<void()> beforeSetterFunc = nullptr)
        : beforeSetter(beforeSetterFunc) {}

    const std::vector<T>& get() const {
        return value;
    }

    void set(const std::vector<T>& vec) {
        if (beforeSetter) beforeSetter();
        value = vec;
    }

    void add(const T& element) {
        if (beforeSetter) beforeSetter();
        value.push_back(element);
    }

    bool remove(const T& element) {
        if (beforeSetter) beforeSetter();
        auto it = std::find(value.begin(), value.end(), element);
        if (it != value.end()) {
            value.erase(it);
            return true;
        }
        return false;
    }

    std::size_t size() const {
        return value.size();
    }

    T& operator[](std::size_t idx) {
        return value[idx];
    }

    const T& operator[](std::size_t idx) const {
        return value[idx];
    }
};

It's a nice pattern but I'm not sure if it's better than just calling invalidateCache(), what do you think?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Looks nice, but I think its not worth it in the current case. invalidateCache() will most likely be inlined as it is a single bool operation. Going through std::function removes that possibility as that is a call through function pointer, which the compiler doesn't know anything about at compile time.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

There is also the fact that this will also need to be setup in the derived classes, which makes it no different than simply calling invalidateCache().

@Dimi1010 Dimi1010 requested a review from Copilot September 20, 2025 11:12
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

Adds const-qualified match methods and caching to BpfFilterWrapper and GeneralFilter to enable matching packets on const instances, introduces move semantics, and refactors filter compilation and caching.

  • Add const overloads: BpfFilterWrapper::matchPacketWithFilter and new matches(...) helpers
  • Introduce filter program caching with on-demand recompilation when link type changes
  • Make parseToString methods const across filters and add cache invalidation hooks in setters

Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
Pcap++/src/PcapFilter.cpp Implements new const matching logic, filter compilation helper, cache handling, and updates parseToString definitions.
Pcap++/header/PcapFilter.h Declares new API (const matches methods, move ctor/assign), adds caching state to GeneralFilter and const parseToString signatures, and adds invalidateCache to mutators.

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

/// @param filter A filter in BPF syntax
/// @param linkType An optional parameter to set the filter's link type. The default is LINKTYPE_ETHERNET
/// @throws std::runtime_error if the filter could not be compiled
explicit BpfFilterWrapper(std::string filter, LinkLayerType linkType = LINKTYPE_ETHERNET);
Copy link
Owner

Choose a reason for hiding this comment

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

We never use this constructor, why do we need it?
It may throw an exception, which we almost don't do in this file, so it's better to avoid the c'tor altogether

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I try to add RAII constructors when I can, so full initialization can be done in a single step.

Conceptually, the default constructor creates an "always true" noop filter. It makes sense to have a separate constructor that allows a specific filter to be set from the time of initialization instead of having to do a second call to `setFilter°.

Copy link
Owner

Choose a reason for hiding this comment

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

I am all for adding constructors and overloads if we need them, but if we don't, and especially when BpfFilterWrapper is mainly used internally, I don't see the point in adding code that we don't need

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hmm, sure, I will remove it then.

Comment on lines +105 to +109
// This should never happen, but just in case
if (m_CachedProgram == nullptr)
{
return false;
throw std::runtime_error("No compiled BPF program available");
}
Copy link
Owner

Choose a reason for hiding this comment

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

This could potentially happen if BpfFilterWrapper is created using the default c'tor and then the user calls matches(). I wouldn't throw an exception here to make it compatible with the rest of the file, instead, I'd write an error log and return false

Copy link
Collaborator Author

@Dimi1010 Dimi1010 Sep 24, 2025

Choose a reason for hiding this comment

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

Huh, I didn't notice that. Good catch.

If it is a valid case, then it can be merged into the recompile condition. If there is no compiled filter, it will attempt to recompile and proceed as normal from there.

Comment on lines +167 to +173
if (!m_CachedFilter)
{
if (!cacheFilter())
{
return false;
}
}
Copy link
Owner

Choose a reason for hiding this comment

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

Maybe we can do:

if (!m_CachedFilter && !cacheFilter())
{
    return false;
}

Comment on lines +182 to +183
if (!m_BpfWrapper.setFilter(filterStr))
return false;
Copy link
Owner

Choose a reason for hiding this comment

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

nit: in newer code I prefer using curly brackets {} even if an if clause has just one line

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

Successfully merging this pull request may close these issues.

2 participants