Тонкая настройка маршрутизации для MetalLB в режиме L2

Тонкая настройка маршрутизации для MetalLB в режиме L2
Не так давно я столкнулся с весьма нестандартной задачей настройки маршрутищации для MetalLB. Всё бы ничего, т.к. обычно для MetalLB не требуется никаких дополнительных действий, но в нашем случае имеется достаточно большой кластер с весьма нехитрой конфигурацией сети.

В данной статье я расскажу как настроить source-based и policy-based routing для внешней сети вашего кластера.

Я не буду подробно останавливаться на установке и настройке MetalLB, так как предполагаю вы уже имеете некоторый опыт. Предлагаю сразу перейти к делу, а именно к настройке маршрутизации. Итак мы имеем четыре кейса:

Случай 1: Когда настройка не требуется

Разберём простой кейс.

Тонкая настройка маршрутизации для MetalLB в режиме L2

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

Например вы имеете подсеть 192.168.1.0/24, в ней имеется маршрутизатор 192.168.1.1, и ваши ноды получают адреса: 192.168.1.10-30, тогда для MetalLB вы можете настроить рейндж 192.168.1.100-120 и быть уверенным что они будут работать без какой либо дополнительной настройки.

Почему так? Потому что ваши ноды уже имеют настроенные маршруты:

# ip route
default via 192.168.1.1 dev eth0 onlink 
192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.10

И адреса из того же рейджа будут переиспользовать их без каких-либо дополнительных телодвижений.

Случай 2: Когда требуется дополнительная настройка

Тонкая настройка маршрутизации для MetalLB в режиме L2

Вам следует настроить дополнительные маршруты всякий раз тогда, когда ваши ноды не имеют настроенного IP-адреса или маршрута в подсеть для которой MetalLB выдаёт адреса.

Объясню чуть подробней. Всякий раз когда MetalLB выдёт адрес, это можно сравнить с простым назначением вида:

ip addr add 10.9.8.7/32 dev lo

Обратите внимание, на:

  • a) Адрес назначается с префиксом /32 то есть маршрут в подсеть для него автоматически не добавится (это просто адрес)
  • b) Адрес вешается на любой интерфейс ноды (например loopback). Здесь стоит упомянуть, об особенности сетевого стека Linux. Не важно на какой интерфейс вы добавите адрес, ядро всегда будет обрабатывать arp-запросы и отправлять arp-ответы на любой из них, это поведение считается коректным и, кроме того, достаточно широко используется в такой динамической среде как Kubernetes.

Данное поведение можно настраивать, например включив strict arp:

echo 1 > /proc/sys/net/ipv4/conf/all/arp_ignore
echo 2 > /proc/sys/net/ipv4/conf/all/arp_announce

В этом случае arp-ответы будут отправляться только в том случае, если интерфейс явно содержит конкретный IP-адрес. Данная настройка является обязательной в случае если вы планируете использовать MetalLB и ваш kube-proxy работает в режиме IPVS.

Тем не менее MetalLB не использует ядро для обработки arp-запросов, а делает это самостоятельно в user-space, таким образом данная опция не повлияет на работу MetalLB.

Вернёмся к нашей задаче. Если маршрута для выдаваемых адресов на ваших нодах не существует, добавьте его заранее на все ноды:

ip route add 10.9.8.0/24 dev eth1

Случай 3: Когда понадобится source-based routing

Source-based routing вам потребуется настроить тогда, когда вы получаете пакеты через отдельный gateway, не тот что настроен у вас по умолчанию, соответственно ответные пакеты также должы уходить через этот же gateway.

Например, у вас есть всё таже подсеть 192.168.1.0/24 выделенная для ваших нод, но вы хотите выдавать внешние адреса с помощью MetalLB. Предположим, у вас есть несколько адресов из подсети 1.2.3.0/24 находящихся во VLAN 100, и вы хотите использовать их для доступа Kubernetes-служб извне.

Тонкая настройка маршрутизации для MetalLB в режиме L2

При обращении на 1.2.3.4 вы будете совершать запросы из другой подсети нежели 1.2.3.0/24 и ожидать ответ. Нода, которая в данный момент является мастером для выданного MetalLB адреса 1.2.3.4, получит пакет от маршрутизатора 1.2.3.1, но ответ для него обязательно должен уйти тем же маршрутом, через 1.2.3.1.

Так как наша нода уже имеет настроенный default gateway 192.168.1.1, то по умолчанию ответ пойдёт к нему, а не к 1.2.3.1, через который мы получили пакет.

