原始套接字
一、概述
- 发送一个自定义的IP包。
- 发送ICMP数据报。
- 网卡的侦听模式,监听网络上的数据包。
- 伪装IP地址。
- 自定义协议的实现。
要解决上面这些问题需要原始套接字。原始套接字主要应用在底层网络编程上,之前的TCP、UDP的套接字称为标准套接字,下图所示为标准套接字与原始套接字之间的关系。标准套接字与网络协议栈的TCP、UDP层打交道,而原始套接字则与IP层级网络协议栈核心打交道。
原始套接字提供以下3种标准套接字不具备的功能。
-
使用原始套接字可以读/写
ICMP、IGMP
分组。例:ping程序就使用原始套接字发送ICMP回显请求,并接受ICMP回显应答。用于多播的守护程序mrouted
,同样适用原始套接字来发送和接收IGMP分组。上述功能同样允许使用ICMP或者IGMP
构造的应用程序完全作为用户进程处理,而不必再增加过多的内核编码。例如,路由发现守护进程即以这种方式构造。它处理内核完全不知道的两个ICMP消息。 -
使用原始套接字可以读写特殊的IP数据报,内核不处理这些数据报的协议字段。大多数内核只处理
ICMP 、CIGMP、TCP和UDP
的数据报。但协议字段还可能为其他值。大多数内核只处理CICMP、CIGMP 、TCP和UDP
的数据报。但协议字段还可能为其他值。例如,OSPF路由协议
就不适用TCP或者UDP
,而直接使用IP
, 将IP数据报的协议字段设为89。因此,由于这些数据报包含内核完全不知道的协议字段,实现OSPF协议的gated程序
必须使用原始套接字来读写它们。 -
使用原始套接字,利用函数setsockopt()设置套接字选项,使用
IP _HDRINGCL
可以对IP头部进行操作,因此可以修改IP数据和IP层之上的各层数据,构造自己的特定类型的TCP或者UDP的分组。
二、原始套接字的创建
原始套接字的创建使用与通用套接字创建的方法是一致的,只是在套接字类型的选项上使用的是另 一 个 SOCK_RAW
。在使用 socket()函数进行函数创建完毕的时候,还要进行套接字数据中格式类型的指定,设置从套接字中可以接收到的网络数据格式。
1.SOCK_RAW选项
创建原始套接字使用socket()函数,第二个参数设置为SOCK_ RAW
:
//AF_INET协议族中的原始套接字
//协议类型protocol
int rawsock = socket(AF_INET,SOCK_RAW,protocol);
//protocol, 一般情况下不能设置为0, 需要用户自己设置想要的类型,形如 IPPROTO_ xxx的常量,在文件<netinet/in.h>中定义。
//例:IPPROTO _ICMP 表示是一 个ICM P 协议。
常用协议的类型和含义:
IPPROTO_IP
: IP协议,接收或者发送IP数据包,包含IP头部。IPPROTO_ICMP
: ICMP协议,接收或者发送ICMP的数据包,IP的头部不需要处理。IPPROTO_TCP
: TCP协议,接收或者发送TCP数据包。IPPROTO_UDP
: UDP协议,接收或者发送UDP数据包。IPPROTO _RAW
: 原始IP包。
2.IP_HDRINCL套接字选项
IP_HDRINCL
设置套接字,在之后进行的接收和发送时,接收到的数据包含IP的头部。
用户之后需要对IP层相关的数据段进行处理,例如IP头部数据的设置和分析,校验和的计算等。设置方法如下:
int set = 1;
if(setsocket(rawsock,IPPROTO_IP,IP_HDRINCL,&set,sizeof(set))<0){
//错误处理
}
3.不需绑定bind()函数
原始套接字不需使用bind(函数,因为进行发送和接收数据的时候可以指定要发送和接收的目的地址的IP。
//使用函数sendto()和函数recvfrom()来发送和接收数据,
//sendto()和recvfrom()函数分别需要指定IP地址。
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头部的填写工作由内核进行,内核还进行校验和的计算。
例如
sendto(rawsock,buffer,len,O,(struct sockaddr*)&to,sizeof(to));
//当IP_RINCL已经设置的时候,buffer指向的就是用户构建包含IP头部在内的数据结构
//IP_RINCL没有设置,则buffer指向了IP头部后面缓冲区的数据
//例如后面为ICMP数据报文,则需要填写ICMP的类型、代码等,并计算其校验和。
四、原始套接字接收报文
原始套接字接收报文原则:
recvfrom()或者recv()及read()
获得数据。- 设置了
IP_RINCL
,接收的缓冲区为IP头部的第 一 个字节。 - 没有设置IP_RINCL,接收的缓冲区为IP数据区域的第 一 个字节。
接收报文还有自己的一 些特点,主要有如下几个:
-
ICMP的协议,绝大部分数据可以通过原始套接字获得,例如回显请求、响
应、时间戳请求等。 -
接收的UDP和TCP协议的数据不会传给任何原始套接字接口,这些协议的数据需
要通过数据链路层获得。 -
如果IP以分片形式到达,则所有分片都已经接收到并重组后才传给原始套接字。
-
内核不能识别的协议、格式等传给原始套接字,因此,可以使用原始套接字定义用户自己的协议格式。
原始套接字接收报文的规则如下:
-
若接收的报文数据中的协议类型与自定义的原始套接字匹配,那么将接收的所有数据复制入套接字中。
-
若套接字绑定了本地地址,那么只有当接收的报文数据IP头中的目的地址等于本地地址时,接收到的数据才复制到套接字中;
- 若套接字定义了远端地址,那么,只有接收数据IP头中对应的源地址与远端地址匹配,接收的数据才复制到套接字中。
-
五、原始套接字报文处理时的结构
1.IP头部的结构
IP头部结构:
Linux下结构struct ip的数据类型:
struct ip
{
#if _BYTE ORDER == _LITTLE_ENDIAN //如果为小端
unsigned int ip_hl:4;//头部长度
unsigned int ip_v:4;//版本
#endif
#if _BYTE_ORDER == _BIG_ENDIAN//如果为大端
unsigned int ip_v:4;//版本
unsigned int ip_hl:4;//头部长度
#endif
u_int8_t ip_tos;//TOS,服务类型
u_short ip_len;//总长度
u_short ip_id;//标识值
u_short ip_off;//段偏移值
...
...
u_int8_t ip_ttl;TTL, //生存时间
u_int8_t ip_p;//协议类型
u_short ip_sum;//校验和订
struct irt_addr ip_src, ip_dst;//源地址和目的地址
};
Liunx成员示意图:
2.ICMP头部结构
ICMP的头部结构比较复杂,主要包含消息类型icmp_type
、消息代码icmp_code
、校验和icmp_cksum
等,不同的ICMP类型其他部分有不同的实现。ICMP的头部结构如下图所示。
①.ICMP的头部结构
常用的ICMP报文包括:
- ECHO-REQUEST(响应请求消息)、ECHO-REPLY(响应应答消息)
- DestinationUmeachable (目标不可到达消息)、TimeExceeded (超时消息)
- Parameter Problems(参数错误消息)、SourceQuenchs (源抑制消息)
- Redirects(重定向消息)、Timestamps(时间戳消息)
- TimestampReplies (时间戳响应消息)、AddressMasks (地址掩码请求消息)
- Address Mask Replies (地址掩码响应消息)
ICMP的头部结构代码在Linux下,如下所示:
struct icmp
{
u_int8_t icmp_type; /* 消息类型 */
u_int8_t icmp_code; /* 消息类型的子码 */
u_int16_t icmp_cksum; /* 校验和 */
union
{
u_char ih_pptr; /* ICMP_PARAMPROB */
struct in_addr ih_gwaddr; /* 网关地址 */
struct ih_idseq /* 显示数据报 */
{
u_int16_t icd_id;//数据报ID
u_int16_t icd_seq;//数据报的序号
} ih_idseq;
u_int32_t ih_void;
/* ICMP_UNREACH_NEEDFRAG -- Path MTU Discovery (RFC1191) */
struct ih_pmtu
{
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
{
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请求
子网掩码请求协议的位置如下图所示,增加了标识符icmp_id、序列号icmp_seq和掩码icmp_mask。
时间戳请求协议如图所示,增加了标识符icmp_id、序列号icmp_seq及表示请求时间的icmp_ctime、接收时间的icmp_rtime和传输时间icmp_ttime。
3.UDP头部结构
UDP的头部结构包含发送端的源端口号、数据接收端的目的端口号、UDP数据的长度,以及UDP的校验和等信息。UDP头部结构如下图所示。
在Linux下UDP头部的结构类型为struct udphdr
,提供两种类型:
#ifdef _FAVOR_BSD //BSD样式
struct udphdr
{
u_int16_t uh_sport;//源地址端口
u_int16_t uh_dport;//目的地址端口
u_int16_t uh_ulen;//UDP长度
u_int16_t uh_sum;//UDP校验和
};
#else //Linux样式
struct udphdr
{
u_int16_t source;//源地址端口
u_int16_t dest;//目的地址端口
u_int16_t len;//UDP长度
u_int16_t check;//UDP校验和
};
#endif
Liunx下UDP示意图:
4.TCP头部结构
TCP 的头部结构主要包含发送端的源端口、接收端的目的端口、数据的序列号、上一个数据的确认号、滑动窗口大小、数据的校验和、紧急数据的偏移指针,以及一 些控制位等信息。 TCP 头部结构如图 13.11所示。
Linux 下 TCP 头部结构 struct tcphdr
定义如下,对于小端和大端系统,有不一致的定义:
struct tcphdr
{
_u16 source;//源地址端口
_u16 dest;//目的地址端口
_u16 seq;//序列号
_u16 ack_seq;//确认序列号
#if defined(_LITTLE_ENDIAN_BITFIELD)
_u16 resl:4,//保留
doff:4,//偏移
fin:1,// 关闭连接标志
syn: 1,//请求连接标志
rst: 1,//重置连接标志
psh:1,//接收方尽快将数据放到应用层标志
ack: 1,//确认序号标志
urg:1, //紧急指针标志
ece:1, //拥塞标志位
cwr:1;//拥塞标志位
#elif defiend(_BIG_ENDIAN_BITFIELD)
_u16 doff:4,//偏移
resl:4,//保留
cwr:1,//拥塞标志位
ece:1,//拥塞标志位
urg:1, //紧急指针标志
ack:1,//确认序号标志
psh:1,//接收方尽快将数据放到应用层标志
rst:1,//重置连接标志
syn:1,//请求连接标志
fin:1;//关闭连接标志
#else
#error "Adjust your <asm/byteorder.h> defines"
#endif
_u16 window;//滑动窗口大小
_u16 check;//校验和
_u16 urg_ptr;//紧急字段指针
};
Linux下的TCP:
六、ping的例子
ping 命令向目的主机发送ICMPECHO_ REQUEST
请求并接收目的主机返回的响应报文,用来检验本地主机和远程的主机是否连接。
1.协议格式
ICMP协议报文的格式, ping 的客户端方式的类型为8,代码值为 0,表示ICMP的回显请求。类型为0, 代码为0时,是ICMP回显应答。校验和为 16 位的crc16
的算法。
下图所示为ping 所使用的类型和代码格式。包含16位的标识符和16位的序列号。序列号是用于标识发送或者响应的序号,而标识符通常用于表明发送和接收此报的用户,一 般用进程的PID来识别。
例如,一个用户的进程PID为 1000
, 发送了一个序列号为1的回显请求报文,当此报文被目的主机正确处理并返回后,可以用PID来识别是否为当前的用户,并且用序列号来识别哪个报文被返回。通过发送报文到目的主机并接受响应,可以计算发送和接收二者之间的时间差,来判断网络的状况。
ping 程序一 般按照图中的框架进行设计。
主要分为发送数据和接收数据及计算时间差。
发送数据对组织好的数据进行发送,接收数据从网络上接收数据并判断其合法性,例如判断是否本进程发出的报文等。
由于 ICMP 必须使用原始套接字进行设计,要手动设置 IP 的头部和 ICMP 的头部并进行校验。
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>
#include <unistd.h>
#include <signal.h>
#include <arpa/inet.h>
#include <errno.h>
#include <sys/time.h>
#include <stdio.h>
#include <string.h> /*bzero*/
#include <netdb.h>
#include <pthread.h>
/*保存已经发送包的状态值*/
typedef struct pingm_pakcet{
struct timeval tv_begin; /*发送的时间*/
struct timeval tv_end; /*接收到的时间*/
short seq; /*序列号*/
int flag; /*1,表示已经发送但没有接收到回应包0,表示接收到回应包*/
}pingm_pakcet;
static pingm_pakcet pingpacket[128];
static pingm_pakcet *icmp_findpacket(int seq);
static unsigned short icmp_cksum(unsigned char *data, int len);
static struct timeval icmp_tvsub(struct timeval end,struct timeval begin);
static void icmp_statistics(void);
static void icmp_pack(struct icmp *icmph, int seq, struct timeval *tv, int length );
static int icmp_unpack(char *buf,int len);
static void *icmp_recv(void *argv);
static void *icmp_send(void *argv);
static void icmp_sigint(int signo);
static void icmp_usage();
#define K