当前位置: 首页 > article >正文

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

相关文章:

Linux 网络虚拟化深度解析:从 veth 设备对到容器网络实战

第一部分:veth 设备对 —— 虚拟世界的 "网线" 1.1 什么是 veth 设备对? veth(Virtual Ethernet)设备对,可以理解为软件模拟的一对 "虚拟网卡",它们总是成对出现,就像用一…...

绍兴geo优化:亲测高性价比公司分享

绍兴GEO优化:亲测高性价比公司分享 随着AI搜索流量占比持续攀升,绍兴企业正面临传统推广方式成本高、效率低的挑战。在这样的背景下,GEO(地理围栏优化)技术成为了提高本地精准流量获取的关键手段。本文基于最新的调研…...

深度解析 Gemini CLI:架构剖析、高级配置与自动化工作流的高级使用技巧报告

深度解析 Gemini CLI:架构剖析、高级配置与自动化工作流的高级使用技巧报告 Gemini Command Line Interface (CLI) 代表了终端环境下人工智能辅助开发的根本性范式转变。该工具并非仅仅是一个简单的应用程序接口(API)封装,而是一…...

从“抢人”到“识人”,回归匹配本质

金融校招如何穿透简历迷雾锁定真才? 在校园招聘的春季战场上,HR们往往陷入一种矛盾:一方面是后台爆满的简历收件箱,另一方面却是面试环节频频出现的“货不对板”。对于金融、咨询等对软素质要求极高的行业而言,校招实…...

Python课后感

今天把这几个笔记整理了一下,感觉对Python的理解又深了一点。先说包和模块这块吧。以前我老分不清啥是包啥是模块,现在明白了——每个.py文件就是个模块,而包其实就是个文件夹,只不过里面得有个__init__.py文件。这个文件挺有意思…...

掌握Windows虚拟显示技术:ParsecVDisplay打造高效多屏工作环境

掌握Windows虚拟显示技术:ParsecVDisplay打造高效多屏工作环境 【免费下载链接】parsec-vdd ✨ Perfect virtual display for game streaming 项目地址: https://gitcode.com/gh_mirrors/pa/parsec-vdd 在现代计算环境中,无论是远程办公、游戏直播…...

Python性能优化实战:Numba JIT编译器原理与高性能计算应用

1. 项目概述:当Python遇上性能瓶颈,Numba如何成为“救火队长”?在数据科学、科学计算和机器学习领域,Python以其简洁的语法和丰富的生态库(如NumPy、Pandas、SciPy)成为了事实上的标准语言。然而&#xff0…...

Kubernetes应用管理新范式:kapp-controller控制器模式详解与实践

1. 项目概述:Kubernetes应用管理的“控制器”模式新范式如果你在Kubernetes世界里摸爬滚打了一段时间,尤其是在尝试将应用打包、部署和生命周期管理进行标准化时,大概率会感到一丝疲惫。Helm Chart的模板、Kustomize的重叠、以及如何让这些配…...

Xenos DLL注入器:Windows系统动态加载完整指南

Xenos DLL注入器:Windows系统动态加载完整指南 【免费下载链接】Xenos Windows dll injector 项目地址: https://gitcode.com/gh_mirrors/xe/Xenos 在Windows系统开发和逆向工程领域,DLL注入技术是开发者和安全研究人员必须掌握的核心技能之一。X…...

AI应用开发脚手架:基于Next.js与LangChain的快速原型构建指南

1. 项目概述:一个为AI产品快速启动而生的脚手架最近在GitHub上闲逛,发现了一个名为ThanhWilliamLe/ai-product-bootstrap的项目,点进去一看,立刻就被吸引住了。这本质上是一个为AI应用开发者准备的“一站式”项目脚手架。如果你和…...

零基础录音转日程教程包教包会避坑,看完就能直接上手

做销售近5年,日常需频繁跑客户拜访、对接客户,每次沟通结束后,将录音整理成待办日程都十分繁琐,先和大家分享我之前踩过的一些坑,不少同行可能也有类似经历。第一个坑是误以为录音转日程,只需先将录音转成文…...

苏州配电工程为什么优先本地一站式厂家?

配电工程常见的落地痛点在苏州,各类配电工程项目数量众多,推进过程中普遍存在多方对接复杂、流程繁琐、责任推诿等问题。若将设计、生产、安装、售后等环节分别委托给不同单位,一旦出现问题,各方往往互相推诿,责任难以…...

基于 HarmonyOS 6.0 的校园闲置市集应用开发实战:从页面构建到跨端设计深度解析

基于 HarmonyOS 6.0 的校园闲置市集应用开发实战:从页面构建到跨端设计深度解析 前言 随着 HarmonyOS 生态不断完善,HarmonyOS 6.0 在分布式能力、跨端协同以及 ArkUI 声明式开发方面再次进行了大幅升级。相比传统 Android 页面开发模式,Harm…...

挑选工作效率提升工具,必这4个核心筛选标准

2026年挑选工作效率提升工具,尤其是多次尝试AI工具、希望找到合适选择的HR,不妨参考这四个核心筛选方向,减少不必要的试错时间。身边有位做招聘的HR小林,秋招高峰期一天安排8场面试,群面、结构化面试连轴转&#xff0c…...

GelSight 视触觉3D显微系统 4.4 软件版本上线,粗糙度测量维度全面拓展

近日,GelSight推出V4.4软件版本,同步适配 GelSight视触觉3D显微系统全系列产品,围绕3D表面形貌检测、表面粗糙度测量、无损弹性3D成像核心能力优化,为材料科学、精密制造、航空航天、增材制造等领域科研人员提供非接触式检测方案。…...

使用pretty-log美化终端日志:提升开发调试效率的实践指南

