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 не реализует сеть подов сам, но ставит любой реализации три требования:
- У каждого пода свой IP-адрес, общий для всех его контейнеров. Контейнеры одного пода живут в одном сетевом пространстве имён и видят друг друга через
localhost. - Любой под достаёт до любого другого пода по его адресу без NAT — на том же узле или на другом. Под A видит в пакетах от пода B настоящий адрес B.
- Агенты на узле (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):
- Ядро пода
net: адресат не в своей сети, отправить на шлюз169.254.1.1черезeth0; MAC шлюза —ee:ee:ee:ee:ee:ee. - Кадр выходит из veth на стороне узла — интерфейс
cali4d7612d46be. - Ядро узла: пакет на 172.20.211.43, в таблице маршрутизации —
172.20.211.43 dev calia68edcf6ff3 scope link. Пакет проходит хук forward netfilter — здесь в цепочках nftables Calico проверяются сетевые политики — и уходит вcalia68edcf6ff3. - Под
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-2172.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.
Источники
- Services, Load Balancing, and Networking — сетевая модель Kubernetes: адрес на под, под — под без NAT, агенты узла; кто реализует сеть подов.
- Cluster Networking — какие сетевые задачи решает кластер и как реализуется сеть подов.
- Network Plugins — CNI загружает среда выполнения, поддерживаемые версии спецификации, loopback,
portmapиbandwidthс аннотациями. - CNI Specification — операции, переменные окружения, цепочки плагинов, идемпотентность DEL.
- portmap plugin —
hostPortчерез цепочку CNI. - containerd: CRI Plugin Config —
bin_dirs,conf_dir,max_conf_num. - kube-controller-manager —
--allocate-node-cidrsи--cluster-cidr. - Calico: Component architecture — Felix, BIRD, confd, Typha, CNI- и IPAM-плагины.
- Calico: FAQ — шлюз
169.254.1.1, proxy ARP, MACee:ee:ee:ee:ee:ee,natOutgoing. - Calico: Get started with IP address management — блоки /26, привязка к узлу, адреса вне своего блока.
- Calico: IP pool — поля пула:
blockSize,vxlanMode,ipipMode,natOutgoing,allowedUses. - Calico: Overlay networking — VXLAN и IP-in-IP,
CrossSubnet, маршруты VXLAN от Felix и VXLAN без BGP. - Calico: Configure MTU — накладные расходы инкапсуляций и автоматический выбор MTU.
- Calico: Configure outgoing NAT —
natOutgoingдля выхода подов за пределы кластера. - Calico: Data plane guide: nftables — датаплейн nftables и требование к kube-proxy.
- Calico: Workload endpoint — WorkloadEndpoint и кто управляет его жизненным циклом.
Дальше — Service и kube-proxy: зачем подам стабильный адрес, как виртуальный ClusterIP, которого нет ни на одном интерфейсе, превращается в DNAT на адрес пода, как EndpointSlice следит за готовыми подами и что kube-proxy в режиме nftables пишет на каждом узле; NodePort, политики трафика, headless-сервисы и LoadBalancer, который без облака висит в Pending.
Оставить комментарий могут только зарегистрированные пользователи.
Войдите на сайт или зарегистрируйтесь, чтобы оставить комментарий.