协议包结构

作者: acBool | 来源:发表于2019-12-31 09:46 被阅读0次

原文链接

IP包的格式

理论

IP数据包由IP头部和数据负载两部分组成。IP数据包长度不固定,其中头部长度不固定,负载长度也不固定。IP包的格式如下图:

image

注意,图中的位指的是bit,下文中说包的长度是单位是字节,1字节=8位。

接下来,依次介绍下每块代表的含义。

  1. 版本:占4位,表示IP协议的版本。如果是IPv4,则取值为0100,如果是IPv6,则取值为0110

  2. IP包头长度:占4位,从0000-1111,也就是最小为0,最大为15。实际上,IP包头的长度至少为20字节,最大为60字节。IP包头长度的值 * 4就是ip包头所占的字节数。

    举例来说,IP包头长度的值是0110,值为6,那么IP包头的长度就是6 * 4 = 24。固定区域20字节,可选区域4字节。

  3. 服务类型:占8位,包含优先级、标志位等,实际中很少使用,不做介绍。

  4. IP包总长度:占16位,表示该IP包的总长度。即IP包头长度+IP包数据长度。

  5. 标识:占16位,在数据包分段重组时,标识表示包的序列号。当数据包比较大时,会分成多个IP包发送,每个IP包到达目的地的时间是不确定的,此时就需要根据标识进行重组。标识符与下面的标志、偏移量结合使用。

  6. 标志:占3位。从左至右依次是MF、DF、未使用字段。MF = 1,表示后面还有分段数据包,MF = 0表示后面没有分段数据包,也就是最后一个。DF = 1,表示该数据包不能被分段,DF = 0表示数据包可以被分段。

  7. 偏移量:占13位。表示该数据段在上层初始数据报文中的偏移量。和标识符、标志位结合使用。

  8. 生存时间:占8位。生存时间由操作系统初始化,每经过一次转发,生存时间减1,如果生存时间为0,则该包丢弃。生存时间是为了防止数据包在网络中无休止发送,占用网络资源。

  9. 协议:占8位。常用的UDP的值是17,TCP的值是6。

  10. 首部校验和:占16位。对IP数据包首部校验得到的值,校验和有具体的算法。

  11. 源IP地址:占32位

  12. 目的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包的数据部分。两者的关系可以用下图表示:

image

TCP包的格式如下:

image

依次介绍每块的含义。

  1. 源端口:占16位。
  2. 目的端口:占16位。
  3. 序号:sequence number,占32位。表示从发送端向接收端发送的字节流编号。
  4. 确认号:Acknowledgement number,占32位。表示接收端所期望接收到的下一序号。
  5. 数据偏移:占4位。数据偏移其实就是TCP包的头部长度,理论取值从0000-1111,最小为0,最大为15。同IP头部一样,TCP包的头部长度至少为20字节,最多为60字节,且计算方式和IP头部长度的计算方式也一样。如果数据偏移的值为6,那么TCP包的头部长度为6 * 4 = 24。
  6. 保留值:占6位。目前没用,为了之后新功能所保留。
  7. 6位标志位。分别介绍:
    1. URG:紧急标志位,为1说明紧急指针有效。
    2. ACK:确认标志位,为1说明确认序号有效。
    3. PSH:值为1,表示需要将数据立刻发送给应用程序。
    4. RST:值为1时,表示需要重连。
    5. SYN:握手时使用。值为1,表示连接请求报文。
    6. FIN:值为1时,需要断开连接。
  8. 窗口大小:占16位,表示接收端的窗口大小,用于控制网络流量速率。
  9. 校验和:占16位,和IP包头部中的校验和作用一致。
  10. 紧急指针:占16位,和上面提到的URG字段结合使用。
  11. 可选部分,包含窗口扩大选项、时间戳等。最小为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。

image

UDP包由头部和数据两部分组成,头部长度固定,数据部分长度不固定。UDP包的格式如下:

image

可以看到,UDP头部的长度为固定8字节,剩下的是数据部分。依次介绍每块的含义。

  1. 源端口:占16位
  2. 目的端口:占16位
  3. UDP长度:包含头部和数据包总长度,注意和TCP包、IP包的区别
  4. 校验和:占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包的数据部分,两者的关系如下:

image

DNS包同样分为头部和数据部分,头部长度固定,数据部分长度不固定。DNS包的格式如下:

image

