VbngManager.java 14.6 KB
/*
 * Copyright 2015 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.virtualbng;

import static com.google.common.base.Preconditions.checkNotNull;

import com.google.common.collect.Sets;

import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

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.apache.felix.scr.annotations.Service;
import org.onlab.packet.Ethernet;
import org.onlab.packet.IpAddress;
import org.onlab.packet.IpPrefix;
import org.onlab.packet.MacAddress;
import org.onosproject.core.ApplicationId;
import org.onosproject.core.CoreService;
import org.onosproject.net.ConnectPoint;
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.HostEvent;
import org.onosproject.net.host.HostListener;
import org.onosproject.net.host.HostService;
import org.onosproject.net.intent.IntentService;
import org.onosproject.net.intent.Key;
import org.onosproject.net.intent.PointToPointIntent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This is a virtual Broadband Network Gateway (BNG) application. It mainly
 * has 3 functions:
 * (1) assigns and replies a public IP address to a REST request with a private
 * IP address
 * (2) maintains the mapping from the private IP address to the public IP address
 * (3) installs point to point intents for the host configured with private IP
 * address to access Internet
 */
@Component(immediate = true)
@Service
public class VbngManager implements VbngService {

    private static final String APP_NAME = "org.onosproject.virtualbng";

    private final Logger log = LoggerFactory.getLogger(getClass());

    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
    protected CoreService coreService;

    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
    protected HostService hostService;

    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
    protected IntentService intentService;

    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
    protected VbngConfigurationService vbngConfigurationService;

    private ApplicationId appId;
    private Map<IpAddress, PointToPointIntent> p2pIntentsFromHost;
    private Map<IpAddress, PointToPointIntent> p2pIntentsToHost;

    // This set stores all the private IP addresses we failed to create vBNGs
    // for the first time.
    private Set<IpAddress> privateIpAddressSet;

    private HostListener hostListener;
    private IpAddress nextHopIpAddress;


    @Activate
    public void activate() {
        appId = coreService.registerApplication(APP_NAME);
        p2pIntentsFromHost = new ConcurrentHashMap<>();
        p2pIntentsToHost = new ConcurrentHashMap<>();
        privateIpAddressSet = Sets.newConcurrentHashSet();

        nextHopIpAddress = vbngConfigurationService.getNextHopIpAddress();
        hostListener = new InternalHostListener();
        hostService.addListener(hostListener);

        log.info("vBNG Started");
    }

    @Deactivate
    public void deactivate() {
        hostService.removeListener(hostListener);
        log.info("vBNG Stopped");
    }

    @Override
    public IpAddress createVbng(IpAddress privateIpAddress) {

        IpAddress publicIpAddress =
                vbngConfigurationService.getAvailablePublicIpAddress(
                                                     privateIpAddress);
        if (publicIpAddress == null) {
            log.info("Did not find an available public IP address to use.");
            return null;
        }
        log.info("Private IP to Public IP mapping: {} --> {}",
                 privateIpAddress, publicIpAddress);

        // Setup paths between the host configured with private IP and
        // next hop
        if (!setupForwardingPaths(privateIpAddress, publicIpAddress)) {
            privateIpAddressSet.add(privateIpAddress);
        }
        return publicIpAddress;
    }

