← Blog
Kubernetes17 min czytania

CNI — jak działa wtykowa sieć kontenerów

Pod nie ma adresu, ruch nie przechodzi, a w logach nic. Pod Calico, Cilium i Flannelem stoi jedna specyfikacja, w której wtyczka sieciowa to zwykły program wykonywalny czytający JSON ze standardowego wejścia. Rozbieramy sześć operacji protokołu, łańcuchy wtyczek, delegowanie do IPAM oraz to, dlaczego operacja DEL jest zawsze najlepszym staraniem — i skąd wzięła się z tego potrzeba osobnego odśmiecania.

Awarie sieci w Kubernetesie należą do najbardziej nieprzejrzystej klasy problemów, jakie spotykamy przy utrzymaniu. Pod wstaje, ale nie ma adresu IP. Albo ma adres, ale nie odpowiada. Albo odpowiada z jednego węzła, a z drugiego nie. W logach kubeleta jest jedna linia o nieudanym ustawianiu sieci dla sandboksa i nic więcej.

Pod spodem stoi jednak rzecz zaskakująco prosta i całkowicie możliwa do prześledzenia ręcznie: specyfikacja CNI (github.com/containernetworking/cni). Nie jest to biblioteka ani demon, a kontrakt między środowiskiem uruchomieniowym kontenerów a programami wykonywalnymi, które konfigurują sieć. Calico, Cilium, Flannel i wtyczka od dostawcy chmury są implementacjami tego kontraktu.

Wartość poznania go jest bardzo praktyczna: zamienia „sieć nie działa" w sekwencję kroków, z których każdy da się uruchomić z ręki i zobaczyć wynik. Ten wpis jest o tej sekwencji.

Stan projektu

Dane z API GitHuba na 1 października 2026:

  • 6112 gwiazdek i 1164 forki — liczby skromne, ale to specyfikacja, nie narzędzie; korzysta z niej każdy klaster Kubernetesa na świecie,
  • repozytorium założone 5 kwietnia 2015, kod w Go, licencja Apache-2.0, projekt pod skrzydłami CNCF, strona cni.dev,
  • najnowsze wydanie biblioteki v1.3.1 z 3 września 2026 — czyli sprzed kilku dni,
  • repozytorium wtyczek referencyjnych (containernetworking/plugins) miało push w dniu pisania tego tekstu,
  • 156 otwartych zgłoszeń, spotkania zespołu utrzymującego odbywają się co dwa tygodnie.

Jedna rzecz, którą trzeba rozdzielić od początku, bo jest źródłem nieporozumień w dyskusjach: wersja specyfikacji jest niezależna od wersji biblioteki. Aktualna specyfikacja to 1.1.0, a aktualna biblioteka libcni to 1.3.1. To dwa różne numery i dokumentacja mówi o tym wprost, bo ludzie regularnie je mylą, wnioskując o zgodności z niewłaściwej liczby.

Cały pomysł: wtyczka to program

Specyfikacja definiuje pięć rzeczy: format konfiguracji sieci dla administratora, protokół żądań od środowiska uruchomieniowego do wtyczki, procedurę wykonywania wtyczek na podstawie konfiguracji, procedurę delegowania funkcji jednej wtyczki do drugiej oraz typy danych wyników.

Cztery pojęcia są przy tym zdefiniowane bardzo precyzyjnie i warto je znać, bo tłumaczą, dlaczego CNI stosuje się także poza kontenerami:

  • kontener to domena izolacji sieciowej — a specyfikacja świadomie nie mówi, jaką technologią. Może to być przestrzeń nazw sieci, ale może i maszyna wirtualna,
  • sieć to grupa unikalnie adresowalnych punktów końcowych mogących się ze sobą komunikować,
  • środowisko uruchomieniowe to program wykonujący wtyczki CNI,
  • wtyczka to program stosujący zadaną konfigurację sieciową.

