Détection et suppression des DDoS avec Akvorado et Flowspec
Vincent Bernat
Akvorado collecte des flux sFlow et IPFIX, les stocke dans une base de données ClickHouse et les présente dans une console web. Bien qu’il n’y ait pas de détection de DDoS intégrée, il est possible d’en créer une à partir de requêtes pour ClickHouse.
Détection des attaques DDoS#
Supposons que nous voulions détecter des attaques DDoS ciblant nos clients. À titre d’exemple, nous considérons une attaque DDoS comme une collection de flux sur une minute ciblant une adresse IP client unique, à partir d’un port source unique et correspondant à l’une de ces conditions :
- une bande passante moyenne de 1 Gbit/s,
- une bande passante moyenne de 200 Mbit/s lorsque le protocole est UDP,
- plus de 20 adresses IP sources et une bande passante moyenne de 100 Mbit/s, ou
- plus de 10 pays sources et une bande passante moyenne de 100 Mbit/s.
Voici la requête SQL pour détecter de telles attaques au cours des 5 dernières minutes :
SELECT * FROM ( SELECT toStartOfMinute(TimeReceived) AS TimeReceived, DstAddr, SrcPort, dictGetOrDefault('protocols', 'name', Proto, '???') AS Proto, SUM(((((Bytes * SamplingRate) * 8) / 1000) / 1000) / 1000) / 60 AS Gbps, uniq(SrcAddr) AS sources, uniq(SrcCountry) AS countries FROM flows WHERE TimeReceived > now() - INTERVAL 5 MINUTE AND DstNetRole = 'customers' GROUP BY TimeReceived, DstAddr, SrcPort, Proto ) WHERE (Gbps > 1) OR ((Proto = 'UDP') AND (Gbps > 0.2)) OR ((sources > 20) AND (Gbps > 0.1)) OR ((countries > 10) AND (Gbps > 0.1)) ORDER BY TimeReceived DESC, Gbps DESC
Voici un exemple de sortie1 où deux de nos utilisateurs sont attaqués. L’un d’eux subit apparemment une attaque d’amplification NTP, l’autre une attaque d’amplification DNS :
TimeReceived | DstAddr | SrcPort | Proto | Gbps | sources | countries |
---|---|---|---|---|---|---|
2023-02-26 17:44:00 | ::ffff:203.0.113.206 |
123 | UDP | 0.102 | 109 | 13 |
2023-02-26 17:43:00 | ::ffff:203.0.113.206 |
123 | UDP | 0.130 | 133 | 17 |
2023-02-26 17:43:00 | ::ffff:203.0.113.68 |
53 | UDP | 0.129 | 364 | 63 |
2023-02-26 17:43:00 | ::ffff:203.0.113.206 |
123 | UDP | 0.113 | 129 | 21 |
2023-02-26 17:42:00 | ::ffff:203.0.113.206 |
123 | UDP | 0.139 | 50 | 14 |
2023-02-26 17:42:00 | ::ffff:203.0.113.206 |
123 | UDP | 0.105 | 42 | 14 |
2023-02-26 17:40:00 | ::ffff:203.0.113.68 |
53 | UDP | 0.121 | 340 | 65 |
Suppression des attaques DDoS#
Une fois détectée, il existe au moins deux façons d’arrêter l’attaque au niveau du réseau :
- supprimer tout le trafic vers l’utilisateur ciblé (RTBH)
- supprimer sélectivement les paquets correspondant aux motifs de l’attaque (Flowspec)
Suppression du trafic avec RTBH#
La méthode la plus simple consiste à sacrifier l’utilisateur attaqué. Bien que cela aide l’attaquant, cela protège avant tout votre réseau. C’est une méthode prise en charge par tous les routeurs. Vous pouvez également déléguer cette protection à la plupart des fournisseurs de transit. Cela est utile si le volume des attaques dépasse la capacité de votre connexion Internet.
Cela fonctionne en annonçant avec BGP une route vers l’utilisateur attaqué avec
une communauté spécifique. Le routeur de bordure modifie l’adresse du prochain
routeur de ces routes vers une adresse IP spécifique configurée pour transférer
le trafic vers l’interface « nulle ». La RFC 7999 définit 65535:666
à
cette fin. C’est ce qu’on appelle un trou noir déclenché à distance (“remote
triggered blackhole”, RTBH) et expliqué en détail dans la RFC 3882.
Il est également possible de bloquer la source des attaques en utilisant uRPF défini dans la RFC 3704. C’est expliqué dans la RFC 5635. Cependant, uRPF peut être une charge importante pour les ressources de votre routeur. Consultez “NCS5500 uRPF: Configuration et Impact sur l’échelle” pour un exemple des types de restrictions auxquelles vous devez vous attendre lorsque vous activez uRPF.
Pour annoncer les routes, nous pouvons utiliser BIRD. Voici un fichier de configuration complet pour permettre à tout routeur de les collecter :
log stderr all; router id 192.0.2.1; protocol device { scan time 10; } protocol bgp exporter { ipv4 { import none; export where proto = "blackhole4"; }; ipv6 { import none; export where proto = "blackhole6"; }; local as 64666; neighbor range 192.0.2.0/24 external; multihop; dynamic name "exporter"; dynamic name digits 2; graceful restart yes; graceful restart time 0; long lived graceful restart yes; long lived stale time 3600; # keep routes for 1 hour! } protocol static blackhole4 { ipv4; route 203.0.113.206/32 blackhole { bgp_community.add((65535, 666)); }; route 203.0.113.68/32 blackhole { bgp_community.add((65535, 666)); }; } protocol static blackhole6 { ipv6; }
Nous utilisons BGP LLGR pour garantir que les routes sont conservées pendant une heure, même si la connexion BGP est interrompue, notamment pendant une maintenance.
Du côté du récepteur, si vous disposez d’un routeur Cisco fonctionnant sous IOS
XR, vous pouvez utiliser la configuration suivante pour éliminer le trafic reçu
sur la session BGP. Comme la session BGP est dédiée à cette utilisation, la
communauté 65535:666
n’est pas utilisée, mais vous pouvez aussi transférer ces
routes à vos fournisseurs de transit.
router static vrf public address-family ipv4 unicast 192.0.2.1/32 Null0 description "BGP blackhole" ! address-family ipv6 unicast 2001:db8::1/128 Null0 description "BGP blackhole" ! ! ! route-policy blackhole_ipv4_in_public if destination in (0.0.0.0/0 le 31) then drop endif set next-hop 192.0.2.1 done end-policy ! route-policy blackhole_ipv6_in_public if destination in (::/0 le 127) then drop endif set next-hop 2001:db8::1 done end-policy ! router bgp 12322 neighbor-group BLACKHOLE_IPV4_PUBLIC remote-as 64666 ebgp-multihop 255 update-source Loopback10 address-family ipv4 unicast maximum-prefix 100 90 route-policy blackhole_ipv4_in_public in route-policy drop out long-lived-graceful-restart stale-time send 86400 accept 86400 ! address-family ipv6 unicast maximum-prefix 100 90 route-policy blackhole_ipv6_in_public in route-policy drop out long-lived-graceful-restart stale-time send 86400 accept 86400 ! ! vrf public neighbor 192.0.2.1 use neighbor-group BLACKHOLE_IPV4_PUBLIC description akvorado-1
Lorsque le trafic est éliminé, il est toujours remonté par IPFIX et sFlow.
Dans Akvorado, utilisez ForwardingStatus >= 128
comme filtre pour
sélectionner celui-ci.
Bien que cette méthode soit compatible avec tous les routeurs, elle permet à l’attaque de réussir car la cible est complètement inaccessible. Si votre routeur le prend en charge, Flowspec peut filtrer sélectivement les flux pour arrêter l’attaque sans affecter le client.
Suppression du trafic avec Flowspec#
Flowspec est défini dans la RFC 8955 et permet la transmission de spécifications de flux dans des sessions BGP. Une spécification de flux est un ensemble de critères à appliquer au trafic IP. Ceux-ci incluent le préfixe source et destination, le protocole IP, le port source et destination et la longueur des paquets. Chaque spécification de flux est associée à une action, encodée sous forme d’une communauté étendue : limitation du trafic, marquage ou redirection.
Pour annoncer des spécifications de flux avec BIRD, nous complétons notre configuration. La communauté étendue utilisée limite le trafic correspondant à 0 octet par seconde.
flow4 table flowtab4; flow6 table flowtab6; protocol bgp exporter { flow4 { import none; export where proto = "flowspec4"; }; flow6 { import none; export where proto = "flowspec6"; }; # […] } protocol static flowspec4 { flow4; route flow4 { dst 203.0.113.68/32; sport = 53; length >= 1476 && <= 1500; proto = 17; }{ bgp_ext_community.add((generic, 0x80060000, 0x00000000)); }; route flow4 { dst 203.0.113.206/32; sport = 123; length = 468; proto = 17; }{ bgp_ext_community.add((generic, 0x80060000, 0x00000000)); }; } protocol static flowspec6 { flow6; }
Si vous avez un routeur ASR tournant sous IOS XR, la configuration ressemble à ceci :
vrf public address-family ipv4 flowspec address-family ipv6 flowspec ! router bgp 12322 address-family vpnv4 flowspec address-family vpnv6 flowspec neighbor-group FLOWSPEC_IPV4_PUBLIC remote-as 64666 ebgp-multihop 255 update-source Loopback10 address-family ipv4 flowspec long-lived-graceful-restart stale-time send 86400 accept 86400 route-policy accept in route-policy drop out maximum-prefix 100 90 validation disable ! address-family ipv6 flowspec long-lived-graceful-restart stale-time send 86400 accept 86400 route-policy accept in route-policy drop out maximum-prefix 100 90 validation disable ! ! vrf public address-family ipv4 flowspec address-family ipv6 flowspec neighbor 192.0.2.1 use neighbor-group FLOWSPEC_IPV4_PUBLIC description akvorado-1
Ensuite, il convient d’activer Flowspec sur toutes les interfaces :
flowspec vrf public address-family ipv4 local-install interface-all ! address-family ipv6 local-install interface-all ! ! !
Comme pour la configuration à base de RTBH, vous pouvez afficher les flux
éliminés avec le filtre ForwardingStatus >= 128
.
Détection des attaques DDoS (suite)#
Dans l’exemple utilisant Flowspec, les flux sont aussi filtrés selon la longueur des paquets :
route flow4 { dst 203.0.113.68/32; sport = 53; length >= 1476 && <= 1500; proto = 17; }{ bgp_ext_community.add((generic, 0x80060000, 0x00000000)); };
C’est une addition importante : les requêtes DNS légitimes ont une taille
inférieure à cela et ne sont donc pas filtrées2. Avec ClickHouse, vous pouvez
obtenir les 10ème et 90ème centiles des tailles de paquets
avec quantiles(0.1, 0.9)(Bytes/Packets)
.
Le dernier problème que nous devons résoudre est de rendre les requêtes plus rapides : elles peuvent avoir besoin de plusieurs secondes pour collecter les données et sont susceptibles de consommer des ressources substantielles de votre base de données ClickHouse. Une solution consiste à créer une vue matérialisée pour pré-agréger les résultats :
CREATE TABLE ddos_logs ( TimeReceived DateTime, DstAddr IPv6, Proto UInt32, SrcPort UInt16, Gbps SimpleAggregateFunction(sum, Float64), Mpps SimpleAggregateFunction(sum, Float64), sources AggregateFunction(uniqCombined(12), IPv6), countries AggregateFunction(uniqCombined(12), FixedString(2)), size AggregateFunction(quantiles(0.1, 0.9), UInt64) ) ENGINE = SummingMergeTree PARTITION BY toStartOfHour(TimeReceived) ORDER BY (TimeReceived, DstAddr, Proto, SrcPort) TTL toStartOfHour(TimeReceived) + INTERVAL 6 HOUR DELETE ; CREATE MATERIALIZED VIEW ddos_logs_view TO ddos_logs AS SELECT toStartOfMinute(TimeReceived) AS TimeReceived, DstAddr, Proto, SrcPort, sum(((((Bytes * SamplingRate) * 8) / 1000) / 1000) / 1000) / 60 AS Gbps, sum(((Packets * SamplingRate) / 1000) / 1000) / 60 AS Mpps, uniqCombinedState(12)(SrcAddr) AS sources, uniqCombinedState(12)(SrcCountry) AS countries, quantilesState(0.1, 0.9)(toUInt64(Bytes/Packets)) AS size FROM flows WHERE DstNetRole = 'customers' GROUP BY TimeReceived, DstAddr, Proto, SrcPort
La table ddos_logs
utilise le moteur SummingMergeTree
. Lorsque la table
reçoit de nouvelles données, ClickHouse remplace toutes les lignes ayant la
même clé de tri, telle que définie par la directive ORDER BY
, par une seule
ligne qui contient des valeurs résumées3 en utilisant soit la
fonction sum()
soit la fonction d’agrégation explicitement spécifiée
(uniqCombined
et quantiles
dans notre exemple).
Enfin, nous modifions notre requête initiale avec la requête suivante :
SELECT * FROM ( SELECT TimeReceived, DstAddr, dictGetOrDefault('protocols', 'name', Proto, '???') AS Proto, SrcPort, sum(Gbps) AS Gbps, sum(Mpps) AS Mpps, uniqCombinedMerge(12)(sources) AS sources, uniqCombinedMerge(12)(countries) AS countries, quantilesMerge(0.1, 0.9)(size) AS size FROM ddos_logs WHERE TimeReceived > now() - INTERVAL 60 MINUTE GROUP BY TimeReceived, DstAddr, Proto, SrcPort ) WHERE (Gbps > 1) OR ((Proto = 'UDP') AND (Gbps > 0.2)) OR ((sources > 20) AND (Gbps > 0.1)) OR ((countries > 10) AND (Gbps > 0.1)) ORDER BY TimeReceived DESC, Gbps DESC
Assemblage final#
Pour résumer, la construction d’un système de suppression des attaques DDoS nécessite de suivre les étapes suivantes :
- définir un ensemble de critères pour détecter une attaque DDoS
- traduire ces critères en requêtes SQL
- pré-agréger les flux dans des tables
SummingMergeTree
- interroger et transformer les résultats en un fichier de configuration pour BIRD
- configurer vos routeurs pour extraire les routes de BIRD
Un script Python comme celui qui suit permet de gérer la quatrième étape. Pour chaque cible attaquée, il génère à la fois une règle Flowspec et une route de type « trou noir ».
import socket import types from clickhouse_driver import Client as CHClient # Insérez ici votre requête SQL SQL_QUERY = "…" # Combien de règles anti-DDoS doit-on publier au plus ? MAX_DDOS_RULES = 20 def empty_ruleset(): ruleset = types.SimpleNamespace() ruleset.flowspec = types.SimpleNamespace() ruleset.blackhole = types.SimpleNamespace() ruleset.flowspec.v4 = [] ruleset.flowspec.v6 = [] ruleset.blackhole.v4 = [] ruleset.blackhole.v6 = [] return ruleset current_ruleset = empty_ruleset() client = CHClient(host="clickhouse.akvorado.net") while True: results = client.execute(SQL_QUERY) seen = {} new_ruleset = empty_ruleset() for (t, addr, proto, port, gbps, mpps, sources, countries, size) in results: if (addr, proto, port) in seen: continue seen[(addr, proto, port)] = True # Flowspec if addr.ipv4_mapped: address = addr.ipv4_mapped rules = new_ruleset.flowspec.v4 table = "flow4" mask = 32 nh = "proto" else: address = addr rules = new_ruleset.flowspec.v6 table = "flow6" mask = 128 nh = "next header" if size[0] == size[1]: length = f"length = {int(size[0])}" else: length = f"length >= {int(size[0])} && <= {int(size[1])}" header = f""" # Time: {t} # Source: {address}, protocol: {proto}, port: {port} # Gbps/Mpps: {gbps:.3}/{mpps:.3}, packet size: {int(size[0])}<=X<={int(size[1])} # Flows: {flows}, sources: {sources}, countries: {countries} """ rules.append( f"""{header} route {table} {{ dst {address}/{mask}; sport = {port}; {length}; {nh} = {socket.getprotobyname(proto)}; }}{{ bgp_ext_community.add((generic, 0x80060000, 0x00000000)); }}; """ ) # Blackhole if addr.ipv4_mapped: rules = new_ruleset.blackhole.v4 else: rules = new_ruleset.blackhole.v6 rules.append( f"""{header} route {address}/{mask} blackhole {{ bgp_community.add((65535, 666)); }}; """ ) new_ruleset.flowspec.v4 = list( set(new_ruleset.flowspec.v4[:MAX_DDOS_RULES]) ) new_ruleset.flowspec.v6 = list( set(new_ruleset.flowspec.v6[:MAX_DDOS_RULES]) ) # TODO: publier les changements par courriel ou chat current_ruleset = new_ruleset changes = False for rules, path in ( (current_ruleset.flowspec.v4, "v4-flowspec"), (current_ruleset.flowspec.v6, "v6-flowspec"), (current_ruleset.blackhole.v4, "v4-blackhole"), (current_ruleset.blackhole.v6, "v6-blackhole"), ): path = os.path.join("/etc/bird/", f"{path}.conf") with open(f"{path}.tmp", "w") as f: for r in rules: f.write(r) changes = ( changes or not os.path.exists(path) or not samefile(path, f"{path}.tmp") ) os.rename(f"{path}.tmp", path) if not changes: continue proc = subprocess.Popen( ["birdc", "configure"], stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) stdout, stderr = proc.communicate(None) stdout = stdout.decode("utf-8", "replace") stderr = stderr.decode("utf-8", "replace") if proc.returncode != 0: logger.error( "{} error:\n{}\n{}".format( "birdc reconfigure", "\n".join( [" O: {}".format(line) for line in stdout.rstrip().split("\n")] ), "\n".join( [" E: {}".format(line) for line in stderr.rstrip().split("\n")] ), ) )
Jusqu’à ce qu’Akvorado intègre la détection et la lutte contre les attaques DDoS, les idées présentées dans ce billet fournissent une base solide pour commencer à construire votre propre système anti-DDoS. 🛡️
-
ClickHouse peut exporter les résultats au format Markdown en ajoutant
FORMAT Markdown
à la requête. ↩︎ -
Bien que la plupart des clients DNS réessaient avec TCP en cas d’échec, ce n’est pas toujours le cas : jusqu’à récemment, la bibliothèque Musl ne le faisait pas. ↩︎
-
La vue matérialisée agrège également les données qu’elle a sous la main, à la fois pour l’efficacité et pour s’assurer que nous travaillons avec les bons types de données. ↩︎