Skip to content

Commit dfcf76b

Browse files
committed
plugin opnsense: add basic rule-matching logic
1 parent fad0d56 commit dfcf76b

File tree

7 files changed

+222
-116
lines changed

7 files changed

+222
-116
lines changed

docs/source/plugins/firewall_netfilter.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,4 @@ These rule-expressions are unsupported for now:
9999
* :code:`ct helper`
100100
* :code:`&`
101101
* TCP flags
102+
* ICMP Types (codes are supported)

docs/source/plugins/firewall_opnsense.rst

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ Here is an example on how to run it with the exported config:
3838
--file-ruleset 'config.xml' \
3939
--file-interfaces 'network.json' \
4040
--file-routes 'network.json' \
41-
--src-ip 172.17.11.5 \
41+
--src-ip 10.57.65.44 \
4242
--dst-ip 1.1.1.1
4343
4444
----
@@ -52,3 +52,46 @@ Source Code
5252

5353
* **Traffic Matching**: `system/firewall_opnsense.py <https://github.com/O-X-L/firewall-testing-framework/blob/latest/src/firewall_test/plugins/system/firewall_opnsense.py>`_
5454

55+
----
56+
57+
Config-Parsing
58+
##############
59+
60+
The current implementation focused on supporting the most widely used rule matches like:
61+
62+
* Layer 3 Protocol (IPv4/IPv6)
63+
* Layer 4 Protocol (tcp/udp/icmp)
64+
* Source- and Destination-IP filters
65+
* Source- and Destination-Port filters
66+
* Source- and Destination-NAT (including masquerade)
67+
* Inbound- and Outbound-Network-Interfaces
68+
* Aliases (including IPs/networks, ports, port-ranges, nested aliases, interface-groups, interface-IPs and -networks, :code:`This Firewall`, bogons, DNS-resolved hosts, URLTable/IPLists, ...)
69+
* Quick/Lazy actions
70+
71+
The main match-parsing logic can be found here: `translate/opnsense/ruleset.py <https://github.com/O-X-L/firewall-testing-framework/tree/latest/src/firewall_test/plugins/translate/opnsense/ruleset.py>`_ & `translate/opnsense/rule.py <https://github.com/O-X-L/firewall-testing-framework/tree/latest/src/firewall_test/plugins/translate/opnsense/rule.py>`_
72+
73+
If we were not able to parse any match from the rule-config - the rule will be skipped.
74+
75+
If this happens you will see a warning at runtime! (:code:`Unsupported rule`)
76+
77+
.. warning::
78+
79+
The plugin does not validate the syntax of the config-export you provide!
80+
81+
You may encounter unexpected errors when manually modifying it!
82+
83+
Unsupported Expressions
84+
=======================
85+
86+
Rules containing unsupported expressions will be skipped for now.
87+
88+
If this happens you will see a warning at runtime! (:code:`Unsupported rule-expression`)
89+
90+
These rule-expressions are unsupported for now:
91+
92+
* :code:`GeoIP aliases`
93+
* :code:`URLTable JSON`
94+
* :code:`ICMP Types`
95+
* :code:`tagging`
96+
* TCP flags
97+
* Connection states

src/firewall_test/config.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ class ProtoL3IP4IP6(ProtoL3):
7474
N = 'ip'
7575

7676

