diff --git a/changelogs/fragments/49796-ufw-insert-relative-to.yml b/changelogs/fragments/49796-ufw-insert-relative-to.yml new file mode 100644 index 0000000000..6a07fa1320 --- /dev/null +++ b/changelogs/fragments/49796-ufw-insert-relative-to.yml @@ -0,0 +1,3 @@ +minor_changes: +- "ufw - type of option ``insert`` is now enforced to be ``int``." +- "ufw - new ``insert_relative_to`` option allows to specify rule insertion position relative to first/last IPv4/IPv6 address." diff --git a/lib/ansible/modules/system/ufw.py b/lib/ansible/modules/system/ufw.py index 2fca18ea62..5c94e2b4ea 100644 --- a/lib/ansible/modules/system/ufw.py +++ b/lib/ansible/modules/system/ufw.py @@ -52,7 +52,34 @@ options: choices: [ on, off, low, medium, high, full ] insert: description: - - Insert the corresponding rule as rule number NUM + - Insert the corresponding rule as rule number NUM. + - Note that ufw numbers rules starting with 1. + type: int + insert_relative_to: + description: + - Allows to interpret the index in I(insert) relative to a position. + - C(zero) interprets the rule number as an absolute index (i.e. 1 is + the first rule). + - C(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. + - C(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. + - C(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. + - C(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. + choices: + - zero + - first-ipv4 + - last-ipv4 + - first-ipv6 + - last-ipv6 + default: zero + version_added: "2.8" rule: description: - Add firewall rule @@ -197,6 +224,30 @@ EXAMPLES = ''' 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 + 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.) + 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 ufw: @@ -247,7 +298,8 @@ def main(): 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='str'), + 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']), log=dict(type='bool', default=False), @@ -427,7 +479,32 @@ def main(): # [proto protocol] [app application] [comment COMMENT] cmd.append([module.boolean(params['route']), 'route']) cmd.append([module.boolean(params['delete']), 'delete']) - cmd.append([params['insert'], "insert %s" % params['insert']]) + if params['insert'] is not None: + relative_to_cmd = params['insert_relative_to'] + if relative_to_cmd == 'zero': + insert_to = params['insert'] + else: + (_, numbered_state, _) = 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']]) diff --git a/test/integration/targets/ufw/tasks/run-test.yml b/test/integration/targets/ufw/tasks/run-test.yml index 469336e078..e9c5d2929c 100644 --- a/test/integration/targets/ufw/tasks/run-test.yml +++ b/test/integration/targets/ufw/tasks/run-test.yml @@ -16,3 +16,6 @@ state: disabled - name: "Loading tasks from {{ item }}" include_tasks: "{{ item }}" +- name: Reset to factory defaults + ufw: + state: reset diff --git a/test/integration/targets/ufw/tasks/tests/insert_relative_to.yml b/test/integration/targets/ufw/tasks/tests/insert_relative_to.yml new file mode 100644 index 0000000000..3bb44a0e27 --- /dev/null +++ b/test/integration/targets/ufw/tasks/tests/insert_relative_to.yml @@ -0,0 +1,80 @@ +--- +- name: Enable + ufw: + state: enabled + register: enable + +# ## CREATE RULES ############################ +- name: ipv4 + ufw: + rule: deny + port: 22 + to_ip: 0.0.0.0 +- name: ipv4 + ufw: + rule: deny + port: 23 + to_ip: 0.0.0.0 + +- name: ipv6 + ufw: + rule: deny + port: 122 + to_ip: "::" +- name: ipv6 + ufw: + rule: deny + port: 123 + to_ip: "::" + +- name: first-ipv4 + ufw: + rule: deny + port: 10 + to_ip: 0.0.0.0 + insert: 0 + insert_relative_to: first-ipv4 +- name: last-ipv4 + ufw: + rule: deny + port: 11 + to_ip: 0.0.0.0 + insert: 0 + insert_relative_to: last-ipv4 + +- name: first-ipv6 + ufw: + rule: deny + port: 110 + to_ip: "::" + insert: 0 + insert_relative_to: first-ipv6 +- name: last-ipv6 + ufw: + rule: deny + port: 111 + to_ip: "::" + insert: 0 + insert_relative_to: last-ipv6 + +# ## CHECK RESULT ############################ +- name: Get rules + shell: | + ufw status | grep DENY | cut -f 1-2 -d ' ' | grep -E "^(0\.0\.0\.0|::) [123]+" + # Note that there was also a rule "ff02::fb mDNS" on at least one CI run; + # to ignore these, the extra filtering (grepping for DENY and the regex) makes + # sure to remove all rules not added here. + register: ufw_status +- assert: + that: + - ufw_status.stdout_lines == expected_stdout + vars: + expected_stdout: + - "0.0.0.0 10" + - "0.0.0.0 22" + - "0.0.0.0 11" + - "0.0.0.0 23" + - ":: 110" + - ":: 122" + - ":: 111" + - ":: 123"