blob: 92358c414e41fed5092a9910e395ebfad2957e83 [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.openflow.controller.impl;
import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Sets;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.epoll.EpollEventLoopGroup;
import io.netty.channel.epoll.EpollServerSocketChannel;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.util.concurrent.GlobalEventExecutor;
import org.onlab.util.ItemNotFoundException;
import org.onosproject.net.DeviceId;
import org.onosproject.net.config.NetworkConfigRegistry;
import org.onosproject.net.driver.DefaultDriverData;
import org.onosproject.net.driver.DefaultDriverHandler;
import org.onosproject.net.driver.Driver;
import org.onosproject.net.driver.DriverService;
import org.onosproject.openflow.config.OpenFlowDeviceConfig;
import org.onosproject.openflow.controller.Dpid;
import org.onosproject.openflow.controller.driver.OpenFlowAgent;
import org.onosproject.openflow.controller.driver.OpenFlowSwitchDriver;
import org.projectfloodlight.openflow.protocol.OFDescStatsReply;
import org.projectfloodlight.openflow.protocol.OFVersion;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.util.Collection;
import java.util.Collections;
import java.util.Dictionary;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.onlab.util.Tools.get;
import static org.onlab.util.Tools.groupedThreads;
import static org.onosproject.net.DeviceId.deviceId;
import static org.onosproject.openflow.controller.Dpid.uri;
/**
* The main controller class. Handles all setup and network listeners
*/
public class Controller {
private static final Logger log = LoggerFactory.getLogger(Controller.class);
private static final short MIN_KS_LENGTH = 6;
protected HashMap<String, String> controllerNodeIPsCache;
private ChannelGroup cg;
// Configuration options
protected List<Integer> openFlowPorts = ImmutableList.of(6633, 6653);
protected int workerThreads = 0;
// Start time of the controller
protected long systemStartTime;
private OpenFlowAgent agent;
private EventLoopGroup bossGroup;
private EventLoopGroup workerGroup;
enum TlsMode {
DISABLED, // TLS is not used for OpenFlow connections
ENABLED, // Clients are required use TLS and present a client certificate
STRICT, // Clients must use TLS, and certificate must match the one specified in netcfg
}
private static final EnumSet<TlsMode> TLS_ENABLED = EnumSet.of(TlsMode.ENABLED, TlsMode.STRICT);
protected TlsParams tlsParams;
protected SSLContext sslContext;
protected KeyStore keyStore;
// Perf. related configuration
protected static final int SEND_BUFFER_SIZE = 4 * 1024 * 1024;
private DriverService driverService;
private NetworkConfigRegistry netCfgService;
// **************
// Initialization
// **************
private void addListeningPorts(Collection<Integer> ports) {
if (cg == null) {
return;
}
final ServerBootstrap bootstrap = createServerBootStrap();
bootstrap.option(ChannelOption.SO_REUSEADDR, true);
bootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);
bootstrap.childOption(ChannelOption.TCP_NODELAY, true);
bootstrap.childOption(ChannelOption.SO_SNDBUF, Controller.SEND_BUFFER_SIZE);
// bootstrap.childOption(ChannelOption.WRITE_BUFFER_WATER_MARK,
// new WriteBufferWaterMark(8 * 1024, 32 * 1024));
bootstrap.childHandler(new OFChannelInitializer(this, null, sslContext));
Set<Integer> existingPorts = cg.stream()
.map(Channel::localAddress)
.filter(InetSocketAddress.class::isInstance)
.map(InetSocketAddress.class::cast)
.map(InetSocketAddress::getPort)
.collect(Collectors.toSet());
ports.removeAll(existingPorts);
ports.forEach(port -> {
// TODO revisit if this is best way to listen to multiple ports
cg.add(bootstrap.bind(port).syncUninterruptibly().channel());
log.info("Listening for OF switch connections on {}", port);
});
}
private void removeListeningPorts(Collection<Integer> ports) {
if (cg == null) {
return;
}
Iterator<Channel> itr = cg.iterator();
while (itr.hasNext()) {
Channel c = itr.next();
SocketAddress addr = c.localAddress();
if (addr instanceof InetSocketAddress) {
InetSocketAddress inetAddr = (InetSocketAddress) addr;
Integer port = inetAddr.getPort();
if (ports.contains(port)) {
log.info("No longer listening for OF switch connections on {}", port);
c.close();
itr.remove();
}
}
}
}
private ServerBootstrap createServerBootStrap() {
int bossThreads = Math.max(1, openFlowPorts.size());
try {
bossGroup = new EpollEventLoopGroup(bossThreads, groupedThreads("onos/of", "boss-%d", log));
workerGroup = new EpollEventLoopGroup(workerThreads, groupedThreads("onos/of", "worker-%d", log));
ServerBootstrap bs = new ServerBootstrap()
.group(bossGroup, workerGroup)
.channel(EpollServerSocketChannel.class);
log.info("Using Epoll transport");
return bs;
} catch (Throwable e) {
log.debug("Failed to initialize native (epoll) transport: {}", e.getMessage());
}
// Requires 4.1.11 or later
// try {
// bossGroup = new KQueueEventLoopGroup(bossThreads, groupedThreads("onos/of", "boss-%d", log));
// workerGroup = new KQueueEventLoopGroup(workerThreads, groupedThreads("onos/of", "worker-%d", log));
// ServerBootstrap bs = new ServerBootstrap()
// .group(bossGroup, workerGroup)
// .channel(KQueueServerSocketChannel.class);
// log.info("Using Kqueue transport");
// return bs;
// } catch (Throwable e) {
// log.debug("Failed to initialize native (kqueue) transport. ", e.getMessage());
// }
bossGroup = new NioEventLoopGroup(bossThreads, groupedThreads("onos/of", "boss-%d", log));
workerGroup = new NioEventLoopGroup(workerThreads, groupedThreads("onos/of", "worker-%d", log));
log.info("Using Nio transport");
return new ServerBootstrap()
.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class);
}
public void setConfigParams(Dictionary<?, ?> properties) {
boolean restartRequired = setOpenFlowPorts(properties);
restartRequired |= setWorkerThreads(properties);
restartRequired |= setTlsParameters(properties);
if (restartRequired) {
restart();
}
}
/**
* Gets the list of listening ports from property dict.
*
* @param properties dictionary
* @return true if restart is required
*/
private boolean setWorkerThreads(Dictionary<?, ?> properties) {
List<Integer> oldPorts = this.openFlowPorts;
String ports = get(properties, "openflowPorts");
List<Integer> newPorts = Collections.emptyList();
if (!Strings.isNullOrEmpty(ports)) {
newPorts = Stream.of(ports.split(","))
.map(s -> Integer.parseInt(s))
.collect(Collectors.toList());
}
Set<Integer> portsToAdd = Sets.newHashSet(newPorts);
portsToAdd.removeAll(oldPorts);
addListeningPorts(portsToAdd);
Set<Integer> portsToRemove = Sets.newHashSet(oldPorts);
portsToRemove.removeAll(newPorts);
removeListeningPorts(portsToRemove);
this.openFlowPorts = newPorts;
log.debug("OpenFlow ports set to {}", this.openFlowPorts);
return false; // restart is never required
}
/**
* Gets the number of worker threads from property dict.
*
* @param properties dictionary
* @return true if restart is required
*/
private boolean setOpenFlowPorts(Dictionary<?, ?> properties) {
Integer oldValue = this.workerThreads;
String threads = get(properties, "workerThreads");
if (!Strings.isNullOrEmpty(threads)) {
this.workerThreads = Integer.parseInt(threads);
}
log.debug("Number of worker threads set to {}", this.workerThreads);
return oldValue != this.workerThreads; // restart if number of threads has changed
}
private static class TlsParams {
TlsMode mode;
String ksLocation;
String tsLocation;
String ksPwd;
String tsPwd;
//TODO add the hash of the keystore file contents, so that we restart if the keystore has changed
public char[] ksPwd() {
return ksPwd.toCharArray();
}
public char[] tsPwd() {
return tsPwd.toCharArray();
}
public boolean isTlsEnabled() {
return TLS_ENABLED.contains(mode);
}
@Override
public int hashCode() {
return 1; //TODO
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj instanceof TlsParams) {
final TlsParams that = (TlsParams) obj;
if (this.getClass() != that.getClass()) {
return false;
} else if (this.mode == that.mode && this.mode == TlsMode.DISABLED) {
// All disabled objects should be equal regardless of other params
return true;
}
return this.mode == that.mode &&
Objects.equals(this.ksLocation, that.ksLocation) &&
Objects.equals(this.tsLocation, that.tsLocation) &&
Objects.equals(this.ksPwd, that.ksPwd) &&
Objects.equals(this.tsPwd, that.tsPwd);
}
return false;
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("tlsMode", mode.toString().toLowerCase())
.add("ksLocation", ksLocation)
.add("tsLocation", tsLocation)
.toString();
}
}
/**
* Gets the TLS parameters from the properties dict, but fallback to the old approach of
* system properties if the parameters are missing from the dict.
*
* @param properties dictionary
* @return true if restart is required
*/
private boolean setTlsParameters(Dictionary<?, ?> properties) {
TlsParams oldParams = this.tlsParams;
TlsParams newParams = new TlsParams();
String tlsString = get(properties, "tlsMode");
if (!Strings.isNullOrEmpty(tlsString)) {
try {
newParams.mode = TlsMode.valueOf(tlsString.toUpperCase());
} catch (IllegalArgumentException e) {
log.info("Invalid TLS mode {}. TLS is disabled.", tlsString);
newParams.mode = TlsMode.DISABLED;
}
} else {
// Fallback to system properties
// TODO this method of configuring TLS is deprecated and should be removed eventually
tlsString = System.getProperty("enableOFTLS");
newParams.mode = !Strings.isNullOrEmpty(tlsString) && Boolean.parseBoolean(tlsString) ?
TlsMode.ENABLED : TlsMode.DISABLED;
}
if (newParams.isTlsEnabled()) {
newParams.ksLocation = get(properties, "keyStore");
if (Strings.isNullOrEmpty(newParams.ksLocation)) {
// Fallback to system properties
// TODO remove this eventually
newParams.ksLocation = System.getProperty("javax.net.ssl.keyStore");
}
if (Strings.isNullOrEmpty(newParams.ksLocation)) {
newParams.mode = TlsMode.DISABLED;
}
newParams.tsLocation = get(properties, "trustStore");
if (Strings.isNullOrEmpty(newParams.tsLocation)) {
// Fallback to system properties
// TODO remove this eventually
newParams.tsLocation = System.getProperty("javax.net.ssl.trustStore");
}
if (Strings.isNullOrEmpty(newParams.tsLocation)) {
newParams.mode = TlsMode.DISABLED;
}
newParams.ksPwd = get(properties, "keyStorePassword");
if (Strings.isNullOrEmpty(newParams.ksPwd)) {
// Fallback to system properties
// TODO remove this eventually
newParams.ksPwd = System.getProperty("javax.net.ssl.keyStorePassword");
}
if (Strings.isNullOrEmpty(newParams.ksPwd) || MIN_KS_LENGTH > newParams.ksPwd.length()) {
newParams.mode = TlsMode.DISABLED;
}
newParams.tsPwd = get(properties, "trustStorePassword");
if (Strings.isNullOrEmpty(newParams.tsPwd)) {
// Fallback to system properties
// TODO remove this eventually
newParams.tsPwd = System.getProperty("javax.net.ssl.trustStorePassword");
}
if (Strings.isNullOrEmpty(newParams.tsPwd) || MIN_KS_LENGTH > newParams.tsPwd.length()) {
newParams.mode = TlsMode.DISABLED;
}
}
this.tlsParams = newParams;
log.info("OpenFlow TLS Params: {}", tlsParams);
return !Objects.equals(newParams, oldParams); // restart if TLS params change
}
/**
* Initialize internal data structures.
*/
public void init() {
// These data structures are initialized here because other
// module's startUp() might be called before ours
this.controllerNodeIPsCache = new HashMap<>();
this.systemStartTime = System.currentTimeMillis();
cg = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
if (tlsParams.isTlsEnabled()) {
initSsl();
}
}
private void initSsl() {
try {
TrustManagerFactory tmFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
KeyStore ts = KeyStore.getInstance("JKS");
ts.load(new FileInputStream(tlsParams.tsLocation), tlsParams.tsPwd());
tmFactory.init(ts);
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyStore = KeyStore.getInstance("JKS");
keyStore.load(new FileInputStream(tlsParams.ksLocation), tlsParams.ksPwd());
kmf.init(keyStore, tlsParams.ksPwd());
sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), tmFactory.getTrustManagers(), null);
} catch (NoSuchAlgorithmException | KeyStoreException | CertificateException |
IOException | KeyManagementException | UnrecoverableKeyException ex) {
log.error("SSL init failed: {}", ex.getMessage());
}
}
// **************
// Utility methods
// **************
public Map<String, Long> getMemory() {
Map<String, Long> m = new HashMap<>();
Runtime runtime = Runtime.getRuntime();
m.put("total", runtime.totalMemory());
m.put("free", runtime.freeMemory());
return m;
}
public Long getSystemUptime() {
RuntimeMXBean rb = ManagementFactory.getRuntimeMXBean();
return rb.getUptime();
}
public long getSystemStartTime() {
return (this.systemStartTime);
}
public boolean isValidCertificate(Long dpid, Certificate peerCert) {
if (!tlsParams.isTlsEnabled()) {
return true;
}
if (netCfgService == null) {
// netcfg service not available; accept any cert if not in strict mode
return tlsParams.mode == TlsMode.ENABLED;
}
DeviceId deviceId = DeviceId.deviceId(Dpid.uri(new Dpid(dpid)));
OpenFlowDeviceConfig config =
netCfgService.getConfig(deviceId, OpenFlowDeviceConfig.class);
if (config == null) {
// Config not set for device, accept any cert if not in strict mode
return tlsParams.mode == TlsMode.ENABLED;
}
Optional<String> alias = config.keyAlias();
if (!alias.isPresent()) {
// Config for device does not specify a certificate chain, accept any cert if not in strict mode
return tlsParams.mode == TlsMode.ENABLED;
}
try {
Certificate configuredCert = keyStore.getCertificate(alias.get());
//TODO there's probably a better way to compare these
return Objects.deepEquals(peerCert, configuredCert);
} catch (KeyStoreException e) {
log.info("failed to load key", e);
}
return false;
}
/**
* Forward to the driver-manager to get an IOFSwitch instance.
*
* @param dpid data path id
* @param desc switch description
* @param ofv OpenFlow version
* @return switch instance
*/
protected OpenFlowSwitchDriver getOFSwitchInstance(long dpid,
OFDescStatsReply desc,
OFVersion ofv) {
Dpid dpidObj = new Dpid(dpid);
Driver driver;
try {
driver = driverService.getDriver(DeviceId.deviceId(Dpid.uri(dpidObj)));
} catch (ItemNotFoundException e) {
driver = driverService.getDriver(desc.getMfrDesc(), desc.getHwDesc(), desc.getSwDesc());
}
if (driver == null) {
log.error("No OpenFlow driver for {} : {}", dpidObj, desc);
return null;
}
log.info("Driver '{}' assigned to device {}", driver.name(), dpidObj);
if (!driver.hasBehaviour(OpenFlowSwitchDriver.class)) {
log.error("Driver {} does not support OpenFlowSwitchDriver behaviour", driver.name());
return null;
}
DefaultDriverHandler handler =
new DefaultDriverHandler(new DefaultDriverData(driver, deviceId(uri(dpidObj))));
OpenFlowSwitchDriver ofSwitchDriver =
driver.createBehaviour(handler, OpenFlowSwitchDriver.class);
ofSwitchDriver.init(dpidObj, desc, ofv);
ofSwitchDriver.setAgent(agent);
ofSwitchDriver.setRoleHandler(new RoleManager(ofSwitchDriver));
return ofSwitchDriver;
}
@Deprecated
public void start(OpenFlowAgent ag, DriverService driverService) {
start(ag, driverService, null);
}
public void start(OpenFlowAgent ag, DriverService driverService,
NetworkConfigRegistry netCfgService) {
log.info("Starting OpenFlow IO");
this.agent = ag;
this.driverService = driverService;
this.netCfgService = netCfgService;
this.init();
this.addListeningPorts(this.openFlowPorts);
}
public void stop() {
log.info("Stopping OpenFlow IO");
if (cg != null) {
cg.close();
cg = null;
}
// Shut down all event loops to terminate all threads.
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
// Wait until all threads are terminated.
try {
bossGroup.terminationFuture().sync();
workerGroup.terminationFuture().sync();
} catch (InterruptedException e) {
log.warn("Interrupted while stopping", e);
Thread.currentThread().interrupt();
}
}
private void restart() {
// only restart if we are already running
if (cg != null) {
stop();
start(this.agent, this.driverService, this.netCfgService);
}
}
}