Socket网络编程---利用SOCK_RAW实现ping命令功能

一、SOCK_RAW

SOCK_RAW(原始套接字)是一种底层的SOCKET编程接口,它在系统核心实现,需要用户自行构造数据报文,编程比较复杂。

然而,原始套接字能做什么呢?

首先来说,普通的套接字无法处理ICMP、IGMP等网络报文,而SOCK_RAW可以;其次,SOCK_RAW也可以处理特殊的IPv4报文;此外,利用原始套接字,可以通过IP_HDRINCL套接字选项由用户构造IP头。总体来说,SOCK_RAW可以处理普通的网络报文之外,还可以处理一些特殊协议报文以及操作IP层及其以上的数据。

既然SOCK_RAW有以上特性,所以在某些处理流程上它区别于普通套接字。

· 内核处理流程:

  • 接收到的TCP、UDP分组不会传递给任何SOCK_RAW
  • ICMP、IGMP报文分组传递给SOCK_RAW
  • 内核不识别的IP报文传递给SOCK_RAW

SOCK_RAW是否接收报文:

  • Protocol指定类型需要匹配,否则不传递给该SOCK_RAW
  • 如果使用bind函数绑定了源IP,则报文目的IP必须和绑定的IP匹配,否则不传递给该SOCK_RAW
  • 如果使用connect函数绑定了目的IP,则报文源IP必须和指定的IP匹配,否则不传递给该SOCK_RAW

综上所述,原始套接字处理的只是IP层及其以上的数据,比如实现SYN FLOOD攻击、处理PING报文等。当需要操作更底层的数据的时候,就需要采用其他的方式。

接下来,以SOCK_RAW实现ping命令功能为实例,介绍SOCK_RAW的应用。ping命令利用ICMP数据报文探测网络中指定主机是否在线,因此,要实现ping命令的功能,需要先了解ICMP的工作原理和数据报文格式。

二、ICMP报文

ICMP属于TCP/IP的网络层协议,主要用于在主机之间或者主机与路由器之间传递状态信息,例如,数据报文的目标主机不可到达、数据报文因超时被丢弃、路由器通告信息、主机回应信息等。ICMP报文被封装在IP报文中作为IP报文的数据部分,如图:

Socket网络编程---利用SOCK_RAW实现ping命令功能_第1张图片

ICMP报文被封装在IP报文的数据部分,载有ICMP报文的IP报文首部协议字段值为1,表示该IP报文数据部分为ICMP报文。ICMP报文包括8B的报文头部和长度可变的报文数据部分,如下图所示:

Socket网络编程---利用SOCK_RAW实现ping命令功能_第2张图片

ICMP报文类型如下表所示:

Socket网络编程---利用SOCK_RAW实现ping命令功能_第3张图片

代码:占8位,1字节,表示发送该报文的原因。

校验和:占16位,2字节,用来检测数据报文在传输过程中是否出现错误。

选项:占32位,4字节,依类型而定,不同类型对应不同的选项。

数据部分:长度可变,不同类型报文,数据部分也不相同。

用于探测网络中主机是否存在的ICMP报文类型值为8或0,类型值为8时,表示该报文为探测目标主机是否存在的回应请求报文;类型值为0时,表示该报文为回复源主机的回应应答报文。报文类型值为8或0时,报文头部的选项部分被分为占16位的标识符部分和占16位的序号部分,其中,标识符部分由源主机设定,目标主机原样返回,用于判断请求报文与应答报文是否成对;序号部分也由源主机设定,目标主机原样返回,表示请求报文发送序号。报文数据部分为8B的bytes类型的时间。


然后说说关于ICMP报文校验和计算,以及如何将数据转换为bytes格式。

  • ICMP报文校验和计算

Socket网络编程---利用SOCK_RAW实现ping命令功能_第4张图片

  • 在ICMP报文校验和计算中,需要将报文头部与报文数据部分转换为bytes字节流格式。字符串型与bytes型相互转换比较容易,但其他类型与bytes型相互转换就比较困难,Python中的struct模块可以实现bytes型与其他类型数据的相互转换。

  • struct模块中常用的函数为struct.pack()、struct.unpack()和struct.calcsize(),其中,struct.pack()函数将其他类型数据转换并打包为bytes字节流格式数据,struct.unpack()函数将bytes字节流格式数据解包还原为原来类型的数据,struct.calcsize()计算struct.pack()或者struct.unpack()中所使用的格式串字节数,也就是包的大小。struct.pack()函数格式为struct.pack(fmt, v1, v2, …),其中,fmt为格式串,v1、v2等为需要打包为bytes的数据,例如,xx=struct.unpack(‘2if’, 1, 2, 3.5)表示将整数1、2以及浮点数3.5打包成bytes字节流数据并保存在变量xx中,其中格式串中的2i表示2个整数,f表示1个浮点数。struct.unpack()函数格式为struct.unpack(fmt, xx),其中,fmt为格式串,xx为需要解包的bytes数据,例如,(x1, x2, x3)=struct.pack(‘2if’, xx)表示将xx解包为2个整数和1个浮点数,分别存入变量x1、x2和x3中。struct.calcsize()函数格式为struct. calcsize(fmt),其中,fmt为格式串,例如,num=struct.calcsize(‘2if’)表示将2个整数和1个浮点数打包成bytes字节流格式后所占的字节数,并存入变量num中。struct格式符如表所示:

Socket网络编程---利用SOCK_RAW实现ping命令功能_第5张图片

