Контекст

Типичный кластер платформы представляет собой ванильную инсталляцию Кубернетеса с рабочими нодами на Azure Linux и Windows Server. Сетевой уровень представлен Azure CNI v1 для обеспечения сетевой связности между подами и аллокации IP адреса и Azure NPM для управления сетевыми политиками кластера (идентично аналогам от Calico и Cilium).

graph TB
    subgraph Node["Kubernetes Node"]
        kubelet
        kube-proxy
        azure-npm
        azure-cni
    end

    kubelet --> ???
    kube-proxy --> ???
    azure-npm --> ???

Симптомы

В начале мы наблюдали CrashLoopBackOff статус на части Azure NPM подов в некоторых кластерах, в логах есть сообщения об ошибках инициализации плагина.

Эти ошибки дают нам идею, в каком направлении нужно продолжать траблшутинг.

error: There was an error running command: [iptables-nft -w 60 -L KUBE-KUBELET-CANARY -t mangle -n] Stderr: [exit status 1, # Warning: iptables-legacy tables present, use iptables-legacy to see them
iptables: No chain/target/match by that name.]
executing iptables command [iptables-legacy[] with args [-w 60 -L KUBE-KUBELET-CANARY -t mangle -n]
error: There was an error running command: [iptables-legacy -w 60 -L KUBE-IPTABLES-HINT -t mangle -n] Stderr: [exit status 1, iptables: No chain/target/match by that name.]
error: There was an error running command: [iptables-legacy -w 60 -L KUBE-KUBELET-CANARY -t mangle -n] Stderr: [exit status 1, iptables: No chain/target/match by that name.]

# и после нескольких попыток плагин сдается и завершается с ошибкой
failed to detect iptables version: unable to locate which iptables version kube proxy is using

На гитхабе можно быстро найти кусок кода, который ответственен за логику инициализации. Ниже приведена основная суть:

246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
// detectIptablesVersion sets the global iptables variable to nft if detected or legacy if detected.
// NPM will crash if it fails to detect either.
// This global variable is referenced in all iptables related functions.
// NPM should use the same iptables version as kube-proxy.
// kube-proxy creates an iptables chain as a hint for which version it uses.
// For more details, see: https://kubernetes.io/blog/2022/09/07/iptables-chains-not-api/#use-case-iptables-mode
func (pMgr *PolicyManager) detectIptablesVersion() error {
	klog.Info("first attempt detecting iptables version. looking for hint/canary chain in iptables-nft")
	if pMgr.hintOrCanaryChainExist(util.IptablesNft) {
		util.SetIptablesToNft()
		return nil
	}

	klog.Info("second attempt detecting iptables version. looking for hint/canary chain in iptables-legacy")
	if pMgr.hintOrCanaryChainExist(util.IptablesLegacy) {
		util.SetIptablesToLegacy()
		return nil
	}

	return errDetectingIptablesVersion
}

source on GitHub ↗

Как кублет работает с iptables

В логах Azure NPM упомянуты две цепочки iptables: KUBE-IPTABLES-HINT and KUBE-KUBELET-CANARY. Давайте разберемся, как они появляются на ноде. Согласно документации, обе создаются кублетом в какой-то момент.

На эту тему написан очень подробный KEP-3178: Cleaning up IPTables Chain Ownership, который углубляется в историю, детали и будущие изменения всех цепочек, которыми управляет кублет.

KUBE-MARK-MASQ и KUBE-POSTROUTING

KUBE-MARK-MASQ помечает пакеты для маскарадинга.

KUBE-POSTROUTING проверяет наличие отметки на пакетах и выполняет -j MASQUERADE, на пакетах отмеченных для маскарадинга. Эти цепочки раньше использовались для имплементации HostPort режима в dockershim, но после миграции больше не используются кублетом.

Куб-прокси (в режиме iptables или ipvs) создает копии этих цепочек для реализации логики управления объектами типа Service.

KUBE-MARK-DROP и KUBE-FIREWALL

KUBE-MARK-DROP отмечает пакеты, которые должны быть отброшены.

KUBE-FIREWALL проверяет наличие отметки на пакетах и выполняет -j DROP, на пакетах отмеченных для дропа. Эти цепочки всегда создавались кублетом, но использовались только куб-прокси.

KUBE-KUBELET-CANARY

KUBE-KUBELET-CANARY - это цепочка, которая используется utiliptables.Monitor для определения ситуации, когда iptables правила, созданные кублетом, были удалены или изменены. Если монитор обнаруживает, что эта цепочка была удалена, он считает, что все цепочки, которыми управляет кублет, были удалены и запускает процесс восстановления.

80
81
82
83
84
85
86
87
88
89
90
	// Monitor detects when the given iptables tables have been flushed by an external
	// tool (e.g. a firewall reload) by creating canary chains and polling to see if
	// they have been deleted. (Specifically, it polls tables[0] every interval until
	// the canary has been deleted from there, then waits a short additional time for
	// the canaries to be deleted from the remaining tables as well. You can optimize
	// the polling by listing a relatively empty table in tables[0]). When a flush is
	// detected, this calls the reloadFunc so the caller can reload their own iptables
	// rules. If it is unable to create the canary chains (either initially or after
	// a reload) it will log an error and stop monitoring.
	// (This function should be called from a goroutine.)
	Monitor(canary Chain, tables []Table, reloadFunc func(), interval time.Duration, stopCh <-chan struct{})

source on GitHub ↗

KUBE-IPTABLES-HINT

KUBE-IPTABLES-HINT — это цепочка, которая используется как подсказка для определения версии iptables, которую использует куб-прокси.

RCA

Теперь самое время выяснить, почему кублет не смог создать нужные правила.

В логах при старте кублета мы видим следующее информационное сообщение:

I1008 05:38:38.192213    2825 kubelet_network_linux.go:58] "Failed to initialize iptables rules; some functionality may be missing." protocol="IPv4"
iptables v1.8.10 (nf_tables): Chain 'KUBE-FIREWALL' does not exist
Try `iptables -h' or 'iptables --help' for more information.

Инициализация происходит при старте кублета и выполняется однократно (one-shot). Если она падает, кублет логирует ошибку и продолжает работать с ограниченной функциональностью — цепочки так и не создаются. Monitor умеет восстанавливать удаленные цепочки позже, но только если самая первая инициализация прошла успешно.

38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
func (kl *Kubelet) initNetworkUtil(logger klog.Logger) {
	iptClients := utiliptables.NewBestEffort()
	if len(iptClients) == 0 {
		// We don't log this as an error because kubelet itself doesn't need any
		// of this (it sets up these rules for the benefit of *other* components),
		// and because we *expect* this to fail on hosts where only nftables is
		// supported (in which case there can't be any other components using
		// iptables that would need these rules anyway).
		logger.Info("No iptables support on this system; not creating the KUBE-IPTABLES-HINT chain")
		return
	}

	for family := range iptClients {
		iptClient := iptClients[family]
		if kl.syncIPTablesRules(logger, iptClient) {
			logger.Info("Initialized iptables rules.", "protocol", iptClient.Protocol())
			go iptClient.Monitor(
				utiliptables.Chain("KUBE-KUBELET-CANARY"),
				[]utiliptables.Table{utiliptables.TableMangle, utiliptables.TableNAT, utiliptables.TableFilter},
				func() { kl.syncIPTablesRules(logger, iptClient) },
				1*time.Minute, wait.NeverStop,
			)
		} else {
			logger.Info("Failed to initialize iptables rules; some functionality may be missing.", "protocol", iptClient.Protocol())
		}
	}
}

source on GitHub ↗

Самое непонятное — что изначально вызвало ошибку инициализации? Все как в меме:

scooby doo reveal mask meme

Из-за редкого race condition, systemctl restart iptables сбросил все пользовательские цепочки (включая KUBE-FIREWALL) в тот момент, когда кублет пытался инициализировать свои. Так как в коде инициализации нет ретраев, эти цепочки так и не были созданы, и функциональность, которая от них зависит, сломалась.

git blame

Чиним

git show <fix_commit>

+iptables_save() {
+  info "Saving iptables"
   iptables-save > /etc/systemd/scripts/ip4save
   ip6tables-save > /etc/systemd/scripts/ip6save
-  systemctl restart iptables
 }

Почему необязательно перезапускать iptables после применения изменений?

Операция iptables-save сохраняет текущее состояние правил в файл на диске. Этот снепшот позволяет восстановить правила после перезагрузки системы.

С другой стороны, systemctl restart iptables запускает процесс перезапуска, который вызывает iptables -F для очистки всех правил и цепочек, потом iptables -X для удаления всех пользовательских цепочек, и только после этого выполняет iptables-restore для загрузки правил из сохраненного файла.

Выводы

  • Инициализация iptables в кублете — это одноразовая операция (one-shot). Если она падает при старте, цепочки так и не создаются. После успешной инициализации Monitor кублета умеет восстанавливать потерянные цепочки — но только если самая первая инициализация прошла успешно.
  • systemctl restart iptables — деструктивная операция. Stop-скрипт выполняет iptables -F && iptables -X, удаляя все пользовательские цепочки. Перезапуск не атомарен — между остановкой и запуском есть реальный промежуток времени, когда цепочек не существует.
  • Цепочки iptables всё ещё не API. Некоторые части Кубернетеса построены на очень хрупких зависимостях. Это хороший пример того, как простая функциональность может стать критически важной зависимостью для всего сетевого стека в кластере.