跟我一起学TCP/IP

Web、Mail、Ftp、DNS、Proxy、VPN、Samba、LDAP 等基础网络服务
723937936@qq.com
帖子: 51
注册时间: 2023-02-26 9:59
系统: ubuntu

Re: 跟我一起学TCP/IP

#16

帖子 723937936@qq.com » 2023-03-11 8:26

CIDR(Classless Interdomain Routing)

本书的重点不是路由协议,作者对OSPF和BGP只进行了简单概述,我们也不进行深入研究了

互联网一开始设计的路由架构是基于网络的,将网络分为3类,分别是Class A、Class B、Class C
每个网络要求一个路由表项

回忆一下地址分类
Screen Shot 2023-02-28 at 9.35.06 PM.png
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 次。
723937936@qq.com
帖子: 51
注册时间: 2023-02-26 9:59
系统: ubuntu

Re: 跟我一起学TCP/IP

#17

帖子 723937936@qq.com » 2023-03-12 21:12

第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格式
Screen Shot 2023-03-12 at 11.22.52 AM.png
Screen Shot 2023-03-12 at 11.23.05 AM.png
字段说明:
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,结构如下:
Screen Shot 2023-03-12 at 6.14.00 PM.png
为了计算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)
编程Note:

如果使用raw socket发送UDP数据报,需要应用程序构造完整的UDP数据报(包含UDP header)所以需要计算UDP校验和(参考UNP第28章)
上次由 723937936@qq.com 在 2023-03-15 7:02,总共编辑 2 次。
723937936@qq.com
帖子: 51
注册时间: 2023-02-26 9:59
系统: ubuntu

Re: 跟我一起学TCP/IP

#18

帖子 723937936@qq.com » 2023-03-15 6:28

IP分片

当IP模块发送IP数据报时,IP模块会获取外出接口的MTU或特定socket的PMTU,如果要发送的IP数据报的总长度超过MTU或PMTU,则IP模块可能会执行分片操作或返回EMSGSIZE错误(可以通过socket option来改变行为,下文有说明)

无论发送主机还是中间的路由器都可能会执行分片操作,但只有分片到达最终的目的主机才会执行重组操作

回忆IP数据报格式:
Screen Shot 2023-02-27 at 10.01.38 PM.png
其中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
在另一个终端运行tcpdump

代码: 全选

$ 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
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 错误消息格式:
Screen Shot 2023-03-15 at 12.08.44 AM.png
MTU of next-hop network:指示通往下一跳网络那个接口的MTU值

实验网络结构:
Screen Shot 2023-03-15 at 5.25.26 AM.png
我们从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
其次修改host1主机接口的MTU为2000(这样可以发送超过1500字节的IP数据报)

代码: 全选

$ 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(代码:https://gitee.com/q723937936/tcpip/blob/master/pmtu.cpp

代码: 全选

$ ./pmtu 192.168.0.3 8888 1473
在另一个终端执行tcpdump

代码: 全选

$ 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
观察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程序使用这个值
723937936@qq.com
帖子: 51
注册时间: 2023-02-26 9:59
系统: ubuntu

Re: 跟我一起学TCP/IP

#19

帖子 723937936@qq.com » 2023-03-15 21:59

UDP与ARP的交互

实验:

从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
在另一个终端运行tcpdump

代码: 全选

$ 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
观察tcpdump输出,linux每秒发送一次arp请求,直到arp应答返回才发送6个IP分片

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
在另一个终端运行tcpdump

代码: 全选

$ 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
从上面的实验,可以看出Ubuntu18.04上可以发送的最大UDP数据报为65507字节(生成的IP数据报大小为65507+20+8=65535)
从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
下面的实验验证在Ubuntu18.04上以太网接口可以发送的UDP数据报的最大大小

在一个终端运行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
在另一个终端运行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
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
从上面的输出,Ubuntu18.04是支持发送65535字节的IP数据报(tcpdump没有输出所有的分片)
另外,安卓手机在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
前面提到过主机的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上他们的大小如下:

代码: 全选

$ cat /proc/sys/net/core/wmem_max
212992
$ cat /proc/sys/net/core/rmem_max
212992
ICMP Source Quench Error

该消息是一种简单的流量控制方法,现在已经废弃
723937936@qq.com
帖子: 51
注册时间: 2023-02-26 9:59
系统: ubuntu

Re: 跟我一起学TCP/IP

#20

帖子 723937936@qq.com » 2023-03-16 23:06

UDP Server Design

获取客户端IP地址和端口号

UDP Server 可以通过如下两个函数获取客户端IP地址和端口号

代码: 全选

recvfrom
recvmsg
获取目的IP地址

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 */
};
上述结构中的ipi_addr就是目的IP地址

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
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)手册有如下说明:
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.
多个UDP Server实例

