Linux 网络虚拟化深度解析:从 veth 设备对到容器网络实战
第一部分veth 设备对 —— 虚拟世界的 网线1.1 什么是 veth 设备对vethVirtual Ethernet设备对可以理解为软件模拟的一对 虚拟网卡它们总是成对出现就像用一根虚拟的 网线 把两个网络接口连在一起。物理世界类比想象两台电脑用一根网线直连它们的网卡数据就能互相传输。虚拟世界实现veth pair 就是软件实现的这种 直连网线一端叫 veth0另一端叫 veth1数据从 veth0 发出必然从 veth1 收到反之亦然。它和本机的 lo回环设备不同lo 是 自己发给自己而 veth 是 一端发给另一端是跨命名空间或跨容器通信的基础。1.2 如何创建和配置 veth 设备对在 Linux 系统中你可以通过 ip 命令来创建和管理 veth 设备对。创建 veth 对ip link add veth0 type veth peer name veth1这条命令会创建一对虚拟设备veth0 和 veth1。它们是 对等 的任何一端发出的数据包都会被另一端接收。查看设备ip link show你会看到类似这样的输出5: veth0veth1: ... 6: veth1veth0: ...这里的 符号表示它们是配对的。配置 IP 地址veth 设备需要配置 IP 才能通信ip addr add 192.168.1.1/24 dev veth0 ip addr add 192.168.1.2/24 dev veth1启动设备ip link set veth0 up ip link set veth1 up启动后你可以用 ifconfig 或 ip addr show 查看设备状态确认它们处于 UP 和 RUNNING 状态。1.3 如何让 veth 对之间通信即使配置了 IP 并启动了设备它们之间可能还无法通信因为 Linux 内核默认启用了反向路径过滤rp_filter它会检查数据包的源 IP 是否 合理如果不合理就丢弃。关闭 rp_filterecho 0 /proc/sys/net/ipv4/conf/all/rp_filter echo 0 /proc/sys/net/ipv4/conf/veth0/rp_filter echo 0 /proc/sys/net/ipv4/conf/veth1/rp_filter开启 accept_local为了让设备能接收发往本机 IP 的数据包还需要开启 accept_localecho 1 /proc/sys/net/ipv4/conf/veth0/accept_local echo 1 /proc/sys/net/ipv4/conf/veth1/accept_local完成以上配置后你就可以在 veth0 上 ping veth1 了ping 192.168.1.2 -I veth0你会看到成功的 ping 响应证明 veth 对之间已经可以正常通信。1.4 veth 设备的底层创建过程veth 设备的创建和管理是由 Linux 内核的网络子系统负责的其核心代码位于drivers/net/veth.c。初始化内核模块加载时会调用 veth_init () 函数注册 veth 设备的操作接口static __init int veth_init(void) { return rtnl_link_register(veth_link_ops); }创建设备对当你执行ip link add ... type veth ...时内核会调用 veth_newlink () 函数创建对端设备通过rtnl_create_link()创建 peer 设备比如 veth1。注册设备调用register_netdevice()将 veth0 和 veth1 注册到内核网络设备列表中。建立关联通过netdev_priv()获取设备的私有数据结构 veth_priv并用rcu_assign_pointer()将两个设备的 peer 指针互相指向对方形成 对。struct veth_priv { struct net_device __rcu *peer; atomic64_t dropped; };这样veth0 的 peer 指向 veth1veth1 的 peer 指向 veth0数据包就能在它们之间 穿越。veth 设备的操作函数veth 设备的行为由其操作函数集 veth_netdev_ops 定义其中最关键的是发送函数 ndo_start_xmit它被设置为 veth_xmitstatic const struct net_device_ops veth_netdev_ops { .ndo_init veth_dev_init, .ndo_open veth_open, .ndo_stop veth_close, .ndo_start_xmit veth_xmit, // 数据包发送函数 .ndo_change_mtu veth_change_mtu, ... };当数据从 veth0 发出时内核会调用 veth_xmit ()这个函数会查找 veth0 的 peer即 veth1然后将数据包 转发 给 veth1 的接收队列完成 虚拟网线 的数据传递。1.5 veth 设备的数据传输原理veth 其实是一个 管道。它和日常接触的 lo回环设备非常像只不过 veth 多了个结对的概念。lo 设备自己发给自己数据包在内核里转一圈回到自己。veth 设备A 发给 B。A 是 veth0B 是 veth1。在代码层面veth 的发送函数 veth_xmit 做的事情非常简单它根本不走物理网线也不走复杂的协议栈处理而是直接把数据包 扔 给它的 兄弟peer。发送过程详解veth_xmit当你在 veth0 上发送数据比如 ping 包时内核网络栈会调用 veth 设备的发送函数veth_xmit。第一步找到 兄弟// 获取 veth 设备的对端 struct veth_priv *priv netdev_priv(dev); struct net_device *rcv; rcv rcu_dereference(priv-peer);dev 是当前发送数据的设备比如 veth0。priv-peer 就是 veth0 的 另一半veth1。第二步把包扔过去if (likely(dev_forward_skb(rcv, skb) NET_RX_SUCCESS)) { // 发送成功 }这里调用了 dev_forward_skb。这个函数的作用就是把数据包skb转发给接收端设备rcv即 veth1。注意这里并没有真正把数据发到物理网卡上而是在内存中把数据包 移交 了。移交过程详解dev_forward_skbdev_forward_skb 做了两件事修改归属重新设置 skb 的协议类型和所属设备skb-dev 变成了接收端 veth1。触发接收return netif_rx(skb);这是最关键的一步。netif_rx 是 Linux 网络设备层接收数据包的标准入口。对于物理网卡数据是硬件中断来了之后调用这个函数。对于 veth它是直接在软件里调用这个函数假装是 硬件收到了数据。接收过程详解软中断与队列既然调用了 netif_rx接下来的流程就和物理网卡收到数据一模一样了。入队enqueue_to_backlog数据包skb被放入了 CPU 的 输入队列input_pkt_queue。这就好比把信件扔进了 veth1 的 信箱 里。触发软中断__raise_softirq_irqoff(NET_RX_SOFTIRQ);系统触发了一个软中断SoftIRQ告诉内核嘿veth1 收到数据了快来处理注意这里是软中断不是硬件中断。因为全是软件模拟的效率非常高。处理接收Pollnet_rx_action() |-- process_backlog() |-- __netif_receive_skb() |-- deliver_skb (送到协议栈比如 IP 层)内核的软中断处理函数 net_rx_action 会被调度执行。它会从队列里把刚才放进去的包拿出来。然后一层层往上送经过 IP 层、TCP/UDP 层最终到达应用程序或者如果是 ping 包就由 ICMP 协议处理并回包。1.6 为什么 veth 对如此重要veth pair 是 Docker、Kubernetes 等容器技术实现网络隔离和通信的核心机制容器网络每个容器都有自己的网络命名空间容器内的 eth0 实际上就是 veth pair 的一端另一端在宿主机上连接到网桥bridge或路由表从而实现容器与宿主机、容器与外部网络的通信。网络命名空间通信不同 network namespace 之间无法直接通信veth pair 就是连接它们的 桥梁。第二部分网络命名空间 —— 隔离的基石2.1 什么是网络命名空间默认情况下所有的进程包括 Docker 容器里的进程都在一个叫 host net 的默认命名空间里。大家共用一张路由表、共用所有的网卡eth0, lo 等、共用 iptables。当你创建一个新的网络命名空间比如叫 net1你就相当于凭空变出了一套全新的、独立的网络协议栈独立的网卡在这个空间里你看不到宿主机的 eth0除非特意把它放进去。独立的 IP你可以给这个空间配一个和宿主机完全不同的 IP 段。独立的规则这个空间里的 iptables 规则和宿主机互不干扰。2.2 内核实现原理数据结构关联每个进程task_struct都有一个指针指向它的命名空间nsproxy。nsproxy 里有一个指针指向 struct net。关键点struct net 这个结构体里包含了该空间独享的路由表、iptables、甚至独享的回环设备loopback_dev。这就是为什么你在容器里执行 ifconfig 也能看到 lo 设备的原因 —— 那是它自己独有的 lo不是宿主机的。默认归属所有进程的 task_struct 结构体中都有一个成员叫 nsproxy命名空间代理。默认情况下大家都指向同一个全局变量init_net初始网络命名空间。这意味着大家共用一套路由表、iptables、网卡设备。隔离状态当进程调用 clone 系统调用并带上 CLONE_NEWNET 标志位时内核会为进程分配一个新的 struct net 对象。进程的 nsproxy 指针指向这个新对象。结果进程拥有了独立的网络设备、路由表和 iptables与其他进程彻底隔离。2.3 创建命名空间的内核流程系统的起点init 进程与 init_netLinux 系统的 0 号 / 1 号进程init 进程的初始化代码// file: init/init_task.c struct task_struct init_task INIT_TASK(init_task); // file: include/linux/init_task.h #define INIT_TASK(tsk) \ { \ ... .nsproxy init_nsproxy, \ ... }这行代码硬编码了 init 进程使用初始的命名空间代理。// file: kernel/nsproxy.c struct nsproxy init_nsproxy { ... .net_ns init_net, };init_nsproxy 结构体里.net_ns 指针指向了 init_net。// file: net/core/net_namespace.c struct net init_net { ... }; // 定义了初始网络命名空间init_net 是全局变量代表宿主机原本的那个网络环境。创建新命名空间copy_net_ns当我们在用户态执行ip netns add xxx或者 Docker 启动时底层会调用 clone 系统调用最终进入内核的 copy_net_ns 函数// file: net/core/net_namespace.c struct net *copy_net_ns(unsigned long flags, struct user_namespace *user_ns, struct net *old_net) { struct net *net; // 1. 检查标志位 if (!(flags CLONE_NEWNET)) return get_net(old_net); // 如果没带 CLONE_NEWNET 标志直接增加引用计数 // 2. 申请新空间 net net_alloc(); // 分配一个新的 struct net 内存 // 3. 初始化新空间 rv setup_net(net, user_ns); ... }解析判断标志位如果创建进程时没说要隔离网络CLONE_NEWNET那就直接复用老的old_net。net_alloc()给新容器申请了一个 空房间内存空间。setup_net()最关键的一步相当于给这个 空房间 进行 装修配置家具路由表、iptables 等。插件化机制pernet_operations内核网络功能非常复杂不可能把所有初始化代码都写在 setup_net 里。Linux 采用了 注册回调 的设计模式。// file: include/net/net_namespace.h struct pernet_operations { struct list_head list; // 链表节点 int (*init)(struct net *net); // 初始化函数指针 void (*exit)(struct net *net); // 退出函数指针 ... };定义了一个标准接口。每个网络子系统如路由、iptables、网设备都要遵循这个接口。// file: net/core/net_namespace.c static struct list_head *first_device pernet_list; int register_pernet_subsys(struct pernet_operations *ops) { error register_pernet_operations(first_device, ops); ... }这是一个注册函数。比如路由模块启动时会调用这个函数把自己的初始化函数init注册到全局链表 pernet_list 上。触发初始化setup_net 遍历链表回到创建命名空间时的 setup_net 函数// file: net/core/net_namespace.c static __net_init int setup_net(struct net *net, struct user_namespace *user_ns) { const struct pernet_operations *ops; list_for_each_entry(ops, pernet_list, list) { error ops_init(ops, net); } }list_for_each_entry这是一个宏用来遍历 pernet_list 链表。ops_init(ops, net)遍历到每一个子系统时调用它的 init 函数并把刚才申请的新 net 结构体传进去。实例路由表与 iptables 的初始化案例 A路由表 (FIB)// file: net/ipv4/fib_frontend.c static struct pernet_operations fib_net_ops { .init fib_net_init, .exit fib_net_exit, }; void __init ip_fib_init(void) { register_pernet_subsys(fib_net_ops); }逻辑系统启动时ip_fib_init 被调用把 fib_net_ops 注册到全局链表。当创建新命名空间时setup_net 遍历链表找到了 fib_net_ops。调用 fib_net_init (net)。结果新的命名空间里生成了一套独立的路由表案例 Biptables NAT 表// file: net/ipv4/netfilter/iptable_nat.c static struct pernet_operations iptable_nat_net_ops { .init iptable_nat_net_init, .exit iptable_nat_net_exit, };同理当创建新命名空间时iptable_nat_net_init 被调用为新空间分配独立的 NAT 规则表。2.4 网卡的归属与迁移默认归属当一个网卡设备比如 veth刚被创建出来时它默认是属于默认网络命名空间即 init_net也就是宿主机的。//file: core/dev.c struct net_device *alloc_netdev_mqs(...) { // 关键行创建时默认把设备的 nd_net 指针指向 init_net dev_net_set(dev, init_net); }struct net_device 是内核描述网卡的结构体。它里面有一个成员 nd_net用来记录这个网卡属于哪个命名空间。刚出生时它就被强制指向了全局的 init_net。动态迁移既然默认在宿主机那怎么给容器用呢答案是 搬家。//file: include/linux/netdevice.h void dev_net_set(struct net_device *dev, struct net *net) { release_net(dev-nd_net); // 1. 减少旧命名空间的引用计数 dev-nd_net hold_net(net); // 2. 把指针指向新的命名空间并增加新空间的引用计数 }这就是 Docker 的核心操作在宿主机创建 veth 对都在宿主机。把其中一端如 veth1通过 dev_net_set 操作扔 进容器的命名空间。从此宿主机看不到 veth1只有容器能看到。2.5 Socket 的归属当你在容器里运行 Nginx 监听 80 端口时内核怎么知道这是容器里的 80而不是宿主机的 80核心原理Socket 继承自创建它的进程。进程task_struct手里拿着命名空间的门票nsproxy。当进程创建 Socket 时内核会顺手把这张门票复印一份贴在 Socket 上。// 进程创建 socket 的核心函数 int sock_create(...) { // 关键行获取当前进程的命名空间 current-nsproxy-net_ns // 并传给底层创建函数 return __sock_create(current-nsproxy-net_ns, family, type, protocol, res, 0); } // 底层赋值函数 static inline void sock_net_set(struct sock *sk, struct net *net) { write_pnet(sk-sk_net, net); // 把命名空间指针写入 socket 结构体 }sk 是内核中描述 socket 的结构体。sk-sk_net 是 socket 里的一个指针。这一连串调用确保了谁生的孩子像谁。宿主机进程生的 Socket 归宿主机管容器进程生的 Socket 归容器管。2.6 路由查找的真相当数据包发出去时内核是怎么查到容器自己的路由表而不是宿主机的路由表核心逻辑以前没有命名空间时路由查找函数是全局的。现在路由查找函数多了一个参数struct net *net。代码追踪发送数据调用 ip_queue_xmit。获取上下文// 从 socket 中取出当初贴上去的命名空间标签 sock_net(sk)查找路由// 带着标签去查路由 rt ip_route_output_ports(sock_net(sk), ...);最终落地static inline struct fib_table *fib_get_table(struct net *net, u32 id) { // 关键点net-ipv4.fib_table_hash // 不是查全局变量而是查 net 结构体里的成员变量 ptr id RT_TABLE_LOCAL ? net-ipv4.fib_table_hash[...] : net-ipv4.fib_table_hash[...]; ... }这就好比查字典旧模式全办公室只有一本字典全局路由表大家抢着用。新模式每个人桌子上都有一本字典struct net 里的路由表。查字典时先看你是哪个部门的sock_net (sk)然后直接拿你桌子上的那本查。2.7 所谓的 虚拟化 到底是什么Linux 的网络命名空间实现了多个独立协议栈 这个说法其实不是很准确。真实情况代码只有一套内核里的网络代码TCP/IP 协议栈的实现逻辑只有一份没有复制。数据被隔离了所谓的 隔离仅仅是把全局变量如路由表、iptables 规则、设备列表打包进一个结构体 struct net。指针的魔法每个进程指向一个 struct net。每个网卡指向一个 struct net。每个 Socket 指向一个 struct net。一句话总结网络命名空间不是 克隆了多套内核网络功能而是通过 struct net 结构体做了一层逻辑隔离让不同的进程以为自己独享了整套网络环境。第三部分Bridge 网桥 —— 虚拟交换机3.1 为什么需要 Bridge前面的章节讲了 隔离Namespace但隔离之后容器就成了一个个孤岛无法互相通信。Bridge网桥就是为了解决这个问题而生的。物理世界类比在机房里如果要把几十台服务器连起来我们不会把它们用网线两两互联而是把它们都插在一台交换机上。虚拟世界实现Linux Bridge 就是一个软件交换机它有很多 插口端口可以把多根 veth 网线插进来。3.2 搭建 Bridge 的基本步骤创建交换机brctl addbr br0这行命令在宿主机上虚拟出了一台交换机名字叫 br0。此时它还是悬空的没连任何设备。插网线ip link set dev veth1_p master br0 ip link set dev veth2_p master br0这两行命令把原本孤立的 veth1_p 和 veth2_p 都 挂载 到了 br0 上。这就相当于把两根网线插进了交换机的插口。配置网关 IPip addr add 192.168.0.100/24 dev br0给交换机配置一个 IP。这个 IP 通常作为容器的网关。激活设备ip link set br0 up ip link set veth1_p up ip link set veth2_p up把网卡和交换机都启动UP 状态电路才算真正接通。3.3 Bridge 的内核 真身11 结构在用户态看Bridge 就是一个叫 br0 的设备。但在内核态一个 Bridge 其实是由两个相邻存储的内核对象组成的struct net_device这是 面子。因为 Bridge 在 Linux 眼里首先得是一个网络设备它得有名字、MAC 地址、状态UP/DOWN能被 ifconfig 看到。struct net_bridge这是 里子。这是专门给 Bridge 用的控制结构里面存着转发表MAC 地址表、端口列表等交换机特有的数据。代码解析br_add_bridgealloc_netdev(sizeof(struct net_bridge), ...)这个调用非常精妙。它一次性申请了一块大内存前半部分放 net_device后半部分紧挨着放 net_bridge。这样设计是为了内存访问的局部性提高效率。3.4 Bridge 的诞生从申请到注册当你执行brctl addbr br0时内核走了这几步申请内存调用 alloc_netdev_mqs。注意这里传入了 br_dev_setup 函数指针。初始化alloc_netdev 内部会调用 br_dev_setup。这个函数会初始化刚才申请的 net_device设置名字、MTU和 net_bridge初始化自旋锁、端口列表。注册调用 register_netdev (dev)。这一步把 Bridge 正式注册到内核网络子系统中这时候你在系统里就能看到 br0 了。3.5 核心机制Hook 机制拦截数据包这是理解 Bridge 工作原理最关键的一点。当执行brctl addif br0 veth1_p时不仅仅是把 veth 挂到了 Bridge 的列表里更重要的是修改了 veth 的行为。netdev_rx_handler_register(dev, br_handle_frame, p);这行代码给 veth1_p 安装了一个 拦截器。正常情况网卡收到包 - 交给协议栈IP 层 / TCP 层 - 给应用程序。加入 Bridge 后网卡收到包 - 被 br_handle_frame 拦截 - 交给 Bridge 处理转发 - 不再往上传给协议栈除非是发给 Bridge 自身 IP 的包。结论加入 Bridge 的网卡实际上 退化 成了一个纯粹的交换机端口它不再处理 IP 层逻辑只负责收发数据帧。3.6 数据包转发全流程一个数据包从 Docker1 到 Docker2 的完整旅程┌─────────────────────────────────────────────────────────────────┐ │ 数据包转发流程 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────┐ │ │ │ Docker1 │ │ │ │ veth1 │ ────┐ │ │ │192.168. │ │ │ │ │ 0.101 │ │ Step 1: 发包 │ │ └─────────┘ │ Docker1里的进程发送数据 │ │ │ 数据包通过容器内的veth1发出 │ │ ▼ │ │ ┌───────────┐ │ │ │ veth1_p │ │ │ │ (宿主机端) │ ────┐ │ │ └───────────┘ │ │ │ │ Step 2: 对端接收与拦截 │ │ │ 宿主机的veth1_p收到数据 │ │ │ 因为注册了rx_handler │ │ │ 内核调用br_handle_frame │ │ ▼ │ │ ┌───────────┐ │ │ │ br0 │ │ │ │ (Bridge) │ │ │ └───────────┘ │ │ │ │ │ │ Step 3: Bridge查表转发 │ │ │ 学习: MAC_A在veth1_p端口 │ │ │ 查找: 目标MAC_B在veth2_p端口 │ │ │ 改写: skb-dev改为veth2_p │ │ ▼ │ │ ┌───────────┐ │ │ │ veth2_p │ │ │ │ (宿主机端) │ ────┐ │ │ └───────────┘ │ │ │ │ Step 4: 发往下一个端口 │ │ │ 调用dev_queue_xmit │ │ │ 把包发给veth2_p │ │ ▼ │ │ ┌─────────┐ │ │ │ Docker2 │ │ │ │ veth2 │ │ │ │192.168. │ │ Step 5: 进入目标容器 │ │ │ 0.102 │ │ veth2_p发送的数据瞬间出现在veth2上 │ │ └─────────┘ │ Docker2的veth2收到包 │ │ │ 上传给协议栈最终被应用程序接收 │ │ ▼ │ │ [完成] │ │ │ └─────────────────────────────────────────────────────────────────┘详细步骤解析步骤 1发包Docker1 里的进程发送数据。数据包通过容器内的 veth1 发出。步骤 2对端接收与拦截宿主机的 veth1_p 收到数据。关键转折因为 veth1_p 之前注册了 rx_handler内核发现它属于某个 Bridge于是调用 br_handle_frame。步骤 3Bridge 查表转发br_handle_frame - br_handle_frame_finish。学习Bridge 记录 哦MAC_A 在 veth1_p 这个端口。查找Bridge 查表发现目标 MAC_B 在 veth2_p 端口。改写修改 skb-dev把目标设备从 veth1_p 改为 veth2_p。步骤 4发往下一个端口调用 dev_queue_xmit 把包发给 veth2_p。veth2_p 发送数据。步骤 5进入目标容器因为 veth 是成对的veth2_p 发送的数据会瞬间出现在 veth2 上。Docker2 里的 veth2 收到包上传给协议栈最终被 Docker2 的应用程序接收。3.7 Bridge 的核心价值结构上Bridge 通用网卡设备 专用网桥控制块。机制上Bridge 不是主动去拉数据而是通过 Hook钩子机制在网卡收到数据的第一时间进行拦截。流程上数据包在宿主机内部走的是 veth - Bridge Hook - veth 的路径完全在内核态完成不经过物理网卡也不经过复杂的 IP 路由所以效率非常高。这就是为什么 Docker 容器间通信速度极快的原因。第四部分容器网络实战 —— 从孤岛到互联4.1 实战目标通过一个 纯手工打造 Docker 网络 的实战案例把 Network Namespace、veth pair、Bridge、路由、NAT、iptables 这些核心概念串联起来。核心目标理解容器Container是如何实现网络隔离又是如何与外部世界通信的。4.2 第一阶段搭建 集装箱—— 网络隔离与连接这一阶段的目标是创建一个隔离的网络环境就像给应用造了一个独立的房间。1. 创建 Network Namespaceip netns add net1创建一个隔离的网络空间 net1。在这个空间里有自己的网卡、路由表别人看不到它它也看不到外面。这模拟了 Docker 容器的隔离性。2. 创建 veth pairip link add veth1 type veth peer name veth1_p创建一对虚拟网线。veth1 插在 net1 房间里veth1_p 留在宿主机的大厅里。数据可以通过这根网线在 房间 和 大厅 之间传输。3. 将 veth1 移动到命名空间ip link set veth1 netns net1把 veth1 这头 拔 下来插到了 net1 这个命名空间里。宿主机上只能看到 veth1_p 了veth1消失 了其实是搬家了。4. 创建 Bridgebrctl addbr br0创建一个虚拟交换机 br0。把 veth1_p 插在交换机上。ip link set dev veth1_p master br05. 配置 IP# 进入命名空间配置IP ip netns exec net1 ip addr add 192.168.0.2/24 dev veth1 ip netns exec net1 ip link set veth1 up # 给br0配置网关IP ip addr add 192.168.0.1/24 dev br0 ip link set br0 up ip link set veth1_p up现状此时net1 里是个孤岛。虽然物理连接都通了但它不知道怎么去外面的世界。4.3 第二阶段走出孤岛 —— 路由与转发这一阶段解决 容器访问外网 的问题。遇到的第一个坑路由缺失现象在 net1 里 ping 外部 IP提示 Network is unreachable。原因net1 的路由表里只有 去 192.168.0.x 网段走 veth1 的规则它不知道去其他网段该走哪里。解决添加默认路由ip netns exec net1 ip route add default gw 192.168.0.1 veth1告诉 net1所有不知道去哪的包都扔给网关 192.168.0.1也就是宿主机的 br0。遇到的第二个坑转发未开启现象加了路由还是不通。原因宿主机默认不开启 IP 转发功能它收到包后不知道要转发出去而是直接丢弃。解决开启 IP 转发sysctl net.ipv4.conf.all.forwarding1打开宿主机的 路由器模式。遇到的第三个坑NATSNAT/MASQUERADE现象包发出去了但外网机器不回消息。原因外网机器收到包发现源 IP 是 192.168.0.2私有 IP它根本不认识这个网段不知道怎么回包或者路由器直接就把私有 IP 的包过滤了。终极解决SNAT源地址转换iptables -t nat -A POSTROUTING -s 192.168.0.0/24 -o eth0 -j MASQUERADE原理当包从 br0 走向 eth0物理网卡准备发往外网时iptables 把包的源 IP 从 192.168.0.2 改写成宿主机的 IP比如 10.162.x.x。外网机器收到包以为是宿主机发的回包给宿主机。宿主机收到回包后再把 IP 改回 192.168.0.2 发给容器。这就通了4.4 第三阶段请君入瓮 —— 端口映射这一阶段解决 外网访问容器 的问题比如访问容器里的 Web 服务。需求外网想访问容器 net1 里的 80 端口。难点外网只知道宿主机的 IP不知道怎么找到容器。解决方案DNAT目的地址转换iptables -t nat -A PREROUTING -p tcp --dport 8088 -j DNAT --to-destination 192.168.0.2:80原理外网访问 宿主机 IP:8088。数据包刚到宿主机 eth0在路由判断之前PREROUTING 链iptables 拦截了它。iptables 把包的目的 IP 从 宿主机 IP:8088 修改为 192.168.0.2:80。宿主机根据路由表把这个包转发给 br0进而通过 veth 传给容器。效果这就实现了 Docker 的-p 8088:80端口映射功能。4.5 完整的网络拓扑图┌─────────────────────────────────────────────────────────────────────────┐ │ 容器网络完整拓扑 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────────┐ ┌──────────────────┐ │ │ │ 外部网络 │ │ 外部网络 │ │ │ │ (互联网) │ │ (互联网) │ │ │ └────────┬─────────┘ └────────┬─────────┘ │ │ │ │ │ │ │ 访问宿主机:8088 │ 回包给宿主机IP │ │ ▼ ▼ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ 宿主机 │ │ │ │ │ │ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ │ │ eth0 │ │ iptables│ │ br0 │ │ │ │ │ │物理网卡 │◄──────►│ NAT规则 │◄──────►│ 网桥 │ │ │ │ │ │10.162...│ │ SNAT │ │192.168 │ │ │ │ │ └─────────┘ │ DNAT │ │ .0.1 │ │ │ │ │ └─────────┘ └────┬────┘ │ │ │ │ │ │ │ │ │ ┌─────────────────────┼───────────┐│ │ │ │ │ │ ││ │ │ │ ▼ ▼ ││ │ │ │ ┌──────────┐ ┌──────────┐ ││ │ │ │ │ veth1_p │ │ veth2_p │ ││ │ │ │ │ │ │ │ ││ │ │ │ └────┬─────┘ └────┬─────┘ ││ │ │ └────────────────────┼─────────────────────┼─────────────┘│ │ │ │ │ │ │ │ ┌────────────┼─────────────────────┼────────────┐ │ │ │ │ │ │ │ │ │ │ ▼ │ ▼ │ │ │ │ ┌───────────────┐ │ ┌───────────────┐ │ │ │ │ │ 容器 net1 │ │ │ 容器 net2 │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ┌─────────┐ │ │ │ ┌─────────┐ │ │ │ │ │ │ │ veth1 │ │ │ │ │ veth2 │ │ │ │ │ │ │ │192.168 │ │ │ │ │192.168 │ │ │ │ │ │ │ │ .0.2 │ │ │ │ │ .0.3 │ │ │ │ │ │ │ └─────────┘ │ │ │ └─────────┘ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ 路由表: │ │ │ 路由表: │ │ │ │ │ │ default gw │ │ │ default gw │ │ │ │ │ │ 192.168.0.1 │ │ │ 192.168.0.1 │ │ │ │ │ └───────────────┘ │ └───────────────┘ │ │ │ │ │ │ │ │ │ veth pair │ veth pair │ │ │ │ 虚拟网线 │ 虚拟网线 │ │ │ │ │ │ │ │ └─────────────────────────┼──────────────────────────────────┼─┘ │ │ │ │ └──────────────────────────────────┘ │ │ │ 数据流 │ │ 1. 容器发出数据 → veth → br0 → SNAT改源IP → eth0 → 外网 │ │ 2. 外网回包 → eth0 → DNAT改目的IP → br0 → veth → 容器 │ │ │ └─────────────────────────────────────────────────────────────────────────┘4.6 排错实战常见问题与解决问题 1Network is unreachable原因容器内没有默认路由。解决ip netns exec net1 ip route add default gw 192.168.0.1问题 2能发出包但收不到回包原因宿主机未开启 IP 转发。解决sysctl -w net.ipv4.ip_forward1问题 3外网无法访问容器服务原因未配置 DNAT 规则。解决iptables -t nat -A PREROUTING -p tcp --dport 宿主机端口 -j DNAT --to-destination 容器IP:容器端口问题 4容器无法访问外网原因未配置 SNAT 规则。解决iptables -t nat -A POSTROUTING -s 192.168.0.0/24 -o eth0 -j MASQUERADE4.7 一键获取完整项目代码# 创建命名空间 ip netns add net1 # 创建veth对 ip link add veth1 type veth peer name veth1_p # 将veth1移入命名空间 ip link set veth1 netns net1 # 创建并配置Bridge brctl addbr br0 ip link set dev veth1_p master br0 ip addr add 192.168.0.1/24 dev br0 # 配置容器IP ip netns exec net1 ip addr add 192.168.0.2/24 dev veth1 ip netns exec net1 ip link set veth1 up ip netns exec net1 ip link set lo up # 启动所有设备 ip link set br0 up ip link set veth1_p up # 添加默认路由 ip netns exec net1 ip route add default gw 192.168.0.1 # 开启IP转发 sysctl -w net.ipv4.ip_forward1 # 配置NAT iptables -t nat -A POSTROUTING -s 192.168.0.0/24 -o eth0 -j MASQUERADE # 端口映射示例将宿主机8088端口映射到容器80端口 iptables -t nat -A PREROUTING -p tcp --dport 8088 -j DNAT --to-destination 192.168.0.2:800voice · GitHub
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2614397.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!