Skip to content

feat: add IPv6 ipset support, add support for ipset_options#296

Merged
richm merged 1 commit into
linux-system-roles:mainfrom
richm:ipset-ipv6
Oct 14, 2025
Merged

feat: add IPv6 ipset support, add support for ipset_options#296
richm merged 1 commit into
linux-system-roles:mainfrom
richm:ipset-ipv6

Conversation

@richm

@richm richm commented Oct 6, 2025

Copy link
Copy Markdown
Contributor

feat: add IPv6 ipset support, add support for ipset_options

Feature: Add support for IPv6 addresses to ipsets. You can now specify IPv6 addresses
when using ipset in the ipset_entries list when using hash:ip or hash:net. You
can also specify ipset_options which are extra key/value pairs of options for
the ipset.

Reason: Users need to be able to specify IPv6 addresses in ipset definitions, and need
to be able to specify ipset_options for ipsets.

Result: Users can specify IPv6 addresses and options in ipset definitions.

NOTE: You cannot mix IPv4, IPv6, and MAC addresses in the same ipset_entries list.
This is a limitation of the underlying firewalld implementation.

Signed-off-by: Rich Megginson rmeggins@redhat.com

Summary by Sourcery

Enable IPv6 support for ipsets by validating entry types, propagating the appropriate family option, and updating tests and documentation.

New Features:

  • Allow specifying IPv6 addresses in ipset entries for hash:ip and hash:net types.

Enhancements:

  • Validate ipset_entries to ensure IPv4, IPv6, and MAC addresses are not mixed and fail on mixed types.
  • Propagate "family=inet6" option when creating IPv6 ipsets in both the firewall client and CLI modules.

Documentation:

  • Document the restriction against mixing IPv4, IPv6, and MAC addresses in a single ipset_entries list in the README.

Tests:

  • Expand integration tests to cover creation, querying, updating, and removal of both IPv4 and IPv6 ipsets.
  • Add unit tests for IPv6 ipset creation and mixed-address validation failure.

@sourcery-ai

sourcery-ai Bot commented Oct 6, 2025

Copy link
Copy Markdown

Reviewer's Guide

This PR introduces full IPv6 support for firewall ipsets by validating entry address types, injecting family-specific options, and extending both library code and tests to handle IPv6 entries without mixing types.

Class diagram for updated ipset creation and validation logic

classDiagram
    class FirewallLib {
        +validate_ipset_entries(ipset_entries, module)
        +set_ipset(ipset, description, short, ipset_type, ipset_entries)
        -_create_ipset(ipset, ipset_type, ipset_options)
    }
    class FirewallClientIPSetSettings {
        +setType(ipset_type)
        +setOptions(ipset_options)
    }
    FirewallLib --> FirewallClientIPSetSettings: uses
    FirewallLib : validate_ipset_entries checks address type
    FirewallLib : set_ipset injects family option for IPv6
    FirewallLib : _create_ipset passes ipset_options
Loading

File-Level Changes

Change Details Files
Validate and enforce homogeneous address types in ipset entries
  • Introduce validate_ipset_entries() using ipaddress and check_mac
  • Fail on mixed or invalid entry types
  • Return addr_type for downstream logic
library/firewall_lib.py
Pass IPv6-specific options when creating or modifying ipsets
  • Add ipset_options based on addr_type and include '--option family=inet6' in CLI path
  • Propagate ipset_options into FirewallClientIPSetSettings in API path
library/firewall_lib.py
Extend and refactor tests for IPv6 support and mixed-type failures
  • Add unit tests for IPv6 ipset creation and mixed address error
  • Update integration tests to include ipv6 entries in looped checks
  • Refactor shell commands to test both ipv4 and ipv6 ipsets
tests/unit/test_firewall_lib.py
tests/tests_ipsets.yml
tests/tests_zone.yml
Document non-mixing limitation of IPv4, IPv6, and MAC entries
  • Add NOTE to README about inability to mix address types in one ipset_entries list
