一次看似简单的 curl http://10.10.2.2:8080,至少包含四类定位问题:目标主机是谁、下一跳设备在哪里、数据应该交给哪个进程,以及返回流量如何走回来。
初学网络时常见的误区,是把 IP 地址、MAC 地址和端口理解成三种层级不同但作用相同的“地址”。它们实际上解决的是不同范围的问题:
- IP 地址用于跨网络确定源主机和目标主机,并参与路由选择。
- MAC 地址用于当前二层链路上的帧交付。经过路由器后,帧头通常会被重新构造。
- 端口号用于把到达主机的 TCP 或 UDP 数据交给具体套接字。
- 路由表决定数据包从哪个接口发出,以及应交给哪个下一跳。
只背定义很难形成排障能力。下面使用 Linux Network Namespace 在一台机器内创建客户端、路由器和服务器,完整观察一次跨网段请求。实验不依赖容器平台,但需要 Linux、root 权限,以及 ip、ping、curl、python3 和 tcpdump 等工具。
地址分工:从应用请求到链路帧
假设客户端地址为 10.10.1.2/24,服务器地址为 10.10.2.2/24,中间路由器分别使用 10.10.1.1/24 和 10.10.2.1/24。
客户端准备访问 10.10.2.2:8080 时,内核先用目标 IP 查询路由表。由于目标不在客户端直连的 10.10.1.0/24 网段内,匹配结果是默认路由 via 10.10.1.1。这意味着 IP 包的目标地址仍是 10.10.2.2,但当前以太网帧必须交给网关 10.10.1.1。
如果邻居表中没有网关对应的 MAC 地址,客户端会通过 ARP 询问“谁拥有 10.10.1.1”。得到响应后,它可以构造第一段链路上的帧:
以太网目标 MAC = 路由器客户端侧接口的 MAC
IP 目标地址 = 10.10.2.2
TCP 目标端口 = 8080
路由器收到帧后移除二层头,根据目标 IP 再查自己的路由表。它发现 10.10.2.0/24 是直连网络,于是在服务器侧重新构造以太网帧。此时源、目标 MAC 都会变化,但在没有 NAT 的条件下,IP 源地址和目标地址保持不变。服务器内核最终根据 TCP 目标端口 8080 找到监听套接字。
这也解释了一个重要现象:MAC 地址通常只在当前二层广播域内有意义,而 IP 地址负责端到端寻址,端口负责主机内部的进程分发。
搭建三节点实验网络
以下命令会创建三个独立网络命名空间。请在临时测试环境中执行,并先确认名称 client、router、server 未被其他实验占用。
sudo ip netns add client
sudo ip netns add router
sudo ip netns add server
sudo ip link add c0 type veth peer name r0
sudo ip link add r1 type veth peer name s0
sudo ip link set c0 netns client
sudo ip link set r0 netns router
sudo ip link set r1 netns router
sudo ip link set s0 netns server
sudo ip -n client addr add 10.10.1.2/24 dev c0
sudo ip -n router addr add 10.10.1.1/24 dev r0
sudo ip -n router addr add 10.10.2.1/24 dev r1
sudo ip -n server addr add 10.10.2.2/24 dev s0
sudo ip -n client link set lo up
sudo ip -n router link set lo up
sudo ip -n server link set lo up
sudo ip -n client link set c0 up
sudo ip -n router link set r0 up
sudo ip -n router link set r1 up
sudo ip -n server link set s0 up
为客户端和服务器配置返回路径:
sudo ip -n client route add default via 10.10.1.1
sudo ip -n server route add default via 10.10.2.1
路由器拥有两个直连网段,但 Linux 默认未必转发不同接口之间的 IPv4 数据包,因此还要在路由器命名空间中启用转发:
sudo ip netns exec router sysctl -w net.ipv4.ip_forward=1
查看各节点的地址和路由,确认配置是否符合预期:
sudo ip -n client -br addr
sudo ip -n router -br addr
sudo ip -n server -br addr
sudo ip -n client route
sudo ip -n router route
sudo ip -n server route
启动服务并验证连通性
在服务器节点启动一个仅用于实验的 HTTP 服务:
sudo ip netns exec server \
python3 -m http.server 8080 --bind 10.10.2.2 \
>/tmp/netlab-http.log 2>&1 &
SERVER_PID=$!
先测试三层连通性,再访问应用端口:
sudo ip netns exec client ping -c 2 10.10.2.2
sudo ip netns exec client curl -v --max-time 5 http://10.10.2.2:8080/
ping 成功只能说明 ICMP 往返路径基本可用,不能证明 TCP 8080 端口存在监听。curl 成功则进一步验证了路由、TCP 握手和应用服务。排障时应区分这两个层次。
在服务器节点检查监听套接字:
sudo ip netns exec server ss -lntp
预期应能找到绑定到 10.10.2.2:8080 的监听项。实际进程信息是否完整显示,取决于执行命令时的权限。
用抓包验证每一层的变化
打开两个终端,分别监听路由器两侧接口:
sudo ip netns exec router \
tcpdump -eni r0 'arp or icmp or tcp port 8080'
sudo ip netns exec router \
tcpdump -eni r1 'arp or icmp or tcp port 8080'
清空客户端邻居缓存后重新请求,可以更容易看到 ARP:
sudo ip -n client neigh flush dev c0
sudo ip netns exec client curl --max-time 5 http://10.10.2.2:8080/
对比两个接口上的输出时,应关注以下字段,而不是机械比较整行文本:
r0上出现客户端查询网关 MAC 的 ARP 请求。- TCP 包在两侧的目标 IP 都是
10.10.2.2,源 IP 都是10.10.1.2。 r0与r1上的以太网源、目标 MAC 不同,因为路由器重新封装了二层帧。- 客户端源端口通常由内核临时分配,目标端口固定为
8080。不要假设某次实验中的临时端口在下一次仍相同。 - TCP 三次握手依次表现为 SYN、SYN-ACK 和 ACK,之后才传输 HTTP 数据。
还可以直接检查 ARP/邻居学习结果:
sudo ip -n client neigh show
sudo ip -n router neigh show
sudo ip -n server neigh show
邻居项可能经历 INCOMPLETE、REACHABLE、STALE 等状态。具体状态会随访问时机和内核邻居子系统的状态机变化,因此不应把某个瞬间的状态当作固定结果。
内核实际如何选择路径
与其只查看完整路由表,更直接的方法是询问内核如何路由某个目标:
sudo ip -n client route get 10.10.2.2
sudo ip -n router route get 10.10.2.2
sudo ip -n server route get 10.10.1.2
客户端的结果应指向网关 10.10.1.1 和接口 c0;路由器访问服务器时应走直连接口 r1;服务器返回客户端时应通过网关 10.10.2.1。这里最容易遗漏的是返回路由:请求能够到达服务器,并不代表响应一定知道如何返回客户端。
本实验没有配置 NAT,所以抓包能直接看到原始源 IP。实际家庭网络或云网络中可能存在 SNAT、DNAT、负载均衡或代理,它们会改变观察结果。判断地址是否应当保持不变前,必须先确认路径中是否存在这些组件。
常见问题与排查顺序
客户端提示 Network is unreachable
通常意味着没有匹配的路由。先执行:
sudo ip -n client route
sudo ip -n client route get 10.10.2.2
检查默认路由是否存在、网关是否属于接口的直连网段,以及接口是否处于 UP 状态。
能到网关,但不能到服务器
依次检查路由器的两个接口、IPv4 转发开关和服务器返回路由:
sudo ip -n router -br link
sudo ip netns exec router sysctl net.ipv4.ip_forward
sudo ip -n server route get 10.10.1.2
如果主机或命名空间中额外配置了 nftables/iptables 规则,还需要确认转发链没有丢弃流量。本实验本身不主动修改防火墙规则。
ping 成功,但 curl 连接被拒绝
“连接被拒绝”通常表示目标主机可达,但目标地址和端口没有服务监听,或者防火墙明确拒绝了连接。使用 ss -lntp 检查服务是否启动、端口是否正确,以及服务是否只绑定到了 127.0.0.1。
curl 一直超时
超时和拒绝不同:超时通常表示报文或响应被静默丢弃。应同时在客户端、路由器两侧和服务器抓包,确认 SYN 到达了哪一段,以及 SYN-ACK 是否成功返回。只在请求发起端抓包,往往无法定位丢包位置。
抓不到 ARP
邻居缓存可能已经存在。可以查看 ip neigh,或在确认不会影响其他任务后刷新实验接口的邻居项。ARP 只用于同一二层链路上的 IPv4 邻居解析;客户端不会广播查询远端服务器的 MAC,而是查询下一跳网关的 MAC。
清理实验环境
结束后停止服务并删除命名空间。删除命名空间也会清理由其持有的虚拟接口:
sudo kill "$SERVER_PID" 2>/dev/null || true
sudo ip netns del client
sudo ip netns del router
sudo ip netns del server
rm -f /tmp/netlab-http.log
总结
定位一台主机上的服务不是单一地址完成的:路由表先根据目标 IP 选择出口和下一跳,ARP 把同链路的下一跳 IP 解析为 MAC,二层帧完成当前链路交付,路由器再为下一段链路重新封装,最终由 TCP 或 UDP 端口把数据交给目标套接字。
面对网络故障,可以按照“接口状态 → 路由选择 → 邻居解析 → 转发与防火墙 → 端口监听 → 应用协议”的顺序检查。这个顺序的价值在于把模糊的“网络不通”拆成可观测、可验证的具体环节,而抓包和 ip route get 正是连接理论与现场证据的关键工具。