在表中,格式符前面的数字表示格式符的重复次数,例如,“2i”表示有2个4字节有符号整型数;当格式符为“3x”时,表示填充3个“\0”;当格式符为“2c”时,表示有2个字符,字符以b’x’、b’y’形式给出,不能以b’xy’形式给出;当格式符为“4s”时,表示最多由4个字符组成的字符串,以b’abcd’形式给出;当格式符为“4p”时,表示最多由3个字符组成的字符串,其中,首字节表示字符个数,首字节之后为b’abc’形式的字符。数据默认以Little-endian方式表示,即格式符“〈”,如果指明格式符为“!”或“〉”,则数据将以Big-endian方式表示。以Little-endian方式表示时低位在前、高位在后;以Big-endian方式表示时高位在前、低位在后。例如,struct.pack(’〈i’, 1)=b’\x01\x00\x00\x00’和struct.pack(’!i’, 1)=b’\x00\x00\x00\x01’。两种表示方式的混淆,将使数值产生很大的变化,例如,x=struct.pack(‘i’, 1),y=struct.unpack(’!i’, x),y[0]的值为16777216,而非1。

那么究竟什么是big endian,什么又是little endian呢?

其实big endian是指低地址存放最高有效字节(MSB),即最高字节在地址最低位,最低字节在地址最高位,依次排列。
而little endian则是低地址存放最低有效字节(LSB),即最低字节在最低位,最高字节在最高位,反序排列。

用文字说明可能比较抽象,下面用图像加以说明。比如数字0x12345678在两种不同字节序CPU中的存储顺序如下所示:

Big Endian

低地址 高地址

----------------------------------------->

±±±±±±±±±±±±±±±±±±+

| 12 | 34 | 56 | 78 |

±±±±±±±±±±±±±±±±±±+

Little Endian

低地址 高地址

----------------------------------------->

±±±±±±±±±±±±±±±±±±+

| 78 | 56 | 34 | 12 |

±±±±±±±±±±±±±±±±±±+

从上面两图可以看出,采用big endian方式存储数据是符合我们人类的思维习惯的。而little endian,!@#$%^&*,见鬼去吧 -_-|||

所有网络协议也都是采用big endian的方式来传输数据的。所以有时我们也会把big endian方式称之为网络字节序。当两台采用不同字节序的主机通信时,在发送数据之前都必须经过字节序的转换成为网络字节序后再进行传输。

三、代码实例

实现类似ping命令的功能

#!/usr/bin/env python3
#coding:utf-8
"""
@author:oxff
@github:https://github.com/oxff644
"""
import struct#实现bytes型与其他类型数据的相互转换
import array#用于将bytes字节流数据转换为2B有符号整型数组
import time
import socket
def checksum(packet):
    if len(packet) & 1:#判断报文数据长度是否奇数,若为奇数,则通过第10行在报文末尾追加字符“\0”
        packet =packet+'\0'
    words =array.array('h',packet)
    sum =0
    for word in words:
        sum +=(word & 0xffff)#运算保证累加到sum的值为word的低16位
    sum =(sum>>16)+(sum & 0xffff)#将sum的高16位与低16位相加存入sum中
    sum =sum+(sum>>16)
    return (~sum) & 0xffff#返回值的高16位为0,低16位为sum的反码
header =struct.pack('bbHHh',8,0,0,1234,5)
'''
构造ICMP报文的首部,
其中,类型值为8,表示该报文为探测目标主机是否存在的回应请求报文,
代码为0,校验和为0,标识符为1234,序号为5
'''
data =struct.pack('d',time.time())
packet =header+data#将ICMP报文的首部与数据部分连接起来,形成报文存储在变量packet中
chkSum =checksum(packet)#计算校验和
header =struct.pack('bbHHh',8,0,chkSum,1234,5)#以新的校验和重新生成ICMP报文首部
packet =header+data
s =socket.socket(socket.AF_INET,socket.SOCK_RAW,socket.getprotobyname('icmp'))
s.settimeout(3)
ip =input('please input a ip address: ')
for kk in range(4):#利用循环发送探测主机的ICMP数据报文并接收应答报文
    try:
        t1 =time.time()
        s.sendto(packet,(ip,0))#目标主机的端口号为0
        (r_data,r_addr) =s.recvfrom(1024)#过函数recvfrom()接收来自目标主机的回应报文
        # 其中,回应报文保存在bytes类型变量r_data中,目标主机地址保存在元组类型变量r_addr中,r_data中保存的报文为IP报文,其数据部分为ICMP报文
        t2 =time.time()
    except Exception as e:
        print('This is a error:',e)
        continue
    print('Receive the respond from %s, data is %d bytes,time is %.2fms'\
          % (r_addr[0],len(r_data),(t2-t1)*1000))
    (h1,h2,h3,h4,h5) =struct.unpack('bbHHh',r_data[20:28])#r_data[20:28]取IP报文序号20~27的字节流,这8字节的字节流为ICMP报文的首部,r_data[0:20]为IP报文的首
    print('type= %d,code= %d,chksum= %u,Id= %u,SN= %d' % (h1,h2,h3,h4,h5))#输出ICMP报文首部的5个字段值,分别为类型、代码、校验和、标识符和序号。

运行结果如下:
Socket网络编程---利用SOCK_RAW实现ping命令功能_第6张图片

在目标主机返回的ICMP报文中,首部标识符和序号与源主机发送的ICMP报文头部的相同。如果执行时输入的IP地址对应的主机未上线,则显示超时错误信息。

你可能感兴趣的:(计算机基础知识)