You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

274 lines
16 KiB
Markdown

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 答疑(一)| 第1~5讲思考题答案
你好,我是胜辉。
不知不觉咱们已经完成预习篇、实战一的TCP真实案例篇还有实战二的应用层真实案例篇的学习了。一路走来我自己完成了20多讲的备课过程中是辛劳和满足相伴。当然你也很不容易坚持学完20多讲一共投入的时间也接近20个小时这个过程本身就是一种小小的成就。
由于课程涉及的网络知识面还是比较广泛的如果你还有不少没看懂的地方也别着急这是很正常的情况。就我自己来说最初看那些TCP/IP概念的时候也是摸不到头脑后来我就是反复看从不同的资料、不同的案例来入手才逐步建立起自己的理解。
正因为**通过问题来理解概念**是一种很好的学习方法,所以我在每节课的后面,都留有一两个思考题,希望可以促进思考。这些问题,有些比较容易,有些可能略有难度。所以我整理了答疑篇,一方面给出答案供你参考,一方面也正好把课程里没有覆盖到的细节给你展开一下,作为内容上的补充。总之,我希望通过这个答疑环节,能帮助你把看过的知识都真的消化掉,成为你自己知识体系的一部分。
## 01讲的答疑
### 思考题
1. traceroute默认是用UDP来做探测的那这个又是基于什么原理呢通和不通我们会收到怎样的回复
2. 有时候运行telnet后命令就挂起没有响应了这说明了什么问题呢
### 答案
关于第一个问题,我看到很多同学的回答都已经很到位了,比如以**@webmin**同学的回答为例:
> traceroute使用UDP探测时初始时把TTL设置为1经过路由器时TTL会被减1当TTL变为0时包被丢弃路由器向源地址发回一个ICMP超时通知ICMP Time Exceeded Message。源收到这个通知就会把下一次发送的包的TTL在原来的基础加1这样就可以多前进一步。探测时使用了一个大于30000的端口号去连接随着TTL的增加端口也会加1目地服务器在收到这个数据包的时候会返回一个端口不可达的ICMP错误信息ICMP Port Unreachable当源地址收到ICMP Port Unreachable包时会停止traceroute。
在我的Ubuntu虚拟机上traceroute使用的起始UDP目的端口是33434然后每次探测的TTL加一的同时UDP目的端口也加一每次探测会发送3次探测报文。也就是下图这样
![图片](https://static001.geekbang.org/resource/image/00/b1/004a31737f71e76c5c18f8a6152370b1.jpg?wh=943x395)
当时的traceroute命令行的输出是这样的
```bash
traceroute www.baidu.com
traceroute to www.a.shifen.com (180.101.49.11), 64 hops max
  1   10.0.2.2  0.082ms  0.177ms  0.088ms
  2   192.168.1.1  4.634ms  1.811ms  1.488ms
  3   100.65.0.1  9.972ms  11.511ms  6.189ms
  4   61.152.54.125  16.296ms  3.576ms  4.095ms
  5   61.152.24.102  7.329ms  6.786ms  11.987ms
  6   202.97.71.6  28.938ms  10.568ms  10.069ms
  7   58.213.95.2  15.036ms  9.831ms  17.043ms
  8   *  *  *
  9   58.213.96.54  16.843ms  10.329ms  15.989ms
 10   *  *  *
 11   *  *  *
 12   *  *  *
 13   *  *  *
 14   *  *  *
 15   *  *  *
 16   *  *  *
```
这次traceroute没有停住而是一直到64跳的限制后才停止因为输出过长我没有把全部的信息复制过来。当然一般实践中你看到连续六七个以上的`*`基本就可以判定是目的端对UDP无响应可以按下 Ctrl + C 终止了。
我把整个流程的实现原理,概括成了下面的示意图,希望能帮助你理解:
![](https://static001.geekbang.org/resource/image/47/16/47d5afb17a8a478e3bc1e0700e09d116.jpg?wh=2000x1125)
然后我们再用ICMP模式做一次traceroute输出如下
```bash
$ traceroute -I www.baidu.com
traceroute to www.a.shifen.com (180.101.49.12), 64 hops max
  1   10.0.2.2  0.105ms  0.229ms  0.173ms
  2   192.168.1.1  8.972ms  1.725ms  1.326ms
  3   100.65.0.1  9.850ms  7.893ms  7.311ms
  4   *  *  61.152.53.149  5.579ms
  5   *  61.152.25.230  20.238ms  5.204ms
  6   *  *  *
  7   *  58.213.94.114  21.249ms  *
  8   58.213.94.126  13.869ms  *  *
  9   58.213.96.54  11.736ms  10.094ms  10.644ms
 10   *  *  *
 11   *  *  *
 12   *  *  *
 13   180.101.49.12  24.780ms  9.443ms  10.201ms
```
这次就能顺利完成了这是因为目的IP对ICMP是有回复的所以traceroute就在最后一跳也就是第13跳顺利停止了。
这里还有个小技巧。你有没有发现 **UDP和ICMP这两种模式下路径信息是可以互补的**比如UDP模式下没有显示第5和第7跳的IP但是ICMP模式下就有。这就是为什么我建议你把两种模式分别跑一下这样可以获取到尽可能完整的路径。
![图片](https://static001.geekbang.org/resource/image/86/40/86ae98fb8478cb41eb9fc93a49561d40.jpg?wh=852x290)
第二个问题是关于telnet挂起。这里说的“挂起”是指没有进一步的反应了比如像下图这样
![图片](https://static001.geekbang.org/resource/image/0f/46/0f8c8b279ae1910bc52678a724e75f46.jpg?wh=271x54)
究其原因就是telnet发送了SYN后没有收到SYN+ACK。我们在[第21讲](https://time.geekbang.org/column/article/493040)提到了系统调用那么对于telnet这样的用户空间程序来说它要发起TCP连接就必须调用 **connect()** 这个系统调用后者会负责发出SYN。但是SYN发出去后对端没有回复SYN+ACK这就导致connect()阻塞telnet程序也只好等在那里表象上就是挂起了。
你可以参考这个示意图来理解整个过程:
![](https://static001.geekbang.org/resource/image/2e/ce/2ee1c167f34cdecec04d2e7116a8b2ce.jpg?wh=2000x682)
当然在第21讲我们也学习过strace这个工具了所以我们可以用strace来验证这个过程。直接运行下面的命令
```bash
strace telnet www.baidu.com 500
```
就能看到strace停留在connect()系统调用这里了:
```bash
......
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 3
setsockopt(3, SOL_IP, IP_TOS, [16], 4)  = 0
connect(3, {sa_family=AF_INET, sin_port=htons(500), sin_addr=inet_addr("45.113.192.102")}, 16
```
## 02讲的答疑
### 思考题
1. 请你用偏移量方法写一个tcpdump抓取TCP SYN包的过滤表达式。
2. 如果确定问题是在IP层tcpdump命令如何写可以做到既找到IP层的问题又节约抓包文件大小呢
### 答案
很多同学都写出了正确的偏移量的过滤表达式,也就是:
```bash
tcpdump 'tcp[13]&2 != 0'
```
当然这个题目没有指明是一定只要SYN包我们用上面这条命令可以抓到SYN和SYN+ACK这两种报文。而如果要指定只抓取SYN包而不抓取SYN+ACK可以用下面的表达式
```bash
tcpdump 'tcp[13]|2 = 2'
```
> 注意:这里的运算符是“`|`”,也就是字符或运算符。
第二个问题的关键点是tcpdump的 **\-s参数**我们通过它可以指定要抓取的每个报文的大小。不过那就有个问题了这里的“报文”是指IP分组还是二层帧呢
虽然tcpdump名称里是tcp实际上它能抓取udp、ip、icmp等等各种报文因为它抓取的就是第二层的帧。所以说**这里的“报文大小”,指的是二层帧的大小**。我在[第2讲](https://time.geekbang.org/column/article/478189)里也贴了一张图,展示了在某一次抓包里,一个报文的各层占用的字节数:
![图片](https://static001.geekbang.org/resource/image/0c/b6/0c23b60f7dcb62843e331e9b2972c8b6.png?wh=1860x548)
一般来说IP头部就是20字节加上帧头14字节所以**理论上我们只要抓取前34字节的报文就可以获取到二层和三层的信息了**。
不过且慢,有个同学报告了一个问题:
> 老师对于“问题2”我有个疑问。我最开始用的命令是“`tcpdump -i any -s 34`”,发现-s写成34就抓不到网络层的目的地址字段用Wireshark分析后发现帧头不知道还叫不叫这个Wireshark显示为 `Linux cooked capture v1`占了16个字节写成36就能把信息抓全了但是写成“`tcpdump -i eth0 -s 34`”就可以抓全。
这个问题我之前也没留意到于是去查证了一下。原来在tcpdump里对于any这个接口它在输出帧头的时候用了一种叫 **Linux cooked capture v1** 的格式这个格式的长度是16个字节而以太网帧是14字节。这就造成了2个字节的差异也就是按原先的34字节去抓取就不够了所以要扩大到36字节才可以抓到完整的IP头部。
我们看一个例子:
![图片](https://static001.geekbang.org/resource/image/49/e1/49494a525f79d26abb3d23cefc075fe1.jpg?wh=668x280)
> 补充上面的traceroute udp模式的示例文件就是用-i any模式抓取的所以它的二层帧头就是 `Linux cooked capture v1` 格式。
另外“IP头部20字节”这个描述其实还有一个隐含的前提**IP头部没有做扩展**Options。跟TCP类似IP头部也可以扩展但是一般用的比较少在公网上IP Options的报文有可能被丢弃所以很少被采用。我们在内网也曾经做过实验启用了IP Options结果发现虽然内网设备可以支持IP Options但是延迟增加了很多所以最后还是取消了它。
## 03讲的答疑
### 思考题
1. 在Linux中还有一个内核参数也是关于握手的net.ipv4.tcp\_synack\_retries。你知道这个参数是用来做什么的吗
2. 如果握手双方一方支持Window Scale一方不支持那么在这个连接里Window Scale最终会被启用吗你可以参考[RFC1323](https://datatracker.ietf.org/doc/html/rfc1323),给出你的解答。
### 答案
第一个问题是关于Linux内核参数的。对于各种网络相关的内核参数来说最快捷的方法可能就是直接在Linux主机里面查看TCP的手册了也就是执行
```bash
man tcp
```
然后搜索tcp\_synack\_retries就可以了也就是这个部分
> tcp\_synack\_retries (integer; default: 5; since Linux 2.2)
> The maximum number of times a SYN/ACK segment for a passive TCP connection will be retransmitted.  This number should not be higher than 255.
也就说,**这是TCP回复SYN+ACK后等不到ACK时需要重试的次数**。
第二个问题是关于Window Scale下面简称WS在握手中的具体实现。就像我题目里说的你可以直接去找到[RFC1323](https://datatracker.ietf.org/doc/html/rfc1323#page-8),然后找到这部分的内容:
> This option may be sent in an initial segment (i.e., a segment with the SYN bit on and the ACK bit off). It may also be sent in a <SYN,ACK> segment, but only if a Window Scale option was received in the initial segment. A Window Scale option in a segment without a SYN bit should be ignored.
直译过来就是这个WS选项可以在SYN包中也可以在SYN+ACK包中。但只有收到的SYN中有这个WS选项回复的SYN+ACK才可以加上这个选项。如果收到的报文不带SYN标志位但却带上了WS选项这样的WS应该被忽略。
也就是说如果发过来的SYN里不带WS选项那回复的SYN+ACK也不应该带WS自然这次的连接里也就不会用上WS了。简单来说只要有一方不支持WS这次的连接里就不会用WS。
## 04讲的答疑
### 思考题
1. 如果要在Wireshark中搜索到挥手阶段出现的RST+ACK报文那么这个过滤器该如何写呢
2. 你有没有通过抓包分析,解决过应用层的奇怪问题呢?你是怎么做的呢?
### 答案
第一个问题里挥手阶段出现RST的现象 这个已经不是用FIN完成的标准挥手过程了而是出现了异常。不过从报文特征来说还是比较直接的就是既要带RST标志位也要带ACK标志位所以答案就是**tcp.flags.ack==1 and tcp.flags.reset == 1**。
第二个问题是开放式的,比如**@江山如画**同学分享了一个挺有意思的案例是双方时钟不同步导致了TLS握手失败。时钟问题也是一个挺有“存在感”的问题时不时会在各种故障中扮演一点角色我们可以在排查没什么方向的时候也可以查一下时钟。
另外说到TLS握手的问题我在[第19讲](https://time.geekbang.org/column/article/491674)里介绍了两个典型案例,也是刚过去不久的一讲,我想你应该还有印象。
## 05讲的答疑
### 思考题
这节课里,我介绍了使用裸序列号作为定位两侧同个报文的手段。那么要定位两侧的同个报文,除了这个方法,还有哪些方法呢?你可以从网络七层模型出发,给出自己的思考。
### 答案
定位两侧同个报文,是一个比较关键的技术点,因为抓包分析的一个重要任务,就是对比两侧抓包文件中的报文,从而判定丢包、乱序、重传等行为的严重程度和原因,由此才能针对性地做进一步排查乃至找到解决方案。如果不能找到“同个报文”,那刚才说的一系列工作就无从谈起了。
![](https://static001.geekbang.org/resource/image/d7/e8/d75d0d7acd0654648d39f00d8b027ee8.jpg?wh=2000x894)
> 补充A和B对报文的编号一般是不同的因为各自抓取报文的起始时间、抓取条件等都可能不同就会导致抓取到的报文互有交集。
比如上面就是一个典型的TCP问题的场景也就是报文顺序发生了变化这个现象就是“乱序”发出1、2、3收到的却是2、1、3。那么显然我们需要在两侧的抓包文件里找到对应的同个报文。事实上现在说的1、2、3是A的抓包文件里的报文编号而B那边有自己的视角和报文编号跟A是完全不同的。
我在[第5讲](https://time.geekbang.org/column/article/481042)里介绍的方法是用了裸序列号原始序列号因为TCP报文的这个序列号在传输中是不会变化的所以这就是一个很好的确定报文的方法。其实这属于**第一大类方法按照TCP报文的元信息也就是TCP头部的特征去找到对应的报文**。
既然确定了这个大类的方法原则,你很容易举一反三,想到其他的方法。比如:
* 用TCP确认号比如下面这个过滤器
```bash
tcp.ack_raw == 754313633
```
* 如果有TCP timestamp扩展就用TCP timestamp比如下面两种过滤器
```bash
tcp.options.timestamp.tsval == 2947948748
tcp.options.timestamp.tsecr == 3209788920
```
* 如果有TCP SACK扩展就用SACK号比如用过滤器
```bash
tcp.options.sack_le == 1234
```
而**第二个大类的方法是利用TCP的载荷本身的特征去找到对应的报文**。比如HTTP是文本协议那么我们用下面这个过滤器就可以找到含有这个字符串的报文
```bash
tcp contains "id=abcdafeafeagfeagfaraera1242dfea"
```
其实换成以下这几种方式,也是一样的效果:
```bash
frame contains "id=abcdafeafeagfeagfaraera1242dfea"
ip contains "id=abcdafeafeagfeagfaraera1242dfea"
http contains "id=abcdafeafeagfeagfaraera1242dfea"
```
这个示意图,也表示了这样的两大类寻找对应报文的方法:
![](https://static001.geekbang.org/resource/image/50/25/502a6786f47a81306b62c60f883f8525.jpg?wh=2000x1125)
不过你看了这张图会不会以为我们每个报文都要这么找对应关系呢那当然不用了每个TCP流只要找一个关键报文然后根据这个报文去做Follow -> TCP Stream就好了。
## 小结
以上就是针对课程前5讲思考题的参考答案和拓展解读希望能给你一些启发。另外也非常感谢你对课后思考题的仔细思考和认真解答。在看留言的过程中我从大家的答复中也看到了更加全面或是更加深入的思考我自己受益匪浅。
接下来,我还会针对剩余的课后思考题,以及你的提问来作出解答。有任何问题,还是跟以前一样,欢迎你在留言区跟我交流,我们一同成长。