Kubernetes для самых маленьких. Часть 14: Как устроена сеть Kubernetes?

Все части серии «Kubernetes для самых маленьких»

До сих пор мы пользовались адресами подов, не спрашивая, откуда они берутся: в выводе kubectl get pods -o wide у каждого пода свой адрес из 172.20.0.0/16, и wget из одного пода до другого просто работает, на каком бы узле тот ни жил. Сегодня разберём, почему это так. Сначала — сетевая модель, которую Kubernetes требует от любого кластера, и чего он сам при этом не делает. Потом — CNI, интерфейс, через который containerd поручает сетевому плагину подключить под. И дальше по пути пакета на нашем стенде с Calico из части 4: как под получает адрес, откуда на узле veth cali… и маршрут /32, кто отвечает поду на ARP, как пакет идёт к поду на том же узле и на соседнем, когда включается VXLAN и как поды выходят в интернет. Сетевое пространство имён пода, которое держит pause-контейнер, — из части 6.


Сетевая модель Kubernetes

Kubernetes не реализует сеть подов сам, но ставит любой реализации три требования:

  1. У каждого пода свой IP-адрес, общий для всех его контейнеров. Контейнеры одного пода живут в одном сетевом пространстве имён и видят друг друга через localhost.
  2. Любой под достаёт до любого другого пода по его адресу без NAT — на том же узле или на другом. Под A видит в пакетах от пода B настоящий адрес B.
  3. Агенты на узле (kubelet, системные демоны) достают до всех подов этого узла. На практике в Calico и большинстве других реализаций узел достаёт до всех подов кластера, и поды — до адресов узлов.

Из этого следует ещё одно свойство, которое часто формулируют отдельно: адрес, который под видит у себя на интерфейсе, — тот же адрес, под которым его видят остальные. Внутри кластера нет «внутреннего» и «внешнего» адреса пода.

Чем это удобно, видно на контрасте с Docker на одной машине. Там контейнеры получают адреса из частной сети моста docker0, и чтобы достучаться до них снаружи, нужно пробросить порт узла (-p 8080:80). Два контейнера, которым нужен порт 80, не могут получить один порт узла, поэтому порты приходится распределять и где-то записывать. В Kubernetes такой задачи нет: у каждого пода свой адрес, все поды могут слушать порт 8080, и приложение ведёт себя как на отдельной виртуальной машине. Плата за это — сеть кластера должна уметь доставить пакет на любой из десятков тысяч адресов подов, и это делает сетевой плагин.

Что в модель не входит:

  • стабильный адрес. Под пересоздан — адрес новый. Стабильное имя и адрес даёт Service (часть 15), а не сеть подов;
  • адрес пода снаружи кластера. Роутер 192.168.10.1 о сети 172.20.0.0/16 ничего не знает, и ноутбук администратора до пода напрямую не достанет. Как пускать трафик снаружи, разберём в частях 15, 17 и 18;
  • изоляция. По умолчанию любой под может открыть соединение с любым другим. Ограничения — NetworkPolicy (часть 20).

Кто что делает

Сеть пода создаётся при его запуске, в цепочке kubelet → containerd → плагин CNI:

 kubelet                containerd                    плагин CNI (calico)
    │ RunPodSandbox (CRI) ─►│                                   │
    │                       │ создать netns,                    │
    │                       │ запустить pause                   │
    │                       │ ADD: netns, имя интерфейса ──────►│ запросить адрес (calico-ipam)
    │                       │                                   │ создать veth, eth0 в netns пода,
    │                       │                                   │ адрес, маршруты, маршрут на узле
    │                       │◄────── результат: IP, интерфейсы ─┤
    │◄── sandbox готов, IP ─┤                                   │
    │ записать status.podIP │                                   │
    ▼                       ▼                                   ▼

kubelet через CRI (интерфейс, по которому kubelet управляет containerd) просит создать sandbox пода. containerd создаёт новое сетевое пространство имён (файл в /var/run/netns/cni-…), запускает в нём pause-контейнер и вызывает сетевой плагин по спецификации CNI. Плагин возвращает адрес, containerd отдаёт его kubelet'у, kubelet записывает его в status.podIP и status.podIPs. Всё, что происходит между вызовом и ответом, — забота плагина: Kubernetes не знает, что такое veth, маршрут или VXLAN.

В Kubernetes есть ещё механизм выдачи подсетей узлам: kube-controller-manager с флагом --allocate-node-cidrs (kubeadm включает его вместе с --cluster-cidr, если при init задан podSubnet) выделяет каждому узлу кусок сети подов и пишет его в spec.podCIDR объекта Node. Некоторые плагины берут адреса из этого поля. Calico по умолчанию его не использует: у него своё управление адресами, о нём ниже. Поэтому значение в spec.podCIDR и реальные адреса подов на узле на нашем стенде не совпадают, и это нормально.


CNI

CNI (Container Network Interface) — спецификация проекта CNCF (Cloud Native Computing Foundation — фонда, под которым развиваются Kubernetes, containerd и связанные проекты) о том, как среда запуска контейнеров вызывает сетевые плагины. Она намеренно примитивна:

  • плагин — исполняемый файл. Лежит в каталоге бинарников (на узлах Kubernetes — /opt/cni/bin), запускается заново на каждую операцию и завершается;
  • операция и параметры — в переменных окружения: CNI_COMMAND (ADD, DEL, CHECK, VERSION, а в версии спецификации 1.1 ещё GC и STATUS), CNI_CONTAINERID, CNI_NETNS (путь к сетевому пространству имён), CNI_IFNAME (имя интерфейса внутри, обычно eth0), CNI_ARGS (дополнительные пары «ключ — значение»), CNI_PATH (где искать другие плагины);
  • конфигурация сети — JSON на стандартном вводе, результат — JSON на стандартном выводе: интерфейсы, адреса, маршруты, DNS;
  • цепочка плагинов. Файл .conflist перечисляет несколько плагинов; среда запуска вызывает их по очереди, передавая каждому результат предыдущего. Первый создаёт интерфейс и адрес, следующие дополняют: пробрасывают порты, ограничивают полосу.

ADD — подключить контейнер к сети: создать интерфейс, выдать адрес. DEL — отключить и освободить адрес; вызывается при удалении пода и должен быть идемпотентным (повторный DEL — не ошибка). CHECK — убедиться, что всё на месте. GC — освободить ресурсы контейнеров, которых больше нет. STATUS — готов ли плагин принимать ADD. Конфигурация Calico на стенде объявляет "cniVersion": "1.0.0", так что GC и STATUS ей не адресуются.

Где containerd всё это ищет, задано в его конфигурации:

