Skip to content

Commit 87ca0b8

Browse files
committed
feat: add IPv6 ipset support
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`. Reason: Users need to be able to specify IPv6 addresses in `ipset` definitions. Result: Users can specify IPv6 addresses 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 <[email protected]>
1 parent 0513cd1 commit 87ca0b8

File tree

8 files changed

+355
-50
lines changed

8 files changed

+355
-50
lines changed

.sanity-ansible-ignore-2.18.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
plugins/modules/firewall_lib.py validate-modules:missing-gplv3-license
2+
plugins/modules/firewall_lib_facts.py validate-modules:missing-gplv3-license

.sanity-ansible-ignore-2.19.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
plugins/modules/firewall_lib.py validate-modules:missing-gplv3-license
2+
plugins/modules/firewall_lib_facts.py validate-modules:missing-gplv3-license

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,10 @@ Use `source` to add and remove ipsets from a zone
384384
When creating an ipset, you must also specify `ipset_type`,
385385
and optionally `short`, `description`, `ipset_entries`
386386

387+
**NOTE**: You cannot mix IPv4, IPv6, and MAC addresses in the same
388+
`ipset_entries` list. All addresses must be the same IP type. This is a
389+
limitation of the underlying firewalld implementation.
390+
387391
Defining an ipset with all optional fields:
388392

389393
```yaml
@@ -478,6 +482,10 @@ Used with `ipset`
478482
Entries must be compatible with the ipset type of the `ipset`
479483
being created or modified.
480484