使能SO_REUSEADDR或SO_REUSEPORT选项的UDP server,可以启动多个实例

SO_REUSEADDR:使用该选项,则数据报递送给哪个实例可能不确定(我测试是递送给后启动的实例)
SO_REUSEPORT:使用该选项,多个实例之间会负载均衡
723937936@qq.com
帖子: 51
注册时间: 2023-02-26 9:59
系统: ubuntu

Re: 跟我一起学TCP/IP

#21

帖子 723937936@qq.com » 2023-03-18 11:09

第十二章:广播和多播

有三种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
从上面的输出看,只有192.168.0.3发送了应答,这是因为linux默认不会响应ICMP-echo request,可以通过如下命令修改默认配置:

代码: 全选

$ 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广播地址:

代码: 全选

$ 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!)
可以看到linux主机192.168.0.6也发送了应答(广播包含子网内的所有主机,也包括自己)

编程说明

进程要想发送广播数据报,必须要使能SO_BROADCAST选项

开发一个udpecho程序和一个udpcli程序,测试UDP数据报被广播到所有主机

分别在192.168.0.3和192.168.0.6主机上运行udpecho命令

代码: 全选

$ ./udpecho 8888
然后在192.168.0.6主机上运行udpcli命令,广播UDP数据报

代码: 全选

$ ./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
代码参见:https://gitee.com/q723937936/tcpip/tree/master
UNP 第20章详细描述了广播
723937936@qq.com
帖子: 51
注册时间: 2023-02-26 9:59
系统: ubuntu

Re: 跟我一起学TCP/IP

#22

帖子 723937936@qq.com » 2023-03-18 22:14

IP多播组地址

类D地址称为IP多播组地址,如下图
Screen Shot 2023-03-18 at 5.52.14 PM.png
从上图可知IP多播组地址范围为:224.0.0.0-239.255.255.255,每个IP多播组地址标识一个多播组
整个32位称为IP多播组地址,低28位称为多播组ID,在不引起歧义的情况下,我们可以交换使用这两个术语

IP多播组地址到以太网多播组地址的转换:
Screen Shot 2023-03-18 at 6.06.43 PM.png
前面说过,以太网多播组地址范围是: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数据报,路由器会转发
224.0.0.0-224.0.0.255范围内的多播组地址称为link-local多播组地址

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
从输出看,enp0s3接口加入了224.0.0.251和224.0.0.1多播组

编程说明

加入多播组是通过IP_ADD_MEMBERSHIP选项来实现的,我修改udpecho服务,添加-m选项指定要加入的多播组
udpcli程序不需要做任何修改

分别在192.168.0.3和192.168.0.6两台主机上运行udpecho:

代码: 全选

$ ./udpecho -m 224.0.0.88 8888
在192.168.0.6上运行udpcli:

代码: 全选

$ ./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
上述输出表明,enp0s3接口已经加入224.0.0.88多播组

代码地址:https://gitee.com/q723937936/tcpip/tree/master
UNP第21章有多播的详细描述
723937936@qq.com
帖子: 51
注册时间: 2023-02-26 9:59
系统: ubuntu

Re: 跟我一起学TCP/IP

#23

帖子 723937936@qq.com » 2023-03-20 11:17

IGMP

我们前面学习过目的地址在224.0.0.0-224.0.0.255范围内的IP多播数据报,路由器不会转发,目的地址在其他范围的IP多播数据报会被路由器转发
路由器在转发IP多播数据报时,要么转发到与路由器相连的网络里的主机,要么转发到下一跳路由器,这就要求路由器的路由表里有相关的route entries
那么路由器如何知道向自己的路由表里添加哪些route entries呢?答案就是IGMP协议,IGMP协议是一个多播组发现协议

