Repository: ansible/ansible

Issue:
iptables module tcp_flags bug
<!---
Verify first that your issue/request is not already reported on GitHub.
Also test if the latest release, and devel branch are affected too.
-->

##### ISSUE TYPE
<!--- Pick one below and delete the rest -->
 - Bug Report


##### COMPONENT NAME

`iptables.py`

##### ANSIBLE VERSION
<!--- Paste verbatim output from "ansible --version" between quotes below -->
```
ansible 2.4.2.0
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/usr/share/my_modules']
  ansible python module location = /usr/lib/python2.7/site-packages/ansible
  executable location = /usr/bin/ansible
  python version = 2.7.5 (default, Aug  4 2017, 00:39:18) [GCC 4.8.5 20150623 (Red Hat 4.8.5-16)]
```

##### CONFIGURATION
```
DEFAULT_FORKS(/etc/ansible/ansible.cfg) = 5
DEFAULT_MODULE_PATH(/etc/ansible/ansible.cfg) = [u'/usr/share/my_modules']
DEFAULT_MODULE_UTILS_PATH(/etc/ansible/ansible.cfg) = [u'/usr/share/my_module_utils']
DEFAULT_REMOTE_USER(/etc/ansible/ansible.cfg) = root
DEFAULT_ROLES_PATH(/etc/ansible/ansible.cfg) = [u'/etc/ansible/roles', u'/usr/share/ansible/roles']
HOST_KEY_CHECKING(/etc/ansible/ansible.cfg) = False
```

##### OS / ENVIRONMENT
```
running CentOS Linux release 7.4.1708
manage CentOS Linux 6.X and 7.X
```

##### SUMMARY
<!--- Explain the problem briefly -->
i have iptables rule:
`iptables -A OUTPUT -p tcp --tcp-flags ALL ACK,RST,SYN,FIN -j DROP`

##### STEPS TO REPRODUCE
<!---

-->

<!--- Paste example playbooks or commands between quotes below -->
```
- name: TCP ALL ACK,RST,SYN,FIN
  iptables:
    chain: OUTPUT
    protocol: tcp
    tcp_flags: flags="ALL" flags_set="ACK,RST,SYN,FIN"
    jump: drop
```

<!--- You can also paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- What did you expect to happen when running the steps above? -->
`/usr/sbin/iptables -t filter -A OUTPUT -p tcp --tcp-flags ALL ACK,RST,SYN,FIN -j drop`
##### ACTUAL RESULTS
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->

<!--- Paste verbatim command output between quotes below -->
```
fatal: [192.168.0.151]: FAILED! => {
    "changed": false,
    "cmd": "/usr/sbin/iptables -t filter -A OUTPUT -p tcp --tcp-flags A,L,L A,C,K,,,R,S,T,,,S,Y,N,,,F,I,N -j drop",
    "invocation": {
        "module_args": {
            "action": "append",
            "chain": "OUTPUT",
            "comment": null,
            "ctstate": [],
            "destination": null,
            "destination_port": null,
            "flush": false,
            "fragment": null,
            "goto": null,
            "icmp_type": null,
            "in_interface": null,
            "ip_version": "ipv4",
            "jump": "drop",
            "limit": null,
            "limit_burst": null,
            "match": [],
            "out_interface": null,
            "policy": null,
            "protocol": "tcp",
            "reject_with": null,
            "set_counters": null,
            "set_dscp_mark": null,
            "set_dscp_mark_class": null,
            "source": null,
            "source_port": null,
            "state": "present",
            "table": "filter",
            "tcp_flags": {
                "flags": "ALL",
                "flags_set": "ACK,RST,SYN,FIN"
            },
            "to_destination": null,
            "to_ports": null,
            "to_source": null,
            "uid_owner": null
        }
    },
    "msg": "iptables v1.4.21: Unknown TCP flag `A'\nTry `iptables -h' or 'iptables --help' for more information.",
    "rc": 2,
    "stderr": "iptables v1.4.21: Unknown TCP flag `A'\nTry `iptables -h' or 'iptables --help' for more information.\n",
    "stderr_lines": [
        "iptables v1.4.21: Unknown TCP flag `A'",
        "Try `iptables -h' or 'iptables --help' for more information."
    ],
    "stdout": "",
    "stdout_lines": []
}


```



Discussion:
module https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/system/iptables.py
!component=lib/ansible/modules/system/iptables.py
cc @LinusU @sebastiendarocha
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
I'm able to duplicate this and found the problem: [this join](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/system/iptables.py#L376) is turning the strings into lists.
Sorry, didn't have time to check this bug. I think you can handle the flags like this:

```
---
- hosts: p4
  tasks:
  - name: TCP ALL ACK,RST,SYN,FIN
    iptables:
      chain: OUTPUT
      protocol: tcp
      tcp_flags:
        flags: ["ALL"]
        flags_set: ["ACK","RST","SYN","FIN"]
      jump: DROP

