mirror of
https://github.com/nttgin/BGPalerter.git
synced 2024-05-19 06:50:08 +00:00
introduced monitoring of neighbors in AS path
This commit is contained in:
@@ -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).
|
||||
|
@@ -54,6 +54,12 @@ monitors:
|
||||
channel: rpki
|
||||
name: rpki-diff
|
||||
|
||||
- file: monitorPathPoisoning
|
||||
channel: hijack
|
||||
name: path-poisoning
|
||||
params:
|
||||
thresholdMinPeers: 3
|
||||
|
||||
reports:
|
||||
- file: reportFile
|
||||
channels:
|
||||
|
@@ -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
|
||||
|
||||
|
68
docs/path-poisoning.md
Normal file
68
docs/path-poisoning.md
Normal file
@@ -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`.|
|
@@ -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 |
|
||||
|
||||
|
||||
## <a name="prefixes-fields"></a>Prefixes list fields
|
||||
|
10
index.js
10
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")));
|
||||
}
|
||||
|
44
package-lock.json
generated
44
package-lock.json
generated
@@ -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",
|
||||
|
@@ -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",
|
||||
|
79
prefixes.yml.bak
Normal file
79
prefixes.yml.bak
Normal file
@@ -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
|
@@ -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: [
|
||||
|
@@ -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({
|
||||
|
@@ -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(() => {
|
||||
|
@@ -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.");
|
||||
}
|
||||
|
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
25
src/model.js
25
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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
120
src/monitors/monitorPathPoisoning.js
Normal file
120
src/monitors/monitorPathPoisoning.js
Normal file
@@ -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);
|
||||
});
|
||||
|
||||
}
|
Reference in New Issue
Block a user