diff --git a/docs/configuration.rst b/docs/configuration.rst index a16f411323..e742ca600d 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -903,6 +903,9 @@ and actions. Matches are key/values based on the `ryu RESTFul API. `_ Actions is a dictionary of actions to apply upon match. +.. note:: When setting allow to true, the packet will be submitted to the + next table AFTER having the output actions applied to it. + .. list-table:: : acls: : - rule: actions: {} :widths: 30 15 15 40 :header-rows: 1 @@ -933,12 +936,21 @@ Actions is a dictionary of actions to apply upon match. - None - Copy the packet, before any modifications, to the specified port (NOTE: ACL mirroring is done in input direction only) * - output - - dictionary + - dictionary or list - None - - Used to output a packet directly. Details below. + - Used to apply more specific output actions for an ACL The output action contains a dictionary with the following elements: +.. note:: When using the dictionary format, Faucet will + build the actions in the following order: pop_vlans, vlan_vids, swap_vid, + vlan_vids, set_fields, port, ports and then failover. + The ACL dictionary format also restricts using port & ports, vlan_vid & vlan_vids + at the same time. + +.. note:: When using the list format, the output actions will be applied in the + user defined order. + .. list-table:: : acls: : - rule: actions: output: {} :widths: 30 15 15 40 :header-rows: 1 @@ -979,6 +991,10 @@ The output action contains a dictionary with the following elements: - dictionary - None - Output with a failover port (see below). + * - Tunnel + - dictionary + - None + - Generic port output to any port in the stack Failover is an experimental option, but can be configured as follows: @@ -999,6 +1015,35 @@ Failover is an experimental option, but can be configured as follows: - None - The list of ports the packet can be output through. +A tunnel ACL will encapsulate a packet before sending it through the stack topology + +.. note:: Currently tunnel ACLs only support VLAN encapsulation. + +.. list-table:: : acls: : - rule: actions: output: tunnel: {} + :widths: 30 15 15 40 + :header-rows: 1 + + * - Attribute + - Type + - Default + - Description + * - type + - str + - 'vlan' + - The encapsulation type for the packet. Default is to encapsulate using QinQ. + * - tunnel_id + - int/str + - VID that is greater than the largest configured VID + - The ID for the encapsulation type + * - dp + - int/str + - None + - The name or dp_id of the dp where the output port belongs + * - port + - int/str + - None + - The name or port number of the interface on the remote DP to output the packet + .. _gauge-configuration: Gauge configuration diff --git a/docs/tutorials/acls.rst b/docs/tutorials/acls.rst index 9356ba8b00..a1defb06c7 100644 --- a/docs/tutorials/acls.rst +++ b/docs/tutorials/acls.rst @@ -266,14 +266,14 @@ There is also the 'output' action which can be used to achieve the same thing. actions: allow: False output: - port: 4 + - port: 4 - rule: dl_type: 0x86dd ip_proto: 58 actions: allow: False output: - port: 4 + - port: 4 The output action also allows us to change the packet by setting fields @@ -302,7 +302,7 @@ Let's create a new ACL for host2's port that will change the MAC source address. actions: allow: True output: - set_fields: + - set_fields: - eth_src: "00:00:00:00:00:02" ... @@ -356,16 +356,16 @@ To do this we will use both the 'port' & 'vlan_vid' output fields. actions: allow: False output: - vlan_vid: 3 - port: 4 + - vlan_vid: 3 + - port: 4 - rule: dl_type: 0x86dd ip_proto: 58 actions: allow: False output: - vlan_vid: 3 - port: 4 + - vlan_vid: 3 + - port: 4 Again reload Faucet, start a tcpdump on host4, and ping from host1 to host3. diff --git a/faucet/acl.py b/faucet/acl.py index c8a2295ad2..27fb2508e9 100644 --- a/faucet/acl.py +++ b/faucet/acl.py @@ -82,7 +82,7 @@ class ACL(Conf): actions_types = { 'meter': str, 'mirror': (str, int), - 'output': dict, + 'output': (dict, list), 'allow': int, 'force_port_vlan': int, } @@ -98,12 +98,14 @@ class ACL(Conf): 'vlan_vids': list, } tunnel_types = { - 'type': str, - 'tunnel_id': (str, int), + 'type': (str, None), + 'tunnel_id': (str, int, None), 'dp': str, 'port': (str, int), } + mutable_attrs = frozenset(['tunnel_sources']) + def __init__(self, _id, dp_id, conf): self.rules = [] self.exact_match = None @@ -113,21 +115,12 @@ def __init__(self, _id, dp_id, conf): self.set_fields = set() self._ports_resolved = False - #TODO: Would be possible to save the names instead of the DP and port objects - # TUNNEL: - # src_port: PORT object / port number - # source port - # src_dp: DP object - # source dp - # dst_port: PORT object / port number - # final destination port - # dst_dp: DP object - # final destination dp - # tunnel_id: int - # ID to represent the tunnel - # tunnel_type: str ('vlan') - # tunnel type specification + # Tunnel info maintains the tunnel output information for each tunnel rule self.tunnel_info = {} + # Tunnel sources is a list of the sources in the network for this ACL + self.tunnel_sources = [] + # Tunnel rules is the rules for each tunnel in the ACL for each source + self.dyn_tunnel_rules = {} for match_fields in (MATCH_FIELDS, OLD_MATCH_FIELDS): self.rule_types.update({match: (str, int) for match in match_fields}) @@ -173,8 +166,19 @@ def check_config(self): self._check_conf_types(rule_conf, self.actions_types) for action_name, action_conf in rule_conf.items(): if action_name == 'output': - self._check_conf_types( - action_conf, self.output_actions_types) + if isinstance(action_conf, (list, tuple)): + # New ordered format + for subconf in action_conf: + # Make sure only one specified action per list element + test_config_condition( + len(subconf) > 1, + 'ACL ordered output must have only one action per element') + # Ensure correct action format + self._check_conf_types(subconf, self.output_actions_types) + else: + # Old format + self._check_conf_types( + action_conf, self.output_actions_types) def build(self, meters, vid, port_num): """Check that ACL can be built from config.""" @@ -224,151 +228,119 @@ class NullRyuDatapath: return (self.matches, self.set_fields, self.meter) def get_meters(self): + """Yield meters for each rule in ACL""" for rule in self.rules: if 'actions' not in rule or 'meter' not in rule['actions']: continue yield rule['actions']['meter'] def get_mirror_destinations(self): + """Yield mirror destinations for each rule in ACL""" for rule in self.rules: if 'actions' not in rule or 'mirror' not in rule['actions']: continue yield rule['actions']['mirror'] - def get_in_port_match(self, tunnel_id): - """ - Returns a port number of the src_port of the tunnel that the ingress tunnel ACL \ - will need to match to. - Args: - tunnel_id (int): tunnel identifier to obtain the src_port - Returns: - int OR None: src_port number if it exists, None otherwise - """ - return self.tunnel_info[tunnel_id]['src_port'] - - def get_tunnel_id(self, rule_index): - """ - Gets the tunnel ID for the rule - Args: - rule_index (int): Index of the tunnel rule in the self.rules list - Returns: - tunnel_id (int): Identifier for the tunnel - """ - tunnel_conf = self.rules[rule_index] - return tunnel_conf['actions']['output']['tunnel'] - - def unpack_tunnel(self, tunnel_id): - """ - Retrieves the information from the tunnel dict for the tunnel with id - Args: - tunnel_id (int): Identifier for the tunnel - Returns: - (src_dp, src_port, dst_dp, dst_port): Tunnel information - """ - src_dp = self.tunnel_info[tunnel_id]['src_dp'] - src_port = self.tunnel_info[tunnel_id]['src_port'] - dst_dp = self.tunnel_info[tunnel_id]['dst_dp'] - dst_port = self.tunnel_info[tunnel_id]['dst_port'] - return (src_dp, src_port, dst_dp, dst_port) - - def get_tunnel_rule_indices(self): - """ - Get the rules from the rule conf that contain tunnel outputs - Returns: - rules (list): list of integer indices into the self.rules rule list that \ - contain tunnel information - """ - rules = [] - for i, rule_conf in enumerate(self.rules): - if 'actions' in rule_conf: - if 'output' in rule_conf['actions']: - if 'tunnel' in rule_conf['actions']['output']: - rules.append(i) - return rules - - def remove_non_tunnel_rules(self): - """ - Removes all non-tunnel rules from the ACL \ - and removes all match fields and non-tunnel required actions from the tunnel rules - """ - new_rules = [] - tunnel_indices = self.get_tunnel_rule_indices() - for tunnel_index in tunnel_indices: - tunnel_id = self.get_tunnel_id(tunnel_index) - new_rule = {'actions': {'output': {'tunnel': tunnel_id}}} - new_rules.append(new_rule) - self.rules = new_rules - - def verify_tunnel_rules(self, dp): - """ - Verify the actions in the tunnel ACL to by making sure the user hasn't specified an \ - action/match that will create a clash. - Args: - dp (DP): The dp that this tunnel acl object belongs to - TODO: Choose what combinations of matches & actions to disallow with a tunnel - """ - for tunnel_index in self.get_tunnel_rule_indices(): - tunnel_id = self.get_tunnel_id(tunnel_index) - src_dp, _, _, _ = self.unpack_tunnel(tunnel_id) - if dp == src_dp: - self.matches['in_port'] = False - self.set_fields.add('vlan_vid') - else: - self.matches['vlan_vid'] = False - self.remove_non_tunnel_rules() - - def update_tunnel_acl_conf(self, dp): - """ - Update the ACL rule conf if the DP is in the path - Args: - dp (DP): The dp that this tunnel acl object belongs to - Returns: - bool: True if any value was updated - """ - updated = False - for tunnel_index in self.get_tunnel_rule_indices(): - tunnel_id = self.get_tunnel_id(tunnel_index) - src_dp, _, dst_dp, dst_port = self.unpack_tunnel(tunnel_id) - tunnel_rule = self.rules[tunnel_index] - if dp.is_in_path(src_dp, dst_dp): - output_rule = tunnel_rule['actions']['output'] - orig_output_rule = copy.deepcopy(output_rule) - if dp == dst_dp: - if src_dp != dst_dp: - if not 'pop_vlans' in output_rule: - output_rule['pop_vlans'] = 1 - if not 'port' in output_rule: - output_rule['port'] = dst_port + def _resolve_ordered_output_ports(self, output_list, resolve_port_cb, resolve_tunnel_objects): + """Resolve output actions in the ordered list format""" + result = [] + for action in output_list: + for key, value in action.items(): + if key == 'tunnel': + tunnel = value + # Fetch tunnel items from the tunnel output dict + test_config_condition( + 'dp' not in tunnel, + 'ACL (%s) tunnel DP not defined' % self._id) + tunnel_dp = tunnel['dp'] + test_config_condition( + 'port' not in tunnel, + 'ACL (%s) tunnel port not defined' % self._id) + tunnel_port = tunnel['port'] + tunnel_id = tunnel.get('tunnel_id', None) + tunnel_type = tunnel.get('type', 'vlan') + # Resolve the tunnel items + dst_dp, dst_port, tunnel_id = resolve_tunnel_objects( + tunnel_dp, tunnel_port, tunnel_id) + # Compile the tunnel into an easy-access dictionary + tunnel_dict = { + 'dst_dp': dst_dp, + 'dst_port': dst_port, + 'tunnel_id': tunnel_id, + 'type': tunnel_type + } + self.tunnel_info[tunnel_id] = tunnel_dict + result.append({key: tunnel_id}) + elif key == 'port': + port_name = value + port = resolve_port_cb(port_name) + test_config_condition( + not port, + 'ACL (%s) output port undefined in DP: %s' % (self._id, self.dp_id)) + result.append({key: port}) + elif key == 'ports': + resolved_ports = [ + resolve_port_cb(p) for p in value] + test_config_condition( + None in resolved_ports, + 'ACL (%s) output port(s) not defined in DP: %s' % (self._id, self.dp_id)) + result.append({key: resolved_ports}) + elif key == 'failover': + failover = value + test_config_condition(not isinstance(failover, dict), ( + 'failover is not a dictionary')) + failover_dict = {} + for failover_name, failover_values in failover.items(): + if failover_name == 'ports': + resolved_ports = [ + resolve_port_cb(p) for p in failover_values] + test_config_condition( + None in resolved_ports, + 'ACL (%s) failover port(s) not defined in DP: %s' % ( + self._id, self.dp_id)) + failover_dict[failover_name] = resolved_ports + else: + failover_dict[failover_name] = failover_values + result.append({key: failover_dict}) else: - output_port = dp.shortest_path_port(dst_dp.name) - if output_port is None: - continue - port_number = output_port.number - if 'port' not in output_rule: - output_rule['port'] = port_number - elif port_number != output_rule['port']: - output_rule['port'] = port_number - if dp == src_dp: - output_rule['vlan_vid'] = tunnel_id - if output_rule != orig_output_rule: - updated = True - return updated + result.append(action) + return result def _resolve_output_ports(self, action_conf, resolve_port_cb, resolve_tunnel_objects): + """Resolve the values for output actions in the ACL""" + if isinstance(action_conf, (list, tuple)): + return self._resolve_ordered_output_ports( + action_conf, resolve_port_cb, resolve_tunnel_objects) result = {} + test_config_condition( + 'vlan_vid' in action_conf and 'vlan_vids' in action_conf, + 'ACL %s has both vlan_vid and vlan_vids defined' % self._id) + test_config_condition( + 'port' in action_conf and 'ports' in action_conf, + 'ACL %s has both port and ports defined' % self._id) for output_action, output_action_values in action_conf.items(): if output_action == 'tunnel': tunnel = output_action_values - self._check_conf_types(tunnel, self.tunnel_types) - src_dp, src_port, dst_dp, dst_port, tunnel_id = resolve_tunnel_objects( - tunnel['dp'], tunnel['port'], tunnel['tunnel_id']) + # Fetch tunnel items from the tunnel output dict + test_config_condition( + 'dp' not in tunnel, + 'ACL (%s) tunnel DP not defined' % self._id) + tunnel_dp = tunnel['dp'] + test_config_condition( + 'port' not in tunnel, + 'ACL (%s) tunnel port not defined' % self._id) + tunnel_port = tunnel['port'] + tunnel_id = tunnel.get('tunnel_id', None) + tunnel_type = tunnel.get('type', 'vlan') + # Resolve the tunnel items + dst_dp, dst_port, tunnel_id = resolve_tunnel_objects( + tunnel_dp, tunnel_port, tunnel_id) + # Compile the tunnel into an easy-access dictionary tunnel_dict = { - 'src_dp': src_dp, - 'src_port': src_port, 'dst_dp': dst_dp, 'dst_port': dst_port, 'tunnel_id': tunnel_id, - 'type': tunnel['type'], + 'type': tunnel_type } self.tunnel_info[tunnel_id] = tunnel_dict result[output_action] = tunnel_id @@ -412,6 +384,7 @@ def _resolve_output_ports(self, action_conf, resolve_port_cb, resolve_tunnel_obj return result def resolve_ports(self, resolve_port_cb, resolve_tunnel_objects): + """Resolve the values for the actions of an ACL""" if self._ports_resolved: return for rule_conf in self.rules: @@ -438,6 +411,103 @@ def resolve_ports(self, resolve_port_cb, resolve_tunnel_objects): rule_conf['actions'] = resolved_actions self._ports_resolved = True + def get_num_tunnels(self): + """Returns the number of tunnels specified in the ACL""" + num_tunnels = 0 + for rule_conf in self.rules: + if self.does_rule_contain_tunnel(rule_conf): + output_conf = rule_conf['actions']['output'] + if isinstance(output_conf, list): + for action in output_conf: + for key in action: + if key == 'tunnel': + num_tunnels += 1 + else: + if 'tunnel' in output_conf: + num_tunnels += 1 + return num_tunnels + + def get_tunnel_rules(self, tunnel_id): + """Return the list of rules that apply a specific tunnel ID""" + rules = [] + for rule_conf in self.rules: + if self.does_rule_contain_tunnel(rule_conf): + output_conf = rule_conf['actions']['output'] + if isinstance(output_conf, (list, tuple)): + for action in output_conf: + for key, value in action.items(): + if key == 'tunnel' and value == tunnel_id: + rules.append(rule_conf) + continue + else: + if output_conf['tunnel'] == tunnel_id: + rules.append(rule_conf) + return rules + + def does_rule_contain_tunnel(self, rule_conf): + """Return true if the ACL rule contains a tunnel""" + if 'actions' in rule_conf: + if 'output' in rule_conf['actions']: + output_conf = rule_conf['actions']['output'] + if isinstance(output_conf, (list, tuple)): + for action in output_conf: + for key in action: + if key == 'tunnel': + return True + else: + if 'tunnel' in output_conf: + return True + return False + + def is_tunnel_acl(self): + """Return true if the ACL contains a tunnel""" + if self.tunnel_info: + return True + for rule_conf in self.rules: + if self.does_rule_contain_tunnel(rule_conf): + return True + return False + + def add_tunnel_source(self, dp, port): + """Add a source dp/port pair for the tunnel ACL""" + self.tunnel_sources += ({'dp': dp, 'port': port},) + for _id in self.tunnel_info: + self.dyn_tunnel_rules.setdefault(_id, []) + self.dyn_tunnel_rules[_id].append([]) + + def verify_tunnel_rules(self): + """Make sure that matches & set fields are configured correctly to handle tunnels""" + if 'in_port' not in self.matches: + self.matches['in_port'] = False + if 'vlan_vid' not in self.matches: + self.matches['vlan_vid'] = True + if 'vlan_vid' not in self.set_fields: + self.set_fields.add('vlan_vid') + + def update_source_tunnel_rules(self, curr_dp, source_id, tunnel_id, out_port): + """Update the tunnel rulelist for when the output port has changed""" + src_dp = self.tunnel_sources[source_id]['dp'] + dst_dp = self.tunnel_info[tunnel_id]['dst_dp'] + prev_list = self.dyn_tunnel_rules[tunnel_id][source_id] + new_list = [] + if curr_dp == src_dp and curr_dp != dst_dp: + # SRC DP: in_port, actions=[push_vlan, output, pop_vlans] + new_list.append({'vlan_vid': tunnel_id}) + new_list.append({'port': out_port}) + new_list.append({'pop_vlans': 1}) + elif curr_dp == dst_dp and curr_dp != src_dp: + # DST DP: in_port, vlan_vid, actions=[pop_vlan, output] + new_list.append({'pop_vlans': 1}) + new_list.append({'port': out_port}) + else: + # SINGLE DP: in_port, actions=[out_port] + # TRANSIT DP: in_port, vlan_vid, actions=[output] + new_list.append({'port': out_port}) + if new_list != prev_list: + self.dyn_tunnel_rules[tunnel_id][source_id] = new_list + return True + return True + # NOTE: 802.1x steals the port ACL table. PORT_ACL_8021X = ACL( diff --git a/faucet/dp.py b/faucet/dp.py index c08261ca8d..f2b123d085 100644 --- a/faucet/dp.py +++ b/faucet/dp.py @@ -329,19 +329,9 @@ def __init__(self, _id, dp_id, conf): self.table_sizes = {} self.dyn_up_port_nos = set() self.has_externals = None + self.tunnel_acls = [] self.stack_graph = None - #tunnel_id: int - # ID of the tunnel, for now this will be the VLAN ID - #acl: ACL object - # ACL rule to create the relevant tunnel conditions - #updated: bool - # Whether the object has been updated, which will imply that it needs - # to be applied by building the ofmsgs - #{tunnel_id: ACL} - self.tunnel_acls = {} - self.tunnel_updated_flags = {} - super(DP, self).__init__(_id, dp_id, conf) def __str__(self): @@ -433,8 +423,8 @@ def _generate_acl_tables(self): if self.dp_acls: test_config_condition(self.dot1x, ( 'DP ACLs and 802.1x cannot be configured together')) - for acl in self.dp_acls: - all_acls['port_acl'] = self.dp_acls + all_acls.setdefault('port_acl', []) + all_acls['port_acl'].extend(self.dp_acls) else: for port in self.ports.values(): if port.acls_in: @@ -442,13 +432,13 @@ def _generate_acl_tables(self): 'port ACLs and 802.1x cannot be configured together')) all_acls.setdefault('port_acl', []) all_acls['port_acl'].extend(port.acls_in) - if self.dot1x and port.number == self.dot1x['nfv_sw_port']: test_config_condition(not port.output_only, ( 'NFV Ports must have output_only set to True.' )) - - + if self.tunnel_acls: + all_acls.setdefault('port_acl', []) + all_acls['port_acl'].extend(self.tunnel_acls) table_config = {} for table_name, acls in all_acls.items(): matches = {} @@ -472,7 +462,7 @@ def _generate_acl_tables(self): match_types=tuple(sorted(matches.items())), set_fields=tuple(sorted(set_fields)), next_tables=default.next_tables) - # TODO: dynamically configure output attribue + # TODO: dynamically configure output attribute return table_config def _configure_tables(self): @@ -891,29 +881,21 @@ def stack_longest_path_to_root_len(self): return None def finalize_tunnel_acls(self, dps): - """Turn off ACLs not in use and resolve the ACL src dp and port. - - Args: - dps (list): DPs. - """ - remove_ids = [] - for tunnel_id, tunnel_acl in self.tunnel_acls.items(): - asrc_dp = tunnel_acl.tunnel_info[tunnel_id]['src_dp'] - if asrc_dp is not None: - continue + """Resolve each tunnels sources""" + # Resolve the source of the tunnels + for tunnel_acl in self.tunnel_acls: for dp in dps: - if tunnel_id in dp.tunnel_acls: - other_acl = dp.tunnel_acls[tunnel_id] - bsrc_dp = other_acl.tunnel_info[tunnel_id]['src_dp'] - if bsrc_dp is not None: - tunnel_acl.tunnel_info[tunnel_id]['src_dp'] = bsrc_dp - break - if tunnel_acl.tunnel_info[tunnel_id]['src_dp'] is None: - remove_ids.append(tunnel_id) - for tunnel_id in remove_ids: - self.tunnel_acls.pop(tunnel_id) - self.tunnel_updated_flags.update({ - tunnel_id: False for tunnel_id in self.tunnel_acls}) + # Loop through each DP for each port acl + for port in dp.ports.values(): + if port.acls_in: + for acl in port.acls_in: + # Same ACL applied to port + if acl._id == tunnel_acl._id: + tunnel_acl.add_tunnel_source(dp.name, port.number) + # If still no tunnel sources, then ACL is not used + for source in tunnel_acl.tunnel_sources: + if not source: + self.tunnel_acls.remove(tunnel_acl) def shortest_path(self, dest_dp, src_dp=None): """Return shortest path to a DP, as a list of DPs.""" @@ -965,16 +947,31 @@ def shortest_path_port(self, dest_dp): def is_in_path(self, src_dp, dst_dp): """Return True if the current DP is in the path from src_dp to dst_dp - Args: - src_dp (DP): DP - dst_dp (DP): DP + src_dp (str): DP name + dst_dp (str): DP name Returns: bool: True if self is in the path from the src_dp to the dst_dp. """ - path = self.shortest_path(dst_dp.name, src_dp.name) + path = self.shortest_path(dst_dp, src_dp=src_dp) return self.name in path + def peer_symmetric_up_ports(self, peer_dp): + """Return list of stack ports that are up towards us from a peer""" + # Sort adjacent ports by canonical port order + return self.canonical_port_order([ + port.stack['port'] for port in self.stack_ports if port.running() and ( + port.stack['dp'].name == peer_dp)]) + + def shortest_symmetric_path_port(self, adj_dp): + """Return port on our DP that is the first port of the adjacent DP towards us""" + shortest_path = self.shortest_path(self.name, src_dp=adj_dp) + if len(shortest_path) == 2: + adjacent_up_ports = self.peer_symmetric_up_ports(adj_dp) + if adjacent_up_ports: + return adjacent_up_ports[0].stack['port'] + return None + def is_transit_stack_switch(self): """ Return true if this is a stack switch @@ -1019,6 +1016,21 @@ def finalize_config(self, dps): dp_by_name = {} vlan_by_name = {} + def first_unused_vlan_id(vid): + """Returns the first unused VID from the starting vid""" + used_vids = sorted([vlan.vid for vlan in self.vlans.values()]) + while vid in used_vids: + vid += 1 + return vid + + def create_vlan(vid): + """Creates a VLAN object with the VID""" + test_config_condition(vid in self.vlans, ( + 'Attempting to dynamically create a VLAN with ID that already exists')) + vlan = VLAN(vid, self.dp_id, None) + self.vlans[vlan.vid] = vlan + return vlan + def resolve_ports(port_names): """Resolve list of ports, by port by name or number.""" resolved_ports = [] @@ -1112,25 +1124,44 @@ def resolve_tunnel_objects(dst_dp_name, dst_port_name, tunnel_id_name): Args: dst_dp (str): DP of the tunnel's destination port dst_port (int): Destination port of the tunnel - tunnel_id_name (int or str): Tunnel identification number or VLAN reference + tunnel_id_name (int/str/None): Tunnel identification number or VLAN reference Returns: - src_dp, src_port, dst_dp, dst_port (4-Tuple): Resolved - src_dp DP obj, src_port PORT obj, dst_dp DP obj and dst_port PORT obj + dst_dp name, dst_port name and tunnel id """ - tunnel_vlan = resolve_vlan(tunnel_id_name) - if tunnel_vlan: - test_config_condition(not tunnel_vlan.reserved_internal_vlan, ( - 'VLAN %s is required for use by tunnel %s but is not reserved' % ( - tunnel_vlan.name, tunnel_id_name))) - else: - test_config_condition(isinstance(tunnel_id_name, str), ( - 'Tunnel VLAN (%s) does not exist' % tunnel_id_name)) - tunnel_vlan = VLAN(tunnel_id_name, self.dp_id, None) + if not tunnel_id_name: + # Create a VLAN using the first unused VLAN ID + # Get highest non-reserved VLAN + non_res = [vlan.vid for vlan in self.vlans.values() + if not vlan.reserved_internal_vlan] + vlan_offset = sorted(non_res)[-1] + # Also need to account for the potential number of tunnels + ordered_acls = sorted(self.acls) + index = ordered_acls.index(acl_in) + 1 + acl_tunnels = [self.acls[name].get_num_tunnels() for name in ordered_acls] + tunnel_offset = sum(acl_tunnels[:index]) + start_pos = vlan_offset + tunnel_offset + tunnel_vid = first_unused_vlan_id(start_pos) + tunnel_vlan = create_vlan(tunnel_vid) tunnel_vlan.reserved_internal_vlan = True - self.vlans[tunnel_vlan.vid] = tunnel_vlan + else: + # Tunnel ID has been specified, so search for the VLAN + tunnel_vlan = resolve_vlan(tunnel_id_name) + if tunnel_vlan: + # VLAN exists, i.e: user specified the VLAN so check if it is reserved + test_config_condition(not tunnel_vlan.reserved_internal_vlan, ( + 'VLAN %s is required for use by tunnel %s but is not reserved' % ( + tunnel_vlan.name, tunnel_id_name))) + else: + # VLAN does not exist, so the ID should be the VID the user wants + test_config_condition(isinstance(tunnel_id_name, str), ( + 'Tunnel VLAN (%s) does not exist' % tunnel_id_name)) + # Create the tunnel VLAN object + tunnel_vlan = create_vlan(tunnel_id_name) + tunnel_vlan.reserved_internal_vlan = True tunnel_id = tunnel_vlan.vid - test_config_condition(tunnel_id in self.tunnel_acls, ( - 'Tunnel ID %s is already applied to DP %s' % (tunnel_id, self.name))) + test_config_condition( + [t_acl for t_acl in self.tunnel_acls if tunnel_id in t_acl.tunnel_info], + 'Tunnel ID %s is already applied to DP %s' % (tunnel_id, self.name)) test_config_condition(dst_dp_name not in dp_by_name, ( 'Could not find referenced destination DP (%s) for tunnel ACL %s' % ( dst_dp_name, acl_in))) @@ -1140,28 +1171,16 @@ def resolve_tunnel_objects(dst_dp_name, dst_port_name, tunnel_id_name): 'Could not find referenced destination port (%s) for tunnel ACL %s' % ( dst_port_name, acl_in))) dst_port = dst_port.number + dst_dp = dst_dp.name if vid is not None: - #VLAN ACL + # VLAN ACL test_config_condition(True, 'Tunnels do not support VLAN-ACLs') elif dp is not None: - #DP ACL + # DP ACL test_config_condition(True, 'Tunnels do not support DP-ACLs') - elif port_num is not None: - #Port ACL - src_dp = self - src_port = src_dp.resolve_port(port_num) - test_config_condition(src_port is None, ( - 'Could not find source port (%s) in source DP (%s) for tunnel ACL %s' % ( - port_num, src_dp.name, acl_in))) - if src_port is not None: - src_port = src_port.number - elif vid is None and port_num is None: - #Forwarding ACL - src_dp = None - src_port = None - tunnel_vlan.acls_in = [acl] - self.tunnel_acls[tunnel_id] = acl - return (src_dp, src_port, dst_dp, dst_port, tunnel_id) + # Sources will be resolved later on + self.tunnel_acls.append(self.acls[acl_in]) + return (dst_dp, dst_port, tunnel_id) acl.resolve_ports(resolve_port_cb, resolve_tunnel_objects) for meter_name in acl.get_meters(): @@ -1228,12 +1247,13 @@ def resolve_acls(): resolve_acl(acl, dp=self) acls.append(self.acls[acl]) self.dp_acls = acls + # Build unbuilt tunnel ACL rules (DP is not the source of the tunnel) for acl in self.acls: - if self.acls[acl].get_tunnel_rule_indices(): + if self.acls[acl].is_tunnel_acl(): resolve_acl(acl, None) if self.tunnel_acls: - for tunnel_acl in self.tunnel_acls.values(): - tunnel_acl.verify_tunnel_rules(self) + for tunnel_acl in self.tunnel_acls: + tunnel_acl.verify_tunnel_rules() def resolve_routers(): """Resolve VLAN references in routers.""" diff --git a/faucet/port.py b/faucet/port.py index 23b18be21a..706a230b46 100644 --- a/faucet/port.py +++ b/faucet/port.py @@ -459,6 +459,18 @@ def non_stack_forwarding(self): return False return True + def contains_tunnel_acl(self, tunnel_id=None): + """Searches through acls_in for a tunnel ACL with a matching tunnel_id""" + if self.acls_in: + for acl in self.acls_in: + if acl.is_tunnel_acl(): + if tunnel_id: + if acl.get_tunnel_rules(tunnel_id): + return True + else: + return True + return False + # LACP functions def lacp_actor_update(self, lacp_up, now=None, lacp_pkt=None, cold_start=False): """ diff --git a/faucet/valve.py b/faucet/valve.py index 6645882e4c..79d9d5e226 100644 --- a/faucet/valve.py +++ b/faucet/valve.py @@ -646,13 +646,12 @@ def _update_stack_link_state(self, ports, now, other_valves): self.logger.info('%u stack ports changed state' % stack_changes) notify_dps = {} for valve in stacked_valves: - valve.update_tunnel_flowrules() if not valve.dp.dyn_running: continue - ofmsgs_by_valve[valve].extend(valve.get_tunnel_flowmods()) ofmsgs_by_valve[valve].extend(valve.add_vlans(valve.dp.vlans.values())) for port in valve.dp.stack_ports: ofmsgs_by_valve[valve].extend(valve.host_manager.del_port(port)) + ofmsgs_by_valve[valve].extend(valve.get_tunnel_flowmods()) path_port = valve.dp.shortest_path_port(valve.dp.stack_root_name) path_port_number = path_port.number if path_port else 0.0 self._set_var( @@ -669,23 +668,53 @@ def _update_stack_link_state(self, ports, now, other_valves): 'dps': notify_dps }}) break - return ofmsgs_by_valve - def update_tunnel_flowrules(self): - """Update tunnel ACL rules because the stack topology has changed""" - if self.dp.tunnel_acls: - for tunnel_id, tunnel_acl in self.dp.tunnel_acls.items(): - updated = tunnel_acl.update_tunnel_acl_conf(self.dp) - if updated: - self.dp.tunnel_updated_flags[tunnel_id] = True - self.logger.info('updated tunnel %s' % tunnel_id) + def acl_update_tunnel(self, acl): + """Return ofmsgs for a ACL with a tunnel rule""" + ofmsgs = [] + source_vids = {} + for _id, info in acl.tunnel_info.items(): + # Update the tunnel rules for each tunnel action specified + updated_sources = [] + for i, source in enumerate(acl.tunnel_sources): + # Update each tunnel rule for each tunnel source + src_dp = source['dp'] + dst_dp, dst_port = info['dst_dp'], info['dst_port'] + out_port = None + shortest_path = self.dp.shortest_path(dst_dp, src_dp=src_dp) + if self.dp.name in shortest_path: + # We are in the path, so we need to update + if self.dp.name == dst_dp: + out_port = dst_port + if not out_port: + out_port = self.dp.shortest_path_port(dst_dp).number + updated = acl.update_source_tunnel_rules( + self.dp.name, i, _id, out_port) + if updated: + if self.dp.name == src_dp: + source_vids.setdefault(i, []) + source_vids[i].append(_id) + else: + updated_sources.append(i) + if updated_sources: + for source_id in updated_sources: + ofmsgs.extend(self.acl_manager.build_tunnel_rules_ofmsgs( + source_id, _id, acl)) + if source_vids: + for source_id, vids in source_vids.items(): + for vid in vids: + ofmsgs.extend(self.acl_manager.build_tunnel_acl_rule_ofmsgs( + source_id, vid, acl)) + return ofmsgs def get_tunnel_flowmods(self): - """Returns flowmods for the tunnels""" - if self.acl_manager: - return self.acl_manager.create_acl_tunnel(self.dp) - return [] + """Return ofmsgs for all tunnel ACLs in the DP""" + ofmsgs = [] + if self.dp.tunnel_acls: + for acl in self.dp.tunnel_acls: + ofmsgs.extend(self.acl_update_tunnel(acl)) + return ofmsgs def fast_state_expire(self, now, other_valves): """Called periodically to verify the state of stack ports.""" diff --git a/faucet/valve_acl.py b/faucet/valve_acl.py index 8633ac49a7..3e5381040e 100644 --- a/faucet/valve_acl.py +++ b/faucet/valve_acl.py @@ -23,7 +23,6 @@ from faucet.conf import InvalidConfigError - def push_vlan(acl_table, vlan_vid): """Push a VLAN tag with optional selection of eth type.""" vid = vlan_vid @@ -38,6 +37,50 @@ def push_vlan(acl_table, vlan_vid): acl_table, vid, eth_type=vlan_eth_type) +def build_ordered_output_actions(acl_table, output_list, tunnel_rules=None, source_id=None): + """Build actions from ordered ACL output list""" + output_actions = [] + output_ports = [] + output_ofmsgs = [] + for action in output_list: + for key, value in action.items(): + if key == 'pop_vlans': + for _ in range(value): + output_actions.append(valve_of.pop_vlan()) + if key == 'vlan_vid': + output_actions.extend(push_vlan(acl_table, value)) + if key == 'swap_vid': + output_actions.append(acl_table.set_vlan_vid(value)) + if key == 'vlan_vids': + for vlan_vid in value: + output_actions.extend(push_vlan(acl_table, vlan_vid)) + if key == 'set_fields': + for set_field in value: + output_actions.append(acl_table.set_field(**set_field)) + if key == 'port': + output_ports.append(value) + output_actions.append(valve_of.output_port(value)) + if key == 'ports': + for output_port in value: + output_ports.append(output_port) + output_actions.append(valve_of.output_port(output_port)) + if key == 'failover': + group_id = value['group_id'] + buckets = [] + for port in value['ports']: + buckets.append(valve_of.bucket( + watch_port=port, actions=[valve_of.output_port(port)])) + output_ofmsgs.append(valve_of.groupdel(group_id=group_id)) + output_ofmsgs.append(valve_of.groupadd_ff(group_id=group_id, buckets=buckets)) + output_actions.append(valve_of.group_act(group_id=group_id)) + if key == 'tunnel' and tunnel_rules and source_id is not None: + source_rule = tunnel_rules[value][source_id] + _, tunnel_actions, tunnel_ofmsgs = build_output_actions(acl_table, source_rule) + output_actions.extend(tunnel_actions) + tunnel_ofmsgs.extend(tunnel_ofmsgs) + return (output_ports, output_actions, output_ofmsgs) + + def rewrite_vlan(acl_table, output_dict): """Implement actions to rewrite VLAN headers.""" vlan_actions = [] @@ -58,8 +101,10 @@ def rewrite_vlan(acl_table, output_dict): return vlan_actions -def build_output_actions(acl_table, output_dict): +def build_output_actions(acl_table, output_dict, tunnel_rules=None, source_id=None): """Implement actions to alter packet/output.""" + if isinstance(output_dict, (list, tuple)): + return build_ordered_output_actions(acl_table, output_dict, tunnel_rules, source_id) output_actions = [] output_port = None ofmsgs = [] @@ -86,15 +131,20 @@ def build_output_actions(acl_table, output_dict): ofmsgs.append(valve_of.groupdel(group_id=group_id)) ofmsgs.append(valve_of.groupadd_ff(group_id=group_id, buckets=buckets)) output_actions.append(valve_of.group_act(group_id=group_id)) + if 'tunnel' in output_dict and tunnel_rules and source_id is not None: + tunnel_id = output_dict['tunnel'] + source_rule = tunnel_rules[tunnel_id][source_id] + _, tunnel_actions, tunnel_ofmsgs = build_output_actions(acl_table, source_rule) + output_actions.extend(tunnel_actions) + tunnel_ofmsgs.extend(tunnel_ofmsgs) return (output_port, output_actions, ofmsgs) -# TODO: change this, maybe this can be rewritten easily # possibly replace with a class for ACLs def build_acl_entry( # pylint: disable=too-many-arguments,too-many-branches,too-many-statements acl_table, rule_conf, meters, acl_allow_inst, acl_force_port_vlan_inst, - port_num=None, vlan_vid=None): + port_num=None, vlan_vid=None, tunnel_rules=None, source_id=None): """Build flow/groupmods for one ACL rule entry.""" acl_inst = [] acl_act = [] @@ -131,7 +181,7 @@ def build_acl_entry( # pylint: disable=too-many-arguments,too-many-branches,too allow = True if 'output' in attrib_value: output_port, output_actions, output_ofmsgs = build_output_actions( - acl_table, attrib_value['output']) + acl_table, attrib_value['output'], tunnel_rules, source_id) acl_act.extend(output_actions) acl_ofmsgs.extend(output_ofmsgs) @@ -156,6 +206,51 @@ def build_acl_entry( # pylint: disable=too-many-arguments,too-many-branches,too return (acl_match, acl_inst, acl_cookie, acl_ofmsgs) +def build_tunnel_ofmsgs(rule_conf, acl_table, priority, + port_num=None, vlan_vid=None, flowdel=False): + """Build a specific tunnel only ofmsgs""" + ofmsgs = [] + acl_inst = [] + acl_match = [] + acl_match_dict = {} + _, output_actions, output_ofmsgs = build_output_actions(acl_table, rule_conf) + ofmsgs.extend(output_ofmsgs) + acl_inst.append(valve_of.apply_actions(output_actions)) + if port_num is not None: + acl_match_dict['in_port'] = port_num + if vlan_vid is not None: + acl_match_dict['vlan_vid'] = valve_of.vid_present(vlan_vid) + acl_match = valve_of.match_from_dict(acl_match_dict) + flowmod = acl_table.flowmod(acl_match, priority=priority, inst=acl_inst) + if flowdel: + ofmsgs.append(acl_table.flowdel(match=acl_match, priority=priority, strict=False)) + ofmsgs.append(flowmod) + return ofmsgs + + +def build_rule_ofmsgs(rule_conf, acl_table, + acl_allow_inst, acl_force_port_vlan_inst, + highest_priority, acl_rule_priority, meters, + exact_match, port_num=None, vlan_vid=None, + tunnel_rules=None, source_id=None, flowdel=False): + """Build an ACL rule and return OFMSGs""" + ofmsgs = [] + acl_match, acl_inst, acl_cookie, acl_ofmsgs = build_acl_entry( + acl_table, rule_conf, meters, + acl_allow_inst, acl_force_port_vlan_inst, + port_num, vlan_vid, tunnel_rules, source_id) + ofmsgs.extend(acl_ofmsgs) + priority = acl_rule_priority + if exact_match: + priority = highest_priority + flowmod = acl_table.flowmod( + acl_match, priority=priority, inst=acl_inst, cookie=acl_cookie) + if flowdel: + ofmsgs.append(acl_table.flowdel(match=acl_match, priority=priority)) + ofmsgs.append(flowmod) + return ofmsgs + + def build_acl_ofmsgs(acls, acl_table, acl_allow_inst, acl_force_port_vlan_inst, highest_priority, meters, @@ -165,24 +260,17 @@ def build_acl_ofmsgs(acls, acl_table, acl_rule_priority = highest_priority for acl in acls: for rule_conf in acl.rules: - acl_match, acl_inst, acl_cookie, acl_ofmsgs = build_acl_entry( - acl_table, rule_conf, meters, + ofmsgs.extend(build_rule_ofmsgs( + rule_conf, acl_table, acl_allow_inst, acl_force_port_vlan_inst, - port_num, vlan_vid) - ofmsgs.extend(acl_ofmsgs) - if exact_match: - flowmod = acl_table.flowmod( - acl_match, priority=highest_priority, inst=acl_inst, cookie=acl_cookie) - else: - flowmod = acl_table.flowmod( - acl_match, priority=acl_rule_priority, inst=acl_inst, cookie=acl_cookie) - ofmsgs.append(flowmod) + highest_priority, acl_rule_priority, meters, + exact_match, port_num, vlan_vid)) acl_rule_priority -= 1 return ofmsgs def build_acl_port_of_msgs(acl, vid, port_num, acl_table, goto_table, priority): - '''A Helper function for building Openflow Mod Messages for Port ACLs''' + """A Helper function for building Openflow Mod Messages for Port ACLs""" ofmsgs = None if acl.rules: ofmsgs = build_acl_ofmsgs( @@ -216,11 +304,10 @@ def __init__(self, port_acl_table, vlan_acl_table, egress_acl_table, self.vlan_acl_table = vlan_acl_table self.egress_acl_table = egress_acl_table self.pipeline = pipeline - # TODO Rename priorities self.acl_priority = self._FILTER_PRIORITY - self.dot1x_static_rules_priority = self.acl_priority + 1 + self.override_priority = self.acl_priority + 1 self.auth_priority = self._HIGH_PRIORITY - self.dot1x_low_priority = self.auth_priority - 1 + self.low_priority = self.auth_priority - 1 self.meters = meters def initialise_tables(self): @@ -348,7 +435,7 @@ def create_dot1x_flow_pair(self, port_num, nfv_sw_port_num, mac): match=self.port_acl_table.match( in_port=port_num, eth_type=valve_packet.ETH_EAPOL), - priority=self.dot1x_static_rules_priority, + priority=self.override_priority, inst=[valve_of.apply_actions([ self.port_acl_table.set_field(eth_dst=mac), valve_of.output_port(nfv_sw_port_num)])], @@ -358,7 +445,7 @@ def create_dot1x_flow_pair(self, port_num, nfv_sw_port_num, mac): in_port=nfv_sw_port_num, eth_type=valve_packet.ETH_EAPOL, eth_src=mac), - priority=self.dot1x_static_rules_priority, + priority=self.override_priority, inst=[valve_of.apply_actions([ self.port_acl_table.set_field( eth_src=valve_packet.EAPOL_ETH_DST), @@ -377,13 +464,13 @@ def del_dot1x_flow_pair(self, port_num, nfv_sw_port_num, mac): in_port=nfv_sw_port_num, eth_type=valve_packet.ETH_EAPOL, eth_src=mac), - priority=self.dot1x_static_rules_priority, + priority=self.override_priority, ), self.port_acl_table.flowdel( match=self.port_acl_table.match( in_port=port_num, eth_type=valve_packet.ETH_EAPOL), - priority=self.dot1x_static_rules_priority, + priority=self.override_priority, ) ] return ofmsgs @@ -407,7 +494,7 @@ def create_mab_flow(self, port_num, nfv_sw_port_num, mac): udp_src=68, udp_dst=67, ), - priority=self.dot1x_low_priority, + priority=self.low_priority, inst=[valve_of.apply_actions([ self.port_acl_table.set_field(eth_dst=mac), valve_of.output_port(nfv_sw_port_num)])], @@ -432,45 +519,33 @@ def del_mab_flow(self, port_num, nfv_sw_port_num, mac): udp_src=68, udp_dst=67, ), - priority=self.dot1x_low_priority, + priority=self.low_priority, strict=True )] - def create_acl_tunnel(self, dp): - """ - Create tunnel acls from ACLs that require applying in DP \ - Returns flowmods for the tunnel - Args: - dp (DP): DP that contains the tunnel acls to build - """ + def build_tunnel_rules_ofmsgs(self, source_id, tunnel_id, acl): + """Build a tunnel only generated rule""" ofmsgs = [] - if dp.tunnel_acls: - for tunnel_id, tunnel_acl in dp.tunnel_acls.items(): - if not dp.tunnel_updated_flags[tunnel_id]: - continue - in_port_match = tunnel_acl.get_in_port_match(tunnel_id) - vlan_match = None - if in_port_match is None: - vlan_match = tunnel_id - vlan_table = dp.tables.get('vlan') - acl_table = self.vlan_acl_table - acl_allow_inst = None - acl_force_port_vlan_inst = None - #TODO: This will be handled by the vlan manager - # VLANs with reserved_internal_vlan=True will - # handle creating this flow for us - ofmsgs.append(vlan_table.flowmod( - match=vlan_table.match(vlan=tunnel_id), - priority=self.auth_priority, - inst=[vlan_table.goto(acl_table)] - )) - else: - acl_table = self.port_acl_table - acl_allow_inst = self.pipeline.accept_to_vlan() - acl_force_port_vlan_inst = self.pipeline.accept_to_l2_forwarding() - ofmsgs.extend(build_acl_ofmsgs( - [tunnel_acl], acl_table, acl_allow_inst, - acl_force_port_vlan_inst, self.acl_priority, - self.meters, False, in_port_match, vlan_match - )) + acl_table = self.port_acl_table + priority = self.override_priority + ofmsgs.extend(build_tunnel_ofmsgs( + acl.dyn_tunnel_rules[tunnel_id][source_id], acl_table, priority, + None, tunnel_id, flowdel=True)) + return ofmsgs + + def build_tunnel_acl_rule_ofmsgs(self, source_id, tunnel_id, acl): + """Build a rule of an ACL that contains a tunnel""" + ofmsgs = [] + acl_table = self.port_acl_table + acl_allow_inst = self.pipeline.accept_to_vlan() + acl_force_port_vlan_inst = self.pipeline.accept_to_l2_forwarding() + rules = acl.get_tunnel_rules(tunnel_id) + for rule_conf in rules: + rule_index = acl.rules.index(rule_conf) + priority = self.acl_priority - rule_index + in_port = acl.tunnel_sources[source_id]['port'] + ofmsgs.extend(build_rule_ofmsgs( + rule_conf, acl_table, acl_allow_inst, acl_force_port_vlan_inst, + self.acl_priority, priority, self.meters, + acl.exact_match, in_port, None, acl.dyn_tunnel_rules, source_id, flowdel=True)) return ofmsgs diff --git a/tests/integration/mininet_multidp_tests.py b/tests/integration/mininet_multidp_tests.py index 9812fb9262..fa6828e7df 100644 --- a/tests/integration/mininet_multidp_tests.py +++ b/tests/integration/mininet_multidp_tests.py @@ -582,6 +582,144 @@ def test_broadcast(self): self.verify_no_cable_errors() +class FaucetSingleStackOrderedAclControlTest(FaucetMultiDPTest): + """Test ACL control of stacked datapaths with untagged hosts.""" + + NUM_DPS = 3 + NUM_HOSTS = 3 + + def acls(self): + map1, map2, map3 = [self.port_maps[dpid] for dpid in self.dpids] + return { + 1: [ + {'rule': { + 'dl_type': IPV4_ETH, + 'nw_dst': '10.1.0.2', + 'actions': { + 'output': [ + {'port': map1['port_2']} + ] + }, + }}, + {'rule': { + 'dl_type': IPV4_ETH, + 'dl_dst': 'ff:ff:ff:ff:ff:ff', + 'actions': { + 'output': [ + {'ports': [ + map1['port_2'], + map1['port_4']]} + ] + }, + }}, + {'rule': { + 'dl_type': IPV4_ETH, + 'actions': { + 'output': [ + {'port': map1['port_4']} + ] + }, + }}, + {'rule': { + 'actions': { + 'allow': 1, + }, + }}, + ], + 2: [ + {'rule': { + 'dl_type': IPV4_ETH, + 'actions': { + 'output': [ + {'port': map2['port_5']} + ] + }, + }}, + {'rule': { + 'actions': { + 'allow': 1, + }, + }}, + ], + 3: [ + {'rule': { + 'dl_type': IPV4_ETH, + 'nw_dst': '10.1.0.7', + 'actions': { + 'output': { + 'port': map3['port_1'] + } + }, + }}, + {'rule': { + 'dl_type': IPV4_ETH, + 'dl_dst': 'ff:ff:ff:ff:ff:ff', + 'actions': { + 'output': [ + {'ports': [map3['port_1']]} + ] + }, + }}, + {'rule': { + 'dl_type': IPV4_ETH, + 'actions': { + 'allow': 0, + }, + }}, + {'rule': { + 'actions': { + 'allow': 1, + }, + }}, + ], + } + + # DP-to-acl_in port mapping. + def acl_in_dp(self): + map1, map2, map3 = [self.port_maps[dpid] for dpid in self.dpids] + return { + 0: { + # Port 1, acl_in = 1 + map1['port_1']: 1, + }, + 1: { + # Port 4, acl_in = 2 + map2['port_4']: 2, + }, + 2: { + # Port 4, acl_in = 3 + map3['port_4']: 3, + }, + } + + def setUp(self): # pylint: disable=invalid-name + super(FaucetSingleStackOrderedAclControlTest, self).set_up( + stack=True, + n_dps=self.NUM_DPS, + n_untagged=self.NUM_HOSTS, + ) + + def test_unicast(self): + """Hosts in stack topology can appropriately reach each other over unicast.""" + hosts = self.hosts_name_ordered() + self.verify_stack_up() + self.verify_tp_dst_notblocked(5000, hosts[0], hosts[1], table_id=None) + self.verify_tp_dst_blocked(5000, hosts[0], hosts[3], table_id=None) + self.verify_tp_dst_notblocked(5000, hosts[0], hosts[6], table_id=None) + self.verify_tp_dst_blocked(5000, hosts[0], hosts[7], table_id=None) + self.verify_no_cable_errors() + + def test_broadcast(self): + """Hosts in stack topology can appropriately reach each other over broadcast.""" + hosts = self.hosts_name_ordered() + self.verify_stack_up() + self.verify_bcast_dst_notblocked(5000, hosts[0], hosts[1]) + self.verify_bcast_dst_blocked(5000, hosts[0], hosts[3]) + self.verify_bcast_dst_notblocked(5000, hosts[0], hosts[6]) + self.verify_bcast_dst_blocked(5000, hosts[0], hosts[7]) + self.verify_no_cable_errors() + + class FaucetStringOfDPACLOverrideTest(FaucetMultiDPTest): """Test overriding ACL rules""" @@ -736,7 +874,7 @@ def test_tunnel_established(self): self.verify_tunnel_established(src_host, dst_host, other_host) -class FaucetTunnelTest(FaucetMultiDPTest): +class FaucetSingleTunnelTest(FaucetMultiDPTest): """Test the Faucet tunnel ACL option""" NUM_DPS = 2 @@ -779,7 +917,7 @@ def acl_in_dp(self): def setUp(self): # pylint: disable=invalid-name """Start the network""" - super(FaucetTunnelTest, self).set_up( + super(FaucetSingleTunnelTest, self).set_up( stack=True, n_dps=self.NUM_DPS, n_untagged=self.NUM_HOSTS, @@ -793,13 +931,309 @@ def test_tunnel_established(self): self.verify_tunnel_established(src_host, dst_host, other_host) def test_tunnel_path_rerouted(self): - """Test a tunnel path is rerouted when a stack is down.""" + """Test a tunnel path is rerouted when a link is down.""" self.verify_stack_up() + src_host, other_host, dst_host = self.hosts_name_ordered()[:3] + self.verify_tunnel_established(src_host, dst_host, other_host, packets=10) first_stack_port = self.non_host_links(self.dpid)[0].port self.one_stack_port_down(self.dpid, self.DP_NAME, first_stack_port) src_host, other_host, dst_host = self.hosts_name_ordered()[:3] self.verify_tunnel_established(src_host, dst_host, other_host, packets=10) - self.set_port_up(first_stack_port, self.dpid) + + +class FaucetTunnelLoopTest(FaucetSingleTunnelTest): + """Test tunnel on a loop topology""" + + NUM_DPS = 3 + SWITCH_TO_SWITCH_LINKS = 1 + + def setUp(self): # pylint: disable=invalid-name + """Start a loop topology network""" + super(FaucetSingleTunnelTest, self).set_up( + stack=True, + n_dps=self.NUM_DPS, + n_untagged=self.NUM_HOSTS, + switch_to_switch_links=self.SWITCH_TO_SWITCH_LINKS, + hw_dpid=self.hw_dpid, + stack_ring=True) + + +class FaucetTunnelAllowTest(FaucetTopoTestBase): + """Test Tunnels with ACLs containing allow=True""" + + NUM_DPS = 2 + NUM_HOSTS = 4 + NUM_VLANS = 2 + SOFTWARE_ONLY = True + + def acls(self): + """Return config ACL options""" + dpid2 = self.dpids[1] + port2_1 = self.port_maps[dpid2]['port_1'] + return { + 1: [ + {'rule': { + 'dl_type': IPV4_ETH, + 'ip_proto': 1, + 'actions': { + 'allow': 1, + 'output': { + 'tunnel': { + 'type': 'vlan', + 'tunnel_id': 300, + 'dp': 'faucet-2', + 'port': port2_1} + } + } + }}, + {'rule': { + 'actions': { + 'allow': 1, + } + }}, + ] + } + + def acl_in_dp(self): + """DP-to-acl port mapping""" + port_1 = self.port_map['port_1'] + return { + 0: { + # First port 1, acl_in = 1 + port_1: 1, + } + } + + def setUp(self): # pylint: disable=invalid-name + """Start the network""" + super(FaucetTunnelAllowTest, self).setUp() + stack_roots = {0: 1} + dp_links = FaucetTopoGenerator.dp_links_networkx_graph(networkx.path_graph(self.NUM_DPS)) + # LACP host doubly connected to sw0 & sw1 + host_links = {0: [0], 1: [0], 2: [1], 3: [1]} + host_vlans = {0: 0, 1: 0, 2: 1, 3: 0} + self.build_net( + n_dps=self.NUM_DPS, n_vlans=self.NUM_VLANS, dp_links=dp_links, + host_links=host_links, host_vlans=host_vlans, stack_roots=stack_roots) + self.start_net() + + def test_tunnel_continue_through_pipeline_interaction(self): + """Test packets that enter a tunnel with allow, also continue through the pipeline""" + # Should be able to ping from h_{0,100} -> h_{1,100} & h_{3,100} + # and also have the packets arrive at h_{2,200} (the other end of the tunnel) + self.verify_stack_up() + # Ensure connection to the host on the other end of the tunnel can exist + src_host, other_host, dst_host = self.hosts_name_ordered()[:3] + self.verify_tunnel_established(src_host, dst_host, other_host) + # Ensure a connection to a host not in the tunnel can exist + # this implies that the packet is also sent through the pipeline + self.check_host_connectivity_by_id(0, 1) + self.check_host_connectivity_by_id(0, 3) + + +class FaucetTunnelSameDpOrderedTest(FaucetMultiDPTest): + """Test the tunnel ACL option with output to the same DP""" + + NUM_DPS = 2 + NUM_HOSTS = 2 + SOFTWARE_ONLY = True + SWITCH_TO_SWITCH_LINKS = 2 + + def acls(self): + """Return ACL config""" + return { + 1: [ + {'rule': { + 'dl_type': IPV4_ETH, + 'ip_proto': 1, + 'actions': { + 'allow': 0, + 'output': [ + {'tunnel': { + 'type': 'vlan', + 'tunnel_id': 200, + 'dp': 'faucet-1', + 'port': 'b%(port_2)d'}} + ] + } + }} + ] + } + + def acl_in_dp(self): + """DP to acl port mapping""" + port_1 = self.port_map['port_1'] + return { + 0: { + # First port 1, acl_in = 1 + port_1: 1, + } + } + + def test_tunnel_established(self): + """Test a tunnel path can be created.""" + self.set_up(stack=True, n_dps=self.NUM_DPS, n_untagged=self.NUM_HOSTS, + switch_to_switch_links=self.SWITCH_TO_SWITCH_LINKS, hw_dpid=self.hw_dpid) + self.verify_stack_up() + src_host, dst_host, other_host = self.hosts_name_ordered()[:3] + self.verify_tunnel_established(src_host, dst_host, other_host) + + +class FaucetSingleTunnelOrderedTest(FaucetMultiDPTest): + """Test the Faucet tunnel ACL option""" + + NUM_DPS = 2 + NUM_HOSTS = 2 + SOFTWARE_ONLY = True + SWITCH_TO_SWITCH_LINKS = 2 + + def acls(self): + """Return config ACL options""" + dpid2 = self.dpids[1] + port2_1 = self.port_maps[dpid2]['port_1'] + return { + 1: [ + {'rule': { + 'dl_type': IPV4_ETH, + 'ip_proto': 1, + 'actions': { + 'allow': 0, + 'output': [ + {'tunnel': { + 'type': 'vlan', + 'tunnel_id': 200, + 'dp': 'faucet-2', + 'port': port2_1}} + ] + } + }} + ] + } + + def acl_in_dp(self): + """DP-to-acl port mapping""" + port_1 = self.port_map['port_1'] + return { + 0: { + # First port 1, acl_in = 1 + port_1: 1, + } + } + + def setUp(self): # pylint: disable=invalid-name + """Start the network""" + super(FaucetSingleTunnelOrderedTest, self).set_up( + stack=True, + n_dps=self.NUM_DPS, + n_untagged=self.NUM_HOSTS, + switch_to_switch_links=self.SWITCH_TO_SWITCH_LINKS, + hw_dpid=self.hw_dpid) + + def test_tunnel_established(self): + """Test a tunnel path can be created.""" + self.verify_stack_up() + src_host, other_host, dst_host = self.hosts_name_ordered()[:3] + self.verify_tunnel_established(src_host, dst_host, other_host) + + def test_tunnel_path_rerouted(self): + """Test a tunnel path is rerouted when a link is down.""" + self.verify_stack_up() + src_host, other_host, dst_host = self.hosts_name_ordered()[:3] + self.verify_tunnel_established(src_host, dst_host, other_host, packets=10) + first_stack_port = self.non_host_links(self.dpid)[0].port + self.one_stack_port_down(self.dpid, self.DP_NAME, first_stack_port) + src_host, other_host, dst_host = self.hosts_name_ordered()[:3] + self.verify_tunnel_established(src_host, dst_host, other_host, packets=10) + + +class FaucetTunnelLoopOrderedTest(FaucetSingleTunnelOrderedTest): + """Test tunnel on a loop topology""" + + NUM_DPS = 3 + SWITCH_TO_SWITCH_LINKS = 1 + + def setUp(self): # pylint: disable=invalid-name + """Start a loop topology network""" + super(FaucetSingleTunnelOrderedTest, self).set_up( + stack=True, + n_dps=self.NUM_DPS, + n_untagged=self.NUM_HOSTS, + switch_to_switch_links=self.SWITCH_TO_SWITCH_LINKS, + hw_dpid=self.hw_dpid, + stack_ring=True) + + +class FaucetTunnelAllowOrderedTest(FaucetTopoTestBase): + """Test Tunnels with ACLs containing allow=True""" + + NUM_DPS = 2 + NUM_HOSTS = 4 + NUM_VLANS = 2 + SOFTWARE_ONLY = True + + def acls(self): + """Return config ACL options""" + dpid2 = self.dpids[1] + port2_1 = self.port_maps[dpid2]['port_1'] + return { + 1: [ + {'rule': { + 'dl_type': IPV4_ETH, + 'ip_proto': 1, + 'actions': { + 'allow': 1, + 'output': [ + {'tunnel': { + 'type': 'vlan', + 'tunnel_id': 300, + 'dp': 'faucet-2', + 'port': port2_1}} + ] + } + }}, + {'rule': { + 'actions': { + 'allow': 1, + } + }}, + ] + } + + def acl_in_dp(self): + """DP-to-acl port mapping""" + port_1 = self.port_map['port_1'] + return { + 0: { + # First port 1, acl_in = 1 + port_1: 1, + } + } + + def setUp(self): # pylint: disable=invalid-name + """Start the network""" + super(FaucetTunnelAllowOrderedTest, self).setUp() + stack_roots = {0: 1} + dp_links = FaucetTopoGenerator.dp_links_networkx_graph(networkx.path_graph(self.NUM_DPS)) + # LACP host doubly connected to sw0 & sw1 + host_links = {0: [0], 1: [0], 2: [1], 3: [1]} + host_vlans = {0: 0, 1: 0, 2: 1, 3: 0} + self.build_net( + n_dps=self.NUM_DPS, n_vlans=self.NUM_VLANS, dp_links=dp_links, + host_links=host_links, host_vlans=host_vlans, stack_roots=stack_roots) + self.start_net() + + def test_tunnel_continue_through_pipeline_interaction(self): + """Test packets that enter a tunnel with allow, also continue through the pipeline""" + # Should be able to ping from h_{0,100} -> h_{1,100} & h_{3,100} + # and also have the packets arrive at h_{2,200} (the other end of the tunnel) + self.verify_stack_up() + # Ensure connection to the host on the other end of the tunnel can exist + src_host, other_host, dst_host = self.hosts_name_ordered()[:3] + self.verify_tunnel_established(src_host, dst_host, other_host) + # Ensure a connection to a host not in the tunnel can exist + # this implies that the packet is also sent through the pipeline + self.check_host_connectivity_by_id(0, 1) + self.check_host_connectivity_by_id(0, 3) class FaucetSingleUntaggedIPV4RoutingWithStackingTest(FaucetTopoTestBase): diff --git a/tests/integration/mininet_tests.py b/tests/integration/mininet_tests.py index c5311a4182..5d46688586 100644 --- a/tests/integration/mininet_tests.py +++ b/tests/integration/mininet_tests.py @@ -2305,6 +2305,81 @@ def test_untagged(self): second_host, first_host.IP(), require_host_learned=False) +class FaucetNailedForwardingOrderedTest(FaucetUntaggedTest): + + CONFIG_GLOBAL = """ +vlans: + 100: + description: "untagged" +acls: + 1: + - rule: + dl_dst: "0e:00:00:00:02:02" + actions: + output: + - port: %(port_2)d + - rule: + dl_type: 0x806 + dl_dst: "ff:ff:ff:ff:ff:ff" + arp_tpa: "10.0.0.2" + actions: + output: + - port: %(port_2)d + - rule: + actions: + allow: 0 + 2: + - rule: + dl_dst: "0e:00:00:00:01:01" + actions: + output: + - port: %(port_1)d + - rule: + dl_type: 0x806 + dl_dst: "ff:ff:ff:ff:ff:ff" + arp_tpa: "10.0.0.1" + actions: + output: + - port: %(port_1)d + - rule: + actions: + allow: 0 + 3: + - rule: + actions: + allow: 0 + 4: + - rule: + actions: + allow: 0 +""" + + CONFIG = """ + interfaces: + %(port_1)d: + native_vlan: 100 + acl_in: 1 + %(port_2)d: + native_vlan: 100 + acl_in: 2 + %(port_3)d: + native_vlan: 100 + acl_in: 3 + %(port_4)d: + native_vlan: 100 + acl_in: 4 +""" + + def test_untagged(self): + first_host, second_host = self.hosts_name_ordered()[0:2] + first_host.setMAC('0e:00:00:00:01:01') + second_host.setMAC('0e:00:00:00:02:02') + self.one_ipv4_ping( + first_host, second_host.IP(), require_host_learned=False) + self.one_ipv4_ping( + second_host, first_host.IP(), require_host_learned=False) + + class FaucetNailedFailoverForwardingTest(FaucetNailedForwardingTest): CONFIG_GLOBAL = """ @@ -2387,6 +2462,88 @@ def test_untagged(self): third_host, first_host.IP(), require_host_learned=False) +class FaucetNailedFailoverForwardingOrderedTest(FaucetNailedForwardingTest): + + CONFIG_GLOBAL = """ +vlans: + 100: + description: "untagged" +acls: + 1: + - rule: + dl_dst: "0e:00:00:00:02:02" + actions: + output: + - failover: + group_id: 1001 + ports: [%(port_2)d, %(port_3)d] + - rule: + dl_type: 0x806 + dl_dst: "ff:ff:ff:ff:ff:ff" + arp_tpa: "10.0.0.2" + actions: + output: + - failover: + group_id: 1002 + ports: [%(port_2)d, %(port_3)d] + - rule: + actions: + allow: 0 + 2: + - rule: + dl_dst: "0e:00:00:00:01:01" + actions: + output: + - port: %(port_1)d + - rule: + dl_type: 0x806 + dl_dst: "ff:ff:ff:ff:ff:ff" + arp_tpa: "10.0.0.1" + actions: + output: + - port: %(port_1)d + - rule: + actions: + allow: 0 + 3: + - rule: + dl_dst: "0e:00:00:00:01:01" + actions: + output: + - port: %(port_1)d + - rule: + dl_type: 0x806 + dl_dst: "ff:ff:ff:ff:ff:ff" + arp_tpa: "10.0.0.1" + actions: + output: + - port: %(port_1)d + - rule: + actions: + allow: 0 + 4: + - rule: + actions: + allow: 0 +""" + + def test_untagged(self): + first_host, second_host, third_host = self.hosts_name_ordered()[0:3] + first_host.setMAC('0e:00:00:00:01:01') + second_host.setMAC('0e:00:00:00:02:02') + third_host.setMAC('0e:00:00:00:02:02') + third_host.setIP(second_host.IP()) + self.one_ipv4_ping( + first_host, second_host.IP(), require_host_learned=False) + self.one_ipv4_ping( + second_host, first_host.IP(), require_host_learned=False) + self.set_port_down(self.port_map['port_2']) + self.one_ipv4_ping( + first_host, third_host.IP(), require_host_learned=False) + self.one_ipv4_ping( + third_host, first_host.IP(), require_host_learned=False) + + class FaucetUntaggedLLDPBlockedTest(FaucetUntaggedTest): def test_untagged(self): @@ -4714,6 +4871,41 @@ def test_untagged(self): self.verify_ping_mirrored(first_host, second_host, mirror_host) +class FaucetUntaggedOrderedACLOutputMirrorTest(FaucetUntaggedTest): + + CONFIG_GLOBAL = """ +vlans: + 100: + description: "untagged" + unicast_flood: False +acls: + 1: + - rule: + actions: + allow: 1 + output: + - ports: [%(port_3)d] +""" + + CONFIG = """ + interfaces: + %(port_1)d: + native_vlan: 100 + acl_in: 1 + %(port_2)d: + native_vlan: 100 + acl_in: 1 + %(port_3)d: + native_vlan: 100 + %(port_4)d: + native_vlan: 100 +""" + + def test_untagged(self): + first_host, second_host, mirror_host = self.hosts_name_ordered()[0:3] + self.verify_ping_mirrored(first_host, second_host, mirror_host) + + class FaucetUntaggedACLMirrorDefaultAllowTest(FaucetUntaggedACLMirrorTest): CONFIG_GLOBAL = """ @@ -4792,6 +4984,55 @@ def test_untagged(self): '%s: ICMP echo request' % fourth_host.IP(), tcpdump_txt)) +class FaucetMultiOrderedOutputTest(FaucetUntaggedTest): + + CONFIG_GLOBAL = """ +vlans: + 100: + 200: +acls: + multi_out: + - rule: + actions: + output: + - ports: [%(port_2)d, %(port_3)d] +""" + + CONFIG = """ + interfaces: + %(port_1)d: + native_vlan: 100 + acl_in: multi_out + %(port_2)d: + native_vlan: 100 + %(port_3)d: + native_vlan: 200 + %(port_4)d: + native_vlan: 100 +""" + + def test_untagged(self): + first_host, second_host, third_host, fourth_host = self.hosts_name_ordered()[0:4] + tcpdump_filter = ('icmp') + tcpdump_txt = self.tcpdump_helper( + second_host, tcpdump_filter, [ + lambda: first_host.cmd(' '.join((self.FPINGS_ARGS_ONE, second_host.IP())))]) + self.assertTrue(re.search( + '%s: ICMP echo request' % second_host.IP(), tcpdump_txt)) + tcpdump_txt = self.tcpdump_helper( + third_host, tcpdump_filter, [ + lambda: first_host.cmd( + 'arp -s %s %s' % (third_host.IP(), '01:02:03:04:05:06')), + lambda: first_host.cmd(' '.join((self.FPINGS_ARGS_ONE, third_host.IP())))]) + self.assertTrue(re.search( + '%s: ICMP echo request' % third_host.IP(), tcpdump_txt)) + tcpdump_txt = self.tcpdump_helper( + fourth_host, tcpdump_filter, [ + lambda: first_host.cmd(' '.join((self.FPINGS_ARGS_ONE, fourth_host.IP())))]) + self.assertFalse(re.search( + '%s: ICMP echo request' % fourth_host.IP(), tcpdump_txt)) + + class FaucetUntaggedOutputTest(FaucetUntaggedTest): CONFIG_GLOBAL = """ @@ -4839,7 +5080,7 @@ def test_untagged(self): 'vlan 123', tcpdump_txt)) -class FaucetUntaggedMultiVlansOutputTest(FaucetUntaggedTest): +class FaucetUntaggedOrderedOutputTest(FaucetUntaggedTest): CONFIG_GLOBAL = """ vlans: @@ -4852,10 +5093,10 @@ class FaucetUntaggedMultiVlansOutputTest(FaucetUntaggedTest): dl_dst: "01:02:03:04:05:06" actions: output: - set_fields: + - vlan_vid: 123 + - set_fields: - eth_dst: "06:06:06:06:06:06" - vlan_vids: [123, 456] - port: %(port_2)d + - port: %(port_2)d """ CONFIG = """ @@ -4874,7 +5115,7 @@ class FaucetUntaggedMultiVlansOutputTest(FaucetUntaggedTest): def test_untagged(self): first_host, second_host = self.hosts_name_ordered()[0:2] # we expected to see the rewritten address and VLAN - tcpdump_filter = 'vlan' + tcpdump_filter = ('icmp and ether dst 06:06:06:06:06:06') tcpdump_txt = self.tcpdump_helper( second_host, tcpdump_filter, [ lambda: first_host.cmd( @@ -4883,10 +5124,10 @@ def test_untagged(self): self.assertTrue(re.search( '%s: ICMP echo request' % second_host.IP(), tcpdump_txt)) self.assertTrue(re.search( - 'vlan 456.+vlan 123', tcpdump_txt)) + 'vlan 123', tcpdump_txt)) -class FaucetUntaggedMultiConfVlansOutputTest(FaucetUntaggedTest): +class FaucetUntaggedMultiVlansOutputTest(FaucetUntaggedTest): CONFIG_GLOBAL = """ vlans: @@ -4901,7 +5142,7 @@ class FaucetUntaggedMultiConfVlansOutputTest(FaucetUntaggedTest): output: set_fields: - eth_dst: "06:06:06:06:06:06" - vlan_vids: [{vid: 123, eth_type: 0x88a8}, 456] + vlan_vids: [123, 456] port: %(port_2)d """ @@ -4921,20 +5162,162 @@ class FaucetUntaggedMultiConfVlansOutputTest(FaucetUntaggedTest): def test_untagged(self): first_host, second_host = self.hosts_name_ordered()[0:2] # we expected to see the rewritten address and VLAN - tcpdump_filter = 'ether proto 0x88a8' + tcpdump_filter = 'vlan' tcpdump_txt = self.tcpdump_helper( second_host, tcpdump_filter, [ lambda: first_host.cmd( 'arp -s %s %s' % (second_host.IP(), '01:02:03:04:05:06')), - lambda: first_host.cmd(' '.join((self.FPINGS_ARGS_ONE, second_host.IP())))], - packets=1) + lambda: first_host.cmd(' '.join((self.FPINGS_ARGS_ONE, second_host.IP())))]) self.assertTrue(re.search( - '%s: ICMP echo request' % second_host.IP(), tcpdump_txt), msg=tcpdump_txt) + '%s: ICMP echo request' % second_host.IP(), tcpdump_txt)) self.assertTrue(re.search( - 'vlan 456.+ethertype 802.1Q-QinQ, vlan 123', tcpdump_txt), msg=tcpdump_txt) + 'vlan 456.+vlan 123', tcpdump_txt)) -class FaucetUntaggedMirrorTest(FaucetUntaggedTest): +class FaucetUntaggedMultiVlansOrderedOutputTest(FaucetUntaggedTest): + + CONFIG_GLOBAL = """ +vlans: + 100: + description: "untagged" + unicast_flood: False +acls: + 1: + - rule: + dl_dst: "01:02:03:04:05:06" + actions: + output: + - set_fields: + - eth_dst: "06:06:06:06:06:06" + - vlan_vids: [123, 456] + - port: %(port_2)d +""" + + CONFIG = """ + interfaces: + %(port_1)d: + native_vlan: 100 + acl_in: 1 + %(port_2)d: + native_vlan: 100 + %(port_3)d: + native_vlan: 100 + %(port_4)d: + native_vlan: 100 +""" + + def test_untagged(self): + first_host, second_host = self.hosts_name_ordered()[0:2] + # we expected to see the rewritten address and VLAN + tcpdump_filter = 'vlan' + tcpdump_txt = self.tcpdump_helper( + second_host, tcpdump_filter, [ + lambda: first_host.cmd( + 'arp -s %s %s' % (second_host.IP(), '01:02:03:04:05:06')), + lambda: first_host.cmd(' '.join((self.FPINGS_ARGS_ONE, second_host.IP())))]) + self.assertTrue(re.search( + '%s: ICMP echo request' % second_host.IP(), tcpdump_txt)) + self.assertTrue(re.search( + 'vlan 456.+vlan 123', tcpdump_txt)) + + +class FaucetUntaggedMultiConfVlansOutputTest(FaucetUntaggedTest): + + CONFIG_GLOBAL = """ +vlans: + 100: + description: "untagged" + unicast_flood: False +acls: + 1: + - rule: + dl_dst: "01:02:03:04:05:06" + actions: + output: + set_fields: + - eth_dst: "06:06:06:06:06:06" + vlan_vids: [{vid: 123, eth_type: 0x88a8}, 456] + port: %(port_2)d +""" + + CONFIG = """ + interfaces: + %(port_1)d: + native_vlan: 100 + acl_in: 1 + %(port_2)d: + native_vlan: 100 + %(port_3)d: + native_vlan: 100 + %(port_4)d: + native_vlan: 100 +""" + + def test_untagged(self): + first_host, second_host = self.hosts_name_ordered()[0:2] + # we expected to see the rewritten address and VLAN + tcpdump_filter = 'ether proto 0x88a8' + tcpdump_txt = self.tcpdump_helper( + second_host, tcpdump_filter, [ + lambda: first_host.cmd( + 'arp -s %s %s' % (second_host.IP(), '01:02:03:04:05:06')), + lambda: first_host.cmd(' '.join((self.FPINGS_ARGS_ONE, second_host.IP())))], + packets=1) + self.assertTrue(re.search( + '%s: ICMP echo request' % second_host.IP(), tcpdump_txt), msg=tcpdump_txt) + self.assertTrue(re.search( + 'vlan 456.+ethertype 802.1Q-QinQ, vlan 123', tcpdump_txt), msg=tcpdump_txt) + + +class FaucetUntaggedMultiConfVlansOrderedOutputTest(FaucetUntaggedTest): + + CONFIG_GLOBAL = """ +vlans: + 100: + description: "untagged" + unicast_flood: False +acls: + 1: + - rule: + dl_dst: "01:02:03:04:05:06" + actions: + output: + - set_fields: + - eth_dst: "06:06:06:06:06:06" + - vlan_vids: [{vid: 123, eth_type: 0x88a8}, 456] + - port: %(port_2)d +""" + + CONFIG = """ + interfaces: + %(port_1)d: + native_vlan: 100 + acl_in: 1 + %(port_2)d: + native_vlan: 100 + %(port_3)d: + native_vlan: 100 + %(port_4)d: + native_vlan: 100 +""" + + def test_untagged(self): + first_host, second_host = self.hosts_name_ordered()[0:2] + # we expected to see the rewritten address and VLAN + tcpdump_filter = 'ether proto 0x88a8' + tcpdump_txt = self.tcpdump_helper( + second_host, tcpdump_filter, [ + lambda: first_host.cmd( + 'arp -s %s %s' % (second_host.IP(), '01:02:03:04:05:06')), + lambda: first_host.cmd(' '.join((self.FPINGS_ARGS_ONE, second_host.IP())))], + packets=1) + self.assertTrue(re.search( + '%s: ICMP echo request' % second_host.IP(), tcpdump_txt), msg=tcpdump_txt) + self.assertTrue(re.search( + 'vlan 456.+ethertype 802.1Q-QinQ, vlan 123', tcpdump_txt), msg=tcpdump_txt) + + +class FaucetUntaggedMirrorTest(FaucetUntaggedTest): CONFIG_GLOBAL = """ vlans: @@ -5174,6 +5557,56 @@ def test_tagged(self): self.assertTrue(re.search('vlan 100, p 2,', tcpdump_txt)) +class FaucetTaggedVLANPCPOrderedTest(FaucetTaggedTest): + + CONFIG_GLOBAL = """ +vlans: + 100: + description: "tagged" +acls: + 1: + - rule: + vlan_vid: 100 + vlan_pcp: 1 + actions: + output: + - set_fields: + - vlan_pcp: 2 + allow: 1 + - rule: + actions: + allow: 1 +""" + CONFIG = """ + interfaces: + %(port_1)d: + tagged_vlans: [100] + acl_in: 1 + %(port_2)d: + tagged_vlans: [100] + %(port_3)d: + tagged_vlans: [100] + %(port_4)d: + tagged_vlans: [100] +""" + + def test_tagged(self): + first_host, second_host = self.hosts_name_ordered()[:2] + self.quiet_commands( + first_host, + ['ip link set %s type vlan egress %u:1' % ( + first_host.defaultIntf(), i) for i in range(0, 8)]) + self.one_ipv4_ping(first_host, second_host.IP()) + self.wait_nonzero_packet_count_flow( + {'vlan_vid': 100, 'vlan_pcp': 1}, table_id=self._PORT_ACL_TABLE) + tcpdump_filter = 'ether dst %s' % second_host.MAC() + tcpdump_txt = self.tcpdump_helper( + second_host, tcpdump_filter, [ + lambda: first_host.cmd( + 'ping -c3 %s' % second_host.IP())], root_intf=True, packets=1) + self.assertTrue(re.search('vlan 100, p 2,', tcpdump_txt)) + + class FaucetTaggedGlobalIPv4RouteTest(FaucetTaggedTest): def _vids(): # pylint: disable=no-method-argument,no-self-use @@ -5615,6 +6048,60 @@ def test_acl(tcpdump_host, tcpdump_filter): test_acl(third_host, 'vlan 100') +class FaucetTaggedOrderedSwapVidMirrorTest(FaucetTaggedTest): + + CONFIG_GLOBAL = """ +vlans: + 100: + description: "tagged" + 101: + description: "tagged" +acls: + 1: + - rule: + vlan_vid: 100 + actions: + mirror: %(port_3)d + force_port_vlan: 1 + output: + - swap_vid: 101 + allow: 1 +""" + + CONFIG = """ + interfaces: + %(port_1)d: + tagged_vlans: [100] + acl_in: 1 + %(port_2)d: + tagged_vlans: [101] + %(port_3)d: + tagged_vlans: [100] + %(port_4)d: + tagged_vlans: [100] + """ + + def test_tagged(self): + first_host, second_host, third_host = self.hosts_name_ordered()[:3] + + def test_acl(tcpdump_host, tcpdump_filter): + tcpdump_txt = self.tcpdump_helper( + tcpdump_host, tcpdump_filter, [ + lambda: first_host.cmd( + 'arp -s %s %s' % (second_host.IP(), '01:02:03:04:05:06')), + lambda: first_host.cmd(' '.join((self.FPINGS_ARGS_ONE, second_host.IP())))], + root_intf=True) + self.assertTrue(re.search( + '%s: ICMP echo request' % second_host.IP(), tcpdump_txt)) + self.assertTrue(re.search( + tcpdump_filter, tcpdump_txt)) + + # Saw swapped VID on second host + test_acl(second_host, 'vlan 101') + # Saw original VID on mirror host + test_acl(third_host, 'vlan 100') + + class FaucetTaggedSwapVidOutputTest(FaucetTaggedTest): CONFIG_GLOBAL = """ @@ -5664,24 +6151,24 @@ def test_tagged(self): 'vlan 101', tcpdump_txt)) -class FaucetTaggedPopVlansOutputTest(FaucetTaggedTest): +class FaucetTaggedSwapVidOrderedOutputTest(FaucetTaggedTest): CONFIG_GLOBAL = """ vlans: 100: description: "tagged" unicast_flood: False + 101: + description: "tagged" + unicast_flood: False acls: 1: - rule: vlan_vid: 100 - dl_dst: "01:02:03:04:05:06" actions: output: - set_fields: - - eth_dst: "06:06:06:06:06:06" - pop_vlans: 1 - port: %(port_2)d + - swap_vid: 101 + - port: %(port_2)d """ CONFIG = """ @@ -5690,7 +6177,7 @@ class FaucetTaggedPopVlansOutputTest(FaucetTaggedTest): tagged_vlans: [100] acl_in: 1 %(port_2)d: - tagged_vlans: [100] + tagged_vlans: [101] %(port_3)d: tagged_vlans: [100] %(port_4)d: @@ -5699,30 +6186,126 @@ class FaucetTaggedPopVlansOutputTest(FaucetTaggedTest): def test_tagged(self): first_host, second_host = self.hosts_name_ordered()[0:2] - tcpdump_filter = 'not vlan and icmp and ether dst 06:06:06:06:06:06' + # we expected to see the swapped VLAN VID + tcpdump_filter = 'vlan 101' tcpdump_txt = self.tcpdump_helper( second_host, tcpdump_filter, [ lambda: first_host.cmd( 'arp -s %s %s' % (second_host.IP(), '01:02:03:04:05:06')), - lambda: first_host.cmd( - ' '.join((self.FPINGS_ARGS_ONE, second_host.IP())))], - packets=10, root_intf=True) + lambda: first_host.cmd(' '.join((self.FPINGS_ARGS_ONE, second_host.IP())))], + root_intf=True) self.assertTrue(re.search( '%s: ICMP echo request' % second_host.IP(), tcpdump_txt)) + self.assertTrue(re.search( + 'vlan 101', tcpdump_txt)) -class FaucetTaggedIPv4ControlPlaneTest(FaucetTaggedTest): +class FaucetTaggedPopVlansOutputTest(FaucetTaggedTest): CONFIG_GLOBAL = """ vlans: 100: description: "tagged" - faucet_vips: ["10.0.0.254/24"] -""" - - CONFIG = """ - max_resolve_backoff_time: 1 -""" + CONFIG_TAGGED_BOILER + unicast_flood: False +acls: + 1: + - rule: + vlan_vid: 100 + dl_dst: "01:02:03:04:05:06" + actions: + output: + set_fields: + - eth_dst: "06:06:06:06:06:06" + pop_vlans: 1 + port: %(port_2)d +""" + + CONFIG = """ + interfaces: + %(port_1)d: + tagged_vlans: [100] + acl_in: 1 + %(port_2)d: + tagged_vlans: [100] + %(port_3)d: + tagged_vlans: [100] + %(port_4)d: + tagged_vlans: [100] +""" + + def test_tagged(self): + first_host, second_host = self.hosts_name_ordered()[0:2] + tcpdump_filter = 'not vlan and icmp and ether dst 06:06:06:06:06:06' + tcpdump_txt = self.tcpdump_helper( + second_host, tcpdump_filter, [ + lambda: first_host.cmd( + 'arp -s %s %s' % (second_host.IP(), '01:02:03:04:05:06')), + lambda: first_host.cmd( + ' '.join((self.FPINGS_ARGS_ONE, second_host.IP())))], + packets=10, root_intf=True) + self.assertTrue(re.search( + '%s: ICMP echo request' % second_host.IP(), tcpdump_txt)) + + +class FaucetTaggedPopVlansOrderedOutputTest(FaucetTaggedTest): + + CONFIG_GLOBAL = """ +vlans: + 100: + description: "tagged" + unicast_flood: False +acls: + 1: + - rule: + vlan_vid: 100 + dl_dst: "01:02:03:04:05:06" + actions: + output: + - set_fields: + - eth_dst: "06:06:06:06:06:06" + - pop_vlans: 1 + - port: %(port_2)d +""" + + CONFIG = """ + interfaces: + %(port_1)d: + tagged_vlans: [100] + acl_in: 1 + %(port_2)d: + tagged_vlans: [100] + %(port_3)d: + tagged_vlans: [100] + %(port_4)d: + tagged_vlans: [100] +""" + + def test_tagged(self): + first_host, second_host = self.hosts_name_ordered()[0:2] + tcpdump_filter = 'not vlan and icmp and ether dst 06:06:06:06:06:06' + tcpdump_txt = self.tcpdump_helper( + second_host, tcpdump_filter, [ + lambda: first_host.cmd( + 'arp -s %s %s' % (second_host.IP(), '01:02:03:04:05:06')), + lambda: first_host.cmd( + ' '.join((self.FPINGS_ARGS_ONE, second_host.IP())))], + packets=10, root_intf=True) + self.assertTrue(re.search( + '%s: ICMP echo request' % second_host.IP(), tcpdump_txt)) + + +class FaucetTaggedIPv4ControlPlaneTest(FaucetTaggedTest): + + CONFIG_GLOBAL = """ +vlans: + 100: + description: "tagged" + faucet_vips: ["10.0.0.254/24"] +""" + + CONFIG = """ + max_resolve_backoff_time: 1 +""" + CONFIG_TAGGED_BOILER def test_ping_controller(self): first_host, second_host = self.hosts_name_ordered()[0:2] @@ -5800,6 +6383,53 @@ def test_icmpv6_acl_match(self): 'ipv6_nd_target': 'fc00::1:2'}, table_id=self._PORT_ACL_TABLE) +class FaucetTaggedICMPv6OrderedACLTest(FaucetTaggedTest): + + CONFIG_GLOBAL = """ +acls: + 1: + - rule: + dl_type: %u + vlan_vid: 100 + ip_proto: 58 + icmpv6_type: 135 + ipv6_nd_target: "fc00::1:2" + actions: + output: + - port: %s + - rule: + actions: + allow: 1 +vlans: + 100: + description: "tagged" + faucet_vips: ["fc00::1:254/112"] +""" % (IPV6_ETH, '%(port_2)d') + + CONFIG = """ + max_resolve_backoff_time: 1 + interfaces: + %(port_1)d: + tagged_vlans: [100] + acl_in: 1 + %(port_2)d: + tagged_vlans: [100] + %(port_3)d: + tagged_vlans: [100] + %(port_4)d: + tagged_vlans: [100] +""" + + def test_icmpv6_acl_match(self): + first_host, second_host = self.hosts_name_ordered()[0:2] + self.add_host_ipv6_address(first_host, 'fc00::1:1/112') + self.add_host_ipv6_address(second_host, 'fc00::1:2/112') + self.one_ipv6_ping(first_host, 'fc00::1:2') + self.wait_nonzero_packet_count_flow( + {'dl_type': IPV6_ETH, 'ip_proto': 58, 'icmpv6_type': 135, + 'ipv6_nd_target': 'fc00::1:2'}, table_id=self._PORT_ACL_TABLE) + + class FaucetTaggedIPv4RouteTest(FaucetTaggedTest): CONFIG_GLOBAL = """ @@ -6338,6 +6968,102 @@ def test_untagged(self): self.one_ipv4_ping(first_host, remote_ip2.ip) +class FaucetUntaggedIPv4PolicyRouteOrdereredTest(FaucetUntaggedTest): + + CONFIG_GLOBAL = """ +vlans: + 100: + description: "100" + faucet_vips: ["10.0.0.254/24"] + acl_in: pbr + 200: + description: "200" + faucet_vips: ["10.20.0.254/24"] + routes: + - route: + ip_dst: "10.99.0.0/24" + ip_gw: "10.20.0.2" + 300: + description: "300" + faucet_vips: ["10.30.0.254/24"] + routes: + - route: + ip_dst: "10.99.0.0/24" + ip_gw: "10.30.0.3" +acls: + pbr: + - rule: + vlan_vid: 100 + dl_type: 0x800 + nw_dst: "10.99.0.2" + actions: + allow: 1 + output: + - swap_vid: 300 + - rule: + vlan_vid: 100 + dl_type: 0x800 + nw_dst: "10.99.0.0/24" + actions: + allow: 1 + output: + - swap_vid: 200 + - rule: + actions: + allow: 1 +routers: + router-100-200: + vlans: [100, 200] + router-100-300: + vlans: [100, 300] +""" + CONFIG = """ + arp_neighbor_timeout: 2 + max_resolve_backoff_time: 1 + interfaces: + %(port_1)d: + native_vlan: 100 + %(port_2)d: + native_vlan: 200 + %(port_3)d: + native_vlan: 300 + %(port_4)d: + native_vlan: 100 +""" + + def test_untagged(self): + # 10.99.0.1 is on b2, and 10.99.0.2 is on b3 + # we want to route 10.99.0.0/24 to b2, but we want + # want to PBR 10.99.0.2/32 to b3. + first_host_ip = ipaddress.ip_interface('10.0.0.1/24') + first_faucet_vip = ipaddress.ip_interface('10.0.0.254/24') + second_host_ip = ipaddress.ip_interface('10.20.0.2/24') + second_faucet_vip = ipaddress.ip_interface('10.20.0.254/24') + third_host_ip = ipaddress.ip_interface('10.30.0.3/24') + third_faucet_vip = ipaddress.ip_interface('10.30.0.254/24') + first_host, second_host, third_host = self.hosts_name_ordered()[:3] + remote_ip = ipaddress.ip_interface('10.99.0.1/24') + remote_ip2 = ipaddress.ip_interface('10.99.0.2/24') + second_host.setIP(str(second_host_ip.ip), prefixLen=24) + third_host.setIP(str(third_host_ip.ip), prefixLen=24) + self.host_ipv4_alias(second_host, remote_ip) + self.host_ipv4_alias(third_host, remote_ip2) + self.add_host_route(first_host, remote_ip, first_faucet_vip.ip) + self.add_host_route(second_host, first_host_ip, second_faucet_vip.ip) + self.add_host_route(third_host, first_host_ip, third_faucet_vip.ip) + # ensure all nexthops resolved. + self.one_ipv4_ping(first_host, first_faucet_vip.ip) + self.one_ipv4_ping(second_host, second_faucet_vip.ip) + self.one_ipv4_ping(third_host, third_faucet_vip.ip) + self.wait_for_route_as_flow( + second_host.MAC(), ipaddress.IPv4Network('10.99.0.0/24'), vlan_vid=200) + self.wait_for_route_as_flow( + third_host.MAC(), ipaddress.IPv4Network('10.99.0.0/24'), vlan_vid=300) + # verify b1 can reach 10.99.0.1 and .2 on b2 and b3 respectively. + self.one_ipv4_ping(first_host, remote_ip.ip) + self.one_ipv4_ping(first_host, remote_ip2.ip) + + class FaucetUntaggedMixedIPv4RouteTest(FaucetUntaggedTest): CONFIG_GLOBAL = """ @@ -6844,6 +7570,96 @@ def test_switching(self): source_host, overridden_host, rewrite_host, overridden_host) +class FaucetDestRewriteOrderedTest(FaucetUntaggedTest): + + def override_mac(): # pylint: disable=no-method-argument,no-self-use + return '0e:00:00:00:00:02' + + OVERRIDE_MAC = override_mac() + + def rewrite_mac(): # pylint: disable=no-method-argument,no-self-use + return '0e:00:00:00:00:03' + + REWRITE_MAC = rewrite_mac() + + CONFIG_GLOBAL = """ +vlans: + 100: + description: "untagged" + +acls: + 1: + - rule: + dl_dst: "%s" + actions: + allow: 1 + output: + - set_fields: + - eth_dst: "%s" + - rule: + actions: + allow: 1 +""" % (override_mac(), rewrite_mac()) + CONFIG = """ + interfaces: + %(port_1)d: + native_vlan: 100 + acl_in: 1 + %(port_2)d: + native_vlan: 100 + %(port_3)d: + native_vlan: 100 + %(port_4)d: + native_vlan: 100 +""" + + def test_untagged(self): + first_host, second_host = self.hosts_name_ordered()[0:2] + # we expect to see the rewritten mac address. + tcpdump_filter = ('icmp and ether dst %s' % self.REWRITE_MAC) + tcpdump_txt = self.tcpdump_helper( + second_host, tcpdump_filter, [ + lambda: first_host.cmd( + 'arp -s %s %s' % (second_host.IP(), self.OVERRIDE_MAC)), + lambda: first_host.cmd(' '.join((self.FPINGS_ARGS_ONE, second_host.IP())))], + timeout=5, packets=1) + self.assertTrue(re.search( + '%s: ICMP echo request' % second_host.IP(), tcpdump_txt)) + + def verify_dest_rewrite(self, source_host, overridden_host, rewrite_host, tcpdump_host): + overridden_host.setMAC(self.OVERRIDE_MAC) + rewrite_host.setMAC(self.REWRITE_MAC) + rewrite_host.cmd('arp -s %s %s' % (overridden_host.IP(), overridden_host.MAC())) + rewrite_host.cmd(' '.join((self.FPINGS_ARGS_ONE, overridden_host.IP()))) + self.wait_until_matching_flow( + {'dl_dst': self.REWRITE_MAC}, + table_id=self._ETH_DST_TABLE, + actions=['OUTPUT:%u' % self.port_map['port_3']]) + tcpdump_filter = ('icmp and ether src %s and ether dst %s' % ( + source_host.MAC(), rewrite_host.MAC())) + tcpdump_txt = self.tcpdump_helper( + tcpdump_host, tcpdump_filter, [ + lambda: source_host.cmd( + 'arp -s %s %s' % (rewrite_host.IP(), overridden_host.MAC())), + # this will fail if no reply + lambda: self.one_ipv4_ping( + source_host, rewrite_host.IP(), require_host_learned=False)], + timeout=3, packets=1) + # ping from h1 to h2.mac should appear in third host, and not second host, as + # the acl should rewrite the dst mac. + self.assertFalse(re.search( + '%s: ICMP echo request' % rewrite_host.IP(), tcpdump_txt)) + + def test_switching(self): + """Tests that a acl can rewrite the destination mac address, + and the packet will only go out the port of the new mac. + (Continues through faucet pipeline) + """ + source_host, overridden_host, rewrite_host = self.hosts_name_ordered()[0:3] + self.verify_dest_rewrite( + source_host, overridden_host, rewrite_host, overridden_host) + + class FaucetSetFieldsTest(FaucetUntaggedTest): # A generic test to verify that a flow will set fields specified for # matching packets @@ -6947,6 +7763,110 @@ def test_set_fields_icmp(self): def test_untagged(self): pass + +class FaucetOrderedSetFieldsTest(FaucetUntaggedTest): + # A generic test to verify that a flow will set fields specified for + # matching packets + OUTPUT_MAC = '0f:00:12:23:48:03' + SRC_MAC = '0f:12:00:00:00:ff' + + IP_DSCP_VAL = 46 + # this is the converted DSCP value that is displayed + NW_TOS_VAL = 184 + IPV4_SRC_VAL = "192.0.2.0" + IPV4_DST_VAL = "198.51.100.0" + # ICMP echo request + ICMPV4_TYPE_VAL = 8 + UDP_SRC_PORT = 68 + UDP_DST_PORT = 67 + + CONFIG_GLOBAL = """ +vlans: + 100: + description: "untagged" + +acls: + 1: + - rule: + eth_type: 0x0800 + actions: + allow: 1 + output: + - set_fields: + - ipv4_src: '%s' + - ipv4_dst: '%s' + - ip_dscp: %d + - rule: + eth_type: 0x0800 + ip_proto: 1 + actions: + allow: 1 + output: + - set_fields: + - icmpv4_type: %d +""" % (IPV4_SRC_VAL, IPV4_DST_VAL, IP_DSCP_VAL, ICMPV4_TYPE_VAL) + CONFIG = """ + interfaces: + %(port_1)d: + native_vlan: 100 + acl_in: 1 + %(port_2)d: + native_vlan: 100 + %(port_3)d: + native_vlan: 100 + %(port_4)d: + native_vlan: 100 + """ + + def test_set_fields_generic_udp(self): + # Send a basic UDP packet through the faucet pipeline and verify that + # the expected fields were updated via tcpdump output + source_host, dest_host = self.hosts_name_ordered()[0:2] + dest_host.setMAC(self.OUTPUT_MAC) + + # scapy command to create and send a UDP packet + scapy_pkt = self.scapy_base_udp( + self.SRC_MAC, source_host.defaultIntf(), source_host.IP(), + dest_host.IP(), self.UDP_DST_PORT, self.UDP_SRC_PORT, + dst=self.OUTPUT_MAC) + + tcpdump_filter = "ether dst %s" % self.OUTPUT_MAC + tcpdump_txt = self.tcpdump_helper( + dest_host, tcpdump_filter, [lambda: source_host.cmd(scapy_pkt)], + root_intf=True, packets=1) + + # verify that the packet we've received on the dest_host has the + # overwritten values + self.assertTrue( + re.search("%s.%s > %s.%s" % (self.IPV4_SRC_VAL, self.UDP_SRC_PORT, + self.IPV4_DST_VAL, self.UDP_DST_PORT), + tcpdump_txt)) + # check the packet's converted dscp value + self.assertTrue(re.search("tos %s" % hex(self.NW_TOS_VAL), tcpdump_txt)) + + def test_set_fields_icmp(self): + # Send a basic ICMP packet through the faucet pipeline and verify that + # the expected fields were updated via tcpdump output + source_host, dest_host = self.hosts_name_ordered()[0:2] + dest_host.setMAC(self.OUTPUT_MAC) + + # scapy command to create and send an ICMP packet + scapy_pkt = self.scapy_icmp( + self.SRC_MAC, source_host.defaultIntf(), source_host.IP(), + dest_host.IP(), dst=self.OUTPUT_MAC) + + tcpdump_filter = "ether dst %s" % self.OUTPUT_MAC + tcpdump_txt = self.tcpdump_helper( + dest_host, tcpdump_filter, [lambda: source_host.cmd(scapy_pkt)], + root_intf=True, packets=1) + + # verify that the packet we've received on the dest_host has been + # overwritten to be an ICMP echo request + self.assertTrue(re.search("ICMP echo request", tcpdump_txt)) + + def test_untagged(self): + pass + class FaucetDscpMatchTest(FaucetUntaggedTest): # Match all packets with this IP_DSP and eth_type, based on the ryu API def # e.g {"ip_dscp": 3, "eth_type": 2048} @@ -7017,6 +7937,76 @@ def test_untagged(self): tcpdump_txt)) +class FaucetOrderedDscpMatchTest(FaucetUntaggedTest): + # Match all packets with this IP_DSP and eth_type, based on the ryu API def + # e.g {"ip_dscp": 3, "eth_type": 2048} + # Note: the ip_dscp field is translated to nw_tos in OpenFlow 1.0: + # see https://tools.ietf.org/html/rfc2474#section-3 + IP_DSCP_MATCH = 46 + ETH_TYPE = 2048 + + SRC_MAC = '0e:00:00:00:00:ff' + DST_MAC = '0e:00:00:00:00:02' + + REWRITE_MAC = '0f:00:12:23:48:03' + + CONFIG_GLOBAL = """ +vlans: + 100: + description: "untagged" + +acls: + 1: + - rule: + ip_dscp: %d + dl_type: 0x800 + actions: + allow: 1 + output: + - set_fields: + - eth_dst: "%s" + - rule: + actions: + allow: 1 +""" % (IP_DSCP_MATCH, REWRITE_MAC) + CONFIG = """ + interfaces: + %(port_1)d: + native_vlan: 100 + acl_in: 1 + %(port_2)d: + native_vlan: 100 + %(port_3)d: + native_vlan: 100 + %(port_4)d: + native_vlan: 100 + """ + + def test_untagged(self): + # Tests that a packet with an ip_dscp field will be appropriately + # matched and proceeds through the faucet pipeline. This test verifies + # that packets with the dscp field can have their eth_dst field modified + source_host, dest_host = self.hosts_name_ordered()[0:2] + dest_host.setMAC(self.REWRITE_MAC) + self.wait_until_matching_flow( + {'ip_dscp': self.IP_DSCP_MATCH, + 'eth_type': self.ETH_TYPE}, + table_id=self._PORT_ACL_TABLE) + + # scapy command to create and send a packet with the specified fields + scapy_pkt = self.scapy_dscp(self.SRC_MAC, self.DST_MAC, 184, + source_host.defaultIntf()) + + tcpdump_filter = "ether dst %s" % self.REWRITE_MAC + tcpdump_txt = self.tcpdump_helper( + dest_host, tcpdump_filter, [lambda: source_host.cmd(scapy_pkt)], + root_intf=True, packets=1) + # verify that the packet we've received on the dest_host is from the + # source MAC address + self.assertTrue(re.search("%s > %s" % (self.SRC_MAC, self.REWRITE_MAC), + tcpdump_txt)) + + @unittest.skip('use_idle_timeout unreliable') class FaucetWithUseIdleTimeoutTest(FaucetUntaggedTest): CONFIG_GLOBAL = """ diff --git a/tests/unit/faucet/test_config.py b/tests/unit/faucet/test_config.py index 83fe6fd2ff..c74c63de88 100755 --- a/tests/unit/faucet/test_config.py +++ b/tests/unit/faucet/test_config.py @@ -390,6 +390,30 @@ def test_good_set_fields(self): """ self.check_config_success(config, cp.dp_parser) + def test_good_set_fields_ordered(self): + """Test good set_fields.""" + config = """ +acls: + good_acl: + rules: + - rule: + actions: + output: + - set_fields: + - eth_dst: "0e:00:00:00:00:01" +vlans: + guest: + vid: 100 +dps: + sw1: + dp_id: 0x1 + interfaces: + 1: + native_vlan: 100 + acl_in: good_acl +""" + self.check_config_success(config, cp.dp_parser) + def test_push_pop_vlans_acl(self): """Test push and pop VLAN ACL fields.""" config = """ @@ -415,6 +439,31 @@ def test_push_pop_vlans_acl(self): """ self.check_config_success(config, cp.dp_parser) + def test_push_pop_vlans_acl_ordered(self): + """Test push and pop VLAN ACL fields.""" + config = """ +acls: + good_acl: + rules: + - rule: + actions: + output: + - pop_vlans: 1 + - vlan_vids: + - { vid: 200, eth_type: 0x8100 } +vlans: + guest: + vid: 100 +dps: + sw1: + dp_id: 0x1 + interfaces: + 1: + native_vlan: 100 + acl_in: good_acl +""" + self.check_config_success(config, cp.dp_parser) + def test_dp_acls(self): """Test DP ACLs.""" config = """ @@ -438,6 +487,29 @@ def test_dp_acls(self): """ self.check_config_success(config, cp.dp_parser) + def test_dp_acls_ordered(self): + """Test DP ACLs.""" + config = """ +acls: + good_acl: + rules: + - rule: + actions: + output: + - port: 1 +vlans: + guest: + vid: 100 +dps: + sw1: + dp_id: 0x1 + dp_acls: [good_acl] + interfaces: + 1: + native_vlan: 100 +""" + self.check_config_success(config, cp.dp_parser) + def test_force_port_vlan(self): """Test push force_port_vlan.""" config = """ @@ -463,6 +535,31 @@ def test_force_port_vlan(self): """ self.check_config_success(config, cp.dp_parser) + def test_force_port_vlan_ordered(self): + """Test push force_port_vlan.""" + config = """ +acls: + good_acl: + rules: + - rule: + actions: + allow: 1 + force_port_vlan: 1 + output: + - swap_vid: 101 +vlans: + guest: + vid: 100 +dps: + sw1: + dp_id: 0x1 + interfaces: + 1: + tagged_vlans: [100] + acl_in: good_acl +""" + self.check_config_success(config, cp.dp_parser) + def test_failover_acl(self): """Test failover ACL fields.""" config = """ @@ -488,6 +585,31 @@ def test_failover_acl(self): """ self.check_config_success(config, cp.dp_parser) + def test_failover_acl_ordered(self): + """Test failover ACL fields.""" + config = """ +acls: + good_acl: + rules: + - rule: + actions: + output: + - failover: + group_id: 1 + ports: [1] +vlans: + guest: + vid: 100 +dps: + sw1: + dp_id: 0x1 + interfaces: + 1: + native_vlan: 100 + acl_in: good_acl +""" + self.check_config_success(config, cp.dp_parser) + def test_unreferenced_acl(self): """Test an unresolveable port in an ACL that is not referenced is OK.""" config = """ @@ -510,6 +632,28 @@ def test_unreferenced_acl(self): """ self.check_config_success(config, cp.dp_parser) + def test_unreferenced_acl_ordered(self): + """Test an unresolveable port in an ACL that is not referenced is OK.""" + config = """ +acls: + unreferenced_acl: + rules: + - rule: + actions: + output: + - port: 99 +vlans: + guest: + vid: 100 +dps: + sw1: + dp_id: 0x1 + interfaces: + 1: + native_vlan: 100 +""" + self.check_config_success(config, cp.dp_parser) + def test_default_bgp_connect(self): """Test default setting for BGP connect mode.""" config = """ @@ -816,6 +960,59 @@ def test_acl_multi_dp_output_rule(self): msg='acl output port resolved incorrectly' ) + def test_acl_multi_dp_output_rule_ordered(self): + """Verify that an acl can output to different ports with the same name + on different DPs' + """ + config = """ +vlans: + v100: + vid: 100 + acls_in: [test] +acls: + test: + - rule: + dl_type: 0x800 # ipv4 + actions: + output: + - port: 'target' +dps: + s1: + dp_id: 0x1 + hardware: "Open vSwitch" + interfaces: + 1: + native_vlan: 'v100' + 2: + name: 'target' + native_vlan: 'v100' + s2: + dp_id: 0x2 + hardware: "Open vSwitch" + interfaces: + 1: + native_vlan: 'v100' + 3: + name: 'target' + native_vlan: 'v100' +""" + conf_file = self.create_config_file(config) + _, _, dps, _ = cp.dp_parser(conf_file, LOGNAME) + outputs = { + 's1': 2, + 's2': 3 + } + for dp in dps: + v100 = dp.vlans[100] + for acl in v100.acls_in: + for rule in acl.rules: + port = rule['actions']['output'][0]['port'] + self.assertEqual( + outputs[dp.name], + port, + msg='acl output port resolved incorrectly' + ) + def test_port_range_valid_config(self): """Test if port range config applied correctly""" config = """ @@ -1189,6 +1386,43 @@ def test_tunnel_config_valid_accepted(self): """ self.check_config_success(config, cp.dp_parser) + def test_tunnel_config_valid_accepted_ordered(self): + """Test config is accepted when tunnel acl is valid""" + config = """ +acls: + tunnel-acl: + - rule: + actions: + output: + - tunnel: {type: 'vlan', tunnel_id: 200, dp: sw2, port: 1} +vlans: + vlan100: + vid: 100 +dps: + sw1: + dp_id: 0x1 + stack: + priority: 1 + interfaces: + 1: + native_vlan: vlan100 + acls_in: [tunnel-acl] + 2: + stack: + dp: sw2 + port: 2 + sw2: + dp_id: 0x2 + interfaces: + 1: + native_vlan: vlan100 + 2: + stack: + dp: sw1 + port: 2 +""" + self.check_config_success(config, cp.dp_parser) + def test_multiple_tunnel_acls_mirror_no_stack(self): """ Test config success with same tunnel ACL multiply applied to mirror @@ -1222,6 +1456,39 @@ def test_multiple_tunnel_acls_mirror_no_stack(self): """ self.check_config_success(config, cp.dp_parser) + def test_multiple_tunnel_acls_mirror_no_stack_ordered(self): + """ + Test config success with same tunnel ACL multiply applied to mirror + without stacking. + """ + config = """ +acls: + tunnel-acl: + - rule: + actions: + mirror: 3 + allow: 1 + output: + - tunnel: {type: 'vlan', tunnel_id: 200, dp: sw1, port: 3} +vlans: + vlan100: + vid: 100 +dps: + sw1: + dp_id: 0x1 + interfaces: + 1: + native_vlan: vlan100 + acls_in: [tunnel-acl] + 2: + native_vlan: vlan100 + acls_in: [tunnel-acl] + 3: + description: mirror + output_only: true +""" + self.check_config_success(config, cp.dp_parser) + def test_multiple_tunnel_acls(self): """Test config success with same tunnel ACL multiply applied.""" config = """ @@ -1265,15 +1532,15 @@ def test_multiple_tunnel_acls(self): """ self.check_config_success(config, cp.dp_parser) - def test_tunnel_id_by_vlan_name(self): - """Test config success by referencing tunnel id by a vlan name""" + def test_multiple_tunnel_acls_ordered(self): + """Test config success with same tunnel ACL multiply applied.""" config = """ acls: tunnel-acl: - rule: actions: output: - tunnel: {type: 'vlan', tunnel_id: tunnelvlan, dp: sw2, port: 2} + - tunnel: {type: 'vlan', tunnel_id: tunnelvlan, dp: sw2, port: 2} vlans: vlan100: vid: 100 @@ -1290,6 +1557,9 @@ def test_tunnel_id_by_vlan_name(self): native_vlan: vlan100 acls_in: [tunnel-acl] 2: + native_vlan: vlan100 + acls_in: [tunnel-acl] + 3: stack: dp: sw2 port: 1 @@ -1299,24 +1569,27 @@ def test_tunnel_id_by_vlan_name(self): 1: stack: dp: sw1 - port: 2 + port: 3 2: native_vlan: vlan100 """ self.check_config_success(config, cp.dp_parser) - def test_creating_tunnel_rule_conf(self): - """Test acl creates correct initial tunnel rule conf""" + def test_tunnel_id_by_vlan_name(self): + """Test config success by referencing tunnel id by a vlan name""" config = """ acls: tunnel-acl: - rule: actions: output: - tunnel: {type: 'vlan', tunnel_id: 200, dp: sw3, port: 2} + tunnel: {type: 'vlan', tunnel_id: tunnelvlan, dp: sw2, port: 2} vlans: vlan100: vid: 100 + tunnelvlan: + vid: 200 + reserved_internal_vlan: True dps: sw1: dp_id: 0x1 @@ -1329,67 +1602,68 @@ def test_creating_tunnel_rule_conf(self): 2: stack: dp: sw2 - port: 2 + port: 1 sw2: dp_id: 0x2 interfaces: 1: - native_vlan: vlan100 - 2: stack: dp: sw1 port: 2 - 3: + 2: + native_vlan: vlan100 +""" + self.check_config_success(config, cp.dp_parser) + + def test_tunnel_id_by_vlan_name_ordered(self): + """Test config success by referencing tunnel id by a vlan name""" + config = """ +acls: + tunnel-acl: + - rule: + actions: + output: + - tunnel: {type: 'vlan', tunnel_id: tunnelvlan, dp: sw2, port: 2} +vlans: + vlan100: + vid: 100 + tunnelvlan: + vid: 200 + reserved_internal_vlan: True +dps: + sw1: + dp_id: 0x1 + stack: + priority: 1 + interfaces: + 1: + native_vlan: vlan100 + acls_in: [tunnel-acl] + 2: stack: - dp: sw3 + dp: sw2 port: 1 - sw3: - dp_id: 0x3 + sw2: + dp_id: 0x2 interfaces: 1: stack: - dp: sw2 - port: 3 + dp: sw1 + port: 2 2: native_vlan: vlan100 """ - def enable_dp_ports(dp): - for port in dp.ports.values(): - port.dyn_finalized = False - port.enabled = True - port.dyn_phys_up = True - port.dyn_finalized = False - self.check_config_success(config, cp.dp_parser) - dps = self._get_dps_as_dict(config) - tunnel_id = 200 - for dp in dps.values(): - enable_dp_ports(dp) - self.assertIsNotNone(dp.tunnel_acls, 'Did not generate tunnel acls') - tunnel_acl = dp.tunnel_acls[tunnel_id] - self.assertIsNotNone(tunnel_acl.tunnel_info[tunnel_id]['src_dp'], ( - 'Did not resolve tunnel src_dp')) - tunnel_acl.update_tunnel_acl_conf(dp) - tunnel_rule = tunnel_acl.rules[0] - output_rule = tunnel_rule['actions']['output'] - self.assertIn('port', output_rule, ( - 'missing output port in initial tunnel')) - if dp is dps[0x1]: - self.assertIn('vlan_vid', output_rule, ( - 'missing output vlan in initial tunnel')) - elif dp is dps[0x3]: - self.assertIn('pop_vlans', output_rule, ( - 'missing pop vlan output in initial tunnel')) - - def test_updating_tunnel_acl_rule(self): - """Test updating output port (stack info) in tunnel rule conf""" + + def test_tunnel_no_vlan_specification(self): + """Test success when missing tunnel type and id values (Faucet will generate them)""" config = """ acls: tunnel-acl: - rule: actions: output: - tunnel: {type: 'vlan', tunnel_id: 200, dp: sw3, port: 2} + tunnel: {dp: sw2, port: 1} vlans: vlan100: vid: 100 @@ -1406,10 +1680,6 @@ def test_updating_tunnel_acl_rule(self): stack: dp: sw2 port: 2 - 3: - stack: - dp: sw2 - port: 4 sw2: dp_id: 0x2 interfaces: @@ -1419,59 +1689,447 @@ def test_updating_tunnel_acl_rule(self): stack: dp: sw1 port: 2 - 3: +""" + self.check_config_success(config, cp.dp_parser) + + def test_tunnel_no_vlan_specification_ordered(self): + """Test success when missing tunnel type and id values (Faucet will generate them)""" + config = """ +acls: + tunnel-acl: + - rule: + actions: + output: + - tunnel: {dp: sw2, port: 1} +vlans: + vlan100: + vid: 100 +dps: + sw1: + dp_id: 0x1 + stack: + priority: 1 + interfaces: + 1: + native_vlan: vlan100 + acls_in: [tunnel-acl] + 2: stack: - dp: sw3 - port: 1 - 4: + dp: sw2 + port: 2 + sw2: + dp_id: 0x2 + interfaces: + 1: + native_vlan: vlan100 + 2: stack: dp: sw1 - port: 3 - 5: - stack: - dp: sw3 - port: 3 - sw3: - dp_id: 0x3 + port: 2 +""" + self.check_config_success(config, cp.dp_parser) + + def test_dynamic_vlan_tunnel(self): + """Test tunnel ACL correctly generates the tunnel ID""" + config = """ +acls: + forward_tunnel: + - rule: + actions: + output: + tunnel: {dp: s2, port: 1} + reverse_tunnel: + - rule: + actions: + output: + tunnel: {dp: s1, port: 1} +vlans: + vlan100: + vid: 100 +dps: + s1: + dp_id: 0x1 + stack: + priority: 1 interfaces: 1: - stack: - dp: sw2 - port: 3 + native_vlan: vlan100 + acls_in: [forward_tunnel] + 2: + stack: {dp: s2, port: 2} + s2: + dp_id: 0x2 + interfaces: + 1: + native_vlan: vlan100 + acls_in: [reverse_tunnel] + 2: + stack: {dp: s1, port: 2} +""" + self.check_config_success(config, cp.dp_parser) + sw1, sw2 = self._get_dps_as_dict(config).values() + self.assertTrue(sw1.tunnel_acls, 'Did not generate tunnel ACL') + self.assertEqual( + len(sw1.tunnel_acls), 2, + 'Did not generate the correct number of tunnel ACLs') + self.assertTrue(sw2.tunnel_acls, 'Did not generate tunnel ACL') + self.assertEqual( + len(sw2.tunnel_acls), 2, + 'Did not generate the correct number of tunnel ACLs') + sw1_ids = {} + sw2_ids = {} + for acl in sw1.tunnel_acls: + sw1_ids[acl._id] = list(acl.tunnel_info.keys())[0] + for acl in sw2.tunnel_acls: + sw2_ids[acl._id] = list(acl.tunnel_info.keys())[0] + self.assertEqual( + sw1_ids, sw2_ids, + 'Did not generate the same ID for same tunnels on different DPs') + + def test_dynamic_vlan_tunnel_ordered(self): + """Test tunnel ACL correctly generates the tunnel ID""" + config = """ +acls: + forward_tunnel: + - rule: + actions: + output: + - tunnel: {dp: s2, port: 1} + reverse_tunnel: + - rule: + actions: + output: + - tunnel: {dp: s1, port: 1} +vlans: + vlan100: + vid: 100 +dps: + s1: + dp_id: 0x1 + stack: + priority: 1 + interfaces: + 1: + native_vlan: vlan100 + acls_in: [forward_tunnel] + 2: + stack: {dp: s2, port: 2} + s2: + dp_id: 0x2 + interfaces: + 1: + native_vlan: vlan100 + acls_in: [reverse_tunnel] + 2: + stack: {dp: s1, port: 2} +""" + self.check_config_success(config, cp.dp_parser) + sw1, sw2 = self._get_dps_as_dict(config).values() + self.assertTrue(sw1.tunnel_acls, 'Did not generate tunnel ACL') + self.assertEqual( + len(sw1.tunnel_acls), 2, + 'Did not generate the correct number of tunnel ACLs') + self.assertTrue(sw2.tunnel_acls, 'Did not generate tunnel ACL') + self.assertEqual( + len(sw2.tunnel_acls), 2, + 'Did not generate the correct number of tunnel ACLs') + sw1_ids = {} + sw2_ids = {} + for acl in sw1.tunnel_acls: + sw1_ids[acl._id] = list(acl.tunnel_info.keys())[0] + for acl in sw2.tunnel_acls: + sw2_ids[acl._id] = list(acl.tunnel_info.keys())[0] + self.assertEqual( + sw1_ids, sw2_ids, + 'Did not generate the same ID for same tunnels on different DPs') + + def test_dynamic_specified_vlan_tunnel(self): + """Test tunnel ACL can generate without clashing with a specified tunnel ACL""" + config = """ +acls: + forward_tunnel: + - rule: + actions: + output: + tunnel: {dp: s2, port: 1} + reverse_tunnel: + - rule: + actions: + output: + tunnel: {vlan: 101, dp: s1, port: 1} +vlans: + vlan100: + vid: 100 +dps: + s1: + dp_id: 0x1 + stack: + priority: 1 + interfaces: + 1: + native_vlan: vlan100 + acls_in: [forward_tunnel] 2: + stack: {dp: s2, port: 2} + s2: + dp_id: 0x2 + interfaces: + 1: native_vlan: vlan100 + acls_in: [forward_tunnel] + 2: + stack: {dp: s1, port: 2} 3: - stack: - dp: sw2 - port: 5 -""" - def enable_dp_ports(dp): - for port in dp.ports.values(): - port.dyn_finalized = False - port.enabled = True - port.dyn_phys_up = True - port.dyn_finalized = True - - def disable_port(dp, port_number): - port = dp.ports[port_number] - port.dyn_finalized = False - port.enabled = False - port.dyn_phys_up = False - port.dyn_finalized = True + native_vlan: vlan100 + acls_in: [reverse_tunnel] +""" + self.check_config_success(config, cp.dp_parser) + sw1, sw2 = self._get_dps_as_dict(config).values() + self.assertTrue(sw1.tunnel_acls, 'Did not generate tunnel ACL') + self.assertEqual( + len(sw1.tunnel_acls), 2, + 'Did not generate the correct number of tunnel ACLs') + self.assertTrue(sw2.tunnel_acls, 'Did not generate tunnel ACL') + self.assertEqual( + len(sw2.tunnel_acls), 2, + 'Did not generate the correct number of tunnel ACLs') + sw1_ids = {} + sw2_ids = {} + for acl in sw1.tunnel_acls: + sw1_ids[acl._id] = list(acl.tunnel_info.keys())[0] + for acl in sw2.tunnel_acls: + sw2_ids[acl._id] = list(acl.tunnel_info.keys())[0] + self.assertEqual( + sw1_ids, sw2_ids, + 'Did not generate the same ID for same tunnels on different DPs') - dps = self._get_dps_as_dict(config) - dps.pop(0x3) - for dp in dps.values(): - enable_dp_ports(dp) - tunnel_id = 200 - tunnel_acl = dp.tunnel_acls[tunnel_id] - tunnel_acl.update_tunnel_acl_conf(dp) - initial_output_port = tunnel_acl.rules[0]['actions']['output']['port'] - disable_port(dp, initial_output_port) - tunnel_acl.update_tunnel_acl_conf(dp) - final_output_port = tunnel_acl.rules[0]['actions']['output']['port'] - self.assertNotEqual(initial_output_port, final_output_port, ( - 'Tunnel output port did not update')) + def test_dynamic_specified_vlan_tunnel_ordered(self): + """Test tunnel ACL can generate without clashing with a specified tunnel ACL""" + config = """ +acls: + forward_tunnel: + - rule: + actions: + output: + - tunnel: {dp: s2, port: 1} + reverse_tunnel: + - rule: + actions: + output: + - tunnel: {vlan: 101, dp: s1, port: 1} +vlans: + vlan100: + vid: 100 +dps: + s1: + dp_id: 0x1 + stack: + priority: 1 + interfaces: + 1: + native_vlan: vlan100 + acls_in: [forward_tunnel] + 2: + stack: {dp: s2, port: 2} + s2: + dp_id: 0x2 + interfaces: + 1: + native_vlan: vlan100 + acls_in: [forward_tunnel] + 2: + stack: {dp: s1, port: 2} + 3: + native_vlan: vlan100 + acls_in: [reverse_tunnel] +""" + self.check_config_success(config, cp.dp_parser) + sw1, sw2 = self._get_dps_as_dict(config).values() + self.assertTrue(sw1.tunnel_acls, 'Did not generate tunnel ACL') + self.assertEqual( + len(sw1.tunnel_acls), 2, + 'Did not generate the correct number of tunnel ACLs') + self.assertTrue(sw2.tunnel_acls, 'Did not generate tunnel ACL') + self.assertEqual( + len(sw2.tunnel_acls), 2, + 'Did not generate the correct number of tunnel ACLs') + sw1_ids = {} + sw2_ids = {} + for acl in sw1.tunnel_acls: + sw1_ids[acl._id] = list(acl.tunnel_info.keys())[0] + for acl in sw2.tunnel_acls: + sw2_ids[acl._id] = list(acl.tunnel_info.keys())[0] + self.assertEqual( + sw1_ids, sw2_ids, + 'Did not generate the same ID for same tunnels on different DPs') + + def test_tunnel_two_ports(self): + """Test tunnel ACL does not try to generate different VIDs for the same tunnel""" + config = """ +acls: + forward_tunnel: + - rule: + actions: + output: + tunnel: {dp: s2, port: 1} +vlans: + vlan100: + vid: 100 +dps: + s1: + dp_id: 0x1 + stack: + priority: 1 + interfaces: + 1: + native_vlan: vlan100 + acls_in: [forward_tunnel] + 2: + stack: {dp: s2, port: 2} + 3: + native_vlan: vlan100 + acls_in: [forward_tunnel] + s2: + dp_id: 0x2 + interfaces: + 1: + native_vlan: vlan100 + acls_in: [forward_tunnel] + 2: + stack: {dp: s1, port: 2} +""" + self.check_config_success(config, cp.dp_parser) + sw1, sw2 = self._get_dps_as_dict(config).values() + self.assertEqual(sw1.vlans.keys(), sw2.vlans.keys(), 'Did not generate the same VLANs') + + def test_tunnel_two_ports_ordered(self): + """Test tunnel ACL does not try to generate different VIDs for the same tunnel""" + config = """ +acls: + forward_tunnel: + - rule: + actions: + output: + - tunnel: {dp: s2, port: 1} +vlans: + vlan100: + vid: 100 +dps: + s1: + dp_id: 0x1 + stack: + priority: 1 + interfaces: + 1: + native_vlan: vlan100 + acls_in: [forward_tunnel] + 2: + stack: {dp: s2, port: 2} + 3: + native_vlan: vlan100 + acls_in: [forward_tunnel] + s2: + dp_id: 0x2 + interfaces: + 1: + native_vlan: vlan100 + acls_in: [forward_tunnel] + 2: + stack: {dp: s1, port: 2} +""" + self.check_config_success(config, cp.dp_parser) + sw1, sw2 = self._get_dps_as_dict(config).values() + self.assertEqual(sw1.vlans.keys(), sw2.vlans.keys(), 'Did not generate the same VLANs') + + def test_two_tunnel_acl(self): + """Test tunnel ACL correctly allocates VLANs for an ACL with two tunnel rules""" + config = """ +acls: + tunnel_acl: + - rule: + dl_vlan: 100 + actions: + output: + tunnel: {dp: s2, port: 1} + - rule: + dl_vlan: 200 + actions: + output: + tunnel: {dp: s2, port: 2} +vlans: + vlan100: + vid: 100 + vlan200: + vid: 200 +dps: + s1: + dp_id: 0x1 + stack: + priority: 1 + interfaces: + 1: + tagged_vlans: [vlan100, vlan200] + acls_in: [tunnel_acl] + 2: + stack: {dp: s2, port: 3} + s2: + dp_id: 0x2 + interfaces: + 1: + native_vlan: vlan100 + 2: + native_vlan: vlan200 + 3: + stack: {dp: s1, port: 2} +""" + self.check_config_success(config, cp.dp_parser) + sw1, sw2 = self._get_dps_as_dict(config).values() + self.assertEqual(sw1.vlans.keys(), sw2.vlans.keys(), 'Did not generate the same VLANs') + + def test_two_tunnel_acl_ordered(self): + """Test tunnel ACL correctly allocates VLANs for an ACL with two tunnel rules""" + config = """ +acls: + tunnel_acl: + - rule: + dl_vlan: 100 + actions: + output: + - tunnel: {dp: s2, port: 1} + - rule: + dl_vlan: 200 + actions: + output: + - tunnel: {dp: s2, port: 2} +vlans: + vlan100: + vid: 100 + vlan200: + vid: 200 +dps: + s1: + dp_id: 0x1 + stack: + priority: 1 + interfaces: + 1: + tagged_vlans: [vlan100, vlan200] + acls_in: [tunnel_acl] + 2: + stack: {dp: s2, port: 3} + s2: + dp_id: 0x2 + interfaces: + 1: + native_vlan: vlan100 + 2: + native_vlan: vlan200 + 3: + stack: {dp: s1, port: 2} +""" + self.check_config_success(config, cp.dp_parser) + sw1, sw2 = self._get_dps_as_dict(config).values() + self.assertEqual(sw1.vlans.keys(), sw2.vlans.keys(), 'Did not generate the same VLANs') def test_lacp_port_options(self): """Test LACP port selection options pass config checking""" @@ -1504,10 +2162,110 @@ def test_lacp_port_options(self): self.assertTrue(selected.lacp_selected) self.assertTrue(unselected.lacp_unselected) + def test_ordered_acl_output_actions(self): + """Test that ordered ACL output is accepted""" + config = """ +acls: + ordered: + - rule: + actions: + output: + - port: 2 + old: + - rule: + actions: + output: + port: 1 +vlans: + vlan100: + vid: 100 +dps: + sw1: + dp_id: 0x1 + interfaces: + 1: + acls_in: [ordered] + native_vlan: vlan100 + 2: + acls_in: [old] + native_vlan: vlan100 +""" + self.check_config_success(config, cp.dp_parser) + ########################################### # Tests of Configuration Failure Handling # ########################################### + def test_ordered_acl_multiple_vlan_output(self): + """Test that ordered ACL output multiple VLAN keys specified""" + config = """ +acls: + ordered: + - rule: + actions: + output: + vlan_vid: 2 + vlan_vids: [1, 2] + port: 1 +vlans: + vlan100: + vid: 100 +dps: + sw1: + dp_id: 0x1 + interfaces: + 1: + acls_in: [ordered] + native_vlan: vlan100 +""" + self.check_config_failure(config, cp.dp_parser) + + def test_ordered_acl_multiple_port_output(self): + """Test that ordered ACL output multiple port keys specified""" + config = """ +acls: + ordered: + - rule: + actions: + output: + port: 2 + ports: [1, 2] +vlans: + vlan100: + vid: 100 +dps: + sw1: + dp_id: 0x1 + interfaces: + 1: + acls_in: [ordered] + native_vlan: vlan100 +""" + self.check_config_failure(config, cp.dp_parser) + + def test_ordered_acl_multiple_output(self): + """Test that ordered ACL output with multiple actions in an element is rejected""" + config = """ +acls: + ordered: + - rule: + actions: + output: + - port: 2 + pop_vlans: 1 +vlans: + vlan100: + vid: 100 +dps: + sw1: + dp_id: 0x1 + interfaces: + 1: + acls_in: [ordered] + native_vlan: vlan100 +""" + self.check_config_failure(config, cp.dp_parser) + def test_lacp_port_options_exclusivity(self): """Ensure config fails if more than one LACP port option has been specified""" config = """ @@ -2151,19 +2909,66 @@ def test_unresolved_actions_output_ports(self): interfaces: 1: native_vlan: office - acl_in: output_unresolved + acl_in: output_unresolved +acls: + output_unresolved: + - rule: + actions: + output: + set_fields: + - eth_dst: '01:00:00:00:00:00' + port: UNRESOLVED +""" + self.check_config_failure(config, cp.dp_parser) + + def test_unresolved_actions_output_ports_ordered(self): + """Test invalid output port name with actions""" + config = """ +vlans: + office: + vid: 100 +dps: + sw1: + dp_id: 0x1 + interfaces: + 1: + native_vlan: office + acl_in: output_unresolved +acls: + output_unresolved: + - rule: + actions: + output: + - set_fields: + - eth_dst: '01:00:00:00:00:00' + - port: UNRESOLVED +""" + self.check_config_failure(config, cp.dp_parser) + + def test_unknown_output_ports(self): + """Test invalid mirror ACL port.""" + config = """ +vlans: + office: + vid: 100 +dps: + sw1: + dp_id: 0x1 + interfaces: + 1: + native_vlan: office + acl_in: mirror_all acls: - output_unresolved: + mirror_all: - rule: actions: output: - set_fields: - - eth_dst: '01:00:00:00:00:00' - port: UNRESOLVED + port: 2 + allow: 1 """ self.check_config_failure(config, cp.dp_parser) - def test_unknown_output_ports(self): + def test_unknown_output_ports_ordered(self): """Test invalid mirror ACL port.""" config = """ vlans: @@ -2181,7 +2986,7 @@ def test_unknown_output_ports(self): - rule: actions: output: - port: 2 + - port: 2 allow: 1 """ self.check_config_failure(config, cp.dp_parser) @@ -2630,6 +3435,28 @@ def test_empty_eth_dst(self): """ self.check_config_failure(config, cp.dp_parser) + def test_empty_eth_dst_ordered(self): + """Test eth_dst/dl_dst is empty""" + config = """ +vlans: + 100: +acls: + 101: + - rule: + dl_dst: + actions: + output: + - port: 1 +dps: + switch1: + dp_id: 0xcafef00d + interfaces: + 1: + native_vlan: 100 + acl_in: 101 +""" + self.check_config_failure(config, cp.dp_parser) + def test_router_vlan_invalid_type(self): """Test when router vlans forms a dict""" config = """ @@ -2965,6 +3792,31 @@ def test_bad_match_fields(self): """ self.check_config_failure(config, cp.dp_parser) + def test_bad_match_fields_ordered(self): + """Test bad match fields.""" + config = """ +acls: + bad_acl: + rules: + - rule: + notsuch: "match" + actions: + output: + - set_fields: + - eth_dst: "0e:00:00:00:00:01" +vlans: + guest: + vid: 100 +dps: + sw1: + dp_id: 0x1 + interfaces: + 1: + native_vlan: 100 + acl_in: bad_acl +""" + self.check_config_failure(config, cp.dp_parser) + def test_bad_cookie(self): """Test bad cookie value.""" config = """ @@ -2989,6 +3841,30 @@ def test_bad_cookie(self): """ self.check_config_failure(config, cp.dp_parser) + def test_bad_cookie_ordered(self): + """Test bad cookie value.""" + config = """ +acls: + bad_cookie_acl: + rules: + - rule: + cookie: 999999 + actions: + output: + - port: 1 +vlans: + guest: + vid: 100 +dps: + sw1: + dp_id: 0x1 + interfaces: + 1: + native_vlan: 100 + acl_in: bad_cookie_acl +""" + self.check_config_failure(config, cp.dp_parser) + def test_routers_overlapping_vips(self): """Test with unreferenced router config.""" config = """ @@ -3387,7 +4263,6 @@ def test_conf_type_invalid(self): """ self.check_config_failure(config, cp.dp_parser) -#TODO: Need to have checks for invalid types i.e. tunnel_id is an int etc... def test_tunnel_bad_dst_dp(self): """Test config fails when tunnel destination DP is not valid""" config = """ @@ -3425,6 +4300,43 @@ def test_tunnel_bad_dst_dp(self): """ self.check_config_failure(config, cp.dp_parser) + def test_tunnel_bad_dst_dp_ordered(self): + """Test config fails when tunnel destination DP is not valid""" + config = """ +acls: + tunnel-acl: + - rule: + actions: + output: + - tunnel: {type: 'vlan', tunnel_id: 200, dp: sw3, port: 2} +vlans: + vlan100: + vid: 100 +dps: + sw1: + dp_id: 0x1 + stack: + priority: 1 + interfaces: + 1: + native_vlan: vlan100 + acls_in: [tunnel-acl] + 2: + stack: + dp: sw2 + port: 1 + sw2: + dp_id: 0x2 + interfaces: + 1: + stack: + dp: sw1 + port: 2 + 2: + native_vlan: vlan100 +""" + self.check_config_failure(config, cp.dp_parser) + def test_tunnel_bad_dst_port(self): """Test config failes when tunnel destination port is not valid""" config = """ @@ -3462,6 +4374,43 @@ def test_tunnel_bad_dst_port(self): """ self.check_config_failure(config, cp.dp_parser) + def test_tunnel_bad_dst_port_ordered(self): + """Test config failes when tunnel destination port is not valid""" + config = """ +acls: + tunnel-acl: + - rule: + actions: + output: + - tunnel: {type: 'vlan', tunnel_id: 200, dp: sw2, port: 3} +vlans: + vlan100: + vid: 100 +dps: + sw1: + dp_id: 0x1 + stack: + priority: 1 + interfaces: + 1: + native_vlan: vlan100 + acls_in: [tunnel-acl] + 2: + stack: + dp: sw2 + port: 1 + sw2: + dp_id: 0x2 + interfaces: + 1: + stack: + dp: sw1 + port: 2 + 2: + native_vlan: vlan100 +""" + self.check_config_failure(config, cp.dp_parser) + def test_different_tunnels_same_id(self): """Test config fails when two different tunnels use the same id""" config = """ @@ -3505,6 +4454,49 @@ def test_different_tunnels_same_id(self): """ self.check_config_failure(config, cp.dp_parser) + def test_different_tunnels_same_id_ordered(self): + """Test config fails when two different tunnels use the same id""" + config = """ +acls: + tunnel-acl: + - rule: + actions: + output: + - tunnel: {type: 'vlan', tunnel_id: 200, dp: sw2, port: 3} + reverse-tunnel: + - rule: + actions: + output: + - tunnel: {type: 'vlan', tunnel_id: 200, dp: sw1, port: 3} +vlans: + vlan100: + vid: 100 +dps: + sw1: + dp_id: 0x1 + stack: + priority: 1 + interfaces: + 1: + native_vlan: vlan100 + acls_in: [tunnel-acl] + 2: + stack: + dp: sw2 + port: 1 + sw2: + dp_id: 0x2 + interfaces: + 1: + stack: + dp: sw1 + port: 2 + 2: + native_vlan: vlan100 + acls_in: [reverse-tunnel] +""" + self.check_config_failure(config, cp.dp_parser) + def test_tunnel_id_same_vlan(self): """Test config fails when tunnel id clashes with a vlan id""" config = """ @@ -3542,6 +4534,43 @@ def test_tunnel_id_same_vlan(self): """ self.check_config_failure(config, cp.dp_parser) + def test_tunnel_id_same_vlan_ordered(self): + """Test config fails when tunnel id clashes with a vlan id""" + config = """ +acls: + tunnel-acl: + - rule: + actions: + output: + - tunnel: {type: 'vlan', tunnel_id: 100, dp: sw2, port: 2} +vlans: + vlan100: + vid: 100 +dps: + sw1: + dp_id: 0x1 + stack: + priority: 1 + interfaces: + 1: + native_vlan: vlan100 + acls_in: [tunnel-acl] + 2: + stack: + dp: sw2 + port: 1 + sw2: + dp_id: 0x2 + interfaces: + 1: + stack: + dp: sw1 + port: 2 + 2: + native_vlan: vlan100 + """ + self.check_config_failure(config, cp.dp_parser) + def test_tunnel_id_by_nonexistant_vlan_name_failure(self): """Test config failure by referencing tunnel id by a vlan name that doesn't exist""" config = """ @@ -3579,6 +4608,43 @@ def test_tunnel_id_by_nonexistant_vlan_name_failure(self): """ self.check_config_failure(config, cp.dp_parser) + def test_tunnel_id_by_nonexistant_vlan_name_failure_ordered(self): + """Test config failure by referencing tunnel id by a vlan name that doesn't exist""" + config = """ +acls: + tunnel-acl: + - rule: + actions: + output: + - tunnel: {type: 'vlan', tunnel_id: tunnelvlan, dp: sw2, port: 2} +vlans: + vlan100: + vid: 100 +dps: + sw1: + dp_id: 0x1 + stack: + priority: 1 + interfaces: + 1: + native_vlan: vlan100 + acls_in: [tunnel-acl] + 2: + stack: + dp: sw2 + port: 1 + sw2: + dp_id: 0x2 + interfaces: + 1: + stack: + dp: sw1 + port: 2 + 2: + native_vlan: vlan100 +""" + self.check_config_failure(config, cp.dp_parser) + def test_stacking_with_intervlan_routing_accepted(self): """Test config accepted when using intervlan routing with stacking""" config = """ diff --git a/tests/unit/faucet/test_valve_stack.py b/tests/unit/faucet/test_valve_stack.py index 87b05f6362..b68c0a86e7 100755 --- a/tests/unit/faucet/test_valve_stack.py +++ b/tests/unit/faucet/test_valve_stack.py @@ -1341,20 +1341,47 @@ def test_flood_towards_root_from_s4(self): self.assertTrue(self.packet_outs_from_flows(ofmsgs)) -class ValveTestTunnel(ValveTestBases.ValveTestSmall): - """Test valve tunnel methods""" +class ValveTestTunnel2DP(ValveTestBases.ValveTestSmall): + """Test Tunnel ACL implementation""" + + SRC_ID = 5 + DST_ID = 2 + SAME_ID = 4 + NONE_ID = 3 - TUNNEL_ID = 200 CONFIG = """ acls: - tunnel_acl: + src_acl: + - rule: + dl_type: 0x0800 + ip_proto: 1 + actions: + output: + tunnel: {dp: s2, port: 1} + dst_acl: + - rule: + dl_type: 0x0800 + ip_proto: 1 + actions: + output: + tunnel: {dp: s1, port: 1} + same_acl: + - rule: + dl_type: 0x0800 + ip_proto: 1 + actions: + output: + tunnel: {dp: s1, port: 1} + none_acl: - rule: + dl_type: 0x0800 + ip_proto: 1 actions: output: - tunnel: {type: 'vlan', tunnel_id: %u, dp: s3, port: 1} + tunnel: {dp: s2, port: 1} vlans: vlan100: - vid: 100 + vid: 1 dps: s1: dp_id: 0x1 @@ -1363,120 +1390,691 @@ class ValveTestTunnel(ValveTestBases.ValveTestSmall): priority: 1 interfaces: 1: + name: src_tunnel_host native_vlan: vlan100 + acls_in: [src_acl] 2: - stack: - dp: s2 - port: 2 + name: same_tunnel_host + native_vlan: vlan100 + acls_in: [same_acl] 3: - stack: - dp: s2 - port: 3 + stack: {dp: s2, port: 3} 4: - stack: - dp: s3 - port: 2 - 5: - stack: - dp: s3 - port: 3 + stack: {dp: s2, port: 4} s2: dp_id: 0x2 + hardware: 'GenericTFM' interfaces: 1: + name: dst_tunnel_host native_vlan: vlan100 - acls_in: [tunnel_acl] + acls_in: [dst_acl] 2: - stack: - dp: s1 - port: 2 + name: transit_tunnel_host + native_vlan: vlan100 + acls_in: [none_acl] 3: - stack: - dp: s1 - port: 3 + stack: {dp: s1, port: 3} + 4: + stack: {dp: s1, port: 4} +""" + + def setUp(self): + """Create a stacking config file.""" + self.setup_valve(self.CONFIG) + self.activate_all_ports() + for valve in self.valves_manager.valves.values(): + for port in valve.dp.ports.values(): + if port.stack: + self.set_stack_port_up(port.number, valve) + + def validate_tunnel(self, in_port, in_vid, out_port, out_vid, expected, msg): + if in_vid: + in_vid = in_vid | ofp.OFPVID_PRESENT + bcast_match = { + 'in_port': in_port, + 'eth_dst': mac.BROADCAST_STR, + 'vlan_vid': in_vid, + 'eth_type': 0x0800, + 'ip_proto': 1 + } + if out_vid: + out_vid = out_vid | ofp.OFPVID_PRESENT + if expected: + self.assertTrue(self.table.is_output(bcast_match, port=out_port, vid=out_vid), msg=msg) + else: + self.assertFalse(self.table.is_output(bcast_match, port=out_port, vid=out_vid), msg=msg) + + def test_update_src_tunnel(self): + """Test tunnel rules when encapsulating and forwarding to the destination switch""" + valve = self.valves_manager.valves[0x1] + port = valve.dp.ports[3] + # Apply tunnel to ofmsgs on valve + self.apply_ofmsgs(valve.get_tunnel_flowmods()) + # Should encapsulate and output packet towards tunnel destination s3 + self.validate_tunnel( + 1, 0, 3, self.SRC_ID, True, + 'Did not encapsulate and forward') + # Set the chosen port down to force a recalculation on the tunnel path + self.set_port_down(port.number) + ofmsgs = valve.get_tunnel_flowmods() + self.assertTrue(ofmsgs, 'No tunnel ofmsgs returned after a topology change') + self.apply_ofmsgs(ofmsgs) + # Should encapsulate and output packet using the new path + self.validate_tunnel( + 1, 0, 4, self.SRC_ID, True, + 'Did not encapsulate and forward out re-calculated port') + + def test_update_same_tunnel(self): + """Test tunnel rules when outputting to host on the same switch as the source""" + valve = self.valves_manager.valves[0x1] + self.apply_ofmsgs(valve.get_tunnel_flowmods()) + self.validate_tunnel(2, 0, 1, 0, True, 'Did not forward to host on same DP') + + def test_update_dst_tunnel(self): + """Test a tunnel outputting to the correct tunnel destination""" + valve = self.valves_manager.valves[0x1] + port = valve.dp.ports[3] + # Apply tunnel to ofmsgs on valve + self.apply_ofmsgs(valve.get_tunnel_flowmods()) + # Should accept encapsulated packet and output to the destination host + self.validate_tunnel(3, self.DST_ID, 1, 0, True, 'Did not output to host') + # Set the chosen port down to force a recalculation on the tunnel path + self.set_port_down(port.number) + ofmsgs = valve.get_tunnel_flowmods() + self.assertTrue(ofmsgs, 'No tunnel ofmsgs returned after a topology change') + self.apply_ofmsgs(ofmsgs) + # Should ccept encapsulated packet and output using the new path + self.validate_tunnel(4, self.DST_ID, 1, 0, True, 'Did not output to host') + + def test_update_none_tunnel(self): + """Test tunnel on a switch not using a tunnel ACL""" + valve = self.valves_manager.valves[0x1] + self.apply_ofmsgs(valve.get_tunnel_flowmods()) + # Should drop any packets received from the tunnel + self.validate_tunnel( + 5, self.NONE_ID, None, None, False, + 'Should not output a packet') + self.validate_tunnel( + 6, self.NONE_ID, None, None, False, + 'Should not output a packet') + + +class ValveTestTransitTunnel(ValveTestBases.ValveTestSmall): + """Test tunnel ACL implementation""" + + TRANSIT_ID = 2 + + CONFIG = """ +acls: + transit_acl: + - rule: + dl_type: 0x0800 + ip_proto: 1 + actions: + output: + tunnel: {dp: s3, port: 1} +vlans: + vlan100: + vid: 1 +dps: + s1: + dp_id: 0x1 + hardware: 'GenericTFM' + stack: + priority: 1 + interfaces: + 3: + stack: {dp: s2, port: 3} + 4: + stack: {dp: s2, port: 4} + 5: + stack: {dp: s3, port: 5} + 6: + stack: {dp: s3, port: 6} + s2: + dp_id: 0x2 + hardware: 'GenericTFM' + interfaces: + 1: + name: source_host + native_vlan: vlan100 + acls_in: [transit_acl] + 3: + stack: {dp: s1, port: 3} + 4: + stack: {dp: s1, port: 4} s3: dp_id: 0x3 + hardware: 'GenericTFM' interfaces: 1: + name: destination_host native_vlan: vlan100 + 5: + stack: {dp: s1, port: 5} + 6: + stack: {dp: s1, port: 6} +""" + + def setUp(self): + """Create a stacking config file.""" + self.setup_valve(self.CONFIG) + self.activate_all_ports() + for valve in self.valves_manager.valves.values(): + for port in valve.dp.ports.values(): + if port.stack: + self.set_stack_port_up(port.number, valve) + + def validate_tunnel(self, in_port, in_vid, out_port, out_vid, expected, msg): + if in_vid: + in_vid = in_vid | ofp.OFPVID_PRESENT + bcast_match = { + 'in_port': in_port, + 'eth_dst': mac.BROADCAST_STR, + 'vlan_vid': in_vid, + 'eth_type': 0x0800, + } + if out_vid: + out_vid = out_vid | ofp.OFPVID_PRESENT + if expected: + self.assertTrue(self.table.is_output(bcast_match, port=out_port, vid=out_vid), msg=msg) + else: + self.assertFalse(self.table.is_output(bcast_match, port=out_port, vid=out_vid), msg=msg) + + def test_update_transit_tunnel(self): + """Test a tunnel through a transit switch (forwards to the correct switch)""" + valve = self.valves_manager.valves[0x1] + port1 = valve.dp.ports[3] + port2 = valve.dp.ports[5] + # Apply tunnel to ofmsgs on valve + self.apply_ofmsgs(valve.get_tunnel_flowmods()) + # Should accept packet from stack and output to the next switch + self.validate_tunnel( + 3, self.TRANSIT_ID, 5, self.TRANSIT_ID, True, + 'Did not output to next switch') + # Set the chosen port down to force a recalculation on the tunnel path + self.set_port_down(port1.number) + # Should accept encapsulated packet and output using the new path + self.validate_tunnel( + 4, self.TRANSIT_ID, 5, self.TRANSIT_ID, True, + 'Did not output to next switch') + # Set the chosen port to the next switch down to force a path recalculation + self.set_port_down(port2.number) + ofmsgs = valve.get_tunnel_flowmods() + self.assertTrue(ofmsgs, 'No tunnel ofmsgs returned after a topology change') + self.apply_ofmsgs(ofmsgs) + # Should accept encapsulated packet and output using the new path + self.validate_tunnel( + 4, self.TRANSIT_ID, 6, self.TRANSIT_ID, True, + 'Did not output to next switch') + + +class ValveTestMultipleTunnel(ValveTestBases.ValveTestSmall): + """Test tunnel ACL implementation with multiple hosts containing tunnel ACL""" + + TUNNEL_ID = 2 + + CONFIG = """ +acls: + tunnel_acl: + - rule: + dl_type: 0x0800 + ip_proto: 1 + actions: + output: + tunnel: {dp: s2, port: 1} +vlans: + vlan100: + vid: 1 +dps: + s1: + dp_id: 0x1 + hardware: 'GenericTFM' + stack: + priority: 1 + interfaces: + 1: + native_vlan: vlan100 + acls_in: [tunnel_acl] 2: - stack: - dp: s1 - port: 4 + native_vlan: vlan100 + acls_in: [tunnel_acl] 3: - stack: - dp: s1 - port: 5 -""" % TUNNEL_ID + stack: {dp: s2, port: 3} + 4: + stack: {dp: s2, port: 4} + s2: + dp_id: 0x2 + hardware: 'GenericTFM' + interfaces: + 1: + native_vlan: vlan100 + 3: + stack: {dp: s1, port: 3} + 4: + stack: {dp: s1, port: 4} +""" def setUp(self): + """Create a stacking config file.""" self.setup_valve(self.CONFIG) - - def all_stack_up(self): - """Force stack ports UP and enabled""" - for valve in self.valves_manager.valves.values(): - valve.dp.dyn_running = True - for port in valve.dp.stack_ports: - port.stack_up() - port.dyn_finalized = False - port.enabled = True - port.dyn_phys_up = True - port.dyn_finalized = True - self.assertFalse(port.non_stack_forwarding()) - - def down_stack_port(self, port): - """Force stack port DOWN""" - peer_port = port.stack['port'] - peer_port.stack_gone() - port.dyn_finalized = False - port.enabled = False - port.dyn_phys_up = False - port.dyn_finalized = True - self.assertFalse(port.non_stack_forwarding()) - - def update_all_flowrules(self): - """Update all valve tunnel flowrules""" + self.activate_all_ports() for valve in self.valves_manager.valves.values(): - valve.update_tunnel_flowrules() + for port in valve.dp.ports.values(): + if port.stack: + self.set_stack_port_up(port.number, valve) + + def validate_tunnel(self, in_port, in_vid, out_port, out_vid, expected, msg): + if in_vid: + in_vid = in_vid | ofp.OFPVID_PRESENT + bcast_match = { + 'in_port': in_port, + 'eth_dst': mac.BROADCAST_STR, + 'vlan_vid': in_vid, + 'eth_type': 0x0800, + 'ip_proto': 1 + } + if out_vid: + out_vid = out_vid | ofp.OFPVID_PRESENT + if expected: + self.assertTrue(self.table.is_output(bcast_match, port=out_port, vid=out_vid), msg=msg) + else: + self.assertFalse(self.table.is_output(bcast_match, port=out_port, vid=out_vid), msg=msg) + + def test_tunnel_update_multiple_tunnels(self): + """Test having multiple hosts with the same tunnel""" + valve = self.valves_manager.valves[0x1] + port = valve.dp.ports[3] + # Apply tunnel to ofmsgs on valve + self.apply_ofmsgs(valve.get_tunnel_flowmods()) + # Should encapsulate and output packet towards tunnel destination s3 + self.validate_tunnel( + 1, 0, 3, self.TUNNEL_ID, True, + 'Did not encapsulate and forward') + self.validate_tunnel( + 2, 0, 3, self.TUNNEL_ID, True, + 'Did not encapsulate and forward') + # Set the chosen port down to force a recalculation on the tunnel path + self.set_port_down(port.number) + ofmsgs = valve.get_tunnel_flowmods() + self.assertTrue(ofmsgs, 'No tunnel ofmsgs returned after a topology change') + self.apply_ofmsgs(ofmsgs) + # Should encapsulate and output packet using the new path + self.validate_tunnel( + 1, 0, 4, self.TUNNEL_ID, True, + 'Did not encapsulate and forward out re-calculated port') + self.validate_tunnel( + 1, 0, 4, self.TUNNEL_ID, True, + 'Did not encapsulate and forward out re-calculated port') + + +class ValveTestOrderedTunnel2DP(ValveTestBases.ValveTestSmall): + """Test Tunnel ACL implementation""" + + SRC_ID = 5 + DST_ID = 2 + SAME_ID = 4 + NONE_ID = 3 + + CONFIG = """ +acls: + src_acl: + - rule: + dl_type: 0x0800 + ip_proto: 1 + actions: + output: + - tunnel: {dp: s2, port: 1} + dst_acl: + - rule: + dl_type: 0x0800 + ip_proto: 1 + actions: + output: + - tunnel: {dp: s1, port: 1} + same_acl: + - rule: + dl_type: 0x0800 + ip_proto: 1 + actions: + output: + - tunnel: {dp: s1, port: 1} + none_acl: + - rule: + dl_type: 0x0800 + ip_proto: 1 + actions: + output: + - tunnel: {dp: s2, port: 1} +vlans: + vlan100: + vid: 1 +dps: + s1: + dp_id: 0x1 + hardware: 'GenericTFM' + stack: + priority: 1 + interfaces: + 1: + name: src_tunnel_host + native_vlan: vlan100 + acls_in: [src_acl] + 2: + name: same_tunnel_host + native_vlan: vlan100 + acls_in: [same_acl] + 3: + stack: {dp: s2, port: 3} + 4: + stack: {dp: s2, port: 4} + s2: + dp_id: 0x2 + hardware: 'GenericTFM' + interfaces: + 1: + name: dst_tunnel_host + native_vlan: vlan100 + acls_in: [dst_acl] + 2: + name: transit_tunnel_host + native_vlan: vlan100 + acls_in: [none_acl] + 3: + stack: {dp: s1, port: 3} + 4: + stack: {dp: s1, port: 4} +""" - def update_all_tunnels(self, state): - """Force DP tunnel updated flag state""" + def setUp(self): + """Create a stacking config file.""" + self.setup_valve(self.CONFIG) + self.activate_all_ports() for valve in self.valves_manager.valves.values(): - valve.dp.tunnel_updated_flags[self.TUNNEL_ID] = state + for port in valve.dp.ports.values(): + if port.stack: + self.set_stack_port_up(port.number, valve) - def get_valve(self, dp_id): - """Get valve with dp_id""" - return self.valves_manager.valves[dp_id] + def validate_tunnel(self, in_port, in_vid, out_port, out_vid, expected, msg): + if in_vid: + in_vid = in_vid | ofp.OFPVID_PRESENT + bcast_match = { + 'in_port': in_port, + 'eth_dst': mac.BROADCAST_STR, + 'vlan_vid': in_vid, + 'eth_type': 0x0800, + 'ip_proto': 1 + } + if out_vid: + out_vid = out_vid | ofp.OFPVID_PRESENT + if expected: + self.assertTrue(self.table.is_output(bcast_match, port=out_port, vid=out_vid), msg=msg) + else: + self.assertFalse(self.table.is_output(bcast_match, port=out_port, vid=out_vid), msg=msg) + + def test_update_src_tunnel(self): + """Test tunnel rules when encapsulating and forwarding to the destination switch""" + valve = self.valves_manager.valves[0x1] + port = valve.dp.ports[3] + # Apply tunnel to ofmsgs on valve + self.apply_ofmsgs(valve.get_tunnel_flowmods()) + # Should encapsulate and output packet towards tunnel destination s3 + self.validate_tunnel( + 1, 0, 3, self.SRC_ID, True, + 'Did not encapsulate and forward') + # Set the chosen port down to force a recalculation on the tunnel path + self.set_port_down(port.number) + ofmsgs = valve.get_tunnel_flowmods() + self.assertTrue(ofmsgs, 'No tunnel ofmsgs returned after a topology change') + self.apply_ofmsgs(ofmsgs) + # Should encapsulate and output packet using the new path + self.validate_tunnel( + 1, 0, 4, self.SRC_ID, True, + 'Did not encapsulate and forward out re-calculated port') + + def test_update_same_tunnel(self): + """Test tunnel rules when outputting to host on the same switch as the source""" + valve = self.valves_manager.valves[0x1] + self.apply_ofmsgs(valve.get_tunnel_flowmods()) + self.validate_tunnel(2, 0, 1, 0, True, 'Did not forward to host on same DP') - def test_update_on_stack_link_up(self): - """Test updating acl tunnel rules on stack link status UP""" - self.all_stack_up() - self.update_all_flowrules() + def test_update_dst_tunnel(self): + """Test a tunnel outputting to the correct tunnel destination""" + valve = self.valves_manager.valves[0x1] + port = valve.dp.ports[3] + # Apply tunnel to ofmsgs on valve + self.apply_ofmsgs(valve.get_tunnel_flowmods()) + # Should accept encapsulated packet and output to the destination host + self.validate_tunnel(3, self.DST_ID, 1, 0, True, 'Did not output to host') + # Set the chosen port down to force a recalculation on the tunnel path + self.set_port_down(port.number) + ofmsgs = valve.get_tunnel_flowmods() + self.assertTrue(ofmsgs, 'No tunnel ofmsgs returned after a topology change') + self.apply_ofmsgs(ofmsgs) + # Should ccept encapsulated packet and output using the new path + self.validate_tunnel(4, self.DST_ID, 1, 0, True, 'Did not output to host') + + def test_update_none_tunnel(self): + """Test tunnel on a switch not using a tunnel ACL""" + valve = self.valves_manager.valves[0x1] + self.apply_ofmsgs(valve.get_tunnel_flowmods()) + # Should drop any packets received from the tunnel + self.validate_tunnel( + 5, self.NONE_ID, None, None, False, + 'Should not output a packet') + self.validate_tunnel( + 6, self.NONE_ID, None, None, False, + 'Should not output a packet') + + +class ValveTestTransitOrderedTunnel(ValveTestBases.ValveTestSmall): + """Test tunnel ACL implementation""" + + TRANSIT_ID = 2 + + CONFIG = """ +acls: + transit_acl: + - rule: + dl_type: 0x0800 + ip_proto: 1 + actions: + output: + - tunnel: {dp: s3, port: 1} +vlans: + vlan100: + vid: 1 +dps: + s1: + dp_id: 0x1 + hardware: 'GenericTFM' + stack: + priority: 1 + interfaces: + 3: + stack: {dp: s2, port: 3} + 4: + stack: {dp: s2, port: 4} + 5: + stack: {dp: s3, port: 5} + 6: + stack: {dp: s3, port: 6} + s2: + dp_id: 0x2 + hardware: 'GenericTFM' + interfaces: + 1: + name: source_host + native_vlan: vlan100 + acls_in: [transit_acl] + 3: + stack: {dp: s1, port: 3} + 4: + stack: {dp: s1, port: 4} + s3: + dp_id: 0x3 + hardware: 'GenericTFM' + interfaces: + 1: + name: destination_host + native_vlan: vlan100 + 5: + stack: {dp: s1, port: 5} + 6: + stack: {dp: s1, port: 6} +""" + + def setUp(self): + """Create a stacking config file.""" + self.setup_valve(self.CONFIG) + self.activate_all_ports() for valve in self.valves_manager.valves.values(): - self.assertTrue(valve.dp.tunnel_updated_flags[self.TUNNEL_ID]) + for port in valve.dp.ports.values(): + if port.stack: + self.set_stack_port_up(port.number, valve) - def test_update_on_stack_link_down(self): - """Test updating acl tunnel rules on stack link status DOWN""" - self.all_stack_up() - self.update_all_flowrules() - self.update_all_tunnels(False) - self.down_stack_port(self.get_valve(0x1).dp.ports[2]) - self.down_stack_port(self.get_valve(0x1).dp.ports[4]) - self.down_stack_port(self.get_valve(0x2).dp.ports[2]) - self.down_stack_port(self.get_valve(0x3).dp.ports[2]) - self.update_all_flowrules() - self.assertTrue(self.get_valve(0x1).dp.tunnel_updated_flags[self.TUNNEL_ID]) - self.assertTrue(self.get_valve(0x2).dp.tunnel_updated_flags[self.TUNNEL_ID]) - - def test_tunnel_flowmod_count(self): - """Test the correct number of tunnel flowmods are created""" + def validate_tunnel(self, in_port, in_vid, out_port, out_vid, expected, msg): + if in_vid: + in_vid = in_vid | ofp.OFPVID_PRESENT + bcast_match = { + 'in_port': in_port, + 'eth_dst': mac.BROADCAST_STR, + 'vlan_vid': in_vid, + 'eth_type': 0x0800, + } + if out_vid: + out_vid = out_vid | ofp.OFPVID_PRESENT + if expected: + self.assertTrue(self.table.is_output(bcast_match, port=out_port, vid=out_vid), msg=msg) + else: + self.assertFalse(self.table.is_output(bcast_match, port=out_port, vid=out_vid), msg=msg) + + def test_update_transit_tunnel(self): + """Test a tunnel through a transit switch (forwards to the correct switch)""" + valve = self.valves_manager.valves[0x1] + port1 = valve.dp.ports[3] + port2 = valve.dp.ports[5] + # Apply tunnel to ofmsgs on valve + self.apply_ofmsgs(valve.get_tunnel_flowmods()) + # Should accept packet from stack and output to the next switch + self.validate_tunnel( + 3, self.TRANSIT_ID, 5, self.TRANSIT_ID, True, + 'Did not output to next switch') + # Set the chosen port down to force a recalculation on the tunnel path + self.set_port_down(port1.number) + # Should accept encapsulated packet and output using the new path + self.validate_tunnel( + 4, self.TRANSIT_ID, 5, self.TRANSIT_ID, True, + 'Did not output to next switch') + # Set the chosen port to the next switch down to force a path recalculation + self.set_port_down(port2.number) + ofmsgs = valve.get_tunnel_flowmods() + self.assertTrue(ofmsgs, 'No tunnel ofmsgs returned after a topology change') + self.apply_ofmsgs(ofmsgs) + # Should accept encapsulated packet and output using the new path + self.validate_tunnel( + 4, self.TRANSIT_ID, 6, self.TRANSIT_ID, True, + 'Did not output to next switch') + + +class ValveTestMultipleOrderedTunnel(ValveTestBases.ValveTestSmall): + """Test tunnel ACL implementation with multiple hosts containing tunnel ACL""" + + TUNNEL_ID = 2 + + CONFIG = """ +acls: + tunnel_acl: + - rule: + dl_type: 0x0800 + ip_proto: 1 + actions: + output: + - tunnel: {dp: s2, port: 1} +vlans: + vlan100: + vid: 1 +dps: + s1: + dp_id: 0x1 + hardware: 'GenericTFM' + stack: + priority: 1 + interfaces: + 1: + native_vlan: vlan100 + acls_in: [tunnel_acl] + 2: + native_vlan: vlan100 + acls_in: [tunnel_acl] + 3: + stack: {dp: s2, port: 3} + 4: + stack: {dp: s2, port: 4} + s2: + dp_id: 0x2 + hardware: 'GenericTFM' + interfaces: + 1: + native_vlan: vlan100 + 3: + stack: {dp: s1, port: 3} + 4: + stack: {dp: s1, port: 4} +""" + + def setUp(self): + """Create a stacking config file.""" + self.setup_valve(self.CONFIG) + self.activate_all_ports() for valve in self.valves_manager.valves.values(): - self.assertEqual(len(valve.get_tunnel_flowmods()), 0) - self.all_stack_up() - self.update_all_flowrules() - self.assertEqual(len(self.get_valve(0x1).get_tunnel_flowmods()), 2) - self.assertEqual(len(self.get_valve(0x2).get_tunnel_flowmods()), 1) - self.assertEqual(len(self.get_valve(0x3).get_tunnel_flowmods()), 2) + for port in valve.dp.ports.values(): + if port.stack: + self.set_stack_port_up(port.number, valve) + + def validate_tunnel(self, in_port, in_vid, out_port, out_vid, expected, msg): + if in_vid: + in_vid = in_vid | ofp.OFPVID_PRESENT + bcast_match = { + 'in_port': in_port, + 'eth_dst': mac.BROADCAST_STR, + 'vlan_vid': in_vid, + 'eth_type': 0x0800, + 'ip_proto': 1 + } + if out_vid: + out_vid = out_vid | ofp.OFPVID_PRESENT + if expected: + self.assertTrue(self.table.is_output(bcast_match, port=out_port, vid=out_vid), msg=msg) + else: + self.assertFalse(self.table.is_output(bcast_match, port=out_port, vid=out_vid), msg=msg) + + def test_tunnel_update_multiple_tunnels(self): + """Test having multiple hosts with the same tunnel""" + valve = self.valves_manager.valves[0x1] + port = valve.dp.ports[3] + # Apply tunnel to ofmsgs on valve + self.apply_ofmsgs(valve.get_tunnel_flowmods()) + # Should encapsulate and output packet towards tunnel destination s3 + self.validate_tunnel( + 1, 0, 3, self.TUNNEL_ID, True, + 'Did not encapsulate and forward') + self.validate_tunnel( + 2, 0, 3, self.TUNNEL_ID, True, + 'Did not encapsulate and forward') + # Set the chosen port down to force a recalculation on the tunnel path + self.set_port_down(port.number) + ofmsgs = valve.get_tunnel_flowmods() + self.assertTrue(ofmsgs, 'No tunnel ofmsgs returned after a topology change') + self.apply_ofmsgs(ofmsgs) + # Should encapsulate and output packet using the new path + self.validate_tunnel( + 1, 0, 4, self.TUNNEL_ID, True, + 'Did not encapsulate and forward out re-calculated port') + self.validate_tunnel( + 1, 0, 4, self.TUNNEL_ID, True, + 'Did not encapsulate and forward out re-calculated port') class ValveTwoDpRoot(ValveTestBases.ValveTestSmall): diff --git a/tests/unit/faucet/valve_test_lib.py b/tests/unit/faucet/valve_test_lib.py index b286c90bbc..d0c3c8dfa0 100644 --- a/tests/unit/faucet/valve_test_lib.py +++ b/tests/unit/faucet/valve_test_lib.py @@ -1589,6 +1589,71 @@ def test_dp_acl_deny(self): self.table.is_output(accept_match, port=3, vid=self.V200), msg='packet not allowed by ACL') + def test_dp_acl_deny_ordered(self): + """Test DP acl denies forwarding""" + acl_config = """ +dps: + s1: + dp_acls: [drop_non_ospf_ipv4] +%s + interfaces: + p2: + number: 2 + native_vlan: v200 + p3: + number: 3 + tagged_vlans: [v200] +vlans: + v200: + vid: 0x200 +acls: + drop_non_ospf_ipv4: + - rule: + nw_dst: '224.0.0.5' + dl_type: 0x800 + actions: + meter: testmeter + allow: 1 + - rule: + dl_type: 0x800 + actions: + output: + - set_fields: + - eth_dst: 00:00:00:00:00:01 + allow: 0 +meters: + testmeter: + meter_id: 99 + entry: + flags: "KBPS" + bands: + [ + { + type: "DROP", + rate: 1 + } + ] +""" % DP1_CONFIG + + drop_match = { + 'in_port': 2, + 'vlan_vid': 0, + 'eth_type': 0x800, + 'ipv4_dst': '192.0.2.1'} + accept_match = { + 'in_port': 2, + 'vlan_vid': 0, + 'eth_type': 0x800, + 'ipv4_dst': '224.0.0.5'} + self.update_config(acl_config) + self.flap_port(2) + self.assertFalse( + self.table.is_output(drop_match), + msg='packet not blocked by ACL') + self.assertTrue( + self.table.is_output(accept_match, port=3, vid=self.V200), + msg='packet not allowed by ACL') + def test_port_acl_deny(self): """Test that port ACL denies forwarding.""" acl_config = """