diff --git a/config.yml.example b/config.yml.example index 55dc3d1..a088fe8 100644 --- a/config.yml.example +++ b/config.yml.example @@ -43,6 +43,13 @@ monitors: params: thresholdMinPeers: 2 + - file: monitorRPKI + channel: rpki + name: rpki-monitor + params: + thresholdMinPeers: 1 + checkUncovered: false + reports: - file: reportFile channels: @@ -51,6 +58,7 @@ reports: - visibility - path - misconfiguration + - rpki params: persistAlertData: false alertDataDirectory: alertdata/ @@ -62,6 +70,7 @@ reports: # - visibility # - path # - misconfiguration +# - rpki # params: # showPaths: 5 # Amount of AS_PATHs to report in the alert # senderEmail: bgpalerter@xxxx @@ -91,6 +100,7 @@ reports: # - visibility # - path # - misconfiguration +# - rpki # params: # showPaths: 0 # Amount of AS_PATHs to report in the alert # colors: @@ -108,6 +118,7 @@ reports: # - visibility # - path # - misconfiguration +# - rpki # params: # host: localhost # port: 9092 @@ -122,6 +133,7 @@ reports: # - path # - asn-monitor # - misconfiguration +# - rpki # params: # host: localhost # port: 514 @@ -139,6 +151,7 @@ reports: # - visibility # - path # - misconfiguration +# - rpki # params: # severity: # hijack: critical @@ -160,6 +173,7 @@ reports: # - visibility # - path # - misconfiguration +# - rpki # params: # hooks: # default: _YOUR_WEBEX_WEBHOOK_URL_ @@ -171,6 +185,7 @@ reports: # - visibility # - path # - misconfiguration +# - rpki # params: # templates: # See here how to write a template https://github.com/nttgin/BGPalerter/blob/master/docs/context.md # default: '{"text": "${summary}"}' diff --git a/docs/configuration.md b/docs/configuration.md index 8011784..79d64b4 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -222,8 +222,11 @@ Parameters for this monitor module: #### monitorAS -This monitor will listen for all announcements produced by the monitored Autonomous Systems and will detect when a prefix, which is not in the monitored prefixes list, is announced. -This is useful if you want to be alerted in case your AS starts announcing something you didn't intend to announce (e.g. misconfiguration, typo). +This monitor will listen for all announcements produced by the monitored Autonomous Systems and for all the announcements +involving any of the monitored prefixes (independently from who is announcing them) and it will trigger an alert if any of the announcements is RPKI invalid or not covered by ROAs (optional). + +This monitor is particularly useful while you are deploying RPKI since it will let you know if any of your announcements are +invalid, and after RPKI deployment, in order to be sure that all future BGP configuration will be covered by ROAs. > Example: @@ -256,6 +259,44 @@ Parameters for this monitor module: |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`.| + +#### monitorRPKI + +This monitor will listen for all announcements produced by the monitored Autonomous Systems and will detect when a prefix, which is not in the monitored prefixes list, is announced. +This is useful if you want to be alerted in case your AS starts announcing something you didn't intend to announce (e.g. misconfiguration, typo). + + +> Example: +> The prefixes list of BGPalerter has an options.monitorASns list declared, such as: +> ```yaml +> 50.82.0.0/20: +> asn: 58302 +> description: an example +> ignoreMorespecifics: false +> +> options: +> monitorASns: +> 58302: +> group: default +> ``` +> If in config.yml monitorRPKI is enabled, you will receive alerts every time: +> * 50.82.0.0/20 is announced and it is not covered by ROAs or the announcement is RPKI invalid; +> * AS58302 announces something that is not covered by ROAs or the announcement is RPKI invalid; + + +Example of alert: +> The route 103.21.244.0/24 announced by AS13335 is not RPKI valid. + +Parameters for this monitor module: + +|Parameter| Description| +|---|---| +|thresholdMinPeers| Minimum number of peers that need to see the BGP update before to trigger an alert. | +|checkUncovered| If set to true, the monitor will alert also for prefixes not covered by ROAs in addition of RPKI invalid prefixes. | +|preCacheROAs| This parameter allows to download locally VRPs lists. This is suggested in the case you want to validate many BGP updates (e.g. for research purposes). For normal production monitoring do NOT set this parameter. | +|refreshVrpListMinutes| If `preCacheROAs` is set to true, this parameter allows to specify a refresh time for the VRPs lists (it has to be > 15 minutes) | + + ### Reports Possible reports are: diff --git a/package-lock.json b/package-lock.json index 5958ce1..0aadf45 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3537,7 +3537,6 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "optional": true, "requires": { "once": "^1.4.0" } @@ -7397,9 +7396,9 @@ } }, "rpki-validator": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/rpki-validator/-/rpki-validator-1.0.15.tgz", - "integrity": "sha512-YG2S0mcKi8sEAYHzo3CeT/2ovasXsn8WjTLPlpwOwLYuvzknH0Rhs9ZeBtM7dmPds0iCAlHjulLpFRIv24pUxw==", + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/rpki-validator/-/rpki-validator-1.0.16.tgz", + "integrity": "sha512-b8geGsGeiFANWoG5fOyVq0AC4MhG0nT9ypIhMIpCsKQd5TzOLL7nVi42I1OrvREBgIyDC2t3SsYvIP88F6d+pA==", "requires": { "axios": "^0.19.2", "brembo": "^2.0.3", diff --git a/package.json b/package.json index f783d16..db40b61 100644 --- a/package.json +++ b/package.json @@ -30,8 +30,8 @@ "chai": "^4.2.0", "chai-subset": "^1.6.0", "mocha": "^7.1.2", - "pkg": "^4.4.8", "nodemon": "^2.0.3", + "pkg": "^4.4.8", "read-last-lines": "^1.7.2" }, "dependencies": { @@ -47,7 +47,7 @@ "nodemailer": "^6.4.6", "path": "^0.12.7", "restify": "^8.5.1", - "rpki-validator": "^1.0.15", + "rpki-validator": "^1.0.16", "semver": "^7.3.2", "syslog-client": "^1.1.1", "ws": "^7.2.5", diff --git a/src/connectors/connectorTest.js b/src/connectors/connectorTest.js index a71b6ed..d787236 100644 --- a/src/connectors/connectorTest.js +++ b/src/connectors/connectorTest.js @@ -382,6 +382,44 @@ export default class ConnectorTest extends Connector{ } ]; break; + + case "rpki": + updates = [ + { + data: { + announcements: [{ + prefixes: ["82.112.100.0/24"], // Valid + next_hop: "124.0.0.3" + }], + peer: "124.0.0.4", + path: [1, 2, 3, 4321, 2914] + }, + type: "ris_message" + }, + { + data: { + announcements: [{ + prefixes: ["8.8.8.8/22"], // Not covered + next_hop: "124.0.0.3" + }], + peer: "124.0.0.4", + path: [1, 2, 3, 4321, 5060, 2914] + }, + type: "ris_message" + }, + { + data: { + announcements: [{ + prefixes: ["103.21.244.0/24"], // Invalid + next_hop: "124.0.0.3" + }], + peer: "124.0.0.4", + path: [1, 2, 3, 4321, 13335] + }, + type: "ris_message" + } + ]; + break; default: return; } diff --git a/src/env.js b/src/env.js index 37133dc..ec3d7f8 100644 --- a/src/env.js +++ b/src/env.js @@ -106,12 +106,21 @@ let config = { params: { thresholdMinPeers: 2 } + }, + { + file: "monitorRPKI", + channel: "rpki", + name: "rpki-monitor", + params: { + thresholdMinPeers: 1, + checkUncovered: false + } } ], reports: [ { file: "reportFile", - channels: ["hijack", "newprefix", "visibility", "path", "misconfiguration"] + channels: ["hijack", "newprefix", "visibility", "path", "misconfiguration", "rpki"] } ], notificationIntervalSeconds: 14400, diff --git a/src/monitors/monitor.js b/src/monitors/monitor.js index 7340f6b..e350b2d 100644 --- a/src/monitors/monitor.js +++ b/src/monitors/monitor.js @@ -184,6 +184,16 @@ export default class Monitor { return alert; }; + getMonitoredAsMatch = (originAS) => { + const monitored = this.input.getMonitoredASns(); + + for (let m of monitored) { + if (originAS.includes(m.asn)) { + return m; + } + } + }; + getMoreSpecificMatch = (prefix, includeIgnoredMorespecifics) => { const matched = this.input.getMoreSpecificMatch(prefix, includeIgnoredMorespecifics); diff --git a/src/monitors/monitorAS.js b/src/monitors/monitorAS.js index 981ed25..8cabe17 100644 --- a/src/monitors/monitorAS.js +++ b/src/monitors/monitorAS.js @@ -41,7 +41,7 @@ export default class MonitorAS extends Monitor { }; updateMonitoredResources = () => { - this.monitored = this.input.getMonitoredASns(); + // nothing }; filter = (message) => { @@ -75,22 +75,12 @@ export default class MonitorAS extends Monitor { return false; }; - _getMonitoredAS = (message) => { - const monitored = this.monitored; - - for (let m of monitored) { - if (message.originAS.includes(m.asn)) { - return m; - } - } - }; - monitor = (message) => new Promise((resolve, reject) => { const messageOrigin = message.originAS; const messagePrefix = message.prefix; - const matchedRule = this._getMonitoredAS(message); + const matchedRule = this.getMonitoredAsMatch(messageOrigin); if (matchedRule) { diff --git a/src/monitors/monitorRPKI.js b/src/monitors/monitorRPKI.js index a18dec1..cadd5dd 100644 --- a/src/monitors/monitorRPKI.js +++ b/src/monitors/monitorRPKI.js @@ -8,37 +8,53 @@ export default class MonitorRPKI extends Monitor { this.input.onChange(() => { this.updateMonitoredResources(); }); + this.validationQueue = []; - rpki.preCache(60) - .then(() => { - setInterval(this.validateBatch, 400); - }) - + if (this.params.preCacheROAs) { + rpki.preCache(Math.max(this.params.refreshVrpListMinutes, 15)) + .then(() => { + console.log("Downloaded"); + // setInterval(this.validateBatch, 400); + }) + .catch(() => { + this.logger.log({ + level: 'error', + message: "One of the VRPs lists cannot be downloaded. Anyway, the RPKI monitoring should be working." + }); + }); + } else { + setInterval(this.validateBatch, 400); + } }; updateMonitoredResources = () => { - // nothing + this.monitored = this.input.getMonitoredASns(); }; - validateBatch = () => { - const queue = this.validationQueue; - this.validationQueue = []; - queue.forEach(this.validate); + validateBatch = () => { + this.validationQueue.forEach(this.validate); + this.validationQueue = []; }; filter = (message) => { - return message.type === 'announcement' && message.originAS.numbers.length == 1; + return message.type === 'announcement' && message.originAS.numbers.length === 1; }; squashAlerts = (alerts) => { - const message = alerts[0].matchedMessage; - const covering = (alerts[0].extra.covering && alerts[0].extra.covering[0]) ? alerts[0].extra.covering[0] : false; + const firstAlert = alerts[0]; + const message = firstAlert.matchedMessage; + const extra = firstAlert.extra; + const covering = (extra.covering && extra.covering[0]) ? extra.covering[0] : false; const coveringString = (covering) ? `Valid ROA: origin AS${covering.origin} prefix ${covering.prefix} max length ${covering.maxLength}` : ''; - return `The route ${message.prefix} announced by ${message.originAS} is not RPKI valid. Accepted with AS path: ${message.path}. ${coveringString}`; + if (extra.valid === null && this.params.checkUncovered) { + return `The route ${message.prefix} announced by ${message.originAS} is not covered by a ROA. Accepted with AS path: ${message.path}`; + } else { + return `The route ${message.prefix} announced by ${message.originAS} is not RPKI valid. Accepted with AS path: ${message.path}. ${coveringString}`; + } }; @@ -46,31 +62,54 @@ export default class MonitorRPKI extends Monitor { const prefix = message.prefix; const origin = message.originAS.getValue(); - const result = rpki.validateFromCacheSync(prefix, origin, true); + rpki.validate(prefix, origin, true) + .then(result => { + if (result) { + const key = "a" + [prefix, origin] + .join("AS") + .replace(/\./g, "_") + .replace(/\:/g, "_") + .replace(/\//g, "_"); + + if (result.valid === false) { + this.publishAlert(key, + prefix, + matchedRule, + message, + { covering: result.covering, valid: result.valid }); + } else if (result.valid === null && this.params.checkUncovered) { + this.publishAlert(key, + prefix, + matchedRule, + message, + { covering: null, valid: null }); + } + } + }) + .catch(error => { + this.logger.log({ + level: 'error', + message: error + }); + }); - if (result.valid === false) { - const key = "a" + [prefix, origin] - .join("AS") - .replace(/\./g, "_") - .replace(/\:/g, "_") - .replace(/\//g, "_"); - this.publishAlert(key, - prefix, - matchedRule, - message, - { covering: result.covering }); - } }; monitor = (message) => { - const prefix = message.prefix; - const matchedRule = this.input.getMoreSpecificMatch(prefix, false); - if (matchedRule) { - this.validationQueue.push({ message, matchedRule }); + const messageOrigin = message.originAS; + const prefix = message.prefix; + const matchedASRule = this.getMonitoredAsMatch(messageOrigin); + const matchedPrefixRule = this.getMoreSpecificMatch(prefix, false); + + if (matchedPrefixRule) { + this.validationQueue.push({ message, matchedRule: matchedPrefixRule }); + } else if (matchedASRule) { + this.validationQueue.push({ message, matchedRule: matchedASRule }); } + return Promise.resolve(true); }; diff --git a/src/monitors/monitorSwUpdates.js b/src/monitors/monitorSwUpdates.js index b4cba0e..915a234 100644 --- a/src/monitors/monitorSwUpdates.js +++ b/src/monitors/monitorSwUpdates.js @@ -39,7 +39,7 @@ export default class MonitorSwUpdates extends Monitor { }; updateMonitoredResources = () => { - // throw new Error('The method updateMonitoredResources must be implemented in ' + this.name); + // nothing }; filter = (message) => { diff --git a/src/reports/report.js b/src/reports/report.js index 530918e..2a78e6e 100644 --- a/src/reports/report.js +++ b/src/reports/report.js @@ -145,11 +145,20 @@ export default class Report { case "misconfiguration": context.asn = content.data[0].matchedRule.asn.toString(); + break; + case "rpki": + matched = content.data[0].matchedRule; + context.asn = matched.asn.toString(); + context.prefix = matched.prefix; + context.description = matched.description; break; default: - return false; + matched = content.data[0].matchedRule; + context.prefix = matched.prefix; + context.description = matched.description; + context.asn = matched.asn.toString(); } return context; diff --git a/tests/1_config.js b/tests/1_config.js index 43686cd..ee914a0 100644 --- a/tests/1_config.js +++ b/tests/1_config.js @@ -97,7 +97,7 @@ describe("Composition", function() { it("loading monitors", function () { - expect(config.monitors.length).to.equal(6); + expect(config.monitors.length).to.equal(7); expect(config.monitors[0]).to .containSubset({ @@ -144,6 +144,18 @@ describe("Composition", function() { } }); + expect(config.monitors[5]).to + .containSubset({ + "channel": "rpki", + "name": "rpki-monitor", + "params": { + "thresholdMinPeers": 1, + "preCacheROAs": false, + "refreshVrpListMinutes": 15, + "checkUncovered": true + } + }); + expect(config.monitors[config.monitors.length - 1]).to .containSubset({ "channel": "software-update", @@ -297,7 +309,7 @@ describe("Composition", function() { ] }); - expect(input.asns.map(i => i.asn.getValue())).to.eql([ 2914, 3333, 65000 ]); + expect(input.asns.map(i => i.asn.getValue())).to.eql([ 2914, 3333, 13335, 65000 ]); }); }); diff --git a/tests/2_alerting.js b/tests/2_alerting.js index 85c8dd0..efe4bc0 100644 --- a/tests/2_alerting.js +++ b/tests/2_alerting.js @@ -477,8 +477,6 @@ describe("Alerting", function () { }).timeout(asyncTimeout); - - it("asn monitoring reporting", function (done) { pubSub.publish("test-type", "misconfiguration"); @@ -529,6 +527,57 @@ describe("Alerting", function () { }).timeout(asyncTimeout); + it("RPKI monitoring", function (done) { + + pubSub.publish("test-type", "rpki"); + + const expectedData = { + + "a103_21_244_0_24AS13335": { + id: "a103_21_244_0_24AS13335", + origin: 'rpki-monitor', + affected: '103.21.244.0/24', + message: 'The route 103.21.244.0/24 announced by AS13335 is not RPKI valid. Accepted with AS path: [1,2,3,4321,13335]. Valid ROA: origin AS0 prefix 103.21.244.0/23 max length 23', + }, + + "a8_8_8_8_22AS2914": { + id: "a8_8_8_8_22AS2914", + origin: 'rpki-monitor', + affected: '8.8.8.8/22', + message: 'The route 8.8.8.8/22 announced by AS2914 is not covered by a ROA. Accepted with AS path: [1,2,3,4321,5060,2914]', + } + }; + + let rpkiTestCompleted = false; + pubSub.subscribe("rpki", function (type, message) { + + if (!rpkiTestCompleted) { + message = JSON.parse(JSON.stringify(message)); + const id = message.id; + + expect(Object.keys(expectedData).includes(id)).to.equal(true); + expect(expectedData[id] != null).to.equal(true); + + expect(message).to + .containSubset(expectedData[id]); + + expect(message).to.contain + .keys([ + "latest", + "earliest" + ]); + + delete expectedData[id]; + if (Object.keys(expectedData).length === 0) { + setTimeout(() => { + rpkiTestCompleted = true; + done(); + }, 5000); + } + } + }); + + }).timeout(asyncTimeout); it("fading alerting", function (done) { diff --git a/tests/config.test.yml b/tests/config.test.yml index b8c3b9f..667d78f 100644 --- a/tests/config.test.yml +++ b/tests/config.test.yml @@ -37,6 +37,16 @@ monitors: params: thresholdMinPeers: 2 + - file: monitorRPKI + channel: rpki + name: rpki-monitor + params: + thresholdMinPeers: 1 + preCacheROAs: false + refreshVrpListMinutes: 15 + checkUncovered: true + + reports: - file: reportFile channels: @@ -45,14 +55,12 @@ reports: - visibility - path - misconfiguration + - rpki params: persistAlertData: false alertDataDirectory: alertdata/ -notificationIntervalSeconds: 1800 # Repeat the same alert (which keeps being triggered) after x seconds -alertOnlyOnce: false -fadeOffSeconds: 10 -checkFadeOffGroupsSeconds: 2 + # The file containing the monitored prefixes. Please see monitored_prefixes_test.yml for an example # This is an array (use new lines and dashes!) @@ -76,6 +84,11 @@ processMonitors: host: null port: 8011 + +notificationIntervalSeconds: 1800 # Repeat the same alert (which keeps being triggered) after x seconds +alertOnlyOnce: false +fadeOffSeconds: 10 +checkFadeOffGroupsSeconds: 2 pidFile: bgpalerter.pid multiProcess: false maxMessagesPerSecond: 6000 \ No newline at end of file diff --git a/tests/prefixes.test.yml b/tests/prefixes.test.yml index 75f1d06..fedf18c 100644 --- a/tests/prefixes.test.yml +++ b/tests/prefixes.test.yml @@ -101,4 +101,6 @@ options: 2914: group: default 3333: + group: default + 13335: group: default \ No newline at end of file