L2Networkで自由なセグメントを作る¶
SubnetはIPv4を前提にしていて、SecurityGroupもNetworkACLもServiceもIPの上に載っています。IP以外のプロトコルを流したい、Podの中でbridgeを組みたい、DHCPサーバを自分で立てたい、という場合はSubnetでは足りません。
L2Networkは宛先MACアドレスだけで転送するセグメントです。JuneauはL3を一切解釈しないので、任意のEtherTypeと任意のIPプロトコルが通ります。 このガイドでは、CIDRを持たないL2Networkを2つのPodの追加NICに繋いで、自分で決めたアドレスで通信させます。
このガイドで構築するもの¶
- 専用Vpc (
lab-vpc) とSubnet 1つlab-subnet(10.91.0.0/24): Podのeth0を置くSubnet
- CIDRを持たないL2Network (
lab-net) - eth0が
lab-subnet、eth1がlab-netのPod 2つ - eth1に手で付けたアドレス同士でのpingと、非IPのフレームの通過
- gatewayを持つL2Network (
lab-gw-net) と、そこからSubnetのPodへの疎通
前提条件¶
- Juneauのcontroller/daemonが動作しているクラスター
- kubectlが利用可能なこと
手順¶
1. VpcとSubnetとL2Networkを作成¶
L2Networkはspec.vpcが必須です。default Vpcは使えないので、専用のVpcを作ります。
apiVersion: juneau.loutres.me/v1alpha1
kind: Vpc
metadata:
name: lab-vpc
---
apiVersion: juneau.loutres.me/v1alpha1
kind: Subnet
metadata:
name: lab-subnet
spec:
vpc: lab-vpc
cidr: 10.91.0.0/24
---
apiVersion: juneau.loutres.me/v1alpha1
kind: L2Network
metadata:
name: lab-net
spec:
vpc: lab-vpc
spec.cidrを書かなければJuneauはアドレスを配りません。VNIとMTUだけがstatusに入ります。
$ kubectl get l2network
NAME VPC CIDR MTU READY
lab-net lab-vpc 1450 True
2. L2Networkに繋がるPodを2つ作成¶
eth0は今まで通りjuneau.loutres.me/subnetで、追加NICはjuneau.loutres.me/networksで指定します。エントリにsubnetではなくl2Networkを書きます。
apiVersion: v1
kind: Pod
metadata:
name: lab-a
annotations:
juneau.loutres.me/subnet: lab-subnet
juneau.loutres.me/networks: |
[
{"interface": "eth1", "l2Network": "lab-net"}
]
spec:
containers:
- name: shell
image: nicolaka/netshoot:v0.16
command: ["sleep", "3600"]
securityContext:
capabilities:
add: ["NET_ADMIN"]
---
apiVersion: v1
kind: Pod
metadata:
name: lab-b
annotations:
juneau.loutres.me/subnet: lab-subnet
juneau.loutres.me/networks: |
[
{"interface": "eth1", "l2Network": "lab-net"}
]
spec:
containers:
- name: shell
image: nicolaka/netshoot:v0.16
command: ["sleep", "3600"]
securityContext:
capabilities:
add: ["NET_ADMIN"]
自分でアドレスを振るためにNET_ADMINを付けています。DHCPサーバをセグメントに置く場合や、Podの中でbridgeを組む場合も同じです。
Podが起動したら、eth1にアドレスが付いていないことを確認します。
$ kubectl exec lab-a -- ip -4 addr show eth1
3: eth1@if42: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 ...
MTUの1450はL2Networkのstatus.mtuです。eth0の方は今まで通り1500のままで、L2NetworkのNICだけがセグメントのMTUを受け取ります。
3. 自分でアドレスを振って通信¶
$ kubectl exec lab-a -- ip addr add 192.168.50.1/24 dev eth1
$ kubectl exec lab-b -- ip addr add 192.168.50.2/24 dev eth1
$ kubectl exec lab-a -- ping -c 3 192.168.50.2
PING 192.168.50.2 (192.168.50.2) 56(84) bytes of data.
64 bytes from 192.168.50.2: icmp_seq=1 ttl=64 time=0.412 ms
Juneauは192.168.50.0/24を知りません。最初のARPリクエストはセグメントの全ポートに複製され、返ってきたARPリプライで両側のMACが学習されます。それ以降のフレームは学習したポートへ直接送られます。
Nodeを跨いでいても同じです。ARPリクエストは参加している全Nodeへ複製され、返事が来たNodeのVTEPが学習されます。
4. 学習の様子を確認¶
kubectl juneau bpf dumpでセグメントの転送テーブルを読めます。VNIはkubectl get l2network lab-net -o jsonpath='{.status.vni}'で確認できます。
$ kubectl juneau bpf dump l2_fdb --all-nodes --inner-key vni=4242
NODE VNI MAC IFINDEX VTEP_IP LAST_SEEN_NS
worker-1 4242 1a:2b:3c:00:00:01 42 0.0.0.0 913842771203
worker-1 4242 1a:2b:3c:00:00:02 0 10.0.0.12 913844015886
ifindexが入っているのがこのNodeのvethに居るMAC、vtep_ipが入っているのが別Nodeに居るMACです。300秒フレームを見なかったエントリは掃除されます。
フレームがどのhookを通ったかはkubectl juneau traceで追えます。どのNICの話なのかと、自分で振ったアドレスの両方を渡してください。
$ kubectl juneau trace --from-pod default/lab-a --from-interface eth1 --from-ip 192.168.50.1 \
--to-pod default/lab-b --to-interface eth1 --to-ip 192.168.50.2 --proto icmp
--from-ipには、そのPodが実際にそのアドレスから送るものを書いてください。probeはPodの中でpingを実行するだけなので、送信元アドレスはPodのルーティングが選びます。
5. IP以外のフレームが通ることを確認¶
3で通ったpingは、その前のARPが届いていたということです。ARPのEtherTypeは0x0806で、IPv4ではありません。tcpdumpで実際に流れているところを見ることができます。
$ kubectl exec lab-b -- ip neigh flush dev eth1
$ kubectl exec lab-b -- timeout 10 tcpdump -i eth1 -e -n arp
別のターミナルからpingを打つと、リクエストがブロードキャストで、リプライがユニキャストで返っているのが見えます。
$ kubectl exec lab-a -- ping -c 1 192.168.50.2
Subnetでは事情が違います。policyの付いたPodの非IPv4フレームはPOLICY_ETHERTYPE_DROPで落ち、ARPだけがデータプレーンの都合で例外として通ります。L2Networkにはpolicyの評価そのものがありません。IPv6もSTPも独自のEtherTypeも同じように通ります。
gatewayを足してVpcに繋ぐ¶
ここまでのセグメントは閉じています。lab-netのeth1同士は届きますが、lab-subnetのPodにも、外にも届きません。
spec.gatewayを書くと、セグメントに出口が生えます。ここからはspec.cidrを持つ別のL2Network (lab-gw-net) を作って、そのPodから同じVpcのSubnetのPodへ届くところまでを見ます。
1. CIDRとgatewayを持つL2Networkを作成¶
apiVersion: juneau.loutres.me/v1alpha1
kind: L2Network
metadata:
name: lab-gw-net
spec:
vpc: lab-vpc
cidr: 10.92.0.0/24
gateway: {}
gateway: {}だけで、アドレスはspec.cidrの先頭、つまり10.92.0.1になります。RouteTableはVpcのメインのものが使われます。
$ kubectl get l2network lab-gw-net -o jsonpath='{.status.gateway} {.status.gatewayMAC}{"\n"}'
10.92.0.1 92:1c:4d:0a:33:7e
同じVpcのRouteTableには、このセグメントへのconnected routeが自動で入ります。
$ kubectl get routetable lab-vpc -o jsonpath='{range .status.routes[*]}{.dst} {.subnet}{.l2Network}{"\n"}{end}'
10.91.0.0/24 lab-subnet
10.92.0.0/24 lab-gw-net
2. gatewayを使うPodを作成¶
spec.cidrがあるので、JuneauがNICにアドレスを配ります。手でアドレスを振る必要はありません。
apiVersion: v1
kind: Pod
metadata:
name: lab-c
annotations:
juneau.loutres.me/subnet: lab-subnet
juneau.loutres.me/networks: |
[
{"interface": "eth1", "l2Network": "lab-gw-net"}
]
spec:
containers:
- name: shell
image: nicolaka/netshoot:v0.16
command: ["sleep", "3600"]
$ kubectl exec lab-c -- ip -4 addr show eth1
3: eth1@if55: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 ...
inet 10.92.0.2/24 scope global eth1
デフォルトルートを持つのはPodの1枚目のNICだけなので、eth1側の経路はPodの中で自分で足します。
$ kubectl exec lab-c -- ip route add 10.91.0.0/24 via 10.92.0.1 dev eth1
3. Subnetのpodへ届くことを確認¶
lab-subnetにPodを1つ置いて、そのアドレスへpingを打ちます。
$ kubectl get pod lab-a -o jsonpath='{.status.podIP}{"\n"}'
10.91.0.5
$ kubectl exec lab-c -- ping -c 3 10.91.0.5
PING 10.91.0.5 (10.91.0.5) 56(84) bytes of data.
64 bytes from 10.91.0.5: icmp_seq=1 ttl=64 time=0.283 ms
パケットはeth1から出て、gatewayのMAC宛のフレームとしてgateway vethに渡り、そこから先はSubnetのPodと同じ経路を通ります。gatewayが何をするかを決めるのは、そのVpcのRouteTableです。
ClusterIP Serviceも同じ経路で叩けます。Podの中でServiceのCIDRをこのgatewayへ向けてください。
$ kubectl exec lab-c -- ip route add 10.96.0.0/12 via 10.92.0.1 dev eth1
$ kubectl exec lab-c -- curl -s -o /dev/null -w '%{http_code}\n' http://my-service.default.svc:80/
200
0.0.0.0/0を向ければ、NATGateway経由の外部にも同じ経路で出られます。ただしeth0のデフォルトルートを置き換えることになるので、eth0側の通信が要らない場合だけにしてください。
4. gatewayを通ったことを確認¶
$ kubectl juneau trace --from-pod default/lab-c --from-interface eth1 --from-ip 10.92.0.2 \
--to-pod default/lab-a --to-ip 10.91.0.5 --proto icmp
l2_egressでgateway MACへのredirectが出て、次にpod_egressがgateway vethで動き、fib lookupの後にSubnet側へ転送されるところまで並びます。
gatewayが宛先を引くために覚えたアドレスは、l2_arpで読めます。
$ kubectl juneau bpf dump l2_arp --inner-key vni=4243
VNI IPV4 MAC
4243 10.92.0.2 1a:2b:3c:00:00:03
ここに載っていないアドレス宛のパケットが来ると、gatewayはセグメントにARPリクエストを出して、そのパケットは落とします。返事が返れば次から通ります。同じアドレスへ聞くのは1秒に1回までで、聞いた時刻はl2_arp_probeに残ります。
$ kubectl juneau bpf dump l2_arp_probe --inner-key vni=4243
VNI IPV4 ASKED_NS
4243 10.92.0.7 882431907714
ここに載っていてl2_arpに載らないアドレスは、聞いても誰も答えていないアドレスです。
返事は聞いたNodeへ返ります。gatewayは全Nodeが同じMACで答えるので、ホストは自分の乗っているNodeのgatewayに返事をします。そのNodeが聞いてきたNodeへ送り直すので、そのL2NetworkのNICを1枚も持たないNodeのPodからでも、手で振ったアドレスやNICの後ろのbridgeの向こうのホストに届きます。誰が聞いたかは、答えを運ぶ側のNodeで読めます。
$ kubectl juneau bpf dump l2_arp_asker --inner-key vni=4243
VNI IPV4 VTEP_IP ASKED_NS
4243 10.92.0.204 10.89.0.11 882431907714
VTEP_IPが答えの送り先のNodeです。
注意点¶
eth0には使えません¶
CIDRを持たないL2NetworkのNICにはアドレスが載りません。コンテナランタイムはCNIの結果のeth0にアドレスが1つも無いとsandboxの作成を失敗させるので、L2Networkは追加NIC専用です。juneau.loutres.me/subnetもSubnet名しか受け付けません。
spec.cidrを書いたL2NetworkならJuneauがアドレスを配るので、この制限はかかりません。
セグメントの中にpolicyは効きません¶
SecurityGroupもNetworkACLも、L2Network上のNIC同士の通信には一切効きません。データプレーンがpolicyを読まないからです。どちらもgatewayを跨ぐ通信にだけ適用されるので、spec.networkACLもNICのSecurityGroupも、gatewayを持つL2Networkでしか書くことができません。
同じL2Networkに繋いだPod同士は、互いに何でもできます。テナント境界はspec.vpcで引いてください。
MACの詐称を止めません¶
誰がどのMACを名乗るかを制限していません。NICの後ろでbridgeを組んだりnested VMを動かしたりすると、NIC自身のものではないMACが必ず出てくるので、制限するとL2Networkの使い道が消えます。
ブロードキャストは全ポートに複製されます¶
ブロードキャストも、マルチキャストも、まだ学習していない宛先へのユニキャストも、セグメントの全ポートに複製されます。参加しているNodeが多いほどNodeを跨ぐ複製が増えるので、ブロードキャストの多いワークロードを大きなセグメントに置くときは気をつけてください。
追加NICには経路が入りません¶
spec.cidrのあるL2NetworkはNICにアドレスを配りますが、経路は入れません。デフォルトルートを持つのはPodの1枚目のNICだけで、2枚目が同じ経路を入れようとすると失敗してPodごと落ちるからです。gatewayの先へ出したい宛先は、Podの中で自分で経路を足してください。
gatewayを後から足すときはアドレスに注意¶
spec.gatewayは後から足すことができます。ただし、既定の.1をワークロードが持っている場合、webhookが拒否します。
$ kubectl apply -f l2network-with-gateway.yaml
Error from server (Forbidden): ... address 10.92.0.1 is already held by AllocationLease ...
spec.gateway.addressに空いているアドレスを書くか、そのアドレスを持っているPodを消してから足してください。
未解決の宛先への1発目は落ちます¶
gatewayがまだ知らないアドレス宛のパケットが来ると、ARPリクエストを出してそのパケットは捨てます。返事を待つ間パケットを持っておく手段がデータプレーンに無いためです。TCPは再送が拾うのでほとんど気付きませんが、ping -c 1のような単発は1つ目が返ってきません。kubectl juneau traceではMISS_L2_ARPの後にL2_ARP_ASKEDが並びます。
聞きに行くのはspec.cidrの中のアドレスだけです。その外へはl2_arpに載る道が無いので届きません。
返事はNodeを跨いでも届きます。gatewayは全Nodeが同じMACで答えるので、ホストの返事はそのホストが乗っているNodeのgatewayに渡ります。聞いたのが別のNodeでも困らないように、gateway宛のARPリプライはセグメントに参加している全Nodeへ配られ、各Nodeがそこからアドレスを読みます。この共有はkubectl juneau traceには出ません。ARPフレームにはtraceが引くタプルが無いためです。届いたかどうかはkubectl juneau bpf dump l2_arpで見てください。
ただし配る先はそのセグメントにNICを持つNodeだけです。セグメントのNICが1枚も無いNodeのPodからだと、Juneauが配っていないアドレスへは今も届きません。そのアドレスをNetworkEndpointに載せるか、対象のホストから一度何か外へ送ってください。
IPv6はgatewayを越えられません¶
セグメントの中のIPv6はBUMのフラッドで動きます。gatewayが解決に使うテーブルはIPv4専用なので、IPv6のパケットをgatewayへ送っても落ちます。
MTUを合わせてください¶
L2Networkの既定MTUは1450です。1500バイトのunderlayからVXLANの50バイトを引いた値で、underlayがこれと違う場合はspec.mtuで合わせてください。
非IPプロトコルはフラグメントできないので、MTUが大きすぎるとフレームが黙って消えます。IPv4ならPMTUDが救ってくれることもありますが、L2Networkで流すものはそうとは限りません。