Skip to content

第12章 网络调试

叙事密度:中高 | 主语言:Python | Bash/Shell 差异窗 | Vol 4·计算机网络


元数据卡

属性内容
卷号Vol 4 — 计算机网络
章节第12章:网络调试
前置第1章(分层模型)、第3章(TCP 深度剖析)、第4章(HTTP)、第5章(HTTPS)
后置第13章(IPv6 基础)、第14章(网络实战与性能优化)
理论深度(2/5)
Python 相关度≈60%;scapy 构造包、curl 耗时分析脚本
核心模型分层排障模型、调试工具链
代码量~120 行

你的进度

"学了这么多传送咒和驿道概念——但如果它们不工作呢?传送阵打不开、法术应用很慢、连接被拒绝——驿道调试是每个通信法师的必修课。这一章不讲新传送咒,只教你在驿道世界的施法工具里找出问题在哪里。"

前面十章的课程里,你学完了驿道分层模型的每一层——从物理层的魔力脉冲到应用层的 HTTP 和 QUIC。你知道三次法力握手、拥塞控制、TLS 加密封印、法术域名解析是怎么工作的。

但有个问题一直悬着:当这些东西不工作的时候,你该怎么办?

  • "传送阵打不开"——是你写错了法术坐标,还是域名解析法阵失败,还是信标塔挂了?
  • "应用很慢"——是驿道带宽不够,还是连接建立太慢,还是 TLS 封印握手卡住了?
  • "连接被拒绝"——法术服务没启动,还是护城法阵挡住了?

驿道调试不是玄学。它是一门系统性的分层排障方法。前面学的每一层知识,都会在这一章变成实际武器。


本章分层

  • 必读:分层排障思路、ping/mtr、dig、curl、ss、tcpdump 基本用法
  • 选读:Wireshark 的 Follow TCP Stream、HTTP 请求分析
  • 进阶:scapy 构造自定义包、NAT 导致的问题深入分析

本章是实战章:没有复杂的理论推导,但每个工具都需要你亲手测试才能内化。建议跟着每段命令在你的机器上跑一次。

你的任务

学完本章,你应当:

  1. 用"分层排障法"系统地定位网络故障——从物理层到应用层逐层排查
  2. 熟练使用 ping、dig、curl、tcpdump 在实战中排障
  3. 用 tcpdump + Wireshark 抓包分析一个真实的 HTTP 请求
  4. 区分"连接被拒绝"和"超时"的根本原因
  5. 用 openssl s_client 调试 TLS 握手失败
  6. 写一个 Python 脚本自动化网络性能分析

破局 · 溯源

第1战:分层排障法——"网页打不开"不是问题,只是现象

问题: 你打开法师观测镜,输入 https://example.com/path/to/page,然后观测镜一直转圈,最后报错 "无法连接驿道"。

你的第一反应可能是"我是不是写错法术坐标了"——但如果是域名解析法阵失败、驿道不通、信标塔挂了,法师观测镜都会显示相似的错误信息。

系统性的排障方法:从底层到应用层逐层排查

你问的问题用什么工具
1⃣ 物理/链路层网线插了吗?WiFi 连上了吗?ip linkifconfignmcli
2⃣ 网络层能通到对方吗?路由对吗?pingtraceroutemtr
3⃣ 传输层端口开着吗?连接状态正常吗?ssnmapnc
4⃣ DNS 层域名能解析吗?IP 正确吗?dignslookuphost
5⃣ 应用层HTTP 返回了什么?TLS 握手正常吗?curl -vopenssl s_clienttcpdump

黄金法则: 从底往上查。如果底层不通,上层怎么查也是白费。

bash
# 快速排障流水线(逐层检查)
# 第1层:物理层
ip link show | grep "state UP" || echo " 网络接口没起来"

# 第2层:网络层
ping -c 3 8.8.8.8 || echo " 连不上互联网"

# 第3层:传输层
ss -tlnp | grep :443 || echo " 本地443端口没监听"

# 第4层:DNS
dig +short example.com || echo " DNS 解析失败"

# 第5层:应用层
curl -v --connect-timeout 3 https://example.com/ || echo " HTTP 请求失败"

第2战:网络层排障——ping、traceroute、mtr

ping——最简单的连通性测试

bash
# 基本用法:ping IP 或域名
ping -c 5 8.8.8.8

预期输出:

PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=118 time=12.3 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=118 time=11.8 ms
64 bytes from 8.8.8.8: icmp_seq=3 ttl=118 time=12.1 ms
64 bytes from 8.8.8.8: icmp_seq=4 ttl=118 time=12.5 ms
64 bytes from 8.8.8.8: icmp_seq=5 ttl=118 time=12.0 ms

--- 8.8.8.8 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 4005ms
rtt min/avg/max/mdev = 11.80/12.14/12.50/0.24 ms

ping 告诉你的:

  • 丢包率:0% = 路径正常。>0% = 路径可能拥堵或有问题
  • RTT:平均 12ms 是正常。如果 RTT 突然到 200ms+ → 路由有问题或链路拥堵
  • TTL:118 不太小,往回算大概跳数。如果 TTL=1,说明包都快死了

ping 不告诉你的事情:

  • 服务器端口开着没(ping 用 ICMP,不是 TCP/UDP)
  • 防火墙可能允许 ICMP 但阻挡 TCP(某些网络只让 ping 过)
bash
# 用 ping 检测网络波动(持续 ping)
ping 8.8.8.8

# 用 ping 检测丢包模式:是持续丢包还是间歇性?
# 持续丢包 → 物理问题(网线、WiFi信号)
# 间歇性丢包 → 网络拥堵

常见错误解读:

# "ping 通" ≠ "服务可用"
ping google.com    ← ICMP echo 通了
curl https://google.com/    ← 可能 443 端口被防火墙挡了
# "ping 不通" ≠ "网络断了"
ping some-server.internal    ← 可能是该服务器禁用了 ICMP

traceroute——看你的包走了哪条路

