$ 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防火墙通常是一个无状态软件(现在让我们忽略conntrack和IP分片重组)。防火墙分析IP数据包并决定是否接受或丢弃它们。请注意:在这一层,我们讨论的是_数据包_和_端口号_,而不是_应用程序_或_套接字_。
然后是网络堆栈。这只“野兽”有着大量的态。它的主要任务是将入站IP数据包分派到套接字中,然后由用户空间应用程序进行处理。网络堆栈管理着(与用户空间共享的)抽象。它负责重组TCP流,处理路由,并了解哪些IP地址是本地的。
魔术粉尘
资料来源: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分配给一个入站包——完成调度。
从帽子里变出兔子
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!