一、IP分片原理
1.1 基本原理
- IP包长计算
IP 包全长由头部中的 total length 字段决定,该字段共 16 位,因此一个 IP 包最大可达 2^16-1 ,即 65535 字节。除去头部 20字节(无Options字段),IP 包最多可承载65535-20,即 65515 字节的数据。 - MTU计算
MTU是最大传输单元( Maximum Transmission Unit)的缩写,指一个接口无需分片所能发送的数据包的最大字节数。
MTU范围在46 ~ 1500字节,默认一般都是1500(7字节前导码+1字节帧开始定界符+6字节的目的MAC+6字节的源MAC+2字节的帧类型+1500字节IP头及数据+4字节的 FCS = 1526字节。抓包软件抓到的是去掉前导码、帧开始定界符、FCS之外的数据,其最大值是 6+6+2+1500=1514) - 产生IP分片的原因
IP分片发生在IP层,不仅源端主机会进行分片,中间的路由器也有可能分片,因为不同的网络的MTU是不一样的,如果传输路径上的某个网络的MTU比源端网络的MTU要小,路由器就可能对IP数据报再次进行分片。而分片数据的重组只会发生在目的端的IP层。
1.2 与分片相关的IP头部字段

-
标识符( identification ),IP 包的 ID ,全局自增,短时间内不会重复,可唯一标识一个 IP 包;
-
标志位( flags ),包括两个用于控制和识别分片的标志位;
- DF 标志位禁止中间路由对该包进行分片;
- MF 标志位表明该包之后还有其他分片;
-
偏移量( fragment offset ),表示一个分片相对于原始 IP 包开头的偏移量,以8字节为单位;
-
分片大小计算
假设主机①出口 MTU 是 1500 ,它准备发一个长度为 4000 字节的 IP包给主机②。这个包必须分片
分片
如上图,原包长达 4000 字节,其中头部 20 字节,数据部分为 3980 字节。分片包最大长度为 1500 ,除去头部的 20 字节,数据部分只剩 1480 。这意味着,原包 3980 字节至少需要分为 3 片。
由于偏移量字段以 8 字节为单位,因此每个分片的数据长度必须为 8 的倍数,最后一片除外。由于 1480 刚好可以被 8 整除,因此分片数据长度可以选择 1480 。 -
片偏移计算
第一个分片,包含原包前 1480 字节数据,因此偏移量 offset=0(数据的首个字节离原始IP头部距离为0) ;而 MF=1 表示后面还有其他分片。第二个分片,包含原包紧接着的 1480 字节数据,偏移量offset=(1480+1480)/8=185 ;同样 MF=1 表示后面还有其他分片。最后一个分片,包含原包最后 1020 字节数据,偏移量(1480*2)/8=370 ;而 MF=0 表示它是最后一片了。 -
分片重组
分片到达目标主机后,系统根据包头中的源地址、目的地址、标识符、偏移量等字段,将它们重组合成原包。
实际上,系统会分配一块内存作为重组分片的缓冲区。一个分片包首个分片达到后,系统将其移入到该缓冲区,等待其他分片达到:
分片重组
后续分片达到后,系统先根据源地址、目的地址和标识符确定它属于哪个包;再根据偏移量确定它属于原包的哪个部分;最后将分片数据拼接到原包中。
二、实现IP分片
2.1实验拓扑
实验拓扑如下,linux向R2发送IP分片,并在R2接口上抓包。

