像 Docker 一样创建虚拟网络
众所周知,Docker 可以创建虚拟网络,以便在容器内进行安全便捷的网络交互。本文将以 Linux 操作系统下单机环境下的基本网络操作为例,讲解 Docker 的具体实现方式。
一小部分理论部分
为了理解本文接下来的内容,你需要深入了解一些理论知识。如果你已经尝试过在 Linux 中设置网络,可以直接进入下一部分。
为了以防万一,我将在此代码块中简要解释一下类似 mean 这样的行的含义
192.168.0.1/16
。该部分/16
表示 IP 地址的前 16 位用于标识网络,其余 16 位用于标识节点。在前面的示例中,192.168
这是标识网络的部分,而0.1
用来标识节点。
/32
表示指定节点的具体IP地址。
网络命名空间(netns)
Linux 中的网络命名空间是一个独立的网络资源环境。它拥有自己的 IP 地址、路由表、防火墙(详见下文)等等。
例如,当你有两个虚拟机想要互相连接,但又要与其他网络隔离时,它们就很有用。或者,如果你需要做同样的事情,但使用容器的话 :)。
虚拟网络接口
虚拟网络接口本质上是物理网络接口的逻辑表示。它们用于路由、隧道、创建虚拟网络等。
我们将使用 loopback
(或 lo 或 localhost) veth
和 bridge
,因此让我们更详细地讨论它们。
loopback
(localhost) – 提供在设备内部交换流量的能力。用于内部应用程序通信和测试。
可在地址 127.0.0.1 - 127.255.255.255 上使用。
veth
(虚拟以太网)——虚拟以太网电缆:连接虚拟网络的两个点,并使得它们之间可以传输流量。bridge
– 本质上,它是一个虚拟交换机。
交换机是一种可以用来连接不同的网络设备,交换流量的设备。
例如,通过将计算机和打印机连接到同一个交换机,计算机将能够“告诉”打印机要打印什么以及打印多少:)。
防火墙
防火墙用于过滤传入和传出的流量。它可以是物理设备,也可以是虚拟逻辑实现。我们将使用后者来进一步隔离网络命名空间。
NAT
NAT(网络地址转换)是一种将内部 IP 地址转换为外部 IP 地址,反之亦然的技术。例如,当您的手机和电脑连接到同一个 Wi-Fi 时,这两个设备将具有相同的外部 IP 地址。NAT 会将传入流量的路由器 IP 地址更改为数据包目标设备的内部 IP 地址。反之,传出数据包的 IP 地址将被更改为路由器的 IP 地址。
所有这些都是为了节省公共 IP 地址,因为公共 IP 地址的数量非常有限(约 43 亿,其中81%已被分配或保留)。
Docker 做什么?
docker 网络
Docker 为每个容器创建一个网络命名空间 (netns)。如果在创建容器时未指定要连接的网络,容器将连接到 bridge
具有一对接口的默认网络(即 ) veth
。这是必要的,以便容器默认能够相互通信。
但仅凭这一点还不够。要发送 IP 数据包,容器必须知道哪个节点将成为构建通往连接到同一 Docker 网络(即 bridge
y)的其他容器的路由的起点。这个起点(gateway
em)将成为容器本身 bridge
,容器连接到该容器。因此,它看起来像这样:“对于发送到 172.17.0.0/16 子网中 IP 地址的流量,设置网关 172.17.0.1。”
我们刚刚研究了 docker 网络驱动程序 bridge,但还有其他几个驱动程序,例如:
--network none
– 容器将完全不连接任何东西,甚至无法通过主机网络访问互联网。它只有loopback
一个接口。
--network host
– 容器将连接到主机网络。
为了让容器能够访问互联网,Docker 会在 NAT 中添加规则,用于替换从 bridge
Docker 网络接口发送的数据包的 IP 地址。这样做是必要的,因为容器的 IP 地址会被替换为输出接口的 IP 地址(或主机的 IP 地址),并通过该接口构建到互联网的路由。此外,每个容器都会在路由表中添加一个默认网关,这样所有地址的数据包都会通过 bridge
该接口发送,因为它可以访问主机网络。
此外,Docker 还添加了 firewall
规则,禁止将流量从一个 Docker 网络重定向到另一个 Docker 网络,以将它们彼此隔离。
当你从容器发布端口时, 该内部端口将在主机的本地网络上可用。默认情况下,Docker 现在为此维护一个“docker-proxy”进程,该进程监听所需的端口并重定向到所需容器的地址及其内部端口。这是一个不必要的遗留问题,很快Docker会将其彻底移除, 以支持 Hairpin NAT,但目前只能通过添加 daemon 标志来手动禁用它 --userland-proxy=false
。
简单来说,发夹 NAT 是将数据包发送到外部主机地址,然后路由回同一本地网络内的主机的过程。
Docker 还可以:通过连接多个 Docker 守护进程来创建覆盖网络,通过组合容器(无需使用接口)来创建 IPvlan 网络
bridge
等等。这些功能都非常有趣,但由于信息量太大,本文不会涵盖。
实践部分
让我们创建一个可以访问 Internet 的虚拟网络,并将 TCP 端口 8000 带到主机网络,这样 127.0.0.1
我们网络中的其他机器就可以通过该虚拟网络访问。
大部分解释将在代码块中的注释中。
在后续关于防火墙和 NAT 修改的文章中,我将使用
iptables
。我知道它已经过时了,但 Docker 仍在使用它,所以我决定使用它来代替nftables
。
步骤#1 - 创建网络命名空间
让我们首先创建 netns 并在其中运行一个简单的 Python http 服务器。
ip netns add redip -n red addr add 127.0.0.1/8 dev lo ip -n red link set lo upip netns exec red python3 -m http.server
我们只能在 netns ( ) 内部向服务器请求任何数据ip netns exec red curl 127.0.0.1:8000
。目前,它是完全隔离的。
步骤2 - 创建桥接接口
现在我们需要创建相同的 bridge
接口并将之前创建的 netns red 连接到它。
除此之外,我们还将设置网络的 IP 地址以及 bridge
它们应该匹配的子网指示。
ip link add br0 type bridgeip addr add 10.100.0.1/24 dev br0ip link set br0 upip link add red0 type veth peer name red0.br0ip link set red0.br0 master br0
ip link set red0.br0 upip link set red0 netns redip -n red addr add 10.100.0.2/24 dev red0
ip -n red link set red0 up
现在您可以从主机网络 ping netns 并向 http 服务器发送请求。
ping 10.100.0.2
curl 10.100.0.2:8000
对于实验,您可以类似地创建第二个网络,将其连接到一个网络 bridge
并尝试从另一个网络 ping 通一个网络,反之亦然。
ip netns exec blue ping 10.100.0.2
ip netns exec red ping 10.100.0.3
您将看到 ping
它通过了,因为两个网络都连接到同一个 bridge
。
步骤#3 – 所需系统参数
为了使一切正常工作,我们需要更改两个系统参数:
net.ipv4.ip_forward
– 需要 IP 端口转发才能重定向接口之间的流量。net.ipv4.conf.<interface>.route_localnet
–route_localnet=1
允许流量重定向到本地网络。在本例中,我们需要在bridge
接口上允许此功能。
sysctl -w net.ipv4.ip_forward=1
sysctl -w net.ipv4.conf.br0.route_localnet=1
步骤#4 - 从网络命名空间授予Internet访问权限
您需要在 iptables 的 NAT 表中添加一条规则,该规则适用于所有源地址与我们子网相同的数据包, bridge
并将数据包的 IP 地址替换为数据包所经过的接口的 IP 地址(在本例中为 eth0 接口的 IP 地址)。您需要将 eth0 替换为可访问 Internet 的主网络接口名称。
此外,通过相同的 iptables,有必要向 bridge
FILTER 表添加允许从我们的主要网络接口转发流量到该接口以及反之亦然的规则。
最后,为 netns 添加一个默认网关,以便默认情况下发往外部互联网的数据包会通过 bridge
接口地址。
iptables -t nat -A POSTROUTING -s 10.100.0.0/24 -o eth0 -j MASQUERADEiptables -A FORWARD -o eth0 -i br0 -j ACCEPT
iptables -A FORWARD -i eth0 -o br0 -j ACCEPTip netns exec red ip route add default via 10.100.0.1
我们来尝试ping一下外部IP地址:
ip netns exec red ping 8.8.8.8
步骤#5 - 将TCP端口暴露给主机网络
在这个阶段,我们将添加敲入我们的 http 服务器的能力 http://localhost:8000
,同时使机器能够从外部访问它,也就是说,连接到您的专用网络的其他真实计算机,或者,如果您直接连接到公共网络,那么一般来说,对于互联网上的所有计算机来说。
iptables -t nat -A PREROUTING -p tcp -m addrtype --dst-type LOCAL -m tcp --dport 8000 -j DNAT --to-destination 10.100.0.2:8000iptables -t nat -A OUTPUT -p tcp -m addrtype --dst-type LOCAL -m tcp --dport 8000 -j DNAT --to-destination 10.100.0.2:8000iptables -t nat -A POSTROUTING -o br0 -m addrtype --src-type LOCAL -j MASQUERADEiptables -t nat -A POSTROUTING -m addrtype --dst-type LOCAL -p tcp -m tcp --dport 8000 -j MASQUERADEiptables -A FORWARD -o br0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPTiptables -A FORWARD -d 10.100.0.2/32 ! -i br0 -o br0 -p tcp -m tcp --dport 8000 -j ACCEPTiptables -A FORWARD -i br0 -j ACCEPT
现在我们可以尝试从主机网络敲击我们的 http 服务器:
curl 127.0.0.1:8000
并且还通过主机的 IP 地址从其他机器向其发送请求:
curl 192.168.1.15:8000
全部!
结果我们得到:
http服务器运行在隔离的网络空间中。
从主机的环回(localhost)接口访问端口 8000 上的此服务器。
将来自 tcp 端口 8000 上其他机器的数据包重定向到我们的 http 服务器。