你好!
最近我一直在做一个项目,我在不使用任何库的情况下在 Python 中实现了一堆计算机网络协议的小型玩具工作版本,作为解释计算机网络如何工作的一种方式。
我仍在努力编写那个项目,但今天我想谈谈如何做第一步:用 Python 发送网络数据包。
在这篇文章中,我们将从一个小型 Python 程序发送一个 SYN 数据包(TCP 连接中的第一个数据包),并从example.com
获得回复。这篇文章中的所有代码都在这个 gist中。
什么是网络数据包?
网络数据包是一个字节串。例如,这是 TCP 连接中的第一个数据包:
b'E\x00\x00,\x00\x01\x00\x00@\x06\x00\xc4\xc0\x00\x02\x02"\xc2\x95Cx\x0c\x00P\xf4p\x98\x8b\x00\x00\x00\x00`\x02\xff\xff\x18\xc6\x00\x00\x02\x04\x05\xb4'
我不会在这篇文章中讨论这个字节串的结构(虽然我会说这个特定的字节串有两部分:前 20 个字节是 IP 地址部分,其余的是 TCP 部分)
关键是要发送网络数据包,我们需要能够发送和接收字节串。
为什么 tun/tap?
在 Linux(或任何操作系统)上编写自己的 TCP 实现的问题是——Linux 内核已经有了 TCP 实现!
因此,如果您在普通网络接口上向 example.com 之类的主机发送 SYN 数据包,将会发生以下情况:
- 你向 example.com 发送一个 SYN 数据包
- example.com 回复 SYN ACK(到目前为止一切顺利!)
- 你机器上的 Linux 内核收到 SYN ACK,认为“wtf??我没有建立这个连接??”,然后关闭连接
- 你很难过。没有适合您的 TCP 连接。
几年前我和一位朋友谈论过这个问题,他说“你应该使用 tun/tap!”。不过,我花了好几个小时才弄清楚如何做到这一点,这就是我写这篇博文的原因:)
tun/tap 给你一个“虚拟网络设备”
我喜欢思考tun/tap
的方式是——想象我的网络中有一台小型计算机,它正在发送和接收网络数据包。但它不是一台真正的计算机,它只是我编写的一个 Python 程序。
老实说,这种解释比我想要的还要糟糕。我希望我能准确理解 tun/tap 设备是如何与真正的 Linux 网络堆栈交互的,但不幸的是我不知道,所以“虚拟网络设备”就是你所得到的。希望下面的代码示例将使这一切变得更加清晰。
tun vs tap
名为“tun/tap”的系统允许您创建两种网络接口:
- “tun”,可让您设置 IP 层数据包
- “tap”,可让您设置以太网层数据包
我们将使用tun ,因为这就是我可以弄清楚如何开始工作的方法。水龙头也可能起作用。
如何创建一个tun接口
以下是我创建 IP 地址为 192.0.2.2 的 tun 接口的方法。
sudo ip tuntap add name tun0 mode tun user $USER sudo ip link set tun0 up sudo ip addr add 192.0.2.1 peer 192.0.2.2 dev tun0 sudo iptables -t nat -A POSTROUTING -s 192.0.2.2 -j MASQUERADE sudo iptables -A FORWARD -i tun0 -s 192.0.2.2 -j ACCEPT sudo iptables -A FORWARD -o tun0 -d 192.0.2.2 -j ACCEPT
这些命令做两件事:
- 使用 IP
192.0.2.2
创建tun
设备(并授予您的用户写入权限) - 设置
iptables
以使用 NAT 将来自该 tun 设备的数据包代理到互联网
iptables 部分非常重要,否则数据包将只存在于我的计算机内部,不会发送到互联网,那会有什么乐趣?
我不打算解释这个ip addr add
命令,因为我不明白它,我发现ip
非常难以理解,现在我只能在没有完全理解它们的情况下复制和粘贴ip
命令。不过它确实有效。
Python中如何连接tun接口
这是一个打开 tun 接口的函数,你可以像openTun('tun0')
一样调用它。我通过在scapy源代码中搜索“tun”找到了如何编写它。
import struct from fcntl import ioctl def openTun(tunName): tun = open("/dev/net/tun", "r+b", buffering=0) LINUX_IFF_TUN = 0x0001 LINUX_IFF_NO_PI = 0x1000 LINUX_TUNSETIFF = 0x400454CA flags = LINUX_IFF_TUN | LINUX_IFF_NO_PI ifs = struct.pack("16sH22s", tunName, flags, b"") ioctl(tun, LINUX_TUNSETIFF, ifs) return tun
这一切都是
- 以二进制模式打开
/dev/net/tun
- 调用
ioctl
来告诉 Linux 我们想要一个tun
设备,而我们想要的设备称为tun0
(或者我们传递给函数的任何tunName
)。
一旦它打开,我们就可以像 Python 中的任何其他文件一样read
和write
它。
让我们发送一个SYN数据包!
现在我们有了openTun
函数,我们可以发送一个 SYN 数据包了!
这是使用openTun
函数的 Python 代码的样子。
syn = b'E\x00\x00,\x00\x01\x00\x00@\x06\x00\xc4\xc0\x00\x02\x02"\xc2\x95Cx\x0c\x00P\xf4p\x98\x8b\x00\x00\x00\x00`\x02\xff\xff\x18\xc6\x00\x00\x02\x04\x05\xb4' tun = openTun(b"tun0") tun.write(syn) reply = tun.read(1024) print(repr(reply))
如果我将其作为sudo python3 syn.py
运行,它会打印出来自example.com
的回复:
b'E\x00\x00,\x00\x00@\x00&\x06\xda\xc4"\xc2\x95C\xc0\x00\x02\x02\x00Px\x0cyvL\x84\xf4p\x98\x8c`\x12\xfb\xe0W\xb5\x00\x00\x02\x04\x04\xd8'
显然,这是发送 SYN 数据包的一种非常愚蠢的方式——真正的实现将使用实际代码来生成该字节字符串,而不是对其进行硬编码,并且我们将解析回复而不是仅仅打印出原始字节字符串。但我不想在这篇文章中深入探讨 TCP 的结构,所以这就是我们正在做的事情。
用 tcpdump 查看这些数据包
如果我们在tun0
接口上运行 tcpdump,我们可以看到我们发送的数据包和来自example.com
的答案:
$ sudo tcpdump -ni tun0 12:51:01.905933 IP 192.0.2.2.30732 > 34.194.149.67.80: Flags [S], seq 4101019787, win 65535, options [mss 1460], length 0 12:51:01.932178 IP 34.194.149.67.80 > 192.0.2.2.30732: Flags [S.], seq 3300937416, ack 4101019788, win 64480, options [mss 1240], length 0
Flags [S]
是我们发送的 SYN, Flags [S.]
是响应的 SYN ACK 数据包!我们沟通成功! Linux 网络堆栈根本没有干扰!
tcpdump 还向我们展示了 NAT 是如何工作的
我们还可以在我的真实网络接口( wlp3so
,我的无线网卡)上运行tcpdump
,以查看正在发送和接收的数据包。我们将通过-i wlp3s0
而不是-i tun0
。
$ sudo tcpdump -ni wlp3s0 host 34.194.149.67 tcpdump: verbose output suppressed, use -v[v]... for full protocol decode listening on wlp3s0, link-type EN10MB (Ethernet), snapshot length 262144 bytes 12:56:01.204382 IP 192.168.1.181.30732 > 34.194.149.67.80: Flags [S], seq 4101019787, win 65535, options [mss 1460], length 0 12:56:01.228239 IP 34.194.149.67.80 > 192.168.1.181.30732: Flags [S.], seq 144769955, ack 4101019788, win 64480, options [mss 1240], length 0 12:56:05.334427 IP 34.194.149.67.80 > 192.168.1.181.30732: Flags [S.], seq 144769955, ack 4101019788, win 64480, options [mss 1240], length 0 12:56:13.524973 IP 34.194.149.67.80 > 192.168.1.181.30732: Flags [S.], seq 144769955, ack 4101019788, win 64480, options [mss 1240], length 0 12:56:29.705007 IP 34.194.149.67.80 > 192.168.1.181.30732: Flags [S.], seq 144769955, ack 4101019788, win 64480, options [mss 1240], length 0
这里有几点需要注意:
- IP 地址是不同的——上面的 IPtables 规则已将它们从
192.0.2.2
重写为192.168.1.181
。这种重写称为“网络地址转换”或“NAT”。 - 我们从
example.com
收到了一堆回复——它正在做一个指数退避,它在 4 秒后重试,然后是 8 秒,然后是 16 秒。这是因为我们没有完成 TCP 握手——我们只是发送了一个 SYN 并让它挂起!实际上有一种像这样的 DDOS 攻击,称为 SYN 泛洪,但仅仅发送一个或两个 SYN 数据包并不是什么大问题。 - 我必须添加
host 34.194.149.67
因为在我的真实 wifi 连接上发送了很多 TCP 数据包,所以我需要忽略这些
我不完全确定为什么我们在wlp3s0
上看到比在tun0
上更多的 SYN 回复,我猜这是因为我们在 Python 程序中只阅读了 1 个回复。
这很容易而且非常可靠
上次我尝试在 Python 中实现 TCP 时,我使用了一种叫做“ARP 欺骗”的东西。我不会在这里谈论这个(早在 2013 年这个博客上有一些关于它的帖子),但这种方式更可靠。
并且 ARP 欺骗是在您不拥有的网络上进行的一种粗略的事情。
这是代码
我把这篇博文中的所有代码都放在了这个 gist中,如果你想自己尝试一下,你可以运行
bash setup.sh # needs to run as root, has lots of `sudo` commands python3 syn.py # runs as a regular user
它只适用于 Linux,但我认为也有一种方法可以在 Mac 上设置 tun/tap。
scapy 的插头
我将在这里用一个scapy插件作为结尾:它是一个非常棒的 Python 网络库,无需自己编写所有代码即可进行此类实验。
这篇文章是关于自己编写所有代码的,所以我不会多说。
原文: https://jvns.ca/blog/2022/09/06/send-network-packets-python-tun-tap/