Aller au contenu
Créons un petit load balancer avec XDP
  1. Ebpf-Another-Types/

Créons un petit load balancer avec XDP

·1461 mots·7 mins·
Joseph Ligier
Auteur
Joseph Ligier
CNCF ambassador | Kubestronaut 🐝
Sommaire
Apprenons XDP avec Aya - Cet article fait partie d'une série.
Partie 4: Cet article

Nous avons vu ce qu’était un programme XDP dans la première partie : cela peut être un moyen de créer un répartisseur de charge ou plus couramment appelé : load balancer.

Dans cette partie, je vous propose de créer un programme XDP qui va répartir la charge de paquets de type ICMP. On va faire cela avec le framework Rust Aya.

Suivez le guide !

lab

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 :

Killer coda screenshot


Qu’allons nous faire ?
#

Load Balancer ICMP
#

Nous allons créer un load balancer ICMP (Internet Control Message Protocol). Ainsi en pingant le load balancer on va, en réalité, pinger un autre serveur qu’on appelle backend.

Load Balancer

Rappelons brièvement comment fonctionne le protocole ICMP. Le client envoie à un serveur un paquet ICMP de type echo request. Le serveur répond par un autre paquet ICMP de type echo reply.

Pour en savoir plus sur ICMP, vous pouvez consulter cette page.

Pourquoi pas TCP ou UDP ?
#

C’est vrai qu’un load balancer ICMP peut sembler pas super utile. Cependant cela me semble le plus pédagogique pour commencer car il est le plus facile à créer. Par ailleurs, la plupart des concepts dont on va parler dans cet article sera encore utile pour ces load balancers.

À titre indicatif, voici l’ordre de difficulté selon moi :

  1. Load Balancer ICMP
  2. Load Balancer UDP
  3. Load Balancer TCP

Créons naïvement le load balancer
#

Recréons l’environnement de développement
#

Comme nous l’avons vu dans la première partie, je vous propose d’installer des namespaces :

git clone https://github.com/littlejo/eunomia.dev
cd eunomia.dev/docs/tutorials/42-xdp-loadbalancer/
./setup.sh

Rappelons ce que cela a généré :

namepaces

Nous allons créer un load balancer XDP au niveau de l’interface veth6 qui va répartir la charge sur h2 et h3. Nous allons modifier l’adresse de destination dans XDP de 10.0.0.10 par 10.0.0.2 ou 10.0.0.3 :

namepaces

C’est ce qu’on appelle un DNAT (Destination Network Address Translation).

Pour commencer nous allons rediriger les paquets réseaux uniquement vers h2 (10.0.0.2), ça donnerait donc :

namepaces

Créons le programme XDP Hello world
#

Générons déjà le programme Aya :

cargo generate --name lb-xdp \
               -d program_type=xdp \
               -d default_iface=veth6 \
               https://github.com/aya-rs/aya-template
cd lb-xdp/

En prévision, nous allons utiliser des crates supplémentaires :

  • 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 lb-xdp-ebpf/Cargo.toml et rajouter dans la section dependencies :

network-types = "0.1.0"
blog-xdp = { git = "https://github.com/littlejo/blog-xdp" }

Buildons le programme hello world :

cargo build

Testons le rapidement :

ip netns exec lb cargo run

on fait ping -c1 10.0.0.10, ça fait bien received a packet du côté cargo run.

Comme le namespace n’a pas accès à Internet, il faut commencer par télécharger les crates pour les compiler au niveau de l’hôte puis installer le programme eBPF au niveau du namespace.

Récupérons uniquement les pings
#

Maintenant, on va modifier le programme Aya du côté kernel : lb-xdp-ebpf/src/main.rs. On va récupérer uniquement les paquets ICMP.

headers

Nous avons déjà écrit un code similaire dans un précédent épisode donc je vous donne le résultat des modifications :

use network_types::{
    eth::{EthHdr, EtherType},
    ip::{Ipv4Hdr, IpProto},
    icmp::IcmpHdr,
};
use blog_xdp::helper::ptr_at;

#[inline(always)]
fn filter_icmp(ctx: &XdpContext) -> Option<(*const Ipv4Hdr, *const IcmpHdr)> {
    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()? ;
    let icmphdr: *const IcmpHdr = ptr_at(&ctx, EthHdr::LEN + Ipv4Hdr::LEN).ok()? ;

    match unsafe { (*ipv4hdr).proto } {
         IpProto::Icmp => {}
         _ => return None,
    }

    Some((ipv4hdr, icmphdr))
}

fn try_lb_xdp(ctx: XdpContext) -> Result<u32, u32> {
    let (ipv4hdr, icmphdr) = match filter_icmp(&ctx) {
        Some(x) => x,
        None => return Ok(xdp_action::XDP_PASS),
    };
    let dst_addr = unsafe { (*ipv4hdr).dst_addr };
    let src_addr = unsafe { (*ipv4hdr).src_addr };

    info!(&ctx,
        "src={:i}, dst={:i}", src_addr, dst_addr);
    Ok(xdp_action::XDP_PASS)
}

Le message de type : src=10.0.0.1, dst=10.0.0.10 est affiché seulement quand un paquet ICMP est envoyé. C’est à cet endroit où on va développer le load balancer.

Vérifions :

ip netns exec lb cargo run

Sur un autre terminal, pingons le futur load balancer :

ping -c 2 10.0.0.10

Sur le terminal cargo run, il est alors affiché :

[INFO  lb_xdp] src=10.0.0.1, dst=10.0.0.10
[INFO  lb_xdp] src=10.0.0.1, dst=10.0.0.10

