イーサネットフレームはなぜDockerを使用した仮想環境ではパディングされないのか?
はじめに
最近、「体験しながら学ぶネットワーク技術入門」という技術体験書を読みました。tinetを使って構築した仮想ネットワーク環境の中で色々操作して"インターネット"を体感してみようという意欲的な内容で、コマンド操作を通じてPCを立ち上げることが単に楽しかったあの頃に里戻りした気がします。
その書籍の「イーサネットペイロード」の注釈で気になる記述がありました。二章の途中から該当箇所を引用します。
イーサネットペイロードは最小データサイズが決まってるよ、その為にパディングを追加することがあるよ、でもDockerなどの環境は例外だよという話でした。
なぜ、Dockerではパディングが付加されないのか?どんな環境ならパディングが付加されるのか?
そもそも、イーサネットペイロードは可変値なのに、パディングという最低値を保証する仕様があるのか?
調べてみました。
実際に動かして確認してみる
まずは皆大好きpingコマンドでこの記述は本当なのか確かめてみます。
具体的には2つのサーバーを用意して、送信側のサーバーでpingを実行し、受信側のサーバーでtcpdumpでパケットを捕まえた後、中身を解析してイーサネットペイロードのサイズを調べてみます。
コマンド解説
ping、その実態はICMP(Internet Control Message Protocol)のEcho Request/Replyメッセージを送受信し、
①到達性: 相手のコンピューターにデータが到達するかどうか
②往復遅延: データを送受信するのにどれくらい時間がかかったか
を計測するツールです。
ICMPには他にも色々なメッセージがありますが、pingはこのうち2つを利用してるにすぎません。
コマンドはこんな感じで送ります。
# 送信先のIPアドレスに5回ICMPのメッセージを送る
ping -c 5 -s 0 ${送信先のIPアドレス}
このコマンドを実行すると、
①IPのヘッダー情報
②ICMPのヘッダー情報
③ICMPのデータ情報
④イーサネットフレームのパディング?
がイーサネットペイロードに入って送られます。④番が今回知りたい情報です。
pingで送信する際に「素」で使う人が多いと思いますが、何もオプションをつけないと、ICMPのデータ部にダミーデータ(pad bytes=これ自体はイーサネットフレームと無関係)が入って46Bを確実に超えてしまうので、データ部が0になるように引数で「-s 0」で設定しています。
そのメッセージを受信側のコンピューターでキャプチャするのがtcpdumpです。
tcpdump -i enp3s0 icmp -w ~/capture.pcap
このコマンドは、ネットワークインターフェース enp3s0 を流れるICMPのメッセージ(たとえば ping コマンドによる通信)をキャプチャし、その内容をcapture.pcapに保存します。
ping で送られた ICMP Echo Request メッセージは、鏡のように自動で返ってくるわけではありません。宛先のコンピューターがそのメッセージを受け取り、内容を確認し、適切に処理したうえで ICMP Echo Reply として送り返す必要があります。今回は受け取り手側のLinuxカーネルが役割を担っています。
tcpdump は、そのような Request/Reply のやりとりをネットワークの途中で「盗み見る」(=パケットを傍受・観察する)ツールです。通常は管理者や開発者が、通信の内容を調査・デバッグするために使用します。
実験
Docker間の通信をキャプチャしたファイルをdocker.pcap、Linux物理サーバー間の通信をキャプチャしたファイルをserver.pcapとして取得し、下記のコマンドで解析します。[1]
- 実行例
user@UBUNTU:/work# tshark -r ./test.pcap -Y "frame.number==1" -T fields -e frame.number -e frame.len -e eth.type -e eth.padding -e icmp.type -E header=y -E separator=/t
frame.number frame.len eth.src eth.dst eth.type eth.padding ip.src ip.dst icmp.type
1 60 0x00000800 000000000000000000000000000000000000 8
- 比較結果
docker.pcap | server.pcap | |
---|---|---|
frame.number | 1 | 1 |
frame.len | 42 | 60 |
eth.type | 0x00000800(IPv4) | 0x00000800(IPv4) |
eth.padding | (なし) | 000000000000000000000000000000000000 |
icmp.type | 8 | 8 |
たしかに、イーサネットペイロードのサイズが異なっています。また、サーバー間の通信にのみパディングが存在することもわかりました。
なぜ送信するネットワーク経路によってイーサネットペイロードのサイズは異なるのか?
送信先はコンテナかサーバーかという違いはあれど、ICMPのメッセージを処理するLinuxカーネルの処理はほぼ一緒です。
このパディングがどこで付与されたのか?その理由は通信経路にあります。
データ通信は玉ねぎなんかによくたとえられるそうです。玉ねぎの核にあたるICMPパケットを送るために周りをさらに色んな皮で覆っていくところを想像してみてください。たとえば、ICMPリクエストを送ろうとすると、実際の通信は、ICMP → TCP → IP → イーサネットという順に何重にも包まれて、最終的にデータリンク層であるイーサネットの形式に変換されて送信準備が整います。
ここで登場するのが 物理NIC(Network Interface Card) です。NICは、カーネルから渡されたIPパケットに対してイーサネットヘッダーを付け加え、イーサネットフレームとして完成させます。このとき、フレーム全体のサイズが64バイト未満であれば、イーサネットの仕様(IEEE 802.3)に従って、パディングを追加し、強制的に最小サイズに調整します。
一方、Dockerなどの仮想化環境では、通信経路が物理NICを通らず、カーネル内の仮想ネットワークスタック内で完結します。たとえば、コンテナ間通信では、veth(仮想Ethernet)ペアと呼ばれる仮想インターフェース同士をブリッジ経由で接続する形でパケットがやりとりされます。
要は物理NICを通らないのでパディングは付与されていません。
これを裏付けるために、Linuxサーバー間のtcpdumpの結果を再び振り返ってみましょう。
"1","0.000000","pingの送信元のIPアドレス","tcpdumpの受信IPアドレス","ICMP","60","Echo (ping) request id=0x0001, seq=1/256, ttl=64 (reply in 2)"
"2","0.000100","tcpdumpの受信IPアドレス","pingの送信元のIPアドレス","ICMP","42","Echo (ping) reply id=0x0001, seq=1/256, ttl=64 (request in 1)"
1行目のICMPのRequestメッセージと2行目のICMPのReplyメッセージは本来同じメッセージサイズのはずですが、実際は異なってますね。
ICMPのRequestメッセージでは物理NICを通った後なのでFCS(4byte)を除いたEthernetフレームの最小長である60byteがメッセージサイズとなります。
逆にReqlyメッセージでは、まだ物理NICを通っていないのでパディングが付与される前の素のEthernetフレーム長が計測されていることがわかります。
そもそも何故パディングは必要なのか?
ここまでの検証で、物理NICを通った場合のみパディングが付与されることがわかりました。
その理由を解き明かす前に、そもそもなぜパディングが必要とされたのか、イーサネットの歴史的経緯を紐解いてみましょう。
イーサネットは1973年にカリフォルニア州パロアルトのゼロックス研究所で初めて開発され、その後、1976年に開発者らによって論文『Ethernet: Distributed Packet Switching for Local Computer Networks』が発表されました。当時のイーサネットはバス型トポロジーを採用し、複数の端末が1本のケーブルを共有して通信を行う方式でした。
その後、DIXコンソーシアムという企業連合が主導してイーサネットの標準化が進み、1985年にIEEEが『IEEE 802.3 Carrier Sense Multiple Access With Collision Detection (CSMA/CD) Access Method and Physical Layer Specifications』として正式な規格を定めました。これが現在のIEEE 802.3の最初の規格であり、ここで導入されたのがCSMA/CD(Carrier Sense Multiple Access with Collision Detection)方式です。
CSMA/CD方式では、1本の同軸ケーブルを複数の端末が共有する「半二重バス」構造を取っています。そのため、複数の端末が同時に送信するとデータが衝突(コリジョン)し、正常に通信できなくなります。このデータ送信中の衝突を確実に検知するために設けられたのがSlotTime(衝突検知に必要な最小送信時間)でした。
その値は衝突検出に最も時間が掛かる場合を想定し、ここでは500mの同軸ケーブルがリピータで5本結合された場合の最大長2.5kmを上限として算出された。衝突検出に必要な最長の時間は、送信信号が通信路の左端から右端へ伝播した瞬間に右端で別の端末が送信開始した場合である。この場合は、信号が通信路全長を伝播する時間と、衝突が発生した信号が同じ道を戻る時間の合計となり、これは単純に全長の倍の距離の伝播時間となる。5kmの同軸ケーブルにおける伝播時間約26μSecと、リピータやトランシーバーの処理時間約20μSecの合計で46.38μSecと見積もられた。これは10メガビット・イーサネットでは464ビットに相当するため、マージンを取って最小フレームサイズは512ビット(64オクテット)時間と設計されている。
https://ja.wikipedia.org/wiki/CSMA/CD#cite_ref-7 より引用
つまり、最小64バイトという制約(およびパディング)は、「データ送信が終了する前に衝突を検知できる長さ」を保証するために導入されたのでした。
CSMA/CD方式はデータ衝突をそもそも引き起こさないネットワーク機器の進化(イーサネットスイッチなど)により廃れ、また後続の通信規格(1000BASE-T/Carrier Extension)では最小64バイトという制約も意味をなさなくなりましたが、現在でも後方互換性を保つため仕様として残されています。
物理的制約なので、veth(仮想Ethernet)等のネットワークドライバー等にはパディングの実装はありません。それを必要とする物理デバイスに任せればいいやという思想に基づくためです。中身のIPやTCPなどのデータはデータでヘッダーを持ち、その中にデータサイズの情報もあるため、パディングされていなくても問題なく動作します。
過去にはパディング絡みの脆弱性もあった
過去には物理NICのパディングの実装方法が仕様通りでない脆弱性をついた攻撃がありました。「EtherLeak(CVE-2003-0001)」です。これはかつて多くの物理NICドライバーが以前送ったイーサネットフレームの残骸を再利用してパディングしていたので[2]、意図して64Bに満たない小さいデータを送信することで、パディング領域に別のデータが載ってしまうことを利用したものでした。
3行まとめ
- イーサネットフレームは最小64バイトにするため物理NICがパディングを追加するが、Dockerの仮想環境などでは物理NICを通らないためパディングが発生しない。
- 最小フレームサイズの仕様は、CSMA/CD方式で衝突検知を保証するための物理的制約から生まれたもの。
- パディング自体は通信上必須ではなく後方互換性のために残されており、過去には実装ミスによる情報漏洩脆弱性(EtherLeak)も存在した。
-
Docker間通信の環境構築方法は是非「体験しながら学ぶネットワーク技術入門」をご参照ください。物理サーバー間通信に関しては以前ざつに作ったk8s環境のサーバーを再利用しています。 ↩︎
-
RFC1042にはパディングデータは0埋めしろよと書いてあったのに、一部のネットワークドライバーでは守られてなかったそうです。「When necessary, the data field should be padded (with octets of zero) to meet the IEEE 802 minimum frame size requirements.」 ↩︎
Discussion