Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

5.6 容器网络与 Kubernetes 原语

本章回答

容器里的 eth0 是从哪来的,包怎么走到宿主网卡?-p 8080:80 是开了一个代理进程还是改了 iptables?CNI 插件到底被谁调用、输入输出是什么?Kubernetes 的 Pod、Service、NetworkPolicy 分别落在哪些 Linux 原语上?

读完你会

用 ip netns/veth/bridge 手工搭一套容器网络并做 NAT;看懂 Docker 与 kube-proxy 的 DNAT 规则;写一份 CNI conflist 与一条 NetworkPolicy;把 Pod/Service/Node 逐项映射到 namespace、veth、iptables、cgroup。

第 4 篇讲了包怎么从网卡进内核、怎么从 socket 发出去;这一章讲包的起点和终点被换掉了:容器有自己的网络栈,宿主要把 veth、bridge、路由、NAT 拼起来,才能让容器里的 eth0 看起来像一张真网卡。

容器网络没有专用内核模块。它复用的全是第 4 篇的零件:network namespace 提供独立的网卡/路由/端口空间,veth pair 像一根虚拟网线,bridge 像一台虚拟交换机,iptables/nftables 做 NAT 与 DNAT。Kubernetes 也没有发明新网络,它只是把这些零件按 Pod/Service 的语义组合起来,并约定 CNI 作为「插网线」的标准接口。

这一章先跟一个包从容器走到宿主,再手工搭一套最小网络,然后依次看端口映射、CNI、VXLAN、Service 与 NetworkPolicy。所有网络实验都在 root 下进行,清理命令会删掉 netns、veth、bridge 与 iptables 规则,请按顺序执行。

5.6.1 一个包从容器到宿主

先看 Docker 默认 bridge 网络的结构。容器里只有一张 eth0,它的对端是宿主上的一根 veth,插在 docker0 网桥上:

 container netns(172.17.0.2/16)
 ┌──────────────────────────────┐
 │ eth0 ──────┐                 │
 │ 默认路由 → 172.17.0.1        │
 └────────────┼─────────────────┘
              │ veth pair(一根虚拟网线)
 ┌────────────┼─────────────────┐
 │ vethXXXX   │                 │
 │      ┌─────▼──────┐          │
 │      │  docker0   │ 172.17.0.1/16
 │      └─────┬──────┘          │
 │            │                 │
 │      宿主路由表              │
 │            │                 │
 │      POSTROUTING MASQUERADE  │
 │            │                 │
 │         物理网卡 enp0s3      │
 └────────────┼─────────────────┘
              ▼
           外部网络

读这张图抓住三个「转换」:

  1. L2 转换:容器把帧发给 eth0,veth 把帧原样送到宿主侧的 vethXXXX,后者挂在 docker0 上。bridge 根据 MAC 学习决定转发到哪个端口。
  2. L3 转换:容器不知道宿主的存在,它按默认路由把包发给 172.17.0.1(docker0 的地址)。宿主打开 ip_forward 后按路由表转发。
  3. NAT 转换:包从物理网卡出去前,POSTROUTING 链把源地址 172.17.0.2 改成宿主地址,同时 conntrack 记下映射;回包按 conntrack 反向改回 172.17.0.2。

容器与容器之间同网段通信不经过 NAT:bridge 直接按 MAC 转发。跨网段或出宿主才走路由 + MASQUERADE。

定义 5.6.1 · veth pair

veth(virtual Ethernet)是一对双向连通的虚拟网卡:从一端发出的帧会立即从另一端收到。它没有「内部交换」,只是把两个网络栈接起来。把一端放进 network namespace、另一端留在宿主并挂到 bridge,就得到「容器一张网卡、宿主一根网线」的效果。

5.6.2 手工搭一套容器网络(root)

先不借助 Docker,用 ip 命令搭出「两个 netns + 一个 bridge + NAT」的最小网络。这样每一步都可验证,出问题也知道该看哪一层。

第一步:建 bridge 与两个 netns

# 需要 root
sudo ip netns add ns1
sudo ip netns add ns2
sudo ip link add br0 type bridge
sudo ip link set br0 up
sudo ip addr add 10.20.0.1/24 dev br0

第二步:给每个 netns 接一根 veth

# 需要 root;ns1
sudo ip link add veth1 type veth peer name veth1-ns
sudo ip link set veth1 master br0
sudo ip link set veth1 up
sudo ip link set veth1-ns netns ns1
sudo ip -n ns1 addr add 10.20.0.11/24 dev veth1-ns
sudo ip -n ns1 link set veth1-ns up
sudo ip -n ns1 link set lo up
sudo ip -n ns1 route add default via 10.20.0.1

# ns2
sudo ip link add veth2 type veth peer name veth2-ns
sudo ip link set veth2 master br0
sudo ip link set veth2 up
sudo ip link set veth2-ns netns ns2
sudo ip -n ns2 addr add 10.20.0.12/24 dev veth2-ns
sudo ip -n ns2 link set veth2-ns up
sudo ip -n ns2 link set lo up
sudo ip -n ns2 route add default via 10.20.0.1

第三步:验证二层与三层连通

# bridge 已学到两个 MAC
sudo bridge fdb show br br0 | grep -E 'veth1|veth2'

# netns 之间互 ping
sudo ip netns exec ns1 ping -c 2 10.20.0.12

# 看 ns1 的路由与邻居表
sudo ip -n ns1 route
sudo ip -n ns1 neigh

这一步不需要 ip_forward:同一网段内,bridge 直接按 MAC 转发,包不经过宿主路由。ping 通说明 veth、bridge、IP 配置都对。

第四步:加 NAT 让 netns 访问外网

# 需要 root;开启转发并给 10.20.0.0/24 做源地址伪装
sudo sysctl -w net.ipv4.ip_forward=1
sudo iptables -t nat -A POSTROUTING -s 10.20.0.0/24 ! -o br0 -j MASQUERADE

# 如果有外网,验证;没有就跳过
sudo ip netns exec ns1 ping -c 2 1.1.1.1

! -o br0 的作用是「从非 br0 接口出去的包才伪装」;如果不加,netns 互访的包也会被 MASQUERADE,虽然能通,但抓包时看不到原始源地址,排障会变难。

第五步:清理

# 先删规则,再删 netns(veth 会随 netns 一起消失)
sudo iptables -t nat -D POSTROUTING -s 10.20.0.0/24 ! -o br0 -j MASQUERADE
sudo ip netns del ns1
sudo ip netns del ns2
sudo ip link del br0

删除 netns 时,里面的 veth 端会被销毁,宿主侧的 peer 也会自动消失;br0 需要单独删。清理后用 ip netns list 与 ip link show type bridge 确认没有残留。

手工容器网络的五个坑
  1. 「veth 一端放进 netns 就自动 up」——不会。两端都要 ip link set up,netns 里的 lo 也要单独 up。
  2. 「同网段互 ping 需要 ip_forward」——不需要。bridge 是二层转发;ip_forward 只在跨网段路由时才需要。
  3. 「MASQUERADE 写在 PREROUTING」——源地址伪装必须放 POSTROUTING,因为要等路由决定出接口后才能选源地址。
  4. 「netns 里的默认路由可以写 dev veth1-ns」——可以写,但没有网关时无法到达其他网段;跨网段必须 via 10.20.0.1。
  5. 「删了 veth 宿主端,netns 端会留着」——veth 是成对的,删任意一端,另一端一起消失;反过来,删 netns 也会带走里面的那端。

5.6.3 端口映射:DNAT、docker-proxy 与 hairpin

docker run -p 8080:80 让宿主 8080 的流量进容器 80。它不是「开一个端口转发进程」这么简单:主流路径是 iptables/nftables 的 DNAT,docker-proxy 只在少数场景兜底。

# 不需要 root(用户在 docker 组)
docker run -d --name web -p 8080:80 nginx:alpine
docker port web
# 80/tcp -> 0.0.0.0:8080

# 需要 root:看 nat 表的 DOCKER 链
sudo iptables -t nat -L DOCKER -n -v
# DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:8080 to:172.17.0.2:80

# 看是否有 docker-proxy 进程
ps -ef | grep docker-proxy | grep -v grep

docker rm -f web