IGMP格式
Screen Shot 2023-03-20 at 8.51.52 AM.png
Screen Shot 2023-03-20 at 8.52.08 AM.png
上图所示为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
$
在另一个终端执行tcpdump

代码: 全选

$ 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)]
从tcpdump的输出,看到4条igmp v3记录,前两条是加入224.0.0.88多播组时报告的,后两条是离开224.0.0.88多播组时报告的
在IGMPv1版本协议操作里,离开多播组是不报告的IGMP消息的

发送igmp查询消息

开发一个igmpquery程序,发送igmp查询消息

示例1:

在一个终端执行igmpquery

代码: 全选

$ sudo ./igmpquery
在一个终端执行tcpdump

代码: 全选

$ 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
从上面的输出看到,当发送IGMPv1版本的查询消息时,内核也发送v1版本的应答

示例2:

在一个终端执行udpecho加入多播组224.0.0.88,然后执行igmpquery

代码: 全选

$ ./udpecho -m 224.0.0.88 8888 &
$ sudo ./igmpquery
在另一个终端执行tcpdump

代码: 全选

$ 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
前两条的输出是udpecho加入多播组224.0.0.88时报告的,紧接着是igmp query消息,随后有两台主机报告了两个多播组
久速服务器
帖子: 1
注册时间: 2023-03-21 16:13
系统: win10

Re: 跟我一起学TCP/IP

#24

帖子 久速服务器 » 2023-03-21 16:44

全是专业人士。
723937936@qq.com
帖子: 51
注册时间: 2023-02-26 9:59
系统: ubuntu

Re: 跟我一起学TCP/IP

#25

帖子 723937936@qq.com » 2023-03-22 12:07

第十四章: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的层次结构非常有帮助
Screen Shot 2023-03-22 at 9.53.36 AM.png
上图中的圆圈表示node(host、router等),对比Unix文件系统中的file(unix中的目录也是一种文件)
下面用表格列出Domain Name System和Unix File System的对比
Screen Shot 2023-03-22 at 10.25.19 AM.png
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.
顶级域com.也由13个name servers:

代码: 全选

$ 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.
二级域google.com.由4个name servers:

代码: 全选

$ 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.
三级域www.google.com. 没有自己的zone,由二级域的name servers管理

代码: 全选

$ host -t ns www.google.com.
www.google.com has no NS record
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
Screen Shot 2023-03-22 at 11.49.37 AM.png
上图中resolver发送的是递归请求,Local DNS Server发送的是迭代请求(这是BIND的默认方式)

递归式:图片来源于《Computer Networking A Top-Down Approach》 6th Editon
Screen Shot 2023-03-22 at 11.50.03 AM.png
上图中resolver和Local DNS Server发送的都是递归请求

DNS cache

为了提高性能,上图中的Local DNS Server在第一次查询到映射信息时,会缓存到本地,下次收到客户端发送同样的请求就不需要再向其他name server查询了,缓存时间由DNS reply消息里的字段指定
上次由 723937936@qq.com 在 2023-03-23 12:19,总共编辑 1 次。
723937936@qq.com
帖子: 51
注册时间: 2023-02-26 9:59
系统: ubuntu

Re: 跟我一起学TCP/IP

#26

帖子 723937936@qq.com » 2023-03-22 14:22

DNS Message Format
Screen Shot 2023-03-22 at 2.11.09 PM.png
查询消息和应答消息都使用上图所示的消息格式

字段说明:
identification:用于匹配查询消息和应答消息
flags:查询消息和应答消息的一些flags,格式如下图
Screen Shot 2023-03-22 at 2.11.20 PM.png
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;
stackoverflow上有个相关帖子:https://stackoverflow.com/questions/595 ... -structure

number of questions:问题数
number of answer RRs:回答数

question格式:
Screen Shot 2023-03-22 at 2.11.34 PM.png
字段说明:
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格式
Screen Shot 2023-03-22 at 2.11.50 PM.png
字段说明:
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命令的输出

代码: 全选

$ 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命令输出的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
723937936@qq.com
帖子: 51
注册时间: 2023-02-26 9:59
系统: ubuntu

Re: 跟我一起学TCP/IP

#27

帖子 723937936@qq.com » 2023-03-23 15:07

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类型的资源记录:

代码: 全选