I tu jest cały sekret tej architektury, przez jedenaście lat niezmieniony: wtyczka jest zwykłym plikiem wykonywalnym. Nie ma gniazda, nie ma gRPC, nie ma demona, nie ma rejestracji w API. Środowisko uruchomieniowe:

  • ustawia zmienne środowiskowe opisujące żądanie,
  • podaje JSON z konfiguracją na standardowe wejście,
  • odczytuje wynik ze standardowego wyjścia,
  • sprawdza kod wyjścia: zero to sukces, cokolwiek innego to błąd, a wtedy na wyjściu powinna pojawić się struktura błędu.

Parametry protokołu to sześć zmiennych, które warto rozpoznawać ze śladów w logach:

CNI_COMMAND      # ADD, DEL, CHECK, GC albo VERSION
CNI_CONTAINERID  # unikalny identyfikator kontenera, nie może być pusty
CNI_NETNS        # ścieżka do domeny izolacji, np. /run/netns/nazwa
CNI_IFNAME       # nazwa interfejsu do utworzenia w kontenerze
CNI_ARGS         # dodatkowe pary klucz-wartość, np. FOO=BAR;ABC=123
CNI_PATH         # katalogi, w których szukać plików wykonywalnych wtyczek

Konsekwencja tej prostoty jest taka, że każde wywołanie CNI da się powtórzyć z powłoki. To nie jest ciekawostka — to podstawa diagnostyki, do której wrócimy przy narzędziu cnitool.

Sześć operacji i to, co w nich nieoczywiste

ADD tworzy interfejs o nazwie z CNI_IFNAME w domenie izolacji wskazanej przez CNI_NETNS albo modyfikuje jego konfigurację. Dwie reguły są tu twarde: jeśli interfejs o żądanej nazwie już istnieje, wtyczka musi zwrócić błąd, a środowisko uruchomieniowe nie powinno wołać ADD dwa razy dla tej samej pary identyfikatora kontenera i nazwy interfejsu bez DEL pomiędzy. Wynika z tego rzecz przydatna: ten sam kontener można dodać do sieci wielokrotnie, ale tylko pod różnymi nazwami interfejsów.

DEL usuwa interfejs albo cofa zmiany wprowadzone przez ADD — i tu jest najważniejsze zdanie całej specyfikacji z punktu widzenia utrzymania:

Wywołania DEL są zawsze traktowane jako najlepsze staranie. Wtyczka powinna zakończyć usuwanie bez błędu w najszerszym możliwym zakresie, nawet jeśli części zasobów albo stanu już nie ma. Wtyczka musi też przyjmować wielokrotne wywołania DEL dla tej samej pary i zwracać sukces, gdy interfejsu albo zmian już nie ma.

Specyfikacja podaje własne przykłady tej zasady: wtyczka IPAM powinna zwolnić przydział adresu i zwrócić sukces, choćby przestrzeń nazw sieci kontenera już nie istniała; wtyczka DHCP zwykle wysyła komunikat zwolnienia dzierżawy, ale skoro dzierżawa ma czas życia, niepowodzenie tej czynności nie jest krytyczne; wtyczka mostka ma posprzątać po sobie i przekazać usunięcie do IPAM, nawet jeśli ani przestrzeni nazw, ani interfejsu już nie ma.

Ta zasada jest projektowo słuszna — inaczej pojedynczy błąd blokowałby usuwanie poda na zawsze — ale ma cenę i to ona tłumaczy klasę problemów, którą się w klastrach spotyka. Skoro usuwanie ma nie zgłaszać błędów, to zasoby mogą po cichu wyciekać: przydział adresu w IPAM zostaje, reguła zapory zostaje, a nikt nie dowiaduje się o tym z wyniku operacji. Wyczerpanie adresów w podsieci węzła po tygodniach pracy klastra ma zwykle właśnie tutaj swój początek.

Właśnie dlatego w specyfikacji 1.1 pojawiła się operacja GC. Środowisko uruchomieniowe podaje w JSON-ie klucz cni.dev/valid-attachments z listą nadal prawidłowych podłączeń do sieci, a wtyczka może usunąć wszystko, co do tej listy nie należy — przydziały IPAM, reguły zapory. Wtyczka powinna usunąć jak najwięcej, a napotkane błędy raportować, nie przerywając pracy, i musi przekazać wywołanie GC wtyczkom, którym coś delegowała. Jedno ostrzeżenie jest w specyfikacji postawione wyraźnie: środowisko uruchomieniowe nie może używać GC zamiast DEL, bo są zasoby, które da się posprzątać przy usuwaniu, a przy odśmiecaniu już nie.

