Skip to content

Commit d80bcd2

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 <rmeggins@redhat.com>
1 parent 0513cd1 commit d80bcd2

File tree

8 files changed

+368
-52
lines changed

8 files changed

+368
-52
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: 93 additions & 5 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

@@ -280,9 +280,28 @@
280280
"""
281281

282282
from ansible.module_utils.basic import AnsibleModule
283+
from ansible.module_utils.six import string_types
283284
import re
284285
import os
285286

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

@@ -315,6 +334,16 @@
315334
NM_IMPORTED = False
316335

317336

337+
# The argument to ip_interface must be a unicode string
338+
# Must be "cast" in python2, python3 does not need this
339+
def ip_interface(entry):
340+
try:
341+
entry_str = unicode(entry)
342+
except NameError: # unicode is not defined in python3
343+
entry_str = entry
344+
return ipaddress.ip_interface(entry_str)
345+
346+
318347
def try_get_connection_of_interface(interface):
319348
try:
320349
return nm_get_connection_of_interface(interface)
@@ -343,6 +372,42 @@ def try_set_zone_of_interface(module, _zone, interface):
343372
return (False, False)
344373

345374

375+
# Check that all of the ipset entries are ipv4, ipv6, or mac addresses
376+
# if not, fail the module
377+
# if they are, return "ipv4", "ipv6", or "mac"
378+
def get_ipset_entries_type(ipset_entries, module):
379+
addr_type = None
380+
is_mixed_addr_types = False
381+
for entry in ipset_entries:
382+
if check_mac(entry):
383+
if addr_type is None:
384+
addr_type = "mac"
385+
elif addr_type != "mac":
386+
is_mixed_addr_types = True
387+
else:
388+
if HAS_IPADDRESS:
389+
addr = ip_interface(entry)
390+
else:
391+
module.fail_json(msg="No IP address library found")
392+
if addr.version == 4:
393+
if addr_type is None:
394+
addr_type = "ipv4"
395+
elif addr_type != "ipv4":
396+
is_mixed_addr_types = True
397+
elif addr.version == 6:
398+
if addr_type is None:
399+
addr_type = "ipv6"
400+
elif addr_type != "ipv6":
401+
is_mixed_addr_types = True
402+
else:
403+
module.fail_json(msg="Invalid IP address - " + entry)
404+
if is_mixed_addr_types:
405+
module.fail_json(
406+
msg="Address types cannot be mixed in ipset entries - " + str(ipset_entries)
407+
)
408+
return addr_type
409+
410+
346411
# Above: adapted from firewall-cmd source code
347412

348413

@@ -613,13 +678,14 @@ def set_service(
613678
else:
614679
self.module.fail_json(msg="INVALID SERVICE - " + item)
615680

616-
def _create_ipset(self, ipset, ipset_type):
681+
def _create_ipset(self, ipset, ipset_type, ipset_options):
617682
if not ipset_type:
618683
self.module.fail_json(msg="ipset_type needed when creating a new ipset")
619684

620685
fw_ipset = None
621686
fw_ipset_settings = FirewallClientIPSetSettings()
622687
fw_ipset_settings.setType(ipset_type)
688+
fw_ipset_settings.setOptions(ipset_options)
623689
if not self.module.check_mode:
624690
self.fw.config().addIPSet(ipset, fw_ipset_settings)
625691
fw_ipset = self.fw.config().getIPSetByName(ipset)
@@ -628,6 +694,14 @@ def _create_ipset(self, ipset, ipset_type):
628694
return fw_ipset, fw_ipset_settings
629695

630696
def set_ipset(self, ipset, description, short, ipset_type, ipset_entries):
697+
addr_type = get_ipset_entries_type(ipset_entries, self.module)
698+
if addr_type is None and ipset_entries:
699+
self.module.fail_json(msg="Invalid IP address - " + str(ipset_entries))
700+
ipset_options = {}
701+
if addr_type == "ipv6":
702+
ipset_options = {
703+
"family": "inet6",
704+
}
631705
ipset_exists = ipset in self.fw.config().getIPSetNames()
632706
fw_ipset = None
633707
fw_ipset_settings = None
@@ -641,7 +715,9 @@ def set_ipset(self, ipset, description, short, ipset_type, ipset_entries):
641715
% (ipset, fw_ipset_settings.getType())
642716
)
643717
elif self.state == "present":
644-
fw_ipset, fw_ipset_settings = self._create_ipset(ipset, ipset_type)
718+
fw_ipset, fw_ipset_settings = self._create_ipset(
719+
ipset, ipset_type, ipset_options
720+
)
645721
self.changed = True
646722
ipset_exists = True
647723
if self.state == "present":
@@ -1256,6 +1332,12 @@ def set_service(
12561332
self.change("--zone", self.zone, op + item)
12571333

12581334
def set_ipset(self, ipset, description, short, ipset_type, ipset_entries):
1335+
addr_type = get_ipset_entries_type(ipset_entries, self.module)
1336+
if addr_type is None and ipset_entries:
1337+
self.module.fail_json(msg="Invalid IP address - " + str(ipset_entries))
1338+
ipset_options = []
1339+
if addr_type == "ipv6":
1340+
ipset_options = ["--option", "family=inet6"]
12591341
present = self.check_state(["present", "absent"], "ipset")
12601342
known_ipsets = self.cmd("--get-ipsets").split()
12611343
ipset_exists = ipset in known_ipsets
@@ -1280,7 +1362,9 @@ def set_ipset(self, ipset, description, short, ipset_type, ipset_entries):
12801362
self.module.fail_json(
12811363
msg="ipset_type needed when creating a new ipset"
12821364
)
1283-
self.change("--new-ipset", ipset, "--type=%s" % ipset_type)
1365+
self.change(
1366+
"--new-ipset", ipset, "--type=%s" % ipset_type, *ipset_options
1367+
)
12841368

12851369
existing_description = self.cmd("--ipset", ipset, "--get-description")
12861370
if description is not None and description != existing_description:
@@ -1635,6 +1719,10 @@ def get_forward_port(module):
16351719
def parse_forward_port(module, item):
16361720
type_string = "forward_port"
16371721

1722+
_port = None
1723+
_protocol = None
1724+
_to_port = None
1725+
_to_addr = None
16381726
if isinstance(item, dict):
16391727
if "port" not in item:
16401728
module.fail_json(
@@ -1653,7 +1741,7 @@ def parse_forward_port(module, item):
16531741
else:
16541742
_to_port = None
16551743
_to_addr = item.get("toaddr")
1656-
elif isinstance(item, str):
1744+
elif isinstance(item, string_types):
16571745
args = item.split(";")
16581746
if len(args) == 3:
16591747
__port, _to_port, _to_addr = args

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)