Skip to content

Commit 1fb2e30

Browse files
committed
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>
1 parent 0513cd1 commit 1fb2e30

13 files changed

Lines changed: 530 additions & 61 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: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -381,8 +381,12 @@ For more information about custom services, see <https://firewalld.org/documenta
381381
Name of the ipset being created, modified, or removed.
382382
Use `source` to add and remove ipsets from a zone
383383

384-
When creating an ipset, you must also specify `ipset_type`,
385-
and optionally `short`, `description`, `ipset_entries`
384+
When creating an ipset, you must also specify `ipset_type`, and optionally
385+
`short`, `description`, `ipset_entries` and `ipset_options`.
386+
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.
386390

387391
Defining an ipset with all optional fields:
388392

@@ -398,6 +402,24 @@ firewall:
398402
- 3.3.3.3
399403
- 8.8.8.8
400404
- 127.0.0.1
405+
ipset_options:
406+
timeout: 120
407+
state: present
408+
permanent: true
409+
```
410+
411+
Defining an ipset with IPv6 addresses:
412+
413+
```yaml
414+
firewall:
415+
- ipset: customipset
416+
ipset_type: "hash:ip"
417+
short: Custom IPSet
418+
description: set of ip addresses specified in entries
419+
ipset_entries:
420+
- 2001:db8::/32
421+
ipset_options:
422+
timeout: 120
401423
state: present
402424
permanent: true
403425
```
@@ -478,6 +500,10 @@ Used with `ipset`
478500
Entries must be compatible with the ipset type of the `ipset`
479501
being created or modified.
480502

503+
**NOTE**: You cannot mix IPv4, IPv6, and MAC addresses in the same
504+
`ipset_entries` list. All addresses must be the same IP type. This is a
505+
limitation of the underlying firewalld implementation.
506+
481507
```yaml
482508
ipset: customipset
483509
ipset_entries:
@@ -486,6 +512,16 @@ ipset_entries:
486512

487513
See `ipset` for more usage information
488514