    /**
     * Sets up forwarding paths in both two directions between host configured
     * with private IP and next hop.
     *
     * @param privateIp the private IP address of a local host
     * @param publicIp the public IP address assigned for the private IP address
     */
    private boolean setupForwardingPaths(IpAddress privateIp, IpAddress publicIp) {
        checkNotNull(privateIp);
        checkNotNull(publicIp);

        if (nextHopIpAddress == null) {
            log.warn("Did not find next hop IP address");
            return false;
        }

        // If there are already intents for private IP address in the system,
        // we will do nothing and directly return.
        if (p2pIntentsFromHost.containsKey(privateIp)
                && p2pIntentsToHost.containsKey(privateIp)) {
            return true;
        }

        Host localHost = null;
        Host nextHopHost = null;
        if (!hostService.getHostsByIp(nextHopIpAddress).isEmpty()) {
            nextHopHost = hostService.getHostsByIp(nextHopIpAddress)
                    .iterator().next();
        } else {
            hostService.startMonitoringIp(nextHopIpAddress);
            if (hostService.getHostsByIp(privateIp).isEmpty()) {
                hostService.startMonitoringIp(privateIp);
            }
            return false;
        }

        if (!hostService.getHostsByIp(privateIp).isEmpty()) {
            localHost =
                    hostService.getHostsByIp(privateIp).iterator().next();
        } else {
            hostService.startMonitoringIp(privateIp);
            return false;
        }

        ConnectPoint nextHopConnectPoint =
                new ConnectPoint(nextHopHost.location().elementId(),
                                 nextHopHost.location().port());
        ConnectPoint localHostConnectPoint =
                new ConnectPoint(localHost.location().elementId(),
                                 localHost.location().port());

        // Generate and install intent for traffic from host configured with
        // private IP
        if (!p2pIntentsFromHost.containsKey(privateIp)) {
            PointToPointIntent toNextHopIntent
                    = srcMatchIntentGenerator(privateIp,
                                              publicIp,
                                              nextHopHost.mac(),
                                              nextHopConnectPoint,
                                              localHostConnectPoint
                                              );
            p2pIntentsFromHost.put(privateIp, toNextHopIntent);
            intentService.submit(toNextHopIntent);
        }

        // Generate and install intent for traffic to host configured with
        // private IP
        if (!p2pIntentsToHost.containsKey(privateIp)) {
            PointToPointIntent toLocalHostIntent
                    = dstMatchIntentGenerator(publicIp,
                                              privateIp,
                                              localHost.mac(),
                                              localHostConnectPoint,
                                              nextHopConnectPoint);
            p2pIntentsToHost.put(nextHopIpAddress, toLocalHostIntent);
            intentService.submit(toLocalHostIntent);
        }

        return true;
    }

    /**
     * Listener for host events.
     */
    private class InternalHostListener implements HostListener {
        @Override
        public void event(HostEvent event) {
            log.debug("Received HostEvent {}", event);

            Host host = event.subject();
            if (event.type() != HostEvent.Type.HOST_ADDED) {
                return;
            }

            for (IpAddress ipAddress: host.ipAddresses()) {
                if (privateIpAddressSet.contains(ipAddress)) {
                    createVbngAgain(ipAddress);
                }

                if (nextHopIpAddress != null &&
                        ipAddress.equals(nextHopIpAddress)) {
                    Iterator<IpAddress> ipAddresses =
                            privateIpAddressSet.iterator();
                    while (ipAddresses.hasNext()) {
                        IpAddress privateIpAddress = ipAddresses.next();
                        createVbngAgain(privateIpAddress);
                    }
                }
            }
        }
    }

    /**
     * Tries to create vBNG again after receiving a host event if the IP
     * address of the host is a private IP address or the next hop IP
     * address.
     *
     * @param privateIpAddress the private IP address
     */
    private void createVbngAgain(IpAddress privateIpAddress) {
        IpAddress publicIpAddress = vbngConfigurationService
                .getAssignedPublicIpAddress(privateIpAddress);
        if (publicIpAddress == null) {
            // We only need to handle the private IP addresses for which we
            // already returned the REST replies with assigned public IP
            // addresses. If a private IP addresses does not have an assigned
            // public IP address, we should not get it an available public IP
            // address here, and we should delete it in the unhandled private
            // IP address set.
            privateIpAddressSet.remove(privateIpAddress);
            return;
        }
        if (setupForwardingPaths(privateIpAddress, publicIpAddress)) {
            // At this moment it is still possible to fail to create a vBNG,
            // because creating a vBNG needs two hosts, one is the local host
            // configured with private IP address, the other is the next hop
            // host.
            privateIpAddressSet.remove(privateIpAddress);
        }
    }

