/* * Copyright 2017-present Open Networking Laboratory * * 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. */ package org.onosproject.routing.impl; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import org.apache.felix.scr.annotations.Activate; import org.apache.felix.scr.annotations.Component; import org.apache.felix.scr.annotations.Deactivate; import org.apache.felix.scr.annotations.Modified; import org.apache.felix.scr.annotations.Property; import org.apache.felix.scr.annotations.Reference; import org.apache.felix.scr.annotations.ReferenceCardinality; import org.onlab.packet.EthType; import org.onlab.packet.Ethernet; import org.onlab.packet.IPv4; import org.onlab.packet.IPv6; import org.onlab.packet.IP; import org.onlab.packet.IpAddress; import org.onlab.packet.MacAddress; import org.onlab.packet.VlanId; import org.onlab.util.Tools; import org.onosproject.cfg.ComponentConfigService; import org.onosproject.core.ApplicationId; import org.onosproject.core.CoreService; import org.onosproject.incubator.net.intf.Interface; import org.onosproject.incubator.net.intf.InterfaceService; import org.onosproject.net.ConnectPoint; import org.onosproject.net.Host; import org.onosproject.net.flow.DefaultTrafficSelector; import org.onosproject.net.flow.DefaultTrafficTreatment; import org.onosproject.net.flow.TrafficSelector; import org.onosproject.net.host.HostEvent; import org.onosproject.net.host.HostListener; import org.onosproject.net.host.HostService; import org.onosproject.net.packet.DefaultOutboundPacket; import org.onosproject.net.packet.OutboundPacket; import org.onosproject.net.packet.PacketContext; import org.onosproject.net.packet.PacketPriority; import org.onosproject.net.packet.PacketProcessor; import org.onosproject.net.packet.PacketService; import org.osgi.service.component.ComponentContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.nio.ByteBuffer; import java.util.Optional; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.TimeUnit; import static com.google.common.base.Preconditions.checkNotNull; import static org.onlab.packet.IpAddress.Version.INET6; /** * Reactively handles sending packets to hosts that are directly connected to * router interfaces. */ @Component(immediate = true, enabled = false) public class DirectHostManager { private Logger log = LoggerFactory.getLogger(getClass()); @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY) protected PacketService packetService; @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY) protected InterfaceService interfaceService; @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY) protected HostService hostService; @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY) protected CoreService coreService; @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY) protected ComponentConfigService componentConfigService; private static final boolean DEFAULT_ENABLED = false; @Property(name = "enabled", boolValue = DEFAULT_ENABLED, label = "Enable reactive directly-connected host processing") private volatile boolean enabled = DEFAULT_ENABLED; private static final String APP_NAME = "org.onosproject.directhost"; private static final long MAX_QUEUED_PACKETS = 10000; private static final long MAX_QUEUE_DURATION = 2; // seconds private ApplicationId appId; private InternalPacketProcessor packetProcessor = new InternalPacketProcessor(); private InternalHostListener hostListener = new InternalHostListener(); private Cache<IpAddress, Queue<IP>> ipPacketCache = CacheBuilder.newBuilder() .weigher((IpAddress key, Queue<IP> value) -> value.size()) .maximumWeight(MAX_QUEUED_PACKETS) .expireAfterAccess(MAX_QUEUE_DURATION, TimeUnit.SECONDS) .build(); @Activate public void activate(ComponentContext context) { componentConfigService.registerProperties(getClass()); appId = coreService.registerApplication(APP_NAME); modified(context); } @Modified private void modified(ComponentContext context) { Boolean boolEnabled = Tools.isPropertyEnabled(context.getProperties(), "enabled"); if (boolEnabled != null) { if (enabled && !boolEnabled) { enabled = false; disable(); } else if (!enabled && boolEnabled) { enabled = true; enable(); } } } private void enable() { hostService.addListener(hostListener); packetService.addProcessor(packetProcessor, PacketProcessor.director(3)); // Requests packets for IPv4 traffic. TrafficSelector selector = DefaultTrafficSelector.builder() .matchEthType(EthType.EtherType.IPV4.ethType().toShort()).build(); packetService.requestPackets(selector, PacketPriority.REACTIVE, appId, Optional.empty()); // Requests packets for IPv6 traffic. selector = DefaultTrafficSelector.builder() .matchEthType(EthType.EtherType.IPV6.ethType().toShort()).build(); packetService.requestPackets(selector, PacketPriority.REACTIVE, appId, Optional.empty()); } private void disable() { packetService.removeProcessor(packetProcessor); hostService.removeListener(hostListener); // Withdraws IPv4 request. TrafficSelector selector = DefaultTrafficSelector.builder() .matchEthType(EthType.EtherType.IPV4.ethType().toShort()).build(); packetService.cancelPackets(selector, PacketPriority.REACTIVE, appId, Optional.empty()); // Withdraws IPv6 request. selector = DefaultTrafficSelector.builder() .matchEthType(EthType.EtherType.IPV6.ethType().toShort()).build(); packetService.cancelPackets(selector, PacketPriority.REACTIVE, appId, Optional.empty()); } @Deactivate public void deactivate() { if (enabled) { disable(); } componentConfigService.unregisterProperties(getClass(), false); } private boolean handle(Ethernet eth) { checkNotNull(eth); // If the DirectHostManager is not enabled and the // packets are different from what we expect just // skip them. if (!enabled || (eth.getEtherType() != Ethernet.TYPE_IPV6 && eth.getEtherType() != Ethernet.TYPE_IPV4)) { return false; } // According to the type we set the destIp. IpAddress dstIp; if (eth.getEtherType() == Ethernet.TYPE_IPV4) { IPv4 ip = (IPv4) eth.getPayload(); dstIp = IpAddress.valueOf(ip.getDestinationAddress()); } else { IPv6 ip = (IPv6) eth.getPayload(); dstIp = IpAddress.valueOf(INET6, ip.getDestinationAddress()); } // Looking for a candidate output port. Interface egressInterface = interfaceService.getMatchingInterface(dstIp); if (egressInterface == null) { log.info("No egress interface found for {}", dstIp); return false; } // Looking for the destination mac. Optional<Host> host = hostService.getHostsByIp(dstIp).stream() .filter(h -> h.location().equals(egressInterface.connectPoint())) .filter(h -> h.vlan().equals(egressInterface.vlan())) .findAny(); // If we don't have a destination we start the monitoring // and we queue the packets waiting for a destination. if (host.isPresent()) { transformAndSend( (IP) eth.getPayload(), eth.getEtherType(), egressInterface, host.get().mac() ); } else { hostService.startMonitoringIp(dstIp); ipPacketCache.asMap().compute(dstIp, (ip, queue) -> { if (queue == null) { queue = new ConcurrentLinkedQueue<>(); } queue.add((IP) eth.getPayload()); return queue; }); } return true; } private void transformAndSend(IP ip, short ethType, Interface egressInterface, MacAddress macAddress) { // Base processing for IPv4 if (ethType == Ethernet.TYPE_IPV4) { IPv4 ipv4 = (IPv4) ip; ipv4.setTtl((byte) (ipv4.getTtl() - 1)); ipv4.setChecksum((short) 0); // Base processing for IPv6. } else { IPv6 ipv6 = (IPv6) ip; ipv6.setHopLimit((byte) (ipv6.getHopLimit() - 1)); ipv6.resetChecksum(); } // Sends and serializes. Ethernet eth = new Ethernet(); eth.setDestinationMACAddress(macAddress); eth.setSourceMACAddress(egressInterface.mac()); eth.setEtherType(ethType); eth.setPayload(ip); if (!egressInterface.vlan().equals(VlanId.NONE)) { eth.setVlanID(egressInterface.vlan().toShort()); } send(eth, egressInterface.connectPoint()); } private void send(Ethernet eth, ConnectPoint cp) { OutboundPacket packet = new DefaultOutboundPacket(cp.deviceId(), DefaultTrafficTreatment.builder().setOutput(cp.port()).build(), ByteBuffer.wrap(eth.serialize())); packetService.emit(packet); } private void sendQueued(IpAddress ipAddress, MacAddress macAddress) { log.debug("Sending queued packets for {} ({})", ipAddress, macAddress); ipPacketCache.asMap().computeIfPresent(ipAddress, (ip, packets) -> { packets.forEach(ipPackets -> { Interface egressInterface = interfaceService.getMatchingInterface(ipAddress); if (egressInterface == null) { log.info("No egress interface found for {}", ipAddress); return; } // According to the type of the address we set proper // protocol. transformAndSend( ipPackets, ipAddress.isIp4() ? Ethernet.TYPE_IPV4 : Ethernet.TYPE_IPV6, egressInterface, macAddress ); }); return null; }); } private class InternalPacketProcessor implements PacketProcessor { @Override public void process(PacketContext context) { if (context.isHandled()) { return; } if (interfaceService.getInterfacesByPort(context.inPacket().receivedFrom()).isEmpty()) { // Don't handle packets that don't come from one of our configured interfaces return; } Ethernet eth = context.inPacket().parsed(); if (eth == null) { return; } if (!handle(eth)) { return; } context.block(); } } private class InternalHostListener implements HostListener { @Override public void event(HostEvent event) { switch (event.type()) { case HOST_ADDED: event.subject().ipAddresses().forEach(ip -> DirectHostManager.this.sendQueued(ip, event.subject().mac())); break; case HOST_REMOVED: case HOST_UPDATED: case HOST_MOVED: default: break; } } } }