diff --git a/apps/artemis/BUCK b/apps/artemis/BUCK new file mode 100755 index 0000000000..ebde4fc89a --- /dev/null +++ b/apps/artemis/BUCK @@ -0,0 +1,79 @@ +COMPILE_DEPS = [ + '//lib:CORE_DEPS', + '//lib:org.apache.karaf.shell.console', + '//cli:onos-cli', + '//apps/routing-api:onos-apps-routing-api', + '//apps/routing/common:onos-apps-routing-common', + '//lib:okhttp', + '//lib:okio', + ':commons-net', + ':io.socket-client', + ':json', + ':engine.io-client' + ] + +BUNDLES = [ + '//apps/artemis:onos-apps-artemis', + '//apps/routing-api:onos-apps-routing-api', + '//apps/routing/common:onos-apps-routing-common', +] + +EXCLUDED_BUNDLES = [ + '//lib:okhttp', + '//lib:okio', + ':commons-net', + ':io.socket-client', + ':json', + ':engine.io-client' +] + +osgi_jar ( + deps = COMPILE_DEPS, +) + +onos_app ( + app_name = 'org.onosproject.artemis', + title = 'Artemis', + category = 'Monitoring', + url = 'http://onosproject.org', + description = 'Artemis', + included_bundles = BUNDLES, + excluded_bundles = EXCLUDED_BUNDLES, + required_apps = [ 'org.onosproject.sdnip' ], +) + +remote_jar ( + name = 'commons-net', + out = 'commons-net-3.5.jar', + url = 'mvn:commons-net:commons-net:jar:3.5', + sha1 = '342fc284019f590e1308056990fdb24a08f06318', + maven_coords = 'commons-net:commons-net:3.5', + visibility = [ 'PUBLIC' ], +) + +remote_jar ( + name = 'io.socket-client', + out = 'socket.io-client-0.8.3.jar', + url = 'mvn:io.socket:socket.io-client:jar:0.8.3', + sha1 = 'b30500232ff0668a47c9f91f02e6935457a52fb5', + maven_coords = 'io.socket:socket.io-client:jar:NON-OSGI:0.8.3', + visibility = [ 'PUBLIC' ], +) + +remote_jar ( + name = 'json', + out = 'json-20090211.jar', + url = 'mvn:org.json:json:jar:20090211', + sha1 = 'c183aa3a2a6250293808bba12262c8920ce5a51c', + maven_coords = 'org.json:json:jar:NON-OSGI:20090211', + visibility = [ 'PUBLIC' ], +) + +remote_jar ( + name = 'engine.io-client', + out = 'engine.io-client-0.8.3.jar', + url = 'mvn:io.socket:engine.io-client:jar:0.8.3', + sha1 = '854b49396e1e9f9bb0ab025062ddb49c4ed65ca1', + maven_coords = 'io.socket:engine.io-client:jar:NON-OSGI:0.8.3', + visibility = [ 'PUBLIC' ], +) \ No newline at end of file diff --git a/apps/artemis/src/main/java/org/onosproject/artemis/cli/LogOptionsCommand.java b/apps/artemis/src/main/java/org/onosproject/artemis/cli/LogOptionsCommand.java new file mode 100644 index 0000000000..1812e913bf --- /dev/null +++ b/apps/artemis/src/main/java/org/onosproject/artemis/cli/LogOptionsCommand.java @@ -0,0 +1,48 @@ +/* + * 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.artemis.cli; + +import org.apache.karaf.shell.commands.Command; +import org.apache.karaf.shell.commands.Option; +import org.onosproject.artemis.impl.ArtemisService; +import org.onosproject.cli.AbstractShellCommand; + +/** + * CLI to enable or disable BGP Update message logging. + */ +@Command(scope = "artemis", name = "log-messages", + description = "Show RIS messages in logger.") +public class LogOptionsCommand extends AbstractShellCommand { + + @Option(name = "--enable", aliases = "-e", description = "Enable RIS message logging", + required = false, multiValued = false) + private boolean enable = false; + + @Option(name = "--disable", aliases = "-d", description = "Disable RIS message logging", + required = false, multiValued = false) + private boolean disable = false; + + @Override + protected void execute() { + ArtemisService artemisService = get(ArtemisService.class); + if (enable) { + artemisService.setLogger(true); + } else if (disable) { + artemisService.setLogger(false); + } + } +} \ No newline at end of file diff --git a/apps/artemis/src/main/java/org/onosproject/artemis/cli/package-info.java b/apps/artemis/src/main/java/org/onosproject/artemis/cli/package-info.java new file mode 100755 index 0000000000..09a3e140b6 --- /dev/null +++ b/apps/artemis/src/main/java/org/onosproject/artemis/cli/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2016-present 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. + */ + +/** + * Artemis CLI commands. + */ +package org.onosproject.artemis.cli; \ No newline at end of file diff --git a/apps/artemis/src/main/java/org/onosproject/artemis/impl/ArtemisConfig.java b/apps/artemis/src/main/java/org/onosproject/artemis/impl/ArtemisConfig.java new file mode 100644 index 0000000000..741a4fe7d9 --- /dev/null +++ b/apps/artemis/src/main/java/org/onosproject/artemis/impl/ArtemisConfig.java @@ -0,0 +1,308 @@ +/* + * Copyright 2015-present 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.artemis.impl; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import org.json.JSONArray; +import org.json.JSONException; +import org.onlab.packet.IpPrefix; +import org.onosproject.core.ApplicationId; +import org.onosproject.net.config.Config; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Artemis Configuration Class. + */ +class ArtemisConfig extends Config { + + private static final String PREFIXES = "prefixes"; + /* */ + private static final String PREFIX = "prefix"; + private static final String PATHS = "paths"; + private static final String MOAS = "moas"; + /* */ + private static final String ORIGIN = "origin"; + private static final String NEIGHBOR = "neighbor"; + private static final String ASN = "asn"; + /* */ + + private static final String MONITORS = "monitors"; + /* */ + private static final String RIPE = "ripe"; + private static final String EXABGP = "exabgp"; + /* */ + + private static final String FREQUENCY = "frequency"; + + + private final Logger log = LoggerFactory.getLogger(getClass()); + + /** + * Gets the set of monitored prefixes with the details (prefix, paths and MOAS). + * + * @return artemis class prefixes + */ + Set monitoredPrefixes() { + Set prefixes = Sets.newHashSet(); + + JsonNode prefixesNode = object.get(PREFIXES); + if (prefixesNode == null) { + log.warn("prefixes field is null!"); + return prefixes; + } + + prefixesNode.forEach(jsonNode -> { + IpPrefix prefix = IpPrefix.valueOf(jsonNode.get(PREFIX).asText()); + + Set moasNumbers = Sets.newHashSet(); + JsonNode moasNode = jsonNode.get(MOAS); + moasNode.forEach(asn -> + moasNumbers.add(asn.asInt()) + ); + + /* + "paths" : [{ + "origin" : 65004, + "neighbor" : [{ + "asn" : 65002, + "neighbor": [{ + "asn" : 65001, + }] + }] + }] + */ + + Map>> paths = Maps.newHashMap(); + JsonNode pathsNode = jsonNode.get(PATHS); + pathsNode.forEach(path -> { + addPath(paths, path); + }); + + // printPaths(paths); + + prefixes.add(new ArtemisPrefixes(prefix, moasNumbers, paths)); + }); + + return prefixes; + } + + /** + * Appends an ASN path on the ASN paths list of the Artemis application. + * + * @param paths active ASN paths list + * @param path ASN path to be added + */ + private void addPath(Map>> paths, JsonNode path) { + Integer origin = path.path(ORIGIN).asInt(); + + JsonNode firstNeighborNode = path.path(NEIGHBOR); + // Check if neighbor exists in the configuration + if (!firstNeighborNode.isMissingNode()) { + firstNeighborNode.forEach(firstNeighbor -> { + Integer firstNeighborAsn = firstNeighbor.get(ASN).asInt(); + + JsonNode secondNeighborNode = firstNeighbor.path(NEIGHBOR); + // check if second neighbor exists in configuration + if (!secondNeighborNode.isMissingNode()) { + secondNeighborNode.forEach(secondNeighbor -> { + Integer secondNeighborAsn = secondNeighbor.asInt(); + + if (paths.containsKey(origin)) { + // paths already contain origin ASN. + Map> integerSetMap = paths.get(origin); + if (integerSetMap.containsKey(firstNeighborAsn)) { + integerSetMap.get(firstNeighborAsn).add(secondNeighborAsn); + } else { + paths.get(origin).put(firstNeighborAsn, Sets.newHashSet(secondNeighborAsn)); + } + } else { + // origin ASN does not exist in Map. + Map> first2second = Maps.newHashMap(); + first2second.put(firstNeighborAsn, Sets.newHashSet(secondNeighborAsn)); + paths.put(origin, first2second); + } + }); + // else append to paths without second neighbor + } else { + if (!paths.containsKey(origin)) { + Map> first2second = Maps.newHashMap(); + first2second.put(firstNeighborAsn, Sets.newHashSet()); + paths.put(origin, first2second); + } else { + // paths already contain origin ASN. + Map> integerSetMap = paths.get(origin); + if (!integerSetMap.containsKey(firstNeighborAsn)) { + paths.get(origin).put(firstNeighborAsn, Sets.newHashSet()); + } + } + } + }); + // else append to paths only the origin + } else { + if (!paths.containsKey(origin)) { + paths.put(origin, Maps.newHashMap()); + } + } + } + + /** + * Helper function to print the loaded ASN paths. + * + * @param paths ASN paths to print + */ + private void printPaths(Map>> paths) { + log.warn("------------------------------------"); + paths.forEach((k, v) -> v.forEach((l, n) -> { + n.forEach(p -> log.warn("Origin: " + k + ", 1st: " + l + ", 2nd: " + p)); + })); + } + + /** + * Gets the frequency of the detection module in milliseconds. + * + * @return frequency (ms) + */ + int detectionFrequency() { + JsonNode thresholdNode = object.get(FREQUENCY); + int threshold = 0; + + if (thresholdNode == null) { + log.warn("threshold field is null!"); + return threshold; + } + + return thresholdNode.asInt(); + } + + /** + * Gets the active route collectors. + * + * @return map with type as a key and host as a value. + */ + Map> activeMonitors() { + Map> monitors = Maps.newHashMap(); + + JsonNode monitorsNode = object.get(MONITORS); + + JsonNode ripeNode = monitorsNode.path(RIPE); + if (!ripeNode.isMissingNode()) { + Set hosts = Sets.newHashSet(); + ripeNode.forEach(host -> hosts.add(host.asText())); + monitors.put(RIPE, hosts); + } + + JsonNode exabgpNode = monitorsNode.path(EXABGP); + if (!exabgpNode.isMissingNode()) { + Set hosts = Sets.newHashSet(); + exabgpNode.forEach(host -> hosts.add(host.asText())); + monitors.put(EXABGP, hosts); + } + + return monitors; + } + + /** + * Configuration for a specific prefix. + */ + static class ArtemisPrefixes { + private IpPrefix prefix; + private Set moas; + private Map>> paths; + + private final Logger log = LoggerFactory.getLogger(getClass()); + + ArtemisPrefixes(IpPrefix prefix, Set moas, Map>> paths) { + this.prefix = checkNotNull(prefix); + this.moas = checkNotNull(moas); + this.paths = checkNotNull(paths); + } + + protected IpPrefix prefix() { + return prefix; + } + + protected Set moas() { + return moas; + } + + protected Map>> paths() { + return paths; + } + + /** + * Given a path we check if the origin is a friendly MOAS or our ASN. + * If the origin ASN is not ours the we have a hijack of type 0. Next, in case that the first neighbor is + * not a legit neighbor from our configuration we detect a hijack of type 1 and lastly, if the second + * neighbor is not a legit neighbor we detect a type 2 hijack. + * + * @param path as-path that announces our prefix and found from monitors + * @return 0 no bgp hijack detected + * 50 friendly anycaster announcing our prefix + * 100+i BGP hijack type i (0 <= i <=2) + */ + int checkPath(JSONArray path) { + // TODO add MOAS check + ArrayList asnPath = new ArrayList<>(); + for (int i = 0; i < path.length(); i++) { + try { + asnPath.add(path.getInt(i)); + } catch (JSONException e) { + e.printStackTrace(); + } + } + // reverse the list to get path starting from origin + Collections.reverse(asnPath); + + if (asnPath.size() > 0 && !paths.containsKey(asnPath.get(0))) { + return 100; + } else if (asnPath.size() > 1 && !paths.get(asnPath.get(0)).containsKey(asnPath.get(1))) { + return 101; + } else if (asnPath.size() > 2 && !paths.get(asnPath.get(0)).get(asnPath.get(1)).contains(asnPath.get(2))) { + return 102; + } + return 0; + } + + @Override + public int hashCode() { + return Objects.hashCode(prefix); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof PrefixHandler) { + final PrefixHandler that = (PrefixHandler) obj; + return Objects.equals(this.prefix, that.getPrefix()); + } + return false; + } + } + +} diff --git a/apps/artemis/src/main/java/org/onosproject/artemis/impl/ArtemisManager.java b/apps/artemis/src/main/java/org/onosproject/artemis/impl/ArtemisManager.java new file mode 100755 index 0000000000..0bf5eb5f3d --- /dev/null +++ b/apps/artemis/src/main/java/org/onosproject/artemis/impl/ArtemisManager.java @@ -0,0 +1,178 @@ +/* + * Copyright 2016-present 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.artemis.impl; + +import com.google.common.collect.Sets; +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.onosproject.core.ApplicationId; +import org.onosproject.core.CoreService; +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.config.NetworkConfigService; +import org.onosproject.net.config.basics.SubjectFactories; +import org.onosproject.routing.bgp.BgpInfoService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.Timer; + +/** + * Artemis Component. + */ +@Component(immediate = true) +@Service +public class ArtemisManager implements ArtemisService { + private static final String ARTEMIS_APP_ID = "org.onosproject.artemis"; + private static final Class CONFIG_CLASS = ArtemisConfig.class; + + @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY) + private NetworkConfigRegistry registry; + + @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY) + private NetworkConfigService configService; + + @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY) + private CoreService coreService; + + @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY) + private BgpInfoService bgpInfoService; + + private final Logger log = LoggerFactory.getLogger(getClass()); + + private ApplicationId appId; + public static boolean logging = false; + + private Set prefixHandlers = Sets.newHashSet(); + private Deaggregator deaggr; + private Timer timer; + + private final InternalNetworkConfigListener configListener = + new InternalNetworkConfigListener(); + + private ConfigFactory artemisConfigFactory = + new ConfigFactory( + SubjectFactories.APP_SUBJECT_FACTORY, ArtemisConfig.class, "artemis") { + @Override + public ArtemisConfig createConfig() { + return new ArtemisConfig(); + } + }; + + @Activate + protected void activate() { + appId = coreService.registerApplication(ARTEMIS_APP_ID); + configService.addListener(configListener); + registry.registerConfigFactory(artemisConfigFactory); + log.info("Artemis Started"); + } + + @Deactivate + protected void deactivate() { + configService.removeListener(configListener); + registry.unregisterConfigFactory(artemisConfigFactory); + prefixHandlers.forEach(PrefixHandler::stopPrefixMonitors); + log.info("Artemis Stopped"); + } + + /** + * Helper function to start and stop monitors on configuration changes. + */ + private void setUpConfiguration() { + ArtemisConfig config = configService.getConfig(appId, CONFIG_CLASS); + + if (config == null) { + log.warn("No artemis config available!"); + return; + } + + final Set prefixes = config.monitoredPrefixes(); + final Integer frequency = config.detectionFrequency(); + final Map> monitors = config.activeMonitors(); + + Set toRemove = Sets.newHashSet(prefixHandlers); + + for (ArtemisConfig.ArtemisPrefixes curr : prefixes) { + final Optional handler = prefixHandlers + .stream() + .filter(prefixHandler -> prefixHandler.getPrefix().equals(curr.prefix())) + .findFirst(); + + if (handler.isPresent()) { + PrefixHandler oldHandler = handler.get(); + oldHandler.changeMonitors(monitors); + + // remove the ones we are going to keep from toRemove list + toRemove.remove(oldHandler); + } else { + // Add new handler + PrefixHandler newHandler = new PrefixHandler(curr.prefix(), monitors); + newHandler.startPrefixMonitors(); + prefixHandlers.add(newHandler); + } + } + + // stop and remove old monitors that do not exist on new configuration + toRemove.forEach(PrefixHandler::stopPrefixMonitors); + prefixHandlers.removeAll(toRemove); + + // new timer task with updated bgp speakers + deaggr = new Deaggregator(bgpInfoService); + deaggr.setPrefixes(prefixes); + + if (timer != null) { + timer.cancel(); + } + timer = new Timer(); + timer.scheduleAtFixedRate(deaggr, frequency, frequency); + } + + @Override + public void setLogger(boolean value) { + logging = value; + } + + private class InternalNetworkConfigListener implements NetworkConfigListener { + + @Override + public void event(NetworkConfigEvent event) { + switch (event.type()) { + case CONFIG_REGISTERED: + break; + case CONFIG_UNREGISTERED: + break; + case CONFIG_ADDED: + case CONFIG_UPDATED: + case CONFIG_REMOVED: + if (event.configClass() == CONFIG_CLASS) { + setUpConfiguration(); + } + break; + default: + break; + } + } + } +} diff --git a/apps/artemis/src/main/java/org/onosproject/artemis/impl/ArtemisService.java b/apps/artemis/src/main/java/org/onosproject/artemis/impl/ArtemisService.java new file mode 100755 index 0000000000..4fc73b45ea --- /dev/null +++ b/apps/artemis/src/main/java/org/onosproject/artemis/impl/ArtemisService.java @@ -0,0 +1,30 @@ +/* + * Copyright 2016-present 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.artemis.impl; + +/** + * Artemis Service. + */ +public interface ArtemisService { + + /** + * Set logger to print incoming packets or not. + * + * @param value true to print incoming BGP messages + */ + void setLogger(boolean value); + +} diff --git a/apps/artemis/src/main/java/org/onosproject/artemis/impl/DataHandler.java b/apps/artemis/src/main/java/org/onosproject/artemis/impl/DataHandler.java new file mode 100644 index 0000000000..c0257a6781 --- /dev/null +++ b/apps/artemis/src/main/java/org/onosproject/artemis/impl/DataHandler.java @@ -0,0 +1,134 @@ +/* + * Copyright 2016-present 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.artemis.impl; + +import org.json.JSONObject; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Helper class that handles BGP Update messages. + */ +public final class DataHandler { + private static final File DATA_FILE = new File("data.json"); + private static final File HIJACKS_FILE = new File("hijack.json"); + + private final AtomicReference> data = new AtomicReference>(); + + private static DataHandler instance = new DataHandler(); + + private DataHandler() { + data.set(new ArrayList<>()); + } + + /** + * Singleton for data handler class. + * + * @return instance of class + */ + public static synchronized DataHandler getInstance() { + if (instance == null) { + instance = new DataHandler(); + } + return instance; + } + + /** + * Atomic append a BGP update message to a list. + * + * @param obj BGP update message + */ + public synchronized void appendData(JSONObject obj) { + data.get().add(obj); + } + + /** + * Atomic read and clear a list of BGP updates. + * + * @return list of messages that received in 'threshold' period + */ + synchronized ArrayList getData() { + ArrayList tmp = (ArrayList) data.get().clone(); + data.get().clear(); + return tmp; + } + + /** + * A serializer to write incoming BGP updates and hijack attempts to json files. + */ + public static class Serializer { + private static RandomAccessFile fwData, fwHijack; + private static long lengthData, lengthHijack; + + static { + try { + if (DATA_FILE.exists()) { + fwData = new RandomAccessFile(DATA_FILE, "rw"); + } else { + fwData = new RandomAccessFile(DATA_FILE, "rw"); + fwData.writeBytes("[\n]"); + } + lengthData = fwData.length() - 1; + + if (HIJACKS_FILE.exists()) { + fwHijack = new RandomAccessFile(HIJACKS_FILE, "rw"); + } else { + fwHijack = new RandomAccessFile(HIJACKS_FILE, "rw"); + fwHijack.writeBytes("[\n]"); + } + lengthHijack = fwHijack.length() - 1; + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * Writes BGP update to json file. + * + * @param data BGP update + */ + public static synchronized void writeData(Object data) { + try { + String entry = data.toString() + ",\n]"; + fwData.seek(lengthData); + fwData.writeBytes(entry); + lengthData += entry.length() - 1; + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * Writes detected BGP hijack to json file. + * + * @param data BGP update of hijack + */ + static synchronized void writeHijack(Object data) { + try { + String entry = data.toString() + ",\n]"; + fwHijack.seek(lengthHijack); + fwHijack.writeBytes(entry); + lengthHijack += entry.length() - 1; + } catch (IOException e) { + e.printStackTrace(); + } + } + } +} diff --git a/apps/artemis/src/main/java/org/onosproject/artemis/impl/Deaggregator.java b/apps/artemis/src/main/java/org/onosproject/artemis/impl/Deaggregator.java new file mode 100644 index 0000000000..d0cb96c6bd --- /dev/null +++ b/apps/artemis/src/main/java/org/onosproject/artemis/impl/Deaggregator.java @@ -0,0 +1,124 @@ +/* + * Copyright 2016-present 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.artemis.impl; + +import com.google.common.collect.Sets; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.onlab.packet.IpAddress; +import org.onlab.packet.IpPrefix; +import org.onosproject.artemis.impl.bgpspeakers.BgpSpeakers; +import org.onosproject.artemis.impl.bgpspeakers.QuaggaBgpSpeakers; +import org.onosproject.routing.bgp.BgpInfoService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Set; + +/** + * Timertask class which detects and mitigates BGP hijacks. + */ +class Deaggregator extends java.util.TimerTask { + + private final Logger log = LoggerFactory.getLogger(getClass()); + private Set prefixes = Sets.newHashSet(); + private Set bgpSpeakers = Sets.newHashSet(); + + Deaggregator(BgpInfoService bgpInfoService) { + super(); + // deaggregator must know the type of the connected BGP speakers and the BGP info. + // for this example we only have one Quagga BGP speaker. + bgpSpeakers.add(new QuaggaBgpSpeakers(bgpInfoService)); + } + + @Override + public void run() { + ArrayList messagesArray = DataHandler.getInstance().getData(); +// log.info("Messages size: " + messagesArray.size()); + + // Example of BGP Update message: + // { + // "path":[65001, 65002, 65004], (origin being last) + // "prefix":"12.0.0.0/8", + // } + + prefixes.forEach(prefix -> { + IpPrefix monitoredPrefix = prefix.prefix(); + + // for each update message in memory check for hijack + for (JSONObject tmp : messagesArray) { + IpPrefix receivedPrefix = null; + try { + receivedPrefix = IpPrefix.valueOf(tmp.getString("prefix")); + } catch (JSONException e) { + log.warn("JSONException: " + e.getMessage()); + e.printStackTrace(); + } + if (receivedPrefix == null) { + continue; + } + + // check if the announced network address is inside our subnet + if (monitoredPrefix.contains(receivedPrefix)) { + JSONArray path = null; + try { + path = tmp.getJSONArray("path"); + } catch (JSONException e) { + log.warn("JSONException: " + e.getMessage()); + e.printStackTrace(); + } + if (path == null) { + continue; + } + + int state = prefix.checkPath(path); + if (state >= 100) { + log.warn("BGP Hijack detected of type " + + (state - 100) + "\n" + tmp.toString()); + DataHandler.Serializer.writeHijack(tmp); + // can only de-aggregate /23 subnets and higher + int cidr = receivedPrefix.prefixLength(); + if (receivedPrefix.prefixLength() < 24) { + byte[] octets = receivedPrefix.address().toOctets(); + int byteGroup = (cidr + 1) / 8, + bitPos = 8 - (cidr + 1) % 8; + + octets[byteGroup] = (byte) (octets[byteGroup] & ~(1 << bitPos)); + String low = IpPrefix.valueOf(IpAddress.Version.INET, octets, cidr + 1).toString(); + octets[byteGroup] = (byte) (octets[byteGroup] | (1 << bitPos)); + String high = IpPrefix.valueOf(IpAddress.Version.INET, octets, cidr + 1).toString(); + + String[] prefixes = {low, high}; + bgpSpeakers.forEach(bgpSpeakers -> bgpSpeakers.announceSubPrefixes(prefixes)); + } else { + log.warn("Cannot announce smaller prefix than /24"); + } + } + } + } + }); + } + + public Set getPrefixes() { + return prefixes; + } + + public void setPrefixes(Set prefixes) { + this.prefixes = prefixes; + } +} \ No newline at end of file diff --git a/apps/artemis/src/main/java/org/onosproject/artemis/impl/PrefixHandler.java b/apps/artemis/src/main/java/org/onosproject/artemis/impl/PrefixHandler.java new file mode 100644 index 0000000000..a334e4e8fe --- /dev/null +++ b/apps/artemis/src/main/java/org/onosproject/artemis/impl/PrefixHandler.java @@ -0,0 +1,136 @@ +/* + * Copyright 2016-present 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.artemis.impl; + +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import org.onlab.packet.IpPrefix; +import org.onosproject.artemis.impl.monitors.ExaBgpMonitor; +import org.onosproject.artemis.impl.monitors.Monitor; +import org.onosproject.artemis.impl.monitors.RipeMonitor; + +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * Handler of monitoring step for each different prefix. + * This class contains all the running monitors of the specified prefix. + */ +class PrefixHandler { + + private IpPrefix prefix; + private Set prefixMonitors = Sets.newHashSet(); + + /** + * Constructor that takes a CIDR-notation string and a list of monitors. + * + * @param prefix A CIDR-notation string, e.g. "192.168.0.1/24" + * @param monitors A map of strings to a set of string for monitors, e.g. "ripe", ["host1","host2",..] + */ + PrefixHandler(IpPrefix prefix, Map> monitors) { + this.prefix = prefix; + + monitors.forEach((type, values) -> { + if (type.equals(Monitor.Types.RIPE.toString())) { + values.forEach(host -> prefixMonitors.add(new RipeMonitor(prefix, host))); + } else if (type.equals(Monitor.Types.EXABGP.toString())) { + values.forEach(host -> prefixMonitors.add(new ExaBgpMonitor(prefix, host))); + } + }); + } + + /** + * Start all monitors for this prefix. + */ + void startPrefixMonitors() { + prefixMonitors.forEach(Monitor::startMonitor); + } + + /** + * Stop all monitors for this prefix. + */ + void stopPrefixMonitors() { + prefixMonitors.forEach(Monitor::stopMonitor); + } + + /** + * Return a CIDR-notation string of prefix. + * + * @return the prefix in CIDR-notation + */ + IpPrefix getPrefix() { + return prefix; + } + + /** + * Changes the monitors based on the new list given. + * + * @param newMonitors monitors to be added + */ + void changeMonitors(Map> newMonitors) { + Set newTypes = newMonitors.keySet(); + Set monToRemove = Sets.newHashSet(); + Map> monToAdd = Maps.newHashMap(newMonitors); + + prefixMonitors.forEach(monitor -> { + String oldType = monitor.getType().toString(); + if (newTypes.contains(oldType)) { + Set newHosts = newMonitors.get(oldType); + String oldHost = monitor.getHost(); + if (newHosts.contains(oldHost)) { + monToAdd.remove(oldHost, oldHost); + } else { + monToRemove.add(monitor); + } + } else { + monToRemove.add(monitor); + } + }); + + monToRemove.forEach(Monitor::stopMonitor); + prefixMonitors.removeAll(monToRemove); + + //TODO + monToAdd.forEach((type, values) -> { + if (type.equals(Monitor.Types.RIPE.toString())) { + values.forEach(host -> prefixMonitors.add(new RipeMonitor(prefix, host))); + } else if (type.equals(Monitor.Types.EXABGP.toString())) { + values.forEach(host -> prefixMonitors.add(new ExaBgpMonitor(prefix, host))); + } + }); + + startPrefixMonitors(); + } + + @Override + public int hashCode() { + return Objects.hashCode(prefix); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof PrefixHandler) { + final PrefixHandler that = (PrefixHandler) obj; + return Objects.equals(this.prefix, that.prefix); + } + return false; + } + +} diff --git a/apps/artemis/src/main/java/org/onosproject/artemis/impl/bgpspeakers/BgpSpeakers.java b/apps/artemis/src/main/java/org/onosproject/artemis/impl/bgpspeakers/BgpSpeakers.java new file mode 100644 index 0000000000..0677b08f3c --- /dev/null +++ b/apps/artemis/src/main/java/org/onosproject/artemis/impl/bgpspeakers/BgpSpeakers.java @@ -0,0 +1,42 @@ +/* + * Copyright 2016-present 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.artemis.impl.bgpspeakers; + +import org.onosproject.routing.bgp.BgpInfoService; +import org.onosproject.routing.bgp.BgpSession; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collection; + +/** + * Abstract class for all the types of BGP Speakers. + */ +public abstract class BgpSpeakers { + + final Logger log = LoggerFactory.getLogger(getClass()); + Collection bgpSessions; + + BgpSpeakers(BgpInfoService bgpInfoService) { + this.bgpSessions = bgpInfoService.getBgpSessions(); + } + + /** + * Abstract function which announces the two new subprefixes on the BGP Speaker. + * @param prefixes list of two prefixes + */ + public abstract void announceSubPrefixes(String[] prefixes); +} diff --git a/apps/artemis/src/main/java/org/onosproject/artemis/impl/bgpspeakers/QuaggaBgpSpeakers.java b/apps/artemis/src/main/java/org/onosproject/artemis/impl/bgpspeakers/QuaggaBgpSpeakers.java new file mode 100644 index 0000000000..30a0022475 --- /dev/null +++ b/apps/artemis/src/main/java/org/onosproject/artemis/impl/bgpspeakers/QuaggaBgpSpeakers.java @@ -0,0 +1,137 @@ +/* + * Copyright 2016-present 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.artemis.impl.bgpspeakers; + +import org.apache.commons.net.telnet.TelnetClient; +import org.onosproject.routing.bgp.BgpInfoService; + +import java.io.InputStream; +import java.io.PrintStream; +import java.util.Arrays; + +/** + * Quagga interface to connect and announce prefixes. + */ +public class QuaggaBgpSpeakers extends BgpSpeakers { + private static final String PASSWORD = "sdnip"; + + private TelnetClient telnet = new TelnetClient(); + private InputStream in; + private PrintStream out; + + public QuaggaBgpSpeakers(BgpInfoService bgpInfoService) { + super(bgpInfoService); + } + + @Override + public void announceSubPrefixes(String[] prefixes) { + bgpSessions.forEach((session) -> { + String peerIp = session.remoteInfo().ip4Address().toString(), + localAs = String.valueOf(session.remoteInfo().as4Number()); + assert peerIp != null; + + try { + telnet.connect(peerIp, 2605); + in = telnet.getInputStream(); + out = new PrintStream(telnet.getOutputStream()); + + readUntil("Password: "); + write(PASSWORD); + readUntil("> "); + + // we user remote AS as local because he is iBGP neighbor. + announcePrefix(prefixes, localAs); + + disconnect(); + + log.info("Announced " + prefixes[0] + " and " + prefixes[1] + " at " + peerIp); + } catch (Exception e) { + log.warn(e.getMessage()); + } + }); + } + + /** + * Read telnet buffer until a pattern is met. + * + * @param pattern string pattern to match in terminal + * @return matched string + */ + private String readUntil(String pattern) { + try { + char lastChar = pattern.charAt(pattern.length() - 1); + StringBuffer sb = new StringBuffer(); + char ch = (char) in.read(); + while (true) { + sb.append(ch); + if (ch == lastChar) { + if (sb.toString().endsWith(pattern)) { + return sb.toString(); + } + } + ch = (char) in.read(); + } + } catch (Exception e) { + log.warn(e.getMessage()); + } + return null; + } + + /** + * Write to the telnet client. + * + * @param value string to write + */ + private void write(String value) { + try { + out.println(value); + out.flush(); + } catch (Exception e) { + log.warn(e.getMessage()); + } + } + + /** + * Configure terminal and announce prefix inside the Quagga router. + * + * @param prefixes prefixes to announce + * @param localAs ASN of BGP Speaker + */ + private void announcePrefix(String[] prefixes, String localAs) { + write("en"); + readUntil("# "); + write("configure terminal"); + readUntil("(config)# "); + write("router bgp " + localAs); + readUntil("(config-router)# "); + Arrays.stream(prefixes).forEach((prefix) -> { + write("network " + prefix); + readUntil("(config-router)# "); + }); + write("end"); + } + + /** + * Disconnect from the telnet session. + */ + private void disconnect() { + try { + telnet.disconnect(); + } catch (Exception e) { + log.warn(e.getMessage()); + } + } +} diff --git a/apps/artemis/src/main/java/org/onosproject/artemis/impl/bgpspeakers/package-info.java b/apps/artemis/src/main/java/org/onosproject/artemis/impl/bgpspeakers/package-info.java new file mode 100644 index 0000000000..bd12a31f2b --- /dev/null +++ b/apps/artemis/src/main/java/org/onosproject/artemis/impl/bgpspeakers/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2016-present 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. + */ + +/** + * BGP Speakers. + */ +package org.onosproject.artemis.impl.bgpspeakers; \ No newline at end of file diff --git a/apps/artemis/src/main/java/org/onosproject/artemis/impl/monitors/ExaBgpMonitor.java b/apps/artemis/src/main/java/org/onosproject/artemis/impl/monitors/ExaBgpMonitor.java new file mode 100644 index 0000000000..fbf5d2780b --- /dev/null +++ b/apps/artemis/src/main/java/org/onosproject/artemis/impl/monitors/ExaBgpMonitor.java @@ -0,0 +1,160 @@ +/* + * Copyright 2016-present 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.artemis.impl.monitors; + +import io.socket.client.IO; +import io.socket.client.Socket; +import org.json.JSONException; +import org.json.JSONObject; +import org.onlab.packet.IpPrefix; +import org.onosproject.artemis.impl.ArtemisManager; +import org.onosproject.artemis.impl.DataHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.URISyntaxException; +import java.util.Objects; + +/** + * Implementation of ExaBGP Route Collector Monitor. + */ +public class ExaBgpMonitor extends Monitor { + private String host; + private Socket socket; + + private final Logger log = LoggerFactory.getLogger(getClass()); + + public ExaBgpMonitor(IpPrefix prefix, String host) { + super(prefix); + this.host = host; + } + + /** + * socket.io onConnect event handler. + */ + private void onConnect() { + try { + JSONObject parameters = new JSONObject(); + parameters.put("prefix", this.prefix); + + socket.emit("exa_subscribe", parameters); + } catch (JSONException e) { + e.printStackTrace(); + } + } + + private void onExaMessage(Object[] args) { + JSONObject message = (JSONObject) args[0]; + + try { + if (message.getString("type").equals("A")) { + // Write BGP message to a json database + DataHandler.Serializer.writeData(args[0]); + + if (ArtemisManager.logging) { + log.info(message.toString()); + } + + // Example of BGP Update message: + // { + // "path":[65001], + // "peer":"1.1.1.1", + // "prefix":"12.0.0.0/8", + // "host":"exabgp", <-- Can put IP here + // "type":"A", + // "timestamp":1488120484 + // } + + // We want to keep only prefix and path in memory. + message.remove("peer"); + message.remove("host"); + message.remove("type"); + message.remove("timestamp"); + + // Append synchronized message to message list in memory. + DataHandler.getInstance().appendData(message); + } + } catch (JSONException e) { + e.printStackTrace(); + } + } + + @Override + public void startMonitor() { + if (!isRunning()) { + log.info("Starting EXA monitor for " + prefix + " / " + host); + try { + this.socket = IO.socket("http://" + this.host + "/onos"); + this.socket.on(Socket.EVENT_CONNECT, args -> onConnect()); + this.socket.on(Socket.EVENT_PING, args -> socket.emit("pong")); + this.socket.on("exa_message", this::onExaMessage); + } catch (URISyntaxException e) { + e.printStackTrace(); + } + this.socket.connect(); + } + } + + @Override + public void stopMonitor() { + if (isRunning()) { + log.info("Stopping EXA monitor for " + prefix + " / " + host); + this.socket.off(); + this.socket.disconnect(); + this.socket.close(); + this.socket = null; + } + } + + @Override + public Types getType() { + return Types.EXABGP; + } + + @Override + public boolean isRunning() { + return this.socket != null; + } + + @Override + public String getHost() { + return host; + } + + @Override + public void setHost(String host) { + this.host = host; + } + + @Override + public int hashCode() { + return Objects.hash(prefix, host); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof ExaBgpMonitor) { + final ExaBgpMonitor that = (ExaBgpMonitor) obj; + return Objects.equals(this.prefix, that.prefix) && + Objects.equals(this.host, that.host); + } + return false; + } + +} diff --git a/apps/artemis/src/main/java/org/onosproject/artemis/impl/monitors/Monitor.java b/apps/artemis/src/main/java/org/onosproject/artemis/impl/monitors/Monitor.java new file mode 100644 index 0000000000..f93de3cf09 --- /dev/null +++ b/apps/artemis/src/main/java/org/onosproject/artemis/impl/monitors/Monitor.java @@ -0,0 +1,102 @@ +/* + * Copyright 2016-present 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.artemis.impl.monitors; + +import org.onlab.packet.IpPrefix; + +/** + * Abstract class for Monitors. + */ +public abstract class Monitor { + /** + * Match enum type with monitor type inside configuration to map them. + */ + public enum Types { + RIPE { + @Override + public String toString() { + return "ripe"; + } + }, + EXABGP { + @Override + public String toString() { + return "exabgp"; + } + } + } + + IpPrefix prefix; + Monitor(IpPrefix prefix) { + this.prefix = prefix; + } + + /** + * Get prefix of the specific monitor. + * + * @return prefix + */ + public IpPrefix getPrefix() { + return prefix; + } + + /** + * Set prefix for monitor. + * + * @param prefix prefix + */ + public void setPrefix(IpPrefix prefix) { + this.prefix = prefix; + } + + /** + * Start monitor to begin capturing incoming BGP packets. + */ + public abstract void startMonitor(); + + /** + * Stop monitor from capturing incoming BGP packets. + */ + public abstract void stopMonitor(); + + /** + * Get type of monitor. + * + * @return enum type + */ + public abstract Types getType(); + + /** + * Check if monitor is running. + * + * @return true if running + */ + public abstract boolean isRunning(); + + /** + * Get host alias e.g. IP address, name. + * + * @return host alias + */ + public abstract String getHost(); + + /** + * Set alias of host. + * + * @param host alias + */ + public abstract void setHost(String host); +} diff --git a/apps/artemis/src/main/java/org/onosproject/artemis/impl/monitors/RipeMonitor.java b/apps/artemis/src/main/java/org/onosproject/artemis/impl/monitors/RipeMonitor.java new file mode 100644 index 0000000000..e121c10a16 --- /dev/null +++ b/apps/artemis/src/main/java/org/onosproject/artemis/impl/monitors/RipeMonitor.java @@ -0,0 +1,181 @@ +/* + * Copyright 2016-present 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.artemis.impl.monitors; + +import io.socket.client.IO; +import io.socket.client.Socket; +import org.json.JSONException; +import org.json.JSONObject; +import org.onlab.packet.IpPrefix; +import org.onosproject.artemis.impl.ArtemisManager; +import org.onosproject.artemis.impl.DataHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.URISyntaxException; +import java.util.Objects; + +/** + * Implementation of RIPE Route Collector Monitor. + */ +public class RipeMonitor extends Monitor { + private String host; + private Socket socket; + + private final Logger log = LoggerFactory.getLogger(getClass()); + + public RipeMonitor(IpPrefix prefix, String host) { + super(prefix); + this.host = host; + } + + /** + * socket.io onConnect event handler. + */ + private void onConnect() { + try { + socket.emit("ping"); + + JSONObject parameters = new JSONObject(); + parameters.put("origin", (Object) null); + parameters.put("type", (Object) null); + parameters.put("moreSpecific", true); + parameters.put("lessSpecific", false); + parameters.put("peer", (Object) null); + parameters.put("host", this.host); + parameters.put("prefix", this.prefix); + + socket.emit("ris_subscribe", parameters); + } catch (JSONException e) { + e.printStackTrace(); + } + } + + /** + * socket.io onRisMessage event handler. + * This event is custom made that triggers when it receives an BGP update/withdraw for our prefix. + * + * @param args RIS message + */ + private void onRisMessage(Object[] args) { + try { + JSONObject message = (JSONObject) args[0]; + if (message.getString("type").equals("A")) { + // Write BGP message to a json database + DataHandler.Serializer.writeData(args[0]); + + if (ArtemisManager.logging) { + log.info(message.toString()); + } + + // Example of BGP Update message: + // { + // "timestamp":1488044022.97, + // "prefix":"101.1.46.0/24", + // "host":"rrc21", + // "next_hop":"37.49.236.246", + // "peer":"37.49.236.246", + // "path":[2613,25091,9318,9524], + // "type":"A" + // } + + // We want to keep only prefix and path in memory. + message.remove("community"); + message.remove("timestamp"); + message.remove("next_hop"); + message.remove("peer"); + message.remove("type"); + message.remove("host"); + + // Append synchronized message to message list in memory. + DataHandler.getInstance().appendData(message); + } + } catch (JSONException e) { + e.printStackTrace(); + } + socket.emit("ping"); + } + + @Override + public void startMonitor() { + if (!isRunning()) { + log.info("Starting RIPE monitor for " + prefix + " / " + host); + IO.Options opts = new IO.Options(); + opts.path = "/stream/socket.io/"; + + try { + this.socket = IO.socket("http://stream-dev.ris.ripe.net/", opts); + this.socket.on(Socket.EVENT_CONNECT, args -> onConnect()); + this.socket.on(Socket.EVENT_PONG, args -> socket.emit("ping")); + this.socket.on("ris_message", this::onRisMessage); + } catch (URISyntaxException e) { + e.printStackTrace(); + } + + this.socket.connect(); + } + } + + @Override + public void stopMonitor() { + if (isRunning()) { + log.info("Stopping RIPE monitor for " + prefix + " / " + host); + this.socket.off(); + this.socket.disconnect(); + this.socket.close(); + this.socket = null; + } + } + + @Override + public Types getType() { + return Types.RIPE; + } + + @Override + public boolean isRunning() { + return this.socket != null; + } + + @Override + public String getHost() { + return host; + } + + @Override + public void setHost(String host) { + this.host = host; + } + + @Override + public int hashCode() { + return Objects.hash(prefix, host); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof RipeMonitor) { + final RipeMonitor that = (RipeMonitor) obj; + return Objects.equals(this.prefix, that.prefix) && + Objects.equals(this.host, that.host); + } + return false; + } + +} diff --git a/apps/artemis/src/main/java/org/onosproject/artemis/impl/monitors/package-info.java b/apps/artemis/src/main/java/org/onosproject/artemis/impl/monitors/package-info.java new file mode 100644 index 0000000000..aa8ded7501 --- /dev/null +++ b/apps/artemis/src/main/java/org/onosproject/artemis/impl/monitors/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2016-present 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. + */ + +/** + * Route Collector Monitors. + */ +package org.onosproject.artemis.impl.monitors; \ No newline at end of file diff --git a/apps/artemis/src/main/java/org/onosproject/artemis/impl/package-info.java b/apps/artemis/src/main/java/org/onosproject/artemis/impl/package-info.java new file mode 100755 index 0000000000..6445f380c2 --- /dev/null +++ b/apps/artemis/src/main/java/org/onosproject/artemis/impl/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2016-present 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. + */ + +/** + * Artemis component. + */ +package org.onosproject.artemis.impl; \ No newline at end of file diff --git a/apps/artemis/src/main/resources/OSGI-INF/blueprint/shell-config.xml b/apps/artemis/src/main/resources/OSGI-INF/blueprint/shell-config.xml new file mode 100755 index 0000000000..9f9666a99f --- /dev/null +++ b/apps/artemis/src/main/resources/OSGI-INF/blueprint/shell-config.xml @@ -0,0 +1,24 @@ + + + + + + + + + + diff --git a/modules.defs b/modules.defs index ec14130e8c..3c51154b66 100644 --- a/modules.defs +++ b/modules.defs @@ -207,6 +207,7 @@ ONOS_APPS = [ '//apps/network-troubleshoot:onos-apps-network-troubleshoot-oar', '//apps/l3vpn:onos-apps-l3vpn-oar', '//apps/openroadm:onos-apps-openroadm-oar', + '//apps/artemis:onos-apps-artemis-oar', ] MODELS = [ diff --git a/tools/tutorials/artemis/configs/R1-quagga.conf b/tools/tutorials/artemis/configs/R1-quagga.conf new file mode 100644 index 0000000000..271a18f88d --- /dev/null +++ b/tools/tutorials/artemis/configs/R1-quagga.conf @@ -0,0 +1,14 @@ +! +hostname bgp +password sdnip +! +! +router bgp 65001 + bgp router-id 1.1.1.1 + network 10.0.0.0/8 + neighbor 10.0.0.3 remote-as 65001 + neighbor 150.1.1.1 remote-as 65003 + neighbor 150.1.2.2 remote-as 65002 +! + +log stdout \ No newline at end of file diff --git a/tools/tutorials/artemis/configs/R2-quagga.conf b/tools/tutorials/artemis/configs/R2-quagga.conf new file mode 100644 index 0000000000..bacdafd4c7 --- /dev/null +++ b/tools/tutorials/artemis/configs/R2-quagga.conf @@ -0,0 +1,14 @@ +! +hostname bgp +password sdnip +! +! +router bgp 65002 + bgp router-id 2.2.2.2 + network 20.0.0.0/8 + neighbor 150.1.2.1 remote-as 65001 + neighbor 150.1.3.2 remote-as 65004 +! + + +log stdout \ No newline at end of file diff --git a/tools/tutorials/artemis/configs/R3-quagga.conf b/tools/tutorials/artemis/configs/R3-quagga.conf new file mode 100644 index 0000000000..d02b0538e9 --- /dev/null +++ b/tools/tutorials/artemis/configs/R3-quagga.conf @@ -0,0 +1,12 @@ +! +hostname bgp +password sdnip +! +! +router bgp 65003 + bgp router-id 3.3.3.3 + network 30.0.0.0/8 + neighbor 150.1.1.2 remote-as 65001 +! + +log stdout \ No newline at end of file diff --git a/tools/tutorials/artemis/configs/R4-quagga.conf b/tools/tutorials/artemis/configs/R4-quagga.conf new file mode 100644 index 0000000000..777d866e62 --- /dev/null +++ b/tools/tutorials/artemis/configs/R4-quagga.conf @@ -0,0 +1,16 @@ +! +hostname bgp +password sdnip +! +! +router bgp 65004 +bgp router-id 4.4.4.4 + network 40.0.0.0/8 + neighbor 10.10.10.2 remote-as 65004 + neighbor 10.10.10.2 port 2000 + neighbor 150.1.3.1 remote-as 65002 + neighbor 150.1.3.1 next-hop-self +! + + +log stdout \ No newline at end of file diff --git a/tools/tutorials/artemis/configs/exabgp.conf b/tools/tutorials/artemis/configs/exabgp.conf new file mode 100644 index 0000000000..1dfd456b9f --- /dev/null +++ b/tools/tutorials/artemis/configs/exabgp.conf @@ -0,0 +1,20 @@ +group r1 { + router-id 10.0.0.3; + + process message-logger { + encoder json; + receive { + parsed; + update; + neighbor-changes; + } + run ./absolute/path/to/onos/tools/tutorials/artemis/server.py; + } + + neighbor 10.0.0.1 { + local-address 10.0.0.3; + local-as 65001; + peer-as 65001; + } + +} diff --git a/tools/tutorials/artemis/configs/network-cfg.json b/tools/tutorials/artemis/configs/network-cfg.json new file mode 100644 index 0000000000..8b5d56fe28 --- /dev/null +++ b/tools/tutorials/artemis/configs/network-cfg.json @@ -0,0 +1,84 @@ +{ + "ports" : { + "of:00002a45d713e141/2" : { + "interfaces" : [ + { + "name" : "sw1-1", + "ips" : [ "150.1.3.2/30" ], + "mac" : "e2:f5:32:16:9a:46" + } + ] + }, + "of:00002a45d713e141/3" : { + "interfaces" : [ + { + "name" : "sw1-1", + "ips" : [ "40.0.0.1/24" ], + "mac" : "e2:f5:32:16:9a:46" + } + ] + } + }, + "apps" : { + "org.onosproject.router" : { + "bgp" : { + "bgpSpeakers" : [ + { + "name" : "speaker1", + "connectPoint" : "of:00002a45d713e141/4", + "peers" : [ + "150.1.3.1" + ] + } + ] + } + }, + "org.onosproject.reactive.routing" : { + "reactiveRouting" : { + "ip4LocalPrefixes" : [ + { + "ipPrefix" : "40.0.0.0/24", + "type" : "PUBLIC", + "gatewayIp" : "40.0.0.1" + }, + { + "ipPrefix" : "150.1.3.0/30", + "type" : "PRIVATE", + "gatewayIp" : "150.1.3.2" + } + ], + "ip6LocalPrefixes" : [ + ], + "virtualGatewayMacAddress" : "e2:f5:32:16:9a:46" + } + }, + "org.onosproject.artemis" : { + "artemis" : { + "prefixes" : [ + { + "prefix" : "40.0.0.0/8", + "paths" : [ + { + "origin" : 65004, + "neighbor" : [ + { + "asn" : 65002, + "neighbor": [ + 65001 + ] + } + ] + } + ], + "moas" : [ ] + } + ], + "frequency" : 3000, + "monitors" : { + "ripe" : [ ], + "exabgp": [ "192.168.1.2:5000" ] + } + } + } + } +} diff --git a/tools/tutorials/artemis/configs/zebra.conf b/tools/tutorials/artemis/configs/zebra.conf new file mode 100644 index 0000000000..eafc15ea17 --- /dev/null +++ b/tools/tutorials/artemis/configs/zebra.conf @@ -0,0 +1,5 @@ +! Configuration for zebra (NB: it is the same for all routers) +! +hostname zebra +password sdnip +log stdout \ No newline at end of file diff --git a/tools/tutorials/artemis/requirements.txt b/tools/tutorials/artemis/requirements.txt new file mode 100644 index 0000000000..8cc6ce6005 --- /dev/null +++ b/tools/tutorials/artemis/requirements.txt @@ -0,0 +1,3 @@ +python-socketio==1.7.6 +netaddr==0.7.19 +Flask==0.12.2 \ No newline at end of file diff --git a/tools/tutorials/artemis/server.py b/tools/tutorials/artemis/server.py new file mode 100755 index 0000000000..a5947b9abd --- /dev/null +++ b/tools/tutorials/artemis/server.py @@ -0,0 +1,80 @@ +#!/usr/bin/python3 + +async_mode = 'threading' + +import time +from flask import Flask, render_template, abort +import socketio +from sys import stdin, stdout, stderr +import json +import time +from netaddr import IPNetwork, IPAddress + +sio = socketio.Server(logger=False, async_mode=async_mode) +app = Flask(__name__) +app.wsgi_app = socketio.Middleware(sio, app.wsgi_app) +app.config['SECRET_KEY'] = 'secret!' +thread = None +clients = {} + + +def message_parser(line): + try: + temp_message = json.loads(line) + if temp_message['type'] == 'update': + for origin in temp_message['neighbor']['message']['update']['announce']['ipv4 unicast']: + message = { + 'type': 'A', + 'timestamp': temp_message['time'], + 'peer': temp_message['neighbor']['ip'], + 'host': 'exabgp', + 'path': temp_message['neighbor']['message']['update']['attribute']['as-path'], + } + for prefix in temp_message['neighbor']['message']['update']['announce']['ipv4 unicast'][origin]: + message['prefix'] = prefix + for sid in clients.keys(): + try: + if IPAddress(str(prefix).split('/')[0]) in clients[sid][0]: + print('Sending exa_message to ' + + str(clients[sid][0]), file=stderr) + sio.emit( + 'exa_message', message, room=sid, namespace='/onos') + except: + print('Invalid format received from %s'.format(str(sid))) + except Exception as e: + print(str(e), file=stderr) + + +def exabgp_update_event(): + while True: + line = stdin.readline().strip() + messages = message_parser(line) + + +@app.route('/') +def index(): + abort(404) + + +@sio.on('connect', namespace='/onos') +def onos_connect(sid, environ): + global thread + if thread is None: + thread = sio.start_background_task(exabgp_update_event) + + +@sio.on('disconnect', namespace='/onos') +def onos_disconnect(sid): + if sid in clients: + del clients[sid] + + +@sio.on('exa_subscribe', namespace='/onos') +def onos_exa_subscribe(sid, message): + try: + clients[sid] = [IPNetwork(message['prefix']), True] + except: + print('Invalid format received from %s'.format(str(sid))) + +if __name__ == '__main__': + app.run(host='0.0.0.0', threaded=True) diff --git a/tools/tutorials/artemis/topo.py b/tools/tutorials/artemis/topo.py new file mode 100755 index 0000000000..a61261183c --- /dev/null +++ b/tools/tutorials/artemis/topo.py @@ -0,0 +1,259 @@ +#!/usr/bin/python + +from mininet.topo import Topo +from mininet.net import Mininet +from mininet.cli import CLI +from mininet.log import setLogLevel, info, debug +from mininet.node import Host, RemoteController, OVSSwitch +import os + +QUAGGA_DIR = '/usr/lib/quagga' +# Must exist and be owned by quagga user (quagga:quagga by default on Ubuntu) +QUAGGA_RUN_DIR = '/var/run/quagga' +EXABGP_RUN_EXE = '~/exabgp/sbin/exabgp' +CONFIG_DIR = 'configs/' + +onos = RemoteController('onos', ip='192.168.0.1', port=6633) + + +class Onos(Host): + + def __init__(self, name, intfDict, *args, **kwargs): + Host.__init__(self, name, *args, **kwargs) + + self.intfDict = intfDict + + def config(self, **kwargs): + Host.config(self, **kwargs) + + for intf, attrs in self.intfDict.items(): + self.cmd('ip addr flush dev %s' % intf) + if 'mac' in attrs: + self.cmd('ip link set %s down' % intf) + self.cmd('ip link set %s address %s' % (intf, attrs['mac'])) + self.cmd('ip link set %s up ' % intf) + for addr in attrs['ipAddrs']: + self.cmd('ip addr add %s dev %s' % (addr, intf)) + + +class QuaggaRouter(Host): + + def __init__(self, name, quaggaConfFile, zebraConfFile, intfDict, *args, **kwargs): + Host.__init__(self, name, *args, **kwargs) + + self.quaggaConfFile = quaggaConfFile + self.zebraConfFile = zebraConfFile + self.intfDict = intfDict + + def config(self, **kwargs): + Host.config(self, **kwargs) + self.cmd('sysctl net.ipv4.ip_forward=1') + + for intf, attrs in self.intfDict.items(): + self.cmd('ip addr flush dev %s' % intf) + if 'mac' in attrs: + self.cmd('ip link set %s down' % intf) + self.cmd('ip link set %s address %s' % (intf, attrs['mac'])) + self.cmd('ip link set %s up ' % intf) + for addr in attrs['ipAddrs']: + self.cmd('ip addr add %s dev %s' % (addr, intf)) + + self.cmd('/usr/lib/quagga/zebra -d -f %s -z %s/zebra%s.api -i %s/zebra%s.pid' % + (self.zebraConfFile, QUAGGA_RUN_DIR, self.name, QUAGGA_RUN_DIR, self.name)) + self.cmd('/usr/lib/quagga/bgpd -d -f %s -z %s/zebra%s.api -i %s/bgpd%s.pid' % + (self.quaggaConfFile, QUAGGA_RUN_DIR, self.name, QUAGGA_RUN_DIR, self.name)) + + def terminate(self): + self.cmd("ps ax | egrep 'bgpd%s.pid|zebra%s.pid' | awk '{print $1}' | xargs kill" % ( + self.name, self.name)) + + Host.terminate(self) + + +class ExaBGPRouter(Host): + + def __init__(self, name, exaBGPconf, intfDict, *args, **kwargs): + Host.__init__(self, name, *args, **kwargs) + + self.exaBGPconf = exaBGPconf + self.intfDict = intfDict + + def config(self, **kwargs): + Host.config(self, **kwargs) + self.cmd('sysctl net.ipv4.ip_forward=1') + + for intf, attrs in self.intfDict.items(): + self.cmd('ip addr flush dev %s' % intf) + if 'mac' in attrs: + self.cmd('ip link set %s down' % intf) + self.cmd('ip link set %s address %s' % (intf, attrs['mac'])) + self.cmd('ip link set %s up ' % intf) + for addr in attrs['ipAddrs']: + self.cmd('ip addr add %s dev %s' % (addr, intf)) + + self.cmd('%s %s > /dev/null 2> exabgp.log &' % (EXABGP_RUN_EXE, self.exaBGPconf)) + + def terminate(self): + self.cmd( + "ps ax | egrep 'lib/exabgp/application/bgp.py' | awk '{print $1}' | xargs kill") + self.cmd( + "ps ax | egrep 'server.py' | awk '{print $1}' | xargs kill") + Host.terminate(self) + + +class ONOSSwitch(OVSSwitch): + + def start(self, controllers): + return OVSSwitch.start(self, [onos]) + + +class L2Switch(OVSSwitch): + + def start(self, controllers): + return OVSSwitch.start(self, []) + + +class ArtemisTopo(Topo): + "Artemis tutorial topology" + + def build(self): + zebraConf = '%szebra.conf' % CONFIG_DIR + + quaggaConf = '%sR1-quagga.conf' % CONFIG_DIR + name = 'R1' + eth0 = { + 'ipAddrs': ['150.1.1.2/30'] + } + eth1 = { + 'ipAddrs': ['10.0.0.1/8'] + } + eth2 = { + 'ipAddrs': ['150.1.2.1/30'] + } + intfs = { + '%s-eth0' % name: eth0, + '%s-eth1' % name: eth1, + '%s-eth2' % name: eth2 + } + r1 = self.addHost(name, cls=QuaggaRouter, quaggaConfFile=quaggaConf, + zebraConfFile=zebraConf, intfDict=intfs) + + quaggaConf = '%sR2-quagga.conf' % CONFIG_DIR + name = 'R2' + eth0 = { + 'ipAddrs': ['150.1.3.1/30'] + } + eth1 = { + 'ipAddrs': ['150.1.2.2/30'] + } + intfs = { + '%s-eth0' % name: eth0, + '%s-eth1' % name: eth1 + } + r2 = self.addHost(name, cls=QuaggaRouter, quaggaConfFile=quaggaConf, + zebraConfFile=zebraConf, intfDict=intfs) + + quaggaConf = '%sR3-quagga.conf' % CONFIG_DIR + name = 'R3' + eth0 = { + 'ipAddrs': ['40.0.0.1/8'] + } + eth1 = { + 'ipAddrs': ['150.1.1.1/30'] + } + intfs = { + '%s-eth0' % name: eth0, + '%s-eth1' % name: eth1 + } + r3 = self.addHost(name, cls=QuaggaRouter, quaggaConfFile=quaggaConf, + zebraConfFile=zebraConf, intfDict=intfs) + + quaggaConf = '%sR4-quagga.conf' % CONFIG_DIR + name = 'R4' + eth0 = { + 'ipAddrs': ['150.1.3.2/30'], + 'mac': 'e2:f5:32:16:9a:46' + } + eth1 = { + 'ipAddrs': ['10.10.10.1/24'] + } + intfs = { + '%s-eth0' % name: eth0, + '%s-eth1' % name: eth1 + } + r4 = self.addHost(name, cls=QuaggaRouter, quaggaConfFile=quaggaConf, + zebraConfFile=zebraConf, intfDict=intfs) + + ovs = self.addSwitch('ovs', dpid='00002a45d713e141', cls=ONOSSwitch) + + l2_switch = self.addSwitch( + 'l2_switch', dpid='0000000000000001', failMode='standalone', cls=L2Switch) + + h1 = self.addHost('h1', ip='10.0.0.100/8', defaultRoute='via 10.0.0.1') + h4 = self.addHost('h4', ip='40.0.0.100/8', defaultRoute='via 40.0.0.1') + + # Set up the internal BGP speaker + + name = 'exabgp' + eth0 = { + 'ipAddrs': ['10.0.0.3/8'] + } + eth1 = { + 'ipAddrs': ['192.168.1.2/24'] + } + intfs = { + '%s-eth0' % name: eth0, + '%s-eth1' % name: eth1 + } + exabgp = self.addHost(name, cls=ExaBGPRouter, + exaBGPconf='%sexabgp.conf' % CONFIG_DIR, + intfDict=intfs) + + self.addLink(r1, r3, port1=0, port2=1) + self.addLink(r1, l2_switch, port1=1, port2=2) + self.addLink(r1, r2, port1=2, port2=1) + + self.addLink(ovs, r2, port1=2, port2=0) + self.addLink(ovs, h4, port1=3, port2=0) + self.addLink(ovs, r4, port1=4, port2=0) + + self.addLink(l2_switch, h1, port1=1, port2=0) + self.addLink(l2_switch, exabgp, port1=3, port2=0) + + name = 'onos' + eth0 = { + 'ipAddrs': ['192.168.0.1/24'] + } + eth1 = { + 'ipAddrs': ['10.10.10.2/24'] + } + eth2 = { + 'ipAddrs': ['192.168.1.1/24'] + } + intfs = { + '%s-eth0' % name: eth0, + '%s-eth1' % name: eth1, + '%s-eth2' % name: eth2 + } + onos = self.addHost(name, inNamespace=False, cls=Onos, intfDict=intfs) + + self.addLink(onos, ovs, port1=0, port2=1) + self.addLink(onos, r4, port1=1, port2=1) + self.addLink(onos, exabgp, port1=2, port2=1) + +topos = {'artemis': ArtemisTopo} + +if __name__ == '__main__': + setLogLevel('debug') + topo = ArtemisTopo() + + net = Mininet(topo=topo, build=False) + net.addController(onos) + net.build() + net.start() + + CLI(net) + + net.stop() + + info("done\n")