summaryrefslogtreecommitdiffstats
path: root/ansible_collections/community/general/plugins/modules/ufw.py
blob: 5d187793bdd1fcda3f1e600aa5d245855bbefbfd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
#!/usr/bin/python
# -*- coding: utf-8 -*-

# Copyright (c) 2014, Ahti Kitsik <ak@ahtik.com>
# Copyright (c) 2014, Jarno Keskikangas <jarno.keskikangas@gmail.com>
# Copyright (c) 2013, Aleksey Ovcharenko <aleksey.ovcharenko@gmail.com>
# Copyright (c) 2013, James Martin <jmartin@basho.com>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

from __future__ import absolute_import, division, print_function
__metaclass__ = type

DOCUMENTATION = r'''
---
module: ufw
short_description: Manage firewall with UFW
description:
    - Manage firewall with UFW.
author:
    - Aleksey Ovcharenko (@ovcharenko)
    - Jarno Keskikangas (@pyykkis)
    - Ahti Kitsik (@ahtik)
notes:
    - See C(man ufw) for more examples.
requirements:
    - C(ufw) package
extends_documentation_fragment:
    - community.general.attributes
attributes:
  check_mode:
    support: full
  diff_mode:
    support: none
options:
  state:
    description:
      - V(enabled) reloads firewall and enables firewall on boot.
      - V(disabled) unloads firewall and disables firewall on boot.
      - V(reloaded) reloads firewall.
      - V(reset) disables and resets firewall to installation defaults.
    type: str
    choices: [ disabled, enabled, reloaded, reset ]
  default:
    description:
      - Change the default policy for incoming or outgoing traffic.
    type: str
    choices: [ allow, deny, reject ]
    aliases: [ policy ]
  direction:
    description:
      - Select direction for a rule or default policy command.  Mutually
        exclusive with O(interface_in) and O(interface_out).
    type: str
    choices: [ in, incoming, out, outgoing, routed ]
  logging:
    description:
      - Toggles logging. Logged packets use the LOG_KERN syslog facility.
    type: str
    choices: [ 'on', 'off', low, medium, high, full ]
  insert:
    description:
      - Insert the corresponding rule as rule number NUM.
      - Note that ufw numbers rules starting with 1.
      - If O(delete=true) and a value is provided for O(insert),
        then O(insert) is ignored.
    type: int
  insert_relative_to:
    description:
      - Allows to interpret the index in O(insert) relative to a position.
      - V(zero) interprets the rule number as an absolute index (i.e. 1 is
        the first rule).
      - V(first-ipv4) interprets the rule number relative to the index of the
        first IPv4 rule, or relative to the position where the first IPv4 rule
        would be if there is currently none.
      - V(last-ipv4) interprets the rule number relative to the index of the
        last IPv4 rule, or relative to the position where the last IPv4 rule
        would be if there is currently none.
      - V(first-ipv6) interprets the rule number relative to the index of the
        first IPv6 rule, or relative to the position where the first IPv6 rule
        would be if there is currently none.
      - V(last-ipv6) interprets the rule number relative to the index of the
        last IPv6 rule, or relative to the position where the last IPv6 rule
        would be if there is currently none.
    type: str
    choices: [ first-ipv4, first-ipv6, last-ipv4, last-ipv6, zero ]
    default: zero
  rule:
    description:
      - Add firewall rule
    type: str
    choices: [ allow, deny, limit, reject ]
  log:
    description:
      - Log new connections matched to this rule
    type: bool
    default: false
  from_ip:
    description:
      - Source IP address.
    type: str
    default: any
    aliases: [ from, src ]
  from_port:
    description:
      - Source port.
    type: str
  to_ip:
    description:
      - Destination IP address.
    type: str
    default: any
    aliases: [ dest, to]
  to_port:
    description:
      - Destination port.
    type: str
    aliases: [ port ]
  proto:
    description:
      - TCP/IP protocol.
    type: str
    choices: [ any, tcp, udp, ipv6, esp, ah, gre, igmp ]
    aliases: [ protocol ]
  name:
    description:
      - Use profile located in C(/etc/ufw/applications.d).
    type: str
    aliases: [ app ]
  delete:
    description:
      - Delete rule.
      - If O(delete=true) and a value is provided for O(insert),
        then O(insert) is ignored.
    type: bool
    default: false
  interface:
    description:
      - Specify interface for the rule.  The direction (in or out) used
        for the interface depends on the value of O(direction).  See
        O(interface_in) and O(interface_out) for routed rules that needs
        to supply both an input and output interface.  Mutually
        exclusive with O(interface_in) and O(interface_out).
    type: str
    aliases: [ if ]
  interface_in:
    description:
      - Specify input interface for the rule.  This is mutually
        exclusive with O(direction) and O(interface).  However, it is
        compatible with O(interface_out) for routed rules.
    type: str
    aliases: [ if_in ]
    version_added: '0.2.0'
  interface_out:
    description:
      - Specify output interface for the rule.  This is mutually
        exclusive with O(direction) and O(interface).  However, it is
        compatible with O(interface_in) for routed rules.
    type: str
    aliases: [ if_out ]
    version_added: '0.2.0'
  route:
    description:
      - Apply the rule to routed/forwarded packets.
    type: bool
    default: false
  comment:
    description:
      - Add a comment to the rule. Requires UFW version >=0.35.
    type: str
'''

