Linux小知识--原始套接字(raw socket)之模拟ping

原始套接字-raw socket

最近在研究高并发下扫描存活主机,基本想法是通过socket来模拟ICMP报文,然后就发现了socket的一片新天地----原始套接字(raw socket)。

raw socket,即原始套接字,可以接收本机网卡上的数据帧或者数据包,对于监听网络的流量和分析是很有作用的,一共可以有4种方式创建这种socket。
1.socket(PF_INET, SOCK_RAW, IPPROTO_TCP|IPPROTO_UDP|IPPROTO_ICMP)发送接收ip数据包
2.socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP|ETH_P_ARP|ETH_P_ALL))发送接收以太网数据帧
3.socket(PF_PACKET, SOCK_DGRAM, htons(ETH_P_IP|ETH_P_ARP|ETH_P_ALL))发送接收以太网数据帧(不包括以太网头部) [1]
4.socket(PF_INET, SOCK_PACKET, htons(ETH_P_IP|ETH_P_ARP|ETH_P_ALL))过时了,不要用啊

我们可看到通过原始套接字,我们可以拿到2层和3层的全部信息,也可以模拟二层和三层的各种协议,是不是很厉害了
在这里插入图片描述

所以如果要模拟ping,需要用第一种方法,
socket(PF_INET, SOCK_RAW, IPPROTO_ICMP)。
我们可以通过socket函数第三个参数,可以模拟创建各种三层的协议。后面一期将模拟TCP协议的时候,还会用到其他类型。

下面是封装之后的创建ICMP函数

/* create a socke to icmp */
int icmp_socket(char *ipv4)
{
    int sockfd;
    int size = ICMP_RECVBUF_SIZE;
    
    if ((sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP)) < 0)
    {
       	usleep(1000);
		if ((sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP)) < 0)
	    {
	        ICMP_LOG_ERR("创建socket失败");
	        return -1;
	    }
    }

	//
	//要过滤响应来源地址,需要这里绑定来源IP
    {
		struct sockaddr_in serv_addr;
		unsigned long ul = 1;
		
		ioctl(sockfd, FIONBIO, &ul); //设置为非阻塞模式
		
		memset(&serv_addr, 0, sizeof(serv_addr));  //每个字节都用0填充
		serv_addr.sin_family = AF_INET;  //使用IPv4地址
		serv_addr.sin_addr.s_addr = inet_addr(ipv4);  //具体的IP地址
		serv_addr.sin_port = htons(1234);  //端口随意填写,

		//将套接字和IP、端口绑定
		if(connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)
        {
        	struct timeval tm;
			fd_set set;
            int error=-1, len;
			
            len = sizeof(int);
            tm.tv_sec = 1;
            tm.tv_usec = 0;
            FD_ZERO(&set);
            FD_SET(sockfd, &set);
            if( select(sockfd+1, NULL, &set, NULL, &tm) > 0)
            {
                getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, (socklen_t *)&len);
                if(error == 0)
                {
					//ret = true;
				}
                else
                {
					ICMP_LOG_ERR("connect[%s] socket失败1",ipv4);
					close(sockfd);
					return -1;
				}
            } 
			else
			{
				ICMP_LOG_ERR("connect[%s] socket失败2",ipv4);
				close(sockfd);
				return -1;
			}
        }
        else
        {
			//ret = true;
		}
        ul = 0;
        ioctl(sockfd, FIONBIO, &ul); //设置为阻塞模式
	}
	//设置接收buf大小
    if(setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size))!=0)
    {
        ICMP_LOG_ERR("setsockopt SO_RCVBUF error.\n\r");
        close(sockfd);
        return -1;
    }

	//设置超时时间,影响后面revform函数,让其不会一直阻塞
    if(setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (char*)&icmp_wait_timeout, sizeof(struct timeval))!=0)
    {
        ICMP_LOG_ERR("setsockopt SO_RCVTIMEO error.\n\r");
        close(sockfd);
        return -1;
    }
   
    return sockfd;
}

这个函数中比较重要的点,在于要过滤响应来源地址,需要这里绑定来源IP,这句话的含义就是我们创建的Raw socket,是比较松散的过滤,如果不绑定来源IP,那么所有的ICMP报文我们都收到,如果网络环境比较干净,倒是无所谓,但是如果是复杂的环境,里面有其他ICMP报文的话,会干扰到我们分析响应结果。
下面这段话,是对于Raw Socket的一段非常重要的话

