"""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--2017 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.
import time
import random
from faucet import valve_of
[docs]class ValveHostManager(object):
def __init__(self, logger, ports, vlans, eth_src_table, eth_dst_table,
learn_timeout, learn_jitter, learn_ban_timeout, low_priority, host_priority):
self.logger = logger
self.ports = ports
self.vlans = vlans
self.eth_src_table = eth_src_table
self.eth_dst_table = eth_dst_table
self.learn_timeout = learn_timeout
self.learn_jitter = learn_jitter
self.learn_ban_timeout = learn_ban_timeout
self.low_priority = low_priority
self.host_priority = host_priority
[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
if eth_src not in vlan.host_cache:
if port.max_hosts:
hosts = port.hosts()
if len(hosts) == port.max_hosts:
ofmsgs.append(self._temp_ban_host_learning_on_port(port))
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.max_hosts:
hosts_count = vlan.hosts_count()
if hosts_count == vlan.max_hosts:
ofmsgs.append(self._temp_ban_host_learning_on_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
def _temp_ban_host_learning_on_port(self, port):
return self.eth_src_table.flowdrop(
self.eth_src_table.match(in_port=port.number),
priority=(self.low_priority + 1),
hard_timeout=self.learn_ban_timeout)
def _temp_ban_host_learning_on_vlan(self, vlan):
return self.eth_src_table.flowdrop(
self.eth_src_table.match(vlan=vlan),
priority=(self.low_priority + 1),
hard_timeout=self.learn_ban_timeout)
[docs] def build_port_out_inst(self, vlan, port, port_number=None):
"""Return instructions to output a packet on a given port."""
if port_number is None:
port_number = port.number
dst_act = []
if not vlan.port_is_tagged(port) and port.stack is None:
dst_act.append(valve_of.pop_vlan())
dst_act.append(valve_of.output_port(port_number))
if port.mirror is not None:
mirror_acts = [valve_of.output_port(port.mirror)]
dst_act.extend(mirror_acts)
return [valve_of.apply_actions(dst_act)]
[docs] def delete_host_from_vlan(self, eth_src, vlan):
"""Delete a host from a VLAN."""
ofmsgs = []
ofmsgs.extend(self.eth_src_table.flowdel(
self.eth_src_table.match(vlan=vlan, eth_src=eth_src)))
ofmsgs.extend(self.eth_dst_table.flowdel(
self.eth_dst_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:
self.logger.info(
'%u recently active hosts on VLAN %u, expired %s' % (
vlan.hosts_count(), vlan.vid, expired_hosts))
[docs] def learn_host_timeouts(self, port):
"""Calculate flow timeouts for learning on a port."""
# hosts learned on this port never relearned
if port.permanent_learn:
learn_timeout = 0
else:
learn_timeout = self.learn_timeout
if self.learn_timeout:
# Add a jitter to avoid whole bunch of hosts timeout simultaneously
learn_timeout = int(max(abs(
self.learn_timeout -
(self.learn_jitter / 2) + random.randint(0, self.learn_jitter)), 2))
# 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
return (src_rule_idle_timeout, src_rule_hard_timeout, dst_rule_idle_timeout)
[docs] def learn_host_on_vlan_port_flows(self, port, vlan, eth_src, delete_existing,
src_rule_idle_timeout, src_rule_hard_timeout,
dst_rule_idle_timeout):
"""Return flows that implement learning a host on a port."""
ofmsgs = []
if port.permanent_learn:
# Antispoofing rule for this MAC.
ofmsgs.append(self.eth_src_table.flowdrop(
self.eth_src_table.match(vlan=vlan, eth_src=eth_src),
priority=(self.host_priority - 2)))
else:
# Delete any existing entries for MAC.
# TODO: for LAGs, don't delete entries in the same LAG.
if delete_existing:
ofmsgs.extend(self.delete_host_from_vlan(eth_src, vlan))
# Associate this MAC with source port.
ofmsgs.append(self.eth_src_table.flowmod(
self.eth_src_table.match(
in_port=port.number, vlan=vlan, eth_src=eth_src),
priority=(self.host_priority - 1),
inst=[valve_of.goto_table(self.eth_dst_table)],
hard_timeout=src_rule_hard_timeout,
idle_timeout=src_rule_idle_timeout))
# Output packets for this MAC to specified port.
ofmsgs.append(self.eth_dst_table.flowmod(
self.eth_dst_table.match(vlan=vlan, eth_dst=eth_src),
priority=self.host_priority,
inst=self.build_port_out_inst(vlan, port),
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 port.hairpin:
ofmsgs.append(self.eth_dst_table.flowmod(
self.eth_dst_table.match(in_port=port.number, vlan=vlan, eth_dst=eth_src),
priority=(self.host_priority + 1),
inst=self.build_port_out_inst(vlan, port, port_number=valve_of.OFP_IN_PORT),
idle_timeout=dst_rule_idle_timeout))
return ofmsgs
[docs] def learn_host_on_vlan_ports(self, port, vlan, eth_src, delete_existing=True):
"""Learn a host on a port."""
now = time.time()
ofmsgs = []
ban_age = None
learn_ban = False
if port.loop_protect:
if port.dyn_last_ban_time:
ban_age = now - port.dyn_last_ban_time
if ban_age and ban_age < 2:
learn_ban = True
if not learn_ban:
entry = vlan.cached_host(eth_src)
if entry is not None:
cache_age = now - entry.cache_time
if cache_age < 2:
# Don't relearn same host on same port if recently learned.
if entry.port == port:
return ofmsgs
elif port.loop_protect:
# Ban learning on a port if a host rapidly moves to another port.
if ban_age is None or ban_age > 2:
learn_ban = True
port.dyn_learn_ban_count += 1
ofmsgs.append(self._temp_ban_host_learning_on_port(port))
self.logger.info('rapid move of %s from %s to %s, temp loop ban %s' % (
eth_src, entry.port, port, port))
elif ban_age < 2:
learn_ban = True
if learn_ban:
port.dyn_last_ban_time = now
return ofmsgs
(src_rule_idle_timeout,
src_rule_hard_timeout,
dst_rule_idle_timeout) = self.learn_host_timeouts(port)
ofmsgs.extend(self.learn_host_on_vlan_port_flows(
port, vlan, eth_src, delete_existing,
src_rule_idle_timeout, src_rule_hard_timeout,
dst_rule_idle_timeout))
vlan.add_cache_host(eth_src, port, now)
return ofmsgs
[docs] def flow_timeout(self, _table_id, _match):
return []
[docs]class ValveHostFlowRemovedManager(ValveHostManager):
"""Trigger relearning on flow removed notifications.
.. note::
not currently reliable.
"""
[docs] def flow_timeout(self, table_id, match):
ofmsgs = []
if table_id in (self.eth_src_table.table_id, self.eth_dst_table.table_id):
in_port = None
eth_src = None
eth_dst = None
vid = None
for field, value in list(match.items()):
if field == 'in_port':
in_port = value
elif field == 'eth_src':
eth_src = value
elif field == 'eth_dst':
eth_dst = value
elif field == 'vlan_vid':
vid = valve_of.devid_present(value)
if vid:
vlan = self.vlans[vid]
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(vlan, eth_dst))
return ofmsgs
[docs] def expire_hosts_from_vlan(self, _vlan, _now):
return
[docs] def learn_host_timeouts(self, port):
"""Calculate flow timeouts for learning on a port."""
# hosts learned on this port never relearned
if port.permanent_learn:
learn_timeout = 0
else:
# Add a jitter to avoid whole bunch of hosts timeout simultaneously
learn_timeout = int(max(abs(
self.learn_timeout -
(self.learn_jitter / 2) + random.randint(0, self.learn_jitter)), 2))
# 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 + 2
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:
entry.expired = True
self.logger.info('expired src_rule for host %s' % eth_src)
return ofmsgs
def _dst_rule_expire(self, 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 = []
if eth_dst in vlan.host_cache:
entry = vlan.host_cache[eth_dst]
if not entry.expired:
ofmsgs.extend(self.learn_host_on_vlan_ports(
entry.port, vlan, eth_dst, False))
self.logger.info(
'refreshing host %s from VLAN %u' % (eth_dst, vlan.vid))
return ofmsgs