user@k8s-node-1:~$ grep -A4 "cri.v1.runtime'.cni]" /etc/containerd/config.toml
    [plugins.'io.containerd.cri.v1.runtime'.cni]
      bin_dir = ''
      bin_dirs = ['/opt/cni/bin']
      conf_dir = '/etc/cni/net.d'
      max_conf_num = 1

max_conf_num = 1 — containerd берёт один, первый по алфавиту файл конфигурации из /etc/cni/net.d. Отсюда частая поломка: после экспериментов с другим плагином в каталоге остаётся его конфигурация, и если её имя по алфавиту раньше 10-calico.conflist — например, 05-cilium.conflist у Cilium, сетевого плагина на eBPF (о нём ниже), — поды получают адреса не той сети. Пока в каталоге нет ни одной конфигурации, containerd сообщает, что сеть не готова, и kubelet держит узел в Ready=False с сообщением container runtime network not ready: NetworkReady=false reason:NetworkPluginNotReady message:Network plugin returns error: cni plugin not initialized; на узел не планируются поды, кроме тех, что работают в сети узла (hostNetwork: true). Так ведёт себя только что присоединённый узел до установки Calico — мы видели это в части 4.

Бинарники в /opt/cni/bin приходят из двух мест. Набор эталонных плагинов проекта CNI (bridge, host-local, loopback, portmap, bandwidth и другие) кладёт пакет kubernetes-cni, который apt поставил зависимостью kubeadm. Свои бинарники calico, calico-ipam и flannel и обновлённые копии части эталонных Calico кладёт сам: это делает init-контейнер install-cni пода calico-node, который Tigera operator — контроллер, устанавливающий и обновляющий Calico, — запускает DaemonSet'ом на каждом узле. Он же пишет 10-calico.conflist. Поэтому новый узел получает CNI сам, как только на нём стартует calico-node.


Как Calico выдаёт адрес

Адреса выдаёт IPAM-плагин calico-ipam, который вызывает основной плагин calico. Он раздаёт адреса из IP-пула — ресурса Calico, созданного оператором при установке:

$ kubectl get ippool default-ipv4-ippool -o yaml | grep -A10 '^spec:'
spec:
  allowedUses:
  - Workload
  - Tunnel
  assignmentMode: Automatic
  blockSize: 26
  cidr: 172.20.0.0/16
  ipipMode: Never
  natOutgoing: true
  nodeSelector: all()
  vxlanMode: CrossSubnet

allowedUses разрешает брать из пула адреса и для подов (Workload), и для туннельных интерфейсов узлов (Tunnel), assignmentMode: Automatic — раздавать их автоматически, а не только по явному запросу пода. Пул не раздаётся по одному адресу. Он нарезан на блоки размера blockSize — по умолчанию /26, 64 адреса, — и каждый блок закрепляется за узлом (affinity). Когда на узле запускается первый под, calico-ipam закрепляет за узлом свободный блок и берёт из него адрес; следующие поды этого узла получают адреса из того же блока, пока он не кончится, а потом узлу закрепляется второй блок. Если свободных блоков в пуле нет, узел может занять адрес из чужого блока (borrowing); это работает, но портит агрегацию маршрутов, и на нормально спланированном пуле не случается. Внутри блока адреса выдаются не обязательно подряд: блок хранит список свободных адресов, через узел за прошлые части прошло много подов, и на стенде поды одного узла получили .43, .45 и .53.

Зачем блоки: маршрут на соседний узел нужен один на блок, а не на под. На узле с сотней подов — два маршрута /26 у соседей вместо сотни /32.

Состояние IPAM Calico хранит в API Kubernetes, в своих CRD группы crd.projectcalico.org:

Ресурс Что хранит
IPPool пул, размер блока, режим инкапсуляции, natOutgoing
IPAMBlock один блок: CIDR, узел-владелец, занятые и свободные адреса, кто их занял
BlockAffinity закрепление блока за узлом
IPAMHandle идентификатор выделения и его адреса, чтобы DEL освободил именно их: k8s-pod-network.<ID sandbox пода> для пода, vxlan-tunnel-addr-<узел> для туннельного адреса