CHECK jest sondą stanu istniejącego kontenera. Wtyczka musi porównać rzeczywistość z prevResult i zgłosić błąd, jeśli interfejs, adres albo trasa z wyniku zniknęły lub są w nieprawidłowym stanie — ale też jeśli brakuje zasobów, których w typie wyniku nie ma: reguł zapory, sterowania przepustowością, rezerwacji adresów albo demona wymaganego do łączności. Do tego musi znieść wywołanie CHECK natychmiast po ADD, dopuszczając rozsądny czas zbieżności zasobów asynchronicznych. Administrator może wyłączyć tę operację dla danej konfiguracji flagą disableCheck — i specyfikacja podaje po co: gdy wiadomo, że konkretne połączenie wtyczek zwraca błędy pozorne.

STATUS to nowsza operacja o innym charakterze: mówi, czy wtyczka jest gotowa obsługiwać żądania ADD. Ma dwa zdefiniowane kody błędu i różnica między nimi jest istotna operacyjnie:

  • 50 — wtyczka jest niedostępna, czyli nie obsłuży nowych żądań,
  • 51 — wtyczka jest niedostępna, a istniejące kontenery w tej sieci mogą mieć ograniczoną łączność.

Pięćdziesiąt jeden to sygnał „nie tylko nic nowego nie wstanie, ale i to, co działa, może już nie działać" — dokładnie ta informacja, której brakuje przy diagnozowaniu awarii. Przy czym specyfikacja od razu ostudza oczekiwania: STATUS jest wyłącznie informacyjny, wtyczka nie może polegać na tym, że ktokolwiek go wywoła, a błąd z tej operacji nie blokuje pozostałych żądań.

VERSION zwraca listę wersji protokołu, które wtyczka obsługuje — mechanizm, dzięki któremu środowisko uruchomieniowe potrafi dogadać się ze starszą wtyczką.

Łańcuch wtyczek: jedna sieć, kilka programów

Od specyfikacji 0.3.0 konfiguracja sieci nie wskazuje jednej wtyczki, a listę wtyczek wykonywanych po kolei. To jest miejsce, w którym CNI przestaje być tylko „utwórz interfejs", a staje się małym potokiem przetwarzania. Przykład z dokumentacji, skrócony:

{
  "cniVersion": "1.1.0",
  "cniVersions": ["0.3.1", "0.4.0", "1.0.0", "1.1.0"],
  "name": "dbnet",
  "plugins": [
    {
      "type": "bridge",
      "bridge": "cni0",
      "ipam": {
        "type": "host-local",
        "subnet": "10.1.0.0/16",
        "gateway": "10.1.0.1",
        "routes": [ {"dst": "0.0.0.0/0"} ]
      },
      "dns": { "nameservers": ["10.1.0.1"] }
    },
    { "type": "tuning", "sysctl": { "net.core.somaxconn": "500" } },
    { "type": "portmap", "capabilities": {"portMappings": true} }
  ]
}

Trzy programy, trzy zadania: bridge tworzy mostek i wkłada do niego kontener, tuning ustawia parametry sysctl na gotowym interfejsie, portmap dokłada reguły przekierowania portów z adresu hosta. Każda kolejna wtyczka dostaje prevResult, czyli wynik poprzedniej, i musi go przekazać dalej albo odpowiednio zmodyfikować. Przy DEL łańcuch idzie w drugą stronę, a pierwsza wtyczka dostaje jako prevResult ostateczny wynik poprzedniego ADD.