$ ./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
选择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记录:

代码: 全选

$ ./mydig -t a google.com
google.com.        124     IN      A       142.251.42.238
PTR记录:

代码: 全选

$ ./mydig -t ptr 142.251.42.238
238.42.251.142.in-addr.arpa.        304     IN      PTR     tsa01s11-in-f14.1e100.net.
CNAME记录:

代码: 全选

$ ./mydig -t cname www.baidu.com
www.baidu.com.        124     IN      CNAME   www.a.shifen.com.
MX记录:

代码: 全选

$ ./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.
NS记录:

代码: 全选

$ ./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.
Caching

书上说cache是dns server维护的,resolver不会,但是现在linux上的resolver(systemd-resolved)也会维护一个cache,因为systemd-resolved是一个独立的服务,cache也是被主机上的所有应用共享,从而提高性能
723937936@qq.com
帖子: 51
注册时间: 2023-02-26 9:59
系统: ubuntu

Re: 跟我一起学TCP/IP

#28

帖子 723937936@qq.com » 2023-03-24 19:46

第十五章:TFTP

TFTP(Trivial File transfer Protocol):是一个简单的文件传输协议,该协议最初主要用于无盘系统获取引导镜像
如今,TFTP可能用于一些嵌入式设备上的文件上传和下载

TFTP协议的坐骑是UDP协议,知名端口是69
TFTP协议比较简单,本章只有寥寥数页,我就不复述书上的协议格式了

tftp的安装:

在Ubuntu18.04上常用的tftp服务端和客户端程序是:tftpd-hpa和tftp-hpa

代码: 全选

$ sudo apt install tftpd-hpa tftp-hpa
安装好后,tftpd已经自动运行了

代码: 全选

$ ps -ef | grep [t]ftpd
root      6823     1  0 19:10 ?        00:00:00 /usr/sbin/in.tftpd --listen --user tftp --address :69 --secure /var/lib/tftpboot
上面的命令名字是in.tftpd

可以查看一下tftpd-hpa软件包安装的内容

代码: 全选

$ dpkg -L tftpd-hpa
...                                 # 省略一些行
/usr/sbin/in.tftpd
...                                 # 省略一些行

tftpd的配置:

tftpd-hpa的配置文件是/etc/default/tftpd-hpa

代码: 全选

$ cat /etc/default/tftpd-hpa
# /etc/default/tftpd-hpa

TFTP_USERNAME="tftp"
TFTP_DIRECTORY="/var/lib/tftpboot"
TFTP_ADDRESS=":69"
TFTP_OPTIONS="--secure"
我们需要对这个配置文件做两个修改:
1. 修改TFTP_USERNAME
2. 修改TFTP_OPTIONS

修改后如下:

代码: 全选

$ cat /etc/default/tftpd-hpa
# /etc/default/tftpd-hpa

TFTP_USERNAME="root"
TFTP_DIRECTORY="/var/lib/tftpboot"
TFTP_ADDRESS=":69"
TFTP_OPTIONS="--secure --create"
TFTP_USERNAME修改为root后,就可以在目录创建文件了
TFTP_OPTIONS选项里增加--create选项才可以上传任意文件(tftpd-hpa默认只能覆盖文件,不能新建文件)

修改完后重启服务:

代码: 全选

$ sudo systemctl restart tftpd-hpa.service
重启后,再ps一次:

代码: 全选

$ ps -ef | grep [t]ftpd
root      6823     1  0 19:10 ?        00:00:00 /usr/sbin/in.tftpd --listen --user root --address :69 --secure --create /var/lib/tftpboot
命令行选项已经变为修改后的了

上传文件

代码: 全选

$ echo hello > file                   # 创建一个文件用于上传
$ tftp localhost -c put file        # 上传文件file
$ cat /var/lib/tftpboot/file       # 验证文件是否被上传到/var/lib/tftpboot目录下
hello
下载文件

代码: 全选

$ sudo bash -c 'echo abc > /var/lib/tftpboot/test'        # 在/var/lib/tftpboot目录下创建一个test文件
$ tftp localhost -c get test                                             # 下载test文件到当前目录
$ cat test                                                                        # 验证test文件内容
abc
723937936@qq.com
帖子: 51
注册时间: 2023-02-26 9:59
系统: ubuntu

