BPF、XDP、数据包过滤器和UDP

2020-10-22 08:54:47

想象一下,您正在运行Docker容器的内容分发网络。您可以使用未经修改的任意应用程序,并让它们在世界各地靠近其用户的服务器上运行,然后将这些服务器与WireGuard结合在一起。如果你愿意,可以想象一下,内容交付网络有一个容易打字的名字,可能就像fly.io;,如果你真的想要实现这个白日梦,人们可以在大约2分钟内注册这项服务,不到5分钟就能在全球部署一个Docker容器。这就是我想说的远大梦想。

这很容易让你的头脑了解这将如何在网络应用程序中发挥作用。您的Worker服务器为您的客户应用程序运行Firecracker实例;您的边缘服务器通告任播地址,并运行代理服务器将请求路由到相应的Worker。这里隐藏了很多细节,但设计很简单,因为Web应用程序是要被代理的;几乎每个大规模部署的Web应用程序都在某种代理之后运行。

除了在TCP上运行之外,HTTP还是代理友好的,因为它的请求和响应携带任意元数据。因此,HTTP请求从智利圣地亚哥的一个地址到达边缘服务器;该边缘服务器上的代理读取该请求,对其执行X-Forwarded-For,向正确的Worker服务器发出自己的HTTP请求,并通过它转发请求,这很好地工作;如果Worker关心,它就可以找出请求来自哪里,大多数Worker不需要关心。

其他协议--实际上,所有的非HTTP协议--对代理都不友好。对于这个问题,有一种标准的解决方案:HAProxy的代理协议,它本质上只是将消息封装在传送原始源和目标套接字地址的报头中。但请记住,我们的工作是尽可能接近未修改的Docker容器,使应用程序支持代理协议是一个很大的修改。

您可以使任何协议与自定义代理一起工作。以DNS为例:您的边缘服务器侦听UDP数据包,在其上附加代理报头,将数据包中继到工作服务器,将其解包,然后将其发送到容器。您可以使用AF_PACKET套接字拦截所有UDP,并以这种方式写入最后一跳数据包以伪造地址。起初,这就是我为Fly实现这一点的方式。

但是这种方法有一个问题。两个,真的。首先,要在用户领域实现这一点,您需要向网络上的所有边缘服务器和工作服务器添加一项服务。该服务所做的一切就是提供一个您真正希望Linux内核为您提供的功能。服务就会停机!你得看着他们!其次:它很慢-不,这不是真的,现代的AF_Packet超级快-但它并不好玩。这才是真正的问题所在。

数据包过滤器有着悠久而非常有趣的历史。它们可以追溯到比“防火墙”这个词在今天召唤得更久远的地方;至少可以一直追溯到施乐的阿尔托(Xerox Alto)。下面是对那段历史的固执己见和不准确的背诵。

在过去20年的大部分时间里,数据包过滤的目标是可观察性(tcpdump和Wireshark)和访问控制。但这并不是他们的激励用例!它们可以追溯到操作系统中,在那个操作系统中,内核网络堆栈只是一个美化的以太网驱动程序。网络协议变化很快,没有人想要不断修改内核,人们希望可以构建一个单一的可扩展网络框架来支持每种协议。

因此,早在20世纪80年代中期,你就有了CSPF:Alto包过滤器的一个端口,它基于基于堆栈的虚拟机(Alto只有一个地址空间,只使用本机代码),它评估过滤程序,以确定哪个4.3BSD用户端程序将接收哪个以太网帧。内核将数据包接收划分为由/dev;中的设备表示的插槽(";ports";)。进程声明一个端口并使用ioctl加载筛选器。其想法是,这就是您为守护进程声明TCP端口的方式。

CSPF VM非常简单:您可以将传入数据包中的文字、常量或数据推送到堆栈上,可以比较堆栈上的前两个值,还可以对前两个值执行AND、OR和XOR操作。您可以立即从筛选器返回几条指令;否则,如果程序结束时堆栈上的顶值为零,则筛选器会传递一个包。此扩展的…。有点像…。对于每天高达一百万包的速率。您使用筛选器而不是原生内核IP代码造成了3-6倍的性能损失。

快进4年后,到了麦凯恩、范雅各布森和TCPdump。内核VM用于过滤是个好主意,但是CSPF在1991年过于简单,不能快速运行。因此,用一对寄存器、暂存存储器和包存储器交换堆栈。在该内存上执行通用指令-加载、存储、条件跳转和ALU操作;当命中RET指令时,过滤器结束,这将返回数据包结果。您已经有了伯克利数据包过滤器。

如果您将任意程序从用户区域重新加载到内核中,您就会遇到两个问题:防止程序损坏内核内存,以及防止程序在无限循环中锁定内核。BPF通过只允许程序访问少量经过边界检查的内存来缓解第一个问题。后一个问题BPF通过不允许向后跳转来解决:您根本不能用BPF编写循环。

关于BPF最有趣的事情不是虚拟机(即使在内核中,它也像是一两页代码;只有一个for循环和一个switch语句)。它的tcpdump,这是一种针对向下编译为BPF的高级语言的不受愚弄的优化编译器。在本世纪初,我有幸尝试扩展该编译器以添加多路分解功能,并且可以证明:它是很好的代码,而且并不简单。当您运行tcpdump(和Wireshark,它通过libpcap引入编译器)时,您几乎没有注意到这一点。

BPF和libpcap是成功的(至少在它们设计的网络可观察性领域是如此),在接下来的20年里,这几乎是数据包过滤的艺术状态。就像,在BPF之后的一两年,你得到了防火墙和类似iptables的过滤器的发明。但这些过滤器很无聊:对一组预定义的参数化规则进行线性搜索,选择性地丢弃数据包。兹兹。

有些事情确实会发生。在';94中,Mach尝试使用BPF作为其微内核数据包分配器,将数据包路由到每个都有自己的TCP/IP堆栈的用户服务。为每个数据包顺序评估数百个过滤器是行不通的,因此BPF的Mach';s&34;mpf&34;变体允许您将查找表编码到指令流中(注意:该文件是实际的tfile),因此您只需解码TCP或UDP一次,然后从表中调度。

McCanne&39;s可以追溯到90年代末,当时有BPF+。与累加器寄存器一起输出,与严重的32位寄存器堆一起输入。否则,您必须眯起眼睛才能看到BPF+VM与BPF有何不同。不过,编译器从根本上说是不同的;现在它是SSA形式,就像LLVM(暂且不提这一点)。BPF+对SSA进行优化,传递MPF对查找表所做的事情。然后,它将JIT归结为本机代码。这是整洁的工作,而且它不会去任何地方,至少在BPF+的名下是不会去的。

与此同时,Linux的事情也在发生。为了高效地驱动像tcpdump这样的东西,Linux从FreeBSD挖来了BPF。添加了一些数据包访问扩展。

然后,在2011年左右,Linux内核BPF JIT落地。BPF是如此简单,JIT实际上是一个相当小的改变。

现在是2014年。您是Linux内核。如果几乎每个包的BPF评估都是在64位代码中进行的,那么您不妨在64位机器上运行速度很快的VM。所以:

在此期间,让我们让BPF调用内核函数,并给它提供查找表。

关于这些虚拟机,顺便说一句:它们都是如此相似--bpf、bpf+、ebpf,让我震惊不已。通用寄存器堆,加载/存储(可能有一些特殊的存储器和寻址模式,但越来越少),ALU,条件分支,到此为止。