/* * Copyright 2014-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.net.proxyarp.impl; 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.Reference; import org.apache.felix.scr.annotations.ReferenceCardinality; import org.apache.felix.scr.annotations.Service; import org.onlab.packet.ARP; import org.onlab.packet.Ethernet; import org.onlab.packet.ICMP6; import org.onlab.packet.IPv6; import org.onlab.packet.Ip4Address; import org.onlab.packet.Ip6Address; import org.onlab.packet.IpAddress; import org.onlab.packet.MacAddress; import org.onlab.packet.VlanId; import org.onlab.packet.ndp.NeighborAdvertisement; import org.onlab.packet.ndp.NeighborDiscoveryOptions; import org.onlab.packet.ndp.NeighborSolicitation; 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.device.DeviceService; import org.onosproject.net.edge.EdgePortService; import org.onosproject.net.flow.DefaultTrafficTreatment; import org.onosproject.net.flow.TrafficTreatment; import org.onosproject.net.host.HostService; import org.onosproject.net.link.LinkService; import org.onosproject.net.packet.DefaultOutboundPacket; import org.onosproject.net.packet.InboundPacket; import org.onosproject.net.packet.PacketContext; import org.onosproject.net.packet.PacketService; import org.onosproject.net.proxyarp.ProxyArpService; import org.onosproject.net.proxyarp.ProxyArpStore; import org.slf4j.Logger; import java.nio.ByteBuffer; import java.util.Set; import java.util.stream.Collectors; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static org.onlab.packet.VlanId.vlanId; import static org.onosproject.net.HostId.hostId; import static org.onosproject.security.AppGuard.checkPermission; import static org.slf4j.LoggerFactory.getLogger; import static org.onosproject.security.AppPermission.Type.*; /** * Implementation of the proxy ARP service. * * @deprecated in Hummingbird release */ @Deprecated @Component(immediate = true) @Service public class ProxyArpManager implements ProxyArpService { private final Logger log = getLogger(getClass()); private static final String MAC_ADDR_NULL = "MAC address cannot be null."; private static final String REQUEST_NULL = "ARP or NDP request cannot be null."; private static final String MSG_NOT_REQUEST = "Message is not an ARP or NDP request"; @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY) protected EdgePortService edgeService; @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY) protected HostService hostService; @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY) protected PacketService packetService; @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY) protected LinkService linkService; @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY) protected DeviceService deviceService; @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY) protected ProxyArpStore store; @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY) protected InterfaceService interfaceService; private enum Protocol { ARP, NDP } private enum MessageType { REQUEST, REPLY } @Activate public void activate() { store.setDelegate(this::sendTo); log.info("Started"); } @Deactivate public void deactivate() { store.setDelegate(null); log.info("Stopped"); } @Override public boolean isKnown(IpAddress addr) { checkPermission(PACKET_READ); checkNotNull(addr, MAC_ADDR_NULL); Set<Host> hosts = hostService.getHostsByIp(addr); return !hosts.isEmpty(); } @Override public void reply(Ethernet eth, ConnectPoint inPort) { checkPermission(PACKET_WRITE); checkNotNull(eth, REQUEST_NULL); MessageContext context = createContext(eth, inPort); if (context != null) { replyInternal(context); } } /** * Handles a request message. * * If the MAC address of the target is known, we can reply directly to the * requestor. Otherwise, we forward the request out other ports in an * attempt to find the correct host. * * @param context request message context to process */ private void replyInternal(MessageContext context) { checkNotNull(context); checkArgument(context.type() == MessageType.REQUEST, MSG_NOT_REQUEST); if (hasIpAddress(context.inPort())) { // If the request came from outside the network, only reply if it was // for one of our external addresses. interfaceService.getInterfacesByPort(context.inPort()) .stream() .filter(intf -> intf.ipAddresses() .stream() .anyMatch(ia -> ia.ipAddress().equals(context.target()))) .forEach(intf -> buildAndSendReply(context, intf.mac())); // Stop here and don't proxy ARPs if the port has an IP address return; } // See if we have the target host in the host store Set<Host> hosts = hostService.getHostsByIp(context.target()); Host dst = null; Host src = hostService.getHost(hostId(context.srcMac(), context.vlan())); for (Host host : hosts) { if (host.vlan().equals(context.vlan())) { dst = host; break; } } if (src != null && dst != null) { // We know the target host so we can respond buildAndSendReply(context, dst.mac()); return; } // If the source address matches one of our external addresses // it could be a request from an internal host to an external // address. Forward it over to the correct port. boolean matched = false; Set<Interface> interfaces = interfaceService.getInterfacesByIp(context.sender()); for (Interface intf : interfaces) { if (intf.vlan().equals(context.vlan())) { matched = true; sendTo(context.packet(), intf.connectPoint()); break; } } if (matched) { return; } // If the packets has a vlanId look if there are some other // interfaces in the configuration on the same vlan and broadcast // the packet out just of through those interfaces. VlanId vlanId = context.vlan(); Set<Interface> filteredVlanInterfaces = filterVlanInterfacesNoIp(interfaceService.getInterfacesByVlan(vlanId)); if (vlanId != null && !vlanId.equals(VlanId.NONE) && confContainsVlans(vlanId, context.inPort())) { vlanFlood(context.packet(), filteredVlanInterfaces, context.inPort); return; } // The request couldn't be resolved. // Flood the request on all ports except the incoming port. flood(context.packet(), context.inPort()); } private Set<Interface> filterVlanInterfacesNoIp(Set<Interface> vlanInterfaces) { return vlanInterfaces .stream() .filter(intf -> intf.ipAddresses().isEmpty()) .collect(Collectors.toSet()); } /** * States if the interface configuration contains more than one interface configured * on a specific vlan, including the interface passed as argument. * * @param vlanId the vlanid to look for in the interface configuration * @param connectPoint the connect point to exclude from the search * @return true if interfaces are found. False otherwise */ private boolean confContainsVlans(VlanId vlanId, ConnectPoint connectPoint) { Set<Interface> vlanInterfaces = interfaceService.getInterfacesByVlan(vlanId); return interfaceService.getInterfacesByVlan(vlanId) .stream() .anyMatch(intf -> intf.connectPoint().equals(connectPoint) && intf.ipAddresses().isEmpty()) && vlanInterfaces.size() > 1; } /** * Builds and sends a reply message given a request context and the resolved * MAC address to answer with. * * @param context message context of request * @param targetMac MAC address to be given in the response */ private void buildAndSendReply(MessageContext context, MacAddress targetMac) { switch (context.protocol()) { case ARP: sendTo(ARP.buildArpReply((Ip4Address) context.target(), targetMac, context.packet()), context.inPort()); break; case NDP: sendTo(buildNdpReply((Ip6Address) context.target(), targetMac, context.packet()), context.inPort()); break; default: break; } } /** * Outputs a packet out a specific port. * * @param packet the packet to send * @param outPort the port to send it out */ private void sendTo(Ethernet packet, ConnectPoint outPort) { sendTo(outPort, ByteBuffer.wrap(packet.serialize())); } /** * Outputs a packet out a specific port. * * @param outPort port to send it out * @param packet packet to send */ private void sendTo(ConnectPoint outPort, ByteBuffer packet) { if (!edgeService.isEdgePoint(outPort)) { // Sanity check to make sure we don't send the packet out an // internal port and create a loop (could happen due to // misconfiguration). return; } TrafficTreatment.Builder builder = DefaultTrafficTreatment.builder(); builder.setOutput(outPort.port()); packetService.emit(new DefaultOutboundPacket(outPort.deviceId(), builder.build(), packet)); } /** * Returns whether the given port has any IP addresses configured or not. * * @param connectPoint the port to check * @return true if the port has at least one IP address configured, * false otherwise */ private boolean hasIpAddress(ConnectPoint connectPoint) { return interfaceService.getInterfacesByPort(connectPoint) .stream() .flatMap(intf -> intf.ipAddresses().stream()) .findAny() .isPresent(); } /** * Returns whether the given port has any VLAN configured or not. * * @param connectPoint the port to check * @return true if the port has at least one VLAN configured, * false otherwise */ private boolean hasVlan(ConnectPoint connectPoint) { return interfaceService.getInterfacesByPort(connectPoint) .stream() .filter(intf -> !intf.vlan().equals(VlanId.NONE)) .findAny() .isPresent(); } @Override public void forward(Ethernet eth, ConnectPoint inPort) { checkPermission(PACKET_WRITE); checkNotNull(eth, REQUEST_NULL); Host h = hostService.getHost(hostId(eth.getDestinationMAC(), vlanId(eth.getVlanID()))); if (h == null) { flood(eth, inPort); } else { Host subject = hostService.getHost(hostId(eth.getSourceMAC(), vlanId(eth.getVlanID()))); store.forward(h.location(), subject, ByteBuffer.wrap(eth.serialize())); } } @Override public boolean handlePacket(PacketContext context) { checkPermission(PACKET_WRITE); InboundPacket pkt = context.inPacket(); Ethernet ethPkt = pkt.parsed(); if (ethPkt == null) { return false; } MessageContext msgContext = createContext(ethPkt, pkt.receivedFrom()); if (msgContext == null) { return false; } switch (msgContext.type()) { case REPLY: forward(msgContext.packet(), msgContext.inPort()); break; case REQUEST: replyInternal(msgContext); break; default: return false; } context.block(); return true; } /** * Flood the arp request at all edges on a specifc VLAN. * * @param request the arp request * @param dsts the destination interfaces * @param inPort the connect point the arp request was received on */ private void vlanFlood(Ethernet request, Set<Interface> dsts, ConnectPoint inPort) { TrafficTreatment.Builder builder = null; ByteBuffer buf = ByteBuffer.wrap(request.serialize()); for (Interface intf : dsts) { ConnectPoint cPoint = intf.connectPoint(); if (cPoint.equals(inPort)) { continue; } builder = DefaultTrafficTreatment.builder(); builder.setOutput(cPoint.port()); packetService.emit(new DefaultOutboundPacket(cPoint.deviceId(), builder.build(), buf)); } } /** * Flood the arp request at all edges in the network. * * @param request the arp request * @param inPort the connect point the arp request was received on */ private void flood(Ethernet request, ConnectPoint inPort) { TrafficTreatment.Builder builder = null; ByteBuffer buf = ByteBuffer.wrap(request.serialize()); for (ConnectPoint connectPoint : edgeService.getEdgePoints()) { if (hasIpAddress(connectPoint) || hasVlan(connectPoint) || connectPoint.equals(inPort)) { continue; } builder = DefaultTrafficTreatment.builder(); builder.setOutput(connectPoint.port()); packetService.emit(new DefaultOutboundPacket(connectPoint.deviceId(), builder.build(), buf)); } } /** * Builds an Neighbor Discovery reply based on a request. * * @param srcIp the IP address to use as the reply source * @param srcMac the MAC address to use as the reply source * @param request the Neighbor Solicitation request we got * @return an Ethernet frame containing the Neighbor Advertisement reply */ private Ethernet buildNdpReply(Ip6Address srcIp, MacAddress srcMac, Ethernet request) { Ethernet eth = new Ethernet(); eth.setDestinationMACAddress(request.getSourceMAC()); eth.setSourceMACAddress(srcMac); eth.setEtherType(Ethernet.TYPE_IPV6); eth.setVlanID(request.getVlanID()); IPv6 requestIp = (IPv6) request.getPayload(); IPv6 ipv6 = new IPv6(); ipv6.setSourceAddress(srcIp.toOctets()); ipv6.setDestinationAddress(requestIp.getSourceAddress()); ipv6.setHopLimit((byte) 255); ICMP6 icmp6 = new ICMP6(); icmp6.setIcmpType(ICMP6.NEIGHBOR_ADVERTISEMENT); icmp6.setIcmpCode((byte) 0); NeighborAdvertisement nadv = new NeighborAdvertisement(); nadv.setTargetAddress(srcIp.toOctets()); nadv.setSolicitedFlag((byte) 1); nadv.setOverrideFlag((byte) 1); nadv.addOption(NeighborDiscoveryOptions.TYPE_TARGET_LL_ADDRESS, srcMac.toBytes()); icmp6.setPayload(nadv); ipv6.setPayload(icmp6); eth.setPayload(ipv6); return eth; } /** * Attempts to create a MessageContext for the given Ethernet frame. If the * frame is a valid ARP or NDP request or response, a context will be * created. * * @param eth input Ethernet frame * @param inPort in port * @return MessageContext if the packet was ARP or NDP, otherwise null */ private MessageContext createContext(Ethernet eth, ConnectPoint inPort) { if (eth.getEtherType() == Ethernet.TYPE_ARP) { return createArpContext(eth, inPort); } else if (eth.getEtherType() == Ethernet.TYPE_IPV6) { return createNdpContext(eth, inPort); } return null; } /** * Extracts context information from ARP packets. * * @param eth input Ethernet frame that is thought to be ARP * @param inPort in port * @return MessageContext object if the packet was a valid ARP packet, * otherwise null */ private MessageContext createArpContext(Ethernet eth, ConnectPoint inPort) { if (eth.getEtherType() != Ethernet.TYPE_ARP) { return null; } ARP arp = (ARP) eth.getPayload(); IpAddress target = Ip4Address.valueOf(arp.getTargetProtocolAddress()); IpAddress sender = Ip4Address.valueOf(arp.getSenderProtocolAddress()); MessageType type; if (arp.getOpCode() == ARP.OP_REQUEST) { type = MessageType.REQUEST; } else if (arp.getOpCode() == ARP.OP_REPLY) { type = MessageType.REPLY; } else { return null; } return new MessageContext(eth, inPort, Protocol.ARP, type, target, sender); } /** * Extracts context information from NDP packets. * * @param eth input Ethernet frame that is thought to be NDP * @param inPort in port * @return MessageContext object if the packet was a valid NDP packet, * otherwise null */ private MessageContext createNdpContext(Ethernet eth, ConnectPoint inPort) { if (eth.getEtherType() != Ethernet.TYPE_IPV6) { return null; } IPv6 ipv6 = (IPv6) eth.getPayload(); if (ipv6.getNextHeader() != IPv6.PROTOCOL_ICMP6) { return null; } ICMP6 icmpv6 = (ICMP6) ipv6.getPayload(); IpAddress sender = Ip6Address.valueOf(ipv6.getSourceAddress()); IpAddress target = null; MessageType type; if (icmpv6.getIcmpType() == ICMP6.NEIGHBOR_SOLICITATION) { type = MessageType.REQUEST; NeighborSolicitation nsol = (NeighborSolicitation) icmpv6.getPayload(); target = Ip6Address.valueOf(nsol.getTargetAddress()); } else if (icmpv6.getIcmpType() == ICMP6.NEIGHBOR_ADVERTISEMENT) { type = MessageType.REPLY; } else { return null; } return new MessageContext(eth, inPort, Protocol.NDP, type, target, sender); } /** * Provides context information for a particular ARP or NDP message, with * a unified interface to access data regardless of protocol. */ private class MessageContext { private Protocol protocol; private MessageType type; private IpAddress target; private IpAddress sender; private Ethernet eth; private ConnectPoint inPort; public MessageContext(Ethernet eth, ConnectPoint inPort, Protocol protocol, MessageType type, IpAddress target, IpAddress sender) { this.eth = eth; this.inPort = inPort; this.protocol = protocol; this.type = type; this.target = target; this.sender = sender; } public ConnectPoint inPort() { return inPort; } public Ethernet packet() { return eth; } public Protocol protocol() { return protocol; } public MessageType type() { return type; } public VlanId vlan() { return VlanId.vlanId(eth.getVlanID()); } public MacAddress srcMac() { return MacAddress.valueOf(eth.getSourceMACAddress()); } public IpAddress target() { return target; } public IpAddress sender() { return sender; } } }