当内核有一个需要传递到原始套接字的IP数据报时,它将检查所有进程上的原始套接字,以寻找所有匹配的套接字。每个匹配懂得套接字将被递送以该iP数据报的一个副本。(事实证明,如果进程过多,匹配的套接字过多,内核会忙于数据报的软过滤和分发,而实际的套接字却空闲,导致性能下降)
内核对每个原始套接字均执行如下3个测试,只有这三个测试为真,内核才把接收到的数据报递送到这个套接字。
【1】如果创建这个套接字时制订了非0的协议参数(socket的第三个参数),那么接受到的数据报的协议字段必须匹配该值,否则数据报不递送到这个套接字
【2】如果这个原始套接字已由bind调用绑定了某个本地IP地址,那么接受到的数据报的目的IP地址必须匹配这个绑定的地址,否则该数据报不递送到这个套接字。
【3】如果这个原始套接字已由connect调用指定了某个外地IP地址,那么接受到的数据报的源IP地址必须匹配这个已连接地址,否则该数据报不递送到这个套接字。

bind和connect用的不恰当的话,直接影响你接收数据,处理不好,要么收的多,要么收不到。
在这里插入图片描述

下面是各个模块的功能函数。
计算校验码函数

unsigned short icmp_gen_chksum(unsigned short * data, int len)
{
    int             nleft   = len;
    int             sum     = 0;
    unsigned short  *w      = data;
    unsigned short  answer  = 0;
 
    while (nleft > 1)
    {
        sum += *w++;
        nleft -= 2;
    }
 
    if (nleft == 1)
    {
        * (unsigned char *) (&answer) = *(unsigned char *)w;
        sum += answer;
    }
 
    sum     = (sum >> 16) + (sum & 0xffff);
    sum     += (sum >> 16);
    answer  = ~sum;
    
    return answer;
}

生成ICMP报文函数,这里注意如果前面没有绑定IP地址,那么也可以通过在此处配置icmp_id的方式来进行区分,这个ID是每一次操作会往返传递与两地之间,所以可以作为身份验证用。
在这里插入图片描述

int icmp_pkg_pack(void *buffer,const void *data, int data_size)
{
    int  packsize = 0;
    struct icmp * icmp  = malloc(sizeof(struct icmp));
    icmp->icmp_type     = ICMP_ECHO;
    icmp->icmp_code     = 0;
    icmp->icmp_cksum    = 0;
    icmp->icmp_seq      = htons(0);
    icmp->icmp_id       = 0xff00;

    gettimeofday((struct timeval *) &icmp->icmp_data, NULL);
 
    memcpy(buffer, icmp, sizeof(struct icmp));
    packsize += sizeof(struct icmp);
 
    if(data && data_size)
    {
        memcpy(buffer+packsize, data, data_size);
        packsize += data_size;
    }
 
    return packsize;
}
 

发送ICMP报文函数,这个校验和的函数经过测试,没有任何问题

 
/* send icmp package */
int icmp_send_pkg(char* ipv4,int socket, const void *data, int size)
{
    int             packetsize;
    unsigned short  checksum = 0;
    int             n = 0;
  	struct sockaddr_in dst_addr;
     
    char pkg_buffer[ICMP_BUF_SIZE];
    
    packetsize  = icmp_pkg_pack(pkg_buffer, data, size);
    checksum    = icmp_gen_chksum((unsigned short *)pkg_buffer, packetsize);
    
     icmp_dst_addr(ipv4, &dst_addr);

    memcpy(pkg_buffer + ICMP_PKG_CHKSUM_OFFSET, &checksum, ICMP_PKG_CHKSUM_SIZE);
 
    if ((n = sendto(socket, pkg_buffer, packetsize, 0, (struct sockaddr *) &dst_addr, sizeof(struct sockaddr_in)))< 0)
    {
	        ICMP_LOG_ERR("发送ICMP失败n = %d", n);
	        return 0;
    }
                       
    return n;
}

接收ICMP报文函数,Revice函数在前面的socket定义的超时时间之后会自动返回,保证用户进程不会一直阻塞。
在这里插入图片描述

