Aller au contenu

Le chemin d'un paquet

300 Concevoir ⏱ 1 h 20 kubernetesciliumiptablesebpf

À la fin, vous saurez

  • Décrire le trajet d'un paquet entre deux pods du même nœud, puis de nœuds différents
  • Expliquer où et quand l'adresse d'un Service est traduite, et comment la réponse revient
  • Identifier où a lieu une traduction de source (masquerade) et pourquoi
  • Comparer externalTrafficPolicy Cluster et Local pour l'adresse source et la répartition
  • Choisir l'outil et l'endroit pour observer un paquet (ip route, nsenter, tcpdump, conntrack, Hubble)

Prérequis

Testé avec cilium 1.20.2 kubernetes 1.36 , vérifié le 5 octobre 2026

Pourquoi

« Le Service ne répond pas » : la plainte se reçoit sans plus de précision. Entre le client et la réponse de Signalements, un paquet peut franchir une paire veth, un pont ou un programme eBPF, une route ou un tunnel, une traduction de destination, une traduction de source, et, venant de l'extérieur, un répartiteur de charge puis un nœud qui n'héberge même pas le pod. Chacune de ces étapes peut échouer d'une façon qui lui est propre.

Savoir où regarder est ce qui distingue un diagnostic d'une série d'essais. La leçon suit un paquet dans six situations, du plus simple au plus long, en s'appuyant sur ce que vous avez vu dans le cours TCP/IP : le routage, la traduction d'adresses et son suivi de connexion. Elle ajoute ensuite les commandes pour observer chaque étape, sans présenter de sortie inventée.

Les concepts

Quelques repères valent pour toute la leçon. Les adresses sont celles du fil rouge sur kind : le pod client diag a l'adresse 10.244.1.5 sur le nœud W1, un pod de Signalements 10.244.2.7 sur le nœud W2, un autre 10.244.1.9 sur W1, et le Service signalements l'adresse virtuelle 10.96.40.12. Ce sont des valeurs d'illustration.

Cas 1 : deux pods sur le même nœud

Chaque pod a une extrémité de paire veth dans son espace de noms (eth0) et l'autre extrémité dans l'espace de noms du nœud. Le paquet de diag vers 10.244.1.9 sort par son eth0, apparaît côté nœud, et doit être remis à l'autre veth. Deux réalisations :

  • Avec un pont Linux (plugins bridge, Flannel) : toutes les extrémités côté nœud sont branchées sur un pont cni0, qui se comporte comme un commutateur de niveau 2. Le pont apprend les adresses MAC et transmet la trame à la bonne veth. Du point de vue de diag, les pods du nœud sont sur un même segment Ethernet (ARP inclus).
  • Avec routage ou eBPF (kindnet, Cilium) : pas de pont. Le nœud route par adresse IP, vers l'interface veth du pod. Avec Cilium, un programme eBPF attaché à la veth du pod (côté nœud) examine le paquet, décide de sa destination en consultant une table, puis le redirige directement vers la veth du destinataire, sans repasser par la pile IP complète du nœud. Le gain est de la latence et du travail processeur ; la logique visible reste « un saut local ».

Cas 2 : deux pods sur des nœuds différents