2.2 Python手动制作分片
- Python脚本
#!/usr/bin/python3.4
# -*- coding=utf-8 -*-
from kamene.all import *
#严重注意ICMP的校验和是整ICMP头部和数据部分一起计算的!!!
#flags="MF"或flags=1表示后面有分片,flags=0表示为最后一个分片,flags="DF"或flag=2表示不分片
#frag一位表示8字节,因此frag数值乘以8,才是真正的偏移量字节数!第一个分片需加上ICMP首部8字节。
send(IP(flags="MF",frag=0,id=1,dst='172.16.10.2')/ICMP(chksum=0x7932)/b'Farst Hello Word!!!!!!!!')
send(IP(flags=1,frag=4,id=1,proto=1,dst='172.16.10.2')/(b'second Hello Word!!!!!!!'))
send(IP(flags=0,frag=7,id=1,proto=1,dst='172.16.10.2')/(b'third Hello Word!!'))
-
执行效果
执行效果如下:
分片
抓包结果如下,发送三个数据包,前两包MF位置1.
分片1
分片2
分片3
2.2.1 ICMP校验和计算
ICMP包校验和是连通头部信息加数据本身一起进行校验(ip包只需要校验头部信息)而Scapy自动添加ICMP校验和时只会计算第一分片的数据,当三个分片到达目标主机进行重组后校验不通过,将重组后的数据包丢弃;因此在手动设置IP分片时,需要手动将校验和添加入ICMP首部中。
由于手动计算校验和过程较复杂,可通过wireshark抓包,可以获取到正确的校验和。

2.3 Scapy自动分片
- python脚本
#!/usr/bin/python3.4
# -*- coding=utf-8 -*-
from kamene.all import *
##############################自动制造Fragment################################
frags = fragment(IP(dst='172.16.10.2')/ICMP()/(b"Hello Word"*300))
#产生每一个分片,可以对分片就行修改!!!!
send(frags)
#正常发包,系统会自动进行分片处理!!!!
#send(IP(dst='172.16.10.2')/ICMP()/(b"Hello Word"*300))
抓包结果如下,由于单个数据包长度超过MTU,系统自动将ICMP request包分片发送,同样的ICMP reply系统也进行了分片。

三、网络路径MTU探测
3.1 基本原理
当主机发送分组的长度超过MTU又不可以分片(IP flags位DF置1),则这个分组丢弃,并用ICMP差错报文向主机报告。
3.2 具体实现
-
实验拓扑
实验拓扑如下,在路由器出接口设置不同的MTU值。
MTU路径
- Python脚本
#!/usr/bin/python3.4
# -*- coding=utf-8 -*-
from kamene.all import *
import time
import re
def ping_df(dst,mtu):
pyload = b'v'*(int(mtu) - 28) #28为20字节IP头部和8字节ICMP头部的长度
ping_one_reply = sr1(IP(dst=dst,flags='DF')/ICMP()/pyload, timeout = 1, verbose=False)
#发送DF位的数据包
try:
if ping_one_reply.getlayer(ICMP).type == 3 and ping_one_reply.getlayer(ICMP).code == 4:
#如果返回ICMP不可达信息,就返回1和当前的mtu
MTU=ping_one_reply.getlayer(ICMP).unused#获取经过设备出接口的MTU值
print("中间设备出接口MTU值为:", MTU)
return 1
elif ping_one_reply.getlayer(ICMP).type == 0 and ping_one_reply.getlayer(ICMP).code == 0:
#如果返回ICMP echo reply,就返回2和当前的mtu
return 2, mtu
except Exception as e:
if re.match('.*NoneType.*',str(e)):
return None #如果测试失败,就返回None
def discover_path_mtu(dst):
mtu = 1500 #mtu从1500开始向下减
while True:
Result = ping_df(dst,mtu)
if Result == None: #如果测试失败就打印信息,并且跳出循环
print('目标: ' + dst + '不可达!')
break
elif Result[0] == 2: #如果PING测试成功,就打印信息,并且跳出循环
print('目标: ' + dst + '的Path MTU为: ' + str(Result[1]))
break
elif Result[0] == 1: #如果得到不可达信息,就较少MTU,打印消息,并且继续循环
mtu = mtu - 50
time.sleep(1)
if __name__ == '__main__':
dest=input("目标IP>>>")
discover_path_mtu(dest)
-
执行效果
执行结果如下,返回经过的中间设备出接口的MTU值,如此则可得知路径中最小的MTU值。
MTU探测
抓包结果如下:
request包中Flags DF置1,不允许分片。
request
由于分组长度超过了该设备出接口的MTU值,故返回一个ICMP差错报文告知分组长度超过MTU,并将下一跳的MTU值返回。
ICMP 不允许分片
网友评论