Skip to content

Commit 19bb116

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 19bb116

12 files changed

Lines changed: 378 additions & 53 deletions
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
python-ipaddress
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
python-ipaddress

.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: 96 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,19 @@
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, module):
340+
try:
341+
entry_str = unicode(entry)
342+
except NameError: # unicode is not defined in python3
343+
entry_str = entry
344+
try:
345+
return ipaddress.ip_interface(entry_str)
346+
except ValueError:
347+
module.fail_json(msg="Invalid IP address - " + entry)
348+
349+
318350
def try_get_connection_of_interface(interface):
319351
try:
320352
return nm_get_connection_of_interface(interface)
@@ -343,6 +375,42 @@ def try_set_zone_of_interface(module, _zone, interface):
343375
return (False, False)
344376

345377

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

348416

@@ -613,13 +681,14 @@ def set_service(
613681
else:
614682
self.module.fail_json(msg="INVALID SERVICE - " + item)
615683

616-
def _create_ipset(self, ipset, ipset_type):
684+
def _create_ipset(self, ipset, ipset_type, ipset_options):
617685
if not ipset_type:
618686
self.module.fail_json(msg="ipset_type needed when creating a new ipset")
619687

620688
fw_ipset = None
621689
fw_ipset_settings = FirewallClientIPSetSettings()
622690
fw_ipset_settings.setType(ipset_type)
691+
fw_ipset_settings.setOptions(ipset_options)
623692
if not self.module.check_mode:
624693
self.fw.config().addIPSet(ipset, fw_ipset_settings)
625694
fw_ipset = self.fw.config().getIPSetByName(ipset)
@@ -628,6 +697,14 @@ def _create_ipset(self, ipset, ipset_type):
628697
return fw_ipset, fw_ipset_settings
629698

630699
def set_ipset(self, ipset, description, short, ipset_type, ipset_entries):
700+
addr_type = get_ipset_entries_type(ipset_entries, self.module)
701+
if addr_type is None and ipset_entries:
702+
self.module.fail_json(msg="Invalid IP address - " + str(ipset_entries))
703+
ipset_options = {}
704+
if addr_type == "ipv6":
705+
ipset_options = {
706+
"family": "inet6",
707+
}
631708
ipset_exists = ipset in self.fw.config().getIPSetNames()
632709
fw_ipset = None
633710
fw_ipset_settings = None
@@ -641,7 +718,9 @@ def set_ipset(self, ipset, description, short, ipset_type, ipset_entries):
641718
% (ipset, fw_ipset_settings.getType())
642719
)
643720
elif self.state == "present":
644-
fw_ipset, fw_ipset_settings = self._create_ipset(ipset, ipset_type)
721+
fw_ipset, fw_ipset_settings = self._create_ipset(
722+
ipset, ipset_type, ipset_options
723+
)
645724
self.changed = True
646725
ipset_exists = True
647726
if self.state == "present":
@@ -1256,6 +1335,12 @@ def set_service(
12561335
self.change("--zone", self.zone, op + item)
12571336

12581337
def set_ipset(self, ipset, description, short, ipset_type, ipset_entries):
1338+
addr_type = get_ipset_entries_type(ipset_entries, self.module)
1339+
if addr_type is None and ipset_entries:
1340+
self.module.fail_json(msg="Invalid IP address - " + str(ipset_entries))
1341+
ipset_options = []
1342+
if addr_type == "ipv6":
1343+
ipset_options = ["--option", "family=inet6"]
12591344
present = self.check_state(["present", "absent"], "ipset")
12601345
known_ipsets = self.cmd("--get-ipsets").split()
12611346
ipset_exists = ipset in known_ipsets
@@ -1280,7 +1365,9 @@ def set_ipset(self, ipset, description, short, ipset_type, ipset_entries):
12801365
self.module.fail_json(
12811366
msg="ipset_type needed when creating a new ipset"
12821367
)
1283-
self.change("--new-ipset", ipset, "--type=%s" % ipset_type)
1368+
self.change(
1369+
"--new-ipset", ipset, "--type=%s" % ipset_type, *ipset_options
1370+
)
12841371

12851372
existing_description = self.cmd("--ipset", ipset, "--get-description")
12861373
if description is not None and description != existing_description:
@@ -1635,6 +1722,10 @@ def get_forward_port(module):
16351722
def parse_forward_port(module, item):
16361723
type_string = "forward_port"
16371724

1725+
_port = None
1726+
_protocol = None
1727+
_to_port = None
1728+
_to_addr = None
16381729
if isinstance(item, dict):
16391730
if "port" not in item:
16401731
module.fail_json(
@@ -1653,7 +1744,7 @@ def parse_forward_port(module, item):
16531744
else:
16541745
_to_port = None
16551746
_to_addr = item.get("toaddr")
1656-
elif isinstance(item, str):
1747+
elif isinstance(item, string_types):
16571748
args = item.split(";")
16581749
if len(args) == 3:
16591750
__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:

tasks/firewalld.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151

5252
- name: Install firewalld
5353
package:
54-
name: "{{ __firewall_packages_base }}"
54+
name: "{{ __firewall_packages_base + __firewall_packages_extra }}"
5555
state: present
5656
use: "{{ (__firewall_is_ostree | d(false)) |
5757
ternary('ansible.posix.rhel_rpm_ostree', omit) }}"

0 commit comments

Comments
 (0)