Pole capabilities to osobny mechanizm: mówi, których parametrów dynamicznych wtyczka oczekuje od środowiska uruchomieniowego przy każdym wywołaniu — bo mapowanie portów zna dopiero kubelet, nie administrator piszący plik konfiguracyjny. Warto też zwrócić uwagę na pole cniVersions obok cniVersion: lista wszystkich wersji, które ta konfiguracja obsługuje, żeby środowisko mogło wybrać najwyższą wspólną.

Delegowanie i skąd biorą się adresy

Sekcja czwarta specyfikacji opisuje mechanizm, który w praktyce widać wszędzie: wtyczka może zlecić część pracy innej wtyczce. Kanonicznym przykładem jest przydzielanie adresów. Zagnieżdżony obiekt ipam nie jest parametrem mostka — jest konfiguracją osobnego programu, którego mostek wywoła i którego wynik wykorzysta.

Rozdzielenie jest elegancko czyste: wtyczka tworząca interfejs nie musi nic wiedzieć o zarządzaniu adresami, a wtyczka IPAM nic o mostkach. W repozytorium referencyjnym są trzy implementacje IPAM:

  • host-local — trzyma lokalną bazę przydzielonych adresów na węźle. To ten, którego stan wycieka, gdy DEL nie dojdzie,
  • dhcp — uruchamia na hoście demona wysyłającego żądania DHCP w imieniu kontenera,
  • static — jeden adres na stałe, przydatny głównie przy diagnostyce.

Wtyczka nadrzędna ma przy tym obowiązek przekazywania w dół nie tylko ADD i DEL, ale też CHECK, STATUS i GC — i przekazania błędu z delegowanej wtyczki do góry. Wtyczka, która polega na IPAM, musi więc zwrócić błąd STATUS, gdy IPAM zgłasza wyczerpanie adresów.

Katalog wtyczek referencyjnych

Drugie repozytorium projektu dostarcza wtyczki podzielone na trzy grupy, i warto tę listę znać, bo połowa problemów sieciowych w klastrze dotyczy jednej z nich:

  • tworzące interfejsy: bridge, ipvlan, macvlan, ptp (para veth), vlan, host-device (przenosi istniejące urządzenie do kontenera), dummy, loopback, a dla Windowsa win-bridge i win-overlay,
  • IPAM: host-local, dhcp, static,
  • pozostałe, do łańcucha: tuning (parametry sysctl), portmap (przekierowanie portów przez iptables), bandwidth (ograniczanie przepustowości przez sterowanie ruchem, wejściowo i wyjściowo), sbr (trasowanie na podstawie źródła) i firewall (reguły przez iptables albo firewalld).

Jest też wtyczka przykładowa, pomyślana jako punkt startowy do napisania własnej — a to zadanie jest realniejsze, niż brzmi, właśnie dlatego, że wtyczka to program czytający JSON i wypisujący JSON.

Co nowego w wydaniach 1.3

Wydanie 1.3.0 wprowadziło zmianę, którą warto znać, jeśli utrzymuje się klaster z siatką usług albo agentem bezpieczeństwa: ładowanie konfiguracji wtyczek z podkatalogu. Dla sieci o nazwie bar obiekty konfiguracyjne wtyczek mogą być doczytywane z katalogu bar obok głównego pliku konfiguracyjnego. Uzasadnienie w notatkach wydania jest wprost adresowane do dostawców wtyczek łańcuchowych: można dodać swoją wtyczkę do łańcucha bez edytowania cudzego pliku w miejscu. Kto kiedykolwiek widział, jak dwa agenty nadpisują sobie ten sam plik .conflist na węźle, doceni to od razu.

W specyfikacji odpowiada temu flaga loadOnlyInlinedPlugins: przy wartości fałszywej (domyślnej) konfiguracje wtyczek mogą być zbierane z wielu źródeł i dopisywane do listy, a przy prawdziwej — źródła poza głównym plikiem są ignorowane. Czyli administrator ma czym zablokować doklejanie się wtyczek, jeśli tego nie chce.