Re: 跟我一起学TCP/IP

#29

帖子 723937936@qq.com » 2023-03-25 8:03

第十六章:BOOTP

无盘系统的引导分为两个步骤:
1. 从服务器上下载一个引导镜像(使用TFTP协议),然后使用该镜像启动
2. 启动完成后再配置主机的IP地址、子网掩码、默认网关以及DNS server的IP地址等(使用BOOTP协议)

无盘系统因为没有磁盘,所有上述2个步骤的程序是烧写在ROM中的,在这个ROM中至少实现了BOOTP、TFTP、UDP、IP and device driver等程序

如今无盘系统应该比较罕见了,我们本章只关注第二个步骤,因为如今有盘系统也使用类似的协议(DHCP)来获取主机的配置信息

在BOOTP协议出现之前,无盘系统获取IP地址是使用RARP协议,获取子网掩码是使用ICMP(address mask)协议,在BOOTP出现后,这两种技术已经废弃

BOOTP(Bootstrap Protocol):BOOTP是一种主机配置协议,是DHCP的前身,主要用于获取主机的IP地址、子网掩码、默认网关以及DNS server的IP地址

BOOTP协议的坐骑是UDP协议
BOOTP服务端知名端口是67,客户端也分配了一个知名端口68,原因是允许服务端发送广播应答(因为向临时端口发送广播数据报是不好的)
在Unix系统上的BOOTP应答一般采用单播方式

BOOTP协议的格式:
Screen Shot 2023-03-25 at 6.56.43 AM.png
Screen Shot 2023-03-25 at 6.57.00 AM.png
Screen Shot 2023-03-25 at 7.23.49 AM.png
BOOTP协议总长度固定为300个字节

字段说明:
opcode:1-request;2-reply
hardware type:值为1,表示Ethernet
hardware address length:值为6,表示以太网地址长度
hop count:当路由器作为proxy server时,可以用来转发bootp数据报,这种用法现在应该已经废弃了
transaction ID:客户端用于匹配request和reply
number of seconds:客户端填写,比如客户端第一次发送BOOTP请求时填0,如果客户端5秒内未收到应答,则第二次发送BOOTP请求填5,目的是让备份的BOOTP server响应应答(因为备份的BOOTP server认为主server可能down机了)
client IP address:客户端IP地址,一般填0.0.0.0,因为客户端还不知道自己的IP地址
your IP address:由服务端填写为客户端分配的IP地址
server IP address:由服务端填写自己的IP地址
gateway IP address:由服务端填写默认网关地址
client hardware address:由客户端填写自己的硬件地址(服务端根据硬件地址从自己的配置文件中获取客户端的IP地址)
server hostname:服务端填写自己的主机名,可忽略
boot filename:用于客户端下载的引导镜像的文件名,可忽略
vendor-specific information:扩展字段,主要包括子网掩码和DNS server的IP地址
扩展信息以99.130.83.99的magic cookie开头,表示随后是扩展字段
随后的扩展字段是以一个字节的tag开头,表示扩展字段的类型,其中tag为0表示pad字节,tag为0xff表示最后一个扩展字段
tag 1:表示子网掩码,后跟一个字节的长度字段,表示后面数据的长度,子网掩码为4个字节,后跟4个字节的子网掩码
tag 6:表示DNS server的IP地址


BOOTP reply

BOOTP request是使用广播方式,目的IP地址为255.255.255.255

BOOTP reply一般是使用单播方式,目的IP地址为客户端的IP地址,但是这里存在一个鸡生蛋,蛋生鸡的问题,当服务端发送IP单播数据报时,IP模块会调用ARP模块获取目的主机的硬件地址,但是因为客户端此时还不知道自己的IP地址,所以不会响应ARP请求,服务端的解决办法是使用ioctl接口手动添加一个arp entry到自己的arp cache里,这样就不需要使用ARP查询目的主机的硬件地址了
723937936@qq.com
帖子: 51
注册时间: 2023-02-26 9:59
系统: ubuntu

Re: 跟我一起学TCP/IP

#30

帖子 723937936@qq.com » 2023-03-25 23:16

第17章:TCP

TCP(Transmission Control Protocol):TCP是一个传输层协议,它为应用层提供面向连接的、可靠的、字节流服务

