Nous avons vu ce qu’était un programme XDP dans la première partie : cela peut être un moyen de filtrer un flux réseau.
Dans cette partie je vous propose une petite balade dans les différentes en-têtes et de montrer comment on peut opérer le filtrage. Nous allons ainsi voir comment redévelopper xdp-filter
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 le hello world en XDP#
Recréons l’environnement de développement#
Comme nous l’avons vu dans la précédente 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 qu’on a créé :
Créons le programme “hello world”#
Comme nous l’avons fait avec xdp-filter
, nous allons installer le programme XDP sur veth0
.
Générons déjà le programme Aya :
cargo generate --name browser-xdp \
-d program_type=xdp \
-d default_iface=veth0 \
https://github.com/aya-rs/aya-template
Buildons et installons le programme “hello world” :
cd browser-xdp/
cargo run
Sur un autre terminal, testons le programme :
ip netns exec lb ping -c 2 10.0.0.1
Du côté du terminal cargo run
, on voit bien deux paquets :
[INFO browser_xdp] received a packet
[INFO browser_xdp] received a packet
Ainsi à chaque fois que l’interface reçoit un paquet, le programme XDP est lancé.
Maintenant nous allons voir comment arrêter un paquet et quel type de paquet on a affaire.
Les bases pour créer un programme XDP#
Les actions XDP#
Regardons d’abord le code généré par cargo
.
Le fichier le plus important est celui-là : browser-xdp-ebpf/src/main.rs
c’est à dire le code côté noyau.
Globalement le code ressemble aux codes générés pour d’autres types de programmes eBPF comme les Tracepoints :
#[xdp]
pub fn browser_xdp(ctx: XdpContext) -> u32 {
match try_browser_xdp(ctx) {
Ok(ret) => ret,
Err(_) => xdp_action::XDP_ABORTED,
}
}
fn try_browser_xdp(ctx: XdpContext) -> Result<u32, u32> {
info!(&ctx, "received a packet");
Ok(xdp_action::XDP_PASS)
}
Cependant on peut remarquer une petite spécificité : xdp_action::*
.
Cela permet de déterminer ce que fait le paquet une fois qu’il a fini de parcourir le programme eBPF.
On a 5 actions différentes :
XDP_ABORTED
: Arrête le paquet avec erreurXDP_DROP
: Arrête le paquet silencieusementXDP_PASS
: Laisse passer le paquetXDP_REDIRECT
: Redirige le paquet vers une autre interface ou une socketAF_XDP
attachée à une interfaceXDP_TX
: Redirige le paquet vers la même interface
Dans cet article, nous utiliserons uniquement XDP_PASS
et XDP_DROP
.
Le contexte XDP et ses méthodes#
Comme pour tous les autres programmes eBPF, pour aller au-delà du hello world, il faut comprendre comment jouer avec le contexte.
Regardons la documentation :
Contrairement aux précédents types de programmes où on utilisait qu’une seule méthode, il y en a 4 différentes pour XDP :
data()
retourne l’adresse mémoire du début du paquet réseau.data_end()
retourne l’adresse mémoire de fin du paquet réseau.metadata()
retourne l’adresse mémoire du début de la métadonnées de XDP lié au kernel linux ou au driver.metadata_end()
retourne l’adresse mémoire de fin de la métadonnées. De la même manière quedata_end()
,
Dans cet article, nous utiliserons uniquement les méthodes data()
et data_end()
pour récupérer les différentes en-têtes.
Comment récupérer les en-têtes ?#
Rappelons le schéma suivant :
data()
se trouve au début de l’en-tête ethHdrdata_end()
se trouve à la fin du payload
Comment récupérer l’en-tête ethernet (ethHdr) ? Elle se situe au tout début du paquet.
let ethhdr = ctx.data();
Pour récupérer l’en-tête suivante, il suffit de rajouter la longueur de l’en-tête ethernet, en pseudo-code Rust ça donnerait :
let ipv4hdr = ctx.data() + sizeof(ethhdr);
Etc.
Concentrons-nous sur l’en-tête ethernet. Comment récupérer ses éléments ? ctx.data()
est de type usize
. Un entier naturel qui représente une adresse mémoire. Il faut donc récupérer son contenu. Comment faire cela ?
De la même manière que *const u8
représente un pointeur vers une chaine de caractères, ici on doit faire la même chose pour l’en-tête ethernet.
Ainsi il faut convertir dans une structure qui représente l’en-tête de la trame Ethernet. On a plusieurs possibilités pour le faire comme aller voir le code C et le convertir en Rust ce qui est un bon exercice mais un peu fastidieux. Heureusement, comme Rust est désormais bien présent dans l’écosystème d’eBPF, il y a déjà une crate Rust qui nous a fait le travail : network-types.
Pour récupérer cette crate, on va modifier le fichier browser-xdp-ebpf/Cargo.toml
et rajouter dans la section dependencies :
network-types = "0.1.0"
On va ainsi pouvoir l’inclure dans le code principal (browser-xdp-ebpf/src/main.rs
) :
use network_types::eth::EthHdr;
[...]
let ethhdr = ctx.data() as *const EthHdr;
Maintenant qu’on a récupéré la structure de l’en-tête ethernet, nous pouvons récupérer les différents éléments qui composent l’en-tête :
- DST MAC : l’adresse MAC destination
- SRC MAC : l’adresse MAC source
- ETHERTYPE : détermine le protocole du niveau supérieur (Comme IPv4, IPv6, Arp pour citer les plus connus).
Comment le faire avec Aya ? Regardons la documentation :
Ainsi pour récupérer l’adresse de destination, il suffit de coder :
let dst_addr = unsafe { (*ethhdr).dst_addr};
info!(&ctx, "dst_addr {:mac} ", dst_addr);
Nous devons utiliser unsafe car nous déréférençons un pointeur brut.
info!
nous permet de convertir des tableaux de 6 octets en une adresse MAC. Si vous préférez la notation en majuscule, il suffit d’écrire :MAC
.Essayons maintenant. Voici le code principal modifié :
use network_types::eth::EthHdr;
fn try_browser_xdp(ctx: XdpContext) -> Result<u32, u32> {
let ethhdr = ctx.data() as *const EthHdr;
let dst_addr = unsafe { (*ethhdr).dst_addr};
info!(&ctx, "dst_addr {:mac} ", dst_addr);
info!(&ctx, "received a packet");
Ok(xdp_action::XDP_PASS)
}
Buildons le programme :
cargo build
La compilation fonctionne.
Installons le dans le noyau :
cargo run
On a alors l’erreur suivante :
Error: the BPF_PROG_LOAD syscall failed. Verifier output: 0: R1=ctx() R10=fp0
; unsafe { (*self.ctx).data as usize } @ xdp.rs:16
0: (61) r1 = *(u32 *)(r1 +0) ; R1_w=pkt(r=0)
; let dst_addr = unsafe { (*ethhdr).dst_addr}; @ main.rs:20
1: (71) r0 = *(u8 *)(r1 +1)
invalid access to packet, off=1 size=1, R1(id=0,off=1,r=0)
R1 offset is outside of the packet
verification time 65 usec
stack depth 0
processed 2 insns (limit 1000000) max_states_per_insn 0 total_states 0 peak_states 0 mark_read 0
Caused by:
Permission denied (os error 13)
Le verifier n’aime pas vraiment cette partie du code :
let ethhdr = ctx.data() as *const EthHdr;
L’erreur peut se traduire en français par :
Accès invalide au paquet.
L'offset du registre 1 est en dehors du paquet
Pas super clair. Essayons d’expliquer mieux. Quand on tente let ethhdr = ctx.data() as *const EthHdr;
, Il y a un accès à la mémoire. Le verifier exige qu’on prouve que cet accès soit toujours dans le paquet (entre data()
et data_end()
).
Il faut donc faire une vérification pour le rassurer avant cet accès :
if ctx.data() + EthHdr::LEN > ctx.data_end() {
return Ok(xdp_action::XDP_PASS);
}
On peut traduire par : si l’accès ne se trouve pas au niveau du paquet, le programme XDP ne traite pas le paquet : il le laisse continuer son chemin dans la pile réseau de noyau Linux.
Sinon on peut alors convertir en EthHdr
:
let ethhdr = ctx.data() as *const EthHdr;
Voici le code complet de la fonction principale :
fn try_browser_xdp(ctx: XdpContext) -> Result<u32, u32> {
let start = ctx.data() ;
let end = ctx.data_end() ;
if start + EthHdr::LEN > end { //Check for the verifier
return Ok(xdp_action::XDP_PASS);
}
let ethhdr = start as *const EthHdr;
let dst_addr = unsafe { (*ethhdr).dst_addr};
info!(&ctx, "dst_addr {:mac} ", dst_addr);
info!(&ctx, "received a packet");
Ok(xdp_action::XDP_PASS)
}
Vérifions que ça marche :
cargo run
Lançons des ping. On récupère bien l’adresse mac :
[INFO browser_xdp] dst_addr de:ad:be:ef:00:01
[INFO browser_xdp] received a packet
[INFO browser_xdp] dst_addr de:ad:be:ef:00:01
[INFO browser_xdp] received a packet
Maintenant qu’on a réussi à récupérer l’en-tête ethHdr
, comment récupérer les autres en-têtes ? Il suffit de rajouter la longueur des en-têtes qui les précèdent.
De manière assez évidente, nous allons utiliser des méthodes très proches pour toutes les en-têtes. Il serait judicieux de créer une fonction. La voilà :
|
|
Quelques remarques sur la création de cette fonction :
- (L3) On inline la fonction pour des questions de performance
- (L4)
<T>
: permet de créer une fonction générique non dépendante de la structure - l’offset permet de naviguer d’en-tête en en-tête (offset = 0 pour ethHdr)
- (L7) On a dû changer la méthode pour calculer la longueur de la structure avec un
size_of
pour que ça soit possible pour n’importe quelle structure
Le code principal devient alors :
fn try_browser_xdp(ctx: XdpContext) -> Result<u32, u32> {
let ethhdr: *const EthHdr = ptr_at(&ctx, 0)?;
let dst_addr = unsafe { (*ethhdr).dst_addr};
info!(&ctx, "dst_addr {:mac} ", dst_addr);
info!(&ctx, "received a packet");
Ok(xdp_action::XDP_PASS)
}
Maintenant qu’on a créé la fonction permettant de se balader dans les différentes en-têtes, regardons comment on fait pour bloquer ou laisser passer les différents paquets en fonction des en-têtes réseaux.
Visite guidée des différentes en-têtes#
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