Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sflow-rt
GitHub Repository: sflow-rt/ddos-protect
Path: blob/master/scripts/ddos.js
50 views
1
// author: InMon
2
// version: 2.7
3
// date: 3/5/2025
4
// description: Use BGP to mitigate DDoS flood attacks
5
// copyright: Copyright (c) 2015-2025 InMon Corp.
6
7
include(scriptdir()+'/inc/trend.js');
8
9
var router = getSystemProperty("ddos_protect.router") || '127.0.0.1';
10
var my_as = getSystemProperty("ddos_protect.as") || '65000';
11
var my_id = getSystemProperty("ddos_protect.id") || '0.6.6.6';
12
var community = getSystemProperty("ddos_protect.community") || '65535:666'; // RFC7999
13
var nexthop = getSystemProperty("ddos_protect.nexthop") || '192.0.2.1';
14
var nexthop6 = getSystemProperty("ddos_protect.nexthop6") || '100::1';
15
var localpref = getSystemProperty("ddos_protect.localpref") || '100';
16
17
var route_max = getSystemProperty("ddos_protect.maxroutes") || '1000';
18
var flowspec_max = getSystemProperty("ddos_protect.maxflows") || '100';
19
20
var ipv6_enable = getSystemProperty("ddos_protect.enable.ipv6") === 'yes';
21
var flowspec_enable = getSystemProperty("ddos_protect.enable.flowspec") === 'yes';
22
var flowspec6_enable = getSystemProperty("ddos_protect.enable.flowspec6") === 'yes';
23
24
var flowspec_dscp = getSystemProperty("ddos_protect.flowspec.dscp") || 'le';
25
var flowspec_rate = getSystemProperty("ddos_protect.flowspec.rate") || 12500; // 100Kbps
26
var flowspec_redirect_method = getSystemProperty("ddos_protect.flowspec.redirect.method") || 'as';
27
var flowspec_redirect_as = getSystemProperty("ddos_protect.flowspec.redirect.as") || '65000:666';
28
var flowspec_redirect_as4 = getSystemProperty("ddos_protect.flowspec.redirect.as4") || '65000:666';
29
var flowspec_redirect_ip = getSystemProperty("ddos_protect.flowspec.redirect.ip") || '192.0.2.1:666';
30
var flowspec_redirect_nexthop = getSystemProperty("ddos_protect.flowspec.redirect.nexthop") || '192.0.2.1';
31
var flowspec_redirect_nexthop6 = getSystemProperty("ddos_protect.flowspec.redirect.nexthop6") || '100::1';
32
var flowspec_redirect_ipaction = getSystemProperty("ddos_protect.flowspec.redirect.ipaction") || '192.0.2.1';
33
var flowspec_community = getSystemProperty("ddos_protect.flowspec.community") || '128:6:0'; // drop
34
35
var sourceCountFilterFlag = getSystemProperty("ddos_protect.scf") === 'yes';
36
var sourceCountFilterThreshold = getSystemProperty("ddos_protect.scf_sources") || '10';
37
var flow_t = getSystemProperty("ddos_protect.flow_seconds") || '2';
38
var threshold_t = getSystemProperty("ddos_protect.threshold_seconds") || '60';
39
40
var externalGroup = getSystemProperty("ddos_protect.externalgroup") || 'external';
41
var excludedGroups = getSystemProperty("ddos_protect.excludedgroups") || 'external,private,multicast,exclude';
42
var bgpGroup = getSystemProperty("ddos_protect.bgpgroup");
43
var externalGroupSet = externalGroup ? new Set(externalGroup.split(',')) : new Set();
44
45
var syslogHost = getSystemProperty("ddos_protect.syslog.host");
46
var syslogPort = getSystemProperty("ddos_protect.syslog.port") || '514';
47
var syslogFacility = getSystemProperty("ddos_protect.syslog.facility") || '16'; // local0
48
var syslogSeverity = getSystemProperty("ddos_protect.syslog.severity") || '5'; // notice
49
50
const prometheus_prefix = getSystemProperty("prometheus.metric.prefix") || 'sflow_';
51
52
var routers = router.split(',');
53
54
var syslogHosts = syslogHost ? syslogHost.split(',') : [];
55
function sendEvent(action,attack,target,group,protocol) {
56
if(syslogHosts.length === 0) return;
57
58
var msg = {app:'ddos-protect',action:action,attack:attack,ip:target,group:group,protocol:protocol};
59
syslogHosts.forEach(function(host) {
60
try {
61
syslog(host,syslogPort,syslogFacility,syslogSeverity,msg);
62
} catch(e) {
63
logWarning('DDoS cannot send syslog to ' + host);
64
}
65
});
66
}
67
68
var defaultGroups = {
69
external:['0.0.0.0/0','::/0'],
70
private:['10.0.0.0/8','172.16.0.0/12','192.168.0.0/16','169.254.0.0/16','fc00::/7'],
71
multicast:['224.0.0.0/4','ff00::/8']
72
};
73
74
getSystemPropertyNames()
75
.filter(p => p.match('^ddos_protect\\.group\\.'))
76
.forEach(function(prop) {
77
var val = getSystemProperty(prop);
78
var [,,name] = prop.split('.');
79
if(val) {
80
defaultGroups[name] = val.split(',');
81
} else {
82
delete defaultGroups[name];
83
}
84
});
85
86
var groups = storeGet('groups') || defaultGroups;
87
88
var defaultSettings = {
89
ip_flood:{threshold:1000000, timeout:180, action:'ignore', include:'', exclude:''},
90
ip_fragmentation:{threshold:500000, timeout:60, action:'ignore', include:'', exclude:''},
91
icmp_flood:{threshold:500000, timeout:60, action:'ignore', include:'', exclude:''},
92
udp_amplification:{threshold:500000, timeout:60, action:'ignore', include:'', exclude:''},
93
udp_flood:{threshold:500000, timeout:60, action:'ignore', include:'', exclude:''},
94
tcp_amplification:{threshold:500000, timeout:60, action:'ignore', include:'', exclude:''},
95
tcp_flood:{threshold:500000, timeout:60, action:'ignore', include:'', exclude:''}
96
};
97
Object.keys(defaultSettings).forEach(function(key) {
98
var entry = defaultSettings[key];
99
Object.keys(entry).forEach(function(attr) {
100
entry[attr] = getSystemProperty('ddos_protect.'+key+'.'+attr) || entry[attr];
101
});
102
});
103
var settings = Object.assign(defaultSettings, storeGet('settings'));
104
105
var controls = {};
106
107
var controlsUpdate = 0;
108
var counts = {};
109
function updateControlCounts() {
110
controlsUpdate++;
111
counts = {n:0, blocked:0, pending:0, failed:0};
112
for(var key in controls) {
113
counts.n++;
114
switch(controls[key].status) {
115
case 'blocked':
116
if(routers.reduce((flag, router_ip) => flag && controls[key].success[router_ip], true)) {
117
counts.blocked++;
118
} else {
119
counts.failed++;
120
}
121
break;
122
case 'pending':
123
counts.pending++;
124
break;
125
}
126
}
127
}
128
129
var enabled = storeGet('enabled') || ("automatic" === getSystemProperty("ddos_protect.mode")) || false;
130
131
var bgpUp = {};
132
133
function bgpBlackHole(router_ip, ctl) {
134
if(bgpRouteCount(router_ip) >= route_max) {
135
logWarning("DDoS exceeds table limit, router "+router_ip+", "+ctl.attack+" "+ctl.target+" "+ctl.group+" "+ctl.protocol);
136
ctl.success[router_ip] = false;
137
return;
138
}
139
switch(ctl.ipversion) {
140
case '4':
141
if(bgpAddRoute(router_ip,{prefix:ctl.target,nexthop:nexthop,communities:community,localpref:localpref})) {
142
ctl.success[router_ip] = true;
143
} else {
144
logWarning("DDoS failed, router "+router_ip+", "+ctl.attack+" "+ctl.target+" "+ctl.group+" "+ctl.protocol);
145
ctl.success[router_ip] = false;
146
}
147
break;
148
case '6':
149
if(ipv6_enable) {
150
if(bgpAddRoute(router_ip,{prefix:ctl.target,nexthop:nexthop6,communities:community,localpref:localpref})) {
151
ctl.success[router_ip] = true;
152
} else {
153
logWarning("DDoS failed, router "+router_ip+", "+ctl.attack+" "+ctl.target+" "+ctl.group+" "+ctl.protocol);
154
ctl.success[router_ip] = false;
155
}
156
} else {
157
logWarning("DDoS IPv6 disabled, router "+router_ip+", "+ctl.attack+" "+ctl.target+" "+ctl.group+" "+ctl.protocol);
158
ctl.success[router_ip] = false;
159
}
160
break;
161
}
162
}
163
164
function bgpFlowSpec(router_ip, ctl) {
165
if(bgpFlowCount(router_ip) >= flowspec_max) {
166
logWarning("DDoS exceeds Flowspec table limit, router "+router_ip+", "+ctl.attack+" "+ctl.target+" "+ctl.group+" "+ctl.protocol);
167
ctl.success[router_ip] = false;
168
return;
169
}
170
switch(ctl.ipversion) {
171
case '4':
172
if(flowspec_enable) {
173
if(bgpAddFlow(router_ip,ctl.flowspec)) {
174
ctl.success[router_ip] = true;
175
} else {
176
logWarning("DDoS failed, router "+router_ip+", "+ctl.attack+" "+ctl.target+" "+ctl.group+" "+ctl.protocol);
177
ctl.success[router_ip] = false;
178
}
179
} else {
180
logWarning("DDoS Flowspec disabled, router "+router_ip+", "+ctl.attack+" "+ctl.target+" "+ctl.group+" "+ctl.protocol);
181
ctl.success[router_ip] = false;
182
}
183
break;
184
case '6':
185
if(flowspec6_enable) {
186
if(bgpAddFlow(router_ip,ctl.flowspec)) {
187
ctl.success[router_ip] = true;
188
} else {
189
logWarning("DDoS failed, router "+router_ip+", "+ctl.attack+" "+ctl.target+" "+ctl.group+" "+ctl.protocol);
190
ctl.success[router_ip] = false;
191
}
192
} else {
193
logWarning("DDoS IPv6 Flowspec disabled, router "+router_ip+", "+ctl.attack+" "+ctl.target+" "+ctl.group+" "+ctl.protocol);
194
ctl.success[router_ip] = false;
195
}
196
break;
197
}
198
}
199
200
function bgpAddControl(router_ip, ctl) {
201
switch(ctl.action) {
202
case 'drop':
203
bgpBlackHole(router_ip, ctl);
204
break;
205
case 'filter':
206
ctl.flowspec.then={'traffic-rate':'0'};
207
bgpFlowSpec(router_ip, ctl);
208
break;
209
case 'mark':
210
ctl.flowspec.then={'traffic-marking':flowspec_dscp};
211
bgpFlowSpec(router_ip, ctl);
212
break;
213
case 'limit':
214
ctl.flowspec.then={'traffic-rate':flowspec_rate};
215
bgpFlowSpec(router_ip, ctl);
216
break;
217
case 'redirect':
218
ctl.flowspec.then={};
219
switch(flowspec_redirect_method) {
220
case 'as':
221
ctl.flowspec.then['redirect-as'] = flowspec_redirect_as;
222
break;
223
case 'as4':
224
ctl.flowspec.then['redirect-as4'] = flowspec_redirect_as4;
225
break;
226
case 'ip':
227
ctl.flowspec.then['redirect-ip'] = flowspec_redirect_ip;
228
break;
229
case 'nexthop':
230
switch(ctl.ipversion) {
231
case '4':
232
ctl.flowspec.then['redirect-nexthop'] = flowspec_redirect_nexthop;
233
break;
234
case '6':
235
ctl.flowspec.then['redirect-nexthop'] = flowspec_redirect_nexthop6;
236
break;
237
}
238
break;
239
case 'ipaction':
240
ctl.flowspec.then['redirect-action-ip'] = flowspec_redirect_ipaction;
241
break;
242
}
243
bgpFlowSpec(router_ip, ctl);
244
break;
245
case 'community':
246
ctl.flowspec.then={'communities':flowspec_community};
247
bgpFlowSpec(router_ip, ctl);
248
break;
249
case 'ignore':
250
break;
251
}
252
}
253
254
function bgpRemoveControl(router_ip, ctl) {
255
if(ctl.status !== 'blocked' || !ctl.success[router_ip]) return;
256
257
switch(ctl.action) {
258
case 'drop':
259
bgpRemoveRoute(router_ip, ctl.target);
260
break;
261
case 'filter':
262
case 'mark':
263
case 'limit':
264
case 'redirect':
265
case 'community':
266
bgpRemoveFlow(router_ip, ctl.flowspec);
267
break;
268
case 'ignore':
269
break;
270
}
271
}
272
273
function bgpOpen(router_ip) {
274
bgpUp[router_ip] = true;
275
276
// re-install controls
277
for(var key in controls) {
278
let ctl = controls[key];
279
if(ctl.status === 'blocked') {
280
bgpAddControl(router_ip, ctl);
281
}
282
}
283
updateControlCounts();
284
}
285
286
function bgpClose(router_ip) {
287
bgpUp[router_ip] = false;
288
289
// update control status
290
for(var key in controls) {
291
let ctl = controls[key];
292
if(ctl.status === 'blocked') {
293
ctl.success[router_ip] = false;
294
}
295
}
296
updateControlCounts();
297
}
298
299
var bgpOpts = {ipv6:ipv6_enable, flowspec:flowspec_enable, flowspec6:flowspec6_enable};
300
routers.forEach(function(router,idx) {
301
bgpAddNeighbor(router, my_as, my_id, bgpOpts, bgpOpen, bgpClose);
302
// Optionally map sFlow agents to routers to add BGP metadata to flows
303
// Note: Only needed for routers that don't support sFlow extended_gateway structure
304
var agt = getSystemProperty("ddos_protect.router."+idx+".agent");
305
if(agt) {
306
agt.split(',').forEach(agent => bgpAddSource(agent,router));
307
}
308
});
309
310
function configureGroups(groups) {
311
if(groups && Object.keys(groups).some((x) => /[<&">]/.test(x))) return false;
312
if(bgpGroup) {
313
// replace external groups since these are learned via BGP
314
let filtered = {};
315
filtered[bgpGroup] = ['0.0.0.0/0','::/0'];
316
Object.keys(groups).forEach(function(key) {
317
if(!externalGroupSet.has(key)) filtered[key] = groups[key];
318
});
319
return setGroups('ddos_protect', filtered);
320
}
321
return setGroups('ddos_protect', groups);
322
}
323
324
325
configureGroups(groups);
326
327
function protocolFilter(filter,key,setting) {
328
var result = filter;
329
if(setting.include) result = key+'='+setting.include+'&'+result;
330
if(setting.exclude) result = key+'!='+setting.exclude+'&'+result;
331
return result;
332
}
333
334
function setFlows() {
335
// IPv4 attacks
336
var keys = 'ipdestination,group:ipdestination:ddos_protect';
337
var filter = 'first:stack:.:ip:ip6=ip';
338
var value = 'frames';
339
var values = 'count:ipsource,avg:ipbytes';
340
var bgpExcludedGroups;
341
if(bgpGroup) {
342
filter += '&eq:bgpsourceas:bgpas=false&eq:bgpdestinationas:bgpas=true';
343
344
// remove the external group from excluded groups since internal / external determination made via BGP
345
bgpExcludedGroups = excludedGroups.split(',').filter(group => !externalGroupSet.has(group)).join(',');
346
if(bgpExcludedGroups) {
347
filter += '&group:ipdestination:ddos_protect!='+bgpExcludedGroups;
348
}
349
} else {
350
filter += '&group:ipsource:ddos_protect='+externalGroup+'&group:ipdestination:ddos_protect!='+excludedGroups;
351
}
352
setFlow('ddos_protect_ip_flood', {
353
keys:keys+',ipprotocol',
354
value:value,
355
values:values,
356
filter:protocolFilter(filter,'ipprotocol',settings.ip_flood),
357
aggMode:'AGENT',
358
t:flow_t
359
});
360
setFlow('ddos_protect_ip_fragmentation', {
361
keys:keys+',ipprotocol',
362
value:value,
363
values:values,
364
filter:protocolFilter('(ipflags=001|range:ipfragoffset:1=true)&'+filter,'ipprotocol',settings.ip_fragmentation),
365
aggMode:'AGENT',
366
t:flow_t
367
});
368
setFlow('ddos_protect_udp_amplification', {
369
keys:keys+',udpsourceport',
370
value:value,
371
values:values,
372
filter:protocolFilter('ipprotocol=17&'+filter,'udpsourceport',settings.udp_amplification),
373
aggMode:'AGENT',
374
t:flow_t
375
});
376
setFlow('ddos_protect_udp_flood', {
377
keys:keys+',udpdestinationport',
378
value:value,
379
values:values,
380
filter:protocolFilter('ipprotocol=17&'+filter,'udpdestinationport',settings.udp_flood),
381
aggMode:'AGENT',
382
t:flow_t
383
});
384
setFlow('ddos_protect_icmp_flood', {
385
keys:keys+',icmptype',
386
value:value,
387
values:values,
388
filter:protocolFilter('ipprotocol=1&'+filter,'icmptype',settings.icmp_flood),
389
aggMode:'AGENT',
390
t:flow_t
391
});
392
setFlow('ddos_protect_tcp_flood', {
393
keys:keys+',tcpdestinationport',
394
value:value,
395
values:values,
396
filter:protocolFilter('ipprotocol=6&'+filter,'tcpdestinationport',settings.tcp_flood),
397
aggMode:'AGENT',
398
t:flow_t
399
});
400
setFlow('ddos_protect_tcp_amplification', {
401
keys:keys+',tcpsourceport',
402
value:value,
403
values:values,
404
filter:protocolFilter('ipprotocol=6&tcpflags~....1..1.&'+filter,'tcpsourceport',settings.tcp_amplification),
405
aggMode:'AGENT',
406
t:flow_t
407
});
408
409
// IPv6 attacks
410
var keys6 = 'ip6destination,group:ip6destination:ddos_protect';
411
var values6 = 'count:ip6source,avg:ip6bytes';
412
var filter6 = 'first:stack:.:ip:ip6=ip6';
413
if(bgpGroup) {
414
filter6 += '&eq:bgpsourceas:bgpas=false&eq:bgpdestinationas:bgpas=true';
415
if(bgpExcludedGroups) {
416
filter6 += '&group:ip6destination:ddos_protect!='+bgpExcludedGroups;
417
}
418
} else {
419
filter6 += '&group:ip6source:ddos_protect='+externalGroup+'&group:ip6destination:ddos_protect!='+excludedGroups;
420
}
421
setFlow('ddos_protect_ip6_flood', {
422
keys:keys6+',ip6nexthdr',
423
value:value,
424
values:values6,
425
filter:protocolFilter(filter6,'ip6nexthdr',settings.ip_flood),
426
aggMode:'AGENT',
427
t:flow_t
428
});
429
setFlow('ddos_protect_ip6_fragmentation', {
430
keys: keys6+',ip6nexthdr',
431
value:value,
432
values:values6,
433
filter:protocolFilter('(ip6fragm=yes|range:ip6fragoffset:1=true)&'+filter6,'ip6nexthdr',settings.ip_fragmentation),
434
aggMode:'AGENT',
435
t:flow_t
436
});
437
setFlow('ddos_protect_udp6_amplification', {
438
keys:keys6+',udpsourceport',
439
value:value,
440
values:values6,
441
filter:protocolFilter('ip6nexthdr=17&'+filter6,'udpsourceport',settings.udp_amplification),
442
aggMode:'AGENT',
443
t:flow_t
444
});
445
setFlow('ddos_protect_udp6_flood', {
446
keys:keys6+',udpdestinationport',
447
value:value,
448
values:values6,
449
filter:protocolFilter('ip6nexthdr=17&'+filter6,'udpdestinationport',settings.udp_flood),
450
aggMode:'AGENT',
451
t:flow_t
452
});
453
setFlow('ddos_protect_icmp6_flood', {
454
keys:keys6+',icmp6type',
455
value:value,
456
values:values6,
457
filter:protocolFilter('ip6nexthdr=58&'+filter6,'icmp6type',settings.icmp_flood),
458
aggMode:'AGENT',
459
t:flow_t
460
});
461
setFlow('ddos_protect_tcp6_flood', {
462
keys:keys6+',tcpdestinationport',
463
value:value,
464
values:values6,
465
filter:protocolFilter('ip6nexthdr=6&'+filter6,'tcpdestinationport',settings.tcp_flood),
466
aggMode:'AGENT',
467
t:flow_t
468
});
469
setFlow('ddos_protect_tcp6_amplification', {
470
keys:keys6+',tcpsourceport',
471
value:value,
472
values:values6,
473
filter:protocolFilter('ip6nexthdr=6&tcpflags~....1..1.&'+filter6,'tcpsourceport',settings.tcp_amplification),
474
aggMode:'AGENT',
475
t:flow_t
476
});
477
}
478
479
setFlows();
480
481
function setThresholds() {
482
setThreshold('ddos_protect_ip_flood',
483
{metric:'ddos_protect_ip_flood', value:settings.ip_flood.threshold, byFlow:true, timeout:threshold_t}
484
);
485
setThreshold('ddos_protect_ip6_flood',
486
{metric:'ddos_protect_ip6_flood', value:settings.ip_flood.threshold, byFlow:true, timeout:threshold_t}
487
);
488
setThreshold('ddos_protect_icmp_flood',
489
{metric:'ddos_protect_icmp_flood', value:settings.icmp_flood.threshold, byFlow:true, timeout:threshold_t}
490
);
491
setThreshold('ddos_protect_icmp6_flood',
492
{metric:'ddos_protect_icmp6_flood', value:settings.icmp_flood.threshold, byFlow:true, timeout:threshold_t}
493
);
494
setThreshold('ddos_protect_tcp_flood',
495
{metric:'ddos_protect_tcp_flood', value:settings.tcp_flood.threshold, byFlow:true, timeout:threshold_t}
496
);
497
setThreshold('ddos_protect_tcp6_flood',
498
{metric:'ddos_protect_tcp6_flood', value:settings.tcp_flood.threshold, byFlow:true, timeout:threshold_t}
499
);
500
setThreshold('ddos_protect_tcp_amplification',
501
{metric:'ddos_protect_tcp_amplification', value:settings.tcp_amplification.threshold, byFlow:true, timeout:threshold_t}
502
);
503
setThreshold('ddos_protect_tcp6_amplification',
504
{metric:'ddos_protect_tcp6_amplification', value:settings.tcp_amplification.threshold, byFlow:true, timeout:threshold_t}
505
);
506
setThreshold('ddos_protect_udp_flood',
507
{metric:'ddos_protect_udp_flood', value:settings.udp_flood.threshold, byFlow:true, timeout:threshold_t}
508
);
509
setThreshold('ddos_protect_udp6_flood',
510
{metric:'ddos_protect_udp6_flood', value:settings.udp_flood.threshold, byFlow:true, timeout:threshold_t}
511
);
512
setThreshold('ddos_protect_udp_amplification',
513
{metric:'ddos_protect_udp_amplification', value:settings.udp_amplification.threshold, byFlow:true, timeout:threshold_t}
514
);
515
setThreshold('ddos_protect_udp6_amplification',
516
{metric:'ddos_protect_udp6_amplification', value:settings.udp_amplification.threshold, byFlow:true, timeout:threshold_t}
517
);
518
setThreshold('ddos_protect_ip_fragmentation',
519
{metric:'ddos_protect_ip_fragmentation', value:settings.ip_fragmentation.threshold, byFlow:true, timeout:threshold_t}
520
);
521
setThreshold('ddos_protect_ip6_fragmentation',
522
{metric:'ddos_protect_ip6_fragmentation', value:settings.ip_fragmentation.threshold, byFlow:true, timeout:threshold_t}
523
);
524
}
525
526
setThresholds();
527
528
function applyControl(ctl) {
529
ctl.action = settings[ctl.attack].action;
530
if('ignore' === ctl.action) return;
531
532
logInfo("DDoS "+ctl.action+" "+ctl.attack+" "+ctl.target+" "+ctl.group+" "+ctl.protocol);
533
sendEvent(ctl.action,ctl.attack,ctl.target,ctl.group,ctl.protocol);
534
535
controls[ctl.key] = ctl;
536
if(enabled) {
537
routers.forEach(router_ip => bgpAddControl(router_ip, ctl));
538
ctl.status = 'blocked';
539
}
540
updateControlCounts();
541
}
542
543
function releaseControl(ctl) {
544
// should we always do this? maybe it means that the control is ineffective and should be removed?
545
// maybe if the operator initiated the removal we should go back to pending, but keep the record
546
var evt = ctl.event;
547
if(thresholdTriggered(evt.thresholdID,evt.agent,evt.dataSource+'.'+evt.metric,evt.flowKey)) {
548
return;
549
}
550
551
logInfo("DDoS release "+ctl.attack+" "+ctl.target+" "+ctl.group+" "+ctl.protocol);
552
sendEvent("release",ctl.attack,ctl.target,ctl.group,ctl.protocol);
553
554
routers.forEach(router_ip => bgpRemoveControl(router_ip, ctl));
555
delete controls[ctl.key];
556
updateControlCounts();
557
}
558
559
function getControlForId(id) {
560
var key, entry;
561
for(key in controls) {
562
entry = controls[key];
563
if(id === entry.id) {
564
return entry;
565
}
566
}
567
return null;
568
}
569
570
function operatorConfirm(id) {
571
var ctl = getControlForId(id);
572
if(!ctl) return;
573
routers.forEach(router_ip => bgpAddControl(router_ip, ctl));
574
ctl.status = 'blocked';
575
updateControlCounts();
576
}
577
578
function operatorIgnore(id) {
579
var ctl = getControlForId(id);
580
if(!ctl) return;
581
routers.forEach(router_ip => bgpRemoveControl(router_ip, ctl));
582
delete controls[ctl.key];
583
updateControlCounts();
584
}
585
586
var idx = 0;
587
setEventHandler(function(evt) {
588
var key = evt.thresholdID+'-'+evt.flowKey;
589
if(controls[key]) return;
590
591
var [target,group,protocol] = evt.flowKey.split(',');
592
var [attackers,packetsize] = evt.values ? evt.values : [0,0];
593
594
// avoid false positives by ignoring events with small number of attackers
595
if(sourceCountFilterFlag && attackers > 0) {
596
if(attackers < sourceCountFilterThreshold) {
597
logWarning("DDoS source count "+attackers+" too small for "+evt.thresholdID+" "+target+" "+group+" "+protocol);
598
return;
599
}
600
}
601
602
var ctl = {
603
id:'c' + idx++,
604
time:evt.timestamp,
605
status:'pending',
606
key:key,
607
target:target,
608
group:group,
609
protocol:protocol,
610
attackers:attackers,
611
packetsize:packetsize,
612
flowspec:{},
613
event:evt,
614
success:{}
615
};
616
617
switch(evt.thresholdID) {
618
case 'ddos_protect_ip_flood':
619
ctl.attack = 'ip_flood';
620
ctl.ipversion = '4';
621
ctl.flowspec.match = {
622
destination:target,
623
version:'4',
624
protocol:'='+protocol
625
};
626
break;
627
case 'ddos_protect_ip6_flood':
628
ctl.attack = 'ip_flood';
629
ctl.ipversion = '6';
630
ctl.flowspec.match = {
631
destination:target,
632
version:'6',
633
protocol:'='+protocol
634
};
635
break;
636
case 'ddos_protect_icmp_flood':
637
ctl.attack = 'icmp_flood';
638
ctl.ipversion = '4';
639
ctl.flowspec.match = {
640
destination:target,
641
version:'4',
642
protocol:'=1',
643
'icmp-type':'='+protocol
644
};
645
break;
646
case 'ddos_protect_icmp6_flood':
647
ctl.attack = 'icmp_flood';
648
ctl.ipversion = '6';
649
ctl.flowspec.match = {
650
destination:target,
651
version:'6',
652
protocol:'=58',
653
'icmp-type':'='+protocol
654
};
655
break;
656
case 'ddos_protect_tcp_flood':
657
ctl.attack = 'tcp_flood';
658
ctl.ipversion = '4';
659
ctl.flowspec.match = {
660
destination:target,
661
version:'4',
662
protocol:'=6',
663
'destination-port':'='+protocol
664
};
665
break;
666
case 'ddos_protect_tcp6_flood':
667
ctl.attack = 'tcp_flood';
668
ctl.ipversion = '6';
669
ctl.flowspec.match = {
670
destination:target,
671
version:'6',
672
protocol:'=6',
673
'destination-port':'='+protocol
674
};
675
break;
676
case 'ddos_protect_tcp_amplification':
677
ctl.attack = 'tcp_amplification';
678
ctl.ipversion = '4';
679
ctl.flowspec.match = {
680
destination:target,
681
version:'4',
682
protocol:'=6',
683
'tcp-flags':'=SA',
684
'source-port':'='+protocol
685
};
686
break;
687
case 'ddos_protect_tcp6_amplification':
688
ctl.attack = 'tcp_amplification';
689
ctl.ipversion = '6';
690
ctl.flowspec.match = {
691
destination:target,
692
version:'6',
693
protocol:'=6',
694
'tcp-flags':'=SA',
695
'source-port':'='+protocol
696
};
697
break;
698
case 'ddos_protect_udp_flood':
699
ctl.attack = 'udp_flood';
700
ctl.ipversion = '4';
701
ctl.flowspec.match = {
702
destination:target,
703
version:'4',
704
protocol:'=17',
705
'destination-port':'='+protocol
706
};
707
break;
708
case 'ddos_protect_udp6_flood':
709
ctl.attack = 'udp_flood';
710
ctl.ipversion = '6';
711
ctl.flowspec.match = {
712
destination:target,
713
version:'6',
714
protocol:'=17',
715
'destination-port':'='+protocol
716
};
717
break;
718
case 'ddos_protect_udp_amplification':
719
ctl.attack = 'udp_amplification';
720
ctl.ipversion = '4';
721
ctl.flowspec.match = {
722
destination:target,
723
version:'4',
724
protocol:'=17',
725
'source-port':'='+protocol
726
};
727
break;
728
case 'ddos_protect_udp6_amplification':
729
ctl.attack = 'udp_amplification';
730
ctl.ipversion = '6';
731
ctl.flowspec.match = {
732
destination:target,
733
version:'6',
734
protocol:'=17',
735
'source-port':'='+protocol
736
};
737
break;
738
case 'ddos_protect_ip_fragmentation':
739
ctl.attack = 'ip_fragmentation';
740
ctl.ipversion = '4';
741
ctl.flowspec.match = {
742
destination:target,
743
version:'4',
744
protocol:'='+protocol,
745
fragment:'=I'
746
};
747
break;
748
case 'ddos_protect_ip6_fragmentation':
749
ctl.attack = 'ip_fragmentation';
750
ctl.ipversion = '6';
751
ctl.flowspec.match = {
752
destination:target,
753
version:'6',
754
protocol:'='+protocol,
755
fragment:'=I'
756
};
757
break;
758
}
759
applyControl(ctl);
760
},[
761
'ddos_protect_ip_flood',
762
'ddos_protect_ip6_flood',
763
'ddos_protect_icmp_flood',
764
'ddos_protect_icmp6_flood',
765
'ddos_protect_tcp_flood',
766
'ddos_protect_tcp6_flood',
767
'ddos_protect_tcp_amplification',
768
'ddos_protect_tcp6_amplification',
769
'ddos_protect_udp_flood',
770
'ddos_protect_udp6_flood',
771
'ddos_protect_udp_amplification',
772
'ddos_protect_udp6_amplification',
773
'ddos_protect_ip_fragmentation',
774
'ddos_protect_ip6_fragmentation'
775
]);
776
777
var trend = new Trend(300,1);
778
var points;
779
780
function calculateTopN(metric_names,n,minVal) {
781
var top, topN, i, lim;
782
top = metric_names.reduce(function(accum,name) {
783
return accum.concat(activeFlows('ALL',name,n,minVal,'max') || []);
784
}, []);
785
top.sort((a,b) => b.value - a.value);
786
var topN = {};
787
788
lim = Math.min(n,top.length);
789
for(i = 0; i < lim; i++) {
790
topN[top[i].key] = top[i].value;
791
}
792
return topN;
793
}
794
795
setIntervalHandler(function(now) {
796
points = {};
797
points['controls'] = counts.n || 0;
798
points['controls_pending'] = counts.pending || 0;
799
points['controls_failed'] = counts.failed || 0;
800
points['controls_blocked'] = counts.blocked || 0;
801
points['connections'] = routers.reduce((sum, router_ip) => sum + (bgpUp[router_ip] ? 1 : 0), 0);
802
points['top-5-ip-flood'] = calculateTopN(['ddos_protect_ip_flood','ddos_protect_ip6_flood'],5,1);
803
points['top-5-ip-fragmentation'] = calculateTopN(['ddos_protect_ip_fragmentation','ddos_protect_ip6_fragmentation'],5,1);
804
points['top-5-udp-flood'] = calculateTopN(['ddos_protect_udp_flood','ddos_protect_udp6_flood'],5,1);
805
points['top-5-udp-amplification'] = calculateTopN(['ddos_protect_udp_amplification','ddos_protect_udp6_amplification'],5,1);
806
points['top-5-icmp-flood'] = calculateTopN(['ddos_protect_icmp_flood','ddos_protect_icmp6_flood'],5,1);
807
points['top-5-tcp-flood'] = calculateTopN(['ddos_protect_tcp_flood','ddos_protect_tcp6_flood'],5,1);
808
points['top-5-tcp-amplification'] = calculateTopN(['ddos_protect_tcp_amplification','ddos_protect_tcp6_amplification'],5,1);
809
trend.addPoints(now,points);
810
811
for(var key in controls) {
812
var ctl = controls[key];
813
if(now - ctl.time > settings[ctl.attack].timeout * 60000) releaseControl(ctl);
814
}
815
}, 1);
816
817
var settingsUpdate = 0;
818
function updateSettings(vals) {
819
var newSettings = {};
820
for(var attack in settings) {
821
let entry = settings[attack];
822
if(vals[attack]) {
823
let newEntry = {};
824
for(param in entry) {
825
let val = vals[attack][param];
826
switch(param) {
827
case 'action':
828
if('ignore' === val
829
|| 'drop' === val
830
|| 'filter' === val
831
|| 'mark' === val
832
|| 'limit' === val
833
|| 'redirect' === val
834
|| 'community' === val) {
835
newEntry[param] = val;
836
} else {
837
return false;
838
}
839
break;
840
case 'threshold':
841
case 'timeout':
842
if(0 <= val) {
843
newEntry[param] = val;
844
} else {
845
return false;
846
}
847
break;
848
case 'include':
849
case 'exclude':
850
if(!val) {
851
newEntry[param] = '';
852
} else {
853
let newVal = String(val).replace(/\s/g, '');
854
if(newVal.length === 0 || newVal.match(/^\d+(,\d+)*$/)) {
855
newEntry[param] = newVal;
856
} else {
857
return false;
858
}
859
}
860
break;
861
default:
862
newEntry[param] = entry[param];
863
}
864
}
865
newSettings[attack] = newEntry;
866
} else {
867
newSettings[attack] = entry;
868
}
869
}
870
settingsUpdate++;
871
settings = newSettings;
872
storeSet('settings',settings);
873
setFlows();
874
setThresholds();
875
return true;
876
}
877
878
function prometheus() {
879
var connections = routers.reduce((sum, router_ip) => sum + (bgpUp[router_ip] ? 1 : 0), 0);
880
var results = prometheus_prefix+'ddos_protect_controls{status="active"} '+(counts.blocked||0)+'\n';
881
results += prometheus_prefix+'ddos_protect_controls{status="failed"} '+(counts.failed||0)+'\n';
882
results += prometheus_prefix+'ddos_protect_controls{status="pending"} '+(counts.pending||0)+'\n';
883
results += prometheus_prefix+'ddos_protect_connections_current '+connections+'\n';
884
results += prometheus_prefix+'ddos_protect_connections_configured '+routers.length+'\n';
885
return results;
886
}
887
888
var groupsUpdate = 0;
889
setHttpHandler(function(req) {
890
var result, key, path = req.path;
891
if(!path || path.length == 0) throw "not_found";
892
if(path.length === 1 && 'txt' === req.format) {
893
return prometheus();
894
}
895
if('json' !== req.format) throw "not_found";
896
switch(path[0]) {
897
case 'trend':
898
if(path.length > 1) throw "not_found";
899
result = {
900
controlsUpdate: controlsUpdate,
901
settingsUpdate: settingsUpdate,
902
groupsUpdate:groupsUpdate,
903
mode: enabled ? 'automatic' : 'manual'
904
};
905
result.trend = req.query.after ? trend.after(parseInt(req.query.after)) : trend;
906
result.trend.values = {};
907
Object.keys(settings).forEach(function(key) { result.trend.values['threshold_'+key] = settings[key].threshold; });
908
result.trend.values['threshold_connections'] = routers.length;
909
break;
910
case 'controls':
911
result = {};
912
switch(req.method) {
913
case 'POST':
914
case 'PUT':
915
switch(req.body.action) {
916
case 'block':
917
operatorConfirm(req.body.id);
918
break;
919
case 'allow':
920
operatorIgnore(req.body.id);
921
break;
922
}
923
break;
924
}
925
result.controls = [];
926
for(key in controls) {
927
let ctl = controls[key];
928
let status = ctl.status;
929
if(status === 'blocked') {
930
if(!routers.reduce((flag, router_ip) => flag && controls[key].success[router_ip], true)) {
931
status = 'failed';
932
}
933
}
934
let entry = {
935
id: ctl.id,
936
target: ctl.target,
937
group: ctl.group,
938
protocol: ctl.protocol,
939
attack: ctl.attack,
940
attackers:ctl.attackers,
941
packetsize:ctl.packetsize,
942
time: ctl.time,
943
action: ctl.action,
944
status: status
945
}
946
result.controls.push(entry);
947
};
948
result.update = controlsUpdate;
949
break;
950
case 'mode':
951
if(path.length > 1) throw "not_found";
952
enabled = 'automatic' === req.body;
953
storeSet('enabled',enabled);
954
settingsUpdate++;
955
result = enabled ? 'automatic' : 'manual';
956
break;
957
case 'settings':
958
if(path.length > 1) throw "not_found";
959
switch(req.method) {
960
case 'POST':
961
case 'PUT':
962
if(req.error) throw "bad_request";
963
if(!updateSettings(req.body)) throw "bad_request";
964
break;
965
}
966
result = {
967
update: settingsUpdate,
968
mode: enabled ? 'automatic' : 'manual',
969
settings: settings
970
};
971
break;
972
case 'groups':
973
if(path.length > 1) throw "not_found";
974
switch(req.method) {
975
case 'POST':
976
case 'PUT':
977
if(req.error) throw "bad_request";
978
if(!configureGroups(req.body)) throw "bad_request";
979
groups = req.body;
980
storeSet('groups', groups);
981
groupsUpdate++;
982
break;
983
}
984
result = {
985
update: groupsUpdate,
986
groups: groups,
987
external: externalGroup.split(','),
988
excluded: excludedGroups.split(',')
989
};
990
break;
991
default: throw 'not_found';
992
}
993
return result;
994
});
995
996