返回博客列表

Wednesday, February 12, 2025

使用 Rust 添加 iptables 规则实现容器访问 Service 服务

cover

使用 Rust 添加 iptables 规则实现容器访问 Service 服务

在云原生环境中,容器之间的通信是一个常见的需求。本文将介绍如何通过静态编写 iptables 规则来实现不同主机上的容器内相互访问的配置,并模拟 Kubernetes 中的 Kube-proxy 配置 iptables 规则的行为来支持 Service 的访问。

1. 搭建测试环境

首先,我们需要搭建一个测试环境,包括以下组件:

  • test 主机: VMware 虚拟机运行 CentOS 7,两块网卡:ens33(仅主机模式10.15.0.10)、ens36(NAT192.168.129.138),并安装了 Docker 和 Rust 运行环境。
  • service 主机:VMware 虚拟机运行 CentOS 7,两块网卡:ens33(仅主机模式10.15.0.15)、ens36(NAT192.168.129.138),并安装了 Docker 和 tcpdump 工具。
  • test1 容器:运行在 test 主机上,容器镜像:busybox:latest,IP 地址为 172.17.0.2
  • nginx_service1 容器:运行在 service 主机上,容器镜像:nginx:latest,IP 地址为 172.20.0.2,并通过主机的 8080 端口映射到容器的 80 端口提供服务。
  • nginx_service2 容器:运行在 service 主机上,容器镜像:nginx:latest,IP 地址为 172.20.0.3,并通过主机的 1234 端口映射到容器的 80 端口提供服务。

搭建好的测试环境如下图所示:

在进行下一步实验前,我们先来验证一下搭建好的测试环境:

  • test 主机路由规则:根据路由规则可以看出,test 主机可以访问外网,service 主机,以及运行在本主机的容器,我们可以通过 ping 命令来验证
  • Ping Baidu
  • Ping service 主机
  • Ping test1 容器
  • Service 主机上提供的 Nginx 服务:通过访问 Service 主机的 8080 端口,来获取服务
  • test1 容器无法通过 nginx_service1 容器 IP 来获取服务

2. 修改 iptables 规则

为了使得 test1 容器能够通过 172.20.0.2 访问到 Nginx 服务,我们需要在 iptables 中添加 DNAT 规则。具体步骤如下:

  • 在 test 主机的nat表中的PREROUTING链和OUTPUT链中添加 DNAT 规则,将 test1 容器的流量转发到 nginx_service1 容器。
//修改 PREROUTING 链
iptables -t nat -A PREROUTING -d 172.20.0.2 -p tcp --dport 80 -j DNAT --to-destination 10.15.0.15:8080
//修改 OUTPUT 链
iptables -t nat -A OUTPUT -d 172.20.0.2 -p tcp --dport 80 -j DNAT --to-destination 10.15.0.15:8080
  • 查看修改后的 iptables 规则
  • 验证规则是否生效,确保 test1 容器能够成功访问 nginx_service1 以获得 Nginx 服务。 在 service 主机用tcpdump工具抓包可以发现,这些数据包的源 IP 均被 DNAT 为 test 主机的 IP

3. 使用 Rust 编写 iptables 规则

为了自动化 iptables 规则的配置,我们可以使用 Rust 的 iptables crate 来编写规则。以下是一个示例代码片段,展示了如何使用该 crate :

// 创建 iptables 实例
let ipt = iptables::new(false).unwrap();

// 创建自定义链
ipt.new_chain("nat", "KUBE-SERVICES").unwrap();

// 查看链和规则
let chains = ipt.list("filter", "INPUT").unwrap(); 
for chain in chains {
    println!("Rule: {}", chain);
}

// 为链创建规则
ipt.append("filter", "INPUT", "-j ACCEPT");  

4. Kube-proxy 为 Service 配置的 iptables 规则