Wydanie 1.3.1 jest zbiorem drobnych, ale sensownych porządków: kody błędów operacji STATUS trafiły do typów w kodzie, dołożono błąd wykonania, gdy wtyczka zapisała coś na standardowe wyjście błędów, cnitool został przepisany na bibliotekę cobra, a usuwanie sieci toleruje teraz nieprawidłowy wpis w pamięci podręcznej. Do gałęzi głównej trafiła też propagacja kontekstu śledzenia OpenTelemetry w warstwie wywoływania wtyczek — czyli zapowiedź tego, że wywołania CNI staną się widoczne w rozproszonym śledzeniu razem z resztą systemu.

Diagnostyka: powtórz wywołanie z ręki

To jest najbardziej praktyczna część i powód, dla którego warto poznać ten protokół. W repozytorium jest cnitool — program wykonujący konfigurację CNI na już utworzonej przestrzeni nazw sieci. Ma polecenia add, check, del, gc i status, czyli dokładnie operacje protokołu.

Konfiguruje się go dwiema zmiennymi:

NETCONFPATH=/etc/cni/net.d   # gdzie szukać konfiguracji sieci (domyślnie)
CNI_PATH=/opt/cni/bin        # gdzie szukać plików wykonywalnych wtyczek
CNI_IFNAME=eth0              # nazwa interfejsu, domyślnie eth0

Kolejność wyszukiwania konfiguracji jest udokumentowana i sama w sobie warta zapamiętania: najpierw pliki *.conflist (listy wtyczek), a tylko jeśli żadnego nie ma — pliki *.conf i *.json z konfiguracją pojedynczej wtyczki. Wynika z tego jedna z częstszych zagadek na węźle: dorzucony plik .conf jest ignorowany, dopóki w katalogu leży jakikolwiek .conflist.

Dwie ścieżki z powyższego bloku to zresztą pierwsze miejsca, w które zaglądam przy problemach z siecią poda: /etc/cni/net.d mówi, jaka konfiguracja jest naprawdę aktywna na tym węźle, a /opt/cni/bin — czy pliki wykonywalne wtyczek, do których się ona odwołuje, w ogóle tam są. Brak jednego pliku binarnego w tym katalogu daje dokładnie ten objaw, od którego zaczęliśmy: pod bez adresu i jedna niewiele mówiąca linia w logu.

Pułapki

  • Wersja specyfikacji to nie wersja biblioteki. Specyfikacja 1.1.0, libcni 1.3.1 — mieszanie tych numerów prowadzi do złych wniosków o zgodności,
  • DEL nie zgłasza błędów z założenia, więc zasoby wyciekają cicho. Wyczerpanie podsieci węzła po tygodniach to zwykle skutek tej reguły,
  • GC nie zastępuje DEL — specyfikacja zabrania tego wprost, bo część zasobów da się usunąć tylko przy usuwaniu podłączenia,
  • STATUS jest informacyjny: wtyczka nie może zakładać, że ktoś go wywoła, a jego błąd nie blokuje żądań ADD,
  • Kod 51 znaczy więcej niż 50 — to sygnał, że problem dotyczy również działających już kontenerów,
  • ADD na istniejącą nazwę interfejsu musi zwrócić błąd, a środowisko nie powinno wołać go dwa razy bez DEL. Ten sam kontener w tej samej sieci wymaga różnych nazw interfejsów,
  • Pliki .conf są ignorowane, gdy w katalogu jest choć jeden .conflist,
  • Konfiguracje wtyczek mogą być zbierane z wielu źródeł — od 1.3.0 także z podkatalogu. Jeśli tego nie chcesz, jest loadOnlyInlinedPlugins,
  • disableCheck istnieje po to, żeby wyciszyć błędy pozorne — ale wyłączenie sondy stanu jest wyłączeniem diagnostyki, nie naprawą,
  • Typ wyniku nie opisuje wszystkiego, co wtyczka zmieniła. Reguły zapory, sterowanie przepustowością i rezerwacje adresów w nim nie występują, więc jedyną drogą sprawdzenia ich stanu jest operacja CHECK — sam zapisany wynik ADD nie wystarczy do odtworzenia obrazu sytuacji,
  • CNI opisuje pojedynczy węzeł i pojedyncze podłączenie. Zasady sieciowe, szyfrowanie ruchu między węzłami i obserwowalność to zakres implementacji — Calico, Cilium i pozostałych — a nie tej specyfikacji.

