Skip to content

Commit 34e7640

Browse files
committed
add opnsense snat & dnat
1 parent 5f8921f commit 34e7640

File tree

2 files changed

+166
-94
lines changed

2 files changed

+166
-94
lines changed

src/firewall_test/plugins/translate/opnsense/ruleset.py

Lines changed: 165 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,25 @@
1111
from oxl_utils.ps import process_list_in_threads
1212

1313
from config import ProtoL3IP4IP6, DNS_RESOLVE_TIMEOUT, DNS_RESOLVE_THREADS, IPLIST_DOWNLOAD_TIMEOUT, \
14-
IPLIST_COMMENT_CHARS, ProtoL4ICMP, ProtoL4TCP, ProtoL4UDP, ProtoL3IP4, ProtoL3IP6, BOGONS
14+
IPLIST_COMMENT_CHARS, ProtoL4ICMP, ProtoL4TCP, ProtoL4UDP, ProtoL3IP4, ProtoL3IP6, BOGONS, \
15+
RuleActionSNAT, RuleActionDNAT
1516
from plugins.system.system_opnsense import SystemOPNsense
1617
from plugins.translate.abstract import Ruleset, TranslatePluginRuleset, Table, Chain, Rule, \
1718
RuleActionAccept, RuleActionDrop, RuleActionGoTo
1819
from plugins.translate.opnsense.rule import OPNsenseRule, RULE_SEQUENCE_NEXT_CHAIN
19-
from utils.logger import log_warn
20+
from utils.logger import log_warn, log_info
2021

2122
XML_ELEMENT_NIS = 'interfaces'
2223
XML_ELEMENT_NI_GROUPS = 'ifgroups'
2324
XML_ELEMENT_VIPS = 'virtualip'
24-
XML_ELEMENT_RULESET_OLD = 'filter'
25-
XML_ELEMENT_RULESET_NEW = 'OPNsense/Firewall/Filter/rules'
25+
XML_ELEMENT_RULESET_OLD = 'filter' # Firewall - Rules
26+
XML_ELEMENT_RULESET_NEW = 'OPNsense/Firewall/Filter/rules' # Firewall - Automation - Filter
2627
XML_ELEMENT_ALIASES = 'OPNsense/Firewall/Alias/aliases'
2728
XML_ELEMENT_GEOIP_URL = 'OPNsense/Firewall/Alias/geoip'
28-
XML_ELEMENT_NAT_SNAT_OLD = 'nat'
29-
XML_ELEMENT_NAT_SNAT_NEW = 'OPNsense/Firewall/Filter/snatrules'
30-
XML_ELEMENT_NAT_DNAT = 'nat/outbound'
29+
XML_ELEMENT_NAT_DNAT_OLD = 'nat' # Firewall - NAT - Port Forward
30+
XML_ELEMENT_NAT_SNAT_NEW = 'OPNsense/Firewall/Filter/snatrules' # Firewall - Automation - Source NAT
31+
XML_ELEMENT_NAT_SNAT_OLD = 'nat/outbound' # Firewall - NAT - Outbound
32+
XML_ELEMENT_NAT_SNAT_MODE = 'nat/outbound/mode'
3133
XML_ELEMENT_NAT_O2O_NEW = 'OPNsense/Firewall/Filter/onetoone'
3234
XML_ELEMENT_NAT_NPT_NEW = 'OPNsense/Firewall/Filter/npt'
3335

@@ -141,6 +143,8 @@ def get(self) -> Ruleset:
141143
self.aliases['(self)'] = self.local_ips
142144
self.aliases['bogons'] = BOGONS
143145