Как же справиться с данной ситуацией?

В этом случае вам необходимо подготовить все ваши ноды таким образом, чтобы они были готовы обслуживать внешние адреса без дополнительной настройки. То есть для вышеприведённого примера вам нужно заранее создать VLAN-интерфейс на ноде:

ip link add link eth0 name eth0.100 type vlan id 100
ip link set eth0.100 up

А затем добавить маршруты:

ip route add 1.2.3.0/24 dev eth0.100 table 100
ip route add default via 1.2.3.1 table 100

Обратите внимание маршруты мы добавляем в отдельную табицу маршрутизации 100 она будет содержать только два маршрута необходимых для отправки ответного пакета через гейтвей 1.2.3.1, находящийся за интерфейсом eth0.100.

Теперь нам нужно добавить простое правило:

ip rule add from 1.2.3.0/24 lookup 100

которое явно говорит: если адрес источника пакета находится в 1.2.3.0/24, то нужно использовать табицу маршрутизации 100. В ней у нас уже описан маршрут который отправит его через 1.2.3.1

Случай 4: Когда понадобится policy-based routing

Топология сети как и в предыдущем примере, но допустим вы хотите также иметь возиможность обращаться к внешним адресам пула 1.2.3.0/24 из ваших подов:

Тонкая настройка маршрутизации для MetalLB в режиме L2

Особенность заключается в том что при обращении к любому адресу в 1.2.3.0/24, ответный пакет попадая на ноду и имея адрес источника в диапазоне 1.2.3.0/24 будет послушно отправлен в eth0.100, но мы-то хотим чтобы Kubernetes перенаправил его в наш первый под, который и сгенерировал изначальный запрос.

Решить данную проблему оказалось непросто, но это стало возможным благодаря policy-based routing:

Для большего понимания процесса приведу блок схему netfilter:
Тонкая настройка маршрутизации для MetalLB в режиме L2

Для начала, как и в предыдущем примере, создадим дополнительную таблицу маршрутизации:

ip route add 1.2.3.0/24 dev eth0.100 table 100
ip route add default via 1.2.3.1 table 100

Теперь добавим несколько правил в iptables:

iptables -t mangle -A PREROUTING -i eth0.100 -j CONNMARK --set-mark 0x100
iptables -t mangle -A PREROUTING  -j CONNMARK --restore-mark
iptables -t mangle -A PREROUTING -m mark ! --mark 0 -j RETURN
iptables -t mangle -A POSTROUTING -j CONNMARK --save-mark

Эти правила будут маркировать входящие подключения на интерфейс eth0.100, помечая все пакеты тэгом 0x100, этим же тэгом будут помечены и ответы в рамках одного подключения.

Теперь мы можем добавить правило роутинга:

ip rule add from 1.2.3.0/24 fwmark 0x100 lookup 100

То есть все пакеты с адресом источника 1.2.3.0/24 и тэгом 0x100 должны быть смаршрутизированны используя таблицу 100.

Таким образом другие пакеты, полученные на другой интерфейс, под это правило не попадают, что позволит им быть смаршрутизированными стандартными средствами Kubernetes.

Есть ещё одно но, в Linux существует так называемый reverse path filter, который портит всю малину осуществляет простую проверку: для всех входящих пакетов он меняет адрес источника пакета с адресом отправителя и проверяет может-ли пакет уйти через тот-же интерфейс на который был получен, если нет, то отфильтровывет его.

Проблема в том что в нашем случае он будет работать некорректно, но мы можем отключить его:

echo 0 > /proc/sys/net/ipv4/conf/all/rp_filter
echo 0 > /proc/sys/net/ipv4/conf/eth0.100/rp_filter

Обратите внимание, первая команда котролирует глобальное поведение rp_filter, если его не отключить, то вторая команда не будет иметь никакого эфекта. Тем не менее остальные интерфейсы останутся со включённым rp_filter.

Чтобы не ограничивать работу фильтра полностью мы можем воспользоваться реализацией rp_filter для netfilter. Используя rpfilter в качестве модуля iptables можно настроить достаточно гибкие правила, например:

iptables -t raw -A PREROUTING -i eth0.100 -d 1.2.3.0/24 -j RETURN
iptables -t raw -A PREROUTING -i eth0.100 -m rpfilter --invert -j DROP

включат rp_filter на интерфейсе eth0.100 для всех адресов кроме 1.2.3.0/24.

Источник: habr.com