EXAMPLES = r'''
- name: Allow everything and enable UFW
  community.general.ufw:
    state: enabled
    policy: allow

- name: Set logging
  community.general.ufw:
    logging: 'on'

# Sometimes it is desirable to let the sender know when traffic is
# being denied, rather than simply ignoring it. In these cases, use
# reject instead of deny. In addition, log rejected connections:
- community.general.ufw:
    rule: reject
    port: auth
    log: true

# ufw supports connection rate limiting, which is useful for protecting
# against brute-force login attacks. ufw will deny connections if an IP
# address has attempted to initiate 6 or more connections in the last
# 30 seconds. See  http://www.debian-administration.org/articles/187
# for details. Typical usage is:
- community.general.ufw:
    rule: limit
    port: ssh
    proto: tcp

# Allow OpenSSH. (Note that as ufw manages its own state, simply removing
# a rule=allow task can leave those ports exposed. Either use delete=true
# or a separate state=reset task)
- community.general.ufw:
    rule: allow
    name: OpenSSH

- name: Delete OpenSSH rule
  community.general.ufw:
    rule: allow
    name: OpenSSH
    delete: true

- name: Deny all access to port 53
  community.general.ufw:
    rule: deny
    port: '53'

- name: Allow port range 60000-61000
  community.general.ufw:
    rule: allow
    port: 60000:61000
    proto: tcp

- name: Allow all access to tcp port 80
  community.general.ufw:
    rule: allow
    port: '80'
    proto: tcp

- name: Allow all access from RFC1918 networks to this host
  community.general.ufw:
    rule: allow
    src: '{{ item }}'
  loop:
    - 10.0.0.0/8
    - 172.16.0.0/12
    - 192.168.0.0/16

- name: Deny access to udp port 514 from host 1.2.3.4 and include a comment
  community.general.ufw:
    rule: deny
    proto: udp
    src: 1.2.3.4
    port: '514'
    comment: Block syslog

- name: Allow incoming access to eth0 from 1.2.3.5 port 5469 to 1.2.3.4 port 5469
  community.general.ufw:
    rule: allow
    interface: eth0
    direction: in
    proto: udp
    src: 1.2.3.5
    from_port: '5469'
    dest: 1.2.3.4
    to_port: '5469'

# Note that IPv6 must be enabled in /etc/default/ufw for IPv6 firewalling to work.
- name: Deny all traffic from the IPv6 2001:db8::/32 to tcp port 25 on this host
  community.general.ufw:
    rule: deny
    proto: tcp
    src: 2001:db8::/32
    port: '25'

- name: Deny all IPv6 traffic to tcp port 20 on this host
  # this should be the first IPv6 rule
  community.general.ufw:
    rule: deny
    proto: tcp
    port: '20'
    to_ip: "::"
    insert: 0
    insert_relative_to: first-ipv6

- name: Deny all IPv4 traffic to tcp port 20 on this host
  # This should be the third to last IPv4 rule
  # (insert: -1 addresses the second to last IPv4 rule;
  #  so the new rule will be inserted before the second
  #  to last IPv4 rule, and will be come the third to last
  #  IPv4 rule.)
  community.general.ufw:
    rule: deny
    proto: tcp
    port: '20'
    to_ip: "::"
    insert: -1
    insert_relative_to: last-ipv4

# Can be used to further restrict a global FORWARD policy set to allow
- name: Deny forwarded/routed traffic from subnet 1.2.3.0/24 to subnet 4.5.6.0/24
  community.general.ufw:
    rule: deny
    route: true
    src: 192.0.2.0/24
    dest: 198.51.100.0/24
'''