146+
self._parse_snat_old()
147+
self._parse_dnat_old()
144148
self._parse_rules_old()
145149
self.chain_floating.rules.append(Rule(
146150
action=RuleActionGoTo,
@@ -256,21 +260,121 @@ def log_unsupported_rule(chain: Chain, rule_raw: dict, rule: dict, result: (any,
256260
desc = '' if rule['desc'] is None else f" ({rule['desc']})"
257261
log_warn(
258262
'Firewall Plugin',
259-
f'Unsupported rule: Chain {chain.name}, Rule {rule["seq"]}{desc}',
263+
f'Unsupported rule: Chain {chain.name}, Rule {rule_raw["seq"]}{desc}',
260264
v4=f' | {rule_raw}'
261265
)
262266
return True
263267

264268
return False
265269

266-
# pylint: disable=R0912,R0914,R0915
270+
# pylint: disable=R0912
271+
def _build_rule_old(self, rule: dict, chain: Chain, nr: int, nis: list = None) -> tuple[bool, dict]:
272+
if nis is None:
273+
nis = split_csv(rule.get('interface', ''))
274+
275+
if 'rule' in rule:
276+
# edge-case: snat has rule nested sometimes?
277+
rule = rule['rule']
278+
279+
invalid_matches = False
280+
build = {
281+
'uuid': rule.get('uuid', None),
282+
'desc': rule.get('descr', None),
283+
'nr': nr,
284+
285+
### RESOLVE NETWORK-INTERFACE GROUPS ###
286+
'nis': [],
287+
'ni_direction': rule.get('direction', 'any'),
288+
}
289+
for ni in nis:
290+
if ni in self.ni_grp:
291+
build['nis'].extend(self.ni_grp[ni])
292+
293+
else:
294+
build['nis'].append(ni)
295+
296+
### SIMPLE VALUE-MAPPING ###
297+
for field, mapping in RULE_MAPPING.items():
298+
if field not in rule:
299+
build[field] = mapping.get('_default', None)
300+
continue
301+
302+
if rule[field] not in mapping:
303+
log_warn(
304+
'Firewall Plugin',
305+
f'Unable to parse rule-field value: "{field}" => "{rule[field]}"',
306+
)
307+
build[field] = None
308+
invalid_matches = True
309+
310+
else:
311+
build[field] = mapping[rule[field]]
312+
313+
### SRC / DST ###
314+
for key in ['source', 'destination']:
315+
value = rule.get(key, {})
316+
build[f'{key}_invert'] = 'not' in value and value['not'] == '1'
317+
build[f'{key}_any'] = 'any' in value and value['any'] == '1'
318+
319+
if 'address' in value:
320+
build[key] = self._parse_rule_address(value['address'])
321+
invalid_matches = self.log_unsupported_rule(
322+
chain=chain, rule_raw=rule, rule=build, result=build[key], invalid=invalid_matches
323+
)
324+
325+
elif 'network' in value:
326+
build[key] = self._parse_rule_network(value['network'])
327+
invalid_matches = self.log_unsupported_rule(
328+
chain=chain, rule_raw=rule, rule=build, result=build[key], invalid=invalid_matches
329+
)
330+
331+
else:
332+
build[key] = None
333+
334+
if 'port' in value:
335+
build[f'{key}_port'] = self._parse_rule_port(value['port'])
336+
invalid_matches = self.log_unsupported_rule(
337+
chain=chain, rule_raw=rule, rule=build, result=build[f'{key}_port'], invalid=invalid_matches
338+
)
339+
340+
# weird DNAT overrides..
341+
if 'targetip' in rule:
342+
build['destination'] = self._parse_rule_address(rule['targetip'])
343+
invalid_matches = self.log_unsupported_rule(
344+
chain=chain, rule_raw=rule, rule=build, result=build['destination'], invalid=invalid_matches
345+
)
346+
347+
elif 'target' in rule:
348+
build['destination'] = self._parse_rule_address(rule['target'])
349+
invalid_matches = self.log_unsupported_rule(
350+
chain=chain, rule_raw=rule, rule=build, result=build['destination'], invalid=invalid_matches
351+
)
352+
353+
if 'local-port' in rule:
354+
build['destination_port'] = self._parse_rule_port(rule['local-port'])
355+
invalid_matches = self.log_unsupported_rule(
356+
chain=chain, rule_raw=rule, rule=build, result=build['destination_port'], invalid=invalid_matches
357+
)
358+
359+
elif 'dstport' in rule:
360+
build['destination_port'] = self._parse_rule_port(rule['dstport'])
361+
invalid_matches = self.log_unsupported_rule(
362+
chain=chain, rule_raw=rule, rule=build, result=build['destination_port'], invalid=invalid_matches
363+
)
364+
365+
if 'sourceport' in rule:
366+
build['source_port'] = self._parse_rule_port(rule['sourceport'])
367+
invalid_matches = self.log_unsupported_rule(
368+
chain=chain, rule_raw=rule, rule=build, result=build['source_port'], invalid=invalid_matches
369+
)
370+
371+
return invalid_matches, build
372+
267373
def _parse_rules_old(self):
268374
rules_raw = self.raw.getroot().find(XML_ELEMENT_RULESET_OLD)
269375
seq_float, seq_grp, seq_ni, nr = 0, 0, 0, 0
270376
for rule_raw in rules_raw:
271-
invalid_matches = False
272377
rule = xml_to_dict(rule_raw)
273-
build = {}
274378
chain_floating = rule.get('floating', None) == 'yes'
275379
nis = split_csv(rule.get('interface', ''))
276380
chain_grp = len(nis) == 1 and nis[0] in self.ni_grp
@@ -283,106 +387,73 @@ def _parse_rules_old(self):
283387
else:
284388
chain = self.chain_ni
285389

286-
build['desc'] = rule.get('descr', None)
287-
build['uuid'] = rule.get('uuid', None)
288-
if build['uuid'] is None:
289-
continue
290-
291390
### SEQUENCE ###
292391
nr += 1
293-
build['nr'] = nr
294392
if chain_floating:
295393
seq_float += 1
296-
build['seq'] = seq_float
394+
seq = seq_float
297395

298396
elif chain_grp:
299397
seq_grp += 1
300-
build['seq'] = seq_grp
398+
seq = seq_grp
301399

302400
else:
303401
seq_ni += 1
304-
build['seq'] = seq_ni
305-
306-
### RESOLVE NETWORK-INTERFACE GROUPS ###
307-
build['nis'] = []
308-
build['ni_direction'] = rule.get('direction', 'any')
309-
for ni in nis:
310-
if ni in self.ni_grp:
311-
build['nis'].extend(self.ni_grp[ni])
312-
313-
else:
314-
build['nis'].append(ni)
315-
316-
### SIMPLE VALUE-MAPPING ###
317-
for field, mapping in RULE_MAPPING.items():
318-
if field not in rule:
319-
build[field] = mapping.get('_default', None)
320-
continue
321-
322-
if rule[field] not in mapping:
323-
log_warn(
324-
'Firewall Plugin',
325-
f'Unable to parse rule-field value: "{field}" => "{rule[field]}"',
326-
)
327-
build[field] = None
328-
invalid_matches = True
329-
330-
else:
331-
build[field] = mapping[rule[field]]
332-
333-
### SRC / DST ###
334-
for key in ['source', 'destination']:
335-
value = rule.get(key, {})
336-
build[f'{key}_invert'] = 'not' in value and value['not'] == '1'
337-
build[f'{key}_any'] = 'any' in value and value['any'] == '1'
338-
339-
if 'address' in value:
340-
build[key] = self._parse_rule_address(value['address'])
341-
invalid_matches = self.log_unsupported_rule(
342-
chain=chain, rule_raw=rule, rule=build, result=build[key], invalid=invalid_matches
343-
)
344-
345-
elif 'network' in value:
346-
build[key] = self._parse_rule_network(value['network'])
347-
invalid_matches = self.log_unsupported_rule(
348-
chain=chain, rule_raw=rule, rule=build, result=build[key], invalid=invalid_matches
349-
)
350-
351-
else:
352-
build[key] = None
353-
354-
if 'port' in value:
355-
build[f'{key}_port'] = self._parse_rule_port(value['port'])
356-
invalid_matches = self.log_unsupported_rule(
357-
chain=chain, rule_raw=rule, rule=build, result=build[f'{key}_port'], invalid=invalid_matches
358-
)
359-
360-
# weird DNAT overrides..
361-
if 'targetip' in rule:
362-
build['destination'] = self._parse_rule_address(rule['targetip'])
363-
invalid_matches = self.log_unsupported_rule(
364-
chain=chain, rule_raw=rule, rule=build, result=build['destination'], invalid=invalid_matches
365-
)
402+
seq = seq_ni
366403

367-
elif 'target' in rule:
368-
build['destination'] = self._parse_rule_address(rule['target'])
369-
invalid_matches = self.log_unsupported_rule(
370-
chain=chain, rule_raw=rule, rule=build, result=build['destination'], invalid=invalid_matches
371-
)
372-
373-
if 'local-port' in rule:
374-
build['destination_port'] = self._parse_rule_port(rule['local-port'])
375-
invalid_matches = self.log_unsupported_rule(
376-
chain=chain, rule_raw=rule, rule=build, result=build['destination_port'], invalid=invalid_matches
377-
)
404+
rule['seq'] = seq
405+
invalid_matches, build = self._build_rule_old(rule=rule, chain=chain, nr=nr, nis=nis)
406+
if build['uuid'] is None:
407+
continue
378408

379409
if invalid_matches:
380410
continue
381411

382412
### CREATE RULE ###
383413
chain.rules.append(Rule(
384414
action=build.pop('type'),
385-
seq=build.pop('seq'),
415+
seq=seq,
416+
action_lazy=not build.pop('quick'),
417+
raw=OPNsenseRule(**build),
418+
))
419+
420+
def _parse_snat_old(self):
421+
snat_mode = self.raw.getroot().find(XML_ELEMENT_NAT_SNAT_MODE).text
422+
log_info('Firewall Plugin', f'SNAT MODE: "{snat_mode}"')
423+
424+
rules_raw = self.raw.getroot().find(XML_ELEMENT_NAT_SNAT_OLD)
425+
for seq, rule_raw in enumerate(rules_raw):
426+
rule = xml_to_dict(rule_raw)
427+
rule['uuid'] = '' # has no uuid (?)
428+
rule['seq'] = seq
429+
invalid_matches, build = self._build_rule_old(rule=rule, chain=self.chain_snat, nr=seq)
430+
if invalid_matches:
431+
continue
432+
433+
build.pop('type')
434+
435+
self.chain_snat.rules.append(Rule(
436+
action=RuleActionSNAT,
437+
seq=seq,
438+
action_lazy=not build.pop('quick'),
439+
raw=OPNsenseRule(**build),
440+
))
441+
442+
def _parse_dnat_old(self):
443+
rules_raw = self.raw.getroot().find(XML_ELEMENT_NAT_DNAT_OLD)
444+
for seq, rule_raw in enumerate(rules_raw):
445+
rule = xml_to_dict(rule_raw)
446+
rule['uuid'] = '' # has no uuid (?)
447+
rule['seq'] = seq
448+
invalid_matches, build = self._build_rule_old(rule=rule, chain=self.chain_dnat, nr=seq)
449+
if invalid_matches:
450+
continue
451+
452+
build.pop('type')
453+
454+
self.chain_dnat.rules.append(Rule(
455+
action=RuleActionDNAT,
456+
seq=seq,
386457
action_lazy=not build.pop('quick'),
387458
raw=OPNsenseRule(**build),
388459
))
@@ -534,6 +605,7 @@ def _download_alias_iplist(url: str) -> (HttpResponse, None):
534605
log_warn('Firewall Plugin', f'Unable to download IP-List from URL: "{url}"')
535606
return None
536607

608+
# pylint: disable=R0914,R0915
537609
def _parse_aliases(self):
538610
aliases_raw = self.raw.getroot().find(XML_ELEMENT_ALIASES)
539611
aliases = {}

src/firewall_test/simulator/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def __init__(self, packet: PACKET_KINDS, simulator):
6161
_, self.dnat = self._s.fw.process_dnat(packet=packet, flow=self.flow_type)
6262
self._dnat_done = True
6363
if self.dnat is not None:
64-
log_info(label='Firewall', v1=f'Performed DNAT: {self.packet.dnat_str}')
64+
log_info(label='Firewall', v1=f'Performed DNAT: {self.packet.dnat_str()}')
6565

6666
### UPDATE TRAFFIC FLOW AND OUTBOUND-NETWORK-INTERFACE ###
6767

0 commit comments

Comments
 (0)