int icmp_recv_pkg(int socket, void *recvbuf, int size)
{
    int n;
	socklen_t fromlen;
	struct sockaddr_in from_addr; 
	 
    fromlen = sizeof(struct sockaddr_in);
 
    if((n = recvfrom(socket, recvbuf, size, 0, (struct sockaddr *) &from_addr, &fromlen)) < 0)
    {
        ICMP_LOG_ERR("recvfrom error.n = %d\n\r", n);
        return 0;
    }
    return n;
}

解析ICMP报文函数,这里主要是判断了有没有收到响应,还可以通过时间戳,计算经过了多少时间到达
(这里没写)在这里插入图片描述

int icmp_pkg_unpack(char * buf, int len)
{
    int		iphdrlen;
    struct  ip * ip = NULL;
    struct  icmp * icmp = NULL;
 
    ip          = (struct ip *)buf;
    iphdrlen    = ip->ip_hl << 2;
    icmp        = (struct icmp *) (buf + iphdrlen);
    len        -= iphdrlen;
 
    if (len < 8)
    {
        ICMP_LOG_ERR("ICMP packet\'s length is less than 8\n\r");
        return - 1;
    }
    if (icmp->icmp_type != ICMP_ECHOREPLY) 
    {
        return - 1;
    }
    return 0;
}

主函数测试ping函数和一些定义,还有一些核心头文件,基础头文件请自己添加
在这里插入图片描述

#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>
#ifndef offsetof
#define offsetof(type, member)    ((int) & ((type*)0) -> member )
#endif
 
#define ICMP_BUF_SIZE           256
#define ICMP_RECVBUF_SIZE       (50 * 1024)

#define ICMP_PROTO_NAME         "icmp"
#define ICMP_DATA               "my ICMP Ping"
 
#define ICMP_PING_SUCC          __ICMP_PING_SUCC
#define ICMP_PING_FAIL          __ICMP_PING_FAIL
 
#define ICMP_LOG(fmt...)        printf(fmt);
 
#define ICMP_LOG_ERR(fmt...)   	printf(fmt);

#define ICMP_PKG_CHKSUM_OFFSET  offsetof(struct icmp, icmp_cksum)
#define ICMP_PKG_CHKSUM_SIZE    2

typedef enum 
{
    __ICMP_PING_FAIL = 0,
    __ICMP_PING_SUCC,
}icmp_ping_rlst_t;

static struct timeval icmp_wait_timeout = {0,100000}; //sec

int icmp_ping_fun(char *ipv4)
{
	int res=__ICMP_PING_FAIL;
    char pkg_buffer[ICMP_BUF_SIZE];
    int ping_socket = 0;
	
	ping_socket =icmp_socket(ipv4);

	if(ping_socket ==NULL)
	{
		return __ICMP_PING_FAIL;
	}
	else
	{
		int n=0;
		n = icmp_send_pkg(ipv4,ping_socket, ICMP_DATA, sizeof(ICMP_DATA));
		if(n < 8) 
		{
			close(ping_socket );
			return __ICMP_PING_FAIL;
		}
		else
		{
			n = icmp_recv_pkg(ping_socket, pkg_buffer, ICMP_BUF_SIZE);
			if(n<8) 
			{
				close(ping_socket );
				return __ICMP_PING_FAIL;
			}
			else
			{
				if(icmp_pkg_unpack(pkg_buffer, n)==0)
				{
    				close(ping_socket );
   					return __ICMP_PING_SUCC;
    				}
				else
				{
					close(ping_socket );
					return __ICMP_PING_FAIL;
				}
			}
		}
	}
}

这种写法,配合上多线程,同时ping65535个IP,3秒就能得到全部结果。
在这里插入图片描述

最后感谢这些人的奉献,参考了人家的代码,进行了一些改动。

C语言实现ICMP协议,并进行PING测试
raw_socket(原始套接字)以及普通socket使用终极总结

下一章会再次介绍一下利用原始套接字模拟tcp通讯,来进行远端端口扫描。
前一阵为了给孩子提供一个可以玩土的地方,和邻居合租的大棚,小孩子看到土的想法就是
在这里插入图片描述
结果发现还是我们这些农村出身的大人更喜欢这种种地的感觉。
在这里插入图片描述
从旁边大棚的废土里面捡到了几个草莓植株,居然开花了
在这里插入图片描述
希望大家以后能像这株草莓一样,被喜欢你的人发现。

  • 12
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

胖哥王老师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值