协议包结构
IP包的格式
理论
IP数据包由IP头部和数据负载两部分组成。IP数据包长度不固定,其中头部长度不固定,负载长度也不固定。IP包的格式如下图:
image注意,图中的位指的是bit,下文中说包的长度是单位是字节,1字节=8位。
接下来,依次介绍下每块代表的含义。
-
版本:占4位,表示IP协议的版本。如果是IPv4,则取值为0100,如果是IPv6,则取值为0110
-
IP包头长度:占4位,从0000-1111,也就是最小为0,最大为15。实际上,IP包头的长度至少为20字节,最大为60字节。IP包头长度的值 * 4就是ip包头所占的字节数。
举例来说,IP包头长度的值是0110,值为6,那么IP包头的长度就是6 * 4 = 24。固定区域20字节,可选区域4字节。
-
服务类型:占8位,包含优先级、标志位等,实际中很少使用,不做介绍。
-
IP包总长度:占16位,表示该IP包的总长度。即IP包头长度+IP包数据长度。
-
标识:占16位,在数据包分段重组时,标识表示包的序列号。当数据包比较大时,会分成多个IP包发送,每个IP包到达目的地的时间是不确定的,此时就需要根据标识进行重组。标识符与下面的标志、偏移量结合使用。
-
标志:占3位。从左至右依次是MF、DF、未使用字段。MF = 1,表示后面还有分段数据包,MF = 0表示后面没有分段数据包,也就是最后一个。DF = 1,表示该数据包不能被分段,DF = 0表示数据包可以被分段。
-
偏移量:占13位。表示该数据段在上层初始数据报文中的偏移量。和标识符、标志位结合使用。
-
生存时间:占8位。生存时间由操作系统初始化,每经过一次转发,生存时间减1,如果生存时间为0,则该包丢弃。生存时间是为了防止数据包在网络中无休止发送,占用网络资源。
-
协议:占8位。常用的UDP的值是17,TCP的值是6。
-
首部校验和:占16位。对IP数据包首部校验得到的值,校验和有具体的算法。
-
源IP地址:占32位
-
目的IP地址:占32位。
上面的内容共占20个字节,这些部分是固定的,每一个IP包的头部都包含这些,所以IP包头的长度至少
20字节。
下面的可选部分包含安全处理机制、记录路径、时间戳等信息,长度为0-40字节。
再下面就是IP包的数据部分。IP包的数据包含TCP包、UDP包两种。
抓包验证
使用Wireshark抓取IP包,格式如下:
image版本号值为4,说明是IPv4,对应的二进制是
0100
IP包头部长度值为5,说明IP包头部长度为20字节,对应的二进制值是
0101
服务类型值为0,对应的二进制是
00000000
IP包总长度为40,对应的二进制值是
00000000 00101000
标识为0,对应的二进制是
00000000 00000000
3位标识依次是0、1、0,表示该包后无分段数据,且该数据包不能被分段。
Time to live值为64,对应的二进制是
01000000
协议的值是6,对应的二进制是
00000110 // 6,表示是TCP
头部校验和值为0xcb59。
源IP值是 10.0.6.33,对应的二进制是
00001010 00000000 00000110 00100001
目的IP值是40.100.54.242,对应的二进制是
00101000 01100100 00110110 11110010
共20字节。
代码验证
使用代码验证下IP包的格式,打印出源ip、目的ip、头部长度、总长度。代码如下:
- (void)ipTest
{
// 从文件中读取IP数据流
NSString *txtFilePath = [[NSBundle mainBundle] pathForResource:@"tcp" ofType:@"txt"];
NSData *data = [NSData dataWithContentsOfFile:txtFilePath ];
const uint8_t *bytes = data.bytes;
// 因为确认是IP包,所以可以直接转为struct ip类型
struct ip *iphdr = (struct ip *)bytes;
char src_ip[16];
char dst_ip[16];
uint32_t src_addr = iphdr->ip_src.s_addr;
uint32_t dst_addr = iphdr->ip_dst.s_addr;
inet_ntop(AF_INET,&src_addr,src_ip,sizeof(src_ip));
inet_ntop(AF_INET,&dst_addr,dst_ip,sizeof(dst_ip));
printf("src = %s dst = %s headLen = %d dataLen = %d protocol = %d",src_ip,dst_ip,iphdr->ip_hl,iphdr->ip_len,iphdr->ip_p);
}
输出为:
src = 192.0.2.1 dst = 216.58.200.234 headLen = 5 dataLen = 16384 protocol = 6
TCP包格式
理论
TCP包的格式和IP包格式类似,同样由头部和数据两部分组成。TCP包的长度不固定,其中头部长度不固定,数据部分长度不固定。
由IP部分的内容可知,TCP包实际上就是IP包的数据部分。两者的关系可以用下图表示:
imageTCP包的格式如下:
image依次介绍每块的含义。
- 源端口:占16位。
- 目的端口:占16位。
- 序号:sequence number,占32位。表示从发送端向接收端发送的字节流编号。
- 确认号:Acknowledgement number,占32位。表示接收端所期望接收到的下一序号。
- 数据偏移:占4位。数据偏移其实就是TCP包的头部长度,理论取值从0000-1111,最小为0,最大为15。同IP头部一样,TCP包的头部长度至少为20字节,最多为60字节,且计算方式和IP头部长度的计算方式也一样。如果数据偏移的值为6,那么TCP包的头部长度为6 * 4 = 24。
- 保留值:占6位。目前没用,为了之后新功能所保留。
- 6位标志位。分别介绍:
- URG:紧急标志位,为1说明紧急指针有效。
- ACK:确认标志位,为1说明确认序号有效。
- PSH:值为1,表示需要将数据立刻发送给应用程序。
- RST:值为1时,表示需要重连。
- SYN:握手时使用。值为1,表示连接请求报文。
- FIN:值为1时,需要断开连接。
- 窗口大小:占16位,表示接收端的窗口大小,用于控制网络流量速率。
- 校验和:占16位,和IP包头部中的校验和作用一致。
- 紧急指针:占16位,和上面提到的URG字段结合使用。
- 可选部分,包含窗口扩大选项、时间戳等。最小为0,最大为40字节。
TCP包固定头部20字节,可选部分在0-40字节之间。剩下的就是TCP包的数据部分。
抓包验证
使用Wireshark抓取TCP的包,如下:
image可以看出,源端口是57119,对应的二进制是
11011111 00011111 // 对应571119
目的端口是443,对应的二进制是
00000001 10111011 // 对应443,说明是https请求
接下来是Sequence Number和Ack Number,各占4个字节。
然后是头部长度(偏移量),二进制是
0101 // 值为5,说明该IP包头部没有附加部分,头部长度为20字节
然后是保留位和一些flag标志位,标志位中Ack为1,说明Ack Number有效。Urgent为0,说明没有紧急指针无效。
窗口大小为4095,二进制为
00001111 11111111
校验和的值为0x268a。
紧急指针值为0,对应的二进制为
00000000 00000000
共20字节。
代码验证
使用代码验证tcp包格式,打印出tcp包的源端口、目的端口、头部长度,代码如下:
- (void)tcpTest
{
// 从文件中读取TCP数据流
NSString *txtFilePath = [[NSBundle mainBundle] pathForResource:@"tcp" ofType:@"txt"];
NSData *data = [NSData dataWithContentsOfFile:txtFilePath ];
const uint8_t *bytes = data.bytes;
// 因为确认是IP包,所以可以直接转为struct ip类型
struct ip *iphdr = (struct ip *)bytes;
// 左移两位,即*4,就是ip包的头部长度
const int ip_hlength = iphdr->ip_hl << 2;
// 偏移ip_hlength个字节,到TCP数据部分
const uint8_t *tcpPacket = bytes + ip_hlength;
// 系统API提供的有tcp结构体,可以直接转为 struct tcphdr类型
struct tcphdr tcp;
memcpy(&tcp,tcpPacket,sizeof(struct tcphdr));
// 打印源端口、目的端口、头部长度
printf("sourceport = %d dstport = %d len = %d",ntohs(tcp.th_sport),ntohs(tcp.th_dport),tcp.th_off);
}
输出为:
sourceport = 60495 dstport = 443 len = 11
UDP包格式
理论
相对TCP来说,UDP是无序、无状态的,因此UDP包相对TCP包来说要简单一些。UDP包实际上也是IP包的数据部分。上图中IP包和TCP包的关系,同样也适用于IP包和UDP。
imageUDP包由头部和数据两部分组成,头部长度固定,数据部分长度不固定。UDP包的格式如下:
image可以看到,UDP头部的长度为固定8字节,剩下的是数据部分。依次介绍每块的含义。
- 源端口:占16位
- 目的端口:占16位
- UDP长度:包含头部和数据包总长度,注意和TCP包、IP包的区别
- 校验和:占16位,同IP包、TCP包校验和。
剩下的就是UDP的数据部分。
注意UDP头部中UDP长度,这个地方和TCP、IP包有明显的区别。因为UDP包头部长度是固定8个字节,所以该部分的取值最小为1000。
抓包验证
使用Wireshark抓取UDP的包,如下图:
image可以看到,该UDP包的源端口是53,对应的二进制是
00000000 11101010 // 对应53
目的端口是60155,对应的二进制是
11101010 11111011 // 对应60153
长度是84,对应的二进制是
00000000 01010100 //对应84
校验和是0xc061,对应的二进制是
11000000 01100001 //对应0xc061
共8个字节。
代码验证
使用代码验证普通UDP包格式,打印出UDP包的源端口、目的端口、长度,代码如下:
- (void)udpTest
{
// 从文件中读取UDP数据流
NSString *txtFilePath = [[NSBundle mainBundle] pathForResource:@"dns" ofType:@"txt"];
NSData *data = [NSData dataWithContentsOfFile:txtFilePath ];
const uint8_t *bytes = data.bytes;
// 因为确认是IP包,所以可以直接转为struct ip类型
struct ip *iphdr = (struct ip *)bytes;
// 左移两位,即*4,就是ip包的头部长度
const int ip_hlength = iphdr->ip_hl << 2;
// 偏移ip_hlength个字节,到UDP数据部分
const uint8_t *udpPacket = bytes + ip_hlength;
// 系统API提供的有udp结构体,可以直接转为 struct udphdr类型
struct udphdr udp;
memcpy(&udp, udpPacket, sizeof(struct udphdr));
printf("sourceport = %d dstport = %d len = %d",ntohs(udp.uh_sport),ntohs(udp.uh_dport),udp.uh_ulen);
}
输出:
sourceport = 63354 dstport = 53 len = 10496
DNS包格式
理论
除了IP、TCP、UDP之外,DNS也是经常用到的协议,介绍下DNS包的格式。
DNS包属于UDP包,实际上,DNS包是UDP包的数据部分,两者的关系如下:
imageDNS包同样分为头部和数据部分,头部长度固定,数据部分长度不固定。DNS包的格式如下:
image依次介绍每块的含义。
- TranscationID:会话标识,占2个字节。DNS请求报文和DNS应到报文的TranscationID是相同的。
- Flags:占2个字节,包含多个标志位,结构如下
- QR:占1位。0查询报文,1响应报文
- Opcode:占4位。0标准查询,1反向查询,2服务器状态查询,3~15保留未用
- AA:占1位。为1,表示授权回答
- TC:占1位。为1,表示报文被截断
- RD:占1位。0,表示客户端期望域名解析服务器采用迭代的方式解析;1表示客户端期望域名解析服务器采用递归的方式解析
- RA:占1位。0,表示域名解析服务器采用迭代的方式解析;1表示域名服务器采用递归的方式解析
- Zero:占3位。全0,保留位。
- Rcode:占4位。响应码,0表示无差错;1表示查询格式错误;2表示服务器失效;3表示域名错误;4表示查询没有被执行;5表示查询被拒绝。6~15保留。
- Questions:占2个字节,表示查询问题区域的数量
- Answers RRS:占2个字节,表示回答区域的数量
- Authority RRs:占2个字节,表示授权区域的数量
- Additional RRs:占2个字节,表示附加区域的数量
- Quries:问题区域,长度不固定,可以有多个。Quries区域的格式如下:
- Name区域,就是查询的域名,如"www.baidu.com",Name区域也有固定的格式。以百度域名举例,格式如下:
由于域名的长度不固定,所以name区域长度不固定。
- Type,查询类型,占2个字节。DNS有多种查询类型,常见的有
查询类型 | 助记符 | 解释 |
---|---|---|
1 | A | 获得IPv4地址 |
28 | AAAA | 获得IPv6地址 |
15 | MX | 邮件服务器 |
2 | NS | 指定域名服务器 |
5 | CNAME | 将域名指向另一个域名时 |
- Class,查询类,通常为1,表示Internet数据。
- Answers:回答区域,长度不固定。回答区域的格式和授权区域、附加区域的格式是类似的,因此,只介绍回答区域的格式。
回答区域的格式固定,格式如下:
image介绍下每块的含义。
- Name:查询的域名,同问题区域的域名。但是格式和问题区域的域名不同,为了减小包大小,当包中出现重复域名的时候,回答区域的域名部分使用偏移量来表示。当使用偏移量来表示时,占2个字节。不使用偏移量时,长度不固定(和域名本身长度有关)。
使用偏移量来表示时,首字节固定为C0,用于识别,后面字节用于表示偏移量。通过上面的介绍可知,DNS包的头部固定占12字节,头部之后就是查询问题区域,查询问题区域的第一部分就是域名。因此,常见的偏移量是C00C,如下图:
image- Type:同请求部分
- Class:同请求部分
- Time To Live:生存时间,占4个字节。实际上是客户端缓存的时间。通常情况,域名所对应的IP不会经常改变。因此,为了提高网络传输效率,可以将结果缓存,下次访问时跳过域名解析。
- 数据长度:占2个字节,下文数据部分的长度。
- 数据:不定长。
- Authoritative:同回答区域。
- Additional:同回答区域。
抓包验证
请求包
使用Wireshark抓取DNS请求包,如下图:
image可以看到,Transcation ID为 0xa090,多对应的二进制是:
10100000 10010000 //对应a090
接下来是标志区域,占2个字节,对应的二进制是:
00000001 00000000 //首位为0,表示是一个请求包
问题区域,占两个字节:
00000000 00000001 // 只有1个问题
响应区域、授权区域、附加区域都为0,各占2个字节:
00000000 00000000
之后是正文,首先是Queries,也就是请求区域的内容。包含三部分:Name、Type、Class。
Name是dss1.baidu.com,对应的二进制是:
00000100 01100100 01110011 01110011 00110001 00000101 01100010 01100001 01101001 01100100 01110101 00000011 01100011 01101111 01101101 00000000
共十六个字节,对应的内容依次是:
4 d s s 1 5 b a i d u 3 c o m 0
Type为A,占2个字节:
00000000 00000001 // 表示获得IPv4地址
Class 为IN,占2个字节:
00000000 00000001 // 表示网络数据
该请求包的回答区域、授权区域、附加区域都为0,所以下面没有数据。
响应包
使用Wireshark抓取的响应包,如下图:
imageTranscationID的值为0xa090,说明和上文的请求包是一对。
Flags的二进制值为:
10000001 10000000 //首位为1,说明是响应包;后四位为0,表示没有错误。
Questions为1和上文保持一致;Answer RRs为2,说明有2个应答区域。附加区域和授权区域都为0。
Quries和请求报文一致,不再介绍。
Answers区域,分为6部分,分别是Name、Type、Class、Time To Live、Data length、数据。
- 第一个回答区域
- Name:对应的二进制是
11000000 00001100 //对应的是C00C,说明使用的是偏移量表示
- Type: 对应的二进制是
00000000 00000101 //值为5,对应CNAME
- Class 为1,同请求包
- Time to live: 对应的二进制是
00000000 00000000 00010001 01000110 // 对应4422
- Data length,对应的二进制是
00000000 00010101 // 21,表示后面数据的长度
- 数据部分,数据部分对应的内容是 sslbaiduv6.jomodns.com。存储域名使用的还是上面介绍的方式,需要注意的是,因为上面已经存储过"com"了,所以这里存储"com"时使用的是偏移量,这也是为何data length是21的原因。数据部分对应的二进制是:
00001010 01110011 01110011 01101100 01100010 01100001 01101001 01100100 01110101 01110110 00110110 00000111 01101010 01101111 01101101 01101111 01100100 01101110 01110011 11000000 00010111
前19个字节对应的内容分别是
10 s s l b a i d u v 6 7 j o m o d n s
第20个字节是0xc0,表示后面一个字节是偏移量;第21个字节表示的内容是23,说明偏移量是23。看一下包中第23个字节是什么:
image从第23个字节开始,正好拼成了域名"sslbaiduv6.jomodns.com"。
- 第二个回答区域
- Name,对应的二进制是:
11000000 00101100 //首字节是C0,说明是偏移量,偏移值是44,第一个应答区域的域名首字节index正好是44
- Type,二进制值为:
00000000 00000001 // 说明内容是IPv4地址
- Class、Time to live、Data length和第一个应答区域类似
- 数据区域:返回的是IP地址,对应的二进制是:
01110001 01100000 00011110 00100001
对应的十进制分别是:
113 96 30 33 // 正好是IP地址
该包没有附加区域和授权区域。
代码验证
项目中有从DNS请求包中获取请求域名的需求,写代码简单验证一下。为了方便测试,将DNS流以文件的形式存储在本地,代码中直接读取文件。代码如下:
- (void)dnsTest
{
// 从文件中读取DNS数据流
NSString *txtFilePath = [[NSBundle mainBundle] pathForResource:@"dns" ofType:@"txt"];
NSData *data = [NSData dataWithContentsOfFile:txtFilePath ];
const uint8_t *bytes = data.bytes;
// 因为确认是IP包,所以可以直接转为struct ip类型
struct ip *iphdr = (struct ip *)bytes;
// 左移两位,即*4,就是ip包的头部长度
const int ip_hlength = iphdr->ip_hl << 2;
// 偏移ip_hlength个字节,到UDP数据部分。UDP包头部固定8个字节,再偏移8个字节,即是DNS数据部分
const uint8_t *dnsPacket = bytes + ip_hlength + 8;
size_t dnsLen = data.length - ip_hlength - 8;
// 用于存储域名
char *domain = (char *) malloc(sizeof(char) * 256);
if (!domain)
return ;
memset(domain, 0, 256);
domain[0] = '\0';
int domain_len = 0;
const uint8_t *ptr = dnsPacket;
// 偏移12,因为DNS包的头部固定12字节
ptr += 12;
for (int n = 0; n < dnsLen;) {
uint8_t c = ptr[n];
// 全为0的字节,就是域名的最后一位
if (c == 0x00)
break;
if ((n + c + 1) > dnsLen)
break;
if ((n + c + 1) > 256)
break;
n += 1;
// 以"baidu.com"举例,首先把"baidu"加到domain中
strncat(domain, (const char *) (ptr + n), c);
// 然后加"."
strncat(domain, ".", 1);
domain_len += (c + 1);
// 依次循环
n += c;
}
if (domain_len >= 1)
domain[domain_len - 1] = '\0';
printf("domain = %s",domain);
}
输出结果为:
domain = ccdace.hupu.com
因为只是验证,所以已经确保了该包是DNS包。实际在项目中,还应该判断包是UDP包,且是DNS请求。
另外还可以从响应包中获得返回的IP地址,逻辑是类似的,不再举例。