diff --git a/library/iptables_raw.py b/library/iptables_raw.py deleted file mode 100644 index ee62ee5..0000000 --- a/library/iptables_raw.py +++ /dev/null @@ -1,1089 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# https://github.com/Nordeus/ansible_iptables_raw - -# Make coding more python3-ish -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -""" -(c) 2016, Strahinja Kustudic -(c) 2016, Damir Markovic - -This file is part of Ansible - -Ansible is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -Ansible is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Ansible. If not, see . -""" - -ANSIBLE_METADATA = { - 'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community' -} - -DOCUMENTATION = ''' ---- -module: iptables_raw -short_description: Manage iptables rules -version_added: "2.5" -description: - - Add/remove iptables rules while keeping state. -options: - backup: - description: - - Create a backup of the iptables state file before overwriting it. - required: false - choices: ["yes", "no"] - default: "no" - ipversion: - description: - - Target the IP version this rule is for. - required: false - default: "4" - choices: ["4", "6"] - keep_unmanaged: - description: - - If set to C(yes) keeps active iptables (unmanaged) rules for the target - C(table) and gives them C(weight=90). This means these rules will be - ordered after most of the rules, since default priority is 40, so they - shouldn't be able to block any allow rules. If set to C(no) deletes all - rules which are not set by this module. - - "WARNING: Be very careful when running C(keep_unmanaged=no) for the - first time, since if you don't specify correct rules, you can block - yourself out of the managed host." - required: false - choices: ["yes", "no"] - default: "yes" - name: - description: - - Name that will be used as an identifier for these rules. It can contain - alphanumeric characters, underscore, hyphen, dot, or a space; has to be - UNIQUE for a specified C(table). You can also pass C(name=*) with - C(state=absent) to flush all rules in the selected table, or even all - tables with C(table=*). - required: true - rules: - description: - - The rules that we want to add. Accepts multiline values. - - "Note: You can only use C(-A)/C(--append), C(-N)/C(--new-chain), and - C(-P)/C(--policy) to specify rules." - required: false - state: - description: - - The state this rules fragment should be in. - choices: ["present", "absent"] - required: false - default: present - table: - description: - - The table this rule applies to. You can specify C(table=*) only with - with C(name=*) and C(state=absent) to flush all rules in all tables. - choices: ["filter", "nat", "mangle", "raw", "security", "*"] - required: false - default: filter - weight: - description: - - Determines the order of the rules. Lower C(weight) means higher - priority. Supported range is C(0 - 99) - choices: ["0 - 99"] - required: false - default: 40 -notes: - - Requires C(iptables) package. Debian-based distributions additionally - require C(iptables-persistent). - - "Depending on the distribution, iptables rules are saved in different - locations, so that they can be loaded on boot. Red Hat distributions (RHEL, - CentOS, etc): C(/etc/sysconfig/iptables) and C(/etc/sysconfig/ip6tables); - Debian distributions (Debian, Ubuntu, etc): C(/etc/iptables/rules.v4) and - C(/etc/iptables/rules.v6); other distributions: C(/etc/sysconfig/iptables) - and C(/etc/sysconfig/ip6tables)." - - This module saves state in C(/etc/ansible-iptables) directory, so don't - modify this directory! -author: - - "Strahinja Kustudic (@kustodian)" - - "Damir Markovic (@damirda)" -''' - -EXAMPLES = ''' -# Allow all IPv4 traffic coming in on port 80 (http) -- iptables_raw: - name: allow_tcp_80 - rules: '-A INPUT -p tcp -m tcp --dport 80 -j ACCEPT' - -# Set default rules with weight 10 and disregard all unmanaged rules -- iptables_raw: - name: default_rules - weight: 10 - keep_unmanaged: no - rules: | - -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT - -A INPUT -i lo -j ACCEPT - -A INPUT -p tcp -m state --state NEW -m tcp --dport 22 -j ACCEPT - -P INPUT DROP - -P FORWARD DROP - -P OUTPUT ACCEPT - -# Allow all IPv6 traffic coming in on port 443 (https) with weight 50 -- iptables_raw: - ipversion: 6 - weight: 50 - name: allow_tcp_443 - rules: '-A INPUT -p tcp -m tcp --dport 443 -j ACCEPT' - -# Remove the above rule -- iptables_raw: - state: absent - ipversion: 6 - name: allow_tcp_443 - -# Define rules with a custom chain -- iptables_raw: - name: custom1_rules - rules: | - -N CUSTOM1 - -A CUSTOM1 -s 192.168.0.0/24 -j ACCEPT - -# Reset all IPv4 iptables rules in all tables and allow all traffic -- iptables_raw: - name: '*' - table: '*' - state: absent -''' - -RETURN = ''' -state: - description: state of the rules - returned: success - type: string - sample: present -name: - description: name of the rules - returned: success - type: string - sample: open_tcp_80 -weight: - description: weight of the rules - returned: success - type: int - sample: 40 -ipversion: - description: IP version of iptables used - returned: success - type: int - sample: 6 -rules: - description: passed rules - returned: success - type: string - sample: "-A INPUT -p tcp -m tcp --dport 80 -j ACCEPT" -table: - description: iptables table used - returned: success - type: string - sample: filter -backup: - description: if the iptables file should backed up - returned: success - type: boolean - sample: False -keep_unmanaged: - description: if it should keep unmanaged rules - returned: success - type: boolean - sample: True -''' - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.basic import json - -import time -import fcntl -import re -import shlex -import os -import tempfile - -try: - from collections import defaultdict -except ImportError: - # This is a workaround for Python 2.4 which doesn't have defaultdict. - class defaultdict(dict): - def __init__(self, default_factory, *args, **kwargs): - super(defaultdict, self).__init__(*args, **kwargs) - self.default_factory = default_factory - - def __getitem__(self, key): - try: - return super(defaultdict, self).__getitem__(key) - except KeyError: - return self.__missing__(key) - - def __missing__(self, key): - try: - self[key] = self.default_factory() - except TypeError: - raise KeyError("Missing key %s" % (key, )) - else: - return self[key] - - -# Genereates a diff dictionary from an old and new table dump. -def generate_diff(dump_old, dump_new): - diff = dict() - if dump_old != dump_new: - diff['before'] = dump_old - diff['after'] = dump_new - return diff - - -def compare_dictionaries(dict1, dict2): - if dict1 is None or dict2 is None: - return False - if not (isinstance(dict1, dict) and isinstance(dict2, dict)): - return False - shared_keys = set(dict2.keys()) & set(dict2.keys()) - if not (len(shared_keys) == len(dict1.keys()) and len(shared_keys) == len(dict2.keys())): - return False - dicts_are_equal = True - for key in dict1.keys(): - if isinstance(dict1[key], dict): - dicts_are_equal = dicts_are_equal and compare_dictionaries(dict1[key], dict2[key]) - else: - dicts_are_equal = dicts_are_equal and (dict1[key] == dict2[key]) - if not dicts_are_equal: - break - return dicts_are_equal - - -class Iptables: - - # Default chains for each table - DEFAULT_CHAINS = { - 'filter': ['INPUT', 'FORWARD', 'OUTPUT'], - 'raw': ['PREROUTING', 'OUTPUT'], - 'nat': ['PREROUTING', 'INPUT', 'OUTPUT', 'POSTROUTING'], - 'mangle': ['PREROUTING', 'INPUT', 'FORWARD', 'OUTPUT', 'POSTROUTING'], - 'security': ['INPUT', 'FORWARD', 'OUTPUT'] - } - - # List of tables - TABLES = list(DEFAULT_CHAINS.copy().keys()) - - # Directory which will store the state file. - STATE_DIR = '/etc/ansible-iptables' - - # Key used for unmanaged rules - UNMANAGED_RULES_KEY_NAME = '$unmanaged_rules$' - - # Only allow alphanumeric characters, underscore, hyphen, dots, or a space for - # now. We don't want to have problems while parsing comments using regular - # expressions. - RULE_NAME_ALLOWED_CHARS = 'a-zA-Z0-9_ .-' - - module = None - - def __init__(self, module, ipversion): - # Create directory for json files. - if not os.path.exists(self.STATE_DIR): - os.makedirs(self.STATE_DIR) - if Iptables.module is None: - Iptables.module = module - self.state_save_path = self._get_state_save_path(ipversion) - self.system_save_path = self._get_system_save_path(ipversion) - self.state_dict = self._read_state_file() - self.bins = self._get_bins(ipversion) - self.iptables_names_file = self._get_iptables_names_file(ipversion) - # Check if we have a required iptables version. - self._check_compatibility() - # Save active iptables rules for all tables, so that we don't - # need to fetch them every time using 'iptables-save' command. - self._active_rules = {} - self._refresh_active_rules(table='*') - - def __eq__(self, other): - return (isinstance(other, self.__class__) and compare_dictionaries(other.state_dict, self.state_dict)) - - def __ne__(self, other): - return not self.__eq__(other) - - def _get_bins(self, ipversion): - if ipversion == '4': - return {'iptables': Iptables.module.get_bin_path('iptables'), - 'iptables-save': Iptables.module.get_bin_path('iptables-save'), - 'iptables-restore': Iptables.module.get_bin_path('iptables-restore')} - else: - return {'iptables': Iptables.module.get_bin_path('ip6tables'), - 'iptables-save': Iptables.module.get_bin_path('ip6tables-save'), - 'iptables-restore': Iptables.module.get_bin_path('ip6tables-restore')} - - def _get_iptables_names_file(self, ipversion): - if ipversion == '4': - return '/proc/net/ip_tables_names' - else: - return '/proc/net/ip6_tables_names' - - # Return a list of active iptables tables - def _get_list_of_active_tables(self): - if os.path.isfile(self.iptables_names_file): - table_names = open(self.iptables_names_file, 'r').read() - return table_names.splitlines() - else: - return [] - - # If /etc/debian_version exist, this means this is a debian based OS (Ubuntu, Mint, etc...) - def _is_debian(self): - return os.path.isfile('/etc/debian_version') - # If /etc/alpine-release exist, this means this is AlpineLinux OS - def _is_alpine(self): - return os.path.isfile('/etc/alpine-release') - - # If /etc/arch-release exist, this means this is an ArchLinux OS - def _is_arch_linux(self): - return os.path.isfile('/etc/arch-release') - - # If /etc/gentoo-release exist, this means this is Gentoo - def _is_gentoo(self): - return os.path.isfile('/etc/gentoo-release') - - # Get the iptables system save path. - # Supports RHEL/CentOS '/etc/sysconfig/' location. - # Supports Debian/Ubuntu/Mint, '/etc/iptables/' location. - # Supports Gentoo, '/var/lib/iptables/' location. - def _get_system_save_path(self, ipversion): - # distro detection, path setting should be added - if self._is_debian(): - # Check if iptables-persistent packages is installed - if not os.path.isdir('/etc/iptables'): - Iptables.module.fail_json(msg="This module requires 'iptables-persistent' package!") - if ipversion == '4': - return '/etc/iptables/rules.v4' - else: - return '/etc/iptables/rules.v6' - elif self._is_arch_linux(): - if ipversion == '4': - return '/etc/iptables/iptables.rules' - else: - return '/etc/iptables/ip6tables.rules' - elif self._is_gentoo(): - if ipversion == '4': - return '/var/lib/iptables/rules-save' - else: - return '/var/lib/ip6tables/rules-save' - - elif self._is_alpine(): - if ipversion == '4': - return '/etc/iptables/rules-save' - else: - return '/etc/iptables/rules6-save' - else: - if ipversion == '4': - return '/etc/sysconfig/iptables' - else: - return '/etc/sysconfig/ip6tables' - - # Return path to json state file. - def _get_state_save_path(self, ipversion): - if ipversion == '4': - return self.STATE_DIR + '/iptables.json' - else: - return self.STATE_DIR + '/ip6tables.json' - - # Checks if iptables is installed and if we have a correct version. - def _check_compatibility(self): - from distutils.version import StrictVersion - cmd = [self.bins['iptables'], '--version'] - rc, stdout, stderr = Iptables.module.run_command(cmd, check_rc=False) - if rc == 0: - result = re.search(r'^ip6tables\s+v(\d+\.\d+)\.\d+$', stdout) - if result: - version = result.group(1) - # CentOS 5 ip6tables (v1.3.x) doesn't support comments, - # which means it cannot be used with this module. - if StrictVersion(version) < StrictVersion('1.4'): - Iptables.module.fail_json(msg="This module isn't compatible with ip6tables versions older than 1.4.x") - else: - Iptables.module.fail_json(msg="Could not fetch iptables version! Is iptables installed?") - - # Read rules from the json state file and return a dict. - def _read_state_file(self): - json_str = '{}' - if os.path.isfile(self.state_save_path): - try: - json_str = open(self.state_save_path, 'r').read() - except: - Iptables.module.fail_json(msg="Could not read the state file '%s'!" % self.state_save_path) - try: - read_dict = defaultdict(lambda: dict(dump='', rules_dict={}), json.loads(json_str)) - except: - Iptables.module.fail_json(msg="Could not parse the state file '%s'! Please manually delete it to continue." % self.state_save_path) - return read_dict - - # Checks if a table exists in the state_dict. - def _has_table(self, tbl): - return tbl in self.state_dict - - # Deletes table from the state_dict. - def _delete_table(self, tbl): - if self._has_table(tbl): - del self.state_dict[tbl] - - # Acquires lock or exits after wait_for_seconds if it cannot be acquired. - def acquire_lock_or_exit(self, wait_for_seconds=10): - lock_file = self.STATE_DIR + '/.iptables.lock' - i = 0 - f = open(lock_file, 'w+') - while i < wait_for_seconds: - try: - fcntl.flock(f, fcntl.LOCK_EX | fcntl.LOCK_NB) - return - except IOError: - i += 1 - time.sleep(1) - Iptables.module.fail_json(msg="Could not acquire lock to continue execution! " - "Probably another instance of this module is running.") - - # Check if a table has anything to flush (to check all tables pass table='*'). - def table_needs_flush(self, table): - needs_flush = False - if table == '*': - for tbl in Iptables.TABLES: - # If the table exists or if it needs to be flushed that means will make changes. - if self._has_table(tbl) or self._single_table_needs_flush(tbl): - needs_flush = True - break - # Only flush the specified table - else: - if self._has_table(table) or self._single_table_needs_flush(table): - needs_flush = True - return needs_flush - - # Check if a passed table needs to be flushed. - def _single_table_needs_flush(self, table): - needs_flush = False - active_rules = self._get_active_rules(table) - if active_rules: - policies = self._filter_default_chain_policies(active_rules, table) - chains = self._filter_custom_chains(active_rules, table) - rules = self._filter_rules(active_rules, table) - # Go over default policies and check if they are all ACCEPT. - for line in policies.splitlines(): - if not re.search(r'\bACCEPT\b', line): - needs_flush = True - break - # If there is at least one rule or custom chain, that means we need flush. - if len(chains) > 0 or len(rules) > 0: - needs_flush = True - return needs_flush - - # Returns a copy of the rules dict of a passed table. - def _get_table_rules_dict(self, table): - return self.state_dict[table]['rules_dict'].copy() - - # Returns saved table dump. - def get_saved_table_dump(self, table): - return self.state_dict[table]['dump'] - - # Sets saved table dump. - def _set_saved_table_dump(self, table, dump): - self.state_dict[table]['dump'] = dump - - # Updates saved table dump from the active rules. - def refresh_saved_table_dump(self, table): - active_rules = self._get_active_rules(table) - self._set_saved_table_dump(table, active_rules) - - # Sets active rules of the passed table. - def _set_active_rules(self, table, rules): - self._active_rules[table] = rules - - # Return active rules of the passed table. - def _get_active_rules(self, table, clean=True): - active_rules = '' - if table == '*': - all_rules = [] - for tbl in Iptables.TABLES: - if tbl in self._active_rules: - all_rules.append(self._active_rules[tbl]) - active_rules = '\n'.join(all_rules) - else: - active_rules = self._active_rules[table] - if clean: - return self._clean_save_dump(active_rules) - else: - return active_rules - - # Refresh active rules of a table ('*' for all tables). - def _refresh_active_rules(self, table): - if table == '*': - for tbl in Iptables.TABLES: - self._set_active_rules(tbl, self._get_system_active_rules(tbl)) - else: - self._set_active_rules(table, self._get_system_active_rules(table)) - - # Get iptables-save dump of active rules of one or all tables (pass '*') and return it as a string. - def _get_system_active_rules(self, table): - active_tables = self._get_list_of_active_tables() - if table == '*': - cmd = [self.bins['iptables-save']] - # If there are no active tables, that means there are no rules - if not active_tables: - return "" - else: - cmd = [self.bins['iptables-save'], '-t', table] - # If the table is not active, that means it has no rules - if table not in active_tables: - return "" - rc, stdout, stderr = Iptables.module.run_command(cmd, check_rc=True) - return stdout - - # Splits a rule into tokens - def _split_rule_into_tokens(self, rule): - try: - return shlex.split(rule, comments=True) - except: - msg = "Could not parse the iptables rule:\n%s" % rule - Iptables.module.fail_json(msg=msg) - - # Removes comment lines and empty lines from rules. - @staticmethod - def clean_up_rules(rules): - cleaned_rules = [] - for line in rules.splitlines(): - # Remove lines with comments and empty lines. - if not (Iptables.is_comment(line) or Iptables.is_empty_line(line)): - cleaned_rules.append(line) - return '\n'.join(cleaned_rules) - - # Checks if the line is a custom chain in specific iptables table. - @staticmethod - def is_custom_chain(line, table): - default_chains = Iptables.DEFAULT_CHAINS[table] - if re.match(r'\s*(:|(-N|--new-chain)\s+)[^\s]+', line) \ - and not re.match(r'\s*(:|(-N|--new-chain)\s+)\b(' + '|'.join(default_chains) + r')\b', line): - return True - else: - return False - - # Checks if the line is a default chain of an iptables table. - @staticmethod - def is_default_chain(line, table): - default_chains = Iptables.DEFAULT_CHAINS[table] - if re.match(r'\s*(:|(-P|--policy)\s+)\b(' + '|'.join(default_chains) + r')\b\s+(ACCEPT|DROP)', line): - return True - else: - return False - - # Checks if a line is an iptables rule. - @staticmethod - def is_rule(line): - # We should only allow adding rules with '-A/--append', since others don't make any sense. - if re.match(r'\s*(-A|--append)\s+[^\s]+', line): - return True - else: - return False - - # Checks if a line starts with '#'. - @staticmethod - def is_comment(line): - if re.match(r'\s*#', line): - return True - else: - return False - - # Checks if a line is empty. - @staticmethod - def is_empty_line(line): - if re.match(r'^$', line.strip()): - return True - else: - return False - - # Return name of custom chain from the rule. - def _get_custom_chain_name(self, line, table): - if Iptables.is_custom_chain(line, table): - return re.match(r'\s*(:|(-N|--new-chain)\s+)([^\s]+)', line).group(3) - else: - return '' - - # Return name of default chain from the rule. - def _get_default_chain_name(self, line, table): - if Iptables.is_default_chain(line, table): - return re.match(r'\s*(:|(-N|--new-chain)\s+)([^\s]+)', line).group(3) - else: - return '' - - # Return target of the default chain from the rule. - def _get_default_chain_target(self, line, table): - if Iptables.is_default_chain(line, table): - return re.match(r'\s*(:|(-N|--new-chain)\s+)([^\s]+)\s+([A-Z]+)', line).group(4) - else: - return '' - - # Removes duplicate custom chains from the table rules. - def _remove_duplicate_custom_chains(self, rules, table): - all_rules = [] - custom_chain_names = [] - for line in rules.splitlines(): - # Extract custom chains. - if Iptables.is_custom_chain(line, table): - chain_name = self._get_custom_chain_name(line, table) - if chain_name not in custom_chain_names: - custom_chain_names.append(chain_name) - all_rules.append(line) - else: - all_rules.append(line) - return '\n'.join(all_rules) - - # Returns current iptables-save dump cleaned from comments and packet/byte counters. - def _clean_save_dump(self, simple_rules): - cleaned_dump = [] - for line in simple_rules.splitlines(): - # Ignore comments. - if Iptables.is_comment(line): - continue - # Reset counters for chains (begin with ':'), for easier comparing later on. - if re.match(r'\s*:', line): - cleaned_dump.append(re.sub(r'\[([0-9]+):([0-9]+)\]', '[0:0]', line)) - else: - cleaned_dump.append(line) - cleaned_dump.append('\n') - return '\n'.join(cleaned_dump) - - # Returns lines with default chain policies. - def _filter_default_chain_policies(self, rules, table): - chains = [] - for line in rules.splitlines(): - if Iptables.is_default_chain(line, table): - chains.append(line) - return '\n'.join(chains) - - # Returns lines with iptables rules from an iptables-save table dump - # (removes chain policies, custom chains, comments and everything else). By - # default returns all rules, if 'only_unmanged=True' returns rules which - # are not managed by Ansible. - def _filter_rules(self, rules, table, only_unmanaged=False): - filtered_rules = [] - for line in rules.splitlines(): - if Iptables.is_rule(line): - if only_unmanaged: - tokens = self._split_rule_into_tokens(line) - # We need to check if a rule has a comment which starts with 'ansible[name]' - if '--comment' in tokens: - comment_index = tokens.index('--comment') + 1 - if comment_index < len(tokens): - # Fetch the comment - comment = tokens[comment_index] - # Skip the rule if the comment starts with 'ansible[name]' - if not re.match(r'ansible\[[' + Iptables.RULE_NAME_ALLOWED_CHARS + r']+\]', comment): - filtered_rules.append(line) - else: - # Fail if there is no comment after the --comment parameter - msg = "Iptables rule is missing a comment after the '--comment' parameter:\n%s" % line - Iptables.module.fail_json(msg=msg) - # If it doesn't have comment, this means it is not managed by Ansible and we should append it. - else: - filtered_rules.append(line) - else: - filtered_rules.append(line) - return '\n'.join(filtered_rules) - - # Same as _filter_rules(), but returns custom chains - def _filter_custom_chains(self, rules, table, only_unmanaged=False): - filtered_chains = [] - # Get list of managed custom chains, which is needed to detect unmanaged custom chains - managed_custom_chains_list = self._get_custom_chains_list(table) - for line in rules.splitlines(): - if Iptables.is_custom_chain(line, table): - if only_unmanaged: - # The chain is not managed by this module if it's not in the list of managed custom chains. - chain_name = self._get_custom_chain_name(line, table) - if chain_name not in managed_custom_chains_list: - filtered_chains.append(line) - else: - filtered_chains.append(line) - return '\n'.join(filtered_chains) - - # Returns list of custom chains of a table. - def _get_custom_chains_list(self, table): - custom_chains_list = [] - for key, value in self._get_table_rules_dict(table).items(): - # Ignore UNMANAGED_RULES_KEY_NAME key, since we only want managed custom chains. - if key != Iptables.UNMANAGED_RULES_KEY_NAME: - for line in value['rules'].splitlines(): - if Iptables.is_custom_chain(line, table): - chain_name = self._get_custom_chain_name(line, table) - if chain_name not in custom_chains_list: - custom_chains_list.append(chain_name) - return custom_chains_list - - # Prepends 'ansible[name]: ' to iptables rule '--comment' argument, - # or adds 'ansible[name]' as a comment if there is no comment. - def _prepend_ansible_comment(self, rules, name): - commented_lines = [] - for line in rules.splitlines(): - # Extract rules only since we cannot add comments to custom chains. - if Iptables.is_rule(line): - tokens = self._split_rule_into_tokens(line) - if '--comment' in tokens: - # If there is a comment parameter, we need to prepand 'ansible[name]: '. - comment_index = tokens.index('--comment') + 1 - if comment_index < len(tokens): - # We need to remove double quotes from comments, since there - # is an incompatiblity with older iptables versions - comment_text = tokens[comment_index].replace('"', '') - tokens[comment_index] = 'ansible[' + name + ']: ' + comment_text - else: - # Fail if there is no comment after the --comment parameter - msg = "Iptables rule is missing a comment after the '--comment' parameter:\n%s" % line - Iptables.module.fail_json(msg=msg) - else: - # If comment doesn't exist, we add a comment 'ansible[name]' - tokens += ['-m', 'comment', '--comment', 'ansible[' + name + ']'] - # Escape and quote tokens in case they have spaces - tokens = [self._escape_and_quote_string(x) for x in tokens] - commented_lines.append(" ".join(tokens)) - # Otherwise it's a chain, and we should just return it. - else: - commented_lines.append(line) - return '\n'.join(commented_lines) - - # Double quote a string if it contains a space and escape double quotes. - def _escape_and_quote_string(self, s): - escaped = s.replace('"', r'\"') - if re.search(r'\s', escaped): - return '"' + escaped + '"' - else: - return escaped - - # Add table rule to the state_dict. - def add_table_rule(self, table, name, weight, rules, prepend_ansible_comment=True): - self._fail_on_bad_rules(rules, table) - if prepend_ansible_comment: - self.state_dict[table]['rules_dict'][name] = {'weight': weight, 'rules': self._prepend_ansible_comment(rules, name)} - else: - self.state_dict[table]['rules_dict'][name] = {'weight': weight, 'rules': rules} - - # Remove table rule from the state_dict. - def remove_table_rule(self, table, name): - if name in self.state_dict[table]['rules_dict']: - del self.state_dict[table]['rules_dict'][name] - - # TODO: Add sorting of rules so that diffs in check_mode look nicer and easier to follow. - # Sorting would be done from top to bottom like this: - # * default chain policies - # * custom chains - # * rules - # - # Converts rules from a state_dict to an iptables-save readable format. - def get_table_rules(self, table): - generated_rules = '' - # We first add a header e.g. '*filter'. - generated_rules += '*' + table + '\n' - rules_list = [] - custom_chains_list = [] - default_chain_policies = [] - dict_rules = self._get_table_rules_dict(table) - # Return list of rule names sorted by ('weight', 'rules') tuple. - for rule_name in sorted(dict_rules, key=lambda x: (dict_rules[x]['weight'], dict_rules[x]['rules'])): - rules = dict_rules[rule_name]['rules'] - # Fail if some of the rules are bad - self._fail_on_bad_rules(rules, table) - rules_list.append(self._filter_rules(rules, table)) - custom_chains_list.append(self._filter_custom_chains(rules, table)) - default_chain_policies.append(self._filter_default_chain_policies(rules, table)) - # Clean up empty strings from these two lists. - rules_list = list(filter(None, rules_list)) - custom_chains_list = list(filter(None, custom_chains_list)) - default_chain_policies = list(filter(None, default_chain_policies)) - if default_chain_policies: - # Since iptables-restore applies the last chain policy it reads, we - # have to reverse the order of chain policies so that those with - # the lowest weight (higher priority) are read last. - generated_rules += '\n'.join(reversed(default_chain_policies)) + '\n' - if custom_chains_list: - # We remove duplicate custom chains so that iptables-restore - # doesn't fail because of that. - generated_rules += self._remove_duplicate_custom_chains('\n'.join(sorted(custom_chains_list)), table) + '\n' - if rules_list: - generated_rules += '\n'.join(rules_list) + '\n' - generated_rules += 'COMMIT\n' - return generated_rules - - # Sets unmanaged rules for the passed table in the state_dict. - def _set_unmanaged_rules(self, table, rules): - self.add_table_rule(table, Iptables.UNMANAGED_RULES_KEY_NAME, 90, rules, prepend_ansible_comment=False) - - # Clears unmanaged rules of a table. - def clear_unmanaged_rules(self, table): - self._set_unmanaged_rules(table, '') - - # Updates unmanaged rules of a table from the active rules. - def refresh_unmanaged_rules(self, table): - # Get active iptables rules and clean them up. - active_rules = self._get_active_rules(table) - unmanaged_chains_and_rules = [] - unmanaged_chains_and_rules.append(self._filter_custom_chains(active_rules, table, only_unmanaged=True)) - unmanaged_chains_and_rules.append(self._filter_rules(active_rules, table, only_unmanaged=True)) - # Clean items which are empty strings - unmanaged_chains_and_rules = list(filter(None, unmanaged_chains_and_rules)) - self._set_unmanaged_rules(table, '\n'.join(unmanaged_chains_and_rules)) - - # Check if there are bad lines in the specified rules. - def _fail_on_bad_rules(self, rules, table): - for line in rules.splitlines(): - tokens = self._split_rule_into_tokens(line) - if '-t' in tokens or '--table' in tokens: - msg = ("Iptables rules cannot contain '-t/--table' parameter. " - "You should use the 'table' parameter of the module to set rules " - "for a specific table.") - Iptables.module.fail_json(msg=msg) - # Fail if the parameter --comment doesn't have a comment after - if '--comment' in tokens and len(tokens) <= tokens.index('--comment') + 1: - msg = "Iptables rule is missing a comment after the '--comment' parameter:\n%s" % line - Iptables.module.fail_json(msg=msg) - if not (Iptables.is_rule(line) or - Iptables.is_custom_chain(line, table) or - Iptables.is_default_chain(line, table) or - Iptables.is_comment(line)): - msg = ("Bad iptables rule '%s'! You can only use -A/--append, -N/--new-chain " - "and -P/--policy to specify rules." % line) - Iptables.module.fail_json(msg=msg) - - # Write rules to dest path. - def _write_rules_to_file(self, rules, dest): - tmp_path = self._write_to_temp_file(rules) - Iptables.module.atomic_move(tmp_path, dest) - - # Write text to a temp file and return path to that file. - def _write_to_temp_file(self, text): - fd, path = tempfile.mkstemp() - Iptables.module.add_cleanup_file(path) # add file for cleanup later - tmp = os.fdopen(fd, 'w') - tmp.write(text) - tmp.close() - return path - - # - # Public and private methods which make changes on the system - # are named 'system_*' and '_system_*', respectively. - # - - # Flush all rules in a passed table. - def _system_flush_single_table_rules(self, table): - # Set all default chain policies to ACCEPT. - for chain in Iptables.DEFAULT_CHAINS[table]: - cmd = [self.bins['iptables'], '-t', table, '-P', chain, 'ACCEPT'] - Iptables.module.run_command(cmd, check_rc=True) - # Then flush all rules. - cmd = [self.bins['iptables'], '-t', table, '-F'] - Iptables.module.run_command(cmd, check_rc=True) - # And delete custom chains. - cmd = [self.bins['iptables'], '-t', table, '-X'] - Iptables.module.run_command(cmd, check_rc=True) - # Update active rules in the object. - self._refresh_active_rules(table) - - # Save active iptables rules to the system path. - def _system_save_active(self, backup=False): - # Backup if needed - if backup: - Iptables.module.backup_local(self.system_save_path) - # Get iptables-save dump of all tables - all_active_rules = self._get_active_rules(table='*', clean=False) - # Move iptables-save dump of all tables to the iptables_save_path - self._write_rules_to_file(all_active_rules, self.system_save_path) - - # Apply table dict rules to the system. - def system_apply_table_rules(self, table, test=False): - dump_path = self._write_to_temp_file(self.get_table_rules(table)) - if test: - cmd = [self.bins['iptables-restore'], '-t', dump_path] - else: - cmd = [self.bins['iptables-restore'], dump_path] - rc, stdout, stderr = Iptables.module.run_command(cmd, check_rc=False) - if rc != 0: - if test: - dump_contents_file = open(dump_path, 'r') - dump_contents = dump_contents_file.read() - dump_contents_file.close() - msg = "There is a problem with the iptables rules:" \ - + '\n\nError message:\n' \ - + stderr \ - + '\nGenerated rules:\n#######\n' \ - + dump_contents + '#####' - else: - msg = "Could not load iptables rules:\n\n" + stderr - Iptables.module.fail_json(msg=msg) - self._refresh_active_rules(table) - - # Flush one or all tables (to flush all tables pass table='*'). - def system_flush_table_rules(self, table): - if table == '*': - for tbl in Iptables.TABLES: - self._delete_table(tbl) - if self._single_table_needs_flush(tbl): - self._system_flush_single_table_rules(tbl) - # Only flush the specified table. - else: - self._delete_table(table) - if self._single_table_needs_flush(table): - self._system_flush_single_table_rules(table) - - # Saves state file and system iptables rules. - def system_save(self, backup=False): - self._system_save_active(backup=backup) - rules = json.dumps(self.state_dict, sort_keys=True, indent=4, separators=(',', ': ')) - self._write_rules_to_file(rules, self.state_save_path) - - -def main(): - - module = AnsibleModule( - argument_spec=dict( - ipversion=dict(required=False, choices=["4", "6"], type='str', default="4"), - state=dict(required=False, choices=['present', 'absent'], default='present', type='str'), - weight=dict(required=False, type='int', default=40), - name=dict(required=True, type='str'), - table=dict(required=False, choices=Iptables.TABLES + ['*'], default="filter", type='str'), - rules=dict(required=False, type='str', default=""), - backup=dict(required=False, type='bool', default=False), - keep_unmanaged=dict(required=False, type='bool', default=True), - ), - supports_check_mode=True, - ) - - check_mode = module.check_mode - changed = False - ipversion = module.params['ipversion'] - state = module.params['state'] - weight = module.params['weight'] - name = module.params['name'] - table = module.params['table'] - rules = module.params['rules'] - backup = module.params['backup'] - keep_unmanaged = module.params['keep_unmanaged'] - - kw = dict(state=state, name=name, rules=rules, weight=weight, ipversion=ipversion, - table=table, backup=backup, keep_unmanaged=keep_unmanaged) - - iptables = Iptables(module, ipversion) - - # Acquire lock so that only one instance of this object can exist. - # Fail if the lock cannot be acquired within 10 seconds. - iptables.acquire_lock_or_exit(wait_for_seconds=10) - - # Clean up rules of comments and empty lines. - rules = Iptables.clean_up_rules(rules) - - # Check additional parameter requirements - if state == 'present' and name == '*': - module.fail_json(msg="Parameter 'name' can only be '*' if 'state=absent'") - if state == 'present' and table == '*': - module.fail_json(msg="Parameter 'table' can only be '*' if 'name=*' and 'state=absent'") - if state == 'present' and not name: - module.fail_json(msg="Parameter 'name' cannot be empty") - if state == 'present' and not re.match('^[' + Iptables.RULE_NAME_ALLOWED_CHARS + ']+$', name): - module.fail_json(msg="Parameter 'name' not valid! It can only contain alphanumeric characters, " - "underscore, hyphen, or a space, got: '%s'" % name) - if weight < 0 or weight > 99: - module.fail_json(msg="Parameter 'weight' can be 0-99, got: %d" % weight) - if state == 'present' and rules == '': - module.fail_json(msg="Parameter 'rules' cannot be empty when 'state=present'") - - # Flush rules of one or all tables - if state == 'absent' and name == '*': - # Check if table(s) need to be flushed - if iptables.table_needs_flush(table): - changed = True - if not check_mode: - # Flush table(s) - iptables.system_flush_table_rules(table) - # Save state and system iptables rules - iptables.system_save(backup=backup) - # Exit since there is nothing else to do - kw['changed'] = changed - module.exit_json(**kw) - - # Initialize new iptables object which will store new rules - iptables_new = Iptables(module, ipversion) - - if state == 'present': - iptables_new.add_table_rule(table, name, weight, rules) - else: - iptables_new.remove_table_rule(table, name) - - if keep_unmanaged: - iptables_new.refresh_unmanaged_rules(table) - else: - iptables_new.clear_unmanaged_rules(table) - - # Refresh saved table dump with active iptables rules - iptables_new.refresh_saved_table_dump(table) - - # Check if there are changes in iptables, and if yes load new rules - if iptables != iptables_new: - - changed = True - - # Test generated rules - iptables_new.system_apply_table_rules(table, test=True) - - if check_mode: - # Create a predicted diff for check_mode. - # Diff will be created from rules generated from the state dictionary. - if hasattr(module, '_diff') and module._diff: - # Update unmanaged rules in the old object so the generated diff - # from the rules dictionaries is more accurate. - iptables.refresh_unmanaged_rules(table) - # Generate table rules from rules dictionaries. - table_rules_old = iptables.get_table_rules(table) - table_rules_new = iptables_new.get_table_rules(table) - # If rules generated from dicts are not equal, we generate a diff from them. - if table_rules_old != table_rules_new: - kw['diff'] = generate_diff(table_rules_old, table_rules_new) - else: - # TODO: Update this comment to be better. - kw['diff'] = {'prepared': "System rules were not changed (e.g. rule " - "weight changed, redundant rule, etc)"} - else: - # We need to fetch active table dump before we apply new rules - # since we will need them to generate a diff. - table_active_rules = iptables_new.get_saved_table_dump(table) - - # Apply generated rules. - iptables_new.system_apply_table_rules(table) - - # Refresh saved table dump with active iptables rules. - iptables_new.refresh_saved_table_dump(table) - - # Save state and system iptables rules. - iptables_new.system_save(backup=backup) - - # Generate a diff. - if hasattr(module, '_diff') and module._diff: - table_active_rules_new = iptables_new.get_saved_table_dump(table) - if table_active_rules != table_active_rules_new: - kw['diff'] = generate_diff(table_active_rules, table_active_rules_new) - else: - # TODO: Update this comment to be better. - kw['diff'] = {'prepared': "System rules were not changed (e.g. rule " - "weight changed, redundant rule, etc)"} - - kw['changed'] = changed - module.exit_json(**kw) - - -if __name__ == '__main__': - main()