Адрес, выданный поду, Calico записывает и в аннотации пода — cni.projectcalico.org/podIP и cni.projectcalico.org/podIPs, — вместе с идентификатором sandbox (cni.projectcalico.org/containerID). Из пода и этих аннотаций Calico строит WorkloadEndpoint — своё представление «интерфейс пода на узле», к которому затем привязываются сетевые политики. Отдельным объектом в API Kubernetes он не хранится, и kubectl его не видит (error: the server doesn't have a resource type "workloadendpoints"); его показывает утилита Calico calicoctl, а имя вида k8s--node--1-k8s-net-eth0 (узел с удвоенными дефисами, оркестратор, под, интерфейс) встречается в журнале CNI.


veth, /32 и 169.254.1.1

Получив адрес, плагин подключает под к узлу. Calico не использует мост вроде docker0: каждый под соединён с узлом своей veth-парой, а дальше работает обычная маршрутизация ядра узла. Это L3-модель: между подами нет общего L2-сегмента.

            под hello (netns cni-2880cb32…)                узел k8s-node-1
 ┌───────────────────────────────────────┐   ┌───────────────────────────────────────────┐
 │ eth0@if172  172.20.211.43/32          │   │ calia68edcf6ff3  без адреса,              │
 │             MAC 4a:17:f0:d5:da:0a     │◄─►│                  MAC ee:ee:ee:ee:ee:ee    │
 │ default via 169.254.1.1 dev eth0      │   │                  proxy_arp = 1            │
 │ 169.254.1.1 dev eth0 scope link       │   │ 172.20.211.43 dev calia68edcf6ff3         │
 │                                       │   │              scope link                   │
 └───────────────────────────────────────┘   └───────────────────────────────────────────┘

Сторона пода. Интерфейс eth0 с адресом /32 — у пода нет «своей подсети», в которой он мог бы искать соседей по ARP. Маршрут по умолчанию ведёт на 169.254.1.1 — адрес из диапазона link-local, которого на самом деле нет ни у кого. Он нужен только потому, что маршрут по умолчанию должен указывать на какой-то шлюз, а Calico не хочет тратить на шлюз адрес из пула.

Сторона узла. Интерфейс cali плюс первые 11 символов SHA-1 от строки <неймспейс>.<имя пода>: для пода net из неймспейса demo ниже это cali4d7612d46be, и printf demo.net | sha1sum начинается с 4d7612d46be. Адреса у него нет, MAC-адрес у всех таких интерфейсов одинаковый — ee:ee:ee:ee:ee:ee (он нигде не покидает пару, поэтому конфликтов нет). На узле — маршрут /32 на адрес пода через этот интерфейс: так ядро узла знает, в какую трубу отдавать пакеты для пода.

Кто отвечает на ARP за 169.254.1.1. Под хочет отправить пакет куда угодно — ядро пода смотрит маршрут, видит шлюз 169.254.1.1 и спрашивает по ARP его MAC-адрес (ARP — в части 7 сетевой серии: широковещательный вопрос «у кого такой IP» и ответ с MAC-адресом). Запрос по veth приходит на cali… узла. Адреса 169.254.1.1 у узла нет, но на интерфейсе включён proxy ARP — ядро отвечает на ARP-запрос своим MAC-адресом, если у него есть маршрут к запрошенному адресу через другой интерфейс. Маршрут по умолчанию у узла есть, и под получает ответ «169.254.1.1 — это ee:ee:ee:ee:ee:ee». Дальше все пакеты пода уходят на этот MAC, то есть в ядро узла, а оно маршрутизирует их как обычный маршрутизатор.

Итог: под думает, что за его eth0 стоит шлюз; на самом деле шлюз — сам узел, и всё, что под отправляет, попадает в таблицу маршрутизации узла и в правила nftables, где Felix применяет сетевые политики.

Интерфейс и маршрут создаёт CNI-плагин при ADD, а дальше их поддерживает Felix — агент Calico на узле: включает на cali… нужные параметры ядра (proxy ARP, пересылку), следит, чтобы маршруты к подам узла совпадали с данными IPAM, и подвешивает к интерфейсу правила политик.


Пакет к поду на том же узле

net на k8s-node-1 (172.20.211.53) обращается к поду hello на том же узле (172.20.211.43):

  1. Ядро пода net: адресат не в своей сети, отправить на шлюз 169.254.1.1 через eth0; MAC шлюза — ee:ee:ee:ee:ee:ee.
  2. Кадр выходит из veth на стороне узла — интерфейс cali4d7612d46be.
  3. Ядро узла: пакет на 172.20.211.43, в таблице маршрутизации — 172.20.211.43 dev calia68edcf6ff3 scope link. Пакет проходит хук forward netfilter — здесь в цепочках nftables Calico проверяются сетевые политики — и уходит в calia68edcf6ff3.
  4. Под hello получает пакет на свой eth0 с исходным адресом 172.20.211.53. NAT нет.

Пакет не покидает узел и не проходит ни через какой мост — только два veth и маршрутизация ядра.


Пакет к поду на другом узле

Теперь net на k8s-node-1 обращается к поду hello на k8s-node-2 (172.20.172.6). Маршруты k8s-node-1:

user@k8s-node-1:~$ ip route show default
default via 192.168.10.1 dev ens18 proto dhcp src 192.168.10.20 metric 100
user@k8s-node-1:~$ ip route | grep 172.20
172.20.70.192/26 via 192.168.10.17 dev ens18 proto 80 onlink
172.20.172.0/26 via 192.168.10.21 dev ens18 proto 80 onlink
blackhole 172.20.211.0/26 proto 80
172.20.211.1 dev cali01e594701c5 scope link metric 1024
172.20.211.43 dev calia68edcf6ff3 scope link metric 1024
172.20.211.53 dev cali4d7612d46be scope link metric 1024
  • /32 на поды своего узла — через их cali…;
  • blackhole на собственный блок 172.20.211.0/26 — если пакет пришёл на адрес из блока узла, а пода с таким адресом нет (удалён), он отбрасывается здесь, а не уходит по маршруту по умолчанию к роутеру. Более специфичные /32 к живым подам этот маршрут перекрывают;
  • блоки соседей /26 — блок мастера 172.20.70.192/26 и блок k8s-node-2 172.20.172.0/26, через их адреса на ens18; onlink говорит ядру считать следующий шаг доступным прямо через этот интерфейс.

proto 80 — номер, которым помечает свои маршруты Felix. Маршруты к блокам соседей и blackhole у нашего пула с VXLAN прописывает именно он, по данным IPAM из API: он знает, какой блок закреплён за каким узлом и какой у того узла адрес. BGP-демон BIRD, который тоже работает в calico-node, в ядро эти маршруты не пишет — к нему вернёмся в разделе о компонентах Calico.

Путь пакета:

 под net                  k8s-node-1                     k8s-node-2              под hello
 172.20.211.53            192.168.10.20                  192.168.10.21           172.20.172.6
     │ eth0 → cali…  ─────►│ маршрут 172.20.172.0/26       │                          │
     │                     │ via 192.168.10.21 dev ens18   │                          │
     │                     │ ──── Ethernet, без обёртки ──►│ маршрут /32              │
     │                     │  src 172.20.211.53            │ dev cali… ──────────────►│ eth0
     │                     │  dst 172.20.172.6             │                          │

Узлы стоят в одной сети 192.168.10.0/24 и для ядра друг другу соседи на L2: k8s-node-1 выясняет по ARP MAC-адрес 192.168.10.21 и отправляет кадр прямо ему, с IP-заголовком пода внутри как есть. k8s-node-2 видит пакет на адрес 172.20.172.6, находит маршрут /32 на cali… и отдаёт поду. Роутер 192.168.10.1 в этом не участвует, и знать о сети подов ему не нужно. Это и есть прямая маршрутизация — самый быстрый вариант: ни лишних заголовков, ни уменьшения MTU.

VXLANCrossSubnet: когда нужна обёртка

Прямая маршрутизация работает, пока узлы — L2-соседи. Если бы узел k8s-node-3 стоял в другой подсети, 192.168.20.0/24, пакет с адресатом 172.20.x.x пришлось бы отдать роутеру, а роутер про сеть подов не знает и отправит его по своему маршруту по умолчанию. Есть три выхода.

Инкапсуляция VXLAN. VXLAN — протокол, который заворачивает Ethernet-кадр целиком в UDP-датаграмму (порт 4789) с заголовком, где указан 24-битный номер сегмента VNI; между узлами идёт обычный UDP с адресами узлов, и роутер видит только его. Подробно — в части 18 сетевой серии. Calico создаёт на каждом узле интерфейс vxlan.calico (VNI 4096) с туннельным адресом — первым адресом блока узла, — а Felix заранее записывает на нём ARP-записи и записи таблицы пересылки для туннельных адресов всех остальных узлов: «MAC туннеля соседа такой-то, отправлять на его адрес в сети узлов». Для блоков узлов из другой подсети Felix прописывает маршрут через vxlan.calico, шлюзом — туннельный адрес узла-владельца. Для воображаемого k8s-node-3 с блоком 172.20.88.0/26 такой маршрут выглядел бы так:

172.20.88.0/26 via 172.20.88.0 dev vxlan.calico onlink

Цена — 50 байт заголовков на каждом пакете (внешние Ethernet, IP, UDP и сам VXLAN), поэтому MTU интерфейсов подов при включённом VXLAN на 50 меньше, чем у узла: 1450 при обычных 1500.

Режим нашего пула vxlanMode: CrossSubnet — компромисс: Felix сравнивает подсеть узла-получателя со своей и заворачивает в VXLAN только трафик между подсетями, а соседям в своей подсети отправляет пакеты напрямую, как выше. На стенде все узлы в одной сети, поэтому VXLAN фактически не используется: интерфейс vxlan.calico существует и его записи о соседях готовы, но маршрутов через него нет. Режим Always заворачивал бы всё; Never — ничего.

Инкапсуляция IPIP. IP-in-IP (IP-протокол номер 4) кладёт IP-пакет пода в ещё один IP-заголовок с адресами узлов — без UDP и без Ethernet-кадра, поэтому накладные расходы меньше, 20 байт. Интерфейс на узле — tunl0. Ограничение: некоторые сети и облака не пропускают протокол 4 (документация Calico называет Azure), тогда как UDP проходит везде. В Calico IPIP включается полем ipipMode пула с теми же значениями Always и CrossSubnet, а маршруты к блокам для него по умолчанию распространяет и прописывает BGP-демон BIRD.

BGP без инкапсуляции. BGP — протокол, которым маршрутизаторы сообщают друг другу, какие сети через них доступны (часть 22 сетевой серии). Calico умеет анонсировать блоки и роутеру, и тогда роутер знает, что 172.20.211.0/26 — через 192.168.10.20: пакеты между подсетями идут без обёртки, а ноутбук администратора может достать до пода напрямую. Это требует роутера, который говорит по BGP, и разобрано в серии «Отказоустойчивый Kubernetes на BGP».

Режим Между узлами одной подсети Между подсетями Накладные расходы Что нужно от сети
VXLAN CrossSubnet (стенд) напрямую UDP 4789 50 байт только между подсетями UDP 4789 между узлами
VXLAN Always UDP 4789 UDP 4789 50 байт всегда UDP 4789
IPIP CrossSubnet напрямую IP-протокол 4 20 байт между подсетями протокол 4 и BGP между узлами
без инкапсуляции, BGP напрямую напрямую нет роутер с BGP

Выход подов наружу: natOutgoing

Под hello обращается к серверу в локальной сети, например к 192.168.10.32, или в интернет. Пакет уходит с узла с исходным адресом 172.20.211.43, и ответить на него получатель не сможет: роутер не знает, где сеть 172.20.0.0/16. Поэтому в пуле включён natOutgoing: true: Felix добавляет правило маскарадинга — пакеты из пула к адресатам вне всех пулов Calico получают исходным адресом адрес узла. Это обычный source NAT, как в домашнем роутере (часть 6 сетевой серии): ядро запоминает соединение в conntrack и на обратном пути возвращает адресату-поду его адрес.

Трафик под → под под это правило не попадает (адресат внутри пула), поэтому требование «без NAT» не нарушается. А для внешних серверов все поды узла выглядят как сам узел. Это важно для безопасности: если NFS-сервер разрешает доступ «с адресов узлов кластера», он разрешает его и любому поду на этих узлах. К этой ловушке вернёмся в части 22.


Компоненты Calico

$ kubectl get pods -n calico-system -o wide
NAME                                       READY   STATUS    RESTARTS        AGE     IP              NODE           NOMINATED NODE   READINESS GATES
calico-apiserver-7c56559b65-dm42w          1/1     Running   0               4d9h    172.20.70.198   k8s-master-1   <none>           <none>
calico-apiserver-7c56559b65-phtkx          1/1     Running   0               4d9h    172.20.70.196   k8s-master-1   <none>           <none>
calico-kube-controllers-6879897867-8jnzf   1/1     Running   0               4d9h    172.20.70.195   k8s-master-1   <none>           <none>
calico-node-6d2qb                          1/1     Running   0               4d9h    192.168.10.20   k8s-node-1     <none>           <none>
calico-node-7mx4w                          1/1     Running   2 (6h16m ago)   4d8h    192.168.10.21   k8s-node-2     <none>           <none>
calico-node-l9g44                          1/1     Running   0               4d9h    192.168.10.17   k8s-master-1   <none>           <none>
calico-typha-55d594d885-dsh7d              1/1     Running   0               4d9h    192.168.10.17   k8s-master-1   <none>           <none>
calico-typha-55d594d885-q7nvz              1/1     Running   0               4h10m   192.168.10.20   k8s-node-1     <none>           <none>
csi-node-driver-6nzck                      2/2     Running   4 (6h16m ago)   4d8h    172.20.172.16   k8s-node-2     <none>           <none>
csi-node-driver-7jlkp                      2/2     Running   0               4d9h    172.20.70.193   k8s-master-1   <none>           <none>
csi-node-driver-q6t25                      2/2     Running   0               4d9h    172.20.211.1    k8s-node-1     <none>           <none>

Поды с адресами узлов (192.168.10.x) работают в сети узла, остальные — обычные поды с адресами из блоков.

calico-node (DaemonSet, в сети узла) — главный агент. В его единственном контейнере под присмотром супервизора runsv работают несколько процессов: calico-node -felix, bird, calico-node -confd и служебный calico-node -node-services.

  • Felix — программирует узел: создаёт и настраивает cali…, прописывает маршруты /32 к своим подам и маршруты к блокам других узлов (напрямую или через vxlan.calico), поднимает vxlan.calico, пишет правила сетевых политик и natOutgoing в nftables (в других датаплейнах — в iptables или программами eBPF), сообщает о своём состоянии в API;
  • BIRD — демон маршрутизации, который говорит по BGP (TCP-порт 179). Оператор по умолчанию включает BGP (spec.calicoNetwork.bgp: Enabled в Installation), и каждый узел стенда держит BGP-сессию с каждым другим (полная сетка, full mesh): узлы анонсируют друг другу свои блоки. Но в ядро маршруты из пула с VXLAN BIRD не пишет — это делает Felix, а в фильтре конфигурации BIRD так и написано. BGP в такой конфигурации нужен, только если анонсировать сети подов или сервисов наружу, на роутеры; документация Calico прямо говорит, что пулам только с VXLAN BGP не требуется. В режиме IPIP и без инкапсуляции маршруты к блокам ставит как раз BIRD;
  • confd — генерирует конфигурацию BIRD из ресурсов Calico (номер AS, соседи, пулы) и перезагружает его при изменениях.

calico-typha — кэширующий посредник между API Kubernetes и Felix. Без него каждый Felix держал бы собственные watch на поды, узлы, политики и ресурсы Calico, и на сотнях узлов это сотни одинаковых потоков из kube-apiserver. Typha держит одно подключение, фильтрует и раздаёт изменения всем Felix. Оператор ставит Typha всегда и сам подбирает число реплик по числу узлов (на стенде их две); реплики размещаются на разных узлах и работают в сети узла.

calico-kube-controllers — контроллеры, которые убирают за удалёнными объектами: освобождают адреса IPAM и блоки узлов, которых больше нет, синхронизируют метки узлов и ServiceAccount'ов для политик.

calico-apiserver — API-сервер Calico из части 4: через него kubectl работает с ресурсами projectcalico.org/v3 (пулами, политиками, закреплениями блоков).

Если при установке применён стандартный custom-resources.yaml целиком, в неймспейсе будут ещё поды Goldmane (агрегатор журналов сетевых потоков, которые собирает Felix) и Whisker (веб-интерфейс к ним); на работу сети они не влияют, и на стенде их нет.

csi-node-driver — узловой CSI-плагин Calico, через который поды получают служебные тома для политик уровня приложения; к сети подов он отношения не имеет.

Сам Tigera operator живёт в неймспейсе tigera-operator, читает ресурс Installation и приводит все эти DaemonSet и Deployment к описанному там состоянию.

Датаплейн. То, как Felix применяет правила, задаёт spec.calicoNetwork.linuxDataplane. На стенде — Nftables (часть 4): Felix пишет правила напрямую в собственную таблицу nftables, рядом с таблицами kube-proxy, который у нас тоже работает в режиме nftables (документация Calico требует этого сочетания). Значение по умолчанию у оператора — Iptables: те же правила через iptables. Третий вариант — BPF: eBPF — механизм ядра Linux, позволяющий загружать в ядро проверенные программы обработки пакетов, и такие программы Calico заменяют и правила netfilter, и kube-proxy; eBPF-датаплейн разобран в серии про BGP.


Смотрим руками

Возьмём под hello на k8s-node-1 и запустим рядом под с сетевыми инструментами. hello здесь — Deployment из трёх реплик в неймспейсе demo из части 11. Образ hello собран без оболочки (часть 5), поэтому заглядывать внутрь будем через busybox, поставленный на тот же узел: узел зададим явно полем nodeName, мимо планировщика (часть 13).

$ kubectl -n demo get pod -l app.kubernetes.io/name=hello -o wide --field-selector spec.nodeName=k8s-node-1
NAME                     READY   STATUS    RESTARTS   AGE   IP              NODE         NOMINATED NODE   READINESS GATES
hello-6d5c6db6b4-nqsp8   1/1     Running   0          15s   172.20.211.43   k8s-node-1   <none>           <none>
$ kubectl -n demo run net --image=busybox:1.37 --overrides='{"spec":{"nodeName":"k8s-node-1"}}' -- sleep 3600
pod/net created
$ kubectl -n demo get pod net -o wide
NAME   READY   STATUS    RESTARTS   AGE   IP              NODE         NOMINATED NODE   READINESS GATES
net    1/1     Running   0          6s    172.20.211.53   k8s-node-1   <none>           <none>
$ kubectl -n demo exec net -- ip addr show eth0
2: eth0@if173: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1450 qdisc noqueue qlen 1000
    link/ether 36:c6:f6:49:44:cf brd ff:ff:ff:ff:ff:ff
    inet 172.20.211.53/32 scope global eth0
       valid_lft forever preferred_lft forever
    …
$ kubectl -n demo exec net -- ip route
default via 169.254.1.1 dev eth0
169.254.1.1 dev eth0 scope link

eth0@if173 — пара veth, у которой второй конец — интерфейс номер 173 на узле. Адрес /32, MTU 1450: Calico вычисляет MTU сам по MTU узла (1500) и включённым инкапсуляциям, а VXLAN в пуле включён, хотя на стенде и не используется (значение лежит на узле в /var/lib/calico/mtu). Пропингуем hello и посмотрим ARP-таблицу пода:

$ kubectl -n demo exec net -- ping -c1 172.20.211.43
PING 172.20.211.43 (172.20.211.43): 56 data bytes
64 bytes from 172.20.211.43: seq=0 ttl=63 time=0.103 ms

--- 172.20.211.43 ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max = 0.103/0.103/0.103 ms
$ kubectl -n demo exec net -- ip neigh
169.254.1.1 dev eth0 lladdr ee:ee:ee:ee:ee:ee ref 1 used 0/0/0 probes 4 REACHABLE

ttl=63 — пакет прошёл через один маршрутизатор (ядро узла), хотя оба пода на одном узле. В ARP-таблице пода единственная запись — фиктивный шлюз с MAC-адресом cali….

На узле — интерфейсы и параметры:

user@k8s-node-1:~$ ip -br link | grep -E 'cali|vxlan'
vxlan.calico     UNKNOWN        66:ee:f4:61:98:50 <BROADCAST,MULTICAST,UP,LOWER_UP>
cali01e594701c5@if2 UP             ee:ee:ee:ee:ee:ee <BROADCAST,MULTICAST,UP,LOWER_UP>
calia68edcf6ff3@if2 UP             ee:ee:ee:ee:ee:ee <BROADCAST,MULTICAST,UP,LOWER_UP>
cali4d7612d46be@if2 UP             ee:ee:ee:ee:ee:ee <BROADCAST,MULTICAST,UP,LOWER_UP>
user@k8s-node-1:~$ cat /proc/sys/net/ipv4/conf/cali4d7612d46be/proxy_arp
1
user@k8s-node-1:~$ ip route get 172.20.211.53
172.20.211.53 dev cali4d7612d46be src 192.168.10.20 uid 1000
    cache

У всех cali… один MAC, на каждом включён proxy ARP; @if2 — номер eth0 внутри пространства имён своего пода (в каждом поде он второй, после lo). calia68edcf6ff3 — это hello, cali4d7612d46be — net, а cali01e594701c5 — под csi-node-driver с адресом 172.20.211.1. Туннельный интерфейс:

user@k8s-node-1:~$ ip -d link show vxlan.calico | grep -o 'vxlan id .* nolearning'
vxlan id 4096 local 192.168.10.20 dev ens18 srcport 0 0 dstport 4789 ttl auto ageing 300 reserved_bits 0xf7ffffff000000ff nolearning
user@k8s-node-1:~$ ip addr show vxlan.calico | grep 'inet '
    inet 172.20.211.0/32 scope global vxlan.calico
user@k8s-node-1:~$ ip neigh show dev vxlan.calico
172.20.70.192 lladdr 66:79:81:e0:1a:7d PERMANENT
172.20.172.0 lladdr 66:05:38:46:ab:ea PERMANENT
user@k8s-node-1:~$ bridge fdb show dev vxlan.calico
66:05:38:46:ab:ea dst 192.168.10.21 self permanent
66:79:81:e0:1a:7d dst 192.168.10.17 self permanent

VNI 4096, порт 4789, туннельный адрес — первый адрес блока узла, 172.20.211.0. nolearning значит, что ядро не изучает адреса соседей по пришедшим пакетам: записи PERMANENT о туннельных адресах мастера и k8s-node-2 (MAC их vxlan.calico и куда отправлять UDP) Felix прописал сам. Но, как видно в ip route выше, маршрутов через vxlan.calico нет: на стенде все соседи в своей подсети.

Пакеты. Запустим два tcpdump на k8s-node-1 — на veth пода net и на ens18 — и пропингуем из net под hello на k8s-node-2:

$ kubectl -n demo exec net -- ping -c1 172.20.172.6
PING 172.20.172.6 (172.20.172.6): 56 data bytes
64 bytes from 172.20.172.6: seq=0 ttl=62 time=0.536 ms

--- 172.20.172.6 ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max = 0.536/0.536/0.536 ms

user@k8s-node-1:~$ sudo tcpdump -eni cali4d7612d46be -c 4 'icmp or arp'
22:47:09.834467 36:c6:f6:49:44:cf > ee:ee:ee:ee:ee:ee, ethertype IPv4 (0x0800), length 98: 172.20.211.53 > 172.20.172.6: ICMP echo request, id 46, seq 0, length 64
22:47:09.834894 ee:ee:ee:ee:ee:ee > 36:c6:f6:49:44:cf, ethertype IPv4 (0x0800), length 98: 172.20.172.6 > 172.20.211.53: ICMP echo reply, id 46, seq 0, length 64
22:47:14.960527 ee:ee:ee:ee:ee:ee > 36:c6:f6:49:44:cf, ethertype ARP (0x0806), length 42: Request who-has 172.20.211.53 tell 192.168.10.20, length 28
22:47:14.960534 36:c6:f6:49:44:cf > ee:ee:ee:ee:ee:ee, ethertype ARP (0x0806), length 42: Request who-has 169.254.1.1 tell 172.20.211.53, length 28

user@k8s-node-1:~$ sudo tcpdump -eni ens18 -c 2 'icmp and host 172.20.172.6'
22:47:09.834514 00:15:5d:0a:00:02 > 00:15:5d:0a:00:03, ethertype IPv4 (0x0800), length 98: 172.20.211.53 > 172.20.172.6: ICMP echo request, id 46, seq 0, length 64
22:47:09.834865 00:15:5d:0a:00:03 > 00:15:5d:0a:00:02, ethertype IPv4 (0x0800), length 98: 172.20.172.6 > 172.20.211.53: ICMP echo reply, id 46, seq 0, length 64

На veth — MAC пода и ee:ee:…, на ens18 — MAC-адреса двух узлов, а IP-адреса в обоих случаях подов, без NAT и без обёртки. ttl=62 — два маршрутизатора: ядро k8s-node-1 и ядро k8s-node-2. Через пять секунд после обмена обе стороны veth перепроверяют соседа: узел спрашивает адрес пода, под — свой фиктивный шлюз. Будь получатель на узле из другой подсети, на ens18 вместо ICMP между адресами подов шли бы UDP-датаграммы на порт 4789 между адресами узлов, а пакет пода лежал бы внутри; на стенде такого узла нет, и показать это вживую нельзя.

Как proxy ARP отвечает поду, лучше всего видно на новом поде: его первый ARP-запрос уходит ещё при запуске. Слушаем все интерфейсы и создаём net2:

user@k8s-node-1:~$ sudo tcpdump -eni any -c 2 'arp and host 169.254.1.1'
22:47:50.883928 calie01d6b79688 B   ifindex 174 f2:36:0d:6b:6f:f8 ethertype ARP (0x0806), length 48: Request who-has 169.254.1.1 tell 172.20.211.45, length 28
22:47:50.883945 calie01d6b79688 Out ifindex 174 ee:ee:ee:ee:ee:ee ethertype ARP (0x0806), length 48: Reply 169.254.1.1 is-at ee:ee:ee:ee:ee:ee, length 28

$ kubectl -n demo run net2 --image=busybox:1.37 --overrides='{"spec":{"nodeName":"k8s-node-1"}}' -- sleep 3600
pod/net2 created

Широковещательный (B) вопрос «у кого 169.254.1.1» от нового пода 172.20.211.45 и через 17 микросекунд ответ узла: ee:ee:ee:ee:ee:ee. Удаление пода тоже видно на узле: после kubectl -n demo delete pod net2 (DEL) интерфейс calie01d6b79688, маршрут на 172.20.211.45 и сетевое пространство имён исчезают, а адрес возвращается в блок — ip route | grep 172.20.211 снова показывает только blackhole и три /32.

Конфигурация и бинарники CNI:

user@k8s-node-1:~$ ls /opt/cni/bin | xargs
LICENSE README.md bandwidth bridge calico calico-ipam dhcp dummy firewall flannel host-device host-local ipvlan loopback macvlan portmap ptp sbr static tap tuning vlan vrf
user@k8s-node-1:~$ ls -l /opt/cni/bin/bandwidth /opt/cni/bin/calico
-rwxr-xr-x 1 root root  5042926 Apr 24  2026 /opt/cni/bin/bandwidth
-rwxr-xr-x 1 root root 95727950 Oct 24 13:39 /opt/cni/bin/calico
user@k8s-node-1:~$ ls /etc/cni/net.d
ls: cannot open directory '/etc/cni/net.d': Permission denied
user@k8s-node-1:~$ sudo ls /etc/cni/net.d
10-calico.conflist
calico-kubeconfig
user@k8s-node-1:~$ sudo jq '.plugins[] | {type, ipam, capabilities}' /etc/cni/net.d/10-calico.conflist
{
  "type": "calico",
  "ipam": {
    …
    "type": "calico-ipam"
  },
  "capabilities": null
}
{
  "type": "portmap",
  "ipam": null,
  "capabilities": {
    "portMappings": true
  }
}

Каталог конфигурации закрыт для всех, кроме root (права 700): в calico-kubeconfig лежат учётные данные, с которыми плагин ходит в API (ServiceAccount calico-cni-plugin в calico-system). Цепочка из двух плагинов: calico (интерфейс и адрес через calico-ipam) и portmap (поддержка hostPort, её включает hostPorts: Enabled в Installation). bandwidth в каталоге бинарников есть, но в цепочку не входит, поэтому аннотации пода kubernetes.io/ingress-bandwidth и kubernetes.io/egress-bandwidth, которые он обрабатывает, на стенде ничего не ограничат. Остальные плагины из /opt/cni/bin наша конфигурация не использует. Время изменения выдаёт, кто что положил: у bandwidth — дата сборки пакета, а у calico, calico-ipam, flannel, host-local, loopback, portmap и tuning — момент установки Calico на узел, их записал install-cni.

Что делал плагин при ADD пода net, записано в его журнале:

user@k8s-node-1:~$ sudo grep 1d36abcf9f7f /var/log/calico/cni/cni.log | grep -oE '(Calico CNI IPAM assigned addresses IPv4=\[[^]]*\]|Setting the host side veth name to [a-z0-9]+|Adding IPv4 route \(inside container\)|Disabling IPv4 forwarding|Wrote updated endpoint to datastore)'
Calico CNI IPAM assigned addresses IPv4=[172.20.211.53/26]
Setting the host side veth name to cali4d7612d46be
Adding IPv4 route (inside container)
Disabling IPv4 forwarding
Wrote updated endpoint to datastore

Ровно шаги из схемы в начале: адрес от calico-ipam, veth с именем cali…, маршрут внутри пода, запрет пересылки пакетов внутри пода и запись WorkloadEndpoint. 1d36abcf9f7f — начало идентификатора sandbox пода, который мы сейчас найдём через crictl.

Сетевое пространство имён пода с узла — через crictl и nsenter:

user@k8s-node-1:~$ POD=$(sudo crictl pods --name '^net$' -q)
user@k8s-node-1:~$ sudo crictl inspectp $POD | jq -r '.info.runtimeSpec.linux.namespaces[] | select(.type=="network").path'
/var/run/netns/cni-0d0c14d6-a330-4d39-54cf-8c500df46f20
user@k8s-node-1:~$ sudo nsenter --net=/var/run/netns/cni-0d0c14d6-a330-4d39-54cf-8c500df46f20 ip -br -4 addr
lo               UNKNOWN        127.0.0.1/8
eth0@if173       UP             172.20.211.53/32

Пространство имён держит pause-контейнер sandbox, а контейнер net только входит в него:

user@k8s-node-1:~$ sudo crictl inspectp $POD | jq -r '.info.image, .info.pid'
registry.k8s.io/pause:3.10.2
657193
user@k8s-node-1:~$ sudo crictl inspect $(sudo crictl ps --pod $POD -q) | jq -r .info.pid
657233
user@k8s-node-1:~$ ps -o pid,args -p 657193
    PID COMMAND
 657193 /pause
user@k8s-node-1:~$ ps -o pid,args -p 657233
    PID COMMAND
 657233 sleep 3600
user@k8s-node-1:~$ sudo readlink /proc/657193/ns/net /proc/657233/ns/net
net:[4026532358]
net:[4026532358]

Один и тот же номер пространства имён у /pause и у sleep 3600: перезапуск контейнера net не трогает ни адрес, ни veth, они живут, пока жив sandbox.

IPAM. Утилита Calico calicoctl (ставится отдельным бинарником той же версии, что Calico) показывает состояние IPAM сводкой — calicoctl ipam show --show-blocks, — но всё то же видно и через kubectl, ресурсами Calico:

$ kubectl get blockaffinities
NAME                            CREATED AT
k8s-master-1-172-20-70-192-26   2026-10-24T10:34:56Z
k8s-node-1-172-20-211-0-26      2026-10-24T10:39:23Z
k8s-node-2-172-20-172-0-26      2026-10-24T10:51:40Z
$ kubectl get ipamblock 172-20-211-0-26 -o json | jq -c '.spec | {cidr, affinity, free: (.unallocated|length)}, (.attributes[] | {handle_id, pod: .secondary.pod, type: .secondary.type})'
{"cidr":"172.20.211.0/26","affinity":"host:k8s-node-1","free":60}
{"handle_id":"vxlan-tunnel-addr-k8s-node-1","pod":null,"type":"vxlanTunnelAddress"}
{"handle_id":"k8s-pod-network.9457999e6ebeb782c8e51628afc15ebd3a4cec62d1e67040f6862822c9a5bfb9","pod":"csi-node-driver-q6t25","type":null}
{"handle_id":"k8s-pod-network.167ac68289d80f244a698f115447dc85e2a75fa9e2c11671dd2ee994dba24a2d","pod":"hello-6d5c6db6b4-nqsp8","type":null}
{"handle_id":"k8s-pod-network.1d36abcf9f7fb65581a2a7228afe555a954a9abfff43f909aaa686840325b880","pod":"net","type":null}

Блок k8s-node-1 — 64 адреса, четыре заняты, 60 свободны. Первый адрес каждого блока (172.20.70.192, 172.20.211.0, 172.20.172.0) занят туннельным интерфейсом vxlan.calico своего узла: в пулах с VXLAN Calico берёт адрес для него из того же блока. Идентификатор выделения пода — имя сети из конфигурации CNI (k8s-pod-network) и идентификатор sandbox. Аннотации пода:

$ kubectl -n demo get pod net -o jsonpath='{.metadata.annotations}' | jq
{
  "cni.projectcalico.org/containerID": "1d36abcf9f7fb65581a2a7228afe555a954a9abfff43f909aaa686840325b880",
  "cni.projectcalico.org/podIP": "172.20.211.53/32",
  "cni.projectcalico.org/podIPs": "172.20.211.53/32",
  "kubernetes.io/limit-ranger": "LimitRanger plugin set: cpu, memory request for container net; memory limit for container net"
}

Последнюю аннотацию поставил LimitRange неймспейса demo из части 11, остальные — Calico.

spec.podCIDR узлов, выделенный kube-controller-manager, — для сравнения:

$ kubectl get nodes -o custom-columns=NAME:.metadata.name,PODCIDR:.spec.podCIDR
NAME           PODCIDR
k8s-master-1   172.20.0.0/24
k8s-node-1     172.20.1.0/24
k8s-node-2     172.20.3.0/24

С реальными блоками Calico ничего общего — Calico это поле не читает.

BGP на узле. Сессии BIRD и то, что он знает о блоке k8s-node-2:

user@k8s-node-1:~$ C=$(sudo crictl ps --name calico-node -q)
user@k8s-node-1:~$ sudo crictl exec $C birdcl show protocols
BIRD v0.3.3+birdv1.6.8 ready.
name     proto    table    state  since       info
static1  Static   master   up     2026-10-24
kernel1  Kernel   master   up     2026-10-24
device1  Device   master   up     2026-10-24
direct1  Direct   master   up     2026-10-24
Mesh_192_168_10_17 BGP      master   up     2026-10-24  Established
Mesh_192_168_10_21 BGP      master   up     13:33:41    Established
user@k8s-node-1:~$ sudo crictl exec $C birdcl show route 172.20.172.0/26
BIRD v0.3.3+birdv1.6.8 ready.
172.20.172.0/26    via 192.168.10.21 on ens18 [Mesh_192_168_10_17 2026-10-24 from 192.168.10.17] * (200/0) [i]
                   via 192.168.10.21 on ens18 [Mesh_192_168_10_21 13:33:41] (200/0) [i]
                   via 192.168.10.21 on ens18 [Mesh_192_168_10_21 2026-10-24] (100/0) [i]
                   via 192.168.10.21 on ens18 [kernel1 2026-10-24] (10)
user@k8s-node-1:~$ sudo crictl exec $C grep 'handled by Felix' /etc/calico/confd/config/bird_ipam.cfg
  if (net ~ 172.20.0.0/16) then { reject; } # VXLAN routes are handled by Felix.

Сессии с мастером и k8s-node-2 установлены (Established), и BIRD знает блок соседа и от соседей по BGP (Mesh_…), и из ядра (kernel1 — это маршрут, который прописал Felix). Но в ядро он ничего из пула не отдаёт: фильтр calico_kernel_programming, который confd сгенерировал для пула с VXLAN, отбрасывает все адреса 172.20.0.0/16 с пояснением прямо в комментарии.

И правило natOutgoing. Сначала проверим, что выход наружу маскируется: в tcpdump на ens18 при соединении из пода с портом 443 сервера в локальной сети исходный адрес — адрес узла:

user@k8s-node-1:~$ sudo tcpdump -ni ens18 -c 2 'host 192.168.10.32 and tcp[tcpflags] & tcp-syn != 0'
22:49:46.116936 IP 192.168.10.20.60722 > 192.168.10.32.443: Flags [S], seq 1035344350, win 64860, options [mss 1410,sackOK,TS val 1430924388 ecr 0,nop,wscale 9], length 0
22:49:46.117256 IP 192.168.10.32.443 > 192.168.10.20.60722: Flags [S.], seq 587632908, ack 1035344351, win 65535, options [mss 1460,nop,wscale 8,sackOK,TS val 1277288463 ecr 1430924388], length 0

$ kubectl -n demo exec net -- nc -z -w 2 192.168.10.32 443

nc -z ничего не печатает и завершается с кодом 0 — порт открыт. Заодно видно MTU пода: mss 1410 — это 1450 минус 40 байт заголовков IP и TCP.

Где живёт это правило. В режиме Nftables Felix держит все свои правила в одной таблице ip calico, рядом с таблицами kube-proxy:

user@k8s-node-1:~$ sudo nft list tables | grep -E 'table (ip|arp) '
table ip mangle
table ip filter
table ip nat
table ip kube-proxy
table ip calico
table arp calico-arp

mangle, filter и nat — таблицы слоя совместимости iptables-nft, через который пишет свои правила kubelet (часть 4). calico-arp — маленькая служебная таблица в семействе arp: через неё Felix не даёт узлу отвечать поду по ARP на его собственный адрес. На каждый cali… в ней своя цепочка:

user@k8s-node-1:~$ sudo nft list chain arp calico-arp filter-cali-arp-cali4d7612d46be
table arp calico-arp {
	chain filter-cali-arp-cali4d7612d46be {
		oifname "cali4d7612d46be" arp operation reply arp saddr ip 172.20.211.53 counter packets 0 bytes 0 drop comment "cali:NLnSDII0c5kZJj0t;"
	}
}

«ARP-ответ, уходящий в cali4d7612d46be от имени 172.20.211.53, — отбросить»: иначе proxy ARP ответил бы поду net и на вопрос о его же адресе. Внутри ip calico логика та же, что у Calico в режиме iptables, но вместо четырёх таблиц iptables (raw, mangle, nat, filter) — одна таблица, а имена цепочек получили префикс бывшей таблицы. Базовые цепочки (filter-FORWARD, nat-POSTROUTING и другие) подвешены к хукам netfilter с приоритетами, подобранными так, чтобы правильно чередоваться с цепочками kube-proxy; рабочие называются filter-cali-…, nat-cali-…. Вместо ipset — наборы (sets) nftables в той же таблице. Цепочка маскарадинга и путь к ней:

user@k8s-node-1:~$ sudo nft list chain ip calico nat-POSTROUTING
table ip calico {
	chain nat-POSTROUTING {
		type nat hook postrouting priority srcnat + 10; policy accept;
		counter packets 41613 bytes 2566587 jump nat-cali-POSTROUTING comment "cali:z057H0Z77Fcko47w;"
	}
}
user@k8s-node-1:~$ sudo nft list chain ip calico nat-cali-nat-outgoing
table ip calico {
	chain nat-cali-nat-outgoing {
		ip saddr @cali40masq-ipam-pools ip daddr != @cali40network-ip-pools counter packets 4 bytes 240 masquerade fully-random comment "cali:oMLt0S09xBKlYFS5;"
	}
}
user@k8s-node-1:~$ sudo nft list set ip calico cali40masq-ipam-pools
table ip calico {
	set cali40masq-ipam-pools {
		type ipv4_addr
		flags interval
		elements = { 172.20.0.0/16 }
	}
}

«Источник — в пулах с natOutgoing (cali40masq-ipam-pools), адресат — не в пулах Calico (cali40network-ip-pools, тоже 172.20.0.0/16): маскарадинг». Сюда пакет попадает по цепочке nat-POSTROUTING → nat-cali-POSTROUTING → nat-cali-nat-outgoing; базовая nat-POSTROUTING стоит на хуке postrouting с приоритетом srcnat + 10, то есть чуть позже NAT-цепочки kube-proxy с приоритетом srcnat. Счётчик правила растёт с каждым новым соединением наружу — правило NAT срабатывает только на первый пакет соединения, остальные conntrack обрабатывает сам. Наборы cali40… — то, что в режиме iptables было бы ipset'ами: при изменении пулов Felix меняет элементы набора, не трогая правила. Комментарий cali:… — хеш правила, по которому Felix узнаёт свои правила и замечает, если их кто-то изменил. Что лежит в таблицах kube-proxy — в следующей части.

Убрать за собой: kubectl -n demo delete pod net.


Источники


Дальше — Service и kube-proxy: зачем подам стабильный адрес, как виртуальный ClusterIP, которого нет ни на одном интерфейсе, превращается в DNAT на адрес пода, как EndpointSlice следит за готовыми подами и что kube-proxy в режиме nftables пишет на каждом узле; NodePort, политики трафика, headless-сервисы и LoadBalancer, который без облака висит в Pending.

Оставить комментарий могут только зарегистрированные пользователи.

Войдите на сайт или зарегистрируйтесь, чтобы оставить комментарий.