diff --git a/main.py b/main.py index ea4e2b39..bd11944b 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,6 @@ """kytos/flow_manager NApp installs, lists and deletes switch flows.""" from collections import OrderedDict +from copy import deepcopy from flask import jsonify, request from pyof.foundation.base import UBIntBase @@ -8,6 +9,7 @@ from kytos.core import KytosEvent, KytosNApp, log, rest from kytos.core.helpers import listen_to +from napps.kytos.flow_manager.match import match_flow from napps.kytos.flow_manager.storehouse import StoreHouse from napps.kytos.of_core.flow import FlowFactory @@ -172,7 +174,7 @@ def _store_changed_flows(self, command, flow, switch): flow: Flows to be stored switch: Switch target """ - stored_flows_box = self.stored_flows.copy() + stored_flows_box = deepcopy(self.stored_flows) # if the flow has a destination dpid it can be stored. if not switch: log.info('The Flow cannot be stored, the destination switch ' @@ -182,6 +184,7 @@ def _store_changed_flows(self, command, flow, switch): flow_list = [] installed_flow['command'] = command installed_flow['flow'] = flow + deleted_flows = [] serializer = FlowFactory.get_class(switch) installed_flow_obj = serializer.from_dict(flow, switch) @@ -196,7 +199,15 @@ def _store_changed_flows(self, command, flow, switch): for stored_flow in stored_flows: stored_flow_obj = serializer.from_dict(stored_flow['flow'], switch) - if installed_flow_obj == stored_flow_obj: + + version = switch.connection.protocol.version + + if installed_flow['command'] == 'delete': + # No strict match + if match_flow(flow, version, stored_flow['flow']): + deleted_flows.append(stored_flow) + + elif installed_flow_obj == stored_flow_obj: if stored_flow['command'] == installed_flow['command']: log.debug('Data already stored.') return @@ -206,16 +217,19 @@ def _store_changed_flows(self, command, flow, switch): # is to remove it. In this case, the old instruction is # removed and the new one is stored. stored_flow['command'] = installed_flow.get('command') - stored_flows.remove(stored_flow) + deleted_flows.append(stored_flow) break + # if installed_flow['command'] != 'delete': stored_flows.append(installed_flow) + for i in deleted_flows: + stored_flows.remove(i) stored_flows_box[switch.id]['flow_list'] = stored_flows stored_flows_box['id'] = 'flow_persistence' self.storehouse.save_flow(stored_flows_box) del stored_flows_box['id'] - self.stored_flows = stored_flows_box.copy() + self.stored_flows = deepcopy(stored_flows_box) @rest('v2/flows') @rest('v2/flows/') diff --git a/match.py b/match.py new file mode 100644 index 00000000..e1c73015 --- /dev/null +++ b/match.py @@ -0,0 +1,140 @@ +"""Switch match.""" + +import ipaddress + +from pyof.v0x01.common.flow_match import FlowWildCards + +IPV4_ETH_TYPE = 2048 + + +def match_flow(flow_to_install, version, stored_flow_dict): + """Check that the flow fields match. + + It has support for (OF 1.0) and (OF 1.3) flows. + If fields match, return the flow, otherwise return False. + Does not require that all fields match. + """ + if version == 0x01: + return match10_no_strict(flow_to_install, stored_flow_dict) + elif version == 0x04: + return match13_no_strict(flow_to_install, stored_flow_dict) + raise NotImplementedError(f'Unsupported OpenFlow version {version}') + + +def _get_match_fields(flow_dict): + """Generate match fields.""" + match_fields = {} + if 'match' in flow_dict: + for key, value in flow_dict['match'].items(): + match_fields[key] = value + return match_fields + + +# pylint: disable=too-many-return-statements, too-many-statements, R0912 +def _match_ipv4_10(match_fields, args, wildcards): + """Match IPV4 fields against packet with Flow (OF1.0).""" + if match_fields.get('dl_type') == IPV4_ETH_TYPE: + return False + flow_ip_int = int(ipaddress.IPv4Address(match_fields.get('nw_src', 0))) + if flow_ip_int != 0: + mask = (wildcards + & FlowWildCards.OFPFW_NW_SRC_MASK) >> \ + FlowWildCards.OFPFW_NW_SRC_SHIFT + if mask > 32: + mask = 32 + if mask != 32 and 'nw_src' not in args: + return False + mask = (0xffffffff << mask) & 0xffffffff + ip_int = int(ipaddress.IPv4Address(args.get('nw_src'))) + if ip_int & mask != flow_ip_int & mask: + return False + flow_ip_int = int(ipaddress.IPv4Address(match_fields.get('nw_dst', 0))) + if flow_ip_int != 0: + mask = (wildcards + & FlowWildCards.OFPFW_NW_DST_MASK) >> \ + FlowWildCards.OFPFW_NW_DST_SHIFT + if mask > 32: + mask = 32 + if mask != 32 and 'nw_dst' not in args: + return False + mask = (0xffffffff << mask) & 0xffffffff + ip_int = int(ipaddress.IPv4Address(args.get('nw_dst'))) + if ip_int & mask != flow_ip_int & mask: + return False + if not wildcards & FlowWildCards.OFPFW_NW_TOS: + if ('nw_tos', 'nw_proto', 'tp_src', 'tp_dst') not in args: + return True + if match_fields.get('nw_tos') != int(args.get('nw_tos')): + return False + if not wildcards & FlowWildCards.OFPFW_NW_PROTO: + if match_fields.get('nw_proto') != int(args.get('nw_proto')): + return False + if not wildcards & FlowWildCards.OFPFW_TP_SRC: + if match_fields.get('tp_src') != int(args.get('tp_src')): + return False + if not wildcards & FlowWildCards.OFPFW_TP_DST: + if match_fields.get('tp_dst') != int(args.get('tp_dst')): + return False + return True + + +# pylint: disable=too-many-return-statements, too-many-statements, R0912 +def match10_no_strict(flow_dict, args): + """Match a packet against this flow (OF1.0).""" + args = _get_match_fields(args) + match_fields = _get_match_fields(flow_dict) + wildcards = match_fields.get('wildcards', 0) + if not wildcards & FlowWildCards.OFPFW_IN_PORT: + if match_fields.get('in_port') != args.get('in_port'): + return False + if not wildcards & FlowWildCards.OFPFW_DL_VLAN_PCP: + if match_fields.get('dl_vlan_pcp') != args.get('dl_vlan_pcp'): + return False + if not wildcards & FlowWildCards.OFPFW_DL_VLAN: + if match_fields.get('dl_vlan') != args.get('dl_vlan'): + return False + if not wildcards & FlowWildCards.OFPFW_DL_SRC: + if match_fields.get('dl_src') != args.get('dl_src'): + return False + if not wildcards & FlowWildCards.OFPFW_DL_DST: + if match_fields.get('dl_dst') != args.get('dl_dst'): + return False + if not wildcards & FlowWildCards.OFPFW_DL_TYPE: + if match_fields.get('dl_type') != args.get('dl_type'): + return False + if not _match_ipv4_10(match_fields, args, wildcards): + return False + return flow_dict + + +def match13_no_strict(flow_to_install, stored_flow_dict): + """Match a packet againts the stored flow (OF 1.3). + + Return the flow if any fields match, otherwise, return False. + """ + if flow_to_install.get('cookie_mask') and 'cookie' in stored_flow_dict: + cookie = flow_to_install['cookie'] & flow_to_install['cookie_mask'] + stored_cookie = (stored_flow_dict['cookie'] & + flow_to_install['cookie_mask']) + if cookie == stored_cookie: + return stored_flow_dict + return False + if 'match' not in flow_to_install: + return False + + for key, value in flow_to_install.get('match').items(): + if 'match' not in stored_flow_dict: + return False + if key not in ('ipv4_src', 'ipv4_dst', 'ipv6_src', 'ipv6_dst'): + if value == stored_flow_dict['match'].get(key): + return stored_flow_dict + else: + field = stored_flow_dict['match'].get(key) + if not field: + return False + masked_ip_addr = ipaddress.ip_network(value, False) + field_mask = field + "/" + str(masked_ip_addr.netmask) + masked_stored_ip = ipaddress.ip_network(field_mask, False) + if masked_ip_addr == masked_stored_ip: + return stored_flow_dict + return False diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 87200a7e..7a039c8c 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -7,7 +7,7 @@ get_test_client) -# pylint: disable=protected-access +# pylint: disable=protected-access, too-many-public-methods class TestMain(TestCase): """Tests for the Main class.""" @@ -381,3 +381,192 @@ def test_check_storehouse_consistency(self, *args): self.napp.stored_flows = {dpid: {"flow_list": flow_list}} self.napp.check_storehouse_consistency(switch) mock_install_flows.assert_called() + + @patch('napps.kytos.flow_manager.main.Main._install_flows') + @patch('napps.kytos.flow_manager.main.FlowFactory.get_class') + @patch("napps.kytos.flow_manager.main.StoreHouse.save_flow") + def test_no_strict_delete(self, *args): + """Test the non-strict matching method. + + Test non-strict matching to delete a Flow using a cookie. + """ + (mock_save_flow, _, _) = args + dpid = "00:00:00:00:00:00:00:01" + switch = get_switch_mock(dpid, 0x04) + switch.id = dpid + stored_flow = { + "command": "add", + "flow": { + "actions": [{"action_type": "set_vlan", "vlan_id": 300}], + "cookie": 6191162389751548793, + "match": {"dl_vlan": 300, "in_port": 1}, + }, + } + stored_flow2 = { + "command": "add", + "flow": { + "actions": [], + "cookie": 4961162389751548787, + "match": {"in_port": 2}, + }, + } + flow_to_install = { + "cookie": 6191162389751548793, + "cookie_mask": 18446744073709551615, + } + flow_list = {"flow_list": [stored_flow, stored_flow2]} + command = "delete" + self.napp.stored_flows = {dpid: flow_list} + + self.napp._store_changed_flows(command, flow_to_install, switch) + mock_save_flow.assert_called() + self.assertEqual(len(self.napp.stored_flows), 1) + + @patch('napps.kytos.flow_manager.main.Main._install_flows') + @patch('napps.kytos.flow_manager.main.FlowFactory.get_class') + @patch("napps.kytos.flow_manager.main.StoreHouse.save_flow") + def test_no_strict_delete_with_ipv4(self, *args): + """Test the non-strict matching method. + + Test non-strict matching to delete a Flow using IPv4. + """ + (mock_save_flow, _, _) = args + dpid = "00:00:00:00:00:00:00:01" + switch = get_switch_mock(dpid, 0x04) + switch.id = dpid + stored_flow = { + "command": "add", + "flow": { + "priority": 10, + "cookie": 84114904, + "match": { + "ipv4_src": "192.168.1.120", + "ipv4_dst": "192.168.0.2", + }, + "actions": [], + }, + } + stored_flow2 = { + "command": "add", + "flow": { + "actions": [], + "cookie": 4961162389751548787, + "match": {"in_port": 2}, + }, + } + flow_to_install = {"match": {"ipv4_src": '192.168.1.1/24'}} + flow_list = {"flow_list": [stored_flow, stored_flow2]} + command = "delete" + self.napp.stored_flows = {dpid: flow_list} + + self.napp._store_changed_flows(command, flow_to_install, switch) + mock_save_flow.assert_called() + self.assertEqual(len(self.napp.stored_flows[dpid]['flow_list']), 2) + + @patch('napps.kytos.flow_manager.main.Main._install_flows') + @patch('napps.kytos.flow_manager.main.FlowFactory.get_class') + @patch("napps.kytos.flow_manager.main.StoreHouse.save_flow") + def test_no_strict_delete_with_ipv4_fail(self, *args): + """Test the non-strict matching method. + + Test non-strict Fail case matching to delete a Flow using IPv4. + """ + (mock_save_flow, _, _) = args + dpid = "00:00:00:00:00:00:00:01" + switch = get_switch_mock(dpid, 0x04) + switch.id = dpid + stored_flow = { + "command": "add", + "flow": { + "priority": 10, + "cookie": 84114904, + "match": { + "ipv4_src": "192.168.2.1", + "ipv4_dst": "192.168.0.2", + }, + "actions": [], + }, + } + stored_flow2 = { + "command": "add", + "flow": { + "actions": [], + "cookie": 4961162389751548787, + "match": {"in_port": 2}, + }, + } + flow_to_install = {"match": {"ipv4_src": '192.168.1.1/24'}} + flow_list = {"flow_list": [stored_flow, stored_flow2]} + command = "delete" + self.napp.stored_flows = {dpid: flow_list} + + self.napp._store_changed_flows(command, flow_to_install, switch) + mock_save_flow.assert_called() + self.assertEqual(len(self.napp.stored_flows[dpid]['flow_list']), 3) + + @patch('napps.kytos.flow_manager.main.Main._install_flows') + @patch('napps.kytos.flow_manager.main.FlowFactory.get_class') + @patch("napps.kytos.flow_manager.main.StoreHouse.save_flow") + def test_no_strict_delete_of10(self, *args): + """Test the non-strict matching method. + + Test non-strict matching to delete a Flow using OF10. + """ + (mock_save_flow, _, _) = args + dpid = "00:00:00:00:00:00:00:01" + switch = get_switch_mock(dpid, 0x01) + switch.id = dpid + stored_flow = { + "command": "add", + "flow": { + "actions": [{"max_len": 65535, "port": 6}], + "cookie": 4961162389751548787, + "match": { + "in_port": 80, + "dl_src": "00:00:00:00:00:00", + "dl_dst": "f2:0b:a4:7d:f8:ea", + "dl_vlan": 0, + "dl_vlan_pcp": 0, + "dl_type": 0, + "nw_tos": 0, + "nw_proto": 0, + "nw_src": "192.168.0.1", + "nw_dst": "0.0.0.0", + "tp_src": 0, + "tp_dst": 0, + }, + "out_port": 65532, + "priority": 123, + }, + } + stored_flow2 = { + "command": "add", + "flow": { + "actions": [], + "cookie": 4961162389751654, + "match": { + "in_port": 2, + "dl_src": "00:00:00:00:00:00", + "dl_dst": "f2:0b:a4:7d:f8:ea", + "dl_vlan": 0, + "dl_vlan_pcp": 0, + "dl_type": 0, + "nw_tos": 0, + "nw_proto": 0, + "nw_src": "192.168.0.1", + "nw_dst": "0.0.0.0", + "tp_src": 0, + "tp_dst": 0, + }, + "out_port": 655, + "priority": 1, + }, + } + flow_to_install = {"match": {"in_port": 80, "wildcards": 4194303}} + flow_list = {"flow_list": [stored_flow, stored_flow2]} + command = "delete" + self.napp.stored_flows = {dpid: flow_list} + + self.napp._store_changed_flows(command, flow_to_install, switch) + mock_save_flow.assert_called() + self.assertEqual(len(self.napp.stored_flows[dpid]['flow_list']), 1)