Source code for faucet.valve_host

"""Manage host learning 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--2019 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.valve_manager_base import ValveManagerBase


[docs]class ValveHostManager(ValveManagerBase): """Manage host learning on VLANs.""" def __init__(self, logger, ports, vlans, eth_src_table, eth_dst_table, eth_dst_hairpin_table, pipeline, learn_timeout, learn_jitter, learn_ban_timeout, cache_update_guard_time, idle_dst, stack, has_externals, stack_root_flood_reflection): self.logger = logger self.ports = ports self.vlans = vlans self.eth_src_table = eth_src_table self.eth_dst_table = eth_dst_table self.eth_dst_hairpin_table = eth_dst_hairpin_table self.pipeline = pipeline self.learn_timeout = learn_timeout self.learn_jitter = learn_jitter self.learn_ban_timeout = learn_ban_timeout self.low_priority = self._LOW_PRIORITY self.host_priority = self._MATCH_PRIORITY self.high_priority = self._HIGH_PRIORITY self.cache_update_guard_time = cache_update_guard_time self.output_table = self.eth_dst_table self.idle_dst = idle_dst self.stack = stack self.has_externals = has_externals self.stack_root_flood_reflection = stack_root_flood_reflection if self.eth_dst_hairpin_table: self.output_table = self.eth_dst_hairpin_table
[docs] def ban_rules(self, pkt_meta): """Limit learning to a maximum configured on this port/VLAN. Args: pkt_meta: PacketMeta instance. Returns: list: OpenFlow messages, if any. """ ofmsgs = [] port = pkt_meta.port eth_src = pkt_meta.eth_src vlan = pkt_meta.vlan entry = vlan.cached_host(eth_src) if entry is None: if port.max_hosts: if port.hosts_count() == port.max_hosts: ofmsgs.append(self._temp_ban_host_learning( self.eth_src_table.match(in_port=port.number))) port.dyn_learn_ban_count += 1 self.logger.info( 'max hosts %u reached on %s, ' 'temporarily banning learning on this port, ' 'and not learning %s' % ( port.max_hosts, port, eth_src)) if vlan is not None and vlan.max_hosts: hosts_count = vlan.hosts_count() if hosts_count == vlan.max_hosts: ofmsgs.append(self._temp_ban_host_learning(self.eth_src_table.match(vlan=vlan))) vlan.dyn_learn_ban_count += 1 self.logger.info( 'max hosts %u reached on VLAN %u, ' 'temporarily banning learning on this VLAN, ' 'and not learning %s on %s' % ( vlan.max_hosts, vlan.vid, eth_src, port)) return ofmsgs
[docs] def add_port(self, port): ofmsgs = [] if port.coprocessor: ofmsgs.append(self.eth_src_table.flowmod( match=self.eth_src_table.match(in_port=port.number), priority=self.high_priority, inst=[self.eth_src_table.goto(self.output_table)])) return ofmsgs
[docs] def del_port(self, port): ofmsgs = [] ofmsgs.append( self.eth_src_table.flowdel(self.eth_src_table.match(in_port=port.number))) for table in (self.eth_dst_table, self.eth_dst_hairpin_table): if table: # per OF 1.3.5 B.6.23, the OFA will match flows # that have an action targeting this port. ofmsgs.append(table.flowdel(out_port=port.number)) vlans = port.vlans() if port.stack: vlans = self.vlans.values() for vlan in vlans: vlan.clear_cache_hosts_on_port(port) return ofmsgs
[docs] def initialise_tables(self): ofmsgs = [] for vlan in self.vlans.values(): ofmsgs.append(self.eth_src_table.flowcontroller( match=self.eth_src_table.match(vlan=vlan), priority=self.low_priority, inst=[self.eth_src_table.goto(self.output_table)])) return ofmsgs
def _temp_ban_host_learning(self, match): return self.eth_src_table.flowdrop( match, priority=(self.low_priority + 1), hard_timeout=self.learn_ban_timeout)
[docs] def delete_host_from_vlan(self, eth_src, vlan): """Delete a host from a VLAN.""" ofmsgs = [self.eth_src_table.flowdel( self.eth_src_table.match(vlan=vlan, eth_src=eth_src))] for table in (self.eth_dst_table, self.eth_dst_hairpin_table): if table: ofmsgs.append(table.flowdel(table.match(vlan=vlan, eth_dst=eth_src))) return ofmsgs
[docs] def expire_hosts_from_vlan(self, vlan, now): """Expire hosts from VLAN cache.""" expired_hosts = vlan.expire_cache_hosts(now, self.learn_timeout) if expired_hosts: vlan.dyn_last_time_hosts_expired = now self.logger.info( '%u recently active hosts on VLAN %u, expired %s' % ( vlan.hosts_count(), vlan.vid, expired_hosts)) return expired_hosts
def _jitter_learn_timeout(self, base_learn_timeout, port, eth_dst): """Calculate jittered learning timeout to avoid synchronized host timeouts.""" # Hosts on this port never timeout. if port.permanent_learn: return 0 if not base_learn_timeout: return 0 # Jitter learn timeout based on eth address, so timeout processing is jittered, # the same hosts will timeout approximately the same time on a stack. jitter = hash(eth_dst) % self.learn_jitter min_learn_timeout = base_learn_timeout - self.learn_jitter return int(max(abs(min_learn_timeout + jitter), self.cache_update_guard_time))
[docs] def learn_host_timeouts(self, port, eth_src): """Calculate flow timeouts for learning on a port.""" learn_timeout = self._jitter_learn_timeout(self.learn_timeout, port, eth_src) # Update datapath to no longer send packets from this mac to controller # note the use of hard_timeout here and idle_timeout for the dst table # this is to ensure that the source rules will always be deleted before # any rules on the dst table. Otherwise if the dst table rule expires # but the src table rule is still being hit intermittantly the switch # will flood packets to that dst and not realise it needs to relearn # the rule # NB: Must be lower than highest priority otherwise it can match # flows destined to controller src_rule_idle_timeout = 0 src_rule_hard_timeout = learn_timeout dst_rule_idle_timeout = learn_timeout + self.cache_update_guard_time if not self.idle_dst: dst_rule_idle_timeout = 0 return (src_rule_idle_timeout, src_rule_hard_timeout, dst_rule_idle_timeout)
[docs] def learn_host_intervlan_routing_flows(self, port, vlan, eth_src, eth_dst): """Returns flows for the eth_src_table that enable packets that have been routed to be accepted from an adjacent DP and then switched to the destination. Eth_src_table flow rule to match on port, eth_src, eth_dst and vlan Args: port (Port): Port to match on. vlan (VLAN): VLAN to match on eth_src: source MAC address (should be the router MAC) eth_dst: destination MAC address """ ofmsgs = [] (src_rule_idle_timeout, src_rule_hard_timeout, _) = self.learn_host_timeouts(port, eth_src) src_match = self.eth_src_table.match(vlan=vlan, eth_src=eth_src, eth_dst=eth_dst) src_priority = self.host_priority - 1 inst = [self.eth_src_table.goto(self.output_table)] ofmsgs.extend([self.eth_src_table.flowmod( match=src_match, priority=src_priority, inst=inst, hard_timeout=src_rule_hard_timeout, idle_timeout=src_rule_idle_timeout)]) return ofmsgs
[docs] def learn_host_on_vlan_port_flows(self, port, vlan, eth_src, delete_existing, refresh_rules, src_rule_idle_timeout, src_rule_hard_timeout, dst_rule_idle_timeout): """Return flows that implement learning a host on a port.""" ofmsgs = [] # Delete any existing entries for MAC. if delete_existing: ofmsgs.extend(self.delete_host_from_vlan(eth_src, vlan)) # Associate this MAC with source port. src_match = self.eth_src_table.match( in_port=port.number, vlan=vlan, eth_src=eth_src) src_priority = self.host_priority - 1 inst = [] inst.append(self.eth_src_table.goto(self.output_table)) ofmsgs.append(self.eth_src_table.flowmod( match=src_match, priority=src_priority, inst=inst, hard_timeout=src_rule_hard_timeout, idle_timeout=src_rule_idle_timeout)) hairpinning = port.hairpin or port.hairpin_unicast # If we are refreshing only and not in hairpin mode, leave existing eth_dst alone. if refresh_rules and not hairpinning: return ofmsgs external_forwarding_requested = None match_dict = { 'vlan': vlan, 'eth_dst': eth_src, valve_of.EXTERNAL_FORWARDING_FIELD: None} if self.has_externals: match_dict.update({ valve_of.EXTERNAL_FORWARDING_FIELD: valve_of.PCP_EXT_PORT_FLAG}) if port.tagged_vlans and port.loop_protect_external and self.stack: external_forwarding_requested = False elif not port.stack: external_forwarding_requested = True inst = self.pipeline.output( port, vlan, external_forwarding_requested=external_forwarding_requested) # Output packets for this MAC to specified port. ofmsgs.append(self.eth_dst_table.flowmod( self.eth_dst_table.match(**match_dict), priority=self.host_priority, inst=inst, idle_timeout=dst_rule_idle_timeout)) if self.has_externals and not port.loop_protect_external: match_dict.update({ valve_of.EXTERNAL_FORWARDING_FIELD: valve_of.PCP_NONEXT_PORT_FLAG}) ofmsgs.append(self.eth_dst_table.flowmod( self.eth_dst_table.match(**match_dict), priority=self.host_priority, inst=inst, idle_timeout=dst_rule_idle_timeout)) # If port is in hairpin mode, install a special rule # that outputs packets destined to this MAC back out the same # port they came in (e.g. multiple hosts on same WiFi AP, # and FAUCET is switching between them on the same port). if hairpinning: ofmsgs.append(self.eth_dst_hairpin_table.flowmod( self.eth_dst_hairpin_table.match(in_port=port.number, vlan=vlan, eth_dst=eth_src), priority=self.host_priority, inst=self.pipeline.output(port, vlan, hairpin=True), idle_timeout=dst_rule_idle_timeout)) return ofmsgs
[docs] def learn_host_on_vlan_ports(self, now, port, vlan, eth_src, delete_existing=True, last_dp_coldstart_time=None): """Learn a host on a port.""" ofmsgs = [] cache_port = None cache_age = None entry = vlan.cached_host(eth_src) refresh_rules = False # Host not cached, and no hosts expired since we cold started # Enable faster learning by assuming there's no previous host to delete if entry is None: if (last_dp_coldstart_time and (vlan.dyn_last_time_hosts_expired is None or vlan.dyn_last_time_hosts_expired < last_dp_coldstart_time)): delete_existing = False elif entry.port.permanent_learn: if entry.port != port: ofmsgs.extend(self.pipeline.filter_packets( {'eth_src': eth_src, 'in_port': port.number})) return (ofmsgs, entry.port, False) else: cache_age = now - entry.cache_time cache_port = entry.port if cache_port is not None: # packet was received on same member of a LAG. same_lag = (port.lacp and port.lacp == cache_port.lacp) # stacks of size > 2 will have an unknown MAC flooded towards the root, # and flooded down again. If we learned the MAC on a local port and # heard the reflected flooded copy, discard the reflection. local_stack_learn = ( self.stack_root_flood_reflection and port.stack and not cache_port.stack) guard_time = self.cache_update_guard_time if cache_port == port or same_lag or local_stack_learn: # aggressively re-learn on LAGs, and prefer recently learned # locally learned hosts on a stack. if same_lag or local_stack_learn: guard_time = 2 # recent cache update, don't do anything. if cache_age < guard_time: return (ofmsgs, cache_port, False) # skip delete if host didn't change ports or on same LAG. if cache_port == port or same_lag: delete_existing = False refresh_rules = True if port.loop_protect: ban_age = None learn_ban = False # if recently in loop protect mode and still receiving packets, # prolong the ban if port.dyn_last_ban_time: ban_age = now - port.dyn_last_ban_time if ban_age < self.cache_update_guard_time: learn_ban = True # if not in protect mode and we get a rapid move, enact protect mode if not learn_ban and entry is not None: if port != cache_port and cache_age < self.cache_update_guard_time: learn_ban = True port.dyn_learn_ban_count += 1 self.logger.info('rapid move of %s from %s to %s, temp loop ban %s' % ( eth_src, cache_port, port, port)) # already, or newly in protect mode, apply the ban rules. if learn_ban: port.dyn_last_ban_time = now ofmsgs.append(self._temp_ban_host_learning( self.eth_src_table.match(in_port=port.number))) return (ofmsgs, cache_port, False) (src_rule_idle_timeout, src_rule_hard_timeout, dst_rule_idle_timeout) = self.learn_host_timeouts(port, eth_src) ofmsgs.extend(self.learn_host_on_vlan_port_flows( port, vlan, eth_src, delete_existing, refresh_rules, src_rule_idle_timeout, src_rule_hard_timeout, dst_rule_idle_timeout)) return (ofmsgs, cache_port, True)
[docs] def flow_timeout(self, _now, _table_id, _match): """Handle a flow timed out message from dataplane.""" return []
[docs]class ValveHostFlowRemovedManager(ValveHostManager): """Trigger relearning on flow removed notifications. .. note:: not currently reliable. """
[docs] def flow_timeout(self, now, table_id, match): ofmsgs = [] if table_id in (self.eth_src_table.table_id, self.eth_dst_table.table_id): if 'vlan_vid' in match: vlan = self.vlans[valve_of.devid_present(match['vlan_vid'])] in_port = None eth_src = None eth_dst = None for field, value in match.items(): if field == 'in_port': in_port = value elif field == 'eth_src': eth_src = value elif field == 'eth_dst': eth_dst = value if eth_src and in_port: port = self.ports[in_port] ofmsgs.extend(self._src_rule_expire(vlan, port, eth_src)) elif eth_dst: ofmsgs.extend(self._dst_rule_expire(now, vlan, eth_dst)) return ofmsgs
[docs] def expire_hosts_from_vlan(self, _vlan, _now): return []
[docs] def learn_host_timeouts(self, port, eth_src): """Calculate flow timeouts for learning on a port.""" learn_timeout = self._jitter_learn_timeout(self.learn_timeout, port, eth_src) # Disable hard_time, dst rule expires after src rule. src_rule_idle_timeout = learn_timeout src_rule_hard_timeout = 0 dst_rule_idle_timeout = learn_timeout + self.cache_update_guard_time return (src_rule_idle_timeout, src_rule_hard_timeout, dst_rule_idle_timeout)
def _src_rule_expire(self, vlan, port, eth_src): """When a src rule expires, the host is probably inactive or active in receiving but not sending. We mark just mark the host as expired.""" ofmsgs = [] entry = vlan.cached_host_on_port(eth_src, port) if entry is not None: vlan.expire_cache_host(eth_src) self.logger.info('expired src_rule for host %s' % eth_src) return ofmsgs def _dst_rule_expire(self, now, vlan, eth_dst): """Expiring a dst rule may indicate that the host is actively sending traffic but not receving. If the src rule not yet expires, we reinstall host rules.""" ofmsgs = [] entry = vlan.cached_host(eth_dst) if entry is not None: ofmsgs.extend(self.learn_host_on_vlan_ports( now, entry.port, vlan, eth_dst, delete_existing=False)) self.logger.info( 'refreshing host %s from VLAN %u' % (eth_dst, vlan.vid)) return ofmsgs