/* * Copyright (c) 2013 Big Switch Networks, Inc. * * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/legal/epl-v10.html * * 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. */ package org.sdnplatform.netvirt.virtualrouting.internal; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArraySet; import org.openflow.protocol.OFMatch; import org.openflow.protocol.OFMessage; import org.openflow.protocol.OFPacketIn; import org.openflow.protocol.OFPacketOut; import org.openflow.protocol.OFType; import org.openflow.protocol.OFPacketIn.OFPacketInReason; import org.openflow.protocol.factory.BasicFactory; import org.openflow.util.HexString; import org.sdnplatform.core.ListenerContext; import org.sdnplatform.core.IControllerService; import org.sdnplatform.core.IOFMessageListener; import org.sdnplatform.core.IOFSwitch; import org.sdnplatform.core.annotations.LogMessageCategory; import org.sdnplatform.devicemanager.IDevice; import org.sdnplatform.devicemanager.IDeviceListener; import org.sdnplatform.devicemanager.IDeviceService; import org.sdnplatform.devicemanager.SwitchPort; import org.sdnplatform.netvirt.core.VNSInterface; import org.sdnplatform.netvirt.core.VNS.ARPMode; import org.sdnplatform.netvirt.manager.INetVirtManagerService; import org.sdnplatform.netvirt.virtualrouting.IARPListener; import org.sdnplatform.netvirt.virtualrouting.IVirtualRoutingService; import org.sdnplatform.netvirt.virtualrouting.IARPListener.ARPCommand; import org.sdnplatform.packet.ARP; import org.sdnplatform.packet.Ethernet; import org.sdnplatform.packet.IPacket; import org.sdnplatform.packet.IPv4; import org.sdnplatform.routing.RoutingDecision; import org.sdnplatform.routing.IRoutingDecision.RoutingAction; import org.sdnplatform.topology.ITopologyService; import org.sdnplatform.topology.NodePortTuple; import org.sdnplatform.tunnelmanager.ITunnelManagerService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Manage ARP requests on the network to reduce broadcast traffic by * converting broadcast ARP requests into unicast ARP requests. * @author readams * */ @LogMessageCategory("Network Virtualization") public class ArpManager implements IOFMessageListener { protected static Logger logger = LoggerFactory.getLogger(ArpManager.class); public static final short ARP_FLOWMOD_HARD_TIMEOUT = 5; protected IControllerService controllerProvider; protected IDeviceService deviceManager; protected ITopologyService topology; protected IVirtualRoutingService virtualRouting; protected ITunnelManagerService tunnelManager; protected BasicFactory factory; protected Set<IARPListener> arpListeners; protected int ARP_CACHE_DEFAULT_TIMEOUT_MS = 2000; protected int INTER_NetVirt_BROADCAST_SUPPRESSION_TIMEOUT_MS = 30000; protected Map<Long, Long> unicastARPRequestTime; protected DeviceListenerImpl deviceListener; public ArpManager() { arpListeners = new CopyOnWriteArraySet<IARPListener>(); unicastARPRequestTime = new ConcurrentHashMap<Long, Long>(); deviceListener = new DeviceListenerImpl(); } public void startUp() { factory = controllerProvider.getOFMessageFactory(); virtualRouting.addPacketListener(this); deviceManager.addListener(this.deviceListener); } public void addArpListener(IARPListener al) { arpListeners.add(al); if (logger.isTraceEnabled()) { String listeners = "ARP listeners: "; for (IARPListener a : arpListeners) listeners += a.getName() + ", "; logger.trace(listeners); } } public void setControllerProvider( IControllerService controllerProvider) { this.controllerProvider = controllerProvider; } public void setDeviceManager(IDeviceService deviceManager) { this.deviceManager = deviceManager; } public void setVirtualRouting(IVirtualRoutingService virtualRouting) { this.virtualRouting = virtualRouting; } public void setTopology(ITopologyService topology) { this.topology = topology; } public void setTunnelManager(ITunnelManagerService tunnelManager) { this.tunnelManager = tunnelManager; } // ******************* // Internal Methods - ARP packet processing related // ******************* private RoutingDecision setupDecision(IOFSwitch sw, IDevice src, IDevice dest, OFPacketIn pi, Ethernet eth, RoutingAction action) { RoutingDecision vrd = new RoutingDecision(sw.getId(), pi.getInPort(), src, action); // For ARPs, // 1. We wildcard the L4 ports and IP ToS as they have no meaning // in ARP packets. // 2. While the IP proto field in the match can be used to match on // ARP opcode, we wildcard it as hardware may or may not support it // 3. While IP src and dst addresses can also be specified in the match, // we wildcard it as hardware may or may not support it for ARPs // 4. We also wildcard the dl_vlan_pcp field as we are matching on // untagged packets vrd.setWildcards(OFMatch.OFPFW_TP_DST | OFMatch.OFPFW_TP_SRC | OFMatch.OFPFW_NW_TOS | OFMatch.OFPFW_NW_PROTO | OFMatch.OFPFW_NW_SRC_ALL | OFMatch.OFPFW_NW_DST_ALL | OFMatch.OFPFW_DL_VLAN_PCP); /* NetVirt-254 ARP flows need to have a hard timeout. */ vrd.setHardTimeout(ARP_FLOWMOD_HARD_TIMEOUT); if (dest != null) vrd.addDestinationDevice(dest); return vrd; } private boolean isDeviceKnownToCluster(IDevice d, long switchId) { SwitchPort[] aps = d.getAttachmentPoints(); if (aps == null || aps.length == 0) return false; long swclid = topology.getL2DomainId(switchId); for (SwitchPort ap : aps) { long apclid = topology.getL2DomainId(ap.getSwitchDPID()); if (apclid == swclid) return true; } return false; } /** * Gets the ARPMode configuration based on the source device and the NetVirt it * is in. If the device is in multiple NetVirt it will return the 'loosest' * configuration mode. * @param cntx The ListenerContext associated with this processing chain * @return The ARPMode config setting for the device */ private ARPMode getARPMode(ListenerContext cntx) { // TODO - See if we can come up with a config mode // that takes both the source and destination into account ARPMode config = ARPMode.DROP_IF_UNKNOWN; List<VNSInterface> srcIfaces = INetVirtManagerService.bcStore.get(cntx, INetVirtManagerService.CONTEXT_SRC_IFACES); if (srcIfaces != null) { for (VNSInterface iface : srcIfaces) { ARPMode bc = iface.getParentVNS().getArpManagerMode(); if (bc.compareTo(config) > 0) { config = bc; } } } return config; } /** * Get the device corresponding to sender hardware and protocol address * fields in the ARP packet. */ private IDevice getSenderDevice(ARP arp, short vlan, long swdpid, int port) { if (arp == null) return null; byte[] senderAddr = arp.getSenderHardwareAddress(); long senderMAC = Ethernet.toLong(senderAddr); byte[] senderIPAddr = arp.getSenderProtocolAddress(); int senderIP = IPv4.toIPv4Address(senderIPAddr); IDevice device = deviceManager.findDevice(senderMAC, vlan, senderIP, swdpid, port); return device; } private Command processPacketInMessage(IOFSwitch sw, OFPacketIn pi, ListenerContext cntx) { Ethernet eth = IControllerService.bcStore.get(cntx, IControllerService.CONTEXT_PI_PAYLOAD); if (!(eth.getPayload() instanceof ARP)) { if (logger.isTraceEnabled()) { logger.trace("Received a packet from switch {} that was not " + "an ARP, PacketIn={}", HexString.toHexString(sw.getId()), pi); } return Command.CONTINUE; } ARP arp = (ARP) eth.getPayload(); // If this is an ARP packet for something other than IP; handle like // any other layer 2 packet if (arp.getProtocolType() != ARP.PROTO_TYPE_IP) { if (logger.isDebugEnabled()) { logger.debug("Received an ARP packet from switch {} that " + "was not an IP ARP, PacketIn={}", HexString.toHexString(sw.getId()), pi); } return Command.CONTINUE; } // The sender device is obtained from the ARP data IDevice senderDevice = getSenderDevice(arp, eth.getVlanID(), sw.getId(), pi.getInPort()); if (senderDevice == null) return Command.CONTINUE; IDevice src = IDeviceService.fcStore. get(cntx, IDeviceService.CONTEXT_SRC_DEVICE); if (src == null) return Command.CONTINUE; IDevice dst = IDeviceService.fcStore. get(cntx, IDeviceService.CONTEXT_DST_DEVICE); if (logger.isTraceEnabled()) { logger.trace("Received an ARP packet, opcode {}, from switch " + "{}/{} with sender-ip {} target-ip {}", new Object[] {arp.getOpCode(), HexString.toHexString(sw.getId()), pi.getInPort(), IPv4.fromIPv4Address(IPv4.toIPv4Address( arp.getSenderProtocolAddress())), IPv4.fromIPv4Address(IPv4.toIPv4Address( arp.getTargetProtocolAddress()))}); } ARPMode config = getARPMode(cntx); // Call all the ARP handlers for (IARPListener al : arpListeners) { ARPCommand ret = null; if (arp.getOpCode() == ARP.OP_REQUEST) { ret = al.ARPRequestHandler(sw, pi, cntx, config); } else if (arp.getOpCode() == ARP.OP_REPLY) { ret = al.ARPReplyHandler(sw, pi, cntx, config); } else if (arp.getOpCode() == ARP.OP_RARP_REQUEST) { ret = al.RARPRequestHandler(sw, pi, cntx, config); } else if (arp.getOpCode() == ARP.OP_RARP_REPLY) { ret = al.RARPReplyHandler(sw, pi, cntx, config); } if (ret == null || ret == ARPCommand.CONTINUE) { continue; } else if (ret == ARPCommand.SKIP) { // Don't process the rest of the modules. break; } else if (ret == ARPCommand.STOP) { // Stop processing this packet. RoutingAction ra = RoutingAction.NONE; RoutingDecision vrd = setupDecision(sw, src, dst, pi, eth, ra); vrd.addToContext(cntx); return Command.STOP; } } int dstip = IPv4.toIPv4Address(arp.getTargetProtocolAddress()); int srcip = IPv4.toIPv4Address(arp.getSenderProtocolAddress()); // If sender IP is the same as the target IP, // it is a gratuitous ARP. // Let it proceed down the packet processing chain. if (srcip == dstip) { return Command.CONTINUE; } // Remove the unicastARPRequestTime entry from the map // when an ARP reply is received. if (arp.getOpCode() == ARP.OP_REPLY) { // We need to use the sender device here instead of source // device (as the source MAC address in the VRRP ARP response // will be different from the sender hardware address). unicastARPRequestTime.remove(senderDevice.getDeviceKey()); } RoutingAction action = RoutingAction.FORWARD; if (ARPMode.FLOOD_IF_UNKNOWN.equals(config) || ARPMode.ALWAYS_FLOOD.equals(config)) { action = RoutingAction.FORWARD_OR_FLOOD; } boolean isBroadcast = eth.isBroadcast(); if (isBroadcast && !ARPMode.ALWAYS_FLOOD.equals(config)) { Iterator<? extends IDevice> dstiter = deviceManager.queryClassDevices(src.getEntityClass(), null, null, dstip, null, null); boolean found = false; while (dstiter.hasNext()) { dst = dstiter.next(); Integer[] ipv4addrs = dst.getIPv4Addresses(); for (Integer ipv4addr : ipv4addrs) { if (ipv4addr.equals(dstip)) { found = true; break; } } if (found) break; } if (!found) dst = null; if (dst == null && ARPMode.DROP_IF_UNKNOWN.equals(config)) { /** * Drop the packetIn since the destination is unknown. * Do Not install DROP flowmod since dst mac in flowmod is FF, * which would prevent the host to ARP for known hosts. * * FIXME: Not installing DROP flowmod on the switch exposes * the controller to ARP DOS attack. This should be * handled together with other security weakness in the * controller. */ action = RoutingAction.NONE; } else if (dst != null && arp.getOpCode() == ARP.OP_REQUEST) { long currTime = System.currentTimeMillis(); Date dstLastSeen = dst.getLastSeen(); Date threshold = new Date(currTime - INTER_NetVirt_BROADCAST_SUPPRESSION_TIMEOUT_MS); /** * If the dst device is last seen within last * ARP_CACHE_DEFAULT_TIMEOUT_MS and src and dest * devices are not in the same NetVirt, drop the request */ if (virtualRouting.connected(src, srcip, dst, dstip) == false && dstLastSeen != null && dstLastSeen.after(threshold)) { // None will tell virtual routing to drop. action = RoutingAction.NONE; } else if (isDeviceKnownToCluster(dst, sw.getId())) { /** * If dst device has not been heard from, or doesn't have AP in the * cluster where the PI is received, flood the ARP request * as a regular broadcast */ Long lastUArpTime = unicastARPRequestTime.get(dst.getDeviceKey()); if (lastUArpTime != null && lastUArpTime > 0 && (currTime - lastUArpTime > ARP_CACHE_DEFAULT_TIMEOUT_MS) && !ARPMode.DROP_IF_UNKNOWN.equals(config)) { // if lastUArpTime > 0, then it is valid. // It has been more than TIMEOUT (ms) since the last // unicast ARP was sent, and we have not seen any // response, so go back to flooding this ARP request. action = RoutingAction.FORWARD_OR_FLOOD; } else { if (isValidIncomingUnicastPort(src, dst, sw.getId(), pi.getInPort())) { if (lastUArpTime == null) unicastARPRequestTime.put(dst.getDeviceKey(), currTime); OFPacketIn unicastARPRequest = createUnicastARPPacketIn(pi, eth, arp, dst); if (logger.isTraceEnabled()) { logger.trace("Converting ARP to unicast and" + "re-injecting {}", arp); } controllerProvider.injectOfMessage(sw, unicastARPRequest); // None will tell virtual routing to drop. action = RoutingAction.NONE; } else { action = RoutingAction.NONE; if (logger.isTraceEnabled()) { logger.trace("Drop ARP packet as the packet " + "can be converted to unicast, however " + "packet-in switchport is invalid for " + "unicast transmission. " + "AP {}/{} for src device {}", new Object[] { HexString.toHexString(sw.getId()), pi.getInPort(), src.getMACAddressString()}); } } } } } } // Don't program a broadcast packet with a specific dst device if (isBroadcast && (action == RoutingAction.FORWARD || action == RoutingAction.FORWARD_OR_FLOOD)) dst = null; RoutingDecision vrd = setupDecision(sw, src, dst, pi, eth, action); if (logger.isTraceEnabled()) { logger.trace("Handling ARP with action {}: {}", vrd.getRoutingAction(), arp); } vrd.addToContext(cntx); return Command.STOP; } private boolean isValidIncomingUnicastPort(IDevice srcDevice, IDevice dstDevice, long inSwitch, short inPort) { int i; // First, identify if the src-dst traffic is allowed to use tunnels // or not. boolean isTunnelTraffic = tunnelManager.isTunnelEndpoint(srcDevice) || tunnelManager.isTunnelEndpoint(dstDevice); boolean tunnelEnabled = !isTunnelTraffic; // If the packet-in switch port is not an attachment point port, // then, allow unicast conversion to take place. if (!topology.isAttachmentPointPort(inSwitch, inPort, tunnelEnabled)) return true; // Get the source and destination attachment points on the same // L2 domain as the inSwitch. SwitchPort[] srcAPs = srcDevice.getAttachmentPoints(); SwitchPort[] dstAPs = dstDevice.getAttachmentPoints(); SwitchPort srcSwitchPort = null; SwitchPort dstSwitchPort = null; for (i=0; i<srcAPs.length; ++i) { if (topology.inSameL2Domain(inSwitch, srcAPs[i].getSwitchDPID(), tunnelEnabled)) { srcSwitchPort = srcAPs[i]; break; } } // Attachment point for source not found in the same L2 domain if (srcSwitchPort == null) return false; for (i=0; i<dstAPs.length; ++i) { if (topology.inSameL2Domain(inSwitch, dstAPs[i].getSwitchDPID(), tunnelEnabled)) { dstSwitchPort = dstAPs[i]; break; } } // Attachment point for destination not found in the same L2 domain if (dstSwitchPort == null) return false; // Check if the inSwitch, inPort are consistent with the source // attachment point port. If inconsistent, return false. if (!topology.isConsistent(srcSwitchPort.getSwitchDPID(), (short)srcSwitchPort.getPort(), inSwitch, inPort, tunnelEnabled)) return false; // Since the switch port is consistent, use this packet-in switch // packet-in port to verify if it the right switchport for unicast // traffic or not. // Get the incoming switch port for the unicast traffic from // srcAP to dstAP. NodePortTuple npt = topology.getIncomingSwitchPort(inSwitch, inPort, dstSwitchPort.getSwitchDPID(), (short)dstSwitchPort.getPort(), tunnelEnabled); // Verify if npt is the same as the inSwitch and inPort. if (npt == null) return false; return (npt.getNodeId() == inSwitch && npt.getPortId() == inPort); } /** * Converts an ARP into unicast Ethernet frame. We expect * that the destination device has only a single MAC address. * @param pi The original OFPacketIn. * @param eth The Ethernet frame containing the ARP request. * @param arp The ARP packet in the ethernet frame. * @param dst The IDevice destination device */ private OFPacketIn createUnicastARPPacketIn(OFPacketIn pi, Ethernet eth, ARP arp, IDevice dst) { byte[] dstMac = Ethernet.toByteArray(dst.getMACAddress()); IPacket arpRequest = new Ethernet() .setSourceMACAddress(eth.getSourceMACAddress()) .setDestinationMACAddress(dstMac) .setEtherType(Ethernet.TYPE_ARP) .setVlanID(eth.getVlanID()) .setPriorityCode(eth.getPriorityCode()) .setPayload( new ARP() .setHardwareType(ARP.HW_TYPE_ETHERNET) .setProtocolType(ARP.PROTO_TYPE_IP) .setHardwareAddressLength((byte) 6) .setProtocolAddressLength((byte) 4) .setOpCode(ARP.OP_REQUEST) .setSenderHardwareAddress(arp.getSenderHardwareAddress()) .setSenderProtocolAddress(arp.getSenderProtocolAddress()) .setTargetHardwareAddress(arp.getTargetHardwareAddress()) .setTargetProtocolAddress(arp.getTargetProtocolAddress())); byte[] arpRequestSerialized = arpRequest.serialize(); OFPacketIn newpi = (OFPacketIn) controllerProvider.getOFMessageFactory(). getMessage(OFType.PACKET_IN); newpi.setInPort(pi.getInPort()); newpi.setBufferId(OFPacketOut.BUFFER_ID_NONE); newpi.setReason(OFPacketInReason.NO_MATCH); newpi.setPacketData(arpRequestSerialized); newpi.setTotalLength((short) arpRequestSerialized.length); newpi.setLength((short)(OFPacketIn.MINIMUM_LENGTH + arpRequestSerialized.length)); return newpi; } // ******************* // IOFMessageListener // ******************* @Override public Command receive(IOFSwitch sw, OFMessage msg, ListenerContext cntx) { switch (msg.getType()) { case PACKET_IN: return this.processPacketInMessage(sw, (OFPacketIn) msg, cntx); default: return Command.CONTINUE; } } @Override public String getName() { return "arpmanager"; } @Override public boolean isCallbackOrderingPrereq(OFType type, String name) { return false; } @Override public boolean isCallbackOrderingPostreq(OFType type, String name) { return false; } // ******************* // IDeviceListener // ******************* class DeviceListenerImpl implements IDeviceListener { @Override public void deviceAdded(IDevice device) { unicastARPRequestTime.remove(device.getDeviceKey()); } @Override public void deviceRemoved(IDevice device) { unicastARPRequestTime.remove(device.getDeviceKey()); } @Override public void deviceMoved(IDevice device) { unicastARPRequestTime.remove(device.getDeviceKey()); } @Override public void deviceIPV4AddrChanged(IDevice device) { unicastARPRequestTime.remove(device.getDeviceKey()); } @Override public void deviceVlanChanged(IDevice device) { } @Override public String getName() { return ArpManager.this.getName(); } @Override public boolean isCallbackOrderingPrereq(String type, String name) { return false; } @Override public boolean isCallbackOrderingPostreq(String type, String name) { return false; } } }