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

Не так давно я столкнулся с довольно необычной задачей настройки маршрутизации для MetalLB. Всё бы ничего, ведь MetalLB обычно не требует какой-либо дополнительной настройки со стороны пользователя, но в нашем случае есть довольно большой кластер с весьма простой конфигурацией сети.
В этой статье я покажу, как настроить маршрутизацию на основе источника и на основе политик для внешней сети в вашем кластере.
Я не буду подробно останавливаться на установке и настройке MetalLB, так как предполагаю, что у вас уже есть некоторый опыт. Давайте разберёмся в сути и настроим маршрутизацию. Итак, у нас есть четыре случая:
Случай 1: Когда ничего настраивать не нужно
Давайте рассмотрим простой случай.

Дополнительная настройка маршрутизации не требуется, когда адреса, выдаваемые MetalLB, находятся в той же подсети, что и адреса, настроенные на ваших узлах.
Например, у вас есть подсеть 192.168.1.0/24, в ней есть маршрутизатор 192.168.1.1, а ваши узлы имеют адреса 192.168.1.10–30, тогда вы можете настроить диапазон 192.168.1.100–120 для MetalLB и быть уверены, что он будет работать без какой-либо дополнительной настройки.
Почему так? Потому что на ваших узлах уже настроены маршруты:
# 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: Когда нужна дополнительная настройка

Вам нужно настроить дополнительные маршруты всякий раз, когда на ваших узлах нет настроенного IP-адреса или маршрута для подсети, в которой MetalLB выдаёт адреса.
Объясню подробнее. Всякий раз, когда MetalLB выдаёт адрес, это можно сравнить с простым назначением вроде:
ip addr add 10.9.8.7/32 dev lo
Обратите внимание на:
- a) Адрес назначается с префиксом
/32, поэтому маршрут для этой подсети не будет добавлен автоматически (это просто IP-адрес) - 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: Когда нужна маршрутизация на основе источника
Маршрутизацию на основе источника нужно настраивать, когда вы получаете пакеты через отдельный шлюз, а не через тот, что настроен по умолчанию; соответственно, ответные пакеты должны идти через тот же шлюз.
Например, у вас та же подсеть 192.168.1.0/24, выделенная для ваших узлов, но вы хотите выдавать внешние адреса с помощью MetalLB. Предположим, у вас есть несколько адресов из подсети 1.2.3.0/24, расположенной в VLAN 100, и вы хотите обращаться к сервисам Kubernetes извне, используя их.

При обращении к 1.2.3.4 вы будете делать запросы из другой подсети, вне 1.2.3.0/24, и ожидать ответа. Узел, который в данный момент содержит адрес 1.2.3.4, назначенный MetalLB, будет получать пакеты от маршрутизатора 1.2.3.1, но ответы на них должны идти по тому же маршруту, через 1.2.3.1.
Поскольку на нашем узле уже настроен шлюз по умолчанию 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; она будет содержать только два маршрута, необходимых для отправки ответного пакета через интерфейс eth0.100 и шлюз 1.2.3.1.
Теперь нам нужно добавить простое правило:
ip rule add from 1.2.3.0/24 lookup 100
которое явно указывает: если адрес источника пакета находится в 1.2.3.0/24, то использовать таблицу маршрутизации 100. Мы уже добавили маршрут, который отправит его через шлюз 1.2.3.1.
Случай 4: Когда нужна маршрутизация на основе политик
Топология сети та же, что и в предыдущем примере, но допустим, что вы также хотите иметь возможность обращаться к внешним адресам диапазона 1.2.3.0/24 изнутри ваших подов:

Особенность в том, что при обращении к любому адресу в 1.2.3.0/24 ответный пакет, приходящий на узел и имеющий адрес источника в диапазоне 1.2.3.0/24, будет послушно отправлен через eth0.100, но мы хотим, чтобы Kubernetes перенаправил его обратно к нашему первому поду, который сгенерировал исходный запрос.
Решить эту задачу было непросто, но это стало возможным благодаря маршрутизации на основе политик.
Начнём с того же, что и в предыдущем случае: создадим дополнительную таблицу маршрутизации и добавим в неё необходимые маршруты:
ip route add 1.2.3.0/24 dev eth0.100 table 100
ip route add default via 1.2.3.1 table 100
Метод, предложенный George Shuklin
После публикации этой статьи мне предложили более простой и элегантный метод решения этой задачи: для этого нужно всего лишь добавить два правила:
ip rule add from 1.2.3.0/24 lookup 100
ip rule add from 1.2.3.0/24 to 10.112.0.0/12 lookup main
Где:
1.2.3.0/24— это внешняя сеть10.112.0.0/12— это ваша podNetwork
Второе правило должно быть добавлено после первого, чтобы получить наивысший приоритет.
Метод с маркировкой соединений
Для лучшего понимания приведу здесь блок-схему netfilter:

Теперь добавим несколько правил iptables:
iptables -t mangle -A PREROUTING -j CONNMARK --restore-mark
iptables -t mangle -A PREROUTING -m mark ! --mark 0 -j RETURN
iptables -t mangle -A PREROUTING -i bond0.100 -j MARK --set-mark 0x100
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.