广播和多播不适用于TCP协议

TCP传输的数据单元称为segment,这个词非常贴切,因为TCP将应用层传入的字节流按自己的想法切割成一个个segment,然后将一个个segment传递给IP模块,由IP模块将segment封装到IP数据报中发送

本章主要学习TCP的消息格式:
Screen Shot 2023-03-25 at 11.15.58 PM.png
字段说明:
source port number:源端口
destination port number:目的端口
sequence number:TCP发送的数据流的每个字节都有一个序号,sequence number字段表示的是该segment里包含的数据的首字节的序号
当建立连接时,客户端和服务端要同步sequence number,在建立连接时客户端发送的SYN segment和服务端发送的SYN+ACK segment里携带的sequence number称为initial sequence number(ISN),连接建立成功后,发送的数据的第一个字节的sequence number是ISN+1,因为SYN segment消耗一个序号(FIN segment也消耗一个序号)
acknowledgment number:发送ACK segment的一端期望接收的下一个字节的序号(意思好比说,前面的数据我都收到了,我期望接收的下一个segment里的sequence numbe是这个序号)
header length:header长度,包含选项,单位是4字节
flags字段:
URG:该flag置位表示urgent pointer字段有效(Out-of-Band Data)
ACK:该flag置位表示acknowledgment number字段有效
PSH:该flag置位表示要求接收端立即将数据传给应用(交互式应用)
RST:reset the connection,TCP报告错误的手段
SYN:用于客户端和服务端同步sequence number
FIN:关闭连接
window size:接收端通告自己可以接收的最大数据大小(动态变化)
checksum:与UDP的checksum计算方法一样,也要包含pseudo-header
urgent pointer:表示紧急数据的偏移量(相对于sequence number)
options:TCP选项字段,后面学到再说

连接的唯一标识

四元组唯一标识一个连接:
(source IP address, source port number, destination IP address, destination port number)
前两个合起来称为client socket,后两个合起来称为server socket,client socket和server socket合起来称为socket pair

观察TCP segment

同样为了加深映像,我们开发一个mytcpdump程序,打印segment的header信息

在192.168.0.6主机运行tcp server

代码: 全选

$ nc -l 8888
hello
在192.168.0.3主机运行tcp client

代码: 全选

$ nc 192.168.0.6 8888
hello
^C
在192.168.0.6主机运行mytcpdump

代码: 全选

$ sudo ./mytcpdump port 8888
IP 192.168.0.3.59034 > 192.168.0.6.8888: flags [S], seq 1722868799, win 65535, length 0
IP 192.168.0.6.8888 > 192.168.0.3.59034: flags [SA], seq 3750771232, ack 1722868800, win 65160, length 0
IP 192.168.0.3.59034 > 192.168.0.6.8888: flags [A], seq 1722868800, ack 3750771233, win 2058, length 0

IP 192.168.0.3.59034 > 192.168.0.6.8888: flags [PA], seq 1722868800, ack 3750771233, win 2058, length 6
IP 192.168.0.6.8888 > 192.168.0.3.59034: flags [A], seq 3750771233, ack 1722868806, win 510, length 0

IP 192.168.0.3.59034 > 192.168.0.6.8888: flags [FA], seq 1722868806, ack 3750771233, win 2058, length 0
IP 192.168.0.6.8888 > 192.168.0.3.59034: flags [FA], seq 3750771233, ack 1722868807, win 510, length 0
IP 192.168.0.3.59034 > 192.168.0.6.8888: flags [A], seq 1722868807, ack 3750771234, win 2058, length 0
前面三个segment是tcp建立连接时的交互
中间二个segment是客户端发送一个6字节的数据("hello\n")以及服务端的确认
后面三个segment是tcp终止连接的交互

注意点:
1. 除了第一个segment,其他segment都设置了ACK flag
2. 第一个segment通告他的ISN为1722868799
3. 第二个segment的确认序号为1722868800(=1722868799+1),另外他通告的ISN为3750771232
4. 第三个segment的确认序号为3750771233(=3750771232+1)
5. 除了第四个segment的数据长度为6字节,其他segment的数据长度都为0
6. 倒数第二个segment,服务端发送FIN的同时捎带了ACK,这称为delayed ACK,也叫piggyback(搭顺风车)
回复