依次介绍每块的含义。

  1. TranscationID:会话标识,占2个字节。DNS请求报文和DNS应到报文的TranscationID是相同的。
  2. Flags:占2个字节,包含多个标志位,结构如下
image
  • 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保留。
  1. Questions:占2个字节,表示查询问题区域的数量
  2. Answers RRS:占2个字节,表示回答区域的数量
  3. Authority RRs:占2个字节,表示授权区域的数量
  4. Additional RRs:占2个字节,表示附加区域的数量
  5. Quries:问题区域,长度不固定,可以有多个。Quries区域的格式如下:
image
  • Name区域,就是查询的域名,如"www.baidu.com",Name区域也有固定的格式。以百度域名举例,格式如下:
image

由于域名的长度不固定,所以name区域长度不固定。

  • Type,查询类型,占2个字节。DNS有多种查询类型,常见的有
查询类型 助记符 解释
1 A 获得IPv4地址
28 AAAA 获得IPv6地址
15 MX 邮件服务器
2 NS 指定域名服务器
5 CNAME 将域名指向另一个域名时
  • Class,查询类,通常为1,表示Internet数据。
  1. Answers:回答区域,长度不固定。回答区域的格式和授权区域、附加区域的格式是类似的,因此,只介绍回答区域的格式。

回答区域的格式固定,格式如下:

image

介绍下每块的含义。

  • Name:查询的域名,同问题区域的域名。但是格式和问题区域的域名不同,为了减小包大小,当包中出现重复域名的时候,回答区域的域名部分使用偏移量来表示。当使用偏移量来表示时,占2个字节。不使用偏移量时,长度不固定(和域名本身长度有关)。

使用偏移量来表示时,首字节固定为C0,用于识别,后面字节用于表示偏移量。通过上面的介绍可知,DNS包的头部固定占12字节,头部之后就是查询问题区域,查询问题区域的第一部分就是域名。因此,常见的偏移量是C00C,如下图:

image
  • Type:同请求部分
  • Class:同请求部分
  • Time To Live:生存时间,占4个字节。实际上是客户端缓存的时间。通常情况,域名所对应的IP不会经常改变。因此,为了提高网络传输效率,可以将结果缓存,下次访问时跳过域名解析。
  • 数据长度:占2个字节,下文数据部分的长度。
  • 数据:不定长。
  1. Authoritative:同回答区域。
  2. 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抓取的响应包,如下图:

image

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、数据。

  1. 第一个回答区域
  • 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"。

  1. 第二个回答区域
  • 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地址,逻辑是类似的,不再举例。

相关文章

  • Swift 语言简单介绍(二)

    本片文字主要介绍: 结构体 类 协议 扩展 闭包 结构体 类 协议 扩展 枚举 闭包

  • 协议包结构

    原文链接 IP包的格式 理论 IP数据包由IP头部和数据负载两部分组成。IP数据包长度不固定,其中头部长度不固定,...

  • http协议之http包结构

    1.请求行 2.请求头 3.空行 4.消息主体 Http协议 数据包结构 及 请求方式 HTTP协议结构

  • Swift(2)

    可选类型(?) 隐式解析可选类型(!) 结构体 类 协议 扩展 闭包

  • Swift3.0函数的枚举 函数 结构体 类

    //枚举 函数 结构体 类 协议 代理设计模式 闭包 import Foundation //枚举 enum //...

  • Http网络协议包

    1.网络协议包: 2.常见网络协议: 3.Http网络协议包: 4.Http请求协议包与Http响应协议包介绍: ...

  • Swift基础2

    本文接着上一篇,对Swift的结构体、类、协议、扩展、枚举以及闭包做一个简单的介绍。 七、结构体 Swift的结构...

  • TCP协议

    TCP 协议是网络通信中最核心的协议之一,位于七层网络协议的第四层:传输层。 TCP 数据包结构 整个 TCP 数...

  • Swift初探(二)

    继Swift初探之后,我们来继续学习下Swift里的结构体,类,协议,闭包 结构体 两种调用结构体的方法1.调用结...

  • 🚀 tcp粘包问题处理详解

    ? tcp粘包问题处理 以上图的协议结构,来举个粟子: ? 1.我们收到完整的一个协议串为hex码为: ? 2.对...

网友评论

    本文标题:协议包结构

    本文链接:https://www.haomeiwen.com/subject/wufboctx.html