blob: 27c54f148dd8ebac444e720792a1fff59403beae [file] [log] [blame]
/*
* Copyright 2016-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.openstacknetworking.routing;
import com.google.common.collect.Maps;
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.onlab.packet.Ethernet;
import org.onlab.packet.ICMP;
import org.onlab.packet.IPv4;
import org.onlab.packet.Ip4Address;
import org.onosproject.core.ApplicationId;
import org.onosproject.core.CoreService;
import org.onosproject.net.DeviceId;
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.flow.TrafficTreatment;
import org.onosproject.net.host.HostService;
import org.onosproject.net.packet.DefaultOutboundPacket;
import org.onosproject.net.packet.InboundPacket;
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.onosproject.openstackinterface.OpenstackRouter;
import org.onosproject.openstacknetworking.Constants;
import org.onosproject.openstackinterface.OpenstackInterfaceService;
import org.onosproject.openstackinterface.OpenstackPort;
import org.onosproject.openstacknode.OpenstackNode;
import org.onosproject.openstacknode.OpenstackNodeEvent;
import org.onosproject.openstacknode.OpenstackNodeListener;
import org.onosproject.openstacknode.OpenstackNodeService;
import org.onosproject.scalablegateway.api.GatewayNode;
import org.onosproject.scalablegateway.api.ScalableGatewayService;
import org.slf4j.Logger;
import java.nio.ByteBuffer;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor;
import static org.onlab.util.Tools.groupedThreads;
import static org.onosproject.openstacknetworking.Constants.*;
import static org.onosproject.openstacknode.OpenstackNodeService.NodeType.GATEWAY;
import static org.slf4j.LoggerFactory.getLogger;
/**
* Handle ICMP packet sent from OpenStack Gateway nodes.
* For a request to any private network gateway IPs, it generates fake reply.
* For a request to the external network, it does source NAT with a public IP and
* forward the request to the external only if the request instance has external
* connection setups.
*/
@Component(immediate = true)
public class OpenstackIcmpHandler {
protected final Logger log = getLogger(getClass());
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected CoreService coreService;
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected PacketService packetService;
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected HostService hostService;
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected OpenstackInterfaceService openstackService;
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected ScalableGatewayService gatewayService;
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected OpenstackNodeService nodeService;
private final ExecutorService eventExecutor = newSingleThreadScheduledExecutor(
groupedThreads(this.getClass().getSimpleName(), "event-handler", log));
private final InternalPacketProcessor packetProcessor = new InternalPacketProcessor();
private final InternalNodeListener nodeListener = new InternalNodeListener();
private final Map<String, Host> icmpInfoMap = Maps.newHashMap();
ApplicationId appId;
@Activate
protected void activate() {
appId = coreService.registerApplication(ROUTING_APP_ID);
packetService.addProcessor(packetProcessor, PacketProcessor.director(1));
nodeService.addListener(nodeListener);
requestPacket(appId);
log.info("Started");
}
@Deactivate
protected void deactivate() {
packetService.removeProcessor(packetProcessor);
log.info("Stopped");
}
private void requestPacket(ApplicationId appId) {
TrafficSelector icmpSelector = DefaultTrafficSelector.builder()
.matchEthType(Ethernet.TYPE_IPV4)
.matchIPProtocol(IPv4.PROTOCOL_ICMP)
.build();
gatewayService.getGatewayDeviceIds().forEach(gateway -> {
packetService.requestPackets(icmpSelector,
PacketPriority.CONTROL,
appId,
Optional.of(gateway));
log.debug("Requested ICMP packet on {}", gateway);
});
}
private void processIcmpPacket(PacketContext context, Ethernet ethernet) {
IPv4 ipPacket = (IPv4) ethernet.getPayload();
log.trace("Processing ICMP packet from ip {}, mac {}",
Ip4Address.valueOf(ipPacket.getSourceAddress()),
ethernet.getSourceMAC());
ICMP icmp = (ICMP) ipPacket.getPayload();
short icmpId = getIcmpId(icmp);
DeviceId srcDevice = context.inPacket().receivedFrom().deviceId();
switch (icmp.getIcmpType()) {
case ICMP.TYPE_ECHO_REQUEST:
Optional<Host> reqHost = hostService.getHostsByMac(ethernet.getSourceMAC())
.stream().findFirst();
if (!reqHost.isPresent()) {
log.warn("No host found for MAC {}", ethernet.getSourceMAC());
return;
}
// TODO Considers icmp between internal subnets belong to the same router.
// TODO do we have to support ICMP reply for non-existing gateway?
Ip4Address gatewayIp = Ip4Address.valueOf(
reqHost.get().annotations().value(Constants.GATEWAY_IP));
if (Objects.equals(ipPacket.getDestinationAddress(), gatewayIp.toInt())) {
processRequestToGateway(ipPacket, reqHost.get());
} else {
Optional<Ip4Address> srcNatIp = getSrcNatIp(reqHost.get());
if (!srcNatIp.isPresent()) {
log.trace("VM {} has no external connection", reqHost.get());
return;
}
sendRequestToExternal(ipPacket, srcDevice, srcNatIp.get());
String icmpInfoKey = String.valueOf(icmpId)
.concat(String.valueOf(srcNatIp.get().toInt()))
.concat(String.valueOf(ipPacket.getDestinationAddress()));
icmpInfoMap.putIfAbsent(icmpInfoKey, reqHost.get());
}
break;
case ICMP.TYPE_ECHO_REPLY:
String icmpInfoKey = String.valueOf(icmpId)
.concat(String.valueOf(ipPacket.getDestinationAddress()))
.concat(String.valueOf(ipPacket.getSourceAddress()));
processReplyFromExternal(ipPacket, icmpInfoMap.get(icmpInfoKey));
icmpInfoMap.remove(icmpInfoKey);
break;
default:
break;
}
}
// TODO do we have to handle the request to the fake gateway?
private void processRequestToGateway(IPv4 ipPacket, Host reqHost) {
ICMP icmpReq = (ICMP) ipPacket.getPayload();
icmpReq.setChecksum((short) 0);
icmpReq.setIcmpType(ICMP.TYPE_ECHO_REPLY).resetChecksum();
int destinationAddress = ipPacket.getSourceAddress();
ipPacket.setSourceAddress(ipPacket.getDestinationAddress())
.setDestinationAddress(destinationAddress)
.resetChecksum();
ipPacket.setPayload(icmpReq);
Ethernet icmpReply = new Ethernet();
icmpReply.setEtherType(Ethernet.TYPE_IPV4)
.setSourceMACAddress(Constants.DEFAULT_GATEWAY_MAC)
.setDestinationMACAddress(reqHost.mac())
.setPayload(ipPacket);
sendReply(icmpReply, reqHost);
}
private void sendRequestToExternal(IPv4 ipPacket, DeviceId srcDevice, Ip4Address srcNatIp) {
ICMP icmpReq = (ICMP) ipPacket.getPayload();
icmpReq.resetChecksum();
ipPacket.setSourceAddress(srcNatIp.toInt()).resetChecksum();
ipPacket.setPayload(icmpReq);
Ethernet icmpRequestEth = new Ethernet();
icmpRequestEth.setEtherType(Ethernet.TYPE_IPV4)
.setSourceMACAddress(DEFAULT_GATEWAY_MAC)
.setDestinationMACAddress(DEFAULT_EXTERNAL_ROUTER_MAC)
.setPayload(ipPacket);
TrafficTreatment treatment = DefaultTrafficTreatment.builder()
.setOutput(gatewayService.getUplinkPort(srcDevice))
.build();
OutboundPacket packet = new DefaultOutboundPacket(
srcDevice,
treatment,
ByteBuffer.wrap(icmpRequestEth.serialize()));
packetService.emit(packet);
}
private void processReplyFromExternal(IPv4 ipPacket, Host dstHost) {
ICMP icmpReply = (ICMP) ipPacket.getPayload();
icmpReply.resetChecksum();
Ip4Address ipAddress = dstHost.ipAddresses().stream().findFirst().get().getIp4Address();
ipPacket.setDestinationAddress(ipAddress.toInt())
.resetChecksum();
ipPacket.setPayload(icmpReply);
Ethernet icmpResponseEth = new Ethernet();
icmpResponseEth.setEtherType(Ethernet.TYPE_IPV4)
.setSourceMACAddress(Constants.DEFAULT_GATEWAY_MAC)
.setDestinationMACAddress(dstHost.mac())
.setPayload(ipPacket);
sendReply(icmpResponseEth, dstHost);
}
private void sendReply(Ethernet icmpReply, Host dstHost) {
TrafficTreatment treatment = DefaultTrafficTreatment.builder()
.setOutput(dstHost.location().port())
.build();
OutboundPacket packet = new DefaultOutboundPacket(
dstHost.location().deviceId(),
treatment,
ByteBuffer.wrap(icmpReply.serialize()));
packetService.emit(packet);
}
private Optional<Ip4Address> getSrcNatIp(Host host) {
// TODO cache external gateway IP for each network because
// asking Neutron for every ICMP request is a bad idea
Optional<OpenstackPort> osPort = openstackService.ports().stream()
.filter(port -> port.deviceOwner().equals(DEVICE_OWNER_ROUTER_INTERFACE) &&
Objects.equals(host.annotations().value(NETWORK_ID),
port.networkId()))
.findAny();
if (!osPort.isPresent()) {
return Optional.empty();
}
OpenstackRouter osRouter = openstackService.router(osPort.get().deviceId());
if (osRouter == null) {
return Optional.empty();
}
return osRouter.gatewayExternalInfo().externalFixedIps()
.values().stream().findAny();
}
private short getIcmpId(ICMP icmp) {
return ByteBuffer.wrap(icmp.serialize(), 4, 2).getShort();
}
private class InternalPacketProcessor implements PacketProcessor {
@Override
public void process(PacketContext context) {
if (context.isHandled()) {
return;
} else if (!gatewayService.getGatewayDeviceIds().contains(
context.inPacket().receivedFrom().deviceId())) {
// return if the packet is not from gateway nodes
return;
}
InboundPacket pkt = context.inPacket();
Ethernet ethernet = pkt.parsed();
if (ethernet == null || ethernet.getEtherType() == Ethernet.TYPE_ARP) {
return;
}
IPv4 iPacket = (IPv4) ethernet.getPayload();
if (iPacket.getProtocol() == IPv4.PROTOCOL_ICMP) {
eventExecutor.execute(() -> processIcmpPacket(context, ethernet));
}
}
}
private class InternalNodeListener implements OpenstackNodeListener {
@Override
public void event(OpenstackNodeEvent event) {
OpenstackNode node = event.node();
switch (event.type()) {
case COMPLETE:
if (node.type() == GATEWAY) {
log.info("GATEWAY node {} detected", node.hostname());
eventExecutor.execute(() -> {
GatewayNode gnode = GatewayNode.builder()
.gatewayDeviceId(node.intBridge())
.dataIpAddress(node.dataIp().getIp4Address())
.uplinkIntf(node.externalPortName().get())
.build();
gatewayService.addGatewayNode(gnode);
requestPacket(appId);
});
}
break;
case INIT:
case DEVICE_CREATED:
case INCOMPLETE:
default:
break;
}
}
}
}