blob: 237eccee39acbcb7a51ecb890ca9d3874c8ead18 [file] [log] [blame]
/*
* Copyright 2019-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.bng.impl;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.Maps;
import org.apache.commons.lang3.tuple.ImmutableTriple;
import org.onlab.packet.Data;
import org.onlab.packet.DeserializationException;
import org.onlab.packet.Ethernet;
import org.onlab.packet.IpAddress;
import org.onlab.packet.MacAddress;
import org.onlab.packet.VlanId;
import org.onlab.util.ItemNotFoundException;
import org.onlab.util.SharedExecutors;
import org.onosproject.core.ApplicationId;
import org.onosproject.core.CoreService;
import org.onosproject.event.AbstractListenerManager;
import org.onosproject.net.ConnectPoint;
import org.onosproject.net.Port;
import org.onosproject.net.config.ConfigFactory;
import org.onosproject.net.config.NetworkConfigEvent;
import org.onosproject.net.config.NetworkConfigListener;
import org.onosproject.net.config.NetworkConfigRegistry;
import org.onosproject.net.device.DeviceService;
import org.onosproject.net.driver.DriverService;
import org.onosproject.net.flow.DefaultTrafficTreatment;
import org.onosproject.net.flow.TrafficTreatment;
import org.onosproject.net.intf.Interface;
import org.onosproject.net.intf.InterfaceService;
import org.onosproject.net.link.LinkService;
import org.onosproject.net.packet.DefaultOutboundPacket;
import org.onosproject.net.packet.OutboundPacket;
import org.onosproject.net.packet.PacketContext;
import org.onosproject.net.packet.PacketProcessor;
import org.onosproject.net.packet.PacketService;
import org.opencord.bng.BngAttachment;
import org.opencord.bng.PppoeBngAttachment;
import org.opencord.bng.PppoeBngControlHandler;
import org.opencord.bng.PppoeEvent;
import org.opencord.bng.PppoeEventListener;
import org.opencord.bng.PppoeEventSubject;
import org.opencord.bng.config.PppoeRelayConfig;
import org.opencord.bng.packets.GenericPpp;
import org.opencord.bng.packets.Ipcp;
import org.opencord.bng.packets.PppProtocolType;
import org.opencord.bng.packets.Pppoe;
import org.opencord.sadis.SadisService;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static org.onosproject.net.config.basics.SubjectFactories.APP_SUBJECT_FACTORY;
@Component(immediate = true)
public class PppoeHandlerRelay
extends AbstractListenerManager<PppoeEvent, PppoeEventListener>
implements PppoeBngControlHandler {
private static final IpAddress IP_ADDRESS_ZERO = IpAddress.valueOf(0);
private final Logger log = LoggerFactory.getLogger(getClass());
private final InternalConfigListener cfgListener = new InternalConfigListener();
@Reference(cardinality = ReferenceCardinality.MANDATORY)
protected NetworkConfigRegistry cfgService;
@Reference(cardinality = ReferenceCardinality.MANDATORY)
protected InterfaceService interfaceService;
@Reference(cardinality = ReferenceCardinality.MANDATORY)
protected CoreService coreService;
@Reference(cardinality = ReferenceCardinality.MANDATORY)
protected PacketService packetService;
@Reference(cardinality = ReferenceCardinality.MANDATORY)
protected SadisService sadisService;
@Reference(cardinality = ReferenceCardinality.MANDATORY)
protected DeviceService deviceService;
@Reference(cardinality = ReferenceCardinality.MANDATORY)
protected LinkService linkService;
@Reference(cardinality = ReferenceCardinality.MANDATORY)
protected DriverService driverService;
private ConfigFactory<ApplicationId, PppoeRelayConfig> cfgFactory = new ConfigFactory<>(
APP_SUBJECT_FACTORY,
PppoeRelayConfig.class,
PppoeRelayConfig.KEY) {
@Override
public PppoeRelayConfig createConfig() {
return new PppoeRelayConfig();
}
};
private ApplicationId appId;
private InternalPacketProcessor internalPacketProcessor;
private PppoeRelayConfig pppoeRelayConfig;
private MacAddress macPppoeServer;
/**
* Ephemeral internal map to trace the attachment information. This map is
* mainly used to modify the packet towards the PPPoE server or towards the
* attachment.
* FIXME: this map should be cleaned after some time.
* FIXME: consider the case of user that moves around
*/
private Map<MacAddress, BngAttachment> mapSrcMacToAttInfo;
/**
* Cache to cache Sadis results during PPPoE connection establishment.
*/
private final LoadingCache<ImmutableTriple<VlanId, VlanId, ConnectPoint>, ConnectPoint>
oltCpCache = CacheBuilder.newBuilder()
.expireAfterWrite(30, TimeUnit.SECONDS)
.build(new CacheLoader<>() {
@Override
public ConnectPoint load(ImmutableTriple<VlanId, VlanId, ConnectPoint> key) throws Exception {
return getOltConnectPoint(key.left, key.middle, key.right).orElseThrow();
}
});
@Activate
protected void activate() {
mapSrcMacToAttInfo = Maps.newConcurrentMap();
appId = coreService.getAppId(BngManager.BNG_APP);
cfgService.addListener(cfgListener);
cfgService.registerConfigFactory(cfgFactory);
eventDispatcher.addSink(PppoeEvent.class, listenerRegistry);
updateConfig();
internalPacketProcessor = new InternalPacketProcessor();
packetService.addProcessor(internalPacketProcessor, PacketProcessor.director(0));
log.info("PPPoE Handler Relay activated");
}
@Deactivate
protected void deactivate() {
eventDispatcher.removeSink(PppoeEvent.class);
packetService.removeProcessor(internalPacketProcessor);
cfgService.unregisterConfigFactory(cfgFactory);
oltCpCache.invalidateAll();
pppoeRelayConfig = null;
mapSrcMacToAttInfo = null;
internalPacketProcessor = null;
macPppoeServer = null;
log.info("PPPoE Handler Relay deactivated");
}
private void updateConfig() {
PppoeRelayConfig newPppoeRelayConfig = cfgService.getConfig(appId, PppoeRelayConfig.class);
log.info("{}", newPppoeRelayConfig);
if (this.pppoeRelayConfig == null &&
newPppoeRelayConfig != null &&
newPppoeRelayConfig.isValid()) {
// TODO: what happens if this is triggered in the middle of a session auth/packet relay?
this.pppoeRelayConfig = newPppoeRelayConfig;
}
}
private void processPppoePacket(PacketContext context) {
if (!isConfigured()) {
log.warn("Missing BNG PPPoE handler relay config. Abort packet processing");
return;
}
Ethernet eth = context.inPacket().parsed();
log.debug("Parsing the PPPoE header");
//FIXME: PPPoE and above headers are extracted from the ethernet
// payload. In case we want to modify the PPPoE/upper-layer header, remember to
// serialize it back on the Ethernet payload.
Pppoe pppoe = parsePppoeHeader(eth);
if (pppoe == null) {
return;
}
log.debug("Processing PPPoE header");
// Check from where the packet is received and if the interface is configured
ConnectPoint heardOn = context.inPacket().receivedFrom();
if (!heardOn.equals(pppoeRelayConfig.getPppoeServerConnectPoint()) &&
!heardOn.equals(pppoeRelayConfig.getAsgToOltConnectPoint()) &&
!interfaceService.getInterfacesByPort(heardOn).isEmpty()) {
log.info("PPPoE packet from unregistered port {}", heardOn);
return;
}
// Retrieve the MAC address of the device that intercepted the packet.
// This MAC address is the actual PPPoE server MAC address seen by the attachment
MacAddress bnguMac = interfaceService.getInterfacesByPort(heardOn).iterator().next().mac();
VlanId cTag = VlanId.vlanId(eth.getVlanID());
VlanId sTag = VlanId.vlanId(eth.getQinQVID());
// --------------------------------------- DEBUG ----------------------------------------------
if (Pppoe.isPPPoED(eth)) {
log.info("Received {} packet from {}",
pppoe.getPacketType(),
heardOn);
}
StringBuilder logPacketPppoes = new StringBuilder();
if (Pppoe.isPPPoES(eth)) {
logPacketPppoes.append("Received PPPoES ")
.append(PppProtocolType.lookup(pppoe.getPppProtocol()).type())
.append(" packet from ").append(heardOn).append(".");
}
if (logPacketPppoes.length() > 0) {
log.debug(logPacketPppoes.toString());
}
log.debug(eth.toString());
// --------------------------------------------------------------------------------------------
if (heardOn.equals(pppoeRelayConfig.getPppoeServerConnectPoint())) {
// DOWNSTREAM PACKET: from the PPPoE server to the attachment.
// Learn the MAC address of the PPPoE server
if (macPppoeServer == null) {
macPppoeServer = eth.getSourceMAC();
}
MacAddress dstMac = eth.getDestinationMAC();
log.debug("Packet to the attachment: {}", eth);
if (!mapSrcMacToAttInfo.containsKey(dstMac)) {
BngAttachment newAttInfo = PppoeBngAttachment.builder()
.withMacAddress(dstMac)
.withSTag(sTag)
.withCTag(cTag)
.withQinqTpid(eth.getQinQTPID())
.build();
mapSrcMacToAttInfo.put(dstMac, newAttInfo);
}
// Retrieve the information about the attachment from the internal MAP
BngAttachment attInfo = mapSrcMacToAttInfo.get(dstMac);
// Generate the events for this attachment
manageAttachmentStateDownstream(eth, pppoe, attInfo);
modPacketForAttachment(eth, attInfo, bnguMac);
log.debug("Packet modified as: {}", eth);
// Send out the packet towards the OLT
forwardPacket(pppoeRelayConfig.getAsgToOltConnectPoint(), eth);
} else {
// UPSTREAM DIRECTION: from the attachment to the PPPoE server
MacAddress srcMac = eth.getSourceMAC();
if (!mapSrcMacToAttInfo.containsKey(srcMac)) {
BngAttachment newAttInfo = PppoeBngAttachment.builder()
.withMacAddress(srcMac)
.withSTag(sTag)
.withCTag(cTag)
.withQinqTpid(eth.getQinQTPID())
.build();
mapSrcMacToAttInfo.put(srcMac, newAttInfo);
}
manageAttachmentStateUpstream(eth, pppoe);
modPacketForPPPoEServer(eth);
log.debug("Packet modified as: {}", eth);
// Forward packet to the PPPoE server connect point
forwardPacket(pppoeRelayConfig.getPppoeServerConnectPoint(), eth);
}
}
/**
* Generate an event related to PPPoE or IPCP state change.
*
* @param bngAppEventType Event type
* @param ip IP Address if it has been assigned, otherwise
* 0.0.0.0
* @param attInfo Local attachment information
*/
private void generateEventPppoe(PppoeEvent.EventType bngAppEventType,
BngAttachment attInfo, short pppoeSessionId,
IpAddress ip) {
// Retrive the NNI connect point
ConnectPoint oltConnectPoint;
try {
oltConnectPoint = oltCpCache.get(ImmutableTriple.of(attInfo.sTag(), attInfo.cTag(),
pppoeRelayConfig.getAsgToOltConnectPoint()));
} catch (ExecutionException e) {
// If unable to retrieve the OLT Connect Point log error and return.
// In this way we do NOT propagate the event and eventually create an
// inconsistent BNG Attachment.
log.error("Unable to retrieve the OLT Connect Point (\"NNI\" Connect Point)", e);
return;
}
log.info("Generating event of type {}", bngAppEventType);
post(new PppoeEvent(
bngAppEventType,
new PppoeEventSubject(
oltConnectPoint,
ip,
attInfo.macAddress(),
getPortNameAnnotation(oltConnectPoint),
pppoeSessionId,
attInfo.sTag(),
attInfo.cTag())
)
);
}
/**
* Generate attachment related state for the upstream direction.
*
* @param eth The ethernet packet
* @param pppoe PPPoE header
*/
private void manageAttachmentStateUpstream(Ethernet eth, Pppoe pppoe) {
PppoeEvent.EventType eventType = null;
MacAddress srcMac = eth.getSourceMAC();
VlanId cTag = VlanId.vlanId(eth.getVlanID());
VlanId sTag = VlanId.vlanId(eth.getQinQVID());
BngAttachment attInfo = mapSrcMacToAttInfo.get(srcMac);
switch (PppProtocolType.lookup(pppoe.getPppProtocol())) {
case IPCP:
// Attachment information should be already present
Ipcp ipcp = (Ipcp) pppoe.getPayload();
if (ipcp.getCode() == Ipcp.CONF_REQ) {
log.debug("IPCP configuration request from attachment");
eventType = PppoeEvent.EventType.IPCP_CONF_REQUEST;
}
break;
case NO_PROTOCOL:
if (Pppoe.isPPPoED(eth) &&
pppoe.getPacketType() == Pppoe.PppoeType.PADI) {
log.info("PADI received from attachment {}/{}. Saved in internal store",
srcMac, sTag);
eventType = PppoeEvent.EventType.SESSION_INIT;
}
break;
default:
}
if (eventType != null) {
generateEventPppoe(eventType, attInfo, pppoe.getSessionId(), IP_ADDRESS_ZERO);
}
}
private String getPortNameAnnotation(ConnectPoint oltConnectPoint) {
return deviceService.getPort(oltConnectPoint.deviceId(),
oltConnectPoint.port()).annotations().value("portName");
}
/**
* Generate attachment related state for the downstream direction.
*
* @param eth The ethernet packet
* @param pppoe PPPoE header
* @param attInfo Attachment info stored in the internal store
*/
private void manageAttachmentStateDownstream(Ethernet eth, Pppoe pppoe,
BngAttachment attInfo) {
PppoeEvent.EventType eventType = null;
IpAddress assignedIpAddress = IP_ADDRESS_ZERO;
switch (PppProtocolType.lookup(pppoe.getPppProtocol())) {
case IPCP:
Ipcp ipcp = (Ipcp) pppoe.getPayload();
if (ipcp.getCode() == Ipcp.ACK) {
log.info("Received a IPCP ACK from Server. Assigned IP Address {}",
ipcp.getIpAddress());
assignedIpAddress = ipcp.getIpAddress();
eventType = PppoeEvent.EventType.IPCP_CONF_ACK;
}
break;
case CHAP:
// Check if server has correctly authenticated the attachment
GenericPpp chap = (GenericPpp) pppoe.getPayload();
if (chap.getCode() == GenericPpp.CHAP_CODE_SUCCESS) {
log.info("CHAP authentication success: {}", attInfo.macAddress());
eventType = PppoeEvent.EventType.AUTH_SUCCESS;
}
if (chap.getCode() == GenericPpp.CHAP_CODE_FAILURE) {
log.info("CHAP authentication failed: {}", attInfo.macAddress());
eventType = PppoeEvent.EventType.AUTH_FAILURE;
}
break;
case PAP:
// Check if server has correctly authenticated the attachment
GenericPpp pap = (GenericPpp) pppoe.getPayload();
if (pap.getCode() == GenericPpp.PAP_AUTH_ACK) {
log.info("PAP authentication success: {}", attInfo.macAddress());
eventType = PppoeEvent.EventType.AUTH_SUCCESS;
}
if (pap.getCode() == GenericPpp.PAP_AUTH_NACK) {
log.info("PAP authentication failed: {}", attInfo.macAddress());
eventType = PppoeEvent.EventType.AUTH_FAILURE;
}
break;
case LCP:
GenericPpp lcp = (GenericPpp) pppoe.getPayload();
if (lcp.getCode() == GenericPpp.CODE_TERM_REQ) {
log.info("LCP Termination request from PPPoE server");
eventType = PppoeEvent.EventType.SESSION_TERMINATION;
}
break;
case NO_PROTOCOL:
if (Pppoe.isPPPoED(eth)) {
switch (pppoe.getPacketType()) {
case PADS:
// Set the current PPPoE session ID
eventType = PppoeEvent.EventType.SESSION_CONFIRMATION;
break;
case PADT:
log.info("PADT received from PPPoE server");
eventType = PppoeEvent.EventType.SESSION_TERMINATION;
break;
default:
}
}
break;
default:
}
// Generate and event if needed
if (eventType != null) {
generateEventPppoe(eventType, attInfo, pppoe.getSessionId(), assignedIpAddress);
}
}
private Pppoe parsePppoeHeader(Ethernet eth) {
try {
return Pppoe.deserializer().deserialize(((Data) eth.getPayload()).getData(),
0,
((Data) eth.getPayload()).getData().length);
} catch (DeserializationException e) {
log.error("Error parsing the PPPoE Headers, packet skipped. \n" + e.getMessage());
return null;
}
}
/**
* Apply the modification to the packet to send it to the attachment.
*
* @param eth Packet to be modified
* @param attInfo Attachment information store in the internal map
*/
private void modPacketForAttachment(Ethernet eth,
BngAttachment attInfo,
MacAddress newSourceMac) {
eth.setVlanID(attInfo.cTag().toShort());
eth.setQinQVID(attInfo.sTag().toShort());
eth.setQinQTPID(attInfo.qinqTpid());
eth.setSourceMACAddress(newSourceMac);
}
/**
* Apply the modification to the packet to send it to the PPPoE Server.
*
* @param eth Packet to be modified
*/
private void modPacketForPPPoEServer(Ethernet eth) {
Set<Interface> interfaces = interfaceService.getInterfacesByPort(pppoeRelayConfig.getPppoeServerConnectPoint());
if (interfaces != null &&
interfaces.iterator().hasNext() &&
interfaces.iterator().next().vlanTagged() != null &&
interfaces.iterator().next().vlanTagged().iterator().hasNext()) {
VlanId vlanId = interfaces.iterator().next().vlanTagged().iterator().next();
if (vlanId != null && vlanId != VlanId.NONE) {
eth.setVlanID(vlanId.toShort());
eth.setQinQVID(Ethernet.VLAN_UNTAGGED);
} else {
eth.setVlanID(Ethernet.VLAN_UNTAGGED);
eth.setQinQVID(Ethernet.VLAN_UNTAGGED);
}
} else {
eth.setVlanID(Ethernet.VLAN_UNTAGGED);
eth.setQinQVID(Ethernet.VLAN_UNTAGGED);
}
// Modify DST Mac Address with the one of the PPPoE Server
if (!eth.getDestinationMAC().isBroadcast()) {
if (macPppoeServer == null) {
log.warn("NO Mac address for PPPoE server available! Dropping packet");
return;
}
eth.setDestinationMACAddress(macPppoeServer);
}
}
/**
* Retrieve the NNI Connect Point given the S-Tag, C-Tag and the OLT facing
* ASG connect point.
*
* @param sTag The S-Tag VLAN tag
* @param cTag The C-Tag VLAN tag
* @param asgToOltConnectPoint Connect point from ASG to OLT.
* @return
*/
private Optional<ConnectPoint> getOltConnectPoint(
VlanId sTag, VlanId cTag, ConnectPoint asgToOltConnectPoint) {
// Retrieve the UNI port where this attachment is attached to. We assume
// an attachment is uniquely identified by its c-tag and s-tag in the
// scope of an OLT. In lack of a better API in SADIS, we retrieve info
// for all OLT ports and match those that have same c-tag and s-tag as
// the given attachemnt info.
var oltDeviceIds = linkService.getIngressLinks(asgToOltConnectPoint)
.stream()
.map(link -> link.src().deviceId())
.filter(deviceId -> {
try {
return driverService.getDriver(deviceId)
.name().contains("voltha");
} catch (ItemNotFoundException e) {
log.warn("Unable to find driver for {}", deviceId);
return false;
}
})
.collect(Collectors.toSet());
var oltConnectPoints = oltDeviceIds.stream()
.flatMap(deviceId -> deviceService.getPorts(deviceId).stream())
.filter(Port::isEnabled)
.filter(port -> {
var portName = port.annotations().value("portName");
// FIXME: here we support a single UNI per ONU port
if (portName == null ||
(portName.contains("-") && !portName.endsWith("-1"))) {
return false;
}
var info = sadisService.getSubscriberInfoService()
.get(portName);
return info != null &&
Objects.equals(cTag, info.cTag()) &&
Objects.equals(sTag, info.sTag());
})
.map(port -> new ConnectPoint(port.element().id(), port.number()))
.collect(Collectors.toSet());
if (oltConnectPoints.isEmpty()) {
log.error("Unable to find a connect point for attachment with S-Tag {} C-Tag {} on OLTs {}",
sTag, cTag, oltDeviceIds);
return Optional.empty();
} else if (oltConnectPoints.size() > 1) {
log.error("Multiple OLT connect points found for attachment S-Tag {} C-Tag {}," +
"aborting discovery as this is NOT supported (yet)..." +
"oltConnectPoints={}",
sTag, cTag, oltConnectPoints);
return Optional.empty();
}
return Optional.of(oltConnectPoints.iterator().next());
}
/**
* Send the specified packet, out to the specified connect point.
*
* @param toPort Output port to send the packet
* @param packet Packet to be sent
*/
private void forwardPacket(ConnectPoint toPort, Ethernet packet) {
TrafficTreatment toPortTreatment = DefaultTrafficTreatment.builder()
.setOutput(toPort.port()).build();
OutboundPacket outboundPacket = new DefaultOutboundPacket(
toPort.deviceId(), toPortTreatment, ByteBuffer.wrap(packet.serialize()));
packetService.emit(outboundPacket);
}
/**
* Check if the handler is correctly configured.
*
* @return True if it is correctly configure, False otherwise
*/
private boolean isConfigured() {
return pppoeRelayConfig != null;
}
/**
* The internal packet processor for PPPoE packets.
*/
private class InternalPacketProcessor implements PacketProcessor {
@Override
public void process(PacketContext context) {
processPacketInternal(context);
}
private void processPacketInternal(PacketContext context) {
if (context == null || context.isHandled()) {
return;
}
Ethernet eth = context.inPacket().parsed();
if (eth == null) {
return;
}
if (!Pppoe.isPPPoES(eth) && !Pppoe.isPPPoED(eth)) {
return;
}
SharedExecutors.getPoolThreadExecutor().submit(() -> {
try {
processPppoePacket(context);
} catch (Throwable e) {
log.error("Exception while processing packet", e);
}
});
}
}
/**
* Listener for network config events.
*/
private class InternalConfigListener implements NetworkConfigListener {
@Override
public void event(NetworkConfigEvent event) {
switch (event.type()) {
case CONFIG_ADDED:
log.info("CONFIG_ADDED");
event.config().ifPresent(config -> {
pppoeRelayConfig = ((PppoeRelayConfig) config);
log.info("{} added", config.getClass().getSimpleName());
});
break;
// TODO: support at least updated and removed events
case CONFIG_UPDATED:
case CONFIG_REGISTERED:
case CONFIG_UNREGISTERED:
case CONFIG_REMOVED:
default:
log.warn("Unsupported event type {}", event.type());
break;
}
}
@Override
public boolean isRelevant(NetworkConfigEvent event) {
if (event.configClass().equals(PppoeRelayConfig.class)) {
return true;
}
log.debug("Ignore irrelevant event class {}", event.configClass().getName());
return false;
}
}
}