使用 BPF 自定义安全组

上一篇文章阅读量比较多,看起来网络的主题比较受欢迎。这一篇文章我们继续探索 BPF 在网络领域的应用:使用 BPF 来实现安全组。

按照腾讯云的文档,安全组的概念如下:

安全组是一种虚拟防火墙,具备有状态的数据包过滤功能,用于设置云服务器、负载均衡、云数据库等实例的网络访问控制,控制实例级别的出入流量,是重要的网络安全隔离手段。

在本文中,我们将实现如下的规则:

0.0.0.0/0:10216 ---> TCP:12160
0.0.0.0/0:*     -x-> TCP:12160

即允许所有源端口为 10216TCP 流量访问服务端 12160 端口,通过其他端口访问的流量都丢弃(本文仅讨论入站流量的过滤,当然,对出站流量的限制也是可行的)。

XDP 与 BPF

XDPeXpress Data Path 的缩写,在 Linux 内核中为 BPF 提供了一个框架,用于实现高性能的可编程的数据包处理。它在整个软件栈的起始点,即网络驱动程序收到以太网帧的时刻运行 BPF 程序。

回到本文的主题,我们通过把安全组规则翻译成 BPF 程序,利用 XDP 挂载至网卡设备上执行,即可达成目标。

实现上述功能的 BPF 程序如下:

#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/in.h>
#include <linux/ip.h>
#include <linux/tcp.h>

#include "bpf_helpers.h"
#include "bpf_endian.h"

static int is_secure_source(void *data_begin, void *data_end)
{
	struct ethhdr *eth_header = data_begin;

	if ((void *)(eth_header + 1) > data_end) {
		return 1;
	}

	if (eth_header->h_proto != bpf_htons(ETH_P_IP)) {
		return 1;
	}

	struct iphdr *ip_header = (struct iphdr *)(eth_header + 1);

	if ((void *)(ip_header + 1) > data_end) {
		return 1;
	}

	if (ip_header->protocol != IPPROTO_TCP) {
		return 1;
	}

	struct tcphdr *tcp_header = (struct tcphdr *)(ip_header + 1);

	if ((void *)(tcp_header + 1) > data_end) {
		return 1;
	}

	if (tcp_header->dest == bpf_htons(12160)) {
		if (tcp_header->source != bpf_htons(10216)) {
			return 0;	// reject
		} else {
			return 1;	// accept
		}
	} else {
		return 1;
	}
}

SEC("xdp")
int xdp_secure_policy(struct xdp_md *ctx)
{
	void *data = (void *)(__u64)ctx->data;
	void *data_end = (void *)(__u64)ctx->data_end;
	if (is_secure_source(data, data_end)) {
		return XDP_PASS;
	} else {
		return XDP_DROP;
	}
}

char __license[] SEC("license") = "GPL";

程序的功能是,对于网卡收到的每一个数据,依次跳过合法的以太网帧首部,IP数据报首部,最后查看 TCP 报文首部的目的端口是否是 12160,若是,则进一步判断源端口是否是 10216,以此决定是否允许入站流量。在整个程序中,对于指针边界的判断是必需的,若缺失,会导致程序不能通过内核 BPF 验证器的校验。(在程序中,我们对不认识/不完整的数据都予以放过)最后编译为二进制文件 sg.bpf.o

另外我们还需要一个简单的服务端程序来验证功能:

package main

import (
	"io"
	"log"
	"net"
)

func serve(c net.Conn) {
	defer c.Close()

	log.Printf("client connected: %s\\n", c.RemoteAddr().String())
	io.Copy(c, c)
	log.Printf("client closed: %s\\n", c.RemoteAddr().String())
}

func main() {
	lis, err := net.Listen("tcp", ":12160")
	if err != nil {
		panic(err)
	}

	for {
		conn, err := lis.Accept()
		if err != nil {
			panic(err)
		}
		go serve(conn)
	}
}

程序的功能很简单,监听在 12160 端口,对于每一个连接上的客户端, echo 客户端的输入。最后编译为可执行程序 testserver

实验

在加载 BPF 程序之前,我们先运行测试用的服务端程序,

$ ./testserver

然后从另一台主机上连接到这个服务上,在两个不同的终端,分别执行如下命令:

$ nc $(SERVER_IP) 12160 -p 10216
hello
hello
$ nc $(SERVER_IP) 12160
hi
hi

可以看到,两个客户端都能正常访问服务端,现在,我们加载上述的 BPF 程序:

$ sudo ip link set dev eth0 xdpgeneric obj sg.bpf.o sec xdp verbose

即把 BPF 程序加载到 eth0 网卡上(这里操作模式选择了 xdpgeneric,因为实验环境的虚拟机不支持 xdpdrv/xdpoffload )。

现在,再次尝试在两个终端发送数据到服务端:

$ nc $(SERVER_IP) 12160 -p 10216
hello
hello
hey
hey
$ nc $(SERVER_IP) 12160
hi
hi
no reply

表现符合预期。源端口为 10216 的客户端仍然能将数据发送给服务端并接收响应,其他客户端则一直在重传,直至服务端重置连接。

本文的代码可以在这里找到。

结论

本文探讨了使用 XDP 和 BPF 实现自定义安全组,通过可编程的方式实现了对入站流量的访问控制。

本站文章资源均来源自网络,除非特别声明,否则均不代表站方观点,并仅供查阅,不作为任何参考依据!
如有侵权请及时跟我们联系,本站将及时删除!
如遇版权问题,请查看 本站版权声明
THE END
分享
二维码
海报
使用 BPF 自定义安全组
上一篇文章阅读量比较多,看起来网络的主题比较受欢迎。这一篇文章我们继续探索 BPF 在网络领域的应用:使用 BPF 来实现安全组。
<<上一篇
下一篇>>