1
0
mirror of https://github.com/nttgin/BGPalerter.git synced 2024-05-19 06:50:08 +00:00

first release of expring ROAs alerting + improvements on diffing

This commit is contained in:
Massimo Candela
2021-05-22 19:09:58 +02:00
parent 863ded5538
commit 524f276283
11 changed files with 314 additions and 72 deletions

View File

@@ -53,6 +53,11 @@ monitors:
- file: monitorROAS
channel: rpki
name: rpki-diff
params:
enableDiffAlerts: true,
enableExpirationAlerts: true,
roaExpirationAlertHours: 2,
checkOnlyAsns: false
- file: monitorPathNeighbors
channel: hijack

View File

@@ -343,19 +343,48 @@ Note, while BGPalerter will perform the check near real time, many RIRs have del
> asn: 1234
> description: an example
> ignoreMorespecifics: false
> group: noc1
>
> options:
> monitorASns:
> 2914:
> group: default
> group: noc2
> ```
> If in config.yml monitorROAS is enabled, you will receive alerts every time:
> * A ROA that is, or was, involving 1.2.3.4/24 is added/edited/removed.
> * A ROA that is, or was, involving AS2914 is added/edited/removed.
> * A ROA that is, or was, involving 1.2.3.4/24 is added/edited/removed (based on the prefix `1.2.3.4/24` matching rule).
> * A ROA that is, or was, involving AS2914 is added/edited/removed (based on the `monitorASns` section).
**Important 1:** for a complete monitoring, configure also the `monitorASns` section. Setting only prefix matching rules is not sufficient: prefix matching rules are based on the longest prefix match, less specific ROAs impacting the prefix will NOT be matched. On the other side, setting only the `monitorASns` section is instead perfectly fine for ROA monitoring purposes.
**Important 2:** prefix matching rules have always priorities on `monitorASns` rules. If an alert matches both a prefix rule and an AS rule, it will be sent only to the prefix rule, except if the `checkOnlyAsns` params is set to true (see parameters below). In the example above, a ROA change impacting `1.2.3.4/24` is only sent to the user group `noc1` and not to `noc2`; whatever other ROA change impacting a prefix not in the list (no prefix matching rule) will be sent to `noc2` instead.
Example of alerts:
> ROAs change detected: removed <1.2.3.4/24, 1234, 25, apnic>
> ROAs change detected: removed <1.2.3.4/24, 1234, 25, apnic>; added <5.5.3.4/24, 1234, 25, apnic>
**This monitor also alerts about ROAs expiration.**
This feature requires a vrps file having a `expires` field for each vrp, currently supported only by [rpki-client](https://www.rpki-client.org/). To enable this feature, provide a file having such field or use as vrp provider one of: `ntt`, `rpkiclient` ([more info](rpki.md)).
ROAs are affected by a series of expiration times:
* Certificate Authority's "notAfter" date;
* each CRL's "nextUpdate" date;
* each manifest's EE certificate notAfter, and each manifests eContent "nextUpdate";
* the ROA's own EE certificate "notAfter".
The field `expire` must be the closest expiration time of all of the above.
Example of alerts:
> The following ROAs will expire in less than 2 hours: <1.2.3.4/24, 1234, 25, apnic>; <5.5.3.4/24, 1234, 25, apnic>
Parameters for this monitor module:
|Parameter| Description|
|---|---|
|enableDiffAlerts| Enables alerts showing edits impacting ROAs for the monitored resources. Default true|
|enableExpirationAlerts| Enables alerts about expiring ROAs. Default true.|
|roaExpirationAlertHours| If a ROA is expiring in less than this amount of hours, an alert will be triggered. The default is 2 hours. I strongly suggest to keep this value, ROAs are almost expiring every day, read above what this expiration time means. |
|checkOnlyAsns| If set to true (default false), ROAs diff alerts will be generated based only on the ASns contained in the `monitorASns` of `prefixes.yml`. This means that no ROA diffs will be matched against prefix matching rules (see example above). |
#### monitorPathNeighbors

14
package-lock.json generated
View File

@@ -27,7 +27,7 @@
"nodemailer": "^6.6.0",
"path": "^0.12.7",
"restify": "^8.5.1",
"rpki-validator": "^2.6.0",
"rpki-validator": "^2.6.1",
"semver": "^7.3.5",
"syslog-client": "^1.1.1",
"ws": "^7.4.5",
@@ -6361,9 +6361,9 @@
}
},
"node_modules/rpki-validator": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/rpki-validator/-/rpki-validator-2.6.0.tgz",
"integrity": "sha512-XTybuQPein6VYerScna2ifTknHJv0pV5vRS7tGLvOLaVLC/qmO6TG/wRYVEVxMkSagMdVOu96s6SYYZ0//DHsA==",
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/rpki-validator/-/rpki-validator-2.6.1.tgz",
"integrity": "sha512-vnUrq8WVSRufuEsBbhFYtGw89ceRghQQQ3NmFnOQp/SsH61twoHlpkbWI2ZmFHW5t7sPUC693+OStDo73uzHbQ==",
"dependencies": {
"axios": "^0.21.1",
"brembo": "^2.0.5",
@@ -12760,9 +12760,9 @@
}
},
"rpki-validator": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/rpki-validator/-/rpki-validator-2.6.0.tgz",
"integrity": "sha512-XTybuQPein6VYerScna2ifTknHJv0pV5vRS7tGLvOLaVLC/qmO6TG/wRYVEVxMkSagMdVOu96s6SYYZ0//DHsA==",
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/rpki-validator/-/rpki-validator-2.6.1.tgz",
"integrity": "sha512-vnUrq8WVSRufuEsBbhFYtGw89ceRghQQQ3NmFnOQp/SsH61twoHlpkbWI2ZmFHW5t7sPUC693+OStDo73uzHbQ==",
"requires": {
"axios": "^0.21.1",
"brembo": "^2.0.5",

View File

@@ -75,7 +75,7 @@
"nodemailer": "^6.6.0",
"path": "^0.12.7",
"restify": "^8.5.1",
"rpki-validator": "^2.6.0",
"rpki-validator": "^2.6.1",
"semver": "^7.3.5",
"syslog-client": "^1.1.1",
"ws": "^7.4.5",

View File

@@ -77,7 +77,12 @@ export default class Config {
file: "monitorROAS",
channel: "rpki",
name: "rpki-diff",
params: {}
params: {
enableDiffAlerts: true,
enableExpirationAlerts: true,
roaExpirationAlertHours: 2,
checkOnlyAsns: false
}
},
{
file: "monitorPathNeighbors",

View File

@@ -1,7 +1,8 @@
import Monitor from "./monitor";
import md5 from "md5";
import diff from "../utils/rpkiDiffingTool";
import { getPrefixes, getRelevant, diff } from "../utils/rpkiDiffingTool";
import {AS} from "../model";
import moment from "moment";
export default class MonitorROAS extends Monitor {
@@ -10,29 +11,74 @@ export default class MonitorROAS extends Monitor {
this.logger = env.logger;
this.rpki = env.rpki;
setInterval(this._diffVrps, 20000);
this.roaExpirationAlertHours = params.roaExpirationAlertHours || 2;
this.checkOnlyAsns = params.checkOnlyAsns || false;
this.enableDiffAlerts = params.enableDiffAlerts != null ? params.enableDiffAlerts : true;
this.enableExpirationAlerts = params.enableExpirationAlerts != null ? params.enableExpirationAlerts : true;
if (this.enableDiffAlerts) {
setInterval(this._diffVrps, 20000);
}
if (this.enableExpirationAlerts) {
setInterval(this._verifyExpiration, global.EXTERNAL_ROA_EXPIRATION_TEST || 440000);
}
};
_diffVrps = () => {
_verifyExpiration = () => {
const vrps = this.rpki.getVrps()
.filter(i => !!i.expires && (i.expires - moment.utc().unix() < this.roaExpirationAlertHours * 3600));
// We can check here if too many vrps are expiring, maybe TA malfunction
const prefixesIn = this.monitored.prefixes.map(i => i.prefix);
const asnsIn = this.monitored.asns.map(i => i.asn.getValue());
const relevantVrps = getRelevant(vrps, prefixesIn, asnsIn);
let alerts = [];
if (relevantVrps.length) {
if (!this.checkOnlyAsns) {
alerts = this._checkExpirationPrefixes(relevantVrps);
}
for (let asn of asnsIn) {
this._checkExpirationAs(relevantVrps, asn, alerts);
}
}
};
_checkExpirationPrefixes = (vrps) => {
let alerts = [];
for (let prefix of [...new Set(vrps.map(i => i.prefix))]) {
const roas = vrps.filter(i => i.prefix === prefix); // Get only the ROAs for this prefix
const matchedRule = this.getMoreSpecificMatch(prefix, false); // Get the matching rule
if (matchedRule) {
const alertsStrings = [...new Set(roas.map(this._roaToString))];
const message = `The following ROAs will expire in less than ${this.roaExpirationAlertHours} hours: ${alertsStrings.join("; ")}`;
alerts = alerts.concat(alertsStrings);
this.publishAlert(md5(message), // The hash will prevent alert duplications in case multiple ASes/prefixes are involved
matchedRule.prefix,
matchedRule,
message,
{});
}
}
return alerts;
};
_checkExpirationAs = (vrps, asn, sent) => {
try {
let roaDiff;
const newVrps = this.rpki.getVrps(); // Get all the vrps as retrieved from the rpki validator
let alerts = [];
const impactedASes = [...new Set(vrps.map(i => i.asn))];
const matchedRules = impactedASes.map(asn => this.getMonitoredAsMatch(new AS(asn)));
if (this._oldVrps) { // No diff if there were no vrps before
roaDiff = [].concat.apply([], this.monitored
.map(i => diff(this._oldVrps, newVrps, i.asn.getValue()))); // Get the diff for each monitored AS
}
if (newVrps.length) {
this._oldVrps = newVrps;
}
if (roaDiff && roaDiff.length) { // Differences found
const impactedASes = [...new Set(roaDiff.map(i => i.asn))];
const matchedRules = impactedASes.map(asn => this.getMonitoredAsMatch(new AS(asn)));
for (let matchedRule of matchedRules.filter(i => !!i)) { // An alert for each AS involved (they may have different user group)
const message = "ROAs change detected: " + [...new Set(roaDiff.map(this._roaToString))].join("; ");
for (let matchedRule of matchedRules.filter(i => !!i)) { // An alert for each AS involved (they may have different user group)
const alertsStrings = [...new Set(vrps.map(this._roaToString))].filter(i => !sent.includes(i));
if (alertsStrings.length) {
const message = `The following ROAs will expire in less than ${this.roaExpirationAlertHours} hours: ${alertsStrings.join("; ")}`;
alerts = alerts.concat(alertsStrings);
this.publishAlert(md5(message), // The hash will prevent alert duplications in case multiple ASes/prefixes are involved
matchedRule.asn.getId(),
@@ -41,6 +87,95 @@ export default class MonitorROAS extends Monitor {
{});
}
}
return alerts;
} catch (error) {
this.logger.log({
level: 'error',
message: error
});
}
};
_diffVrps = () => {
const newVrps = this.rpki.getVrps(); // Get all the vrps as retrieved from the rpki validator
if (this._oldVrps) { // No diff if there were no vrps before
const prefixesIn = this.monitored.prefixes.map(i => i.prefix);
const asns = this.monitored.asns.map(i => i.asn.getValue());
let alerts = [];
if (!this.checkOnlyAsns){
alerts = this._diffVrpsPrefixes(this._oldVrps, newVrps, prefixesIn);
}
for (let asn of asns) {
this._diffVrpsAs(this._oldVrps, newVrps, asn, alerts);
}
}
if (newVrps.length) {
this._oldVrps = newVrps;
}
};
_diffVrpsPrefixes = (oldVrps, newVrps, prefixesIn) => {
try {
const roaDiff = diff(oldVrps, newVrps, [], prefixesIn);
let alerts = [];
if (roaDiff && roaDiff.length) { // Differences found
for (let prefix of [...new Set(roaDiff.map(i => i.prefix))]) {
const roas = roaDiff.filter(i => i.prefix === prefix); // Get only the ROAs for this prefix
const matchedRule = this.getMoreSpecificMatch(prefix, false); // Get the matching rule
if (matchedRule) {
const alertsStrings = [...new Set(roas.map(this._roaToString))];
const message = `ROAs change detected: ${alertsStrings.join("; ")}`;
alerts = alerts.concat(alertsStrings);
this.publishAlert(md5(message), // The hash will prevent alert duplications in case multiple ASes/prefixes are involved
matchedRule.prefix,
matchedRule,
message,
{});
}
}
}
return alerts;
} catch (error) {
this.logger.log({
level: 'error',
message: error
});
}
};
_diffVrpsAs = (oldVrps, newVrps, asn, sent) => {
try {
const roaDiff = diff(oldVrps, newVrps, asn, []);
let alerts = [];
if (roaDiff && roaDiff.length) { // Differences found
const impactedASes = [...new Set(roaDiff.map(i => i.asn))];
const matchedRules = impactedASes.map(asn => this.getMonitoredAsMatch(new AS(asn)));
for (let matchedRule of matchedRules.filter(i => !!i)) { // An alert for each AS involved (they may have different user group)
const alertsStrings = [...new Set(roaDiff.map(this._roaToString))].filter(i => !sent.includes(i));
if (alertsStrings.length) {
const message = `ROAs change detected: ${alertsStrings.join("; ")}`;
alerts = alerts.concat(alertsStrings);
this.publishAlert(md5(message), // The hash will prevent alert duplications in case multiple ASes/prefixes are involved
matchedRule.asn.getId(),
matchedRule,
message,
{});
}
}
}
return alerts;
} catch (error) {
this.logger.log({
level: 'error',
@@ -50,11 +185,18 @@ export default class MonitorROAS extends Monitor {
};
_roaToString = (roa) => {
return `${roa.status} <${roa.prefix}, ${roa.asn}, ${roa.maxLength}, ${roa.ta || ""}>`;
if (roa.status) {
return `${roa.status} <${roa.prefix}, ${roa.asn}, ${roa.maxLength}, ${roa.ta || ""}>`;
} else {
return `<${roa.prefix}, ${roa.asn}, ${roa.maxLength}, ${roa.ta || ""}>`;
}
};
updateMonitoredResources = () => {
this.monitored = this.input.getMonitoredASns();
this.monitored = {
asns: this.input.getMonitoredASns(),
prefixes: this.input.getMonitoredPrefixes()
}
};
filter = (message) => false;

View File

@@ -1,35 +1,45 @@
export default function diff (vrpsOld, vrpsNew, asn) {
const getKey = (vrp) => {
return `${vrp.ta}-${vrp.prefix}-${vrp.asn}-${vrp.maxLength}`;
};
const getDiff = (vrpsOld, vrpsNew, asn) => {
asn = parseInt(asn);
const prefixes = [...new Set(vrpsOld.concat(vrpsNew).filter(i => i.asn === asn).map(i => i.prefix))];
const filteredVrpsOld = vrpsOld.filter(i => i.asn === asn || prefixes.includes(i.prefix))
.map(i => {
i.status = "removed";
return i;
});
const filteredVrpsNew = vrpsNew.filter(i => i.asn === asn || prefixes.includes(i.prefix))
.map(i => {
i.status = "added";
return i;
});
const index = {};
for (let vrp of filteredVrpsOld.concat(filteredVrpsNew)) {
const key = getKey(vrp);
index[key] = index[key] || [];
index[key].push(vrp);
}
return Object.values(index).filter(i => i.length === 1).map(i => i[0]);
};
return getDiff(vrpsOld, vrpsNew, asn);
function getPrefixes(vrp, asn) {
return [...new Set(vrp.filter(i => i.asn === asn).map(i => i.prefix))];
}
function getRelevant(vrp, prefixes, asns=[]){
return vrp.filter(i => asns.includes(i.asn) || prefixes.includes(i.prefix));
}
function diff(vrpsOld, vrpsNew, asn, prefixesIn=[]) {
asn = parseInt(asn);
let prefixes;
if (asn) {
prefixes = [...new Set(prefixesIn)];
} else {
prefixes = [...new Set([...prefixesIn, ...getPrefixes(vrpsOld, asn), ...getPrefixes(vrpsNew, asn)])];
}
const filteredVrpsOld = JSON.parse(JSON.stringify(getRelevant(vrpsOld, prefixes, [asn])))
.map(i => {
i.status = "removed";
return i;
});
const filteredVrpsNew = JSON.parse(JSON.stringify(getRelevant(vrpsNew, prefixes, [asn])))
.map(i => {
i.status = "added";
return i;
});
const index = {};
for (let vrp of filteredVrpsOld.concat(filteredVrpsNew)) {
const key = `${vrp.ta}-${vrp.prefix}-${vrp.asn}-${vrp.maxLength}`;
index[key] = index[key] || [];
index[key].push(vrp);
}
return Object.values(index).filter(i => i.length === 1).map(i => i[0]);
}
export {
getPrefixes,
getRelevant,
diff
};

View File

@@ -3,7 +3,8 @@
"asn": "2914",
"prefix": "1.2.3.0/24",
"maxLength": 24,
"ta": "ripe"
"ta": "ripe",
"expires": 1621691602
},
{
"asn": 2914,

View File

@@ -16,5 +16,17 @@
"prefix": "2001:db8:123::/48",
"maxLength": 48,
"ta": "ripe"
},
{
"asn": 65000,
"prefix": "2001:db8:123::/48",
"maxLength": 48,
"ta": "ripe"
},
{
"asn": 2914,
"prefix": "94.5.4.3/22",
"maxLength": 22,
"ta": "ripe"
}
]

View File

@@ -5,10 +5,28 @@
"maxLength": 24,
"ta": "ripe"
},
{
"asn": "AS2914",
"prefix": "2.3.4.0/24",
"maxLength": 24,
"ta": "ripe"
},
{
"asn": 2914,
"prefix": "2001:db8:123::/48",
"maxLength": 48,
"ta": "ripe"
},
{
"asn": 65000,
"prefix": "2001:db8:123::/48",
"maxLength": 48,
"ta": "ripe"
},
{
"asn": 2914,
"prefix": "94.5.4.3/22",
"maxLength": 22,
"ta": "ripe"
}
]

View File

@@ -38,6 +38,7 @@ const asyncTimeout = 200000;
chai.use(chaiSubset);
global.EXTERNAL_CONFIG_FILE = "tests/rpki_tests/config.rpki.test.external-roas.yml";
global.EXTERNAL_ROA_EXPIRATION_TEST = 5000;
// ROAs before
fs.copyFileSync("tests/rpki_tests/roas.before.json", "tests/rpki_tests/roas.json");
@@ -52,10 +53,31 @@ const pubSub = worker.pubSub;
describe("RPKI monitoring external", function() {
it("ROA diff - external connector", function (done) {
it("ROA diff and expiration - external connector", function (done) {
const expectedData = {
"28c7aa78b6286e0e3c6583797f7df47c": {
id: '28c7aa78b6286e0e3c6583797f7df47c',
truncated: false,
origin: 'rpki-monitor',
affected: 2914,
message: 'The following ROAs will expire in less than 2 hours: <1.2.3.0/24, 2914, 24, ripe>',
},
"47807c7558dbe001b4aad9f3a87eb427": {
id: '47807c7558dbe001b4aad9f3a87eb427',
truncated: false,
origin: 'rpki-monitor',
affected: '94.5.4.3/22',
message: 'ROAs change detected: removed <94.5.4.3/22, 2914, 22, ripe>'
},
"de3bd9a6cdeeb05e1c2c7c04f7220485" : {
id: 'de3bd9a6cdeeb05e1c2c7c04f7220485',
truncated: false,
origin: 'rpki-monitor',
affected: '2001:db8:123::/48',
message: 'ROAs change detected: removed <2001:db8:123::/48, 65000, 48, ripe>'
},
"129aafe3c8402fb045b71e810a73d425": {
id: '129aafe3c8402fb045b71e810a73d425',
truncated: false,
@@ -63,12 +85,10 @@ describe("RPKI monitoring external", function() {
affected: 2914,
message: 'ROAs change detected: removed <2.3.4.0/24, 2914, 24, ripe>'
}
};
let rpkiTestCompletedExternal = false;
pubSub.subscribe("rpki", function (message, type) {
try {
if (!rpkiTestCompletedExternal) {
message = JSON.parse(JSON.stringify(message));