Gdzie ta wiedza się zwraca

  • Diagnostyka podów bez sieci. Znając protokół, sprawdzasz po kolei: czy jest konfiguracja w /etc/cni/net.d, czy pliki wykonywalne są w /opt/cni/bin, co zwraca cnitool status, a co check na istniejącej przestrzeni nazw. Zamiast czytać logi kubeleta w nadziei na wskazówkę,
  • Zrozumienie, za co odpowiada dostawca sieci, a za co my. Przy rozmowie o awarii z dostawcą chmury albo z zespołem klienta różnica między „wtyczka nie odpowiada" a „adresy w podsieci węzła się skończyły" to różnica między dwiema zupełnie innymi ścieżkami naprawy,
  • Świadome dokładanie wtyczek do łańcucha. Ograniczenie przepustowości poda, reguły zapory albo parametry sysctl to dołożenie jednego obiektu do listy, nie zmiana wtyczki sieciowej,
  • Ocena narzędzi, które instalują się na węzłach. Agent, który dokłada własną wtyczkę do łańcucha, robi to albo przez podkatalog konfiguracji, albo edytując plik w miejscu. Warto wiedzieć, który wariant wybrał, zanim dwa takie agenty spotkają się na jednym węźle,
  • Wyjaśnianie wycieków adresów. Skoro usuwanie jest najlepszym staraniem, to okresowe odśmiecanie nie jest fanaberią, a wymogiem projektowym — i teraz wiadomo, którą operacją się je robi.

Podsumowanie

  • Wtyczka CNI to program wykonywalny: zmienne środowiskowe, JSON na wejściu, JSON na wyjściu, kod wyjścia. Żadnego demona ani API,
  • Sześć operacji: ADD, DEL, CHECK, STATUS, VERSION, GC,
  • DEL jest zawsze najlepszym staraniem i musi znieść wielokrotne wywołanie — stąd ciche wycieki i stąd potrzeba GC,
  • GC dostaje listę nadal prawidłowych podłączeń i sprząta resztę; nie wolno go używać zamiast DEL,
  • Konfiguracja to lista wtyczek, a każda kolejna dostaje prevResult poprzedniej,
  • IPAM jest wtyczką delegowaną, nie parametrem — host-local, dhcp albo static,
  • Od 1.3.0 konfiguracje wtyczek można ładować z podkatalogu, więc dostawcy nie muszą edytować cudzych plików,
  • cnitool pozwala powtórzyć każdą operację z ręki — to najkrótsza droga od objawu do przyczyny,
  • Zajrzyj do /etc/cni/net.d i /opt/cni/bin, zanim zaczniesz czytać logi.

Licencja: CNI jest na Apache-2.0 i to najbardziej komfortowy wariant licencji permisywnej dla pracy komercyjnej: wolno używać, modyfikować, wpinać w produkty zamknięte i redystrybuować, przy zachowaniu noty licencyjnej oraz pliku NOTICE, jeśli występuje, i z odnotowaniem istotnych zmian w plikach, które się modyfikuje. Apache-2.0 dodaje przy tym jawną licencję patentową od kontrybutorów wraz z klauzulą odwetową — kto pozwie projekt o naruszenie patentu, traci przyznane prawa patentowe. Przy specyfikacji sieciowej, czyli obszarze gęsto zaminowanym patentami, jest to zaleta praktyczna, nie formalność, i jeden z powodów, dla których projekty CNCF stoją właśnie na tej licencji. Warto natomiast pamiętać, że licencja specyfikacji i biblioteki nie rozciąga się na implementacje: Calico, Cilium i wtyczki dostawców chmur mają własne licencje i własne modele komercyjne, część z nich w układzie open core z płatnymi funkcjami korporacyjnymi. Zgodność z CNI nie mówi więc nic o warunkach, na jakich wolno używać konkretnej wtyczki — i to jest rozgraniczenie, które warto zrobić przed wyborem sieci dla klastra klienta, nie po.