如果覺得文章對你有所啟發,可以考慮用 🌟 支持 Gthulhu 專案,短期目標是集齊 300 個 🌟 藉此被 CNCF Landscape 採納 [ref]。
在上一篇文章中,我們學習了 XDP 的基本概念和封包過濾技術。今天我們將進行一個稍具挑戰性的實戰專案:使用 XDP 技術打造一個完整的 Tiny Load Balancer。
該專案受 eBPF Summit 2021: An eBPF Load Balancer from scratch 啟發,並且修改自 lb-from-scratch 專案。
這個專案將帶領我們深入學習:
通過這個實戰專案,您將對 eBPF 在網路程式設計中的強大能力有更深刻的理解。
┌─────────────┐ ┌─────────────────┐ ┌─────────────┐
│ Client │───▶│ Load Balancer │───▶│ Backend A │
│192.17.0.4 │ │ 192.17.0.5 │ │192.17.0.2 │
└─────────────┘ │ │ └─────────────┘
│ XDP Hook │
│ │ ┌─────────────┐
└─────────────────┘───▶│ Backend B │
│192.17.0.3 │
└─────────────┘
我們的 TinyLB 工作原理:
建立 xdp_lb_kern.c
:
#include "xdp_lb_kern.h"
#define IP_ADDRESS(x) (unsigned int)(192 + (17 << 8) + (0 << 16) + (x << 24))
#define BACKEND_A 2
#define BACKEND_B 3
#define CLIENT 4
#define LB 5
#define HTTP_PORT 80
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__type(key, __u32);
__type(value, __u32);
__uint(max_entries, 64);
} lb_map SEC(".maps");
SEC("xdp")
int tiny_lb(struct xdp_md *ctx)
{
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
struct ethhdr *eth = data;
if (data + sizeof(struct ethhdr) > data_end)
return XDP_ABORTED;
if (bpf_ntohs(eth->h_proto) != ETH_P_IP)
return XDP_PASS;
struct iphdr *iph = data + sizeof(struct ethhdr);
if (data + sizeof(struct ethhdr) + sizeof(struct iphdr) > data_end)
return XDP_ABORTED;
if (iph->protocol != IPPROTO_TCP)
return XDP_PASS;
struct tcphdr *tcph = data + sizeof(struct ethhdr) + sizeof(struct iphdr);
if (data + sizeof(struct ethhdr) + sizeof(struct iphdr) + sizeof(struct tcphdr) > data_end)
return XDP_ABORTED;
__be32 ip_saddr = iph->saddr;
if (ip_saddr == IP_ADDRESS(CLIENT) &&
bpf_ntohs(tcph->dest) == HTTP_PORT)
{
bpf_printk("Got http request from %x", ip_saddr);
int dst = BACKEND_A;
if (bpf_ktime_get_ns() % 2)
dst = BACKEND_B;
__u32 *dst_ip = bpf_map_lookup_elem(&lb_map, &dst);
if (!dst_ip) {
bpf_printk("Error: Destination IP not found in the map");
return XDP_PASS;
}
iph->daddr = bpf_htonl(*dst_ip);
eth->h_dest[5] = dst;
} else if (iph->saddr == IP_ADDRESS(BACKEND_A) || iph->saddr == IP_ADDRESS(BACKEND_B))
{
bpf_printk("Got the http response from backend [%x]: forward to client %x", iph->saddr, iph->daddr);
int c = CLIENT;
__u32 *client_ip = bpf_map_lookup_elem(&lb_map, &c);
if (!client_ip) {
bpf_printk("Error: Client IP not found in the map");
return XDP_PASS;
}
iph->daddr = bpf_htonl(*client_ip);
eth->h_dest[5] = CLIENT;
}
iph->saddr = IP_ADDRESS(LB);
eth->h_source[5] = LB;
iph->check = iph_csum(iph);
bpf_printk("Forward from lb %x to client %x", iph->saddr, iph->daddr);
return XDP_TX;
}
char _license[] SEC("license") = "GPL";
參考上方的程式碼,XDP type program 允許接受 pointer to struct xdp_md 作為 program context。這個 context 本身就是 L2 以上的資料內容,所以我們可以做型別轉換來取得每一層的資料的起始位置:
SEC("xdp")
int tiny_lb(struct xdp_md *ctx)
{
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
struct ethhdr *eth = data;
if (data + sizeof(struct ethhdr) > data_end)
return XDP_ABORTED;
if (bpf_ntohs(eth->h_proto) != ETH_P_IP)
return XDP_PASS;
struct iphdr *iph = data + sizeof(struct ethhdr);
if (data + sizeof(struct ethhdr) + sizeof(struct iphdr) > data_end)
return XDP_ABORTED;
if (iph->protocol != IPPROTO_TCP)
return XDP_PASS;
struct tcphdr *tcph = data + sizeof(struct ethhdr) + sizeof(struct iphdr);
if (data + sizeof(struct ethhdr) + sizeof(struct iphdr) + sizeof(struct tcphdr) > data_end)
return XDP_ABORTED;
藉由這個方式,我們就能將不屬於 TCP 的網路封包過濾掉囉。
讓我們繼續向下追蹤,會發現範例程式使用 eBPF Maps 來管理 Load Balancer 後面 Backend Service 的 entity:
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__type(key, __u32);
__type(value, __u32);
__uint(max_entries, 64);
} lb_map SEC(".maps");
稍後會在 Golang 撰寫的管理程式看到操作 MAP 的程式碼。
若 Dst Port 為 80
且 Src IP 來自給定的 Client,那麼就根據當前的時間 %2 決定要將請求轉發到哪一個後端伺服器中:
if (ip_saddr == IP_ADDRESS(CLIENT) &&
bpf_ntohs(tcph->dest) == HTTP_PORT)
{
bpf_printk("Got http request from %x", ip_saddr);
int dst = BACKEND_A;
if (bpf_ktime_get_ns() % 2)
dst = BACKEND_B;
__u32 *dst_ip = bpf_map_lookup_elem(&lb_map, &dst);
if (!dst_ip) {
bpf_printk("Error: Destination IP not found in the map");
return XDP_PASS;
}
iph->daddr = bpf_htonl(*dst_ip);
eth->h_dest[5] = dst;
}
反之,若 Src IP 來自後端伺服器,我們就將請求的 Dst IP 設定為 Client IP:
else if (iph->saddr == IP_ADDRESS(BACKEND_A) || iph->saddr == IP_ADDRESS(BACKEND_B))
{
bpf_printk("Got the http response from backend [%x]: forward to client %x", iph->saddr, iph->daddr);
int c = CLIENT;
__u32 *client_ip = bpf_map_lookup_elem(&lb_map, &c);
if (!client_ip) {
bpf_printk("Error: Client IP not found in the map");
return XDP_PASS;
}
iph->daddr = bpf_htonl(*client_ip);
eth->h_dest[5] = CLIENT;
}
建立 main.go
:
package main
import (
"os"
"time"
"unsafe"
bpf "github.com/aquasecurity/libbpfgo"
)
func IP_ADDRESS(d, c, b, a int) uint32 {
return uint32(a + b<<8 + c<<16 + d<<24)
}
func main() {
bpfModule, err := bpf.NewModuleFromFile("xdp_lb_kern.o")
if err != nil {
panic(err)
}
defer bpfModule.Close()
if err := bpfModule.BPFLoadObject(); err != nil {
panic(err)
}
prog, err := bpfModule.GetProgram("tiny_lb")
if err != nil {
panic(err)
}
// TODO: support xdpgeneric
// link, err := prog.AttachXDP("eth0")
// if err != nil {
// panic(err)
// }
// if link.FileDescriptor() == 0 {
// os.Exit(-1)
// }
prog_map, err := bpfModule.GetMap("lb_map")
if err != nil {
panic(err)
} else {
clientIP := IP_ADDRESS(192, 17, 0, 4)
backendAIP := IP_ADDRESS(192, 17, 0, 2)
backendBIP := IP_ADDRESS(192, 17, 0, 3)
lbIP := IP_ADDRESS(192, 17, 0, 5)
c := uint32(4)
a := uint32(2)
b := uint32(3)
lb := uint32(5)
prog_map.Update(unsafe.Pointer(&c), unsafe.Pointer(&clientIP))
prog_map.Update(unsafe.Pointer(&a), unsafe.Pointer(&backendAIP))
prog_map.Update(unsafe.Pointer(&b), unsafe.Pointer(&backendBIP))
prog_map.Update(unsafe.Pointer(&lb), unsafe.Pointer(&lbIP))
}
prog, err = bpfModule.GetProgram("capture_skb")
if err != nil {
panic(err)
}
link, err := prog.AttachGeneric()
if err != nil {
panic(err)
}
if link.FileDescriptor() == 0 {
os.Exit(-1)
}
for {
time.Sleep(10 * time.Second)
}
}
由於 Client、Backend A 與 Backend B 的 IP 在上面的範例中都是固定的,為了精簡測試環境的搭建,我們使用 docker-compose 部署測試必要的服務:
建立 docker-compose.yml
:
version: '3.8'
services:
client:
container_name: client
image: ubuntu:20.04
command: bash -c "apt-get update && apt-get install -y curl iputils-ping && sleep infinity"
privileged: true
cap_add:
- NET_ADMIN
networks:
lbnet:
ipv4_address: 192.17.0.4
depends_on:
- lb
lb:
container_name: lb
image: ianchen0119/lb:latest
command: bash -c "make run"
privileged: true
cap_add:
- NET_ADMIN
volumes:
- ./xdp_lb_user:/ianchen/xdp_lb_user
- ./Makefile:/ianchen/Makefile
- ./xdp_lb_kern.h:/ianchen/xdp_lb_kern.h
- ./xdp_lb_kern.c:/ianchen/xdp_lb_kern.c
- ./vmlinux:/ianchen/vmlinux
- ./main:/ianchen/main
networks:
lbnet:
ipv4_address: 192.17.0.5
backend-a:
container_name: backend-a
image: nginxdemos/hello:plain-text
privileged: true
cap_add:
- NET_ADMIN
networks:
lbnet:
ipv4_address: 192.17.0.2
depends_on:
- lb
backend-b:
container_name: backend-b
image: nginxdemos/hello:plain-text
privileged: true
cap_add:
- NET_ADMIN
networks:
lbnet:
ipv4_address: 192.17.0.3
depends_on:
- lb
networks:
lbnet:
ipam:
driver: default
config:
- subnet: 192.17.0.0/24
driver_opts:
com.docker.network.bridge.name: br-lb
volumes:
dbdata:
請參考 tinyLB README,內含詳細的說明。
這次的範例比起先前筆者發表於 StarBugs 的文章有些許的差異:
在本篇文章中,我們實作了一個完整的 Tiny Load Balancer:
這個實戰專案展示了 XDP 在實際網路應用中的強大能力,為後續學習更複雜的 eBPF 應用打下堅實基礎。
筆者的話:
Coding Agent 產出文章的品質還是有待加強,所以今日的內容我有手動修改一番。
XDP 雖然能高速、巨量的處理網路包,但完全早於 kernel 的網路堆疊也會造成一些問題:
- 若有 NAT 功能的需求,必須自行實作,否則因改用 TC type 的 eBPF。當然,這也會損失一些效能。
- XDP 與 TC 都沒辦法處理 IP Fragment 的問題,如果有這部分的需求得使用其他方法。
總而言之,XDP 非常適合打造 FireWall、LoadBalancer 這類網路功能,但如果有更複雜的封包處理要求,仍需要放棄 XDP 的吞吐量轉而使用更適合的 eBPF program type。