515+
### ipset_options
516+
517+
A `dict` of key/value pairs of ipset options for the given ipset.
518+
See [firewalld ipset options](https://firewalld.org/documentation/ipset/options.html)
519+
for more information.
520+
521+
You usually do not have to specify the family. The role will default to
522+
`family: inet` if `ipset_entries` contains IPv4 addresses, and will default to
523+
`family: inet6` if `ipset_entries` contains IPv6 addresses
524+
489525
### source_port
490526

491527
Port or port range or a list of them to add or remove source port access to. It

library/firewall_lib.py

Lines changed: 154 additions & 11 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

@@ -184,11 +184,20 @@
184184
ipset_entries:
185185
description:
186186
List of addresses to add/remove from ipset.
187+
Must be compatible with the ipset type of the `ipset`
188+
being created or modified.
187189
Will only do something when set with ipset.
188190
required: false
189191
type: list
190192
elements: str
191193
default: []
194+
ipset_options:
195+
description:
196+
Dict of key/value pairs of ipset options for the given ipset.
197+
Will only do something when set with ipset.
198+
required: false
199+
type: dict
200+
default: {}
192201
permanent:
193202
description:
194203
The permanent bool flag.
@@ -280,9 +289,28 @@
280289
"""
281290

282291
from ansible.module_utils.basic import AnsibleModule
292+
from ansible.module_utils.six import string_types
283293
import re
284294
import os
285295

296+
try:
297+
import ipaddress
298+
299+
HAS_IPADDRESS = True
300+
except ImportError:
301+
HAS_IPADDRESS = False
302+
303+
try:
304+
from firewall.functions import check_mac
305+
306+
HAS_CHECK_MAC = True
307+
except ImportError:
308+
HAS_CHECK_MAC = False
309+
310+
def check_mac(mac):
311+
return False
312+
313+
286314
try:
287315
import firewall.config
288316

@@ -315,6 +343,20 @@
315343
NM_IMPORTED = False
316344

317345

346+
# The argument to ip_interface must be a unicode string
347+
# Must be "cast" in python2, python3 does not need this
348+
def ip_interface(entry, module):
349+
try:
350+
entry_str = unicode(entry)
351+
except NameError: # unicode is not defined in python3
352+
entry_str = entry
353+
try:
354+
return ipaddress.ip_interface(entry_str)
355+
except ValueError:
356+
module.fail_json(msg="Invalid IP address - " + entry)
357+
return None
358+
359+
318360
def try_get_connection_of_interface(interface):
319361
try:
320362
return nm_get_connection_of_interface(interface)
@@ -343,6 +385,46 @@ def try_set_zone_of_interface(module, _zone, interface):
343385
return (False, False)
344386

345387

388+
# Check that all of the ipset entries are ipv4, ipv6, or mac addresses
389+
# if not, fail the module
390+
# if they are, return "ipv4", "ipv6", or "mac"
391+
def get_ipset_entries_type(ipset_entries, module):
392+
addr_type = None
393+
is_mixed_addr_types = False
394+
for entry in ipset_entries:
395+
if check_mac(entry):
396+
if addr_type is None:
397+
addr_type = "mac"
398+
elif addr_type != "mac":
399+
is_mixed_addr_types = True
400+
else:
401+
if HAS_IPADDRESS:
402+
addr = ip_interface(entry, module)
403+
# ip_interface will fail the module if the address is invalid
404+
if addr is None:
405+
continue
406+
else:
407+
module.fail_json(msg="No IP address library found")
408+
continue
409+
if addr.version == 4:
410+
if addr_type is None:
411+
addr_type = "ipv4"
412+
elif addr_type != "ipv4":
413+
is_mixed_addr_types = True
414+
elif addr.version == 6:
415+
if addr_type is None:
416+
addr_type = "ipv6"
417+
elif addr_type != "ipv6":
418+
is_mixed_addr_types = True
419+
else:
420+
module.fail_json(msg="Invalid IP address - " + entry)
421+
if is_mixed_addr_types:
422+
module.fail_json(
423+
msg="Address types cannot be mixed in ipset entries - " + str(ipset_entries)
424+
)
425+
return addr_type
426+
427+
346428
# Above: adapted from firewall-cmd source code
347429

348430

@@ -613,21 +695,43 @@ def set_service(
613695
else:
614696
self.module.fail_json(msg="INVALID SERVICE - " + item)
615697

616-
def _create_ipset(self, ipset, ipset_type):
698+
def _create_ipset(self, ipset, ipset_type, ipset_options):
617699
if not ipset_type:
618700
self.module.fail_json(msg="ipset_type needed when creating a new ipset")
619701

620702
fw_ipset = None
621703
fw_ipset_settings = FirewallClientIPSetSettings()
622704
fw_ipset_settings.setType(ipset_type)
705+
fw_ipset_settings.setOptions(ipset_options)
623706
if not self.module.check_mode:
624707
self.fw.config().addIPSet(ipset, fw_ipset_settings)
625708
fw_ipset = self.fw.config().getIPSetByName(ipset)
626709
fw_ipset_settings = fw_ipset.getSettings()
627710

628711
return fw_ipset, fw_ipset_settings
629712

630-
def set_ipset(self, ipset, description, short, ipset_type, ipset_entries):
713+
def set_ipset(
714+
self, ipset, description, short, ipset_type, ipset_entries, ipset_options
715+
):
716+
addr_type = get_ipset_entries_type(ipset_entries, self.module)
717+
if addr_type is None and ipset_entries:
718+
self.module.fail_json(
719+
msg="ipset %s: Invalid IP address - %s " % (ipset, str(ipset_entries))
720+
)
721+
if not ipset_options:
722+
ipset_options = {}
723+
if addr_type == "ipv6" and "family" not in ipset_options:
724+
ipset_options["family"] = "inet6"
725+
if addr_type == "ipv4" and ipset_options.get("family") == "inet6":
726+
self.module.fail_json(
727+
msg="ipset %s: family=inet6 is not supported for IPv4 ipset_entries %s"
728+
% (ipset, ", ".join(ipset_entries))
729+
)
730+
if addr_type == "ipv6" and ipset_options.get("family") == "inet":
731+
self.module.fail_json(
732+
msg="ipset %s: family=inet is not supported for IPv6 ipset_entries %s"
733+
% (ipset, ", ".join(ipset_entries))
734+
)
631735
ipset_exists = ipset in self.fw.config().getIPSetNames()
632736
fw_ipset = None
633737
fw_ipset_settings = None
@@ -641,7 +745,9 @@ def set_ipset(self, ipset, description, short, ipset_type, ipset_entries):
641745
% (ipset, fw_ipset_settings.getType())
642746
)
643747
elif self.state == "present":
644-
fw_ipset, fw_ipset_settings = self._create_ipset(ipset, ipset_type)
748+
fw_ipset, fw_ipset_settings = self._create_ipset(
749+
ipset, ipset_type, ipset_options
750+
)
645751
self.changed = True
646752
ipset_exists = True
647753
if self.state == "present":
@@ -1255,7 +1361,32 @@ def set_service(
12551361
op = "--add-service=" if enable else "--remove-service-from-zone="
12561362
self.change("--zone", self.zone, op + item)
12571363

1258-
def set_ipset(self, ipset, description, short, ipset_type, ipset_entries):
1364+
def set_ipset(
1365+
self, ipset, description, short, ipset_type, ipset_entries, ipset_options
1366+
):
1367+
addr_type = get_ipset_entries_type(ipset_entries, self.module)
1368+
if addr_type is None and ipset_entries:
1369+
self.module.fail_json(
1370+
msg="ipset %s: Invalid IP address - %s" % (ipset, str(ipset_entries))
1371+
)
1372+
ipset_options_list = []
1373+
if ipset_options:
1374+
for kk, vv in ipset_options.items():
1375+
ipset_options_list.append("--option")
1376+
ipset_options_list.append(kk + "=" + str(vv))
1377+
if addr_type == "ipv6" and "family=inet6" not in ipset_options_list:
1378+
ipset_options_list.append("--option")
1379+
ipset_options_list.append("family=inet6")
1380+
if addr_type == "ipv4" and "family=inet6" in ipset_options_list:
1381+
self.module.fail_json(
1382+
msg="ipset %s: family=inet6 is not supported for IPv4 ipset_entries %s"
1383+
% (ipset, ", ".join(ipset_entries))
1384+
)
1385+
if addr_type == "ipv6" and "family=inet" in ipset_options_list:
1386+
self.module.fail_json(
1387+
msg="ipset %s: family=inet is not supported for IPv6 ipset_entries %s"
1388+
% (ipset, ", ".join(ipset_entries))
1389+
)
12591390
present = self.check_state(["present", "absent"], "ipset")
12601391
known_ipsets = self.cmd("--get-ipsets").split()
12611392
ipset_exists = ipset in known_ipsets
@@ -1278,9 +1409,12 @@ def set_ipset(self, ipset, description, short, ipset_type, ipset_entries):
12781409
if not ipset_exists:
12791410
if not ipset_type:
12801411
self.module.fail_json(
1281-
msg="ipset_type needed when creating a new ipset"
1412+
msg="ipset %s: ipset_type needed when creating a new ipset"
1413+
% ipset
12821414
)
1283-
self.change("--new-ipset", ipset, "--type=%s" % ipset_type)
1415+
self.change(
1416+
"--new-ipset", ipset, "--type=%s" % ipset_type, *ipset_options_list
1417+
)
12841418

12851419
existing_description = self.cmd("--ipset", ipset, "--get-description")
12861420
if description is not None and description != existing_description:
@@ -1635,6 +1769,10 @@ def get_forward_port(module):
16351769
def parse_forward_port(module, item):
16361770
type_string = "forward_port"
16371771

1772+
_port = None
1773+
_protocol = None
1774+
_to_port = None
1775+
_to_addr = None
16381776
if isinstance(item, dict):
16391777
if "port" not in item:
16401778
module.fail_json(
@@ -1653,7 +1791,7 @@ def parse_forward_port(module, item):
16531791
else:
16541792
_to_port = None
16551793
_to_addr = item.get("toaddr")
1656-
elif isinstance(item, str):
1794+
elif isinstance(item, string_types):
16571795
args = item.split(";")
16581796
if len(args) == 3:
16591797
__port, _to_port, _to_addr = args
@@ -1737,6 +1875,7 @@ def main():
17371875
ipset=dict(required=False, type="str", default=None),
17381876
ipset_type=dict(required=False, type="str", default=None),
17391877
ipset_entries=dict(required=False, type="list", elements="str", default=[]),
1878+
ipset_options=dict(required=False, type="dict", default={}),
17401879
permanent=dict(required=False, type="bool", default=None),
17411880
runtime=dict(
17421881
required=False,
@@ -1840,6 +1979,7 @@ def main():
18401979
ipset = module.params["ipset"]
18411980
ipset_type = module.params["ipset_type"]
18421981
ipset_entries = module.params["ipset_entries"]
1982+
ipset_options = module.params["ipset_options"]
18431983
permanent = module.params["permanent"]
18441984
runtime = module.params["runtime"]
18451985
state = module.params["state"]
@@ -1942,11 +2082,12 @@ def main():
19422082
destination_ipv6,
19432083
ipset_entries,
19442084
ipset_type,
2085+
ipset_options,
19452086
)
19462087
):
19472088
module.fail_json(
19482089
msg="short, description, port, source_port, helper_module, "
1949-
"protocol, destination, ipset_type or ipset_entries cannot be set "
2090+
"protocol, destination, ipset_type, ipset_entries, or ipset_options cannot be set "
19502091
"while zone is specified "
19512092
"and state is set to present or absent"
19522093
)
@@ -1975,7 +2116,7 @@ def main():
19752116
msg="permanent must be enabled for service configuration. "
19762117
"Additionally, service runtime configuration is not possible"
19772118
)
1978-
elif ipset_entries or ipset_type:
2119+
elif ipset_entries or ipset_type or ipset_options:
19792120
module.fail_json(
19802121
msg="ipset parameters cannot be set when configuring services"
19812122
)
@@ -2090,7 +2231,9 @@ def main():
20902231
includes,
20912232
)
20922233
if ipset_operation:
2093-
backend.set_ipset(ipset, description, short, ipset_type, ipset_entries)
2234+
backend.set_ipset(
2235+
ipset, description, short, ipset_type, ipset_entries, ipset_options
2236+
)
20942237
if port and not service_operation:
20952238
backend.set_port(port)
20962239
if source_port and not service_operation:

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)