Hotaru's Notebook

The ICMP Tunnel

Preface

经过一段时间的学习和研究,总算是把 ICMP Tunnel 的理论知识了解个大概了。 研究这项技术期间,出现次数对多的问题大概就是下面这几个:

  1. 包从哪里来的?
  2. 包去哪儿了?
  3. Tunnel 的另一端是谁? 答:操作系统,由操作系统进行包转发,就算设置了 tunnel 的对等端(Peer-to-Peer)也得由操作系统进行转发。于是 前两个问题基本上解决了。

本文包含如下内容: ICMP Tunnel 的基本技术细节, 包括但不限于 IP包、路由、iptables、Python 代码.

The ICMP Tunnel

首先贴一张图,这张图展示了IP包从客户端到服务器再到客户端的过程。

The ICMP Tunnel

名词 解释
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 的包。也就是说:

  1. tun0 会对进入 tunnel 的包进行 NAT,导致 Python 程序 读出来的包全是来自 10.0.2.1 的包
  2. 因为上一条,所以 Python 程序 将目标地址为 10.0.2.1 的 IP 包写入 tun0,就会被 tun0 自动将NAT后的地址转换回NAT之前的地址,然后再由操作系统把数据包调配到指定的地点。

Python 程序 读写 tun0 的数据时候要注意:

  1. 每次读取数据的长度应该是 tun0 的 MTU, 这样的话, 每读一次就是一个 IP 包.
  2. tun0 写 IP 包的时候,IP 包的来源地址必须是 tun0addr
  3. 不管是由操作系统路由到 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:

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):

TCP 和 UDP 协议都有端口号信息,ICMP 也有类似的信息 叫做 Identifier,参考下图里的 Identifier 字段.

ICMP echo-request header

ICMP Keepalive

客户端通过发送一个 ICMP 包到服务器 可以在 NAT 上创建一条 NAT 记录,但这个记录是有时效的。为了维持这个时效,需要客户端向服务器(也可能同时需要服务器向客户端)定时或不定时发送 Keepalive 包。

6 ⇆ 7: 读写服务器上的网卡上的 ICMP 包

参考 3 ⇆ 4: 特定静态路由 / IP over ICMP, 大致思路和原理都是一样的, 但不需要默认路由 也不需要特定路由。

7 ⇆ 8: 服务器端的 tunnel 读写

直接取出 ICMP 包的 Payload 然后写入 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:

References

  1. NAT的特殊处理 (ICMP 包是如何穿透 NAT 的)
  2. ping (networking utility)
  3. python-icmp-tunnel - A Python implementation of ICMP Tunnel - github.com
  4. Linux Tunnel Device and Route

#ICMP Tunnel #Packet Tunneling #tun #Linux #route #iptables #IP over ICMP