77-
PROTOS_L3 = [ProtoL3IP4, ProtoL3IP6, ProtoL3IP4IP6]
77+
PROTOS_L3 = (ProtoL3IP4, ProtoL3IP6, ProtoL3IP4IP6)
7878
PROTO_L3_MAPPING = {
7979
ProtoL3IP4.N: ProtoL3IP4,
8080
ProtoL3IP6.N: ProtoL3IP6,
@@ -97,7 +97,7 @@ class ProtoL4ICMP(ProtoL4):
9797
N = 'icmp'
9898

9999

100-
PROTOS_L4 = [ProtoL4TCP, ProtoL4UDP, ProtoL4ICMP]
100+
PROTOS_L4 = (ProtoL4TCP, ProtoL4UDP, ProtoL4ICMP)
101101
PROTO_L4_MAPPING = {
102102
ProtoL4TCP.N: ProtoL4TCP,
103103
ProtoL4UDP.N: ProtoL4UDP,
@@ -190,11 +190,11 @@ class RuleActionSNAT(RuleActionKindNAT):
190190
N = 'snat'
191191

192192

193-
RULE_ACTIONS = [
193+
RULE_ACTIONS = (
194194
RuleActionAccept, RuleActionDrop, RuleActionReject,
195195
RuleActionJump, RuleActionGoTo, RuleActionContinue, RuleActionReturn,
196196
RuleActionDNAT, RuleActionSNAT,
197-
]
197+
)
198198
RULE_ACTION_MAPPING = {
199199
RuleActionAccept.N: RuleActionAccept,
200200
RuleActionDrop.N: RuleActionDrop,
Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,102 @@
1-
from simulator.packet import PacketIP
1+
from config import ProtoL3IP4IP6
2+
from simulator.packet import PACKET_KINDS, PacketTCPUDP
23
from plugins.translate.abstract import Rule
34
from plugins.system.abstract_rule_match import RuleMatcher, RuleMatchResult
45
from plugins.translate.opnsense.rule import OPNsenseRule
6+
from utils.logger import log_warn, log_debug
57

8+
# todo: add explicit match-tests
69

10+
11+
# pylint: disable=R0912
712
class RuleMatcherOPNsense(RuleMatcher):
8-
def matches(self, packet: PacketIP, rule: Rule) -> RuleMatchResult:
13+
def matches(self, packet: PACKET_KINDS, rule: Rule) -> RuleMatchResult:
914
"""
1015
:param packet: Packet to match
1116
:param rule: Rule to check
1217
:return: RuleMatchResult
1318
"""
1419
opn_rule: OPNsenseRule = rule.raw
15-
del opn_rule
20+
21+
results = []
22+
### NETWORK INTERFACES ###
23+
if opn_rule.match_ni_in:
24+
results.append(packet.ni_in in opn_rule.nis)
25+
26+
if opn_rule.match_ni_out:
27+
results.append(packet.ni_out in opn_rule.nis)
28+
29+
### PROTOCOLS ###
30+
if opn_rule.ipp is not None:
31+
if opn_rule.ipp == ProtoL3IP4IP6:
32+
results.append(True)
33+
34+
else:
35+
results.append(opn_rule.ipp == packet.proto_l3)
36+
37+
if opn_rule.proto is not None:
38+
if not hasattr(packet, 'proto_l4'):
39+
results.append(False)
40+
41+
else:
42+
results.append(packet.proto_l4 in opn_rule.proto)
43+
44+
### SOURCE / DESTINATION ###
45+
if opn_rule.match_ip_saddr:
46+
if opn_rule.src_any:
47+
match = True
48+
49+
else:
50+
match = any(packet.src in net for net in opn_rule.src)
51+
52+
if opn_rule.src_invert:
53+
results.append(not match)
54+
55+
else:
56+
results.append(match)
57+
58+
if opn_rule.match_ip_daddr:
59+
if opn_rule.dst_any:
60+
match = True
61+
62+
else:
63+
match = any(packet.dst in net for net in opn_rule.dst)
64+
65+
if opn_rule.dst_invert:
66+
results.append(not match)
67+
68+
else:
69+
results.append(match)
70+
71+
### PORTS ###
72+
if opn_rule.src_port is not None and len(opn_rule.src_port) > 0:
73+
if not isinstance(packet, PacketTCPUDP):
74+
results.append(False)
75+
76+
else:
77+
results.append(packet.sport in opn_rule.src_port)
78+
79+
if opn_rule.dst_port is not None and len(opn_rule.dst_port) > 0:
80+
if not isinstance(packet, PacketTCPUDP):
81+
results.append(False)
82+
83+
else:
84+
results.append(packet.dport in opn_rule.dst_port)
85+
86+
if len(results) == 0:
87+
log_warn('Firewall Plugin', ' > Matches: Found not matches we could process - skipping rule')
88+
89+
else:
90+
log_debug('Firewall Plugin', f' > Match Results: {opn_rule.get_matches()} => {results}')
91+
92+
return RuleMatchResult(
93+
matched=all(results),
94+
action=rule.action,
95+
target_chain_name=None,
96+
target_nat_ip=None,
97+
target_nat_port=None,
98+
)
99+
100+
# todo: SNAT / DNAT
101+
16102
return RuleMatchResult(False, None, None, None, None)

src/firewall_test/plugins/translate/abstract.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,9 @@ def dump(self) -> dict:
266266
'raw': self.raw,
267267
}
268268

269+
def log(self) -> str:
270+
return f'Seq {self.seq}, Action: {self.action.N}, {self.raw}'
271+
269272
def validate(self):
270273
r = self.dump()
271274
if self.action is not None:

0 commit comments

Comments
 (0)