【eBPF】使用Go运行socket filter程序

254 阅读3分钟

原文:【ebpf】使用Go运行socket filter程序

在学习使用Cilium ebpf的过程中遇到了个问题:socket ebpf程序如何加载到Socket上?

这里对ebpf内核程序代码不做具体讲解,简单来说是通过socket解析以太网帧,再通过以太网帧获取IP数据报,接着再获取TCP,通过TCP获取HTTP请求头。具体源码请参考:GitHub

1. 尝试使用Cilium ebpf函数

在"github.com/cilium/ebpf…"包下有一个函数叫做link.AttachSocketFilter() 这个函数似乎是可以用来加载socket ebpf程序的。

但查看其源码可以发现,第一个参数是syscall.Conn类型的接口。

func AttachSocketFilter(conn syscall.Conn, program *ebpf.Program) error {
	rawConn, err := conn.SyscallConn()
	if err != nil {
		return err
	}
	var ssoErr error
	err = rawConn.Control(func(fd uintptr) {
		ssoErr = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_ATTACH_BPF, program.FD())
	})
	if ssoErr != nil {
		return ssoErr
	}
	return err
}

那不就意味着这个ebpf加载程序必须得写在业务代码中,这不就有点扯了嘛。

因为我想监控的是另一个程序中的http服务。

2. 使用syscall进行绑定

于是参考eunomia中的http案例,需要自己实现一套syscall函数;案例中的方式是创建一个新的socket描述符,然后将网卡绑定到这个socket上。

所以我在Docker中跑了一个HTTP应用

image.png

2.1 绑定网卡

eunomia的案例中,可以看到有一个open_raw_sock 函数,这个函数的作用是将一个网卡绑定到一个socket上,那么我们只需要用Go去实现一下对应的方法即可。

static int open_raw_sock(const char *name)
{
	struct sockaddr_ll sll;
	int sock;

	sock = socket(PF_PACKET, SOCK_RAW | SOCK_NONBLOCK | SOCK_CLOEXEC, htons(ETH_P_ALL));
	if (sock < 0) {
		fprintf(stderr, "Failed to create raw socket\n");
		return -1;
	}

	memset(&sll, 0, sizeof(sll));
	sll.sll_family = AF_PACKET;
	sll.sll_ifindex = if_nametoindex(name);
	sll.sll_protocol = htons(ETH_P_ALL);
	if (bind(sock, (struct sockaddr *)&sll, sizeof(sll)) < 0) {
		fprintf(stderr, "Failed to bind to %s: %s\n", name, strerror(errno));
		close(sock);
		return -1;
	}

	return sock;
}

可以看到案例中open_raw_sock 的参数是一个字符串,查看代码可以知道,这个参数指的是网卡的名称,通过if_nametoindex 函数获取对应网卡的index。

那么在我们自己的项目中,我们只需要将docker容器对应的虚拟网卡进行绑定即可。使用github.com/vishvananda/netlink 包可以很方便得获取当前宿主机上的网卡,通过其Type即可过滤出docker容器的虚拟网卡,并获取到它的index。

	list, _ := netlink.LinkList()
	for _, v := range list {
		if v.Type() != "veth" {
			continue
		}
		fmt.Printf("name: %v, index: %v\n", v.Attrs().Name, v.Attrs().Index)
	}

仿照案例中的代码,再问问GPT,就可以改造出以下的Go代码,其中的Htons 函数可以将小端字节序的数据转换成大端,因为网络是基于大端传输的。

func OpenRawSock(index int) (int, error) {
	sock, err := unix.Socket(unix.AF_PACKET,
		unix.SOCK_RAW|unix.SOCK_NONBLOCK|unix.SOCK_CLOEXEC, int(Htons(unix.ETH_P_ALL)))
	if err != nil {
		return 0, err
	}

	sll := syscall.SockaddrLinklayer{}
	sll.Protocol = Htons(unix.ETH_P_ALL)
	sll.Ifindex = index
	if err := syscall.Bind(sock, &sll); err != nil {
		return 0, err
	}

	return sock, nil
}

3. 加载ebpf程序

接着参考Cilium ebpf中的AttachSocketFilter 函数,将ebpf手动加载到socket上。

obj := bpfObjects{}
if err := loadBpfObjects(&obj, nil); err != nil {
	log.Println("load object error: ", err)
}
defer obj.Close()
	
/**
......
**/
	
if err = unix.SetsockoptInt(sock, unix.SOL_SOCKET, unix.SO_ATTACH_BPF, obj.bpfPrograms.SocketHandler.FD()); err != nil {
	log.Fatalln(err)
	return
}

然后让我们运行一下,向这个docker服务发送个请求试试。可以看到请求已经被正确过滤了出来,程序已经可以正常执行了,但是又有个疑问,既然Cilium ebpf已经封装好了AttachSocketFilter 那我能不能将sock转换成其参数类型syscall.Conn呢?

image.png

image.png

4. 使用AttachSocketFilter()

我翻阅了github上的一些开源项目,参考了一位日本的开发者的案例ebpf-http-monitor-go,其实可以将socket转换成file,因为file本身就是syscall.Conn的实现。

		file := os.NewFile(uintptr(sock), fmt.Sprintf("raw_sock_%v", sock))
		if file == nil {
			log.Fatalf("Failed to create os.File from raw socket")
		}
		
		if err = link.AttachSocketFilter(file, obj.SocketHandler); err != nil {
			log.Fatalln(err)
			return
		}