485+
**NOTE**: You cannot mix IPv4, IPv6, and MAC addresses in the same
486+
`ipset_entries` list. All addresses must be the same IP type. This is a
487+
limitation of the underlying firewalld implementation.
488+
481489
```yaml
482490
ipset: customipset
483491
ipset_entries:

library/firewall_lib.py

Lines changed: 81 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
# You should have received a copy of the GNU General Public License
2121
# along with this program. If not, see <http://www.gnu.org/licenses/>.
2222

23-
from __future__ import absolute_import, division, print_function
23+
from __future__ import absolute_import, division, print_function, unicode_literals
2424

2525
__metaclass__ = type
2626

@@ -283,6 +283,24 @@
283283
import re
284284
import os
285285

286+
try:
287+
import ipaddress
288+
289+
HAS_IPADDRESS = True
290+
except ImportError:
291+
HAS_IPADDRESS = False
292+
293+
try:
294+
from firewall.functions import check_mac
295+
296+
HAS_CHECK_MAC = True
297+
except ImportError:
298+
HAS_CHECK_MAC = False
299+
300+
def check_mac(mac):
301+
return False
302+
303+
286304
try:
287305
import firewall.config
288306

@@ -343,6 +361,42 @@ def try_set_zone_of_interface(module, _zone, interface):
343361
return (False, False)
344362

345363

364+
# Check that all of the ipset entries are ipv4, ipv6, or mac addresses
365+
# if not, fail the module
366+
# if they are, return "ipv4", "ipv6", or "mac"
367+
def validate_ipset_entries(ipset_entries, module):
368+
addr_type = None
369+
is_mixed_addr_types = False
370+
for entry in ipset_entries:
371+
if check_mac(entry):
372+
if addr_type is None:
373+
addr_type = "mac"
374+
elif addr_type != "mac":
375+
is_mixed_addr_types = True
376+
else:
377+
if HAS_IPADDRESS:
378+
addr = ipaddress.ip_interface(entry)
379+
else:
380+
module.fail_json(msg="No IP address library found")
381+
if addr.version == 4:
382+
if addr_type is None:
383+
addr_type = "ipv4"
384+
elif addr_type != "ipv4":
385+
is_mixed_addr_types = True
386+
elif addr.version == 6:
387+
if addr_type is None:
388+
addr_type = "ipv6"
389+
elif addr_type != "ipv6":
390+
is_mixed_addr_types = True
391+
else:
392+
module.fail_json(msg="Invalid IP address - " + entry)
393+
if is_mixed_addr_types:
394+
module.fail_json(
395+
msg="Address types cannot be mixed in ipset entries - " + str(ipset_entries)
396+
)
397+
return addr_type
398+
399+
346400
# Above: adapted from firewall-cmd source code
347401

348402

@@ -613,13 +667,14 @@ def set_service(
613667
else:
614668
self.module.fail_json(msg="INVALID SERVICE - " + item)
615669

616-
def _create_ipset(self, ipset, ipset_type):
670+
def _create_ipset(self, ipset, ipset_type, ipset_options):
617671
if not ipset_type:
618672
self.module.fail_json(msg="ipset_type needed when creating a new ipset")
619673

620674
fw_ipset = None
621675
fw_ipset_settings = FirewallClientIPSetSettings()
622676
fw_ipset_settings.setType(ipset_type)
677+
fw_ipset_settings.setOptions(ipset_options)
623678
if not self.module.check_mode:
624679
self.fw.config().addIPSet(ipset, fw_ipset_settings)
625680
fw_ipset = self.fw.config().getIPSetByName(ipset)
@@ -628,6 +683,14 @@ def _create_ipset(self, ipset, ipset_type):
628683
return fw_ipset, fw_ipset_settings
629684

630685
def set_ipset(self, ipset, description, short, ipset_type, ipset_entries):
686+
addr_type = validate_ipset_entries(ipset_entries, self.module)
687+
if addr_type is None and ipset_entries:
688+
self.module.fail_json(msg="Invalid IP address - " + str(ipset_entries))
689+
ipset_options = {}
690+
if addr_type == "ipv6":
691+
ipset_options = {
692+
"family": "inet6",
693+
}
631694
ipset_exists = ipset in self.fw.config().getIPSetNames()
632695
fw_ipset = None
633696
fw_ipset_settings = None
@@ -641,7 +704,9 @@ def set_ipset(self, ipset, description, short, ipset_type, ipset_entries):
641704
% (ipset, fw_ipset_settings.getType())
642705
)
643706
elif self.state == "present":
644-
fw_ipset, fw_ipset_settings = self._create_ipset(ipset, ipset_type)
707+
fw_ipset, fw_ipset_settings = self._create_ipset(
708+
ipset, ipset_type, ipset_options
709+
)
645710
self.changed = True
646711
ipset_exists = True
647712
if self.state == "present":
@@ -1256,6 +1321,12 @@ def set_service(
12561321
self.change("--zone", self.zone, op + item)
12571322

12581323
def set_ipset(self, ipset, description, short, ipset_type, ipset_entries):
1324+
addr_type = validate_ipset_entries(ipset_entries, self.module)
1325+
if addr_type is None and ipset_entries:
1326+
self.module.fail_json(msg="Invalid IP address - " + str(ipset_entries))
1327+
ipset_options = []
1328+
if addr_type == "ipv6":
1329+
ipset_options = ["--option", "family=inet6"]
12591330
present = self.check_state(["present", "absent"], "ipset")
12601331
known_ipsets = self.cmd("--get-ipsets").split()
12611332
ipset_exists = ipset in known_ipsets
@@ -1280,7 +1351,9 @@ def set_ipset(self, ipset, description, short, ipset_type, ipset_entries):
12801351
self.module.fail_json(
12811352
msg="ipset_type needed when creating a new ipset"
12821353
)
1283-
self.change("--new-ipset", ipset, "--type=%s" % ipset_type)
1354+
self.change(
1355+
"--new-ipset", ipset, "--type=%s" % ipset_type, *ipset_options
1356+
)
12841357

12851358
existing_description = self.cmd("--ipset", ipset, "--get-description")
12861359
if description is not None and description != existing_description:
@@ -1635,6 +1708,10 @@ def get_forward_port(module):
16351708
def parse_forward_port(module, item):
16361709
type_string = "forward_port"
16371710

1711+
_port = None
1712+
_protocol = None
1713+
_to_port = None
1714+
_to_addr = None
16381715
if isinstance(item, dict):
16391716
if "port" not in item:
16401717
module.fail_json(

pytest_extra_requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# SPDX-License-Identifier: MIT
22

33
# Write extra requirements for running pytest here:
4+
firewall
45
# If you need ansible then uncomment the following line:
56
-ransible_pytest_extra_requirements.txt
67
# If you need mock then uncomment the following line:

0 commit comments

Comments
 (0)