blob: 67c0341e929fffc93345914373044b7eae90ac7a [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.onosproject.pim.impl;
import com.google.common.collect.ImmutableList;
import org.onlab.packet.Ethernet;
import org.onlab.packet.IPv4;
import org.onlab.packet.Ip4Address;
import org.onlab.packet.IpAddress;
import org.onlab.packet.IpPrefix;
import org.onlab.packet.MacAddress;
import org.onlab.packet.PIM;
import org.onlab.packet.pim.PIMAddrUnicast;
import org.onlab.packet.pim.PIMHello;
import org.onlab.packet.pim.PIMHelloOption;
import org.onlab.packet.pim.PIMJoinPrune;
import org.onlab.packet.pim.PIMJoinPruneGroup;
import org.onosproject.incubator.net.intf.Interface;
import org.onosproject.net.flow.DefaultTrafficTreatment;
import org.onosproject.net.flow.TrafficTreatment;
import org.onosproject.net.host.InterfaceIpAddress;
import org.onosproject.net.mcast.McastRoute;
import org.onosproject.net.packet.DefaultOutboundPacket;
import org.onosproject.net.packet.PacketService;
import org.slf4j.Logger;
import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.slf4j.LoggerFactory.getLogger;
/**
* PIM Interface represents an ONOS Interface with IP and MAC addresses for
* a given ConnectPoint.
*/
public final class PimInterface {
private final Logger log = getLogger(getClass());
private static final int JOIN_PERIOD = 60;
private static final double HOLD_TIME_MULTIPLIER = 3.5;
private final PacketService packetService;
private Interface onosInterface;
private final TrafficTreatment outputTreatment;
// Our hello opt holdtime
private short holdtime = PIMHelloOption.DEFAULT_HOLDTIME;
// Our hello opt prune delay
private int pruneDelay = PIMHelloOption.DEFAULT_PRUNEDELAY;
// Neighbor priority
private int priority = PIMHelloOption.DEFAULT_PRIORITY;
private final int helloInterval;
private long lastHello;
// Our current genid
private final int generationId;
// The IP address of the DR
private IpAddress drIpaddress;
// A map of all our PIM neighbors keyed on our neighbors IP address
private Map<IpAddress, PimNeighbor> pimNeighbors = new ConcurrentHashMap<>();
private Map<McastRoute, RouteData> routes = new ConcurrentHashMap<>();
/**
* Create a PIMInterface from an ONOS Interface.
*
* @param intf the ONOS Interface.
* @param holdTime hold time
* @param priority priority
* @param propagationDelay propagation delay
* @param overrideInterval override interval
* @param packetService reference to the packet service
*/
private PimInterface(Interface intf,
int helloInterval,
short holdTime,
int priority,
short propagationDelay,
short overrideInterval,
PacketService packetService) {
onosInterface = intf;
outputTreatment = createOutputTreatment();
this.helloInterval = helloInterval;
this.holdtime = holdTime;
this.packetService = packetService;
IpAddress ourIp = getIpAddress();
MacAddress mac = intf.mac();
lastHello = 0;
generationId = new Random().nextInt();
// Create a PIM Neighbor to represent ourselves for DR election.
PimNeighbor us = new PimNeighbor(ourIp, mac, holdTime, 0, priority, generationId);
pimNeighbors.put(ourIp, us);
drIpaddress = ourIp;
}
private TrafficTreatment createOutputTreatment() {
return DefaultTrafficTreatment.builder()
.setOutput(onosInterface.connectPoint().port())
.build();
}
/**
* Return the ONOS Interface.
*
* @return ONOS Interface.
*/
public Interface getInterface() {
return onosInterface;
}
/**
* Set the ONOS Interface, it will override a previous value.
*
* @param intf ONOS Interface
* @return PIM interface instance
*/
public PimInterface setInterface(Interface intf) {
onosInterface = intf;
return this;
}
/**
* Get the set of IP Addresses associated with this interface.
*
* @return a set of Ip Addresses on this interface
*/
public List<InterfaceIpAddress> getIpAddresses() {
return onosInterface.ipAddressesList();
}
/**
* Return a single "best" IP address.
*
* @return the choosen IP address or null if none
*/
public IpAddress getIpAddress() {
if (onosInterface.ipAddressesList().isEmpty()) {
return null;
}
IpAddress ipaddr = null;
for (InterfaceIpAddress ifipaddr : onosInterface.ipAddressesList()) {
ipaddr = ifipaddr.ipAddress();
break;
}
return ipaddr;
}
/**
* Get the holdtime.
*
* @return the holdtime
*/
public short getHoldtime() {
return holdtime;
}
/**
* Get the prune delay.
*
* @return The prune delay
*/
public int getPruneDelay() {
return pruneDelay;
}
/**
* Get our hello priority.
*
* @return our priority
*/
public int getPriority() {
return priority;
}
/**
* Get our generation ID.
*
* @return our generation ID
*/
public int getGenerationId() {
return generationId;
}
/**
* Gets the neighbors seen on this interface.
*
* @return PIM neighbors
*/
public Collection<PimNeighbor> getNeighbors() {
return ImmutableList.copyOf(pimNeighbors.values());
}
public Collection<McastRoute> getRoutes() {
return routes.keySet();
}
/**
* Checks whether any of our neighbors have expired, and cleans up their
* state if they have.
*/
public void checkNeighborTimeouts() {
Set<PimNeighbor> expired = pimNeighbors.values().stream()
// Don't time ourselves out!
.filter(neighbor -> !neighbor.ipAddress().equals(getIpAddress()))
.filter(neighbor -> neighbor.isExpired())
.collect(Collectors.toSet());
for (PimNeighbor neighbor : expired) {
log.info("Timing out neighbor {}", neighbor);
pimNeighbors.remove(neighbor.ipAddress(), neighbor);
}
}
/**
* Multicast a hello message out our interface. This hello message is sent
* periodically during the normal PIM Neighbor refresh time, as well as a
* result of a newly created interface.
*/
public void sendHello() {
if (lastHello + TimeUnit.SECONDS.toMillis(helloInterval) >
System.currentTimeMillis()) {
return;
}
lastHello = System.currentTimeMillis();
// Create the base PIM Packet and mark it a hello packet
PimPacket pimPacket = new PimPacket(PIM.TYPE_HELLO);
// We need to set the source MAC and IPv4 addresses
pimPacket.setSrcMacAddr(onosInterface.mac());
pimPacket.setSrcIpAddress(Ip4Address.valueOf(getIpAddress().toOctets()));
// Create the hello message with options
PIMHello hello = new PIMHello();
hello.createDefaultOptions();
hello.addOption(PIMHelloOption.createHoldTime(holdtime));
hello.addOption(PIMHelloOption.createPriority(priority));
hello.addOption(PIMHelloOption.createGenID(generationId));
// Now set the hello option payload
pimPacket.setPimPayload(hello);
packetService.emit(new DefaultOutboundPacket(
onosInterface.connectPoint().deviceId(),
outputTreatment,
ByteBuffer.wrap(pimPacket.getEthernet().serialize())));
}
/**
* Process an incoming PIM Hello message. There are a few things going on in
* this method:
* <ul>
* <li>We <em>may</em> have to create a new neighbor if one does not already exist</li>
* <li>We <em>may</em> need to re-elect a new DR if new information is received</li>
* <li>We <em>may</em> need to send an existing neighbor all joins if the genid changed</li>
* <li>We will refresh the neighbor's timestamp</li>
* </ul>
*
* @param ethPkt the Ethernet packet header
*/
public void processHello(Ethernet ethPkt) {
if (log.isTraceEnabled()) {
log.trace("Received a PIM hello packet");
}
// We'll need to save our neighbors MAC address
MacAddress nbrmac = ethPkt.getSourceMAC();
// And we'll need to save neighbors IP Address.
IPv4 iphdr = (IPv4) ethPkt.getPayload();
IpAddress srcip = IpAddress.valueOf(iphdr.getSourceAddress());
PIM pimhdr = (PIM) iphdr.getPayload();
if (pimhdr.getPimMsgType() != PIM.TYPE_HELLO) {
log.error("process Hello has received a non hello packet type: " + pimhdr.getPimMsgType());
return;
}
// get the DR values for later calculation
PimNeighbor dr = pimNeighbors.get(drIpaddress);
checkNotNull(dr);
IpAddress drip = drIpaddress;
int drpri = dr.priority();
// Assume we do not need to run a DR election
boolean reElectDr = false;
boolean genidChanged = false;
PIMHello hello = (PIMHello) pimhdr.getPayload();
// Determine if we already have a PIMNeighbor
PimNeighbor nbr = pimNeighbors.getOrDefault(srcip, null);
PimNeighbor newNbr = PimNeighbor.createPimNeighbor(srcip, nbrmac, hello.getOptions().values());
if (nbr == null) {
pimNeighbors.putIfAbsent(srcip, newNbr);
nbr = newNbr;
} else if (!nbr.equals(newNbr)) {
if (newNbr.holdtime() == 0) {
// Neighbor has shut down. Remove them and clean up
pimNeighbors.remove(srcip, nbr);
return;
} else {
// Neighbor has changed one of their options.
pimNeighbors.put(srcip, newNbr);
nbr = newNbr;
}
}
// Refresh this neighbor's timestamp
nbr.refreshTimestamp();
/*
* the election method will first determine if an election
* needs to be run, if so it will run the election. The
* IP address of the DR will be returned. If the IP address
* of the DR is different from what we already have we know a
* new DR has been elected.
*/
IpAddress electedIp = election(nbr, drip, drpri);
if (!drip.equals(electedIp)) {
// we have a new DR.
drIpaddress = electedIp;
}
}
// Run an election if we need to. Return the elected IP address.
private IpAddress election(PimNeighbor nbr, IpAddress drIp, int drPriority) {
IpAddress nbrIp = nbr.ipAddress();
if (nbr.priority() > drPriority) {
return nbrIp;
}
if (nbrIp.compareTo(drIp) > 0) {
return nbrIp;
}
return drIp;
}
/**
* Process an incoming PIM JoinPrune message.
*
* @param ethPkt the Ethernet packet header.
*/
public void processJoinPrune(Ethernet ethPkt) {
IPv4 ip = (IPv4) ethPkt.getPayload();
checkNotNull(ip);
PIM pim = (PIM) ip.getPayload();
checkNotNull(pim);
PIMJoinPrune jpHdr = (PIMJoinPrune) pim.getPayload();
checkNotNull(jpHdr);
/*
* The Join/Prune messages are grouped by Group address. We'll walk each group address
* where we will possibly have to walk a list of source address for the joins and prunes.
*/
Collection<PIMJoinPruneGroup> jpgs = jpHdr.getJoinPrunes();
for (PIMJoinPruneGroup jpg : jpgs) {
IpPrefix gpfx = jpg.getGroup();
// Walk the joins first.
for (IpPrefix spfx : jpg.getJoins().values()) {
// We may need
}
for (IpPrefix spfx : jpg.getPrunes().values()) {
// TODO: this is where we many need to remove multi-cast state and possibly intents.
}
}
}
public void addRoute(McastRoute route, IpAddress nextHop, MacAddress nextHopMac) {
RouteData data = new RouteData(nextHop, nextHopMac);
routes.put(route, data);
sendJoinPrune(route, data, true);
}
public void removeRoute(McastRoute route) {
RouteData data = routes.remove(route);
if (data != null) {
sendJoinPrune(route, data, false);
}
}
public void sendJoins() {
routes.entrySet().forEach(entry -> {
if (entry.getValue().timestamp + TimeUnit.SECONDS.toMillis(JOIN_PERIOD) >
System.currentTimeMillis()) {
return;
}
sendJoinPrune(entry.getKey(), entry.getValue(), true);
});
}
private void sendJoinPrune(McastRoute route, RouteData data, boolean join) {
PIMJoinPrune jp = new PIMJoinPrune();
jp.addJoinPrune(route.source().toIpPrefix(), route.group().toIpPrefix(), join);
jp.setHoldTime(join ? (short) Math.floor(JOIN_PERIOD * HOLD_TIME_MULTIPLIER) : 0);
jp.setUpstreamAddr(new PIMAddrUnicast(data.ipAddress.toString()));
PIM pim = new PIM();
pim.setPIMType(PIM.TYPE_JOIN_PRUNE_REQUEST);
pim.setPayload(jp);
IPv4 ipv4 = new IPv4();
ipv4.setDestinationAddress(PIM.PIM_ADDRESS.getIp4Address().toInt());
ipv4.setSourceAddress(getIpAddress().getIp4Address().toInt());
ipv4.setProtocol(IPv4.PROTOCOL_PIM);
ipv4.setTtl((byte) 1);
ipv4.setDiffServ((byte) 0xc0);
ipv4.setPayload(pim);
Ethernet eth = new Ethernet();
eth.setSourceMACAddress(onosInterface.mac());
eth.setDestinationMACAddress(MacAddress.valueOf("01:00:5E:00:00:0d"));
eth.setEtherType(Ethernet.TYPE_IPV4);
eth.setPayload(ipv4);
TrafficTreatment treatment = DefaultTrafficTreatment.builder()
.setOutput(onosInterface.connectPoint().port())
.build();
packetService.emit(new DefaultOutboundPacket(onosInterface.connectPoint().deviceId(),
treatment, ByteBuffer.wrap(eth.serialize())));
data.timestamp = System.currentTimeMillis();
}
/**
* Returns a builder for a PIM interface.
*
* @return PIM interface builder
*/
public static Builder builder() {
return new Builder();
}
/**
* Builder for a PIM interface.
*/
public static class Builder {
private Interface intf;
private PacketService packetService;
private int helloInterval = PimInterfaceManager.DEFAULT_HELLO_INTERVAL;
private short holdtime = PIMHelloOption.DEFAULT_HOLDTIME;
private int priority = PIMHelloOption.DEFAULT_PRIORITY;
private short propagationDelay = PIMHelloOption.DEFAULT_PRUNEDELAY;
private short overrideInterval = PIMHelloOption.DEFAULT_OVERRIDEINTERVAL;
/**
* Uses the specified ONOS interface.
*
* @param intf ONOS interface
* @return this PIM interface builder
*/
public Builder withInterface(Interface intf) {
this.intf = checkNotNull(intf);
return this;
}
/**
* Sets the reference to the packet service.
*
* @param packetService packet service
* @return this PIM interface builder
*/
public Builder withPacketService(PacketService packetService) {
this.packetService = checkNotNull(packetService);
return this;
}
/**
* Users the specified hello interval.
*
* @param helloInterval hello interval in seconds
* @return this PIM interface builder
*/
public Builder withHelloInterval(int helloInterval) {
this.helloInterval = helloInterval;
return this;
}
/**
* Uses the specified hold time.
*
* @param holdTime hold time in seconds
* @return this PIM interface builder
*/
public Builder withHoldTime(short holdTime) {
this.holdtime = holdTime;
return this;
}
/**
* Uses the specified DR priority.
*
* @param priority DR priority
* @return this PIM interface builder
*/
public Builder withPriority(int priority) {
this.priority = priority;
return this;
}
/**
* Uses the specified propagation delay.
*
* @param propagationDelay propagation delay in ms
* @return this PIM interface builder
*/
public Builder withPropagationDelay(short propagationDelay) {
this.propagationDelay = propagationDelay;
return this;
}
/**
* Uses the specified override interval.
*
* @param overrideInterval override interval in ms
* @return this PIM interface builder
*/
public Builder withOverrideInterval(short overrideInterval) {
this.overrideInterval = overrideInterval;
return this;
}
/**
* Builds the PIM interface.
*
* @return PIM interface
*/
public PimInterface build() {
checkArgument(intf != null, "Must provide an interface");
checkArgument(packetService != null, "Must provide a packet service");
return new PimInterface(intf, helloInterval, holdtime, priority,
propagationDelay, overrideInterval, packetService);
}
}
private static class RouteData {
final IpAddress ipAddress;
final MacAddress macAddress;
long timestamp;
public RouteData(IpAddress ip, MacAddress mac) {
this.ipAddress = ip;
this.macAddress = mac;
timestamp = System.currentTimeMillis();
}
}
}