订阅以接收新文章的通知:

充分利用Linux的防火墙:一个黑客行为,使我们得以构建Spectrum

2018-04-12

4 分钟阅读时间
这篇博文也有 EnglishDeutsch日本語한국어EspañolFrançais版本。

今天,我们将介绍Spectrum:一个可为任意基于TCP的协议提供DDoS保护、负载均衡和内容加速的Cloudflare新特性。

13334109713_0b32435032_z

CC BY-SA 2.0 图片Staffan Vilcans提供

在开始构建Spectrum之后不久,我们就遇到了一个主要的技术障碍:Spectrum要求我们可以在任何有效的TCP端口(从1到65535)上接受连接。在我们的Linux边缘服务器上,我们不可能实现“在任何端口上接受入站连接”。这不是特定于Linux的限制:这是BSD sockets API的一个特性,它是大多数操作系统上网络应用程序的基础。为了能够交付Spectrum,我们需要解决两个重叠的问题:

  • 如何在1到65535的所有端口号上接受TCP连接

  • 如何配置单个Linux服务器以接受大量IP地址上的连接(在我们的任播范围内有数千个IP地址)

为服务器分配数百万个IP

Cloudflare的边缘服务器拥有几乎相同的配置。在早期,我们通常将特定的/32(和/128)IP地址分配给环回网络接口[1]。当我们有几十个IP地址时,这种方法效果甚好。但随着我们的发展,这种方法却无法扩展。

随之而来的是“AnyIP”技巧。AnyIP允许我们将整个IP前缀(子网)分配给环回接口,扩展特定的IP地址。AnyIP的应用已经非常普遍了:您的计算机就已将127.0.0.0/8分配给了环回接口。从您的计算机的角度来看,从127.0.0.1到127.255.255.254的所有IP地址都属于本地计算机。

此技巧不仅仅适用于127.0.0.1/8的块。要在本地分配整个192.0.2.0/24范围,只需要运行:

ip route add local 192.0.2.0/24 dev lo

然后,您可以将以下IP地址之一绑定到端口8080:

nc -l 192.0.2.1 8080

使IPv6正常工作会更加困难:

ip route add local 2001:db8::/64 dev lo

遗憾的是,您不能像IPv4示例那样对v6的IP地址进行绑定。要实现这一工作,您必须使用IP_FREEBIND套接字选项,这需要更高级的权限。出于完整性,这里还给出了一个sysctl net.ipv6.ip_nonlocal_bind,但我们不建议您接触它:

这个AnyIP技巧使我们能够在本地为每台服务器分配数百万个IP地址:

$ ip addr show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536
    inet 1.1.1.0/24 scope global lo
       valid_lft forever preferred_lft forever
    inet 104.16.0.0/16 scope global lo
       valid_lft forever preferred_lft forever
...

绑定到所有端口

第二个主要问题是为任意端口号开启TCP套接字。在Linux中,通常在任意支持BSD sockets API的系统中,您只能通过一个bind系统调用绑定特定的TCP端口号。在一个操作中不可能绑定到多个端口。

一个简单的解决方案是绑定65535次,在每个可能的端口都绑定一次。事实上,这可能是一个解决方案,但这会带来可怕的后果:

在内部,Linux内核将监听套接字存储在一个按端口号LHTABLE索引的哈希表中,使用了32个bucket(存储桶):

/* Yes, really, this is all you need. */
#define INET_LHTABLE_SIZE       32

如果我们打开65k个端口,那么对该表的查找将大大减慢:每个哈希表存储桶将包含2000个项目。

解决这个问题的另一种方法是使用iptables丰富的NAT特性:我们可以将入站数据包的目的地重写为某个特定的地址/端口,我们的应用程序将绑定到这个地址/端口。

但我们不想这样做,因为这需要启用iptables conntrack模块。从历史上看,我们发现了一些性能边缘情况,conntrack无法应对我们遇到的一些大型DDoS攻击。