Le paquet de 10.244.1.5 vers 10.244.2.7 quitte W1. Le nœud doit savoir que 10.244.2.0/24 est derrière W2 : c'est la route par sous-réseau de nœud installée par le plugin (leçon 1). Selon le mode de transport (leçon 2) :

  • Routage direct : le nœud route le paquet tel quel vers l'adresse de W2 comme prochain saut. Le réseau des nœuds voit un paquet dont la source est 10.244.1.5 : il doit l'accepter (c'est son rôle, en routage natif). W2 le reçoit, le route vers la veth du pod de destination.
  • Encapsulation : une interface de tunnel (cilium_vxlan chez Cilium, flannel.1 chez Flannel) enveloppe le paquet dans un paquet UDP de W1 vers W2 (port 8472 pour VXLAN chez Cilium). W2 désencapsule et livre.
    sequenceDiagram
  participant D as diag (W1) 10.244.1.5
  participant W1 as Nœud W1
  participant R as Réseau des nœuds
  participant W2 as Nœud W2
  participant S as signalements (W2) 10.244.2.7
  D->>W1: paquet vers 10.244.2.7 (via veth)
  W1->>R: routage direct, ou UDP W1 vers W2 (tunnel)
  R->>W2: livraison au nœud
  W2->>S: veth du pod
  S-->>D: réponse, chemin inverse (la source reste 10.244.2.7)
  

Dans les deux cas, aucune traduction d'adresses : l'adresse source reste celle de diag du début à la fin. C'est la garantie 2 du modèle.

Cas 3 : vers un Service

diag appelle signalements:80, donc 10.96.40.12:80. Cette adresse n'existe sur aucune interface ; le paquet doit être réécrit avant d'aller où que ce soit. Deux réalisations.

kube-proxy (modes iptables ou nftables). Le paquet sort de la veth de diag et entre dans la pile du nœud. Dans le hook PREROUTING de la table nat, une règle reconnaît l'adresse et le port du Service, et saute dans une chaîne qui choisit un point d'accès parmi les pods prêts (voir la leçon kube-proxy et ses alternatives). Elle fait un DNAT : la destination devient 10.244.2.7:8000. À partir de cet instant, le paquet est un paquet ordinaire vers un pod, routé comme au cas 2. Pour un client qui est un processus du nœud lui-même (le kubelet, un agent), la même règle existe dans le hook OUTPUT.

Le suivi de connexion (conntrack) enregistre la correspondance dans la table de suivi : tous les paquets suivants de la même connexion sont réécrits de la même façon, sans nouveau tirage. C'est ce qui rend la répartition « par connexion ».

Cilium sans kube-proxy. Cilium peut traduire avant que le paquet n'existe : un programme eBPF attaché à l'appel système connect() du conteneur (socket-LB, répartition « côté socket ») remplace l'adresse du Service par celle d'un pod au moment où le processus ouvre la connexion. Le paquet naît déjà avec la bonne destination, et aucune règle ni entrée de conntrack n'est nécessaire pour la traduction du trafic interne. Pour le trafic qui ne passe pas par un socket du nœud (arrivant de l'extérieur), un programme eBPF attaché à l'interface réseau fait la traduction paquet par paquet. La documentation de Cilium précise que le remplacement de kube-proxy repose sur cette fonction de socket.

Cas 4 : le retour

Le pod 10.244.2.7 répond à 10.244.1.5 (l'adresse qu'il a vue). Pour le client, cependant, la réponse doit paraître venir de 10.96.40.12:80 : s'il l'appelait en signalements:80 et qu'une réponse arrive d'une autre adresse, la pile TCP du client la rejetterait (ce n'est pas la connexion qu'elle connaît).

  • Avec kube-proxy : la réponse revient sur le nœud W1 (route ordinaire), traverse la table de conntrack, qui retrouve l'entrée de la connexion et applique la traduction inverse (la source 10.244.2.7:8000 redevient 10.96.40.12:80) avant de remettre le paquet au pod diag. C'est le fonctionnement normal du NAT à état vu dans le cours TCP/IP.
  • Avec la traduction au connect() : le client sait lui-même (le noyau garde la correspondance par socket) qui est son interlocuteur réel et, lors de la lecture, présente l'adresse du Service ; la traduction inverse est, là encore, faite sans conntrack.

Un cas particulier est le retour en épingle à cheveux (hairpin) : un pod appelle un Service dont il est lui-même un point d'accès, et la règle tire son propre pod. Sans précaution, le pod verrait arriver chez lui un paquet dont la source est sa propre adresse, et la réponse partirait directement, sans repasser par la traduction. Pour l'éviter, le paquet est marqué et masqué (SNAT vers une adresse du nœud) : le pod voit l'appel venir du nœud. C'est la raison de la chaîne KUBE-MARK-MASQ, qui marque les paquets à masquer avant le hook POSTROUTING.

Cas 5 : un pod appelle Internet

L'adresse du pod (10.244.1.5) est privée et n'a aucune route sur Internet. Pour sortir, le paquet est routé par le nœud vers sa passerelle par défaut, et, en sortant du cluster, subit une traduction de source : le nœud remplace la source par sa propre adresse (ou celle du dispositif de sortie). On parle de masquerade : c'est la forme de SNAT qui prend l'adresse de l'interface de sortie, sans la fixer d'avance. La table de conntrack se souvient de la correspondance pour retraduire la réponse.

