From a89c7e9ad24de070450909427490ed6f6e232fcf Mon Sep 17 00:00:00 2001 From: Oliver Kraitschy Date: Tue, 7 Aug 2018 14:46:44 +0200 Subject: [PATCH 01/40] [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 02/40] 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 03/40] 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 04/40] 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 05/40] 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 06/40] 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 07/40] 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 08/40] 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() From a9cb284ae3eddfe2cae2c6ca9695d213f612d877 Mon Sep 17 00:00:00 2001 From: "Jonathan G. Underwood" Date: Wed, 29 Jul 2020 19:46:08 +0100 Subject: [PATCH 09/40] Add OpenWRT firewall redirect handling This commit: - Adds a schema for redirects - Adds a renderer and parser for redirects - Adds initial tests for redirects --- .../backends/openwrt/converters/firewall.py | 53 +++- netjsonconfig/backends/openwrt/schema.py | 279 +++++++++++++++++- tests/openwrt/test_firewall.py | 40 +++ 3 files changed, 363 insertions(+), 9 deletions(-) diff --git a/netjsonconfig/backends/openwrt/converters/firewall.py b/netjsonconfig/backends/openwrt/converters/firewall.py index e6298e2ba..c4567784d 100644 --- a/netjsonconfig/backends/openwrt/converters/firewall.py +++ b/netjsonconfig/backends/openwrt/converters/firewall.py @@ -14,16 +14,19 @@ class Firewall(OpenWrtConverter): netjson_key = "firewall" intermediate_key = "firewall" - _uci_types = ["defaults", "forwarding", "zone", "rule"] + _uci_types = ["defaults", "forwarding", "zone", "rule", "redirect"] _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", {})) + redirects = self.__intermediate_redirects(block.pop("redirects", {})) block.update({".type": "defaults", ".name": block.pop("id", "defaults")}) result.setdefault("firewall", []) - result["firewall"] = [self.sorted_dict(block)] + forwardings + zones + rules + result["firewall"] = ( + [self.sorted_dict(block)] + forwardings + zones + rules + redirects + ) return result def __intermediate_forwardings(self, forwardings): @@ -104,6 +107,37 @@ def __intermediate_rules(self, rules): def __get_auto_name_rule(self, rule): return "rule_{0}".format(self._get_uci_name(rule["name"])) + def __intermediate_redirects(self, redirects): + """ + converts NetJSON redirect to + UCI intermediate data structure + """ + result = [] + for redirect in redirects: + if "config_name" in redirect: + del redirect["config_name"] + resultdict = OrderedDict( + ( + (".name", self.__get_auto_name_redirect(redirect)), + (".type", "redirect"), + ) + ) + if "proto" in redirect: + # 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 = redirect["proto"] + if len(proto) == 1: + redirect["proto"] = proto[0] + elif set(proto) == {"tcp", "udp"}: + redirect["proto"] = "tcpudp" + resultdict.update(redirect) + result.append(resultdict) + return result + + def __get_auto_name_redirect(self, redirect): + return "redirect_{0}".format(self._get_uci_name(redirect["name"])) + def to_netjson_loop(self, block, result, index): result.setdefault("firewall", {}) @@ -122,6 +156,10 @@ def to_netjson_loop(self, block, result, index): forwarding = self.__netjson_forwarding(block) result["firewall"].setdefault("forwardings", []) result["firewall"]["forwardings"].append(forwarding) + if _type == "redirect": + redirect = self.__netjson_redirect(block) + result["firewall"].setdefault("redirects", []) + result["firewall"]["redirects"].append(redirect) return self.type_cast(result) @@ -156,3 +194,14 @@ def __netjson_zone(self, zone): def __netjson_forwarding(self, forwarding): return self.type_cast(forwarding) + + def __netjson_redirect(self, redirect): + if "proto" in redirect: + proto = redirect.pop("proto") + if not isinstance(proto, list): + if proto == "tcpudp": + redirect["proto"] = ["tcp", "udp"] + else: + redirect["proto"] = [proto] + + return self.type_cast(redirect) diff --git a/netjsonconfig/backends/openwrt/schema.py b/netjsonconfig/backends/openwrt/schema.py index e1b070f02..ec427ea2c 100644 --- a/netjsonconfig/backends/openwrt/schema.py +++ b/netjsonconfig/backends/openwrt/schema.py @@ -8,6 +8,19 @@ default_radio_driver = "mac80211" +# The following regex will match against a single valid port, or a port range e.g. 1234-5000 +port_range_regex = "^([1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(-([1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?$" # noqa + +# Match against a MAC address +mac_address_regex = "^([0-9A-F]{2}[:-]){5}([0-9A-F]{2})$" + +# Match against a yyyy-mm-dd format date. Note that draft07 of the JSON schema standard +# include a "date" pattern which can replace this. +# https://json-schema.org/understanding-json-schema/reference/string.html +date_regex = "^([0-9]{4})-(0[1-9]|[12][0-9]|3[01])-([012][0-9]|[3][01])$" + +# Match against a time in the format hh:mm:ss +time_regex = "^([01][0-9]|2[0123])(:([012345][0-9])){2}$" schema = merge_config( default_schema, @@ -32,7 +45,7 @@ "network": { "type": "array", "title": "Attached Networks", - "description": "override OpenWRT \"network\" config option of of wifi-iface " + "description": 'override OpenWRT "network" config option of of wifi-iface ' "directive; will be automatically determined if left blank", "uniqueItems": True, "additionalItems": True, @@ -71,9 +84,9 @@ "macfilter": { "type": "string", "title": "MAC Filter", - "description": "specifies the mac filter policy, \"disable\" to disable " - "the filter, \"allow\" to treat it as whitelist or " - "\"deny\" to treat it as blacklist", + "description": 'specifies the mac filter policy, "disable" to disable ' + 'the filter, "allow" to treat it as whitelist or ' + '"deny" to treat it as blacklist', "enum": ["disable", "allow", "deny"], "default": "disable", "propertyOrder": 15, @@ -82,7 +95,7 @@ "type": "array", "title": "MAC List", "description": "mac addresses that will be filtered according to the policy " - "specified in the \"macfilter\" option", + 'specified in the "macfilter" option', "propertyOrder": 16, "items": { "type": "string", @@ -103,7 +116,7 @@ "igmp_snooping": { "type": "boolean", "title": "IGMP snooping", - "description": "sets the \"multicast_snooping\" kernel setting for a bridge", + "description": 'sets the "multicast_snooping" kernel setting for a bridge', "default": True, "format": "checkbox", "propertyOrder": 4, @@ -693,7 +706,8 @@ }, "enabled": { "type": "boolean", - "title": "enable rule", + "title": "enable", + "description": "Enable this rule.", "default": True, "format": "checkbox", "propertyOrder": 14, @@ -701,6 +715,257 @@ }, }, }, + "redirects": { + "type": "array", + "title": "Redirects", + "propertyOrder": 8, + "items": { + "type": "object", + "title": "Redirect", + "additionalProperties": False, + "properties": { + "name": { + "type": "string", + "title": "name", + "description": "Name of redirect", + "propertyOrder": 1, + }, + "src": { + "type": "string", + "title": "src", + "description": "Specifies the traffic source zone. " + "Must refer to one of the defined zone names. " + "For typical port forwards this usually is wan.", + "propertyOrder": 2, + }, + "src_ip": { + "type": "string", + "title": "src_ip", + "description": "Match incoming traffic from the specified source ip " + "address.", + "format": "ipv4", + "propertyOrder": 3, + }, + "src_dip": { + "type": "string", + "title": "src_dip", + "description": "For DNAT, match incoming traffic directed at the " + "given destination ip address. For SNAT rewrite the source address " + "to the given address.", + "format": "ipv4", + "propertyOrder": 4, + }, + "src_mac": { + "type": "string", + "title": "src_mac", + "description": "Match incoming traffic from the specified MAC address.", + "pattern": mac_address_regex, + "propertyOrder": 5, + }, + "src_port": { + "type": "string", + "title": "src_port", + "description": "Match incoming traffic originating from the given source " + "port or port range on the client host.", + "pattern": port_range_regex, + "propertyOrder": 6, + }, + "src_dport": { + "type": "string", + "title": "src_dport", + "description": "For DNAT, match incoming traffic directed at the given " + "destination port or port range on this host. For SNAT rewrite the " + "source ports to the given value.", + "pattern": port_range_regex, + "propertyOrder": 7, + }, + "proto": { + "type": "array", + "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": ["tcp", "udp"], + "propertyOrder": 8, + "items": { + "title": "Protocol type", + "type": "string", + }, + }, + "dest": { + "type": "string", + "title": "dest", + "description": "Specifies the traffic destination zone. Must refer to " + "on of the defined zone names. For DNAT target on Attitude Adjustment, " + 'NAT reflection works only if this is equal to "lan".', + "propertyOrder": 9, + }, + "dest_ip": { + "type": "string", + "title": "dest_ip", + "description": "For DNAT, redirect matches incoming traffic to the " + "specified internal host. For SNAT, it matches traffic directed at " + "the given address. For DNAT, if the dest_ip is not specified, the rule " + "is translated in a iptables/REDIRECT rule, otherwise it is a " + "iptables/DNAT rule.", + "format": "ipv4", + "propertyOrder": 10, + }, + "dest_port": { + "type": "string", + "title": "dest_port", + "description": "For DNAT, redirect matched incoming traffic to the given " + "port on the internal host. For SNAT, match traffic directed at the " + "given ports. Only a single port or range can be specified.", + "pattern": port_range_regex, + "propertyOrder": 11, + }, + "ipset": { + "type": "string", + "title": "ipset", + "description": "Match traffic against the given ipset. The match can be " + "inverted by prefixing the value with an exclamation mark.", + "propertyOrder": 12, + }, + "mark": { + "type": "string", + "title": "mark", + "description": 'Match traffic against the given firewall mark, e.g. ' + '"0xFF" to match mark 255 or "0x0/0x1" to match any even mark value. ' + 'The match can be inverted by prefixing the value with an exclamation ' + 'mark, e.g. "!0x10" to match all but mark #16.', + "propertyOrder": 13, + }, + "start_date": { + "type": "string", + "title": "start_date", + "description": "Only match traffic after the given date (inclusive).", + "pattern": date_regex, + # "format": "date", TODO: replace pattern with this + # when adopt draft07 + "propertyOrder": 14, + }, + "stop_date": { + "type": "string", + "title": "stop_date", + "description": "Only match traffic before the given date (inclusive).", + "pattern": date_regex, + # "format": "date", TODO: replace pattern with this + # when adopt draft07 + "propertyOrder": 15, + }, + "start_time": { + "type": "string", + "title": "start_time", + "description": "Only match traffic after the given time of day " + "(inclusive).", + "pattern": time_regex, + "propertyOrder": 16, + }, + "stop_time": { + "type": "string", + "title": "stop_time", + "description": "Only match traffic before the given time of day " + "(inclusive).", + "pattern": time_regex, + "propertyOrder": 17, + }, + # FIXME: regex needed. Also, should this be an array? + "weekdays": { + "type": "string", + "title": "weekdays", + "description": "Only match traffic during the given week days, " + 'e.g. "sun mon thu fri" to only match on Sundays, Mondays, Thursdays and ' + "Fridays. The list can be inverted by prefixing it with an exclamation " + 'mark, e.g. "! sat sun" to always match but not on Saturdays and ' + "Sundays.", + "propertyOrder": 18, + }, + # FIXME: regex needed. Also, should this be an array? + "monthdays": { + "type": "string", + "title": "monthdays", + "description": "Only match traffic during the given days of the " + 'month, e.g. "2 5 30" to only match on every 2nd, 5th and 30th ' + "day of the month. The list can be inverted by prefixing it with " + 'an exclamation mark, e.g. "! 31" to always match but on the ' + "31st of the month.", + "propertyOrder": 19, + }, + "utc_time": { + "type": "boolean", + "title": "utc_time", + "description": "Treat all given time values as UTC time instead of local " + "time.", + "default": False, + "propertyOrder": 20, + }, + "target": { + "type": "string", + "title": "target", + "description": "NAT target (DNAT or SNAT) to use when generating the " + "rule.", + "enum": ["DNAT", "SNAT"], + "default": "DNAT", + "propertyOrder": 21, + }, + "family": { + "type": "string", + "title": "family", + "description": "Protocol family (ipv4, ipv6 or any) to generate iptables " + "rules for", + "enum": ["ipv4", "ipv6", "any"], + "default": "any", + "propertyOrder": 22, + }, + "reflection": { + "type": "boolean", + "title": "reflection", + "description": "Activate NAT reflection for this redirect. Applicable to " + "DNAT targets.", + "default": True, + "propertyOrder": 23, + }, + "reflection_src": { + "type": "string", + "title": "reflection_src", + "description": "The source address to use for NAT-reflected packets if " + "reflection is True. This can be internal or external, specifying which " + "interface’s address to use. Applicable to DNAT targets.", + "enum": ["internal", "external"], + "default": "internal", + "propertyOrder": 24, + }, + "limit": { + "type": "string", + "title": "limit", + "description": "Maximum average matching rate; specified as a number, " + "with an optional /second, /minute, /hour or /day suffix. " + "Examples: 3/second, 3/sec or 3/s.", + "propertyOrder": 25, + }, + "limit_burst": { + "type": "integer", + "title": "limit_burst", + "description": "Maximum initial number of packets to match, allowing a " + "short-term average above limit.", + "default": 5, + "propertyOrder": 26, + }, + "enabled": { + "type": "boolean", + "title": "enable", + "description": "Enable this redirect.", + "default": True, + "format": "checkbox", + "propertyOrder": 27, + }, + }, + }, + }, }, }, }, diff --git a/tests/openwrt/test_firewall.py b/tests/openwrt/test_firewall.py index 8190ac807..478336d0b 100644 --- a/tests/openwrt/test_firewall.py +++ b/tests/openwrt/test_firewall.py @@ -373,3 +373,43 @@ def test_forwarding_validation_error(self): ) with self.assertRaises(ValidationError): o.validate() + + _redirect_1_netjson = { + "firewall": { + "redirects": [ + { + "name": "Adblock DNS, port 53", + "src": "lan", + "proto": ["tcp", "udp"], + "src_dport": "53", + "dest_port": "53", + "target": "DNAT", + } + ] + } + } + + _redirect_1_uci = textwrap.dedent( + """\ + package firewall + + config defaults 'defaults' + + config redirect 'redirect_Adblock DNS, port 53' + option name 'Adblock DNS, port 53' + option src 'lan' + option proto 'tcpudp' + option src_dport '53' + option dest_port '53' + option target 'DNAT' + """ + ) + + def test_render_redirect_1(self): + o = OpenWrt(self._redirect_1_netjson) + expected = self._tabs(self._redirect_1_uci) + self.assertEqual(o.render(), expected) + + def test_parse_redirect_1(self): + o = OpenWrt(native=self._redirect_1_uci) + self.assertEqual(o.config, self._redirect_1_netjson) From b9cf32282163884d485ca3237f3e0700945f3a44 Mon Sep 17 00:00:00 2001 From: "Jonathan G. Underwood" Date: Sat, 1 Aug 2020 12:41:55 +0100 Subject: [PATCH 10/40] Fix redirect weekdays and monthdays handling Treat these parameters as an array, and drop support for negating the values with a "!" in JSON as it is unneeded. --- .../backends/openwrt/converters/firewall.py | 4 ++ netjsonconfig/backends/openwrt/schema.py | 41 +++++++---- tests/openwrt/test_firewall.py | 71 +++++++++++++++++++ 3 files changed, 104 insertions(+), 12 deletions(-) diff --git a/netjsonconfig/backends/openwrt/converters/firewall.py b/netjsonconfig/backends/openwrt/converters/firewall.py index c4567784d..a45cbd440 100644 --- a/netjsonconfig/backends/openwrt/converters/firewall.py +++ b/netjsonconfig/backends/openwrt/converters/firewall.py @@ -131,8 +131,10 @@ def __intermediate_redirects(self, redirects): redirect["proto"] = proto[0] elif set(proto) == {"tcp", "udp"}: redirect["proto"] = "tcpudp" + resultdict.update(redirect) result.append(resultdict) + return result def __get_auto_name_redirect(self, redirect): @@ -203,5 +205,7 @@ def __netjson_redirect(self, redirect): redirect["proto"] = ["tcp", "udp"] else: redirect["proto"] = [proto] + if "monthdays" in redirect: + redirect["monthdays"] = [int(x) for x in redirect["monthdays"]] return self.type_cast(redirect) diff --git a/netjsonconfig/backends/openwrt/schema.py b/netjsonconfig/backends/openwrt/schema.py index ec427ea2c..a20ff7f46 100644 --- a/netjsonconfig/backends/openwrt/schema.py +++ b/netjsonconfig/backends/openwrt/schema.py @@ -873,27 +873,44 @@ "pattern": time_regex, "propertyOrder": 17, }, - # FIXME: regex needed. Also, should this be an array? + # Note: here we don't support negation of values like + # the UCI syntax does, as it's not necessary. "weekdays": { - "type": "string", + "type": "array", "title": "weekdays", "description": "Only match traffic during the given week days, " - 'e.g. "sun mon thu fri" to only match on Sundays, Mondays, Thursdays and ' - "Fridays. The list can be inverted by prefixing it with an exclamation " - 'mark, e.g. "! sat sun" to always match but not on Saturdays and ' - "Sundays.", + 'e.g. ["sun", "mon", "thu", "fri"] to only match on Sundays, ' + "Mondays, Thursdays and Fridays.", "propertyOrder": 18, + "items": { + "type": "string", + "title": "weekday", + "enum": [ + "mon", + "tue", + "wed", + "thu", + "fri", + "sat", + "sun", + ], + }, }, - # FIXME: regex needed. Also, should this be an array? + # Note: here we don't support negation of values like + # the UCI syntax does, as it's not necessary. "monthdays": { - "type": "string", + "type": "array", "title": "monthdays", "description": "Only match traffic during the given days of the " - 'month, e.g. "2 5 30" to only match on every 2nd, 5th and 30th ' - "day of the month. The list can be inverted by prefixing it with " - 'an exclamation mark, e.g. "! 31" to always match but on the ' - "31st of the month.", + "month, e.g. [2, 5, 30] to only match on every 2nd, 5th and 30th " + "day of the month.", "propertyOrder": 19, + "items": { + "type": "integer", + "title": "day of month", + "minimum": 1, + "maximum": 31, + }, }, "utc_time": { "type": "boolean", diff --git a/tests/openwrt/test_firewall.py b/tests/openwrt/test_firewall.py index 478336d0b..0fe7ca1eb 100644 --- a/tests/openwrt/test_firewall.py +++ b/tests/openwrt/test_firewall.py @@ -413,3 +413,74 @@ def test_render_redirect_1(self): def test_parse_redirect_1(self): o = OpenWrt(native=self._redirect_1_uci) self.assertEqual(o.config, self._redirect_1_netjson) + + _redirect_2_netjson = { + "firewall": { + "redirects": [ + { + "name": "Adblock DNS, port 53", + "src": "lan", + "proto": ["tcp", "udp"], + "src_dport": "53", + "dest_port": "53", + "target": "DNAT", + # Contrived, unrealistic example for testing + "weekdays": ["mon", "tue", "wed"], + "monthdays": [1, 2, 3, 29, 30], + } + ] + } + } + + _redirect_2_uci = textwrap.dedent( + """\ + package firewall + + config defaults 'defaults' + + config redirect 'redirect_Adblock DNS, port 53' + option name 'Adblock DNS, port 53' + option src 'lan' + option proto 'tcpudp' + option src_dport '53' + option dest_port '53' + option target 'DNAT' + list weekdays 'mon' + list weekdays 'tue' + list weekdays 'wed' + list monthdays '1' + list monthdays '2' + list monthdays '3' + list monthdays '29' + list monthdays '30' + """ + ) + + def test_render_redirect_2(self): + o = OpenWrt(self._redirect_2_netjson) + expected = self._tabs(self._redirect_2_uci) + self.assertEqual(o.render(), expected) + + def test_parse_redirect_2(self): + o = OpenWrt(native=self._redirect_2_uci) + self.assertEqual(o.config, self._redirect_2_netjson) + + def test_redirect_weekdays_validation_error_1(self): + o = OpenWrt({"firewall": {"redirects": [{"weekdays": ["mon", "xxx"]}]}}) + with self.assertRaises(ValidationError): + o.validate() + + def test_redirect_weekdays_validation_error_2(self): + o = OpenWrt({"firewall": {"redirects": [{"weekdays": ["mon", 1]}]}}) + with self.assertRaises(ValidationError): + o.validate() + + def test_redirect_monthdays_validation_error_1(self): + o = OpenWrt({"firewall": {"redirects": [{"monthdays": [2, 8, 32]}]}}) + with self.assertRaises(ValidationError): + o.validate() + + def test_redirect_monthdays_validation_error_2(self): + o = OpenWrt({"firewall": {"redirects": [{"monthdays": [0, 2, 8]}]}}) + with self.assertRaises(ValidationError): + o.validate() From 3dd9f67928e761ffbe9557117e4842c447ebc8fd Mon Sep 17 00:00:00 2001 From: "Jonathan G. Underwood" Date: Sat, 1 Aug 2020 13:37:26 +0100 Subject: [PATCH 11/40] Enable negation when parsing monthdays and weekdays This change enables parsing of the "!" character for monthdays and weekdays paraemters of a firewall redirect object when present in UCI configuration. This change does not add support for negation in NetJSON config. --- .../backends/openwrt/converters/firewall.py | 26 ++++++- tests/openwrt/test_firewall.py | 67 +++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/netjsonconfig/backends/openwrt/converters/firewall.py b/netjsonconfig/backends/openwrt/converters/firewall.py index a45cbd440..67fd2e56c 100644 --- a/netjsonconfig/backends/openwrt/converters/firewall.py +++ b/netjsonconfig/backends/openwrt/converters/firewall.py @@ -205,7 +205,31 @@ def __netjson_redirect(self, redirect): redirect["proto"] = ["tcp", "udp"] else: redirect["proto"] = [proto] + + if "weekdays" in redirect: + weekdays = redirect["weekdays"] + if not isinstance(weekdays, list): + weekdays = weekdays.split() + # UCI allows the first entry to be "!" which means negate the remaining + # entries + if weekdays[0] == "!": + all_days = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"] + wd = set([x for x in weekdays[1:]]) + redirect["weekdays"] = list(set(all_days) - wd) + # Sort the days for predictability when testing + redirect["weekdays"].sort(key=lambda v: all_days.index(v)) + if "monthdays" in redirect: - redirect["monthdays"] = [int(x) for x in redirect["monthdays"]] + monthdays = redirect["monthdays"] + if not isinstance(monthdays, list): + monthdays = monthdays.split() + # UCI allows the first entry to be "!" which means negate the remaining + # entries + if monthdays[0] == "!": + all_days = set(range(1, 32)) + md = set([int(x) for x in monthdays[1:]]) + redirect["monthdays"] = list(all_days - md) + else: + redirect["monthdays"] = [int(x) for x in monthdays] return self.type_cast(redirect) diff --git a/tests/openwrt/test_firewall.py b/tests/openwrt/test_firewall.py index 0fe7ca1eb..f19b14006 100644 --- a/tests/openwrt/test_firewall.py +++ b/tests/openwrt/test_firewall.py @@ -484,3 +484,70 @@ def test_redirect_monthdays_validation_error_2(self): o = OpenWrt({"firewall": {"redirects": [{"monthdays": [0, 2, 8]}]}}) with self.assertRaises(ValidationError): o.validate() + + _redirect_3_uci = textwrap.dedent( + """\ + package firewall + + config defaults 'defaults' + + config redirect 'redirect_Adblock DNS, port 53' + option name 'Adblock DNS, port 53' + option src 'lan' + option proto 'tcpudp' + option src_dport '53' + option dest_port '53' + option target 'DNAT' + option weekdays '! mon tue wed' + option monthdays '! 1 2 3 4 5' + """ + ) + + _redirect_3_netjson = { + "firewall": { + "redirects": [ + { + "name": "Adblock DNS, port 53", + "src": "lan", + "proto": ["tcp", "udp"], + "src_dport": "53", + "dest_port": "53", + "target": "DNAT", + "weekdays": ["sun", "thu", "fri", "sat"], + "monthdays": [ + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + ], + } + ] + } + } + + def test_parse_redirect_3(self): + o = OpenWrt(native=self._redirect_3_uci) + print(o.config) + self.assertEqual(o.config, self._redirect_3_netjson) From 9aff7a196ff078c003de3e0404e3d661341cff73 Mon Sep 17 00:00:00 2001 From: "Jonathan G. Underwood" Date: Sat, 1 Aug 2020 16:26:07 +0100 Subject: [PATCH 12/40] Enhance and test redirect parser --- .../backends/openwrt/converters/firewall.py | 12 +++ tests/openwrt/test_firewall.py | 87 ++++++++++++++++++- 2 files changed, 98 insertions(+), 1 deletion(-) diff --git a/netjsonconfig/backends/openwrt/converters/firewall.py b/netjsonconfig/backends/openwrt/converters/firewall.py index 67fd2e56c..bc7d1617b 100644 --- a/netjsonconfig/backends/openwrt/converters/firewall.py +++ b/netjsonconfig/backends/openwrt/converters/firewall.py @@ -232,4 +232,16 @@ def __netjson_redirect(self, redirect): else: redirect["monthdays"] = [int(x) for x in monthdays] + if "utc_time" in redirect: + redirect["utc_time"] = redirect["utc_time"] == "1" + + if "reflection" in redirect: + redirect["reflection"] = redirect["reflection"] == "1" + + if "limit_burst" in redirect: + redirect["limit_burst"] = int(redirect["limit_burst"]) + + if "enabled" in redirect: + redirect["enabled"] = redirect["enabled"] == "1" + return self.type_cast(redirect) diff --git a/tests/openwrt/test_firewall.py b/tests/openwrt/test_firewall.py index f19b14006..bf96e9b3d 100644 --- a/tests/openwrt/test_firewall.py +++ b/tests/openwrt/test_firewall.py @@ -549,5 +549,90 @@ def test_redirect_monthdays_validation_error_2(self): def test_parse_redirect_3(self): o = OpenWrt(native=self._redirect_3_uci) - print(o.config) self.assertEqual(o.config, self._redirect_3_netjson) + + _redirect_4_uci = textwrap.dedent( + """\ + package firewall + + config defaults 'defaults' + + config redirect 'redirect_Adblock DNS, port 53' + option name 'Adblock DNS, port 53' + option src 'lan' + option proto 'tcpudp' + option src_dport '53' + option dest_port '53' + option target 'DNAT' + list weekdays 'mon' + list weekdays 'tue' + list weekdays 'wed' + list monthdays '1' + list monthdays '2' + list monthdays '31' + option src_ip '192.168.1.1' + option src_dip '192.168.1.1' + option src_mac 'AA:AA:AA:AA:AA:AA' + option src_port '1-1064' + option dest 'wan' + option dest_ip '10.0.0.1' + option ipset 'myipset' + option mark '0xff' + option start_date '2020-02-02' + option stop_date '2020-03-02' + option start_time '12:12:12' + option stop_time '23:23:23' + option utc_time '1' + option family 'any' + option reflection '0' + option reflection_src 'external' + option limit '3/sec' + option limit_burst '5' + option enabled '0' + """ + ) + + _redirect_4_netjson = { + "firewall": { + "redirects": [ + { + "name": "Adblock DNS, port 53", + "src": "lan", + "proto": ["tcp", "udp"], + "src_dport": "53", + "dest_port": "53", + "target": "DNAT", + "weekdays": ["mon", "tue", "wed"], + "monthdays": [1, 2, 31], + "src_ip": "192.168.1.1", + "src_dip": "192.168.1.1", + "src_mac": "AA:AA:AA:AA:AA:AA", + "src_port": "1-1064", + "dest": "wan", + "dest_ip": "10.0.0.1", + "ipset": "myipset", + "mark": "0xff", + "start_date": "2020-02-02", + "stop_date": "2020-03-02", + "start_time": "12:12:12", + "stop_time": "23:23:23", + "utc_time": True, + "family": "any", + "reflection": False, + "reflection_src": "external", + "limit": "3/sec", + "limit_burst": 5, + "enabled": False, + } + ] + } + } + + def test_render_redirect_4(self): + o = OpenWrt(self._redirect_4_netjson) + expected = self._tabs(self._redirect_4_uci) + self.assertEqual(o.render(), expected) + + def test_parse_redirect_4(self): + o = OpenWrt(native=self._redirect_4_uci) + self.assertEqual(o.config, self._redirect_4_netjson) From b21ac816f4b66fb23a259fdf738e2c1e791b0961 Mon Sep 17 00:00:00 2001 From: "Jonathan G. Underwood" Date: Sat, 1 Aug 2020 20:14:32 +0100 Subject: [PATCH 13/40] Refactor __netjson_redirect() to reduce complexity --- .../backends/openwrt/converters/firewall.py | 73 ++++++++++++------- 1 file changed, 45 insertions(+), 28 deletions(-) diff --git a/netjsonconfig/backends/openwrt/converters/firewall.py b/netjsonconfig/backends/openwrt/converters/firewall.py index bc7d1617b..9ac3eef14 100644 --- a/netjsonconfig/backends/openwrt/converters/firewall.py +++ b/netjsonconfig/backends/openwrt/converters/firewall.py @@ -199,38 +199,17 @@ def __netjson_forwarding(self, forwarding): def __netjson_redirect(self, redirect): if "proto" in redirect: - proto = redirect.pop("proto") - if not isinstance(proto, list): - if proto == "tcpudp": - redirect["proto"] = ["tcp", "udp"] - else: - redirect["proto"] = [proto] + redirect["proto"] = self.__netjson_redirect_proto(redirect["proto"]) if "weekdays" in redirect: - weekdays = redirect["weekdays"] - if not isinstance(weekdays, list): - weekdays = weekdays.split() - # UCI allows the first entry to be "!" which means negate the remaining - # entries - if weekdays[0] == "!": - all_days = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"] - wd = set([x for x in weekdays[1:]]) - redirect["weekdays"] = list(set(all_days) - wd) - # Sort the days for predictability when testing - redirect["weekdays"].sort(key=lambda v: all_days.index(v)) + redirect["weekdays"] = self.__netjson_redirect_weekdays( + redirect["weekdays"] + ) if "monthdays" in redirect: - monthdays = redirect["monthdays"] - if not isinstance(monthdays, list): - monthdays = monthdays.split() - # UCI allows the first entry to be "!" which means negate the remaining - # entries - if monthdays[0] == "!": - all_days = set(range(1, 32)) - md = set([int(x) for x in monthdays[1:]]) - redirect["monthdays"] = list(all_days - md) - else: - redirect["monthdays"] = [int(x) for x in monthdays] + redirect["monthdays"] = self.__netjson_redirect_monthdays( + redirect["monthdays"] + ) if "utc_time" in redirect: redirect["utc_time"] = redirect["utc_time"] == "1" @@ -245,3 +224,41 @@ def __netjson_redirect(self, redirect): redirect["enabled"] = redirect["enabled"] == "1" return self.type_cast(redirect) + + def __netjson_redirect_proto(self, proto): + if isinstance(proto, list): + return proto.copy() + else: + if proto == "tcpudp": + return ["tcp", "udp"] + else: + return proto.split() + + def __netjson_redirect_weekdays(self, weekdays): + if not isinstance(weekdays, list): + wd = weekdays.split() + else: + wd = weekdays.copy() + + # UCI allows the first entry to be "!" which means negate the remaining entries + if wd[0] == "!": + all_days = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"] + wd = [day for day in all_days if day not in wd[1:]] + + return wd + + def __netjson_redirect_monthdays(self, monthdays): + if not isinstance(monthdays, list): + md = monthdays.split() + else: + md = monthdays.copy() + + # UCI allows the first entry to be "!" which means negate the remaining entries + if md[0] == "!": + md = [int(day) for day in md[1:]] + all_days = range(1, 32) + md = [day for day in all_days if day not in md] + else: + md = [int(day) for day in md] + + return md From 986a49fb11468cfcfac76401249f47c5b11a7ef8 Mon Sep 17 00:00:00 2001 From: "Jonathan G. Underwood" Date: Sat, 1 Aug 2020 23:14:45 +0100 Subject: [PATCH 14/40] Refactor handling of proto parameter --- .../backends/openwrt/converters/firewall.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/netjsonconfig/backends/openwrt/converters/firewall.py b/netjsonconfig/backends/openwrt/converters/firewall.py index 9ac3eef14..cb1b8a49c 100644 --- a/netjsonconfig/backends/openwrt/converters/firewall.py +++ b/netjsonconfig/backends/openwrt/converters/firewall.py @@ -168,13 +168,9 @@ 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] + rule["proto"] = self.__netjson_generic_proto(rule["proto"]) return self.type_cast(rule) @@ -199,7 +195,7 @@ def __netjson_forwarding(self, forwarding): def __netjson_redirect(self, redirect): if "proto" in redirect: - redirect["proto"] = self.__netjson_redirect_proto(redirect["proto"]) + redirect["proto"] = self.__netjson_generic_proto(redirect["proto"]) if "weekdays" in redirect: redirect["weekdays"] = self.__netjson_redirect_weekdays( @@ -225,7 +221,7 @@ def __netjson_redirect(self, redirect): return self.type_cast(redirect) - def __netjson_redirect_proto(self, proto): + def __netjson_generic_proto(self, proto): if isinstance(proto, list): return proto.copy() else: From c5a79ce588ab32c99fa65b0cdf472c719f5517bf Mon Sep 17 00:00:00 2001 From: "Jonathan G. Underwood" Date: Sat, 1 Aug 2020 23:39:31 +0100 Subject: [PATCH 15/40] Refactor mac_address_regex usage in schema --- netjsonconfig/backends/openwrt/schema.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/netjsonconfig/backends/openwrt/schema.py b/netjsonconfig/backends/openwrt/schema.py index a20ff7f46..59def8978 100644 --- a/netjsonconfig/backends/openwrt/schema.py +++ b/netjsonconfig/backends/openwrt/schema.py @@ -12,7 +12,7 @@ port_range_regex = "^([1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(-([1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?$" # noqa # Match against a MAC address -mac_address_regex = "^([0-9A-F]{2}[:-]){5}([0-9A-F]{2})$" +mac_address_regex = "^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$" # Match against a yyyy-mm-dd format date. Note that draft07 of the JSON schema standard # include a "date" pattern which can replace this. @@ -100,7 +100,7 @@ "items": { "type": "string", "title": "MAC address", - "pattern": "^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$", + "pattern": mac_address_regex, "minLength": 17, "maxLength": 17, }, @@ -613,7 +613,7 @@ "title": "src_mac", "description": "match incoming traffic from the specified " "mac address", - "pattern": "^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$", + "pattern": mac_address_regex, "minLength": 17, "maxLength": 17, "propertyOrder": 4, @@ -760,6 +760,8 @@ "title": "src_mac", "description": "Match incoming traffic from the specified MAC address.", "pattern": mac_address_regex, + "minLength": 17, + "maxLength": 17, "propertyOrder": 5, }, "src_port": { From 51a254f67a6369c8cfedd930f85c245cb0f3c6dd Mon Sep 17 00:00:00 2001 From: "Jonathan G. Underwood" Date: Sun, 2 Aug 2020 14:15:43 +0100 Subject: [PATCH 16/40] Refactor firewall bool handling --- .../backends/openwrt/converters/firewall.py | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/netjsonconfig/backends/openwrt/converters/firewall.py b/netjsonconfig/backends/openwrt/converters/firewall.py index cb1b8a49c..bfeb3019e 100644 --- a/netjsonconfig/backends/openwrt/converters/firewall.py +++ b/netjsonconfig/backends/openwrt/converters/firewall.py @@ -167,7 +167,7 @@ def to_netjson_loop(self, block, result, index): def __netjson_rule(self, rule): if "enabled" in rule: - rule["enabled"] = rule.pop("enabled") == "1" + rule["enabled"] = self.__netjson_generic_boolean(rule["enabled"]) if "proto" in rule: rule["proto"] = self.__netjson_generic_proto(rule["proto"]) @@ -182,11 +182,9 @@ def __netjson_zone(self, zone): 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" + for param in ["mtu_fix", "masq"]: + if param in zone: + zone[param] = self.__netjson_generic_boolean(zone[param]) return self.type_cast(zone) @@ -207,20 +205,22 @@ def __netjson_redirect(self, redirect): redirect["monthdays"] ) - if "utc_time" in redirect: - redirect["utc_time"] = redirect["utc_time"] == "1" - - if "reflection" in redirect: - redirect["reflection"] = redirect["reflection"] == "1" + for param in ["utc_time", "reflection", "enabled"]: + if param in redirect: + redirect[param] = self.__netjson_generic_boolean(redirect[param]) if "limit_burst" in redirect: redirect["limit_burst"] = int(redirect["limit_burst"]) - if "enabled" in redirect: - redirect["enabled"] = redirect["enabled"] == "1" - return self.type_cast(redirect) + def __netjson_generic_boolean(self, boolean): + # Per convention, boolean options may have one of the values '0', 'no', 'off', + # 'false' or 'disabled' to specify a false value or '1' , 'yes', 'on', 'true' or + # 'enabled' to specify a true value. + # https://openwrt.org/docs/guide-user/base-system/uci + return boolean in ["1", "yes", "on", "true", "enabled"] + def __netjson_generic_proto(self, proto): if isinstance(proto, list): return proto.copy() From f0aac957ca19dc93ee092f3732ce2e0bb44562c6 Mon Sep 17 00:00:00 2001 From: "Jonathan G. Underwood" Date: Sun, 2 Aug 2020 22:50:40 +0100 Subject: [PATCH 17/40] Refactor the OpenWRT schema for ease of reading This commit decomposes the OpenWRT schema, splitting out the firewall part. This makes evolving the schema easier. --- netjsonconfig/backends/openwrt/schema.py | 1184 +++++++++++----------- 1 file changed, 589 insertions(+), 595 deletions(-) diff --git a/netjsonconfig/backends/openwrt/schema.py b/netjsonconfig/backends/openwrt/schema.py index 59def8978..5c297b64b 100644 --- a/netjsonconfig/backends/openwrt/schema.py +++ b/netjsonconfig/backends/openwrt/schema.py @@ -22,11 +22,23 @@ # Match against a time in the format hh:mm:ss time_regex = "^([01][0-9]|2[0123])(:([012345][0-9])){2}$" +# Match against a range of IPv4 addresses +ipv4_cidr_regex = "^([0-9]{1,3}.){3}[0-9]{1,3}(/([0-9]|[1-2][0-9]|3[0-2]))?$" + +# Match against a negatable range of IPv4 addresses. This variant allows for an optional +# "!" in front of the CIDR. +ipv4_negatable_cidr_regex = ( + "^!?([0-9]{1,3}.){3}[0-9]{1,3}(/([0-9]|[1-2][0-9]|3[0-2]))?$" +) + +# Match against a range of IPv6 addresses +ipv6_cidr_regex = "^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$" # noqa + schema = merge_config( default_schema, { "definitions": { - "interface_settings": { + "interface_settings": { # Overrides default schema "properties": { "network": { "type": "string", @@ -38,7 +50,7 @@ } } }, - "wireless_interface": { + "wireless_interface": { # Overrides default schema "properties": { "wireless": { "properties": { @@ -60,7 +72,7 @@ } } }, - "ap_wireless_settings": { + "ap_wireless_settings": { # Overrides default schema "allOf": [ { "properties": { @@ -109,7 +121,7 @@ } ] }, - "bridge_interface": { + "bridge_interface": { # Overrides default schema "allOf": [ { "properties": { @@ -125,27 +137,7 @@ } ] }, - "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", - }, - "base_radio_settings": { + "base_radio_settings": { # Overrides default schema "properties": { "driver": { "type": "string", @@ -179,16 +171,16 @@ } } }, - "radio_80211gn_settings": { + "radio_80211gn_settings": { # Overrides default schema "allOf": [{"$ref": "#/definitions/radio_hwmode_11g"}] }, - "radio_80211an_settings": { + "radio_80211an_settings": { # Overrides default schema "allOf": [{"$ref": "#/definitions/radio_hwmode_11a"}] }, - "radio_80211ac_2ghz_settings": { + "radio_80211ac_2ghz_settings": { # Overrides default schema "allOf": [{"$ref": "#/definitions/radio_hwmode_11g"}] }, - "radio_80211ac_5ghz_settings": { + "radio_80211ac_5ghz_settings": { # Overrides default schema "allOf": [{"$ref": "#/definitions/radio_hwmode_11a"}] }, }, @@ -419,578 +411,580 @@ }, }, }, - "firewall": { - "type": "object", - "title": "Firewall", - "additionalProperties": True, + }, + }, +) + +firewall_definitions = { + "firewall": { + "name": { + "type": "string", + "title": "name", + "description": "Name of redirect", + "propertyOrder": 1, + }, + "enabled": { + "type": "boolean", + "title": "enable", + "description": "Enable this configuration entity.", + "default": True, + "format": "checkbox", + "propertyOrder": 2, + }, + "zone_name": { + "type": "string", + "title": "A Zone name", + "description": "A unique zone name. Has a maximum" + "length of 11 characters.", + "maxLength": 11, + "propertyOrder": 3, + }, + "ipv4_negatable_cidr": { + "type": "string", + "pattern": ipv4_negatable_cidr_regex, + }, + "src": { + "type": "string", + "title": "src", + "description": "Specifies the traffic source zone. " + "Must refer to one of the defined zone names. " + "For typical port forwards this usually is wan.", + "maxLength": 11, + "propertyOrder": 4, + }, + "src_ip": { + "type": "string", + "title": "src_ip", + "description": "Match incoming traffic from the specified source ip " + "address.", + "propertyOrder": 5, + }, + "src_mac": { + "type": "string", + "title": "src_mac", + "description": "Match incoming traffic from the specified MAC address.", + "pattern": mac_address_regex, + "minLength": 17, + "maxLength": 17, + "propertyOrder": 6, + }, + "src_port": { + "type": "string", + "title": "src_port", + "description": "Match incoming traffic originating from the given source " + "port or port range on the client host.", + "pattern": port_range_regex, + "propertyOrder": 7, + }, + "proto": { + "type": "array", + "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": ["tcp", "udp"], + "propertyOrder": 8, + "items": {"title": "Protocol type", "type": "string"}, + }, + "dest": { + "type": "string", + "title": "dest", + "description": "Specifies the traffic destination zone. Must refer to " + "on of the defined zone names. For DNAT target on Attitude Adjustment, " + 'NAT reflection works only if this is equal to "lan".', + "maxLength": 11, + "propertyOrder": 9, + }, + "dest_ip": { + "type": "string", + "title": "dest_ip", + "description": "For DNAT, redirect matches incoming traffic to the " + "specified internal host. For SNAT, it matches traffic directed at " + "the given address. For DNAT, if the dest_ip is not specified, the rule " + "is translated in a iptables/REDIRECT rule, otherwise it is a " + "iptables/DNAT rule.", + # "format": "ipv4", + "propertyOrder": 10, + }, + "dest_port": { + "type": "string", + "title": "dest_port", + "description": "For DNAT, redirect matched incoming traffic to the given " + "port on the internal host. For SNAT, match traffic directed at the " + "given ports. Only a single port or range can be specified.", + "pattern": port_range_regex, + "propertyOrder": 11, + }, + "ipset": { + "type": "string", + "title": "ipset", + "description": "Match traffic against the given ipset. The match can be " + "inverted by prefixing the value with an exclamation mark.", + "propertyOrder": 12, + }, + "mark": { + "type": "string", + "title": "mark", + "description": 'Match traffic against the given firewall mark, e.g. ' + '"0xFF" to match mark 255 or "0x0/0x1" to match any even mark value. ' + 'The match can be inverted by prefixing the value with an exclamation ' + 'mark, e.g. "!0x10" to match all but mark #16.', + "propertyOrder": 13, + }, + "start_date": { + "type": "string", + "title": "start_date", + "description": "Only match traffic after the given date (inclusive).", + "pattern": date_regex, + # "format": "date", TODO: replace pattern with this + # when adopt draft07 + "propertyOrder": 14, + }, + "stop_date": { + "type": "string", + "title": "stop_date", + "description": "Only match traffic before the given date (inclusive).", + "pattern": date_regex, + # "format": "date", TODO: replace pattern with this + # when adopt draft07 + "propertyOrder": 15, + }, + "start_time": { + "type": "string", + "title": "start_time", + "description": "Only match traffic after the given time of day " + "(inclusive).", + "pattern": time_regex, + "propertyOrder": 16, + }, + "stop_time": { + "type": "string", + "title": "stop_time", + "description": "Only match traffic before the given time of day " + "(inclusive).", + "pattern": time_regex, + "propertyOrder": 17, + }, + # Note: here we don't support negation of values like + # the UCI syntax does, as it's not necessary. + "weekdays": { + "type": "array", + "title": "weekdays", + "description": "Only match traffic during the given week days, " + 'e.g. ["sun", "mon", "thu", "fri"] to only match on Sundays, ' + "Mondays, Thursdays and Fridays.", + "propertyOrder": 18, + "items": { + "type": "string", + "title": "weekday", + "enum": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], + }, + }, + # Note: here we don't support negation of values like + # the UCI syntax does, as it's not necessary. + "monthdays": { + "type": "array", + "title": "monthdays", + "description": "Only match traffic during the given days of the " + "month, e.g. [2, 5, 30] to only match on every 2nd, 5th and 30th " + "day of the month.", + "propertyOrder": 19, + "items": { + "type": "integer", + "title": "day of month", + "minimum": 1, + "maximum": 31, + }, + }, + "utc_time": { + "type": "boolean", + "title": "utc_time", + "description": "Treat all given time values as UTC time instead of local " + "time.", + "default": False, + "propertyOrder": 20, + }, + "family": { + "type": "string", + "title": "family", + "description": "Protocol family (ipv4, ipv6 or any) to generate iptables " + "rules for", + "enum": ["ipv4", "ipv6", "any"], + "default": "any", + "propertyOrder": 22, + }, + "limit": { + "type": "string", + "title": "limit", + "description": "Maximum average matching rate; specified as a number, " + "with an optional /second, /minute, /hour or /day suffix. " + "Examples: 3/second, 3/sec or 3/s.", + "propertyOrder": 25, + }, + "limit_burst": { + "type": "integer", + "title": "limit_burst", + "description": "Maximum initial number of packets to match, allowing a " + "short-term average above limit.", + "default": 5, + "propertyOrder": 26, + }, + "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", + }, + }, +} + +firewall_redirect_properties = { + "name": {"$ref": "#/definitions/firewall/name"}, + "enabled": {"$ref": "#/definitions/firewall/enabled"}, + "src": {"$ref": "#/definitions/firewall/src"}, + "src_ip": { + "allOf": [ + {"$ref": "#/definitions/firewall/src_ip"}, + {"pattern": ipv4_cidr_regex}, + ], + }, + "src_mac": {"$ref": "#/definitions/firewall/src_mac"}, + "src_port": {"$ref": "#/definitions/firewall/src_port"}, + "proto": {"$ref": "#/definitions/firewall/proto"}, + "dest": {"$ref": "#/definitions/firewall/dest"}, + "dest_ip": {"$ref": "#/definitions/firewall/dest_ip"}, + "dest_port": {"$ref": "#/definitions/firewall/dest_port"}, + "ipset": {"$ref": "#/definitions/firewall/ipset"}, + "mark": {"$ref": "#/definitions/firewall/mark"}, + "start_date": {"$ref": "#/definitions/firewall/start_date"}, + "stop_date": {"$ref": "#/definitions/firewall/stop_date"}, + "start_time": {"$ref": "#/definitions/firewall/start_time"}, + "stop_time": {"$ref": "#/definitions/firewall/stop_time"}, + "weekdays": {"$ref": "#/definitions/firewall/weekdays"}, + "monthdays": {"$ref": "#/definitions/firewall/monthdays"}, + "utc_time": {"$ref": "#/definitions/firewall/utc_time"}, + "family": {"$ref": "#/definitions/firewall/family"}, + "limit": {"$ref": "#/definitions/firewall/limit"}, + "limit_burst": {"$ref": "#/definitions/firewall/limit_burst"}, + "src_dip": { + "type": "string", + "title": "src_dip", + "description": "For DNAT, match incoming traffic directed at the " + "given destination ip address. For SNAT rewrite the source address " + "to the given address.", + "format": "ipv4", + "propertyOrder": 101, + }, + "src_dport": { + "type": "string", + "title": "src_dport", + "description": "For DNAT, match incoming traffic directed at the given " + "destination port or port range on this host. For SNAT rewrite the " + "source ports to the given value.", + "pattern": port_range_regex, + "propertyOrder": 102, + }, + "reflection": { + "type": "boolean", + "title": "reflection", + "description": "Activate NAT reflection for this redirect. Applicable to " + "DNAT targets.", + "default": True, + "propertyOrder": 103, + }, + "reflection_src": { + "type": "string", + "title": "reflection_src", + "description": "The source address to use for NAT-reflected packets if " + "reflection is True. This can be internal or external, specifying which " + "interface’s address to use. Applicable to DNAT targets.", + "enum": ["internal", "external"], + "default": "internal", + "propertyOrder": 104, + }, + "target": { + "type": "string", + "title": "target", + "description": "NAT target (DNAT or SNAT) to use when generating the rule.", + "enum": ["DNAT", "SNAT"], + "default": "DNAT", + "propertyOrder": 21, + }, +} + +firewall_rules_properties = { + "name": {"$ref": "#/definitions/firewall/name"}, + "enabled": {"$ref": "#/definitions/firewall/enabled"}, + "src": {"$ref": "#/definitions/firewall/src"}, + "src_ip": { + "allOf": [ + {"$ref": "#/definitions/firewall/src_ip"}, + {"oneOf": [{"pattern": ipv4_cidr_regex}, {"pattern": ipv6_cidr_regex}]}, + ], + }, + "src_mac": {"$ref": "#/definitions/firewall/src_mac"}, + "src_port": {"$ref": "#/definitions/firewall/src_port"}, + "proto": {"$ref": "#/definitions/firewall/proto"}, + "dest": {"$ref": "#/definitions/firewall/dest"}, + "dest_ip": {"$ref": "#/definitions/firewall/dest_ip"}, + "dest_port": {"$ref": "#/definitions/firewall/dest_port"}, + "ipset": {"$ref": "#/definitions/firewall/ipset"}, + "mark": {"$ref": "#/definitions/firewall/mark"}, + "start_date": {"$ref": "#/definitions/firewall/start_date"}, + "stop_date": {"$ref": "#/definitions/firewall/stop_date"}, + "start_time": {"$ref": "#/definitions/firewall/start_time"}, + "stop_time": {"$ref": "#/definitions/firewall/stop_time"}, + "weekdays": {"$ref": "#/definitions/firewall/weekdays"}, + "monthdays": {"$ref": "#/definitions/firewall/monthdays"}, + "utc_time": {"$ref": "#/definitions/firewall/utc_time"}, + "family": {"$ref": "#/definitions/firewall/family"}, + "limit": {"$ref": "#/definitions/firewall/limit"}, + "limit_burst": {"$ref": "#/definitions/firewall/limit_burst"}, + "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": 101, + "items": {"title": "ICMP type", "type": "string"}, + }, + "target": { + "allOf": [ + {"$ref": "#/definitions/firewall/rule_policy"}, + { + "title": "target", + "description": "firewall action for matched traffic", "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": mac_address_regex, - "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": "array", - "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": ["tcp", "udp"], - "propertyOrder": 6, - "items": { - "title": "Protocol type", - "type": "string", - }, - }, - "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", - "description": "Enable this rule.", - "default": True, - "format": "checkbox", - "propertyOrder": 14, - }, - }, - }, - }, - "redirects": { - "type": "array", - "title": "Redirects", - "propertyOrder": 8, - "items": { - "type": "object", - "title": "Redirect", - "additionalProperties": False, - "properties": { - "name": { - "type": "string", - "title": "name", - "description": "Name of redirect", - "propertyOrder": 1, - }, - "src": { - "type": "string", - "title": "src", - "description": "Specifies the traffic source zone. " - "Must refer to one of the defined zone names. " - "For typical port forwards this usually is wan.", - "propertyOrder": 2, - }, - "src_ip": { - "type": "string", - "title": "src_ip", - "description": "Match incoming traffic from the specified source ip " - "address.", - "format": "ipv4", - "propertyOrder": 3, - }, - "src_dip": { - "type": "string", - "title": "src_dip", - "description": "For DNAT, match incoming traffic directed at the " - "given destination ip address. For SNAT rewrite the source address " - "to the given address.", - "format": "ipv4", - "propertyOrder": 4, - }, - "src_mac": { - "type": "string", - "title": "src_mac", - "description": "Match incoming traffic from the specified MAC address.", - "pattern": mac_address_regex, - "minLength": 17, - "maxLength": 17, - "propertyOrder": 5, - }, - "src_port": { - "type": "string", - "title": "src_port", - "description": "Match incoming traffic originating from the given source " - "port or port range on the client host.", - "pattern": port_range_regex, - "propertyOrder": 6, - }, - "src_dport": { - "type": "string", - "title": "src_dport", - "description": "For DNAT, match incoming traffic directed at the given " - "destination port or port range on this host. For SNAT rewrite the " - "source ports to the given value.", - "pattern": port_range_regex, - "propertyOrder": 7, - }, - "proto": { - "type": "array", - "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": ["tcp", "udp"], - "propertyOrder": 8, - "items": { - "title": "Protocol type", - "type": "string", - }, - }, - "dest": { - "type": "string", - "title": "dest", - "description": "Specifies the traffic destination zone. Must refer to " - "on of the defined zone names. For DNAT target on Attitude Adjustment, " - 'NAT reflection works only if this is equal to "lan".', - "propertyOrder": 9, - }, - "dest_ip": { - "type": "string", - "title": "dest_ip", - "description": "For DNAT, redirect matches incoming traffic to the " - "specified internal host. For SNAT, it matches traffic directed at " - "the given address. For DNAT, if the dest_ip is not specified, the rule " - "is translated in a iptables/REDIRECT rule, otherwise it is a " - "iptables/DNAT rule.", - "format": "ipv4", - "propertyOrder": 10, - }, - "dest_port": { - "type": "string", - "title": "dest_port", - "description": "For DNAT, redirect matched incoming traffic to the given " - "port on the internal host. For SNAT, match traffic directed at the " - "given ports. Only a single port or range can be specified.", - "pattern": port_range_regex, - "propertyOrder": 11, - }, - "ipset": { - "type": "string", - "title": "ipset", - "description": "Match traffic against the given ipset. The match can be " - "inverted by prefixing the value with an exclamation mark.", - "propertyOrder": 12, - }, - "mark": { - "type": "string", - "title": "mark", - "description": 'Match traffic against the given firewall mark, e.g. ' - '"0xFF" to match mark 255 or "0x0/0x1" to match any even mark value. ' - 'The match can be inverted by prefixing the value with an exclamation ' - 'mark, e.g. "!0x10" to match all but mark #16.', - "propertyOrder": 13, - }, - "start_date": { - "type": "string", - "title": "start_date", - "description": "Only match traffic after the given date (inclusive).", - "pattern": date_regex, - # "format": "date", TODO: replace pattern with this - # when adopt draft07 - "propertyOrder": 14, - }, - "stop_date": { - "type": "string", - "title": "stop_date", - "description": "Only match traffic before the given date (inclusive).", - "pattern": date_regex, - # "format": "date", TODO: replace pattern with this - # when adopt draft07 - "propertyOrder": 15, - }, - "start_time": { - "type": "string", - "title": "start_time", - "description": "Only match traffic after the given time of day " - "(inclusive).", - "pattern": time_regex, - "propertyOrder": 16, - }, - "stop_time": { - "type": "string", - "title": "stop_time", - "description": "Only match traffic before the given time of day " - "(inclusive).", - "pattern": time_regex, - "propertyOrder": 17, - }, - # Note: here we don't support negation of values like - # the UCI syntax does, as it's not necessary. - "weekdays": { - "type": "array", - "title": "weekdays", - "description": "Only match traffic during the given week days, " - 'e.g. ["sun", "mon", "thu", "fri"] to only match on Sundays, ' - "Mondays, Thursdays and Fridays.", - "propertyOrder": 18, - "items": { - "type": "string", - "title": "weekday", - "enum": [ - "mon", - "tue", - "wed", - "thu", - "fri", - "sat", - "sun", - ], - }, - }, - # Note: here we don't support negation of values like - # the UCI syntax does, as it's not necessary. - "monthdays": { - "type": "array", - "title": "monthdays", - "description": "Only match traffic during the given days of the " - "month, e.g. [2, 5, 30] to only match on every 2nd, 5th and 30th " - "day of the month.", - "propertyOrder": 19, - "items": { - "type": "integer", - "title": "day of month", - "minimum": 1, - "maximum": 31, - }, - }, - "utc_time": { - "type": "boolean", - "title": "utc_time", - "description": "Treat all given time values as UTC time instead of local " - "time.", - "default": False, - "propertyOrder": 20, - }, - "target": { - "type": "string", - "title": "target", - "description": "NAT target (DNAT or SNAT) to use when generating the " - "rule.", - "enum": ["DNAT", "SNAT"], - "default": "DNAT", - "propertyOrder": 21, - }, - "family": { - "type": "string", - "title": "family", - "description": "Protocol family (ipv4, ipv6 or any) to generate iptables " - "rules for", - "enum": ["ipv4", "ipv6", "any"], - "default": "any", - "propertyOrder": 22, - }, - "reflection": { - "type": "boolean", - "title": "reflection", - "description": "Activate NAT reflection for this redirect. Applicable to " - "DNAT targets.", - "default": True, - "propertyOrder": 23, - }, - "reflection_src": { - "type": "string", - "title": "reflection_src", - "description": "The source address to use for NAT-reflected packets if " - "reflection is True. This can be internal or external, specifying which " - "interface’s address to use. Applicable to DNAT targets.", - "enum": ["internal", "external"], - "default": "internal", - "propertyOrder": 24, - }, - "limit": { - "type": "string", - "title": "limit", - "description": "Maximum average matching rate; specified as a number, " - "with an optional /second, /minute, /hour or /day suffix. " - "Examples: 3/second, 3/sec or 3/s.", - "propertyOrder": 25, - }, - "limit_burst": { - "type": "integer", - "title": "limit_burst", - "description": "Maximum initial number of packets to match, allowing a " - "short-term average above limit.", - "default": 5, - "propertyOrder": 26, - }, - "enabled": { - "type": "boolean", - "title": "enable", - "description": "Enable this redirect.", - "default": True, - "format": "checkbox", - "propertyOrder": 27, - }, - }, - }, - }, + }, + ] + }, +} + +firewall_forwardings_properties = { + "name": {"$ref": "#/definitions/firewall/name"}, + "enabled": {"$ref": "#/definitions/firewall/enabled"}, + "src": {"$ref": "#/definitions/firewall/src"}, + "dest": {"$ref": "#/definitions/firewall/dest"}, + "family": {"$ref": "#/definitions/firewall/family"}, +} + +# Note: this is currently incomplete and needs other properties adding +# https://openwrt.org/docs/guide-user/firewall/firewall_configuration#zones +firewall_zones_properties = { + "name": {"$ref": "#/definitions/firewall/zone_name"}, + "enabled": {"$ref": "#/definitions/firewall/enabled"}, + "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 whether 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/firewall/zone_policy"}, + { + "title": "Input policy", + "description": "Default policy for incoming zone traffic.", + "propertyOrder": 5, + }, + ] + }, + "output": { + "allOf": [ + {"$ref": "#/definitions/firewall/zone_policy"}, + { + "title": "Output policy", + "description": "Default policy for outgoing zone traffic.", + "propertyOrder": 6, + }, + ] + }, + "forward": { + "allOf": [ + {"$ref": "#/definitions/firewall/zone_policy"}, + { + "title": "Forward policy.", + "description": "Default policy for forwarded zone traffic.", + "propertyOrder": 7, + }, + ] + }, + "masq_src": { + "type": "array", + "title": "Masqueraded source CIDR list.", + "description": "List of source IPv4 CIDRs that require masquerading.", + "propertyOrder": 8, + "items": { + "allOf": [ + {"$ref": "#/definitions/firewall/ipv4_cidr"}, + { + "title": "Masqueraded source CIDR.", + "description": "Source CIDR to enable masquerading for. " + 'Negation is possible by prefixing the subnet with a "!". ', + }, + ], + }, + }, + "masq_dest": { + "type": "array", + "title": "Masqueraded destination CIDR list.", + "description": "List of destination IPv4 CIDRs that require masquerading.", + "propertyOrder": 9, + "items": { + "allOf": [ + {"$ref": "#/definitions/firewall/ipv4_cidr"}, + { + "title": "Masquerade destination CIDR.", + "description": "Destination CIDR to enable masquerading for. " + 'Negation is possible by prefixing the subnet with a "!". ', }, + ], + }, + }, +} + +firewall_properties = { + "syn_flood": { + "type": "boolean", + "title": "enable SYN flood protection", + "default": False, + "format": "checkbox", + "propertyOrder": 1, + }, + "input": { + "allOf": [ + {"$ref": "#/definitions/firewall/firewall_policy"}, + { + "title": "input", + "description": "policy for the INPUT chain of the filter table", + "propertyOrder": 2, + }, + ] + }, + "output": { + "allOf": [ + {"$ref": "#/definitions/firewall/firewall_policy"}, + { + "title": "output", + "description": "policy for the OUTPUT chain of the filter table", + "propertyOrder": 3, }, + ] + }, + "forward": { + "allOf": [ + {"$ref": "#/definitions/firewall/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": firewall_forwardings_properties, }, }, -) + "zones": { + "type": "array", + "title": "Zones", + "propertyOrder": 6, + "items": { + "type": "object", + "title": "Zones", + "additionalProperties": True, + "required": ["name"], + "properties": firewall_zones_properties, + }, + }, + "rules": { + "type": "array", + "title": "Rules", + "propertyOrder": 7, + "items": { + "type": "object", + "title": "Rules", + "additionalProperties": True, + "required": ["src", "target"], + "properties": firewall_rules_properties, + }, + }, + "redirects": { + "type": "array", + "title": "Redirects", + "propertyOrder": 8, + "items": { + "type": "object", + "title": "Redirect", + "additionalProperties": False, + "properties": firewall_redirect_properties, + }, + }, +} + +firewall = { + "definitions": firewall_definitions, + "properties": { + "firewall": { + "type": "object", + "title": "Firewall", + "additionalProperties": True, + "propertyOrder": 11, + "properties": firewall_properties, + }, + }, +} +schema = merge_config(schema, firewall) # add OpenVPN schema schema = merge_config(schema, base_openvpn_schema) From 976ecea19cb6d3ee01b9a4b2c95529658cebe5c5 Mon Sep 17 00:00:00 2001 From: "Jonathan G. Underwood" Date: Mon, 17 Aug 2020 00:23:30 +0100 Subject: [PATCH 18/40] Add more OpenWRT firewall zone parameters --- netjsonconfig/backends/openwrt/schema.py | 45 ++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/netjsonconfig/backends/openwrt/schema.py b/netjsonconfig/backends/openwrt/schema.py index 5c297b64b..dbe391be3 100644 --- a/netjsonconfig/backends/openwrt/schema.py +++ b/netjsonconfig/backends/openwrt/schema.py @@ -723,7 +723,7 @@ "description": "NAT target (DNAT or SNAT) to use when generating the rule.", "enum": ["DNAT", "SNAT"], "default": "DNAT", - "propertyOrder": 21, + "propertyOrder": 105, }, } @@ -882,12 +882,53 @@ ], }, }, + "masq_allow_invalid": { + "type": "boolean", + "title": "Allow invalid packets.", + "description": "Do not add DROP INVALID rules to the firewall if masquerading " + "is used. The DROP rules are supposed to prevent NAT leakage." + "default": False, + "format": "checkbox", + "propertyOrder": 10, + }, + "family": {"$ref": "#/definitions/firewall/family"}, + "log": { + "type": "integer", + "title": "Enable logging for the filter and/or mangle table.", + "description": "Bit field to enable logging in the filter and/or mangle tables, " + "bit 0 = filter, bit 1 = mangle.", + "min": 0, + "max": 3, + "default": 0, + "propertyOrder": 10, + }, + "log_limit": { + "type": "string", + "title": "Limit on the number of log messages.", + "description": "Limits the amount of log messages per interval. For example, " + '"10/minute" will limit the logging to 10 messages per minute', + "default": "10/minute", + "propertyOrder": 11, + }, + "device": { + "type": "array", + "title": "Raw devices to attach to this zone." + "description": "A list of raw device names to associate with this zone. " + "items": { + "type": "string", + "title": "A device to attach to the zone.", + "description": "A device to attach to the zone." + 'For example, "ppp+" to match any PPP interface to the zone.', + }, + "propertyOrder": 12, + }, } firewall_properties = { "syn_flood": { "type": "boolean", - "title": "enable SYN flood protection", + "title": "SYN flood protection.", + "description": "Enables SYN flood protection.", "default": False, "format": "checkbox", "propertyOrder": 1, From 0d82e0e5613ba8ff4c710ef9ef1dc469661a6e3e Mon Sep 17 00:00:00 2001 From: "Jonathan G. Underwood" Date: Fri, 16 Oct 2020 17:11:10 +0100 Subject: [PATCH 19/40] [openwrt] Fix formatting error --- netjsonconfig/backends/openwrt/schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netjsonconfig/backends/openwrt/schema.py b/netjsonconfig/backends/openwrt/schema.py index 6acb52c1f..134a6c000 100644 --- a/netjsonconfig/backends/openwrt/schema.py +++ b/netjsonconfig/backends/openwrt/schema.py @@ -960,7 +960,7 @@ }, "device": { "type": "array", - "title": "Raw devices to attach to this zone.", + "title": "Raw devices to attach to this zone.", "description": "A list of raw device names to associate with this zone. ", "items": { "type": "string", From 5b3438c57bc6e6ea10b77b59cf4ab87c9868647b Mon Sep 17 00:00:00 2001 From: "Jonathan G. Underwood" Date: Fri, 16 Oct 2020 18:05:41 +0100 Subject: [PATCH 20/40] [openwrt] Fix unnecessary quotation changes --- .../backends/openwrt/converters/__init__.py | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/netjsonconfig/backends/openwrt/converters/__init__.py b/netjsonconfig/backends/openwrt/converters/__init__.py index 2864d12c9..9e400a21d 100644 --- a/netjsonconfig/backends/openwrt/converters/__init__.py +++ b/netjsonconfig/backends/openwrt/converters/__init__.py @@ -12,16 +12,16 @@ from .wireless import Wireless __all__ = [ - "Default", - "Interfaces", - "General", - "Led", - "Ntp", - "OpenVpn", - "Radios", - "Routes", - "Rules", - "Switch", - "Wireless", - "Firewall", + 'Default', + 'Interfaces', + 'General', + 'Led', + 'Ntp', + 'OpenVpn', + 'Radios', + 'Routes', + 'Rules', + 'Switch', + 'Wireless', + 'Firewall', ] From 9f6c0fcbf8923d8e97f1ad9e3d95c7a034d05769 Mon Sep 17 00:00:00 2001 From: "Jonathan G. Underwood" Date: Fri, 16 Oct 2020 18:10:35 +0100 Subject: [PATCH 21/40] [openwrt] Remove debugging print statements --- tests/openwrt/test_default.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/openwrt/test_default.py b/tests/openwrt/test_default.py index f21f04b8d..cabf9c6a7 100644 --- a/tests/openwrt/test_default.py +++ b/tests/openwrt/test_default.py @@ -175,11 +175,6 @@ def test_parse_default(self): ], } - print("*" * 80) - import json - - print(json.dumps(o.config, indent=4)) - print("*" * 80) self.assertDictEqual(o.config, expected) def test_skip(self): From 4c134bcf97f6efeca47ef8040d838a8e12d758e1 Mon Sep 17 00:00:00 2001 From: "Jonathan G. Underwood" Date: Sat, 30 Jan 2021 13:21:49 +0000 Subject: [PATCH 22/40] [openwrt] Add firewall rules tests --- tests/openwrt/test_firewall.py | 43 ++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/openwrt/test_firewall.py b/tests/openwrt/test_firewall.py index bf96e9b3d..eb4236d0f 100644 --- a/tests/openwrt/test_firewall.py +++ b/tests/openwrt/test_firewall.py @@ -178,6 +178,49 @@ def test_parse_rule_4(self): o = OpenWrt(native=self._rule_4_uci) self.assertEqual(o.config, self._rule_4_netjson) + _rule_5_netjson = { + "firewall": { + "rules": [ + { + "name": "Allow-Isolated-DHCP", + "src_ip": "10.10.10.10", + "src_mac": "fc:aa:14:18:12:98", + "src": "isolated", + "proto": ["udp"], + "dest_port": "67-68", + "target": "ACCEPT", + + } + ] + } + } + + _rule_5_uci = textwrap.dedent( + """\ + package firewall + + config defaults 'defaults' + + config rule 'rule_Allow_Isolated_DHCP' + option name 'Allow-Isolated-DHCP' + option src_ip '10.10.10.10' + option src_mac 'fc:aa:14:18:12:98' + option src 'isolated' + option proto 'udp' + option dest_port '67-68' + option target 'ACCEPT' + """ + ) + + def test_render_rule_5(self): + o = OpenWrt(self._rule_5_netjson) + expected = self._tabs(self._rule_5_uci) + self.assertEqual(o.render(), expected) + + def test_parse_rule_5(self): + o = OpenWrt(native=self._rule_5_uci) + self.assertEqual(o.config, self._rule_5_netjson) + _zone_1_netjson = { "firewall": { "zones": [ From e65f06c86b949e52f872b97a6d178867a0dd77e5 Mon Sep 17 00:00:00 2001 From: "Jonathan G. Underwood" Date: Sat, 30 Jan 2021 21:09:14 +0000 Subject: [PATCH 23/40] [openwrt] Add firewall rule test --- tests/openwrt/test_firewall.py | 71 +++++++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/tests/openwrt/test_firewall.py b/tests/openwrt/test_firewall.py index eb4236d0f..0a92bf221 100644 --- a/tests/openwrt/test_firewall.py +++ b/tests/openwrt/test_firewall.py @@ -189,7 +189,6 @@ def test_parse_rule_4(self): "proto": ["udp"], "dest_port": "67-68", "target": "ACCEPT", - } ] } @@ -221,6 +220,76 @@ def test_parse_rule_5(self): o = OpenWrt(native=self._rule_5_uci) self.assertEqual(o.config, self._rule_5_netjson) + _rule_6_netjson = { + "firewall": { + "rules": [ + { + "name": "Allow-Isolated-DHCP", + "src_ip": "10.10.10.10", + "src_mac": "fc:aa:14:18:12:98", + "src": "isolated", + "proto": ["udp"], + "dest_port": "67-68", + "target": "ACCEPT", + "dest": "dest_zone", + "dest_ip": "192.168.1.2", + "ipset": "my_ipset", + "mark": "DROP", + "start_date": "2021-01-21", + "stop_date": "2021-01-22", + "start_time": "01:01:01", + "stop_time": "11:11:11", + "weekdays": ["sun", "mon"], + "monthdays": [2, 10], + "utc_time": True, + "family": "any", + "limit": "3/second", + "limit_burst": 30, + "enabled": True, + } + ] + } + } + + _rule_6_uci = textwrap.dedent( + """\ + package firewall + + config defaults 'defaults' + + config rule 'rule_Allow_Isolated_DHCP' + option name 'Allow-Isolated-DHCP' + option src_ip '10.10.10.10' + option src_mac 'fc:aa:14:18:12:98' + option src 'isolated' + option proto 'udp' + option dest_port '67-68' + option target 'ACCEPT' + option dest 'dest_zone' + option dest_ip '192.168.1.2' + option ipset 'my_ipset' + option mark 'DROP' + option start_date '2021-01-21' + option stop_date '2021-01-22' + option start_time '01:01:01' + option stop_time '11:11:11' + list weekdays 'sun' + list weekdays 'mon' + list monthdays '2' + list monthdays '10' + option utc_time '1' + option family 'any' + option limit '3/second' + option limit_burst '30' + option enabled '1' + """ + ) + + def test_render_rule_6(self): + o = OpenWrt(self._rule_6_netjson) + expected = self._tabs(self._rule_6_uci) + self.assertEqual(o.render(), expected) + _zone_1_netjson = { "firewall": { "zones": [ From 84787b2e9d7c0694cb3a497800ee304a0cc98e99 Mon Sep 17 00:00:00 2001 From: "Jonathan G. Underwood" Date: Sat, 30 Jan 2021 22:22:30 +0000 Subject: [PATCH 24/40] [openwrt] Enhance firewall uci rule parser --- .../backends/openwrt/converters/firewall.py | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/netjsonconfig/backends/openwrt/converters/firewall.py b/netjsonconfig/backends/openwrt/converters/firewall.py index bfeb3019e..798a33950 100644 --- a/netjsonconfig/backends/openwrt/converters/firewall.py +++ b/netjsonconfig/backends/openwrt/converters/firewall.py @@ -166,12 +166,26 @@ def to_netjson_loop(self, block, result, index): return self.type_cast(result) def __netjson_rule(self, rule): - if "enabled" in rule: - rule["enabled"] = self.__netjson_generic_boolean(rule["enabled"]) + for param in ["enabled", "utc_time"]: + if param in rule: + rule[param] = self.__netjson_generic_boolean(rule[param]) if "proto" in rule: rule["proto"] = self.__netjson_generic_proto(rule["proto"]) + if "weekdays" in rule: + rule["weekdays"] = self.__netjson_generic_weekdays( + rule["weekdays"] + ) + + if "monthdays" in rule: + rule["monthdays"] = self.__netjson_generic_monthdays( + rule["monthdays"] + ) + + if "limit_burst" in rule: + rule["limit_burst"] = int(rule["limit_burst"]) + return self.type_cast(rule) def __netjson_zone(self, zone): @@ -196,12 +210,12 @@ def __netjson_redirect(self, redirect): redirect["proto"] = self.__netjson_generic_proto(redirect["proto"]) if "weekdays" in redirect: - redirect["weekdays"] = self.__netjson_redirect_weekdays( + redirect["weekdays"] = self.__netjson_generic_weekdays( redirect["weekdays"] ) if "monthdays" in redirect: - redirect["monthdays"] = self.__netjson_redirect_monthdays( + redirect["monthdays"] = self.__netjson_generic_monthdays( redirect["monthdays"] ) @@ -230,7 +244,7 @@ def __netjson_generic_proto(self, proto): else: return proto.split() - def __netjson_redirect_weekdays(self, weekdays): + def __netjson_generic_weekdays(self, weekdays): if not isinstance(weekdays, list): wd = weekdays.split() else: @@ -243,7 +257,7 @@ def __netjson_redirect_weekdays(self, weekdays): return wd - def __netjson_redirect_monthdays(self, monthdays): + def __netjson_generic_monthdays(self, monthdays): if not isinstance(monthdays, list): md = monthdays.split() else: From 89d80b03b84222dccb18a2826d32f0dde38efdff Mon Sep 17 00:00:00 2001 From: "Jonathan G. Underwood" Date: Sat, 30 Jan 2021 22:22:56 +0000 Subject: [PATCH 25/40] [openwrt] Add firewall rule uci parsing test --- tests/openwrt/test_firewall.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/openwrt/test_firewall.py b/tests/openwrt/test_firewall.py index 0a92bf221..64e65b558 100644 --- a/tests/openwrt/test_firewall.py +++ b/tests/openwrt/test_firewall.py @@ -290,6 +290,10 @@ def test_render_rule_6(self): expected = self._tabs(self._rule_6_uci) self.assertEqual(o.render(), expected) + def test_parse_rule_6(self): + o = OpenWrt(native=self._rule_6_uci) + self.assertEqual(o.config, self._rule_6_netjson) + _zone_1_netjson = { "firewall": { "zones": [ From 2bd9f08707dce995ff36bfdf72adbbf6c12a92e7 Mon Sep 17 00:00:00 2001 From: "Jonathan G. Underwood" Date: Sun, 7 Feb 2021 22:45:02 +0000 Subject: [PATCH 26/40] [openwrt] Refactor firewall defaults schema --- netjsonconfig/backends/openwrt/schema.py | 47 +++++++++++++++--------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/netjsonconfig/backends/openwrt/schema.py b/netjsonconfig/backends/openwrt/schema.py index 048e9dc8b..02b3aa7a9 100644 --- a/netjsonconfig/backends/openwrt/schema.py +++ b/netjsonconfig/backends/openwrt/schema.py @@ -1027,22 +1027,14 @@ }, } -firewall_properties = { - "syn_flood": { - "type": "boolean", - "title": "SYN flood protection.", - "description": "Enables SYN flood protection.", - "default": False, - "format": "checkbox", - "propertyOrder": 1, - }, +firewall_defaults = { "input": { "allOf": [ {"$ref": "#/definitions/firewall/firewall_policy"}, { - "title": "input", - "description": "policy for the INPUT chain of the filter table", - "propertyOrder": 2, + "title": "Default input policy", + "description": "Default policy for the INPUT chain of the filter table", + "propertyOrder": 1, }, ] }, @@ -1050,9 +1042,9 @@ "allOf": [ {"$ref": "#/definitions/firewall/firewall_policy"}, { - "title": "output", - "description": "policy for the OUTPUT chain of the filter table", - "propertyOrder": 3, + "title": "Default output policy", + "description": "Default policy for the OUTPUT chain of the filter table", + "propertyOrder": 2, }, ] }, @@ -1060,12 +1052,31 @@ "allOf": [ {"$ref": "#/definitions/firewall/firewall_policy"}, { - "title": "forward", - "description": "policy for the FORWARD chain of the filter table", - "propertyOrder": 4, + "title": "Default forward policy", + "description": "Defulat policy for the FORWARD chain of the filter table", + "propertyOrder": 3, }, ] }, + "synflood_protect": { + "type": "boolean", + "title": "Enable SYN flood protection.", + "description": "Enables SYN flood protection.", + "default": False, + "format": "checkbox", + "propertyOrder": 4, + }, +} + +firewall_properties = { + "defaults": { + "type": "object", + "title": "Defaults", + "description": "Defaults for the fireall", + "propertyOrder": 4, + "properties": firewall_defaults, + "required": ["input", "output", "forward", "synflood_protect"], + }, "forwardings": { "type": "array", "title": "Forwardings", From cd7186fd39bf2bcc434e1d78b4e58b760ab5692a Mon Sep 17 00:00:00 2001 From: "Jonathan G. Underwood" Date: Sun, 7 Feb 2021 22:46:51 +0000 Subject: [PATCH 27/40] [openwrt] Add firewall defaults parser and renderer --- .../backends/openwrt/converters/firewall.py | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/netjsonconfig/backends/openwrt/converters/firewall.py b/netjsonconfig/backends/openwrt/converters/firewall.py index 798a33950..a72b5a7fd 100644 --- a/netjsonconfig/backends/openwrt/converters/firewall.py +++ b/netjsonconfig/backends/openwrt/converters/firewall.py @@ -18,17 +18,29 @@ class Firewall(OpenWrtConverter): _schema = schema["properties"]["firewall"] def to_intermediate_loop(self, block, result, index=None): + defaults = self.__intermediate_defaults(block.pop("defaults", {})) forwardings = self.__intermediate_forwardings(block.pop("forwardings", {})) zones = self.__intermediate_zones(block.pop("zones", {})) rules = self.__intermediate_rules(block.pop("rules", {})) redirects = self.__intermediate_redirects(block.pop("redirects", {})) - block.update({".type": "defaults", ".name": block.pop("id", "defaults")}) result.setdefault("firewall", []) - result["firewall"] = ( - [self.sorted_dict(block)] + forwardings + zones + rules + redirects - ) + result["firewall"] = defaults + forwardings + zones + rules + redirects return result + def __intermediate_defaults(self, defaults): + """ + converts NetJSON defaults to + UCI intermediate data structure + """ + result = OrderedDict( + ( + (".name", "defaults"), + (".type", "defaults") + ) + ) + result.update(defaults) + return [result] + def __intermediate_forwardings(self, forwardings): """ converts NetJSON forwarding to @@ -146,6 +158,10 @@ def to_netjson_loop(self, block, result, index): block.pop(".name") _type = block.pop(".type") + if _type == "defaults": + defaults = self.__netjson_defaults(block) + result["firewall"].setdefault("defaults", {}) + result["firewall"]["defaults"].update(defaults) if _type == "rule": rule = self.__netjson_rule(block) result["firewall"].setdefault("rules", []) @@ -165,6 +181,12 @@ def to_netjson_loop(self, block, result, index): return self.type_cast(result) + def __netjson_defaults(self, defaults): + for param in ["synflood_protect"]: + if param in defaults: + defaults[param] = self.__netjson_generic_boolean(defaults[param]) + return self.type_cast(defaults) + def __netjson_rule(self, rule): for param in ["enabled", "utc_time"]: if param in rule: From 3413b94b69d4a7f8a5731f4e374c98e6f0378503 Mon Sep 17 00:00:00 2001 From: "Jonathan G. Underwood" Date: Sun, 7 Feb 2021 22:47:28 +0000 Subject: [PATCH 28/40] [openwrt] Add firewall defaults tests --- tests/openwrt/test_firewall.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/openwrt/test_firewall.py b/tests/openwrt/test_firewall.py index 64e65b558..0c230d509 100644 --- a/tests/openwrt/test_firewall.py +++ b/tests/openwrt/test_firewall.py @@ -9,6 +9,39 @@ class TestFirewall(unittest.TestCase, _TabsMixin): maxDiff = None + _defaults_1_netjson = { + "firewall": { + "defaults": { + "input": "ACCEPT", + "forward": "REJECT", + "output": "ACCEPT", + "synflood_protect": True, + } + } + } + + _defaults_1_uci = textwrap.dedent( + """\ + package firewall + + config defaults 'defaults' + option input 'ACCEPT' + option forward 'REJECT' + option output 'ACCEPT' + option synflood_protect '1' + """ + ) + + + def test_render_defaults_1(self): + o = OpenWrt(self._defaults_1_netjson) + expected = self._tabs(self._defaults_1_uci) + self.assertEqual(o.render(), expected) + + def test_parse_defaults_1(self): + o = OpenWrt(native=self._defaults_1_uci) + self.assertEqual(o.config, self._defaults_1_netjson) + _rule_1_netjson = { "firewall": { "rules": [ From b4f18cf338e47ae2756f90e28b18ccd6b33c0a61 Mon Sep 17 00:00:00 2001 From: "Jonathan G. Underwood" Date: Sun, 7 Feb 2021 22:54:42 +0000 Subject: [PATCH 29/40] [openwrt] Fix formatting in firewall.py --- .../backends/openwrt/converters/firewall.py | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/netjsonconfig/backends/openwrt/converters/firewall.py b/netjsonconfig/backends/openwrt/converters/firewall.py index a72b5a7fd..536ccee46 100644 --- a/netjsonconfig/backends/openwrt/converters/firewall.py +++ b/netjsonconfig/backends/openwrt/converters/firewall.py @@ -32,12 +32,7 @@ def __intermediate_defaults(self, defaults): converts NetJSON defaults to UCI intermediate data structure """ - result = OrderedDict( - ( - (".name", "defaults"), - (".type", "defaults") - ) - ) + result = OrderedDict(((".name", "defaults"), (".type", "defaults"))) result.update(defaults) return [result] @@ -196,14 +191,10 @@ def __netjson_rule(self, rule): rule["proto"] = self.__netjson_generic_proto(rule["proto"]) if "weekdays" in rule: - rule["weekdays"] = self.__netjson_generic_weekdays( - rule["weekdays"] - ) + rule["weekdays"] = self.__netjson_generic_weekdays(rule["weekdays"]) if "monthdays" in rule: - rule["monthdays"] = self.__netjson_generic_monthdays( - rule["monthdays"] - ) + rule["monthdays"] = self.__netjson_generic_monthdays(rule["monthdays"]) if "limit_burst" in rule: rule["limit_burst"] = int(rule["limit_burst"]) @@ -232,9 +223,7 @@ def __netjson_redirect(self, redirect): redirect["proto"] = self.__netjson_generic_proto(redirect["proto"]) if "weekdays" in redirect: - redirect["weekdays"] = self.__netjson_generic_weekdays( - redirect["weekdays"] - ) + redirect["weekdays"] = self.__netjson_generic_weekdays(redirect["weekdays"]) if "monthdays" in redirect: redirect["monthdays"] = self.__netjson_generic_monthdays( From 657f03bf57c201a5074d719c66e111239e1e7424 Mon Sep 17 00:00:00 2001 From: "Jonathan G. Underwood" Date: Sun, 7 Feb 2021 22:54:55 +0000 Subject: [PATCH 30/40] [openwrt] Fix formatting in test_firewall.py --- tests/openwrt/test_firewall.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/openwrt/test_firewall.py b/tests/openwrt/test_firewall.py index 0c230d509..1687bd2f1 100644 --- a/tests/openwrt/test_firewall.py +++ b/tests/openwrt/test_firewall.py @@ -32,7 +32,6 @@ class TestFirewall(unittest.TestCase, _TabsMixin): """ ) - def test_render_defaults_1(self): o = OpenWrt(self._defaults_1_netjson) expected = self._tabs(self._defaults_1_uci) From fe2404af93fae1d69c2d28d5b8cf4822538b47cb Mon Sep 17 00:00:00 2001 From: "Jonathan G. Underwood" Date: Tue, 9 Feb 2021 20:56:11 +0000 Subject: [PATCH 31/40] [openwrt] Add more parameters to firewall defaults schema --- .../backends/openwrt/converters/firewall.py | 18 ++- netjsonconfig/backends/openwrt/schema.py | 110 +++++++++++++++++- tests/openwrt/test_firewall.py | 52 +++++++++ 3 files changed, 177 insertions(+), 3 deletions(-) diff --git a/netjsonconfig/backends/openwrt/converters/firewall.py b/netjsonconfig/backends/openwrt/converters/firewall.py index 536ccee46..de0c046ca 100644 --- a/netjsonconfig/backends/openwrt/converters/firewall.py +++ b/netjsonconfig/backends/openwrt/converters/firewall.py @@ -177,9 +177,25 @@ def to_netjson_loop(self, block, result, index): return self.type_cast(result) def __netjson_defaults(self, defaults): - for param in ["synflood_protect"]: + for param in [ + "drop_invalid", + "synflood_protect", + "tcp_syncookies", + "tcp_ecn", + "tcp_window_scaling", + "accept_redirects", + "accept_source_route", + "custom_chains", + "disable_ipv6", + "flow_offloading", + "flow_offloading_hw", + "auto_helper", + ]: if param in defaults: defaults[param] = self.__netjson_generic_boolean(defaults[param]) + for param in ["synflood_limit", "synflood_burst"]: + if param in defaults: + defaults[param] = int(defaults[param]) return self.type_cast(defaults) def __netjson_rule(self, rule): diff --git a/netjsonconfig/backends/openwrt/schema.py b/netjsonconfig/backends/openwrt/schema.py index 02b3aa7a9..76c6d0ec3 100644 --- a/netjsonconfig/backends/openwrt/schema.py +++ b/netjsonconfig/backends/openwrt/schema.py @@ -1058,13 +1058,120 @@ }, ] }, + "drop_invalid": { + "type": "boolean", + "title": "Drop invalid packets.", + "description": "If True then any invalid packets will be dropped.", + "default": False, + "format": "checkbox", + "propertyOrder": 4, + }, "synflood_protect": { "type": "boolean", "title": "Enable SYN flood protection.", "description": "Enables SYN flood protection.", "default": False, "format": "checkbox", - "propertyOrder": 4, + "propertyOrder": 5, + }, + "synflood_rate": { + "type": "integer", + "title": "Rate limit (packets/second) for SYN packets above which the traffic is considered a flood.", + "description": "Number of packets/second for SYN packets above which the traffic is considered a " + "flood.", + "default": 25, + "propertyOrder": 6, + }, + "synflood_burst": { + "type": "integer", + "title": "Burst limit (packets/second) for SYN packets above which the traffic is considered a " + "flood.", + "description": "Set burst limit for SYN packets above which the traffic is considered a flood if it " + "exceeds the allowed rate.", + "default": 50, + "propertyOrder": 7, + }, + "tcp_syncookies": { + "type": "boolean", + "title": "Enable the use of TCP SYN cookies.", + "description": "If True, enables the use of SYN cookies.", + "default": True, + "format": "checkbox", + "propertyOrder": 8, + }, + "tcp_ecn": { + "type": "boolean", + "title": "Enable Explicit Congestion Notification.", + "description": "If True, enables Explicit Congestion Notification.", + "default": False, + "format": "checkbox", + "propertyOrder": 9, + }, + "tcp_window_scaling": { + "type": "boolean", + "title": "Enable TCP window scaling.", + "description": "If True, enables TCP window scaling.", + "default": True, + "format": "checkbox", + "propertyOrder": 10, + }, + "accept_redirects": { + "type": "boolean", + "title": "Accept redirects.", + "description": "If True, accept redirects.", + "default": False, + "format": "checkbox", + "propertyOrder": 11, + }, + "accept_source_route": { + "type": "boolean", + "title": "Accept source routes.", + "description": "If True, accept source routes.", + "default": False, + "format": "checkbox", + "propertyOrder": 12, + }, + "custom_chains": { + "type": "boolean", + "title": "Enable generation of custom rule chain hooks for user generated rules.", + "description": "If True, enable generation of custom rule chain hooks for user generated rules. " + "User rules would be typically stored in firewall.user but some packages e.g. BCP38 also make use " + "of these hooks.", + "default": True, + "format": "checkbox", + "propertyOrder": 13, + }, + "disable_ipv6": { + "type": "boolean", + "title": "Disable IPv6 firewall rules.", + "description": "If True, disable IPv6 firewall rules.", + "default": False, + "format": "checkbox", + "propertyOrder": 14, + }, + "flow_offlocaing": { + "type": "boolean", + "title": "Enable software flow offloading for connections.", + "description": "If True, enable software flow offloading for connections.", + "default": False, + "format": "checkbox", + "propertyOrder": 15, + }, + "flow_offlocaing_hw": { + "type": "boolean", + "title": "Enable hardware flow offloading for connections.", + "description": "If True, enable hardware flow offloading for connections.", + "default": False, + "format": "checkbox", + "propertyOrder": 16, + }, + "auto_helper": { + "type": "boolean", + "title": "Enable Conntrack helpers ", + "description": "If True, enable Conntrack helpers ", + "default": True, + "format": "checkbox", + "propertyOrder": 17, }, } @@ -1075,7 +1182,6 @@ "description": "Defaults for the fireall", "propertyOrder": 4, "properties": firewall_defaults, - "required": ["input", "output", "forward", "synflood_protect"], }, "forwardings": { "type": "array", diff --git a/tests/openwrt/test_firewall.py b/tests/openwrt/test_firewall.py index 1687bd2f1..38f76508c 100644 --- a/tests/openwrt/test_firewall.py +++ b/tests/openwrt/test_firewall.py @@ -41,6 +41,58 @@ def test_parse_defaults_1(self): o = OpenWrt(native=self._defaults_1_uci) self.assertEqual(o.config, self._defaults_1_netjson) + _defaults_2_netjson = { + "firewall": { + "defaults": { + "input": "ACCEPT", + "output": "ACCEPT", + "forward": "REJECT", + "custom_chains": True, + "drop_invalid": True, + "synflood_protect": True, + "synflood_burst": 50, + "tcp_ecn": True, + "tcp_syncookies": True, + "tcp_window_scaling": True, + "disable_ipv6": False, + "flow_offloading": False, + "flow_offloading_hw": False, + "auto_helper": True, + } + } + } + + _defaults_2_uci = textwrap.dedent( + """\ + package firewall + + config defaults 'defaults' + option input 'ACCEPT' + option output 'ACCEPT' + option forward 'REJECT' + option custom_chains '1' + option drop_invalid '1' + option synflood_protect '1' + option synflood_burst '50' + option tcp_ecn '1' + option tcp_syncookies '1' + option tcp_window_scaling '1' + option disable_ipv6 '0' + option flow_offloading '0' + option flow_offloading_hw '0' + option auto_helper '1' + """ + ) + + def test_render_defaults_2(self): + o = OpenWrt(self._defaults_2_netjson) + expected = self._tabs(self._defaults_2_uci) + self.assertEqual(o.render(), expected) + + def test_parse_defaults_2(self): + o = OpenWrt(native=self._defaults_2_uci) + self.assertEqual(o.config, self._defaults_2_netjson) + _rule_1_netjson = { "firewall": { "rules": [ From bc7dc888f040f3237a7a8301102cf32dc3f42d76 Mon Sep 17 00:00:00 2001 From: "Jonathan G. Underwood" Date: Tue, 9 Feb 2021 22:52:02 +0000 Subject: [PATCH 32/40] [openwrt] Fix title and description of firewall defaults --- netjsonconfig/backends/openwrt/schema.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netjsonconfig/backends/openwrt/schema.py b/netjsonconfig/backends/openwrt/schema.py index 76c6d0ec3..b8fc9c6d2 100644 --- a/netjsonconfig/backends/openwrt/schema.py +++ b/netjsonconfig/backends/openwrt/schema.py @@ -1178,8 +1178,8 @@ firewall_properties = { "defaults": { "type": "object", - "title": "Defaults", - "description": "Defaults for the fireall", + "title": "Firewall defaults", + "description": "Defaults for the firewall", "propertyOrder": 4, "properties": firewall_defaults, }, From f15cf3c5f9593af6a19fa314387ad71fdc2f86d8 Mon Sep 17 00:00:00 2001 From: "Jonathan G. Underwood" Date: Tue, 9 Feb 2021 22:52:26 +0000 Subject: [PATCH 33/40] [openwrt] Fix firewall defaults parser The defaults section can be empty, so when parsing UCI to netjson it is necessary to remove the defaults section if it is empty. --- netjsonconfig/backends/openwrt/converters/firewall.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/netjsonconfig/backends/openwrt/converters/firewall.py b/netjsonconfig/backends/openwrt/converters/firewall.py index de0c046ca..514d7629e 100644 --- a/netjsonconfig/backends/openwrt/converters/firewall.py +++ b/netjsonconfig/backends/openwrt/converters/firewall.py @@ -155,8 +155,9 @@ def to_netjson_loop(self, block, result, index): if _type == "defaults": defaults = self.__netjson_defaults(block) - result["firewall"].setdefault("defaults", {}) - result["firewall"]["defaults"].update(defaults) + if defaults: # note: default section can be empty + result["firewall"].setdefault("defaults", {}) + result["firewall"]["defaults"].update(defaults) if _type == "rule": rule = self.__netjson_rule(block) result["firewall"].setdefault("rules", []) From 006b196c8d60b50babbc3a306200b0c03f50cf17 Mon Sep 17 00:00:00 2001 From: "Jonathan G. Underwood" Date: Sun, 21 Feb 2021 22:03:54 +0000 Subject: [PATCH 34/40] [openwrt] Make name parameter required for firewall objects --- .../backends/openwrt/converters/firewall.py | 32 ++----- tests/openwrt/test_firewall.py | 93 +++++++++++++++---- 2 files changed, 82 insertions(+), 43 deletions(-) diff --git a/netjsonconfig/backends/openwrt/converters/firewall.py b/netjsonconfig/backends/openwrt/converters/firewall.py index 514d7629e..09325f27e 100644 --- a/netjsonconfig/backends/openwrt/converters/firewall.py +++ b/netjsonconfig/backends/openwrt/converters/firewall.py @@ -45,7 +45,7 @@ def __intermediate_forwardings(self, forwardings): for forwarding in forwardings: resultdict = OrderedDict( ( - (".name", self.__get_auto_name_forwarding(forwarding)), + (".name", self._get_uci_name(forwarding["name"])), (".type", "forwarding"), ) ) @@ -53,17 +53,6 @@ def __intermediate_forwardings(self, forwardings): 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 @@ -72,7 +61,7 @@ def __intermediate_zones(self, zones): result = [] for zone in zones: resultdict = OrderedDict( - ((".name", self.__get_auto_name_zone(zone)), (".type", "zone")) + ((".name", self._get_uci_name(zone["name"])), (".type", "zone")) ) # If network contains only a single value, force the use of a UCI "option" # rather than "list"". @@ -83,9 +72,6 @@ def __intermediate_zones(self, zones): 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 @@ -96,7 +82,7 @@ def __intermediate_rules(self, rules): if "config_name" in rule: del rule["config_name"] resultdict = OrderedDict( - ((".name", self.__get_auto_name_rule(rule)), (".type", "rule")) + ((".name", self._get_uci_name(rule["name"])), (".type", "rule")) ) if "proto" in rule: # If proto is a single value, then force it not to be in a list so that @@ -111,9 +97,6 @@ def __intermediate_rules(self, rules): result.append(resultdict) return result - def __get_auto_name_rule(self, rule): - return "rule_{0}".format(self._get_uci_name(rule["name"])) - def __intermediate_redirects(self, redirects): """ converts NetJSON redirect to @@ -125,7 +108,7 @@ def __intermediate_redirects(self, redirects): del redirect["config_name"] resultdict = OrderedDict( ( - (".name", self.__get_auto_name_redirect(redirect)), + (".name", self._get_uci_name(redirect["name"])), (".type", "redirect"), ) ) @@ -144,9 +127,6 @@ def __intermediate_redirects(self, redirects): return result - def __get_auto_name_redirect(self, redirect): - return "redirect_{0}".format(self._get_uci_name(redirect["name"])) - def to_netjson_loop(self, block, result, index): result.setdefault("firewall", {}) @@ -233,6 +213,10 @@ def __netjson_zone(self, zone): return self.type_cast(zone) def __netjson_forwarding(self, forwarding): + if "enabled" in forwarding: + forwarding["enabled"] = self.__netjson_generic_boolean( + forwarding["enabled"] + ) return self.type_cast(forwarding) def __netjson_redirect(self, redirect): diff --git a/tests/openwrt/test_firewall.py b/tests/openwrt/test_firewall.py index 38f76508c..b5d277a8a 100644 --- a/tests/openwrt/test_firewall.py +++ b/tests/openwrt/test_firewall.py @@ -115,7 +115,7 @@ def test_parse_defaults_2(self): config defaults 'defaults' - config rule 'rule_Allow_MLD' + config rule 'Allow_MLD' option name 'Allow-MLD' option src 'wan' option src_ip 'fe80::/10' @@ -161,7 +161,7 @@ def test_parse_rule_1(self): config defaults 'defaults' - config rule 'rule_Allow_DHCPv6' + config rule 'Allow_DHCPv6' option name 'Allow-DHCPv6' option src 'wan' option src_ip 'fc00::/6' @@ -204,7 +204,7 @@ def test_parse_rule_2(self): config defaults 'defaults' - config rule 'rule_Allow_Ping' + config rule 'Allow_Ping' option name 'Allow-Ping' option src 'wan' option proto 'icmp' @@ -244,7 +244,7 @@ def test_parse_rule_3(self): config defaults 'defaults' - config rule 'rule_Allow_Isolated_DHCP' + config rule 'Allow_Isolated_DHCP' option name 'Allow-Isolated-DHCP' option src 'isolated' option proto 'udp' @@ -284,7 +284,7 @@ def test_parse_rule_4(self): config defaults 'defaults' - config rule 'rule_Allow_Isolated_DHCP' + config rule 'Allow_Isolated_DHCP' option name 'Allow-Isolated-DHCP' option src_ip '10.10.10.10' option src_mac 'fc:aa:14:18:12:98' @@ -341,7 +341,7 @@ def test_parse_rule_5(self): config defaults 'defaults' - config rule 'rule_Allow_Isolated_DHCP' + config rule 'Allow_Isolated_DHCP' option name 'Allow-Isolated-DHCP' option src_ip '10.10.10.10' option src_mac 'fc:aa:14:18:12:98' @@ -399,7 +399,7 @@ def test_parse_rule_6(self): config defaults 'defaults' - config zone 'zone_lan' + config zone 'lan' option name 'lan' option input 'ACCEPT' option output 'ACCEPT' @@ -440,7 +440,7 @@ def test_parse_zone_1(self): config defaults 'defaults' - config zone 'zone_wan' + config zone 'wan' option name 'wan' option input 'DROP' option output 'ACCEPT' @@ -460,7 +460,7 @@ def test_parse_zone_1(self): config defaults 'defaults' - config zone 'zone_wan' + config zone 'wan' option name 'wan' option input 'DROP' option output 'ACCEPT' @@ -485,7 +485,9 @@ def test_parse_zone_3(self): self.assertEqual(o.config, self._zone_2_netjson) _forwarding_1_netjson = { - "firewall": {"forwardings": [{"src": "isolated", "dest": "wan"}]} + "firewall": { + "forwardings": [{"name": "isolated-wan", "src": "isolated", "dest": "wan"}] + } } _forwarding_1_uci = textwrap.dedent( @@ -494,7 +496,8 @@ def test_parse_zone_3(self): config defaults 'defaults' - config forwarding 'forwarding_isolated_wan' + config forwarding 'isolated_wan' + option name 'isolated-wan' option src 'isolated' option dest 'wan' """ @@ -511,7 +514,14 @@ def test_parse_forwarding_1(self): _forwarding_2_netjson = { "firewall": { - "forwardings": [{"src": "isolated", "dest": "wan", "family": "ipv4"}] + "forwardings": [ + { + "name": "isolated-wan-ipv4", + "src": "isolated", + "dest": "wan", + "family": "ipv4", + } + ] } } @@ -521,7 +531,8 @@ def test_parse_forwarding_1(self): config defaults 'defaults' - config forwarding 'forwarding_isolated_wan_ipv4' + config forwarding 'isolated_wan_ipv4' + option name 'isolated-wan-ipv4' option src 'isolated' option dest 'wan' option family 'ipv4' @@ -538,7 +549,11 @@ def test_parse_forwarding_2(self): self.assertEqual(o.config, self._forwarding_2_netjson) _forwarding_3_netjson = { - "firewall": {"forwardings": [{"src": "lan", "dest": "wan", "family": "any"}]} + "firewall": { + "forwardings": [ + {"name": "lan-wan-any", "src": "lan", "dest": "wan", "family": "any"} + ] + } } _forwarding_3_uci = textwrap.dedent( @@ -547,7 +562,8 @@ def test_parse_forwarding_2(self): config defaults 'defaults' - config forwarding 'forwarding_lan_wan_any' + config forwarding 'lan_wan_any' + option name 'lan-wan-any' option src 'lan' option dest 'wan' option family 'any' @@ -563,6 +579,45 @@ def test_parse_forwarding_3(self): o = OpenWrt(native=self._forwarding_3_uci) self.assertEqual(o.config, self._forwarding_3_netjson) + _forwarding_4_netjson = { + "firewall": { + "forwardings": [ + { + "name": "forward_name", + "src": "lan", + "dest": "wan", + "family": "any", + "enabled": False, + } + ] + } + } + + _forwarding_4_uci = textwrap.dedent( + """\ + package firewall + + config defaults 'defaults' + + config forwarding 'forward_name' + option name 'forward_name' + option src 'lan' + option dest 'wan' + option family 'any' + option enabled '0' + """ + ) + + def test_render_forwarding_4(self): + o = OpenWrt(self._forwarding_4_netjson) + expected = self._tabs(self._forwarding_4_uci) + self.assertEqual(o.render(), expected) + + def test_parse_forwarding_4(self): + o = OpenWrt(native=self._forwarding_4_uci) + print(o.config) + self.assertEqual(o.config, self._forwarding_4_netjson) + def test_forwarding_validation_error(self): o = OpenWrt( { @@ -595,7 +650,7 @@ def test_forwarding_validation_error(self): config defaults 'defaults' - config redirect 'redirect_Adblock DNS, port 53' + config redirect 'Adblock DNS, port 53' option name 'Adblock DNS, port 53' option src 'lan' option proto 'tcpudp' @@ -638,7 +693,7 @@ def test_parse_redirect_1(self): config defaults 'defaults' - config redirect 'redirect_Adblock DNS, port 53' + config redirect 'Adblock DNS, port 53' option name 'Adblock DNS, port 53' option src 'lan' option proto 'tcpudp' @@ -691,7 +746,7 @@ def test_redirect_monthdays_validation_error_2(self): config defaults 'defaults' - config redirect 'redirect_Adblock DNS, port 53' + config redirect 'Adblock DNS, port 53' option name 'Adblock DNS, port 53' option src 'lan' option proto 'tcpudp' @@ -757,7 +812,7 @@ def test_parse_redirect_3(self): config defaults 'defaults' - config redirect 'redirect_Adblock DNS, port 53' + config redirect 'Adblock DNS, port 53' option name 'Adblock DNS, port 53' option src 'lan' option proto 'tcpudp' From 89a4bed731d93f729dac9b35800e94e4e27d68a7 Mon Sep 17 00:00:00 2001 From: "Jonathan G. Underwood" Date: Sun, 21 Feb 2021 22:06:03 +0000 Subject: [PATCH 35/40] [openwrt] Test enabled parameter for firewall forwarding --- tests/openwrt/test_firewall.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/openwrt/test_firewall.py b/tests/openwrt/test_firewall.py index b5d277a8a..6c3d884e1 100644 --- a/tests/openwrt/test_firewall.py +++ b/tests/openwrt/test_firewall.py @@ -551,7 +551,13 @@ def test_parse_forwarding_2(self): _forwarding_3_netjson = { "firewall": { "forwardings": [ - {"name": "lan-wan-any", "src": "lan", "dest": "wan", "family": "any"} + { + "name": "lan-wan-any", + "src": "lan", + "dest": "wan", + "family": "any", + "enabled": False, + } ] } } @@ -567,6 +573,7 @@ def test_parse_forwarding_2(self): option src 'lan' option dest 'wan' option family 'any' + option enabled '0' """ ) From 81b17a00b0152c9bf38ed666145c76fdc4389cc0 Mon Sep 17 00:00:00 2001 From: "Jonathan G. Underwood" Date: Sun, 21 Feb 2021 22:18:07 +0000 Subject: [PATCH 36/40] [openwrt] Fix test_default.py tests --- tests/openwrt/test_default.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/openwrt/test_default.py b/tests/openwrt/test_default.py index 309507a01..230e31b9a 100644 --- a/tests/openwrt/test_default.py +++ b/tests/openwrt/test_default.py @@ -53,7 +53,7 @@ def test_render_default(self): config defaults 'defaults' -config rule 'rule_Allow_MLD' +config rule 'Allow_MLD' option name 'Allow-MLD' option src 'wan' option proto 'icmp' @@ -65,7 +65,7 @@ def test_render_default(self): list icmp_type '132/0' list icmp_type '143/0' -config rule 'rule_Rule2' +config rule 'Rule2' option name 'Rule2' option src 'wan' option proto 'icmp' From 07305f6378a33c14df9acc10ea2748ae4bbabaef Mon Sep 17 00:00:00 2001 From: "Jonathan G. Underwood" Date: Sun, 14 Mar 2021 00:15:08 +0000 Subject: [PATCH 37/40] [openwrt] Add firewall includes to schema --- netjsonconfig/backends/openwrt/schema.py | 42 ++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/netjsonconfig/backends/openwrt/schema.py b/netjsonconfig/backends/openwrt/schema.py index c41daac6a..8772590fa 100644 --- a/netjsonconfig/backends/openwrt/schema.py +++ b/netjsonconfig/backends/openwrt/schema.py @@ -756,6 +756,36 @@ }, } +firewall_includes_properties = { + "name": {"$ref": "#/definitions/firewall/name"}, + "enabled": {"$ref": "#/definitions/firewall/enabled"}, + "family": {"$ref": "#/definitions/firewall/family"}, + "type": { + "type": "string", + "title": "The type of the script", + "description": 'Specifies the type of the include, can be "script" for traditional ' + 'shell script includes or restore for plain files in iptables-restore format.', + "enum": ["script", "restore"], + "propertyOrder": 101, + }, + "path": { + "type": "string", + "title": "Script to include", + "description": "Specifies a shell script to execute on boot or firewall restarts", + "default": "/etc/firewall.user", + "propertyOrder": 102, + }, + "reload": { + "type": "boolean", + "title": "Reload the included file when reloading firewall rules", + "description": "This specifies whether or not the included file should be " + "reloaded when the firewall rules are reloaded. This is only needed if " + "the included file injects rules into internal OpenWRT chains.", + "default": False, + "propertyOrder": 103, + }, +} + firewall_redirect_properties = { "name": {"$ref": "#/definitions/firewall/name"}, "enabled": {"$ref": "#/definitions/firewall/enabled"}, @@ -1230,6 +1260,18 @@ "properties": firewall_redirect_properties, }, }, + "includes": { + "type": "array", + "title": "Includes", + "propertyOrder": 9, + "items": { + "type": "object", + "title": "Include", + "additionalProperties": False, + "required": ["path"], + "properties": firewall_includes_properties, + }, + }, } firewall = { From b2536dd8697a46c82aa79799f3f23babb59e5cc9 Mon Sep 17 00:00:00 2001 From: "Jonathan G. Underwood" Date: Sat, 20 Mar 2021 15:34:06 +0000 Subject: [PATCH 38/40] [openwrt] Add firewall includes parser and renderer --- .../backends/openwrt/converters/firewall.py | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/netjsonconfig/backends/openwrt/converters/firewall.py b/netjsonconfig/backends/openwrt/converters/firewall.py index 09325f27e..30bc3fbd0 100644 --- a/netjsonconfig/backends/openwrt/converters/firewall.py +++ b/netjsonconfig/backends/openwrt/converters/firewall.py @@ -14,7 +14,7 @@ class Firewall(OpenWrtConverter): netjson_key = "firewall" intermediate_key = "firewall" - _uci_types = ["defaults", "forwarding", "zone", "rule", "redirect"] + _uci_types = ["defaults", "forwarding", "zone", "rule", "redirect", "include"] _schema = schema["properties"]["firewall"] def to_intermediate_loop(self, block, result, index=None): @@ -23,8 +23,11 @@ def to_intermediate_loop(self, block, result, index=None): zones = self.__intermediate_zones(block.pop("zones", {})) rules = self.__intermediate_rules(block.pop("rules", {})) redirects = self.__intermediate_redirects(block.pop("redirects", {})) + includes = self.__intermediate_includes(block.pop("includes", {})) result.setdefault("firewall", []) - result["firewall"] = defaults + forwardings + zones + rules + redirects + result["firewall"] = ( + defaults + forwardings + zones + rules + redirects + includes + ) return result def __intermediate_defaults(self, defaults): @@ -127,6 +130,24 @@ def __intermediate_redirects(self, redirects): return result + def __intermediate_includes(self, includes): + """ + converts NetJSON include to + UCI intermediate data structure + """ + result = [] + for include in includes: + if "config_name" in include: + del include["config_name"] + resultdict = OrderedDict( + ((".name", self._get_uci_name(include["name"])), (".type", "include"),) + ) + + resultdict.update(include) + result.append(resultdict) + + return result + def to_netjson_loop(self, block, result, index): result.setdefault("firewall", {}) @@ -154,6 +175,10 @@ def to_netjson_loop(self, block, result, index): redirect = self.__netjson_redirect(block) result["firewall"].setdefault("redirects", []) result["firewall"]["redirects"].append(redirect) + if _type == "include": + include = self.__netjson_include(block) + result["firewall"].setdefault("includes", []) + result["firewall"]["includes"].append(include) return self.type_cast(result) @@ -240,6 +265,13 @@ def __netjson_redirect(self, redirect): return self.type_cast(redirect) + def __netjson_include(self, include): + for param in ["reload", "enabled"]: + if param in include: + include[param] = self.__netjson_generic_boolean(include[param]) + + return self.type_cast(include) + def __netjson_generic_boolean(self, boolean): # Per convention, boolean options may have one of the values '0', 'no', 'off', # 'false' or 'disabled' to specify a false value or '1' , 'yes', 'on', 'true' or From 3b51afee567f5f439795e2ca987b88d363b2a39b Mon Sep 17 00:00:00 2001 From: "Jonathan G. Underwood" Date: Sat, 20 Mar 2021 15:34:43 +0000 Subject: [PATCH 39/40] [openwrt] Add firewall includes tests --- tests/openwrt/test_firewall.py | 40 ++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/openwrt/test_firewall.py b/tests/openwrt/test_firewall.py index 6c3d884e1..33b74d244 100644 --- a/tests/openwrt/test_firewall.py +++ b/tests/openwrt/test_firewall.py @@ -898,3 +898,43 @@ def test_render_redirect_4(self): def test_parse_redirect_4(self): o = OpenWrt(native=self._redirect_4_uci) self.assertEqual(o.config, self._redirect_4_netjson) + + _include_1_uci = textwrap.dedent( + """\ + package firewall + + config defaults 'defaults' + + config include 'Include Test' + option name 'Include Test' + option type 'script' + option family 'any' + option path '/a/b/c.ipt' + option reload '1' + option enabled '0' + """ + ) + + _include_1_netjson = { + "firewall": { + "includes": [ + { + "name": "Include Test", + "type": "script", + "family": "any", + "path": "/a/b/c.ipt", + "reload": True, + "enabled": False, + } + ] + } + } + + def test_render_include_1(self): + o = OpenWrt(self._include_1_netjson) + expected = self._tabs(self._include_1_uci) + self.assertEqual(o.render(), expected) + + def test_parse_include_1(self): + o = OpenWrt(native=self._include_1_uci) + self.assertEqual(o.config, self._include_1_netjson) From 8c6ac92739973f3ccafde00760b55c5f9a9e14c0 Mon Sep 17 00:00:00 2001 From: "Jonathan G. Underwood" Date: Sat, 20 Mar 2021 23:11:06 +0000 Subject: [PATCH 40/40] [openwrt] Remove config_name handling in firewall This removes the remnants of an attempt to manage older firewall configurations from before the firewall schema was introduced. However, this handling was incomplete and flawed, and so this code is pointless. --- netjsonconfig/backends/openwrt/converters/firewall.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/netjsonconfig/backends/openwrt/converters/firewall.py b/netjsonconfig/backends/openwrt/converters/firewall.py index 30bc3fbd0..34c571741 100644 --- a/netjsonconfig/backends/openwrt/converters/firewall.py +++ b/netjsonconfig/backends/openwrt/converters/firewall.py @@ -82,8 +82,6 @@ def __intermediate_rules(self, rules): """ result = [] for rule in rules: - if "config_name" in rule: - del rule["config_name"] resultdict = OrderedDict( ((".name", self._get_uci_name(rule["name"])), (".type", "rule")) ) @@ -107,8 +105,6 @@ def __intermediate_redirects(self, redirects): """ result = [] for redirect in redirects: - if "config_name" in redirect: - del redirect["config_name"] resultdict = OrderedDict( ( (".name", self._get_uci_name(redirect["name"])), @@ -137,8 +133,6 @@ def __intermediate_includes(self, includes): """ result = [] for include in includes: - if "config_name" in include: - del include["config_name"] resultdict = OrderedDict( ((".name", self._get_uci_name(include["name"])), (".type", "include"),) )