原文连接git
IP数据包由IP头部和数据负载两部分组成。IP数据包长度不固定,其中头部长度不固定,负载长度也不固定。IP包的格式以下图:github
注意,图中的位指的是bit,下文中说包的长度是单位是字节,1字节=8位。算法
接下来,依次介绍下每块表明的含义。缓存
版本:占4位,表示IP协议的版本。若是是IPv4,则取值为0100,若是是IPv6,则取值为0110安全
IP包头长度:占4位,从0000-1111,也就是最小为0,最大为15。实际上,IP包头的长度至少为20字节,最大为60字节。IP包头长度的值 * 4就是ip包头所占的字节数。bash
举例来讲,IP包头长度的值是0110,值为6,那么IP包头的长度就是6 * 4 = 24。固定区域20字节,可选区域4字节。服务器
服务类型:占8位,包含优先级、标志位等,实际中不多使用,不作介绍。网络
IP包总长度:占16位,表示该IP包的总长度。即IP包头长度+IP包数据长度。dom
标识:占16位,在数据包分段重组时,标识表示包的序列号。当数据包比较大时,会分红多个IP包发送,每一个IP包到达目的地的时间是不肯定的,此时就须要根据标识进行重组。标识符与下面的标志、偏移量结合使用。tcp
标志:占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包,格式以下:
版本号值为4,说明是IPv4,对应的二进制是
0100
复制代码
IP包头部长度值为5,说明IP包头部长度为20字节,对应的二进制值是
0101
复制代码
服务类型值为0,对应的二进制是
00000000
复制代码
IP包总长度为40,对应的二进制值是
00000000 00101000
复制代码
标识为0,对应的二进制是
00000000 00000000
复制代码
3位标识依次是0、一、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包的格式和IP包格式相似,一样由头部和数据两部分组成。TCP包的长度不固定,其中头部长度不固定,数据部分长度不固定。
由IP部分的内容可知,TCP包实际上就是IP包的数据部分。二者的关系能够用下图表示:
TCP包的格式以下:
依次介绍每块的含义。
TCP包固定头部20字节,可选部分在0-40字节之间。剩下的就是TCP包的数据部分。
使用Wireshark抓取TCP的包,以下:
能够看出,源端口是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
复制代码
相对TCP来讲,UDP是无序、无状态的,所以UDP包相对TCP包来讲要简单一些。UDP包实际上也是IP包的数据部分。上图中IP包和TCP包的关系,一样也适用于IP包和UDP。
UDP包由头部和数据两部分组成,头部长度固定,数据部分长度不固定。UDP包的格式以下:
能够看到,UDP头部的长度为固定8字节,剩下的是数据部分。依次介绍每块的含义。
剩下的就是UDP的数据部分。
注意UDP头部中UDP长度,这个地方和TCP、IP包有明显的区别。由于UDP包头部长度是固定8个字节,因此该部分的取值最小为1000。
使用Wireshark抓取UDP的包,以下图:
能够看到,该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
复制代码
除了IP、TCP、UDP以外,DNS也是常常用到的协议,介绍下DNS包的格式。
DNS包属于UDP包,实际上,DNS包是UDP包的数据部分,二者的关系以下:
DNS包一样分为头部和数据部分,头部长度固定,数据部分长度不固定。DNS包的格式以下:
依次介绍每块的含义。
因为域名的长度不固定,因此name区域长度不固定。
查询类型 | 助记符 | 解释 |
---|---|---|
1 | A | 得到IPv4地址 |
28 | AAAA | 得到IPv6地址 |
15 | MX | 邮件服务器 |
2 | NS | 指定域名服务器 |
5 | CNAME | 将域名指向另外一个域名时 |
回答区域的格式固定,格式以下:
介绍下每块的含义。
使用偏移量来表示时,首字节固定为C0,用于识别,后面字节用于表示偏移量。经过上面的介绍可知,DNS包的头部固定占12字节,头部以后就是查询问题区域,查询问题区域的第一部分就是域名。所以,常见的偏移量是C00C,以下图:
使用Wireshark抓取DNS请求包,以下图:
能够看到,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抓取的响应包,以下图:
TranscationID的值为0xa090,说明和上文的请求包是一对。
Flags的二进制值为:
10000001 10000000 //首位为1,说明是响应包;后四位为0,表示没有错误。
复制代码
Questions为1和上文保持一致;Answer RRs为2,说明有2个应答区域。附加区域和受权区域都为0。
Quries和请求报文一致,再也不介绍。
Answers区域,分为6部分,分别是Name、Type、Class、Time To Live、Data length、数据。
11000000 00001100 //对应的是C00C,说明使用的是偏移量表示
复制代码
00000000 00000101 //值为5,对应CNAME
复制代码
00000000 00000000 00010001 01000110 // 对应4422
复制代码
00000000 00010101 // 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个字节是什么:
从第23个字节开始,正好拼成了域名"sslbaiduv6.jomodns.com"。
11000000 00101100 //首字节是C0,说明是偏移量,偏移值是44,第一个应答区域的域名首字节index正好是44
复制代码
00000000 00000001 // 说明内容是IPv4地址
复制代码
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地址,逻辑是相似的,再也不举例。