The ICMP Tunnel
Preface
经过一段时间的学习和研究,总算是把 ICMP Tunnel 的理论知识了解个大概了。 研究这项技术期间,出现次数对多的问题大概就是下面这几个:
- 包从哪里来的?
- 包去哪儿了?
- Tunnel 的另一端是谁? 答:操作系统,由操作系统进行包转发,就算设置了 tunnel 的对等端(Peer-to-Peer)也得由操作系统进行转发。于是 前两个问题基本上解决了。
本文包含如下内容:
ICMP Tunnel 的基本技术细节, 包括但不限于 IP包、路由、iptables
、Python 代码.
The ICMP Tunnel
首先贴一张图,这张图展示了IP包从客户端到服务器再到客户端的过程。
名词 | 解释 |
---|---|
tun0 | Tunnel 设备的名称 |
Python 程序 | 负责读写 tun0 的数据以及读写 Internet 上的 ICMP 包 |
下面将逐步讲解 IP 包的具体流向。
1 ⇆ 2: 默认路由所有包到 tun0
Linux 下使用如下命令 将整个系统的 IP 包都导向 tun0
设备:
sudo route del default # 删除默认路由
sudo route add default gw 10.0.8.2 dev tun0 # Gateway(gw) 的地址在 10.0.8.0/24 这网段里就行,dev 就写设备名称 比如 tun0.
反之,tun0
会自动把 Python 程序
写进来的包自动转发到对应的程序.
2 ⇆ 3: tun0
NAT IP 包 / Python 程序
读写 tun0
的 IP 包
Wireshark 抓一下 tun0
的包,发现全部是来自 10.0.2.1
的包。也就是说:
tun0
会对进入 tunnel 的包进行 NAT,导致Python 程序
读出来的包全是来自10.0.2.1
的包- 因为上一条,所以
Python 程序
将目标地址为10.0.2.1
的 IP 包写入tun0
,就会被tun0
自动将NAT后的地址转换回NAT之前的地址,然后再由操作系统把数据包调配到指定的地点。
Python 程序
读写 tun0
的数据时候要注意:
- 每次读取数据的长度应该是
tun0
的 MTU, 这样的话, 每读一次就是一个 IP 包. - 向
tun0
写 IP 包的时候,IP 包的来源地址必须是tun0
的addr
- 不管是由操作系统路由到
tun0
的包 还是 由Python 程序
写入的数据包, 必须小于等于tun0
的 MTU, 否则会被分包.
3 ⇆ 4: 特定静态路由 / IP over ICMP
用特定路由让包出站
既然整个系统的包都被默认路由截获了,那 Python 程序
想发送 ICMP 包到 Server Linux
怎么办?
Linux 下使用如下命令 允许到特定地址的包使用特定的路由:
# 所有去往 45.76.63.12 的包都将通过 wlan0 设备发往网关 192.168.1.1
route add -host 45.76.63.12 gw 192.168.1.1 dev wlan0
使用 Payload
传输实际数据
这是 ICMP echo-request 的 header:
最后一个 Payload
字段是可以存放任何数据的,长度的话 理论上 ICMP 包外的 IP 包长度不超过 MTU 即可,但是实际上传不了那么大。
Python 读写某个 interface(或者说 网卡)上的 ICMP 包
import socket
# 创建一个读写 IPv4/ICMP 包的 socket
lSocketICMP = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP)
# 绑定到某个地址上, 以后都将通过这个地址读写 ICMP 包
lSocketICMP.bind(("192.168.1.101", socket.IPPROTO_ICMP))
# IP_HDRINCL 设置为 True 后,调用 socket.sendto 时第一个参数必须是整个 IP 包,并且整个 IP 包都需要自己构造,除了 Checksum 和 Total length.
lSocketICMP.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, True)
# socket.sendto(data, (address, port))
# data: 类型 bytes, 完整的 IP 包.
# address: 类型 str, 要把包发送到哪里, 因为IP头里面已经有目的地的IP地址了,所以这个参数既可以填写网关 也可以填写为目的地的IP地址(填写成目的地的IP地址后, 会交给系统根据路由表再发送到网关).
# port: 类型int, 发送到哪个端口号, 因为是 ICMP 协议所以填写 0 即可.
# 使用 sendto 而不是 send 的原因: IP 包不像 TCP 那样有状态, 而 send 只能用在已经建立好连接的 socket 上, 因此需要使用 sendto 来收发 IP 包.
lSocketICMP.sendto(bytes(ipPacket), ("45.76.63.12", 0))
# 接收一个 IPv4/ICMP 包, 参数填写绑定的地址(192.168.1.101)对应的设备的 MTU 即可, 这样每调用一次 recv(MTU) 就读出一个 IP 包.
lSocketICMP.recv(MTU)
4 ⇆ 5 & ⇆ 6: ICMP 包穿透 NAT
NAT 对 ICMP 包的映射
NAT 想要进行准确无误的地址转换和 IP 包转发,可以通过下面这5个信息来确定两个对等端(Peer-to-Peer):
- 源 IP 地址
- 源端口号
- 通信协议
- 目的地 IP 地址
- 目的地端口号
TCP 和 UDP 协议都有端口号信息,ICMP 也有类似的信息 叫做 Identifier
,参考下图里的 Identifier
字段.
ICMP Keepalive
客户端通过发送一个 ICMP 包到服务器 可以在 NAT 上创建一条 NAT 记录,但这个记录是有时效的。为了维持这个时效,需要客户端向服务器(也可能同时需要服务器向客户端)定时或不定时发送 Keepalive 包。
6 ⇆ 7: 读写服务器上的网卡上的 ICMP 包
参考 3 ⇆ 4: 特定静态路由 / IP over ICMP, 大致思路和原理都是一样的, 但不需要默认路由 也不需要特定路由。
7 ⇆ 8: 服务器端的 tunnel 读写
直接取出 ICMP 包的 Payload 然后写入 tun0
即可,但可能需要注意一下:
- 如果客户端的
tun0
和服务器的tun0
的addr
不一样的话, 可能需要进行 IP 包的源地址进行转换, 要不然包可能发得出去但收不到对应的响应 IP 包(比如TCP, 发出去个 SYN, 很有可能收不到 SYN ACK, 因为tun0
地址不同导致系统没法把不是10.0.2.1
的包路由到tun0
里).
8 ⇆ 9: tunnel 与实际目的交互
Linux 下执行如下命令来打开 IP 包转发:
# 告诉 Linux kernel 开启 IP 包转发.
sysctl -w net.ipv4.ip_forward=1
# SNAT(MASQUERADE 可以当作自动的 SNAT), 自动转换源IP地址(10.0.0.0/8) 转换成发送数据的网卡上的 IP 地址.
iptables -t nat -A POSTROUTING -s 10.0.0.0/8 -o eth0 -j MASQUERADE
# 将 tun0 设备上的包转发到 eth0 上, 当然 有包从 eth0 进来也会自动的转发到 tun0 上.
iptables -A FORWARD -i tun0 -o eth0 -j ACCEPT
然后无脑的读写 tun0
就能正常的与 Internet 上的主机进行通信了.
GitHub Project
在 GitHub 上挖了个坑,也不知道什么时候能填完,并且在写这篇 Blog 的时候我几乎不会 Python。感兴趣的话可以到 GitHub 上贡献代码或开个 Issue 之类的。 python-icmp-tunnel - A Python implementation of ICMP Tunnel - github.com
更新历史
3 Jun 2017:
- 首次发布
- 添加:引用文章 Linux Tunnel Device and Route
- 小修小改
References
#ICMP Tunnel #Packet Tunneling #tun #Linux #route #iptables #IP over ICMP