如下图所示,是否可以像这样通过 Linux 内核路由流量?我希望模拟“内部”网络之外的设备的精确副本,具有相同的 IP 范围,同时使内部和外部设备能够相互通信,而不知道对方具有与其自身相同的 IP。
例如:“内部”的设备 X 联系 192.168.3.5,该数据会转到中间人桥并转发到 IP 192.168.2.5 的“外部”设备 Y。然后响应被发送回中间人,并发送到 IP 192.168.2.5 的设备 X。
我知道这可以通过网络命名空间实现,并且可以进行有效的模拟。但是,我希望避免使用命名空间,而是针对不同的流量方向使用不同的路由表之类的东西。这可能吗?
如果我理解正确的话,由于 IP 范围重复,我无法使用 NAT。它是否正确?
答案1
拓扑结构
我们首先配置 上的网络接口middleman
。我假设您已登录系统控制台,或者可以通过不涉及 或inner
网络的界面进行访问outer
。出于本答案的目的,我们假设接口middleman-eth0
onmiddleman
连接到“内部”网络并middleman-eth1
连接到“外部”网络。这为我们提供了以下网络拓扑:
启用转发
我们需要确保我们已启用数据包转发middleman
:
sysctl -w net.ipv4.ip_forward=1
我们应该从一个空的 netfilter 配置开始。运行iptables-save
不应产生任何输出。
接口配置
为此, 和middleman-eth0
都middleman-eth1
具有相同的网络配置:
ip addr add 192.168.2.1/24 dev middleman-eth0
ip addr add 192.168.2.1/24 dev middleman-eth1
如果您认为这看起来很奇怪,那么您是对的!目前,路由表middleman
如下所示:
192.168.2.0/24 dev middleman-eth1 proto kernel scope link src 192.168.2.1
192.168.2.0/24 dev middleman-eth0 proto kernel scope link src 192.168.2.1
这不会特别有用。
VRF配置
我们将利用 Linux 对“虚拟路由和转发“(“VRF”)。这允许我们在系统上创建多个隔离的路由域,以便传入的流量eth0
将看到与传入的流量不同的路由表eth1
。
我们首先创建 VRF 接口:
ip link add vrf-inner type vrf table 100
ip link set vrf-inner up
ip link add vrf-outer type vrf table 200
ip link set vrf-outer up
这些命令设置两个 VRF 设备,将每个设备与特定的路由表相关联。
接下来,我们将每个物理接口连接到 VRF 设备:
ip link set dev middleman-eth0 master vrf-inner
ip link set dev middleman-eth1 master vrf-outer
经过这些更改,主路由表现在为空:
middleman# ip route show
<no output>
middleman-eth0
在表 100 中,我们看到与(“内部”网络)相关的规则:
middleman# ip route show table 100
broadcast 192.168.2.0 dev middleman-eth0 proto kernel scope link src 192.168.2.1
192.168.2.0/24 dev middleman-eth0 proto kernel scope link src 192.168.2.1
local 192.168.2.1 dev middleman-eth0 proto kernel scope host src 192.168.2.1
broadcast 192.168.2.255 dev middleman-eth0 proto kernel scope link src 192.168.2.1
middleman-eth1
在表 200 中,我们看到(“外部”网络)的规则:
middleman# ip route show table 200
broadcast 192.168.2.0 dev middleman-eth1 proto kernel scope link src 192.168.2.1
192.168.2.0/24 dev middleman-eth1 proto kernel scope link src 192.168.2.1
local 192.168.2.1 dev middleman-eth1 proto kernel scope host src 192.168.2.1
broadcast 192.168.2.255 dev middleman-eth1 proto kernel scope link src 192.168.2.1
此时,我们实际上有两个断开连接的网络,如下所示:
“内部”网络上的主机可以联系 192.168.2.1,他们将与middleman-eth0
。 “外部”网络上的主机可以还联系 192.168.2.1,但他们将与 通话middleman-eth1
。
两人相遇的地方
我们现在需要做的就是设置映射,以便任何一方都可以使用192.168.3.0/24
另一方联系节点的地址。
首先,我们需要告诉所有节点它们192.168.3.0/24
通过以下方式路由到网络middleman
:这意味着在所有节点上,在“内部”和“外部”网络上,我们需要:
ip route add 192.168.3.0/24 via 192.168.2.1
在 上middleman
,我们需要 (a) 将192.168.3.0/24
范围内的地址映射到192.168.2.0/24
范围中,并且 (b) 确保在路由连接时使用正确的路由表。为了实现(a),我们可以创建一些NETMAP
规则:
iptables -t nat -A PREROUTING -d 192.168.3.0/24 -j NETMAP --to 192.168.2.0/24
iptables -t nat -A POSTROUTING -s 192.168.2.0/24 -j NETMAP --to 192.168.3.0/24
为了实现 (b),我们首先根据数据包的入口接口来标记数据包:
iptables -t mangle -A PREROUTING -i middleman-eth0 -d 192.168.3.0/24 -j MARK --set-mark 100
iptables -t mangle -A PREROUTING -i middleman-eth1 -d 192.168.3.0/24 -j MARK --set-mark 200
然后使用这些标记来选择路由表:
ip rule add prio 100 fwmark 100 lookup 200
ip rule add prio 200 fwmark 200 lookup 100
回想一下之前的内容,表 100 具有针对“内部”网络的规则,而表 200 具有针对“外部”网络的规则,因此这些规则表示“对于到达接口 的数据包middleman-eth0
,使用与 关联的路由表做出路由决策middleman-eth1
” ,反之亦然。
跟随弹跳的球
完成所有这些后,如果192.168.2.10
“内部”网络上的节点尝试 ping 192.168.3.10
:
- 由于
192.168.3.0/24 via 192.168.2.1
路由条目,数据包被路由到中间人 - 数据包到达
middleman-eth0
- 数据包到达
MANGLE
表链PREROUTING
并将 fwmark 设置为100
- 数据包到达
NAT
表链PREROUTING
并映射到目的地192.168.2.10
- 数据包进入路由子系统,在那里它符合
fwmark 100 lookup 200
规则 - 在路由表200中,它命中了
192.168.2.0/24 dev middleman-eth1
,因此内核会将其发送到设备middleman-eth1
- 数据包到达
NAT
表链POSTROUTING
,其源映射到192.168.3.10
。 - 数据包到达地址为 的“外部”节点
192.168.2.10
。 - ……深吸一口气……
- 外部节点发送回复
192.168.3.10
- 回复到达
middleman-eth1
- 回复到达
MANGLE
表链PREROUTING
并将 fwmark 设置为200
- 回复到达
NAT
表链PREROUTING
并映射到目的地192.168.2.10
- 回复进入路由子系统,在那里它符合
fwmark 200 lookup 100
规则 - 在路由表100中,它符合
192.168.2.0/24 dev middleman-eth0
规则,因此内核会将其发送到设备middleman-eth0
- 回复到达
NAT
表链POSTROUTING
,其源映射到表链192.168.3.10
- 回复到达“内部”节点
192.168.2.10
,该节点看到对其最初发出的请求的回复。
验证
如果在“内部”节点 0 ( 192.168.2.10
) 上,我们尝试使用 地址 ping “外部”节点 0 192.168.3.10
,则tcpdump -nn -i any icmp
在内部节点 0 上运行会显示:
07:01:58.125370 innernode0-eth0 Out IP 192.168.2.10 > 192.168.3.10: ICMP echo request, id 12999, seq 1, length 64
07:01:58.125533 innernode0-eth0 In IP 192.168.3.10 > 192.168.2.10: ICMP echo reply, id 12999, seq 1, length 64
我们middleman
看到:
07:01:58.125440 middleman-eth0 In IP 192.168.2.10 > 192.168.3.10: ICMP echo request, id 12999, seq 1, length 64
07:01:58.125459 middleman-eth1 Out IP 192.168.3.10 > 192.168.2.10: ICMP echo request, id 12999, seq 1, length 64
07:01:58.125514 middleman-eth1 In IP 192.168.2.10 > 192.168.3.10: ICMP echo reply, id 12999, seq 1, length 64
07:01:58.125518 middleman-eth0 Out IP 192.168.3.10 > 192.168.2.10: ICMP echo reply, id 12999, seq 1, length 64
在“外部”节点 0 上我们看到:
07:01:58.125489 outernode0-eth0 In IP 192.168.3.10 > 192.168.2.10: ICMP echo request, id 12999, seq 1, length 64
07:01:58.125497 outernode0-eth0 Out IP 192.168.2.10 > 192.168.3.10: ICMP echo reply, id 12999, seq 1, length 64
所以我认为我们已经实现了你的目标!
我用了迷你网测试此配置;您可以找到我的测试环境的完整源代码这里。有此配置的实际操作视频这里。
更新
正如AB在评论中指出的,这个配置有问题!默认情况下,内核的连接跟踪逻辑仅查看源/目标地址和源/目标端口。从innernode0
端口 4000 到端口 80 的连接outernode0
似乎是相同的连接为相反方向的连接...也就是说,假设我有一个在所有节点上的端口 80 上运行的网络服务器,这两个命令:
innernode0# curl --local-port 4000 192.168.3.10
和:
outernode0# curl --local-port 4000 192.168.3.10
将导致在以下位置出现单个连接跟踪条目middleman
:
middleman# conntrack -L
tcp 6 118 TIME_WAIT src=192.168.2.10 dst=192.168.3.10 sport=4000 dport=80 src=192.168.2.10 dstroot@mininet-vm:/proc/net=192.168.3.10 sport=80 dport=4000 [ASSURED] mark=0 use=1
我们告诉 conntrack 子系统如何区分这些连接。我们可以通过向表中的链添加一对CT
规则来做到这一点:PREROUTING
RAW
iptables -t raw -A PREROUTING -s 192.168.2.0/24 -i middleman-eth0 -j CT --zone-orig 100
iptables -t raw -A PREROUTING -s 192.168.2.0/24 -i middleman-eth1 -j CT --zone-orig 200
有了这些规则,我们现在可以在 conntrack 表中看到两个单独的连接:
middleman# conntrack -L
tcp 6 113 TIME_WAIT src=192.168.2.10 dst=192.168.3.10 sport=4000 dport=80 zone-orig=200 src=192.168.2.10 dst=192.168.3.10 sport=80 dport=40568 [ASSURED] mark=0 use=1
tcp 6 112 TIME_WAIT src=192.168.2.10 dst=192.168.3.10 sport=4000 dport=80 zone-orig=100 src=192.168.2.10 dst=192.168.3.10 sport=80 dport=4000 [ASSURED] mark=0 use=1