# 計算機網路 - Network Namespace [TOC] ## References ### Linux Bridges, IP Tables, and CNI Plug-Ins - A Container Networking Deepdive 這個影片中除了介紹 `veth` 機制之外,最前面也有簡單介紹一些術語如何使用 `ip-link`、`brctl` 等命令列工具。在後面的實驗中,使用如 `brctl` 這類舊工具的命令,都會以 `bridge` 或 `ip` 當中對應的命令取代。 {%youtube z-ITjDQT7DU %} 這個影片中的實驗需要有 `dummy` 這個模組。所以如果在使用 `ip link add` 時出現以下的錯誤訊息: ``` $ sudo ip link add dummy0 type dummy Error: Unknown device type. ``` 那麼可以先試著載入這個模組: ``` $ sudo modprobe dummy ``` 如果出現類似下面的錯誤訊息的話: ``` modprobe: FATAL: Module dummy not found in directory /lib/modules/5.13.0-1011-raspi ``` 則可以試著安裝以下的套件: ``` $ sudo apt install linux-modules-extra-`uname -r` ``` ### Container Networking From Scratch - Kristen Jacobs, Oracle 下面這個影片中的範例 repo 在[這裡](https://github.com/kristenjacobs/container-networking)。 {%youtube 6v_BDHIgOY8 %} ### SREcon19 Asia/Pacific - Software Networking and Interfaces on Linux 這個影片主要是介紹 Linux 中的 interfaces, bridge, tap 這些術語,跟 network namespace 關係較小。不過在不同的 network namespcae 之間 {%youtube HiktxCMF03A %} ### Tutorial: Communication Is Key - Understanding Kubernetes Networking - Jeff Poole, Vivint Smart Home {%youtube Slce9Nu-NB0 %} 如果比較想知道跟 Kubernetes 有關的介紹,下面有更多: 1. [*Webinar: Kubernetes and Networks: Why is This So Dang Hard?*](https://youtu.be/GgCA2USI5iQ) 2. [*Intro + Deep Dive: Kubernetes (Network) SIG - Tim Hockin, Google*](https://youtu.be/BxDnv7MpJ0I) 3. [*Kubernetes Networking Intro and Deep-Dive - Bowei Du & Tim Hockin, Google*](https://youtu.be/tq9ng_Nz9j8) ## 簡介 關於 Namespace 的一般介紹可以參考[這裡](https://hackmd.io/@0xff07/sp/https%3A%2F%2Fhackmd.io%2F%400xff07%2Fr1wCFz0ut)。簡單地來說,Namespace 是 Linux 中的一種隔離機制。屬於同一個 namespace 的行程,會以為那些被 namespace 隔離出來的資源就是這個作業系統中全部的資源。若一個行程被 mount namesapce 隔離(或說「在那個 namesapce 裡面」),該行程就只能看到特定的 mount point; 而屬於同一個 PID namespace 的行程,在該 namespace 中只看得到那些「跟它屬於同一個 PID namespace」的行程,並且可以有另外一個 PID。這種「限制一個行程只能看到某些資源」也是實作 container 的其中一個基礎設施。 而 network namespace 如其名稱所示,用來隔離作業系統跟網路相關的資源。被 network namespace 隔離的那些行程,會以為自己獨享了一個完整的 network stack。或者更明確地,按照 `man` 中的 [`network_namespaces(7)`](https://www.man7.org/linux/man-pages/man7/network_namespaces.7.html) 中的說法: > *Network namespaces provide isolation of the system resources associated with networking: network devices, IPv4 and IPv6 protocol stacks, IP routing tables, firewall rules...* 也就是說:不同的 network namespce 中的行程會看見不同的 interface、routing table、netfilter 規則等等。 雖然可以用 `unshare -n` 建立一個 network namespace,但更常見的可能是使用 [`ip netns`](https://man7.org/linux/man-pages/man8/ip-netns.8.html)。不同的 network namespace 可以各自有自己的 interface、IP、routing table、port mapping 等等。這些隔離出來的 namespace 之間可以使用虛擬的 bridge 連接起來。關於這個命令更詳細的行為可以在 [`ip-netns(8)`](https://www.man7.org/linux/man-pages/man8/ip-netns.8.html) 找到。 ## 例子一:兩個 Network Namespace 用 veth 連接 本來的配置是參考自 [*Create Your Own Network Namespace*](https://itnext.io/create-your-own-network-namespace-90aaebc745d)。這之類的配置很常被拿來展示各種網路基礎設施的例子,比如 [xdp-project/bpf-examples](https://github.com/xdp-project/bpf-examples) 中的 [tc-basic-classifier](https://github.com/xdp-project/bpf-examples/tree/master/tc-basic-classifier) 中有使用類似的配置來展示 eBPF Qdisc classifier 怎麼使用; 而 [xdp-project/xdp-tutorial](https://github.com/xdp-project/xdp-tutorial) 中也有在 `veth` 上使用 XDP 的例子。裡面使用的架構是這樣: ![](https://raw.githubusercontent.com/xdp-project/bpf-examples/007c0b6883e8ee45fed9d3e09233b788e5b9df3f/tc-basic-classifier/overview.png) 而現在會重製的是 Namespace 的部分,也就是像下面這樣: ![](https://i.imgur.com/MmeOTNw.png) ### 前置準備 為了方便 ```shell $ L_IP=172.16.16.10 $ R_IP=172.16.16.20 $ L_CIDR="${L_IP}/24" $ R_CIDR="${R_IP}/24" $ L_NS="left" $ R_NS="right" $ L_DEV="$L_NS-veth" $ R_DEV="$R_NS-veth" ``` ### 建立兩個 Network Namespace 首先,建立兩個 network namespace: ```shell $ sudo ip netns add "$L_NS" $ sudo ip netns add "$R_NS" ``` 這時候如果使用 `ip-netns` 命令中的 `list` 選項,可以發現多出了剛剛新增的 namespace: ```shell $ ip netns list right left ``` ### 建立 `veth` Pair 接著建立一對 `veth`,假定兩端叫做 `veth0` 與 `veth1` ```shell $ sudo ip link add "$L_DEV" type veth peer "$R_DEV" ``` 這時候如果使用: ```shell $ ip link ``` 應該會出現 `left-veth` 與 `right-veth`: ```clike 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 2: eth0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc mq state DOWN mode DEFAULT group default qlen 1000 link/ether dc:a6:32:dc:90:8f brd ff:ff:ff:ff:ff:ff 3: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DORMANT group default qlen 1000 link/ether dc:a6:32:dc:90:90 brd ff:ff:ff:ff:ff:ff 4: right-veth@left-veth: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000 link/ether 32:1f:54:00:55:2d brd ff:ff:ff:ff:ff:ff 5: left-veth@right-veth: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000 link/ether 52:08:25:fb:ae:f0 brd ff:ff:ff:ff:ff:ff ``` > TODO: 上面輸出的不同欄位的意思 ### 將 `veth`-pair 兩端各自移至 Namespace 中 接著把 `left-veth` 與 `right-veth` 這兩個 interface 分別移到 `left` 與 `right` 兩個 network namespace 中: ```shell $ sudo ip link set "$L_DEV" netns "$L_NS" $ sudo ip link set "$R_DEV" netns "$R_NS" ``` 這時候如果在預設的 namespace 中檢視: ```shell $ ip link ``` 就會發現剛剛的 `veth0` 與 `veth1` 又不見了: ```clike 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 2: eth0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc mq state DOWN mode DEFAULT group default qlen 1000 link/ether dc:a6:32:dc:90:8f brd ff:ff:ff:ff:ff:ff 3: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DORMANT group default qlen 1000 link/ether dc:a6:32:dc:90:90 brd ff:ff:ff:ff:ff:ff ``` 但是另外一方面,在 `$L_NS` 這個 network namespace 中去檢視: ```shell $ sudo ip -n "$L_NS" link ``` 就會發現 `left-veth` 其實在這裡面: ```clike 1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 5: left-veth@if4: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000 link/ether 52:08:25:fb:ae:f0 brd ff:ff:ff:ff:ff:ff link-netns right ``` 類似地,如果在 `$R_NS` 這個 netowrk namespace 中做一樣的事情: ```shell $ sudo ip -n "$R_NS" link ``` 也會找到 `right-veth` 在這當中: ```clike 1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 4: right-veth@if5: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000 link/ether 32:1f:54:00:55:2d brd ff:ff:ff:ff:ff:ff link-netns left ``` ### 分配 IP 位址 接著幫 `left-veth` 與 `right-veth` 分配 IP 位址,分別是 `$L_CIDR` 與 `R_CIDR`: ```shell $ sudo ip -n "$L_NS" addr add "$L_CIDR" dev "$L_DEV" $ sudo ip -n "$R_NS" addr add "$R_CIDR" dev "$R_DEV" ``` > 這個網段是 *private IP*,可以參考 [*Public IP vs. Private IP and Port Forwarding (Explained by Example)*](https://youtu.be/92b-jjBURkw) 這個簡介。 這時,如果在 `left-vnet` 中檢查這個 namespace 中的 interface 對應的位址: ```shell $ sudo ip -n "$L_NS" addr ``` 就會找對剛剛指定的 IP 位址: ```clike 1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 5: left-veth@if4: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000 link/ether 52:08:25:fb:ae:f0 brd ff:ff:ff:ff:ff:ff link-netns right inet 172.16.16.10/24 scope global left-veth valid_lft forever preferred_lft forever ``` 類似地,在 `right-vnet` 中: ```shell $ sudo ip -n "$R_NS" addr ``` 則會出現: ```clike 1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 4: right-veth@if5: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000 link/ether 32:1f:54:00:55:2d brd ff:ff:ff:ff:ff:ff link-netns left inet 172.16.16.20/24 scope global right-veth valid_lft forever preferred_lft forever ``` ### 啟動 `veth` 與 `lo` ```shell $ sudo ip -n "$L_NS" link set "$L_DEV" up $ sudo ip -n "$L_NS" link set lo up ``` 這時,如果再檢視 `vnet0` 中 namespace 中的狀況: ```shell $ sudo ip -n vnet0 link show ``` 就會發現狀態變成 `UP`: ```clike 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 5: left-veth@if4: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state LOWERLAYERDOWN mode DEFAULT group default qlen 1000 link/ether 52:08:25:fb:ae:f0 brd ff:ff:ff:ff:ff:ff link-netns right ``` 類似地: ```shell $ sudo ip -n "$R_NS" link set "$R_DEV" up $ sudo ip -n "$R_NS" link set lo up ``` 若檢視其中的 interface: ```shell $ sudo ip -n "$R_NS" link show ``` 之後也會發現類似的結果: ```clike 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 4: right-veth@if5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000 link/ether 32:1f:54:00:55:2d brd ff:ff:ff:ff:ff:ff link-netns left ``` ### ping 這時候再從 `left` 中執行 `ping`: ```shell $ sudo ip netns exec "$R_NS" ping "$L_IP" ``` 就會發現可以順利的 `ping`: ``` PING 172.16.16.10 (172.16.16.10) 56(84) bytes of data. 64 bytes from 172.16.16.10: icmp_seq=1 ttl=64 time=0.098 ms 64 bytes from 172.16.16.10: icmp_seq=2 ttl=64 time=0.118 ms 64 bytes from 172.16.16.10: icmp_seq=3 ttl=64 time=0.119 ms 64 bytes from 172.16.16.10: icmp_seq=4 ttl=64 time=0.084 ms 64 bytes from 172.16.16.10: icmp_seq=5 ttl=64 time=0.115 ms ... ``` 類似地,在 `right` 中也可以 `ping` 到 `left`: ```shell $ sudo ip netns exec "$L_NS" ping "$R_IP" ``` 就會發現可以成功 `ping` 了: ``` PING 172.16.16.20 (172.16.16.20) 56(84) bytes of data. 64 bytes from 172.16.16.20: icmp_seq=1 ttl=64 time=0.097 ms 64 bytes from 172.16.16.20: icmp_seq=2 ttl=64 time=0.121 ms 64 bytes from 172.16.16.20: icmp_seq=3 ttl=64 time=0.087 ms 64 bytes from 172.16.16.20: icmp_seq=4 ttl=64 time=0.085 ms 64 bytes from 172.16.16.20: icmp_seq=5 ttl=64 time=0.080 ms ... ``` ### 清理 如果想要回覆原狀,只要把兩個 namespace 移除即可: ```shell $ sudo ip netns delete "$L_NS" $ sudo ip netns delete "$R_NS" ``` ## 例子二:兩個 Namespace + 兩組 `veth` + 一個 bridge ![](https://i.imgur.com/jx2jw6u.png) 參考自 [*Using network namespaces and a virtual switch to isolate servers*](https://ops.tips/amp/blog/using-network-namespaces-and-bridge-to-isolate-servers/) ### Namespace 的 veth 參數 (一樣) 首先跟前面是差不多的,只是這時要建立兩對 `veth`,而且還要建立 bridge。 ```bash L_IP=172.16.16.10 R_IP=172.16.16.20 L_CIDR="${L_IP}/24" R_CIDR="${R_IP}/24" L_NS="left" R_NS="right" L_DEV="$L_NS-veth" R_DEV="$R_NS-veth" ``` ### Bridge 的參數 接著是跟 bridge 有關的參數: ```bash BR_IP=172.16.16.1 BR_CIDR="${BR_IP}/24" BR="br0" L_BR_DEV="$L_NS-$BR-veth" R_BR_DEV="$R_NS-$BR-veth" ``` ### 建立 Namespace (一樣) ```bash sudo ip netns add "$L_NS" sudo ip netns add "$R_NS" ``` ### 建立兩對 `veth`-pair ```bash sudo ip link add "$L_DEV" type veth peer "$L_BR_DEV" sudo ip link add "$R_DEV" type veth peer "$R_BR_DEV" ``` ### 移動 `veth`-pair 的其中一端到 Namespace 中 (一樣) ```bash sudo ip link set "$L_DEV" netns "$L_NS" sudo ip link set "$R_DEV" netns "$R_NS" ``` ### 指定 `veth` 的 IP (一樣) ```bash sudo ip -n "$L_NS" addr add "$L_CIDR" dev "$L_DEV" sudo ip -n "$R_NS" addr add "$R_CIDR" dev "$R_DEV" ``` ### 啟動 Namespace 中的 `veth` (一樣) ```bash sudo ip -n "$L_NS" link set "$L_DEV" up sudo ip -n "$L_NS" link set lo up sudo ip -n "$R_NS" link set "$R_DEV" up sudo ip -n "$R_NS" link set lo up ``` ### 建立一個 Bridge ```bash sudo ip link add name "$BR" type bridge ``` ### 將 `veth`-pair 的另外一端與 Bridge 連接 ```bash sudo ip link set "$L_BR_DEV" master "$BR" sudo ip link set "$R_BR_DEV" master "$BR" ``` ### 設定 Bridge 的 IP ```bash sudo ip addr add "$BR_CIDR" brd + dev "$BR" ``` ### 啟動連接 Bridge 上的 `veth` ```bash sudo ip link set "$L_BR_DEV" up sudo ip link set "$R_BR_DEV" up ``` ### 啟動 Bridge ```bash sudo ip link set "$BR" up ``` ## 例子三:使 Namespace 可以連到外面 在目前的狀況下去 `ping` 外面,比如說: ``` $ sudo ip netns exec left ping 8.8.8.8 ``` 會發現: ``` ping: connect: Network is unreachable ``` 首先,把所有(也就是剛剛兩個)namespace 的 default gateway 設定成 `br0` 的 IP: ```shell $ sudo ip -all netns exec ip route add default via "$BR_IP" ``` 接著使用 `nftables` 設定 NAT 的規則: ```shell $ sudo nft add table ip nat $ sudo nft add chain ip nat "POSTROUTING {type nat hook postrouting priority srcnat;}" $ sudo nft add rule ip nat POSTROUTING ip saddr "$BR_CIDR" counter masquerade ``` 這時候檢視 nftables 的規則: ```shell $ sudo nft list ruleset ``` 就會出現類似以下的內容: ```clike table ip nat { chain POSTROUTING { type nat hook postrouting priority srcnat; policy accept; ip saddr 172.16.16.0/24 counter packets 1 bytes 84 masquerade } } ``` 這邊的 NAT 指的是 Nework Address Translation。可以參考 [*Network Address Translation - NAT Explained*](https://youtu.be/RG97rvw1eUo) 與 [*Public IP vs. Private IP and Port Forwarding (Explained by Example)*](https://youtu.be/92b-jjBURkw)。而其中 masquerade 是一個 NAT 的特例。可以參考 nftables wiki 中的 [*Performing Network Address Translation (NAT)*](https://wiki.nftables.org/wiki-nftables/index.php/Performing_Network_Address_Translation_(NAT)#Masquerading) 的說明。 > 文章裡面本來是使用 `iptables`,這裡使用比較新的 nftables。可以使用 [`iptables-translate`](https://manpages.debian.org/bullseye/iptables/iptables-translate.8.en.html) 這個命令把 `iptables` 命令轉換為對應的 nftables 的命令。另外一個轉換方式是使用完 `iptables` 之後,直接用 `nft list ruleset` 來對照產生了什麼新規則(這兩個命令列工具有一樣的後端)。 最後啟動 IP forwarding: ```shell $ sudo sysctl -w net.ipv4.ip_forward=1 ``` 然後就可以在 namespace 裡面 `ping` 到外面的網路了,比如: ```shell $ sudo ip netns exec "$L_NS" ping 8.8.8.8 ```