README.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey there - I've reviewed your changes - here's some feedback:

  • Wrap the ipaddress.ip_interface call in validate_ipset_entries in a try/except so that invalid address formats produce a clear module.fail_json instead of an uncaught ValueError.
  • The ipset_options logic is duplicated in both the library (_create_ipset) and CLI (set_ipset) paths—consider refactoring into a shared helper to DRY up the code.
  • Add a unit test for a pure MAC‐only ipset to exercise the check_mac branch in validate_ipset_entries and ensure MAC addresses are handled correctly.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Wrap the ipaddress.ip_interface call in validate_ipset_entries in a try/except so that invalid address formats produce a clear module.fail_json instead of an uncaught ValueError.
- The `ipset_options` logic is duplicated in both the library (`_create_ipset`) and CLI (`set_ipset`) paths—consider refactoring into a shared helper to DRY up the code.
- Add a unit test for a pure MAC‐only ipset to exercise the `check_mac` branch in validate_ipset_entries and ensure MAC addresses are handled correctly.

## Individual Comments

### Comment 1
<location> `library/firewall_lib.py:352` </location>
<code_context>
+# Check that all of the ipset entries are ipv4, ipv6, or mac addresses
+# if not, fail the module
+# if they are, return "ipv4", "ipv6", or "mac"
+def validate_ipset_entries(ipset_entries, module):
+    addr_type = None
+    is_mixed_addr_types = False
</code_context>

<issue_to_address>
**issue (bug_risk):** Consider handling exceptions from ipaddress.ip_interface for invalid input.

If ipaddress.ip_interface receives invalid input, it raises a ValueError that is currently unhandled. Use a try/except block to catch this and call module.fail_json for consistent error reporting.
</issue_to_address>

### Comment 2
<location> `library/firewall_lib.py:1309-672` </location>
<code_context>
+        addr_type = validate_ipset_entries(ipset_entries, self.module)
+        if addr_type is None and ipset_entries:
+            self.module.fail_json(msg="Invalid IP address - " + str(ipset_entries))
+        ipset_options = []
+        if addr_type == "ipv6":
+            ipset_options = ["--option", "family=inet6"]
         present = self.check_state(["present", "absent"], "ipset")
</code_context>

<issue_to_address>
**issue:** Inconsistent type for ipset_options between implementations.

Standardize ipset_options to use the same type in both implementations to avoid confusion and potential errors during future refactoring.
</issue_to_address>

### Comment 3
<location> `library/firewall_lib.py:668-671` </location>
<code_context>

     def set_ipset(self, ipset, description, short, ipset_type, ipset_entries):
+        addr_type = validate_ipset_entries(ipset_entries, self.module)
+        if addr_type is None and ipset_entries:
+            self.module.fail_json(msg="Invalid IP address - " + str(ipset_entries))
+        ipset_options = {}
</code_context>

<issue_to_address>
**suggestion:** Redundant validation for addr_type after validate_ipset_entries.

Consider removing the addr_type None check, as validate_ipset_entries already handles invalid entries with module.fail_json.

```suggestion
        addr_type = validate_ipset_entries(ipset_entries, self.module)
        ipset_options = {}
```
</issue_to_address>

### Comment 4
<location> `tests/unit/test_firewall_lib.py:1366-1375` </location>
<code_context>
+    def test_create_ipset_ipv6(self, fw_ipset_settings_class, fw_class, am_class):
</code_context>

<issue_to_address>
**suggestion (testing):** Consider adding tests for invalid IPv6 addresses in ipset entries.

Adding a test for invalid IPv6 addresses will help verify that the module's validation logic properly rejects incorrect input.

Suggested implementation:

