Реализация L2-анонсов в Cilium для обеспечения доступа к Kubernetes-сервисам в bare-metal среде
Полное руководство: L2 анонсы Cilium для сервисов LoadBalancer в bare-metal Kubernetes
Оглавление
- Введение
- Настройка окружения
- Деплой Nginx
- Настройка Ingress
- Настройка LoadBalancer IP Pool
- Проблема с ARP
- Включение L2 анонсов
- Как это работает
- Путь пакета
- Дополнительные материалы
Введение: Зачем нужны L2 анонсы в Kubernetes?
Целевая аудитория
Материал предназначен для:
- Администраторов Kubernetes-кластеров
- Сетевых инженеров, работающих с технологиями Cilium (eBPF, CNI)
- Специалистов по инфраструктуре, знакомых с основами L2/L3 сетей
- Специалистов, интересующихся сетевыми технологиями в Kubernetes
📚 Терминология (рекомендуется к ознакомлению)
Gratuitous ARP - специальный ARP-пакет, рассылаемый узлом при изменении своего MAC-адреса или IP-адреса, чтобы обновить ARP-кеши соседних устройств.
Lease в Kubernetes - механизм аренды ресурсов через API, обеспечивающий эксклюзивный доступ к ресурсу (в данном случае - к IP-адресу) для одного узла кластера.
Bare-metal Kubernetes - кластерная инфраструктура, развернутая на физических серверах без использования облачных провайдеров.
Постановка проблемы
При работе с Kubernetes в bare-metal окружениях часто возникает сложность с доступом к сервисам типа LoadBalancer из внешних сетей. Традиционные решения вроде MetalLB требуют дополнительных компонентов и могут быть избыточны для простых сценариев.
Cilium предоставляет нативное решение для L2-анонсов, обладающее следующими возможностями:
- Отвечать на ARP запросы для IP адресов LoadBalancer
- Обеспечивать высокую доступность через механизм аренды (lease)
- Интегрироваться с существующей сетевой инфраструктурой
В рамках данного материала рассматриваются:
- Как настроить L2 анонсы в Cilium
- Как работает механизм ARP ответов через BPF
- Узнаем полезные мелочи при работе с cilium.
Основная задача исследования - продемонстрировать реализацию доступа к Kubernetes-сервисам из внешних сетей в bare-metal окружении, используя исключительно встроенные механизмы Cilium.
Настройка окружения
Для начала надо настроить окружение, я его описал в README.md
Схема сети
graph TD
subgraph L2
L2["192.168.56.0/24"]
end
subgraph Интернет
Internet["Глобальная сеть"]
end
Jumpbox["Jumpbox
192.168.56.10
MAC: 00:0c:29:e4:f1:a4"] --> L2
Server["Server
192.168.56.20
MAC: 00:0c:29:34:87:0e"] --> L2
Node0["Node-0
192.168.56.50
MAC: 00:0c:29:e3:b1:b2"] --> L2
Node1["Node-1
192.168.56.60
MAC: 00:0c:29:0d:b7:76"] --> L2
L2 --> Internet
- Подсети для подов:
10.200.0.0/24для server10.200.1.0/24для node-010.200.2.0/24для node-1
Конфигурация Kubernetes и Cilium
Виртуалки подняты на mac arm с помощью Vagrant.
jumpbox - 192.168.56.10 - клиент не входящий в кластер kubernetes.
graph TD
subgraph Jumpbox[Jumpbox]
eth0["eth0: 172.16.65.138/24<br>MAC: 00:0c:29:e4:f1:9a"]
eth1["eth1: 192.168.56.10/24<br>MAC: 00:0c:29:e4:f1:a4"]
end
subgraph Routing[Routing Table]
default["Default route: 172.16.65.2 via eth0"]
pod_net["Pod network: 10.0.10.0/24 via eth1"]
end
eth0 -->|External Traffic| default
eth1 -->|Pod Network| pod_net
classDef node fill:#e6f3ff,stroke:#333,stroke-width:2px;
classDef interface fill:#fff,stroke:#666,stroke-width:1px;
class Jumpbox node;
class eth0,eth1 interface;
class Routing fill:#f0f0f0,stroke:#999,dashed;
server - 192.168.56.20 - control plane
graph TD
subgraph Server[Server]
eth0["eth0: 172.16.65.134/24<br>MAC: 00:0c:29:34:87:04"]
eth1["eth1: 192.168.56.20/24<br>MAC: 00:0c:29:34:87:0e"]
cilium_host["cilium_host: 10.200.0.7/32"]
end
subgraph Routing[Routing Table]
default["Default route: 172.16.65.2 via eth0"]
pod_net1["Pod network: 10.200.1.0/24 via 192.168.56.50"]
pod_net2["Pod network: 10.200.2.0/24 via 192.168.56.60"]
end
eth0 -->|External Traffic| default
eth1 -->|Pod Networks| pod_net1
eth1 -->|Pod Networks| pod_net2
classDef node fill:#e6f3ff,stroke:#333,stroke-width:2px;
classDef interface fill:#fff,stroke:#666,stroke-width:1px;
classDef cilium fill:#f0f0f0,stroke:#999,dashed;
class Server node;
class eth0,eth1 interface;
class cilium_host,cilium_net cilium;
class Routing fill:#f0f0f0,stroke:#999,dashed;
node-0 - 192.168.56.50 - нода k8s
graph TD
subgraph Node-0[Node-0]
eth0["eth0: 172.16.65.135/24<br>MAC: 00:0c:29:e3:b1:a8"]
eth1["eth1: 192.168.56.50/24<br>MAC: 00:0c:29:e3:b1:b2"]
cilium_host["cilium_host: 10.200.1.54/32"]
end
subgraph Routing[Routing Table]
default["Default route: 172.16.65.2 via eth0"]
pod_net0["Pod network: 10.200.0.0/24 via 192.168.56.20"]
pod_net2["Pod network: 10.200.2.0/24 via 192.168.56.60"]
end
eth0 -->|External Traffic| default
eth1 -->|Pod Networks| pod_net0
eth1 -->|Pod Networks| pod_net2
classDef node fill:#e6f3ff,stroke:#333,stroke-width:2px;
classDef interface fill:#fff,stroke:#666,stroke-width:1px;
classDef cilium fill:#f0f0f0,stroke:#999,dashed;
class Node-0 node;
class eth0,eth1 interface;
class cilium_host,cilium_net cilium;
class Routing fill:#f0f0f0,stroke:#999,dashed;
node-1 - 192.168.56.60 - нода k8s
graph TD
subgraph Node-1[Node-1]
eth0["eth0: 172.16.65.136/24<br>MAC: 00:0c:29:0d:b7:6c"]
eth1["eth1: 192.168.56.60/24<br>MAC: 00:0c:29:0d:b7:76"]
cilium_host["cilium_host: 10.200.2.17/32"]
end
subgraph Routing[Routing Table]
default["Default route: 172.16.65.2 via eth0"]
pod_net0["Pod network: 10.200.0.0/24 via 192.168.56.20"]
pod_net1["Pod network: 10.200.1.0/24 via 192.168.56.50"]
end
eth0 -->|External Traffic| default
eth1 -->|Pod Networks| pod_net0
eth1 -->|Pod Networks| pod_net1
classDef node fill:#e6f3ff,stroke:#333,stroke-width:2px;
classDef interface fill:#fff,stroke:#666,stroke-width:1px;
classDef cilium fill:#f0f0f0,stroke:#999,dashed;
class Node-1 node;
class eth0,eth1 interface;
class cilium_host,cilium_net cilium;
class Routing fill:#f0f0f0,stroke:#999,dashed;
Cilium с нативным роутингом. Туннели не используются. Версия v1.16.5.
helm upgrade --install cilium cilium/cilium --version 1.16.5 --namespace kube-system \
--set l2announcements.enabled=true \
--set externalIPs.enabled=true \
--set kubeProxyReplacement=true \
--set ipam.mode=kubernetes \
--set k8sServiceHost=192.168.56.20 \
--set k8sServicePort=6443 \
--set operator.replicas=1 \
--set routingMode=native \
--set ipv4NativeRoutingCIDR=10.200.0.0/22 \
--set endpointRoutes.enabled=true \
--set ingressController.enabled=true \
--set ingressController.loadbalancerMode=dedicated
Ядро
# uname -a
Linux node-1 6.1.0-20-arm64 #1 SMP Debian 6.1.85-1 (2024-04-11) aarch64 aarch64 aarch64 GNU/Linux
Деплой Nginx
В этом разделе мы развернем простой веб-сервер на основе Nginx.
Шаг 1. Создание Deployment
# vagrant ssh server
# sudo bash
cat << EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
EOF
cat << EOF | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
name: nginx
namespace: default
spec:
selector:
app: nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
EOF
Шаг 2. Проверка работы
Убедимся что сервис работает
# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx-96b9d695-25swg 1/1 Running 0 55s 10.200.2.189 node-1 <none> <none>
# kubectl get svc nginx
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx ClusterIP 10.96.79.80 <none> 80/TCP 89s
# curl 10.96.79.80
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
# curl 10.200.2.189
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
Настройка Ingress
Применяем манифест ingress
cat << EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: basic-ingress
namespace: default
spec:
ingressClassName: cilium
rules:
- http:
paths:
- backend:
service:
name: nginx
port:
number: 80
path: /
pathType: Prefix
EOF
# kubectl get ingress
NAME CLASS HOSTS ADDRESS PORTS AGE
basic-ingress cilium * 80 40s
Так же будет создан сервис для этого ingress
# kubectl get svc cilium-ingress-basic-ingress
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
cilium-ingress-basic-ingress LoadBalancer 10.96.156.194 <pending> 80:31017/TCP,443:32600/TCP 115s
Как видно из вывода, статус EXTERNAL-IP находится в состоянии pending ввиду отсутствия выделенного пула IP-адресов.
Настройка LoadBalancer IP Pool
Цилиум поддерживает назначение IP адреса на тип сервиса LoadBalancer.
CiliumLoadBalancerIPPool
Создадим CiliumLoadBalancerIPPool
cat << EOF | kubectl apply -f -
apiVersion: "cilium.io/v2alpha1"
kind: CiliumLoadBalancerIPPool
metadata:
name: "blue-pool"
spec:
blocks:
- cidr: "10.0.10.0/24"
EOF
И у нас сразу появится IP у EXTERNAL-IP
# kubectl get svc cilium-ingress-basic-ingress
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
cilium-ingress-basic-ingress LoadBalancer 10.96.156.194 10.0.10.0 80:31017/TCP,443:32600/TCP 4m9s
И он сразу работает. (правда только с нод где запущен cilium)
# curl 10.0.10.0
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
Проблема с ARP
Зайдем в другой консоли на наш клиент jumpbox и убедимся что сайт открывается.
# vagrant ssh jumpbox
root@jumpbox:/home/vagrant# curl 10.0.10.0
curl: (7) Failed to connect to 10.0.10.0 port 80 after 3074 ms: Couldn't connect to server
Почему так?
Вы, конечно, догадались: на ARP запрос никто не отвечает.
root@server:/home/vagrant# tcpdump -n -i any arp host 10.0.10.0
tcpdump: data link type LINUX_SLL2
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
21:15:05.927064 eth1 B ARP, Request who-has 10.0.10.0 tell 192.168.56.10, length 46
21:15:06.948513 eth1 B ARP, Request who-has 10.0.10.0 tell 192.168.56.10, length 46
21:15:07.973210 eth1 B ARP, Request who-has 10.0.10.0 tell 192.168.56.10, length 46
21:15:08.998950 eth1 B ARP, Request who-has 10.0.10.0 tell 192.168.56.10, length 46
21:15:10.024080 eth1 B ARP, Request who-has 10.0.10.0 tell 192.168.56.10, length 46
21:15:11.050053 eth1 B ARP, Request who-has 10.0.10.0 tell 192.168.56.10, length 46
root@jumpbox:/home/vagrant# arp -n 10.0.10.0
Address HWtype HWaddress Flags Mask Iface
10.0.10.0 (incomplete) eth1
Включение L2 анонсов
Давайте включим ARP анонсы.
graph RL
subgraph L2 Анонсы
A["L2 Policy (Cilium CRD)"] --> B["Lease захват (Kubernetes API)"]
B --> C["BPF Map (ARP обработка)"]
C --> D["ARP Response"]
end
root@server:/home/vagrant# tcpdump -n -i any arp host 10.0.10.0 & # запустили tcpdump в бекграунде, чтобы сразу увидеть что происходит.
[1] 17207
root@server:/home/vagrant# cat << EOF | kubectl apply -f -
apiVersion: "cilium.io/v2alpha1"
kind: CiliumL2AnnouncementPolicy
metadata:
name: policy1
spec:
nodeSelector:
matchExpressions:
- key: node-role.kubernetes.io/control-plane
operator: DoesNotExist
interfaces:
- ^eth[0-9]+
externalIPs: true
loadBalancerIPs: true
EOF
ciliuml2announcementpolicy.cilium.io/policy1 created
21:18:52.093372 eth1 B ARP, Reply 10.0.10.0 is-at 00:0c:29:0d:b7:76, length 46
21:18:52.102795 eth0 B ARP, Reply 10.0.10.0 is-at 00:0c:29:0d:b7:6c, length 46
root@jumpbox:/home/vagrant# tcpdump -n -i any arp
tcpdump: data link type LINUX_SLL2
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
21:18:52.113122 eth1 B ARP, Reply 10.0.10.0 is-at 00:0c:29:0d:b7:76, length 46
21:18:52.113211 eth1 B ARP, Reply 10.0.10.1 is-at 00:0c:29:e3:b1:b2, length 46
21:18:52.122245 eth0 B ARP, Reply 10.0.10.1 is-at 00:0c:29:e3:b1:a8, length 46
21:18:52.122495 eth0 B ARP, Reply 10.0.10.0 is-at 00:0c:29:0d:b7:6c, length 46
root@jumpbox:/home/vagrant# arp -n 10.0.10.0
Address HWtype HWaddress Flags Mask Iface
10.0.10.0 ether 00:0c:29:0d:b7:76 C eth1
Сразу после активации политики наблюдается генерация Gratuitous ARP-пакетов, что является стандартным механизмом Gratuitous ARP, обеспечивающим распространение информации о соответствии IP-MAC адресов в пределах L2-домена.
Итак, у нас есть mac 00:0c:29:0d:b7:76.
Это мак-адрес интерфейса eth1 ноды node-1
root@node-1:/home/vagrant# ip addr sh | grep -1 00:0c:29:0d:b7:76
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 00:0c:29:0d:b7:76 brd ff:ff:ff:ff:ff:ff
altname enp18s0
Так же легко это выяснить поискав какая нода захватила лизу
# kubectl get lease -n kube-system cilium-l2announce-default-cilium-ingress-basic-ingress
NAME HOLDER AGE
cilium-l2announce-default-cilium-ingress-basic-ingress node-1 8m49s
Как это работает
sequenceDiagram
participant Client as Jumpbox
participant Node-0
participant Node-1
participant K8sAPI
Client->>Node-0: ARP Request (Who has 10.0.10.0?)
Node-0->>K8sAPI: Проверка Lease
K8sAPI-->>Node-0: Lease принадлежит Node-1
Node-0->>Client: Silence (не отвечает)
Client->>Node-1: ARP Request
Node-1->>K8sAPI: Проверка Lease
K8sAPI-->>Node-1: Lease подтверждён
Node-1->>Client: ARP Reply
Основную информацию вы, конечно, можете прочитать в документации, я же расскажу чуть-чуть побольше.
Настройка L2 Policy
1. Настраиваем policy включающие l2 анонсы
apiVersion: "cilium.io/v2alpha1"
kind: CiliumL2AnnouncementPolicy
metadata:
name: policy1
spec:
nodeSelector:
matchExpressions:
- key: node-role.kubernetes.io/control-plane
operator: DoesNotExist
interfaces:
- ^eth[0-9]+
externalIPs: true
loadBalancerIPs: true
Lease захват
2. Цилиум захватывает лизу
root@server:/home/vagrant# kubectl get lease -n kube-system | grep l2announce
cilium-l2announce-default-cilium-ingress-basic-ingress node-1 16m
cilium-l2announce-kube-system-cilium-ingress node-0 16m
BPF Map для ARP
3. На ноде создается bpf map, отвечающая за ARP ответы по анонсируемому IP
# kubectl get svc cilium-ingress-basic-ingress
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
cilium-ingress-basic-ingress LoadBalancer 10.96.156.194 10.0.10.0 80:31017/TCP,443:32600/TCP 24h
root@node-1:/home/cilium# bpftool map show pinned /sys/fs/bpf/tc/globals/cilium_l2_responder_v4
72: hash name cilium_l2_respo flags 0x1
key 8B value 8B max_entries 4096 memlock 65536B
btf_id 125
root@node-1:/home/cilium# bpftool map dump pinned /sys/fs/bpf/tc/globals/cilium_l2_responder_v4
[{
"key": {
"ip4": 655370, # IP
"ifindex": 2 # Номер интерфейса в системе.
},
"value": {
"responses_sent": 0
}
},{
"key": {
"ip4": 655370,
"ifindex": 3
},
"value": {
"responses_sent": 3 # сколько ответов было послано на ARP запрос.
}
}
]
pinned - закреплена в файловой системе и сохраняется между рестартами.
ifindex - номера интерфейсов
root@node-1:/home/cilium# ip link show | grep eth
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
// ...
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
Что за число 655370?
import socket
import struct
# Число в little-endian
number = 655370
# Преобразуем число в IP-адрес
# < — указывает на little-endian.
# I — беззнаковое 32-битное целое число.
ip = socket.inet_ntoa(struct.pack('<I', number))
print(ip)
10.0.10.0 - искомый IP адрес LB.
Сетевой путь ARP-пакета можно представить следующей схемой (для случая отсутствия записи в ARP-кэше):
graph RL
J["jumpbox"] -->|"Broadcast ARP Request<br>'Who has 10.0.10.0?'"| L["L2 domain (все узлы)"]
L -->|Receive| N["node-1"]
N -->|"Unicast ARP Reply<br>'10.0.10.0 is-at 00:0c:29:0d:b7:76'"| J
jumpbox - посылает широковещательный запрос с вопросом кто отвечает за 10.0.10.0.
все сервера l2 домена - получают этот запрос.
node-1 - отвечает на этот запрос с мак адресом интерфейса, который принял ARP запрос.
Путь пакета
Рассмотрим механизм обработки ARP-запросов на node-1:
Предположим что запрос пришел на eth1.
На этом интерфейсе подцеплены bpf программы (у нас же цилиум, елки-палки!).
tc filter show dev eth1 ingress
filter protocol all pref 1 bpf chain 0
filter protocol all pref 1 bpf chain 0 handle 0x1 cil_from_netdev-eth1 direct-action not_in_hw id 3640 tag e1f4a3d35ae9c0f0 jited
И входящий трафик обрабатывает программа cil_from_netdev-eth1.
Чуть больше информации можно посмотреть так
root@node-1:/home/cilium# bpftool prog show id 3640
3640: sched_cls name cil_from_netdev tag e1f4a3d35ae9c0f0 gpl
loaded_at 2025-01-19T18:24:42+0000 uid 0
xlated 3856B jited 3192B memlock 4096B map_ids 55,548,17,72,54
btf_id 3385
direct-action - bpf программа сама примет решение о том что делать с принятым пакетом.
В цилиуме эта bpf программа аттачится к интерфейсу тут
// reloadHostDatapath (re)attaches programs from bpf_host.c to:
// - cilium_host: cil_to_host ingress and cil_from_host to egress
// - cilium_net: cil_to_host to ingress
// - native devices: cil_from_netdev to ingress and (optionally) cil_to_netdev to egress if certain features require it
func (l *loader) reloadHostDatapath(ep datapath.Endpoint, spec *ebpf.CollectionSpec, devices []string) error {
// Replace programs on cilium_host.
// ...
// Attach cil_from_netdev to ingress.
if err := attachSKBProgram(iface, coll.Programs[symbolFromHostNetdevEp], symbolFromHostNetdevEp,
linkDir, netlink.HANDLE_MIN_INGRESS, option.Config.EnableTCX); err != nil {
return fmt.Errorf("interface %s ingress: %w", device, err)
}
Саму программу cil_from_netdev можно найти тут
/*
* from-netdev is attached as a tc ingress filter to one or more physical devices
* managed by Cilium (e.g., eth0). This program is only attached when:
* - the host firewall is enabled, or
* - BPF NodePort is enabled, or
* - L2 announcements are enabled, or
* - WireGuard's host-to-host encryption and BPF NodePort are enabled
*/
__section_entry
int cil_from_netdev(struct __ctx_buff *ctx)
// ...
return handle_netdev(ctx, false);
Запрос передается в handle_netdev и уходит в do_netdev.
/**
* handle_netdev
* @ctx The packet context for this program
* @from_host True if the packet is from the local host
*
* Handle netdev traffic coming towards the Cilium-managed network.
*/
static __always_inline int
handle_netdev(struct __ctx_buff *ctx, const bool from_host)
// ...
return do_netdev(ctx, proto, from_host);
do_netdev обрабатывает ARP
do_netdev(struct __ctx_buff *ctx, __u16 proto, const bool from_host)
// ...
# if defined ENABLE_ARP_PASSTHROUGH || defined ENABLE_ARP_RESPONDER || \
defined ENABLE_L2_ANNOUNCEMENTS
case bpf_htons(ETH_P_ARP):
#ifdef ENABLE_L2_ANNOUNCEMENTS
ret = handle_l2_announcement(ctx);
#else
ret = CTX_ACT_OK;
#endif
break;
# endif
// ...
Заметьте, если анонсы не включены - то пакет просто пропустится (CTX_ACT_OK) и не будет обработан, иначе будет вызвана функция handle_l2_announcement.
#ifdef ENABLE_L2_ANNOUNCEMENTS
static __always_inline int handle_l2_announcement(struct __ctx_buff *ctx)
// ...
В которой будет:
- Проверено что
братцилиум-агент жив (опцияagent-liveness-update-interval)
// ...
__u32 index = RUNTIME_CONFIG_AGENT_LIVENESS;
__u64 *time;
time = map_lookup_elem(&CONFIG_MAP, &index);
if (!time)
return CTX_ACT_OK;
/* If the agent is not active for X seconds, we can't trust the contents
* of the responder map anymore. So stop responding, assuming other nodes
* will take over for a node without an active agent.
*/
if (ktime_get_ns() - (*time) > L2_ANNOUNCEMENTS_MAX_LIVENESS)
return CTX_ACT_OK;
// ...
Как вы думаете где хранится значение time?
Конечно! В еще одной bpf мапе!
root@node-1:/home/cilium# cilium-dbg map get cilium_runtime_config | head -3
Key Value State Error
UTimeOffset 3393110285156250
AgentLiveness 53302401940736 # monolithic time https://docs.redhat.com/en/documentation/red_hat_enterprise_linux_for_real_time/7/html/reference_guide/sect-posix_clocks
- Что пакет реально ARP пакет
if (!arp_validate(ctx, &mac, &smac, &sip, &tip))
return CTX_ACT_OK;
- Проверено что мы вообще должны отвечать на этот
arp(помните про лизу?)
key.ip4 = tip;
key.ifindex = ctx->ingress_ifindex;
stats = map_lookup_elem(&L2_RESPONDER_MAP4, &key);
if (!stats)
return CTX_ACT_OK;
Вот эта мапка!
root@node-1:/home/cilium# bpftool map dump pinned /sys/fs/bpf/tc/globals/cilium_l2_responder_v4
[{
"key": {
"ip4": 655370,
"ifindex": 2
},
"value": {
"responses_sent": 0
}
},{
"key": {
"ip4": 655370,
"ifindex": 3
},
"value": {
"responses_sent": 0
}
}
]
А на node-0 будет в это время так:
root@node-0:/home/cilium# bpftool map dump pinned /sys/fs/bpf/tc/globals/cilium_l2_responder_v4
[]
- Вызовется
arp_respond
ret = arp_respond(ctx, &mac, tip, &smac, sip, 0);
arp_respond вызовет arp_prepare_response и отправит ctx_redirect пакет на интерфейс.
static __always_inline int
arp_respond(struct __ctx_buff *ctx, union macaddr *smac, __be32 sip,
union macaddr *dmac, __be32 tip, int direction)
{
int ret = arp_prepare_response(ctx, smac, sip, dmac, tip);
if (unlikely(ret != 0)) // ну типа ошибка маловероятна
goto error;
cilium_dbg_capture(ctx, DBG_CAPTURE_DELIVERY,
ctx_get_ifindex(ctx));
return ctx_redirect(ctx, ctx_get_ifindex(ctx), direction);
error:
return send_drop_notify_error(ctx, UNKNOWN_ID, ret, CTX_ACT_DROP, METRIC_EGRESS);
}
ctx_redirect в итоге вызовет функцию bpf_redirect о которой хорошо написано тут
Важным аспектом является параметр direction = 0, определяющий направление перенаправления пакета (ingress/egress). Согласно документации bpf_redirect:
Except for XDP, both ingress and egress interfaces can be used
for redirection. The **BPF_F_INGRESS** value in *flags* is used
to make the distinction (ingress path is selected if the flag
is present, egress path otherwise).
Значение BPF_F_INGRESS можно подсмотреть в заголовках bpf:
root@node-1:/home/cilium# grep -Rw BPF_F_INGRESS /var/lib/cilium/bpf/include/linux/bpf.h
# ..
BPF_F_INGRESS = (1ULL << 0),
Что по сути является числом 1, а у нас 0, то есть мы отправили пакет на egress (на выход с eth1).
Итого, путь пакета на node-1
+-------------------+
| Внешний хост |
| (jumpbox) |
+--------+----------+
| ARP запрос "Who has 10.0.10.0?"
v
+-------------------+
| Интерфейс eth1 |
| node-1 |
+--------+----------+
| Пакет попадает в TC ingress
v
+-------------------+
| BPF программа |
| cil_from_netdev |
+--------+----------+
| Обработка в handle_netdev
v
+-------------------+
| do_netdev() |
+--------+----------+
| Проверка типа пакета (ARP)
v
+-------------------+
| handle_l2_announcement() |
+--------+----------+
| Проверки:
| 1. Жив ли Cilium агент
| 2. Это ARP пакет
| 3. Есть ли запись в L2_RESPONDER_MAP4
v
+-------------------+
| arp_respond() |
+--------+----------+
| Подготовка ARP ответа
v
+-------------------+
| ctx_redirect() |
+--------+----------+
| Перенаправление на egress (direction=0)
v
+-------------------+
| Интерфейс eth1 |
| node-1 |
+--------+----------+
| ARP ответ "10.0.10.0 is-at 00:0c:29:0d:b7:76"
v
+-------------------+
| Внешний хост |
| (jumpbox) |
+-------------------+
Дополнительные материалы
Различия между cilium l2 анонсами и proxy_arp
Вопрос: Каковы отличия между L2-анонсами Cilium и активацией proxy_arp на узлах кластера?
Ответ: При использовании proxy_arp обработка ARP-запросов осуществляется на уровне ядра ОС, что сохраняет функциональность, но лишает преимуществ распределенного управления через Kubernetes API.
Отключили ARP анонсы.
root@server:/home/vagrant# kubectl delete -f workshop/l2.yaml
ciliuml2announcementpolicy.cilium.io "policy1" deleted
ARP перестал отвечать на jumphost
root@jumpbox:/home/vagrant# arping 10.0.10.0
ARPING 10.0.10.0
60 bytes from 00:0c:29:e3:b1:a8 (10.0.10.0): index=0 time=257.448 usec
60 bytes from 00:0c:29:e3:b1:a8 (10.0.10.0): index=1 time=377.882 usec
Timeout
Timeout
Timeout
Включили arp_proxy на node-1
root@node-1:/home/vagrant# sysctl -w net.ipv4.conf.eth1.proxy_arp=1
net.ipv4.conf.eth1.proxy_arp = 1
Все заработало
root@jumpbox:/home/vagrant# arping -I eth1 10.0.10.0
ARPING 10.0.10.0
Timeout
Timeout
Timeout
Timeout
60 bytes from 00:0c:29:0d:b7:76 (10.0.10.0): index=0 time=449.655 msec
60 bytes from 00:0c:29:0d:b7:76 (10.0.10.0): index=1 time=792.268 msec
60 bytes from 00:0c:29:0d:b7:76 (10.0.10.0): index=2 time=152.025 msec
root@jumpbox:/home/vagrant# curl 10.0.10.0
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
// ...
Чтобы убедиться что отвечает само ядро можно подцепиться к функции arp_process в линукс ядре
/*
* Process an arp request.
*/
static int arp_process(struct net *net, struct sock *sk, struct sk_buff *skb)
// ...
Напишем bpf программу
root@node-1:/home/vagrant# bpftrace -e '
kprobe:arp_process
{
printf("ARP process called (pid=%d)\n", pid);
printf(" net: %lx\n", arg0);
printf(" sock: %lx\n", arg1);
printf(" sk_buff: %lx\n", arg2);
// Доступ к данным из sk_buff
$skb = (struct sk_buff *)arg2;
printf(" skb->len: %d\n", $skb->len);
printf(" skb->data: %lx\n", $skb->data);
// Выводим первые 64 байт данных
printf(" data: ");
$data = (uint8*)$skb->data;
unroll(64) {
printf("%02x ", *$data);
$data++;
}
printf("\n");
}
'
Пингуем с jumphost - 192.168.56.10
root@jumpbox:/home/vagrant# arping -c 1 -I eth1 10.0.10.0
ARPING 10.0.10.0
60 bytes from 00:0c:29:0d:b7:76 (10.0.10.0): index=0 time=276.063 msec
Видим что ядро обрабатывает пакет
ARP process called (pid=0)
net: ffff80000a19ff00
sock: 0
sk_buff: ffff000006981000
skb->len: 46
skb->data: ffff00003b42e04e
data: 00 01 08 00 06 04 00 01 00 0c 29 e4 f1 a4 c0 a8 38 0a 00 00 00 00 00 00 0a 00 0a 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Давайте расшифруем что получилось.
1 Заголовок ARP (хорошее описание):
• 00 01 — Hardware type (Ethernet).
• 08 00 — Protocol type (IPv4).
• 06 — MAC address length (6 байт).
• 04 — IP address length (4 байта).
• 00 01 — Operation (ARP request).
2 Данные ARP:
• 00 0c 29 e4 f1 a4 — Sender MAC: 00:0c:29:e4:f1:a4.
• c0 a8 38 0a — Sender IP: 192.168.56.10.
• 00 00 00 00 00 00 — Target MAC: 00:00:00:00:00:00 (запрашивается).
• 0a 00 0a 00 — Target IP: 10.0.10.0.
Это ARP запрос от устройства с MAC 00:0c:29:e4:f1:a4 и IP 192.168.56.10, которое ищет устройство с IP 10.0.10.0. В ответе ARP должно быть указано, какой MAC адрес соответствует IP 10.0.10.0.
А вот ответ (arp_send_dst)
bpftrace -e '
kprobe:arp_send_dst
{
printf("ARP send called (pid=%d)\n", pid);
printf(" type: %d\n", arg0); // Тип ARP пакета (1 - Request, 2 - Reply)
printf(" ptype: %d\n", arg1); // Тип протокола (2054 для ARP)
printf(" dest_ip: %d.%d.%d.%d\n", (arg2 >> 0) & 0xff, (arg2 >> 8) & 0xff, (arg2 >> 16) & 0xff, (arg2 >> 24) & 0xff);
printf(" src_ip: %d.%d.%d.%d\n", (arg4 >> 0) & 0xff, (arg4 >> 8) & 0xff, (arg4 >> 16) & 0xff, (arg4 >> 24) & 0xff);
printf(" dest_hw: %02x:%02x:%02x:%02x:%02x:%02x\n", *(uint8*)(arg5 + 0), *(uint8*)(arg5 + 1), *(uint8*)(arg5 + 2), *(uint8*)(arg5 + 3),
*(uint8*)(arg5 + 4), *(uint8*)(arg5 + 5)); // MAC dst
printf(" src_hw: %02x:%02x:%02x:%02x:%02x:%02x\n", *(uint8*)(arg6 + 0), *(uint8*)(arg6 + 1), *(uint8*)(arg6 + 2), *(uint8*)(arg6 + 3),
*(uint8*)(arg6 + 4), *(uint8*)(arg6 + 5)); // MAC src
printf(" target_hw: %02x:%02x:%02x:%02x:%02x:%02x\n", *(uint8*)(arg7 + 0), *(uint8*)(arg7 + 1), *(uint8*)(arg7 + 2), *(uint8*)(arg7 + 3),
*(uint8*)(arg7 + 4), *(uint8*)(arg7 + 5)); // MAC target
}
'
ARP send called (pid=0)
type: 2 // Тип ARP пакета: 2 (ARP Reply, ответ)
ptype: 2054 // Тип протокола: 2054 (0x0806, ARP)
dest_ip: 192.168.56.10 // IP назначения (устройство, которое запросило ARP, например, jumpbox)
src_ip: 10.0.10.0 // IP отправителя (устройство, отправляющее ARP Reply, например, node-1)
dest_hw: 00:0c:29:e4:f1:a4 // MAC назначения (устройство, которое запросило ARP, например, jumpbox)
src_hw: 00:0c:29:0d:b7:76 // MAC отправителя (устройство, отправляющее ARP Reply, например, node-1)
target_hw: 00:0c:29:e4:f1:a4 // MAC цели (обычно совпадает с dest_hw для ARP Reply)
Активация proxy_arp на всех узлах приведет к дублированию ARP-ответов и потенциальным конфликтам маршрутизации.
Особенности работы с кэшированными и некэшированными BPF-картами
Важно учитывать особенности работы с инструментами диагностики при анализе BPF-карт:
root@node-1:/home/cilium# cilium-dbg map list
Name Num entries Num errors Cache enabled
cilium_policy_00215 3 0 true
# ...
cilium_l2_responder_v4 0 0 false
В cilium_l2_responder_v4 0 записей как будто.
Но это не правда.
root@node-1:/home/cilium# bpftool map dump pinned /sys/fs/bpf/tc/globals/cilium_l2_responder_v4
[{
"key": {
"ip4": 655370,
"ifindex": 2
},
"value": {
"responses_sent": 0
}
},{
"key": {
"ip4": 655370,
"ifindex": 3
},
"value": {
"responses_sent": 40
}
}
]
Карта cilium_l2_responder_v4 работает в режиме прямого доступа (Cache enabled: false), что требует использования низкоуровневых инструментов вроде bpftool для получения актуальных данных.
Заключение: Преимущества использования L2-анонсов в Cilium
В данном исследовании была продемонстрирована реализация L2-анонсов в Cilium для обеспечения доступа к сервисам типа LoadBalancer в bare-metal Kubernetes-кластере. Рассмотрены механизмы обработки ARP-запросов с использованием BPF-программ, реализация отказоустойчивости через систему lease-захвата и особенности диагностики работы системы.
Ключевые преимущества подхода:
- Нативная интеграция с Kubernetes API
- Высокая производительность за счет обработки пакетов на уровне eBPF
- Автоматическое распределение нагрузки между узлами кластера
- Отсутствие зависимости от внешних компонентов балансировки нагрузки
- Поддержка стандартных сетевых протоколов L2-уровня
Решение особенно актуально для гибридных и on-premise сред, где требуется сохранить гибкость bare-metal инфраструктуры, обеспечивая при этом облачно-ориентированную модель доступа к сервисам.
Представленные материалы позволяют сделать вывод о эффективности использования встроенных механизмов Cilium для организации сетевой инфраструктуры Kubernetes-кластеров, обеспечивающей соответствие современным требованиям к отказоустойчивости и производительности.
Больше информации о Kubernetes и Cilium вы найдете в Telegram-канале: https://t.me/azalio_tech