容器 23:bridge、端口发布和 DNS 各在哪一跳
两条访问路径要分开画
对同一 bridge 子网中两个端点,发送方按照目标 IP/路由挑下一跳,经 veth 到宿主 bridge,再依据二层目的 MAC 转发。对宿主公开端口,请求的原始目标是宿主 IP:端口;具体实现可通过 NAT/代理把流量送入容器 IP:端口,需要核对当时的规则和 conntrack 元组,不能把容器 IP:端口的直连记录冒充端口发布记录。DNS 把名字变成地址,解析成功后仍可能因服务只在 loopback 监听、规则丢包或目标端口未打开而失败。
专用 VM 正例应给同一个 probe-app 分别测试“容器 IP 直连”“宿主发布端口”“名字解析后访问”,记录请求前后源/目的元组、路由、conntrack 与 DNS 结果。三个反例依次让探针只监听 127.0.0.1、让测试域名解析失败、在仅作用于自建实验 namespace 的过滤规则中拒绝请求;分别检查监听、DNS 服务器返回和抓包。不要改共享宿主防火墙、默认路由或全局解析配置。
bridge 只负责二层可达的一段
22 让两个进程通过一对 veth 对接:帧从一端出来会在另一端进入。要接多个 endpoint,典型 Linux bridge 把若干宿主侧 veth 加入同一桥接域,然后依据 MAC 地址进行转发;它理解的是二层帧与端口,不是在把 localhost:18080 自动翻译成应用容器 IP。Linux kernel bridge 文档提供该桥接对象的概念和端口设置入口,端口在哪个 bridge、是否学习到目标 MAC、每条链路是否 up,要靠本次 VM 上 bridge link、bridge fdb 和接口状态验证;不能把 Docker 文档里的示意拓扑画作本机实际数据。创建 bridge 也不授予 HTTP 探针一个独立 netns,容器的 netns 与宿主桥端点仍需由运行时/网络驱动设置。
同一个用户自建 bridge 网络上的 A 和 B 可以拿到不同容器 IP;A 要访问 B 的容器 IP,先在 A netns 内作路由判断。如果目的属于直连网段,A 从自己的 veth 送帧给 bridge,bridge 再转发给 B 的 veth,B 必须有能够接收该 IP:port 的监听 socket。A 访问 B 容器 IP 的路径原则上不需要先经过宿主 -p 指定的端口;-p 描述另一条从宿主入口接进来的路径。若 B 只监听本 netns 的 127.0.0.1:18080,A 仍不能通过 B 的网卡 IP 抵达应用,即便 ARP 和 FDB 已完全正常。区别在监听层,而非 bridge 里需再补一个“神秘规则”。
一个常见混淆是把容器 IP 当成公网服务地址。自建 bridge 网络的地址空间如何分配、从宿主是否可直达、外部机器是否有路由与访问规则,都受所选网络驱动与主机配置影响;同一个 VM 上两个 endpoint 能互访,只说明这次路径有证据。不应从它推导别的主机也能连同一个容器 IP。专用实验 VM 的负例先用容器内/同网客户端对 B 的 IP 访问,再从 VM loopback 对发布端口访问;两条命令必须区分目标 IP、发起进程所在 namespace 和网络工具所在主机。宿主端口失败时若同网 IP 请求成功,下一步重点是发布端口和转发路径,而不是重建镜像。
发布端口改变请求入口,不改变进程的监听事实
在 docker run -p 127.0.0.1:18080:18080 这个输入里,左侧是宿主侧绑定地址和端口,右侧是容器目标端口;并不等于 Docker 帮应用执行一次 bind(2)。应用的 --host 决定 Python 服务绑定容器内哪个地址。实验专用 VM 从宿主用 curl http://127.0.0.1:18080/health 发请求,原始目标是宿主回环;驱动可能通过 NAT/转发规则或 userland proxy 等路径交给容器 IP:18080。Moby v28.4.0 的 libnetwork/drivers/bridge/port_mapping_linux.go 处理端口映射时有 docker-proxy 及 NAT/forwarding 配置分支,因此绝不能给出所有部署唯一的源/目的地址改写顺序。要说本次实际用哪一种机制,先从对应 daemon 的网络配置与版本、规则集和抓包查证。
对同一次 TCP 流,客户端看到的远端、宿主入口处的五元组、发往容器 veth 时的五元组和容器内应用 accept 时所见远端,可能各不相同。看到容器日志里一个源地址不能在脑中直接推断宿主 NAT 发生了几次;如果中间有代理,新连接甚至会重新选择源端口。应在 VM 的发布端口入口和容器 veth 两处分别做有界抓包,用 conntrack -L(只过滤本实验端口/五元组,专用 VM)协助关联;NAT 表只表述规则,不证明这次请求命中了哪条规则。尤其当宿主到自身回环地址发包时,路径与另一台机器从外部网卡进入不同,不可把本地 loopback 测试描述成“外网已经可达”。
当宿主发布端口能访问,但 A 直接访问 B 的容器 IP 不通时,也不能断言“端口发布自动修复了桥”。先分别检查两个客户端的源 netns、目的 IP、bridge 连接、监听范围与宿主规则;如果它们根本不是同一个请求源,两个结论可同时成立。反过来同网 IP 直连成功而宿主发布失败时,docker inspect 的 HostConfig.PortBindings、宿主 ss、端口占用与规则配置应先核对,避免把 host 端口写错的客户端错误归到 B 的 /health。应用层的 HTTP 404、503 还需要区分路径不存在和就绪条件不满足,不该作为 NAT 错误的唯一证据。
DNS 解决的是名字,不是网络端到端
Docker 为用户定义网络提供服务名解析能力时,程序提出查询、获得地址、再用这个地址进行路由与连接,这是至少两个独立操作。在 Moby v28.4.0 的 libnetwork/resolver.go 中,Resolver 被定义为嵌入式 DNS 服务,监听容器的 loopback 并向网络后端查询或转发外部查询;这是本篇源码阅读版本的具体路径,不应照搬到其他 Docker 或 CNI 插件。每次实验要同时保存客户端内 /etc/resolv.conf、查询的具体名字和查询响应,以及解析到的地址属于哪个 endpoint;curl http://containers-probe-23:18080/health 的成败还取决于其后 TCP 和 HTTP。若用宿主的系统 DNS 查询容器名,查询失败未必说明容器内嵌入式 DNS 故障,首先查使用的是哪个 resolver。
负例可以用一个明确不存在的合成名称请求,不修改 VM 全局 /etc/resolv.conf;保留解析工具的原始输出与返回码,再用 B 的数字容器 IP 成功访问作为对照。如果解析确实失败,但数字 IP 也失败,不能只把业务不可用归咎于 DNS:解析问题与网络问题可能同时存在。另一个相反的负例是 A 已拿到正确地址,B 只在回环监听或端口未开放,DNS 答复成功而 curl 仍失败;这说明名字阶段已过,重点转向连接及监听。服务名在不同 Docker 网络、不同 DNS 范围中的解析结果可能不同,保存实际网络名和客户端容器 ID,避免把来自另一命名空间的解析结果拼进本次请求链。
为什么一张 NAT 表不能代替一条实际连接
NAT 是包经过网络栈时按规则进行地址/端口变换;连接跟踪(conntrack)用于关联同一条流的往返和状态。只看到存在一条目的转换规则,不知道本次客户端是否经过该 hook,也不知道哪个网络命名空间持有对应规则。对于 127.0.0.1 的宿主 loopback 发布,在某些配置下还可能有 userland proxy 路径,不能无条件套用“先 PREROUTING 后 DNAT”这一个示意。实验应先从 docker inspect 保存本轮 HostIP/HostPort/ContainerPort,再结合当前 Moby 版本的 bridge driver 配置、宿主规则、conntrack 记录和双点包追踪;若只有一次 curl 输出,没有这些原始对象,最多能判定从这一台 VM 某地址访问此端口成功/失败,不能宣称已经解释了其 NAT 实现。
在专用 VM 上采 conntrack 时有两个量要严格区分:表中已有条目的存在性和本次请求对应条目的证据。若之前跑过同名实验,旧 TCP 条目可能暂存;要用这次的源/目的地址、端口、时间窗口以及请求 ID 来缩小集合。对照不同源 netns 的两个客户端时,不要把“客户端 A 请求 B 的容器 IP”和“VM 宿主请求发布端口”合并成一条 conntrack 记录。一个请求可能经过用户态转发重新建立连接,五元组改变并不直接说明安全机制绕过;应把两个连接分别记录。若工具不在 VM 上,省略 conntrack 结论,不能用 NAT 配置的静态规则伪造命中过的连接状态。
过滤规则的失败注入要求更严格的作用域。计划中的“过滤拒绝”不允许向共享宿主默认 iptables/nftables 链插一条通用 DROP,也不允许暂停主机其他容器。如果独占 VM 能够进入本次自建客户端容器的 network namespace,并在该 ns 的独立 nft 表里仅针对 B 的目标 IP:port 放置有界规则,则可以先保存该 ns 规则基线,再注入、请求、删除同一规则并验证恢复;测试者还要确认 VM 上的 nft 实现与命名空间确实支持这组命令。没有独立 netns 的过滤工具或管理员同意时,这一项继续 NOT_RUN,正常路径和解析/监听反例仍可完成。哪怕应用请求超时,也要用规则计数、抓包或日志确认是过滤造成,不能仅凭状态码猜测发生了拒绝。
过滤与监听错误表现可能相近,但可用一条链消除歧义:名字是否获得地址;路由选择哪个接口;SYN 是否从客户端发出并到达 B;B 在被请求地址/端口是否存在监听;是否有返回的拒绝或重传;最终 HTTP 层是否出现响应。DNS 失败甚至没有确定的目标 IP,先抓容器内 DNS 查询;连接拒绝可以在 TCP 路径成立但没有监听时发生;过滤如果主动回绝和静默丢弃也可能给客户端不同错误。本文不填某个固定 errno 为“标准答案”,VM 所选内核、防火墙和客户端版本都会影响实际提示,原始输出加当次规则才是可核验结论。
三条路径用同一应用输入对照
选一个用户创建的 bridge 网络,把服务探针 B 与客户端 A 放进这个网络,两者由同一构建归档产生的镜像运行;B 以 --host 0.0.0.0 绑定 18080,只在 VM loopback 发布由本轮分配的宿主端口。客户端 A 从同网请求 B 的容器 IP:18080,验证应用进程和 bridge 路径;VM 宿主请求 127.0.0.1:<发布端口>,验证端口映射路径;客户端 A 请求 B 的可解析网络名,验证 DNS 答复加同网连通。所有路径访问相同 /health 或 /identity,把 URL、容器完整 ID、镜像实际 ID、ns inode、源/目的地址与响应状态写在同一组实验记录里。若使用浮动基础镜像标签而两次构建的镜像 ID 不同,这组比较还需明确制品变化,不能称为“同一探针只改变网络路径”。
固定宿主端口会使实验和另一任务相撞,重复轮次也可能不小心请求到旧容器。专用 VM 可以请求由 Docker 为 127.0.0.1 动态分配一个宿主端口,再从这次运行的 docker port 与 inspect 取得它;端口值不从前一轮日志抄写。容器内监听端口保持 18080 不变,A→B 容器 IP、宿主 loopback→发布端口、A→B 网络名三条路径的目标和源位置各不相同。镜像的本地 image ID 标识本次构建结果,却不是 registry 推送所得 manifest digest;如果本轮没有成功推送,不能给这三条路径补一个虚构的分发摘要。动态端口也只减少碰撞,不会证明 VM 上究竟用 DNAT 还是代理实现本次连接转换。
失败例要逐个恢复条件。先把 B 以 --host 127.0.0.1 重新启动,同样从 A 请求其 veth IP 并从 B 本地访问 loopback;记录本次 PID 与前一次是否不同,避免用正常轮的进程日志解释失败轮。然后恢复 0.0.0.0,在 A 内单独查询不存在的名称,记录解析失败及数字 IP 仍可访问的对照。过滤实验只有满足前述 VM/netns 条件才进行,用已有同网成功路径作基线;删除自建规则后必须再次请求并读取规则集,证明恢复不是另一次“碰巧成功”。若 VM 不能确认驱动使用的是 iptables、nftables 还是用户态代理,不调整其全局设置来迎合推测,先按所选版本检视有效配置并收窄本次结论。
单次 docker run -p 成功后仍应保留对照基线。B 进程有 PID 不保证端口已成功对外发布;端口被其它服务占用时创建或启动请求可能失败,故不宜把创建成功与发布端口可访问画成同一个节点。A 内 DNS 能解析却请求失败时,确认 B 的 IP 与解析答案匹配;如果两个答案不一致,检查网络连接、服务别名与重建后的容器 ID,不能用旧地址继续排查新实例。容器数据与网络不是同一种对象:替换 B 时状态目录可能消失,而 DNS 名称可能被重新赋给新 endpoint,单从名字不应推论老 /state 数据还在。随后由 24 的 CNI 接口解释谁创建/释放这些网络对象,23 只解释当前路径各组件的职责。
清理只删除本篇自建 A/B 容器、网络以及专门在 A netns 中添加的过滤规则,核对发布端口不再由本篇对象占用、网络名字不在 Docker 列表中;如果删除网络失败,先查是不是还有本篇 endpoint,不以强制清理全部网络解决。独占 VM 的管理权限和原始宿主规则应在实验前后分别记录,避免测试误触其他任务;本云端既无 Docker 也无 ip,本篇没有容器 ID、端口转发包或 conntrack 实测,依然 NOT_RUN。能够迁移到其他运行时的判断方法是“解析、路由、二层转发、端口映射、监听、业务返回”按次序问首次不符,而非把任何网络错误笼统写成 bridge 故障。
bridge FDB 和邻居缓存也不是一张表。A 要给 B 发帧,A 网络栈先将目的 IP 对应到链路层地址,邻居条目保存在发包视图;宿主 bridge 则据 MAC 地址选择转发端口,记录的是桥侧学习/配置的内容。若 A 内邻居条目一直未就绪,先确认 A 的地址与路由及 B 侧能否回应 ARP;若 A 已拿到 MAC 而请求仍失败,再查 bridge 端口和 B 的监听。只读宿主 bridge fdb 有条目并不能证明 A 的路由正确,反之 A 内 ip neigh 有历史项也不能保证 bridge 端口仍属这个 endpoint。B 被删除重建时更要刷新 ID、IP、MAC 和时间,不把旧条目当作新容器的路径证据。
还有一个“内部能通、外面不能通”的安全角度:宿主 loopback 上的 127.0.0.1:18080 刻意限制了本次实验入口,外部机器即使知道宿主的公网/内网地址也不能把它当作默认对外暴露的服务。更换 HostIP、启用宿主网卡对外访问可能改变暴露面,因此教学案例不以把端口发布到 0.0.0.0 作为排错快捷方式。若来自另一台 VM 的请求失败,本篇只能根据绑定地址确认这并非已经测试过的路径;不能临时放开宿主防火墙或全局端口去“修复”教学验收。实际平台还需显式网络策略和访问控制,不能从 bridge 的二层接通推出多租户隔离安全性。
命令输出的层次也应分列保存。docker network inspect 可以显示网络和 endpoint 的配置;容器内 ip addr/route 是实际进程看到的网络视图;ss -ltn 是执行它的 namespace 里哪些 socket 在监听;docker inspect ... HostConfig.PortBindings 是 Docker 的发布配置;VM 上的连接追踪与抓包则是活跃请求的痕迹。前四项都是某个时刻的配置/状态,不应脱离第五项宣称完整端到端转发已经发生。尤其当 rootless daemon 或不同网络驱动在场,宿主规则可能与 rootful bridge 模式不同,先核验 20 的 daemon 身份再读取对应实现;不能把 Moby rootful bridge 阅读定点套进所有环境。
在 DNS 路径里另一个容易忽略的变量是应用层缓存。A 的 DNS 查询成功后,应用可以缓存旧结果;B 删除重建后取得新 IP,即使 Docker 的解析服务已经给出新答案,老进程可能仍向旧地址发包。教学探针用标准库 urllib 发起单次请求,可以让每轮显式调用解析并保存返回值,减少无法解释的内部缓存;若换成真实长连接业务,要另外记录解析时刻、连接复用与 TTL。此处不要把“新建容器后名字可解析”直接等同于所有客户端立刻连接新 IP,解析与现有 TCP 连接是不同的生命周期。
若计划要求验证过滤拒绝,实验前还要把允许矩阵写清楚:客户端 A 到 B 的 18080 在基线允许,注入期仅针对这一组地址/端口的连接拒绝,恢复期同一命令再次允许。不能顺手在宿主所有接口上设默认拒绝,也不能在 VM 重启后假设自建 netns 规则自动被清理。原始证据需包含插入前/后/删除后的规则快照、命中计数、每次 curl 返回码、应用日志及两个 endpoint 的 PID;缺少规则命中时,一个 HTTP 超时可能来自其它故障,应把归因保留为未确认。实验环境若无法让规则只限在 A netns,就只写清 NOT_RUN 和准备条件,继续做 DNS 与监听两组反例。
本节若不在 VM 真实执行,唯一可确认的是机制和有界输入已经准备好,不能宣称三种失败已经出现。实际记录的步骤应从取得两个容器实际 ID 和 IP 开始,后续所有请求与抓包按该 ID 追溯。若某轮发生端口冲突,先判断是宿主同端口已有服务、上一轮容器未删除,还是 CLI 发布参数写错;只有排除这些条件之后才讨论 bridge/NAT。若一次 HTTP 请求到达 B 但探针因状态目录不可写而退出,这又是应用/挂载权限问题,不能被归到“网络断了”。从唯一可复现的正常链入手,每次只换一个变量,能防止同一状态码被四个不同的失败层误用。
本篇锁定的 Moby v28.4.0 源码只是阅读对象;VM 若使用不同 daemon 或更换 userland proxy 设置,必须重新对齐 port mapping 和 DNS resolver 的入口,再把同一请求中的不同五元组写成结论。无论工具输出多少行,先固定“本次是谁、从哪里、访问哪个地址的哪条路径”,再去解释某条规则为什么命中。旧文的网络图帮助提问,不能充当本次规则计数或应用日志。
| 现象 | 首先查 | 排除什么误判 |
|---|---|---|
| 直连可通,发布端口不通 | 宿主监听/NAT/转发 | 不先责怪 bridge |
| DNS 返回地址但请求失败 | 地址、路由、端口及监听 | 不把解析成功当成服务成功 |
| 容器能访问自身 loopback | netns 内监听范围 | 外侧不一定可达 |
练习
- 先预测从宿主发布端口访问时目标 IP:端口在每个环节如何变化,在专用 VM 抓包与 conntrack 对照,不凭口头拓扑画最终元组。
- 分别预测“域名无法解析”“域名可解析但 TCP 拒绝”的观察差异,用同一实验服务单独注入这两种故障,并恢复自建资源。
上一篇:22:netns 和 veth;下一篇:24:CNI。
系列总目录:从进程隔离到运行时与编排。



