跟我一起学TCP/IP
-
- 帖子: 26
- 注册时间: 2023-02-26 9:59
- 系统: ubuntu
Re: 跟我一起学TCP/IP
CIDR(Classless Interdomain Routing)
本书的重点不是路由协议,作者对OSPF和BGP只进行了简单概述,我们也不进行深入研究了
互联网一开始设计的路由架构是基于网络的,将网络分为3类,分别是Class A、Class B、Class C
每个网络要求一个路由表项
回忆一下地址分类
A类IP地址的netid有7位,共2^7=128个网络,因此要求128个路由表项
B类IP地址的netid有14位,共2^14=16384个网络,要求16384个路由表项
C类IP地址的netid有21位,共2^21=2097152个网络,要求2097152个路由表项
对于A类和B类网络,路由表项的规模尚可接受,但是对于C类网络,这个路由表项的数目过大,无论从维护还是性能考虑都是不可接受的
CIDR是一种新的路由技术,他不基于网络,而是基于域(domain),也就是说他没有网络的概念了,无论物理网络的拓扑结构是什么样,都可以使用这种新的路由技术
CIDR虽然基于域而不是网络,但是他的思想是一样的,类似进程使用多级页表来索引虚拟地址空间的思想
页表思想介绍:
对于4GB的虚拟地址空间:
如果采用一级页表,那么对于4KB大小的页,就需要2^20=1048576个页表项
如果采用两级页表,那么第一级页表项只需要2^10=1024个(第一级页表索引10位,第二级页表索引10位,页内偏移12位)
我们可以看出采用二级页表节省的是第一级页表项的个数
对于路由表项来说,节省的是Internet Routing table entries
Internet router 大概是指连接各自治系统的路由器
IP地址分类就类似只采用一级页表,特别是对于C类网络(hostid占8位,可以理解为页大小,netid占21位(可以理解为页表索引)),最初引入CIDR就是为了减少索引C类网络的路由表项数目
CIDR也是采用多级页表思想,且级数不固定
任意连续的IP地址范围都可以组成一个域,以书上的例子194.0.0.0-195.255.255.255这个地址范围,为了方便观察,我们转换成32位的二进制
11000010 00000000 00000000 00000000
11000011 11111111 11111111 11111111
我们观察到这个地址范围内所有的地址的前7位都相同,因此所有以1100001开头的IP地址都在这个域内,所以只需要一个路由表项就可以将IP数据报转发到这个域内,剩下的25位还可以继续划分成不同的子域,比如继续使用额外的7位组成一个子域,那么第一个子域的范围如下
11000010 00000000 00000000 00000000 (194.0.0.0)
11000010 00000011 11111111 11111111 (194.3.255.255)
子域还可以继续划分子子域,划分的级数没有限制
标识域的范围也是使用一个32位的mask,称为domain mask
要识别一个IP地址属于哪个域,只需要将IP地址和domain mask进行与操作就得到了域号
通往某个域的路由表项类似: domain-number gateway domain-mask
在IP模块执行IP routing时,会选择最长匹配(longest match),也就是选择mask_one_bit_count(mask)最大的那条route
假如将路由表里所有路由条目按mask_one_bit_count(mask)从大到小排序,那么第一个匹配的条目就是最佳匹配(best match)的route
兼容性:
CIDR路由条目与先前的host-route、net-route、default-route是兼容的,因为host-route对应的mask是全1(这个域中只有一台主机),default-route对应的mask全0,所以匹配顺序是按host-route、net-route、default-route的顺序进行的
本书的重点不是路由协议,作者对OSPF和BGP只进行了简单概述,我们也不进行深入研究了
互联网一开始设计的路由架构是基于网络的,将网络分为3类,分别是Class A、Class B、Class C
每个网络要求一个路由表项
回忆一下地址分类
A类IP地址的netid有7位,共2^7=128个网络,因此要求128个路由表项
B类IP地址的netid有14位,共2^14=16384个网络,要求16384个路由表项
C类IP地址的netid有21位,共2^21=2097152个网络,要求2097152个路由表项
对于A类和B类网络,路由表项的规模尚可接受,但是对于C类网络,这个路由表项的数目过大,无论从维护还是性能考虑都是不可接受的
CIDR是一种新的路由技术,他不基于网络,而是基于域(domain),也就是说他没有网络的概念了,无论物理网络的拓扑结构是什么样,都可以使用这种新的路由技术
CIDR虽然基于域而不是网络,但是他的思想是一样的,类似进程使用多级页表来索引虚拟地址空间的思想
页表思想介绍:
对于4GB的虚拟地址空间:
如果采用一级页表,那么对于4KB大小的页,就需要2^20=1048576个页表项
如果采用两级页表,那么第一级页表项只需要2^10=1024个(第一级页表索引10位,第二级页表索引10位,页内偏移12位)
我们可以看出采用二级页表节省的是第一级页表项的个数
对于路由表项来说,节省的是Internet Routing table entries
Internet router 大概是指连接各自治系统的路由器
IP地址分类就类似只采用一级页表,特别是对于C类网络(hostid占8位,可以理解为页大小,netid占21位(可以理解为页表索引)),最初引入CIDR就是为了减少索引C类网络的路由表项数目
CIDR也是采用多级页表思想,且级数不固定
任意连续的IP地址范围都可以组成一个域,以书上的例子194.0.0.0-195.255.255.255这个地址范围,为了方便观察,我们转换成32位的二进制
11000010 00000000 00000000 00000000
11000011 11111111 11111111 11111111
我们观察到这个地址范围内所有的地址的前7位都相同,因此所有以1100001开头的IP地址都在这个域内,所以只需要一个路由表项就可以将IP数据报转发到这个域内,剩下的25位还可以继续划分成不同的子域,比如继续使用额外的7位组成一个子域,那么第一个子域的范围如下
11000010 00000000 00000000 00000000 (194.0.0.0)
11000010 00000011 11111111 11111111 (194.3.255.255)
子域还可以继续划分子子域,划分的级数没有限制
标识域的范围也是使用一个32位的mask,称为domain mask
要识别一个IP地址属于哪个域,只需要将IP地址和domain mask进行与操作就得到了域号
通往某个域的路由表项类似: domain-number gateway domain-mask
在IP模块执行IP routing时,会选择最长匹配(longest match),也就是选择mask_one_bit_count(mask)最大的那条route
假如将路由表里所有路由条目按mask_one_bit_count(mask)从大到小排序,那么第一个匹配的条目就是最佳匹配(best match)的route
兼容性:
CIDR路由条目与先前的host-route、net-route、default-route是兼容的,因为host-route对应的mask是全1(这个域中只有一台主机),default-route对应的mask全0,所以匹配顺序是按host-route、net-route、default-route的顺序进行的
上次由 723937936@qq.com 在 2023-03-12 22:12,总共编辑 1 次。
-
- 帖子: 26
- 注册时间: 2023-02-26 9:59
- 系统: ubuntu
Re: 跟我一起学TCP/IP
第11章:UDP
UDP是一个面向数据报的传输层协议,应用程序的每个sendto调用都会产生一个udp数据报(封装在一个ip数据报中)
应用调用sendto发送udp数据报,需要考虑生成的ip数据报的大小限制:
1. path MTU:生成的ip数据报的总长度不能超过path MTU,否则会导致分片
2. IP Maximum Datagram Size:IPv4实现定义该值,表示可以处理的最大ip数据报大小,该值的可移植大小为576字节(包含ip header)
UDP格式
字段说明:
source port number:标识发送进程
destination port number:标识接收进程
UDP length:UDP数据报总长度(包含header和data),该字段最小值8,也就是允许data的长度为0
checksum:包括 UDP pseudo header、UDP header和data的校验和(可选,默认使能,填0表示发送者未计算校验和)
ones' complement
要计算UDP checksum需要理解是什么ones' complement,要理解什么是ones' complement,先理解什么是complement?
complement:我翻译成补数(在计算机中一般翻译成补码,但是我认为翻译成补数更具一般性)
理解补数最好的例子是时钟,我们以24时制为例,在24时制里,比如11+13=24,我们说11和13相对24互为补数,我们称24为模数
时钟的算术运算:
举例说明:
比如现在23点,再过2个小时,是1点,也就是:
23+2=1
我们脑子里是这样计算的,23+2=25,然后25-24=1
再举一个例子,比如现在是2点,再过24小时,还是2点,也就是:
2+24=2
也就是一个数加上他的模数等于自己
还有0点和24点也表示一个值
下面假如一天只有15小时,比如3+12=15,我们称3和12相对15互为补数
下面的例子用二进制做补数运算
比如:
3+12=15
转换为二进制:
0011+1100=1111
像这种模数为全1的情况有一个专门的名字,称为一补数(ones complement),其实叫什么名字不重要,运算方法完全一样
同样,我们计算13+5=3,我们是这样计算的,13+5=18,然后18-15=3
换算成二进制再计算一遍:
1101+0101=10010,然后10010-1111=0011,结果为3
发生进位后要减去模数的操作也可以如下计算:
10010-1111=10010-(10000-1)=10010-10000+1=0010+1=0011
这种计算的技巧是:将进位直接加到最低位上
另外相对1111互补的两个数对应位正好相反,这是一补数的特点
UDP checksum
UDP checksum是计算16-bit word的和(按补数方法计算)
16-bit word的模数是0xffff
计算UDP校验和算法:
1. 将所有16-bit word相加(按补数方法计算)
2. 将得到的结果取补数就是UDP的校验和
具体实现参考UNP的源代码:
https://github.com/unpbook/unpv13e/blob ... in_cksum.c
UDP pseudo header
UDP校验和的计算要包含一些额外的内容,称为UDP pseudo header,结构如下:
为了计算UDP校验和,需要先构造上图所示结构(此结构只用于计算校验和,并不会发送此结构)
因为计算校验和要求数据总长度必须是偶数,所以如果UDP length字段为奇数,则需要在数据末尾添加一个字节('\0'),(上图中的两个16-bit UDP length字段的值不要加1)
计算校验和之前,上述结构中的16-bit UDP checksum字段必须为0
上面提到如果UDP数据报里的checksum字段为0表示发送者没有计算校验和,但是如果发送者计算的校验和恰巧为0,那么就在checksum字段填0xffff(这种情况接收端不需要做特殊处理,因为0x0000和0xffff在补数的世界里是等价物,想象前面举的24时制的例子:0点和24点是一样的)
接收端检查校验和算法:
1. 将所有16-bit word相加(按补数方法计算)
2. 如果得到的结果为0xffff,则表示检查正确
接收主机的UDP模块同样需要在接收的UDP数据报前面添加UDP pseudo header,以及(如果UDP数据报长度不是偶数)在尾部添加'\0'
为什么接收端计算出的结果是0xffff?(假设接收到的UDP数据报没有损坏)
发送端计算校验和的最后一个步骤是求补数,假设发送端第一步计算的结果为0x0001,那么第二步得到的校验和就是0xfffe(对应的补数)
那么接收端计算的结果就是0x0001+0xfffe(接收端在第一步计算时会连带checksum字段一起相加),所以结果为0xffff
一句话就是互补的两个数相加等于模数,这是补数的定义
前面提到如果发送端计算的校验和恰巧为0x0000,需要在checksum字段填0xffff,这种情况对于接收端不需要做特殊处理,原因是
在补数的世界里,0和模数是等价物,也就是
0x0000+0xffff=0xffff
0xffff+0xffff=0xffff(想象一下24点再过24小时还是24点)
接收主机的UDP模块如果检测到UDP数据报的校验和错误,则默默丢弃该UDP数据报,并不会回送ICMP错误消息给发送者(IP模块如果检测到IP数据报头的校验和错误,也会默默丢弃该IP数据报)
注意:
1. 即使UDP checksum检查正确,也不表示UDP数据报内容没有变化,因为这个校验和算法比较简陋,并不能检查出所有错误
2. 说UDP是一个不可靠的协议,并不是指UDP的checksum无法检测所有错误,而是指可达性,也就是UDP数据报能否送达目的主机。就checksum来说,TCP也使用相同checksum算法,同样无法检测TCP segment的所有错误,但是TCP是保证到达目的主机或无法送达时报告错误(有确认机制)。
如果需要确保内容正确,应该使用密码算法(比如哈希算法、消息认证码、数字签名等)
网络层:IP+密码算法=IPsec
传输层:TCP+密码算法=TLS
抓包示例:
一个终端发送udp数据报,data="hello\n"
另一个终端抓包
编程Note:
如果使用raw socket发送UDP数据报,需要应用程序构造完整的UDP数据报(包含UDP header)所以需要计算UDP校验和(参考UNP第28章)
UDP是一个面向数据报的传输层协议,应用程序的每个sendto调用都会产生一个udp数据报(封装在一个ip数据报中)
应用调用sendto发送udp数据报,需要考虑生成的ip数据报的大小限制:
1. path MTU:生成的ip数据报的总长度不能超过path MTU,否则会导致分片
2. IP Maximum Datagram Size:IPv4实现定义该值,表示可以处理的最大ip数据报大小,该值的可移植大小为576字节(包含ip header)
UDP格式
字段说明:
source port number:标识发送进程
destination port number:标识接收进程
UDP length:UDP数据报总长度(包含header和data),该字段最小值8,也就是允许data的长度为0
checksum:包括 UDP pseudo header、UDP header和data的校验和(可选,默认使能,填0表示发送者未计算校验和)
ones' complement
要计算UDP checksum需要理解是什么ones' complement,要理解什么是ones' complement,先理解什么是complement?
complement:我翻译成补数(在计算机中一般翻译成补码,但是我认为翻译成补数更具一般性)
理解补数最好的例子是时钟,我们以24时制为例,在24时制里,比如11+13=24,我们说11和13相对24互为补数,我们称24为模数
时钟的算术运算:
举例说明:
比如现在23点,再过2个小时,是1点,也就是:
23+2=1
我们脑子里是这样计算的,23+2=25,然后25-24=1
再举一个例子,比如现在是2点,再过24小时,还是2点,也就是:
2+24=2
也就是一个数加上他的模数等于自己
还有0点和24点也表示一个值
下面假如一天只有15小时,比如3+12=15,我们称3和12相对15互为补数
下面的例子用二进制做补数运算
比如:
3+12=15
转换为二进制:
0011+1100=1111
像这种模数为全1的情况有一个专门的名字,称为一补数(ones complement),其实叫什么名字不重要,运算方法完全一样
同样,我们计算13+5=3,我们是这样计算的,13+5=18,然后18-15=3
换算成二进制再计算一遍:
1101+0101=10010,然后10010-1111=0011,结果为3
发生进位后要减去模数的操作也可以如下计算:
10010-1111=10010-(10000-1)=10010-10000+1=0010+1=0011
这种计算的技巧是:将进位直接加到最低位上
另外相对1111互补的两个数对应位正好相反,这是一补数的特点
UDP checksum
UDP checksum是计算16-bit word的和(按补数方法计算)
16-bit word的模数是0xffff
计算UDP校验和算法:
1. 将所有16-bit word相加(按补数方法计算)
2. 将得到的结果取补数就是UDP的校验和
具体实现参考UNP的源代码:
https://github.com/unpbook/unpv13e/blob ... in_cksum.c
UDP pseudo header
UDP校验和的计算要包含一些额外的内容,称为UDP pseudo header,结构如下:
为了计算UDP校验和,需要先构造上图所示结构(此结构只用于计算校验和,并不会发送此结构)
因为计算校验和要求数据总长度必须是偶数,所以如果UDP length字段为奇数,则需要在数据末尾添加一个字节('\0'),(上图中的两个16-bit UDP length字段的值不要加1)
计算校验和之前,上述结构中的16-bit UDP checksum字段必须为0
上面提到如果UDP数据报里的checksum字段为0表示发送者没有计算校验和,但是如果发送者计算的校验和恰巧为0,那么就在checksum字段填0xffff(这种情况接收端不需要做特殊处理,因为0x0000和0xffff在补数的世界里是等价物,想象前面举的24时制的例子:0点和24点是一样的)
接收端检查校验和算法:
1. 将所有16-bit word相加(按补数方法计算)
2. 如果得到的结果为0xffff,则表示检查正确
接收主机的UDP模块同样需要在接收的UDP数据报前面添加UDP pseudo header,以及(如果UDP数据报长度不是偶数)在尾部添加'\0'
为什么接收端计算出的结果是0xffff?(假设接收到的UDP数据报没有损坏)
发送端计算校验和的最后一个步骤是求补数,假设发送端第一步计算的结果为0x0001,那么第二步得到的校验和就是0xfffe(对应的补数)
那么接收端计算的结果就是0x0001+0xfffe(接收端在第一步计算时会连带checksum字段一起相加),所以结果为0xffff
一句话就是互补的两个数相加等于模数,这是补数的定义
前面提到如果发送端计算的校验和恰巧为0x0000,需要在checksum字段填0xffff,这种情况对于接收端不需要做特殊处理,原因是
在补数的世界里,0和模数是等价物,也就是
0x0000+0xffff=0xffff
0xffff+0xffff=0xffff(想象一下24点再过24小时还是24点)
接收主机的UDP模块如果检测到UDP数据报的校验和错误,则默默丢弃该UDP数据报,并不会回送ICMP错误消息给发送者(IP模块如果检测到IP数据报头的校验和错误,也会默默丢弃该IP数据报)
注意:
1. 即使UDP checksum检查正确,也不表示UDP数据报内容没有变化,因为这个校验和算法比较简陋,并不能检查出所有错误
2. 说UDP是一个不可靠的协议,并不是指UDP的checksum无法检测所有错误,而是指可达性,也就是UDP数据报能否送达目的主机。就checksum来说,TCP也使用相同checksum算法,同样无法检测TCP segment的所有错误,但是TCP是保证到达目的主机或无法送达时报告错误(有确认机制)。
如果需要确保内容正确,应该使用密码算法(比如哈希算法、消息认证码、数字签名等)
网络层:IP+密码算法=IPsec
传输层:TCP+密码算法=TLS
抓包示例:
一个终端发送udp数据报,data="hello\n"
代码: 全选
$ nc -u -p 7777 192.168.0.3 8888
hello
代码: 全选
$ sudo ./udpdump
IP 192.168.0.6.7777 > 192.168.0.3.8888: UDP, length 6 (UDP cksum=8179)
如果使用raw socket发送UDP数据报,需要应用程序构造完整的UDP数据报(包含UDP header)所以需要计算UDP校验和(参考UNP第28章)
上次由 723937936@qq.com 在 2023-03-15 7:02,总共编辑 2 次。
-
- 帖子: 26
- 注册时间: 2023-02-26 9:59
- 系统: ubuntu
Re: 跟我一起学TCP/IP
IP分片
当IP模块发送IP数据报时,IP模块会获取外出接口的MTU或特定socket的PMTU,如果要发送的IP数据报的总长度超过MTU或PMTU,则IP模块可能会执行分片操作或返回EMSGSIZE错误(可以通过socket option来改变行为,下文有说明)
无论发送主机还是中间的路由器都可能会执行分片操作,但只有分片到达最终的目的主机才会执行重组操作
回忆IP数据报格式: 其中16-bit identification、3-bit flags、13-bit fragment offset与IP分片有关
16-bit identification:用于标识同一个IP数据报的不同分片,目的主机根据此id来判断一个IP数据报有哪些分片
3-bit flags:最低位是MF(more fragments flag),中间位是DF(dont fragment flag),最高位保留
13-bit fragment offset:分片包含的数据的偏移,单位8字节
分片被认为是不好的,是因为一个分片丢失,整个IP数据报都要重传
观察分片
在一个终端执行ping
在另一个终端运行tcpdump
tcpdump的输出(我标了序号)
第1个IP数据报(echo request)设置了DF标志,IP数据报的总长度为1500字节(包含20字节的IP头,8字节的icmp头,1472字节的数据)
第2个IP数据报(echo reply)设置了DF标志,IP数据报的总长度为1500字节(包含20字节的IP头,8字节的icmp头,1472字节的数据)
第3个IP数据报(echo request)设置了MF表示(+),这个分片的长度为1500字节(包含20字节的IP头,8字节的icmp头,1472字节的数据)
第4个IP数据报(echo request)没有设置flags(最后一个分片),这个分片的长度为21字节(包含20字节的IP头,1字节的数据)
第5个IP数据报(echo reply)设置了MF表示(+),这个分片的长度为1500字节(包含20字节的IP头,8字节的icmp头,1472字节的数据)
第6个IP数据报(echo reply)没有设置flags(最后一个分片),这个分片的长度为21字节(包含20字节的IP头,1字节的数据)
另外:
4、6这两个分片,只打印了IP头里的协议字段为1(表示icmp),因为这个分片里不包含icmp消息的头
3、4这两个分片的id为10526
5、6这两个分片的id为58106
以太网的MTU为1500字节,所以最大的IP数据报总长度为1500字节(20+8+1472)时不需要分片,当发送1501字节的IP数据报时会执行分片
PMTU发现
ICMP unreachable - need to frag 错误消息格式: MTU of next-hop network:指示通往下一跳网络那个接口的MTU值
实验网络结构: 我们从host1主机上发送一个udp数据报到host2主机
首先在host1主机上配置一条host-route,网关是192.168.0.1
其次修改host1主机接口的MTU为2000(这样可以发送超过1500字节的IP数据报)
在一个终端执行pmtu(代码:https://gitee.com/q723937936/tcpip/blob/master/pmtu.cpp)
在另一个终端执行tcpdump
观察tcpdump输出(我标了序号):
第1个IP数据报设置了DF标志(表示不允许中间路由器执行分片操作),该IP数据报的总长度为1501字节
第2个IP数据报显示路由器给主机host1发送了一个ICMP错误消息:unreachable - need to frag (mtu 1500),该错误消息报告了下一跳网络的接口MTU为1500
最后一行是触发该ICMP错误的那个IP数据报(存放在ICMP错误消息的数据部分,共28个字节)
pmtu程序使用setsockopt设置了IP_MTU_DISCOVER选项,值为IP_PMTUDISC_PROBE
IP_MTU_DISCOVER选项有4个可能值,分别为:
IP_PMTUDISC_WANT - 发送主机IP模块执行pmtu发现,并根据ptmu的值和上层发送的数据长度来决定是否进行分片,如未进行分片,则IP数据报的DF置位,如执行了分片,则所有分片都不设置DF。这是linux上默认行为
IP_PMTUDISC_DONT - 发送主机IP模块不执行pmtu发现,使用本机的接口MTU和上层应用发送数据的长度来决定是否进行分片,无论是否执行了分片,外出的IP数据报的DF都不设置,也就是给中间路由器执行分片的机会
IP_PMTUDISC_DO - 发送主机的IP模块会执行pmtu发现,如果上层发送的数据报大于该pmtu则返回EMSGSIZE错误,否则外出的IP数据报DF置位
IP_PMTUDISC_PROBE - 发送主机的IP模块会执行pmtu发现,但是发送IP数据报时忽略该pmtu,如果上层发送的数据报大于本机的接口MTU则返回EMSGSIZE错误,否则外出的IP数据报DF置位
对于已连接的UDP socket,如果发送主机执行pmtu发现,可以使用IP_MTU socket option来获取ptmu的值
IP_PMTUDISC_PROBE这个选项值是给应用层发送超过pmtu大小(且DF置位)的IP数据报的能力,pmtu程序使用这个值
当IP模块发送IP数据报时,IP模块会获取外出接口的MTU或特定socket的PMTU,如果要发送的IP数据报的总长度超过MTU或PMTU,则IP模块可能会执行分片操作或返回EMSGSIZE错误(可以通过socket option来改变行为,下文有说明)
无论发送主机还是中间的路由器都可能会执行分片操作,但只有分片到达最终的目的主机才会执行重组操作
回忆IP数据报格式: 其中16-bit identification、3-bit flags、13-bit fragment offset与IP分片有关
16-bit identification:用于标识同一个IP数据报的不同分片,目的主机根据此id来判断一个IP数据报有哪些分片
3-bit flags:最低位是MF(more fragments flag),中间位是DF(dont fragment flag),最高位保留
13-bit fragment offset:分片包含的数据的偏移,单位8字节
分片被认为是不好的,是因为一个分片丢失,整个IP数据报都要重传
观察分片
在一个终端执行ping
代码: 全选
$ ping -c 1 -s 1472 192.168.0.3
$ ping -c 1 -s 1473 192.168.0.3
代码: 全选
$ sudo tcpdump -i enp0s3 -n -v icmp
tcpdump: listening on enp0s3, link-type EN10MB (Ethernet), capture size 262144 bytes
1. 23:28:43.900886 IP (tos 0x0, ttl 64, id 9008, offset 0, flags [DF], proto ICMP (1), length 1500)
192.168.0.6 > 192.168.0.3: ICMP echo request, id 12521, seq 1, length 1480
2. 23:28:43.901048 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto ICMP (1), length 1500)
192.168.0.3 > 192.168.0.6: ICMP echo reply, id 12521, seq 1, length 1480
3. 23:28:50.439809 IP (tos 0x0, ttl 64, id 10526, offset 0, flags [+], proto ICMP (1), length 1500)
192.168.0.6 > 192.168.0.3: ICMP echo request, id 12522, seq 1, length 1480
4. 23:28:50.439829 IP (tos 0x0, ttl 64, id 10526, offset 1480, flags [none], proto ICMP (1), length 21)
192.168.0.6 > 192.168.0.3: ip-proto-1
5. 23:28:50.440038 IP (tos 0x0, ttl 64, id 58106, offset 0, flags [+], proto ICMP (1), length 1500)
192.168.0.3 > 192.168.0.6: ICMP echo reply, id 12522, seq 1, length 1480
6. 23:28:50.440038 IP (tos 0x0, ttl 64, id 58106, offset 1480, flags [none], proto ICMP (1), length 21)
192.168.0.3 > 192.168.0.6: ip-proto-1
第1个IP数据报(echo request)设置了DF标志,IP数据报的总长度为1500字节(包含20字节的IP头,8字节的icmp头,1472字节的数据)
第2个IP数据报(echo reply)设置了DF标志,IP数据报的总长度为1500字节(包含20字节的IP头,8字节的icmp头,1472字节的数据)
第3个IP数据报(echo request)设置了MF表示(+),这个分片的长度为1500字节(包含20字节的IP头,8字节的icmp头,1472字节的数据)
第4个IP数据报(echo request)没有设置flags(最后一个分片),这个分片的长度为21字节(包含20字节的IP头,1字节的数据)
第5个IP数据报(echo reply)设置了MF表示(+),这个分片的长度为1500字节(包含20字节的IP头,8字节的icmp头,1472字节的数据)
第6个IP数据报(echo reply)没有设置flags(最后一个分片),这个分片的长度为21字节(包含20字节的IP头,1字节的数据)
另外:
4、6这两个分片,只打印了IP头里的协议字段为1(表示icmp),因为这个分片里不包含icmp消息的头
3、4这两个分片的id为10526
5、6这两个分片的id为58106
以太网的MTU为1500字节,所以最大的IP数据报总长度为1500字节(20+8+1472)时不需要分片,当发送1501字节的IP数据报时会执行分片
PMTU发现
ICMP unreachable - need to frag 错误消息格式: MTU of next-hop network:指示通往下一跳网络那个接口的MTU值
实验网络结构: 我们从host1主机上发送一个udp数据报到host2主机
首先在host1主机上配置一条host-route,网关是192.168.0.1
代码: 全选
$ sudo route add -host 192.168.0.3 gw 192.168.0.1 dev enp0s3
代码: 全选
$ sudo ifconfig enp0s3 mtu 2000
$ ifconfig enp0s3
enp0s3: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 2000
inet 192.168.0.6 netmask 255.255.255.0 broadcast 192.168.0.255
inet6 fe80::1a8b:c9c0:743f:9226 prefixlen 64 scopeid 0x20<link>
ether 08:00:27:c2:f3:77 txqueuelen 1000 (Ethernet)
RX packets 891271 bytes 272337548 (272.3 MB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 417697 bytes 108466468 (108.4 MB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
代码: 全选
$ ./pmtu 192.168.0.3 8888 1473
代码: 全选
$ sudo tcpdump -i enp0s3 -n -v udp or icmp
tcpdump: listening on enp0s3, link-type EN10MB (Ethernet), capture size 262144 bytes
1. 00:06:15.560808 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto UDP (17), length 1501)
192.168.0.6.34991 > 192.168.0.3.8888: UDP, length 1473
2. 00:06:15.565712 IP (tos 0xc0, ttl 64, id 18149, offset 0, flags [none], proto ICMP (1), length 576)
192.168.0.1 > 192.168.0.6: ICMP 192.168.0.3 unreachable - need to frag (mtu 1500), length 556
IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto UDP (17), length 1501)
192.168.0.6.34991 > 192.168.0.3.8888: UDP, length 1473
第1个IP数据报设置了DF标志(表示不允许中间路由器执行分片操作),该IP数据报的总长度为1501字节
第2个IP数据报显示路由器给主机host1发送了一个ICMP错误消息:unreachable - need to frag (mtu 1500),该错误消息报告了下一跳网络的接口MTU为1500
最后一行是触发该ICMP错误的那个IP数据报(存放在ICMP错误消息的数据部分,共28个字节)
pmtu程序使用setsockopt设置了IP_MTU_DISCOVER选项,值为IP_PMTUDISC_PROBE
IP_MTU_DISCOVER选项有4个可能值,分别为:
IP_PMTUDISC_WANT - 发送主机IP模块执行pmtu发现,并根据ptmu的值和上层发送的数据长度来决定是否进行分片,如未进行分片,则IP数据报的DF置位,如执行了分片,则所有分片都不设置DF。这是linux上默认行为
IP_PMTUDISC_DONT - 发送主机IP模块不执行pmtu发现,使用本机的接口MTU和上层应用发送数据的长度来决定是否进行分片,无论是否执行了分片,外出的IP数据报的DF都不设置,也就是给中间路由器执行分片的机会
IP_PMTUDISC_DO - 发送主机的IP模块会执行pmtu发现,如果上层发送的数据报大于该pmtu则返回EMSGSIZE错误,否则外出的IP数据报DF置位
IP_PMTUDISC_PROBE - 发送主机的IP模块会执行pmtu发现,但是发送IP数据报时忽略该pmtu,如果上层发送的数据报大于本机的接口MTU则返回EMSGSIZE错误,否则外出的IP数据报DF置位
对于已连接的UDP socket,如果发送主机执行pmtu发现,可以使用IP_MTU socket option来获取ptmu的值
IP_PMTUDISC_PROBE这个选项值是给应用层发送超过pmtu大小(且DF置位)的IP数据报的能力,pmtu程序使用这个值
-
- 帖子: 26
- 注册时间: 2023-02-26 9:59
- 系统: ubuntu
Re: 跟我一起学TCP/IP
UDP与ARP的交互
实验:
从ubuntu18.04主机192.168.0.6,向安卓手机192.168.0.4发送8192字节的udp数据
发送前确保arp cache没有缓存安卓手机的mac地址
sock程序地址:https://github.com/unpbook/unpv13e
在一个终端运行sock
在另一个终端运行tcpdump
观察tcpdump输出,linux每秒发送一次arp请求,直到arp应答返回才发送6个IP分片
Maximum UDP Datagram Size
IP数据报的总长度字段是两个字节,也就是理论上最大的IP数据报长度为65535字节
下面的实验验证在Ubuntu18.04上loopback接口可以发送的UDP数据报的最大大小
在一个终端运行sock
在另一个终端运行tcpdump
从上面的实验,可以看出Ubuntu18.04上可以发送的最大UDP数据报为65507字节(生成的IP数据报大小为65507+20+8=65535)
从tcpdump的输出看IP数据报没有分片,查看loopback的接口MTU为65536,因此IP模块不会执行分片操作
下面的实验验证在Ubuntu18.04上以太网接口可以发送的UDP数据报的最大大小
在一个终端运行sock
在另一个终端运行tcpdump
从上面的输出,Ubuntu18.04是支持发送65535字节的IP数据报(tcpdump没有输出所有的分片)
另外,安卓手机在30秒后回送了一个ICMP ip reassembly time exceeded错误消息,这说明安卓手机没有收到所有分片(什么原因?),因此重组分片超时了
上面的例子重试了几次,大部分时候是可以收到ICMP 192.168.0.4 udp port 8888 unreachable,这说明安卓手机成功接收了完整的IP数据报,tcpdump输出如下
前面提到过主机的IP实现不要求接收超过576字节的IP数据报,由上面的实验可知linux上无此限制。
但是当应用使用UDP时,我们仍然要考虑MTU,应用发送的数据大小最好不超过1500-20-8=1472字节,以避免IP分片
另外应用可以通过SO_RCVBUF和SO_SNDBUF这两个socket选项来设置发送和接收buffer,这两个buffer也会影响应用可以发送和接收的UDP数据报大小,这两个buffer的默认值保存在如下文件
send buffer:/proc/sys/net/core/wmem_max
recv buffer:/proc/sys/net/core/rmem_max
在Ubuntu18.04上他们的大小如下:
ICMP Source Quench Error
该消息是一种简单的流量控制方法,现在已经废弃
实验:
从ubuntu18.04主机192.168.0.6,向安卓手机192.168.0.4发送8192字节的udp数据
发送前确保arp cache没有缓存安卓手机的mac地址
sock程序地址:https://github.com/unpbook/unpv13e
在一个终端运行sock
代码: 全选
$ arp -n
Address HWtype HWaddress Flags Mask Iface
192.168.0.3 ether 90:9c:4a:c0:be:d0 C enp0s3
192.168.0.1 ether 2c:61:04:ba:ff:fa C enp0s3
$ sock -u -i -n1 -w8192 192.168.0.4 8888
代码: 全选
$ sudo tcpdump -i enp0s3 -n 'host 192.168.0.4 and (icmp or udp or arp)'
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on enp0s3, link-type EN10MB (Ethernet), capture size 262144 bytes
20:35:42.865074 ARP, Request who-has 192.168.0.4 tell 192.168.0.6, length 28
20:35:43.893040 ARP, Request who-has 192.168.0.4 tell 192.168.0.6, length 28
20:35:44.002977 ARP, Reply 192.168.0.4 is-at a4:45:19:6b:e4:d8, length 46
20:35:44.003021 IP 192.168.0.6.42718 > 192.168.0.4.8888: UDP, bad length 8192 > 1472
20:35:44.003057 IP 192.168.0.6 > 192.168.0.4: ip-proto-17
20:35:44.003071 IP 192.168.0.6 > 192.168.0.4: ip-proto-17
20:35:44.003086 IP 192.168.0.6 > 192.168.0.4: ip-proto-17
20:35:44.003100 IP 192.168.0.6 > 192.168.0.4: ip-proto-17
20:35:44.003115 IP 192.168.0.6 > 192.168.0.4: ip-proto-17
20:35:44.026525 IP 192.168.0.4 > 192.168.0.6: ICMP 192.168.0.4 udp port 8888 unreachable, length 556
Maximum UDP Datagram Size
IP数据报的总长度字段是两个字节,也就是理论上最大的IP数据报长度为65535字节
下面的实验验证在Ubuntu18.04上loopback接口可以发送的UDP数据报的最大大小
在一个终端运行sock
代码: 全选
$ sock -u -i -n1 -w65507 127.0.0.1 8888
$ sock -u -i -n1 -w65508 127.0.0.1 8888
write returned -1, expected 65508: Message too long
代码: 全选
$ sudo tcpdump -n -i lo udp or icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on lo, link-type EN10MB (Ethernet), capture size 262144 bytes
21:54:26.510097 IP 127.0.0.1.42965 > 127.0.0.1.8888: UDP, length 65507
21:54:26.510107 IP 127.0.0.1 > 127.0.0.1: ICMP 127.0.0.1 udp port 8888 unreachable, length 556
从tcpdump的输出看IP数据报没有分片,查看loopback的接口MTU为65536,因此IP模块不会执行分片操作
代码: 全选
$ ifconfig lo
lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
inet6 ::1 prefixlen 128 scopeid 0x10<host>
loop txqueuelen 1000 (Local Loopback)
RX packets 210 bytes 84038 (84.0 KB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 210 bytes 84038 (84.0 KB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
在一个终端运行sock
代码: 全选
$ sock -u -i -n1 -w65507 192.168.0.4 8888
$ sock -u -i -n1 -w65508 192.168.0.4 8888
write returned -1, expected 65508: Message too long
代码: 全选
sudo tcpdump -i enp0s3 -n 'host 192.168.0.4 and (udp or icmp)'
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on enp0s3, link-type EN10MB (Ethernet), capture size 262144 bytes
20:59:41.462183 IP 192.168.0.6.33612 > 192.168.0.4.8888: UDP, bad length 65507 > 1472
20:59:41.462212 IP 192.168.0.6 > 192.168.0.4: ip-proto-17
20:59:41.462217 IP 192.168.0.6 > 192.168.0.4: ip-proto-17
20:59:41.462221 IP 192.168.0.6 > 192.168.0.4: ip-proto-17
20:59:41.462225 IP 192.168.0.6 > 192.168.0.4: ip-proto-17
20:59:41.462229 IP 192.168.0.6 > 192.168.0.4: ip-proto-17
20:59:41.462233 IP 192.168.0.6 > 192.168.0.4: ip-proto-17
20:59:41.462635 IP 192.168.0.6 > 192.168.0.4: ip-proto-17
20:59:41.462684 IP 192.168.0.6 > 192.168.0.4: ip-proto-17
20:59:41.462685 IP 192.168.0.6 > 192.168.0.4: ip-proto-17
20:59:41.462686 IP 192.168.0.6 > 192.168.0.4: ip-proto-17
20:59:41.462687 IP 192.168.0.6 > 192.168.0.4: ip-proto-17
20:59:41.462785 IP 192.168.0.6 > 192.168.0.4: ip-proto-17
20:59:41.462867 IP 192.168.0.6 > 192.168.0.4: ip-proto-17
20:59:41.462869 IP 192.168.0.6 > 192.168.0.4: ip-proto-17
20:59:41.463017 IP 192.168.0.6 > 192.168.0.4: ip-proto-17
20:59:41.463025 IP 192.168.0.6 > 192.168.0.4: ip-proto-17
20:59:41.463026 IP 192.168.0.6 > 192.168.0.4: ip-proto-17
20:59:41.463027 IP 192.168.0.6 > 192.168.0.4: ip-proto-17
20:59:41.463028 IP 192.168.0.6 > 192.168.0.4: ip-proto-17
20:59:41.463028 IP 192.168.0.6 > 192.168.0.4: ip-proto-17
20:59:41.463042 IP 192.168.0.6 > 192.168.0.4: ip-proto-17
20:59:41.463146 IP 192.168.0.6 > 192.168.0.4: ip-proto-17
20:59:41.463377 IP 192.168.0.6 > 192.168.0.4: ip-proto-17
20:59:41.463384 IP 192.168.0.6 > 192.168.0.4: ip-proto-17
20:59:41.463385 IP 192.168.0.6 > 192.168.0.4: ip-proto-17
20:59:41.463386 IP 192.168.0.6 > 192.168.0.4: ip-proto-17
20:59:41.463387 IP 192.168.0.6 > 192.168.0.4: ip-proto-17
20:59:41.463388 IP 192.168.0.6 > 192.168.0.4: ip-proto-17
20:59:41.463389 IP 192.168.0.6 > 192.168.0.4: ip-proto-17
21:00:12.037916 IP 192.168.0.4 > 192.168.0.6: ICMP ip reassembly time exceeded, length 556
另外,安卓手机在30秒后回送了一个ICMP ip reassembly time exceeded错误消息,这说明安卓手机没有收到所有分片(什么原因?),因此重组分片超时了
上面的例子重试了几次,大部分时候是可以收到ICMP 192.168.0.4 udp port 8888 unreachable,这说明安卓手机成功接收了完整的IP数据报,tcpdump输出如下
代码: 全选
sudo tcpdump -i enp0s3 -n 'host 192.168.0.4 and (udp or icmp)'
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on enp0s3, link-type EN10MB (Ethernet), capture size 262144 bytes
21:24:48.212063 IP 192.168.0.6.47475 > 192.168.0.4.8888: UDP, bad length 65507 > 1472
21:24:48.212100 IP 192.168.0.6 > 192.168.0.4: ip-proto-17
21:24:48.212104 IP 192.168.0.6 > 192.168.0.4: ip-proto-17
21:24:48.212110 IP 192.168.0.6 > 192.168.0.4: ip-proto-17
21:24:48.212115 IP 192.168.0.6 > 192.168.0.4: ip-proto-17
21:24:48.212121 IP 192.168.0.6 > 192.168.0.4: ip-proto-17
21:24:48.212125 IP 192.168.0.6 > 192.168.0.4: ip-proto-17
21:24:48.212790 IP 192.168.0.6 > 192.168.0.4: ip-proto-17
21:24:48.324355 IP 192.168.0.4 > 192.168.0.6: ICMP 192.168.0.4 udp port 8888 unreachable, length 556
但是当应用使用UDP时,我们仍然要考虑MTU,应用发送的数据大小最好不超过1500-20-8=1472字节,以避免IP分片
另外应用可以通过SO_RCVBUF和SO_SNDBUF这两个socket选项来设置发送和接收buffer,这两个buffer也会影响应用可以发送和接收的UDP数据报大小,这两个buffer的默认值保存在如下文件
send buffer:/proc/sys/net/core/wmem_max
recv buffer:/proc/sys/net/core/rmem_max
在Ubuntu18.04上他们的大小如下:
代码: 全选
$ cat /proc/sys/net/core/wmem_max
212992
$ cat /proc/sys/net/core/rmem_max
212992
该消息是一种简单的流量控制方法,现在已经废弃
-
- 帖子: 26
- 注册时间: 2023-02-26 9:59
- 系统: ubuntu
Re: 跟我一起学TCP/IP
UDP Server Design
获取客户端IP地址和端口号
UDP Server 可以通过如下两个函数获取客户端IP地址和端口号
获取目的IP地址
linux上不支持IP_RECVDSTADDR socket选项,替代的选项是IP_PKTINFO,使能该选项后可以通过recvmsg接收辅助数据,辅助数据内存放的是如下结构:
上述结构中的ipi_addr就是目的IP地址
UDP Input Queue
UDP输入队列,就是receive buffer,该buffer的大小可以通过SO_RCVBUF设置
服务端
客户端
linux上的SO_RCVBUF选项有最小值限制,命令指定的接收buffer大小是256,实际linux设置的是2304
当客户端连续发送9个UDP数据报时,服务端只收到了4个UDP数据报,说明服务端的接收队列溢出了,后面接收的5个UDP数据报被丢弃了
sock程序地址:https://gitee.com/q723937936/unpv13e
Restricting Local IP Address
默认情况下,多个进程不能同时bind相同IP地址和相同端口号的地址结构
相同IP地址指:IP地址完全相同或其中一个IP地址为wildcard
要解除该限制,可以使用SO_REUSEADDR或SO_REUSEPORT选项
Restricting Foreign IP Address
UDP server也可以调用connect函数,只有connect指定的远程客户端才能向该UDP server发送UDP数据报
connect(2)手册有如下说明:
使能SO_REUSEADDR或SO_REUSEPORT选项的UDP server,可以启动多个实例
SO_REUSEADDR:使用该选项,则数据报递送给哪个实例可能不确定(我测试是递送给后启动的实例)
SO_REUSEPORT:使用该选项,多个实例之间会负载均衡
获取客户端IP地址和端口号
UDP Server 可以通过如下两个函数获取客户端IP地址和端口号
代码: 全选
recvfrom
recvmsg
linux上不支持IP_RECVDSTADDR socket选项,替代的选项是IP_PKTINFO,使能该选项后可以通过recvmsg接收辅助数据,辅助数据内存放的是如下结构:
代码: 全选
struct in_pktinfo {
unsigned int ipi_ifindex; /* Interface index */
struct in_addr ipi_spec_dst; /* Local address */
struct in_addr ipi_addr; /* Header Destination address */
};
UDP Input Queue
UDP输入队列,就是receive buffer,该buffer的大小可以通过SO_RCVBUF设置
服务端
代码: 全选
$ sock -s -u -v -E -R256 -r256 -P30000 6666
SO_RCVBUF = 2304
from 192.168.0.3, to 192.168.0.6: 1
from 192.168.0.3, to 192.168.0.6: 2
from 192.168.0.3, to 192.168.0.6: 3
from 192.168.0.3, to 192.168.0.6: 4
代码: 全选
$ nc -u 192.168.0.6 6666
1
2
3
4
5
6
7
8
9
当客户端连续发送9个UDP数据报时,服务端只收到了4个UDP数据报,说明服务端的接收队列溢出了,后面接收的5个UDP数据报被丢弃了
sock程序地址:https://gitee.com/q723937936/unpv13e
Restricting Local IP Address
默认情况下,多个进程不能同时bind相同IP地址和相同端口号的地址结构
相同IP地址指:IP地址完全相同或其中一个IP地址为wildcard
要解除该限制,可以使用SO_REUSEADDR或SO_REUSEPORT选项
Restricting Foreign IP Address
UDP server也可以调用connect函数,只有connect指定的远程客户端才能向该UDP server发送UDP数据报
connect(2)手册有如下说明:
多个UDP Server实例If the socket sockfd is of type SOCK_DGRAM, then addr is the address to which datagrams are sent by default, and the only address from which datagrams are received.
使能SO_REUSEADDR或SO_REUSEPORT选项的UDP server,可以启动多个实例
SO_REUSEADDR:使用该选项,则数据报递送给哪个实例可能不确定(我测试是递送给后启动的实例)
SO_REUSEPORT:使用该选项,多个实例之间会负载均衡
-
- 帖子: 26
- 注册时间: 2023-02-26 9:59
- 系统: ubuntu
Re: 跟我一起学TCP/IP
第十二章:广播和多播
有三种IP地址:
单播(unicast)
广播(broadcast)
多播(multicast)
对应的以太网地址也有三种:
单播(unicast)
广播(broadcast)
多播(multicast)
单播是一对一
广播是一对所有
多播最灵活,介于单播与广播之间,是一对多
广播和多播只适用于UDP协议
广播和多播的能力并不是由UDP模块提供的,而是由接口卡、链路层、网络层三个模块共同提供的
接口卡可以接收单播帧、广播帧、多播帧
默认情况下接口卡只能接收单播帧和广播帧,只有接口卡加入多播组才能接收多播帧
另外还可以将接口卡配置成混杂模式(promiscuous mode),该模式可以接收所有帧
以太网多播地址:最高字节的最低位为1;多播地址可以有很多,每个多播地址标识一个多播组(局域网内的一部分主机)
以太网广播地址:0xFFFFFFFFFFFF;该地址可以认为是一个特殊的多播地址,标识局域网内的所有主机(所有主机在一个多播组里)
广播的缺点是局域网内的所有主机都能收到广播帧,即使该主机对这个广播帧不感兴趣,比如:
以前上学时,老师用的一个网络教学软件控制教室里的所有电脑,假如教室里有50台电脑(都开机了),但是只有30个学生打开了网络教学软件(受控端),老师的每个操作都会发送一个UDP数据报,50台电脑都能收到这个UDP数据报,但是有20台电脑没有打开网络教学软件(受控端),这些UDP数据报要经过完整的协议栈,直到UDP模块才被丢弃(不产生ICMP-port unreachable错误,因为目的地址是广播地址),浪费了主机的处理能力。
多播的目的就是为了解决广播的缺点,多播采用自注册机制,只有打开网络教学软件的主机会将自己注册(加入)到(约定好的)同一个多播组,老师的操作对应的UDP数据报只会发送给这个组内的主机,其他主机不受影响。
网络层的广播
不像链路层的广播地址只有一个(0xFFFFFFFFFFFF),网络层的广播地址是针对(子)网络的,不同(子)网络有不同的广播地址
受限的广播(limited broadcast):也称为local broadcast,IP地址为255.255.255.255,目的地址为255.255.255.255的IP数据报会被广播到局域网内的所有主机,但是路由器绝对不会转发
面向网络的广播(net-directed broadcast):hostid全1的IP地址,路由器通常默认不会转发(为了防止DOS攻击)
面向子网的广播(subnet-directed broadcast):hostid全1的IP地址,路由器通常默认不会转发(为了防止DOS攻击)
上面后两种统称为directed broadcast
如果使用CIDR技术,实际上directed broadcast可以统一为面向域的广播(domain-directed broadcast)
广播还有个用途是服务发现(Service discovery):
比如局域网某个主机部署了一个UDP服务,局域网内的其他主机可以广播一个UDP数据报到指定端口,预期只有一个主机会响应一个应答,由此发现(获得)服务所在主机的IP地址(DHCP是一个例子)
另外ARP也利用了链路层广播来查询目标主机的硬件地址
一个例子
在linux主机上ping广播地址,先查看本机ip地址为192.168.0.6,arp cache显示还有另一台macos主机(192.168.0.3)和一个路由器(192.168.0.1)
从上面的输出看,只有192.168.0.3发送了应答,这是因为linux默认不会响应ICMP-echo request,可以通过如下命令修改默认配置:
再次ping广播地址:
可以看到linux主机192.168.0.6也发送了应答(广播包含子网内的所有主机,也包括自己)
编程说明
进程要想发送广播数据报,必须要使能SO_BROADCAST选项
开发一个udpecho程序和一个udpcli程序,测试UDP数据报被广播到所有主机
分别在192.168.0.3和192.168.0.6主机上运行udpecho命令
然后在192.168.0.6主机上运行udpcli命令,广播UDP数据报
代码参见:https://gitee.com/q723937936/tcpip/tree/master
UNP 第20章详细描述了广播
有三种IP地址:
单播(unicast)
广播(broadcast)
多播(multicast)
对应的以太网地址也有三种:
单播(unicast)
广播(broadcast)
多播(multicast)
单播是一对一
广播是一对所有
多播最灵活,介于单播与广播之间,是一对多
广播和多播只适用于UDP协议
广播和多播的能力并不是由UDP模块提供的,而是由接口卡、链路层、网络层三个模块共同提供的
接口卡可以接收单播帧、广播帧、多播帧
默认情况下接口卡只能接收单播帧和广播帧,只有接口卡加入多播组才能接收多播帧
另外还可以将接口卡配置成混杂模式(promiscuous mode),该模式可以接收所有帧
以太网多播地址:最高字节的最低位为1;多播地址可以有很多,每个多播地址标识一个多播组(局域网内的一部分主机)
以太网广播地址:0xFFFFFFFFFFFF;该地址可以认为是一个特殊的多播地址,标识局域网内的所有主机(所有主机在一个多播组里)
广播的缺点是局域网内的所有主机都能收到广播帧,即使该主机对这个广播帧不感兴趣,比如:
以前上学时,老师用的一个网络教学软件控制教室里的所有电脑,假如教室里有50台电脑(都开机了),但是只有30个学生打开了网络教学软件(受控端),老师的每个操作都会发送一个UDP数据报,50台电脑都能收到这个UDP数据报,但是有20台电脑没有打开网络教学软件(受控端),这些UDP数据报要经过完整的协议栈,直到UDP模块才被丢弃(不产生ICMP-port unreachable错误,因为目的地址是广播地址),浪费了主机的处理能力。
多播的目的就是为了解决广播的缺点,多播采用自注册机制,只有打开网络教学软件的主机会将自己注册(加入)到(约定好的)同一个多播组,老师的操作对应的UDP数据报只会发送给这个组内的主机,其他主机不受影响。
网络层的广播
不像链路层的广播地址只有一个(0xFFFFFFFFFFFF),网络层的广播地址是针对(子)网络的,不同(子)网络有不同的广播地址
受限的广播(limited broadcast):也称为local broadcast,IP地址为255.255.255.255,目的地址为255.255.255.255的IP数据报会被广播到局域网内的所有主机,但是路由器绝对不会转发
面向网络的广播(net-directed broadcast):hostid全1的IP地址,路由器通常默认不会转发(为了防止DOS攻击)
面向子网的广播(subnet-directed broadcast):hostid全1的IP地址,路由器通常默认不会转发(为了防止DOS攻击)
上面后两种统称为directed broadcast
如果使用CIDR技术,实际上directed broadcast可以统一为面向域的广播(domain-directed broadcast)
广播还有个用途是服务发现(Service discovery):
比如局域网某个主机部署了一个UDP服务,局域网内的其他主机可以广播一个UDP数据报到指定端口,预期只有一个主机会响应一个应答,由此发现(获得)服务所在主机的IP地址(DHCP是一个例子)
另外ARP也利用了链路层广播来查询目标主机的硬件地址
一个例子
在linux主机上ping广播地址,先查看本机ip地址为192.168.0.6,arp cache显示还有另一台macos主机(192.168.0.3)和一个路由器(192.168.0.1)
代码: 全选
$ ifconfig enp0s3 | grep inet
inet 192.168.0.6 netmask 255.255.255.0 broadcast 192.168.0.255
inet6 fe80::1a8b:c9c0:743f:9226 prefixlen 64 scopeid 0x20<link>
$ arp -n
Address HWtype HWaddress Flags Mask Iface
192.168.0.3 ether 90:9c:4a:c0:be:d0 C enp0s3
192.168.0.1 ether 2c:61:04:ba:ff:fa C enp0s3
$ ping -b 192.168.0.255
WARNING: pinging broadcast address
PING 192.168.0.255 (192.168.0.255) 56(84) bytes of data.
64 bytes from 192.168.0.3: icmp_seq=1 ttl=64 time=0.449 ms
64 bytes from 192.168.0.3: icmp_seq=2 ttl=64 time=0.705 ms
64 bytes from 192.168.0.3: icmp_seq=3 ttl=64 time=0.560 ms
代码: 全选
$ cat /proc/sys/net/ipv4/icmp_echo_ignore_broadcasts
1
$ sudo bash -c 'echo 0 > /proc/sys/net/ipv4/icmp_echo_ignore_broadcasts'
$ cat /proc/sys/net/ipv4/icmp_echo_ignore_broadcasts
0
代码: 全选
$ ping -b 192.168.0.255
WARNING: pinging broadcast address
PING 192.168.0.255 (192.168.0.255) 56(84) bytes of data.
64 bytes from 192.168.0.6: icmp_seq=1 ttl=64 time=0.059 ms
64 bytes from 192.168.0.3: icmp_seq=1 ttl=64 time=0.300 ms (DUP!)
64 bytes from 192.168.0.6: icmp_seq=2 ttl=64 time=0.026 ms
64 bytes from 192.168.0.3: icmp_seq=2 ttl=64 time=0.276 ms (DUP!)
64 bytes from 192.168.0.6: icmp_seq=3 ttl=64 time=0.063 ms
64 bytes from 192.168.0.3: icmp_seq=3 ttl=64 time=0.654 ms (DUP!)
编程说明
进程要想发送广播数据报,必须要使能SO_BROADCAST选项
开发一个udpecho程序和一个udpcli程序,测试UDP数据报被广播到所有主机
分别在192.168.0.3和192.168.0.6主机上运行udpecho命令
代码: 全选
$ ./udpecho 8888
代码: 全选
$ ./udpcli -b 192.168.0.255 8888
hello
from 192.168.0.6: hello
from 192.168.0.3: hello
how are you
from 192.168.0.6: how are you
from 192.168.0.3: how are you
UNP 第20章详细描述了广播
-
- 帖子: 26
- 注册时间: 2023-02-26 9:59
- 系统: ubuntu
Re: 跟我一起学TCP/IP
IP多播组地址
类D地址称为IP多播组地址,如下图
从上图可知IP多播组地址范围为:224.0.0.0-239.255.255.255,每个IP多播组地址标识一个多播组
整个32位称为IP多播组地址,低28位称为多播组ID,在不引起歧义的情况下,我们可以交换使用这两个术语
IP多播组地址到以太网多播组地址的转换:
前面说过,以太网多播组地址范围是:01:00:5E:00:00:00-01:00:5E:7F:FF:FF,也就是高25位是固定的,只有低23位可以用来映射
IP多播组地址的高4位也是固定的,要把IP多播组地址的低28位(也就是多播组ID)映射到以太网地址的低23位,必须忽略多播组ID的高5位,也就是IP多播组地址到以太网多播组地址的映射不是一一对应的,32个IP多播组地址映射到一个以太网多播组地址
从编程的角度看,我们其实并不关心IP多播组地址到以太网多播组地址是如何转换的,在接口加入多播组时是指定IP多播组地址,内核会将该IP多播组地址转换成相应的以太网多播组地址,并通知接口卡接收目的地址为该以太网多播组地址的帧
IP数据报的可路由性(routable)
目的地址为单播地址的IP数据报,路由器会转发
目的地址为255.255.255.255的IP数据报,路由器不会转发
目的地址为子网广播地址的IP数据报,路由器默认不会转发
目的地址为多播地址的IP数据报,分两种情况,如下:
IANA保留的一些well known多播组地址:
224.0.0.1 - all nodes
224.0.0.2 - all routers
224.0.0.4 - dvmrp
224.0.0.9 - ripv2
224.0.0.22 -igmp
224.0.0.251 - mDNS
查看接口加入的多播组
从输出看,enp0s3接口加入了224.0.0.251和224.0.0.1多播组
编程说明
加入多播组是通过IP_ADD_MEMBERSHIP选项来实现的,我修改udpecho服务,添加-m选项指定要加入的多播组
udpcli程序不需要做任何修改
分别在192.168.0.3和192.168.0.6两台主机上运行udpecho:
在192.168.0.6上运行udpcli:
再次查看接口加入的多播组:
上述输出表明,enp0s3接口已经加入224.0.0.88多播组
代码地址:https://gitee.com/q723937936/tcpip/tree/master
UNP第21章有多播的详细描述
类D地址称为IP多播组地址,如下图
从上图可知IP多播组地址范围为:224.0.0.0-239.255.255.255,每个IP多播组地址标识一个多播组
整个32位称为IP多播组地址,低28位称为多播组ID,在不引起歧义的情况下,我们可以交换使用这两个术语
IP多播组地址到以太网多播组地址的转换:
前面说过,以太网多播组地址范围是:01:00:5E:00:00:00-01:00:5E:7F:FF:FF,也就是高25位是固定的,只有低23位可以用来映射
IP多播组地址的高4位也是固定的,要把IP多播组地址的低28位(也就是多播组ID)映射到以太网地址的低23位,必须忽略多播组ID的高5位,也就是IP多播组地址到以太网多播组地址的映射不是一一对应的,32个IP多播组地址映射到一个以太网多播组地址
从编程的角度看,我们其实并不关心IP多播组地址到以太网多播组地址是如何转换的,在接口加入多播组时是指定IP多播组地址,内核会将该IP多播组地址转换成相应的以太网多播组地址,并通知接口卡接收目的地址为该以太网多播组地址的帧
IP数据报的可路由性(routable)
目的地址为单播地址的IP数据报,路由器会转发
目的地址为255.255.255.255的IP数据报,路由器不会转发
目的地址为子网广播地址的IP数据报,路由器默认不会转发
目的地址为多播地址的IP数据报,分两种情况,如下:
- 目的地址在224.0.0.0-224.0.0.255范围的IP数据报,路由器不会转发
- 目的地址在其他范围的IP数据报,路由器会转发
IANA保留的一些well known多播组地址:
224.0.0.1 - all nodes
224.0.0.2 - all routers
224.0.0.4 - dvmrp
224.0.0.9 - ripv2
224.0.0.22 -igmp
224.0.0.251 - mDNS
查看接口加入的多播组
代码: 全选
$ netstat -g -n
IPv6/IPv4 Group Memberships
Interface RefCnt Group
--------------- ------ ---------------------
lo 1 224.0.0.251
lo 1 224.0.0.1
enp0s3 1 224.0.0.251
enp0s3 1 224.0.0.1
lo 1 ff02::fb
lo 1 ff02::1
lo 1 ff01::1
enp0s3 1 ff02::fb
enp0s3 1 ff02::1:ff3f:9226
enp0s3 1 ff02::1
enp0s3 1 ff01::1
编程说明
加入多播组是通过IP_ADD_MEMBERSHIP选项来实现的,我修改udpecho服务,添加-m选项指定要加入的多播组
udpcli程序不需要做任何修改
分别在192.168.0.3和192.168.0.6两台主机上运行udpecho:
代码: 全选
$ ./udpecho -m 224.0.0.88 8888
代码: 全选
$ ./udpcli 224.0.0.88 8888
hi there
from 192.168.0.6: hi there
from 192.168.0.3: hi there
代码: 全选
$ netstat -g -n
IPv6/IPv4 Group Memberships
Interface RefCnt Group
--------------- ------ ---------------------
lo 1 224.0.0.251
lo 1 224.0.0.1
enp0s3 1 224.0.0.88
enp0s3 1 224.0.0.251
enp0s3 1 224.0.0.1
lo 1 ff02::fb
lo 1 ff02::1
lo 1 ff01::1
enp0s3 1 ff02::fb
enp0s3 1 ff02::1:ff3f:9226
enp0s3 1 ff02::1
enp0s3 1 ff01::1
代码地址:https://gitee.com/q723937936/tcpip/tree/master
UNP第21章有多播的详细描述
-
- 帖子: 26
- 注册时间: 2023-02-26 9:59
- 系统: ubuntu
Re: 跟我一起学TCP/IP
IGMP
我们前面学习过目的地址在224.0.0.0-224.0.0.255范围内的IP多播数据报,路由器不会转发,目的地址在其他范围的IP多播数据报会被路由器转发
路由器在转发IP多播数据报时,要么转发到与路由器相连的网络里的主机,要么转发到下一跳路由器,这就要求路由器的路由表里有相关的route entries
那么路由器如何知道向自己的路由表里添加哪些route entries呢?答案就是IGMP协议,IGMP协议是一个多播组发现协议
IGMP格式
上图所示为IGMPv1版本的协议格式
字段说明:
4-bit IGMP version: 1
4-bit IGMP type:1-query;2-reply
16-bit checksum:计算方法同UDP的checksum计算方法
32-bit group address:主机报告的组地址
路由器发现多播组的基本思路是:
1. 路由器向子网内多播一个IGMP数据报查询消息
2. 子网内的主机自动响应一个或多个IGMP应答
3. 路由器收到IGMP应答,在路由表里添加通往相应多播组地址的一个route
上述过程是由一个实现了DVMRP协议的multicast routing daemon实施的,实现DVMRP协议的路由器称为多播路由器
IGMP的query和reply消息类似ICMP的echo query和echo reply消息,query是由应用进程发起的,reply是内核自动产生的
ICMP echo query是ping程序发送的
IGMP query是multicast routing daemon程序发送的
IGMP详细操作过程
网络内的主机主动报告多播组:当一个接口加入多播组时,内核先后发送两个IGMP(type=2)多播数据报,目的地址是该多播组地址
多播路由器定时查询网络内的多播组:多播路由器定时发送IGMP(type=1)多播数据报,目的地址224.0.0.1(因为网络内所有nodes都默认加入这个组,所以所有node都能收到这条IGMP查询数据报)
网络内的主机收到IGMP查询消息后,查看内核维护的多播组表(netstat -gn),每行(224.0.0.1除外)产生一个IGMP reply,目的地址是该多播组地址
从上面的描述可知:多播路由器的接口必须能接收所有多播帧
TTL字段说明
默认情况下,进程发送的IP多播数据报的TTL字段设置为1,要想发送的IP多播数据报可以被多播路由器转发,发送进程必须显式设置TLL字段,因为IP单播数据报和IP多播数据报的TTL字段是分别维护的,所以设置TTL也使用不同的socket选项
IP单播数据报的TTL使用IP_TTL选项
IP多播数据报的TTL使用IP_MULTICAST_TTL选项
另外IP多播数据报和IP广播数据报一样,不会触发任何ICMP错误消息
观察主机报告IGMP消息
在一个终端执行udpecho
在另一个终端执行tcpdump
从tcpdump的输出,看到4条igmp v3记录,前两条是加入224.0.0.88多播组时报告的,后两条是离开224.0.0.88多播组时报告的
在IGMPv1版本协议操作里,离开多播组是不报告的IGMP消息的
发送igmp查询消息
开发一个igmpquery程序,发送igmp查询消息
示例1:
在一个终端执行igmpquery
在一个终端执行tcpdump
从上面的输出看到,当发送IGMPv1版本的查询消息时,内核也发送v1版本的应答
示例2:
在一个终端执行udpecho加入多播组224.0.0.88,然后执行igmpquery
在另一个终端执行tcpdump
前两条的输出是udpecho加入多播组224.0.0.88时报告的,紧接着是igmp query消息,随后有两台主机报告了两个多播组
我们前面学习过目的地址在224.0.0.0-224.0.0.255范围内的IP多播数据报,路由器不会转发,目的地址在其他范围的IP多播数据报会被路由器转发
路由器在转发IP多播数据报时,要么转发到与路由器相连的网络里的主机,要么转发到下一跳路由器,这就要求路由器的路由表里有相关的route entries
那么路由器如何知道向自己的路由表里添加哪些route entries呢?答案就是IGMP协议,IGMP协议是一个多播组发现协议
IGMP格式
上图所示为IGMPv1版本的协议格式
字段说明:
4-bit IGMP version: 1
4-bit IGMP type:1-query;2-reply
16-bit checksum:计算方法同UDP的checksum计算方法
32-bit group address:主机报告的组地址
路由器发现多播组的基本思路是:
1. 路由器向子网内多播一个IGMP数据报查询消息
2. 子网内的主机自动响应一个或多个IGMP应答
3. 路由器收到IGMP应答,在路由表里添加通往相应多播组地址的一个route
上述过程是由一个实现了DVMRP协议的multicast routing daemon实施的,实现DVMRP协议的路由器称为多播路由器
IGMP的query和reply消息类似ICMP的echo query和echo reply消息,query是由应用进程发起的,reply是内核自动产生的
ICMP echo query是ping程序发送的
IGMP query是multicast routing daemon程序发送的
IGMP详细操作过程
网络内的主机主动报告多播组:当一个接口加入多播组时,内核先后发送两个IGMP(type=2)多播数据报,目的地址是该多播组地址
多播路由器定时查询网络内的多播组:多播路由器定时发送IGMP(type=1)多播数据报,目的地址224.0.0.1(因为网络内所有nodes都默认加入这个组,所以所有node都能收到这条IGMP查询数据报)
网络内的主机收到IGMP查询消息后,查看内核维护的多播组表(netstat -gn),每行(224.0.0.1除外)产生一个IGMP reply,目的地址是该多播组地址
从上面的描述可知:多播路由器的接口必须能接收所有多播帧
TTL字段说明
默认情况下,进程发送的IP多播数据报的TTL字段设置为1,要想发送的IP多播数据报可以被多播路由器转发,发送进程必须显式设置TLL字段,因为IP单播数据报和IP多播数据报的TTL字段是分别维护的,所以设置TTL也使用不同的socket选项
IP单播数据报的TTL使用IP_TTL选项
IP多播数据报的TTL使用IP_MULTICAST_TTL选项
另外IP多播数据报和IP广播数据报一样,不会触发任何ICMP错误消息
观察主机报告IGMP消息
在一个终端执行udpecho
代码: 全选
$ ./udpecho -m 224.0.0.88 8888
^C
$
代码: 全选
$ sudo tcpdump -n -v igmp
tcpdump: listening on enp0s3, link-type EN10MB (Ethernet), capture size 262144 bytes
10:07:07.004416 IP (tos 0xc0, ttl 1, id 0, offset 0, flags [DF], proto IGMP (2), length 40, options (RA))
192.168.0.6 > 224.0.0.22: igmp v3 report, 1 group record(s) [gaddr 224.0.0.88 to_ex, 0 source(s)]
10:07:07.638984 IP (tos 0xc0, ttl 1, id 0, offset 0, flags [DF], proto IGMP (2), length 40, options (RA))
192.168.0.6 > 224.0.0.22: igmp v3 report, 1 group record(s) [gaddr 224.0.0.88 to_ex, 0 source(s)]
10:07:11.099510 IP (tos 0xc0, ttl 1, id 0, offset 0, flags [DF], proto IGMP (2), length 40, options (RA))
192.168.0.6 > 224.0.0.22: igmp v3 report, 1 group record(s) [gaddr 224.0.0.88 to_in, 0 source(s)]
10:07:11.699645 IP (tos 0xc0, ttl 1, id 0, offset 0, flags [DF], proto IGMP (2), length 40, options (RA))
192.168.0.6 > 224.0.0.22: igmp v3 report, 1 group record(s) [gaddr 224.0.0.88 to_in, 0 source(s)]
在IGMPv1版本协议操作里,离开多播组是不报告的IGMP消息的
发送igmp查询消息
开发一个igmpquery程序,发送igmp查询消息
示例1:
在一个终端执行igmpquery
代码: 全选
$ sudo ./igmpquery
代码: 全选
$ sudo tcpdump -n igmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on enp0s3, link-type EN10MB (Ethernet), capture size 262144 bytes
11:05:13.087660 IP 192.168.0.6 > 224.0.0.1: igmp query v1
11:05:13.764182 IP 192.168.0.3 > 224.0.0.251: igmp v1 report 224.0.0.251
示例2:
在一个终端执行udpecho加入多播组224.0.0.88,然后执行igmpquery
代码: 全选
$ ./udpecho -m 224.0.0.88 8888 &
$ sudo ./igmpquery
代码: 全选
$ sudo tcpdump -n igmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on enp0s3, link-type EN10MB (Ethernet), capture size 262144 bytes
11:09:16.507421 IP 192.168.0.6 > 224.0.0.88: igmp v1 report 224.0.0.88
11:09:21.333221 IP 192.168.0.6 > 224.0.0.88: igmp v1 report 224.0.0.88
11:09:25.510708 IP 192.168.0.6 > 224.0.0.1: igmp query v1
11:09:26.072933 IP 192.168.0.6 > 224.0.0.251: igmp v1 report 224.0.0.251
11:09:29.288262 IP 192.168.0.6 > 224.0.0.88: igmp v1 report 224.0.0.88
11:09:34.027950 IP 192.168.0.3 > 224.0.0.251: igmp v1 report 224.0.0.251
-
- 帖子: 1
- 注册时间: 2023-03-21 16:13
- 系统: win10
Re: 跟我一起学TCP/IP
全是专业人士。
-
- 帖子: 26
- 注册时间: 2023-02-26 9:59
- 系统: ubuntu
Re: 跟我一起学TCP/IP
第十四章:DNS
基本概念:
DNS:Domain Name System,该系统维护一个分布式数据库,数据库中保存的数据称为资源记录;分布式指的是该数据库由分布在世界各地的name server共同维护,每个name server维护一部分资源记录
resolver:实现DNS协议的客户端的库或程序,比如gethostbyname和gethostbyaddr就是调用resolver提供的接口
name server:实现DNS协议的服务端程序
对于DNS(Domain Name System)里的domain,我理解他跟CIDR(Classless Interdomain Routing)里的domain没有关系,在DNS里domain的含义类似C++里的name space,是一种组织name的方法。
DNS name space是一个层次结构,类似Unix文件系统,用Unix文件系统来对比理解DNS的层次结构非常有帮助
上图中的圆圈表示node(host、router等),对比Unix文件系统中的file(unix中的目录也是一种文件)
下面用表格列出Domain Name System和Unix File System的对比
node可以分为:
domain(类似Unix文件系统中的目录)
host(类似Unix文件系统中的文件)
但是不管node代表的是domain还是host,他的名字都称为:domain name
域名的管理
前面提到,DNS是一个分布式数据库,由不同的name servers共同维护
具体来说,域名的管理是按照域来划分的,不同域由不同的name servers负责
比如根域.有13个name servers:
顶级域com.也由13个name servers:
二级域google.com.由4个name servers:
三级域www.google.com. 没有自己的zone,由二级域的name servers管理
name server管理的域也称为zone,zone与domain的含义基本相同,一个细微的区别是:domain包含所有子树,就比如Unix文件系统中的目录,包含子目录以及子子目录,而zone有时候只包含子域,不包含子子域,因为子子域可能属于另一个zone
比如:顶级域com.和二级域google.com.就是属于不同的zone,而三级域www.google.com.和二级域google.com.在同一个zone里
zone对应的name servers称为该zone的authoritative name servers
authoritative name server一般有多个,一个primary name server和一个或多个secondary name servers
提供secondary name server是为了避免单点故障(single point of failure)
另外name server管理的数据库,实际上只是一个称为zone file的文件,该文件中记录了相关资源记录
查询方式:
当客户端请求一个name server(比如查询域名对应的ip地址),但是这个name server不是这个域名的权威服务器,也就是这个name server的数据库里没有要查询的域名的记录,此时该name server可以代理客户端向其他name server查询,有两种方式:
1. 迭代式
2. 递归式
迭代式:图片来源于《Computer Networking A Top-Down Approach》 6th Editon 上图中resolver发送的是递归请求,Local DNS Server发送的是迭代请求(这是BIND的默认方式)
递归式:图片来源于《Computer Networking A Top-Down Approach》 6th Editon 上图中resolver和Local DNS Server发送的都是递归请求
DNS cache
为了提高性能,上图中的Local DNS Server在第一次查询到映射信息时,会缓存到本地,下次收到客户端发送同样的请求就不需要再向其他name server查询了,缓存时间由DNS reply消息里的字段指定
基本概念:
DNS:Domain Name System,该系统维护一个分布式数据库,数据库中保存的数据称为资源记录;分布式指的是该数据库由分布在世界各地的name server共同维护,每个name server维护一部分资源记录
resolver:实现DNS协议的客户端的库或程序,比如gethostbyname和gethostbyaddr就是调用resolver提供的接口
name server:实现DNS协议的服务端程序
对于DNS(Domain Name System)里的domain,我理解他跟CIDR(Classless Interdomain Routing)里的domain没有关系,在DNS里domain的含义类似C++里的name space,是一种组织name的方法。
DNS name space是一个层次结构,类似Unix文件系统,用Unix文件系统来对比理解DNS的层次结构非常有帮助
上图中的圆圈表示node(host、router等),对比Unix文件系统中的file(unix中的目录也是一种文件)
下面用表格列出Domain Name System和Unix File System的对比
node可以分为:
domain(类似Unix文件系统中的目录)
host(类似Unix文件系统中的文件)
但是不管node代表的是domain还是host,他的名字都称为:domain name
域名的管理
前面提到,DNS是一个分布式数据库,由不同的name servers共同维护
具体来说,域名的管理是按照域来划分的,不同域由不同的name servers负责
比如根域.有13个name servers:
代码: 全选
$ host -t ns . | sort
. name server a.root-servers.net.
. name server b.root-servers.net.
. name server c.root-servers.net.
. name server d.root-servers.net.
. name server e.root-servers.net.
. name server f.root-servers.net.
. name server g.root-servers.net.
. name server h.root-servers.net.
. name server i.root-servers.net.
. name server j.root-servers.net.
. name server k.root-servers.net.
. name server l.root-servers.net.
. name server m.root-servers.net.
代码: 全选
$ host -t ns com. | sort
com name server a.gtld-servers.net.
com name server b.gtld-servers.net.
com name server c.gtld-servers.net.
com name server d.gtld-servers.net.
com name server e.gtld-servers.net.
com name server f.gtld-servers.net.
com name server g.gtld-servers.net.
com name server h.gtld-servers.net.
com name server i.gtld-servers.net.
com name server j.gtld-servers.net.
com name server k.gtld-servers.net.
com name server l.gtld-servers.net.
com name server m.gtld-servers.net.
代码: 全选
$ host -t ns google.com. | sort
google.com name server ns1.google.com.
google.com name server ns2.google.com.
google.com name server ns3.google.com.
google.com name server ns4.google.com.
代码: 全选
$ host -t ns www.google.com.
www.google.com has no NS record
比如:顶级域com.和二级域google.com.就是属于不同的zone,而三级域www.google.com.和二级域google.com.在同一个zone里
zone对应的name servers称为该zone的authoritative name servers
authoritative name server一般有多个,一个primary name server和一个或多个secondary name servers
提供secondary name server是为了避免单点故障(single point of failure)
另外name server管理的数据库,实际上只是一个称为zone file的文件,该文件中记录了相关资源记录
查询方式:
当客户端请求一个name server(比如查询域名对应的ip地址),但是这个name server不是这个域名的权威服务器,也就是这个name server的数据库里没有要查询的域名的记录,此时该name server可以代理客户端向其他name server查询,有两种方式:
1. 迭代式
2. 递归式
迭代式:图片来源于《Computer Networking A Top-Down Approach》 6th Editon 上图中resolver发送的是递归请求,Local DNS Server发送的是迭代请求(这是BIND的默认方式)
递归式:图片来源于《Computer Networking A Top-Down Approach》 6th Editon 上图中resolver和Local DNS Server发送的都是递归请求
DNS cache
为了提高性能,上图中的Local DNS Server在第一次查询到映射信息时,会缓存到本地,下次收到客户端发送同样的请求就不需要再向其他name server查询了,缓存时间由DNS reply消息里的字段指定
上次由 723937936@qq.com 在 2023-03-23 12:19,总共编辑 1 次。
-
- 帖子: 26
- 注册时间: 2023-02-26 9:59
- 系统: ubuntu
Re: 跟我一起学TCP/IP
DNS Message Format
查询消息和应答消息都使用上图所示的消息格式
字段说明:
identification:用于匹配查询消息和应答消息
flags:查询消息和应答消息的一些flags,格式如下图
QR:0-query;1-reply
opcode:0-standard query;1-inverse query;2-server status request
AA:authoritative answer,指示发送应答的name server是否是所请求的域名的权威服务器,0-no;1-yes
TC:truncated,指示应答消息是否被截断,当DNS使用UDP时,服务器发送的应答的UDP数据报的总长度限制为512字节,如果大于512,则只发送512字节,并设置该位
RD:recursion desired,查询消息设置该位表示使用递归方式查询
RA:recursion available,应答消息设置该位,是告诉客户端自己支持递归方式查询
zero:保留,必须为0
rcode:return code,应答消息使用rcode表示错误,0-no erro; 3-name error
flags字段的字节序说明:
flags字段是两个字节,左边字节存放在低地址,右边字节存放在高地址,也就是说要先发送左边字节后发送右边字节,flags的定义如下:
stackoverflow上有个相关帖子:https://stackoverflow.com/questions/595 ... -structure
number of questions:问题数
number of answer RRs:回答数
question格式:
字段说明:
query name:要查询的域名,表示成label的序列,每个label以1个字节的长度开头,后跟label的ascii码表示
比如:www.baidu.com. 表示成:3www5baidu3com0
name的表示有一种压缩方案:当label的长度字节的高两位被置位时,此时该长度字节和随后的一个字节表示一个16-bit(实际上是低14-bit)的指针,该值表示一个偏移值(相对DNS消息头,单位是字节)
query type:要查询的资源记录类型,1-A,5-CNAME,12-PTR 等等
query class:查询类,填1,表示Internet address
resource record格式
字段说明:
domain name:格式与query name相同
type:同query type,1-A,5-CNAME,12-PTR 等等
class:同query class,填1,表示Internet address
time-to-live:客户端可以cache的时长,单位秒
resource data lenght:resource data的长度,如果是type是1(A record),则为4,单位字节
resource data:如果是type是1(A record),则为4字节的IP address;如果是5(CNAME record),则为canonical name
编程
学习完DNS消息格式后,为了加深理解,我开发了一个简单的客户端工具用于查询域名对应的IP地址
代码地址:https://gitee.com/q723937936/tcpip/blob ... /mydig.cpp
下面是一些示例:
对比dig命令的输出
dig命令输出的ANSWER SECTION部分与mydig基本一致,除了time-to-live字段,是因为dig是做了cache(使用tcpdump验证,并不是每次dig都会发送查询请求)
实际上并不是dig做了cache,而是resolver做了cache,linux上的resolver由systemd-resolved服务实现
另外:
虽然DNS协议支持一个查询请求携带多个questions,但是经过我测试,大多数name server不支持,要么没有应答,要么只应答第一个question
stackoverflow上有一个讨论:https://stackoverflow.com/questions/265 ... cification
查询消息和应答消息都使用上图所示的消息格式
字段说明:
identification:用于匹配查询消息和应答消息
flags:查询消息和应答消息的一些flags,格式如下图
QR:0-query;1-reply
opcode:0-standard query;1-inverse query;2-server status request
AA:authoritative answer,指示发送应答的name server是否是所请求的域名的权威服务器,0-no;1-yes
TC:truncated,指示应答消息是否被截断,当DNS使用UDP时,服务器发送的应答的UDP数据报的总长度限制为512字节,如果大于512,则只发送512字节,并设置该位
RD:recursion desired,查询消息设置该位表示使用递归方式查询
RA:recursion available,应答消息设置该位,是告诉客户端自己支持递归方式查询
zero:保留,必须为0
rcode:return code,应答消息使用rcode表示错误,0-no erro; 3-name error
flags字段的字节序说明:
flags字段是两个字节,左边字节存放在低地址,右边字节存放在高地址,也就是说要先发送左边字节后发送右边字节,flags的定义如下:
代码: 全选
struct {
unsigned short rd:1, tc:1, aa:1, opcode:4, qr:1, rcode:4, zero:3, ra:1;
} flags;
number of questions:问题数
number of answer RRs:回答数
question格式:
字段说明:
query name:要查询的域名,表示成label的序列,每个label以1个字节的长度开头,后跟label的ascii码表示
比如:www.baidu.com. 表示成:3www5baidu3com0
name的表示有一种压缩方案:当label的长度字节的高两位被置位时,此时该长度字节和随后的一个字节表示一个16-bit(实际上是低14-bit)的指针,该值表示一个偏移值(相对DNS消息头,单位是字节)
query type:要查询的资源记录类型,1-A,5-CNAME,12-PTR 等等
query class:查询类,填1,表示Internet address
resource record格式
字段说明:
domain name:格式与query name相同
type:同query type,1-A,5-CNAME,12-PTR 等等
class:同query class,填1,表示Internet address
time-to-live:客户端可以cache的时长,单位秒
resource data lenght:resource data的长度,如果是type是1(A record),则为4,单位字节
resource data:如果是type是1(A record),则为4字节的IP address;如果是5(CNAME record),则为canonical name
编程
学习完DNS消息格式后,为了加深理解,我开发了一个简单的客户端工具用于查询域名对应的IP地址
代码地址:https://gitee.com/q723937936/tcpip/blob ... /mydig.cpp
下面是一些示例:
代码: 全选
$ ./mydig www.baidu.com
www.baidu.com. 124 IN CNAME www.a.shifen.com.
www.a.shifen.com. 124 IN A 180.101.50.188
www.a.shifen.com. 124 IN A 180.101.50.242
代码: 全选
$ ./mydig www.jd.com
www.jd.com. 124 IN CNAME www.jd.com.gslb.qianxun.com.
www.jd.com.gslb.qianxun.com. 124 IN CNAME www.jd.com.s.galileo.jcloud-cdn.com.
www.jd.com.s.galileo.jcloud-cdn.com. 124 IN CNAME wwwv6.jcloudimg.com.
wwwv6.jcloudimg.com. 124 IN A 121.226.246.3
wwwv6.jcloudimg.com. 124 IN A 222.186.184.150
代码: 全选
$ dig www.baidu.com
; <<>> DiG 9.11.3-1ubuntu1.18-Ubuntu <<>> www.baidu.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 47447
;; flags: qr rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 65494
;; QUESTION SECTION:
;www.baidu.com. IN A
;; ANSWER SECTION:
www.baidu.com. 122 IN CNAME www.a.shifen.com.
www.a.shifen.com. 146 IN A 180.101.50.242
www.a.shifen.com. 146 IN A 180.101.50.188
;; Query time: 14 msec
;; SERVER: 127.0.0.53#53(127.0.0.53)
;; WHEN: Wed Mar 22 14:05:14 CST 2023
;; MSG SIZE rcvd: 101
实际上并不是dig做了cache,而是resolver做了cache,linux上的resolver由systemd-resolved服务实现
另外:
虽然DNS协议支持一个查询请求携带多个questions,但是经过我测试,大多数name server不支持,要么没有应答,要么只应答第一个question
stackoverflow上有一个讨论:https://stackoverflow.com/questions/265 ... cification
-
- 帖子: 26
- 注册时间: 2023-02-26 9:59
- 系统: ubuntu
Re: 跟我一起学TCP/IP
Pointer Queries
指针查询,指的是查询ip地址对应的域名
想象一下在Unix文件系统中,一个文件路径实际上是一个key,文件路径是用来索引文件系统里的文件的,文件的内容才是value。
对比DNS,同样的道理,域名也是DNS数据库的key,key对应的value是资源记录
在我们平时写代码时,经常会用到map数据结构,有时候我们想要根据key获取value,有时候反过来,想要根据value获取key,这两种需求都可以通过map数据结构完成。
在DNS里采用相同的思路,根据ip地址获取域名,也是将ip地址表示成一个域名,在DNS里专门保留了一个域来完成这个工作,即:in-addr.arpa.
举例:
比如ip地址180.101.50.188,在dns里的key是这样的:188.50.101.180.in-addr.arpa.
这个key索引的value是一个PTR类型的资源记录,该记录中保存的是key对应的域名
我们之前开发的mydig工具支持查询PTR类型的资源记录:
选择kernel.org这个域名是因为这个域名对应的IP地址有一个PTR记录;我试了几个常见的国内域名,对应的IP地址都没有PTR记录
Hostname Spoofing Check
有的server使用一种技术来认证客户端,该技术的思路是:
1. server端本地配置一些域名,只允许来自这些域名的客户端访问自己
2. 当连接进来后,server用客户端的IP地址做一个PTR查询,来获取对应的域名,如果该域名在自己的配置允许访问的域名范围内,则允许访问
这种技术应该已经废弃了,因为大多数IP地址在DNS里都没有PTR记录
Resource Records
前面说了,域名是key,资源记录是value,但是一个key可以对应多个value(类似multimap),多个value可以是相同类型也可以是不同类型
比如:一个域名可以有一个CNAME记录(canonical name)和多个A记录(IP地址)
资源记录的类型:
A:值是IP地址
PTR:值是IP地址对应的域名
CNAME:值是规范名,如果一个主机有多个域名,那么其中一个是规范名,其他都是别名,别名是为了方便(或许是一种惯例),比如www.baidu.com.是别名,对应的规范名是:www.a.shifen.com.
HINFO:值是主机信息,记录主机的CPU和操作系统信息(我测试了一些常见域名,都没有该记录)
MX:值是mail server的域名
NS:值是name server的域名
下面举一些例子:
mydig工具支持显式指定要查询的值类型
A记录:
PTR记录:
CNAME记录:
MX记录:
NS记录:
Caching
书上说cache是dns server维护的,resolver不会,但是现在linux上的resolver(systemd-resolved)也会维护一个cache,因为systemd-resolved是一个独立的服务,cache也是被主机上的所有应用共享,从而提高性能
指针查询,指的是查询ip地址对应的域名
想象一下在Unix文件系统中,一个文件路径实际上是一个key,文件路径是用来索引文件系统里的文件的,文件的内容才是value。
对比DNS,同样的道理,域名也是DNS数据库的key,key对应的value是资源记录
在我们平时写代码时,经常会用到map数据结构,有时候我们想要根据key获取value,有时候反过来,想要根据value获取key,这两种需求都可以通过map数据结构完成。
在DNS里采用相同的思路,根据ip地址获取域名,也是将ip地址表示成一个域名,在DNS里专门保留了一个域来完成这个工作,即:in-addr.arpa.
举例:
比如ip地址180.101.50.188,在dns里的key是这样的:188.50.101.180.in-addr.arpa.
这个key索引的value是一个PTR类型的资源记录,该记录中保存的是key对应的域名
我们之前开发的mydig工具支持查询PTR类型的资源记录:
代码: 全选
$ ./mydig kernel.org
kernel.org. 124 IN A 139.178.84.217
$ ./mydig 139.178.84.217
217.84.178.139.in-addr.arpa. 3600 IN PTR dfw.source.kernel.org.
$ ./mydig dfw.source.kernel.org.
dfw.source.kernel.org. 3600 IN A 139.178.84.217
Hostname Spoofing Check
有的server使用一种技术来认证客户端,该技术的思路是:
1. server端本地配置一些域名,只允许来自这些域名的客户端访问自己
2. 当连接进来后,server用客户端的IP地址做一个PTR查询,来获取对应的域名,如果该域名在自己的配置允许访问的域名范围内,则允许访问
这种技术应该已经废弃了,因为大多数IP地址在DNS里都没有PTR记录
Resource Records
前面说了,域名是key,资源记录是value,但是一个key可以对应多个value(类似multimap),多个value可以是相同类型也可以是不同类型
比如:一个域名可以有一个CNAME记录(canonical name)和多个A记录(IP地址)
资源记录的类型:
A:值是IP地址
PTR:值是IP地址对应的域名
CNAME:值是规范名,如果一个主机有多个域名,那么其中一个是规范名,其他都是别名,别名是为了方便(或许是一种惯例),比如www.baidu.com.是别名,对应的规范名是:www.a.shifen.com.
HINFO:值是主机信息,记录主机的CPU和操作系统信息(我测试了一些常见域名,都没有该记录)
MX:值是mail server的域名
NS:值是name server的域名
下面举一些例子:
mydig工具支持显式指定要查询的值类型
A记录:
代码: 全选
$ ./mydig -t a google.com
google.com. 124 IN A 142.251.42.238
代码: 全选
$ ./mydig -t ptr 142.251.42.238
238.42.251.142.in-addr.arpa. 304 IN PTR tsa01s11-in-f14.1e100.net.
代码: 全选
$ ./mydig -t cname www.baidu.com
www.baidu.com. 124 IN CNAME www.a.shifen.com.
代码: 全选
$ ./mydig -t mx qq.com
qq.com. 1895 IN MX 10 mx3.qq.com.
qq.com. 1895 IN MX 20 mx2.qq.com.
qq.com. 1895 IN MX 30 mx1.qq.com.
代码: 全选
$ ./mydig -t ns google.com
google.com. 38872 IN NS ns1.google.com.
google.com. 38872 IN NS ns3.google.com.
google.com. 38872 IN NS ns4.google.com.
google.com. 38872 IN NS ns2.google.com.
书上说cache是dns server维护的,resolver不会,但是现在linux上的resolver(systemd-resolved)也会维护一个cache,因为systemd-resolved是一个独立的服务,cache也是被主机上的所有应用共享,从而提高性能