- Linuxでユーザー空間TCP/IPスタックを自作しながらネットワーク層を学ぶシリーズの第一歩として、レイヤー2のイーサネットフレーム処理とARP応答から実装する
- カーネルの低レベルなネットワークトラフィックはTAPデバイスで受け取り、返されたファイルディスクリプタを通じて仮想デバイスのイーサネットバッファを
read/writeできる
- イーサネットヘッダは
dmac、smac、ethertype、payloadとして扱い、ethertypeの値が1536以上ならペイロード種別、それより小さければペイロード長を意味する
- ARPはIPv4アドレスのようなプロトコルアドレスを48ビットMACアドレスへ動的にマッピングし、要求に応答しながら変換テーブルを更新する流れで実装される
arpingテストでは、カスタムスタックのARP応答をLinuxカーネルが認識し、tap0インターフェースのARPキャッシュに10.0.0.4エントリが追加される
ユーザー空間TCP/IPスタックの出発点
- 目標は、Linuxで最小のユーザー空間TCP/IPスタックを実装しながら、ネットワークとシステムプログラミングをより深く理解すること
- TCPは30年以上にわたって複数の仕様が積み重なり複雑だが、実装の中核要素はTCPヘッダの解析、状態機械、輻輳制御、再送タイムアウト計算に絞って考えられる
- イーサネットとIPはTCPより複雑さが低いため、このシリーズはレイヤー2から実装を始める
TAPデバイスでイーサネットトラフィックを受け取る
- Linuxカーネルの低レベルなネットワークトラフィックを横取りするために、Linux TAPデバイスを使う
- TUN/TAPデバイスは、ユーザー空間のネットワーキングアプリケーションがそれぞれL3/L2トラフィックを操作する際によく使われる
- トンネリングは、パケットを別のパケットのペイロードの中に包む方式である
- OpenVPNのようなプログラムもTUN/TAPデバイスを使う
- レイヤー2からネットワークスタックを作るため、TUNではなくTAPデバイスが必要になる
- TAPデバイスは
/dev/net/tapを開き、ioctl(fd, TUNSETIFF, ...)で作成する
IFF_TAPはTAPデバイスを選択する
IFF_NO_PIはイーサネットフレームの前に不要なパケット情報が付かないようにする
- 作成後、返されたファイルディスクリプタ
fdで仮想デバイスのイーサネットバッファを読み書きする
イーサネットフレーム形式
- イーサネットはLANでコンピュータを接続する基盤技術であり、最初のイーサネット標準は1980年にDigital Equipment Corporation、Intel、Xeroxが発表した
- 初期バージョンは約10Mb/sの速度と半二重通信を使っていたため、データフローを調整するMACプロトコルが必要だった
- 半二重のイーサネットインターフェースでは、MAC方式としてCSMA/CDが必要となる
- 100BASE-Tはtwisted-pair配線を用いて全二重通信とより高いスループットを可能にする
- イーサネットスイッチが広く普及したことで、CSMA/CDの必要性は概ね薄れた
- イーサネット標準はIEEE 802.3ワーキンググループが管理している
実装で使うイーサネットヘッダ
- 実装ではLinuxの
if_ether.hを取り込み、ethertypeと16進数値の対応を利用する
- イーサネットヘッダはC構造体で次のフィールドとして表現される
dmac: 宛先MACアドレス
smac: 送信元MACアドレス
ethertype: ペイロード長または種別
payload: ARPまたはIPv4パケットを保持するペイロードポインタ
ethertypeは2オクテットのフィールドで、値によって意味が変わる
- 値が1536以上なら、IPv4やARPのようなペイロード種別を表す
- それより小さい値なら、ペイロード長を表す
- イーサネットフレームにはVLANやQoSを示すタグが付くことがあるが、この実装ではフレームタグを除外している
- ペイロード長がタグなしで最小要件である48バイトより小さい場合、末尾にパディングバイトが付加される
- Ethernet Frame Formatの末尾にはCRCで完全性を確認するFrame Check Sequenceフィールドがあるが、この実装では処理しない
イーサネットフレームのパース方法
- 構造体宣言の
packed属性は、GNU Cコンパイラがデータ整列用のパディングバイトで構造体メモリレイアウトを最適化しないようにする
- 実装でのパースは、バッファを適切なプロトコル構造体へ型キャストする方式である
- 例:
struct eth_hdr *hdr = (struct eth_hdr *) buf;
- より移植性の高い方法は、プロトコルデータを手動でシリアライズすること
- この場合、コンパイラはプロセッサごとのデータ整列要件に合わせてパディングバイトを追加する可能性がある
- 受信したイーサネットフレームの処理は単純な流れに従う
- TAPデバイスからバッファを読む
init_eth_hdr(buf)でイーサネットヘッダを初期化する
handle_frame(&netdev, hdr)がethertype値を見て次の動作を決める
ARPパケット構造と役割
- ARP(Address Resolution Protocol) は、IPv4アドレスのようなプロトコルアドレスを48ビットのイーサネットアドレスであるMACアドレスへ動的にマッピングする
- ARPはIPv4に限定されず、複数のL3プロトコルとともに利用できる
- 例としてCHAOSは16ビットのプロトコルアドレスを定義している
- 一般的なLAN通信では、サービスのIPアドレスを知っていても、実際の送信にはMACアドレスが必要になる
- ARPはネットワークへブロードキャスト問い合わせを送り、そのIPアドレスの所有者に自分のハードウェアアドレスを通知させる
ARPヘッダとIPv4向けペイロード
- ARPヘッダは次のフィールドで構成される
hwtype: リンク層タイプを表す2オクテットフィールドで、イーサネットの値は0x0001
protype: プロトコルタイプを表す2オクテットフィールドで、IPv4の値は0x0800
hwsize: ハードウェアアドレスサイズを表す1オクテットフィールドで、MACアドレスは6バイト
prosize: プロトコルアドレスサイズを表す1オクテットフィールドで、IPv4アドレスは4バイト
opcode: ARPメッセージ種別を表す2オクテットフィールド
opcodeの値は4種類に分かれる
- ARP request:
1
- ARP reply:
2
- RARP request:
3
- RARP reply:
4
- IPv4向けARPデータは
arp_ipv4構造体で扱う
smac: 送信者MACアドレス
sip: 送信者IPアドレス
dmac: 受信者MACアドレス
dip: 受信者IPアドレス
アドレス解決アルゴリズムとキャッシュ
- RFC 826のアドレス解決アルゴリズムは、ハードウェアタイプとプロトコルタイプを確認した後に変換テーブルを更新し、対象アドレスが自分自身なら応答を作る流れになっている
- translation tableはARPの結果を保存し、ホストがすでに持っているエントリをキャッシュから参照できるようにする
- このキャッシュは、重複したARP要求でネットワークが不要に埋まることを防ぐ
- 実装コードは
arp.cにある
ARP応答のテストと次のステップ
- ARP実装の最終テストは、要求に正しく応答するかを確認すること
arping -I tap0 10.0.0.4の実行結果、10.0.0.4から00:0C:29:6D:50:25のMACアドレスでユニキャスト応答が返る
- その後、
arpの出力でLinuxカーネルのARPキャッシュに10.0.0.4 ether 00:0c:29:6d:50:25 tap0エントリが現れる
- 最小限のイーサネットフレーム処理とARP実装だけでも、カスタムイーサネットデバイスがLinuxホストのARPキャッシュを埋めることを確認できる
- プロジェクトのソースコードはGitHubにあり、次のステップはICMP echo/replyであるpingとIPv4パケットのパース実装である
1件のコメント
Hacker Newsのコメント
数年前にCでユーザー空間ネットワークスタックを作ってみたことがあり、TUNインターフェースで生のパケットを処理して、ある程度動くようにしていた
今はIPアドレスやルートなどを設定できる簡単なシェルが入っていて、ネットワークパケットはmbufとsk_bufを混ぜたようなハイブリッド構造に格納している
ただ、UDPの実装を終えたあと、TCPを実装する時間もやる気も出ず、コードはここにある: https://github.com/cakturk/unet
当然、史上もっとも遅くて壊れやすい代物に近かったが、実際に動いたし、かなり面白くもあった。そのコードがまだどこかに残っていたらいいのだが
lwipの「POSIXポート」も同じ方法でTUN/TAPデバイスから生のEthernetバイト列を取得している
https://github.com/lwip-tcpip/lwip/blob/master/contrib/ports...
最小のLinuxカーネルをTCP/IPスタックなしでコンパイルすると400KBで、TCP/IPスタックを入れると800KBになる
温度だけ送れればいいプロジェクトでは、ユーザー空間の小さなCプログラムで自前のUDPメッセージに値を詰めて送っており、容量と複雑さを大きく減らせた
ARPを無効化すると、同じネットワーク上の複数のサーバーに同じIPを設定できる
ルーティングフロントエンドの役割をするサーバーが、MACアドレスを基準にバックエンドサーバーのネットワークインターフェースへパケットを転送できるなら、そのバックエンドは自分宛てだと認識し、送信元/宛先IPを入れ替えてクライアントに直接応答できる。このときルーティングフロントエンドは再び経由しない
あるいはARPを切らずに共通IPアドレスをloopbackインターフェースのエイリアスとして追加しても同じ効果が得られ、バックエンドが自分を宛先だと認識しつつARP競合を避けられる。90〜00年代にIBM WebSphereのソフトウェアロードバランサーが使っていたトリックだ
より広く使われるL3負荷分散より良い点は、IPパケットヘッダーを書き換える必要がないことだ
そのため、この方式を使うなら専用VLANを作るのがよい
似たようなことをPythonでやってみたことがある: https://github.com/georgek/notebooks/blob/master/internet.ip...
たぶんコード品質はもっと低く、正直なところアドレス解決アルゴリズムもただのでっち上げだった。ICMPでインターネットホストにpingを送るところまでは成功した
短いノートブック1つに完全に収まっている点は気に入っている。元の記事は、参照されているより大きなソースコードにある多くの詳細を本文では省いている
これは元記事を見ず、Wikipediaだけを見て作った。ただ、TCPからは複雑さが大きく跳ね上がるので、そこで少し興味が薄れた。第3部がその部分を扱うというので、いつか読んで仕上げるかもしれない。ネットワーキングに興味があるなら、どのレベルのプログラマーにとっても試す価値があり、やりがいのある作業だと思う
数年前、原子力発電所の計装の仕事をしていた。クライアント側の開発はSunワークステーションで行い、実際、TCP/IPの経験があったおかげで採用されたのだが、その経験はCMUの「オペレーティングシステム」の授業で得たものだった
一方、発電所のコンピューターはTCP/IPスタックのないミニコンピューターだったので、そのチームは自分たちでスタックを作らなければならなかった
記事の冒頭1分くらいで「dmacとsmacはかなり自明なフィールド」と言っているが、それが何か分からない読者はそこで離脱してしまうかもしれない
「この記事はそのフィールドが自明な人向けなのだな。自分向けではないから読むのをやめよう」と思ってしまう
しかもネットワークスタックを作る記事なのだから、読者がネットワーキングについてある程度知っていると仮定しても問題ない
関連記事:
Let’s code a TCP/IP stack (2016) - https://news.ycombinator.com/item?id=27654182 - 2021年6月、コメント49件
Let’s code a TCP/IP stack, 1: Ethernet & ARP (2016) - https://news.ycombinator.com/item?id=17316487 - 2018年6月、コメント47件
Let’s Code a TCP/IP Stack: TCP Retransmission - https://news.ycombinator.com/item?id=14701199 - 2017年7月、コメント30件
Let’s code a TCP/IP stack, 1: Ethernet and ARP - https://news.ycombinator.com/item?id=11234229 - 2016年3月、コメント49件
筆者がARP解析テストで使っている 10.0.0.4 IPアドレス をどこから持ってきたのかわからない
これは何のアドレスなのか? ここで作った偽のEthernetデバイスから到達可能な偽のデバイスなのか、それとも筆者のネットワークに実際に存在するデバイスなのか?
TAPデバイスは、ソフトウェアでエミュレートされたEthernetリンクのようなもの。そこにパケットを送るとユーザー空間のプログラムに直接渡され、そのプログラムがどのIPアドレスを持つか、またARPにどう応答するかを決める
普通はこうしたことはOSが処理し、インターフェースにIPアドレスを追加するにはroot権限が必要。TAPデバイスを開くのも同様。ネットワーキングは大半が協調的に成り立っており、ネットワーク上でroot権限を持つ悪意ある行為者は悪さができる
覚えている限りでは、ARPはローカルセグメントでしか動作しない。ルーターが自分のアドレスを入れてパケットを転送する
またRARPもあり、これは「ネットワーク」に自分のIPアドレスを尋ねる方法の1つ。RARPが今でも実環境で動いているのかはわからない