bash
traceroute -n 8.8.8.8
# -n: 不解析主机名,只显示 IP(更快)

预期输出:

traceroute to 8.8.8.8 (8.8.8.8), 30 hops max, 60 byte packets
 1  192.168.1.1  1.2 ms  1.0 ms  1.1 ms        ← 你家路由器
 2  10.0.0.1    5.3 ms  5.1 ms  5.0 ms          ← ISP 网关
 3  172.16.1.2  8.7 ms  12.3 ms  8.5 ms         ← ISP 骨干路由器
 4  72.14.238.0  11.2 ms  11.0 ms  11.5 ms       ← Google 入网点
 5  * * *                                       ← 某些路由器不回复 traceroute(正常)
 6  8.8.8.8     12.3 ms  11.9 ms  12.1 ms        ← 终于到了

traceroute 的工作原理: 发送 TTL=1 的包 → 第一跳路由器看到 TTL 过期 → 返回 ICMP Time Exceeded → 记录它的 IP。然后 TTL=2 → 第二跳回复 → 记录……直到目标。

什么时候看 traceroute:

  1. 延迟很高——看哪一跳突然暴增。如果跳3的 RTT 是 8ms,跳4是 200ms→那第4跳是瓶颈
  2. 路由路径异常——包不应该走这条路线(比如从亚洲跑到美洲再回来)
  3. 丢包位置——* * * 出现的位置往往能告诉你丢在哪

mtr——traceroute 的持续版(最好用的组合工具)

bash
# mtr = ping + traceroute,持续跟踪每一跳的延迟和丢包
mtr -n 8.8.8.8
                               My traceroute  [v0.95]
          home (0.0.0.0)                              2024-01-15T10:30:00+0800
Keys: Help   Display mode   Restart statistics   Order of fields   quit
                                      Packets               Pings
 Host                               Loss%   Snt   Last   Avg  Best  Wrst StDev
 1. 192.168.1.1                     0.0%    50    1.2   1.1   0.8   2.3   0.3
 2. 10.0.0.1                        0.0%    50    5.3   5.1   4.8   8.2   0.5
 3. 172.16.1.2                     0.0%    50    8.2   8.5   7.9  12.1   0.8
 4. 72.14.238.0                   25.0%    50   11.2  12.5  10.5  45.2   5.3  ← 25% 丢包!
 5. ???                            100%    50    0.0   0.0   0.0   0.0   0.0
 6. 8.8.8.8                        0.0%    50   12.1  12.3  11.5  14.2   0.4

mtr 强大的原因: 它持续发送包(不像 traceroute 只发 3 个),所以能告诉你哪一跳在丢包。上面的例子中,跳4 25% 丢包——即使最终跳(目标 8.8.8.8)0% 丢包。这意味着跳4到最终跳之间可能有负载均衡,一部分路径有问题。


第3战:DNS 层排障——dig

问题: 法师观测镜输入 https://example.com → 等了半天 → "找不到信标塔"。

你的域名解析法阵可能出了问题。用 dig 来诊断:

bash
# 基础查询
dig example.com

# 只拿 IP 地址
dig +short example.com
# → 93.184.216.34

预期完整输出:

; <<>> DiG 9.18.24 <<>> example.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 12345
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; QUESTION SECTION:
;example.com.           IN  A

;; ANSWER SECTION:
example.com.    38400   IN  A   93.184.216.34

;; Query time: 45 msec
;; SERVER: 192.168.1.1#53(192.168.1.1)
;; WHEN: Mon Jan 15 10:30:00 CST 2024
;; MSG SIZE  rcvd: 56

dig 告诉你的关键信息:

字段含义排障用途
status: NOERROR查询成功如果 NXDOMAIN → 域名不存在或拼写错误
ANSWER: 1找到了 1 个记录如果 ANSWER: 0 → DNS 不知道这个域名
38400TTL (缓存时间)如果 TTL=0 → 可能是临时策略或调试环境
Query time: 45 msec解析耗时如果 500ms+ → DNS 服务器慢
SERVER: 192.168.1.1#53谁回的你家的路由器在解析(DNS 代理)

高级诊断:

bash
# 查某个记录类型
dig mx gmail.com                # 邮件服务器记录
dig ns google.com               # 权威名称服务器
dig txt google.com              # SPF、DKIM 等文本记录
dig aaaa google.com             # IPv6 地址

# 指定 DNS 服务器查询(绕过本地缓存)
dig @8.8.8.8 example.com        # 用 Google DNS
dig @1.1.1.1 example.com        # 用 Cloudflare DNS

# 追踪 DNS 解析链路
dig +trace example.com

"dig +trace" 的威力:

; <<>> DiG 9.18.24 <<>> +trace example.com
;; Received 525 bytes from 192.168.1.1#53(192.168.1.1) in 1 ms

.                       8573    IN      NS      a.root-servers.net.     ← 根服务器
.                       8573    IN      NS      b.root-servers.net.
;; Received 262 bytes from 198.41.0.4#53(a.root-servers.net) in 4 ms

com.                    172800  IN      NS      a.gtld-servers.net.     ← .com 顶级域
com.                    172800  IN      NS      b.gtld-servers.net.
;; Received 878 bytes from 192.5.6.30#53(a.gtld-servers.net) in 8 ms

example.com.            172800  IN      NS      a.iana-servers.net.     ← 权威服务器
example.com.            172800  IN      NS      b.iana-servers.net.
;; Received 248 bytes from 192.42.93.30#53(a.gtld-servers.net) in 20 ms

example.com.            86400   IN      A       93.184.216.34           ← 最终结果

DNS 排障三步:

  1. 域名存在吗?dig +short example.com → 如果没结果,查拼写或 dig +trace
  2. 本地 DNS 有问题? → 对比 dig @8.8.8.8dig @local-dns。如果 Google DNS 能解但本地不能 → 本地 DNS 问题
  3. DNS 延迟高?dig @8.8.8.8Query time 如果持续 >100ms → 可能网络问题

