最近在做一个ping的功能,用python实现,要分别实现ipv4和ipv6两种栈。虽然也是用开源的ping包,并且只有不到300行,但ipv6改的时候确实感觉无从下手;所以老大改完之后,就仔细研究了一下这个包,发现囊括的知识点确实不少。为了避免自己以后忘记,这里记录下来。
ICMP协议
实现ping主要通过ICMP协议,因为IP协议不是一个可靠的协议,不保证数据被成功送达。所以需要使用ICMP网络控制报文协议。因为它传递差错报文和其他重要信息,所以通常供IP层或TCP/UP层使用,被人为是IP层一个重要组成部分。
ICMPv4和ICMPv6消息的前4个字节(也就是前32位)是相同的。数据链路层所能发送的最大数据帧MTU为1500字节,ICMP协议在实际传输中数据包为20字节IP首部+8字节ICMP首部+1472字节(数据大小)。
ping原理
ping程序的目的是测试另一台主机是否可达。该程序发送一份ICMP回显请求报文给远程主机,并等待ICMP回显应答。如果源主机在一定时间内收到应答,则认为主机可达。ping的原理是用类型码为0的ICMP发请求,收到请求的主机则用类型码为8的ICMP回应。通过计算ICMP应答报文数量和接收与发送报文之间的时间差,判断当前网络状态。ping命令在发送报文的时候,将当前时间值存储在ICMP报文中发出,当应答报文返回时,使用当前值减去存放在ICMP报文数据中存放发送请求的时间值来计算往返时间。Unix系统在实现ping程序时是把ICMP报文中的标识符字段置成发送进程的 ID号。这样 即使在同一台主机上同时运行了多个 ping程序实例,ping程序也可以识别出返回的信息。
ping包分析
my_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, 1)
//ipv6:my_socket = socket.socket(socket.AF_INET6, socket.SOCK_RAW, 58)
my_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, src_nic)
my_id = os.getpid() & 0xFFFF
send_one_ping(my_socket, dest_addr, my_id, psize)
delay = receive_one_ping(my_socket, my_id, timeout)
1. socket(family,type[,protocal]) 使用给定的地址族、套接字类型、协议编号(默认为0)来创建套接字。
socket类型
socket.AF_INET:服务器之间网络通信
socket.SOCK_RAW:原始套接字,普通的套接字无法处理ICMP、IGMP等网络报文,而SOCK_RAW可以;其次,SOCK_RAW也可以处理特殊的IPv4报文
1这里是协议编号,ICMP协议编号为1,ICMPv6的协议编号为58
这是winsock2.h里的定义。
define IPPROTO_ICMP 1
define IPPROTO_ICMPV6 58 /* ICMPv6 */
2. 默认的socket选项不够时,用setsocket来调整。socket.setsockopt(level, optname, value)
level:选项定义的层次--SOL_SOCKET、IPPROTO_TCP、IPPROTO_IP、IPPROTO_IPV6
optname:需要设置的选项
value:选项值
SOL_SOCKET:正在使用socket选项
当level设定为SOL_SOCKET,会有一些常见选项。SO_BINDTODEVICE:可以使socket只在某个特殊的网络接口有效。这时,value就是设备名称,或者为空字符串返回默认值。
3.send_one_ping(my_socket, dest_addr, my_id, psize)
my_id:Unix系统在实现ping程序时是把ICMP报文中的标识符字段置成发送进程的 ID号。这里和0xFFFF做了&运算,表示取字符低16位
在send_one_ping中要完成构造包的工作。
这一步用到了struct模块。当我们需要用python处理二进制数据,如存储文件,socket操作时,可以使用struct模块,来处理C语言中的结构体。
pack(fmt,v1,v2,v3,...)//按照给定格式fmt,把数据封装成字符串(实际上是类似于C结构体字节流)
format C type python type standard size notes
b signed char integer 1 (3)
B unsigned char integer 1 (3)
h short integer 1 (3)
H unsigned short integer 1 (3)
对齐方式:!表示我们要使用网络字节顺序解析,因为数据是从网络中接收到的。(字节对齐,通常是以4个字节为单位的32位系统。故struct根据本地机器字节顺序转换,可以用格式中第一个字符来改变对齐方式。)
这里要提到signed char和unsigned char的区别,作为字符使用,都是存字符的ASCII码,作为整数使用,两种类型取值范围不通,unsigned char取0~255,signed char可取值为-128至127。在ipv4中ICMP_ECHO_REQUEST赋值为8,所以struct.pack时用b转换,在ipv6中,ICMP_ECHO_REQUEST赋值为128,所以用B来转换。
# Remove header size from packet size
psize = psize - 8
# Header is type (8), code (8), checksum (16), id (16), sequence (16)
my_checksum = 0
# Make a dummy heder with a 0 checksum.
header = struct.pack("bbHHh", ICMP_ECHO_REQUEST, 0, my_checksum, id, 1)
//ipv6:header = struct.pack("!BbHHh", ICMP_ECHO_REQUEST, 0, my_checksum, id, 1)
bytes = struct.calcsize("d")
data = (psize - bytes) * "Q"
data = struct.pack("d", time.time()) + data
# Calculate the checksum on the data and the dummy header.
my_checksum = checksum(header + data)
# Now that we have the right checksum, we put that in. It's just easier
# to make up a new header than to stuff it into the dummy.
header = struct.pack("bbHHh", ICMP_ECHO_REQUEST, 0, socket.htons(my_checksum), id, 1)
//ipv6:header = struct.pack( "!BbHHh", ICMP_ECHO_REQUEST, 0, socket.htons(my_checksum), id, 1 )
packet = header + data
my_socket.sendto(packet, (dest_addr, 1)) # Don't know about the 1
//发送UDP数据,将数据发送到套接字,address是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数。
//ipv6:my_socket.sendto(packet, (dest_addr, 58, 0, 0))
4.校验和算法
IP包发送端,首先将校验和字段设置为0,然后将IP数据包头按16比特划分单元,若包头长度非16倍数,则用0补齐,然后对各个单元采用反码加法运算(即高位溢出加到低位),将得到的和的反码填入校验和字段发送。
比如:
一个数据包:45 00 00 29 44 F1 40 00 80 06 61 8D C0 A6 01 AE 4A 7D 47 7D
参考
https://www.cnblogs.com/JetpropelledSnake/p/9177770.html
https://www.xuebuyuan.com/3254723.html