Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 42 additions & 3 deletions plugins/module_utils/rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand All @@ -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'
]


Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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']
Expand Down Expand Up @@ -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 """
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand Down
42 changes: 42 additions & 0 deletions plugins/modules/pfsense_rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = """
Expand Down Expand Up @@ -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 = """
Expand Down
54 changes: 54 additions & 0 deletions tests/unit/plugins/modules/test_pfsense_rule_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)