原文:【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应用
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呢?
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
}