巧用 Linux 网络机制,打通网际孤岛
巧用 Linux 网络机制,打通网际孤岛
问题描述
在日常的服务器管理和网络工作中,我们常常会遇到一个棘手的「网络跳板」问题。这个问题可以被清晰地抽象为一个经典的 A-B-C 模型:
- 场景:我们有 A、B、C 三台位于不同网络的服务器,它们都拥有静态地址。
- 挑战:由于防火墙、安全组或网络隔离策略,节点间的连通性受到了限制:A-B 之间可以互相访问,B-C 之间可以互相访问,但 A-C 之间无法直接建立连接。
- 目标:我们需要建立一条路径,让 A 能够 透明地 访问 C 上的服务,就如同它们之间存在直接路由一样。所有复杂的流量转发都应在幕后由 B 完成,对 A 上的应用程序完全无感。
这个看似抽象的模型,其实在现实世界中有着非常广泛的应用场景,例如:
- 企业内网穿透:A 是仅能访问公司内网的开发机,B 是一台同时拥有内外网权限的网关 / 堡垒机,而 C 是位于公网的某个云服务。我们需要让开发机 A 能直接调用公网 C 的 API。
- 虚拟化开发环境:A 是你的办公电脑,B 是用于部署虚拟化开发环境的服务器,C 是运行在 B 上的虚拟机(使用 NAT 模式,常见的 Docker、Hyper-V 等虚拟化技术均默认采用 NAT 模式)。你想从远程直接访问虚拟机 C 内部署的服务,同时避免将 C 暴露给公网。
- 骨干网 IPv6 改造与兼容:随着网络技术演进,运营商或者企业的骨干网已全面升级为纯 IPv6 环境,但数据中心内仍存在部分仅支持 IPv4 的老旧设备(如存储、监控设备)。A 是老旧的 IPv4 设备,C 是 IPv6 网络中的服务, B 是处于网络边缘且同时拥有 IPv4 和 IPv6 连接能力的双栈服务器。此时需要使 A 无缝访问 C,从而在不淘汰旧有硬件的前提下,平滑地完成网络架构的现代化改造。
- 跨校网络加速与中转:主机 A 在位于大学 A,无法稳定访问位于大学 C 的服务器 C。但你有服务器 B,它与 A 和 C 之间都有良好的网络连接,此时 B 就可以作为理想的中转节点。
面对这类挑战,利用 Linux 内核自带的 隧道(Tunnel,TUN)机制 和 网络地址转换(Network Address Translation,NAT)机制,是一种极其优雅且高效的解决方案。本文将详细介绍其实现原理,并提供一份清晰、可复现的配置指南。
解决方案原理
本文所述解决方案对应的网络拓扑如下图所示。
核心概念解析
隧道 Tunnel
隧道(Tunnel,TUN)是一种网络通信机制,它将一种网络协议的数据包(负载)封装在另一种网络协议的数据包(载体)内进行传输。在本方案中,从 A 发往 C 的原始 IP 数据包会被「包装」成一个新的 IP 数据包,其目标地址是 B。当 B 收到这个包后,它会「拆开」包装,取出原始的数据包,然后根据其真正的目标地址 C 将其转发出去。这个过程就像在 A 和 B 之间挖了一条虚拟的、点对点的通道,也就是「隧道」。Linux 内核支持多种隧道模式,如 ip6ip6
(IPv6-in-IPv6)、ip6ip
(IPv4-in-IPv6)、sit
(IPv6-in-IPv4)和 ipip
(IPv4-in-IPv4)等。
网络地址转换 NAT
网络地址转换(Network Address Translation,NAT)是一种在 IP 数据包通过路由器或防火墙时,重写其源或目标地址的技术。在本方案中,我们主要使用源地址转换(Source NAT,SNAT),特别是 iptables
中的 MASQUERADE
功能。当来自 A 的数据包通过隧道到达 B,并准备被转发给 C 时,B 会将数据包的源地址(A 的隧道地址)替换成 B 自己的出口网卡地址。这样做是为了让 C 能够正确地将响应数据包发回给 B。B 收到响应后,会自动执行反向转换,将目标地址改回 A 的隧道地址,并通过隧道传回给 A。
关键角色:路由器 vs 网关
在本文提供的解决方案中,服务器 B 扮演的角色是一个简单的 路由器,而不是一个 网关。这是理解为什么隧道内承载的协议需要与 B-C 间的网络协议保持一致的关键。
路由器的工作原理是:当它在一个接口(如隧道接口 tun0
)上收到一个数据包时,它会查看该数据包的目标地址,然后根据自己的路由表,将这个数据包 几乎原封不动地 从另一个接口(如物理网卡 eth0
)发送出去。它只负责路由,不负责改变数据包的协议类型(例如从 IPv4 转换到 IPv6)。
因此,就会出现以下两种情况:
- 协议一致时:如果从隧道中出来的数据包是 IPv6,且 B 和 C 之间的网络协议也是 IPv6,那么 B 可以毫无障碍地将这个 IPv6 包经过路由直接转发给 C。整个过程是通畅的。
- 协议不一致时:如果从隧道出来的是一个 IPv4 包,但服务器 C 只支持 IPv6,那么此时如果 B 还只作为一个简单的路由器,就“无能为力”了。要实现这种跨协议通信,B 就必须扮演更复杂的 网关 角色,例如使用 NAT64 / DNS64 这样的技术,但这已超出了本文所讨论的 Linux 内核原生转发功能的范畴。
综上所述,本文之所以只讨论协议一致的场景,是因为它旨在提供一个仅依赖 Linux 内核基础路由和转发功能实现的简洁方案。
网际协议组合完整分析表
下表完整地列出了 A-B、B-C 和 隧道 三个网络之间所有理论上的协议组合以及本文提供解决方案的适用性。
场景 | A-B | B-C | 隧道 | 适用性 | 备注 |
---|---|---|---|---|---|
1 | IPv6 | IPv6 | IPv6 | 是 | 纯 IPv6 环境下的标准场景。 |
2 | IPv6 | IPv6 | IPv4 | 否 | 需要 B 做协议转换(NAT46),配置复杂。 |
3 | IPv6 | IPv4 | IPv6 | 否 | 需要 B 做协议转换(NAT64),配置复杂。 |
4 | IPv6 | IPv4 | IPv4 | 是 | 典型的过渡方案,用 IPv6 网络访问纯 IPv4 服务。 |
5 | IPv4 | IPv6 | IPv6 | 是 | 典型的过渡方案,用 IPv4 网络访问纯 IPv6 服务。 |
6 | IPv4 | IPv6 | IPv4 | 否 | 需要 B 做协议转换(NAT46),配置复杂。 |
7 | IPv4 | IPv4 | IPv6 | 否 | 需要 B 做协议转换(NAT64),配置复杂。 |
8 | IPv4 | IPv4 | IPv4 | 是 | 最常见的场景,用 IPv4 网络访问 IPv4 服务。 |
配置过程
由于需要对网际层的配置进行修改,因此下面所有的配置命令都需要在 root
用户下执行。
隧道网段分配
为了最小化地址使用并遵循最佳实践,我们为隧道分配如下的点对点网段。
协议版本 | 隧道网段 | A 端隧道地址 | B 端隧道地址 |
---|---|---|---|
IPv4 | |||
IPv6 |
在下面的命令中,我们使用 <a_ipv4>
、<b_tun_ipv6>
等占位符来指代服务器的地址,请根据你的实际环境和上表规划替换它们。
开启服务器 B 的内核转发
首先需要开启 B 的内核转发能力。
在 服务器 B 上执行,开启 IPv6 转发。
# 开启 IPv6 转发
sysctl -w net.ipv6.conf.all.forwarding=1
在 服务器 B 上执行,开启 IPv4 转发。
# 开启 IPv4 转发
sysctl -w net.ipv4.ip_forward=1
在 服务器 B 上执行,同时开启 IPv6 和 IPv4 转发。
# 开启 IPv6 & IPv4 转发
sysctl -w net.ipv6.conf.all.forwarding=1
sysctl -w net.ipv4.ip_forward=1
在服务器 B 上配置 NAT 规则
为从隧道到达 B 并转发至 C 的流量配置 SNAT(MASQUERADE),确保 C 可以将返回流量路由回 B。为了遵循权限最小原则,我们同时指定了源地址和目标地址。
在 服务器 B 上执行。
# 配置 SNAT,让来自 A 的隧道流量(源 <a_tun_ipv6>)去往 C(目标 <c_ipv6>)时,源地址伪装成 B 的出口网卡地址
ip6tables -t nat -A POSTROUTING -s <a_tun_ipv6> -d <c_ipv6> -o eth0 -j MASQUERADE
在 服务器 B 上执行。
# 配置 SNAT,让来自 A 的隧道流量(源 <a_tun_ipv4>)去往 C(目标 <c_ipv4>)时,源地址伪装成 B 的出口网卡地址
iptables -t nat -A POSTROUTING -s <a_tun_ipv4> -d <c_ipv4> -o eth0 -j MASQUERADE
在 服务器 B 上执行。
# 配置 SNAT,让来自 A 的隧道流量(源 <a_tun_ipv6>)去往 C(目标 <c_ipv6>)时,源地址伪装成 B 的出口网卡地址
ip6tables -t nat -A POSTROUTING -s <a_tun_ipv6> -d <c_ipv6> -o eth0 -j MASQUERADE
在 服务器 B 上执行。
# 配置 SNAT,让来自 A 的隧道流量(源 <a_tun_ipv4>)去往 C(目标 <c_ipv4>)时,源地址伪装成 B 的出口网卡地址
iptables -t nat -A POSTROUTING -s <a_tun_ipv4> -d <c_ipv4> -o eth0 -j MASQUERADE
在服务器 B 上配置防火墙转发策略
默认情况下 链的策略可能是 ,因此需要显式允许 A 和 C 之间的流量通过 B 转发。同样,我们配置最精细化的规则。
在 服务器 B 上执行。
# 允许从 A 到 C 的流量转发
ip6tables -A FORWARD -i tun0 -o eth0 -s <a_tun_ipv6> -d <c_ipv6> -j ACCEPT
# 允许从 C 到 A 的返回流量转发
ip6tables -A FORWARD -i eth0 -o tun0 -s <c_ipv6> -d <a_tun_ipv6> -j ACCEPT
在 服务器 B 上执行。
# 允许从 A 到 C 的流量转发
iptables -A FORWARD -i tun0 -o eth0 -s <a_tun_ipv4> -d <c_ipv4> -j ACCEPT
# 允许从 C 到 A 的返回流量转发
iptables -A FORWARD -i eth0 -o tun0 -s <c_ipv4> -d <a_tun_ipv4> -j ACCEPT
在 服务器 B 上执行。
# 允许从 A 到 C 的流量转发
ip6tables -A FORWARD -i tun0 -o eth0 -s <a_tun_ipv6> -d <c_ipv6> -j ACCEPT
# 允许从 C 到 A 的返回流量转发
ip6tables -A FORWARD -i eth0 -o tun0 -s <c_ipv6> -d <a_tun_ipv6> -j ACCEPT
在 服务器 B 上执行。
# 允许从 A 到 C 的流量转发
iptables -A FORWARD -i tun0 -o eth0 -s <a_tun_ipv4> -d <c_ipv4> -j ACCEPT
# 允许从 C 到 A 的返回流量转发
iptables -A FORWARD -i eth0 -o tun0 -s <c_ipv4> -d <a_tun_ipv4> -j ACCEPT
在服务器 A 和 B 上建立隧道
此时,我们可以在 A 和 B 之间配置隧道本身。
使用 IP6IP6 隧道(mode ip6ip6
)。
在服务器 A 上执行:
# 创建隧道,remote 是 B 的公网 IPv6,local 是 A 的公网 IPv6
ip -6 tunnel add tun0 mode ip6ip6 remote <b_ipv6> local <a_ipv6>
# 启动隧道
ip link set tun0 up
# 为隧道接口分配 IPv6 地址
ip -6 addr add <a_tun_ipv6> peer <b_tun_ipv6> dev tun0
在服务器 B 上执行:
# 创建隧道,remote 是 A 的公网 IPv6,local 是 B 的公网 IPv6
ip -6 tunnel add tun0 mode ip6ip6 remote <a_ipv6> local <b_ipv6>
# 启动隧道
ip link set tun0 up
# 为隧道接口分配 IPv6 地址
ip -6 addr add <b_tun_ipv6> peer <a_tun_ipv6> dev tun0
使用 IP6IP 隧道(mode ip6ip
)。
在服务器 A 上执行:
# 创建隧道,remote 是 B 的公网 IPv6,local 是 A 的公网 IPv6
ip -6 tunnel add tun0 mode ip6ip remote <b_ipv6> local <a_ipv6>
# 启动隧道
ip link set tun0 up
# 为隧道接口分配 IPv4 地址
ip addr add <a_tun_ipv4> peer <b_tun_ipv4> dev tun0
在服务器 B 上执行:
# 创建隧道,remote 是 A 的公网 IPv6,local 是 B 的公网 IPv6
ip -6 tunnel add tun0 mode ip6ip remote <a_ipv6> local <b_ipv6>
# 启动隧道
ip link set tun0 up
# 为隧道接口分配 IPv4 地址
ip addr add <b_tun_ipv4> peer <a_tun_ipv4> dev tun0
使用 6in4 隧道(mode sit
)。
在服务器 A 上执行:
# 创建隧道,remote 是 B 的公网 IP,local 是 A 的公网 IP
ip tunnel add tun0 mode sit remote <b_ipv4> local <a_ipv4> ttl 255
# 启动隧道
ip link set tun0 up
# 为隧道接口分配 IPv6 地址
ip -6 addr add <a_tun_ipv6> peer <b_tun_ipv6> dev tun0
在服务器 B 上执行:
# 创建隧道,remote 是 A 的公网 IP,local 是 B 的公网 IP
ip tunnel add tun0 mode sit remote <a_ipv4> local <b_ipv4> ttl 255
# 启动隧道
ip link set tun0 up
# 为隧道接口分配 IPv6 地址
ip -6 addr add <b_tun_ipv6> peer <a_tun_ipv6> dev tun0
使用 IPIP 隧道(mode ipip
)。
在服务器 A 上执行:
# 创建隧道,remote 是 B 的公网 IP,local 是 A 的公网 IP
ip tunnel add tun0 mode ipip remote <b_ipv4> local <a_ipv4> ttl 255
# 启动隧道
ip link set tun0 up
# 为隧道接口分配 IP,peer 是对端隧道 IP
ip addr add <a_tun_ipv4> peer <b_tun_ipv4> dev tun0
在服务器 B 上执行:
# 创建隧道,remote 是 A 的公网 IP,local 是 B 的公网 IP
ip tunnel add tun0 mode ipip remote <a_ipv4> local <b_ipv4> ttl 255
# 启动隧道
ip link set tun0 up
# 为隧道接口分配 IP,peer 是对端隧道 IP
ip addr add <b_tun_ipv4> peer <a_tun_ipv4> dev tun0
在服务器 A 上配置路由
最后,在 A 上添加一条静态路由,将发往 C 的所有流量都指向隧道的 B 端。
在 服务器 A 上执行。
# 将 C 的 IPv6 指向隧道 B 的一端
ip -6 route add <c_ipv6> via <b_tun_ipv6> dev tun0
在 服务器 A 上执行。
# 将 C 的 IP 指向隧道 B 的一端
ip route add <c_ipv4> via <b_tun_ipv4> dev tun0
在 服务器 A 上执行。
# 将 C 的 IPv6 指向隧道 B 的一端
ip -6 route add <c_ipv6> via <b_tun_ipv6> dev tun0
在 服务器 A 上执行。
# 将 C 的 IP 指向隧道 B 的一端
ip route add <c_ipv4> via <b_tun_ipv4> dev tun0
测试连通性
此时,从服务器 A 应该可以直接访问服务器 C 上的服务了。可以通过 ping
命令进行测试。
在 服务器 A 上执行。
ping -6 <c_ipv6>
在 服务器 A 上执行。
ping <c_ipv4>
在 服务器 A 上执行。
ping -6 <c_ipv6>
在 服务器 A 上执行。
ping <c_ipv4>
总结
本文通过一个经典的 A-B-C 网络模型,详细阐述了如何利用 Linux 内核原生的隧道(Tunnel)和网络地址转换(NAT)功能,来巧妙地打通网络孤岛。通过在中间节点 B 上建立隧道,并结合精细化的 IP 转发、防火墙策略和 SNAT 规则,我们成功实现了让服务器 A 透明地访问服务器 C 的目标,整个过程对 A 端的应用程序完全无感。
该方案的核心优势在于:
- 原生高效:完全依赖 Linux 内核功能,无需安装任何第三方软件,性能卓越、资源占用极低。
- 透明无感:对于服务器 A 上的应用程序而言,访问 C 的体验与直接连接无异,无需修改任何代码或应用配置。
- 安全可控:遵循权限最小原则配置
iptables
/ip6tables
规则,可以精确控制允许转发的流量,有效保障了系统的安全性。 - 灵活通用:方案全面覆盖了 IPv4 和 IPv6 网络的多种组合场景,无论是纯粹的 IPv4/IPv6 环境还是两者共存的过渡网络,都能轻松应对。
最后需要指出,文中的所有 ip
、sysctl
和 iptables
命令均为临时配置,在系统重启后会失效。若要实现永久生效,建议将这些配置写入系统的网络管理服务配置文件中(例如 systemd-networkd
、ifupdown
)或通过启动脚本固化。
总而言之,深入理解并善用 Linux 强大的网络堆栈,能为我们解决复杂的网络连接问题提供意想不到的简洁与高效。