计算机杂谈TCP/IP协议

协议包结构

2019-12-31  本文已影响0人  acBool

原文链接

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
  1. Questions:占2个字节,表示查询问题区域的数量
  2. Answers RRS:占2个字节,表示回答区域的数量
  3. Authority RRs:占2个字节,表示授权区域的数量
  4. Additional RRs:占2个字节,表示附加区域的数量
  5. Quries:问题区域,长度不固定,可以有多个。Quries区域的格式如下:
image image

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

查询类型 助记符 解释
1 A 获得IPv4地址
28 AAAA 获得IPv6地址
15 MX 邮件服务器
2 NS 指定域名服务器
5 CNAME 将域名指向另一个域名时
  1. Answers:回答区域,长度不固定。回答区域的格式和授权区域、附加区域的格式是类似的,因此,只介绍回答区域的格式。

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

image

介绍下每块的含义。

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

image
  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. 第一个回答区域
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个字节是什么:

image

从第23个字节开始,正好拼成了域名"sslbaiduv6.jomodns.com"。

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

上一篇 下一篇

猜你喜欢

热点阅读