blob: faf9aff70a857b6c30fc36fb52aae07b954f4ee9 [file] [log] [blame]
/*
* Copyright 2015-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.fnl.impl;
import org.onlab.packet.IpPrefix;
import org.onosproject.fnl.intf.NetworkAnomaly;
import org.onosproject.fnl.intf.NetworkDiagnostic;
import org.onosproject.fnl.base.TsLoopPacket;
import org.onosproject.fnl.base.TsReturn;
import org.onosproject.net.ConnectPoint;
import org.onosproject.net.Device;
import org.onosproject.net.DeviceId;
import org.onosproject.net.Link;
import org.onosproject.net.PortNumber;
import org.onosproject.net.device.DeviceService;
import org.onosproject.net.flow.FlowEntry;
import org.onosproject.net.flow.FlowRuleService;
import org.onosproject.net.flow.criteria.Criterion;
import org.onosproject.net.flow.criteria.EthTypeCriterion;
import org.onosproject.net.flow.criteria.IPCriterion;
import org.onosproject.net.flow.criteria.IPProtocolCriterion;
import org.onosproject.net.flow.criteria.PortCriterion;
import org.onosproject.net.flow.instructions.Instruction;
import org.onosproject.net.flow.instructions.Instructions.OutputInstruction;
import org.onosproject.net.host.HostService;
import org.onosproject.net.link.LinkService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Iterator;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
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.EthType.EtherType.IPV4;
import static org.onlab.packet.EthType.EtherType.VLAN;
import static org.onosproject.fnl.base.NetworkDiagnosticUtils.isDevice;
import static org.onosproject.fnl.base.NetworkDiagnosticUtils.sortCriteria;
import static org.onosproject.fnl.base.NetworkDiagnosticUtils.sortFlowTable;
import static org.onosproject.fnl.base.TsLoopPacket.matchBuilder;
import static org.onosproject.fnl.intf.NetworkDiagnostic.Type.LOOP;
import static org.onosproject.net.flow.FlowEntry.FlowEntryState.ADDED;
import static org.onosproject.net.flow.criteria.Criteria.matchInPort;
import static org.onosproject.net.flow.criteria.Criterion.Type.ETH_TYPE;
import static org.onosproject.net.flow.criteria.Criterion.Type.IN_PORT;
import static org.onosproject.net.flow.criteria.Criterion.Type.IP_PROTO;
/**
* Loop Checking Diagnostic Implementation.
*
* Strategy Pattern.
*/
public class DefaultCheckLoop implements NetworkDiagnostic {
private static final int IP_PROTO_TCP_TS = 6;
private static final int IP_PROTO_UDP_TS = 17;
private static final String E_CANNOT_HANDLE = "can not handle {} now.";
private final Logger log = LoggerFactory.getLogger(getClass());
private final DeviceService deviceService;
private final HostService hostService;
private final FlowRuleService flowRuleService;
private final LinkService linkService;
private Map<DeviceId, Device> deviceInfo;
private Map<DeviceId, Iterable<FlowEntry>> flowEntryInfo;
private Set<Device> accessDevices;
//conventionally used by tsGetEgressLinks()
private Map<DeviceId, Set<Link>> egressLinkInfo;
//Two below are hot data in checking process.
private Set<NetworkAnomaly> loops;
private Set<DeviceId> excludeDeviceId;
/**
* Creates and returns an instance of Loop Checking algorithm module.
*
* @param ds reference of DeviceService
* @param hs reference of HostService
* @param frs reference of FlowRuleService
* @param ls reference of LinkService
*/
public DefaultCheckLoop(DeviceService ds,
HostService hs,
FlowRuleService frs,
LinkService ls) {
checkNotNull(ds, "DeviceService cannot be null");
checkNotNull(hs, "HostService cannot be null");
checkNotNull(frs, "FlowRuleService cannot be null");
checkNotNull(ls, "LinkService cannot be null");
deviceService = ds;
hostService = hs;
flowRuleService = frs;
linkService = ls;
}
/**
* Checks for loops and returns any that were found.
* An empty set is returned if there are no loops.
*
* @return the set of loops; may be empty
*/
@Override
public Set<NetworkAnomaly> findAnomalies() {
return findLoop();
}
@Override
public Type type() {
return LOOP;
}
/**
* Enter of the loop checking algorithm.
*
* @return the set of loop results; empty, if there is no loop
*/
private Set<NetworkAnomaly> findLoop() {
getNetworkSnapshot();
loops = new HashSet<>();
excludeDeviceId = new HashSet<>();
for (Device device : accessDevices) {
if (excludeDeviceId.contains(device.id())) {
continue;
}
List<FlowEntry> availableFlowEntries = new ArrayList<>();
flowEntryInfo.get(device.id()).forEach(flowEntry -> {
if (flowEntry.state() == ADDED) {
availableFlowEntries.add(flowEntry);
}
});
List<FlowEntry> sortedFlowEntries = sortFlowTable(availableFlowEntries);
for (FlowEntry flow : sortedFlowEntries) {
TsLoopPacket pkt =
matchBuilder(flow.selector().criteria(), null);
pkt.pushPathFlow(flow);
List<Instruction> inst = flow.treatment().immediate();
for (Instruction instOne : inst) {
// Attention !!!
// if you would like to modify the code here,
// please MAKE VERY SURE that you are clear with
// the relationship of any invoked methods, and
// the relationship of params which are passed in and out.
processOneInstruction(instOne, device.id(),
null, pkt, true, flow);
}
}
}
// TODO - avoid two-hop LOOP
// TODO - another clean operations
return loops;
}
/**
* Iterate one by one at switch hops.
* Return whether we discover a Loop now or not.
*
* When flows form a loop,
* pkt is also a return value indicating the loop header.
*
* @param deviceId the device needed to be checked
* @param pkt virtual packet forwarded by switches
* @return true if a loop is discovered
*/
private boolean matchDeviceFlows(DeviceId deviceId, TsLoopPacket pkt) {
if (pkt.isPassedDevice(deviceId)) {
return true; // Attention: pkt should be held outside
}
List<FlowEntry> availableFlowEntries = new ArrayList<>();
flowEntryInfo.get(deviceId).forEach(flowEntry -> {
if (flowEntry.state() == ADDED) {
availableFlowEntries.add(flowEntry);
}
});
List<FlowEntry> sortedFlowEntries = sortFlowTable(availableFlowEntries);
for (FlowEntry flowEntry : sortedFlowEntries) {
TsReturn<Boolean> isBigger = new TsReturn<>();
TsLoopPacket newPkt = pkt.copyPacketMatch();
if (!matchAndAddFlowEntry(flowEntry, newPkt, isBigger)) {
continue;
}
newPkt.pushPathFlow(flowEntry);
// no need to popPathFlow(),
// because we will drop this newPkt, and copy pkt again
for (Instruction instOne : flowEntry.treatment().immediate()) {
// Attention !!!
// if you would like to modify the code here,
// please MAKE VERY SURE that you are clear with
// the relationship of any invoked methods, and
// the relationship of params which are passed in and out.
if (processOneInstruction(instOne, deviceId,
pkt, newPkt, false, null)) {
return true;
}
}
newPkt.popPathFlow();
if (!isBigger.getValue()) {
break;
}
}
return false;
}
/**
* Process one of every instructions.
*
* The isFindLoop should be true if it is invoked by findLoop method,
* and be false if it is invoked by matchDeviceFlows method.
*
* @param instOne the instruction to be processed
* @param currentDeviceId id of the device we are now in
* @param initPkt the packet before being copied
* @param matchedPkt the packet which matched the flow entry,
* to which this instruction belongs
* @param isFindLoop indicate if it is invoked by findLoop method
* @param firstEntry the flow entry from which the packet is generated
* @return true, if a loop is discovered;
* false, 1. invoked by matchDeviceFlows method, and detected no loop;
* 2. invoked by findLoop method
*/
private boolean processOneInstruction(Instruction instOne,
DeviceId currentDeviceId,
TsLoopPacket initPkt,
TsLoopPacket matchedPkt,
boolean isFindLoop,
FlowEntry firstEntry) {
if (isFindLoop) {
checkArgument(initPkt == null,
"initPkt is not null, while isFindLoop is true.");
} else {
checkArgument(firstEntry == null,
"firstEntry is not null, while isFindLoop is false.");
}
Instruction.Type type = instOne.type();
switch (type) {
case L2MODIFICATION:
//TODO - modify the L2 header of virtual packet
log.warn(E_CANNOT_HANDLE, type);
break;
case L3MODIFICATION:
//TODO - modify the L3 header of virtual packet
log.warn(E_CANNOT_HANDLE, type);
break;
case L4MODIFICATION:
//TODO - modify the L4 header of virtual packet
log.warn(E_CANNOT_HANDLE, type);
break;
case OUTPUT:
if (processOneOutputInstruction(instOne, currentDeviceId,
initPkt, matchedPkt, isFindLoop, firstEntry)) {
return true;
}
break;
default:
log.error("Can't process this type of instruction: {} now.",
instOne.type());
break;
}
return false;
}
/**
* Process one output instruction.
*
* Params are passed from processOneInstruction directly,
* and obey the same rules.
*
* @param instOne the instruction to be processed
* @param currentDeviceId id of the device we are now in
* @param initPkt the packet before being copied
* @param matchedPkt the packet which matched the flow entry,
* to which this instruction belongs
* @param isFindLoop indicate if it is invoked by findLoop method
* @param firstEntry the flow entry from which the packet is generated
* @return true, if a loop is discovered;
* false, 1. invoked by matchDeviceFlows method, and detected no loop;
* 2. invoked by findLoop method
*/
private boolean processOneOutputInstruction(Instruction instOne,
DeviceId currentDeviceId,
TsLoopPacket initPkt,
TsLoopPacket matchedPkt,
boolean isFindLoop,
FlowEntry firstEntry) {
OutputInstruction instOutput = (OutputInstruction) instOne;
PortNumber instPort = instOutput.port();
if (!instPort.isLogical()) {
// single OUTPUT - NIC or normal port
Set<Link> dstLink = tsGetEgressLinks(
new ConnectPoint(currentDeviceId, instPort));
if (!dstLink.isEmpty()) {
// TODO - now, just deal with the first destination.
// will there be more destinations?
Link dstThisLink = dstLink.iterator().next();
ConnectPoint dstPoint = dstThisLink.dst();
// check output to devices only (output to a host is normal)
if (isDevice(dstPoint)) {
PortCriterion oldInPort =
updatePktInportPerHop(matchedPkt, dstPoint);
matchedPkt.pushPathLink(dstThisLink);
TsLoopPacket newNewPkt = matchedPkt.copyPacketMatch();
boolean loopFound =
matchDeviceFlows(dstPoint.deviceId(), newNewPkt);
if (isFindLoop) {
if (loopFound) {
loops.add(newNewPkt);
updateExcludeDeviceSet(newNewPkt);
}
matchedPkt.resetLinkFlow(firstEntry);
} else {
if (loopFound) {
initPkt.handInLoopMatch(newNewPkt);
return true;
}
matchedPkt.popPathLink();
}
restorePktInportPerHop(matchedPkt, oldInPort);
}
} else {
if (!isFindLoop) {
//TODO - NEED
log.warn("no link connecting at device {}, port {}",
currentDeviceId, instPort);
}
}
} else if (instPort.equals(PortNumber.IN_PORT)) {
//TODO - in the future,
// we may need to resolve this condition 1
log.warn("can not handle {} port now.", PortNumber.IN_PORT);
} else if (instPort.equals(PortNumber.NORMAL) ||
instPort.equals(PortNumber.FLOOD) ||
instPort.equals(PortNumber.ALL)) {
//TODO - in the future,
// we may need to resolve this condition 2
log.warn("can not handle {}/{}/{} now.",
PortNumber.NORMAL, PortNumber.FLOOD, PortNumber.ALL);
}
return false;
}
private void updateExcludeDeviceSet(TsLoopPacket loopPkt) {
Iterator<Link> iter = loopPkt.getPathLink();
while (iter.hasNext()) {
excludeDeviceId.add(iter.next().src().deviceId());
}
}
private PortCriterion updatePktInportPerHop(TsLoopPacket oldPkt,
ConnectPoint dstPoint) {
PortCriterion inPort = oldPkt.getInport();
PortCriterion oldInPort =
null != inPort ? (PortCriterion) matchInPort(inPort.port()) : null;
// TODO - check - if it really copies this object
oldPkt.setHeader(matchInPort(dstPoint.port()));
return oldInPort;
}
private void restorePktInportPerHop(TsLoopPacket pkt,
PortCriterion oldInPort) {
if (oldInPort == null) {
pkt.delHeader(IN_PORT);
} else {
pkt.setHeader(oldInPort);
}
}
private void getNetworkSnapshot() {
deviceInfo = new HashMap<>();
deviceService.getDevices().forEach(d -> deviceInfo.put(d.id(), d));
flowEntryInfo = new HashMap<>();
deviceInfo.keySet().forEach(id ->
flowEntryInfo.put(id, flowRuleService.getFlowEntries(id)));
egressLinkInfo = new HashMap<>();
deviceInfo.keySet().forEach(id ->
egressLinkInfo.put(id, linkService.getDeviceEgressLinks(id)));
accessDevices = new HashSet<>();
hostService.getHosts().forEach(h ->
accessDevices.add(deviceInfo.get(h.location().deviceId())));
}
private Set<Link> tsGetEgressLinks(ConnectPoint point) {
Set<Link> portEgressLink = new HashSet<>();
DeviceId deviceId = point.deviceId();
portEgressLink.addAll(
//all egress links
egressLinkInfo.get(deviceId)
.stream()
.filter(l -> l.src().equals(point))
.collect(Collectors.toList()));
return portEgressLink;
}
private boolean matchAndAddFlowEntry(FlowEntry flowEntry,
TsLoopPacket pkt,
TsReturn<Boolean> isBigger) {
isBigger.setValue(false);
List<Criterion> criterionArray =
sortCriteria(flowEntry.selector().criteria());
for (Criterion criterion : criterionArray) {
// TODO - advance
switch (criterion.type()) {
case IN_PORT:
case ETH_SRC:
// At present, not support Ethernet mask (ONOS?)
case ETH_DST:
// At present, not support Ethernet mask (ONOS?)
case ETH_TYPE:
if (!matchAddExactly(pkt, criterion, isBigger)) {
return false;
}
break;
case VLAN_VID:
// At present, not support VLAN mask (ONOS?)
case VLAN_PCP:
if (!pkt.headerExists(ETH_TYPE) ||
!((EthTypeCriterion) pkt.getHeader(ETH_TYPE))
.ethType().equals(VLAN.ethType())) {
return false;
}
if (!matchAddExactly(pkt, criterion, isBigger)) {
return false;
}
break;
case IPV4_SRC:
case IPV4_DST:
if (!pkt.headerExists(ETH_TYPE) ||
!((EthTypeCriterion) pkt.getHeader(ETH_TYPE))
.ethType().equals(IPV4.ethType())) {
return false;
}
if (!matchAddIPV4(pkt, criterion, isBigger)) {
return false;
}
break;
case IP_PROTO:
if (!pkt.headerExists(ETH_TYPE) ||
!((EthTypeCriterion) pkt.getHeader(ETH_TYPE))
.ethType().equals(IPV4.ethType())) {
return false;
}
if (!matchAddExactly(pkt, criterion, isBigger)) {
return false;
}
break;
case IP_DSCP:
// TODO: 10/28/16 support IP_DSCP match field
break;
case IP_ECN:
// TODO: 10/28/16 support IP_DSCP match field
break;
case TCP_SRC:
case TCP_DST:
if (!pkt.headerExists(IP_PROTO) ||
IP_PROTO_TCP_TS !=
((IPProtocolCriterion)
pkt.getHeader(IP_PROTO))
.protocol()
) {
// has TCP match requirement, but can't afford TCP
return false;
}
// in this "for" loop
// avoid IP_PROTO locates after TCP_*
if (!matchAddExactly(pkt, criterion, isBigger)) {
return false;
}
break;
case UDP_SRC:
case UDP_DST:
if (!pkt.headerExists(IP_PROTO) ||
IP_PROTO_UDP_TS !=
((IPProtocolCriterion)
pkt.getHeader(IP_PROTO))
.protocol()
) {
// has UDP match requirement, but can't afford UDP
return false;
}
// in this "for" loop
// avoid IP_PROTO locates after UDP_*
if (!matchAddExactly(pkt, criterion, isBigger)) {
return false;
}
break;
default:
log.debug("{} can't be supported by OF1.0",
criterion.type());
return false;
}
}
return true;
}
private boolean matchAddExactly(TsLoopPacket pkt,
Criterion criterion,
TsReturn<Boolean> isBigger) {
if (pkt.headerExists(criterion.type())) {
if (!pkt.getHeader(criterion.type()).equals(criterion)) {
return false;
}
} else {
// TODO - check if it is IN_PORT or IN_PHY_PORT, should be strict
pkt.setHeader(criterion);
isBigger.setValue(true);
}
return true; // should put it here
}
// before invoking this, MUST insure EtherType is IPv4.
private boolean matchAddIPV4(TsLoopPacket pkt,
Criterion criterion,
TsReturn<Boolean> isBigger) {
if (pkt.headerExists(criterion.type())) {
IpPrefix ipFlow = ((IPCriterion) criterion).ip();
IpPrefix ipPkt =
((IPCriterion) pkt.getHeader(criterion.type())).ip();
// attention - the order below is important
if (ipFlow.equals(ipPkt)) {
// shoot
} else if (ipFlow.contains(ipPkt)) {
// shoot, pkt is more exact than flowEntry
} else if (ipPkt.contains(ipFlow)) {
// pkt should be changed to be more exact
pkt.setHeader(criterion);
isBigger.setValue(true);
} else {
// match fail
return false;
}
} else {
// attention the order of criteria in "for" loop
pkt.setHeader(criterion);
isBigger.setValue(true);
}
return true;
}
}