```python
    def test_create_ipset_ipv6(self, fw_ipset_settings_class, fw_class, am_class):
        am = am_class.return_value
        am.params = {
            "ipset": "test",
            "ipset_type": "hash:ip",
            "ipset_entries": ["2001:db8::1", "2001:db8::2", "2001:db8::3"],
            "description": "test ipset ipv6",
            "short": "Test",
            "state": "present",
            "permanent": True,
        }

    @patch("firewall_lib.HAS_FIREWALLD", True)
    @patch("firewall_lib.FW_VERSION", "0.9.0", create=True)
    @patch("firewall_lib.FirewallClient", create=True)
    @patch("firewall_lib.FirewallClientIPSetSettings", create=True)
    def test_create_ipset_invalid_ipv6(self, fw_ipset_settings_class, fw_class, am_class):
        am = am_class.return_value
        am.params = {
            "ipset": "test",
            "ipset_type": "hash:ip",
            "ipset_entries": [
                "2001:db8::zzzz",   # Invalid hex
                "2001:db8:1",       # Missing colons
                "2001:db8::g",      # Invalid character
                "12345::1",         # Too many digits
                "2001:db8::1::2",   # Double ::
            ],
            "description": "test ipset invalid ipv6",
            "short": "Test",
            "state": "present",
            "permanent": True,
        }
        # Assuming the module raises a ValueError for invalid IPs
        with pytest.raises(ValueError):
            # Replace 'create_ipset' with the actual function/method being tested
            create_ipset(am)

```

- If the function to create the ipset is not named `create_ipset`, replace it with the correct function/method call.
- If the module does not raise `ValueError` but uses another error handling mechanism, adjust the assertion accordingly (e.g., check for a specific error message or result).
- Ensure `pytest` is imported at the top of the file: `import pytest`
</issue_to_address>

### Comment 5
<location> `tests/unit/test_firewall_lib.py:1362-1376` </location>
<code_context>
+    @patch("firewall_lib.FirewallClientIPSetSettings", create=True)
+    def test_create_ipset_ipv6(self, fw_ipset_settings_class, fw_class, am_class):
+        am = am_class.return_value
+        am.params = {
+            "ipset": "test",
+            "ipset_type": "hash:ip",
+            "ipset_entries": ["2001:db8::1", "2001:db8::2", "2001:db8::3"],
+            "description": "test ipset ipv6",
+            "short": "Test",
+            "state": "present",
+            "permanent": True,
+        }
+
</code_context>

<issue_to_address>
**suggestion (testing):** Add a test for MAC address entries in ipset.

Please add a test case with only MAC addresses in ipset_entries to verify correct handling and validation.

```suggestion
    @patch("firewall_lib.HAS_FIREWALLD", True)
    @patch("firewall_lib.FW_VERSION", "0.9.0", create=True)
    @patch("firewall_lib.FirewallClient", create=True)
    @patch("firewall_lib.FirewallClientIPSetSettings", create=True)
    def test_create_ipset_ipv6(self, fw_ipset_settings_class, fw_class, am_class):
        am = am_class.return_value
        am.params = {
            "ipset": "test",
            "ipset_type": "hash:ip",
            "ipset_entries": ["2001:db8::1", "2001:db8::2", "2001:db8::3"],
            "description": "test ipset ipv6",
            "short": "Test",
            "state": "present",
            "permanent": True,
        }

    @patch("firewall_lib.HAS_FIREWALLD", True)
    @patch("firewall_lib.FW_VERSION", "0.9.0", create=True)
    @patch("firewall_lib.FirewallClient", create=True)
    @patch("firewall_lib.FirewallClientIPSetSettings", create=True)
    def test_create_ipset_mac(self, fw_ipset_settings_class, fw_class, am_class):
        am = am_class.return_value
        am.params = {
            "ipset": "test_mac",
            "ipset_type": "hash:mac",
            "ipset_entries": [
                "00:11:22:33:44:55",
                "AA:BB:CC:DD:EE:FF",
                "12:34:56:78:9A:BC"
            ],
            "description": "test ipset mac",
            "short": "TestMAC",
            "state": "present",
            "permanent": True,
        }
        # You may want to add assertions here to verify correct handling and validation
```
</issue_to_address>