import re

from operator import itemgetter

from ansible.module_utils.basic import AnsibleModule


def compile_ipv4_regexp():
    r = r"((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}"
    r += r"(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])"
    return re.compile(r)


def compile_ipv6_regexp():
    """
    validation pattern provided by :
    https://stackoverflow.com/questions/53497/regular-expression-that-matches-
    valid-ipv6-addresses#answer-17871737
    """
    r = r"(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:"
    r += r"|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}"
    r += r"(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4})"
    r += r"{1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]"
    r += r"{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]"
    r += r"{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4})"
    r += r"{0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]"
    r += r"|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}"
    r += r"[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}"
    r += r"[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))"
    return re.compile(r)


def main():
    command_keys = ['state', 'default', 'rule', 'logging']

    module = AnsibleModule(
        argument_spec=dict(
            state=dict(type='str', choices=['enabled', 'disabled', 'reloaded', 'reset']),
            default=dict(type='str', aliases=['policy'], choices=['allow', 'deny', 'reject']),
            logging=dict(type='str', choices=['full', 'high', 'low', 'medium', 'off', 'on']),
            direction=dict(type='str', choices=['in', 'incoming', 'out', 'outgoing', 'routed']),
            delete=dict(type='bool', default=False),
            route=dict(type='bool', default=False),
            insert=dict(type='int'),
            insert_relative_to=dict(choices=['zero', 'first-ipv4', 'last-ipv4', 'first-ipv6', 'last-ipv6'], default='zero'),
            rule=dict(type='str', choices=['allow', 'deny', 'limit', 'reject']),
            interface=dict(type='str', aliases=['if']),
            interface_in=dict(type='str', aliases=['if_in']),
            interface_out=dict(type='str', aliases=['if_out']),
            log=dict(type='bool', default=False),
            from_ip=dict(type='str', default='any', aliases=['from', 'src']),
            from_port=dict(type='str'),
            to_ip=dict(type='str', default='any', aliases=['dest', 'to']),
            to_port=dict(type='str', aliases=['port']),
            proto=dict(type='str', aliases=['protocol'], choices=['ah', 'any', 'esp', 'ipv6', 'tcp', 'udp', 'gre', 'igmp']),
            name=dict(type='str', aliases=['app']),
            comment=dict(type='str'),
        ),
        supports_check_mode=True,
        mutually_exclusive=[
            ['name', 'proto', 'logging'],
            # Mutual exclusivity with `interface` implied by `required_by`.
            ['direction', 'interface_in'],
            ['direction', 'interface_out'],
        ],
        required_one_of=([command_keys]),
        required_by=dict(
            interface=('direction', ),
        ),
    )

    cmds = []

    ipv4_regexp = compile_ipv4_regexp()
    ipv6_regexp = compile_ipv6_regexp()

    def filter_line_that_not_start_with(pattern, content):
        return ''.join([line for line in content.splitlines(True) if line.startswith(pattern)])

    def filter_line_that_contains(pattern, content):
        return [line for line in content.splitlines(True) if pattern in line]

    def filter_line_that_not_contains(pattern, content):
        return ''.join([line for line in content.splitlines(True) if not line.contains(pattern)])

    def filter_line_that_match_func(match_func, content):
        return ''.join([line for line in content.splitlines(True) if match_func(line) is not None])

    def filter_line_that_contains_ipv4(content):
        return filter_line_that_match_func(ipv4_regexp.search, content)

    def filter_line_that_contains_ipv6(content):
        return filter_line_that_match_func(ipv6_regexp.search, content)

    def is_starting_by_ipv4(ip):
        return ipv4_regexp.match(ip) is not None

    def is_starting_by_ipv6(ip):
        return ipv6_regexp.match(ip) is not None

    def execute(cmd, ignore_error=False):
        cmd = ' '.join(map(itemgetter(-1), filter(itemgetter(0), cmd)))

        cmds.append(cmd)
        (rc, out, err) = module.run_command(cmd, environ_update={"LANG": "C"})

        if rc != 0 and not ignore_error:
            module.fail_json(msg=err or out, commands=cmds)

        return out

    def get_current_rules():
        user_rules_files = ["/lib/ufw/user.rules",
                            "/lib/ufw/user6.rules",
                            "/etc/ufw/user.rules",
                            "/etc/ufw/user6.rules",
                            "/var/lib/ufw/user.rules",
                            "/var/lib/ufw/user6.rules"]

        cmd = [[grep_bin], ["-h"], ["'^### tuple'"]]

        cmd.extend([[f] for f in user_rules_files])
        return execute(cmd, ignore_error=True)

    def ufw_version():
        """
        Returns the major and minor version of ufw installed on the system.
        """
        out = execute([[ufw_bin], ["--version"]])

        lines = [x for x in out.split('\n') if x.strip() != '']
        if len(lines) == 0:
            module.fail_json(msg="Failed to get ufw version.", rc=0, out=out)

        matches = re.search(r'^ufw.+(\d+)\.(\d+)(?:\.(\d+))?.*$', lines[0])
        if matches is None:
            module.fail_json(msg="Failed to get ufw version.", rc=0, out=out)

        # Convert version to numbers
        major = int(matches.group(1))
        minor = int(matches.group(2))
        rev = 0
        if matches.group(3) is not None:
            rev = int(matches.group(3))

        return major, minor, rev

    params = module.params

    commands = dict((key, params[key]) for key in command_keys if params[key])

    # Ensure ufw is available
    ufw_bin = module.get_bin_path('ufw', True)
    grep_bin = module.get_bin_path('grep', True)

    # Save the pre state and rules in order to recognize changes
    pre_state = execute([[ufw_bin], ['status verbose']])
    pre_rules = get_current_rules()

    changed = False

    # Execute filter
    for (command, value) in commands.items():

        cmd = [[ufw_bin], [module.check_mode, '--dry-run']]

        if command == 'state':
            states = {'enabled': 'enable', 'disabled': 'disable',
                      'reloaded': 'reload', 'reset': 'reset'}

            if value in ['reloaded', 'reset']:
                changed = True

            if module.check_mode:
                # "active" would also match "inactive", hence the space
                ufw_enabled = pre_state.find(" active") != -1
                if (value == 'disabled' and ufw_enabled) or (value == 'enabled' and not ufw_enabled):
                    changed = True
            else:
                execute(cmd + [['-f'], [states[value]]])

        elif command == 'logging':
            extract = re.search(r'Logging: (on|off)(?: \(([a-z]+)\))?', pre_state)
            if extract:
                current_level = extract.group(2)
                current_on_off_value = extract.group(1)
                if value != "off":
                    if current_on_off_value == "off":
                        changed = True
                    elif value != "on" and value != current_level:
                        changed = True
                elif current_on_off_value != "off":
                    changed = True
            else:
                changed = True

            if not module.check_mode:
                execute(cmd + [[command], [value]])

        elif command == 'default':
            if params['direction'] not in ['outgoing', 'incoming', 'routed', None]:
                module.fail_json(msg='For default, direction must be one of "outgoing", "incoming" and "routed", or direction must not be specified.')
            if module.check_mode:
                regexp = r'Default: (deny|allow|reject) \(incoming\), (deny|allow|reject) \(outgoing\), (deny|allow|reject|disabled) \(routed\)'
                extract = re.search(regexp, pre_state)
                if extract is not None:
                    current_default_values = {}
                    current_default_values["incoming"] = extract.group(1)
                    current_default_values["outgoing"] = extract.group(2)
                    current_default_values["routed"] = extract.group(3)
                    v = current_default_values[params['direction'] or 'incoming']
                    if v not in (value, 'disabled'):
                        changed = True
                else:
                    changed = True
            else:
                execute(cmd + [[command], [value], [params['direction']]])

        elif command == 'rule':
            if params['direction'] not in ['in', 'out', None]:
                module.fail_json(msg='For rules, direction must be one of "in" and "out", or direction must not be specified.')
            if not params['route'] and params['interface_in'] and params['interface_out']:
                module.fail_json(msg='Only route rules can combine '
                                 'interface_in and interface_out')
            # Rules are constructed according to the long format
            #
            # ufw [--dry-run] [route] [delete | insert NUM] allow|deny|reject|limit [in|out on INTERFACE] [log|log-all] \
            #     [from ADDRESS [port PORT]] [to ADDRESS [port PORT]] \
            #     [proto protocol] [app application] [comment COMMENT]
            cmd.append([module.boolean(params['route']), 'route'])
            cmd.append([module.boolean(params['delete']), 'delete'])
            if params['insert'] is not None and not params['delete']:
                relative_to_cmd = params['insert_relative_to']
                if relative_to_cmd == 'zero':
                    insert_to = params['insert']
                else:
                    (dummy, numbered_state, dummy) = module.run_command([ufw_bin, 'status', 'numbered'])
                    numbered_line_re = re.compile(R'^\[ *([0-9]+)\] ')
                    lines = [(numbered_line_re.match(line), '(v6)' in line) for line in numbered_state.splitlines()]
                    lines = [(int(matcher.group(1)), ipv6) for (matcher, ipv6) in lines if matcher]
                    last_number = max([no for (no, ipv6) in lines]) if lines else 0
                    has_ipv4 = any(not ipv6 for (no, ipv6) in lines)
                    has_ipv6 = any(ipv6 for (no, ipv6) in lines)
                    if relative_to_cmd == 'first-ipv4':
                        relative_to = 1
                    elif relative_to_cmd == 'last-ipv4':
                        relative_to = max([no for (no, ipv6) in lines if not ipv6]) if has_ipv4 else 1
                    elif relative_to_cmd == 'first-ipv6':
                        relative_to = max([no for (no, ipv6) in lines if not ipv6]) + 1 if has_ipv4 else 1
                    elif relative_to_cmd == 'last-ipv6':
                        relative_to = last_number if has_ipv6 else last_number + 1
                    insert_to = params['insert'] + relative_to
                    if insert_to > last_number:
                        # ufw does not like it when the insert number is larger than the
                        # maximal rule number for IPv4/IPv6.
                        insert_to = None
                cmd.append([insert_to is not None, "insert %s" % insert_to])
            cmd.append([value])
            cmd.append([params['direction'], "%s" % params['direction']])
            cmd.append([params['interface'], "on %s" % params['interface']])
            cmd.append([params['interface_in'], "in on %s" % params['interface_in']])
            cmd.append([params['interface_out'], "out on %s" % params['interface_out']])
            cmd.append([module.boolean(params['log']), 'log'])

            for (key, template) in [('from_ip', "from %s"), ('from_port', "port %s"),
                                    ('to_ip', "to %s"), ('to_port', "port %s"),
                                    ('proto', "proto %s"), ('name', "app '%s'")]:
                value = params[key]
                cmd.append([value, template % (value)])

            ufw_major, ufw_minor, dummy = ufw_version()
            # comment is supported only in ufw version after 0.35
            if (ufw_major == 0 and ufw_minor >= 35) or ufw_major > 0:
                cmd.append([params['comment'], "comment '%s'" % params['comment']])

            rules_dry = execute(cmd)

            if module.check_mode:

                nb_skipping_line = len(filter_line_that_contains("Skipping", rules_dry))

                if not (nb_skipping_line > 0 and nb_skipping_line == len(rules_dry.splitlines(True))):

                    rules_dry = filter_line_that_not_start_with("### tuple", rules_dry)
                    # ufw dry-run doesn't send all rules so have to compare ipv4 or ipv6 rules
                    if is_starting_by_ipv4(params['from_ip']) or is_starting_by_ipv4(params['to_ip']):
                        if filter_line_that_contains_ipv4(pre_rules) != filter_line_that_contains_ipv4(rules_dry):
                            changed = True
                    elif is_starting_by_ipv6(params['from_ip']) or is_starting_by_ipv6(params['to_ip']):
                        if filter_line_that_contains_ipv6(pre_rules) != filter_line_that_contains_ipv6(rules_dry):
                            changed = True
                    elif pre_rules != rules_dry:
                        changed = True

    # Get the new state
    if module.check_mode:
        return module.exit_json(changed=changed, commands=cmds)
    else:
        post_state = execute([[ufw_bin], ['status'], ['verbose']])
        if not changed:
            post_rules = get_current_rules()
            changed = (pre_state != post_state) or (pre_rules != post_rules)
        return module.exit_json(changed=changed, commands=cmds, msg=post_state.rstrip())


if __name__ == '__main__':
    main()