From d85df19dbdd1f38a43f344f2feaa19e59d8dcf30 Mon Sep 17 00:00:00 2001 From: Massimo Candela Date: Tue, 27 Apr 2021 19:00:11 +0200 Subject: [PATCH] introduced monitoring of neighbors in AS path --- README.md | 2 + config.yml.example | 6 ++ docs/configuration.md | 8 +- docs/path-poisoning.md | 68 +++++++++++++++ docs/prefixes.md | 2 + index.js | 10 +++ package-lock.json | 44 +++++----- package.json | 4 +- prefixes.yml.bak | 79 ++++++++++++++++++ src/config/config.js | 8 ++ src/connectors/connectorRIS.js | 5 +- src/generatePrefixesList.js | 74 +++++++++++++++-- src/inputs/input.js | 72 +++++++++++----- src/inputs/inputYml.js | 11 ++- src/model.js | 25 +++++- src/monitors/monitorPathPoisoning.js | 120 +++++++++++++++++++++++++++ 16 files changed, 482 insertions(+), 56 deletions(-) create mode 100644 docs/path-poisoning.md create mode 100644 prefixes.yml.bak create mode 100644 src/monitors/monitorPathPoisoning.js diff --git a/README.md b/README.md index b5288f9..6c4c60a 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ Self-configuring BGP monitoring tool, which allows you to monitor in **real-time * ROAs covering your prefixes are no longer reachable (e.g., TA malfunction); * a ROA involving any of your prefixes or ASes was deleted/added/edited; * your AS is announcing a new prefix that was never announced before; +* an unexpected upstream (left-side) AS appears in an AS path (possible path poisoning); +* an unexpected downstream (right-side) AS appears in an AS path; * one of the AS paths used to reach your prefix matches a specific condition defined by you. You just run it. You don't need to provide any data source or connect it to anything in your network since it connects to [public repos](docs/datasets.md). diff --git a/config.yml.example b/config.yml.example index abb91a5..21ac3a5 100644 --- a/config.yml.example +++ b/config.yml.example @@ -54,6 +54,12 @@ monitors: channel: rpki name: rpki-diff + - file: monitorPathPoisoning + channel: hijack + name: path-poisoning + params: + thresholdMinPeers: 3 + reports: - file: reportFile channels: diff --git a/docs/configuration.md b/docs/configuration.md index 0eb57d3..698bfe6 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -266,7 +266,7 @@ This is useful if you want to be alerted in case your AS starts announcing somet > > If AS58302 starts announcing 45.230.23.0/24 an alert will be triggered. This happens because such prefix is not already monitored (it's not a sub prefix of 50.82.0.0/20). -You can generate the options block in the prefixes list automatically. Refer to the options `-s` and `-m` in the [auto genere prefixes documentation](prefixes.md#generate). +You can generate the options block in the prefixes list automatically. Refer to the options `-s` and `-m` of the [auto configuration](prefixes.md#generate). Example of alert: @@ -355,6 +355,12 @@ Example of alerts: > ROAs change detected: removed <1.2.3.4/24, 1234, 25, apnic> +#### monitorPathPoisoning + +The component `monitorPathPoisoning` allows to monitor for unexpected neighbor ASes in AS paths. The list of neighbors can be specified in `prefixes.yml` inside the `monitorASns` sections. + +Refer to the [documentation for this monitor](path-poisoning.md). + ### Reports diff --git a/docs/path-poisoning.md b/docs/path-poisoning.md new file mode 100644 index 0000000..853b2a7 --- /dev/null +++ b/docs/path-poisoning.md @@ -0,0 +1,68 @@ +# Path poisoning / upstream and downstream AS monitoring + +The component `monitorPathPoisoning` allows to monitor for unexpected neighbor ASes in AS paths. The list of neighbors can be specified in `prefixes.yml` inside the `monitorASns` sections. + +> For example, imagine AS100 has two upstreams, AS99 and AS98, and one downstream, AS101. You can express the following rule in 'prefixes.yml' +> +> ```yaml +> options: +> monitorASns: +> '2914': +> group: noc +> upstreams: +> - 99 +> - 98 +> downstreams: +> - 101 +> ``` + +Every time an AS path is detected with a different upstream/downstream AS, an alert will be generated. + +**You can generate the upstream/downstream lists automatically. Refer to the options `-u` and `-n` of the [auto configuration](prefixes.md#generate).** + +According to the above configuration, +* the AS path [10, 20, 30, 100, 101] will generate an alert since AS30 is not an upstream of AS100; +* the AS path [10, 20, 30, 100] will generate an alert since AS30 is not an upstream of AS100; +* the AS path [10, 20, 99, 100, 101] will not generate an alert since AS99 is an upstream of AS100; +* the AS path [10, 20, 99, 100, 104] will generate an alert since AS104 is not a downstream of AS100; +* the AS path [100, 104] will generate an alert since AS104 is not a downstream of AS100. + +You can disable the monitoring by removing the upstreams and downstreams lists or by commenting the `monitorPathPoisoning` block in `config.yml`. + +If you delete only one of the upstreams and downstreams lists, the monitoring will continue on the remaining one. + +> E.g., the config below monitors only for upstreams +> +> ```yaml +> options: +> monitorASns: +> '2914': +> group: noc +> upstreams: +> - 99 +> - 98 +> ``` + +If you provide empty lists, the monitoring will be performed and you will receive an alert for every upstream/downstream. + +> E.g., the config below monitors only for downstreams and expects to never see any downstream AS (stub network) +> +> ```yaml +> options: +> monitorASns: +> '2914': +> group: noc +> downstreams: +> ``` + + + +Example of alert: +> A new upstream of AS100 has been detected: AS30 + +Parameters for this monitor module: + +|Parameter| Description| +|---|---| +|thresholdMinPeers| Minimum number of peers that need to see the BGP update before to trigger an alert. | +|maxDataSamples| Maximum number of collected BGP messages for each alert which doesn't reach yet the `thresholdMinPeers`. Default to 1000. As soon as the `thresholdMinPeers` is reached, the collected BGP messages are flushed, independently from the value of `maxDataSamples`.| diff --git a/docs/prefixes.md b/docs/prefixes.md index 2c01f90..d2d89ab 100644 --- a/docs/prefixes.md +++ b/docs/prefixes.md @@ -27,6 +27,8 @@ Below the list of possible parameters. **Remember to prepend them with a `--` in | -D | Enable debug mode. All queries executed in background will be shown. | Nothing | | No | | -H | Use historical visibility data for generating prefix list (prefixes visible in the last week). Useful in case the prefix generation process returns an empty dataset. | Nothing | | No | | -g | The name of the user group that will be assigned to all the generated rules. See [here](usergroups.md). | A string | noc | No | +| -u | Calculate all upstream ASes and enable path poisoning monitoring. See [here](path-poisoning.md). | Nothing | | No | +| -n | Calculate all downstream ASes and enable detection of new customer ASes. See [here](path-poisoning.md). | Nothing | | No | ## Prefixes list fields diff --git a/index.js b/index.js index 93c54c0..56c8415 100644 --- a/index.js +++ b/index.js @@ -114,6 +114,14 @@ const params = yargs .nargs('H', 0) .describe('H', 'Use historical visibility data for generating prefix list (prefixes visible in the last week).') + .alias('u', 'upstreams') + .nargs('u', 0) + .describe('u', 'Detect a list of allowed upstream ASes, useful to monitor for path poisoning.') + + .alias('n', 'downstreams') + .nargs('n', 0) + .describe('n', 'Detect a list of allowed downstream ASes, useful to monitor for path poisoning.') + .demandOption(['o']); }) .example('$0 generate -a 2914 -o prefixes.yml', 'Generate prefixes for AS2914') @@ -166,6 +174,8 @@ switch(params._[0]) { group: params.g || null, append: !!params.A, logger: null, + upstreams: !!params.u, + downstreams: !!params.n, getCurrentPrefixesList: () => { return Promise.resolve(yaml.load(fs.readFileSync(params.o, "utf8"))); } diff --git a/package-lock.json b/package-lock.json index 17341b2..ffe8079 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "https-proxy-agent": "^5.0.0", "inquirer": "^8.0.0", "ip-address": "7.1.0", - "ip-sub": "^1.0.21", + "ip-sub": "^1.0.22", "js-yaml": "^4.1.0", "kafkajs": "^1.15.0", "md5": "^2.3.0", @@ -46,7 +46,7 @@ "chai": "^4.3.4", "chai-subset": "^1.6.0", "mocha": "^8.3.2", - "pkg": "^5.0.0", + "pkg": "^5.1.0", "read-last-lines": "^1.8.0", "syslogd": "^1.1.2" }, @@ -3914,9 +3914,9 @@ } }, "node_modules/ip-sub": { - "version": "1.0.21", - "resolved": "https://registry.npmjs.org/ip-sub/-/ip-sub-1.0.21.tgz", - "integrity": "sha512-EBjB3RGTPxLoszOVk8XB6B5eec8EsWV4Z+4K9EKe7QmGqC3CmHBlpTbIqcdK2QTIb6eJUR7Y9N9kCHT63Go1ew==", + "version": "1.0.22", + "resolved": "https://registry.npmjs.org/ip-sub/-/ip-sub-1.0.22.tgz", + "integrity": "sha512-CsgRTLRmknJqn+/LvRcpLtozYb4CvRvXr1lronZjtuVuEUu9CeAyrTtL5Fi9jOxC6UNBfzCvRrsEhnoV8aIQnQ==", "dependencies": { "ip-address": "^7.1.0" } @@ -5342,9 +5342,9 @@ } }, "node_modules/pkg": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pkg/-/pkg-5.0.0.tgz", - "integrity": "sha512-B/bZZp51wUP00XHme4qoA/VHsRDIL1ldN+oEdg/L8E7mE9B8Oz/mximyQWbKFrMu85Uh3H9jy55ln5Ms6jspwg==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/pkg/-/pkg-5.1.0.tgz", + "integrity": "sha512-rWTwvLJakQnEVg03s97KNtGkhM3pPfxk7XinjR7H1bToMZQMNpBTwahrAPoFHdQyfn6odI76DP6vX3Br9VubNQ==", "dev": true, "dependencies": { "@babel/parser": "7.13.13", @@ -5356,7 +5356,7 @@ "into-stream": "^6.0.0", "minimist": "^1.2.5", "multistream": "^4.1.0", - "pkg-fetch": "3.0.3", + "pkg-fetch": "3.0.4", "prebuild-install": "6.0.1", "progress": "^2.0.3", "resolve": "^1.20.0", @@ -5446,9 +5446,9 @@ } }, "node_modules/pkg-fetch": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pkg-fetch/-/pkg-fetch-3.0.3.tgz", - "integrity": "sha512-w8Nn7EZZFtTcdeERUH5IcMRAbrn4xL55X05dtjIaBlrkkNzMvbgAyTAwEDGmK3cxBU1d/Eh+uZVueeUzIG0AEA==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pkg-fetch/-/pkg-fetch-3.0.4.tgz", + "integrity": "sha512-XgMXcco5hy0/Q7OXfQ/FbBnPvS4e7gWB9BCcUWUgaHYo3JretihmJjr62EZWmxAjvodoWLGMZ3E7XHf8Q2LfBg==", "dev": true, "dependencies": { "axios": "^0.21.1", @@ -10728,9 +10728,9 @@ } }, "ip-sub": { - "version": "1.0.21", - "resolved": "https://registry.npmjs.org/ip-sub/-/ip-sub-1.0.21.tgz", - "integrity": "sha512-EBjB3RGTPxLoszOVk8XB6B5eec8EsWV4Z+4K9EKe7QmGqC3CmHBlpTbIqcdK2QTIb6eJUR7Y9N9kCHT63Go1ew==", + "version": "1.0.22", + "resolved": "https://registry.npmjs.org/ip-sub/-/ip-sub-1.0.22.tgz", + "integrity": "sha512-CsgRTLRmknJqn+/LvRcpLtozYb4CvRvXr1lronZjtuVuEUu9CeAyrTtL5Fi9jOxC6UNBfzCvRrsEhnoV8aIQnQ==", "requires": { "ip-address": "^7.1.0" } @@ -11851,9 +11851,9 @@ } }, "pkg": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pkg/-/pkg-5.0.0.tgz", - "integrity": "sha512-B/bZZp51wUP00XHme4qoA/VHsRDIL1ldN+oEdg/L8E7mE9B8Oz/mximyQWbKFrMu85Uh3H9jy55ln5Ms6jspwg==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/pkg/-/pkg-5.1.0.tgz", + "integrity": "sha512-rWTwvLJakQnEVg03s97KNtGkhM3pPfxk7XinjR7H1bToMZQMNpBTwahrAPoFHdQyfn6odI76DP6vX3Br9VubNQ==", "dev": true, "requires": { "@babel/parser": "7.13.13", @@ -11865,7 +11865,7 @@ "into-stream": "^6.0.0", "minimist": "^1.2.5", "multistream": "^4.1.0", - "pkg-fetch": "3.0.3", + "pkg-fetch": "3.0.4", "prebuild-install": "6.0.1", "progress": "^2.0.3", "resolve": "^1.20.0", @@ -12002,9 +12002,9 @@ } }, "pkg-fetch": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pkg-fetch/-/pkg-fetch-3.0.3.tgz", - "integrity": "sha512-w8Nn7EZZFtTcdeERUH5IcMRAbrn4xL55X05dtjIaBlrkkNzMvbgAyTAwEDGmK3cxBU1d/Eh+uZVueeUzIG0AEA==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pkg-fetch/-/pkg-fetch-3.0.4.tgz", + "integrity": "sha512-XgMXcco5hy0/Q7OXfQ/FbBnPvS4e7gWB9BCcUWUgaHYo3JretihmJjr62EZWmxAjvodoWLGMZ3E7XHf8Q2LfBg==", "dev": true, "requires": { "axios": "^0.21.1", diff --git a/package.json b/package.json index e69cfef..ee03a8e 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "chai": "^4.3.4", "chai-subset": "^1.6.0", "mocha": "^8.3.2", - "pkg": "^5.0.0", + "pkg": "^5.1.0", "read-last-lines": "^1.8.0", "syslogd": "^1.1.2" }, @@ -66,7 +66,7 @@ "https-proxy-agent": "^5.0.0", "inquirer": "^8.0.0", "ip-address": "7.1.0", - "ip-sub": "^1.0.21", + "ip-sub": "^1.0.22", "js-yaml": "^4.1.0", "kafkajs": "^1.15.0", "md5": "^2.3.0", diff --git a/prefixes.yml.bak b/prefixes.yml.bak new file mode 100644 index 0000000..63801bd --- /dev/null +++ b/prefixes.yml.bak @@ -0,0 +1,79 @@ +193.0.22.0/23: + group: noc + ignore: false + description: No description provided + asn: 3333 + ignoreMorespecifics: false +193.0.0.0/21: + group: noc + ignore: false + description: No description provided + asn: 3333 + ignoreMorespecifics: false + path: + - match: .*2194,1234$ + notMatch: .*5054.* + matchDescription: detected scrubbing center + - match: .*123$ + notMatch: .*5056.* + matchDescription: other match +2001:67c:2e8::/48: + group: noc + ignore: false + description: No description provided + asn: 3333 + ignoreMorespecifics: false +193.0.10.0/23: + group: noc + ignore: false + description: No description provided + asn: 3333 + ignoreMorespecifics: false +193.0.12.0/23: + group: noc + ignore: false + description: No description provided + asn: 3333 + ignoreMorespecifics: false +193.0.18.0/23: + group: noc + ignore: false + description: No description provided + asn: 3333 + ignoreMorespecifics: false +193.0.20.0/23: + group: noc + ignore: false + description: No description provided + asn: 3333 + ignoreMorespecifics: false +options: + monitorASns: + '18747': + group: noc + upstreams: + - 1103 + - 1257 + - 1273 + - 12859 + downstreams: + - 12654 + '3333': + group: noc + upstreams: + - 1103 + - 1257 + - 1273 + - 12859 + downstreams: + - 12654 + generate: + asnList: + - '3333' + exclude: [] + excludeDelegated: true + prefixes: null + monitoredASes: + - '3333' + historical: false + group: noc diff --git a/src/config/config.js b/src/config/config.js index 57976ea..9fb53b8 100644 --- a/src/config/config.js +++ b/src/config/config.js @@ -78,6 +78,14 @@ export default class Config { channel: "rpki", name: "rpki-diff", params: {} + }, + { + file: "monitorPathPoisoning", + channel: "hijack", + name: "path-poisoning", + params: { + thresholdMinPeers: 3 + } } ], reports: [ diff --git a/src/connectors/connectorRIS.js b/src/connectors/connectorRIS.js index 7821e83..6962610 100644 --- a/src/connectors/connectorRIS.js +++ b/src/connectors/connectorRIS.js @@ -176,16 +176,17 @@ export default class ConnectorRIS extends Connector { }; _subscribeToASns = (input) => { - const monitoredASns = input.getMonitoredASns().map(i => i.asn); + const monitoredASns = input.getMonitoredASns(); const params = JSON.parse(JSON.stringify(this.params.subscription)); for (let asn of monitoredASns){ - const asnString = asn.getValue(); + const asnString = asn.asn.getValue(); if (!this.subscribed[asnString]) { console.log(`Monitoring AS${asnString}`); this.subscribed[asnString] = true; } + params.path = `${asnString}\$`; this.ws.send(JSON.stringify({ diff --git a/src/generatePrefixesList.js b/src/generatePrefixesList.js index 891e56a..b97de6c 100644 --- a/src/generatePrefixesList.js +++ b/src/generatePrefixesList.js @@ -26,7 +26,9 @@ module.exports = function generatePrefixes(inputParameters) { append, logger, getCurrentPrefixesList, - enriched + enriched, + upstreams, + downstreams } = inputParameters; exclude = exclude || []; @@ -69,6 +71,56 @@ module.exports = function generatePrefixes(inputParameters) { } } + const getNeighbors = (asn) => { + const url = brembo.build("https://stat.ripe.net", { + path: ["data", "asn-neighbours", "data.json"], + params: { + client: clientId, + resource: asn + } + }); + + if (debug) { + logger("Query", url) + } + + return axios({ + url, + method: 'GET', + responseType: 'json', + timeout: apiTimeout + }) + .then(data => { + let neighbors = []; + + if (data.data && data.data.data && data.data.data.neighbours){ + const items = data.data.data.neighbours; + + for (let item of items) { + if (item.type === "left" || item.type === "right") { + neighbors.push({asn: item.asn, type: item.type}); + } + } + + } + + const out = { + asn, + upstreams: neighbors.filter(i => i.type === "left").map(i => i.asn), + downstreams: neighbors.filter(i => i.type === "right").map(i => i.asn), + }; + + logger(`Detected upstreams for ${out.asn}: ${out.upstreams.join(", ")}`); + logger(`Detected downstreams for ${out.asn}: ${out.downstreams.join(", ")}`); + + return out; + }) + .catch((error) => { + logger(error); + logger(`RIPEstat asn-neighbours query failed: cannot retrieve information for ${asn}`); + }); + }; + const getMultipleOrigins = (prefix) => { const url = brembo.build("https://stat.ripe.net", { path: ["data", "prefix-overview", "data.json"], @@ -303,26 +355,36 @@ module.exports = function generatePrefixes(inputParameters) { }) .then(() => { // Add the options for monitorASns - const generateMonitoredAsObject = function (list) { + const generateMonitoredAsObject = function (list, asnNeighbors) { generateList.options = generateList.options || {}; generateList.options.monitorASns = generateList.options.monitorASns || {}; for (let monitoredAs of list) { logger(`Generating generic monitoring rule for AS${monitoredAs}`); + const neighbors = asnNeighbors.filter(i => i.asn.toString() === monitoredAs.toString()); generateList.options.monitorASns[monitoredAs] = { - group: group + group: group, + upstreams: upstreams && neighbors.length ? neighbors[0].upstreams : null, + downstreams: downstreams && neighbors.length ? neighbors[0].downstreams : null }; } }; + + let createASesRules = []; if (monitoredASes === true) { - generateMonitoredAsObject(asnList); + createASesRules = asnList; } else if (monitoredASes.length) { - generateMonitoredAsObject(monitoredASes); + createASesRules = monitoredASes; } + + return batchPromises(1, asnList, getNeighbors) + .then(asnNeighbors => { + generateMonitoredAsObject(createASesRules, asnNeighbors); + }) // Otherwise nothing }) .then(() => { if (someNotValidatedPrefixes) { - logger("WARNING: the generated configuration is a snapshot of what is currently announced. Some of the prefixes don't have ROA objects associated or are RPKI invalid. Please, verify the config file by hand!"); + logger("WARNING: the generated configuration is a snapshot of what is currently announced. Some of the prefixes don't have ROA objects associated. Please, verify the config file by hand!"); } }) .then(() => { diff --git a/src/inputs/input.js b/src/inputs/input.js index 9efdd55..5c8422b 100644 --- a/src/inputs/input.js +++ b/src/inputs/input.js @@ -1,4 +1,3 @@ - /* * BSD 3-Clause License * @@ -42,7 +41,8 @@ export default class Input { this.asns = []; this.cache = { af: {}, - binaries: {} + binaries: {}, + matched: {} }; this.config = env.config; this.storage = env.storage; @@ -50,6 +50,14 @@ export default class Input { this.callbacks = []; this.prefixListDiffFailThreshold = 50; + // This implements a fast basic fixed space cache, other approaches lru-like use too much cpu + setInterval(() => { + if (Object.keys(this.cache.matched).length > 10000) { + delete this.cache.matched; + } + }, 10000); + + // This is to load the prefixes after the application is booted setTimeout(() => { this.loadPrefixes() .then(() => { @@ -120,26 +128,36 @@ export default class Input { }; getMoreSpecificMatch = (prefix, includeIgnoredMorespecifics) => { + const key = `${prefix}-${includeIgnoredMorespecifics}`; + const cached = this.cache.matched[key]; - for (let p of this.prefixes) { - if (ipUtils._isEqualPrefix(p.prefix, prefix)) { - return p; - } else { + if (cached !== undefined) { + return cached; + } else { + for (let p of this.prefixes) { + if (ipUtils._isEqualPrefix(p.prefix, prefix)) { + this.cache.matched[key] = p; + return p; + } else { - if (!this.cache.af[p.prefix] || !this.cache.binaries[p.prefix]) { - this.cache.af[p.prefix] = ipUtils.getAddressFamily(p.prefix); - this.cache.binaries[p.prefix] = ipUtils.getNetmask(p.prefix, this.cache.af[p.prefix]); - } - const prefixAf = ipUtils.getAddressFamily(prefix); + // if (!this.cache.af[p.prefix] || !this.cache.binaries[p.prefix]) { + if (!this.cache.af[p.prefix]) { + this.cache.af[p.prefix] = ipUtils.getAddressFamily(p.prefix); + this.cache.binaries[p.prefix] = ipUtils.getNetmask(p.prefix, this.cache.af[p.prefix]); + } + const prefixAf = ipUtils.getAddressFamily(prefix); - if (prefixAf === this.cache.af[p.prefix]) { + if (prefixAf === this.cache.af[p.prefix]) { - const prefixBinary = ipUtils.getNetmask(prefix, prefixAf); - if (ipUtils.isSubnetBinary(this.cache.binaries[p.prefix], prefixBinary)) { - if (includeIgnoredMorespecifics || !p.ignoreMorespecifics) { - return p; - } else { - return null; + const prefixBinary = ipUtils.getNetmask(prefix, prefixAf); + if (ipUtils.isSubnetBinary(this.cache.binaries[p.prefix], prefixBinary)) { + if (includeIgnoredMorespecifics || !p.ignoreMorespecifics) { + this.cache.matched[key] = p; + return p; + } else { + this.cache.matched[key] = null; + return null; + } } } } @@ -195,6 +213,19 @@ export default class Input { name: 'm', message: "Do you want to be notified when your AS is announcing a new prefix?", default: true + }, + + { + type: 'confirm', + name: 'upstreams', + message: "Do you want to be notified when a new upstream AS appears in a BGP path?", + default: true + }, + { + type: 'confirm', + name: 'downstreams', + message: "Do you want to be notified when a new downstream AS appears in a BGP path?", + default: true } ]) .then((answer) => { @@ -212,6 +243,8 @@ export default class Input { group: null, append: false, logger: null, + upstreams: !!answer.upstreams, + downstreams: !!answer.downstreams, getCurrentPrefixesList: () => { return this.retrieve(); } @@ -255,7 +288,6 @@ export default class Input { } inputParameters.httpProxy = this.config.httpProxy || null; - inputParameters.logger = (message) => { // Nothing, ignore logs in this case (too many otherwise) }; @@ -263,6 +295,8 @@ export default class Input { return generatePrefixes(inputParameters) .then(newPrefixList => { + newPrefixList.options.monitorASns = oldPrefixList.options.monitorASns; + if (Object.keys(newPrefixList).length <= (Object.keys(oldPrefixList).length / 100) * this.prefixListDiffFailThreshold) { throw new Error("Prefix list generation failed."); } diff --git a/src/inputs/inputYml.js b/src/inputs/inputYml.js index 1996f34..a33b9f8 100644 --- a/src/inputs/inputYml.js +++ b/src/inputs/inputYml.js @@ -128,10 +128,15 @@ export default class InputYml extends Input { return; } uniqueAsns[asn] = true; - return Object.assign({ + const item = Object.assign({ asn: new AS(asn), group: 'default' }, monitoredPrefixesFile.options.monitorASns[asn]); + + if (item.upstreams) item.upstreams = new AS(item.upstreams); + if (item.downstreams) item.downstreams = new AS(item.downstreams); + + return item; }); this.asns = this.asns.concat(newAsnSet); @@ -330,7 +335,9 @@ export default class InputYml extends Input { for (let asnRule of this.asns) { monitorASns[asnRule.asn.getValue()] = { - group: asnRule.group + group: asnRule.group, + upstreams: asnRule.upstreams ? asnRule.upstreams.numbers : null, + downstreams: asnRule.downstreams ? asnRule.downstreams.numbers : null, }; } diff --git a/src/model.js b/src/model.js index f1a7eaf..696c6d6 100644 --- a/src/model.js +++ b/src/model.js @@ -21,7 +21,28 @@ export class Path { toJSON () { return this.getValues(); - } + }; + + getNeighbors (asn) { + const path = this.value; + const length = path.length - 1 + for (let n=0; n < length; n++) { + const current = path[n] || null; + if (current.getId() === asn.getId()) { + const left = path[n - 1] || null; + const right = path[n + 1] || null; + + return [left, current, right]; + } + } + + return [null, null, null]; + }; + + includes (asn) { + console.log(this.value); + return this.value.some(i => i.includes(asn)); + }; } @@ -109,5 +130,5 @@ export class AS { toJSON () { return this.numbers; - } + }; } diff --git a/src/monitors/monitorPathPoisoning.js b/src/monitors/monitorPathPoisoning.js new file mode 100644 index 0000000..d7eec66 --- /dev/null +++ b/src/monitors/monitorPathPoisoning.js @@ -0,0 +1,120 @@ +/* + * BSD 3-Clause License + * + * Copyright (c) 2019, NTT Ltd. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import Monitor from "./monitor"; + +export default class MonitorPathPoisoning extends Monitor { + + constructor(name, channel, params, env, input){ + super(name, channel, params, env, input); + this.thresholdMinPeers = (params && params.thresholdMinPeers != null) ? params.thresholdMinPeers : 0; + this.updateMonitoredResources(); + }; + + updateMonitoredResources = () => { + this.monitored = this.input.getMonitoredASns(); + }; + + filter = (message) => { + return message.type === 'announcement'; + }; + + squashAlerts = (alerts) => { + const peers = [...new Set(alerts.map(alert => alert.matchedMessage.peer))].length; + + if (peers >= this.thresholdMinPeers) { + const matchedRule = alerts[0].matchedRule; + const extra = alerts[0].extra; + const asnText = matchedRule.asn; + + return `A new ${extra.side} of ${asnText} has been detected: AS${extra.neighbor}`; + } + + return false; + }; + + monitor = (message) => + new Promise((resolve, reject) => { + + const path = message.path; + + for (let monitoredAs of this.monitored) { + if (monitoredAs.upstreams || monitoredAs.downstreams) { + const [left, _, right] = path.getNeighbors(monitoredAs.asn); + + if (!!left || !!right) { + let match = false; + let side = null; + let id = null; + + if (left) { + if (monitoredAs.upstreams === null) { + side = "upstream"; + id = left.getId(); + match = true; + } else if (monitoredAs.upstreams && !monitoredAs.upstreams.includes(left)) { + side = "upstream"; + id = left.getId(); + match = true; + } + } + + if (right) { + if (monitoredAs.downstreams === null) { + side = "downstream"; + id = right.getId(); + match = true; + } else if (monitoredAs.downstreams && !monitoredAs.downstreams.includes(right)) { + side = "downstream"; + id = right.getId(); + match = true; + } + } + + + if (match) { + const monitoredId = monitoredAs.asn.getId(); + + this.publishAlert([monitoredId, id].join("-"), + monitoredId, + monitoredAs, + message, + {side, neighbor: id}); + } + } + } + } + + resolve(true); + }); + +} \ No newline at end of file