DNAT 发生在 PREROUTING(外部流量)与 OUTPUT(本机流量)之后进入 DOCKER 链。包到达宿主 8080 时,目标地址被改写成 172.17.0.2:80,然后按容器网段路由到 docker0。回包时 conntrack 做反向转换,源地址看起来仍是宿主 8080。

三个容易混淆的点:

  • docker-proxy 不是必须的。它处理「本机访问 127.0.0.1:8080」以及某些 hairpin 场景;纯 DNAT 对来自外部的流量已经够用。可以用 --userland-proxy=false 关掉,观察行为差异。
  • hairpin NAT:容器自己访问「宿主 IP:8080」时,包从容器出发又回到同一 bridge,DNAT 后目标变成自己或同网段容器。某些配置需要额外 MASQUERADE 才能让回包走对路径;Docker 通过规则与 proxy 组合处理。
  • Ubuntu 24.04 的 iptables 是 nft 后端。iptables 命令仍然可用,但底层是 nf_tables;iptables-legacy 与 iptables-nft 的规则不在同一处,排障时先 iptables --version 确认后端。
为什么 Kubernetes 不直接用端口映射

Kubernetes 的 Pod 网络假设「每个 Pod 有独立 IP,Pod 之间可直接互访」,不依赖 NAT。Service 的 ClusterIP 是另一层虚拟 IP,由 kube-proxy 做 DNAT;它与 Docker 的 -p 是两种不同的问题:前者解决「服务发现 + 负载均衡」,后者解决「把容器端口暴露到宿主」。

5.6.4 CNI:插件、配置与调用约定

CNI(Container Network Interface)不是守护进程,也不是网卡驱动,而是一份调用约定:容器运行时在创建/删除 Pod 网络时,执行 /opt/cni/bin 下的插件二进制,通过环境变量与 stdin 传参,插件返回 JSON 结果。

调用时最关键的环境变量:

变量含义
CNI_COMMANDADD、DEL、CHECK、GC、STATUS
CNI_CONTAINERID容器 ID
CNI_NETNS容器 network namespace 的路径(/proc/<pid>/ns/net 或 /run/netns/...)
CNI_IFNAME容器内要配置的网卡名,通常是 eth0
CNI_ARGS额外参数,如 K8S_POD_NAMESPACE=default;K8S_POD_NAME=web
CNI_PATH插件搜索路径,如 /opt/cni/bin

插件从 stdin 读网络配置,向 stdout 写结果。一个 conflist 可以串联多个插件:第一个负责建网卡与分配 IP,后面的负责端口映射、带宽、防火墙。

{
  "cniVersion": "1.0.0",
  "name": "mynet",
  "plugins": [
    {
      "type": "bridge",
      "bridge": "cni0",
      "isGateway": true,
      "ipMasq": true,
      "ipam": {
        "type": "host-local",
        "ranges": [[{ "subnet": "10.244.0.0/24" }]]
      }
    },
    {
      "type": "portmap",
      "capabilities": { "portMappings": true }
    }
  ]
}

这段配置的含义是:创建 cni0 网桥,给容器接 veth 并从 10.244.0.0/24 分配 IP,开启 SNAT;再用 portmap 支持 hostPort。实际集群里,Calico、Cilium、Flannel 会用自己的插件替换 bridge,但调用约定相同。

在节点上观察 CNI 的落点:

# 需要 root 或对应读权限
ls /opt/cni/bin
cat /etc/cni/net.d/*.conflist

# kubelet 通过 CRI 调用运行时,运行时再调 CNI;看 Pod 的 sandbox
sudo crictl pods
sudo crictl inspectp "$(sudo crictl pods -q | head -1)" | jq '.status.network'

crictl inspectp 的输出里能看到 Pod 的 network namespace 路径与 IP。这个 netns 由 pause 容器持有;Pod 里的业务容器通过 setns 共享它(5.6.7 展开)。

CNI 的四个误解
  1. 「CNI 是一个程序」——是一组按约定调用的插件二进制;bridge、host-local、portmap 各是一个独立可执行文件。
  2. 「CNI 负责所有网络策略」——CNI 只负责「给容器接网线、分配 IP」;NetworkPolicy 是否实现、怎么实现,取决于具体 CNI 插件。
  3. 「CNI 配置改完立即生效」——已创建的 Pod 不会自动重配;要重建 Pod 或重启运行时。
  4. 「没有 CNI 集群也能跑 Pod」——Pod 会卡在 ContainerCreating,crictl inspectp 里没有 IP;kubelet 事件会报 network plugin not ready。

5.6.5 overlay 网络:VXLAN 与跨主机

单机 bridge 只能让同一宿主的容器互通。跨主机时,要么让底层网络知道 Pod 网段(路由/BGP),要么把二层帧封装进 UDP 里传(overlay)。VXLAN 是后者最常见的形式。

# 需要 root;在两台宿主上各建一个 VXLAN 设备(VNI 必须一致)
sudo ip link add vxlan0 type vxlan id 42 dev enp0s3 dstport 4789
sudo ip addr add 10.244.1.0/24 dev vxlan0
sudo ip link set vxlan0 up
sudo ip link set vxlan0 mtu 1450

# 添加远端 VTEP:目标宿主的外网 IP
sudo bridge fdb append 00:00:00:00:00:00 dev vxlan0 dst 192.168.1.12

# 查看封装参数
ip -d link show vxlan0

四个要点:

  1. VNI(VXLAN Network Identifier)是 24 位租户标识,两端必须一致;不一致时包能到但对端解不出正确的网络。
  2. dstport 4789 是 IANA 分配的 VXLAN 端口;有些实现用 8472(Flannel 旧默认),两端必须匹配。
  3. MTU 要减 50 字节(外层 IP 20 + UDP 8 + VXLAN 8 + 内层以太网 14)。物理 MTU 1500 时,容器网卡通常设 1450,否则大包会被分片或丢弃,表现为「小包能通、大文件传输卡住」。
  4. underlay 必须能路由到对端 VTEP。VXLAN 不解决底层连通性;它只是把二层帧塞进 UDP。

跨主机方案的对照:

方案原理优点缺点
bridge + 宿主路由每个节点一条静态路由指向 Pod 网段无封装、性能好需要底层网络配合,规模大时路由表膨胀
VXLAN overlay二层帧封装进 UDP不依赖底层、跨子网MTU 减 50、封装开销、排障多一层
BGP(Calico)用 BGP 宣告 Pod 网段无封装、可路由需要网络设备支持,配置复杂
IPIP/IPv6三层隧道简单部分网络屏蔽协议号

5.6.6 Service:ClusterIP 是一次 DNAT

Kubernetes 的 Service 给一组 Pod 一个稳定虚拟 IP(ClusterIP)。这个 IP 不属于任何网卡,ping 它可能不通;它的作用是让 kube-proxy 在包路径上装一条 DNAT 规则,把「访问 VIP:port」改写成「访问某个 PodIP:port」。

# 只作观测;需要可用的集群与 kubeconfig
kubectl create deployment web --image=nginx:alpine --replicas=2
kubectl expose deployment web --port=80 --target-port=80
kubectl get svc web
kubectl get endpointslices -l kubernetes.io/service-name=web

# 需要节点 root:看 kube-proxy 装的 nat 规则
sudo iptables -t nat -L KUBE-SERVICES -n | grep web
sudo iptables -t nat -L KUBE-SVC-XXXXXXXX -n -v
sudo iptables -t nat -L KUBE-SEP-YYYYYYYY -n -v

规则的组织方式(iptables 模式):

KUBE-SERVICES
  └─ 匹配 ClusterIP:port → 跳 KUBE-SVC-<hash>
       ├─ 1/2 概率 → KUBE-SEP-<pod1>
       └─ 其余     → KUBE-SEP-<pod2>
            └─ DNAT --to-destination <podIP>:<targetPort>

KUBE-SVC-* 用 statistic --mode random 在多个 endpoint 之间做选择,KUBE-SEP-* 执行 DNAT。选择发生在每条新连接上;同一条连接后续的包由 conntrack 固定到同一个 Pod,所以不会「一个请求里一半包去 A、一半去 B」。sessionAffinity: ClientIP 则是让 conntrack 按源 IP 保持更久。

不同模式的差别:

模式数据面特点
iptablesnat 表规则 + conntrack成熟、通用;规则随 Service 数量线性增长
IPVSkube-ipvs0 虚拟网卡 + IPVS查找是哈希表,规模大时更快;需要 ip_vs 模块
nftablesnft 规则集新版本提供,语义相同,规则组织更紧凑
eBPFCilium 等替换 kube-proxy绕过 netfilter,可做 L7 与策略

Service 的几种类型只是在同一套 DNAT 上多加一层:

  • ClusterIP:集群内 VIP,kube-proxy 装 DNAT。
  • NodePort:在每个节点开一个端口,KUBE-NODEPORTS 链把 nodeIP:nodePort DNAT 到 Pod。
  • LoadBalancer:云厂商分配外部 LB,转发到 NodePort 或 Pod。
  • Headless(clusterIP: None):不分配 VIP,DNS 直接返回 Pod IP,客户端自己做负载均衡;kube-proxy 不装规则。
  • ExternalName:CNAME 到外部域名,没有 DNAT。
Service 的五个坑
  1. 「ClusterIP 能 ping 通」——iptables 模式下 VIP 没有接口,ICMP 不会被 DNAT,ping 通常不通;TCP 能通不代表 ICMP 能通。
  2. 「Service 是轮询」——iptables 模式按连接随机选择,不是严格轮询;长连接会一直落在同一个 Pod。
  3. 「Service 的 DNAT 在容器里做」——在宿主的 netfilter 里做(Pod 所在节点的 PREROUTING/OUTPUT),容器不感知 VIP。
  4. 「NodePort 的包一定从 nodeIP 进」——也可能从 Pod 所在节点之外进入,取决于 externalTrafficPolicy 与路由。
  5. 「改完 Service,已有连接立即切走」——conntrack 会保留已有连接;要等连接关闭或 conntrack 过期。

5.6.7 Pod、Service、Node 与 Linux 原语的映射

Kubernetes 的每个概念都能在 Linux 里找到落点。把这张表记住,排障时就不会在「YAML 层」打转:

Kubernetes 概念Linux 原语说明
Node一台宿主内核 + kubelet + 容器运行时所有 Pod 共享这个内核
Pod(sandbox)pause 容器 + 独立 net/ipc/uts namespace + cgroupsandbox 持有网络栈,业务容器加入
Pod 内容器同一 cgroup 下的多个进程组共享 network/ipc/uts;默认不共享 PID/mount
Pod IPsandbox netns 里的 veth/eth0由 CNI 分配,跨节点可路由
Serviceiptables/nftables DNAT 或 IPVSkube-proxy 维护,VIP 不在网卡上
K8s NamespaceAPI Server 的逻辑隔离不是 Linux namespace(见 errata)
NetworkPolicyCNI 实现的 iptables/nft/eBPF 规则默认拒绝 + 追加允许
requests/limitscpu.weight/cpu.max、memory.max、pids.max由 kubelet → 运行时 → cgroup
emptyDirbind mount 或 tmpfs随 Pod 生命周期存在
Secret/ConfigMaptmpfs/投影卷 + bind mount挂进容器时是文件
hostPathbind mount 宿主路径高危,等同于把宿主文件系统交给 Pod
hostNetwork: true加入宿主 net namespacePod 直接用宿主 IP 与端口
shareProcessNamespace加入同一 PID namespacePod 内容器互相看见进程
terminationGracePeriodSecondsSIGTERM + 超时 + SIGKILL与 docker stop 同一套语义
SecurityContextcapabilities/seccomp/no_new_privs/uid落到 OCI config.json 的 process/linux

Pod 的网络共享方式值得单独说:kubelet 先让运行时创建 sandbox(pause 容器),由 CNI 给 sandbox 的 netns 配好 eth0 与 IP;然后业务容器以 linux.namespaces 里 path: /proc/<sandbox-pid>/ns/net 的方式加入同一个 netns。所以同一个 Pod 里的容器共享 localhost 与端口空间,但各自有独立的 mount namespace 与 rootfs。

# 需要节点 root;观察 Pod 的 sandbox 与业务容器
SANDBOX=sandbox-id CONTAINER=container-id SPID=sandbox-pid   # 换成 crictl 输出里的实际值
sudo crictl pods
sudo crictl ps
sudo crictl inspectp "$SANDBOX" | jq '.status.network'
sudo crictl inspect "$CONTAINER" | jq '.info.runtimeSpec.linux.namespaces'

# 进入 Pod 网络命名空间抓包
sudo nsenter -t "$SPID" -n ip -brief addr
sudo nsenter -t "$SPID" -n tcpdump -i any -nn -c 5

crictl inspect 的 runtimeSpec 就是 OCI config.json 的运行时展开版;你能在里面看到 linux.namespaces、process.capabilities、linux.seccomp。第 5.5 节学的字段,在这里原样出现。

Kubernetes 与 Linux 概念的四个错位
  1. 「Kubernetes namespace 就是 Linux namespace」——完全不同的东西。K8s Namespace 是 API 对象的租户分组;Linux namespace 是内核的视图隔离。一个 K8s Namespace 下的 Pod 可以分布在多个 Linux namespace 里。
  2. 「Pod 是一个容器」——Pod 是一个 sandbox + 一组容器;pause 容器不跑业务,只持有 netns 与 IPC。
  3. 「Pod 内容器共享所有 namespace」——默认只共享 network/ipc/uts;mount 与 PID 各自独立(除非 shareProcessNamespace)。
  4. 「Service 的 VIP 能在 ip addr 里看到」——iptables 模式看不到;IPVS 模式会在 kube-ipvs0 上看到 VIP,这是两种模式的可见差异。

5.6.8 NetworkPolicy 与包过滤

NetworkPolicy 是 Kubernetes 的 L3/L4 防火墙抽象。它的语义是追加允许:只要有一个策略选中了 Pod,该 Pod 的对应方向就变成默认拒绝,然后逐条匹配允许规则。

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny
  namespace: demo
spec:
  podSelector: {}
  policyTypes: ["Ingress", "Egress"]
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns
  namespace: demo
spec:
  podSelector: {}
  policyTypes: ["Egress"]
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
      ports:
        - protocol: UDP
          port: 53

第一条把 demo 里所有 Pod 的入站与出站都设为默认拒绝;第二条只放行到 kube-system 的 UDP 53(DNS)。两条策略叠加后,Pod 只能发 DNS,其他出站与所有入站都被拒绝。

实现方式取决于 CNI:

CNI数据面特点
Calicoiptables 或 eBPF支持 Policy,规则按 selector 生成
CiliumeBPF支持 L3/L4 与部分 L7,性能好
Flannel不实现 NetworkPolicy需要额外组件(如 Calico policy-only)
云厂商 CNI安全组/网络 ACL行为可能与 Kubernetes 语义有差异
# 只作观测
kubectl get networkpolicy -n demo
kubectl describe networkpolicy default-deny -n demo
kubectl get pods -n demo --show-labels

排障时先确认「策略是否被 CNI 实现」:kubectl describe 能看到对象,但不代表数据面有规则。到节点上看 iptables/eBPF map,或看 CNI 的日志,才能确认。

为什么 NetworkPolicy 不做 L7

L7 策略需要解析 HTTP/gRPC,代价高、语义复杂(mTLS、路径、方法),而且不同协议差异大。Kubernetes 把 L7 留给 Service Mesh(Istio/Linkerd)或 Cilium 的 L7 Policy。L3/L4 策略足够表达「谁能连谁」,也是多数合规要求的落点。

5.6.9 veth 与 bridge 的内核路径

这一节回答:一个帧从容器 eth0 发出来后,在内核里经过哪些函数、在哪些结构体上被转发或丢弃。

veth 不是「一根线」,而是一对 net_device,各自属于一个网络命名空间。发送方的 ndo_start_xmit 是 veth_xmit(),它取出 peer,把 skb 交给 veth_forward_skb(),最终用 netif_rx() 或 dev_forward_skb() 注入对端网络栈。整个过程不复制数据,只转移 skb 的所有权。

bridge 是一个软件交换机:br_handle_frame() 在入口处理帧,br_fdb_update() 学习源 MAC,br_forward() 按目的 MAC 查 FDB 并从对应端口发出;查不到就 br_flood() 泛洪。FDB 表项有老化时间,默认 300 秒,由 br_fdb_cleanup() 周期清理。

container eth0(veth1-ns)
  │ dev_queue_xmit
  ▼
veth_xmit() → veth_forward_skb() → netif_rx()
  │ 对端 veth1 在宿主 netns
  ▼
__netif_receive_skb_core() → br_handle_frame()
  │ br_fdb_update() 学源 MAC
  │ br_forward() 查目的 MAC
  ▼
veth2(去 ns2)或宿主路由(去外部)

内核里与这条路径相关的结构体:

结构体定义关键字段谁维护
net_deviceinclude/linux/netdevice.hname、ifindex、flags、mtu、dev_addr、netdev_ops驱动注册,内核分配
veth_privdrivers/net/veth.cpeer(对端 net_device)、rq(每队列状态)、xdp_progveth 驱动
net_bridgenet/bridge/br_private.hport_list、fdb_hash_tbl、dev、ageing_time、stp_enabledbridge 模块
net_bridge_fdb_entry同上dst(端口)、key.addr、key.vlan_id、flags、usedFDB 学习/老化
net_bridge_port同上dev(挂载的网卡)、state、flags端口加入/离开

对应的内核文件:

文件函数作用
drivers/net/veth.cveth_newlink()、veth_xmit()、veth_forward_skb()创建 veth 对、转发帧
net/core/dev.cdev_queue_xmit()、__netif_receive_skb_core()通用发送/接收入口
net/bridge/br_input.cbr_handle_frame()bridge 入口,处理 STP/组播/转发
net/bridge/br_fdb.cbr_fdb_update()、br_fdb_lookup()MAC 学习与查找
net/bridge/br_forward.cbr_forward()、br_flood()单播转发与泛洪
net/bridge/br_device.cbr_dev_xmit()从 bridge 自身发出(宿主路由)

观测这套路径不需要抓包器,先看计数器:

# 需要 root;veth 的收发/丢包计数
ip -s link show veth1
ethtool -S veth1 2>/dev/null | head -20

# bridge 的端口状态与 FDB
bridge link show br0
bridge fdb show br br0 | head
bridge -s fdb show br br0 | head     # 带命中计数

# 内核丢包原因(需要 root)
nstat -az | grep -Ei 'drop|no_route|noroute' | head

bridge fdb show 里的 self 表示 bridge 自身的 FDB,master br0 表示学习到的端口项;permanent 是静态项,dynamic 会老化。看到 00:00:00:00:00:00 的项不要惊讶,那是 VXLAN 的默认远端项或未学习时的占位。

用 bpftrace 看一次 ping 走了哪些函数:

# 需要 root;另一个终端在 ns1 里 ping ns2
sudo bpftrace -e '
kprobe:veth_xmit      { @veth[comm] = count(); }
kprobe:br_handle_frame { @br[comm] = count(); }
kprobe:br_fdb_update  { @fdb[comm] = count(); }
interval:s:5          { exit(); }'

如果 veth_xmit 的计数为 0 而 br_handle_frame 有值,说明帧没有从容器发出来(可能是容器内路由/网卡 down);两者都为零,检查 ip link 与 netns 是否正确。

veth 的 offload 也会影响行为。ethtool -k veth1 里 tx-checksumming、tso、gso 默认开着,抓包时看到的可能是「未校验和的大包」;用 tcpdump -i veth1 -vv 看 bad cksum 不代表链路有问题,而是校验和卸载到对端才计算。排查 MTU 时先 ip link show 看 mtu,再用 ping -M do -s 验证。

5.6.10 CNI 协议深挖与实验一

这一节回答:CNI 插件被调用时到底收到什么、返回什么,以及如何不经过 Kubernetes 手工跑一次 ADD。

CNI 的协议只有「环境变量 + stdin + stdout + 退出码」四件事:

输入/输出内容说明
CNI_COMMANDADD/DEL/CHECK/GC/STATUS/VERSIONCHECK 从 0.4.0 起,GC/STATUS 从 1.2.0 起
CNI_CONTAINERID容器 ID必须唯一,用于缓存与幂等
CNI_NETNSnetns 路径通常是 /proc/<pid>/ns/net 或 /var/run/netns/<name>
CNI_IFNAME容器内网卡名通常 eth0,必须符合内核接口名规则
CNI_ARGSK=V;K=VKubernetes 传 K8S_POD_NAMESPACE/K8S_POD_NAME 等
CNI_PATH插件搜索路径如 /opt/cni/bin,委托 IPAM 时也用它
stdin网络配置 JSON单个 plugin 配置或 conflist 展开后的配置
stdout结果 JSONcniVersion、interfaces、ips、routes、dns
退出码0 成功,非 0 失败失败时 stdout 应是错误 JSON:code、msg、details

一个 conflist 的每个插件都会被依次执行,前一个插件的输出会作为后一个的输入(通过 prevResult)。ADD 从第一个插件跑到最后一个;DEL 逆序执行;CHECK 与 ADD 同序、逐层校验 prevResult。主插件(bridge、macvlan、ipvlan、ptp)负责建网卡,IPAM 插件(host-local、dhcp、static)负责分配地址,meta 插件(portmap、bandwidth、tuning、firewall)负责附加能力。

host-local 的租约存在 dataDir(默认 /var/lib/cni/networks/<network>)下,每个 IP 一个文件,内容是第一行容器 ID、第二行 IFNAME。这解释了两种常见故障:容器被强杀后租约没释放,新 Pod 可能拿到同一个 IP;DEL 时找不到租约,插件报 failed to delete IP。清理时要同时删租约文件与 netns。

下面的实验直接调用 bridge + host-local,不经过 Docker 或 Kubernetes。需要 root、ip 命令与 CNI 插件(Ubuntu 包 containernetworking-plugins 提供 /opt/cni/bin)。

#!/usr/bin/env bash
# cni-lab.sh — 手工执行 CNI ADD/DEL,验证网卡、IP 与租约
set -euo pipefail
[[ ${EUID:-$(id -u)} -eq 0 ]] || { echo "需要 root"; exit 1; }
CNI_BIN=${CNI_BIN:-/opt/cni/bin}
[[ -x "$CNI_BIN/bridge" && -x "$CNI_BIN/host-local" ]] || {
    echo "缺少 CNI 插件:$CNI_BIN/{bridge,host-local}"; exit 1; }

NS=cni-lab
BR=cni-br0
CID=lab-$$
D=/tmp/cni-lab
cleanup() {
    ip netns del "$NS" 2>/dev/null || true
    ip link del "$BR" 2>/dev/null || true
    rm -rf "$D"
}
trap cleanup EXIT

mkdir -p "$D/ipam"
ip netns add "$NS"
ip netns exec "$NS" ip link set lo up

cat > "$D/netconf.json" <<'JSON'
{
  "cniVersion": "1.0.0",
  "name": "cni-lab",
  "type": "bridge",
  "bridge": "cni-br0",
  "isGateway": true,
  "ipMasq": true,
  "ipam": {
    "type": "host-local",
    "dataDir": "/tmp/cni-lab/ipam",
    "ranges": [[{ "subnet": "10.88.0.0/24" }]]
  }
}
JSON

export CNI_CONTAINERID="$CID" CNI_NETNS="/var/run/netns/$NS" \
       CNI_IFNAME=eth0 CNI_PATH="$CNI_BIN"

CNI_COMMAND=ADD "$CNI_BIN/bridge" < "$D/netconf.json" > "$D/result.json"
jq '{cniVersion, interfaces, ips, routes}' "$D/result.json"

addr=$(jq -r '.ips[0].address' "$D/result.json")
echo "[ok] 分配地址:$addr"
ip netns exec "$NS" ip -brief addr show eth0
ip netns exec "$NS" ping -c 2 -W 1 10.88.0.1
cat "$D/ipam/cni-lab/"* 2>/dev/null || true

CNI_COMMAND=DEL "$CNI_BIN/bridge" < "$D/netconf.json" > /dev/null
ip netns exec "$NS" ip link show eth0 2>&1 || echo "[ok] eth0 已删除"

预期结果:result.json 的 ips[0].address 是 10.88.0.2/24 之类的地址,interfaces 里同时有宿主侧 vethXXX 与容器侧 eth0;ping 10.88.0.1 通;租约目录里出现以 IP 命名的文件;DEL 后容器里的 eth0 消失。如果 bridge 报 failed to set bridge addr,说明 cni-br0 已存在且地址冲突,先 ip link del cni-br0。

注意 CNI_NETNS 用的是 /var/run/netns/cni-lab,这是 ip netns add 创建的 bind mount 路径;用 /proc/<pid>/ns/net 也可以,但要求进程存活。DEL 必须传与 ADD 相同的 CNI_CONTAINERID 与配置,否则插件找不到缓存,可能留下网卡。

CNI 的四个协议细节
  1. 「ADD 失败可以重试」——可以,但插件必须幂等;bridge 会用容器 ID 找已有 veth,重复 ADD 可能报 already exists。重试前先 DEL。
  2. 「DEL 不需要配置」——需要。DEL 用配置里的 bridge、ipam 与 CNI_IFNAME 定位资源;只传环境变量不传 stdin 会失败。
  3. 「CHECK 会修复问题」——不会,它只检查并返回错误;修复要靠 DEL + ADD。
  4. 「CNI 结果里的 ips 就是 Pod IP」——单网卡场景通常是;多网卡或双栈时 ips 可能有多条,要按 interface 索引与地址族筛选。

5.6.11 Service 数据面:iptables、IPVS 与 eBPF

这一节回答:KUBE-SVC/KUBE-SEP 这些链具体怎么排、IPVS 模式差在哪、eBPF 又替换了哪一层。

iptables 模式下,kube-proxy 在 nat 表里维护一棵四层链:

链作用关键规则
KUBE-SERVICES所有 Service 的总入口按 ClusterIP:port 匹配,跳 KUBE-SVC-<hash>
KUBE-SVC-<hash>一个 Service 的负载均衡statistic --mode random 按概率选 endpoint
KUBE-SEP-<hash>一个 endpointKUBE-MARK-MASQ + DNAT --to-destination <podIP>:<port>
KUBE-NODEPORTSNodePort 入口匹配 nodePort 后跳对应 KUBE-SVC
KUBE-MARK-MASQ打标记MARK --set-xmark 0x4000/0x4000
KUBE-POSTROUTING出站伪装对带标记的包 MASQUERADE
KUBE-FIREWALL/KUBE-FORWARDfilter 表兜底丢弃非法状态、放行转发

一条真实规则的样子(注释与 hash 因集群而异):

-A KUBE-SERVICES -d 10.96.0.10/32 -p tcp -m comment --comment "default/kubernetes:https" -m tcp --dport 443 -j KUBE-SVC-XXXX
-A KUBE-SVC-XXXX -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-AAAA
-A KUBE-SVC-XXXX -j KUBE-SEP-BBBB
-A KUBE-SEP-AAAA -s 10.244.1.5/32 -p tcp -m tcp --dport 6443 -j KUBE-MARK-MASQ
-A KUBE-SEP-AAAA -p tcp -m tcp --dport 6443 -j DNAT --to-destination 10.244.1.5:6443

statistic 只作用于新连接:nat 表只对连接的第一个包做 DNAT,后续包由 conntrack 固定到同一 endpoint。最后一个 endpoint 不再写概率,直接跳转,保证所有流量都有归宿;没有 endpoint 时规则改成 REJECT --reject-with icmp-port-unreachable。

IPVS 模式把「按概率跳转」换成内核的 IPVS 哈希表:

# 需要节点 root;IPVS 模式才有 kube-ipvs0
ip addr show kube-ipvs0 | head
sudo ipvsadm -Ln
# TCP  10.96.0.10:443 rr
#   -> 10.244.1.5:6443            Masq    1      0          0
#   -> 10.244.2.7:6443            Masq    1      0          0
sudo ipvsadm -Ln --stats

kube-ipvs0 是一张 dummy 网卡,kube-proxy 把每个 ClusterIP 都挂上去,让本机路由认为 VIP 是本地地址;真正的转发由 IPVS 完成。IPVS 支持 rr、wrr、lc、wlc、sh 等调度器,kube-proxy 默认 rr,可用 --ipvs-scheduler 改。IPVS 模式仍然需要 iptables 做 masquerade、NodePort 的某些路径与防火墙兜底,所以看到 KUBE-* 链不要以为模式没生效。

三种数据面的对照:

维度iptablesIPVSeBPF(Cilium)
查找复杂度O(规则数)O(1) 哈希O(1) map,socket 层可直连
规则规模随 Service/Endpoint 线性增长与数量弱相关map 条目,无链式遍历
连接跟踪conntrackconntrack(可配)可绕过 conntrack
负载均衡随机概率rr/wrr/lc 等Maglev/随机,可一致性哈希
L7 策略不支持不支持支持(Envoy)
可观测性iptables -vnL、计数器ipvsadm -Ln --statscilium monitor、Hubble
主要限制大集群规则多、更新慢需要 ip_vs 模块内核版本、eBPF 校验器、排障门槛

eBPF 不是「更快的 iptables」,而是把 hook 点前移:Cilium 在 tc/XDP 处理包,在 socket connect/sendmsg 时就能决定后端,绕过 netfilter 与 conntrack。它用 identity(标签)而不是 IP 做策略,Pod 重建换 IP 不影响策略;代价是内核依赖更强、抓包位置更靠前、排障要熟悉 BPF map。

手工复现一次 Service DNAT(实验二)
  1. 建 svc-br0 与 cli/srv 两个 netns,配 10.99.0.0/24。
  2. srv 里起 8080 的 HTTP 服务;cli 里把 VIP 10.99.0.100 挂到 lo。
  3. cli 的 nat OUTPUT 链加 DNAT:VIP:80 → 10.99.0.2:8080。
  4. 从 cli 里 curl VIP:80,断言返回 200;看 iptables 计数器与 conntrack。 需要 root、iptables、python3、curl。
#!/usr/bin/env bash
# svc-dnat-lab.sh — 在 netns 里复现 ClusterIP 的 DNAT
set -euo pipefail
[[ ${EUID:-$(id -u)} -eq 0 ]] || { echo "需要 root"; exit 1; }
for c in ip iptables python3 curl; do
    command -v "$c" >/dev/null || { echo "缺少 $c"; exit 1; }
done

BR=svc-br0
cleanup() {
    [[ -n ${SRV_PID:-} ]] && kill "$SRV_PID" 2>/dev/null || true
    ip netns del cli 2>/dev/null || true
    ip netns del srv 2>/dev/null || true
    ip link del "$BR" 2>/dev/null || true
}
trap cleanup EXIT

ip netns add cli
ip netns add srv
ip link add "$BR" type bridge
ip link set "$BR" up
ip addr add 10.99.0.1/24 dev "$BR"

for pair in "cli 10.99.0.11" "srv 10.99.0.2"; do
    set -- $pair
    ns=$1; ip4=$2
    ip link add "veth-$ns" type veth peer name "veth-$ns-ns"
    ip link set "veth-$ns" master "$BR"
    ip link set "veth-$ns" up
    ip link set "veth-$ns-ns" netns "$ns"
    ip -n "$ns" addr add "$ip4/24" dev "veth-$ns-ns"
    ip -n "$ns" link set "veth-$ns-ns" up
    ip -n "$ns" link set lo up
    ip -n "$ns" route add default via 10.99.0.1
done

# 后端:8080 上的 HTTP 服务
ip netns exec srv python3 -m http.server 8080 --bind 0.0.0.0 >/dev/null 2>&1 &
SRV_PID=$!
sleep 1

# 客户端:VIP 挂在 lo 上(等价于 kube-ipvs0 的作用),OUTPUT 链做 DNAT
ip -n cli addr add 10.99.0.100/32 dev lo
ip netns exec cli iptables -t nat -A OUTPUT \
    -d 10.99.0.100/32 -p tcp --dport 80 \
    -j DNAT --to-destination 10.99.0.2:8080

code=$(ip netns exec cli curl -s --noproxy '*' -o /dev/null -w '%{http_code}' --max-time 5 \
    http://10.99.0.100:80/)
echo "HTTP code via VIP: $code"
[[ "$code" == 200 ]] || { echo "DNAT 未生效"; exit 1; }

ip netns exec cli iptables -t nat -L OUTPUT -n -v
ip netns exec cli conntrack -L 2>/dev/null | grep 8080 || true

预期输出 HTTP code via VIP: 200,iptables -t nat -L OUTPUT -v 的 DNAT 规则计数增加;如果装了 conntrack-tools,还能看到 10.99.0.100:80 被映射到 10.99.0.2:8080 的连接。这个实验与 kube-proxy 的差别只在「规则由谁维护」:kube-proxy 监听 Service/EndpointSlice 变化,自动增删同样的 DNAT 规则。

5.6.12 Pod、Namespace 与 NetworkPolicy 的语义细节

这一节回答:Kubernetes 的 Namespace 与 Linux namespace 到底怎么区分,Pod 共享哪些命名空间,NetworkPolicy 的「或/与」怎么写。

先彻底分开两个同名概念:

维度Kubernetes NamespaceLinux namespace
本质API Server 里的对象,etcd 中的路径前缀内核的视图隔离机制
作用资源分组、RBAC、配额、名字作用域进程看到的 PID/网络/挂载等
创建方式kubectl create ns demounshare/clone/setns
删除后果级联删除其中的对象最后一个进程退出后内核释放
与 Pod 的关系Pod 属于某个 K8s NamespacePod 的容器各自加入一组 Linux namespace

一个 K8s Namespace 下的 Pod 可以分布在任意节点、任意 Linux netns 里;反过来,同一个 Linux netns 里的容器可以属于不同的 K8s Namespace(只要调度到同一 Pod)。排障时不要把 kubectl get ns 的输出当成内核 namespace 列表。

Pod 的共享关系由 sandbox 决定:

配置共享的 Linux namespace效果
默认 Podnetwork、ipc、uts同 Pod 容器共享 IP、端口、hostname
shareProcessNamespace: true再加 pid容器间能看见彼此进程,PID 1 是 pause
hostNetwork: true加入宿主 netnsPod 用宿主 IP,不调用 CNI
hostPID: true加入宿主 pid ns看见宿主进程,高危
hostIPC: true加入宿主 ipc ns共享 SysV IPC

hostNetwork 的 Pod 没有独立 IP,kubectl get pod -o wide 显示的是节点 IP;它的 dnsPolicy 通常要设成 ClusterFirstWithHostNet,否则 DNS 解析走宿主配置。shareProcessNamespace 下,业务容器的 /proc/1 是 pause,不是自己的主进程;依赖「自己是 PID 1」做信号处理的应用会受影响。

NetworkPolicy 的匹配规则只有三条,但组合起来容易写错:

写法含义
podSelector: {}选中本 Namespace 的所有 Pod
namespaceSelector: {}选中所有 Namespace
from: [{podSelector: A, namespaceSelector: B}]A 且 在 B 中(同一元素的字段是 AND)
from: [{podSelector: A}, {namespaceSelector: B}]A 或 B(不同元素是 OR)
ipBlock: {cidr: 10.0.0.0/8, except: [10.1.0.0/16]}CIDR 减去 except
ports: [{protocol: TCP, port: 80, endPort: 90}]80–90 端口段(endPort 1.27 稳定)

策略是追加允许:只要有一个策略选中 Pod 的某个方向,该方向先默认拒绝,再按所有策略的允许规则取并集。没有策略选中时默认全放行。策略没有顺序,也没有 deny 规则;想「只拒绝某个 IP」只能「默认拒绝 + 放行其余」,不能写一条单独的拒绝。

一个常见的漏配:加了 default-deny egress 后忘了放行 DNS,Pod 里所有域名解析都超时。DNS 的出站规则要同时放行 UDP 53 与 TCP 53(大响应会走 TCP),目标可以是 kube-system 的 namespaceSelector,也可以写 CoreDNS 的 Pod 标签。

实现层面对照:

CNI数据面NetworkPolicy备注
Calicoiptables 或 eBPF支持规则按 selector 生成,可看 calicoctl
CiliumeBPF支持,含部分 L7identity 级策略,Hubble 可观测
AntreaOVS支持基于 Open vSwitch 流表
Flannel无不支持需要额外装 Calico policy-only
云厂商 CNI安全组/ACL视实现语义可能与 K8s 有差异

eBPF 的 hook 点决定了它能看到什么:

hook位置典型用途
XDP网卡驱动最早DDoS 清洗、快速丢弃
tc ingress/egress协议栈收发Cilium 的主战场:转发、NAT、策略
socket connect/sendmsg应用 syscallkube-proxy 替换、服务发现
cgroup skbcgroup 层按 Pod 限流/审计
kprobe/tracepoint内核函数观测,不参与转发

Cilium 的 kube-proxy replacement 就是在 socket connect 时直接选后端,包还没进 netfilter 就已经决定去向;这让它在大规模 Service 下避免 iptables 的线性遍历。代价是它依赖较新的内核与 BPF 特性,排障要会看 cilium bpf lb list、cilium monitor,而不是 iptables -L。

5.6.13 四个让包静默消失的开关:rp_filter、br_netfilter、conntrack、MTU

Pod 里 ping 得通自己、却连不上 Service;同节点两个 Pod 互相访问正常,跨节点就超时。这类问题经常不是 CNI 配错,而是四个内核开关按默认值工作。它们的共同点是不报错、只计数,所以必须先知道去哪看。

开关默认/典型值作用点症状排查
net.ipv4.ip_forward新 netns 里为 0ip_forward() 的入口判断以 Pod 为路由的流量被丢sysctl net.ipv4.ip_forward
net.ipv4.conf.*.rp_filter0/1/2,发行版不同(RHEL 常 1,Ubuntu 常见 2)fib_validate_source()非对称路由的回包被当 martian 丢弃nstat -az | grep -i addr
net.bridge.bridge-nf-call-iptables未加载时无此文件;加载后默认 1bridge 把帧送进 IPv4 钩子FORWARD/NetworkPolicy 对同节点 Pod 流量不生效lsmod | grep br_netfilter
net.netfilter.nf_conntrack_max随内存缩放,常见 262144conntrack 哈希表容量表满后新连接被丢,dmesg 有 table fullconntrack -C、sysctl
nf_conntrack_tcp_timeout_established432000 秒(5 天)conntrack 老化长连接占满表、NAT 端口耗尽conntrack -L 的 ESTABLISHED
veth/bridge MTU1500发送路径分片/DF大包黑洞,小包正常ip link、ping -M do -s
VXLAN/Geneve 开销50 / 50+ 字节封装后外层包变大underlay 1500 → overlay 1450ip -d link show vxlan0
net.ipv4.tcp_mtu_probing0PMTU 黑洞探测TCP 大包卡住、握手后无数据sysctl、ss -ti 的 mss

rp_filter 的三个值要分清:0 不检查;1 严格模式,回包必须从「去程使用的同一网卡」进来;2 宽松模式,只要求源地址在任意网卡上可达。容器节点常有多张网卡、VXLAN 设备与策略路由,严格模式会把合法的非对称回包丢掉。内核在 fib_validate_source() 里用 IN_DEV_RPFILTER() 取当前值,失败返回 -EINVAL,上层把它记成输入错误,不发 ICMP。

br_netfilter 是另一类陷阱:Linux bridge 默认是纯二层转发,帧不经过 IPv4 的 FORWARD 链。只有加载 br_netfilter 并把 bridge-nf-call-iptables 设为 1,桥接帧才会被送进 netfilter。Kubernetes 的 NetworkPolicy 实现(Calico、Cilium 的 iptables 模式)依赖这一点;如果 kubelet 的 sysctl 没生效,策略看起来「装了但不拦」。

内核路径:一个转发包经过的钩子

ip_rcv()                              net/ipv4/ip_input.c
  → NF_INET_PRE_ROUTING
      → nf_conntrack_in()             net/netfilter/nf_conntrack_core.c
  → ip_rcv_finish() → ip_route_input_noref()   net/ipv4/route.c
      → fib_validate_source()         net/ipv4/fib_frontend.c   ← rp_filter
  → ip_forward()                      net/ipv4/ip_forward.c
      → NF_INET_FORWARD               ← iptables FORWARD / NetworkPolicy
  → ip_output() → NF_INET_POST_ROUTING
      → nf_conntrack_confirm()        ← 只有确认后才进入 conntrack 表

bridge 路径:
br_handle_frame()                     net/bridge/br_input.c
  → br_pass_frame_up() / br_forward()
      →(br_netfilter 加载且 bridge-nf-call-iptables=1)
        br_nf_pre_routing() → ip_rcv() → 上面的 IPv4 钩子

VXLAN 封装:
vxlan_xmit()                          drivers/net/vxlan/vxlan_core.c
  → vxlan_build_skb() → udp_tunnel_xmit_skb()   (外层 +50 字节)

这张路径图解释了三个现象:rp_filter 在路由决策里执行,所以策略路由和多网卡会让它误判;FORWARD 只在路由转发时经过,纯二层桥接必须靠 br_netfilter 才进得来;conntrack 在 PRE_ROUTING 建表项、在 POST_ROUTING 确认,NAT 只在第一个包上做,后续包靠表项命中。

实验一:br_netfilter 决定 FORWARD 链是否管桥接流量

需要 root、iptables、conntrack 与 br_netfilter 模块。两个 netns 接在同一 bridge 上,用一条 FORWARD DROP 规则观察它是否生效。

ip netns add n1; ip netns add n2
ip link add br0 type bridge; ip link set br0 up
ip link add v1 type veth peer name v1b; ip link set v1 netns n1
ip link add v2 type veth peer name v2b; ip link set v2 netns n2
ip link set v1b master br0; ip link set v2b master br0
ip link set v1b up; ip link set v2b up
ip addr add 10.20.0.1/24 dev br0
ip -n n1 addr add 10.20.0.2/24 dev v1; ip -n n1 link set v1 up
ip -n n2 addr add 10.20.0.3/24 dev v2; ip -n n2 link set v2 up

ip netns exec n1 ping -c1 -W1 10.20.0.3
# 1 packets transmitted, 1 received

modprobe br_netfilter
sysctl -qw net.bridge.bridge-nf-call-iptables=0
iptables -A FORWARD -s 10.20.0.2 -d 10.20.0.3 -j DROP
ip netns exec n1 ping -c1 -W1 10.20.0.3
# 仍然 1 received:纯二层转发不经过 FORWARD

sysctl -qw net.bridge.bridge-nf-call-iptables=1
ip netns exec n1 ping -c1 -W1 10.20.0.3
# 100% packet loss:桥接帧进入 IPv4 钩子,被 FORWARD DROP
conntrack -L 2>/dev/null | grep -c '10.20.0' || true

iptables -D FORWARD -s 10.20.0.2 -d 10.20.0.3 -j DROP
ip netns del n1; ip netns del n2; ip link del br0

三行 ping 输出就是证据链:bridge-nf-call-iptables=0 时规则「存在但不生效」,改成 1 后立刻丢包。这就是为什么 kube-proxy/CNI 的 DaemonSet 要在启动时 modprobe br_netfilter 并写这两个 sysctl;漏掉时 NetworkPolicy 只在跨节点路由的流量上生效,同节点 Pod 之间照常互通。

实验二:MTU 与 -M do 的边界

# 仍在实验一的 netns/网卡上(或重建)
ip -n n1 link set v1 mtu 1400
ip netns exec n1 ping -M do -s 1372 -c1 10.20.0.3   # 1400-20-8=1372,成功
ip netns exec n1 ping -M do -s 1373 -c1 10.20.0.3
# ping: local error: message too long, mtu=1400
ip netns exec n1 ping -c1 -s 1400 10.20.0.3         # 允许分片,仍可能通

-M do 设置 DF 位,让内核在超过 MTU 时直接 EMSGSIZE,而不是分片。VXLAN 场景把 v1 换成 vxlan0 即可复现 overlay MTU 1450:underlay 1500 减去外层 IP(20)+UDP(8)+VXLAN(8)+内层以太网(14)=50 字节。Calico 的 IPIP 是 20 字节,overlay MTU 1480;Geneve 通常 50 字节。MTU 配错时,ping -s 56 永远正常,只有大包或 TLS 握手卡住——这是最难查的一类「网络慢」。

四个开关的八个坑
  1. rp_filter=1 在多网卡节点误杀合法回包——策略路由、VXLAN、多默认路由都会造成非对称;容器节点通常设 2 或在 CNI 网卡上单独设 0,并记录原因。
  2. bridge-nf-call-iptables=0 时以为 NetworkPolicy 生效——同节点 Pod 走二层,FORWARD 链根本不参与;先确认 lsmod | grep br_netfilter 与两个 sysctl。
  3. nf_conntrack_max 按 CPU 核数拍脑袋——它约束的是并发五元组数,NAT 场景还要考虑端口范围;表满时内核丢新连接,已有连接不受影响,监控要看 nf_conntrack_count/max 的比例。
  4. 把 VXLAN 的 50 字节开销忘掉——underlay 1500 的集群里 overlay MTU 必须是 1450;Calico IPIP 是 1480,混用两种封装时按最小的算。
  5. 用 ping -M do 验证 TCP 的 PMTU——ICMP 只验证了 ICMP 路径;TCP 还受 tcp_mtu_probing 与中间设备 ICMP 过滤影响,黑洞时表现为「能连上、传不了数据」。
  6. 新 netns 的 ip_forward 默认是 0——它不继承宿主的 1;把 Pod 当路由器(如 Pod 内跑 VPN、代理)时必须显式打开。
  7. conntrack -L 里有 [UNREPLIED] 就以为丢包——它只表示还没看到回包;NAT 与 Service 的 DNAT 只在第一个包上做,后续包靠表项。
  8. iptables -A FORWARD 排到 K8s 链后面——kube-proxy/CNI 会把自己的链插在 FORWARD 顶部,-A 追加的规则可能永远不命中;用 -I FORWARD 1 或 iptables -L -n --line-numbers 确认顺序。

5.6.14 排障顺序与 12 条陷阱

容器网络排障最容易犯的错是「一上来就抓包」。按层往下走,通常三步就能定位:

容器网络排障顺序
  1. Pod 有没有 IP:kubectl get pod -o wide;没有 IP 看 kubelet 事件与 CNI 日志。
  2. sandbox 在不在:crictl pods、crictl inspectp <id> | jq '.status.network';确认 netns 路径与 IP。
  3. 容器内地址/路由:nsenter -t <pid> -n ip -brief addr、ip route;确认默认路由与 MTU。
  4. 二层通不通:ip neigh、bridge fdb show;同网段先 ping 对端 Pod IP。
  5. 三层转发:宿主 ip route get <podIP>、sysctl net.ipv4.ip_forward;跨节点看 underlay 路由/VXLAN FDB。
  6. NAT/DNAT:iptables -t nat -L -n -v、conntrack -L;确认 MASQUERADE 与 Service 规则命中计数。
  7. 策略:kubectl get networkpolicy、CNI 日志;确认不是默认拒绝。
  8. DNS:nslookup kubernetes.default、/etc/resolv.conf;CoreDNS 的 Service 与 endpoints 是否正常。
  9. MTU:ping -M do -s 1472 <ip>;VXLAN 环境把 1472 改成 1452 再试。
# 一组常用观测命令(部分需要 root)
ip -brief addr
ip route get 10.244.1.5
sudo conntrack -L | grep 8080
sudo tcpdump -i any -nn -c 20 'port 8080'
sudo iptables -t nat -L -n -v | head -40
sudo bridge fdb show | head
容器网络的五个高频错误
  1. 「Pod 里能 ping 通外网,Service 就正常」——Service 是 DNAT,ping VIP 可能本来就不通;要用 TCP 连接目标端口验证。
  2. 「CNI 装好就一定有 NetworkPolicy」——Flannel 默认不实现;要确认 CNI 的能力。
  3. 「VXLAN 的 MTU 不用改」——封装多出 50 字节,不改会导致大包被丢,表现为 TLS 握手卡住、大响应失败。
  4. 「Docker 的 -p 一定走 iptables」——本机访问与 hairpin 可能走 docker-proxy;关掉 userland proxy 后行为会变。
  5. 「容器里 ip addr 看到宿主网卡」——那说明用了 hostNetwork 或没进对 netns;先确认 readlink /proc/self/ns/net。
#现象原因修复
1同 bridge 两个容器 ping 不通一端 veth 或 netns 里的 lo 没 up两端都 ip link set up;ip -n ns link 检查
2跨网段不通,同网段正常没开 ip_forward,或默认路由没写网关sysctl -w net.ipv4.ip_forward=1;route add default via
3出宿主不通,抓包看到私网源 IPMASQUERADE 规则没命中规则放 POSTROUTING,检查 -s 网段与 ! -o 条件
4Service 偶发连接重置rp_filter 丢了非对称路由的回包调 net.ipv4.conf.*.rp_filter,或让回包走同一路径
5容器访问 Service 很慢但直连 Pod 正常hairpin/conntrack 路径问题,或 bridge 上的帧没进 iptables(bridge-nf-call-iptables=0)设 bridge-nf-call-iptables=1;检查 KUBE-MARK-MASQ 与 conntrack
6VXLAN 小包通、大包卡MTU 没减 50,分片被丢Pod/容器 MTU 设 1450;ping -M do -s 1452 验证
7Pod 重建后 CNI DEL 失败旧 netns 已消失,缓存/租约还在清 /var/lib/cni/results 与 /var/lib/cni/networks
8两个 Pod 拿到同一个 IPhost-local 租约泄漏(容器被强杀)删租约文件;让 CNI 走 GC;检查 IPAM dataDir
9加了 default-deny 后全部超时没放行 DNS(UDP/TCP 53)加 egress 允许 kube-system 的 53
10NodePort 在本机不通、外部却通hairpin NAT 或 externalTrafficPolicy: Local 无本地端点换 Cluster 策略;检查 endpoints
11kube-ipvs0 上看不到 VIPkube-proxy 是 iptables 模式kubectl -n kube-system logs kube-proxy 看 proxyMode
12iptables 规则与 nft 规则各有一套发行版默认 iptables-nft,脚本用了 legacy统一 iptables --version 与 update-alternatives

第 8 条与第 7 条经常一起出现:Pod 被 SIGKILL 后 netns 立即消失,CNI 的 DEL 来不及执行,IPAM 租约留在磁盘上;下一次调度可能复用同一个 IP,而旧租约还指向已删除的容器。生产上要给 CNI 配 GC,或定期清理孤儿租约。

5.6.15 本章出口检查

检查项达标线没达标去哪
画出一个包从容器到宿主的路径说清 veth、bridge、路由、NAT 四段5.6.1
手工搭 netns + veth + bridge 并互通ping 通,清理干净5.6.2
解释 -p 的 DNAT 与 docker-proxy知道规则在 nat 表哪条链5.6.3
说出 CNI 的调用约定环境变量、stdin、stdout5.6.4
解释 VXLAN 的 MTU 减 50会算封装开销5.6.5
读懂 Service 的 DNAT 链KUBE-SERVICES/SVC/SEP5.6.6
把 Pod/Service/Node 映射到 Linux 原语至少六项5.6.7
写一条默认拒绝 + 放行 DNS 的策略语义正确5.6.8
说出 veth/bridge 的函数路径veth_xmit→br_handle_frame→br_forward5.6.9
手工执行 CNI ADD/DEL读到 result.json 的 IP,DEL 后网卡消失5.6.10
区分 iptables/IPVS/eBPF 数据面至少三点5.6.11
在 netns 里复现 ClusterIP DNATcurl VIP 返回 2005.6.11
区分 K8s Namespace 与 Linux namespace一个是 API 对象5.6.12
写对 NetworkPolicy 的 AND/OR同元素 AND、跨元素 OR5.6.12
按层定位 Pod 网络故障至少六步5.6.13
即时自测 · 5.6

Q1 两个容器在同一宿主、同一 bridge 上,IP 分别是 172.17.0.2 与 172.17.0.3。它们互 ping 时经过 NAT 吗?为什么?

Q2 docker run -p 8080:80 之后,宿主上一定会有 docker-proxy 进程吗?什么情况下需要它?

Q3 Pod 的 ClusterIP 是 10.96.0.10,ping 10.96.0.10 不通,但 curl 10.96.0.10:80 成功。这正常吗?为什么?

Q4 一个 Pod 里的两个容器,默认共享哪些 namespace?localhost:8080 能通吗?

Q5 VXLAN 环境下,容器里 ping -s 1472 大包不通,小包正常。最可能的原因与修法是什么?

Q6 CNI 的 DEL 为什么必须传与 ADD 相同的 CNI_CONTAINERID 与配置?只传 netns 路径行不行?

Q7 iptables 模式下 Service 规则数量随什么增长?IPVS 为什么在大规模下更稳?

Q8 shareProcessNamespace: true 之后,Pod 里的 PID 1 是谁?对应用有什么影响?

答案

A1 不经过 NAT。同一 bridge、同一网段,帧直接按 MAC 从 veth 转发;NAT 只在包要离开该网段(出宿主或跨网段路由)时发生。

A2 不一定。纯 DNAT 规则已能处理外部流量;docker-proxy 主要处理本机 127.0.0.1:8080 访问与部分 hairpin 场景。--userland-proxy=false 可以关闭它,此时本机访问依赖 DNAT 的 OUTPUT 链规则。

A3 正常。iptables 模式下 ClusterIP 是虚拟 IP,没有接口,ICMP 不会被 DNAT;TCP 80 会命中 DNAT 规则并被转发到 Pod。用 TCP 连接验证,不要用 ping。

A4 默认共享 network、IPC、UTS namespace(sandbox 持有),不共享 mount 与 PID。因此同一 Pod 内容器可以通过 localhost:8080 互访,因为它们在同一个网络栈里。

A5 VXLAN 封装增加约 50 字节,容器网卡 MTU 若仍是 1500,大包会在 underlay 被丢弃或分片失败。修法是把容器/Pod 网卡 MTU 设为 1450(或按 underlay 调整),并确认 underlay 的 MTU 足够。

A6 CNI 插件通常用 (network, containerID, ifname) 作为缓存键,DEL 靠缓存找到当初创建的 veth 与分配的 IP;只传 netns 路径时,netns 可能已被删除,插件无法反查资源,容易留下孤儿网卡与租约。所以 DEL 要复用 ADD 的环境变量与 stdin 配置。

A7 iptables 模式的规则数随 Service 数 × Endpoint 数线性增长,每次变更还要重写整条链;包路径上要逐条匹配,规模大时延迟与同步时间都会上升。IPVS 把 VIP 映射放进内核哈希表,查找是 O(1),规则数量对包路径影响小;但它仍依赖 conntrack 与部分 iptables 规则做 masquerade。

A8 PID 1 是 pause(sandbox)容器,不是业务应用。业务容器加入同一个 PID namespace 后能看见彼此进程,但不再拥有「PID 1」身份:它不会自动收到内核发给 PID 1 的默认信号处理,也不能依赖「我是 1 号」来收尸。需要信号转发与僵尸回收时,仍要在容器里加 init。

母本对应

母本章节本书落点程度
N第 10 章 容器网络虚拟化(10.2 veth、10.3 netns、10.4 bridge、10.5 外部通信)5.6.1–5.6.3、5.6.9完整(veth/bridge/netns 与外部网络路径)
内核源码drivers/net/veth.c、net/bridge/{br_input,br_fdb,br_forward}.c、net/ipv4/{ip_input,ip_forward,fib_frontend}.c、net/netfilter/nf_conntrack_core.c(Linux 6.8)5.6.9、5.6.13精选(veth_xmit、br_handle_frame、FDB)
CNIspec 1.0/1.1:ADD/DEL/CHECK/GC、结果与错误 JSON5.6.4、5.6.10完整(环境变量、stdin/stdout、IPAM 租约)
CiliumeBPF datapath、kube-proxy replacement、Hubble5.6.11、5.6.12精选(hook 点与策略模型)
N第 5 章 深度理解本机网络、第 6 章 TCP 连接建立5.6.3、5.6.6背景(DNAT/conntrack 与连接跟踪)
W第 4 章 网络层、第 5 章 运输层5.6.1、5.6.3背景(IP 转发、端口与 NAT 的协议基础)
K第 58 章 SOCKET:TCP/IP 网络基础、第 61 章 高级主题5.6.2、5.6.4背景(socket 与 netns 的关系)
OCIruntime-spec(linux.namespaces 的 path 加入方式)5.6.7精选(Pod sandbox 共享 netns 的机制)
Kubernetes官方文档:Pod 网络、Service、NetworkPolicy、CNI、EndpointSlice5.6.4–5.6.13完整(语义以 Kubernetes 1.30+ 为准)

下一站:第 5 篇到此结束。你已经把「隔离、限额、镜像、运行时、网络」串成一条链。接下来去项目 A · 云原生主线:用 mycontainer 从零实现一个迷你运行时,把这六章的命令变成代码。