---
id: 6e28268047a6c8b9541a0722fb48611e
author: iwanhae
title: 'Cilium-verhaal: hoe een kleine codewijziging zorgde voor een opmerkelijke verbetering van de netwerkstabiliteit'
description: Lees hieronder in detail hoe één kleine codewijziging in het Cilium-project de stabiliteit van LRU-gebaseerd NAT-verbindingsbeheer aanzienlijk heeft verbeterd.
language: nl
date: 2025-04-10T06:07:43.092628696Z
path: /blog/posts/cilium-story-small-but-effective-improvement-zd5a8e472
category: network
---

## Inleiding

Onlangs heb ik even meegekeken met de PR van een voormalige collega voor het Cilium-project.

[bpf:nat: Restore ORG NAT entry if it's not found](https://github.com/cilium/cilium/pull/37747)

De omvang van de wijziging zelf (exclusief de testcode) is klein, neerkomend op het toevoegen van één `if`-blok. Toch vind ik het persoonlijk fascinerend dat de impact van deze wijziging enorm is en dat één eenvoudig idee een zeer grote bijdrage kan leveren aan de stabiliteit van een systeem. Daarom wil ik proberen dit voorbeeld zo uit te leggen dat ook mensen zonder specifieke netwerkkennis het gemakkelijk kunnen begrijpen.

## Achtergrondkennis

Als er naast de smartphone een apparaat is dat voor moderne mensen even onmisbaar is, dan is het waarschijnlijk de wifi-router. De wifi-router communiceert met apparaten via de wifi-communicatiestandaard en vervult de rol van het delen van het publieke IP-adres dat hij bezit, zodat meerdere apparaten het kunnen gebruiken. Hier ontstaat het technisch interessante punt: hoe wordt dit "gedeeld"? Dat is de vraag.

De technologie die hierbij wordt gebruikt, is Network Address Translation (NAT). NAT maakt het mogelijk dat TCP- of UDP-communicatie, die bestaat uit een combinatie van een IP-adres en een poortnummer, de interne communicatie in de vorm `privé IP:Port` koppelt aan een `publiek IP:Port` dat op dat moment niet in gebruik is, zodat er met de buitenwereld kan worden gecommuniceerd.

### NAT

Wanneer een intern apparaat verbinding wil maken met het externe internet, zet het NAT-apparaat de combinatie van het privé IP-adres en het poortnummer van dat apparaat om naar zijn eigen publieke IP-adres en een willekeurig ongebruikt poortnummer. Deze omzettingsinformatie wordt vastgelegd in een tabel binnen het NAT-apparaat, de zogenaamde NAT-tabel.

Stel bijvoorbeeld dat een smartphone thuis (privé IP: `a.a.a.a`, poort: 50000) verbinding wil maken met een webserver (publiek IP: `c.c.c.c`, poort: 80).

```
스마트폰 (a.a.a.a:50000) ==> 공유기 (b.b.b.b) ==> 웹 서버 (c.c.c.c:80)
```

Wanneer de router het verzoek van de smartphone ontvangt, ziet hij het volgende TCP-pakket.

```
# 공유기가 받은 TCP 패킷, 스마트폰 => 공유기
| src ip  | src port | dst ip  | dst port |
-------------------------------------------
| a.a.a.a | 50000    | c.c.c.c | 80       |
```

Als dit pakket zonder meer naar de webserver (`c.c.c.c`) zou worden gestuurd, zou het antwoord nooit terugkomen bij de smartphone met het privé IP-adres (`a.a.a.a`). Daarom zoekt de router eerst een willekeurige poort die op dat moment niet voor de communicatie wordt gebruikt (bijvoorbeeld 60000) en legt deze vast in de interne NAT-tabel.

```
# 공유기 내부 NAT 테이블
| local ip  | local port | global ip  | global port |
-----------------------------------------------------
| a.a.a.a   | 50000      | b.b.b.b    | 60000       |
```

Nadat de router een nieuw item in de NAT-tabel heeft vastgelegd, wijzigt hij het bronadres en het bronpoortnummer van het TCP-pakket dat van de smartphone is ontvangen naar zijn eigen publieke IP-adres (`b.b.b.b`) en het nieuw toegewezen poortnummer (60000), waarna hij het naar de webserver verstuurt.

```
# 공유기가 보낸 TCP 패킷, 공유기 => 웹 서버
# SNAT 수행
| src ip  | src port | dst ip  | dst port |
-------------------------------------------
| b.b.b.b | 60000    | c.c.c.c | 80       |
```

De webserver (`c.c.c.c`) herkent het verzoek nu als afkomstig van poort 60000 van de router (`b.b.b.b`) en stuurt het antwoordpakket als volgt terug naar de router.

```
# 공유기가 받은 TCP 패킷, 웹 서버 => 공유기
| src ip  | src port | dst ip  | dst port |
-------------------------------------------
| c.c.c.c | 80       | b.b.b.b | 60000    |
```

Wanneer de router dit antwoordpakket ontvangt, zoekt hij in de NAT-tabel het oorspronkelijke privé IP-adres (`a.a.a.a`) en poortnummer (50000) die horen bij het doel-IP-adres (`b.b.b.b`) en poortnummer (60000), en wijzigt hij de bestemming van het pakket naar de smartphone.

```
# 공유기가 보낸 TCP 패킷, 공유기 => 스마트폰
# DNAT 수행
| src ip  | src port | dst ip  | dst port |
-------------------------------------------
| c.c.c.c | 80       | a.a.a.a | 50000    |
```

Via dit proces lijkt het voor de smartphone alsof hij zelf met een publiek IP-adres rechtstreeks met de webserver communiceert. Dankzij NAT kunnen meerdere interne apparaten tegelijkertijd het internet gebruiken via één publiek IP-adres.

### Kubernetes

Kubernetes beschikt over een van de meest verfijnde en complexe netwerkstructuren van recente technologieën. Uiteraard wordt ook NAT, zoals hierboven beschreven, op verschillende plaatsen toegepast. De twee meest representatieve voorbeelden zijn de volgende.

#### Wanneer een Pod communiceert met buiten het cluster

Pods binnen een Kubernetes-cluster krijgen doorgaans privé IP-adressen toegewezen die alleen binnen het clusternetwerk kunnen communiceren. Om te communiceren met het externe internet is voor verkeer dat het cluster verlaat daarom NAT nodig. Deze NAT wordt meestal uitgevoerd op de Kubernetes-node (elke server in het cluster) waarop de Pod draait. Wanneer een Pod een pakket naar buiten stuurt, wordt dat pakket eerst naar de node van de Pod doorgestuurd. De node vervangt het bron-IP-adres van het pakket (het privé IP van de Pod) door zijn eigen publieke IP-adres, past ook het bronpoortnummer passend aan, en stuurt het pakket naar buiten. Dit proces is vergelijkbaar met het NAT-proces dat eerder bij de wifi-router is beschreven.

Stel bijvoorbeeld dat een Pod binnen het Kubernetes-cluster (`10.0.1.10`, poort: 40000) verbinding maakt met een externe API-server (`203.0.113.45`, poort: 443). Dan ontvangt de Kubernetes-node het volgende pakket van de Pod.

```
# 노드가 받은 TCP 패킷, 파드 => 노드
| src ip    | src port | dst ip        | dst port |
---------------------------------------------------
| 10.0.1.10 | 40000    | 203.0.113.45  | 443      |
```

De node legt daarna het volgende vast.

```
# 노드 내부 NAT 테이블 (예시)
| local ip    | local port | global ip     | global port |
---------------------------------------------------------
| 10.0.1.10   | 40000      | 192.168.1.5   | 50000       |
```

Vervolgens voert hij SNAT uit en stuurt hij het pakket naar buiten.

```
# 노드가 보낸 TCP 패킷, 노드 => API 서버
# SNAT 수행
| src ip      | src port | dst ip        | dst port |
-----------------------------------------------------
| 192.168.1.5 | 50000    | 203.0.113.45  | 443      |
```

Daarna verloopt het proces op dezelfde manier als in het eerder beschreven voorbeeld van de smartphone en de wifi-router.

#### Wanneer er vanaf buiten het cluster via NodePort met een Pod wordt gecommuniceerd

Een manier om een service in Kubernetes naar buiten toe beschikbaar te maken, is het gebruik van een NodePort-service. Bij een NodePort-service wordt op alle nodes in het cluster een specifieke poort (NodePort) opengezet, en wordt het verkeer dat via deze poort binnenkomt doorgestuurd naar de Pods die bij de service horen. Externe gebruikers kunnen de service bereiken via het IP-adres van een node in het cluster en de NodePort.

Hierbij speelt NAT een belangrijke rol, waarbij met name **DNAT (Destination NAT)** en **SNAT (Source NAT)** **tegelijkertijd** plaatsvinden. Wanneer verkeer van buitenaf via de NodePort van een bepaalde node binnenkomt, moet het Kubernetes-netwerk dit verkeer uiteindelijk doorsturen naar de Pod die de service levert. Hierbij vindt eerst DNAT plaats, waarbij het doel-IP-adres en het doelpoortnummer van het pakket worden gewijzigd naar het IP-adres en poortnummer van de Pod.

Stel bijvoorbeeld dat een externe gebruiker (`203.0.113.10`, poort: 30000) via de NodePort (`30001`) van een node in het Kubernetes-cluster (`192.168.1.5`) toegang zoekt tot de service. Veronderstel dat deze service intern verwijst naar een Pod met IP-adres `10.0.2.15` en poort `8080`.

```
외부 사용자 (203.0.113.10:30000) ==> 쿠버네티스 노드 (외부:192.168.1.5:30001 / 내부: 10.0.1.1:42132) ==> 쿠버네티스 파드 (10.0.2.15:8080)
```

In dit geval heeft de Kubernetes-node zowel het van buitenaf bereikbare IP-adres 192.168.1.5 als het binnen het interne Kubernetes-netwerk geldige IP-adres 10.0.1.1. (Afhankelijk van het gebruikte CNI-type verschilt het beleid hieromtrent, maar in dit artikel wordt uitgegaan van Cilium.)

Wanneer het verzoek van de externe gebruiker bij de node aankomt, moet de node het doorsturen naar de Pod die het verzoek zal verwerken. Hierbij past de node de volgende DNAT-regel toe om het doel-IP-adres en het doelpoortnummer van het pakket te wijzigen.

```
# 노드가 파드로 보낼려고 준비중인 TCP 패킷
# DNAT 적용 후
| src ip       | src port | dst ip    | dst port |
---------------------------------------------------
| 203.0.113.10 | 30000    | 10.0.2.15 | 8080    |
```

Belangrijk hierbij is dat wanneer de Pod een antwoord op dit verzoek verstuurt, het bron-IP-adres het eigen IP-adres van de Pod (`10.0.2.15`) is, en het doel-IP-adres het IP-adres van de externe gebruiker die het verzoek heeft gestuurd (`203.0.113.10`). In dat geval ontvangt de externe gebruiker een antwoord van een onbekend IP-adres dat hij nooit heeft benaderd, en zal hij het pakket simpelweg DROP-pen. Daarom voert de Kubernetes-node, wanneer de Pod een antwoordpakket naar buiten stuurt, aanvullend **SNAT** uit en wijzigt hij het bron-IP-adres naar het IP-adres van de node (192.168.1.5 of het interne netwerk-IP 10.0.1.1; in dit geval wordt 10.0.1.1 gebruikt).

```
# 노드가 파드로 보낼려고 준비중인 TCP 패킷
# DNAT, SNAT 적용 후
| src ip       | src port | dst ip    | dst port |
---------------------------------------------------
| 10.0.1.1     | 40021    | 10.0.2.15 | 8080     |
```

De Pod die dit pakket ontvangt, stuurt het antwoord terug naar de node die het oorspronkelijke verzoek via NodePort heeft ontvangen. De node past vervolgens dezelfde DNAT- en SNAT-processen in omgekeerde volgorde toe en stuurt de informatie terug naar de externe gebruiker. Tijdens dit proces slaat elke node de volgende informatie op.

```
# 노드 내부 DNAT 테이블
| original ip     | original port | destination ip  | destination port |
------------------------------------------------------------------------
| 192.168.1.5     | 30001         | 10.0.2.15       | 8080             |

# 노드 내부 SNAT 테이블
| original ip     | original port | destination ip  | destination port |
------------------------------------------------------------------------
| 203.0.113.10    | 30000         | 10.0.1.1        | 42132            |
```

## Hoofdgedeelte

Over het algemeen worden deze NAT-processen in Linux beheerd en uitgevoerd via iptables, door een subsysteem genaamd conntrack. In de praktijk gebruiken andere CNI-projecten zoals flannel en Calico deze mechanismen om bovenstaande problemen op te lossen. Het probleem is echter dat Cilium gebruikmaakt van een technologie genaamd eBPF en de traditionele Linux-netwerkstack volledig negeert. 🤣

![https://cilium.io/blog/2021/05/11/cni-benchmark/](https://cilium.io/static/7b77faac1700b51b5612abb7ec0c8f40/0bb32/ebpf_hostrouting.png)

Het resultaat is dat Cilium ervoor heeft gekozen om uit de taken die de traditionele Linux-netwerkstack in de afbeelding hierboven uitvoert, alleen de functies die in een Kubernetes-context nodig zijn, zelf te implementeren. Daarom beheert Cilium voor de eerder genoemde SNAT-processen zijn SNAT-tabel direct als een LRU Hash Map (BPF_MAP_TYPE_LRU_HASH).

```
# Cilium SNAT 테이블
# !쉬운 설명을 위한 예시. 실제 정의는: https://github.com/cilium/cilium/blob/v1.18.0-pre.1/bpf/lib/nat.h#L149-L166
| src ip     | src port | dst ip  | dst port | protocol, conntrack 등 기타등등 메타데이터
----------------------------------------------
|            |          |         |          |
```

Omdat het een hashtabel is, wordt voor snel opzoeken een sleutel gebruikt, en als sleutel wordt de combinatie van `src ip`, `src port`, `dst ip` en `dst port` [als sleutelwaarde](https://github.com/cilium/cilium/blob/v1.18.0-pre.1/bpf/lib/common.h#L909-L922) gebruikt.

### Probleemherkenning

#### Verschijnsel 1: opzoeken

Hierbij ontstaat één probleem: om te verifiëren of een pakket dat door eBPF gaat, een SNAT- of DNAT-proces moet ondergaan, moet deze hashtabel worden doorzocht. Zoals eerder gezien zijn er bij SNAT twee soorten pakketten: 1. pakketten die van binnen naar buiten gaan, en 2. het antwoord daarop, dat van buiten naar binnen komt. Deze twee pakketten moeten beide een NAT-omzetting ondergaan, maar hebben het kenmerk dat de waarden van src ip en port en dst ip en port van plaats verwisseld zijn.

Om snel te kunnen opzoeken, moet daarom ofwel een extra item worden toegevoegd aan de hashtabel, met als sleutel de waarden waarbij src en dst zijn verwisseld, ofwel moet voor alle pakketten, ook voor pakketten die niets met SNAT te maken hebben, tweemaal in dezelfde hashtabel worden gezocht. Cilium heeft uiteraard, om betere prestaties te bereiken, gekozen voor de methode waarbij dezelfde gegevens tweemaal worden opgeslagen, onder de naam [RevSNAT](https://github.com/cilium/cilium/blob/v1.18.0-pre.1/bpf/lib/nat.h#L243-L254).

#### Verschijnsel 2: LRU

Daarnaast geldt dat hardware nooit oneindig veel middelen heeft. Vooral bij logica op hardwareniveau, waar snelle prestaties vereist zijn, kunnen ook dynamische datastructuren niet worden gebruikt. Wanneer de middelen tekortschieten, moeten bestaande gegevens worden verwijderd (geëvict). Cilium heeft dit opgelost door de LRU Hash Map te gebruiken, een standaard datastructuur die Linux standaard biedt.

#### Verschijnsel 1 + verschijnsel 2 = verlies van verbindingen

https://github.com/cilium/cilium/issues/31643

Dat wil zeggen, voor één SNAT-verbinding over TCP (of UDP):

1. Zijn in één hashtabel voor het uitgaande pakket en het inkomende pakket dezelfde gegevens twee keer opgeslagen, en
2. kan, volgens de LRU-logica, op elk moment één van die twee gegevens verloren gaan.

Wanneer een van de NAT-gegevens (hierna: entry) voor een uitgaand of een inkomend pakket door de LRU wordt verwijderd, kan NAT niet correct worden uitgevoerd, wat kan leiden tot het verlies van de gehele verbinding.

### Oplossing

Hier verschijnen de volgende PR's die eerder werden genoemd.

[bpf:nat: restore a NAT entry if its REV NAT is not found](https://github.com/cilium/cilium/pull/35304)

[bpf:nat: Restore ORG NAT entry if it's not found](https://github.com/cilium/cilium/pull/37747)

Voorheen werd, wanneer een pakket door eBPF ging, geprobeerd op te zoeken in de SNAT-tabel met een sleutel gevormd uit de combinatie van src ip, src port, dst ip en dst port. Als de sleutel niet bestond, werd volgens de SNAT-regels een nieuwe NAT-informatie aangemaakt en in de tabel vastgelegd. Bij een nieuwe verbinding leidt dit tot normale communicatie. Maar als de sleutel onbedoeld door de LRU was verwijderd, werd NAT opnieuw uitgevoerd met een andere poort dan de poort die voor de bestaande communicatie werd gebruikt. Hierdoor weigerde de ontvangende kant het pakket, en werd de verbinding met een RST-pakket beëindigd.

De aanpak van de bovenstaande PR is hier eenvoudig:

> Wanneer een pakket in een willekeurige richting eenmaal is waargenomen, laten we ook de entry voor de tegenovergestelde richting opnieuw bijwerken.

Wanneer communicatie in een willekeurige richting wordt waargenomen, worden beide entries opnieuw bijgewerkt. Daardoor komen ze verder weg te staan van de prioriteit voor verwijdering in de LRU-logica, en wordt de kans kleiner dat slechts één van beide entries wordt verwijderd, wat zou leiden tot het ineenstorten van de gehele communicatie.

Hoewel dit een zeer eenvoudige aanpak is en het idee eenvoudig lijkt, is het met deze aanpak mogelijk om het probleem dat de NAT-informatie van het antwoordpakket eerder verloopt en de verbinding wordt verbroken, effectief op te lossen en de stabiliteit van het systeem aanzienlijk te verbeteren. Bovendien kan dit worden beschouwd als een belangrijke verbetering die op het gebied van netwerkstabiliteit de volgende resultaten heeft opgeleverd.

![benchmark](/assets/images/cilium-pr-37747/benchmark.png)

## Conclusie

Ik denk dat deze PR een uitstekend voorbeeld is van hoe een eenvoudig idee, zelfs binnen een complex systeem, een grote verandering teweeg kan brengen, te beginnen bij de basale informatica-kennis over hoe NAT werkt.

Natuurlijk heb ik in dit artikel het complexe systeem zelf niet direct getoond. Om deze PR goed te begrijpen, heb ik echter bijna drie uur lang met `DeepSeek V3 0324` gesmeekt, zelfs met het woord `Please` erbij, en daardoor heb ik één stukje kennis over Cilium erbij gekregen, en een diagram zoals hieronder. 😇

![diagram](/assets/images/cilium-pr-37747/diagram.svg)

En omdat ik de issues en PR's las, en daarbij een onheilspellend voorgevoel kreeg dat er door iets wat ik eerder had gemaakt problemen waren ontstaan, schrijf ik dit artikel als een vorm van compensatie.

## Nawoord 1

Overigens bestaat er voor dit issue een zeer effectieve manier om het te omzeilen. Omdat de oorzaak van het probleem een tekort aan ruimte in de NAT-tabel is, kan men simpelweg de grootte van de NAT-tabel vergroten. :-D

Terwijl anderen, toen ze tegen hetzelfde issue aanliepen, misschien zonder het issue zelfs te melden de NAT-tabel vergrootten en wegliepen, heeft [gyutaeb](https://github.com/gyutaeb) grondig geanalyseerd en begrepen, hoewel het issue niet direct met hem te maken had, en heeft hij bijgedragen aan het Cilium-ecosysteem met objectieve, data-gestuurde onderbouwing. Ik heb grote bewondering en respect voor zijn inzet.

Dat was de aanleiding om dit artikel te schrijven.

## Nawoord 2

Dit verhaal past eigenlijk niet direct bij Gosuda, dat zich professioneel richt op de Go-taal. Maar Go en de cloud-ecosystemen zijn nauw met elkaar verbonden, en de bijdragers aan Cilium beschikken in zekere mate over kennis van Go. Daarom heb ik de inhoud, die op een persoonlijk blog thuishoort, toch naar Gosuda gebracht.

Ik denk dat dit waarschijnlijk in orde is, omdat een van de beheerders (ikzelf) hiervoor toestemming heeft gegeven.

Mocht u vinden dat dit niet in orde is, dan kan het op elk moment worden verwijderd, dus sla het alstublieft snel op als PDF. ;)

## Nawoord 3

Bij het schrijven van dit artikel heb ik veel hulp gehad van Cline en Llama 4 Maverick. Hoewel ik met Gemini aan de analyse begon en DeepSeek om hulp smeekte, kwam de echte hulp van Llama 4. Llama 4 is goed. Probeer het zeker eens.