```
The flags_set expects a array, so give him that instead of a string ;-)

Gives me on a standard 2.4.2.0 :
```
TASK [TCP ALL ACK,RST,SYN,FIN] ****************************************************************************************************
changed: [p4] => {"chain": "OUTPUT", "changed": true, "flush": false, "ip_version": "ipv4", "rule": "-p tcp --tcp-flags ALL ACK,RST,SYN,FIN -j DROP", "state": "present", "table": "filter"}
```

Is this what you expect ?
That's true. It's even set to `type=dict` in the argument spec. I would think it'd fail type validation, or be split into a dict. Let me look into some more.

Thanks for pointing that out!
Upon further investigation, I see that problem is `flags` and `flags_set` are expected to be lists, not strings. When `tcp_flags` is passed in as a `k=v` string, Ansible splits the string on spaces and equal signs into a dictionary, resulting in `{'flags': 'ALL', 'flags_set': 'ACK,RST,SYN,FIN'}`. 

I believe the best solution is to ensure the values in those fields are always lists. This would not break backwards compatibility and allow for specifying `tcp_flags` as `k=v` pairs.

What are your thoughts on this?



Yes, it's pyyaml that interprets the string with equal signs as json. You can convert them into a yaml dict of arrays it should work equally.

I agree with you : a check on the type of the dict elememts would be a great help
Looks like the best way to solve this is to create a subspec, which can have a type specification that will ensure it is always a list. I'll push this up shortly after I get the docs worked out.

Resolving patch:
diff --git a/lib/ansible/modules/system/iptables.py b/lib/ansible/modules/system/iptables.py
--- a/lib/ansible/modules/system/iptables.py
+++ b/lib/ansible/modules/system/iptables.py
@@ -106,11 +106,15 @@
     description:
       - TCP flags specification.
       - C(tcp_flags) expects a dict with the two keys C(flags) and C(flags_set).
-        - The C(flags) list is the mask, a list of flags you want to examine.
-        - The C(flags_set) list tells which one(s) should be set.
-        If one of the two values is missing, the --tcp-flags option will be ignored.
     default: {}
     version_added: "2.4"
+    suboptions:
+        flags:
+            description:
+                - List of flags you want to examine.
+        flags_set:
+            description:
+                - Flags to be set.
   match:
     description:
       - Specifies a match to use, that is, an extension module that tests for
@@ -340,6 +344,19 @@
     protocol: tcp
     reject_with: tcp-reset
     ip_version: ipv4
+
+# Set tcp flags
+- iptables:
+    chain: OUTPUT
+    jump: DROP
+    protocol: tcp
+    tcp_flags:
+      flags: ALL
+      flags_set:
+        - ACK
+        - RST
+        - SYN
+        - FIN
 '''
 
 import re
@@ -518,7 +535,11 @@ def main():
             destination=dict(type='str'),
             to_destination=dict(type='str'),
             match=dict(type='list', default=[]),
-            tcp_flags=dict(type='dict', default={}),
+            tcp_flags=dict(type='dict',
+                           options=dict(
+                                flags=dict(type='list'),
+                                flags_set=dict(type='list'))
+                           ),
             jump=dict(type='str'),
             log_prefix=dict(type='str'),
             goto=dict(type='str'),
@@ -605,5 +626,6 @@ def main():
 
     module.exit_json(**args)
 
+
 if __name__ == '__main__':
     main()


Test patch:
diff --git a/test/units/modules/system/test_iptables.py b/test/units/modules/system/test_iptables.py
--- a/test/units/modules/system/test_iptables.py
+++ b/test/units/modules/system/test_iptables.py
@@ -1,4 +1,3 @@
-from ansible.compat.tests import unittest
 from ansible.compat.tests.mock import patch
 from ansible.module_utils import basic
 from ansible.modules.system import iptables
@@ -578,3 +577,62 @@ def test_insert_jump_reject_with_reject(self):
             '--reject-with',
             'tcp-reset',
         ])
+
+    def test_tcp_flags(self):
+        """ Test various ways of inputting tcp_flags """
+        args = [
+            {
+                'chain': 'OUTPUT',
+                'protocol': 'tcp',
+                'jump': 'DROP',
+                'tcp_flags': 'flags=ALL flags_set="ACK,RST,SYN,FIN"'
+            },
+            {
+                'chain': 'OUTPUT',
+                'protocol': 'tcp',
+                'jump': 'DROP',
+                'tcp_flags': {
+                    'flags': 'ALL',
+                    'flags_set': 'ACK,RST,SYN,FIN'
+                }
+            },
+            {
+                'chain': 'OUTPUT',
+                'protocol': 'tcp',
+                'jump': 'DROP',
+                'tcp_flags': {
+                    'flags': ['ALL'],
+                    'flags_set': ['ACK', 'RST', 'SYN', 'FIN']
+                }
+            },
+
+        ]
+
+        for item in args:
+            set_module_args(item)
+
+            commands_results = [
+                (0, '', ''),
+            ]
+
+            with patch.object(basic.AnsibleModule, 'run_command') as run_command:
+                run_command.side_effect = commands_results
+                with self.assertRaises(AnsibleExitJson) as result:
+                    iptables.main()
+                    self.assertTrue(result.exception.args[0]['changed'])
+
+            self.assertEqual(run_command.call_count, 1)
+            self.assertEqual(run_command.call_args_list[0][0][0], [
+                '/sbin/iptables',
+                '-t',
+                'filter',
+                '-C',
+                'OUTPUT',
+                '-p',
+                'tcp',
+                '--tcp-flags',
+                'ALL',
+                'ACK,RST,SYN,FIN',
+                '-j',
+                'DROP'
+            ])