"""VLAN configuration."""
# 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.
import collections
import ipaddress
import random
import netaddr
from faucet import valve_of
from faucet.conf import Conf, test_config_condition, InvalidConfigError
from faucet.valve_packet import FAUCET_MAC
[docs]
class OFVLAN:
"""OpenFlow VLAN."""
def __init__(self, name, vid):
self.name = name
self.vid = vid
[docs]
class NullVLAN:
"""Placeholder null VLAN."""
name = "Null VLAN"
vid = valve_of.ofp.OFPVID_NONE
[docs]
class AnyVLAN:
"""Placeholder any tagged VLAN. NOTE: Not used, not well supported by hardware"""
name = "Any VLAN"
vid = valve_of.ofp.OFPVID_PRESENT
[docs]
class HostCacheEntry:
"""Association of a host with a port."""
__slots__ = [
"cache_time",
"eth_src",
"eth_src_int",
"port",
]
def __init__(self, eth_src, port, cache_time):
self.eth_src = eth_src
self.port = port
self.cache_time = cache_time
self.eth_src_int = int(eth_src.replace(":", ""), 16)
def __hash__(self):
return hash((self.eth_src_int, self.port.number))
def __str__(self):
return "%s on %s" % (self.eth_src, self.port)
def __repr__(self):
return self.__str__()
def __eq__(self, other):
return self.__hash__() == other.__hash__()
def __lt__(self, other):
return self.__hash__() < other.__hash__()
[docs]
class VLAN(Conf):
"""Contains state for one VLAN, including its configuration."""
# Note: while vlans are configured once for each datapath, there will be a
# separate vlan object created for each datapath that the vlan appears on
mutable_attrs = frozenset(["tagged", "untagged", "dot1x_untagged"])
defaults = {
"name": None,
"description": None,
"acl_in": None,
"acls_in": None,
"acl_out": None,
"acls_out": None,
"faucet_vips": None,
"faucet_mac": FAUCET_MAC,
# set MAC for FAUCET VIPs on this VLAN
"unicast_flood": True,
"routes": None,
"max_hosts": 256,
# Limit number of hosts that can be learned on a VLAN.
"vid": None,
"proactive_arp_limit": 0,
# Don't proactively ARP for hosts if over this limit (default 2*max_hosts)
"proactive_nd_limit": 0,
# Don't proactively ND for hosts if over this limit (default 2*max_hosts)
"targeted_gw_resolution": True,
# If True, target the first re-resolution attempt to last known port only.
"minimum_ip_size_check": True,
# If False, don't check that IP packets have a payload (OVS trace/tutorial requires False).
"reserved_internal_vlan": False,
# If True, forward packets from the VLAN table to the VLAN_ACL table matching the VID
"dot1x_assigned": False,
# If True, this VLAN may be dynamically added withTunnel-Private-Group-ID radius attribute.
"edge_learn_stack_root": True,
# If True, this VLAN will learn flows through the stack root, following forwarding path.
}
defaults_types = {
"name": str,
"description": str,
"acl_in": (int, str),
"acls_in": list,
"acl_out": (int, str),
"acls_out": list,
"faucet_vips": list,
"faucet_mac": str,
"unicast_flood": bool,
"routes": list,
"max_hosts": int,
"vid": int,
"proactive_arp_limit": int,
"proactive_nd_limit": int,
"targeted_gw_resolution": bool,
"minimum_ip_size_check": bool,
"reserved_internal_vlan": bool,
"dot1x_assigned": bool,
"edge_learn_stack_root": bool,
}
def __init__(self, _id, dp_id, conf=None):
self.acl_in = None
self.acls_in = None
self.acl_out = None
self.acls_out = None
self.description = None
self.dot1x_assigned = None
self.dot1x_untagged = None
self.dp_id = None
self.edge_learn_stack_root = None
self.faucet_mac = None
self.faucet_vips = None
self.max_hosts = None
self.minimum_ip_size_check = None
self.reserved_internal_vlan = None
self.name = None
self.proactive_arp_limit = None
self.proactive_nd_limit = None
self.routes = None
self.tagged = None
self.targeted_gw_resolution = None
self.unicast_flood = None
self.untagged = None
self.vid = None
self.acls = {}
self.tagged = []
self.untagged = []
self.dot1x_untagged = []
self.dyn_host_cache = None
self.dyn_host_cache_by_port = None
self.dyn_host_cache_stats_stale = None
self.dyn_last_time_hosts_expired = None
self.dyn_learn_ban_count = 0
self.dyn_neigh_cache_by_ipv = None
self.dyn_oldest_host_time = None
self.dyn_last_updated_metrics_sec = None
self.dyn_routes_by_ipv = collections.defaultdict(dict)
self.dyn_gws_by_ipv = collections.defaultdict(dict)
self.dyn_host_gws_by_ipv = collections.defaultdict(set)
self.dyn_route_gws_by_ipv = collections.defaultdict(set)
self.reset_caches()
super().__init__(_id, dp_id, conf)
[docs]
def set_defaults(self):
super().set_defaults()
self._set_default("vid", self._id)
self._set_default("name", str(self._id))
self._set_default("faucet_vips", [])
[docs]
def check_config(self):
super().check_config()
test_config_condition(not self.vid_valid(self.vid), "invalid VID %s" % self.vid)
test_config_condition(
not netaddr.valid_mac(self.faucet_mac),
("invalid MAC address %s" % self.faucet_mac),
)
self.faucet_mac = str(
netaddr.EUI(
self.faucet_mac, dialect=netaddr.strategy.eui48.mac_unix_expanded
)
)
test_config_condition(
self.acl_in and self.acls_in,
"found both acl_in and acls_in, use only acls_in",
)
test_config_condition(
self.acl_out and self.acls_out,
"found both acl_out and acls_out, use only acls_out",
)
if self.acl_in and not isinstance(self.acl_in, list):
self.acls_in = [
self.acl_in,
]
self.acl_in = None
if self.acl_out and not isinstance(self.acl_out, list):
self.acls_out = [
self.acl_out,
]
self.acl_out = None
all_acls = []
if self.acls_in:
all_acls.extend(self.acls_in)
if self.acls_out:
all_acls.extend(self.acls_out)
for acl in all_acls:
test_config_condition(
not isinstance(acl, (int, str)), "acl names must be int or str"
)
if self.max_hosts:
if not self.proactive_arp_limit:
self.proactive_arp_limit = 2 * self.max_hosts
if not self.proactive_nd_limit:
self.proactive_nd_limit = 2 * self.max_hosts
if self.faucet_vips:
self.faucet_vips = frozenset(
[
self._check_ip_str(ip_str, ip_method=ipaddress.ip_interface)
for ip_str in self.faucet_vips
]
)
for faucet_vip in self.faucet_vips:
test_config_condition(
faucet_vip.network.prefixlen == faucet_vip.max_prefixlen,
"VIP cannot be a host address",
)
if self.routes:
test_config_condition(
not isinstance(self.routes, list), "invalid VLAN routes format"
)
try:
self.routes = [route["route"] for route in self.routes]
except TypeError as type_error:
raise InvalidConfigError(
"%s is not a valid routes value" % self.routes
) from type_error
except KeyError:
pass
for route in self.routes:
test_config_condition(
not isinstance(route, dict), "invalid VLAN route format"
)
test_config_condition(
"ip_gw" not in route, "missing ip_gw in VLAN route"
)
test_config_condition(
"ip_dst" not in route, "missing ip_dst in VLAN route"
)
ip_gw = self._check_ip_str(route["ip_gw"])
ip_dst = self._check_ip_str(
route["ip_dst"], ip_method=ipaddress.ip_network
)
test_config_condition(
ip_gw.version != ip_dst.version,
"ip_gw version does not match the ip_dst version",
)
self.add_route(ip_dst, ip_gw)
[docs]
@staticmethod
def vid_valid(vid):
"""Return True if VID valid."""
return isinstance(vid, int) and valve_of.MIN_VID <= vid <= valve_of.MAX_VID
[docs]
def reset_caches(self):
"""Reset dynamic caches."""
self.dyn_host_cache = {}
self.dyn_host_cache_by_port = {}
self.dyn_host_cache_stats_stale = {}
self.dyn_neigh_cache_by_ipv = collections.defaultdict(dict)
self.dyn_unresolved_route_ip_gws = collections.defaultdict(list)
self.dyn_unresolved_host_ip_gws = collections.defaultdict(list)
[docs]
def reset_ports(self, ports):
"""Reset tagged and untagged port lists."""
sorted_ports = sorted(ports, key=lambda i: i.number)
# pylint: disable=consider-using-generator
self.tagged = tuple(
[port for port in sorted_ports if self in port.tagged_vlans]
)
self.untagged = tuple(
[
port
for port in sorted_ports
if (self == port.native_vlan and port.dyn_dot1x_native_vlan is None)
]
)
self.dot1x_untagged = tuple(
[port for port in sorted_ports if self == port.dyn_dot1x_native_vlan]
)
[docs]
def add_cache_host(self, eth_src, port, cache_time):
"""Add/update a host to the cache on a port at at time."""
existing_entry = self.cached_host(eth_src)
if existing_entry is None:
self.dyn_host_cache_stats_stale[port.number] = True
else:
self.dyn_host_cache_by_port[existing_entry.port.number].remove(
existing_entry
)
entry = HostCacheEntry(eth_src, port, cache_time)
if port.number not in self.dyn_host_cache_by_port:
self.dyn_host_cache_by_port[port.number] = set()
self.dyn_host_cache_by_port[port.number].add(entry)
self.dyn_host_cache[eth_src] = entry
[docs]
def expire_cache_host(self, eth_src):
"""Expire a host from caches."""
entry = self.cached_host(eth_src)
if entry is not None:
self.dyn_host_cache_stats_stale[entry.port.number] = True
self.dyn_host_cache_by_port[entry.port.number].remove(entry)
del self.dyn_host_cache[eth_src]
[docs]
def cached_hosts_on_port(self, port):
"""Return all hosts learned on a port."""
if port.number in self.dyn_host_cache_by_port:
return list(self.dyn_host_cache_by_port[port.number])
return []
[docs]
def cached_hosts_count_on_port(self, port):
"""Return count of all hosts learned on a port."""
hosts_count = 0
if port.number in self.dyn_host_cache_by_port:
hosts_count = len(self.dyn_host_cache_by_port[port.number])
return hosts_count
[docs]
def cached_host(self, eth_src):
"""Return host from cache or None."""
return self.dyn_host_cache.get(eth_src, None)
[docs]
def cached_host_on_port(self, eth_src, port):
"""Return host cache entry if host in cache and on specified port."""
entry = self.cached_host(eth_src)
if entry and port == entry.port:
return entry
return None
[docs]
def clear_cache_hosts_on_port(self, port):
"""Clear all hosts learned on a port."""
for entry in self.cached_hosts_on_port(port):
self.expire_cache_host(entry.eth_src)
[docs]
def expire_cache_hosts(self, now, learn_timeout):
"""Expire stale host entries."""
expired_hosts = []
min_cache_time = now - learn_timeout
if (
self.dyn_oldest_host_time is None
or self.dyn_oldest_host_time < min_cache_time
):
expired_hosts = [
entry
for entry in self.dyn_host_cache.values()
if entry.cache_time < min_cache_time and not entry.port.permanent_learn
]
for entry in expired_hosts:
self.expire_cache_host(entry.eth_src)
self.dyn_oldest_host_time = now
if self.dyn_host_cache:
self.dyn_oldest_host_time = min(
[entry.cache_time for entry in self.dyn_host_cache.values()]
)
return expired_hosts
[docs]
def faucet_vips_by_ipv(self, ipv):
"""Return VIPs with specified IP version on this VLAN."""
return self._by_ipv(self.faucet_vips, ipv)
[docs]
def link_and_other_vips(self, ipv):
"""Return link local and non-link local VIPs."""
vips = self.faucet_vips_by_ipv(ipv)
link_local_vips = frozenset([vip for vip in vips if vip.is_link_local])
other_vips = vips - link_local_vips
return (link_local_vips, other_vips)
[docs]
def ipvs(self):
"""Return IP versions configured on this VLAN."""
return self._ipvs(self.faucet_vips)
[docs]
def routes_by_ipv(self, ipv):
"""Return route table for specified IP version on this VLAN."""
return self.dyn_routes_by_ipv[ipv]
[docs]
def route_count_by_ipv(self, ipv):
"""Return route table count for specified IP version on this VLAN."""
return len(self.dyn_routes_by_ipv[ipv])
[docs]
def is_host_fib_route(self, host_ip):
"""Return True if IP destination is a host FIB route.
Args:
host_ip: (ipaddress.ip_address): potential host FIB route.
Returns:
True if a host FIB route (and not used as a gateway).
"""
ip_dsts = self.ip_dsts_for_ip_gw(host_ip)
if (
len(ip_dsts) == 1
and ip_dsts[0].prefixlen == ip_dsts[0].max_prefixlen
and ip_dsts[0].network_address == host_ip
):
return True
return False
def _update_gw_types(self, ip_gw):
"""Update dyn host/route gw information to a different ip version"""
if self.is_host_fib_route(ip_gw):
self.dyn_host_gws_by_ipv[ip_gw.version].add(ip_gw)
self.dyn_route_gws_by_ipv[ip_gw.version] -= set([ip_gw])
else:
self.dyn_route_gws_by_ipv[ip_gw.version].add(ip_gw)
self.dyn_host_gws_by_ipv[ip_gw.version] -= set([ip_gw])
[docs]
def add_route(self, ip_dst, ip_gw):
"""Add an IP route."""
self.dyn_routes_by_ipv[ip_gw.version][ip_dst] = ip_gw
if ip_gw not in self.dyn_gws_by_ipv[ip_gw.version]:
self.dyn_gws_by_ipv[ip_gw.version][ip_gw] = set()
self.dyn_gws_by_ipv[ip_gw.version][ip_gw].add(ip_dst)
self._update_gw_types(ip_gw)
[docs]
def del_route(self, ip_dst):
"""Delete an IP route."""
ip_gw = self.dyn_routes_by_ipv[ip_dst.version][ip_dst]
del self.dyn_routes_by_ipv[ip_dst.version][ip_dst]
self.dyn_gws_by_ipv[ip_gw.version][ip_gw].remove(ip_dst)
if not self.dyn_gws_by_ipv[ip_gw.version][ip_gw]:
del self.dyn_gws_by_ipv[ip_gw.version][ip_gw]
self._update_gw_types(ip_gw)
[docs]
def ip_dsts_for_ip_gw(self, ip_gw):
"""Return list of IP destinations, for specified gateway."""
if ip_gw in self.dyn_gws_by_ipv[ip_gw.version]:
return list(self.dyn_gws_by_ipv[ip_gw.version][ip_gw])
return []
[docs]
def all_ip_gws(self, ipv):
"""Return all IP gateways for specified IP version."""
return frozenset(self.dyn_gws_by_ipv[ipv].keys())
[docs]
def neigh_cache_by_ipv(self, ipv):
"""Return neighbor cache for specified IP version on this VLAN."""
return self.dyn_neigh_cache_by_ipv[ipv]
[docs]
def neigh_cache_count_by_ipv(self, ipv):
"""Return number of hosts in neighbor cache for specified IP version on this VLAN."""
return len(self.neigh_cache_by_ipv(ipv))
[docs]
def hosts_count(self):
"""Return number of hosts learned on this VLAN."""
return len(self.dyn_host_cache)
def __str__(self):
str_ports = []
if self.tagged:
str_ports.append("tagged: %s" % ",".join([str(p) for p in self.tagged]))
if self.untagged:
str_ports.append("untagged: %s" % ",".join([str(p) for p in self.untagged]))
if self.dot1x_untagged:
str_ports.append(
"dot1x_untagged: %s" % ",".join([str(p) for p in self.dot1x_untagged])
)
return "VLAN %s vid:%s %s" % (self.name, self.vid, " ".join(str_ports))
def __repr__(self):
return self.__str__()
[docs]
def get_ports(self):
"""Return all ports on this VLAN."""
return self.tagged + self.untagged + self.dot1x_untagged
[docs]
def restricted_bcast_arpnd_ports(self):
"""Return all ports with restricted broadcast enabled."""
# pylint: disable=consider-using-generator
return tuple([port for port in self.get_ports() if port.restricted_bcast_arpnd])
[docs]
def hairpin_ports(self):
"""Return all ports with hairpin enabled."""
# pylint: disable=consider-using-generator
return tuple([port for port in self.get_ports() if port.hairpin])
[docs]
def mirrored_ports(self):
"""Return ports that are mirrored on this VLAN."""
# pylint: disable=consider-using-generator
return tuple([port for port in self.get_ports() if port.mirror])
[docs]
def loop_protect_external_ports(self):
"""Return ports wth external loop protection set."""
# pylint: disable=consider-using-generator
return tuple([port for port in self.get_ports() if port.loop_protect_external])
[docs]
def loop_protect_external_ports_up(self):
"""Return up ports with external loop protection set."""
# pylint: disable=consider-using-generator
return tuple(
[port for port in self.loop_protect_external_ports() if port.dyn_phys_up]
)
[docs]
def lacp_ports(self):
"""Return ports that have LACP on this VLAN."""
# pylint: disable=consider-using-generator
return tuple([port for port in self.get_ports() if port.lacp])
[docs]
def lacp_up_selected_ports(self):
"""Return LACP ports that have been SELECTED and are UP"""
# pylint: disable=consider-using-generator
return tuple(
[
port
for port in self.lacp_ports()
if port.is_port_selected() and port.is_actor_up()
]
)
[docs]
def lags(self):
"""Return dict of LAGs mapped to member ports."""
lags = collections.defaultdict(list)
for port in self.lacp_ports():
lags[port.lacp].append(port)
return lags
[docs]
def selected_up_lags(self):
"""Return dict of LAGs mapped to member ports that have been selected"""
lags = collections.defaultdict(list)
for port in self.lacp_up_selected_ports():
lags[port.lacp].append(port)
return lags
[docs]
def excluded_lag_ports(self, in_port=None):
"""Ensure output to SELECTED LAG ports & only one LAG member"""
exclude_ports = set()
lags = self.lags()
if lags:
# Need lags that have actor UP & are SELECTED
selected_ports = self.selected_up_lags()
if in_port is not None and in_port.lacp:
# Don't flood to same LAG
exclude_ports.update(lags[in_port.lacp])
# Pick a bundle member to flood to
for lag, ports in lags.items():
selected_lag = selected_ports[lag]
if selected_lag:
ports.remove(selected_lag[0])
exclude_ports.update(ports)
return exclude_ports
[docs]
def exclude_native_if_dot1x(self):
"""Don't output on native vlan, if dynamic (1x) vlan is in use"""
exclude_ports = set()
for port in self.untagged:
if port.dyn_dot1x_native_vlan is None:
continue
if port.dyn_dot1x_native_vlan != self:
exclude_ports.add(port)
return exclude_ports
[docs]
@staticmethod
def flood_ports(configured_ports, exclude_unicast):
"""Return configured ports that allow flooding"""
if exclude_unicast:
return tuple(port for port in configured_ports if port.unicast_flood)
return configured_ports
[docs]
def tagged_flood_ports(self, exclude_unicast):
return self.flood_ports(self.tagged, exclude_unicast)
[docs]
def untagged_flood_ports(self, exclude_unicast):
return self.flood_ports(self.untagged + self.dot1x_untagged, exclude_unicast)
[docs]
def output_port(
self, port, hairpin=False, output_table=None, external_forwarding_requested=None
):
actions = []
if self.port_is_untagged(port):
actions.append(valve_of.pop_vlan())
# Packet is mirrored, as the receiving host sees it (without a tag).
actions.extend(port.mirror_actions())
else:
actions.extend(port.mirror_actions())
if external_forwarding_requested is not None:
if external_forwarding_requested:
actions.append(output_table.set_external_forwarding_requested())
else:
actions.append(output_table.set_no_external_forwarding_requested())
if hairpin:
actions.append(valve_of.output_port(valve_of.OFP_IN_PORT))
else:
actions.append(valve_of.output_port(port.number))
return actions
[docs]
def pkt_out_port(self, packet_builder, port, *args):
"""Return packet-out actions with VLAN tag if port is tagged"""
vid = None
if self.port_is_tagged(port):
vid = self.vid
pkt = packet_builder(vid, *args)
return valve_of.packetout(port.number, bytes(pkt.data))
[docs]
def flood_pkt(self, packet_builder, multi_out, *args):
"""Return Packet-out actions via flooding"""
ofmsgs = []
for vid, ports in (
(self.vid, self.tagged_flood_ports(False)),
(None, self.untagged_flood_ports(False)),
):
if ports:
pkt = packet_builder(vid, *args)
exclude_ports = self.excluded_lag_ports()
running_port_nos = [
port.number
for port in ports
if port.running() and port not in exclude_ports
]
if running_port_nos:
random.shuffle(running_port_nos)
if multi_out:
ofmsgs.append(valve_of.packetouts(running_port_nos, pkt.data))
else:
ofmsgs.extend(
[
valve_of.packetout(port_no, pkt.data)
for port_no in running_port_nos
]
)
return ofmsgs
[docs]
def port_is_tagged(self, port):
"""Return True if port number is an tagged port on this VLAN."""
return port in self.tagged
[docs]
def port_is_untagged(self, port):
"""Return True if port number is an untagged port on this VLAN."""
return port in self.untagged or port in self.dot1x_untagged
[docs]
def vip_map(self, ipa):
"""Return the vip containing ipa"""
for faucet_vip in self.faucet_vips:
if ipa in faucet_vip.network:
return faucet_vip
return None
[docs]
def is_faucet_vip(self, ipa, faucet_vip=None):
"""Return True if IP is a VIP on this VLAN."""
if faucet_vip is None:
faucet_vip = self.vip_map(ipa)
return faucet_vip and ipa == faucet_vip.ip
[docs]
def ip_in_vip_subnet(self, ipa, faucet_vip=None):
"""Return faucet_vip if IP in same IP network as a VIP on this VLAN."""
if faucet_vip is None:
faucet_vip = self.vip_map(ipa)
if faucet_vip:
if ipa not in (
faucet_vip.network.network_address,
faucet_vip.network.broadcast_address,
):
return faucet_vip
return None
[docs]
def from_connected_to_vip(self, src_ip, dst_ip):
"""Return True if src_ip in connected network and dst_ip is a VIP.
Args:
src_ip (ipaddress.ip_address): source IP.
dst_ip (ipaddress.ip_address): destination IP
Returns:
True if local traffic for a VIP.
"""
if self.is_faucet_vip(dst_ip) and self.ip_in_vip_subnet(src_ip):
return True
return False