/* * 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.Iterator; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.openflow.protocol.OFMessage; import org.openflow.protocol.OFPacketIn; import org.openflow.protocol.OFPacketIn.OFPacketInReason; import org.openflow.protocol.OFPacketOut; import org.openflow.protocol.OFType; 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.core.annotations.LogMessageDoc; import org.sdnplatform.core.annotations.LogMessageDocs; import org.sdnplatform.devicemanager.IDevice; import org.sdnplatform.devicemanager.IDeviceListener; import org.sdnplatform.devicemanager.IDeviceService; import org.sdnplatform.netvirt.core.VNS; import org.sdnplatform.netvirt.core.VNSInterface; import org.sdnplatform.netvirt.core.VNS.DHCPMode; import org.sdnplatform.netvirt.manager.INetVirtManagerService; import org.sdnplatform.netvirt.virtualrouting.IVirtualRoutingService; import org.sdnplatform.packet.DHCP; import org.sdnplatform.packet.Ethernet; import org.sdnplatform.packet.IPv4; import org.sdnplatform.packet.UDP; import org.sdnplatform.routing.RoutingDecision; import org.sdnplatform.routing.IRoutingDecision.RoutingAction; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @LogMessageCategory("Network Virtualization") public class DhcpManager implements IOFMessageListener { protected static Logger logger = LoggerFactory.getLogger(DhcpManager.class); /* Maps deviceKeys to the dhcpServer instance (which keeps track of * pending requests, etc.). We will maintain this map for all DHCP servers. */ private ConcurrentMap<Long, DhcpServer> dhcpServers; /* Maps NetVirt (name) to the deviceId of its DhcpServer. Only used for * snooped servers. We currently allow only a single server per NetVirt. * FIXME: we don't remove stale entries from this map */ private ConcurrentMap<String, Long> netVirtDhcpServers; private TimeoutCache<Long> deviceIdsPending; /* This timeout is used to stop converting to unicast in Flood-if-unknown * mode in one of two cases: * * We converted to unicast for a client but the client did not receive * a reply in the last TIMEOUT ms. * * We converted a request to a particular server to unicast but the * server has not send /any/ replies since. * The two cases are subtly different and thus we handle them independently. * (E.g., if the DHCP server changes its config and is not responsible * for a particular NetVirt anymore. The server is till alive but it might * not answer to (some) clients. */ protected long UNICAST_CONV_TIMEOUT = 2000; // in ms protected IControllerService controllerProvider; protected IDeviceService deviceManager; protected IVirtualRoutingService virtualRouting; protected INetVirtManagerService netVirtManager; protected DeviceListenerImpl deviceListener; public void init() { dhcpServers = new ConcurrentHashMap<Long, DhcpServer>(); netVirtDhcpServers = new ConcurrentHashMap<String, Long>(); deviceIdsPending = new TimeoutCache<Long>(UNICAST_CONV_TIMEOUT); deviceListener = new DeviceListenerImpl(); } // ******************* // Getters and Setters // ******************* @Override public String getName() { return "dhcpmanager"; } public IDeviceListener getDeviceListener() { return deviceListener; } public void setControllerProvider(IControllerService controllerProvider) { this.controllerProvider = controllerProvider; } public void setDeviceManager(IDeviceService deviceManager) { this.deviceManager = deviceManager; } // ******************* // Internal Methods // ******************* private OFPacketIn convertBCastToUcast(Ethernet eth, OFPacketIn pi, IDevice dst) { byte[] dstMac = Ethernet.toByteArray(dst.getMACAddress()); eth.setDestinationMACAddress(dstMac); byte[] serializedPacket = eth.serialize(); OFPacketIn fakePi = ((OFPacketIn) controllerProvider.getOFMessageFactory() .getMessage(OFType.PACKET_IN)) .setInPort(pi.getInPort()) .setBufferId(OFPacketOut.BUFFER_ID_NONE) .setReason(OFPacketInReason.NO_MATCH); fakePi.setPacketData(serializedPacket); fakePi.setTotalLength((short) serializedPacket.length); fakePi.setLength((short)(OFPacketIn.MINIMUM_LENGTH + serializedPacket.length)); return fakePi; } /** * Add FORWARD_OR_FLOOD decision, return STOP to indicate that a decision * has been made */ private Command floodDhcpPacket(IOFSwitch sw, OFPacketIn pi, IDevice srcDev, ListenerContext cntx) { RoutingDecision vrd = new RoutingDecision(sw.getId(), pi.getInPort(), srcDev, RoutingAction.FORWARD_OR_FLOOD); // FIXME: what to do with wildcards since we didn't run ACLs. // Sigh. Not using wildcards as a quick and ugly fix vrd.setWildcards(0); vrd.addToContext(cntx); return Command.STOP; } /** * Add a NONE decision and return STOP, the packet is effectively being * ignored. */ private Command ignoreDhcpPacket(IOFSwitch sw, OFPacketIn pi, IDevice srcDev, ListenerContext cntx) { RoutingDecision vrd = new RoutingDecision(sw.getId(), pi.getInPort(), srcDev, RoutingAction.NONE); vrd.addToContext(cntx); return Command.STOP; } /** * Handle a broadcast DHCP request for FLOOD_IF_UNKNOWN mode * @param netVirt * @param srcDevice * @param eth * @param pi * @param sw * @param cntx * @return */ private Command handleFloodIfUnkownRequest(VNS netVirt, IDevice srcDevice, Ethernet eth, OFPacketIn pi, IOFSwitch sw, ListenerContext cntx) { // Get the snooped server for this NetVirt. If none exists we flood the // packet. Long serverDeviceId = netVirtDhcpServers.get(netVirt.getName()); if (serverDeviceId == null) { return floodDhcpPacket(sw, pi, srcDevice, cntx); } DhcpServer server = dhcpServers.get(serverDeviceId); IDevice serverDevice = deviceManager.getDevice(serverDeviceId); if (server == null || serverDevice == null) { return floodDhcpPacket(sw, pi, srcDevice, cntx); } if (deviceIdsPending.isTimeoutExpired(srcDevice.getDeviceKey())) { // Check that this device doesn't have a long outstanding request // that hasn't been answered yet. If we have outstanding requests // we need to flood. if (logger.isDebugEnabled()) { logger.debug("Not converting to unicast. Unanswered DHCP " + "request from {} pending for more than {} ms", srcDevice, UNICAST_CONV_TIMEOUT); } return floodDhcpPacket(sw, pi, srcDevice, cntx); } // Check liveness if (!server.isAlive()) { // If server has been unresponsive to previous request, we // need to flood. if (logger.isDebugEnabled()) { logger.debug("Not converting to unicast. DHCP server {}" + "has been unresponsive", serverDevice); } return floodDhcpPacket(sw, pi, srcDevice, cntx); } OFPacketIn fakePi = convertBCastToUcast(eth, pi, serverDevice); controllerProvider.injectOfMessage(sw, fakePi); deviceIdsPending.putIfAbsent(srcDevice.getDeviceKey()); server.hadRequest(); return ignoreDhcpPacket(sw, pi, srcDevice, cntx); } /** * Handle a broadcast DHCP request for STATIC mode * @param netVirt * @param srcDevice * @param eth * @param pi * @param sw * @param cntx * @return */ @LogMessageDocs({ @LogMessageDoc(level="WARN", message="DHCP server {server or relay IP} configured " + "{NetVirt name} is unknown to DeviceManager. " + "Dropping request", explanation="The named NetVirt uses DHCP-mode static but the " + "configured server or relay ould not be found. " + "The DHCP request is ignored", recommendation=LogMessageDoc.GENERIC_ACTION) }) private Command handleStaticRequest(VNS netVirt, IDevice srcDevice, Ethernet eth, OFPacketIn pi, IOFSwitch sw, ListenerContext cntx) { // Query device manager for the configure IP address of the // DHCP server or relay IDevice dhcpServerDevice = null; Iterator<? extends IDevice> dstiter = deviceManager.queryClassDevices(srcDevice.getEntityClass(), null, null, netVirt.getDhcpIp(), null, null); if (dstiter.hasNext()) { dhcpServerDevice = dstiter.next(); } if (dhcpServerDevice != null) { OFPacketIn fakePi = convertBCastToUcast(eth, pi, dhcpServerDevice); controllerProvider.injectOfMessage(sw, fakePi); } else { // We could not locate a Device for the configured // DHCP server. We need to drop the request. // TODO: should rate-limit this log message logger.warn("DHCP server {} configured for NetVirt {} " + "is unknown to DeviceManager. Dropping request", IPv4.fromIPv4Address(netVirt.getDhcpIp()), netVirt.getName()); } return ignoreDhcpPacket(sw, pi, srcDevice, cntx); } @LogMessageDocs({ @LogMessageDoc(level="ERROR", message="No source device found for MAC {mac address}", explanation="Could not find a source device for the " + "source of a DHCP packet.", recommendation=LogMessageDoc.CHECK_CONTROLLER), @LogMessageDoc(level="WARN", message="Possible rogue DHCP server {ip address} " + "detected, stopping flow", explanation="", recommendation=LogMessageDoc.CHECK_CONTROLLER), @LogMessageDoc(level="ERROR", message="Unknown DHCP Mode {} for NetVirt {}", explanation="The configures DHCP mode is unknown", recommendation=LogMessageDoc.REPORT_CONTROLLER_BUG), @LogMessageDoc(level="INFO", message="NetVirt {name} snooped DHCP {ip address}", explanation="A new DHCP server was discovered for the " + "given NetVirt."), @LogMessageDoc(level="WARN", message="DHCP server {ip address} is not known " + "to the device manager", explanation="A DHCP reply was seen that doesn't correspond " + "to any known device.", recommendation=LogMessageDoc.TRANSIENT_CONDITION), @LogMessageDoc(level="ERROR", message="DHCP reply to unknown destination device, " + "MAC = {mac address}", explanation="A DHCP reply was sent to an address that doesn't " + "correspond to any known device.", recommendation=LogMessageDoc.TRANSIENT_CONDITION) }) private Command processPacketInMessage(IOFSwitch sw, OFPacketIn pi, ListenerContext cntx) { Ethernet eth = IControllerService.bcStore. get(cntx, IControllerService.CONTEXT_PI_PAYLOAD); if (eth.getEtherType() != Ethernet.TYPE_IPv4) return Command.CONTINUE; IPv4 ipv4 = (IPv4) eth.getPayload(); if (ipv4.getProtocol() != IPv4.PROTOCOL_UDP) return Command.CONTINUE; UDP udp = (UDP) ipv4.getPayload(); // Make sure it's a DHCP request/reply if (!(udp.getPayload() instanceof DHCP)) return Command.CONTINUE; // Get source device IDevice srcDevice = IDeviceService.fcStore. get(cntx, IDeviceService.CONTEXT_SRC_DEVICE); if (srcDevice == null) { logger.error("No source device found for MAC {}", HexString.toHexString(eth.getSourceMACAddress())); return ignoreDhcpPacket(sw, pi, null, cntx); } // Get destination device. This can be null. IDevice dstDevice = IDeviceService.fcStore. get(cntx, IDeviceService.CONTEXT_DST_DEVICE); if (logger.isTraceEnabled()) { logger.trace("DHCP Packet found from {}", srcDevice); } List<VNSInterface> srcIfaces = INetVirtManagerService.bcStore. get(cntx, INetVirtManagerService.CONTEXT_SRC_IFACES); DHCP dhcp = (DHCP) udp.getPayload(); byte opcode = dhcp.getOpCode(); // Find the NetVirt and its mode VNS netVirt = null; DHCPMode mode = null; if (srcIfaces != null && srcIfaces.size() > 0) { // ifaces are ordered by priority. In case of unicast packet // VR has already run chooseNetVirt() and replaced the list // of all matching interfaces with the one that actually // matches. netVirt = srcIfaces.get(0).getParentVNS(); mode = netVirt.getDhcpManagerMode(); } else { logger.error("No NetVirt interface found for device {}", srcDevice); return ignoreDhcpPacket(sw, pi, srcDevice, cntx); } /* * If it's a unicast DHCP request we just Command.CONTINUE. * This could either be a host requesting a DHCP lease it had when it * joined this network previously, or it could been re-injected into our * processing chain and we just need to forward it at this stage. * We use flood or forward to ensure that the packet gets there. */ if (opcode == DHCP.OPCODE_REQUEST) { if (eth.isBroadcast()) { switch(mode) { case ALWAYS_FLOOD: return floodDhcpPacket(sw, pi, srcDevice, cntx); case FLOOD_IF_UNKNOWN: return handleFloodIfUnkownRequest(netVirt, srcDevice, eth, pi, sw, cntx); case STATIC: return handleStaticRequest(netVirt, srcDevice, eth, pi, sw, cntx); default: logger.error("Unknown DHCP Mode {} for NetVirt {}", mode, netVirt.getName()); return ignoreDhcpPacket(sw, pi, srcDevice, cntx); } } else { // This is a unicast request. if (dstDevice == null) { // unknown dest device. Flood if appropriate otherwise // drop the packet. // FIXME: we really want the full Virtual Routing logic // to decide here if we can reach the destination // (see processUnicastPacket). But we can't just let // the packet pass through for processUnicastPacket to // handle it because in case the dst is unreachable // we need to flood and not discover the dest via // an ARP. switch(mode) { case ALWAYS_FLOOD: case FLOOD_IF_UNKNOWN: return floodDhcpPacket(sw, pi, srcDevice, cntx); case STATIC: default: return ignoreDhcpPacket(sw, pi, srcDevice, cntx); } } return Command.CONTINUE; // if destination is known, we let the normal VR code // handle it. // FIXME: This implies that we will allow ARP requests to // incorrect servers in STATIC mode. We still prevent // spoofing because we verify the reply. } } else if (opcode == DHCP.OPCODE_REPLY) { RoutingDecision vrd = null; // track server (regardless of DHCPMode and flag that we've // received a response. // TODO: could limit this to FLOOD_IF_UNKOWN only Long serverDeviceId = srcDevice.getDeviceKey(); DhcpServer server = dhcpServers.get(serverDeviceId); if (server == null) { server = new DhcpServer(UNICAST_CONV_TIMEOUT); DhcpServer oldServer = dhcpServers.putIfAbsent(srcDevice.getDeviceKey(), server); if (oldServer != null) server = oldServer; } server.hadResponse(); if (dstDevice != null) deviceIdsPending.remove(dstDevice.getDeviceKey()); switch(mode) { case ALWAYS_FLOOD: if (eth.isBroadcast()) { return floodDhcpPacket(sw, pi, srcDevice, cntx); } else { if (dstDevice == null) return floodDhcpPacket(sw, pi, srcDevice, cntx); } // Unicast and device is known. Let normal VR handle it return Command.CONTINUE; case FLOOD_IF_UNKNOWN: Long oldServerId = netVirtDhcpServers.put(netVirt.getName(), serverDeviceId); if (logger.isDebugEnabled()) { if (oldServerId == null) { logger.debug("NetVirt {} snooped DHCP {}", netVirt.getName(), srcDevice.getMACAddress()); } else if (! oldServerId.equals(serverDeviceId)) { logger.debug("NetVirt {} snooped DHCP changed to {}", netVirt.getName(), srcDevice.getMACAddress()); } } if (eth.isBroadcast()) { // FIXME: get client HW-addr from DHCP and unicast to // it ?? Can we do this in all cases? Why didn't the // server send unicast if it knows the client's HW // address? return floodDhcpPacket(sw, pi, srcDevice, cntx); } else { if (dstDevice == null) { return floodDhcpPacket(sw, pi, srcDevice, cntx); } } return Command.CONTINUE; case STATIC: boolean serverIpFound = false; for(Integer ip: srcDevice.getIPv4Addresses()) { if (ip != null && ip.equals(netVirt.getDhcpIp())) { serverIpFound = true; break; } } if (! serverIpFound) { logger.warn("Possible rogue DHCP server {} detected, stopping flow", IPv4.fromIPv4Address(ipv4.getSourceAddress())); vrd = new RoutingDecision(sw.getId(), pi.getInPort(), srcDevice, RoutingAction.DROP); vrd.addDestinationDevice(dstDevice); vrd.addToContext(cntx); return Command.STOP; } if (eth.isBroadcast()) { // FIXME: get client HW-addr from DHCP and unicast to // it ?? Can we do this in all cases? Why didn't the // server send unicast if it knows the client's HW // address? return floodDhcpPacket(sw, pi, srcDevice, cntx); } else { if (dstDevice == null) { return ignoreDhcpPacket(sw, pi, srcDevice, cntx); } } return Command.CONTINUE; default: break; } } // we should never get here. return Command.CONTINUE; } // ******************* // 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 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) { dhcpServers.remove(device.getDeviceKey()); deviceIdsPending.remove(device.getDeviceKey()); } @Override public void deviceRemoved(IDevice device) { dhcpServers.remove(device.getDeviceKey()); deviceIdsPending.remove(device.getDeviceKey()); } @Override public void deviceMoved(IDevice device) { // no-op } @Override public void deviceIPV4AddrChanged(IDevice device) { // no-op } @Override public void deviceVlanChanged(IDevice device) { // no-op } @Override public String getName() { return DhcpManager.this.getName(); } @Override public boolean isCallbackOrderingPrereq(String type, String name) { return false; } @Override public boolean isCallbackOrderingPostreq(String type, String name) { return false; } } }