Kube-proxy 组件负责维护 node 节点上的防火墙规则和路由规则, 在 iptables 模式下,会根据 Service 以及 Endpoints 对象的改变来实时刷新规则, Kube-proxy 使用了 iptables 的 filter 表和 nat 表, 并对 iptables 的链进行了扩充,自定义了 KUBE-SERVICESKUBE-EXTERNAL-SERVICESKUBE-NODEPORTSKUBE-POSTROUTINGKUBE-MARK-MASQKUBE-MARK-DROPKUBE-FORWARD 七条链, 另外还新增了以KUBE-SVC-xxxKUBE-SEP-xxx开头的数个链, 除了创建自定义的链以外还将自定义链插入到已有链的后面以便劫持数据包。

这些自定义链及原本的 iptables 规则结合后如下图所示

Kube-proxy 为nat表创建的 iptables 规则
-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A OUTPUT -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-A POSTROUTING -m comment --comment "kubernetes postrouting rules" -j KUBE-POSTROUTING
-A KUBE-EXT-2CMXP7HKUVJN7L6M -m comment --comment "masquerade traffic for default/nginx external destinations" -j KUBE-MARK-MASQ
-A KUBE-EXT-2CMXP7HKUVJN7L6M -j KUBE-SVC-2CMXP7HKUVJN7L6M
-A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/nginx:" -m tcp --dport 30964 -j KUBE-EXT-2CMXP7HKUVJN7L6M
-A KUBE-POSTROUTING -m mark ! --mark 0x4000/0x4000 -j RETURN
-A KUBE-POSTROUTING -j MARK --set-xmark 0x4000/0x0
-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -j MASQUERADE
-A KUBE-SEP-IUO72NDO5O3SEP2S -s 172.20.0.2/32 -m comment --comment "default/nginx" -j KUBE-MARK-MASQ
-A KUBE-SEP-IUO72NDO5O3SEP2S -p tcp -m comment --comment "default/nginx" -m tcp -j DNAT --to-destination 10.15.0.15:8080
-A KUBE-SEP-MPFVDV4R4PYEEVND -s 172.20.0.3/32 -m comment --comment "default/nginx" -j KUBE-MARK-MASQ
-A KUBE-SEP-MPFVDV4R4PYEEVND -p tcp -m comment --comment "default/nginx" -m tcp -j DNAT --to-destination 10.15.0.15:1234
-A KUBE-SERVICES -d 172.172.0.2/32 -p tcp -m comment --comment "default/nginx:cluster Ip" -m tcp --dport 80 -j KUBE-SVC-2CMXP7HKUVJN7L6M
-A KUBE-SERVICES -m comment --comment "kubernetes service nodeports; NOTE: this must be the last rule in this chain" -m addrtype --dst-type LOCAL -j KUBE-NODEPORTS
-A KUBE-SVC-2CMXP7HKUVJN7L6M ! -s 172.0.0.0/8 -d 172.172.0.2/32 -p tcp -m comment --comment "default/nginx cluster IP" -m tcp --dport 80 -j KUBE-MARK-MASQ
-A KUBE-SVC-2CMXP7HKUVJN7L6M -m comment --comment "default/nginx -> 10.15.0.15:8080" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-IUO72NDO5O3SEP2S
-A KUBE-SVC-2CMXP7HKUVJN7L6M -m comment --comment "default/nginx -> 10.15.0.15:1234" -j KUBE-SEP-MPFVDV4R4PYEEVND

5. 在测试环境中模拟 Kubernetes 的 Service

模拟过程:通过 Rust 程序配置 test 主机的 iptables 的规则,使得在 test 主机上运行的 test1 容器能用 Cluster IPNotePort两种方式访问运行在 service 主机上的 Nginx 服务

  • 第一步:NodePort 类型服务配置:通过yml文件给出我们要模拟的nginx service, 根据配置我们可以通过 Cluster IP :172.172.0.2和 NodePort :30964两种方式访问该服务
apiVersion: v1
kind: Service
metadata:
  name: nginx
spec:
  clusterIP: 172.172.0.2
  selector:
    app: nginx
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
      nodePort: 30964
  type: NodePort
  • 第二步:后端服务 Pod 模拟:通过 Docker 运行两个nginx容器来模拟提供该服务, 并通过 iptables 的random模块来实现负载平衡
  • 第三步:运用库编写 Rust 程序:注意需要在Cargo.toml中添加依赖iptables = "*"
