/

使用 iproute2 客製化你的 Container 網路

前言

我們都知道 Docker 有提供不同的網路模式供我們使用,從預設的 Bridge 到共用本機 Network Namespace 的 Host。我們甚至可以自訂自己的 Bridge 來切割不同網段,如此一來一些網路拓墣都架設都可以用 Docker 來完成。

區別不同網段我們通常會使用不同的 Bridge 來分隔,通常會先建立自訂的 Bridge

docker network create --driver bridge testing-bridge

之後建立 container 時可以指定該 bridge 來使用該網路空間

docker run -it --net=testing-bridge alpine

1
2
3
4
5
6
7
8
9
/ # ip a
1: lo: mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
35: eth0@if36: mtu 1500 qdisc noqueue state UP
link/ether 02:42:ac:12:00:02 brd ff:ff:ff:ff:ff:ff
inet 172.18.0.2/16 brd 172.18.255.255 scope global eth0
valid_lft forever preferred_lft forever

這時候可以發現 Docker 幫我建立好了 172.18.0.2/16 網段的介面可以使用,這時候你會發現該介面可以直接出得去外網,因為 Docker 建立好 Bridge,所以封包會從該 Bridge 到本機端出去。

那我們可不可以不要讓 Container 有機會從本機讓封包流出去呢?

(有時候要建立網路拓墣,不希望封包走本機的路由出去,希望封包從該網路路由到其他網路,也就是其他 Container)

解決(使用 veth peer)

要達成這件事,最快的做法就是自己建立 veth 並且將 peer 兩端直接綁在兩個 Container 上

我們打算用 iproute2 直接管理網路,所以建立 containers 時使用 --net=none

docker run -itd --name=left --net=none alpine
docker run -itd --name=right --net=none alpine

接著需要自行建立 Containers 們的 Network Namespace

1
2
3
4
5
left_pid=$(docker inspect -f '{{.State.Pid}}' left)`
right_pid=$(docker inspect -f '{{.State.Pid}}' right)`
sudo mkdir -p /var/run/netns
sudo ln -s /proc/$left_pid/ns/net /var/run/netns/$left_pid
sudo ln -s /proc/$right_pid/ns/net /var/run/netns/$right_pid

接著建立 veth peer 並且命名為 A 以及 B

ip link add A type veth peer name B

1
2
3
4
5
ip link set A netns $left_pid # 設定 peer 端 A 到 left container
ip netns exec $left_pid ip link set dev A name eth0 # 設定 namespace 中的介面名稱
ip netns exec $left_pid ip link set eth0 up # 啟動介面
ip netns exec $left_pid ip addr add 10.113.1.1/24 dev eth0 # 設定 IP
#ip netns exec $left_pid ip route add default via 10.113.1.254

新增預設路由的部分註解掉是因為我們將 Containers 用 veth peer 接起來,本來就是通的,所以不需要有預設路由也可以到達另外一個 Container。可以依照情況去設定你的預設路由

接著設定 Container right

1
2
3
4
5
ip link set B netns $right_pid
ip netns exec $right_pid ip link set dev B name eth0
ip netns exec $right_pid ip link set eth0 up
ip netns exec $right_pid ip addr add 10.113.1.2/24 dev eth0
#ip netns exec $right_pid ip route add default via 10.113.1.254

設定完之後進去 Containers 你會發現他的網路介面為

1
2
3
4
5
6
7
8
9
10
ubuntu@docker-env:~$ docker exec -it left sh
/ # ip a
1: lo: mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
33: eth0@if32: mtu 1500 qdisc noqueue state UP qlen 1000
link/ether 4e:1b:e1:22:8c:05 brd ff:ff:ff:ff:ff:ff
inet 10.113.1.1/24 scope global eth0
valid_lft forever preferred_lft forever

並且可以直接 ping 到另外一個 Container

1
2
3
4
/ # ping 10.113.1.2
PING 10.113.1.2 (10.113.1.2): 56 data bytes
64 bytes from 10.113.1.2: seq=0 ttl=64 time=0.062 ms
64 bytes from 10.113.1.2: seq=1 ttl=64 time=0.155 ms

如此一來可以用此概念(搭配 Bridge)完成一些複雜的網路情境

使用 Bridge

實際的網路情況不太可能都用 peer 去接,就達成一個區網內有多台 Host 還是需要使用 Linux Bridge。就像 Docker 預設的 Bridge 模式般,那我們可不可以不要透過 Docker,自己建立 Bridge 來達成網路隔離,答案是可以。自己建立的話你會發現 Docker 除了把 Bridge 建立好以外,還會使用 iptables 把 Container 中的流量從 Host 往外送,我們如果不想要讓封包有機會從 Host 流出,自己建立 Bridge 把 veth peer 一端接上 Container 一端接上 Bridge 也是一個方法!

首先,一樣建立兩個 Containers

1
2
docker run -itd --network=none --name left alpine
docker run -itd --network=none --name right alpine

接下來手動建立 Linux Bridge

1
2
brctl addbr br0
ip link set dev br0 up

建立 veth peer 並且命名為 left-eth0, left-veth0
left-eth0 這端要放入 Container 的 Network Namespace 中

1
2
ip link add dev left-eth0 type veth peer name left-veth0
ip link add dev right-eth0 type veth peer name right-veth0

此時你的 Host 上會有四個新增的介面

取得 Containers 的 Pid 用來將 peer 加入該 Pid 的 Network Namespace

1
2
3
4
5
left_pid=$(docker inspect left -f {{.State.Pid}})
right_pid=$(docker inspect right -f {{.State.Pid}})

ip link set left-eth0 netns ${left_pid} name eth0
ip link set right-eth0 netns ${right_pid} name eth0

加入成功後,在 Host 上可以看到網卡從剛才的四個變成兩個

在 Container 中也會看到裡面的介面多了 eth0

將 veth peer 另一端接上我們自行新增的 Linux Bridge,並且將網卡啟動

1
2
3
4
5
brctl addif br0 left-veth0
brctl addif br0 right-veth0

ip link set dev left-veth0 up
ip link set dev right-veth0 up

接下來我們要從 Host 上設定 Container 中的介面,因為該已經在不同網路空間,所以我們需要自行 mapping,不然 iproute2 會找不到該網路介面

1
2
3
ln -s /proc/$left_pid/ns/net /var/run/netns/$left_pid
ln -s /proc/$right_pid/ns/net /var/run/netns/$right_pid
ip netns show

那為何不在 Container 中做設定?因為這樣子需要 Container 具備特定的 Linux Capability

1
2
3
4
ip netns exec $left_pid ip addr add 10.113.1.1/24 dev eth0
ip netns exec $left_pid ip link set eth0 up
ip netns exec $right_pid ip addr add 10.113.1.2/24 dev eth0
ip netns exec $right_pid ip link set eth0 up

接下來就可以使用 ping 來檢查是否 Containers 之間網路有通

docker exec left ping 10.113.1.2 -c3
docker exec right ping 10.113.1.1 -c3

如果沒通的話,可以看一下 iptables 的 FORWARD chain 是不是被 DROP 掉了,是的話先把 Policy 改成 ACCEPT 即可

通常我們使用 Docker 去建立 Bridge 的話,這些 iptables 規則都是 Docker 幫我們處理的,所以我們現在需要自行修改 iptables。

題外

Docker 提供的網路選項不多,畢竟他主要是用來做資源隔離。除了以上問題沒辦法用內建的網路選項外,一個 Container 假設有多個網路環境,也沒辦法在 Container 啟動時指定預設路由,只能用比較拐彎抹角的方式在 entrypoint 或是 commands 做。