    /**
     * PointToPointIntent Generator.
     * <p>
     * The intent will match the source IP address in packet, rewrite the
     * source IP address, and rewrite the destination MAC address.
     * </p>
     *
     * @param srcIpAddress the source IP address in packet to match
     * @param newSrcIpAddress the new source IP address to set
     * @param dstMacAddress the destination MAC address to set
     * @param dstConnectPoint the egress point
     * @param srcConnectPoint the ingress point
     * @return a PointToPointIntent
     */
    private PointToPointIntent srcMatchIntentGenerator(
                                             IpAddress srcIpAddress,
                                             IpAddress newSrcIpAddress,
                                             MacAddress dstMacAddress,
                                             ConnectPoint dstConnectPoint,
                                             ConnectPoint srcConnectPoint) {
        checkNotNull(srcIpAddress);
        checkNotNull(newSrcIpAddress);
        checkNotNull(dstMacAddress);
        checkNotNull(dstConnectPoint);
        checkNotNull(srcConnectPoint);

        TrafficSelector.Builder selector = DefaultTrafficSelector.builder();
        selector.matchEthType(Ethernet.TYPE_IPV4);
        selector.matchIPSrc(IpPrefix.valueOf(srcIpAddress,
                                             IpPrefix.MAX_INET_MASK_LENGTH));

        TrafficTreatment.Builder treatment = DefaultTrafficTreatment.builder();
        treatment.setEthDst(dstMacAddress);
        treatment.setIpSrc(newSrcIpAddress);

        Key key = Key.of(srcIpAddress.toString() + "MatchSrc", appId);
        PointToPointIntent intent = PointToPointIntent.builder()
                .appId(appId)
                .key(key)
                .selector(selector.build())
                .treatment(treatment.build())
                .egressPoint(dstConnectPoint)
                .ingressPoint(srcConnectPoint)
                .build();

        log.info("Generated a PointToPointIntent for traffic from local host "
                + ": {}", intent);
        return intent;
    }

    /**
     * PointToPointIntent Generator.
     * <p>
     * The intent will match the destination IP address in packet, rewrite the
     * destination IP address, and rewrite the destination MAC address.
     * </p>
     *
     * @param dstIpAddress the destination IP address in packet to match
     * @param newDstIpAddress the new destination IP address to set
     * @param dstMacAddress the destination MAC address to set
     * @param dstConnectPoint the egress point
     * @param srcConnectPoint the ingress point
     * @return a PointToPointIntent
     */
    private PointToPointIntent dstMatchIntentGenerator(
                                                IpAddress dstIpAddress,
                                                IpAddress newDstIpAddress,
                                                MacAddress dstMacAddress,
                                                ConnectPoint dstConnectPoint,
                                                ConnectPoint srcConnectPoint) {
        checkNotNull(dstIpAddress);
        checkNotNull(newDstIpAddress);
        checkNotNull(dstMacAddress);
        checkNotNull(dstConnectPoint);
        checkNotNull(srcConnectPoint);

        TrafficSelector.Builder selector = DefaultTrafficSelector.builder();
        selector.matchEthType(Ethernet.TYPE_IPV4);
        selector.matchIPDst(IpPrefix.valueOf(dstIpAddress,
                                             IpPrefix.MAX_INET_MASK_LENGTH));

        TrafficTreatment.Builder treatment = DefaultTrafficTreatment.builder();
        treatment.setEthDst(dstMacAddress);
        treatment.setIpDst(newDstIpAddress);

        Key key = Key.of(newDstIpAddress.toString() + "MatchDst", appId);
        PointToPointIntent intent = PointToPointIntent.builder()
                .appId(appId)
                .key(key)
                .selector(selector.build())
                .treatment(treatment.build())
                .egressPoint(dstConnectPoint)
                .ingressPoint(srcConnectPoint)
                .build();
        log.info("Generated a PointToPointIntent for traffic to local host "
                + ": {}", intent);

        return intent;
    }
}