blob: 54f6987b5c11426301bdc5a5afa8af44bcb5e183 [file] [log] [blame]
/*
* Copyright 2016-present Open Networking Foundation
*
* 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.opencord.cordvtn.impl;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.primitives.Bytes;
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.DHCP;
import org.onlab.packet.Ethernet;
import org.onlab.packet.IPv4;
import org.onlab.packet.Ip4Address;
import org.onlab.packet.IpPrefix;
import org.onlab.packet.MacAddress;
import org.onlab.packet.TpPort;
import org.onlab.packet.UDP;
import org.onlab.packet.dhcp.DhcpOption;
import org.onlab.util.Tools;
import org.onosproject.cfg.ComponentConfigService;
import org.onosproject.core.ApplicationId;
import org.onosproject.core.CoreService;
import org.onosproject.net.ConnectPoint;
import org.onosproject.net.Host;
import org.onosproject.net.HostId;
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.PacketContext;
import org.onosproject.net.packet.PacketPriority;
import org.onosproject.net.packet.PacketProcessor;
import org.onosproject.net.packet.PacketService;
import org.opencord.cordvtn.api.Constants;
import org.opencord.cordvtn.api.core.Instance;
import org.opencord.cordvtn.api.core.ServiceNetworkService;
import org.opencord.cordvtn.api.net.NetworkId;
import org.opencord.cordvtn.api.net.ServiceNetwork;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Dictionary;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import static org.onlab.packet.DHCP.DHCPOptionCode.OptionCode_BroadcastAddress;
import static org.onlab.packet.DHCP.DHCPOptionCode.OptionCode_DHCPServerIp;
import static org.onlab.packet.DHCP.DHCPOptionCode.OptionCode_DomainServer;
import static org.onlab.packet.DHCP.DHCPOptionCode.OptionCode_END;
import static org.onlab.packet.DHCP.DHCPOptionCode.OptionCode_LeaseTime;
import static org.onlab.packet.DHCP.DHCPOptionCode.OptionCode_MessageType;
import static org.onlab.packet.DHCP.DHCPOptionCode.OptionCode_RouterAddress;
import static org.onlab.packet.DHCP.DHCPOptionCode.OptionCode_SubnetMask;
import static org.onlab.packet.DHCPPacketType.DHCPACK;
import static org.onlab.packet.DHCPPacketType.DHCPOFFER;
import static org.opencord.cordvtn.api.Constants.DEFAULT_GATEWAY_MAC_STR;
import static org.opencord.cordvtn.api.net.ServiceNetwork.DependencyType.BIDIRECTIONAL;
import static org.opencord.cordvtn.api.net.ServiceNetwork.NetworkType.MANAGEMENT_HOST;
import static org.opencord.cordvtn.api.net.ServiceNetwork.NetworkType.MANAGEMENT_LOCAL;
import static org.slf4j.LoggerFactory.getLogger;
/**
* Handles DHCP requests for the virtual instances.
*/
@Component(immediate = true)
public class CordVtnDhcpProxy {
protected final Logger log = getLogger(getClass());
private static final String DHCP_SERVER_MAC = "dhcpServerMac";
private static final byte DHCP_OPTION_MTU = (byte) 26;
private static final byte DHCP_OPTION_CLASSLESS_STATIC_ROUTE = (byte) 121;
private static final Ip4Address DEFAULT_DNS = Ip4Address.valueOf("8.8.8.8");
private static final Ip4Address IP_BROADCAST = Ip4Address.valueOf("255.255.255.255");
private static final byte DEFAULT_PACKET_TTL = (byte) 127;
private static final byte[] DHCP_DATA_LEASE_INFINITE =
ByteBuffer.allocate(4).putInt(-1).array();
private static final byte[] DHCP_DATA_MTU_DEFAULT =
ByteBuffer.allocate(2).putShort((short) 1450).array();
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected CoreService coreService;
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected ComponentConfigService configService;
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected PacketService packetService;
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected HostService hostService;
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected ServiceNetworkService snetService;
@Property(name = DHCP_SERVER_MAC, value = DEFAULT_GATEWAY_MAC_STR,
label = "Fake MAC address for DHCP server interface")
private String dhcpServerMac = DEFAULT_GATEWAY_MAC_STR;
private final PacketProcessor packetProcessor = new InternalPacketProcessor();
private ApplicationId appId;
@Activate
protected void activate() {
appId = coreService.registerApplication(Constants.CORDVTN_APP_ID);
configService.registerProperties(getClass());
packetService.addProcessor(packetProcessor, PacketProcessor.director(0));
requestPackets();
log.info("Started");
}
@Deactivate
protected void deactivate() {
packetService.removeProcessor(packetProcessor);
configService.unregisterProperties(getClass(), false);
cancelPackets();
log.info("Stopped");
}
@Modified
protected void modified(ComponentContext context) {
Dictionary<?, ?> properties = context.getProperties();
String updatedMac;
updatedMac = Tools.get(properties, DHCP_SERVER_MAC);
if (!Strings.isNullOrEmpty(updatedMac) && !updatedMac.equals(dhcpServerMac)) {
dhcpServerMac = updatedMac;
}
log.info("Modified");
}
private void requestPackets() {
TrafficSelector selector = DefaultTrafficSelector.builder()
.matchEthType(Ethernet.TYPE_IPV4)
.matchIPProtocol(IPv4.PROTOCOL_UDP)
.matchUdpDst(TpPort.tpPort(UDP.DHCP_SERVER_PORT))
.matchUdpSrc(TpPort.tpPort(UDP.DHCP_CLIENT_PORT))
.build();
packetService.requestPackets(selector, PacketPriority.CONTROL, appId);
}
private void cancelPackets() {
TrafficSelector selector = DefaultTrafficSelector.builder()
.matchEthType(Ethernet.TYPE_IPV4)
.matchIPProtocol(IPv4.PROTOCOL_UDP)
.matchUdpDst(TpPort.tpPort(UDP.DHCP_SERVER_PORT))
.matchUdpSrc(TpPort.tpPort(UDP.DHCP_CLIENT_PORT))
.build();
packetService.cancelPackets(selector, PacketPriority.CONTROL, appId);
}
private class InternalPacketProcessor implements PacketProcessor {
@Override
public void process(PacketContext context) {
if (context.isHandled()) {
return;
}
Ethernet ethPacket = context.inPacket().parsed();
if (ethPacket == null || ethPacket.getEtherType() != Ethernet.TYPE_IPV4) {
return;
}
IPv4 ipv4Packet = (IPv4) ethPacket.getPayload();
if (ipv4Packet.getProtocol() != IPv4.PROTOCOL_UDP) {
return;
}
UDP udpPacket = (UDP) ipv4Packet.getPayload();
if (udpPacket.getDestinationPort() != UDP.DHCP_SERVER_PORT ||
udpPacket.getSourcePort() != UDP.DHCP_CLIENT_PORT) {
return;
}
DHCP dhcpPacket = (DHCP) udpPacket.getPayload();
processDhcp(context, dhcpPacket);
}
private void processDhcp(PacketContext context, DHCP dhcpPacket) {
if (dhcpPacket == null) {
log.trace("DHCP packet without payload received, do nothing");
return;
}
DHCP.MsgType inPacketType = dhcpPacket.getPacketType();
if (inPacketType == null || dhcpPacket.getClientHardwareAddress() == null) {
log.trace("Malformed DHCP packet received, ignore it");
return;
}
MacAddress clientMac = MacAddress.valueOf(dhcpPacket.getClientHardwareAddress());
Host reqHost = hostService.getHost(HostId.hostId(clientMac));
if (reqHost == null) {
log.debug("DHCP packet from unknown host, ignore it");
return;
}
Instance reqInstance = Instance.of(reqHost);
Ethernet ethPacket = context.inPacket().parsed();
switch (inPacketType) {
case DHCPDISCOVER:
log.trace("DHCP DISCOVER received from {}", reqHost.id());
Ethernet discoverReply = buildReply(
ethPacket,
(byte) DHCPOFFER.getValue(),
reqInstance);
sendReply(context, discoverReply);
log.trace("DHCP OFFER({}) is sent to {}",
reqInstance.ipAddress(), reqHost.id());
break;
case DHCPREQUEST:
log.trace("DHCP REQUEST received from {}", reqHost.id());
Ethernet requestReply = buildReply(
ethPacket,
(byte) DHCPACK.getValue(),
reqInstance);
sendReply(context, requestReply);
log.trace("DHCP ACK({}) is sent to {}",
reqInstance.ipAddress(), reqHost.id());
break;
case DHCPRELEASE:
log.trace("DHCP RELEASE received from {}", reqHost.id());
// do nothing
break;
default:
log.warn("Unknown packet type {}", inPacketType);
break;
}
}
private Ethernet buildReply(Ethernet ethRequest, byte packetType,
Instance reqInstance) {
ServiceNetwork snet = snetService.serviceNetwork(reqInstance.netId());
Ip4Address serverIp = snet.serviceIp().getIp4Address();
Ethernet ethReply = new Ethernet();
ethReply.setSourceMACAddress(dhcpServerMac);
ethReply.setDestinationMACAddress(ethRequest.getSourceMAC());
ethReply.setEtherType(Ethernet.TYPE_IPV4);
IPv4 ipv4Request = (IPv4) ethRequest.getPayload();
IPv4 ipv4Reply = new IPv4();
ipv4Reply.setSourceAddress(serverIp.toInt());
ipv4Reply.setDestinationAddress(reqInstance.ipAddress().toInt());
ipv4Reply.setTtl(DEFAULT_PACKET_TTL);
UDP udpRequest = (UDP) ipv4Request.getPayload();
UDP udpReply = new UDP();
udpReply.setSourcePort((byte) UDP.DHCP_SERVER_PORT);
udpReply.setDestinationPort((byte) UDP.DHCP_CLIENT_PORT);
DHCP dhcpRequest = (DHCP) udpRequest.getPayload();
DHCP dhcpReply = buildDhcpReply(
dhcpRequest, packetType, reqInstance.ipAddress(), snet);
// Overwrite the DstIP and DstMac if broadcast flag is set in DHCP header.
// This Fix alligns the ONOS-VTN app with the DHCP RFC
if ((dhcpRequest.getFlags() & 0x8000) != 0) {
ipv4Reply.setDestinationAddress(IP_BROADCAST.toInt());
ethReply.setDestinationMACAddress(MacAddress.BROADCAST);
}
// End of DHCP Fix.
udpReply.setPayload(dhcpReply);
ipv4Reply.setPayload(udpReply);
ethReply.setPayload(ipv4Reply);
return ethReply;
}
private void sendReply(PacketContext context, Ethernet ethReply) {
if (ethReply == null) {
return;
}
ConnectPoint srcPoint = context.inPacket().receivedFrom();
TrafficTreatment treatment = DefaultTrafficTreatment
.builder()
.setOutput(srcPoint.port())
.build();
packetService.emit(new DefaultOutboundPacket(
srcPoint.deviceId(),
treatment,
ByteBuffer.wrap(ethReply.serialize())));
context.block();
}
private DHCP buildDhcpReply(DHCP request, byte msgType, Ip4Address yourIp,
ServiceNetwork snet) {
Ip4Address serverIp = snet.serviceIp().getIp4Address();
int subnetPrefixLen = snet.subnet().prefixLength();
DHCP dhcpReply = new DHCP();
dhcpReply.setOpCode(DHCP.OPCODE_REPLY);
dhcpReply.setHardwareType(DHCP.HWTYPE_ETHERNET);
dhcpReply.setHardwareAddressLength((byte) 6);
dhcpReply.setTransactionId(request.getTransactionId());
dhcpReply.setFlags(request.getFlags());
dhcpReply.setServerIPAddress(serverIp.toInt());
dhcpReply.setYourIPAddress(yourIp.toInt());
dhcpReply.setClientHardwareAddress(request.getClientHardwareAddress());
List<DhcpOption> options = Lists.newArrayList();
// message type
DhcpOption option = new DhcpOption();
option.setCode(OptionCode_MessageType.getValue());
option.setLength((byte) 1);
byte[] optionData = {msgType};
option.setData(optionData);
options.add(option);
// server identifier
option = new DhcpOption();
option.setCode(OptionCode_DHCPServerIp.getValue());
option.setLength((byte) 4);
option.setData(serverIp.toOctets());
options.add(option);
// lease time
option = new DhcpOption();
option.setCode(OptionCode_LeaseTime.getValue());
option.setLength((byte) 4);
option.setData(DHCP_DATA_LEASE_INFINITE);
options.add(option);
// subnet mask
Ip4Address subnetMask = Ip4Address.makeMaskPrefix(subnetPrefixLen);
option = new DhcpOption();
option.setCode(OptionCode_SubnetMask.getValue());
option.setLength((byte) 4);
option.setData(subnetMask.toOctets());
options.add(option);
// broadcast address
Ip4Address broadcast = Ip4Address.makeMaskedAddress(yourIp, subnetPrefixLen);
option = new DhcpOption();
option.setCode(OptionCode_BroadcastAddress.getValue());
option.setLength((byte) 4);
option.setData(broadcast.toOctets());
options.add(option);
// domain server
option = new DhcpOption();
option.setCode(OptionCode_DomainServer.getValue());
option.setLength((byte) 4);
option.setData(DEFAULT_DNS.toOctets());
options.add(option);
// TODO fix MTU value to be configurable
option = new DhcpOption();
option.setCode(DHCP_OPTION_MTU);
option.setLength((byte) 2);
option.setData(DHCP_DATA_MTU_DEFAULT);
options.add(option);
// router address
if (snet.type() != MANAGEMENT_LOCAL && snet.type() != MANAGEMENT_HOST) {
option = new DhcpOption();
option.setCode(OptionCode_RouterAddress.getValue());
option.setLength((byte) 4);
option.setData(serverIp.toOctets());
options.add(option);
}
// classless static routes
byte[] data = getClasslessStaticRoutesData(snet);
if (data.length >= 5) {
option = new DhcpOption();
option.setCode(DHCP_OPTION_CLASSLESS_STATIC_ROUTE);
option.setLength((byte) data.length);
option.setData(data);
options.add(option);
}
// end option
option = new DhcpOption();
option.setCode(OptionCode_END.getValue());
option.setLength((byte) 1);
options.add(option);
dhcpReply.setOptions(options);
return dhcpReply;
}
private byte[] getClasslessStaticRoutesData(ServiceNetwork snet) {
List<Byte> result = Lists.newArrayList();
List<Byte> router = Bytes.asList(snet.serviceIp().toOctets());
// static routes for the providers
Set<ServiceNetwork> providers = snet.providers().keySet().stream()
.map(provider -> snetService.serviceNetwork(provider))
.filter(Objects::nonNull)
.collect(Collectors.toSet());
providers.forEach(provider -> {
result.add((byte) provider.subnet().prefixLength());
result.addAll(getSignificantOctets(provider.subnet()));
result.addAll(router);
});
// static routes for the bidirectional subscribers
Set<ServiceNetwork> subscribers = snetService.serviceNetworks().stream()
.filter(net -> isBidirectionalProvider(net, snet.id()))
.collect(Collectors.toSet());
subscribers.forEach(subscriber -> {
result.add((byte) subscriber.subnet().prefixLength());
result.addAll(getSignificantOctets(subscriber.subnet()));
result.addAll(router);
});
return Bytes.toArray(result);
}
private boolean isBidirectionalProvider(ServiceNetwork snet, NetworkId targetNetId) {
return snet.providers().entrySet().stream()
.filter(p -> Objects.equals(p.getKey(), targetNetId))
.anyMatch(p -> p.getValue() == BIDIRECTIONAL);
}
private List<Byte> getSignificantOctets(IpPrefix ipPrefix) {
int numOfOctets = ipPrefix.prefixLength() / 8;
if (ipPrefix.prefixLength() % 8 != 0) {
numOfOctets += 1;
}
byte[] result = Arrays.copyOfRange(ipPrefix.address().toOctets(), 0, numOfOctets);
return Bytes.asList(result);
}
}
}