Nous avons vu ce qu’était un programme XDP dans la première partie : cela peut être un moyen de limiter les attaques par déni de service distribué.
Dans cette partie, je vous propose de créer un programme XDP qui va limiter les attaques par SYN flood. On va faire cela avec le framework Rust Aya.
Suivez le guide !
Je suppose que vous êtes déjà dans un environnement pour développer avec Aya. Si ce n’est pas le cas, vous pouvez utiliser le lab Killercoda :
Créons un petit tcpdump avec XDP#
Avant de faire cela, nous allons déjà expliquer comment on analyse un segment TCP avec XDP et ainsi récupérer les paquets SYN. Au fait, vous vous rappelez comment fonctionne TCP ?
Les connexions TCP#
TCP est l’acronyme de Transmission Control Protocol. C’est probablement le protocole réseau le plus utilisé par les protocoles applicatifs : HTTP, SSH, etc. Difficile de faire l’impasse pour des articles sur du réseau. Cependant c’est probablement le plus complexe si on compare à UDP ou à ICMP.
Avant le téléchargement des données utiles, il y a une phase de connexion (3-way handshake) permettant d’établir un canal fiable.
Qu’est-ce que le SYN Flood ?#
SYN est le premier segment TCP qui sert à initier une connexion TCP. Quand un serveur reçoit un SYN, il doit renvoyer un SYN-ACK pour accuser de réception. Une fois cela, le client doit confirmer par un ACK et le téléchargement des données peut enfin opérer.
Supposons qu’un client envoie une connexion SYN mais ne répond jamais, que se passe-t-il ?
- Le serveur envoie un SYN-ACK en réponse puis réessaie 5 fois (valeur par défaut)
- Le serveur ferme enfin la connexion incomplète
Supposons qu’un client innonde de plein de connexions SYN mais ne répond jamais, que se passe-t-il ?
- Le serveur garde alors plein de connexions “en attente” (dont certain probablement légitime)
- Le serveur continue de renvoyer les SYN-ACK jusqu’à saturer ses ressources mémoire
C’est ça le principe d’une attaque SYN Flood.
Dans cet article, pour contrer cette attaque, on va compter le nombre de SYN par IP et s’il y en a trop on bloque temporairement ces segments pour cette IP.
Comment voir les détails des segments#
Si ce n’est pas déjà fait, recréons l’environnement de développement
git clone https://github.com/littlejo/eunomia.dev
cd eunomia.dev/docs/tutorials/42-xdp-loadbalancer/
./setup.sh
Démarrons un petit serveur web au niveau du namespace lb :
ip netns exec lb python3 -m http.server 8080
- Sur l’interface
veth6, nous verrons les paquets TCP qui sont envoyés par le client (comme SYN) ; - Sur l’interface
veth7, nous verrons les paquets TCP qui sont envoyés par le serveur (comme SYN-ACK).
Nous devons donc installer le programme eBPF sur veth6.
Générons le programme Aya :
cargo generate --name antiddos-xdp \
-d program_type=xdp \
-d default_iface=veth6 \
https://github.com/aya-rs/aya-template
cd antiddos-xdp
En prévision, nous allons utiliser des crates supplémentaires :
- aya-ebpf-bindings pour les bindings supplémentaires eBPF
- network-types pour les structures Rust des en-têtes de niveau 1, 2 et 3
- blog-xdp, la crate de ce blog, pour ne pas copier/coller des fonctions helpers précédemment écrites
On va donc modifier le fichier antiddos-xdp-ebpf/Cargo.toml et rajouter dans la section dependencies :
aya-ebpf-bindings = "0.1.1"
network-types = "0.1.0"
blog-xdp = { git = "https://github.com/littlejo/blog-xdp" }
Vérifions que le programme généré fonctionne bien :
On va déjà construire le binaire :
cargo build
Puis on va installer dans le namespace cible où se trouve l’interface veth6 :
ip netns exec lb cargo run
On va maintenant récupérer uniquement les paquets TCP dans le programme eBPF. On va modifier le fichier antiddos-xdp-ebpf/src/main.rs.
On se retrouve avec un code similaire au code de la partie précédente :
use network_types::{
eth::{EthHdr, EtherType},
ip::{Ipv4Hdr, IpProto},
tcp::TcpHdr,
};
use blog_xdp::helper::ptr_at;
fn try_antiddos_xdp(ctx: XdpContext) -> Result<u32, u32> {
let ethhdr: *const EthHdr = ptr_at(&ctx, 0)?;
match unsafe { (*ethhdr).ether_type() } {
Ok(EtherType::Ipv4) => {}
_ => return Ok(xdp_action::XDP_PASS),
}
let ipv4hdr: *const Ipv4Hdr = ptr_at(&ctx, EthHdr::LEN)? ;
match unsafe { (*ipv4hdr).proto } {
IpProto::Tcp => {}
_ => return Ok(xdp_action::XDP_PASS),
}
let tcphdr: *const TcpHdr = ptr_at(&ctx, EthHdr::LEN + Ipv4Hdr::LEN)? ;
let src_addr = unsafe { (*ipv4hdr).src_addr };
let dst_addr = unsafe { (*ipv4hdr).dst_addr };
info!(&ctx, "src:{:i} => dst:{:i}", src_addr, dst_addr);
Ok(xdp_action::XDP_PASS)
}
On peut tester que ça fonctionne :
ip netns exec lb cargo run
Verifions avec cette commande pour tester le programme XDP :
curl 10.0.0.10:8080
Au niveau de cargo, on voit alors plusieurs paquets passés :
[INFO antiddos_xdp] src:10.0.0.1 => dst:10.0.0.10
[INFO antiddos_xdp] src:10.0.0.1 => dst:10.0.0.10
[INFO antiddos_xdp] src:10.0.0.1 => dst:10.0.0.10
[INFO antiddos_xdp] src:10.0.0.1 => dst:10.0.0.10
[INFO antiddos_xdp] src:10.0.0.1 => dst:10.0.0.10
[INFO antiddos_xdp] src:10.0.0.1 => dst:10.0.0.10
Ce que j’expliquais au début, quand on lance une connexion TCP, il n’y a pas qu’un paquet mais plusieurs paquets.
On voit seulement les paquets qui sont envoyés par le client (ceux en ingress).
Si on veut voir les paquets qui sont envoyés par le serveur, il faut installer le programme sur l’interface veth7. C’est tout simple, en surchargeant la variable iface :
cargo run -- --iface veth7
Pour afficher un peu plus de détails sur la nature des paquets, regardons maintenant l’en-tête du segment TCP :
Pour les puristes, le schéma est un peu simplifié.
Voyons brièvement à quoi servent les principales données :
- SRC/DST PORT : les ports de TCP
- SEQUENCE/ACK NUMBER : pour le suivi des connexions
- BITFIELD : c’est là où on définit la nature du segment (SYN, ACK, etc)
- WINDOW : pour le contrôle de flux (utile pour les gros fichiers)
- CHECKSUM : pour l’intégrité du segment
Comme pour UDP, on peut donc facilement récupérer les éléments en regardant la documentation de la crate network-types :
Pour récupérer la nature du segment, il y a même des fonctions pour nous aider :
Pour reproduire un petit tcpdump, ajoutons cela par exemple :
let src_port = u16::from_be_bytes(unsafe { (*tcphdr).source });
let dst_port = u16::from_be_bytes(unsafe { (*tcphdr).dest });
let seq = u32::from_be_bytes(unsafe {(*tcphdr).seq});
let ack_seq = u32::from_be_bytes(unsafe {(*tcphdr).ack_seq});
let syn = unsafe {(*tcphdr).syn()};
let ack = unsafe {(*tcphdr).ack()};
let psh = unsafe {(*tcphdr).psh()};
let fin = unsafe {(*tcphdr).fin()};
info!(&ctx, "seq: {}, ack_seq: {}, syn: {}, ack: {}, data: {}, fin: {}, src: {:i}:{}, dst: {:i}:{}",
seq, ack_seq, syn, ack, psh, fin, src_addr, src_port, dst_addr, dst_port);
Installons ce programme sur les deux interfaces réseaux :
ip netns exec lb cargo run
cargo run -- --iface veth7
Lançons une connexion :
curl 10.0.0.10:8080
Au niveau de veth6, on voit alors les détails des différents segments :
[INFO antiddos_xdp] seq: 2481734691, ack_seq: 0, syn: 1, ack: 0, data: 0, fin: 0, src: 10.0.0.1:47632, dst: 10.0.0.10:8080
[INFO antiddos_xdp] seq: 2481734692, ack_seq: 161636304, syn: 0, ack: 1, data: 0, fin: 0, src: 10.0.0.1:47632, dst: 10.0.0.10:8080
[INFO antiddos_xdp] seq: 2481734692, ack_seq: 161636304, syn: 0, ack: 1, data: 1, fin: 0, src: 10.0.0.1:47632, dst: 10.0.0.10:8080
[INFO antiddos_xdp] seq: 2481734769, ack_seq: 161636460, syn: 0, ack: 1, data: 0, fin: 0, src: 10.0.0.1:47632, dst: 10.0.0.10:8080
[INFO antiddos_xdp] seq: 2481734769, ack_seq: 161637894, syn: 0, ack: 1, data: 0, fin: 0, src: 10.0.0.1:47632, dst: 10.0.0.10:8080
[INFO antiddos_xdp] seq: 2481734769, ack_seq: 161637894, syn: 0, ack: 1, data: 0, fin: 1, src: 10.0.0.1:47632, dst: 10.0.0.10:8080
[INFO antiddos_xdp] seq: 2481734770, ack_seq: 161637895, syn: 0, ack: 1, data: 0, fin: 0, src: 10.0.0.1:47632, dst: 10.0.0.10:8080
Au niveau de veth7 :
[INFO antiddos_xdp] seq: 161636303, ack_seq: 2481734692, syn: 1, ack: 1, data: 0, fin: 0, src: 10.0.0.10:8080, dst: 10.0.0.1:47632,
[INFO antiddos_xdp] seq: 161636304, ack_seq: 2481734769, syn: 0, ack: 1, data: 0, fin: 0, src: 10.0.0.10:8080, dst: 10.0.0.1:47632,
[INFO antiddos_xdp] seq: 161636304, ack_seq: 2481734769, syn: 0, ack: 1, data: 1, fin: 0, src: 10.0.0.10:8080, dst: 10.0.0.1:47632,
[INFO antiddos_xdp] seq: 161636460, ack_seq: 2481734769, syn: 0, ack: 1, data: 1, fin: 0, src: 10.0.0.10:8080, dst: 10.0.0.1:47632,
[INFO antiddos_xdp] seq: 161637894, ack_seq: 2481734769, syn: 0, ack: 1, data: 0, fin: 1, src: 10.0.0.10:8080, dst: 10.0.0.1:47632,
[INFO antiddos_xdp] seq: 161637895, ack_seq: 2481734770, syn: 0, ack: 1, data: 0, fin: 0, src: 10.0.0.10:8080, dst: 10.0.0.1:47632,
Maintenant qu’on a vu tous les détails d’un segment TCP, on va maintenant s’occuper uniquement des segments de type SYN :
match unsafe { (*tcphdr).syn() } {
1 => {}
_ => return Ok(xdp_action::XDP_PASS),
}
Si on a affaire à un SYN, on continue le traitement sinon on laisse passer le paquet.
Comme nous n’allons plus toucher à cette partie du code, nous allons créer une fonction :
#[inline(always)]
fn filter_tcp_syn_src(ctx: &XdpContext) -> Option<[u8; 4]> {
let ethhdr: *const EthHdr = ptr_at(&ctx, 0).ok()?;
match unsafe { (*ethhdr).ether_type() } {
Ok(EtherType::Ipv4) => {}
_ => return None,
}
let ipv4hdr: *const Ipv4Hdr = ptr_at(&ctx, EthHdr::LEN).ok()? ;
match unsafe { (*ipv4hdr).proto } {
IpProto::Tcp => {}
_ => return None,
}
let tcphdr: *const TcpHdr = ptr_at(&ctx, EthHdr::LEN + Ipv4Hdr::LEN).ok()? ;
match unsafe { (*tcphdr).syn() } {
1 => {}
_ => return None,
}
let src_addr = unsafe { (*ipv4hdr).src_addr };
Some(src_addr)
}
fn try_antiddos_xdp(ctx: XdpContext) -> Result<u32, u32> {
let src_addr = match filter_tcp_syn_src(&ctx) {
Some(x) => x,
None => return Ok(xdp_action::XDP_PASS),
};
info!(ctx, "{}", src_addr);
Ok(xdp_action::XDP_PASS)
}
Nous récupérons uniquement l’IP source de cette fonction pour potentiellement la bloquer.
Cela va nous permettre de nous concentrer sur le code pour empêcher l’attaque.
Essayons d’atténuer les attaques SYN flood#
Suite de l’article réservée aux membres premium ✨
L’article complet est accessible uniquement aux membres premium.
Devenir membre premium, c’est simple : il suffit de faire un petit don 💖
En échange, vous aurez pendant 1 an (offre early bird) :
- Accès à tous les articles complets dès leur publication
- Lecture anticipée avant la mise en ligne publique
- Participation au soutien de ce blog indépendant
- Accès exclusif à mes photos de vacances à Dubaï
Votre don permet de :
- Me rendre moins dépendant des grandes plateformes
- M’encourager à créer plus de contenu technique
- Lever le paywall plus rapidement pour tous




