Déboguer un programme XDP qui ne matche pas : checklist complète
Pour déboguer un programme XDP qui refuse obstinément de matcher le trafic attendu, une démarche méthodique s'impose. Vérification du SKB path, ordre des règles BPF, offsets incorrects dans le parsing des headers, mode XDP native versus generic, conflits avec d'autres hooks réseau… cette checklist 2026 recense les pièges classiques et les outils de diagnostic pour valider qu'un programme eBPF XDP analyse bien les paquets au bon endroit et avec la bonne logique.
1. Confirmer que le programme XDP est effectivement attaché
Premier réflexe : vérifier que le hook XDP est bien actif sur l'interface cible. Une faute de frappe dans le nom de l'interface, une commande ip link set dev qui échoue silencieusement, ou un redémarrage réseau qui détache le programme suffisent à faire disparaître le filtre.
ip link show dev eth0
# Chercher la ligne "prog/xdp" ou "xdp/id"
Si aucune mention « xdp » n'apparaît, le programme n'est pas chargé. Dans ce cas, ré-attacher avec :
ip link set dev eth0 xdp obj pakkt_engine.bpf.o sec xdp
En production, PAKKT.io s'assure que l'agent Go maintient le hook actif et remonte une alerte si le programme se détache (par exemple après un ip link set dev eth0 xdp off manuel).
1.1. Mode XDP : native vs. generic vs. offload
XDP peut s'exécuter à trois niveaux :
- Native (driver) : le driver réseau exécute le programme eBPF avant même l'allocation du SKB. Meilleur pour la performance.
- Generic (SKB) : exécuté plus tard, dans le stack réseau, après l'allocation du
sk_buff. Utilisé en fallback si le driver ne supporte pas XDP native. - Offload (NIC) : certaines cartes Netronome/Intel exécutent le bytecode directement sur le hardware.
La commande ip link set dev eth0 xdpgeneric force le mode generic ; inversement, xdpdrv force le mode native. En generic, le parsing peut se comporter différemment (offset, disponibilité de certains helpers). Tester avec :
ip -d link show dev eth0 | grep xdp
Si le mode est « generic » alors que le driver supporte native (i40e, mlx5, ixgbe…), le programme travaille sur des structures différentes et peut ignorer certains paquets très tôt.
2. Valider le parsing des headers réseau : offsets et bounds check
Le verifier du noyau refuse tout programme XDP qui lit au-delà de data_end. Mais un offset calculé à la main (par exemple void *nh = data + 14) peut pointer vers un fragment manquant si le paquet Ethernet est taggé VLAN (802.1Q), MPLS, ou encapsulé GRE/VXLAN.
2.1. Vérifier la taille minimale du paquet
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
if (data + sizeof(struct ethhdr) > data_end)
return XDP_PASS; // trop court pour contenir un header Ethernet
struct ethhdr *eth = data;
Même logique pour IPv4 (20 octets minimum), TCP (20 octets minimum). Sans ces gardes, le verifier rejette le programme ou, pire, une condition de race produit un faux négatif silencieux.
2.2. Gérer les tags VLAN 802.1Q
Un paquet taggé VLAN insère 4 octets après le champ h_proto d'Ethernet. Si votre code suppose un offset fixe de 14 octets, il lira en réalité le TCI VLAN au lieu de l'EtherType IP. Boucle de parsing recommandée :
__u16 eth_proto = eth->h_proto;
int offset = sizeof(struct ethhdr);
if (eth_proto == bpf_htons(ETH_P_8021Q) || eth_proto == bpf_htons(ETH_P_8021AD)) {
if (data + offset + 4 > data_end)
return XDP_PASS;
struct vlan_hdr *vhdr = data + offset;
eth_proto = vhdr->h_vlan_encapsulated_proto;
offset += 4;
}
if (eth_proto != bpf_htons(ETH_P_IP))
return XDP_PASS;
struct iphdr *iph = data + offset;
if ((void *)iph + sizeof(struct iphdr) > data_end)
return XDP_PASS;
Cette logique permet de décaler l'offset dynamiquement. En environnement de production où VLAN et QinQ coexistent (fournisseurs cloud, datacenters), ce détail explique 80 % des « le programme ne voit jamais le port 25565 ».
2.3. TCP et UDP : parser la couche transport
Après l'IPv4, calculer l'offset du header L4 en tenant compte de l'IHL (Internet Header Length). Si des options IP sont présentes, la couche TCP/UDP ne commence pas à iph + 20 mais à iph + (iph->ihl * 4).
__u8 ihl = iph->ihl * 4;
if (iph->protocol == IPPROTO_TCP) {
struct tcphdr *tcph = (void *)iph + ihl;
if ((void *)tcph + sizeof(struct tcphdr) > data_end)
return XDP_PASS;
__u16 dport = bpf_ntohs(tcph->dest);
// Matcher dport ici
}
En cas d'erreur d'offset, tcph->dest lit un octet du payload IP, le port comparé est aléatoire, et la règle rate 100 % des paquets légitimes.
3. Inspecter les BPF maps et les compteurs de règles
Un programme XDP moderne (comme le PAKKT Engine) stocke ses règles dans une BPF map de type BPF_MAP_TYPE_HASH ou BPF_MAP_TYPE_ARRAY. Si la map est vide ou si la clé attendue n'existe pas, le lookup échoue et le paquet passe sans traitement.
3.1. Lister le contenu des maps
bpftool map list
# Identifier l'ID de la map "pakkt_rules" (ou nom équivalent)
bpftool map dump id 42
Chaque entrée affiche la clé (par exemple { port: 25565, proto: 6 }) et la valeur (action block, rate_limit avec max_pps, etc.). Si le port 25565 n'apparaît pas, la règle n'a jamais été insérée côté userspace. Vérifier que l'API ou le daemon PAKKT a bien appliqué la configuration.
3.2. Compteurs par règle (percpu array)
Si la map de stats est de type BPF_MAP_TYPE_PERCPU_ARRAY, chaque core CPU maintient un compteur indépendant. Pour connaître le nombre réel de paquets matchés :
bpftool map dump name pakkt_stats
Additionner les valeurs de tous les CPUs. Si le compteur reste à zéro après injection de trafic (via hping3 ou nping), le programme ne rentre jamais dans la branche de match. Causes fréquentes :
- Clé de lookup incorrecte (endianness : oublier
bpf_ntohssur le port). - Masque IP mal défini (whitelist /32 alors que le paquet vient d'un /24).
- Protocole non supporté (règle TCP, paquet UDP).
3.3. Journalisation avec bpf_printk
Pour tracer la logique interne, insérer des appels bpf_printk (limité à 3 arguments numériques) :
bpf_printk("XDP: proto=%u dport=%u\n", iph->protocol, bpf_ntohs(tcph->dest));
Lire les traces via :
cat /sys/kernel/debug/tracing/trace_pipe
⚠️ En production, bpf_printk impacte les performances (sérialisation vers ftrace). À réserver au debug. PAKKT préfère incrémenter des compteurs dans une map dédiée et les exporter via l'agent Go toutes les 30 secondes vers TimescaleDB.
4. Conflits avec d'autres hooks et ordre d'exécution
XDP s'exécute avant le stack réseau (netfilter, iptables, nftables), mais d'autres outils peuvent interférer :
- TC (Traffic Control) : un programme eBPF attaché à
tc ingressoutc egresspeut drop le paquet après XDP. Vérifier avectc filter show dev eth0 ingress. - nftables/iptables prerouting : si une règle
dropexiste en chaîneprerouting, le paquet disparaît avant d'atteindreINPUT. La tableinet pakktde PAKKT est isolée et priorité 0, mais d'autres tables peuvent avoir priorité négative. - Fail2ban : certaines règles dynamiques insèrent des
rejectdans la tablefilter. Si XDP laisse passer (XDP_PASS), le paquet arrive à netfilter et peut être bloqué là. - Docker : crée des chaînes
DOCKER-USERetDOCKERdansfilter. En général, Docker ne touche que les paquets à destination des conteneurs (NAT), mais une mauvaise règle globale peut drop tout le trafic entrant.
Pour isoler XDP du reste, tester avec un serveur minimal (aucune règle iptables/nftables, aucun TC) :
nft flush ruleset
tc qdisc del dev eth0 clsact 2>/dev/null || true
ip link set dev eth0 xdp obj pakkt_engine.bpf.o sec xdp
Puis injecter du trafic. Si le compteur XDP augmente mais que le service final ne reçoit rien, le problème est post-XDP. Si le compteur reste à zéro, le programme XDP lui-même ne matche pas.
4.1. Ordre des verdicts XDP
Un programme XDP retourne l'un des cinq codes :
| Verdict | Signification |
XDP_ABORTED |
Erreur ; paquet droppé, trace dans kernel log. |
XDP_DROP |
Paquet droppé immédiatement (pas de SKB alloué). |
XDP_PASS |
Continue vers le stack réseau (netfilter, socket). |
XDP_TX |
Renvoie le paquet par la même interface (bounce). |
XDP_REDIRECT |
Redirige vers une autre interface ou CPU. |
Si votre logique retourne XDP_DROP par défaut au lieu de XDP_PASS, tout le trafic non-matché disparaît. Inverser la logique :
// Mauvais
return XDP_DROP; // défaut
// Bon
return XDP_PASS; // défaut, sauf si règle explicite block
Le PAKKT Engine utilise un switch sur rule_type : allow_only inverse la logique (whitelist), sinon défaut = PASS.
5. Tester en isolation avec des outils de fuzzing réseau
Plutôt que d'attendre qu'un vrai DDoS déclenche la règle, forger des paquets synthétiques avec hping3, nping, ou scapy permet de contrôler chaque champ :
# TCP SYN vers port 25565
hping3 -S -p 25565 --flood 203.0.113.42
# UDP flood
hping3 --udp -p 19132 --flood 203.0.113.42
# ICMP echo request
hping3 --icmp --icmptype 8 --flood 203.0.113.42
Observer en parallèle :
watch -n 1 'bpftool map dump name pakkt_stats'
Si les compteurs bougent, la règle matche. Si rien ne bouge, vérifier :
- Le port source/destination dans la map.
- Le protocole (6 = TCP, 17 = UDP, 1 = ICMP).
- L'endianness (le port 25565 en big-endian = 0x63DD ; en little-endian = 0xDD63).
5.1. Exemple de script scapy pour tester un offset
#!/usr/bin/env python3
from scapy.all import *
# Paquet VLAN-taggé + TCP SYN
pkt = Ether()/Dot1Q(vlan=100)/IP(dst="203.0.113.42")/TCP(dport=25565, flags="S")
sendp(pkt, iface="eth0", count=10)
Si le programme XDP ne détecte pas ce paquet, c'est probablement que le parsing VLAN est absent (cf. section 2.2).
6. Checklist récapitulative pour déboguer un programme XDP
- Programme attaché ? →
ip link show dev eth0doit mentionner « xdp ». - Mode XDP correct ? → Préférer
xdpdrv(native) si le driver le supporte. - Parsing des headers valide ? → Vérifier les bounds checks (
data + offset > data_end), gérer VLAN, IHL variable. - Maps peuplées ? →
bpftool map dumppour confirmer que la règle existe. - Endianness correct ? → Utiliser
bpf_ntohssur les ports,bpf_ntohlsur les IPs. - Compteurs incrémentés ? → Si zéro, la branche de match n'est jamais atteinte.
- Logs bpf_printk ? → Tracer
/sys/kernel/debug/tracing/trace_pipepour suivre la logique interne. - Conflits TC/netfilter ? → Tester sur une machine vierge ou flusher les règles tierces.
- Verdict par défaut ? → S'assurer que
XDP_PASSest retourné si aucune règle ne matche (sauf si whitelist stricte). - Test synthétique ? → Forger des paquets ciblés avec hping3/scapy pour isoler le problème.
En suivant cette checklist, 95 % des cas de « programme XDP muet » se résolvent en moins de 10 minutes. Le 5 % restant relève généralement d'un bug driver (corriger le noyau ou basculer en mode generic temporairement) ou d'un malentendu sur la sémantique du verifier eBPF (cf. ebpf.io pour la documentation officielle).
Conclusion
Déboguer un programme XDP exige rigueur et méthode : vérifier l'attachement du hook, valider chaque offset de parsing, inspecter les BPF maps, tracer les compteurs et isoler les conflits avec d'autres couches réseau. Avec les bons outils (bpftool, hping3, tc) et une compréhension claire du path du paquet depuis le driver jusqu'au socket, tout problème de matching devient reproductible et résoluble. Cette checklist 2026 constitue le socle pour opérer des pipelines XDP fiables en production, qu'il s'agisse de filtrage anti-DDoS, de rate-limiting par port ou de whitelisting IP avancé.
FAQ
Pourquoi mon programme XDP matche en mode generic mais pas en mode native ?
En mode generic, le paquet est déjà partiellement parsé par le stack réseau (SKB alloué), ce qui masque certains cas limites (offsets VLAN, fragments). En mode native, le programme travaille directement sur le buffer DMA du driver, avant toute normalisation. Vérifier que le parsing gère explicitement les tags 802.1Q et que les bounds checks couvrent tous les offsets dynamiques. Tester avec tcpdump -i eth0 -e pour confirmer la présence de VLAN dans les trames réelles.
Comment identifier qu'un paquet est droppé par XDP et non par netfilter ou le firewall applicatif ?
Incrémenter un compteur eBPF dédié juste avant chaque return XDP_DROP, puis comparer avec les statistiques de nft list ruleset (compteurs par règle) et les logs applicatifs. Si le compteur XDP augmente mais que netfilter ne voit rien, le drop est bien en XDP. Utiliser bpf_printk pour logger l'IP source et le port au moment du drop, puis corréler avec tcpdump -i eth0 pour confirmer que le paquet arrive bien sur l'interface.
Peut-on attacher plusieurs programmes XDP sur la même interface pour chaîner les traitements ?
Non, une seule instance de programme XDP par interface. Pour chaîner des traitements, soit regrouper toute la logique dans un unique objet BPF (avec des tail calls vers d'autres programmes si nécessaire), soit utiliser des BPF maps partagées pour orchestrer plusieurs passes logiques au sein du même programme. PAKKT maintient un programme unique (PAKKT Engine) qui gère jusqu'à 256 règles simultanées via une map centrale, évitant tout conflit multi-programme.
Déployez PAKKT en 30 secondes
Protection kernel double couche XDP + nftables, pilotable depuis un panel centralisé. Essai gratuit 7 jours.