原始套接字
简介
套接口最常用的两种类型:SOCK_STREAM和SOCK_DGRAM。
- SOCK_STREAM: 流式套接口,传输的是字节流,每次传输的数据没有边界,它是面向连接的,底层使用TCP协议。
- SOCK_DGRAM: 数据报套接口,无连接,使用UDP协议
传送的数据格式是预先定义好的
通过原始套接字,可以了解底层协议的实现细节,自己构造协议首部和数据,发送并接受
WinSock提供了另一种类型的套接口SOCK_RAW,也被称为“原始套接口”。
当用选项IP_HDRINCL调用setsockopt时,用户可以自己构造IPv4首部。
创建套接口时设置一个系统没有处理的协议,可以在应用层实现自己的传输协议
通常网络系统只处理ICMP(1)、IGMP(2)、TCP(6)和UDP(17),使用原始套接口可以发送和接收ICMP和IGMP分组,系统在处理完后,会把数据报复制给原始套接口一份。
API
创建套接字
原始套接口也用socket函数来创建,第二个参数为SOCK_RAW,第三个参数protocol由用户设置,可以使用WinSock2.h中定义的前缀为IPPROTO_XXX的常值。另外,用户也可以选择一个头文件中没有定义的数值,直接在socket函数中传入数值即可。
设置选项
大部分选项的设置与TCP和UDP套接口是一样的
有一个选项IP_HDRINCL,只适用于原始套接口,用于控制是否由用户自己构造IP首部。如果设置为非0值,用户自己构造IP首部及后面的数据;设置为0值时,这是默认情况,用户只需要构造IP首部之后的数据部分,IP首部由系统填写。
绑定套接口
在原始套接口上调用bind函数,只是设置本地地址,系统不关心端口值,因为原始套接口没有端口的概念。
通常原始套接口不调用bind,而是在发送数据时由系统自己选择外出接口。
当bind中的地址不属于本机任何网络接口时,函数会失败。
连接套接口
函数connect设置原始套接口的目的地址,也不关心端口值,并把套接口标识为已连接,这里的已连接只表示设置了目的地址。
本地地址不受connect的影响,没有调用bind时,仍然是未设定的。
发送数据
发送数据通常都用sendto,在参数to中指定要发送的目的地址,如果已经调用了connect,也可以用send发送数据。
接收数据
接收数据通常用recvfrom,如果不关心对方的地址,也可以用recv。
调用recvfrom之前必须调用过bind、connect或者sendto中的一个函数,将本地或目的地址信息告诉系统,如果直接调用recvfrom,WinSock返回失败。
关闭
关闭原始套接口与关闭其他类型的套接口是一样的,调用closesocket函数即可。
输出处理
- 原始套接口发送数据通常用sendto,在第五个参数to中指定要发送的目的地址。
- 如果调用过connect,目的地址已经设定,发送数据时,可以直接调用send,当然也可以用sendto,并且第五个参数to设置为NULL。
当to不为NULL时,系统会把数据发送到to所指定的目的地址,但调用connect时保存在套接口中的目的地址不会改变。
这样产生的问题是如果to中与connect的地址不同,由于这个套接口的目的地址是由connect指定的,故从to中地址输入的数据报就不会在这个套接口上接收到。
- 发送的目的地址可以是任何有效的IP地址,包括广播或多播地址。为了向广播地址发送数据,程序必须用选项SO_BROADCAST调用setsockopt设置套接口才能够广播,否则send或sendto将失败,错误码为WSAEACCES。
使用原始套接口的应用程序不需要加入一个多播组,就可以向该组播地址发送数据。 - 原始套接口发送的数据长度,包括IP首部在内不能大于IP协议允许的最大值65535。
- 如果设置了IP_HDRINCL选项,用户需要自己构造IP首部及其后面的数据,提供给系统的数据长度也包括IP首部在内。
如果IP首部后面的数据需要校验和,则必须自己计算。
对IP首部,校验和由系统计算,标识符(Identification)可以设置为0,系统会设置标识符字段。
Windows中填写IP首部时,各个字段的值都要使用网络字节序。 - 未设置IP_HDRINCL选项时,默认值,发送数据时传给系统的缓冲区是IP首部之后的数据,系统会在用户数据前增加一个IP首部,并填写IP首部中的各个字段。
其中协议字段设置为调用socket函数时的第三个参数,源地址是bind的本地地址,没有调用bind时,系统根据外出接口自动设置。
目的地址是connect或sendto中指定的地址。 - 输出数据的长度超过外出网络接口的最大传输单元MTU时,IP协议对数据分片。
输入处理
- 原始套接口接收数据通常用recvfrom,第五个参数from可以返回对方的地址,如果应用程序不关心对方的地址,也可以用recv接收数据。
- 对于IPv4,应用程序接收到的是整个IP数据报,包含IP首部,即总是指向IP首部的第一个字节,不管是否设置IP_HDRINCL选项,IP首部中的所有字段都是网络字节序。
- 如果数据报是分片的,IP协议在收到所有分片后进行重组,并把组装完整的数据报交给原始套接口。
在TCP/IP协议栈接收一个完整的数据报后,它检查所有的套接口,找到与数据报中信息匹配的套接口,并把该数据报复制一份,复制到匹配的套接口中。 - ·调用socket创建套接口时,当第三个参数protocol不为0时,则接收数据报IP首部中的协议字段必须与该值相等,不相等时,该数据报不会传送给这个套接口。如果参数protocol为0,表示套接口不关心协议字段是否匹配,只要其他条件满足,就接收该数据报。
- 当应用程序调用bind函数绑定了一个本地地址时,输入数据报的目的IP地址必须与绑定的地址相等,否则数据报不会交给这个套接口。如果没有指定本地地址,将不检查数据的目的地址。
- 如果调用connect函数规定了对方IP地址,收到数据报的源IP地址要与该地址相等,不相等时,数据报不会交给这个套接口;如果没有指定对方IP地址,协议栈不检查接收数据报的源地址。
因为满足上面条件的原始套接口会收到一份复制的数据报,所以使用原始套接口的应用程序可能会收到很多无关的数据报。
例: Ping程序会创建一个协议类型为ICMP的原始套接口,到达本机的其他ICMP分组,如目的不可达、重定向、时间戳也会交给应用程序,应用程序必须自己提供机制来识别它要处理的数据报,抛弃与它无关的数据报。Ping程序可以通过检查ICMP首部中的标识符来识别它要处理的数据报。
原始套接口的限制
在Windows上使用原始套接口时要求具有管理员权限,如果用户不属于管理员组成员,运行原始套接口程序时,函数调用会失败,错误码为WSAEACCES。
在Windows7、Windows Vista、Windows XP带有Service Pack 2或3上,使用原始套接口时有下面两个限制:
- 不能发送TCP数据,创建协议类型为IPPROTO_TCP的原始套接口,调用bind或sendto函数时会失败,错误码为WSAEINVAL;
- 协议类型为IPPROTO_UDP的原始套接口,输出UDP数据报的源IP地址必须是本机网络接口的地址,如果源IP地址不是本机的,调用sendto时会失败。
在Windows Server 2008 R2、Windows Server 2008、Windows Server 2003或Windows XP(SP2)的早期版本没有上面的限制。
- 接收到TCP分组不会交给任何原始套接口,TCP分组由系统的协议栈处理。进程想要接收包含TCP首部的IP数据报,必须在数据链路层上读取。
- 对于收到的UDP分组, 如果有在数据报的目的端口侦听的UDP套接口,该分组交给UDP套接口处理,不会再交给原始套接口;如果没有在数据报的目的端口侦听的UDP套接口,系统再查找是否有协议类型为UDP的原始套接口,查到则把该分组放到原始套接口的接收缓冲区中。
- 接收到ICMP分组的IP数据报时,Windows在协议栈中帮助处理Echo请求(8)、时间戳请求(13)、地址掩码请求(17),不会把这三种类型的ICMP数据报交给原始套接口,其他的ICMP分组都将交给对应的原始套接口处理。
- 协议栈对无法识别协议字段的IP数据报,都传递给对应的原始套接口。协议栈会对IP数据报做些基本的检查,包括IP版本、长度、校验和、选项及目的地址。
- 当协议栈处理完IGMP后,把所有IGMP分组交给原始套接口。如果原始套接口要接收IGMP分组的话,需要创建协议类型为IPPROTO_IGMP类型的原始套接口,调用bind绑定本地地址,然后用选项IP_ADD_MEMBERSHIP把本机加入一个多播组中,才能接收到IGMP分组。发送时不用bind和加入多播组,直接构造IGMP分组发送即可。
- windows有很多安全限制,使用原始套接口比较麻烦,默认配置,程序接收不到ICMP分组,调用recvfrom函数总是返回WSAETIMEDOUT,需要做如下的配置才能够接收到ICMP分组:
- 管理员身份, 否则使用原始套接口会失败。
- 关闭UAC, User Account Control(UAC)用户账户控制, 默认是打开的,只允许写到注册表中经过认证的应用程序。
控制面板→ 用户账户 → 更改用户账户控制设置 - 更改/关闭防火墙, 默认不允许输入的ICMPv4分组
Demo–Ping程序
ping用来检查网络的连通性、另一台主机是否可达、测量两台主机的延迟等。
原理:
- 向目标主机发送一个ICMP类型的IP数据报,IP负载是ICMP的Echo请求,目标主机收到分组后,把ICMP的类型修改为Echo应答,并把同样的数据报返回给发送主机
- Ping过程中,记录了包丢失的个数,往返延迟,总结了发送和接收分组的个数、丢失情况、最小和最大及平均往返延迟。
Ping向目标主机发送一个ICMP的Echo请求,Echo请求中的数据必须在应答中返回。
Echo中的标识符和序列号可以帮助发送者识别相匹配的应答消息,如:可以把标识符设置为发送进程的ID,每次发送Echo请求都把序列号加1。另外,为了计算往返时间,通常在可选数据中保存发送Echo请求的时间戳。
ICMP规定:接收者在应答中把标识符、序列号及可选数据返回给发送者。
Ping的负面影响
- 暴露目标主机信息,可以确定目标主机的存在,解析IP首部字段,初步判断目标主机使用的操作系统。
- Ping死亡攻击(Ping of Death),默认情况,Ping发送的数据大小是32字节,包含IP和ICMP首部时是60字节。
当Ping数据报大于IPv4最大允许长度65535时,许多系统都不能处理。Windows为了解决这一漏洞,对Ping数据报的大小做了限制,最多允许发送65500字节的数据,超过这个限制时会失败。 - 拒绝服务攻击。持续地向同一台主机发送大量的Ping数据报,制造ICMP风暴,抢占了大量网络带宽
#include <stdio.h> #include <winsock2.h> #pragma comment(lib, "ws2_32.lib") /* WinSock使用的库函数 */ /* ICMP类型 */ #define ICMP_TYPE_ECHO 8 #define ICMP_TYPE_ECHO_REPLY 0 #define ICMP_MIN_LEN 8 /* ICMP最小长度, 只有首部 */ #define ICMP_DEF_COUNT 4 /* 默认数据次数 */ #define ICMP_DEF_SIZE 32 /* 默认数据长度 */ #define ICMP_DEF_TIMEOUT 1000 /* 默认超时时间, 毫秒 */ #define ICMP_MAX_SIZE 65500 /* 最大数据长度 */ /* IP首部 -- RFC 791 */ struct ip_hdr { unsigned char vers_len; /* 版本和首部长度 */ unsigned char tos; /* 服务类型 */ unsigned short total_len; /* 数据报的总长度 */ unsigned short id; /* 标识符 */ unsigned short frag; /* 标志和片偏移 */ unsigned char ttl; /* 生存时间 */ unsigned char proto; /* 协议 */ unsigned short checksum; /* 校验和 */ unsigned int sour; /* 源IP地址 */ unsigned int dest; /* 目的IP地址 */ }; /* ICMP首部 -- RFC 792 */ struct icmp_hdr { unsigned char type; /* 类型 */ unsigned char code; /* 代码 */ unsigned short checksum; /* 校验和 */ unsigned short id; /* 标识符 */ unsigned short seq; /* 序列号 */ /* 这之后的不是标准ICMP首部, 用于记录时间 */ unsigned long timestamp; }; struct icmp_user_opt { unsigned int persist; /* 一直Ping */ unsigned int count; /* 发送Echo请求的数量 */ unsigned int size; /* 发送数据的大小 */ unsigned int timeout; /* 等待答复的超时时间 */ char *host; /* 主机地址 */ unsigned int send; /* 发送数量 */ unsigned int recv; /* 接收数量 */ unsigned int min_t; /* 最短时间 */ unsigned int max_t; /* 最长时间 */ unsigned int total_t; /* 总的累计时间 */ }; /* 随机数据 */ const char icmp_rand_data[] = "abcdefghigklmnopqrstuvwxyz0123456789" "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; struct icmp_user_opt user_opt_g = { 0, ICMP_DEF_COUNT, ICMP_DEF_SIZE, ICMP_DEF_TIMEOUT, NULL, 0, 0, 0xFFFF,0, 0 }; /* * 计算校验和 http://t.zoukankan.com/Evil-Rebe-p-5043765.html */ unsigned short ip_checksum(unsigned short *buf, int buf_len){ unsigned long cksum = 0; while(buf_len>1) { cksum += *buf++; buf_len -= sizeof(unsigned short); } if(buf_len) { cksum += *(unsigned char*)buf; } cksum = (cksum>>16) + (cksum&0xffff); cksum += (cksum>>16); return (unsigned short)(~cksum); } /** * 构造ICMP数据 * @param icmp_data 数据缓冲区 * @param data_size 缓冲区长度 * @param sequence ICMP序列号 */ void icmp_make_data(char *icmp_data, int data_size, int sequence) { struct icmp_hdr *icmp_hdr; char *data_buf; int data_len; int fill_count = sizeof(icmp_rand_data) / sizeof(icmp_rand_data[0]); /* 填写ICMP数据 */ data_buf = icmp_data + sizeof(struct icmp_hdr); data_len = data_size - sizeof(struct icmp_hdr); while (data_len > fill_count) { memcpy(data_buf, icmp_rand_data, fill_count); data_len -= fill_count; } if (data_len > 0) memcpy(data_buf, icmp_rand_data, data_len); /* 填写ICMP首部 */ icmp_hdr = (struct icmp_hdr *)icmp_data; icmp_hdr->type = ICMP_TYPE_ECHO; icmp_hdr->code = 0; icmp_hdr->id = (unsigned short)GetCurrentProcessId(); icmp_hdr->checksum = 0; icmp_hdr->seq = sequence; icmp_hdr->timestamp = GetTickCount(); icmp_hdr->checksum = ip_checksum((unsigned short*)icmp_data, data_size); } /** * 解析接收到的ICMP响应 * @param buf 接收到的缓冲区 * @param buf_len 数据的长度 * @param from 对方的ip */ int icmp_parse_reply(char *buf, int buf_len,struct sockaddr_in *from) { /* hdr_len : ip首部长度 icmp_len: icmp头部+ 数据的长度 buf_len: icmp_数据的长度 ( 最后会赋为这个值 ) */ struct ip_hdr *ip_hdr; struct icmp_hdr *icmp_hdr; unsigned short hdr_len; int icmp_len; unsigned long trip_t; ip_hdr = (struct ip_hdr *)buf; hdr_len = (ip_hdr->vers_len & 0xf) << 2 ; /* IP首部长度 */ if (buf_len < hdr_len + ICMP_MIN_LEN) { printf("[Ping] Too few bytes from %s\n", inet_ntoa(from->sin_addr)); return -1; } icmp_hdr = (struct icmp_hdr *)(buf + hdr_len); icmp_len = ntohs(ip_hdr->total_len) - hdr_len; /* 检查校验和 */ if (ip_checksum((unsigned short *)icmp_hdr, icmp_len)) { printf("[Ping] icmp checksum error!\n"); return -1; } /* 检查ICMP类型 */ if (icmp_hdr->type != ICMP_TYPE_ECHO_REPLY) { printf("[Ping] not echo reply : %d\n", icmp_hdr->type); return -1; } /* 检查ICMP的ID */ if (icmp_hdr->id != (unsigned short)GetCurrentProcessId()) { printf("[Ping] someone else's message!\n"); return -1; } /* 输出响应信息 */ trip_t = GetTickCount() - icmp_hdr->timestamp; buf_len = ntohs(ip_hdr->total_len) - hdr_len - ICMP_MIN_LEN; printf("%d bytes from %s:", buf_len, inet_ntoa(from->sin_addr)); printf(" icmp_seq = %d time: %d ms\n",icmp_hdr->seq, trip_t); user_opt_g.recv++; user_opt_g.total_t += trip_t; /* 记录返回时间 */ if (user_opt_g.min_t > trip_t) user_opt_g.min_t = trip_t; if (user_opt_g.max_t < trip_t) user_opt_g.max_t = trip_t; return 0; } /** * 接收数据并处理ICMP响应 * @param icmp_soc 原始套接字描述符 */ int icmp_process_reply(SOCKET icmp_soc) { /* 从原始套接字中接收到的数据包含了IP首部 */ struct sockaddr_in from_addr; int result, data_size = user_opt_g.size; int from_len = sizeof(from_addr); char *recv_buf; data_size += sizeof(struct ip_hdr) + sizeof(struct icmp_hdr); recv_buf = (char *)malloc(data_size); /* 接收数据 */ result = recvfrom(icmp_soc, recv_buf, data_size, 0, (struct sockaddr*)&from_addr, &from_len); if (result == SOCKET_ERROR) { if (WSAGetLastError() == WSAETIMEDOUT) printf("timed out\n"); else printf("[PING] recvfrom_ failed: %d\n", WSAGetLastError()); return -1; } result = icmp_parse_reply(recv_buf, result, &from_addr); free(recv_buf); return result; } /** * 打印帮助信息 * @param prog_name 文件名(xxx.c 或者 xxx\\yyy.c) */ void icmp_help(char *prog_name) { char *file_name; file_name = strrchr(prog_name, '\\'); if (file_name != NULL) file_name++; else file_name = prog_name; /* 显示帮助信息 */ printf(" usage: %s host_address [-t] [-n count] [-l size] " "[-w timeout]\n", file_name); printf(" -t Ping the host until stopped.\n"); printf(" -n count the count to send ECHO\n"); printf(" -l size the size to send data\n"); printf(" -w timeout timeout to wait the reply\n"); exit(1); // 直接退出 } /** * 解析用户的输入命令行参数 * t 一直ping * n count ping的次数 * l size 发送数据的大小 * w timeout 等待响应的时间 */ void icmp_parse_param(int argc, char **argv) { int i; for(i = 1; i < argc; i++) { if ((argv[i][0] != '-') && (argv[i][0] != '/')) { /* 处理主机名 */ if (user_opt_g.host) icmp_help(argv[0]); else { user_opt_g.host = argv[i]; continue; } } switch (tolower(argv[i][1])) { case 't': /* 持续Ping */ user_opt_g.persist = 1; break; case 'n': /* 发送请求的数量 */ i++; user_opt_g.count = atoi(argv[i]); break; case 'l': /* 发送数据的大小 */ i++; user_opt_g.size = atoi(argv[i]); if (user_opt_g.size > ICMP_MAX_SIZE) user_opt_g.size = ICMP_MAX_SIZE; break; case 'w': /* 等待接收的超时时间 */ i++; user_opt_g.timeout = atoi(argv[i]); break; default: icmp_help(argv[0]); break; } } } int main(int argc, char **argv) { WSADATA wsaData; SOCKET icmp_soc; struct sockaddr_in dest_addr; struct hostent *host_ent = NULL; int result, data_size, send_len; unsigned int i, timeout, lost; char *icmp_data; unsigned int ip_addr = 0; unsigned short seq_no = 0; if (argc < 2) icmp_help(argv[0]); icmp_parse_param(argc, argv); WSAStartup(MAKEWORD(2,0),&wsaData); /* 解析主机地址 */ ip_addr = inet_addr(user_opt_g.host); if (ip_addr == INADDR_NONE) { host_ent = gethostbyname(user_opt_g.host); if (!host_ent) { printf("[PING] Fail to resolve %s\n", user_opt_g.host); return -1; } memcpy(&ip_addr, host_ent->h_addr_list[0], host_ent->h_length); } icmp_soc = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); if (icmp_soc == INVALID_SOCKET) { printf("[PING] socket() failed: %d\n", WSAGetLastError()); return -1; } /* 设置选项, 接收和发送的超时时间 */ timeout = user_opt_g.timeout; result = setsockopt(icmp_soc, SOL_SOCKET, SO_RCVTIMEO, (char*)&timeout, sizeof(timeout)); timeout = 1000; result = setsockopt(icmp_soc, SOL_SOCKET, SO_SNDTIMEO, (char*)&timeout, sizeof(timeout)); memset(&dest_addr,0,sizeof(dest_addr)); dest_addr.sin_family = AF_INET; dest_addr.sin_addr.s_addr = ip_addr; data_size = user_opt_g.size + sizeof(struct icmp_hdr) - sizeof(long); icmp_data = (char *)malloc(data_size); if (host_ent) printf("Ping %s [%s] with %d bytes data\n", user_opt_g.host, inet_ntoa(dest_addr.sin_addr), user_opt_g.size); else printf("Ping [%s] with %d bytes data\n", inet_ntoa(dest_addr.sin_addr), user_opt_g.size); /* 发送请求并接收响应 */ for (i = 0; i < user_opt_g.count; i++) { icmp_make_data(icmp_data, data_size, seq_no++); send_len = sendto(icmp_soc, icmp_data, data_size, 0, (struct sockaddr*)&dest_addr, sizeof(dest_addr)); if (send_len == SOCKET_ERROR) { if (WSAGetLastError() == WSAETIMEDOUT) { printf("[PING] sendto is timeout\n"); continue; } printf("[PING] sendto failed: %d\n", WSAGetLastError()); break; } user_opt_g.send++; result = icmp_process_reply(icmp_soc); user_opt_g.persist ? i-- : i; /* 持续Ping */ Sleep(1000); /* 延迟 1 秒 */ } lost = user_opt_g.send - user_opt_g.recv; /* 打印统计数据 */ printf("\nStatistic :\n"); printf(" Packet : sent = %d, recv = %d, lost = %d (%3.f%% lost)\n", user_opt_g.send, user_opt_g.recv, lost, (float)lost*100/user_opt_g.send); if (user_opt_g.recv > 0) { printf("Roundtrip time (ms)\n"); printf(" min = %d ms, max = %d ms, avg = %d ms\n", user_opt_g.min_t, user_opt_g.max_t, user_opt_g.total_t / user_opt_g.recv); } free(icmp_data); closesocket(icmp_soc); WSACleanup(); return 0; }