1111from oxl_utils .ps import process_list_in_threads
1212
1313from 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
1516from plugins .system .system_opnsense import SystemOPNsense
1617from plugins .translate .abstract import Ruleset , TranslatePluginRuleset , Table , Chain , Rule , \
1718 RuleActionAccept , RuleActionDrop , RuleActionGoTo
1819from 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
2122XML_ELEMENT_NIS = 'interfaces'
2223XML_ELEMENT_NI_GROUPS = 'ifgroups'
2324XML_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
2627XML_ELEMENT_ALIASES = 'OPNsense/Firewall/Alias/aliases'
2728XML_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'
3133XML_ELEMENT_NAT_O2O_NEW = 'OPNsense/Firewall/Filter/onetoone'
3234XML_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 = {}
0 commit comments