From a89c7e9ad24de070450909427490ed6f6e232fcf Mon Sep 17 00:00:00 2001 From: Oliver Kraitschy Date: Tue, 7 Aug 2018 14:46:44 +0200 Subject: [PATCH 1/8] [openwrt] Add firewall settings --- .../backends/openwrt/converters/__init__.py | 3 +- .../backends/openwrt/converters/firewall.py | 89 +++++ netjsonconfig/backends/openwrt/openwrt.py | 1 + netjsonconfig/backends/openwrt/schema.py | 331 ++++++++++++++++++ tests/openwrt/test_default.py | 96 ++--- 5 files changed, 473 insertions(+), 47 deletions(-) create mode 100644 netjsonconfig/backends/openwrt/converters/firewall.py diff --git a/netjsonconfig/backends/openwrt/converters/__init__.py b/netjsonconfig/backends/openwrt/converters/__init__.py index 989eab33a..8826fd32c 100644 --- a/netjsonconfig/backends/openwrt/converters/__init__.py +++ b/netjsonconfig/backends/openwrt/converters/__init__.py @@ -9,8 +9,9 @@ from .rules import Rules from .switch import Switch from .wireless import Wireless +from .firewall import Firewall __all__ = ['Default', 'Interfaces', 'General', 'Led', 'Ntp', 'OpenVpn', 'Radios', 'Routes', 'Rules', 'Switch', - 'Wireless'] + 'Wireless', 'Firewall'] diff --git a/netjsonconfig/backends/openwrt/converters/firewall.py b/netjsonconfig/backends/openwrt/converters/firewall.py new file mode 100644 index 000000000..f474b3fe4 --- /dev/null +++ b/netjsonconfig/backends/openwrt/converters/firewall.py @@ -0,0 +1,89 @@ +from collections import OrderedDict + +from ..schema import schema +from .base import OpenWrtConverter + + +class Firewall(OpenWrtConverter): + netjson_key = 'firewall' + intermediate_key = 'firewall' + _uci_types = ['defaults', 'forwarding', 'zone', 'rule'] + _schema = schema['properties']['firewall'] + + def to_intermediate_loop(self, block, result, index=None): + forwardings = self.__intermediate_forwardings(block.pop('forwardings', {})) + zones = self.__intermediate_zones(block.pop('zones', {})) + rules = self.__intermediate_rules(block.pop('rules', {})) + block.update({ + '.type': 'defaults', + '.name': block.pop('id', 'defaults'), + }) + result.setdefault('firewall', []) + result['firewall'] = [self.sorted_dict(block)] + forwardings + zones + rules + return result + + def __intermediate_forwardings(self, forwardings): + """ + converts NetJSON forwarding to + UCI intermediate data structure + """ + result = [] + for forwarding in forwardings: + resultdict = OrderedDict((('.name', self.__get_auto_name_forwarding(forwarding)), + ('.type', 'forwarding'))) + resultdict.update(forwarding) + result.append(resultdict) + return result + + def __get_auto_name_forwarding(self, forwarding): + if 'family' in forwarding.keys(): + uci_name = self._get_uci_name('_'.join([forwarding['src'], forwarding['dest'], + forwarding['family']])) + else: + uci_name = self._get_uci_name('_'.join([forwarding['src'], forwarding['dest']])) + return 'forwarding_{0}'.format(uci_name) + + def __intermediate_zones(self, zones): + """ + converts NetJSON zone to + UCI intermediate data structure + """ + result = [] + for zone in zones: + resultdict = OrderedDict((('.name', self.__get_auto_name_zone(zone)), + ('.type', 'zone'))) + resultdict.update(zone) + result.append(resultdict) + return result + + def __get_auto_name_zone(self, zone): + return 'zone_{0}'.format(self._get_uci_name(zone['name'])) + + def __intermediate_rules(self, rules): + """ + converts NetJSON rule to + UCI intermediate data structure + """ + result = [] + for rule in rules: + if 'config_name' in rule: + del rule['config_name'] + resultdict = OrderedDict((('.name', self.__get_auto_name_rule(rule)), + ('.type', 'rule'))) + resultdict.update(rule) + result.append(resultdict) + return result + + def __get_auto_name_rule(self, rule): + return 'rule_{0}'.format(self._get_uci_name(rule['name'])) + + def to_netjson_loop(self, block, result, index): + result['firewall'] = self.__netjson_firewall(block) + return result + + def __netjson_firewall(self, firewall): + del firewall['.type'] + _name = firewall.pop('.name') + if _name != 'firewall': + firewall['id'] = _name + return self.type_cast(firewall) diff --git a/netjsonconfig/backends/openwrt/openwrt.py b/netjsonconfig/backends/openwrt/openwrt.py index 0d18d91ff..235252212 100644 --- a/netjsonconfig/backends/openwrt/openwrt.py +++ b/netjsonconfig/backends/openwrt/openwrt.py @@ -21,6 +21,7 @@ class OpenWrt(BaseBackend): converters.Radios, converters.Wireless, converters.OpenVpn, + converters.Firewall, converters.Default, ] parser = OpenWrtParser diff --git a/netjsonconfig/backends/openwrt/schema.py b/netjsonconfig/backends/openwrt/schema.py index 7aeaadcb9..edc69d5bd 100644 --- a/netjsonconfig/backends/openwrt/schema.py +++ b/netjsonconfig/backends/openwrt/schema.py @@ -166,6 +166,33 @@ "radio_80211ac_5ghz_settings": { "allOf": [{"$ref": "#/definitions/radio_hwmode_11a"}] }, + "firewall_policy": { + "type": "string", + "enum": ["ACCEPT", "REJECT", "DROP"], + "options": { + "enum_titles": [ + "Accept", "Reject", "Drop"] + }, + "default": "REJECT" + }, + "zone_policy": { + "type": "string", + "enum": ["ACCEPT", "REJECT", "DROP"], + "options": { + "enum_titles": [ + "Accept", "Reject", "Drop"] + }, + "default": "DROP" + }, + "rule_policy": { + "type": "string", + "enum": ["ACCEPT", "REJECT", "DROP", "MARK", "NOTRACK"], + "options": { + "enum_titles": [ + "Accept", "Reject", "Drop", "Mark", "Notrack"] + }, + "default": "DROP" + }, }, "properties": { "general": { @@ -454,6 +481,310 @@ } } } + }, + "firewall": { + "type": "object", + "title": "Firewall", + "additionalProperties": True, + "propertyOrder": 11, + "properties": { + "syn_flood": { + "type": "boolean", + "title": "enable SYN flood protection", + "default": False, + "format": "checkbox", + "propertyOrder": 1, + }, + "input": { + "allOf": [ + {"$ref": "#/definitions/firewall_policy"}, + { + "title": "input", + "description": "policy for the INPUT chain of the filter table", + "propertyOrder": 2, + } + ] + }, + "output": { + "allOf": [ + {"$ref": "#/definitions/firewall_policy"}, + { + "title": "output", + "description": "policy for the OUTPUT chain of the filter table", + "propertyOrder": 3, + } + ] + }, + "forward": { + "allOf": [ + {"$ref": "#/definitions/firewall_policy"}, + { + "title": "forward", + "description": "policy for the FORWARD chain of the filter table", + "propertyOrder": 4, + } + ] + }, + "forwardings": { + "type": "array", + "title": "Forwardings", + "propertyOrder": 5, + "items": { + "type": "object", + "title": "Forwarding", + "additionalProperties": False, + "required": [ + "src", + "dest", + ], + "properties": { + "src": { + "type": "string", + "title": "src", + "description": "specifies the traffic source zone and must " + "refer to one of the defined zone names", + "propertyOrder": 1, + }, + "dest": { + "type": "string", + "title": "dest", + "description": "specifies the traffic destination zone and must " + "refer to one of the defined zone names", + "propertyOrder": 2, + }, + "family": { + "type": "string", + "title": "family", + "description": "protocol family (ipv4, ipv6 or any) to generate " + "iptables rules for", + "enum": ["ipv4", "ipv6", "any"], + "default": "any", + "propertyOrder": 3 + } + } + } + }, + "zones": { + "type": "array", + "title": "Zones", + "propertyOrder": 6, + "items": { + "type": "object", + "title": "Zones", + "additionalProperties": True, + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "title": "name", + "description": "unique zone name", + "maxLength": 11, + "propertyOrder": 1 + }, + "network": { + "type": "array", + "title": "Network", + "description": "list of interfaces attached to this zone", + "uniqueItems": True, + "propertyOrder": 2, + "items": { + "title": "Network", + "type": "string", + "maxLength": 15, + "pattern": "^[a-zA-z0-9_\\.\\-]*$" + } + }, + "masq": { + "type": "boolean", + "title": "masq", + "description": "specifies wether outgoing zone traffic should be " + "masqueraded", + "default": False, + "format": "checkbox", + "propertyOrder": 3 + }, + "mtu_fix": { + "type": "boolean", + "title": "mtu_fix", + "description": "enable MSS clamping for outgoing zone traffic", + "default": False, + "format": "checkbox", + "propertyOrder": 4, + }, + "input": { + "allOf": [ + {"$ref": "#/definitions/zone_policy"}, + { + "title": "input", + "description": "default policy for incoming zone traffic", + "propertyOrder": 5, + } + ] + }, + "output": { + "allOf": [ + {"$ref": "#/definitions/zone_policy"}, + { + "title": "output", + "description": "default policy for outgoing zone traffic", + "propertyOrder": 6, + } + ] + }, + "forward": { + "allOf": [ + {"$ref": "#/definitions/zone_policy"}, + { + "title": "forward", + "description": "default policy for forwarded zone traffic", + "propertyOrder": 7, + } + ] + } + } + } + }, + "rules": { + "type": "array", + "title": "Rules", + "propertyOrder": 7, + "items": { + "type": "object", + "title": "Rules", + "additionalProperties": True, + "required": [ + "src", + "target" + ], + "properties": { + "name": { + "type": "string", + "title": "name", + "description": "name of the rule", + "propertyOrder": 1 + }, + "src": { + "type": "string", + "title": "src", + "description": "specifies the traffic source zone and must " + "refer to one of the defined zone names", + "propertyOrder": 2 + }, + "src_ip": { + "type": "string", + "title": "src_ip", + "description": "match incoming traffic from the specified " + "source ip address", + "propertyOrder": 3 + }, + "src_mac": { + "type": "string", + "title": "src_mac", + "description": "match incoming traffic from the specified " + "mac address", + "pattern": "^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$", + "minLength": 17, + "maxLength": 17, + "propertyOrder": 4 + }, + "src_port": { + "type": "string", + "title": "src_port", + "description": "match incoming traffic from the specified " + "source port or port range, if relevant proto " + "is specified. Multiple ports can be specified " + "separated by blanks", + "propertyOrder": 5 + }, + "proto": { + "type": "string", + "title": "proto", + "description": "match incoming traffic using the given protocol. " + "Can be one of tcp, udp, tcpudp, udplite, icmp, esp, " + "ah, sctp, or all or it can be a numeric value, " + "representing one of these protocols or a different one. " + "A protocol name from /etc/protocols is also allowed. " + "The number 0 is equivalent to all", + "default": "tcpudp", + "propertyOrder": 6 + }, + "icmp_type": { + "title": "icmp_type", + "description": "for protocol icmp select specific icmp types to match. " + "Values can be either exact icmp type numbers or type names", + "type": "array", + "uniqueItems": True, + "additionalItems": True, + "propertyOrder": 7, + "items": { + "title": "ICMP type", + "type": "string" + } + }, + "dest": { + "type": "string", + "title": "dest", + "description": "specifies the traffic destination zone and must " + "refer to one of the defined zone names, or * for " + "any zone. If specified, the rule applies to forwarded " + "traffic; otherwise, it is treated as input rule", + "propertyOrder": 8 + }, + "dest_ip": { + "type": "string", + "title": "dest_ip", + "description": "match incoming traffic directed to the specified " + "destination ip address. With no dest zone, this " + "is treated as an input rule", + "propertyOrder": 9 + }, + "dest_port": { + "type": "string", + "title": "dest_port", + "description": "match incoming traffic directed at the given " + "destination port or port range, if relevant " + "proto is specified. Multiple ports can be specified " + "separated by blanks", + "propertyOrder": 10 + }, + "target": { + "allOf": [ + {"$ref": "#/definitions/rule_policy"}, + { + "title": "target", + "description": "firewall action for matched traffic", + "propertyOrder": 11 + } + ] + }, + "family": { + "type": "string", + "title": "family", + "description": "protocol family to generate iptables rules for", + "enum": ["ipv4", "ipv6", "any"], + "default": "any", + "propertyOrder": 12 + }, + "limit": { + "type": "string", + "title": "limit", + "description": "maximum average matching rate; specified as a number, " + "with an optional /second, /minute, /hour or /day suffix", + "propertyOrder": 13 + }, + "enabled": { + "type": "boolean", + "title": "enable rule", + "default": True, + "format": "checkbox", + "propertyOrder": 14 + } + } + } + } + } } } }) diff --git a/tests/openwrt/test_default.py b/tests/openwrt/test_default.py index fddffe322..c5d4d4fce 100644 --- a/tests/openwrt/test_default.py +++ b/tests/openwrt/test_default.py @@ -20,42 +20,44 @@ def test_render_default(self): "boolean": True } ], - "firewall": [ - { - "config_name": "rule", - "name": "Allow-MLD", - "src": "wan", - "proto": "icmp", - "src_ip": "fe80::/10", - "family": "ipv6", - "target": "ACCEPT", - "icmp_type": [ - "130/0", - "131/0", - "132/0", - "143/0" - ] - }, - { - "config_name": "rule", - "name": "Rule2", - "src": "wan", - "proto": "icmp", - "src_ip": "192.168.1.1/24", - "family": "ipv4", - "target": "ACCEPT", - "icmp_type": [ - "130/0", - "131/0", - "132/0", - "143/0" - ] - } - ] + "firewall": { + "rules": [ + { + "config_name": "rule", + "name": "Allow-MLD", + "src": "wan", + "proto": "icmp", + "src_ip": "fe80::/10", + "family": "ipv6", + "target": "ACCEPT", + "icmp_type": [ + "130/0", + "131/0", + "132/0", + "143/0" + ] + }, + { + "config_name": "rule", + "name": "Rule2", + "src": "wan", + "proto": "icmp", + "src_ip": "192.168.1.1/24", + "family": "ipv4", + "target": "ACCEPT", + "icmp_type": [ + "130/0", + "131/0", + "132/0", + "143/0" + ] + } + ] + } }) expected = self._tabs("""package firewall -config rule 'rule_1' +config rule 'rule_Allow_MLD' option family 'ipv6' list icmp_type '130/0' list icmp_type '131/0' @@ -67,7 +69,7 @@ def test_render_default(self): option src_ip 'fe80::/10' option target 'ACCEPT' -config rule 'rule_2' +config rule 'rule_Rule2' option family 'ipv4' list icmp_type '130/0' list icmp_type '131/0' @@ -148,18 +150,20 @@ def test_parse_default(self): "boolean": "1" } ], - "firewall": [ - { - "config_name": "rule", - "name": "Allow-MLD", - "src": "wan", - "proto": "icmp", - "src_ip": "fe80::/10", - "family": "ipv6", - "target": "ACCEPT", - "icmp_type": ["130/0", "131/0", "132/0", "143/0"] - } - ], + "firewall": { + "rules": [ + { + "config_name": "rule", + "name": "Allow-MLD", + "src": "wan", + "proto": "icmp", + "src_ip": "fe80::/10", + "family": "ipv6", + "target": "ACCEPT", + "icmp_type": ["130/0", "131/0", "132/0", "143/0"] + } + ] + }, "led": [ { "name": "USB1", From e49605eb4c393a9008a4f567b96e24d737a393cc Mon Sep 17 00:00:00 2001 From: "Jonathan G. Underwood" Date: Fri, 24 Jul 2020 23:58:05 +0100 Subject: [PATCH 2/8] Format /backends/openwrt/converters/__init__.py --- .../backends/openwrt/converters/__init__.py | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/netjsonconfig/backends/openwrt/converters/__init__.py b/netjsonconfig/backends/openwrt/converters/__init__.py index 68135bc6a..2864d12c9 100644 --- a/netjsonconfig/backends/openwrt/converters/__init__.py +++ b/netjsonconfig/backends/openwrt/converters/__init__.py @@ -1,4 +1,5 @@ from .default import Default +from .firewall import Firewall from .general import General from .interfaces import Interfaces from .led import Led @@ -9,9 +10,18 @@ from .rules import Rules from .switch import Switch from .wireless import Wireless -from .firewall import Firewall -__all__ = ['Default', 'Interfaces', 'General', - 'Led', 'Ntp', 'OpenVpn', 'Radios', - 'Routes', 'Rules', 'Switch', - 'Wireless', 'Firewall'] +__all__ = [ + "Default", + "Interfaces", + "General", + "Led", + "Ntp", + "OpenVpn", + "Radios", + "Routes", + "Rules", + "Switch", + "Wireless", + "Firewall", +] From 5cfddccef100e62c459d06db007ac43525a9cf9e Mon Sep 17 00:00:00 2001 From: "Jonathan G. Underwood" Date: Tue, 28 Jul 2020 11:12:43 +0100 Subject: [PATCH 3/8] Reformat openwrt/schema.py (openwisp-qa-format) --- netjsonconfig/backends/openwrt/schema.py | 170 ++++++++++------------- 1 file changed, 76 insertions(+), 94 deletions(-) diff --git a/netjsonconfig/backends/openwrt/schema.py b/netjsonconfig/backends/openwrt/schema.py index 85a18cfd7..aab6e0dfe 100644 --- a/netjsonconfig/backends/openwrt/schema.py +++ b/netjsonconfig/backends/openwrt/schema.py @@ -115,29 +115,22 @@ "firewall_policy": { "type": "string", "enum": ["ACCEPT", "REJECT", "DROP"], - "options": { - "enum_titles": [ - "Accept", "Reject", "Drop"] - }, - "default": "REJECT" + "options": {"enum_titles": ["Accept", "Reject", "Drop"]}, + "default": "REJECT", }, "zone_policy": { "type": "string", "enum": ["ACCEPT", "REJECT", "DROP"], - "options": { - "enum_titles": [ - "Accept", "Reject", "Drop"] - }, - "default": "DROP" + "options": {"enum_titles": ["Accept", "Reject", "Drop"]}, + "default": "DROP", }, "rule_policy": { "type": "string", "enum": ["ACCEPT", "REJECT", "DROP", "MARK", "NOTRACK"], "options": { - "enum_titles": [ - "Accept", "Reject", "Drop", "Mark", "Notrack"] + "enum_titles": ["Accept", "Reject", "Drop", "Mark", "Notrack"] }, - "default": "DROP" + "default": "DROP", }, "base_radio_settings": { "properties": { @@ -410,8 +403,8 @@ "interval": {"type": "integer", "propertyOrder": 8}, "message": {"type": "string", "propertyOrder": 9}, "mode": {"type": "string", "propertyOrder": 10}, - } - } + }, + }, }, "firewall": { "type": "object", @@ -433,7 +426,7 @@ "title": "input", "description": "policy for the INPUT chain of the filter table", "propertyOrder": 2, - } + }, ] }, "output": { @@ -443,7 +436,7 @@ "title": "output", "description": "policy for the OUTPUT chain of the filter table", "propertyOrder": 3, - } + }, ] }, "forward": { @@ -453,7 +446,7 @@ "title": "forward", "description": "policy for the FORWARD chain of the filter table", "propertyOrder": 4, - } + }, ] }, "forwardings": { @@ -464,36 +457,33 @@ "type": "object", "title": "Forwarding", "additionalProperties": False, - "required": [ - "src", - "dest", - ], + "required": ["src", "dest"], "properties": { "src": { "type": "string", "title": "src", "description": "specifies the traffic source zone and must " - "refer to one of the defined zone names", + "refer to one of the defined zone names", "propertyOrder": 1, }, "dest": { "type": "string", "title": "dest", "description": "specifies the traffic destination zone and must " - "refer to one of the defined zone names", + "refer to one of the defined zone names", "propertyOrder": 2, }, "family": { "type": "string", "title": "family", "description": "protocol family (ipv4, ipv6 or any) to generate " - "iptables rules for", + "iptables rules for", "enum": ["ipv4", "ipv6", "any"], "default": "any", - "propertyOrder": 3 - } - } - } + "propertyOrder": 3, + }, + }, + }, }, "zones": { "type": "array", @@ -503,16 +493,14 @@ "type": "object", "title": "Zones", "additionalProperties": True, - "required": [ - "name" - ], + "required": ["name"], "properties": { "name": { "type": "string", "title": "name", "description": "unique zone name", "maxLength": 11, - "propertyOrder": 1 + "propertyOrder": 1, }, "network": { "type": "array", @@ -524,17 +512,17 @@ "title": "Network", "type": "string", "maxLength": 15, - "pattern": "^[a-zA-z0-9_\\.\\-]*$" - } + "pattern": "^[a-zA-z0-9_\\.\\-]*$", + }, }, "masq": { "type": "boolean", "title": "masq", "description": "specifies wether outgoing zone traffic should be " - "masqueraded", + "masqueraded", "default": False, "format": "checkbox", - "propertyOrder": 3 + "propertyOrder": 3, }, "mtu_fix": { "type": "boolean", @@ -551,7 +539,7 @@ "title": "input", "description": "default policy for incoming zone traffic", "propertyOrder": 5, - } + }, ] }, "output": { @@ -561,7 +549,7 @@ "title": "output", "description": "default policy for outgoing zone traffic", "propertyOrder": 6, - } + }, ] }, "forward": { @@ -571,11 +559,11 @@ "title": "forward", "description": "default policy for forwarded zone traffic", "propertyOrder": 7, - } + }, ] - } - } - } + }, + }, + }, }, "rules": { "type": "array", @@ -585,100 +573,94 @@ "type": "object", "title": "Rules", "additionalProperties": True, - "required": [ - "src", - "target" - ], + "required": ["src", "target"], "properties": { "name": { "type": "string", "title": "name", "description": "name of the rule", - "propertyOrder": 1 + "propertyOrder": 1, }, "src": { "type": "string", "title": "src", "description": "specifies the traffic source zone and must " - "refer to one of the defined zone names", - "propertyOrder": 2 + "refer to one of the defined zone names", + "propertyOrder": 2, }, "src_ip": { "type": "string", "title": "src_ip", "description": "match incoming traffic from the specified " - "source ip address", - "propertyOrder": 3 + "source ip address", + "propertyOrder": 3, }, "src_mac": { "type": "string", "title": "src_mac", "description": "match incoming traffic from the specified " - "mac address", + "mac address", "pattern": "^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$", "minLength": 17, "maxLength": 17, - "propertyOrder": 4 + "propertyOrder": 4, }, "src_port": { "type": "string", "title": "src_port", "description": "match incoming traffic from the specified " - "source port or port range, if relevant proto " - "is specified. Multiple ports can be specified " - "separated by blanks", - "propertyOrder": 5 + "source port or port range, if relevant proto " + "is specified. Multiple ports can be specified " + "separated by blanks", + "propertyOrder": 5, }, "proto": { "type": "string", "title": "proto", "description": "match incoming traffic using the given protocol. " - "Can be one of tcp, udp, tcpudp, udplite, icmp, esp, " - "ah, sctp, or all or it can be a numeric value, " - "representing one of these protocols or a different one. " - "A protocol name from /etc/protocols is also allowed. " - "The number 0 is equivalent to all", + "Can be one of tcp, udp, tcpudp, udplite, icmp, esp, " + "ah, sctp, or all or it can be a numeric value, " + "representing one of these protocols or a different one. " + "A protocol name from /etc/protocols is also allowed. " + "The number 0 is equivalent to all", "default": "tcpudp", - "propertyOrder": 6 + "propertyOrder": 6, }, "icmp_type": { "title": "icmp_type", "description": "for protocol icmp select specific icmp types to match. " - "Values can be either exact icmp type numbers or type names", + "Values can be either exact icmp type numbers or type names", "type": "array", "uniqueItems": True, "additionalItems": True, "propertyOrder": 7, - "items": { - "title": "ICMP type", - "type": "string" - } + "items": {"title": "ICMP type", "type": "string"}, }, "dest": { "type": "string", "title": "dest", "description": "specifies the traffic destination zone and must " - "refer to one of the defined zone names, or * for " - "any zone. If specified, the rule applies to forwarded " - "traffic; otherwise, it is treated as input rule", - "propertyOrder": 8 + "refer to one of the defined zone names, or * for " + "any zone. If specified, the rule applies to forwarded " + "traffic; otherwise, it is treated as input rule", + "propertyOrder": 8, }, "dest_ip": { "type": "string", "title": "dest_ip", "description": "match incoming traffic directed to the specified " - "destination ip address. With no dest zone, this " - "is treated as an input rule", - "propertyOrder": 9 + "destination ip address. With no dest zone, this " + "is treated as an input rule", + "propertyOrder": 9, }, "dest_port": { "type": "string", "title": "dest_port", "description": "match incoming traffic directed at the given " - "destination port or port range, if relevant " - "proto is specified. Multiple ports can be specified " - "separated by blanks", - "propertyOrder": 10 + "destination port or port range, if relevant " + "proto is specified. Multiple ports can be specified " + "separated by blanks", + "propertyOrder": 10, }, "target": { "allOf": [ @@ -686,8 +668,8 @@ { "title": "target", "description": "firewall action for matched traffic", - "propertyOrder": 11 - } + "propertyOrder": 11, + }, ] }, "family": { @@ -696,29 +678,29 @@ "description": "protocol family to generate iptables rules for", "enum": ["ipv4", "ipv6", "any"], "default": "any", - "propertyOrder": 12 + "propertyOrder": 12, }, "limit": { "type": "string", "title": "limit", "description": "maximum average matching rate; specified as a number, " - "with an optional /second, /minute, /hour or /day suffix", - "propertyOrder": 13 + "with an optional /second, /minute, /hour or /day suffix", + "propertyOrder": 13, }, "enabled": { "type": "boolean", "title": "enable rule", "default": True, "format": "checkbox", - "propertyOrder": 14 - } - } - } - } - } - } - } - } + "propertyOrder": 14, + }, + }, + }, + }, + }, + }, + }, + }, ) From 29957e335b37e96794012f1b285123313b89b821 Mon Sep 17 00:00:00 2001 From: "Jonathan G. Underwood" Date: Tue, 28 Jul 2020 10:58:48 +0100 Subject: [PATCH 4/8] Establish OpenWRT firewall rule parser This commit adds a firewall rule UCI parser to the OpenWRT backend. This commit includes: - Fixing test_default.py to use the new parser - New tests in test_firewall.py --- .../backends/openwrt/converters/firewall.py | 86 ++++++---- tests/openwrt/test_default.py | 157 +++++++++--------- tests/openwrt/test_firewall.py | 140 ++++++++++++++++ 3 files changed, 268 insertions(+), 115 deletions(-) create mode 100644 tests/openwrt/test_firewall.py diff --git a/netjsonconfig/backends/openwrt/converters/firewall.py b/netjsonconfig/backends/openwrt/converters/firewall.py index f474b3fe4..7d79a4b6f 100644 --- a/netjsonconfig/backends/openwrt/converters/firewall.py +++ b/netjsonconfig/backends/openwrt/converters/firewall.py @@ -5,21 +5,18 @@ class Firewall(OpenWrtConverter): - netjson_key = 'firewall' - intermediate_key = 'firewall' - _uci_types = ['defaults', 'forwarding', 'zone', 'rule'] - _schema = schema['properties']['firewall'] + netjson_key = "firewall" + intermediate_key = "firewall" + _uci_types = ["defaults", "forwarding", "zone", "rule"] + _schema = schema["properties"]["firewall"] def to_intermediate_loop(self, block, result, index=None): - forwardings = self.__intermediate_forwardings(block.pop('forwardings', {})) - zones = self.__intermediate_zones(block.pop('zones', {})) - rules = self.__intermediate_rules(block.pop('rules', {})) - block.update({ - '.type': 'defaults', - '.name': block.pop('id', 'defaults'), - }) - result.setdefault('firewall', []) - result['firewall'] = [self.sorted_dict(block)] + forwardings + zones + rules + forwardings = self.__intermediate_forwardings(block.pop("forwardings", {})) + zones = self.__intermediate_zones(block.pop("zones", {})) + rules = self.__intermediate_rules(block.pop("rules", {})) + block.update({".type": "defaults", ".name": block.pop("id", "defaults")}) + result.setdefault("firewall", []) + result["firewall"] = [self.sorted_dict(block)] + forwardings + zones + rules return result def __intermediate_forwardings(self, forwardings): @@ -29,19 +26,26 @@ def __intermediate_forwardings(self, forwardings): """ result = [] for forwarding in forwardings: - resultdict = OrderedDict((('.name', self.__get_auto_name_forwarding(forwarding)), - ('.type', 'forwarding'))) + resultdict = OrderedDict( + ( + (".name", self.__get_auto_name_forwarding(forwarding)), + (".type", "forwarding"), + ) + ) resultdict.update(forwarding) result.append(resultdict) return result def __get_auto_name_forwarding(self, forwarding): - if 'family' in forwarding.keys(): - uci_name = self._get_uci_name('_'.join([forwarding['src'], forwarding['dest'], - forwarding['family']])) + if "family" in forwarding.keys(): + uci_name = self._get_uci_name( + "_".join([forwarding["src"], forwarding["dest"], forwarding["family"]]) + ) else: - uci_name = self._get_uci_name('_'.join([forwarding['src'], forwarding['dest']])) - return 'forwarding_{0}'.format(uci_name) + uci_name = self._get_uci_name( + "_".join([forwarding["src"], forwarding["dest"]]) + ) + return "forwarding_{0}".format(uci_name) def __intermediate_zones(self, zones): """ @@ -50,14 +54,15 @@ def __intermediate_zones(self, zones): """ result = [] for zone in zones: - resultdict = OrderedDict((('.name', self.__get_auto_name_zone(zone)), - ('.type', 'zone'))) + resultdict = OrderedDict( + ((".name", self.__get_auto_name_zone(zone)), (".type", "zone")) + ) resultdict.update(zone) result.append(resultdict) return result def __get_auto_name_zone(self, zone): - return 'zone_{0}'.format(self._get_uci_name(zone['name'])) + return "zone_{0}".format(self._get_uci_name(zone["name"])) def __intermediate_rules(self, rules): """ @@ -66,24 +71,33 @@ def __intermediate_rules(self, rules): """ result = [] for rule in rules: - if 'config_name' in rule: - del rule['config_name'] - resultdict = OrderedDict((('.name', self.__get_auto_name_rule(rule)), - ('.type', 'rule'))) + if "config_name" in rule: + del rule["config_name"] + resultdict = OrderedDict( + ((".name", self.__get_auto_name_rule(rule)), (".type", "rule")) + ) resultdict.update(rule) result.append(resultdict) return result def __get_auto_name_rule(self, rule): - return 'rule_{0}'.format(self._get_uci_name(rule['name'])) + return "rule_{0}".format(self._get_uci_name(rule["name"])) def to_netjson_loop(self, block, result, index): - result['firewall'] = self.__netjson_firewall(block) - return result + result.setdefault("firewall", {}) + + block.pop(".name") + _type = block.pop(".type") + + if _type == "rule": + rule = self.__netjson_rule(block) + result["firewall"].setdefault("rules", []) + result["firewall"]["rules"].append(rule) + + return self.type_cast(result) + + def __netjson_rule(self, rule): + if "enabled" in rule: + rule["enabled"] = rule.pop("enabled") == "1" - def __netjson_firewall(self, firewall): - del firewall['.type'] - _name = firewall.pop('.name') - if _name != 'firewall': - firewall['id'] = _name - return self.type_cast(firewall) + return self.type_cast(rule) diff --git a/tests/openwrt/test_default.py b/tests/openwrt/test_default.py index 43b32c3de..6a8e314e1 100644 --- a/tests/openwrt/test_default.py +++ b/tests/openwrt/test_default.py @@ -8,78 +8,72 @@ class TestDefault(unittest.TestCase, _TabsMixin): maxDiff = None def test_render_default(self): - o = OpenWrt({ - "luci": [ - { - "config_name": "core", - "config_value": "main", - "lang": "auto", - "resourcebase": "/luci-static/resources", - "mediaurlbase": "/luci-static/bootstrap", - "number": 4, - "boolean": True - } - ], - "firewall": { - "rules": [ - { - "config_name": "rule", - "name": "Allow-MLD", - "src": "wan", - "proto": "icmp", - "src_ip": "fe80::/10", - "family": "ipv6", - "target": "ACCEPT", - "icmp_type": [ - "130/0", - "131/0", - "132/0", - "143/0" - ] - }, + o = OpenWrt( + { + "luci": [ { - "config_name": "rule", - "name": "Rule2", - "src": "wan", - "proto": "icmp", - "src_ip": "192.168.1.1/24", - "family": "ipv4", - "target": "ACCEPT", - "icmp_type": [ - "130/0", - "131/0", - "132/0", - "143/0" - ] + "config_name": "core", + "config_value": "main", + "lang": "auto", + "resourcebase": "/luci-static/resources", + "mediaurlbase": "/luci-static/bootstrap", + "number": 4, + "boolean": True, } - ] + ], + "firewall": { + "rules": [ + { + "name": "Allow-MLD", + "src": "wan", + "proto": "icmp", + "src_ip": "fe80::/10", + "family": "ipv6", + "target": "ACCEPT", + "icmp_type": ["130/0", "131/0", "132/0", "143/0"], + }, + { + "name": "Rule2", + "src": "wan", + "proto": "icmp", + "src_ip": "192.168.1.1/24", + "family": "ipv4", + "target": "ACCEPT", + "icmp_type": ["130/0", "131/0", "132/0", "143/0"], + }, + ] + }, } - }) - expected = self._tabs("""package firewall + ) + expected = self._tabs( + """\ +package firewall + +config defaults 'defaults' config rule 'rule_Allow_MLD' - option family 'ipv6' - list icmp_type '130/0' - list icmp_type '131/0' - list icmp_type '132/0' - list icmp_type '143/0' option name 'Allow-MLD' - option proto 'icmp' option src 'wan' + option proto 'icmp' option src_ip 'fe80::/10' + option family 'ipv6' option target 'ACCEPT' - -config rule 'rule_Rule2' - option family 'ipv4' list icmp_type '130/0' list icmp_type '131/0' list icmp_type '132/0' list icmp_type '143/0' + +config rule 'rule_Rule2' option name 'Rule2' - option proto 'icmp' option src 'wan' + option proto 'icmp' option src_ip '192.168.1.1/24' + option family 'ipv4' option target 'ACCEPT' + list icmp_type '130/0' + list icmp_type '131/0' + list icmp_type '132/0' + list icmp_type '143/0' package luci @@ -142,54 +136,59 @@ def test_parse_default(self): ) o = OpenWrt(native=native) expected = { - "luci": [ + "led": [ { - "config_name": "core", - "config_value": "main", - "lang": "auto", - "resourcebase": "/luci-static/resources", - "mediaurlbase": "/luci-static/bootstrap", - "number": "4", - "boolean": "1", + "dev": "1-1.1", + "interval": 50, + "name": "USB1", + "sysfs": "tp-link:green:usb1", + "trigger": "usbdev", } ], + "interfaces": [{"name": "eth0", "type": "ethernet"}], "firewall": { "rules": [ { - "config_name": "rule", + "family": "ipv6", + "icmp_type": ["130/0", "131/0", "132/0", "143/0"], "name": "Allow-MLD", - "src": "wan", "proto": "icmp", + "src": "wan", "src_ip": "fe80::/10", - "family": "ipv6", "target": "ACCEPT", - "icmp_type": ["130/0", "131/0", "132/0", "143/0"] } ] }, - "led": [ + "luci": [ { - "name": "USB1", - "sysfs": "tp-link:green:usb1", - "trigger": "usbdev", - "dev": "1-1.1", - "interval": 50, + "boolean": "1", + "lang": "auto", + "mediaurlbase": "/luci-static/bootstrap", + "number": "4", + "resourcebase": "/luci-static/resources", + "config_value": "main", + "config_name": "core", } ], - "interfaces": [{"name": "eth0", "type": "ethernet"}], "system": [ - {"test": "1", "config_name": "custom", "config_value": "custom"} + {"test": "1", "config_value": "custom", "config_name": "custom"} ], } + + print("*" * 80) + import json + + print(json.dumps(o.config, indent=4)) + print("*" * 80) self.assertDictEqual(o.config, expected) def test_skip(self): o = OpenWrt({"skipme": {"enabled": True}}) - self.assertEqual(o.render(), '') + self.assertEqual(o.render(), "") def test_warning(self): o = OpenWrt({"luci": [{"unrecognized": True}]}) - self.assertEqual(o.render(), '') + self.assertEqual(o.render(), "") def test_merge(self): template = { @@ -228,8 +227,8 @@ def test_merge(self): self.assertEqual(o.config, expected) def test_skip_nonlists(self): - o = OpenWrt({"custom_package": {'unknown': True}}) - self.assertEqual(o.render(), '') + o = OpenWrt({"custom_package": {"unknown": True}}) + self.assertEqual(o.render(), "") def test_render_invalid_uci_name(self): o = OpenWrt( diff --git a/tests/openwrt/test_firewall.py b/tests/openwrt/test_firewall.py new file mode 100644 index 000000000..974490b92 --- /dev/null +++ b/tests/openwrt/test_firewall.py @@ -0,0 +1,140 @@ +import textwrap +import unittest + +from netjsonconfig import OpenWrt +from netjsonconfig.utils import _TabsMixin + + +class TestFirewall(unittest.TestCase, _TabsMixin): + maxDiff = None + + _rule_1_netjson = { + "firewall": { + "rules": [ + { + "name": "Allow-MLD", + "src": "wan", + "src_ip": "fe80::/10", + "proto": "icmp", + "icmp_type": ["130/0", "131/0", "132/0", "143/0"], + "target": "ACCEPT", + "family": "ipv6", + } + ] + } + } + + _rule_1_uci = textwrap.dedent( + """\ + package firewall + + config defaults 'defaults' + + config rule 'rule_Allow_MLD' + option name 'Allow-MLD' + option src 'wan' + option src_ip 'fe80::/10' + option proto 'icmp' + list icmp_type '130/0' + list icmp_type '131/0' + list icmp_type '132/0' + list icmp_type '143/0' + option target 'ACCEPT' + option family 'ipv6' + """ + ) + + def test_render_rule_1(self): + o = OpenWrt(self._rule_1_netjson) + expected = self._tabs(self._rule_1_uci) + self.assertEqual(o.render(), expected) + + def test_parse_rule_1(self): + o = OpenWrt(native=self._rule_1_uci) + self.assertEqual(o.config, self._rule_1_netjson) + + _rule_2_netjson = { + "firewall": { + "rules": [ + { + "name": "Allow-DHCPv6", + "src": "wan", + "src_ip": "fc00::/6", + "dest_ip": "fc00::/6", + "dest_port": "546", + "proto": "udp", + "target": "ACCEPT", + "family": "ipv6", + } + ] + } + } + + _rule_2_uci = textwrap.dedent( + """\ + package firewall + + config defaults 'defaults' + + config rule 'rule_Allow_DHCPv6' + option name 'Allow-DHCPv6' + option src 'wan' + option src_ip 'fc00::/6' + option dest_ip 'fc00::/6' + option dest_port '546' + option proto 'udp' + option target 'ACCEPT' + option family 'ipv6' + """ + ) + + def test_render_rule_2(self): + o = OpenWrt(self._rule_2_netjson) + expected = self._tabs(self._rule_2_uci) + self.assertEqual(o.render(), expected) + + def test_parse_rule_2(self): + o = OpenWrt(native=self._rule_2_uci) + self.assertEqual(o.config, self._rule_2_netjson) + + _rule_3_netjson = { + "firewall": { + "rules": [ + { + "name": "Allow-Ping", + "src": "wan", + "proto": "icmp", + "family": "ipv4", + "icmp_type": ["echo-request"], + "target": "ACCEPT", + "enabled": False, + } + ] + } + } + + _rule_3_uci = textwrap.dedent( + """\ + package firewall + + config defaults 'defaults' + + config rule 'rule_Allow_Ping' + option name 'Allow-Ping' + option src 'wan' + option proto 'icmp' + option family 'ipv4' + list icmp_type 'echo-request' + option target 'ACCEPT' + option enabled '0' + """ + ) + + def test_render_rule_3(self): + o = OpenWrt(self._rule_3_netjson) + expected = self._tabs(self._rule_3_uci) + self.assertEqual(o.render(), expected) + + def test_parse_rule_3(self): + o = OpenWrt(native=self._rule_3_uci) + self.assertEqual(o.config, self._rule_3_netjson) From 495b22ab078da061f9f038596ef9c876d68232eb Mon Sep 17 00:00:00 2001 From: "Jonathan G. Underwood" Date: Tue, 28 Jul 2020 17:35:14 +0100 Subject: [PATCH 5/8] Make firewall rule proto parameter a list --- .../backends/openwrt/converters/firewall.py | 23 +++++++++++++++++++ netjsonconfig/backends/openwrt/schema.py | 8 +++++-- tests/openwrt/test_default.py | 6 ++--- tests/openwrt/test_firewall.py | 6 ++--- 4 files changed, 35 insertions(+), 8 deletions(-) diff --git a/netjsonconfig/backends/openwrt/converters/firewall.py b/netjsonconfig/backends/openwrt/converters/firewall.py index 7d79a4b6f..7bbe233d0 100644 --- a/netjsonconfig/backends/openwrt/converters/firewall.py +++ b/netjsonconfig/backends/openwrt/converters/firewall.py @@ -1,3 +1,10 @@ +"""Firewall configuration management for OpenWRT. + +See the following resource for a detailed description of the sections and parameters of +the UCI configuration for the OpenWRT firewall. + + https://openwrt.org/docs/guide-user/firewall/firewall_configuration +""" from collections import OrderedDict from ..schema import schema @@ -76,6 +83,15 @@ def __intermediate_rules(self, rules): resultdict = OrderedDict( ((".name", self.__get_auto_name_rule(rule)), (".type", "rule")) ) + if "proto" in rule: + # If proto is a single value, then force it not to be in a list so that + # the UCI uses "option" rather than "list". If proto is only "tcp" + # and"udp", we can force it to the single special value of "tcpudp". + proto = rule["proto"] + if len(proto) == 1: + rule["proto"] = proto[0] + elif set(proto) == {"tcp", "udp"}: + rule["proto"] = "tcpudp" resultdict.update(rule) result.append(resultdict) return result @@ -99,5 +115,12 @@ def to_netjson_loop(self, block, result, index): def __netjson_rule(self, rule): if "enabled" in rule: rule["enabled"] = rule.pop("enabled") == "1" + if "proto" in rule: + proto = rule.pop("proto") + if not isinstance(proto, list): + if proto == "tcpudp": + rule["proto"] = ["tcp", "udp"] + else: + rule["proto"] = [proto] return self.type_cast(rule) diff --git a/netjsonconfig/backends/openwrt/schema.py b/netjsonconfig/backends/openwrt/schema.py index aab6e0dfe..e1b070f02 100644 --- a/netjsonconfig/backends/openwrt/schema.py +++ b/netjsonconfig/backends/openwrt/schema.py @@ -615,7 +615,7 @@ "propertyOrder": 5, }, "proto": { - "type": "string", + "type": "array", "title": "proto", "description": "match incoming traffic using the given protocol. " "Can be one of tcp, udp, tcpudp, udplite, icmp, esp, " @@ -623,8 +623,12 @@ "representing one of these protocols or a different one. " "A protocol name from /etc/protocols is also allowed. " "The number 0 is equivalent to all", - "default": "tcpudp", + "default": ["tcp", "udp"], "propertyOrder": 6, + "items": { + "title": "Protocol type", + "type": "string", + }, }, "icmp_type": { "title": "icmp_type", diff --git a/tests/openwrt/test_default.py b/tests/openwrt/test_default.py index 6a8e314e1..f21f04b8d 100644 --- a/tests/openwrt/test_default.py +++ b/tests/openwrt/test_default.py @@ -26,7 +26,7 @@ def test_render_default(self): { "name": "Allow-MLD", "src": "wan", - "proto": "icmp", + "proto": ["icmp"], "src_ip": "fe80::/10", "family": "ipv6", "target": "ACCEPT", @@ -35,7 +35,7 @@ def test_render_default(self): { "name": "Rule2", "src": "wan", - "proto": "icmp", + "proto": ["icmp"], "src_ip": "192.168.1.1/24", "family": "ipv4", "target": "ACCEPT", @@ -152,7 +152,7 @@ def test_parse_default(self): "family": "ipv6", "icmp_type": ["130/0", "131/0", "132/0", "143/0"], "name": "Allow-MLD", - "proto": "icmp", + "proto": ["icmp"], "src": "wan", "src_ip": "fe80::/10", "target": "ACCEPT", diff --git a/tests/openwrt/test_firewall.py b/tests/openwrt/test_firewall.py index 974490b92..b0619edd2 100644 --- a/tests/openwrt/test_firewall.py +++ b/tests/openwrt/test_firewall.py @@ -15,7 +15,7 @@ class TestFirewall(unittest.TestCase, _TabsMixin): "name": "Allow-MLD", "src": "wan", "src_ip": "fe80::/10", - "proto": "icmp", + "proto": ["icmp"], "icmp_type": ["130/0", "131/0", "132/0", "143/0"], "target": "ACCEPT", "family": "ipv6", @@ -62,7 +62,7 @@ def test_parse_rule_1(self): "src_ip": "fc00::/6", "dest_ip": "fc00::/6", "dest_port": "546", - "proto": "udp", + "proto": ["udp"], "target": "ACCEPT", "family": "ipv6", } @@ -103,7 +103,7 @@ def test_parse_rule_2(self): { "name": "Allow-Ping", "src": "wan", - "proto": "icmp", + "proto": ["icmp"], "family": "ipv4", "icmp_type": ["echo-request"], "target": "ACCEPT", From f792d6a1194f50d282a8e9802c4972bfbea74b19 Mon Sep 17 00:00:00 2001 From: "Jonathan G. Underwood" Date: Tue, 28 Jul 2020 17:35:51 +0100 Subject: [PATCH 6/8] Add another firewall rule test --- tests/openwrt/test_firewall.py | 38 ++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/openwrt/test_firewall.py b/tests/openwrt/test_firewall.py index b0619edd2..6a79ec32f 100644 --- a/tests/openwrt/test_firewall.py +++ b/tests/openwrt/test_firewall.py @@ -138,3 +138,41 @@ def test_render_rule_3(self): def test_parse_rule_3(self): o = OpenWrt(native=self._rule_3_uci) self.assertEqual(o.config, self._rule_3_netjson) + + _rule_4_netjson = { + "firewall": { + "rules": [ + { + "name": "Allow-Isolated-DHCP", + "src": "isolated", + "proto": ["udp"], + "dest_port": "67-68", + "target": "ACCEPT", + } + ] + } + } + + _rule_4_uci = textwrap.dedent( + """\ + package firewall + + config defaults 'defaults' + + config rule 'rule_Allow_Isolated_DHCP' + option name 'Allow-Isolated-DHCP' + option src 'isolated' + option proto 'udp' + option dest_port '67-68' + option target 'ACCEPT' + """ + ) + + def test_render_rule_4(self): + o = OpenWrt(self._rule_4_netjson) + expected = self._tabs(self._rule_4_uci) + self.assertEqual(o.render(), expected) + + def test_parse_rule_4(self): + o = OpenWrt(native=self._rule_4_uci) + self.assertEqual(o.config, self._rule_4_netjson) From 6d09ae8662c804b00622ff47f6ba9cb783ada492 Mon Sep 17 00:00:00 2001 From: "Jonathan G. Underwood" Date: Wed, 29 Jul 2020 00:35:29 +0100 Subject: [PATCH 7/8] Add firewall zone handling and tests --- .../backends/openwrt/converters/firewall.py | 25 +++++ tests/openwrt/test_firewall.py | 106 ++++++++++++++++++ 2 files changed, 131 insertions(+) diff --git a/netjsonconfig/backends/openwrt/converters/firewall.py b/netjsonconfig/backends/openwrt/converters/firewall.py index 7bbe233d0..7893344d5 100644 --- a/netjsonconfig/backends/openwrt/converters/firewall.py +++ b/netjsonconfig/backends/openwrt/converters/firewall.py @@ -64,6 +64,11 @@ def __intermediate_zones(self, zones): resultdict = OrderedDict( ((".name", self.__get_auto_name_zone(zone)), (".type", "zone")) ) + # If network contains only a single value, force the use of a UCI "option" + # rather than "list"". + network = zone["network"] + if len(network) == 1: + zone["network"] = network[0] resultdict.update(zone) result.append(resultdict) return result @@ -109,6 +114,10 @@ def to_netjson_loop(self, block, result, index): rule = self.__netjson_rule(block) result["firewall"].setdefault("rules", []) result["firewall"]["rules"].append(rule) + if _type == "zone": + zone = self.__netjson_zone(block) + result["firewall"].setdefault("zones", []) + result["firewall"]["zones"].append(zone) return self.type_cast(result) @@ -124,3 +133,19 @@ def __netjson_rule(self, rule): rule["proto"] = [proto] return self.type_cast(rule) + + def __netjson_zone(self, zone): + network = zone["network"] + # network may be specified as a list in a single string e.g. + # option network 'wan wan6' + # Here we ensure that network is always a list. + if not isinstance(network, list): + zone["network"] = network.split() + + if "mtu_fix" in zone: + zone["mtu_fix"] = zone.pop("mtu_fix") == "1" + + if "masq" in zone: + zone["masq"] = zone.pop("masq") == "1" + + return self.type_cast(zone) diff --git a/tests/openwrt/test_firewall.py b/tests/openwrt/test_firewall.py index 6a79ec32f..be5c23ae5 100644 --- a/tests/openwrt/test_firewall.py +++ b/tests/openwrt/test_firewall.py @@ -176,3 +176,109 @@ def test_render_rule_4(self): def test_parse_rule_4(self): o = OpenWrt(native=self._rule_4_uci) self.assertEqual(o.config, self._rule_4_netjson) + + _zone_1_netjson = { + "firewall": { + "zones": [ + { + "name": "lan", + "input": "ACCEPT", + "output": "ACCEPT", + "forward": "ACCEPT", + "network": ["lan"], + "mtu_fix": True, + } + ] + } + } + + _zone_1_uci = textwrap.dedent( + """\ + package firewall + + config defaults 'defaults' + + config zone 'zone_lan' + option name 'lan' + option input 'ACCEPT' + option output 'ACCEPT' + option forward 'ACCEPT' + option network 'lan' + option mtu_fix '1' + """ + ) + + def test_render_zone_1(self): + o = OpenWrt(self._zone_1_netjson) + expected = self._tabs(self._zone_1_uci) + self.assertEqual(o.render(), expected) + + def test_parse_zone_1(self): + o = OpenWrt(native=self._zone_1_uci) + self.assertEqual(o.config, self._zone_1_netjson) + + _zone_2_netjson = { + "firewall": { + "zones": [ + { + "name": "wan", + "input": "DROP", + "output": "ACCEPT", + "forward": "DROP", + "network": ["wan", "wan6"], + "mtu_fix": True, + "masq": True, + } + ] + } + } + + _zone_2_uci = textwrap.dedent( + """\ + package firewall + + config defaults 'defaults' + + config zone 'zone_wan' + option name 'wan' + option input 'DROP' + option output 'ACCEPT' + option forward 'DROP' + list network 'wan' + list network 'wan6' + option mtu_fix '1' + option masq '1' + """ + ) + + # This one is the same as _zone_2_uci with the exception that the "network" + # parameter is specified as a single string. + _zone_3_uci = textwrap.dedent( + """\ + package firewall + + config defaults 'defaults' + + config zone 'zone_wan' + option name 'wan' + option input 'DROP' + option output 'ACCEPT' + option forward 'DROP' + option network 'wan wan6' + option mtu_fix '1' + option masq '1' + """ + ) + + def test_render_zone_2(self): + o = OpenWrt(self._zone_2_netjson) + expected = self._tabs(self._zone_2_uci) + self.assertEqual(o.render(), expected) + + def test_parse_zone_2(self): + o = OpenWrt(native=self._zone_2_uci) + self.assertEqual(o.config, self._zone_2_netjson) + + def test_parse_zone_3(self): + o = OpenWrt(native=self._zone_3_uci) + self.assertEqual(o.config, self._zone_2_netjson) From 9c08c8c36747d0dc4935d319607a76ae6b28d9ad Mon Sep 17 00:00:00 2001 From: "Jonathan G. Underwood" Date: Wed, 29 Jul 2020 12:09:15 +0100 Subject: [PATCH 8/8] Add parser and tests for firewall forwardings --- .../backends/openwrt/converters/firewall.py | 7 ++ tests/openwrt/test_firewall.py | 91 +++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/netjsonconfig/backends/openwrt/converters/firewall.py b/netjsonconfig/backends/openwrt/converters/firewall.py index 7893344d5..e6298e2ba 100644 --- a/netjsonconfig/backends/openwrt/converters/firewall.py +++ b/netjsonconfig/backends/openwrt/converters/firewall.py @@ -118,6 +118,10 @@ def to_netjson_loop(self, block, result, index): zone = self.__netjson_zone(block) result["firewall"].setdefault("zones", []) result["firewall"]["zones"].append(zone) + if _type == "forwarding": + forwarding = self.__netjson_forwarding(block) + result["firewall"].setdefault("forwardings", []) + result["firewall"]["forwardings"].append(forwarding) return self.type_cast(result) @@ -149,3 +153,6 @@ def __netjson_zone(self, zone): zone["masq"] = zone.pop("masq") == "1" return self.type_cast(zone) + + def __netjson_forwarding(self, forwarding): + return self.type_cast(forwarding) diff --git a/tests/openwrt/test_firewall.py b/tests/openwrt/test_firewall.py index be5c23ae5..8190ac807 100644 --- a/tests/openwrt/test_firewall.py +++ b/tests/openwrt/test_firewall.py @@ -2,6 +2,7 @@ import unittest from netjsonconfig import OpenWrt +from netjsonconfig.exceptions import ValidationError from netjsonconfig.utils import _TabsMixin @@ -282,3 +283,93 @@ def test_parse_zone_2(self): def test_parse_zone_3(self): o = OpenWrt(native=self._zone_3_uci) self.assertEqual(o.config, self._zone_2_netjson) + + _forwarding_1_netjson = { + "firewall": {"forwardings": [{"src": "isolated", "dest": "wan"}]} + } + + _forwarding_1_uci = textwrap.dedent( + """\ + package firewall + + config defaults 'defaults' + + config forwarding 'forwarding_isolated_wan' + option src 'isolated' + option dest 'wan' + """ + ) + + def test_render_forwarding_1(self): + o = OpenWrt(self._forwarding_1_netjson) + expected = self._tabs(self._forwarding_1_uci) + self.assertEqual(o.render(), expected) + + def test_parse_forwarding_1(self): + o = OpenWrt(native=self._forwarding_1_uci) + self.assertEqual(o.config, self._forwarding_1_netjson) + + _forwarding_2_netjson = { + "firewall": { + "forwardings": [{"src": "isolated", "dest": "wan", "family": "ipv4"}] + } + } + + _forwarding_2_uci = textwrap.dedent( + """\ + package firewall + + config defaults 'defaults' + + config forwarding 'forwarding_isolated_wan_ipv4' + option src 'isolated' + option dest 'wan' + option family 'ipv4' + """ + ) + + def test_render_forwarding_2(self): + o = OpenWrt(self._forwarding_2_netjson) + expected = self._tabs(self._forwarding_2_uci) + self.assertEqual(o.render(), expected) + + def test_parse_forwarding_2(self): + o = OpenWrt(native=self._forwarding_2_uci) + self.assertEqual(o.config, self._forwarding_2_netjson) + + _forwarding_3_netjson = { + "firewall": {"forwardings": [{"src": "lan", "dest": "wan", "family": "any"}]} + } + + _forwarding_3_uci = textwrap.dedent( + """\ + package firewall + + config defaults 'defaults' + + config forwarding 'forwarding_lan_wan_any' + option src 'lan' + option dest 'wan' + option family 'any' + """ + ) + + def test_render_forwarding_3(self): + o = OpenWrt(self._forwarding_3_netjson) + expected = self._tabs(self._forwarding_3_uci) + self.assertEqual(o.render(), expected) + + def test_parse_forwarding_3(self): + o = OpenWrt(native=self._forwarding_3_uci) + self.assertEqual(o.config, self._forwarding_3_netjson) + + def test_forwarding_validation_error(self): + o = OpenWrt( + { + "firewall": { + "forwardings": [{"src": "lan", "dest": "wan", "family": "XXXXXX"}] + } + } + ) + with self.assertRaises(ValidationError): + o.validate()