使用 eBPF 和 Golang 实现透明网桥
实现透明网桥的主要思路是在内核空间使用 eBPF 程序对网络数据包进行拦截和修改,从而实现对数据包转发的控制。下面是一个基于 eBPF 的简单透明网桥实现,使用 Golang 编写。
- 安装依赖项
首先需要安装 libbpf 和 bpftool,这两个工具是 eBPF 程序编写和调试的必备工具。
sudo apt-get install libbpf-dev
sudo apt-get install linux-tools-common linux-tools-generic linux-tools-`uname -r`
- 编写 eBPF 程序
eBPF 程序的主要作用是拦截和修改网络数据包,下面是一个简单的 eBPF 程序,它将接收到的数据包转发到指定的端口。
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/in.h>
#define ETH_ALEN 6
struct bpf_map_def SEC("maps") port_map = {
.type = BPF_MAP_TYPE_HASH,
.key_size = sizeof(u32),
.value_size = sizeof(u32),
.max_entries = 256,
};
SEC("prog")
int forward(struct xdp_md *ctx) {
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
struct ethhdr *eth = data;
u32 *port = bpf_map_lookup_elem(&port_map, ð->h_dest[ETH_ALEN-2]);
if (!port) {
return XDP_DROP;
}
bpf_clone_redirect(ctx, *port, 0);
return XDP_PASS;
}
上述 eBPF 程序使用了一个哈希表(port_map)来存储 MAC 地址和端口号的映射关系,当收到一个数据包时,首先从哈希表中查找目的 MAC 地址对应的端口号,如果找到则将数据包转发到该端口,否则丢弃该数据包。
- 加载 eBPF 程序
在 Golang 中使用 libbpf 库加载 eBPF 程序,代码如下:
package main
/*
#cgo CFLAGS: -I/usr/include/bpf
#cgo LDFLAGS: -lelf -lz -lbpf
#include <linux/bpf.h>
#include <bpf/libbpf.h>
#include <bpf/bpf.h>
struct bpf_object *load_bpf_program(char *filename) {
struct bpf_object *obj;
int err;
err = bpf_prog_load(filename, BPF_PROG_TYPE_XDP, &obj, &err);
if (err) {
return NULL;
}
return obj;
}
*/
import "C"
import (
"fmt"
"syscall"
"unsafe"
)
func main() {
// 加载 eBPF 程序
obj := C.load_bpf_program(C.CString("./forward.o"))
if obj == nil {
fmt.Println("Failed to load eBPF program")
return
}
defer C.bpf_object__close(obj)
// 获取 XDP 程序 fd
prog := C.bpf_object__find_program_by_name(obj, C.CString("forward"))
if prog == nil {
fmt.Println("Failed to find XDP program")
return
}
fd := C.bpf_program__fd(prog)
if fd < 0 {
fmt.Println("Failed to get XDP program fd")
return
}
// 配置网络接口
iface := "eth0"
err := setInterfaceXdpFd(iface, int(fd))
if err != nil {
fmt.Println("Failed to set XDP program for interface", iface, err)
return
}
fmt.Println("eBPF program loaded and attached to interface", iface)
}
func setInterfaceXdpFd(iface string, fd int) error {
sockFd, err := syscall.Socket(syscall.AF_XDP, syscall.SOCK_RAW, 0)
if err != nil {
return err
}
defer syscall.Close(sockFd)
ifIndex, err := getInterfaceIndex(iface)
if err != nil {
return err
}
xdpFlags := syscall.XDP_FLAGS_UPDATE_IF_NOEXIST
err = syscall.SetsockoptInt(sockFd, syscall.SOL_XDP, syscall.XDP_ATTACH, ifIndex)
if err != nil {
return err
}
err = syscall.SetsockoptInt(sockFd, syscall.SOL_XDP, syscall.XDP_FLAGS, xdpFlags)
if err != nil {
return err
}
err = syscall.SetsockoptInt(sockFd, syscall.SOL_XDP, syscall.XDP_FD, fd)
if err != nil {
return err
}
return nil
}
func getInterfaceIndex(iface string) (int, error) {
ifReq := syscall.Ifreq{}
copy(ifReq.IfrnName[:], iface)
sockFd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, syscall.IPPROTO_IP)
if err != nil {
return 0, err
}
defer syscall.Close(sockFd)
err = syscall.Ioctl(sockFd, syscall.SIOCGIFINDEX, uintptr(unsafe.Pointer(&ifReq)))
if err != nil {
return 0, err
}
return int(ifReq.IfruIndex), nil
}
上述代码首先使用 Cgo 调用 libbpf 库中的函数加载 eBPF 程序,然后使用系统调用将 eBPF 程序绑定到指定的网络接口上。
- 测试透明网桥
在测试透明网桥时,需要将两个网络接口(比如 eth0 和 eth1)连接起来,并将它们的 IP 地址、MAC 地址等参数配置成相同的值。这样可以模拟出一个简单的局域网环境。
测试代码如下:
package main
import (
"fmt"
"net"
)
func main() {
conn, err := net.Dial("udp", "192.168.1.100:8888")
if err != nil {
fmt.Println("Failed to dial UDP server", err)
return
}
defer conn.Close()
data := []byte("Hello, world")
_, err = conn.Write(data)
if err != nil {
fmt.Println("Failed to send UDP packet", err)
return
}
fmt.Println("UDP packet sent successfully")
}
上述代码向 192.168.1.100:8888 发送一个 UDP 数据包,由于该 IP 地址不在本机的子网范围内,因此数据包需要通过透明网桥转发到另一个网络接口上。如果一切正常,就应该能够收到该数据包。
完整代码见下:
原文地址: https://www.cveoy.top/t/topic/luQi 著作权归作者所有。请勿转载和采集!