1. 项目概述:告别混乱,拥抱优雅的日志输出如果你是一名后端开发者,或者经常和服务器、命令行工具打交道,那么对下面这种日志格式一定不会陌生:[2024-05-27 14:30:22] [ERROR] [main] com.example.service.UserService …...

Prisma Relay游标分页库实战:解决GraphQL分页难题

1. 项目概述:一个解决分页痛点的利器如果你在构建一个使用 Prisma 和 GraphQL 的后端应用,并且正在为如何实现高效、标准化的 Relay 风格分页而头疼,那么devoxa/prisma-relay-cursor-connection这个库很可能就是你正在寻找的“瑞士军刀”。它…...

豪门贵公子具象化!庞钦宇现身TOD‘S家宴,举手投足间尽显骑士优雅

如果说马术是勇敢者的游戏,那么庞钦宇便是这场游戏中走出的优雅绅士。近日00后马术新星庞钦宇在TODS春日家宴上完成了一次惊艳的“跨界”。在这场汇聚名流与星光的盛事中,他褪去赛场的戎装,却未减半分骑士的矜贵。举手投足间这位年轻的骑手不…...

广州Ai直播公司供应商

随着互联网技术的快速发展,直播已经成为企业营销和品牌推广的重要手段。然而,传统的真人主播模式存在诸多痛点,如成本高、档期不稳定等。为了解决这些问题,广州有请科技有限公司(以下简称“有请科技”)应运…...

2026年3月 电子学会青少年软件编程机器人技术七级等级考试试卷真题【实际操作】

答案和更多内容请查看网站:【试卷中心 ----->电子学会 ---->机器人技术 ----> 七级】 网站链接 青少年软件编程历年真题模拟题实时更新 青少年机器人技术等级考试实际操作试卷(七级) 2026年3月 一、实操试题 主题&#xff1…...

液冷下半场:两相液冷比拼的不仅是冷板厚度,还比什么?

常见问题(FAQ) Q: 两相液冷能将芯片温差控制在多少? A: 可在2℃以内,典型工况下可达1.5℃。相比单相液冷的8℃以上波动,优势明显。 Q: 存量机房改造后,机柜功率能提升多少? A: 某数据中心改造…...

DMRG-SCF方法:量子化学强关联系统的高效计算方案

1. DMRG-SCF方法概述:量子化学中的强关联系统解决方案密度矩阵重整化群自洽场(DMRG-SCF)方法是近年来量子化学领域最具突破性的进展之一,它巧妙结合了两种经典理论的优势。作为一位长期从事量子化学计算的科研人员,我见…...

基于Arduino与DFPlayer Mini打造可编程声音反馈键盘

1. 项目概述:当键盘不只是键盘 如果你和我一样,每天有超过8小时的时间在和键盘打交道,那你一定对“手感”这个词有执念。薄膜键盘的绵软、机械轴的段落感、静电容的柔和,每一种都代表了一种输入体验。但“BryceWG/BiBi-Keyboard”…...

菲仕技术冲刺港股:年营收16亿,亏6189万 先进制造与京津冀基金是股东

雷递网 雷建平 5月14日宁波菲仕技术股份有限公司(简称:“菲仕技术”)日前更新招股书,准备在港交所上市。年营收16亿 亏6189万菲仕技术成立于2001年,是一家电驱动解决方案供应商,提供综合及定制化的电驱动系…...

《三维动画制作》学习心得

《三维动画制作》学习心得 —— 生产线动画创作感悟 为期一段时间的《三维动画制作》课程学习,我以自动化生产线为主题完成了三维动画作品。从最初的概念构思,到模型搭建、材质渲染,再到关键帧动画调试,整个过程不仅让我系统掌握了…...

前端学习打卡Day9:CSS 关系选择器、综合实战案例|古诗鉴赏网页制作

一、今日学习目标掌握 CSS四种关系选择器的语法、选择范围、使用场景,能区分后代 / 子代、邻接兄弟 / 通用兄弟选择器的差异。理解古诗网页案例的布局结构,能独立分析布局逻辑、读懂代码并知晓优化方向。能结合关系选择器优化网页样式,实现精…...

LTX2.3 最强开源视频生成模型 文生图 / 图生视频 / 音频驱动|低端显卡本地安装

LTX2.3 是 Lightricks 推出的开源音视频生成模型,支持文生视频、图生视频、音频驱动生成视频,原生音画同步、支持 4K / 竖屏,消费级显卡可本地部署,一键整合包开箱即用。 一、LTX2.3 是什么 LTX‑2.3 是 Lightricks 发布的开源视…...

代码可视化工具:从AST解析到自动化图表生成的技术实践

1. 项目概述:从代码到图形的自动化桥梁在软件开发、架构设计乃至技术文档编写的日常工作中,我们常常面临一个共同的痛点:如何清晰、高效地向他人(或未来的自己)解释一段复杂的代码逻辑、一个系统的模块关系&#xff0c…...

10亿条URL的黑名单,如何快速判断一个新请求的URL是否在黑名单内?

在日常开发中,你是否遇到过这样的场景:有一个包含10亿条URL的黑名单,如何快速判断一个新请求的URL是否在黑名单内,同时避免占用几十GB的内存?在我们学习缓存三剑客时,关于缓存穿透,我们常用的解…...

工程化AI编程:claude-code-blueprint项目实战与最佳实践

1. 项目概述与核心价值最近在GitHub上看到一个挺有意思的项目,叫“claude-code-blueprint”,作者是lethilu4796。乍一看这个标题,你可能会觉得这又是一个普通的代码生成工具或者AI辅助编程的脚本。但当我深入研究了它的源码和使用方式后&…...