使用 BPF 自定义安全组
上一篇文章阅读量比较多,看起来网络的主题比较受欢迎。这一篇文章我们继续探索 BPF
在网络领域的应用:使用 BPF
来实现安全组。
按照腾讯云的文档,安全组的概念如下:
安全组是一种虚拟防火墙,具备有状态的数据包过滤功能,用于设置云服务器、负载均衡、云数据库等实例的网络访问控制,控制实例级别的出入流量,是重要的网络安全隔离手段。
在本文中,我们将实现如下的规则:
0.0.0.0/0:10216 ---> TCP:12160
0.0.0.0/0:* -x-> TCP:12160
即允许所有源端口为 10216
的 TCP
流量访问服务端 12160
端口,通过其他端口访问的流量都丢弃(本文仅讨论入站流量的过滤,当然,对出站流量的限制也是可行的)。
XDP 与 BPF
XDP
是 eXpress 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 实现自定义安全组,通过可编程的方式实现了对入站流量的访问控制。