Où cette règle vit-elle ? Cela dépend du plugin : une règle MASQUERADE en POSTROUTING pour les destinations hors du Pod CIDR (installée par le plugin ou par un agent comme ip-masq-agent), ou, chez Cilium, la fonction de masquerade (en iptables ou en eBPF) qui exclut le réseau des pods et, selon la configuration, celui des nœuds. Deux conséquences pratiques :

  • Les serveurs externes (une API tierce, le pare-feu d'un client, une base managée) voient l'adresse du nœud ou de la passerelle, pas celle du pod. Pour les filtrer par adresse, il faut connaître les adresses de sortie du cluster. Sur Kapsule, ce sont celles des nœuds (publiques ou, avec une passerelle réseau, celle de cette passerelle : vérifiez la configuration de votre cluster).
  • Plusieurs pods partagent la même adresse de sortie, ce qui concentre les connexions : les ports source de cette adresse peuvent s'épuiser (voir Pièges).

Cas 6 : depuis l'extérieur

Un client Internet joint https://signalements.exemple.fr. Le DNS le dirige vers l'adresse d'un répartiteur (le Load Balancer Scaleway de la leçon Services et DNS, devant Traefik chez Lyneko). Un Service LoadBalancer s'appuie sur un NodePort : le répartiteur envoie le trafic vers un port élevé d'un nœud (plage 30000 à 32767 par défaut), et le nœud traduit vers un pod. Le comportement dépend du champ externalTrafficPolicy du Service (glossaire).

Cluster (le défaut). Le nœud qui reçoit le trafic peut l'envoyer à n'importe quel pod du Service, y compris sur un autre nœud. Pour que la réponse revienne par le même chemin (et non directement du pod vers le client, ce que le client ne reconnaîtrait pas), le nœud fait aussi un SNAT : il remplace l'adresse du client par la sienne. Résultat : bonne répartition, pas de paquet perdu, mais le pod voit comme source l'adresse d'un nœud, et le trafic fait un saut de plus. D'après la documentation : « l'adresse IP source vue dans le conteneur cible n'est pas l'adresse source d'origine du client ».

Local. Le nœud n'envoie le trafic qu'aux pods qui tournent sur lui. Il n'a plus besoin de masquer : le pod voit l'adresse du client, et il n'y a pas de saut supplémentaire. En contrepartie, un nœud sans pod du Service ne peut rien faire du trafic : il doit ne pas être choisi par le répartiteur. Kubernetes y pourvoit par un port de vérification de santé, healthCheckNodePort, que le répartiteur interroge ; un nœud sans pod local répond « non » et sort de la rotation. Autre limite, relevée par la documentation : le répartiteur ne connaît pas le nombre de pods par nœud, et donne le même poids à chaque nœud : si un nœud porte trois pods et un autre un seul, le pod seul reçoit autant de trafic que les trois autres ensemble.

AspectClusterLocal
Adresse source vue par le podCelle d'un nœud (SNAT)Celle du client
Saut supplémentaire entre nœudsPossibleNon
Répartition entre podsÉquilibréeDépend de la répartition des pods sur les nœuds
Nœud sans pod localTransmet à un pod ailleursDoit sortir de la rotation (healthCheckNodePort)
RisqueSource masquéeRafale vers un nœud, paquets perdus si la vérification est lente

Une troisième voie préserve l'adresse sans Local : le protocole PROXY, que le répartiteur ajoute en tête de la connexion pour transmettre l'adresse du client (c'est l'option proxy-protocol-v2 du Load Balancer Scaleway rencontrée à la leçon 6 du cours précédent). Il faut que le premier destinataire (Traefik) soit configuré pour le lire.

    flowchart LR
  C["Client Internet"] --> LB["Load Balancer"]
  LB -->|"NodePort"| N1["Nœud W1"]
  LB -->|"NodePort"| N2["Nœud W2"]
  N1 -->|"Cluster : DNAT + SNAT"| P2["Pod sur W2"]
  N1 -->|"Local ou Cluster : DNAT"| P1["Pod sur W1"]
  N2 -->|"DNAT"| P2
  

En pratique

Sur le cluster kind formation avec Cilium (leçon 2). Les commandes sont décrites avec ce que vous devez observer ; aucune sortie n'est reproduite. Sur kind, les nœuds sont des conteneurs Docker : docker exec <nœud> <commande> y exécute une commande, comme un accès SSH.

Voir les routes d'un nœud

$ docker exec formation-worker ip route

Sur un nœud, vous voyez la route par défaut vers la passerelle du réseau Docker, la route du réseau des nœuds, et les routes liées aux pods : une route par pod local, vers sa veth (les adresses de pods du nœud, 10.244.1.x ou équivalent), et, selon le mode, soit des routes vers les sous-réseaux des autres nœuds, soit une route qui renvoie ces sous-réseaux vers l'interface de tunnel (cilium_vxlan, ou un autre nom). Dans le mode par défaut de Cilium (VXLAN), le routage vers les autres nœuds passe par l'interface de tunnel plutôt que par une route par nœud ; les décisions précises sont dans les tables eBPF, que l'on consulte par cilium-dbg plutôt que par ip route.

$ docker exec formation-worker ip -br link
$ docker exec formation-worker ip -d link show cilium_vxlan

ip -br link liste les interfaces en une ligne chacune : eth0 (le lien vers le réseau Docker), les veth des pods, et les interfaces de Cilium. ip -d détaille l'interface de tunnel : type vxlan, port de destination 8472.

Entrer dans l'espace de noms d'un pod depuis le nœud

C'est le moyen d'utiliser les outils du nœud (tcpdump, ss) sur le réseau d'un pod dont l'image n'a pas d'outils. On part du processus du conteneur :

$ docker exec formation-worker crictl ps --name signalements
$ docker exec formation-worker crictl inspect --output go-template --template '{{.info.pid}}' <ID-du-conteneur>
$ docker exec formation-worker nsenter -t <PID> -n ip addr
  • crictl ps liste les conteneurs du nœud, filtrés par nom ; on relève l'identifiant ;
  • crictl inspect avec un gabarit extrait le PID du processus principal du conteneur (le champ exact peut varier selon la version du moteur : crictl inspect <ID> sans gabarit montre le JSON complet) ;
  • nsenter -t <PID> -n exécute la commande qui suit dans l'espace de noms réseau du processus, et seulement celui-là (-n), en gardant les outils du nœud. Ici, ip addr affiche eth0 du pod avec son adresse.

Dans la plupart des cas, un conteneur éphémère (kubectl debug) répond au même besoin sans accès au nœud (leçon Diagnostiquer le réseau d'un cluster, à venir).

Capturer sur le nœud

$ docker exec formation-worker tcpdump -ni any -c 20 'host 10.244.1.5 and tcp port 8000'
  • -n : pas de résolution de noms ; -i any : toutes les interfaces ; -c 20 : arrêt après 20 paquets ;
  • le filtre sélectionne le trafic du pod client vers le port 8000.

Si tcpdump est absent de l'image du nœud kind, installez-le dans le nœud ou lancez un conteneur outillé qui partage le réseau du nœud (docker run --rm --net=container:formation-worker nicolaka/netshoot tcpdump ...). Avec -i any, un même paquet apparaît plusieurs fois, une fois par interface qu'il traverse : sur la veth du pod, sur l'interface de tunnel, sur eth0. C'est ce qui permet de voir les étapes : sur la veth, le paquet a encore l'adresse du Service ; sur eth0, il est encapsulé (UDP 8472) ou, en routage direct, avec l'adresse du pod. Voir aussi capture réseau.

Voir la traduction : conntrack

$ docker exec formation-worker conntrack -L -p tcp --dport 80

Quand le mécanisme est kube-proxy (ou Cilium en mode compatible), conntrack -L liste les connexions suivies ; une entrée de Service montre le couple d'adresses : la destination d'origine (adresse du Service) et celle de la réponse (adresse du pod). Avec Cilium sans kube-proxy et traduction au connect(), la table du nœud ne contient en général pas ces traductions pour le trafic interne : l'absence d'entrée n'indique donc pas un défaut. Le même outil s'utilise dans le conteneur de Cilium : cilium-dbg bpf ct list global.

Observer avec Hubble

Hubble est la couche d'observabilité de Cilium : elle journalise les flux avec les identités des pods, pas seulement les adresses. On l'active, puis on installe la CLI hubble :

$ cilium hubble enable
$ cilium hubble port-forward &
$ hubble status

cilium hubble enable déploie les composants ; cilium hubble port-forward ouvre l'accès local à l'API Hubble ; hubble status confirme la connexion (selon la documentation, l'option -P de hubble utilise le port-forward automatiquement). Ensuite, par exemple :

$ hubble observe --namespace signalements --follow
$ hubble observe --to-pod signalements/signalements-7d9f --verdict DROPPED

La première commande affiche en continu les flux du namespace : source et destination (nom de pod, namespace), protocole et port, verdict (FORWARDED, DROPPED...). La seconde ne montre que les paquets refusés vers un pod. Vérifiez les options avec hubble observe --help pour votre version : les noms de filtres évoluent. Cet outil répond directement à « qui a parlé à qui, et le paquet est-il passé ? ».

Sous le capot

Les points d'accroche du noyau. Le trajet d'un paquet traverse des crochets (hooks) de Netfilter, le cadre de filtrage de Linux : PREROUTING (à l'arrivée, avant la décision de routage), puis, selon la destination, FORWARD (paquet à transmettre) ou INPUT (destiné au nœud), et POSTROUTING (juste avant le départ) ; les paquets émis par le nœud passent par OUTPUT. Le DNAT de Service se fait en PREROUTING (ou OUTPUT), car il doit changer la destination avant le routage : c'est lui qui décide si le paquet part vers un autre nœud. Le SNAT de masquerade se fait en POSTROUTING, après le routage, quand on connaît l'interface de sortie.

Conntrack, l'entrée qui fait tout. À la création d'une connexion, le noyau inscrit une entrée avec les deux sens du quintuplet (adresses, ports, protocole) : l'original et celui de la réponse, déjà traduit. Les paquets suivants sont reconnus et traduits par consultation. Cela explique à la fois la cohérence (une connexion garde son pod) et les pièges (une entrée qui expire pendant qu'un client attend, une table pleine : leçon 4).

Le masquerade et les ports. Quand plusieurs pods sortent par la même adresse, deux connexions peuvent avoir le même port source côté pod. Le noyau choisit alors un autre port source côté nœud : la traduction concerne adresse et port, et le nombre de connexions simultanées vers une même destination est limité par les ports éphémères disponibles (environ 28 000 par défaut sous Linux, plage 32768-60999, pour un même triplet adresse source, adresse et port destination).

L'asymétrie qu'évite le SNAT en mode Cluster. Sans SNAT, le trafic entré par le nœud W1 et envoyé au pod de W2 ferait répondre le pod directement au client, avec l'adresse du pod comme source : le client rejetterait ce paquet, qui n'appartient à aucune de ses connexions. Le SNAT garde le retour sur le chemin entrant. Cilium propose en alternative le DSR (Direct Server Return), où le pod répond directement au client en réécrivant la source, au prix d'ajustements de MTU et de conditions réseau ; il n'est pas activé par défaut, le mode par défaut étant le SNAT.

Pièges courants

Tester un Service par ping. L'adresse virtuelle ne répond pas à ICMP ; seul le port déclaré est traduit. Même piège qu'à la leçon Services et DNS, à répéter parce qu'il revient toujours.

Un tunnel qui tombe sur la MTU. Les petites requêtes (/sante) passent, les réponses volumineuses se bloquent : la MTU du pod ne tient pas compte du surcoût. Test : ping -M do -s <taille> depuis un pod vers un pod d'un autre nœud pour trouver la taille maximale qui passe, comme dans ICMP et MTU.

Le pod voit l'adresse d'un nœud au lieu du client. Votre journal d'accès (ou une règle de filtrage par adresse, ou une limitation de débit par client) voit toujours la même adresse : externalTrafficPolicy: Cluster fait un SNAT. Remèdes : Local, ou le protocole PROXY, ou l'en-tête X-Forwarded-For quand c'est de l'HTTP derrière un répartiteur qui le pose.

externalTrafficPolicy: Local et un nœud sans pod. Le répartiteur envoie du trafic à un nœud qui n'héberge pas de pod : les paquets sont perdus, car aucune destination locale. La vérification de santé (healthCheckNodePort) doit être active et rapide ; un Deployment avec trop peu de répliques par rapport au nombre de nœuds rend Local fragile. Ajoutez une contrainte de répartition (topologySpreadConstraints) ou utilisez un DaemonSet pour l'entrée.

Un épuisement de ports de sortie. Un pod très actif vers une même API externe crée des milliers de connexions, toutes masquées vers la même adresse de nœud : les ports sont épuisés, les nouvelles connexions échouent par intermittence. Remèdes : réutiliser les connexions (pools, keep-alive), répartir les pods, ou donner plus d'adresses de sortie.

Sécurité

  • Les paquets entre pods ne sont pas protégés en transit : le trafic encapsulé est visible de qui capture le réseau des nœuds. Chiffrement du plugin (WireGuard, IPsec) ou TLS dans les applications pour les données personnelles.
  • Le NodePort est ouvert sur tous les nœuds, y compris pour un Service qui a déjà un répartiteur : si les nœuds ont une adresse publique, vérifiez qu'un groupe de sécurité restreint ces ports aux adresses du répartiteur.
  • L'adresse source est une information de sécurité. Avec Cluster, filtrage par IP, limitation de débit par client, détection de fraude et journaux d'audit perdent le client réel. Pour une API de signalement qui trace les appels (RGPD : journalisation proportionnée), décidez d'abord comment l'adresse du client arrivera au pod.
  • Le masquerade cache l'origine côté serveur externe : un serveur tiers (la base d'un client) ne peut filtrer que par l'adresse du nœud, donc pour tout pod de ce nœud. Une adresse de sortie dédiée à une application (passerelle NAT par application) restreint mieux ce périmètre.
  • Les politiques s'appliquent à des endroits précis du chemin : à l'entrée et à la sortie du pod pour les NetworkPolicies, avant ou après la traduction de Service selon le plugin. Cilium et Calico traitent ce point différemment ; testez avec des flux réels, en observant les verdicts dans Hubble (leçon 8 pour le détail).

En production

  • Choisir externalTrafficPolicy par service, pas globalement : Local pour le point d'entrée (Traefik) si l'adresse du client compte et que le DaemonSet ou la répartition garantit un pod par nœud ciblé par le répartiteur ; Cluster pour le reste.
  • Surveiller le chemin, pas seulement les pods. Des métriques de latence entre nœuds, de pertes sur les interfaces de tunnel, de saturation de conntrack et de ports de sortie détectent les pannes que les sondes des pods ne voient pas.
  • Prévoir la sortie. Pour les clients qui filtrent les adresses (pare-feu d'un partenaire), obtenir une adresse de sortie stable : un cluster dont les nœuds changent de temps en temps (mises à jour, autoscaling) change d'adresse sinon. Chez Scaleway, une passerelle publique ou des adresses IP flexibles stabilisent cette adresse ; à décider à la conception.

Exercices

1. Dessiner le trajet (niveau 200). Décrivez, étape par étape, le chemin d'un GET /sante de diag (W1) vers le Service signalements, quand le point d'accès choisi est sur W2, avec kube-proxy en mode iptables et un plugin en encapsulation. Indiquez à chaque étape l'adresse source et destination.

Solution
  1. diag envoie 10.244.1.5 -> 10.96.40.12:80 par sa veth. 2. Sur W1, PREROUTING (table nat) reconnaît le Service et fait un DNAT vers un pod prêt : 10.244.1.5 -> 10.244.2.7:8000. conntrack enregistre la traduction. 3. W1 route vers 10.244.2.7 : la destination est dans le sous-réseau de W2, le paquet part dans le tunnel, encapsulé en UDP de l'adresse de W1 vers celle de W2 (le contenu est inchangé). 4. W2 désencapsule, route vers la veth du pod de destination : 10.244.1.5 -> 10.244.2.7:8000. 5. Retour : le pod répond 10.244.2.7:8000 -> 10.244.1.5, tunnel inverse jusqu'à W1, où conntrack réécrit la source en 10.96.40.12:80 avant la remise à diag.

2. Source perdue (niveau 300). L'API de Signalements limite le débit par adresse de client, mais tous les appels externes semblent venir de la même adresse, celle d'un nœud, et le quota est vite atteint. Le Service d'entrée est un LoadBalancer avec externalTrafficPolicy: Cluster. Proposez deux solutions et leurs risques.

Solution

Solution 1 : passer à externalTrafficPolicy: Local sur le Service d'entrée : le pod voit l'adresse réelle du client. Risques : il faut que chaque nœud reçoive du trafic ait un pod d'entrée (sinon paquets perdus), que healthCheckNodePort soit vérifié par le répartiteur, et la répartition peut devenir inégale entre nœuds. Solution 2 : activer le protocole PROXY sur le répartiteur (proxy-protocol-v2 chez Scaleway) et configurer Traefik pour le lire ; risque : si le répartiteur et Traefik ne sont pas tous les deux réglés, les connexions échouent (Traefik reçoit un en-tête qu'il ne comprend pas, ou l'inverse), et il faut limiter les sources autorisées à envoyer cet en-tête, sinon n'importe qui peut usurper une adresse. Variante HTTP : se fier à X-Forwarded-For, en n'acceptant cet en-tête que de proxies de confiance.

3. Où poser le tcpdump (niveau 300). Des gros paquets entre pods de nœuds différents se perdent. Où placeriez-vous deux captures pour confirmer un problème de MTU de tunnel, et que cherchez-vous ?

Solution

Une capture sur la veth (ou l'interface) du pod émetteur côté nœud, une sur l'interface physique eth0 du même nœud, et idéalement une sur eth0 du nœud récepteur. On cherche : sur la veth, des segments de taille proche de la MTU du pod ; sur eth0, des paquets UDP encapsulés dont la taille dépasse la MTU de eth0 (ou leur absence, s'ils sont éliminés plus tôt) ; et, dans les deux sens, des messages ICMP « fragmentation nécessaire » (type 3, code 4) ou leur absence, signe qu'un équipement les filtre (trou noir PMTU). Les petits paquets qui passent et les grands qui disparaissent confirment l'hypothèse ; la correction est de baisser la MTU des pods ou de laisser passer l'ICMP.

4. Le nœud sans pod (niveau 300). Avec externalTrafficPolicy: Local, le répartiteur envoie 1 requête sur 3 à un nœud sans pod d'entrée, et 1 sur 3 échoue. Que vérifier, dans l'ordre ?

Solution
  1. Le Service possède-t-il un healthCheckNodePort (kubectl get service <nom> -o jsonpath='{.spec.healthCheckNodePort}') ? 2. Le répartiteur interroge-t-il bien ce port, et pas le NodePort du Service ? (Sur Scaleway, le type de vérification et le port sont réglés par les annotations du contrôleur ; le contrôleur doit les déduire de healthCheckNodePort.) 3. Le nœud sans pod répond-il bien « non » sur ce port (le code HTTP 503 est le signal attendu) ? 4. La fréquence et le seuil de la vérification laissent-ils un délai pendant lequel un nœud sans pod reste dans la rotation (pendant un déplacement de pod, par exemple) ? Dans ce cas, resserrer la vérification ou garantir un pod d'entrée par nœud (DaemonSet, ou répartition topologique).

Récapitulatif

  • Même nœud : veth, puis pont, routage ou redirection eBPF ; autre nœud : route par sous-réseau de nœud, ou tunnel (VXLAN, Geneve). Aucune traduction entre pods.
  • Service : DNAT de l'adresse virtuelle vers un pod, avant le routage (PREROUTING ou OUTPUT) avec kube-proxy, ou au connect() avec Cilium sans kube-proxy. conntrack retient la traduction et réécrit la réponse.
  • Sortie vers Internet : SNAT de type masquerade vers l'adresse du nœud ; les serveurs externes voient le nœud, et les ports de sortie sont une ressource limitée.
  • Entrée : répartiteur, puis NodePort, puis pod. externalTrafficPolicy: Cluster équilibre mais masque la source (SNAT) ; Local préserve l'adresse du client, évite un saut, mais exige un pod local et healthCheckNodePort. Le protocole PROXY est la troisième voie.
  • Pour observer : ip route et ip -d link sur le nœud, nsenter -t <PID> -n pour entrer dans un pod, tcpdump -i any aux bons endroits, conntrack -L, et hubble observe avec Cilium.

Pour aller plus loin

  • La page Virtual IPs and Service Proxies et le guide Create an External Load Balancer de la documentation de Kubernetes, pour les chaînes de règles et externalTrafficPolicy.
  • La page Kubernetes Without kube-proxy de Cilium, pour la traduction côté socket, le DSR et la validation.
  • La leçon suivante, kube-proxy et ses alternatives, qui ouvre les règles que ce chapitre n'a fait que nommer.
Voir ma constellation →

Sources