希望和广大热爱技术的童鞋一起交流,成长。
分类: LINUX
2011-07-22 13:26:30
原文地址:(13)原始套接字 作者:g_programming
(13)原始套接字
注:所以文章红色字体代表需要特别注意和有问题还未解决的地方,蓝色字体表示需要注意的地方
1. 本文所介绍的程序平台
开发板:arm9-mini2440
虚拟机为:Red Hat Enterprise Linux 5
开发板上系统内核版本:linux-2.6.32.2
2. 原始套接字概述
通常情况下程序设计人员接触的网络知识限于如下两类:
(1)流式套接字(SOCK_STREAM),它是一种面向连接的套接字,对应于TCP应用程序。
(2)数据报套接字(SOCK_DGRAM),它是一种无连接的套接字,对应于的UDP应用程序。
除了以上两种基本的套接字外还有一类原始套接字,它是一种对原始网络报文进行处理的套接字。
前面几章介绍了基础的套接字知识,流式套接字(SOCK_STREAM)和数据报套接字(SOCK_DGRAM)涵盖了一般应用层次的TCP/IP应用。
原始套接字的创建使用与通用的套接字创建的方法是一致的,只是在套接字类型的选项上使用的是另一个SOCK_RAW。在使用socket函数进行函数创建完毕的时候,还要进行套接字数据中格式类型的指定,设置从套接字中可以接收到的网络数据格式。
创建原始套接字使用函数socket,第二个参数设置为SOCK_RAW,函数socket()可以创建一个原始套接字。下面的代码,创建一个AF_INET协议族中的原始套接字,协议类型为protocol。
int rawsock = socket(AF_INET, SOCK_RAW, protocol);
注意:只有超级用户才有权利创建套接字,否则函数返回-1,并设置errno为EACCES。
protocol参数:是一个常量定义在
IPPROTO_IP = 0, /* Dummy protocol for TCP. */
#define IPPROTO_IP IPPROTO_IP
IPPROTO_HOPOPTS = 0, /* IPv6 Hop-by-Hop options. */
#define IPPROTO_HOPOPTS IPPROTO_HOPOPTS
IPPROTO_ICMP = 1, /* Internet Control Message Protocol. */
#define IPPROTO_ICMP IPPROTO_ICMP
IPPROTO_IGMP = 2, /* Internet Group Management Protocol. */
#define IPPROTO_IGMP IPPROTO_IGMP
IPPROTO_IPIP = 4, /* IPIP tunnels (older KA9Q tunnels use 94). */
#define IPPROTO_IPIP IPPROTO_IPIP
IPPROTO_TCP = 6, /* Transmission Control Protocol. */
#define IPPROTO_TCP IPPROTO_TCP
IPPROTO_EGP = 8, /* Exterior Gateway Protocol. */
#define IPPROTO_EGP IPPROTO_EGP
IPPROTO_PUP = 12, /* PUP protocol. */
#define IPPROTO_PUP IPPROTO_PUP
IPPROTO_UDP = 17, /* User Datagram Protocol. */
#define IPPROTO_UDP IPPROTO_UDP
IPPROTO_IDP = 22, /* XNS IDP protocol. */
#define IPPROTO_IDP IPPROTO_IDP
IPPROTO_TP = 29, /* SO Transport Protocol Class 4. */
#define IPPROTO_TP IPPROTO_TP
IPPROTO_IPV6 = 41, /* IPv6 header. */
#define IPPROTO_IPV6 IPPROTO_IPV6
IPPROTO_ROUTING = 43, /* IPv6 routing header. */
#define IPPROTO_ROUTING IPPROTO_ROUTING
IPPROTO_FRAGMENT = 44, /* IPv6 fragmentation header. */
#define IPPROTO_FRAGMENT IPPROTO_FRAGMENT
IPPROTO_RSVP = 46, /* Reservation Protocol. */
#define IPPROTO_RSVP IPPROTO_RSVP
IPPROTO_GRE = 47, /* General Routing Encapsulation. */
#define IPPROTO_GRE IPPROTO_GRE
IPPROTO_ESP = 50, /* encapsulating security payload. */
#define IPPROTO_ESP IPPROTO_ESP
IPPROTO_AH = 51, /* authentication header. */
#define IPPROTO_AH IPPROTO_AH
IPPROTO_ICMPV6 = 58, /* ICMPv6. */
#define IPPROTO_ICMPV6 IPPROTO_ICMPV6
IPPROTO_NONE = 59, /* IPv6 no next header. */
#define IPPROTO_NONE IPPROTO_NONE
IPPROTO_DSTOPTS = 60, /* IPv6 destination options. */
#define IPPROTO_DSTOPTS IPPROTO_DSTOPTS
IPPROTO_MTP = 92, /* Multicast Transport Protocol. */
#define IPPROTO_MTP IPPROTO_MTP
IPPROTO_ENCAP = 98, /* Encapsulation Header. */
#define IPPROTO_ENCAP IPPROTO_ENCAP
IPPROTO_PIM = 103, /* Protocol Independent Multicast. */
#define IPPROTO_PIM IPPROTO_PIM
IPPROTO_COMP = 108, /* Compression Header Protocol. */
#define IPPROTO_COMP IPPROTO_COMP
IPPROTO_SCTP = 132, /* Stream Control Transmission Protocol. */
#define IPPROTO_SCTP IPPROTO_SCTP
IPPROTO_RAW = 255, /* Raw IP packets. */
#define IPPROTO_RAW IPPROTO_RAW
IPPROTO_MAX
使用套接字选项IP_HDRINCL设置套接字,在之后进行的接收和发送的时候,接收到的数据包含IP数据的,包含IP的头部,否则在默认的情况下不能构造自己的IP头部。用户之后需要对IP层相关的数据段进行处理,例如IP头部数据的设置和分析,校验和的计算等。设置方法如下:
int set = 1;
if(setsockopt(rawsock, IPPROTO_IP, IP_HDRINCL, &set, sizeof(set))<0){
}
3.读写原始套接字
原始套接字不需要使用bind()函数,因为进行发送和接收数据的时候可以指定要发送和接收的目的地址的IP。例如使用函数sendto(),sendmsg()和函数recvfrom(),recvmsg()来发送和接收数据,sendto(),sendmsg()和recvfrom(),recvmsg()函数分别需要指定IP地址。
在默认的情况下,写入的数据将由系统内核填入IP包的数据域,而IP头由系统内核自动产生,如果设置了IP套接字选项,则写入等待数据将从IP头的第一个字节开始写入,内核只负责填充IP头的检验和。当包的长度大于MTU(最大传输单位)时,系统会自动拆包。
sendto (rawsock, data, datasize, 0, (struct sockaddr *) &to, sizeof (to));
recvfrom(rawsock, data,size , 0,(struct sockaddr)&from, &len) ;
当系统对socket进行了绑定的时候,发送和接收的函数可以使用send()和recv()及read()和write()等不需要指定目的地址的函数。
原始套接字发送报文有如下的原则:
通常情况下可以使用sendto()函数并指定发送目的地址发送数据,当已经bind()了目标地址的时候可以使用write()或者send()发送数据。
如果使用setsockopt()设置了选项IP_RINCL,则发送的数据缓冲区指向IP头部第一个字节的头部,用户发送的数据包含IP头部之后的所有数据,需要用户自己填写IP头部和计算校验和及所包含数据的处理和计算。
如果没有设置IP_RINCL,则发送缓冲区指向IP头部后面数据区域的第一个字节,不需要用户填写IP头部,IP头部的填写工作有内核进行,内核还进行校验和的计算。
接收报文还有自己的一些特点,主要有如下几个:
(1)对于ICMP的协议,绝大部分数据可以通过原始套接字获得,例如回显请求、响应,时间戳请求等。
(2)接收的UDP和TCP协议的数据不会传给任何原始套接字接口,这些协议的数据需要通过数据链路层获得。
(3)如果IP以分片形式到达,则所有分片都已经接收到并重组后才传给原始套接字。
(4)内核不能识别的协议、格式等传给原始套接字,因此,可以使用原始套接字定义用户自己的协议格式。
(5)原始套接字只能接收如下数据包:
ICMP包
IGMP包
协议域不被系统内核理解的IP包,对于这些IP包,系统内核仅校验其IP版本、头校验、头长度以及目的地址等。
当系统内核收到需要传递给原始套接字的IP包后,要检查所有进程产生的原始套接字,然后将IP包拷贝给所有匹配的原始套接字,系统采用以下原则进行匹配。
(1)如果IP包的协议域是非0值,则只有原始套接字的协议域与之完全匹配,才将IP包传递给该原始套接字;
(2)如果原始套接字绑定到本地IP地址上,则IP包的目的地址必须与套接字绑定的地址匹配,否则不传递IP包给该原始套接字;
(3)如果原始套接字通过connect()函数与远程IP地址连接,则IP包的源地址必须与该远程IP地址匹配,否则不传递IP包给该原始套接字;
(4)如果原始套接字的协议域是0,并且没有绑定或者连接到任何IP地址上,则该原始套接字将接收所有发送给原始套接字的IP包。
4. ICMP简介
ICMP(Internet Control Message,网际控制报文协议)是为网关和目标主机而提供的一种差错控制机制,使它们在遇到差错时能把错误报告给报文源发方。ICMP协议是IP层的一个协议,但是由于差错报告在发送给报文源发方时可能也要经过若干子网,因此牵涉到路由选择等问题,所以ICMP报文需通过IP协议来发送。ICMP数据报的数据发送前需要两级封装:首先添加ICMP报头形成ICMP报文,再添加IP报头形成IP数据报。如下图所示
IP报头 |
ICMP报头 |
ICMP数据报 |
报头格式
由于IP层协议是一种点对点的协议,而非端对端的协议,它提供无连接的数据报服务,没有端口的概念,因此很少使用bind()和connect() 函数,若有使用也只是用于设置IP地址。发送数据使用sendto()函数,接收数据使用recvfrom()函数。IP报头格式如下图:
在Linux中,IP报头格式数据结构(
其中ping程序只使用以下数据:
· IP报头长度IHL(Internet Header Length)以4字节为一个单位来记录IP报头的长度,是上述IP数据结构的ip_hl变量。
· 生存时间TTL(Time To Live)以秒为单位,指出IP数据报能在网络上停留的最长时间,其值由发送方设定,并在经过路由的每一个节点时减一,当该值为0时,数据报将被丢弃,是上述IP数据结构的ip_ttl变量。
报头格式
ICMP报文分为两种,一是错误报告报文,二是查询报文。每个ICMP报头均包含类型、编码和校验和这三项内容,长度为8位,8位和16位,其余选项则随ICMP的功能不同而不同。
Ping命令只使用众多ICMP报文中的两种:"请求回送'(ICMP_ECHO)和"请求回应'(ICMP_ECHOREPLY)。在Linux中定义如下:
|
这两种ICMP类型报头格式如下:
ICMP结构如下的形式:定义在
struct icmp_ra_addr
{
u_int32_t ira_addr;
u_int32_t ira_preference;
};
struct icmp
{
u_int8_t icmp_type; /* type of message, see below */
//这是类型域:ICMP_ECHO(ICMP响应请求)或ICMP_ECHOREPLY(ICMP响应应答)
u_int8_t icmp_code; /* type sub code */ 、 //这是代码域
u_int16_t icmp_cksum; /* ones complement checksum of struct */ //校验和域
/*注意下面用了union结构,这是因为icmp除了探测主机,即ping外还有其他的功能,根据功能码的不同每次发出去的下面的数据段结构其实是不一样的,在这里我们只需用到struct ih_idseq 因为这是ping所需要的结构,其他的icmp管理,我们暂时不用管,因为一次只会用到一种数据类型,所以用union定义一个最大的空间*/
union //发送的各个data的最大值
{
u_char ih_pptr; /* ICMP_PARAMPROB */
struct in_addr ih_gwaddr; /* gateway address */
struct ih_idseq /* echo datagram */ //echo是我们关心程序发送给主机用来探测的
{
u_int16_t icd_id; //这个是id
u_int16_t icd_seq; //这个是seq 就是包的顺序
} ih_idseq;
u_int32_t ih_void;
/* ICMP_UNREACH_NEEDFRAG -- Path MTU Discovery (RFC1191) */
struct ih_pmtu //这个是关于MTU路径的。不用
{
u_int16_t ipm_void;
u_int16_t ipm_nextmtu;
} ih_pmtu;
struct ih_rtradv
{
u_int8_t irt_num_addrs;
u_int8_t irt_wpa;
u_int16_t irt_lifetime;
} ih_rtradv;
} icmp_hun;
#define icmp_pptr icmp_hun.ih_pptr //这个就不多说了,为了使用方便的定义
#define icmp_gwaddr icmp_hun.ih_gwaddr
#define icmp_id icmp_hun.ih_idseq.icd_id//标识域
#define icmp_seq icmp_hun.ih_idseq.icd_seq//序列号域
#define icmp_void icmp_hun.ih_void
#define icmp_pmvoid icmp_hun.ih_pmtu.ipm_void
#define icmp_nextmtu icmp_hun.ih_pmtu.ipm_nextmtu
#define icmp_num_addrs icmp_hun.ih_rtradv.irt_num_addrs
#define icmp_wpa icmp_hun.ih_rtradv.irt_wpa
#define icmp_lifetime icmp_hun.ih_rtradv.irt_lifetime
union //这里定义必要的主机回复的Union结构,Ping中需要用的是id_ts;代表响应时间
{
struct
{
u_int32_t its_otime;
u_int32_t its_rtime;
u_int32_t its_ttime;
} id_ts;
struct
{
struct ip idi_ip;
/* options and then 64 bits of data */
} id_ip;
struct icmp_ra_addr id_radv
u_int32_t id_mask;
u_int8_t id_data[1];
} icmp_dun;
#define icmp_otime icmp_dun.id_ts.its_otime //这个同上
#define icmp_rtime icmp_dun.id_ts.its_rtime
#define icmp_ttime icmp_dun.id_ts.its_ttime
#define icmp_ip icmp_dun.id_ip.idi_ip
#define icmp_radv icmp_dun.id_radv
#define icmp_mask icmp_dun.id_mask
#define icmp_data icmp_dun.id_data//可选数据域
};
使用宏定义令表达更简洁,其中ICMP报头为8字节,数据报长度最大为64K字节。
1. 校验和算法:这一算法称为网际校验和算法,把被校验的数据16位进行累加,然后取反码,若数据字节长度为奇数,则数据尾部补一个字节的0以凑成偶数。此算法适用于IPv4、ICMPv4、IGMPV4、ICMPv6、UDP和TCP校验和,更详细的信息请参考RFC1071,校验和字段为上述ICMP 数据结构的icmp_cksum变量。
2. 标识符:用于唯一标识ICMP报文, 为上述ICMP数据结构的icmp_id宏所指的变量。
3. 顺序号:ping命令的icmp_seq便由这里读出,代表ICMP报文的发送顺序,为上述ICMP数据结构的icmp_seq宏所指的变量。
数据报
Ping命令中需要显示的信息,包括icmp_seq和ttl都已有实现的办法,但还缺rtt往返时间。为了实现这一功能,可利用ICMP数据报携带一个时间戳。使用以下函数生成时间戳:
|
其中tv_sec为秒数,tv_usec微秒数。在发送和接收报文时由gettimeofday分别生成两个timeval结构,两者之差即为往返时间,即 ICMP报文发送与接收的时间差,而timeval结构由ICMP数据报携带,tzp指针表示时区,一般都不使用,赋NULL值。
系统自带的ping命令当它接送完所有ICMP报文后,会对所有发送和所有接收的ICMP报文进行统计,从而计算ICMP报文丢失的比率。为达此目的,定义两个全局变量:接收计数器和发送计数器,用于记录ICMP报文接受和发送数目。丢失数目=发送总数-接收总数,丢失比率=丢失数目/发送总数。
5. 各种协议头
本节介绍进行报文处理时常用的数据结构,包含IP头部、ICMP头部、UDP头部、TCP头部。使用这些数据格式对原始套接字进行处理,可以从底层获取高层的网络数据。
IP头部的结构
ICMP头部结构
ICMP的头部结构比较复杂,主要包含消息类型icmp_type,消息代码icmp_code、校验和icmp_cksum等,不同的ICMP类型其他部分有不同的实现。
1.ICMP的头部结构
2.不同类型的ICMP请求
UDP头部结构
TCP头部结构
TCP的头部结构主要包含发送端的源端口、接收端的目的端口、数据的序列号、上一个数据的确认号、滑动窗口大小、数据的校验和、紧急数据的偏移指针以及一些控制位等信息。
5.ping命令
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define PACKET_SIZE 4096
#define ERROR 0
#define SUCCESS 1
void icmp_type_name (int id) //定义返回类型,提示出我们看到的信息,其中,ICMP_ECHOREPLY是正确的返回,ICMP_ECHO是正确的发送
{
switch (id) {
case ICMP_ECHOREPLY: printf( "Echo Reply\n"); break;
case ICMP_DEST_UNREACH: printf ("Destination Unreachable\n");break;
case ICMP_SOURCE_QUENCH: printf ("Source Quench\n");break;
case ICMP_REDIRECT: printf ("Redirect (change route)\n");break;
case ICMP_ECHO: printf ("Echo Request\n");break;
case ICMP_TIME_EXCEEDED: printf ("Time Exceeded\n");break;
case ICMP_PARAMETERPROB: printf ("Parameter Problem\n");break;
case ICMP_TIMESTAMP: printf ("Timestamp Request\n");break;
case ICMP_TIMESTAMPREPLY: printf ("Timestamp Reply\n");break;
case ICMP_INFO_REQUEST: printf ("Information Request\n");break;
case ICMP_INFO_REPLY: printf ("Information Reply\n");break;
case ICMP_ADDRESS: printf ("Address Mask Request\n");break;
case ICMP_ADDRESSREPLY: printf ("Address Mask Reply\n");break;
default: printf ("unknown ICMP type\n");break;
}
}
// 效验算法
unsigned short cal_chksum(unsigned short *addr, int len)
{
int nleft=len;
int sum=0;
unsigned short *w=addr;
unsigned short answer=0;
/*把ICMP报头二进制数据以2字节为单位累加起来*/
while(nleft > 1)
{
sum += *w++;
nleft -= 2;
}
/*若ICMP报头为奇数个字节,会剩下最后一字节
。把最后一个字节视为一个2字节数据的高字节
,这个2字节数据的低字节为0,继续累加*/
if( nleft == 1)
{
*(unsigned char *)(&answer) = *(unsigned char *)w;
sum += answer;
}
sum = (sum >> 16) + (sum & 0xffff);
sum += (sum >> 16);
answer = ~sum;
return answer;
}
/*两个timeval结构相减 计算时间差*/
void tv_sub(struct timeval *out,struct timeval *in)
{ if( (out->tv_usec-=in->tv_usec)<0)
{ --out->tv_sec;
out->tv_usec+=1000000;
}
out->tv_sec-=in->tv_sec;
}
/*设置ICMP报头,返回报文长度*/
int pack(int pack_no, char *sendpacket)
{ int i,packsize;
int datalen = 56;//为啥是56??
struct icmp *icmp;
struct timeval *tval;
icmp=(struct icmp*)sendpacket;
icmp->icmp_type=ICMP_ECHO;
icmp->icmp_code=0;
icmp->icmp_cksum=0;
icmp->icmp_seq=pack_no;
// 取得PID,作为Ping的Sequence ID
icmp->icmp_id=getpid();
packsize=8+datalen;
tval= (struct timeval *)icmp->icmp_data;
gettimeofday(tval,NULL); /*记录发送时间*/
icmp->icmp_cksum=cal_chksum((unsigned short *)icmp,packsize); /*校验算法*/
return packsize;
}
/*剥去ICMP报头*/
int unpack(char *buf,int len, struct sockaddr_in from, char *ips)
{ int i,iphdrlen;
struct ip *ip;
struct icmp *icmp;
struct timeval *tvsend;
struct timeval *tvrecv;
double time;
char *from_ip;
gettimeofday(tvrecv,NULL); /*记录接收时间*/
ip=(struct ip *)buf;
iphdrlen=ip->ip_hl<<2; /*求ip报头长度,即ip报头的长度标志乘4*/
icmp=(struct icmp *)(buf+iphdrlen); /*越过ip报头,指向ICMP报头*/
len-=iphdrlen; /*ICMP报头及ICMP数据报的总长度*/
if( len<8) /*小于ICMP报头长度则不合理*/
{ printf("ICMP packets\'s length is less than 8\n");
return -1;
}
// 判断是否是自己Ping的回复
from_ip = (char *)inet_ntoa(from.sin_addr);
printf("fomr ip:%s\n",from_ip);
if (strcmp(from_ip,ips) != 0)
{
printf("ip:%s,Ip wang\n",ips);
return -1;
}
/*确保所接收的是我所发的的ICMP的回应*/
if( (icmp->icmp_type==ICMP_ECHOREPLY) && (icmp->icmp_id==getpid()) )
{ tvsend=(struct timeval *)icmp->icmp_data;
tv_sub(tvrecv,tvsend); /*接收和发送的时间差*/
time=tvrecv->tv_sec*1000+tvrecv->tv_usec/1000; /*以毫秒为单位计算rtt*/
/*显示相关信息*/
printf("%d byte from %s: icmp_seq=%u ttl=%d rtt=%.3f ms and reply type : ",
len,
inet_ntoa(from.sin_addr),
icmp->icmp_seq,
ip->ip_ttl,
time
);
icmp_type_name(icmp->icmp_type);
return 0;
}
else
{
return -1;
icmp_type_name(icmp->icmp_type);
};
}
// Ping函数
int ping( char *ips, int timeout)
{
struct timeval timeo;
int sockfd;
struct sockaddr_in addr;
struct sockaddr_in from;
struct timeval *tval;
socklen_t fromlen ;
int i,packsize;
char sendpacket[PACKET_SIZE];
char recvpacket[PACKET_SIZE];
int n;
int maxfds = 0;
fd_set readfds;
// 设定Ip信息
bzero(&addr,sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(ips);
// 取得socket
sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
if (sockfd < 0)
{
printf("ip:%s,socket error\n",ips);
return ERROR;
}
// 设定TimeOut时间
timeo.tv_sec = timeout / 1000;
timeo.tv_usec = timeout % 1000;
/*在send(),recv()过程中有时由于网络状况等原因
,发收不能预期进行,而设置收发时限:
int nNetTimeout=1000;//1秒
//发送时限
setsockopt(socket,SOL_S0CKET,SO_SNDTIMEO,(char *)&nNetTimeout,sizeof(int));
*/
if (setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &timeo, sizeof(timeo)) == -1)
{
printf("ip:%s,setsockopt error\n",ips);
return ERROR;
}
//发送10个包数据
for(i = 0 ; i< 10; i++)
{
// 设定Ping包
memset(sendpacket, 0, sizeof(sendpacket));
packsize=pack(i, sendpacket);
// 发包
n = sendto(sockfd, (char *)&sendpacket, packsize, 0, (struct sockaddr *)&addr, sizeof(addr));
if (n < 1)
{
printf("ip:%s,sendto error\n",ips);
return ERROR;
}
}
i = 0;
// 接受
// 由于可能接受到其他Ping的应答消息,所以这里要用循环
while(1)
{
// 设定TimeOut时间,这次才是真正起作用的
FD_ZERO(&readfds);
FD_SET(sockfd, &readfds);
maxfds = sockfd + 1;
n = select(maxfds, &readfds, NULL, NULL, &timeo);
if (n <= 0)
{
printf("ip:%s,Time out error\n",ips);
close(sockfd);
return ERROR;
}
// 接受
memset(recvpacket, 0, sizeof(recvpacket));
fromlen = sizeof(from);
n = recvfrom(sockfd, recvpacket, sizeof(recvpacket), 0, (struct sockaddr *)&from, &fromlen);
if (n < 1) {
break;
}
if(unpack(recvpacket, n, from, ips) ==0)
i++;
if(i >= 10)//这里假设10包全部能收到
break;
}
// 关闭socket
close(sockfd);
printf("ip:%s,Success\n",ips);
return SUCCESS;
}
int main(int argc, char** argv)
{
if(argc<2)
{ printf("usage:%s hostname/IP address\n",argv[0]);
exit(1);
}
if(ping(argv[1], 10) == ERROR)
printf("ping error \n");
return 0;
}