第4战:传输层排障——ss、netstat、nc

问题: ping 8.8.8.8 通了,但 curl https://example.com/ 显示 "连接被拒绝"。

驿道层没问题(ping OK),但 TCP 传送咒连接失败了。这说明目标法术端口可能没开放,或者护城法阵挡住了。

bash
# 看本地开了哪些端口
ss -tlnp

# -t: TCP
# -l: 仅监听中的端口
# -n: 数字显示(不解析服务名)
# -p: 显示进程信息

预期输出:

State   Recv-Q  Send-Q  Local Address:Port   Peer Address:Port  Process
LISTEN  0       128     0.0.0.0:22           0.0.0.0:*          users:(("sshd",pid=1234,fd=3))
LISTEN  0       128     127.0.0.1:5432       0.0.0.0:*          users:(("postgres",pid=5678,fd=5))

ss 的关键用法:

bash
# 所有连接(包括非监听)
ss -tna

# 看指定端口
ss -tlnp sport = :80
ss -tlnp dport = :443

# 实时监控
watch -n 1 'ss -s'   # 连接统计概览

netstat——ss 的前任(较新的 Linux 推荐用 ss):

bash
netstat -tlnp   # 等效于 ss -tlnp
netstat -s      # 网络统计

nc(netcat)——手动建连接:

bash
# 测试端口是否开放(简单版)
nc -zv example.com 443
# → Connection to example.com 443 port [tcp/https] succeeded!

nc -zv example.com 8080
# → nc: connect to example.com port 8080 (tcp) failed: Connection refused

"Connection refused" vs "Timeout"——核心区别:

Connection refused (ECONNREFUSED):
客户端 ─── SYN →── 服务器(端口没监听)
                   ← RST(TCP RST 包)
客户端收到 RST → "对方拒绝了"

超时 (Timeout):
客户端 ─── SYN →── 防火墙  ──→ 服务器(端口正常)
                   ← 防火墙丢包(什么也不回)
客户端等了好几秒 → "没有回复"
错误含义常见原因
Connection refused服务器明确拒绝了端口没打开、服务没启动、防火墙发 RST
Connection timed out包发了但没回应防火墙静默丢弃、路由不通、服务器完全没响应
No route to host路由器告诉我"找不到路"IP 不可达、路由表缺失

第5战:应用层排障——curl -v

问题: 传送层通了(法术端口开放),但传送阵页面还是加载不了——可能是 HTTP 层面或 TLS 层面的问题。

curl -v 是最好的应用层调试工具。

bash
# 详细模式:看整个 HTTP 请求/响应的每一个步骤
curl -v https://example.com/

预期输出:

*   Trying 93.184.216.34:443...
* Connected to example.com (93.184.216.34) port 443 (#0)
* ALPN: offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* ALPN: server accepted http/1.1
* Server certificate:
*  subject: CN=example.com
*  start date: Jun 10 00:00:00 2024 GMT
*  expire date: Sep 8 23:59:59 2024 GMT
*  subjectAltName: host "example.com" matched cert's "example.com"
*  issuer: C=US; O=Let's Encrypt; CN=R3
*  SSL certificate verify ok.
> GET / HTTP/1.1
> Host: example.com
> User-Agent: curl/7.88.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Age: 549561
< Cache-Control: max-age=604800
< Content-Type: text/html; charset=UTF-8
< Date: Mon, 15 Jan 2024 02:30:00 GMT
< Etag: "3147526947"
< Server: ECS (dcb/7EC6)
< Content-Length: 1256
<

<!doctype html>
<html>
...

每一行意味着什么:

*   Trying 93.184.216.34:443...    ← DNS 解析成功,开始 TCP 连接
* Connected to example.com ...     ← TCP 三次握手完成
* TLSv1.3 ... Client hello         ← TLS 握手开始
* TLSv1.3 ... Certificate          ← 服务器发证书
* SSL certificate verify ok        ← 证书验证通过(重点!失败会显示错误)
> GET / HTTP/1.1                   ← 你的 HTTP 请求
< HTTP/1.1 200 OK                  ← 服务器的 HTTP 响应

curl 的诊断参数:

bash
# 只看响应头(不下载页面内容)
curl -I https://example.com/

# 只显示耗时统计
curl -w "TCP 连接: %{time_connect}s\nTLS 握手: %{time_appconnect}s\n首字节: %{time_starttransfer}s\n总耗时: %{time_total}s\n" -so /dev/null https://example.com/

输出:

TCP 连接: 0.012s
TLS 握手: 0.035s
首字节: 0.048s
总耗时: 0.048s

常见 curl 排障场景:

bash
# 场景1:HTTP 重定向(检查是否被重定向了)
curl -vL https://example.com/    # -L 自动跟随重定向

# 场景2:自定义 Header 看后端怎么处理
curl -v -H "X-Forwarded-For: 10.0.0.5" https://example.com/

# 场景3:代理调试
curl -v -x http://proxy.example.com:8080 https://example.com/

# 场景4:忽略证书错误(只用于测试)
curl -k https://self-signed.badssl.com/

第6战:TLS 调试——openssl s_client

问题: curl 报错 SSL certificate problem: self signed certificatecertificate has expired——你怀疑是 TLS 法力握手出了问题。

openssl s_client 是 TLS 调试的瑞士军刀:

bash
# 基本用法:建立 TLS 连接并输出完整信息
openssl s_client -connect example.com:443

预期输出(关键部分):

CONNECTED(00000003)
depth=2 C=US, O=Internet Security Research Group, CN=ISRG Root X1
verify return:1
depth=1 C=US, O=Let's Encrypt, CN=R3
verify return:1
depth=0 CN=example.com
verify return:1
---
Certificate chain
 0 s:CN=example.com
   i:C=US, O=Let's Encrypt, CN=R3
   a:PKEY: rsaEncryption, 2048 (bit);  sigalg: RSA-SHA256
 1 s:C=US, O=Let's Encrypt, CN=R3
   i:C=US, O=Internet Security Research Group, CN=ISRG Root X1
 2 s:C=US, O=Internet Security Research Group, CN=ISRG Root X1
   i:C=US, O=Internet Security Research Group, CN=ISRG Root X1
---
SSL handshake has read 4466 bytes and written 379 bytes
---
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Server public key is 2048 bit
---

常见调试场景:

bash
# 检查证书过期
echo | openssl s_client -connect example.com:443 2>/dev/null | openssl x509 -noout -dates
# → notBefore=Jun 10 00:00:00 2024 GMT
# → notAfter=Sep 8 23:59:59 2024 GMT

# 检查证书链是否完整
openssl s_client -connect example.com:443 -showcerts

# 检查是否支持指定的 TLS 版本
openssl s_client -tls1_2 -connect example.com:443   # 只尝试 TLS 1.2
openssl s_client -tls1_3 -connect example.com:443   # 只尝试 TLS 1.3

# 测试 SNI(服务器名称指示)
openssl s_client -connect 1.2.3.4:443 -servername example.com

"证书错误"排障流水线:

  1. 检查证书是否过期:openssl x509 -noout -dates
  2. 检查证书域名是否匹配:curl 时看 subjectAltName 是否包含请求的域名
  3. 检查证书链是否完整:openssl s_client -showcerts — 有些服务器没发中间证书
  4. 检查是否自签名证书:verify error:num=18:self signed certificate
  5. 检查系统 CA 包:openssl version -d — 看 CA 证书目录

第7战:抓包——tcpdump + Wireshark

问题: 以上工具能告诉你"出了什么问题",但不能告诉你"到底发生了什么"——你想要的是协议层面的证据。哪一步失败?谁发的什么法术包?

tcpdump——命令行抓包神器

bash
# 抓所有流量(慎用——可能会产生海量数据)
sudo tcpdump -i any

# 基本过滤:只看特定端口
sudo tcpdump -i any port 80

# 只看特定主机的 HTTP 流量
sudo tcpdump -i any host example.com and port 80

# 只抓几个包(限制了数量,适合快速调试)
sudo tcpdump -i any port 443 -c 10

# 保存到文件(后续用 Wireshark 分析)
sudo tcpdump -i any port 443 -w capture.pcap

tcpdump 过滤表达式(必记基础):

bash
# 协议过滤
sudo tcpdump icmp                    # 只看 ping
sudo tcpdump tcp                     # 只看 TCP
sudo tcpdump udp                     # 只看 UDP

# 主机过滤
sudo tcpdump host 8.8.8.8           # 和 8.8.8.8 通信的包
sudo tcpdump src host 8.8.8.8       # 从 8.8.8.8 发出的源包
sudo tcpdump dst host 192.168.1.5   # 发给 192.168.1.5 的目的包

# 端口过滤
sudo tcpdump port 443                # 443 端口
sudo tcpdump src port 53             # 源端口 53(DNS 响应)
sudo tcpdump dst port 22             # 目的端口 22(SSH 进来的)

# 组合过滤
sudo tcpdump "host 8.8.8.8 and (port 53 or port 443)"
sudo tcpdump "tcp[tcpflags] & (tcp-syn|tcp-ack) != 0"  # SYN-ACK 包

用 tcpdump 看三次握手:

bash
# 监控 TCP 连接建立(三次握手)
sudo tcpdump -i any "host example.com and tcp[tcpflags] & (tcp-syn) != 0" -c 3

输出:

12:00:01.123456 IP 192.168.1.5.54321 > 93.184.216.34.443: Flags [S], seq 1000     ← SYN
12:00:01.135678 IP 93.184.216.34.443 > 192.168.1.5.54321: Flags [S.], seq 2000, ack 1001  ← SYN-ACK
12:00:01.135690 IP 192.168.1.5.54321 > 93.184.216.34.443: Flags [.], ack 2001     ← ACK

用 tcpdump 看 HTTP 请求/响应的交互:

bash
sudo tcpdump -i any port 80 -A   # -A: ASCII 输出,能看见 HTTP 内容
12:00:02.123456 IP 192.168.1.5.54322 > 93.184.216.34.80: Flags [P.], seq 1:77
E..q..@.@..X....x.."..5..K.....Z.......
GET / HTTP/1.1
Host: example.com
...

Wireshark——图形化协议分析

tcpdump 抓的 pcap 文件用 Wireshark 打开,你会发现一个完全不同级别的调试体验。

Wireshark 的核心功能:

  1. 捕获过滤器(Capture Filter)——抓的时候过滤,语法和 tcpdump 一样
  2. 显示过滤器(Display Filter)——抓完后的数据分析过滤:
# 基本过滤
http           # 只看 HTTP 包
tcp.port == 443  # 只看 443 端口的包
ip.addr == 1.2.3.4  # 只看和这个 IP 通信的包

# 高级过滤
tcp.flags.syn == 1 and tcp.flags.ack == 0  # 只看 SYN 包(握手第一步)
http.request.method == "POST"               # 只看 POST 请求
tls.handshake.type == 11                    # TLS Certificate 消息
http.time > 0.5                             # 响应时间超过 0.5 秒的 HTTP 请求
  1. Follow TCP Stream——右键 → Follow → TCP Stream,你会看到一个完整的 HTTP 请求/响应对话,就像读一封信的往来:
← [SYN] 我来了
→ [SYN, ACK] 收到!我也来了
← [ACK] 好!连接建立
← [PSH, ACK] GET /index.html HTTP/1.1
→ [ACK] 收到请求
→ [PSH, ACK] HTTP/1.1 200 OK ...
← [ACK] 收到内容
→ [FIN, ACK] 结束

抓包黄金场景:

场景抓包方案
网页很慢抓 HTTP/HTTPS 流量,看每个请求的响应时间分布
某个接口间歇性失败抓 100 个请求,找失败的包模式
登录失败抓应用层包,看是否登录请求被重定向了
证书错误抓 TLS 握手包,看 Certificate 消息的具体内容
DNS 解析问题tcpdump port 53,看 DNS 请求/响应
连接突然中断抓 TCP 包,看 RST 或 FIN 是谁发的

第8战:端口扫描——nmap

问题: 你想知道"这座信标塔上哪些法术端口是开放的"——或者你的信标塔上是不是无意中开了不该开的端口。

bash
# 基本扫描(TCP SYN 扫描,默认不需要完成三次握手)
nmap -sS example.com

# 扫描指定端口范围
nmap -sS -p 1-1000 example.com

# 快速扫描(常用端口)
nmap -F example.com
Starting Nmap 7.80 ( https://nmap.org ) at 2024-01-15 10:30 CST
Nmap scan report for example.com (93.184.216.34)
Host is up (0.041s latency).
Not shown: 996 filtered ports
PORT    STATE    SERVICE
22/tcp  open     ssh
80/tcp  open     http
443/tcp open     https
8080/tcp closed  http-proxy

Nmap done: 1 IP address (1 host up) scanned in 5.23 seconds

端口状态及其含义:

状态含义推测
open端口开放,有程序在监听正常服务
closed收到 RST端口没开任何服务,也没有防火墙阻挡
filtered没收到任何响应(或收到 ICMP 不可达)防火墙在过滤这个端口
`openfiltered`无法区分 open 和 filtered

进阶:Python scapy 构造自定义包

以下内容属于自定义网络包分析,主线不要求掌握。如果你需要深入网络协议分析或编写网络测试工具,再读这里。

前面 tcpdump 只能看现成的包。如果你想要构造自己的包来测试网络行为,Python 的 scapy 库是最强大的工具。

python
# python_scapy_demo.py
# 安装: pip install scapy
# 运行前注意: scapy 需要 root 权限构造原始包
# 警告:不要对未经授权的服务器运行扫描!

from scapy.all import *

# 例1:手动构造 TCP SYN 包并等待回复
def syn_scan(host: str, port: int):
    """
    发送一个 TCP SYN 包到指定端口
    如果收到 SYN-ACK → 端口开放
    如果收到 RST-ACK → 端口关闭
    """
    ip = IP(dst=host)
    syn = TCP(sport=RandShort(), dport=port, flags="S")

    # 发送包并等待回复(超时 2 秒)
    reply = sr1(ip / syn, timeout=2, verbose=False)

    if reply is None:
        return f"Port {port}: filtered (no response)"
    elif reply.haslayer(TCP):
        if reply[TCP].flags == 0x12:  # SYN-ACK
            # 礼貌地关闭握手(否则对方会等)
            rst = IP(dst=host) / TCP(sport=reply[TCP].dport,
                                      dport=port, flags="R")
            send(rst, verbose=False)
            return f"Port {port}: open"
        elif reply[TCP].flags == 0x14:  # RST-ACK
            return f"Port {port}: closed"

    return f"Port {port}: unknown"


# 例2:构造完整的 HTTP GET 请求包
def http_get_raw(host: str, port: int = 80):
    """
    手动构造 TCP 三次握手 + HTTP GET 请求的完整交互
    等同于: curl http://host/
    """
    # 创建套接字(需要 root 权限)
    conf.L3socket = L3RawSocket

    # 1. SYN
    ip = IP(dst=host)
    syn = TCP(sport=RandShort(), dport=port, flags="S", seq=1000)
    syn_ack = sr1(ip / syn, timeout=2, verbose=False)

    if syn_ack is None:
        print("No response to SYN")
        return

    # 2. ACK (完成三次握手)
    my_seq = syn_ack[TCP].ack
    my_ack = syn_ack[TCP].seq + 1
    ack = TCP(sport=syn[TCP].sport, dport=port, flags="A",
              seq=my_seq, ack=my_ack)
    send(ip / ack, verbose=False)

    # 3. HTTP GET 请求
    http_request = (
        "GET / HTTP/1.1\r\n"
        f"Host: {host}\r\n"
        "Connection: close\r\n"
        "\r\n"
    )
    get_pkt = TCP(sport=syn[TCP].sport, dport=port, flags="PA",
                  seq=my_seq, ack=my_ack) / http_request
    response = sr1(ip / get_pkt, timeout=2, verbose=False)

    if response:
        # 4. 发送最后的 ACK
        final_ack = TCP(sport=syn[TCP].sport, dport=port, flags="A",
                        seq=response[TCP].ack, ack=response[TCP].seq + 1)
        send(ip / final_ack, verbose=False)
        print(f"Got {len(response[TCP].payload)} bytes of response:")
        print(response[TCP].payload.load.decode("utf-8", errors="replace")[:500])


# 注意:实际扫描时记得:
# 1. 只扫自己的机器或授权的服务器
# 2. 设置合适的超时避免长时间等待
# 3. 构造 RST 包关闭握手(礼貌扫描)

if __name__ == "__main__":
    # 测试本地端口(不需要 root 权限也可以测 localhost)
    result = syn_scan("127.0.0.1", 22)
    print(result)

如果需要更实用的场景,scapy 也支持构造 DNS 查询包:

python
# 构造 DNS 查询看解析全过程
from scapy.all import *

def dns_query(domain: str, dns_server: str = "8.8.8.8"):
    """手动构造和发送 DNS 查询,查看响应"""
    dns_req = IP(dst=dns_server) / UDP(dport=53) / DNS(rd=1, qd=DNSQR(qname=domain))
    reply = sr1(dns_req, timeout=2, verbose=False)

    if reply and reply.haslayer(DNS):
        dns = reply[DNS]
        for i in range(dns.ancount):
            rr = dns.an[i]
            print(f"{rr.rrname.decode()}{rr.rdata}")


dns_query("google.com")
# → google.com. → 142.250.80.14

实战场景排障大全

场景1:"Connection refused" vs "Timeout"

现象: curl https://example.com/
      curl: (7) Failed to connect to example.com port 443: Connection refused

排障步骤:
1⃣ 能 ping 通吗? → ping example.com  → 能通 → 网络层没问题
2⃣ 端口开放?    → nc -zv example.com 443 → Connection refused
3⃣ 服务在跑?    → ssh 到服务器 `ss -tlnp | grep 443` → 没有监听443
                    → 服务挂了!→ `systemctl restart nginx`

→ 如果是"Connection timed out" vs "Connection refused":
   超时 = 没任何回复 → 防火墙或路由问题
   拒绝 = 有RST回复 → 端口没开或者防火墙发RST

抓包确认:sudo tcpdump -i any port 443
  超时 → 你发了SYN,什么都没回来
  拒绝 → 你发了SYN,回了RST

场景2:DNS 解析失败

现象: curl https://example.com/
      curl: (6) Could not resolve host: example.com

排障步骤:
1⃣ 基本DNS查询  → dig +short example.com → 啥也没有
2⃣ 指定DNS服务器 → dig @8.8.8.8 example.com → 能解析!
                    → 问题在本地DNS配置或路由器
3⃣ 检查本地DNS  → cat /etc/resolv.conf
                    → nameserver 192.168.1.1  ← 你家路由器在DNS代理
4⃣ 检查路由器DNS → 登录路由器管理页面 → DNS 配置错误!
                    → 改为 8.8.8.8 / 1.1.1.1

场景3:SSL/TLS 握手失败

现象: curl https://self-signed.badssl.com/
      curl: (60) SSL certificate problem: self signed certificate

排障步骤:
1⃣ 完整握手信息  → openssl s_client -connect self-signed.badssl.com:443
                    → deep 0 CN=self-signed.badssl.com
                    → verify error:num=18:self signed certificate
                    → 确实是自签名证书
2⃣ 证书详情      → echo | openssl s_client -connect ... | openssl x509 -text
                    → 没找到受信任 CA 的签发者
3⃣ 证书过期?    → openssl x509 -noout -dates → 还在有效期内

→ 如果是生产环境:安装正确证书
→ 如果是测试环境:curl -k 或配置 CA 信任链

场景4:性能瓶颈定位

现象: 应用很慢——哪个环节最耗时?

排障步骤:
1⃣ curl 耗时分析:
curl -w "@curl-format.txt" -o /dev/null -s https://example.com/

创建一个 curl-format.txt 文件:

    time_namelookup:  %{time_namelookup}s\n
       time_connect:  %{time_connect}s\n
    time_appconnect:  %{time_appconnect}s\n
   time_starttransfer:  %{time_starttransfer}s\n
                     ----------\n
      time_total:  %{time_total}s\n

用 Python 自动化分析:

python
#!/usr/bin/env python3
"""curl_time_analyzer.py — 分析 curl 请求的耗时分布"""

import subprocess
import json
import sys
from statistics import mean, stdev


def curl_with_timing(url: str, count: int = 5) -> dict:
    """
    多次请求同一个 URL,收集耗时指标
    返回: 各阶段的平均耗时
    """
    format_vars = [
        "time_namelookup", "time_connect", "time_appconnect",
        "time_starttransfer", "time_total"
    ]

    # 一次性 curl 输出 JSON
    format_str = "{" + ",".join(
        f'"{v}":"%{{{v}}}"' for v in format_vars
    ) + "}"

    times = {v: [] for v in format_vars}

    for i in range(count):
        result = subprocess.run(
            ["curl", "-w", format_str, "-o", "/dev/null", "-s",
             "--connect-timeout", "5", url],
            capture_output=True, text=True, timeout=10
        )
        data = json.loads(result.stdout)
        for v in format_vars:
            times[v].append(float(data[v]))

    print(f"\n=== 耗时分析: {url} ({count} 次请求) ===\n")
    print(f"{'阶段':<25} {'平均':>8} {'最小':>8} {'最大':>8} {'标准差':>8}")
    print("-" * 60)

    for v in format_vars:
        avg = mean(times[v])
        mn = min(times[v])
        mx = max(times[v])
        sd = stdev(times[v]) if len(times[v]) > 1 else 0
        label = {
            "time_namelookup": "DNS 解析",
            "time_connect": "TCP 连接",
            "time_appconnect": "TLS 握手",
            "time_starttransfer": "首字节到达",
            "time_total": "总耗时",
        }[v]
        print(f"{label:<25} {avg:>8.3f}s {mn:>8.3f}s {mx:>8.3f}s {sd:>8.3f}s")

    # 诊断
    avg_dns = mean(times["time_namelookup"])
    avg_tcp = mean(times["time_connect"]) - avg_dns
    avg_tls = mean(times["time_appconnect"]) - mean(times["time_connect"])
    avg_server = mean(times["time_starttransfer"]) - mean(times["time_appconnect"])
    avg_transfer = mean(times["time_total"]) - mean(times["time_starttransfer"])

    print(f"\n{'-'*40}")
    print(f"瓶颈诊断:")
    if avg_dns > 0.2:
        print(f"  DNS 解析慢 ({avg_dns:.3f}s): 可能 DNS 服务器需要优化")
    if avg_tcp > 0.5:
        print(f"  TCP 连接慢 ({avg_tcp:.3f}s): 可能网络延迟大")
    if avg_tls > 0.5:
        print(f"  TLS 握手慢 ({avg_tls:.3f}s): 可能证书链长或服务器压力大")
    if avg_server > 1.0:
        print(f"  服务器处理慢 ({avg_server:.3f}s): 应用层可能是瓶颈")
    if avg_transfer > 5.0:
        print(f"  数据传输慢 ({avg_transfer:.3f}s): 带宽可能是瓶颈")

    return times


if __name__ == "__main__":
    url = sys.argv[1] if len(sys.argv) > 1 else "https://www.google.com/"
    curl_with_timing(url)
bash
python3 curl_time_analyzer.py https://www.google.com/

预期输出:

=== 耗时分析: https://www.google.com/ (5 次请求) ===

阶段                      平均    最小    最大    标准差
DNS 解析                0.003s  0.002s  0.004s  0.001s
TCP 连接                0.012s  0.011s  0.014s  0.001s
TLS 握手                0.028s  0.026s  0.031s  0.002s
首字节到达              0.038s  0.036s  0.042s  0.002s
总耗时                  0.148s  0.142s  0.156s  0.005s

----------------------------------------
瓶颈诊断:
 各阶段都正常

根据耗时分布定位瓶颈:

耗时分布模式可能的根因
DNS 解析 >200msDNS 服务器配置问题或缓存未命中
TCP 连接 >500ms网络延迟大——远距离通信或拥塞
TLS 握手 >1s服务器 CPU 压力大、证书链太长
首字节到达 >2s服务器端处理慢——应用代码、数据库查询等
数据传输 >5s带宽不足或大文件

常见陷阱

陷阱1:tcpdump 只抓了一半的包

场景: 你怀疑某个 HTTP 请求没有到达服务器。你用 tcpdump 抓包,分析之后发现——只看到了 SYN 没看到 SYN-ACK——"服务器没回应!"

可能原因: tcpdump 没有抓到双向流量。

bash
# 错误:
sudo tcpdump -i eth0 port 80   # 只抓 eth0 接口

# 正确:
sudo tcpdump -i any port 80    # 抓所有接口

# 更稳妥的方法(双向验证):
# 在客户端抓:确保你已经打开了"混杂模式"(promiscuous mode)
# 在服务器端也抓一份:对比两边看到的包

诊断方法: 如果 -i any 还是只看到单向流量,就在目标服务器上也抓一份。两边对比。

陷阱2:防火墙规则误判

场景: 你用 nmap -sS 扫描服务器,发现 80 端口是 filtered 状态——"被防火墙挡住了!"

但管理员说"我没设防火墙啊"。

真相: 你的 nmap 扫描包可能被ISP 或云提供商的边界防火墙过滤了。这不是目标服务器的防火墙。

bash
# 用不同的方法验证:
# 1. 如果你有服务器的 SSH 权限
ssh user@server 'nc -zv localhost 80'   # 本地看 80 是否监听

# 2. 如果你没有服务器权限
curl -v http://server:80/               # 如果 server 发回了数据,说明端口正常
# curl 失败不代表端口关闭——可能是:
# - HTTP 响应被拦截
# - TLS 证书不匹配
# - 应用层重定向

# 3. 检查 cloud vendor 安全组规则
aws ec2 describe-security-groups ...
gcloud compute firewall-rules list ...

陷阱3:NAT 导致的问题

场景: 你部署了一个 Web 服务器在家庭网络里,外网通过公网 IP 访问不了。你用 curl localhost:8080 能正常访问,但外网用户说无法连接。

问题: 你的驿道信标塔在做传送门地址转换(NAT)。信标塔有一个公网驿道坐标(如 219.123.45.67),内部驿站网络是 192.168.1.x。外部法术请求映射到你的服务器时:

外部用户                               你家的路由器                       你的服务器
  │                                      │                                │
  │── 连接 219.123.45.67:8080 ──────────→│                                │
  │                                      │── DNAT: 219.123.45.67:8080 ──→│
  │                                      │         → 192.168.1.5:8080     │
  │                                      │                                │
  │                                      │← 回复源为 192.168.1.5:8080 ───│
  │                                      │     源IP 是私有IP!           │
  │                                      │    SNAT 没配好 → 外网用户收不到 │

诊断方法:

bash
# 在服务器上抓包看源 IP
sudo tcpdump -i any port 8080

# 外部请求进来时,你看到源 IP 是:
# - 外部用户的真实 IP → NAT 配置正确
# - 192.168.1.1(路由器内网地址)→ SNAT 把用户IP丢了!

# 另一个症状:服务器上看到所有连接都来自路由器内网
ss -tn | grep 8080
# 应该看到外部 IP 而不是 192.168.1.1

通关挑战

挑战1:跟踪一次完整的 HTTP/HTTPS 请求()

简单但最好的练习

bash
# Step 1: 用 dig 查目标 IP
dig +short example.com

# Step 2: 用 traceroute 看路由
traceroute -n example.com

# Step 3: 用 tcpdump 抓包
sudo tcpdump -i any host example.com -w http-trace.pcap -c 50

# Step 4: 在另一个终端执行 HTTP 请求
curl http://example.com/

# Step 5: 用 tcpdump 读包
tcpdump -r http-trace.pcap -v

# Step 6: 用 Wireshark 打开 pcap 文件分析
# → Follow TCP Stream → 你看到了完整的 HTTP 请求/响应

完成后回答:这次请求经过了哪些步骤?哪一步最耗时?

挑战2:"连接被拒绝" vs "超时"——自己造两种场景()

bash
# 用 Python 启动一个具体测试服务器
python3 -m http.server 9999 &

# 场景A:连接被拒绝 —— 关闭服务器后请求
kill %1
curl -v http://localhost:9999/  # → Connection refused

# 场景B:超时 —— 防火墙拦截(本地 iptables 模拟)
sudo iptables -A INPUT -p tcp --dport 9999 -j DROP
curl -v --connect-timeout 5 http://localhost:9999/  # → Timeout

# 用 tcpdump 看看两个场景的包有什么区别
sudo tcpdump -i lo port 9999

# 清理
sudo iptables -D INPUT -p tcp --dport 9999 -j DROP

挑战3:用 curl 分析你的博客/网站耗时()

bash
# 创建 curl-format.txt 文件(如上面的内容)
cat > /tmp/curl-format.txt << 'EOF'
    time_namelookup:  %{time_namelookup}s\n
       time_connect:  %{time_connect}s\n
    time_appconnect:  %{time_appconnect}s\n
   time_starttransfer:  %{time_starttransfer}s\n
                     ----------\n
      time_total:  %{time_total}s\n
EOF

curl -w "@curl-format.txt" -o /dev/null -s "https://你的网站.com/"

瓶颈诊断: 如果 time_connect(TCP 连接)占了大头——检查网络延迟。如果 time_starttransfer(从 TLS 到首字节)占了大头——服务器代码慢或后端延迟高。

挑战4:Python 脚本——端口扫描器()

用 Python 的 socket 模块(不是 scapy)写一个简单的 TCP 端口扫描器:

python
import socket
import sys

def scan_port(host: str, port: int, timeout: float = 1.0) -> str:
    """用 socket 连接测试端口是否开放"""
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.settimeout(timeout)
    result = sock.connect_ex((host, port))
    sock.close()
    if result == 0:
        return "open"
    elif result == 111:  # ECONNREFUSED
        return "closed"
    elif result == 110:  # ETIMEDOUT
        return "filtered"
    else:
        return f"unknown({result})"

def scan_ports(host: str, ports: list):
    """扫描多个端口"""
    for port in ports:
        status = scan_port(host, port)
        print(f"Port {port}: {status}")

# 测试:扫描常见的 HTTP/HTTPS 端口
scan_ports("example.com", [22, 80, 443, 8080, 3306])

验收标准

完成本章后,你应当可以:

  • [ ] 用分层排障法系统性定位"网页打不开"的根因
  • [ ] 用 ping + traceroute + mtr 排查网络层问题(丢包、延迟异常)
  • [ ] 用 dig 排查 DNS 解析失败(DNS 不通、域名不存在、本地 vs 公共 DNS)
  • [ ] 用 curl -v 诊断 HTTP/TLS 问题(连接被拒绝、证书错误、重定向)
  • [ ] 用 ss 检查端口和服务状态
  • [ ] 区分"连接被拒绝"(ECONNREFUSED)和"超时"的根本差异
  • [ ] 用 tcpdump 抓包并导出为 pcap 文件供 Wireshark 分析
  • [ ] 用 openssl s_client 调试 TLS 证书问题
  • [ ] 知道什么时候该怀疑 NAT、什么时候该怀疑防火墙

常见卡点

卡点原因解药
tcpdump 抓包需要 root 权限tcpdump 创建原始套接字需要 CAP_NET_RAWsudo 或给二进制加 cap:sudo setcap cap_net_raw+ep /usr/sbin/tcpdump
自己的服务从外网访问不了,但 localhost 可以很可能在监听 127.0.0.1 而不是 0.0.0.0ss -tlnpLocal Address 列——如果是 127.0.0.1:8080,改为 0.0.0.0:8080 或 systemctl edit 修改配置
ping 能通但 curl 连不上ICMP 协议和 TCP 协议是两套ping 用 ICMP,curl 用 TCP——防火墙可能允许 ICMP 但阻止 TCP 端口
dig 查到的 IP 和浏览器访问的不同CDN/负载均衡器根据 DNS 解析位置返回不同 IPdig @8.8.8.8dig @1.1.1.1 的结果可能不同——GeoDNS
tcpdump 只抓到单向流量只抓了入站接口,没抓出站接口-i any 抓所有接口,或确保接口的双向流量都被捕获
openssl s_client 连不上但 curl 可以curl 可能在用 HTTP/2 或 HTTP/3,而 openssl 只用 HTTP/1openssl s_client -alpn h2 或直接用 curl --http1.1 排除协议影响

现在不需要理解

  • eBPF 网络调试:Linux 内核的动态调试工具,可以绕过 tcpdump 直接在内核过滤。强大但需要内核编程知识——属于高级系统调试,不是日常网络排障。
  • iperf3 的详细参数iperf3 是带宽测试工具,本章只提了名字。参数很多(并行流、UDP 模式、反向测试),深入需要单独一页。
  • Wireshark 的 1000+ 显示过滤器:记住基础的 httptcp.portip.addrtls.handshake 就够了,其他按需学习。Wireshark 有完整的参考文档。
  • SDN 网络排障:软件定义网络(OpenFlow、OVS)的调试需要不同的工具集(ovs-appctl、ovs-ofctl),属于数据中心运维场景。
  • strace + lsof:这两个虽然不是网络专用工具,但在调试"进程为什么不接受连接"场合非常有用——strace -p <pid> -e network 跟踪系统调用,lsof -i :8080 看哪个进程在用端口。

旅人笔记

驿道调试可能是驿道法术里最实用的技能——它把之前 11 章学到的所有抽象概念(分层、TCP、域名解析法阵、TLS、HTTP)变成了实际法具。

核心心法:"传送阵打不开/应用很慢"是现象,不是问题。排障是逐层挖掘真相的过程:

  1. 物理/魔力链路层 → ip link / ping 通不通?
  2. 驿道层 → ping / traceroute / mtr——能到吗?哪一跳慢/丢包?
  3. 传送咒层 → ss / nc / nmap——法术端口开了吗?
  4. 域名解析法阵 → dig——法术域名解析了吗?
  5. 应用层 → curl -v / openssl s_client——HTTP/TLS 正常吗?

每一层的问题都必须在这一层解决,不能跨层猜测。物理层不通 → 修魔导缆线,不是改 curl 配置。

排障的黄金时间:问题发生时立刻抓魔力包。等 5 分钟再去,传送咒可能已经关闭/咒语缓存清空,关键证据已经没了。

一句话消化本章: 遇到驿道故障,从底往上逐层排查——物理层→驿道层→传送咒层→域名解析法阵→应用层,每个法具只回答一个简单的是/否问题,组合起来就是真相。


下一站预告

IPv6——当我们用完了 IPv4 地址

你排障时可能遇到过一些让人困惑的场景:IPv6 优先还是 IPv4 优先?双栈环境下 DNS 返回了两个 IP 地址哪个更优先?某些网络 IPv6 可以通但 IPv4 不行(或者反过来)。

下一章,我们不只讲 IPv6 地址的格式,而是回答一个更实际的问题:为什么你需要的不是更多的地址,而是一个全新的网络层协议?

Built with VitePress | Software Systems Atlas