### Comment 6
<location> `library/firewall_lib.py:667` </location>
<code_context>
    def set_ipset(self, ipset, description, short, ipset_type, ipset_entries):
        addr_type = validate_ipset_entries(ipset_entries, self.module)
        if addr_type is None and ipset_entries:
            self.module.fail_json(msg="Invalid IP address - " + str(ipset_entries))
        ipset_options = {}
        if addr_type == "ipv6":
            ipset_options = {
                "family": "inet6",
            }
        ipset_exists = ipset in self.fw.config().getIPSetNames()
        fw_ipset = None
        fw_ipset_settings = None
        if ipset_exists:
            fw_ipset = self.fw.config().getIPSetByName(ipset)
            fw_ipset_settings = fw_ipset.getSettings()
            if ipset_type and ipset_type != fw_ipset_settings.getType():
                self.module.fail_json(
                    msg="Name conflict when creating ipset - "
                    "ipset %s of type %s already exists"
                    % (ipset, fw_ipset_settings.getType())
                )
        elif self.state == "present":
            fw_ipset, fw_ipset_settings = self._create_ipset(
                ipset, ipset_type, ipset_options
            )
            self.changed = True
            ipset_exists = True
        if self.state == "present":
            if (
                description is not None
                and description != fw_ipset_settings.getDescription()
            ):
                if not self.module.check_mode:
                    fw_ipset_settings.setDescription(description)
                self.changed = True
            if short is not None and short != fw_ipset_settings.getShort():
                if not self.module.check_mode:
                    fw_ipset_settings.setShort(short)
                self.changed = True
            for entry in ipset_entries:
                if not fw_ipset_settings.queryEntry(entry):
                    if not self.module.check_mode:
                        fw_ipset_settings.addEntry(entry)
                    self.changed = True
        elif ipset_exists:
            if ipset_entries:
                for entry in ipset_entries:
                    if fw_ipset_settings.queryEntry(entry):
                        if not self.module.check_mode:
                            fw_ipset_settings.removeEntry(entry)
                        self.changed = True
            else:
                ipset_source_name = "ipset:%s" % ipset
                bound_zone_permanent = self.fw.config().getZoneOfSource(
                    ipset_source_name
                )
                if bound_zone_permanent:
                    bound_zone_permanent = "permanent - %s" % bound_zone_permanent
                bound_zone_runtime = self.fw.getZoneOfSource(ipset_source_name)
                if bound_zone_runtime:
                    bound_zone_runtime = "runtime - %s" % bound_zone_runtime
                if bound_zone_permanent or bound_zone_runtime:
                    bound_zones = " | ".join(
                        [
                            i
                            for i in [bound_zone_permanent, bound_zone_runtime]
                            if i != ""
                        ]
                    )
                    if self.module.check_mode:
                        self.module.warn(
                            "Ensure %s is removed from all zones before attempting to remove it. Enabled zones: %s"
                            % (ipset_source_name, bound_zones)
                        )
                    else:
                        self.module.fail_json(
                            msg="Remove %s from all permanent and runtime zones before attempting to remove it"
                            % ipset_source_name
                        )
                elif ipset_exists:
                    if not self.module.check_mode:
                        fw_ipset.remove()
                        ipset_exists = False
                    self.changed = True

        if self.changed and not self.module.check_mode:
            if ipset_exists:
                fw_ipset.update(fw_ipset_settings)
            self.need_reload = True

</code_context>

<issue_to_address>
**issue (code-quality):** We've found these issues:

- Use f-string instead of string concatenation ([`use-fstring-for-concatenation`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-fstring-for-concatenation/))
- Split conditional into multiple branches ([`split-or-ifs`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/split-or-ifs/))
- Merge duplicate blocks in conditional ([`merge-duplicate-blocks`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/merge-duplicate-blocks/))
- Remove redundant conditional [×2] ([`remove-redundant-if`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/remove-redundant-if/))
- Low code quality found in OnlineAPIBackend.set\_ipset - 11% ([`low-code-quality`](https://docs.sourcery.ai/Reference/Default-Rules/comments/low-code-quality/))

<br/><details><summary>Explanation</summary>




The quality score for this function is below the quality threshold of 25%.
This score is a combination of the method length, cognitive complexity and working memory.

How can you solve this?

It might be worth refactoring this function to make it shorter and more readable.

- Reduce the function length by extracting pieces of functionality out into
  their own functions. This is the most important thing you can do - ideally a
  function should be less than 10 lines.
- Reduce nesting, perhaps by introducing guard clauses to return early.
- Ensure that variables are tightly scoped, so that code using related concepts
  sits together within the function rather than being scattered.</details>
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread library/firewall_lib.py Outdated
Comment thread library/firewall_lib.py Outdated
Comment thread library/firewall_lib.py Outdated
Comment on lines +668 to +671
addr_type = validate_ipset_entries(ipset_entries, self.module)
if addr_type is None and ipset_entries:
self.module.fail_json(msg="Invalid IP address - " + str(ipset_entries))
ipset_options = {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion: Redundant validation for addr_type after validate_ipset_entries.

Consider removing the addr_type None check, as validate_ipset_entries already handles invalid entries with module.fail_json.

Suggested change
addr_type = validate_ipset_entries(ipset_entries, self.module)
if addr_type is None and ipset_entries:
self.module.fail_json(msg="Invalid IP address - " + str(ipset_entries))
ipset_options = {}
addr_type = validate_ipset_entries(ipset_entries, self.module)
ipset_options = {}

Comment on lines +1366 to +1375
def test_create_ipset_ipv6(self, fw_ipset_settings_class, fw_class, am_class):
am = am_class.return_value
am.params = {
"ipset": "test",
"ipset_type": "hash:ip",
"ipset_entries": ["2001:db8::1", "2001:db8::2", "2001:db8::3"],
"description": "test ipset ipv6",
"short": "Test",
"state": "present",
"permanent": True,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): Consider adding tests for invalid IPv6 addresses in ipset entries.

Adding a test for invalid IPv6 addresses will help verify that the module's validation logic properly rejects incorrect input.

Suggested implementation:

    def test_create_ipset_ipv6(self, fw_ipset_settings_class, fw_class, am_class):
        am = am_class.return_value
        am.params = {
            "ipset": "test",
            "ipset_type": "hash:ip",
            "ipset_entries": ["2001:db8::1", "2001:db8::2", "2001:db8::3"],
            "description": "test ipset ipv6",
            "short": "Test",
            "state": "present",
            "permanent": True,
        }

    @patch("firewall_lib.HAS_FIREWALLD", True)
    @patch("firewall_lib.FW_VERSION", "0.9.0", create=True)
    @patch("firewall_lib.FirewallClient", create=True)
    @patch("firewall_lib.FirewallClientIPSetSettings", create=True)
    def test_create_ipset_invalid_ipv6(self, fw_ipset_settings_class, fw_class, am_class):
        am = am_class.return_value
        am.params = {
            "ipset": "test",
            "ipset_type": "hash:ip",
            "ipset_entries": [
                "2001:db8::zzzz",   # Invalid hex
                "2001:db8:1",       # Missing colons
                "2001:db8::g",      # Invalid character
                "12345::1",         # Too many digits
                "2001:db8::1::2",   # Double ::
            ],
            "description": "test ipset invalid ipv6",
            "short": "Test",
            "state": "present",
            "permanent": True,
        }
        # Assuming the module raises a ValueError for invalid IPs
        with pytest.raises(ValueError):
            # Replace 'create_ipset' with the actual function/method being tested
            create_ipset(am)
  • If the function to create the ipset is not named create_ipset, replace it with the correct function/method call.
  • If the module does not raise ValueError but uses another error handling mechanism, adjust the assertion accordingly (e.g., check for a specific error message or result).
  • Ensure pytest is imported at the top of the file: import pytest

Comment on lines +1362 to +1376
@patch("firewall_lib.HAS_FIREWALLD", True)
@patch("firewall_lib.FW_VERSION", "0.9.0", create=True)
@patch("firewall_lib.FirewallClient", create=True)
@patch("firewall_lib.FirewallClientIPSetSettings", create=True)
def test_create_ipset_ipv6(self, fw_ipset_settings_class, fw_class, am_class):
am = am_class.return_value
am.params = {
"ipset": "test",
"ipset_type": "hash:ip",
"ipset_entries": ["2001:db8::1", "2001:db8::2", "2001:db8::3"],
"description": "test ipset ipv6",
"short": "Test",
"state": "present",
"permanent": True,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): Add a test for MAC address entries in ipset.

Please add a test case with only MAC addresses in ipset_entries to verify correct handling and validation.

Suggested change
@patch("firewall_lib.HAS_FIREWALLD", True)
@patch("firewall_lib.FW_VERSION", "0.9.0", create=True)
@patch("firewall_lib.FirewallClient", create=True)
@patch("firewall_lib.FirewallClientIPSetSettings", create=True)
def test_create_ipset_ipv6(self, fw_ipset_settings_class, fw_class, am_class):
am = am_class.return_value
am.params = {
"ipset": "test",
"ipset_type": "hash:ip",
"ipset_entries": ["2001:db8::1", "2001:db8::2", "2001:db8::3"],
"description": "test ipset ipv6",
"short": "Test",
"state": "present",
"permanent": True,
}
@patch("firewall_lib.HAS_FIREWALLD", True)
@patch("firewall_lib.FW_VERSION", "0.9.0", create=True)
@patch("firewall_lib.FirewallClient", create=True)
@patch("firewall_lib.FirewallClientIPSetSettings", create=True)
def test_create_ipset_ipv6(self, fw_ipset_settings_class, fw_class, am_class):
am = am_class.return_value
am.params = {
"ipset": "test",
"ipset_type": "hash:ip",
"ipset_entries": ["2001:db8::1", "2001:db8::2", "2001:db8::3"],
"description": "test ipset ipv6",
"short": "Test",
"state": "present",
"permanent": True,
}
@patch("firewall_lib.HAS_FIREWALLD", True)
@patch("firewall_lib.FW_VERSION", "0.9.0", create=True)
@patch("firewall_lib.FirewallClient", create=True)
@patch("firewall_lib.FirewallClientIPSetSettings", create=True)
def test_create_ipset_mac(self, fw_ipset_settings_class, fw_class, am_class):
am = am_class.return_value
am.params = {
"ipset": "test_mac",
"ipset_type": "hash:mac",
"ipset_entries": [
"00:11:22:33:44:55",
"AA:BB:CC:DD:EE:FF",
"12:34:56:78:9A:BC"
],
"description": "test ipset mac",
"short": "TestMAC",
"state": "present",
"permanent": True,
}
# You may want to add assertions here to verify correct handling and validation

Comment thread library/firewall_lib.py Outdated
Comment thread library/firewall_lib.py
try:
from firewall.functions import check_mac

HAS_CHECK_MAC = True

Check notice

Code scanning / CodeQL

Unused global variable Note

The global variable 'HAS_CHECK_MAC' is not used.
Comment thread library/firewall_lib.py

HAS_CHECK_MAC = True
except ImportError:
HAS_CHECK_MAC = False

Check notice

Code scanning / CodeQL

Unused global variable Note

The global variable 'HAS_CHECK_MAC' is not used.
@codecov

codecov Bot commented Oct 6, 2025

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 58.77193% with 47 lines in your changes missing coverage. Please review.
✅ Project coverage is 58.26%. Comparing base (2d7c4ba) to head (050bd5b).
⚠️ Report is 102 commits behind head on main.

Files with missing lines Patch % Lines
library/firewall_lib.py 58.77% 47 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #296      +/-   ##
==========================================
- Coverage   61.09%   58.26%   -2.83%     
==========================================
  Files           2        2              
  Lines         910     1294     +384     
==========================================
+ Hits          556      754     +198     
- Misses        354      540     +186     
Flag Coverage Δ
sanity ?

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.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@richm richm force-pushed the ipset-ipv6 branch 3 times, most recently from 441631f to 11a773e Compare October 7, 2025 13:58
@richm

richm commented Oct 7, 2025

Copy link
Copy Markdown
Contributor Author

[citest]

@richm

richm commented Oct 7, 2025

Copy link
Copy Markdown
Contributor Author

[citest]

Comment thread library/firewall_lib.py Outdated
@richm

richm commented Oct 7, 2025

Copy link
Copy Markdown
Contributor Author

[citest]

@erig0 erig0 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Otherwise, lgtm.

Comment thread library/firewall_lib.py
Comment thread library/firewall_lib.py Outdated
Comment thread library/firewall_lib.py Fixed
@richm

richm commented Oct 7, 2025

Copy link
Copy Markdown
Contributor Author

[citest]

@richm richm changed the title feat: add IPv6 ipset support feat: add IPv6 ipset support, add support for ipset_options Oct 8, 2025
@richm richm force-pushed the ipset-ipv6 branch 2 times, most recently from a945d14 to 97bdfa3 Compare October 8, 2025 23:13
@richm

richm commented Oct 8, 2025

Copy link
Copy Markdown
Contributor Author

[citest]

@richm

richm commented Oct 9, 2025

Copy link
Copy Markdown
Contributor Author

[citest]

Comment thread library/firewall_lib.py
@richm richm force-pushed the ipset-ipv6 branch 3 times, most recently from b407932 to 47333e5 Compare October 10, 2025 12:57
@richm

richm commented Oct 10, 2025

Copy link
Copy Markdown
Contributor Author

[citest]

@richm

richm commented Oct 10, 2025

Copy link
Copy Markdown
Contributor Author

[citest_bad]

Feature: Add support for IPv6 addresses to ipsets.  You can now specify IPv6 addresses
when using `ipset` in the `ipset_entries` list when using `hash:ip` or `hash:net`.  You
can also specify `ipset_options` which are extra key/value pairs of options for
the ipset.

Reason: Users need to be able to specify IPv6 addresses in `ipset` definitions, and need
to be able to specify `ipset_options` for ipsets.

Result: Users can specify IPv6 addresses and options in `ipset` definitions.

NOTE: You cannot mix IPv4, IPv6, and MAC addresses in the same `ipset_entries` list.
This is a limitation of the underlying firewalld implementation.

Signed-off-by: Rich Megginson <rmeggins@redhat.com>
@richm

richm commented Oct 10, 2025

Copy link
Copy Markdown
Contributor Author

@erig0 please review - I didn't think it was right to add support for ipv6 ipset entries without also adding support for ipset_options, even though the code will set the option family as needed. The offline code supports adding options but not removing or modifying options. The online code supports modifying options via removing the old value then adding the new value, and removing options by specifying the value as null with state: absent.

I'm not sure how to handle timeout - if the user specifies ipset_options with timeout: somevalue they will get an error

@richm

richm commented Oct 10, 2025

Copy link
Copy Markdown
Contributor Author

There is some problem with the latest centos-9 compose - the ha repo checksums do not match.

@erig0

erig0 commented Oct 10, 2025

Copy link
Copy Markdown
Collaborator

Changes look fine to me, but I don't know this code. 🫣

@erig0 please review - I didn't think it was right to add support for ipv6 ipset entries without also adding support for ipset_options, even though the code will set the option family as needed.

Agree.

The offline code supports adding options but not removing or modifying options. The online code supports modifying options via removing the old value then adding the new value, and removing options by specifying the value as null with state: absent.

ACK.

I'm not sure how to handle timeout - if the user specifies ipset_options with timeout: somevalue they will get an error

Why can't this be supported? Can the role support runtime only addition of ipset entries?

@richm

richm commented Oct 10, 2025

Copy link
Copy Markdown
Contributor Author

Why can't this be supported? Can the role support runtime only addition of ipset entries?

If it is using the runtime api (OnlineAPIBackend which uses FirewallClientIPSetSettings and the xxxEntry methods), and if the timeout option has been set, the use of the xxxEntry method will raise an exception e.g.

    @handle_exceptions
    def addEntry(self, entry):
        if "timeout" in self.settings[4] and self.settings[4]["timeout"] != "0":
            raise FirewallError(errors.IPSET_WITH_TIMEOUT)
        entry = normalize_ipset_entry(entry)
        if entry not in self.settings[5]:
            check_entry_overlaps_existing(entry, self.settings[5])
            self.settings[5].append(entry)
        else:
            raise FirewallError(errors.ALREADY_ENABLED, entry)

maybe if doing an operation that adds/removes entries from the ipset, remove the timeout option, then perform the addEntry/removeEntry, then add back the timeout option after this?

@erig0

erig0 commented Oct 13, 2025

Copy link
Copy Markdown
Collaborator

Why can't this be supported? Can the role support runtime only addition of ipset entries?

If it is using the runtime api (OnlineAPIBackend which uses FirewallClientIPSetSettings and the xxxEntry methods), and if the timeout option has been set, the use of the xxxEntry method will raise an exception e.g.

    @handle_exceptions
    def addEntry(self, entry):
        if "timeout" in self.settings[4] and self.settings[4]["timeout"] != "0":
            raise FirewallError(errors.IPSET_WITH_TIMEOUT)
        entry = normalize_ipset_entry(entry)
        if entry not in self.settings[5]:
            check_entry_overlaps_existing(entry, self.settings[5])
            self.settings[5].append(entry)
        else:
            raise FirewallError(errors.ALREADY_ENABLED, entry)

maybe if doing an operation that adds/removes entries from the ipset, remove the timeout option, then perform the addEntry/removeEntry, then add back the timeout option after this?

Hrm. That class is used for permanent configuration. If you want to add runtime only ipset entries you can use FirewallClient.addEntry().

@richm

richm commented Oct 13, 2025

Copy link
Copy Markdown
Contributor Author

Why can't this be supported? Can the role support runtime only addition of ipset entries?

If it is using the runtime api (OnlineAPIBackend which uses FirewallClientIPSetSettings and the xxxEntry methods), and if the timeout option has been set, the use of the xxxEntry method will raise an exception e.g.

    @handle_exceptions
    def addEntry(self, entry):
        if "timeout" in self.settings[4] and self.settings[4]["timeout"] != "0":
            raise FirewallError(errors.IPSET_WITH_TIMEOUT)
        entry = normalize_ipset_entry(entry)
        if entry not in self.settings[5]:
            check_entry_overlaps_existing(entry, self.settings[5])
            self.settings[5].append(entry)
        else:
            raise FirewallError(errors.ALREADY_ENABLED, entry)

maybe if doing an operation that adds/removes entries from the ipset, remove the timeout option, then perform the addEntry/removeEntry, then add back the timeout option after this?

Hrm. That class is used for permanent configuration. If you want to add runtime only ipset entries you can use FirewallClient.addEntry().

That is what I'm using, AFAICT. The issue is that firewalld does not like to add an ipset with entries and with timeout set. The error I get is from firewalld via the d-bus api:

journalctl -u firewalld
Oct 13 12:25:52 ibm-p8-kvm-03-guest-02.virt.pnr.lab.eng.rdu2.redhat.com systemd[1]: Starting firewalld.service - firewalld - dynamic firewall daemon...
Oct 13 12:25:53 ibm-p8-kvm-03-guest-02.virt.pnr.lab.eng.rdu2.redhat.com systemd[1]: Started firewalld.service - firewalld - dynamic firewall daemon.
Oct 13 12:25:59 ibm-p8-kvm-03-guest-02.virt.pnr.lab.eng.rdu2.redhat.com firewalld[2548]: ERROR: IPSET_WITH_TIMEOUT

@richm

richm commented Oct 14, 2025

Copy link
Copy Markdown
Contributor Author

[citest]

@richm

richm commented Oct 14, 2025

Copy link
Copy Markdown
Contributor Author

Reviewed and approved by customer

@richm richm merged commit 1c6e50a into linux-system-roles:main Oct 14, 2025
46 of 49 checks passed
@richm richm deleted the ipset-ipv6 branch October 14, 2025 18:49
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