diff --git a/plugins/module_utils/rule.py b/plugins/module_utils/rule.py index 451ace7b..62920028 100644 --- a/plugins/module_utils/rule.py +++ b/plugins/module_utils/rule.py @@ -40,6 +40,12 @@ icmptype=dict(default='any', required=False, type='str'), sched=dict(required=False, type='str'), quick=dict(default=False, type='bool'), + max_src_conn=dict(required=False, type='int'), + max_src_states=dict(required=False, type='int'), + max_src_nodes=dict(required=False, type='int'), + max_src_conn_rate=dict(required=False, type='int'), + max_src_conn_rates=dict(required=False, type='int'), + statetimeout=dict(required=False, type='int'), ) RULE_REQUIRED_IF = [ @@ -50,8 +56,8 @@ # These are rule elements that are (currently) unmanaged by this module RULE_UNMANAGED_ELEMENTS = [ - 'created', 'id', 'max', 'max-src-conn', 'max-src-nodes', 'max-src-states', 'os', - 'statetimeout', 'statetype', 'tag', 'tagged', 'updated' + 'created', 'id', 'max', 'os', + 'statetype', 'tag', 'tagged', 'updated' ] @@ -146,6 +152,13 @@ def _params_to_obj(self): self._get_ansible_param_bool(obj, 'quick') self._get_ansible_param_bool(obj, 'tcpflags_any', value='') + self._get_ansible_param(obj, 'max_src_conn', fname='max-src-conn') + self._get_ansible_param(obj, 'max_src_states', fname='max-src-states') + self._get_ansible_param(obj, 'max_src_nodes', fname='max-src-nodes') + self._get_ansible_param(obj, 'max_src_conn_rate', fname='max-src-conn-rate') + self._get_ansible_param(obj, 'max_src_conn_rates', fname='max-src-conn-rates') + self._get_ansible_param(obj, 'statetimeout') + self._floating = 'floating' in self.obj and self.obj['floating'] == 'yes' self._after = params.get('after') self._before = params.get('before') @@ -227,6 +240,19 @@ def _validate_params(self): if params.get('quick') and not params.get('floating'): self.module.fail_json(msg='quick can only be used on floating rules') + # state tracking options require TCP and pass rules + state_tracking_params = ['max_src_conn', 'max_src_states', 'max_src_nodes', 'max_src_conn_rate', 'max_src_conn_rates', 'statetimeout'] + has_state_tracking = any(params.get(p) is not None for p in state_tracking_params) + if has_state_tracking: + if params.get('action') and params['action'] != 'pass': + self.module.fail_json(msg='State tracking options (max_src_conn, max_src_states, etc.) can only be used on pass rules') + if params.get('protocol') and params['protocol'] not in ['tcp', 'any']: + self.module.fail_json(msg='max_src_conn and max_src_conn_rate can only be used with TCP protocol') + + # max_src_conn_rate and max_src_conn_rates must be set together + if (params.get('max_src_conn_rate') is not None) != (params.get('max_src_conn_rates') is not None): + self.module.fail_json(msg='max_src_conn_rate and max_src_conn_rates must both be set') + # ICMP if params.get('protocol') == 'icmp' and params.get('icmptype') is not None: both_types = ['any', 'echorep', 'echoreq', 'paramprob', 'redir', 'routeradv', 'routersol', 'timex', 'unreach'] @@ -490,7 +516,8 @@ def _get_first_deny_rule_xml_index(self): @staticmethod def _get_params_to_remove(): """ returns the list of params to remove if they are not set """ - return ['log', 'protocol', 'disabled', 'defaultqueue', 'ackqueue', 'dnpipe', 'pdnpipe', 'gateway', 'icmptype', 'sched', 'quick', 'tcpflags_any'] + return ['log', 'protocol', 'disabled', 'defaultqueue', 'ackqueue', 'dnpipe', 'pdnpipe', 'gateway', 'icmptype', 'sched', 'quick', 'tcpflags_any', + 'max-src-conn', 'max-src-states', 'max-src-nodes', 'max-src-conn-rate', 'max-src-conn-rates', 'statetimeout'] def _get_rule_position(self, descr=None, fail=True, first=True): """ get rule position in interface/floating """ @@ -601,6 +628,12 @@ def _log_fields(self, before=None): values += self.format_cli_field(self.params, 'tracker') values += self.format_cli_field(self.params, 'sched') values += self.format_cli_field(self.params, 'quick', fvalue=self.fvalue_bool, default=False) + values += self.format_cli_field(self.params, 'max_src_conn') + values += self.format_cli_field(self.params, 'max_src_states') + values += self.format_cli_field(self.params, 'max_src_nodes') + values += self.format_cli_field(self.params, 'max_src_conn_rate') + values += self.format_cli_field(self.params, 'max_src_conn_rates') + values += self.format_cli_field(self.params, 'statetimeout') else: fbefore = self._obj_to_log_fields(before) fafter = self._obj_to_log_fields(self.obj) @@ -633,6 +666,12 @@ def _log_fields(self, before=None): values += self.format_updated_cli_field(self.obj, before, 'tracker', add_comma=(values)) values += self.format_updated_cli_field(self.obj, before, 'sched', add_comma=(values)) values += self.format_updated_cli_field(self.obj, before, 'quick', fvalue=self.fvalue_bool, add_comma=(values)) + values += self.format_updated_cli_field(self.obj, before, 'max-src-conn', fname='max_src_conn', add_comma=(values)) + values += self.format_updated_cli_field(self.obj, before, 'max-src-states', fname='max_src_states', add_comma=(values)) + values += self.format_updated_cli_field(self.obj, before, 'max-src-nodes', fname='max_src_nodes', add_comma=(values)) + values += self.format_updated_cli_field(self.obj, before, 'max-src-conn-rate', fname='max_src_conn_rate', add_comma=(values)) + values += self.format_updated_cli_field(self.obj, before, 'max-src-conn-rates', fname='max_src_conn_rates', add_comma=(values)) + values += self.format_updated_cli_field(self.obj, before, 'statetimeout', add_comma=(values)) return values def _obj_address_to_log_field(self, rule, addr): diff --git a/plugins/modules/pfsense_rule.py b/plugins/modules/pfsense_rule.py index 0c22db1e..f832498a 100644 --- a/plugins/modules/pfsense_rule.py +++ b/plugins/modules/pfsense_rule.py @@ -142,6 +142,29 @@ description: Set this option to apply this action to traffic that matches this rule immediately type: bool default: False + max_src_conn: + description: Maximum number of established connections per source host. Only valid for TCP pass rules. + type: int + max_src_states: + description: Maximum number of state entries per source host. Only valid for pass rules. + type: int + max_src_nodes: + description: Maximum number of unique source hosts. + type: int + max_src_conn_rate: + description: + - Maximum number of new connections per time interval from a single host. + - Must be used together with C(max_src_conn_rates). + - Only valid for TCP pass rules. + type: int + max_src_conn_rates: + description: + - Time interval in seconds for C(max_src_conn_rate). + - Must be used together with C(max_src_conn_rate). + type: int + statetimeout: + description: State timeout in seconds. + type: int """ EXAMPLES = """ @@ -169,6 +192,25 @@ destination_port: 4000-5000 after: 'Allow Internal DNS traffic out' state: present +- name: "Rate-limit inbound connections" + pfsense_rule: + name: 'Rate-limited Qubic inbound' + action: pass + interface: wan + floating: true + direction: in + quick: true + ipprotocol: inet + protocol: tcp + source: any + destination: qubic_nodes + destination_port: 21841 + log: true + max_src_conn: 3 + max_src_states: 3 + max_src_conn_rate: 3 + max_src_conn_rates: 60 + state: present """ RETURN = """ diff --git a/tests/unit/plugins/modules/test_pfsense_rule_create.py b/tests/unit/plugins/modules/test_pfsense_rule_create.py index 388b72aa..aad14273 100644 --- a/tests/unit/plugins/modules/test_pfsense_rule_create.py +++ b/tests/unit/plugins/modules/test_pfsense_rule_create.py @@ -734,3 +734,57 @@ def test_rule_create_pass_before_reject(self): reject_idx = rules.index('reject_all_lan') self.assertLess(pass_idx, block_idx, "pass rule should be before block rule") self.assertLess(pass_idx, reject_idx, "pass rule should be before reject rule") + + ############## + # state tracking options + # + def test_rule_create_max_src_conn(self): + """ test creation of a new rule with max_src_conn """ + obj = dict(name='one_rule', source='any', destination='any', interface='lan', protocol='tcp', max_src_conn=3) + command = "create rule 'one_rule' on 'lan', source='any', destination='any', protocol='tcp', max_src_conn=3" + self.do_module_test(obj, command=command) + + def test_rule_create_max_src_states(self): + """ test creation of a new rule with max_src_states """ + obj = dict(name='one_rule', source='any', destination='any', interface='lan', protocol='tcp', max_src_states=3) + command = "create rule 'one_rule' on 'lan', source='any', destination='any', protocol='tcp', max_src_states=3" + self.do_module_test(obj, command=command) + + def test_rule_create_max_src_nodes(self): + """ test creation of a new rule with max_src_nodes """ + obj = dict(name='one_rule', source='any', destination='any', interface='lan', protocol='tcp', max_src_nodes=100) + command = "create rule 'one_rule' on 'lan', source='any', destination='any', protocol='tcp', max_src_nodes=100" + self.do_module_test(obj, command=command) + + def test_rule_create_max_src_conn_rate(self): + """ test creation of a new rule with max_src_conn_rate """ + obj = dict(name='one_rule', source='any', destination='any', interface='lan', protocol='tcp', max_src_conn_rate=3, max_src_conn_rates=60) + command = ("create rule 'one_rule' on 'lan', source='any', destination='any', protocol='tcp', " + "max_src_conn_rate=3, max_src_conn_rates=60") + self.do_module_test(obj, command=command) + + def test_rule_create_max_src_conn_rate_without_rates(self): + """ test creation of a new rule with max_src_conn_rate but without max_src_conn_rates """ + obj = dict(name='one_rule', source='any', destination='any', interface='lan', protocol='tcp', max_src_conn_rate=3) + msg = "max_src_conn_rate and max_src_conn_rates must both be set" + self.do_module_test(obj, failed=True, msg=msg) + + def test_rule_create_statetimeout(self): + """ test creation of a new rule with statetimeout """ + obj = dict(name='one_rule', source='any', destination='any', interface='lan', protocol='tcp', statetimeout=60) + command = "create rule 'one_rule' on 'lan', source='any', destination='any', protocol='tcp', statetimeout=60" + self.do_module_test(obj, command=command) + + def test_rule_create_state_tracking_block(self): + """ test creation of a block rule with state tracking options fails """ + obj = dict(name='one_rule', source='any', destination='any', interface='lan', action='block', protocol='tcp', max_src_conn=3) + msg = "State tracking options (max_src_conn, max_src_states, etc.) can only be used on pass rules" + self.do_module_test(obj, failed=True, msg=msg) + + def test_rule_create_all_state_tracking(self): + """ test creation of a new rule with all state tracking options """ + obj = dict(name='one_rule', source='any', destination='any', interface='lan', protocol='tcp', + max_src_conn=3, max_src_states=3, max_src_nodes=100, max_src_conn_rate=3, max_src_conn_rates=60, statetimeout=60) + command = ("create rule 'one_rule' on 'lan', source='any', destination='any', protocol='tcp', " + "max_src_conn=3, max_src_states=3, max_src_nodes=100, max_src_conn_rate=3, max_src_conn_rates=60, statetimeout=60") + self.do_module_test(obj, command=command)