同じ Linux マシンでも、コンテナの中とホストでは、見えるネットワーク接続口が違います。 これは Linux が、接続口や IP アドレスなどの設定を、別々に持てるためです。
この章では、その分かれたネットワークを 2 つ作り、仮想のケーブルでつなぎます。
最初は届かなかった ping が、設定を足すと届くようになる。
その変化を確かめながら、コンテナのネットワークを手で組み立てます。
GOAL この章のゴール
- 分離したネットワークを作り、互いに通信させられる
- 仮想のスイッチを使い、複数のネットワークをつなげられる
- 外部へ通信するための設定を加え、動作を確かめられる
コンテナごとにネットワークの見え方が違う理由
コンテナの中で ip addr を実行すると、そのコンテナ用の接続口が表示されます。
同じコマンドをホストで実行しても、一覧は同じになりません。
Linux がネットワークの設定一式を分けて持っているからです。
この区切りが network namespaceネットワークネームスペースネットワークの接続口、IP アドレス、経路などの設定を分けて持つ Linux の仕組み。netns と略す。用語集で見る です。
プロセスはどれか 1 つに属し、その中の接続口を使います。
Docker の通常のブリッジ構成では、コンテナ用の netns に eth0 と lo を用意します。
別々の netns を通信させるには、間をつなぐ必要があります。 これから作るのは、2 つの netns に仮想ケーブルの両端を 1 つずつ入れる構成です。
ここから先は、次の手順で組み立てます。
- netns を 2 つ作り、veth でつないで ping する。
- ブリッジを足して 3 つの netns をつなぎ、
docker0と同じ形にする。 nftで送信元アドレスを書き換え、外へ出られるようにする (前の章の masquerading です)。- ノードで動いている本物の Pod の netns を探す。
ノードの中に入る
この章はノード (KubeVirt VM) の中で root として作業します。
kubectl debug node/$(kubectl get node -o jsonpath='{.items[0].metadata.name}') -it \
--profile=sysadmin --image=busybox:1.37 -- chroot /host nsenter -t 1 -m -u -i -n -p -- bash抜けるときは exit。デバッグ用 Pod は自動では消えないので、終わったら kubectl delete pod -l app.kubernetes.io/managed-by=kubectl-debug かkubectl get pods で確認して消してください。
1. netns を 2 つ作って直結する
ip netns add で netns を作ると、NIC が lo しかない空っぽのネットワークの世界が 1 つできます。
ip-netns(8) によれば、名前付きの netns は /var/run/netns/<名前> のファイルとして置かれ、以降の ip netns exec はこのファイルを開いてその netns の中でコマンドを実行します。
ip netns add red
ip netns add blue
ip link add veth-red type veth peer name veth-blue
ip link set veth-red netns red
ip link set veth-blue netns blue
ip -n red addr add 10.10.0.1/24 dev veth-red
ip -n blue addr add 10.10.0.2/24 dev veth-blue
ip -n red link set lo up; ip -n red link set veth-red up
ip -n blue link set lo up; ip -n blue link set veth-blue up
ip netns exec red ping -c 2 10.10.0.2出力例
PING 10.10.0.2 (10.10.0.2) 56(84) bytes of data.
64 bytes from 10.10.0.2: icmp_seq=1 ttl=64 time=0.08 ms
64 bytes from 10.10.0.2: icmp_seq=2 ttl=64 time=0.05 msip link add … type veth peer name … は vethブイイーサ仮想イーサネットのペア。片方に入ったパケットがもう片方から出てくる、ケーブル 1 本相当の仮想デバイス。用語集で見る を 1 組作ります。
veth(4) は「veth デバイスは必ず相互に接続されたペアとして作られ、片方で送信したパケットは即座にもう片方で受信される」と説明しています。
作った直後は両端ともホストの netns にあり、ip link set <dev> netns <名前> で片方ずつ別の世界へ移します。
ip -n red … は ip netns exec red ip … の省略形です。
veth-red はホストの netns から red へ移ったので、ホスト側で ip link を打っても一覧に出ません。
NIC は同時に 1 つの netns にしか属せないからです。
コンテナの中から eth0 が 1 枚見えるだけなのも、同じ理由です。
2. ブリッジで 3 つ繋ぐ (docker0 を手で作る)
veth ペアはケーブル 1 本なので、直結できるのは 2 つの netns までです。 3 つ以上を繋ぐには、真ん中にスイッチが要ります。 Linux ブリッジがそれです。
さっきの直結ペアは一度消して、ブリッジ構成を組み直します。
ip netns del red; ip netns del blue
ip link add br-lab type bridge
ip addr add 10.10.1.1/24 dev br-lab
ip link set br-lab up
for i in 2 3 4; do
n=$(printf 'red\nblue\ngreen' | sed -n "$((i-1))p")
ip netns add "$n"
ip link add "veth-$n" type veth peer name "eth0" netns "$n"
ip link set "veth-$n" master br-lab up
ip -n "$n" addr add "10.10.1.$i/24" dev eth0
ip -n "$n" link set lo up; ip -n "$n" link set eth0 up
ip -n "$n" route add default via 10.10.1.1
done
ip netns exec red ping -c 1 10.10.1.3
ip netns exec green ping -c 1 10.10.1.2
bridge link show br-lab出力例
64 bytes from 10.10.1.3: icmp_seq=1 ttl=64 time=0.06 ms
64 bytes from 10.10.1.2: icmp_seq=1 ttl=64 time=0.06 ms
12: veth-red@if2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 master br-lab state forwarding ...
13: veth-blue@if2: ... master br-lab ...
14: veth-green@if2: ... master br-lab ...bridge link show は、ブリッジにつながったポート (ここでは 3 本の veth のホスト側) とその状態を表示します。
この手順は、Docker が docker0 と veth で毎回やっていることと同じです。
違いはブリッジの名前と IP 帯だけです。
peer name eth0 netns red のように、作った瞬間に片方を別の netns へ送ることもできます。
コンテナの中の NIC がどれも eth0 という名前なのは、こうして netns ごとに名前の空間が分かれているからです。
3. nft で外に出す
netns の中から外のネットワークへ ping してみます。 この時点では失敗します。
GW=$(ip route | awk '/^default/ {print $3; exit}')
echo "gateway: $GW"
ip netns exec red ping -c 1 -W 1 "$GW" || echo "届かない"パケットは red → br-lab → ノードの eth0 → ゲートウェイと進みますが、送信元が 10.10.1.2 のままです。
ゲートウェイは 10.10.1.0/24 への戻り経路を知らないので、返事が帰ってきません。
この戻り経路の問題を解くのが NAT です。
nftablesエヌエフテーブルズLinux カーネルの netfilter にルールを入れる現在の標準フレームワークと、その CLI nft。iptables の後継で、テーブル / チェーン / ルールに加えてセットやマップ (vmap) を持つ。用語集で見る でルールを入れます。
nft(8) の用語では、テーブルはチェーンの入れ物、チェーンのうち base chain はカーネルのネットワークスタックからパケットが入ってくる入口 (hook) に結びついたもので、ルールはその中に並びます。
手順は「テーブルを作る → hook に繋がったチェーンを作る → ルールを足す」の 3 段です。
sysctl -w net.ipv4.ip_forward=1
nft add table ip lab
nft 'add chain ip lab post { type nat hook postrouting priority srcnat; policy accept; }'
nft add rule ip lab post ip saddr 10.10.1.0/24 oifname != "br-lab" masquerade
nft list table ip lab
ip netns exec red ping -c 2 "$GW"出力例
table ip lab {
chain post {
type nat hook postrouting priority srcnat; policy accept;
ip saddr 10.10.1.0/24 oifname != "br-lab" masquerade
}
}
64 bytes from 10.0.1.1: icmp_seq=1 ttl=64 time=0.4 msip_forward=1 はノードがルーターとして転送してよいという宣言です (Kubernetes のノードでは最初から 1 になっています)。
postrouting はシステムから出ていく全パケットが通る hook で、srcnat はその hook での標準の優先度の名前です。
masquerade は nft(8) が「SNAT の特殊形で、送信元アドレスを出力インターフェースのアドレスにする」と説明する動作です。
nat 型のチェーンを通るのは接続の最初のパケットだけで、以降のパケットと戻りパケットは conntrackコントラックnetfilter の接続追跡。NAT で書き換えた通信の対応表を持ち、戻りパケットを自動で逆変換する。用語集で見る の対応表に従って書き換えられます。
前の章の -p による宛先の書き換え (DNAT) も、Kubernetes の NodePort で外から入った通信の送信元書き換えも、この masquerade と同じく netfilter の NAT で行われます。
4. 本物の Pod の netns を見る
ノードで動いている Pod も、同じ道具でできています。
containerd が Pod ごとに netns を作り、/run/netns/cni-<uuid> という名前を付けるので、ip netns list に並びます。
ip netns list | head
NS=$(ip netns list | awk '/cni-/ {print $1; exit}')
ip -n "$NS" addr show eth0
ip -n "$NS" route
ip link show type veth | grep -c lxc出力例
cni-3f1e8a2b-... (id: 3)
cni-9c0d44aa-... (id: 2)
...
12: eth0@if13: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 ...
inet 10.244.0.23/32 scope global eth0
default via 10.244.0.87 dev eth0 mtu 1450
10.244.0.87 dev eth0 scope link
7Pod の eth0 も veth で、ホスト側の片割れは lxc で始まる名前です (Cilium の流儀)。
このクラスタのネットワークプラグインである Cilium (第 4 章で扱います) は、その片割れに eBPF プログラム (カーネルの中で動かす小さなプログラム) を取り付け、パケットが出てきた時点で行き先を決めます。
ブリッジには接続しません。
Pod の IP が /32 なのは、同じサブネットの隣人をブリッジ経由で探す構成ではないからです。
デフォルトルートの via 10.244.0.87 はホスト側にある cilium_host という NIC の IP で、Pod はこの IP 宛てに ARP を打ち、Cilium が代わりに答えます (ブリッジ構成なら、ここにブリッジの IP が入ります)。
この続きは第 4 章で扱います。
片付け
for n in red blue green; do ip netns del "$n" 2>/dev/null; done
ip link del br-lab 2>/dev/null
nft delete table ip lab 2>/dev/null
ip netns list | grep -v cni- || echo "clean"
exitexit でノードから抜けたら、デバッグ用の Pod も消しておきます。
kubectl delete $(kubectl get pods -o name | grep node-debugger) --wait=falseふりかえり
Qip link set veth-red netns red を実行した後、ホストの netns で ip link を打つとどうなる?
NIC は同時に 1 つの netns にしか属せません。red に移した veth-red はホスト側からは消え、ペアの veth-blue はまだホストに残っています。
Qmasquerade を入れる前に netns から外へ ping が通らなかった理由は?
行きのパケットは届きます。戻りの宛先 10.10.1.2 がゲートウェイから見て未知のネットワークなので返事が帰れません。masquerade で送信元をノードの IP に書き換えると、戻りはノードに届き、conntrack が元に戻します。
QCilium の Pod で eth0 の IP が /32 なのはなぜ?
ブリッジ構成では、同じブリッジにつながった相手と直接話せるよう、/24 などのサブネットを持たせます。Cilium では veth のホスト側で eBPF がパケットを拾って転送するので、隣人を探す必要が無く、/32 と「デフォルトルートはホスト側の IP」という設定で足ります。
参考
- network_namespaces(7) man page (netns が隔離する資源、veth ペアによる接続) https://man7.org/linux/man-pages/man7/network_namespaces.7.html
- veth(4) man page https://man7.org/linux/man-pages/man4/veth.4.html
- ip-netns(8) man page (/var/run/netns/NAME、ip netns exec) https://man7.org/linux/man-pages/man8/ip-netns.8.html
- bridge(8) man page (bridge link show) https://man7.org/linux/man-pages/man8/bridge.8.html
- nft(8) man page (テーブル / チェーン / hook、nat チェーン、masquerade、priority の名前) https://www.netfilter.org/projects/nftables/manpage.html
- Cilium ドキュメント「Routing」 https://docs.cilium.io/en/stable/network/concepts/routing/