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

Merge pull request #220 from nttgin/rpki

RPKI monitoring of AS
This commit is contained in:
Massimo Candela
2020-04-29 00:01:10 +02:00
committed by GitHub
15 changed files with 288 additions and 62 deletions

View File

@@ -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}"}'

View File

@@ -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:

7
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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);
};

View File

@@ -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) => {

View File

@@ -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;

View File

@@ -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 ]);
});
});

View File

@@ -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) {

View File

@@ -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

View File

@@ -101,4 +101,6 @@ options:
2914:
group: default
3333:
group: default
13335:
group: default