"""Manage flooding to ports on VLANs."""
# Copyright (C) 2013 Nippon Telegraph and Telephone Corporation.
# Copyright (C) 2015 Brad Cowie, Christopher Lorier and Joe Stringer.
# Copyright (C) 2015 Research and Education Advanced Network New Zealand Ltd.
# Copyright (C) 2015--2018 The Contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from faucet import valve_of
from faucet import valve_packet
from faucet.valve_manager_base import ValveManagerBase
[docs]class ValveFloodManager(ValveManagerBase):
"""Implement dataplane based flooding for standalone dataplanes."""
# Enumerate possible eth_dst flood destinations.
# First bool says whether to flood this destination, if the VLAN
# has unicast flooding enabled (if unicast flooding is enabled,
# then we flood all destination eth_dsts).
FLOOD_DSTS = (
(True, None, None),
(False, valve_packet.BRIDGE_GROUP_ADDRESS, valve_packet.mac_byte_mask(3)), # 802.x
(False, '01:00:5E:00:00:00', valve_packet.mac_byte_mask(3)), # IPv4 multicast
(False, '33:33:00:00:00:00', valve_packet.mac_byte_mask(2)), # IPv6 multicast
(False, valve_of.mac.BROADCAST_STR, None), # flood on ethernet broadcasts
)
def __init__(self, flood_table, eth_src_table, # pylint: disable=too-many-arguments
flood_priority, bypass_priority,
use_group_table, groups,
combinatorial_port_flood):
self.flood_table = flood_table
self.eth_src_table = eth_src_table
self.bypass_priority = bypass_priority
self.flood_priority = flood_priority
self.use_group_table = use_group_table
self.groups = groups
self.combinatorial_port_flood = combinatorial_port_flood
def _require_combinatorial_flood(self, vlan):
"""Must use in_port style flood rules if configured to or hairpinning/LAGs are in use."""
return self.combinatorial_port_flood or vlan.hairpin_ports() or vlan.lags()
@staticmethod
def _vlan_all_ports(vlan, exclude_unicast):
"""Return list of all ports that should be flooded to on a VLAN."""
return list(vlan.flood_ports(vlan.get_ports(), exclude_unicast))
@staticmethod
def _build_flood_local_rule_actions(vlan, exclude_unicast, in_port):
"""Return a list of flood actions to flood packets from a port."""
exclude_ports = vlan.exclude_same_lag_member_ports(in_port)
return valve_of.flood_port_outputs(
vlan.tagged_flood_ports(exclude_unicast),
vlan.untagged_flood_ports(exclude_unicast),
in_port=in_port,
exclude_ports=exclude_ports)
[docs] def initialise_tables(self):
"""initialise the flood table with filtering flows"""
ofmsgs = []
ofmsgs.append(self.flood_table.flowdrop(
self.flood_table.match(
eth_dst=valve_packet.CISCO_SPANNING_GROUP_ADDRESS),
priority=self.bypass_priority))
ofmsgs.append(self.flood_table.flowdrop(
self.flood_table.match(
eth_dst=valve_packet.BRIDGE_GROUP_ADDRESS,
eth_dst_mask=valve_packet.BRIDGE_GROUP_MASK),
priority=self.bypass_priority))
return ofmsgs
def _build_flood_rule_actions(self, vlan, exclude_unicast, in_port):
return self._build_flood_local_rule_actions(
vlan, exclude_unicast, in_port)
def _build_flood_rule_for_vlan(self, vlan, eth_dst, eth_dst_mask, # pylint: disable=too-many-arguments
exclude_unicast, command, flood_priority,
preflood_acts):
ofmsgs = []
match = self.flood_table.match(
vlan=vlan, eth_dst=eth_dst, eth_dst_mask=eth_dst_mask)
flood_acts = self._build_flood_rule_actions(
vlan, exclude_unicast, None)
ofmsgs.append(self.flood_table.flowmod(
match=match,
command=command,
inst=[valve_of.apply_actions(preflood_acts + flood_acts)],
priority=flood_priority))
return ofmsgs
def _build_flood_rule_for_port(self, vlan, eth_dst, eth_dst_mask, # pylint: disable=too-many-arguments
exclude_unicast, command, flood_priority,
port, preflood_acts):
ofmsgs = []
match = self.flood_table.match(
vlan=vlan, in_port=port.number,
eth_dst=eth_dst, eth_dst_mask=eth_dst_mask)
flood_acts = self._build_flood_rule_actions(
vlan, exclude_unicast, port)
ofmsgs.append(self.flood_table.flowmod(
match=match,
command=command,
inst=[valve_of.apply_actions(preflood_acts + flood_acts)],
priority=flood_priority))
return ofmsgs
def _combinatorial_port_flood(self, vlan, eth_dst, eth_dst_mask, # pylint: disable=too-many-arguments
exclude_unicast, command, flood_priority,
mirror_acts):
ofmsgs = []
# TODO: hairpin rules should use higher priority rules so we
# can use default non-combinatorial rules.
if self._require_combinatorial_flood(vlan):
for port in self._vlan_all_ports(vlan, exclude_unicast):
ofmsgs.extend(self._build_flood_rule_for_port(
vlan, eth_dst, eth_dst_mask,
exclude_unicast, command, flood_priority,
port, mirror_acts))
return ofmsgs
def _build_mask_flood_rules(self, vlan, eth_dst, eth_dst_mask, # pylint: disable=too-many-arguments
exclude_unicast, command, flood_priority,
mirror_acts):
ofmsgs = self._combinatorial_port_flood(
vlan, eth_dst, eth_dst_mask,
exclude_unicast, command, flood_priority, mirror_acts)
if not ofmsgs:
ofmsgs.extend(self._build_flood_rule_for_vlan(
vlan, eth_dst, eth_dst_mask,
exclude_unicast, command, flood_priority, mirror_acts))
return ofmsgs
def _build_multiout_flood_rules(self, vlan, command):
"""Build flooding rules for a VLAN without using groups."""
flood_priority = self.flood_priority
mirror_acts = []
for mirrored_port in vlan.mirrored_ports():
for act in mirrored_port.mirror_actions():
mirror_acts.append(act)
mirror_acts = valve_of.dedupe_output_port_acts(mirror_acts)
ofmsgs = []
for unicast_eth_dst, eth_dst, eth_dst_mask in self.FLOOD_DSTS:
if unicast_eth_dst and not vlan.unicast_flood:
continue
ofmsgs.extend(self._build_mask_flood_rules(
vlan, eth_dst, eth_dst_mask,
unicast_eth_dst, command, flood_priority, list(mirror_acts)))
flood_priority += 1
return ofmsgs
@staticmethod
def _build_group_buckets(vlan, unicast_flood):
buckets = []
tagged_flood_ports = vlan.tagged_flood_ports(unicast_flood)
buckets.extend(valve_of.group_flood_buckets(tagged_flood_ports, False))
untagged_flood_ports = vlan.untagged_flood_ports(unicast_flood)
buckets.extend(valve_of.group_flood_buckets(untagged_flood_ports, True))
return buckets
def _build_group_flood_rules(self, vlan, modify, command):
"""Build flooding rules for a VLAN using groups."""
flood_priority = self.flood_priority
broadcast_group = self.groups.get_entry(
vlan.vid,
self._build_group_buckets(vlan, False))
unicast_group = self.groups.get_entry(
vlan.vid + valve_of.VLAN_GROUP_OFFSET,
self._build_group_buckets(vlan, vlan.unicast_flood))
ofmsgs = []
if modify:
ofmsgs.append(broadcast_group.modify())
ofmsgs.append(unicast_group.modify())
else:
ofmsgs.extend(broadcast_group.add())
ofmsgs.extend(unicast_group.add())
for unicast_eth_dst, eth_dst, eth_dst_mask in self.FLOOD_DSTS:
if unicast_eth_dst and not vlan.unicast_flood:
continue
group = broadcast_group
if not eth_dst:
group = unicast_group
match = self.flood_table.match(
vlan=vlan, eth_dst=eth_dst, eth_dst_mask=eth_dst_mask)
ofmsgs.append(self.flood_table.flowmod(
match=match,
command=command,
inst=[valve_of.apply_actions([valve_of.group_act(group.group_id)])],
priority=flood_priority))
flood_priority += 1
return ofmsgs
[docs] def add_vlan(self, vlan):
return self.build_flood_rules(vlan)
[docs] def build_flood_rules(self, vlan, modify=False):
"""Add flows to flood packets to unknown destinations on a VLAN."""
# TODO: group table support is still fairly uncommon, so
# group tables are currently optional.
command = valve_of.ofp.OFPFC_ADD
if modify:
command = valve_of.ofp.OFPFC_MODIFY_STRICT
if self.use_group_table and not self._require_combinatorial_flood(vlan):
return self._build_group_flood_rules(vlan, modify, command)
return self._build_multiout_flood_rules(vlan, command)
[docs] def update_stack_topo(self, event, dp, port=None): # pylint: disable=unused-argument,invalid-name
"""Update the stack topology. It has nothing to do for non-stacking DPs."""
pass
[docs] @staticmethod
def edge_learn_port(_other_valves, pkt_meta):
"""Possibly learn a host on a port.
Args:
other_valves (list): All Valves other than this one.
pkt_meta (PacketMeta): PacketMeta instance for packet received.
Returns:
port to learn host on.
"""
return pkt_meta.port
[docs]class ValveFloodStackManager(ValveFloodManager):
"""Implement dataplane based flooding for stacked dataplanes."""
def __init__(self, flood_table, eth_src_table, # pylint: disable=too-many-arguments
flood_priority, bypass_priority,
use_group_table, groups,
combinatorial_port_flood,
stack, stack_ports,
dp_shortest_path_to_root, shortest_path_port):
super(ValveFloodStackManager, self).__init__(
flood_table, eth_src_table,
flood_priority, bypass_priority,
use_group_table, groups,
combinatorial_port_flood)
self.stack = stack
self.stack_ports = stack_ports
self.shortest_path_port = shortest_path_port
self.dp_shortest_path_to_root = dp_shortest_path_to_root
self._reset_peer_distances()
def _reset_peer_distances(self):
"""Reset distances to/from root for this DP."""
port_peer_distances = [
(port, len(port.stack['dp'].shortest_path_to_root())) for port in self.stack_ports]
my_root_distance = len(self.dp_shortest_path_to_root())
self.towards_root_stack_ports = [
port for port, port_peer_distance in port_peer_distances
if port_peer_distance < my_root_distance]
self.away_from_root_stack_ports = [
port for port, port_peer_distance in port_peer_distances
if port_peer_distance > my_root_distance]
def _build_flood_rule_actions(self, vlan, exclude_unicast, in_port):
"""Calculate flooding destinations based on this DP's position.
If a standalone switch, then flood to local VLAN ports.
If a distributed switch where all switches are directly
connected to the root (star topology), edge switches flood locally
and to the root, and the root floods to the other edges.
If a non-star distributed switch topologies, use selective
flooding (see the following example).
Hosts
||||
||||
+----+ +----+ +----+
---+1 | |1234| | 1+---
Hosts ---+2 | | | | 2+--- Hosts
---+3 | | | | 3+---
---+4 5+-------+5 6+-------+5 4+---
+----+ +----+ +----+
Root DP
Non-root switches flood only to the root. The root switch
reflects incoming floods back out. Non-root switches
flood packets from the root locally and further away.
Flooding is entirely implemented in the dataplane.
A host connected to a non-root switch can receive a copy
of its own flooded packet (because the non-root switch
does not know it has seen the packet already).
A host connected to the root switch does not have this problem
(because flooding is always away from the root). Therefore,
connections to other non-FAUCET stacking networks should only
be made to the root.
On the root switch (left), flood destinations are:
1: 2 3 4 5(s)
2: 1 3 4 5(s)
3: 1 2 4 5(s)
4: 1 2 3 5(s)
5: 1 2 3 4 5(s, note reflection)
On the middle switch:
1: 5(s)
2: 5(s)
3: 5(s)
4: 5(s)
5: 1 2 3 4 6(s)
6: 5(s)
On the rightmost switch:
1: 5(s)
2: 5(s)
3: 5(s)
4: 5(s)
5: 1 2 3 4
"""
exclude_ports = []
dp_local_in_port = self._port_is_dp_local(in_port)
if not dp_local_in_port:
in_port_peer_dp = in_port.stack['dp']
exclude_ports = [
port for port in self.stack_ports
if port.stack and port.stack['dp'] == in_port_peer_dp]
local_flood_actions = self._build_flood_local_rule_actions(
vlan, exclude_unicast, in_port)
away_flood_actions = valve_of.flood_tagged_port_outputs(
self.away_from_root_stack_ports, in_port, exclude_ports=exclude_ports)
toward_flood_actions = valve_of.flood_tagged_port_outputs(
self.towards_root_stack_ports, in_port)
flood_all_except_self = away_flood_actions + local_flood_actions
# TODO: optimization for 2 layer stack - no need to reflect off root.
# We should generalize the edge switch case, too.
if self.stack.get('longest_path_to_root_len', None) == 2:
if self._dp_is_root():
return away_flood_actions + local_flood_actions
if dp_local_in_port:
return toward_flood_actions + local_flood_actions
return local_flood_actions
if self._dp_is_root():
if dp_local_in_port:
return flood_all_except_self
# If input port non-local, then flood outward again
return [valve_of.output_in_port()] + flood_all_except_self
# We are not the root of the distributed switch
# If input port was connected to a switch closer to the root,
# then flood outwards (local VLAN and stacks further than us)
if in_port in self.towards_root_stack_ports:
return flood_all_except_self
# If input port local or from a further away switch, flood
# towards the root.
return toward_flood_actions
def _build_mask_flood_rules(self, vlan, eth_dst, eth_dst_mask, # pylint: disable=too-many-arguments
exclude_unicast, command, flood_priority,
mirror_acts):
ofmsgs = self._combinatorial_port_flood(
vlan, eth_dst, eth_dst_mask,
exclude_unicast, command, flood_priority, mirror_acts)
if not ofmsgs:
for port in self.stack_ports:
ofmsgs.extend(self._build_flood_rule_for_port(
vlan, eth_dst, eth_dst_mask,
exclude_unicast, command, flood_priority + 1,
port, mirror_acts))
ofmsgs.extend(self._build_flood_rule_for_vlan(
vlan, eth_dst, eth_dst_mask,
exclude_unicast, command, flood_priority, mirror_acts))
return ofmsgs
[docs] def build_flood_rules(self, vlan, modify=False):
"""Add flows to flood packets to unknown destinations on a VLAN."""
command = valve_of.ofp.OFPFC_ADD
if modify:
command = valve_of.ofp.OFPFC_MODIFY_STRICT
# TODO: group tables for stacking
ofmsgs = self._build_multiout_flood_rules(vlan, command)
if self._dp_is_root():
return ofmsgs
# Because stacking uses reflected broadcasts from the root,
# don't try to learn broadcast sources from stacking ports.
for port in self.stack_ports:
ofmsgs.append(self.eth_src_table.flowdrop(
self.eth_src_table.match(
in_port=port.number,
vlan=vlan,
eth_dst=valve_packet.BRIDGE_GROUP_ADDRESS,
eth_dst_mask=valve_packet.BRIDGE_GROUP_MASK),
priority=self.bypass_priority+1))
for unicast_eth_dst, eth_dst, eth_dst_mask in self.FLOOD_DSTS:
if unicast_eth_dst:
continue
for port in self.stack_ports:
match = self.eth_src_table.match(
in_port=port.number,
vlan=vlan,
eth_dst=eth_dst,
eth_dst_mask=eth_dst_mask)
ofmsgs.append(self.eth_src_table.flowmod(
match=match,
command=command,
inst=[self.eth_src_table.goto(self.flood_table)],
priority=self.bypass_priority))
return ofmsgs
def _vlan_all_ports(self, vlan, exclude_unicast):
vlan_all_ports = super(ValveFloodStackManager, self)._vlan_all_ports(
vlan, exclude_unicast)
vlan_all_ports.extend(self.away_from_root_stack_ports)
vlan_all_ports.extend(self.towards_root_stack_ports)
return vlan_all_ports
def _dp_is_root(self):
"""Return True if this datapath is the root of the stack."""
return 'priority' in self.stack
def _port_is_dp_local(self, port):
"""Return True if port is on this datapath."""
if (port in self.away_from_root_stack_ports or
port in self.towards_root_stack_ports):
return False
return True
def _edge_dp_for_host(self, other_valves, pkt_meta):
"""Simple distributed unicast learning.
Args:
other_valves (list): All Valves other than this one.
pkt_meta (PacketMeta): PacketMeta instance for packet received.
Returns:
Valve instance or None (of edge datapath where packet received)
"""
# TODO: simplest possible unicast learning.
# We find just one port that is the shortest unicast path to
# the destination. We could use other factors (eg we could
# load balance over multiple ports based on destination MAC).
# TODO: each DP learns independently. An edge DP could
# call other valves so they learn immediately without waiting
# for packet in.
# TODO: edge DPs could use a different forwarding algorithm
# (for example, just default switch to a neighbor).
# Find port that forwards closer to destination DP that
# has already learned this host (if any).
# TODO: stacking handles failure of redundant links between DPs,
# but not failure of an entire DP (should be able to find
# shortest path via alternate DP).
if pkt_meta.port.stack:
peer_dp = pkt_meta.port.stack['dp']
if peer_dp.is_stack_edge() or peer_dp.is_stack_root():
return peer_dp
stacked_valves = [valve for valve in other_valves if valve.dp.stack is not None]
eth_src = pkt_meta.eth_src
vlan_vid = pkt_meta.vlan.vid
for other_valve in stacked_valves:
if vlan_vid in other_valve.dp.vlans:
other_dp_vlan = other_valve.dp.vlans[vlan_vid]
entry = other_dp_vlan.cached_host(eth_src)
if entry and not entry.port.stack:
return other_valve.dp
return None
[docs] def update_stack_topo(self, event, dp, port=None):
"""Update the stack topo according to the event."""
def _stack_topo_up_dp(_dp): # pylint: disable=invalid-name
for port in [port for port in _dp.stack_ports]:
if port.is_stack_up():
_stack_topo_up_port(_dp, port)
else:
_stack_topo_down_port(_dp, port)
def _stack_topo_down_dp(_dp): # pylint: disable=invalid-name
for port in [port for port in _dp.stack_ports]:
_stack_topo_down_port(_dp, port)
def _stack_topo_up_port(_dp, _port): # pylint: disable=invalid-name
_dp.add_stack_link(self.stack['graph'], _dp, _port)
def _stack_topo_down_port(_dp, _port): # pylint: disable=invalid-name
_dp.remove_stack_link(self.stack['graph'], _dp, _port)
if port:
if event:
_stack_topo_up_port(dp, port)
else:
_stack_topo_down_port(dp, port)
else:
if event:
_stack_topo_up_dp(dp)
else:
_stack_topo_down_dp(dp)
return True
[docs] def edge_learn_port(self, other_valves, pkt_meta):
"""Possibly learn a host on a port.
Args:
other_valves (list): All Valves other than this one.
pkt_meta (PacketMeta): PacketMeta instance for packet received.
Returns:
port to learn host on, or None.
"""
if pkt_meta.port.stack:
edge_dp = self._edge_dp_for_host(other_valves, pkt_meta)
# No edge DP may have learned this host yet.
if edge_dp is None:
return None
return self.shortest_path_port(edge_dp.name)
return super(ValveFloodStackManager, self).edge_learn_port(
other_valves, pkt_meta)