另外,使用NAT方法,我们会丢失目标IP地址信息。为了解决这个问题,还有一个鲜为人知的SO_ORIGINAL_DST套接字选项,但是代码看起来并不令人鼓舞

幸运的是,有一种方法可以实现我们的目标,而不涉及绑定到所有65k个端口或使用conntrack。

防火墙援助

在进一步讨论之前,让我们先回顾一下操作系统中网络数据包入站的一般流程。

通常,入站数据包路径中有两个不同的层:

  • IP防火墙

  • 网络堆栈

它们在概念上是不同的。IP防火墙通常是一个无状态软件(现在让我们忽略conntrack和IP分片重组)。防火墙分析IP数据包并决定是否接受或丢弃它们。请注意:在这一层,我们讨论的是_数据包_和_端口号_,而不是_应用程序_或_套接字_。

然后是网络堆栈。这只“野兽”有着大量的态。它的主要任务是将入站IP数据包分派到套接字中,然后由用户空间应用程序进行处理。网络堆栈管理着(与用户空间共享的)抽象。它负责重组TCP流,处理路由,并了解哪些IP地址是本地的。

魔术粉尘

upload-1

资料来源:YouTube

在某个时候,我们偶然发现了TPROXY iptables模块。这一官方文件很容易被忽视:

TPROXY
This target is only valid in the mangle table, in the 
PREROUTING chain and user-defined chains which are only 
called from this chain.  It redirects the packet to a local 
socket without changing the packet header in any way. It can
also change the mark value which can then be used in 
advanced routing rules. 

我们可以在内核中找到另一篇文档:

我们思考的越多,我们就越好奇……

那么……TPROXY_实际是做什么的_?

揭秘魔术

该TPROXY代码出人意料的简单

case NFT_LOOKUP_LISTENER:
  sk = inet_lookup_listener(net, &tcp_hashinfo, skb,
				    ip_hdrlen(skb) +
				      __tcp_hdrlen(tcph),
				    saddr, sport,
				    daddr, dport,
				    in->ifindex, 0);

让我为您大声读出来:在iptables模块(这也是防火墙的一部分)中,我们调用inet_lookup_listener。该函数读取src/dst port/IP 4-tuple,并返回能够接受该连接的监听套接字。这是网络堆栈的套接字调度的核心功能。

再总结一次:防火墙代码调用套接字分配例程。

稍后在TPROXY上实际做的套接字调度

skb->sk = sk;

这一行将一个套接字struct sock分配给一个入站包——完成调度。

从帽子里变出兔子

3649474619_3b800400e9_z-1

CC BY-SA 2.0 图片Angela Boothroyd提供

有了TPROXY,我们可以非常轻松地执行“绑定到所有端口”的技巧。配置如下:

# Set 192.0.2.0/24 to be routed locally with AnyIP.
# Make it explicit that the source IP used for this network
# when connecting locally should be in 127.0.0.0/8 range.
# This is needed since otherwise the TPROXY rule would match
# both forward and backward traffic. We want it to catch 
# forward traffic only.
sudo ip route add local 192.0.2.0/24 dev lo src 127.0.0.1

# Set the magical TPROXY routing
sudo iptables -t mangle -I PREROUTING \
        -d 192.0.2.0/24 -p tcp \
        -j TPROXY --on-port=1234 --on-ip=127.0.0.1

除了设置这个选项之外,您还需要使用神奇的IP_TRANSPARENT套接字选项启动TCP服务器。下面的示例需要监听tcp://127.0.0.1:1234。IP_TRANSPARENT的手册页显示:

IP_TRANSPARENT (since Linux 2.6.24)
Setting this boolean option enables transparent proxying on
this socket.  This socket option allows the calling applica‐
tion to bind to a nonlocal IP address and operate both as a
client and a server with the foreign address as the local
end‐point.  NOTE: this requires that routing be set up in
a way that packets going to the foreign address are routed 
through the TProxy box (i.e., the system hosting the 
application that employs the IP_TRANSPARENT socket option).
Enabling this socket option requires superuser privileges
(the CAP_NET_ADMIN capability).