N’hésitez pas à tester d’autres protocoles pour vérifier qu’il ne s’affiche rien.

Améliorons les logs
#

Pour mieux comprendre comment va se passer cette répartition de charge, il est intéressant d’afficher un peu plus de détail sur le paquet reçu.

Regardons le contenu d’une en-tête ICMP :

icmphdr

  • Type : le type de requête ICMP (8 correspond à echo request et 0 correspond à echo reply)
  • Code : le code permet de préciser le type (pas utile pour echo request et echo reply)
  • Checksum : pour l’intégrité du paquet

Pour récupérer ces éléments, on peut utiliser la crate network-types.

icmp hdr

Ainsi pour récupérer le type de paquet ICMP, on peut le faire de cette manière :

let type_ = unsafe { (*icmphdr).type_ };

Créons une fonction dédiée au logs :

#[inline(always)]
fn log_icmp(ctx: &XdpContext, ipv4hdr: *const Ipv4Hdr, icmphdr: *const IcmpHdr) {
    let type_ = unsafe { (*icmphdr).type_ };
    let dst_addr = unsafe { (*ipv4hdr).dst_addr };
    let src_addr = unsafe { (*ipv4hdr).src_addr };

    info!(ctx,
        "src={:i} dst={:i} ({})", src_addr, dst_addr, type_);
}

Le code principal devient :

fn try_lb_xdp(ctx: XdpContext) -> Result<u32, u32> {
    let (ipv4hdr, icmphdr) = match filter_icmp(&ctx) {
        Some(x) => x,
        None => return Ok(xdp_action::XDP_PASS),
    };
    log_icmp(&ctx, ipv4hdr, icmphdr); //MODIFY
    Ok(xdp_action::XDP_PASS)
}

Relançons ip netns exec lb cargo run puis le ping :

[INFO  lb_xdp] src=10.0.0.1 dst=10.0.0.10 (8)

On peut maintenant s’assurer que le paquet ICMP est bien de type echo request.

namepaces

On aurait installé le programme sur veth7. Ça serait inversé : l’IP source serait 10.0.0.10 et l’IP destination 10.0.0.1. Le paquet ICMP serait alors de type echo reply.

Modifions l’IP de destination
#

C’est à partir de maintenant que ça devient intéressant. Rappelons que le but est d’obtenir cela dans un premier temps :

namepaces

On doit donc remplacer l’adresse de destination par 10.0.0.2 :

let backend_ip = [10, 0, 0, 2];
unsafe {
    (*ipv4hdr).dst_addr = backend_ip;
}

Cela n’est pas possible de faire ça car la variable ipv4hdr n’est pas mutable. Il faut donc remplacer const par mut :

let ipv4hdr: *const Ipv4Hdr = ptr_at(&ctx, EthHdr::LEN)? ;

La fonction ptr_at retourne un Result<*const T, u16>. On pourrait recréer une fonction quasi identique mais avec comme retour Result<*mut T, u16>. Mais il y a plus propre en reprennant la fonction originelle et en castant le type :

#[inline(always)]
fn ptr_at_mut<T>(ctx: &XdpContext, offset: usize) -> Result<*mut T, u16> {
    let ptr = ptr_at::<T>(ctx, offset)?;
    Ok(ptr as *mut T)
}

On se retrouve alors avec le code principal suivant :

#[inline(always)]
fn filter_icmp(ctx: &XdpContext) -> Option<(*mut Ipv4Hdr, *const IcmpHdr)> { //MODIFY
    let ethhdr: *const EthHdr = ptr_at(ctx, 0).ok()?;

    match unsafe { (*ethhdr).ether_type() } {
            Ok(EtherType::Ipv4) => {}
            _ => return None,
        }

    let ipv4hdr: *mut Ipv4Hdr = ptr_at_mut(ctx, EthHdr::LEN).ok()? ; //MODIFY
    let icmphdr: *const IcmpHdr = ptr_at(&ctx, EthHdr::LEN + Ipv4Hdr::LEN).ok()? ;

    match unsafe { (*ipv4hdr).proto } {
         IpProto::Icmp => {}
         _ => return None,
    }

    Some((ipv4hdr, icmphdr))
}

fn try_lb_xdp(ctx: XdpContext) -> Result<u32, u32> {
    let (ipv4hdr, icmphdr) = match filter_icmp(&ctx) {
        Some(x) => x,
        None => return Ok(xdp_action::XDP_PASS),
    };
    let backend_ip = [10, 0, 0, 2]; //ADD
    unsafe { //ADD
        (*ipv4hdr).dst_addr = backend_ip; //ADD
    } //ADD

    log_icmp(&ctx, ipv4hdr, icmphdr);
    Ok(xdp_action::XDP_PASS)
}

Testons la compilation et l’installation du programme eBPF :

ip netns exec lb cargo run

Pas de soucis.

Testons le ping :

ping -c 2 10.0.0.10

Au niveau de cargo run, on voit bien les paquets avec la nouvelle destination :

[INFO  lb_xdp] src=10.0.0.1 dst=10.0.0.2 (8)
[INFO  lb_xdp] src=10.0.0.1 dst=10.0.0.2 (8)

MAIS cela ne ping plus :

PING 10.0.0.10 (10.0.0.10) 56(84) bytes of data.

--- 10.0.0.10 ping statistics ---
2 packets transmitted, 0 received, 100% packet loss, time 1006ms

On semble à la fois proche et loin du but. Regardons où peut venir le problème.


Résolvons le problème
#

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

👉 Devenir membre premium dès maintenant

Apprenons XDP avec Aya - Cet article fait partie d'une série.
Partie 4: Cet article