容器 22:两个隔离进程怎样通过 veth 通信
从内核对象推导路径
让进程 A、B 分别进入两个 netns:每一侧起初可只看到自己的 loopback 设备;需先把 lo 置 up,建立 veth 对,把两个端点分别放入 netns,给接口分配互通地址与路由,确认 ARP/邻居条目及链路状态,再用同一 probe_app.py 的 /health 观测端到端访问。对同一网段的直连流量,veth 负责把一端送出的二层帧交给另一端;想连第三个网段需路由/网关,不是再加一个 curl 就能自动到达。IP 地址和监听地址各有作用:只监听 127.0.0.1 的服务不会因 veth 配通而自动变得可从另一 netns 访问。
本云端可以安全调用 unshare -Ur -n;原始记录显示新 netns inode net:[4026532574] 与原来 net:[4026531994] 不同,新视图只列出 lo,进程退出后原 namespace inode 不变。这是“视图可以分离”的 LAB_VERIFIED,不是“veth 已连通”。这里缺 ip 工具与专用网络实验 VM,尚未创建/抓包两个 netns、veth、地址和路由;完整通信验收 NOT_RUN。复跑入口与清理和统一源码包保留路径,不改共享宿主默认网络。先前网络 33的静态模型不能代替这次真实连通数据。
反例可在 VM 先只建 veth 而不配地址,观察链路对象存在但请求仍失败;再只让服务监听 127.0.0.1,对比同 netns 和异 netns 访问结果。失败阶段应分别保留接口状态、路由、监听 socket、抓包和 HTTP 返回,不能一口断定为 CNI 的问题。
两个进程各自看见什么
network namespace 不是两台独立机器,也不是 Docker 自带的一份虚拟网卡配置。Linux 的 network_namespaces(7) 明确列出它隔离的网络对象:设备、IP 协议栈、路由表、网络相关的 /proc 与 /sys 视图,以及端口和部分 socket 命名空间。进程 A 和 B 可以仍是同一宿主内核调度的普通进程,拥有不同 netns 后,A 所看到的 lo、路由和监听端口不必等于 B 所看到的;/proc/<pid>/ns/net 可记录各自 namespace inode,但相同或不同的 inode 数字不包含链路连通的证明。一个 namespace 可以被多个进程共享,持有打开的 namespace 文件描述符也会影响其生命周期;因此“进程退出”与“配置对象立即消失”不能视为同一件事,尤其在使用 ip netns add 保留命名 handle 时。
新建 netns 后通常先要确保本侧 lo 被设置为 up。A 绑定本侧 127.0.0.1:18080,只能由处在同一网络命名空间、访问这个回环地址的进程直接到达;B 即便也请求 127.0.0.1:18080,访问的也是 B 自己的回环,不是 A 的。只检查两边各有一个叫 lo 的设备,没有回答如何把帧送过 namespace 边界。若启动时 A 依旧使用探针默认 --host 127.0.0.1,之后才创建 veth,也不会自动修改它的 socket 绑定地址。排障应先查服务绑定的是回环、veth 的地址还是全部本地地址,再解释跨侧请求结果。
veth(4) 把一对虚拟设备描述为成对端点:从一侧发出的帧会在另一侧端点被接收,任一端 link down 时这对链路的状态也会变。它解决两个 namespace 间的二层搬运,不代替三层地址分配和路由,也不是一个有 NAT 功能的容器网桥。实验先在专用 VM 的两个自建 netns 中创建 veth 对,给两端按计划分别命名并移动,随后显示两端的接口索引、peer、MTU 与 up/down 状态。只创建 veth、却既没有给 A 地址也没给 B 地址时,A 的 HTTP 客户端无法把目的 IP 正确映射到这条链路,不能因为 ip link show 有两个条目就写“网络已通”。不要把裸设备上 TCP 失败直接归因于安全策略,先把本地地址、路由和邻居解析补全。
可以在私有实验空间用 192.0.2.1/30 与 192.0.2.2/30 做示意地址。这段文档示例地址只在独占 VM 的私有 netns 内使用,不能改动共享宿主的默认路由或把它宣称为公网可达地址。在两端各加一个地址后,内核会按照网络栈规则为本地直连前缀建立相应路由;是否存在、具体输出仍以 ip -n A route 与 ip -n B route 为准。若 A 与 B 的掩码不一致或接口保持 down,客户端即使知道对方 IP 也可能失败。记录这时 curl 的 errno、路由与邻居条目,比把所有失败统称“ping 不通”更有定位价值。
地址正确且路由指向本侧 veth 后,首次发送报文可能先发生邻居发现:在 IPv4 直连网段,A 需要把 B 的 IP 对应到 B 的链路层地址,观察 ip neigh 的变化和双方抓包可区分“没有路由”“ARP 没拿到响应”“TCP 已经发出但应用没监听”。ICMP echo 能帮助检查三层双向到达,不表示 TCP 端口已开放;TCP 建连成功又不保证 /health 返回 200。最好从 A/B 两侧各抓本次小量报文,保存镜像化时间戳、接口名称与过滤条件,查同一个请求的目标地址、端口和 HTTP 状态;如果 VM 没有 tcpdump,标记抓包缺口,不拼贴教材截图充当原始证据。
监听地址、回环和 veth 形成三个不同的失败点
正常案例用同一 Python 探针在 B 中以 --host 0.0.0.0 绑定 18080,并由 A 请求 192.0.2.2:18080/health。如果路由、地址、链路状态、应用进程都符合条件,A 的请求才有可能获得 HTTP 200;若 B 在日志里打印 listen,它表明监听步骤完成,不会替 A 验证网络。用 B 自己对 127.0.0.1:18080/health 发请求作为应用基线,能把“服务根本没起来”与“跨 namespace 的链路问题”分开;但 B 的回环能答复也不证明 A 的 TCP 能接通,必须保留来自 A 的实际响应。建议还记录两个进程 PID 与 /proc/<pid>/ns/net,否则命令看似执行于 A,实际上进程也许仍在宿主默认 namespace。
第一边界只保留 veth,撤去 A/B 的 IP:预期向 192.0.2.2 请求无法按目标路径完成,但具体报错可能因 VM 里还存在别的路由而不同。若意外请求成功,先查 A 的全部路由与命名空间 inode,确认目标不是沿另一网卡走了;不能为了符合期待重写失败输出。第二边界保留链路与地址,但把 B 的探针改成 --host 127.0.0.1:B 自己访问 loopback 仍可能成功,A 请求 B 的 veth 地址却不会由这个仅回环的 socket 处理。是否得到连接拒绝或超时还取决于网络过滤和实际监听状态;使用 B 的监听表及两端抓包才能分清。实验隔离在两个 netns 内,不尝试修改宿主默认防火墙来制造人工失败。
路由回答的是下一跳,不是服务是否可用
本篇的两端位于同一个 /30 前缀,检查 ip route get 192.0.2.2 可以看到从 A 的哪一接口、以哪个源地址去往目的;从 B 反向检查 A 地址,避免只测试单向 TCP。若把 B 改成不同网段,直连条件不再满足,需要明确的下一跳与能执行转发的设备/进程。单纯在 A 增加“去另一个网段”的 route、却没有任何节点能接收并转发,不会使服务凭空可达。23 才引入 bridge、NAT 与宿主端口映射,本篇不通过偷偷启用宿主 ip_forward 或添加全局防火墙规则来跨越这个先修边界。当前 VM 配一个直连 veth 的成功路径,恰恰是为了给后续复杂路径提供能逐段验证的基线。
一个错误的测试方法是 A 里对 B 地址 ping 一次,看到应答便宣布“B 的服务就绪”。ICMP 可以由 B 的内核回应,即使 Python 进程根本没启动;/health 又只是代码中的一个路径,即使返回 200,/state 写入目标目录也可能因权限报错。观察顺序应当是 B 的进程和 socket、A 发出的目标地址和源地址、两端链路上的 TCP 握手与 HTTP 实际返回;只要哪一步证据不齐,结论就保持在该步。例如 A 有 Network is unreachable,通常应先查 A 内路由,而不是从 B 的容器日志找答案;A 发 SYN 却无回应,接着查 B 接口和过滤;A 建连成功却 HTTP 404,则已越过最初网络路径,转查请求 URI。响应码也可能经代理或别的进程返回,保存目标端口、B 的进程身份和请求时间才能防止误认。
MTU 和链路开关可以在专用 VM 做边界推理,但不能在共享宿主实验。veth 对端 down 时本侧链路状态随之变化,原有 TCP 连接可能失败或被重试;把两端恢复 up 后需要重新确认地址和路由仍然存在。MTU 影响帧大小和路径可转发的数据量,并非短请求能证明大请求也会通过;25 会在双 VM 上再看封装路径的额外开销。若本篇只发很小的 /health,记录“本轮没有验证大报文”,而不从 veth link up 推断任何负载规模都可靠。即便看到接口的 MTU 数字,跨主机实际路径还包含额外设备,其最小可用 MTU 不能单靠本侧值给出。
命名 handle 与实际生命周期
ip netns add containers-22-a 给 namespace 创建了一个可通过名称引用的句柄,ip -n containers-22-a link 在那个网络视图查询;这与后台服务进程退出是两个不同事件。实验结束时需先停止自己启动的 B 探针以及可能还在 A/B 内跑的 tcpdump,再删除自建 netns;不能一边让进程使用其中的设备,一边看到 ip netns list 少了一个名字就宣布清理完成。持有 namespace 文件描述符的进程还可能继续引用旧对象。要核验无泄漏,可对照实验前后 ip netns list、实验接口名称、B 探针 PID 的存在性,以及实验中保存的 /proc/<pid>/ns/net;既有 namespace 名称或其他人创建的接口不能当清理对象。
物理网卡的归属和 veth 的生命周期也不要混写。man-pages 对 physical device 在 network namespace 最后引用消失后的归属有具体描述,而 veth 是可在实验里独立创建与删除的一对虚拟端点。不要把生产物理网卡搬进测试 netns 来验证 man page 的话;那会干扰其他任务。专用 VM 里新建 veth 时给端点加本篇独特前缀,移到 A/B 后仅对这两个端点和自己创建的 netns 做销毁操作。失败退出路径也要试清理并再查残留,尤其 ip netns add A 成功而 B 未创建时,不可假定脚本后半部会替已存在的 A 自动删除。把本轮对象及创建阶段记录下来,按实际存在性清理,比盲目跑“删除所有 netns”安全。
这也是为什么本机已有的实验记录只能得出很窄的结论:unshare -Ur -n 使一个可信普通进程在新的 netns 看到了不同 inode 与 lo,退出后原视图不变。它没有创建 veth、没有网络接口地址、没有抓包,更没有另一个服务进程,所以不是两个容器通信成功的前置实测。即便此环境将来装上 ip,没有宿主权限或专用 VM 也不应尝试把网络配置写进共享网络空间。代码可继续用标准库探针保持一致,但实际 IP、命名空间 inode、容器 ID/Pod UID 一律以未来 VM 现场结果填写,不从本篇示意命令复制。
正常与异常的实验应该怎样对照
在 VM 先列出已有命名空间和接口,用一次性前缀保证接下来创建的 A/B 不碰已有对象。A/B 建成后分别运行 ip -n A link 和 ip -n B link,确认同一 veth 对的端点各位于预期一侧,接口均已 up,再查看 addr 与 route。启动 B 服务时固定源码包 SHA、Python 版本和 --state-dir 路径;在 VM 上运行 ip netns exec B python3 ... --host 0.0.0.0 --port 18080,它改变网络命名空间,但并不自动把进程移入独立 PID/mount namespace。由 A 用 ip netns exec A curl ... 访问 B 侧的文档地址,抓包前后分别保存 A/B 的 ip neigh。这样得到的 PID 和路径属于 VM 上普通进程,不能写成 Docker 容器 ID。若后续用 Docker 来管理 netns,还需要容器 ID 到宿主 PID 的另一条关联链,网络连通本身无法提供制品 digest。
为了让“哪一步才通”可重复,不应把整个配置写入一个命令后只执行一次 HTTP。先在完全未配地址时请求一次,保存失败的原始 curl 状态和 A 侧 route get;再配地址但保留一个端点 down,检查链路与邻居状态;然后把两端 up,重新请求并保存 HTTP 状态。这里明确的是实验顺序,不是伪造的预计错误码:某些情况下 VM 已有别的默认路由,错误报文会与理想化的“网络不可达”不同。若出现这种偏差,应先比对 ip -n A route 是否有非预期出口,必要时重新创建干净的实验 namespace,绝不改写共享 VM 的默认路由。每一轮的变更仅作用于本篇新建的 netns 和 veth,清理后检查前后差异。
应用监听的反例应与地址/链路的反例分开执行。保持同样 IP、同样请求路径、同样 A/B namespace,停止 B 的正常探针进程,再以 --host 127.0.0.1 启动一只相同版本的探针。B 在本侧访问其 127.0.0.1:18080,如果能取到 /health,说明程序至少已绑定回环;随后 A 请求 B 的 veth IP 时若失败,结合 B 的监听地址及抓包解释。若此时 A 反而拿到 200,检查是不是前一只 B 进程仍占着 0.0.0.0:18080、A 命令是否实际上跑在 B 里,或者测试 URL 指错端口。不要因为预期是失败,就删除一次相反的实际输出。状态看起来矛盾,恰恰是要用 PID、socket 和 namespace inode 消除歧义的时刻。
抓包只对自建 veth 使用短时有界采样,如 timeout 5 tcpdump -ni veth22a -c 30,分别采开始和结束时间、过滤条件和退出码。抓包工具因为超时返回非零,并不等于通信实验本身失败,先看它实际收到多少帧;反之某次抓到 ARP 不说明 HTTP 一定成功。在 A 只建链路不配地址时,可能连 ARP 都不会按预期发出,因为本地路由条件尚未成立;要解释“没抓到帧”,先保存对应时刻的路由与接口状态。若在本机没有 tcpdump,就记录未执行原因与 VM 的安装前提,不能把 ip neigh 列表当作双端原始抓包。
安全边界也属于实验有效性。创建 netns/veth 需要足够权限,它可以在独占 VM 里只用新名字实施,不等于应该在云端共享宿主上用 sudo ip link 重现。内核 network namespace 仅负责分隔视图,地址冲突、错误路由、应用暴露和宿主资源消耗仍可能影响实验 VM 本身。压缩包的探针请求小于 4 KiB、curl 设置短超时,tcpdump 设置包数与时间上限,避免无界流量。实验结束先结束自建探针/抓包进程,删除自建 namespace 和 veth,核对没有同名设备;不要使用会清空所有网络命名空间或默认规则的清理命令。
这个最小拓扑的判断方法可以迁移到容器 bridge、CNI 和 Kubernetes Pod:先确定发包进程在哪个 netns,目标 IP 属于哪个 netns 的接口;再找路由决定的出接口和下一跳,检查链路两侧的实际帧,最后验证服务监听与 HTTP 结果。到 23 会在两端之间加入 bridge、NAT、端口发布和 DNS,使链条更长,但仍不能跳过前面的身份证据。veth 对已经连通而业务 503,通常应转向应用就绪;服务正常而 A 没有到 B 的路由,则不应先调整业务 handler。每次只引入一个新的对象或一条新的跳数,保留对应正常与失败的证据,才能在复杂系统里定位第一次失配。
“在 B 里 curl 到 A 成功,A 到 B 一定成功”也不是网络原理给出的保证。两端可以拥有不同的地址、路由和应用监听集合;即使直连网段相同,B 上并不一定在 A 请求的端口提供服务。做双向验证时必须分别写出源进程、目的地址、端口和请求方法,再看各自所在 namespace 的路由与监听。最小拓扑中的失败定位可以写成可证伪的问题:A 本地 route get 的出接口是不是 veth22a?B 是否能看到对应 SYN?B 对该地址端口有 socket 吗?响应有没有返回 A?每一个“是”都需要本轮原始输出,而不是从拓扑图上推断。若中途换掉 B 的进程,保留两个 PID 的启动和结束时间以避免前一轮成功污染后一轮失败。
还应记录命名空间接口之外的前提:ip netns exec 是否具有读取探针源码的文件系统视图,探针状态目录是否可写,Python 是否在 VM 上存在。netns 不会自动隔离 mount namespace 与 user namespace,因此两边的路径可能仍引用同一个宿主目录,不能把 /state 的共享视为“veth 把数据传过去”。仅比较 HTTP /health 的网络结果即可;若要比较文件读写,另建两个独立的实验状态目录,明确写入的是文件系统而非网络对象。PID 与网络对象关联时也不要把 ip netns list 中的名字当作 inode:名字是引用入口,运行进程及其打开的句柄才决定生命周期。实验 VM 中设备已清理但名字仍在或名字不在而进程仍持有,都是需要继续追踪的残留情况。
最后检查工具退出码的语义也要分层:curl --max-time 2 非零可能是 DNS(若错误地使用主机名)、路由、建连、超时或 HTTP 返回策略;curl -i 即使拿到 503,也可能因为传输成功而返回零退出码。实验用数字 IP 跳过 DNS,把 curl 退出码和 HTTP 状态码分开记录;如要制造解析失败,则放到 23 的 DNS 实验,不能塞进本篇冒充 veth 问题。ip netns exec 对目标命令的返回码还应原样保存,避免 shell 管道中 tee 的成功掩盖 curl 失败。这样所谓“请求成功”有明确的判据:来自 A 的同一个输入,B 的探针记录可对应,HTTP 报文到达且返回预期状态。
分阶段失败实验还须排除旧连接:在 B 从 0.0.0.0 改绑 127.0.0.1 后,不能复用旧的持久连接来宣称 A 仍能访问 B 的 veth 地址。每轮单独建立 TCP 连接,记录 A 发出的源/目的地址与端口、B 的监听地址、服务 PID 及本次请求时间;若连接被拒、路由不可达或 HTTP 返回 503,各自指向不同检查点。两侧抓包只证明所采样接口上出现过帧;想断言帧属于这次请求,至少要对上四元组和时间窗,并从 B 的应用日志核对响应。这是从 22 的直连路径走向 23 的 bridge/端口发布时仍需要保留的关联字段。
跨 VM 复现时需要记录内核版本、ip/tcpdump/Python 版本、ip netns list 前后快照、完整创建与清理命令,避免把某个发行版默认的防火墙或网络管理器行为写成内核统一机制。若权限不足,不重试危险的宿主级操作;保留本篇已有 inode 记录的有限结论,转而把具体 ip netns add 拒绝与专用 VM 补跑入口写入实验单。这一层的失败与 Docker/containerd 尚无直接关系,不能因为命令里出现“容器”两个字就把权限不足归责给容器运行时。
专用 VM 的抓包若涉及真实业务流量,应停止实验并换干净环境;只保存本篇自建探针报文。
| 现象 | 对象 | 限定条件 |
|---|---|---|
| netns inode 不同 | /proc/<pid>/ns/net |
不证明已有 veth |
| ping 通但服务失败 | socket 监听与端口 | 传输层以上另核查 |
| 无地址时链路存在 | veth 端点及路由 | veth 不分配 IP |
练习
- 先预测只建立 veth 对、不配地址时两个进程能否互相 HTTP 请求;专用 VM 按步骤逐项补齐并抓包定位第一次成功的条件。
- 预测 A 监听
127.0.0.1与0.0.0.0时 B 的访问差异;在两个自建 netns 用相同探针记录监听地址、请求目标与日志。
上一篇:21:停止与日志;下一篇:23:bridge、端口、DNS。
系列总目录:从进程隔离到运行时与编排。