TProxy redirection with the iptables TPROXY target also
requires that this option be set on the redirected socket.

这是一个简单的Python服务器:

import socket

IP_TRANSPARENT = 19

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.setsockopt(socket.IPPROTO_IP, IP_TRANSPARENT, 1)

s.bind(('127.0.0.1', 1234))
s.listen(32)
print("[+] Bound to tcp://127.0.0.1:1234")
while True:
    c, (r_ip, r_port) = s.accept()
    l_ip, l_port = c.getsockname()
    print("[ ] Connection from tcp://%s:%d to tcp://%s:%d" % (r_ip, r_port, l_ip, l_port))
    c.send(b"hello world\n")
    c.close()

运行服务器后,您可以从任意IP地址连接到它:

$ nc -v 192.0.2.1 9999
Connection to 192.0.2.1 9999 port [tcp/*] succeeded!
hello world

最重要的是,服务器将报告连接确实指向了192.0.2.1端口9999,即便实际上没有人监听这个IP地址和端口

$ sudo python3 transparent2.py
[+] Bound to tcp://127.0.0.1:1234
[ ] Connection from tcp://127.0.0.1:60036 to tcp://192.0.2.1:9999

请看!这就是在不使用conntrack的情况下绑定Linux 上任意端口的方法。

就是这些

在这篇文章中,我们描述了如何使用一个鲜有人关注的iptables模块,这个模块最初是为了帮助透明代理而设计的。在它的帮助下,我们可以使用标准的BSD sockets API执行我们认为不可能完成的事情,从而避免了任何自定义内核补丁的需要。

该TPROXY模块非同寻常——在Linux防火墙的环境中,它执行的事情通常是由Linux网络栈完成的。关于这一模块的官方文档非常之少,我不认为有很多Linux用户可以理解这个模块的全部功能。

可以说,TPROXY使我们的Spectrum产品可以在普通内核上平稳运行。这再次提醒我们,理解iptables和网络堆栈有多重要!


你是否对底层套接字工作感兴趣呢?快来加入我们在伦敦,奥斯汀,旧金山,尚佩恩(Champaign)的世界闻名团队以及在波兰华沙的精英办公室吧


  1. 将IP地址分配给环回接口,加上适当的rp_filter和BGP配置,使我们能够在边缘服务器上处理任意的IP范围。 ↩︎

我们保护整个企业网络,帮助客户高效构建互联网规模的应用程序,加速任何网站或互联网应用程序抵御 DDoS 攻击,防止黑客入侵,并能协助您实现 Zero Trust 的过程

从任何设备访问 1.1.1.1,以开始使用我们的免费应用程序,帮助您更快、更安全地访问互联网。要进一步了解我们帮助构建更美好互联网的使命,请从这里开始。如果您正在寻找新的职业方向,请查看我们的空缺职位
产品新闻SpectrumDDoS安全性Linux速度和可靠性

在 X 上关注

Marek Majkowski|@majek04
Cloudflare|@cloudflare

相关帖子

2024年10月24日 13:00

Durable Objects aren't just durable, they're fast: a 10x speedup for Cloudflare Queues

Learn how we built Cloudflare Queues using our own Developer Platform and how it evolved to a geographically-distributed, horizontally-scalable architecture built on Durable Objects. Our new architecture supports over 10x more throughput and over 3x lower latency compared to the previous version....

2024年10月09日 13:00

Improving platform resilience at Cloudflare through automation

We realized that we need a way to automatically heal our platform from an operations perspective, and designed and built a workflow orchestration platform to provide these self-healing capabilities across our global network. We explore how this has helped us to reduce the impact on our customers due to operational issues, and the rich variety of similar problems it has empowered us to solve....