点击查看完整的代码
extern crate iptables;
fn main() {
  let ipt = iptables::new(false).unwrap();

  // 创建自定义链
     //固定链
  ipt.new_chain("nat", "KUBE-SERVICES").unwrap();
  ipt.new_chain("nat", "KUBE-MARK-MASQ").unwrap();
  ipt.new_chain("nat", "KUBE-NODEPORTS").unwrap();
  ipt.new_chain("nat", "KUBE-POSTROUTING").unwrap();
     //为每个服务创建链
  ipt.new_chain("nat", "KUBE-SVC-2CMXP7HKUVJN7L6M").unwrap();
  ipt.new_chain("nat", "KUBE-SEP-IUO72NDO5O3SEP2S").unwrap();
  ipt.new_chain("nat", "KUBE-SEP-MPFVDV4R4PYEEVND").unwrap();
  ipt.new_chain("nat", "KUBE-EXT-2CMXP7HKUVJN7L6M").unwrap();

  // 添加 PREROUTING 规则
  ipt.append(
      "nat",
      "PREROUTING",
      "-m comment --comment \"kubernetes service portals\" -j KUBE-SERVICES",
  )
  .unwrap();

  // 添加 OUTPUT 规则
  ipt.append(
      "nat",
      "OUTPUT",
      "-m comment --comment \"kubernetes service portals\" -j KUBE-SERVICES",
  )
  .unwrap();

  // 添加 POSTROUTING 规则
  ipt.append(
      "nat",
      "POSTROUTING",
      "-m comment --comment \"kubernetes postrouting rules\" -j KUBE-POSTROUTING",
  )
  .unwrap();

  // KUBE-POSTROUTING添加规则:RETURN 当流量的 mark 不为 0x4000 时直接返回
  ipt.append(
      "nat", 
      "KUBE-POSTROUTING", 
      "-j RETURN -m mark ! --mark 0x4000/0x4000"
  )
  .unwrap();

  // KUBE-POSTROUTING添加规则:MARK 使用 xor 操作修改 mark
  ipt.append(
      "nat", 
      "KUBE-POSTROUTING", 
      "-j MARK --xor-mark 0x4000"
  )
  .unwrap();

  // KUBE-POSTROUTING添加规则:MASQUERADE 对 Kubernetes 服务流量进行 SNAT
  ipt.append(
      "nat", 
      "KUBE-POSTROUTING",
      "-j MASQUERADE -m comment --comment \"kubernetes service traffic requiring SNAT\""
  )
  .unwrap();

  // 添加 KUBE-MARK-MASQ 规则
  ipt.append(
      "nat",
      "KUBE-MARK-MASQ",
      "-j MARK --or-mark 0x4000",
  )
  .unwrap();

  // 添加 KUBE-NODEPORTS 规则
  ipt.append(
      "nat",
      "KUBE-NODEPORTS",
      "-p tcp -m comment --comment \"default/nginx:\" -m tcp --dport 30964 -j KUBE-EXT-2CMXP7HKUVJN7L6M",
  )
  .unwrap();

  // 添加 KUBE-SERVICES 链中的规则 
  //cluster ip
  ipt.append(
      "nat",
      "KUBE-SERVICES",
      "-d 172.172.0.2/32 -p tcp -m comment --comment \"default/nginx:cluster Ip\" -m tcp --dport 80 -j KUBE-SVC-2CMXP7HKUVJN7L6M",
  )
  .unwrap();
  //node port
  ipt.append(
      "nat",
      "KUBE-SERVICES",
      "-m comment --comment \"kubernetes service nodeports; NOTE: this must be the last rule in this chain\" -m addrtype --dst-type LOCAL -j KUBE-NODEPORTS",
  )
  .unwrap();

  // 添加 KUBE-SVC-2CMXP7HKUVJN7L6M 链中的规则用于MARK-MASQ
  ipt.append(
      "nat",
      "KUBE-SVC-2CMXP7HKUVJN7L6M",
      "-m comment --comment \"default/nginx cluster IP\" -j KUBE-MARK-MASQ -p tcp ! --source 172.0.0.0/8 --destination 172.172.0.2 --dport 80 ",
  )
  .unwrap();
  // 添加带有随机概率的规则 KUBE-SVC-2CMXP7HKUVJN7L6M 链中的规则
  ipt.append(
      "nat",
      "KUBE-SVC-2CMXP7HKUVJN7L6M",
      "-m comment --comment \"default/nginx -> 10.15.0.15:8080\" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-IUO72NDO5O3SEP2S",
  )
  .unwrap();

  // 跳转到另一个服务端点
  ipt.append(
      "nat",
      "KUBE-SVC-2CMXP7HKUVJN7L6M",
      "-m comment --comment \"default/nginx -> 10.15.0.15:1234\" -j KUBE-SEP-MPFVDV4R4PYEEVND",
  )
  .unwrap();

  // 添加 KUBE-SEP-IUO72NDO5O3SEP2S 链中的规则
  ipt.append(
      "nat",
      "KUBE-SEP-IUO72NDO5O3SEP2S",
      " -m comment --comment \"default/nginx\" -s 172.20.0.2/32 -j KUBE-MARK-MASQ",
  )
  .unwrap();

  // 添加 KUBE-SEP-IUO72NDO5O3SEP2S 链中的 DNAT 规则
  ipt.append(
      "nat",
      "KUBE-SEP-IUO72NDO5O3SEP2S",
      "-p tcp -m comment --comment \"default/nginx\" -m tcp -j DNAT --to-destination 10.15.0.15:8080",
  )
  .unwrap();

  // 添加 KUBE-SEP-MPFVDV4R4PYEEVND 链中的规则
  ipt.append(
      "nat",
      "KUBE-SEP-MPFVDV4R4PYEEVND",
      " -m comment --comment \"default/nginx\" -s 172.20.0.3/32 -j KUBE-MARK-MASQ",
  )
  .unwrap();

  // 添加 KUBE-SEP-MPFVDV4R4PYEEVND 链中的 DNAT 规则
  ipt.append(
      "nat",
      "KUBE-SEP-MPFVDV4R4PYEEVND",
      "-p tcp -m comment --comment \"default/nginx\" -m tcp -j DNAT --to-destination 10.15.0.15:1234",
  )
  .unwrap();
  
  // 添加 KUBE-EXT-2CMXP7HKUVJN7L6M 链中的规则
  ipt.append(
      "nat",
      "KUBE-EXT-2CMXP7HKUVJN7L6M",
      " -m comment --comment \"masquerade traffic for default/nginx external destinations\" -j KUBE-MARK-MASQ",
  )
  .unwrap();

  // 添加 KUBE-EXT-2CMXP7HKUVJN7L6M 链中的 DNAT 规则
  ipt.append(
      "nat",
      "KUBE-EXT-2CMXP7HKUVJN7L6M",
      "-j KUBE-SVC-2CMXP7HKUVJN7L6M",
  )
  .unwrap();
  
  println!("All rules and chains added successfully.");
}
  • 第四步:运行 Rust 程序配置 iptables 规则:查看 test 主机修改后的 iptables 规则,部分如图
  • 第五步:测试 Cluster IP :在 test1 容器内使用wget工具访问 Cluster IP 获取服务
  • 第六步:测试 NodePort :在 test1 容器内使用wget工具访问 NotePort 获取服务
  • 第七步:验证负载平衡:通过多次请求,验证流量是否被均匀地分发到多个后端容器中 (此处可以通过 tcpdump 分别监听 service 主机的 8080 和 1234 端口,并在 test1 容器内多次发送请求来验证, 感兴趣的读者可以自行验证)

总结

通过静态编写 iptables 规则,我们可以实现容器内访问 Service 的配置,并模拟 Kubernetes 中的 Kube-proxy 行为。 这种方法不仅适用于简单的测试环境,也可以帮助我们更好地理解 KubernetesService 的实现机制。