現在42Tokyoに通っているMoriPと言います。
先日、CommonCore課程を修了しAdvanced課題のpingコマンドの再実装をしようと思いましたが、全く何をしていいか分からず、先輩の記事を見つけました。ICMPというプロトコルを使って実装することを知り、低レイヤーに興味があったのでプロトコルスタック自作入門の本を一通りやってみようと思い、ひとまず終わったのでこちらでアウトプットしようと思います。
実装したプロトコル
今回の本を通じて以下のプロトコルを実装しました。
| プロトコル | 層 |
|---|---|
| TCP | トランスポート層 |
| UDP | トランスポート層 |
| IP | インターネット層 |
| ICMP | インターネット層 |
| ARP | インターネット層 |
| Ethernet | ネットワークインターフェース層 |
まだ浅い知識のため、間違っている部分があれば生成AIが答えてくれるような優しい文章でご指摘いただけると幸いです☺️
1. プロトコルスタックとは
プロトコルスタックとは異なる複数のプロトコルを階層状に積み重ねたソフトウェアの集まりのことです。
現在はTCP/IP階層モデルが主流であるが、OSI参照モデルもあります。
今回の記事ではTCP/IP階層モデルを使って説明していきます。
各階層のプロトコルの仕様はRFC(Request For Comments)(File Fromatsから見れます)というドキュメントでIETF(Internet Engineering Task Force)という団体が管理しています。
本来プロトコルスタックはカーネル空間で動作するものですが、それだと色々と制約があるため、今回この本ではユーザ空間で実行できるよう実装されています。
2. カプセル化・デカプセル化
プロトコルスタックでは、データを送信する際に各層がヘッダを付与しながら下の層へ渡していきます。
この仕組みをカプセル化といいます。逆に受信側では下の層から上の層へ渡すたびにヘッダを取り除いていきます。これをデカプセル化といいます。
カプセル化(送信側)
- アプリ層 がデータを生成する
- トランスポート層 がTCP/UDPヘッダを付与し、セグメントになる
- インターネット層 がIPヘッダを付与し、パケットになる
- ネットワークインターフェース層 がEthernetヘッダとFCSを付与し、フレームになる
- 最終的にビット信号として物理層へ送信される
デカプセル化(受信側)
カプセル化の逆順で処理されます。各層はヘッダを検証・除去したうえで上の層にデータを渡します。
ヘッダの検証ではチェックサムを確認し、データが破損していないかを確認します。
3. チェックサムとは
チェックサムはデータの破損を検出するための仕組みです。送信側がデータを数値として足し合わせた値(チェックサム)をヘッダに付与して送信し、受信側が同じ計算をして値が一致するかを確認します。一致しない場合はデータが壊れていると判断します。
対象のデータの1の補数和を出し、最後のビットを反転(1の補数)した値がチェックサムとなります。
各プロトコルでの対象範囲
| プロトコル | チェックサムの対象 | 備考 |
|---|---|---|
| TCP | ヘッダ+データ+擬似ヘッダ | 擬似ヘッダにはIPアドレス等が含まれる |
| UDP | ヘッダ+データ+擬似ヘッダ | IPv4では省略可(0埋め)、IPv6では必須 |
| IP | IPヘッダのみ | データ部分は対象外 |
| ICMP | ICMPヘッダ+データ | |
| Ethernet | FCS(CRC32)で代替 | チェックサムではなくCRC方式 |
計算方法
1. 対象データを16bitずつに区切る
2. すべて足し合わせる(1の補数和)
3. 桁あふれ(キャリー)が出たら末尾に加算する
4. 最後にビットを反転(1の補数)した値がチェックサム
具体的な計算例
例えば、以下の2つのデータ(計4バイト)を送りたい場合の計算を追ってみます。
- データ1:
0x0123 - データ2:
0x4567
1. 16ビットずつ足す
$$0x0123 + 0x4567 = 0x468A$$
2. キャリー(桁あふれ)の処理
もし合計が 0xFFFF を超えて 0x1468A のようになった場合は、頭の 1 を切り離して一番下の桁に足します。
$$0x468A + 0x0001 = 0x468B$$
(今回の例では 0xFFFF を超えていないので、そのまま 0x468A として進めます)
3. ビット反転
0x468A をビット反転(NOT演算)します。
-
0x468A(2進数:0100 0110 1000 1010) - 反転後 (2進数:
1011 1001 0111 0101) ➔0xB975
この 0xB975 が、ヘッダに書き込むべきチェックサム値になります。
受信側は受け取ったデータ(チェックサム込み)で同じ計算をします。すべてのビットが1(= 0xFFFF)になれば正常です。
補足
チェックサムはあくまで破損検出であり、悪意ある改ざんの検出には使えません。また計算が単純なため、複数ビットが同時に化けると検出できないケースもあります。
擬似ヘッダとは
TCPとUDPのチェックサム計算には、実際には存在しない 擬似ヘッダ(Pseudo Header) が使われます。
擬似ヘッダはIPヘッダの一部の情報を借りて一時的に作られる仮想的なヘッダで、チェックサムの計算にのみ使用されパケットには含まれません。
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Address |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Destination Address |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Zero | Protocol | TCP/UDP Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
フィールドと説明
| フィールド | サイズ | 説明 |
|---|---|---|
| Source Address | 32bit | 送信元IPアドレス(IPヘッダから借用) |
| Destination Address | 32bit | 宛先IPアドレス(IPヘッダから借用) |
| Zero | 8bit | 予約済み(0埋め) |
| Protocol | 8bit | プロトコル番号(TCP=6、UDP=17) |
| TCP/UDP Length | 16bit | TCP/UDPヘッダ+データの合計長 |
なぜ擬似ヘッダが必要なのか
本来TCPとUDPはトランスポート層のプロトコルであり、IPアドレスはネットワーク層の情報です。しかし擬似ヘッダを使ってIPアドレスもチェックサムの計算対象に含めることで、宛先・送信元IPアドレスの誤りも検出できるようになります。
例えばIPヘッダの宛先アドレスが化けてしまい、別のホストにパケットが届いてしまった場合でも、受信側が擬似ヘッダ込みでチェックサムを再計算すると値が一致しないため、誤配送を検出して破棄できます。
4. エンディアン(バイトオーダ)の罠
プロトコルスタックを自作する上で、初心者が最もハマりやすく、かつデバッグが困難なのが エンディアン (バイトオーダ) です。
| 方式 | 説明 | 採用例 |
|---|---|---|
| ビッグエンディアン | 上位バイトから順にメモリに配置する | ネットワーク(TCP/IPプロトコル群) |
| リトルエンディアン | 下位バイトから順にメモリに配置する | 一般的なPC(Mac / Intel / ARM) |
なぜ変換が必要なのか?
私が普段使っているMacはリトルエンディアンですが、TCP/IPの標準規格 (RFC) では、データはビッグエンディアンで送ることが厳格に決められています。これを「ネットワークバイトオーダ」と呼びます。
例えば、ポート番号 80(16進数で 0x0050)をパケットヘッダに書き込む場合を考えてみます。
-
Macのメモリ上(リトルエンディアン):
50 00と並んでいる -
ネットワーク上(ビッグエンディアン):
00 50と並んでいる必要がある
もしそのまま送信してしまうと、相手は 0x5000(10進数で 20480)として解釈してしまい、通信が成立しません。
変換関数
C言語(POSIX)では、ホストのエンディアンとネットワークのエンディアンを相互に変換するための標準関数が用意されています。
#include <arpa/inet.h>
uint16_t htons(uint16_t hostshort); // Host to Network Short (16bit用)
uint16_t ntohs(uint16_t netshort); // Network to Host Short
pingの実装では、ICMPの Identifier や Sequence Number をセットする際、および受信したパケットの値を読み取る際に、これらの関数を通すことが必須になります。
5. トランスポート層
UDP (User Datagram Protocol)
UDPはコネクションレスのため相手とつながっているか気にしません。データを送信するとき相手の宛先が分かっていれば、一方的にデータを送りつけます。データが無事に届いたかどうかは気にしません。そのため相手を気にすることなく送信するのでブロードキャストやマルチキャストで送信するのに向いています。
またUDPでの処理がほぼないため、そのままIPパケットに突っ込んで送信し高速で相手に届けることができます。
主に動画の配信や音楽のストリーミングで使われています。
データ構造
0 7 8 15 16 23 24 31
+--------+--------+--------+--------+
| Source | Destination |
| Port | Port |
+--------+--------+--------+--------+
| | |
| Length | Checksum |
+--------+--------+--------+--------+
|
| data octets ...
+---------------- ...
フィールドと説明
| フィールド | サイズ | 説明 |
|---|---|---|
| Source Port | 16bit | 送信元ポート番号 |
| Destination Port | 16bit | 宛先ポート番号 |
| Length | 16bit | ヘッダ+データの長さ(バイト単位) |
| Checksum | 16bit | エラー検出用チェックサム |
| data octets | 可変長 | 実際に送信するペイロードデータ |
各フィールドの使われ方
-
Source Port / Destination Port
TCPと同様にアプリケーションを識別します。DNSはポート53、NTPは123で待ち受けています。UDPはコネクションレスのため、送信元ポートは省略可能(0埋め)ですが、返信が必要な場合は指定します。 -
Length
UDPヘッダ(8バイト固定)+データの合計バイト数です。最小値は8(データなし)、最大値はIPパケットのサイズ制限に依存します。受信側はこの値でデータの終端を判断します。 -
Checksum
IPv4では省略可能(0埋め)ですが、IPv6では必須です。TCPと同様に擬似ヘッダ(IPアドレスなど)も含めて計算します。省略した場合エラー検出は上位層に委ねられます。
TCP (Transmission Control Protocol)
TCPはUDPと違って相手とのコネクションを必要とします。データを必ず届ける必要があるため送信するためにコネクションを作成する必要があります。それがいわゆる3way handshakeです。お互いが送受信可能であることを確認するために3回の送受信が必要になります。
3-way handshake
| ステップ | 送信元 | フラグ | 説明 |
|---|---|---|---|
| ① SYN | Client → Server | SYN |
接続要求。シーケンス番号 x を送信 |
| ② SYN-ACK | Server → Client |
SYN + ACK |
接続受理。自身のシーケンス番号 y と ack=x+1 を返す |
| ③ ACK | Client → Server | ACK |
確認応答。ack=y+1 を送り接続確立 |
TCPの状態遷移(3つのフェーズ)
TCPは通信の段階によって内部状態(ステート)が変化します。
① 接続確立フェーズ (3-way handshake)
| 状態 | 説明 |
|---|---|
CLOSED |
コネクションなし(初期状態) |
LISTEN |
Serverのみ:クライアントからの接続要求(SYN)を待っている状態 |
SYN_SENT |
Clientのみ:SYNを送信し、相手からの応答(SYN-ACK)を待っている状態 |
SYN_RECEIVED |
Serverのみ:SYNを受信し、SYN-ACKを送信して最終確認(ACK)を待っている状態 |
ESTABLISHED |
接続完了、データの送受信が可能になった状態 |
② データ転送フェーズ
| 状態 | 説明 |
|---|---|
ESTABLISHED |
安定して通信が行われている状態 |
③ 切断フェーズ (4-way handshake)
| 状態 | 説明 |
|---|---|
FIN_WAIT_1 |
自分から切断要求(FIN)を送信し、相手からの確認(ACK)を待っている状態 |
FIN_WAIT_2 |
相手からのACKを受信し、さらに相手側の切断要求(FIN)を待っている状態 |
CLOSE_WAIT |
相手からのFINを受信し、こちら側のアプリが終了処理をするのを待っている状態 |
LAST_ACK |
自分の終了準備が整い、FINを送信して最後の確認(ACK)を待っている状態 |
CLOSING |
双方同時にFINを送信した場合の一時的な状態 |
TIME_WAIT |
最後のACKを送信後、遅延パケットが消えるのを待つ待機状態 |
CLOSED |
完全に終了し、初期状態へ戻る |
イレギュラーな状態遷移
通常、TCPは一方が「能動的(Active)」、もう一方が「受動的(Passive)」に動きますが、双方が同時にアクションを起こす特殊なケースがあります。
1. 同時オープン(Simultaneous Open)
双方がほぼ同時に SYN を送信し合った状態です。この場合、3-way handshakeではなく、双方が SYN_SENT → SYN_RECEIVED → ESTABLISHED という経路を辿ります。最終的にコネクションは1つだけ確立されます。
2. 同時クローズ(Simultaneous Close)
双方がほぼ同時に FIN を送信し合った状態です。この時に登場するのが状態遷移図にある CLOSING ステートです。
双方が FIN_WAIT_1 から CLOSING を経て TIME_WAIT に遷移し、最終的にクローズされます。こうした稀なケースを考慮してスタックを実装することで、堅牢なネットワークプログラムになります。
データ構造
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Port | Destination Port |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Sequence Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Acknowledgment Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Data | |U|A|P|R|S|F| |
| Offset| Reserved |R|C|S|S|Y|I| Window |
| | |G|K|H|T|N|N| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Checksum | Urgent Pointer |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Options | Padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| data |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
フィールドと説明
| フィールド | サイズ | 説明 |
|---|---|---|
| Source Port | 16bit | 送信元ポート番号 |
| Destination Port | 16bit | 宛先ポート番号 |
| Sequence Number | 32bit | 送信データの順序を管理するシーケンス番号 |
| Acknowledgment Number | 32bit | 次に受信を期待するバイト番号(確認応答) |
| Data Offset | 4bit | TCPヘッダの長さ(32bit単位) |
| Reserved | 6bit | 将来の拡張用(現在は0で埋める) |
| Flags | 6bit | URG / ACK / PSH / RST / SYN / FIN の制御フラグ |
| Window | 16bit | 受信バッファの空き容量(フロー制御に使用) |
| Checksum | 16bit | ヘッダ+データのエラー検出用チェックサム |
| Urgent Pointer | 16bit | URGフラグ有効時に緊急データの末尾位置を示す |
| Options | 可変長 | タイムスタンプやMSSなどの拡張オプション |
| Padding | 可変長 | Optionsを32bit境界に揃えるための0埋め |
| data | 可変長 | 実際に送信するペイロードデータ |
各フィールドの使われ方
-
Source Port / Destination Port
アプリケーションを識別するための番号です。例えばHTTPサーバは宛先ポート80、HTTPSは443で待ち受けています。送信元ポートはOSがランダムに割り当てる一時ポート(通常49152〜65535)が使われます。サーバが返信する際はこの送信元ポートを宛先として使うため、複数の通信を同時に識別できます。 -
Sequence Number / Acknowledgment Number
TCPが「順序保証」と「再送制御」を実現する核心です。送信側はデータに通し番号(Sequence Number)を振り、受信側は「次はこの番号が欲しい」という値をAcknowledgment Numberで返します。パケットが届かなかった場合、ACKが返ってこないため送信側が再送します。 -
Data Offset
TCPヘッダの長さを32bit単位で表します。Optionsフィールドが存在する場合はヘッダ長が変わるため、このフィールドを見てデータ部分の開始位置を特定します。通常はOptionsなしの5(= 20バイト)です。 -
Flags(URG/ACK/PSH/RST/SYN/FIN)
通信の制御に使われるビットフラグです。
| フラグ | 役割 |
|---|---|
SYN (Synchronize) |
接続確立要求(3-way handshakeの開始) |
ACK (Acknowledge) |
Acknowledgment Numberが有効であることを示す |
FIN (Finish) |
接続終了要求 |
RST (Reset) |
接続を強制リセット(エラー時) |
PSH (Push) |
受信バッファを待たずにすぐ上位層へ渡す |
URG (Urgent) |
Urgent Pointerで示す緊急データが含まれる |
-
Window
受信側が「今いくつのバイトまで受け取れるか」をフロー制御のために送信側に伝えます。受信バッファが満杯に近づくとWindowサイズを小さくして送信量を抑え、バッファオーバーフローを防ぎます。 -
Checksum
TCPヘッダ+データ+IPアドレスを含む擬似ヘッダを対象に計算されます。受信側で同じ計算をして値が一致しなければパケットを破棄します。 -
Urgent Pointer
URGフラグが立っているときのみ有効で、緊急データの末尾位置を示します。通常の通信ではほとんど使われません。 -
Options / Padding
代表的なオプションとして、接続確立時に送受信できる最大セグメントサイズを決めるMSS(Maximum Segment Size)や、パケットの往復時間を計測するタイムスタンプがあります。OptionsはTCPヘッダを32bitの倍数に揃える必要があるため、足りない部分をPaddingで0埋めします。
6. インターネット層
IP (Internet Protocol)
IPはインターネット層のプロトコルで、異なるネットワーク間でパケットを届ける役割を担います。宛先IPアドレスをもとにルータがパケットを転送(ルーティング)していきます。
ただしTCPと違ってパケットが確実に届くことは保証しません(ベストエフォート型)。現在主流のIPv4と、アドレス枯渇問題に対応したIPv6があります。
データ構造
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Version| IHL |Type of Service| Total Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Identification |Flags| Fragment Offset |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Time to Live | Protocol | Header Checksum |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Address |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Destination Address |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Options | Padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
フィールドと説明
| フィールド | サイズ | 説明 |
|---|---|---|
| Version | 4bit | IPのバージョン(IPv4 = 4、IPv6 = 6) |
| IHL (Internet Header Length) | 4bit | IPヘッダの長さ(32bit単位、通常は5 = 20バイト) |
| Type of Service | 8bit | パケットの優先度やQoS(Quality of Service)の指定 |
| Total Length | 16bit | IPヘッダ+データの合計長(バイト単位、最大65535) |
| Identification | 16bit | フラグメント分割されたパケットを識別するID |
| Flags | 3bit | フラグメント制御(DF: 分割禁止 / MF: 続きあり) |
| Fragment Offset | 13bit | フラグメント分割時の元データ内でのオフセット位置 |
| Time to Live | 8bit | パケットの生存時間(ルータを1つ通過するたびに-1、0で破棄) |
| Protocol | 8bit | ペイロードのプロトコル番号(TCP=6、UDP=17、ICMP=1) |
| Header Checksum | 16bit | IPヘッダのエラー検出用チェックサム |
| Source Address | 32bit | 送信元IPアドレス |
| Destination Address | 32bit | 宛先IPアドレス |
| Options | 可変長 | ルートの記録やタイムスタンプなどの拡張オプション(任意) |
| Padding | 可変長 | Optionsを32bit境界に揃えるための0埋め |
各フィールドの使われ方
-
Version / IHL
VersionはIPv4なら4固定です。IHLはヘッダ長を32bit単位で表し、Optionsなしなら5(= 20バイト)です。受信側はIHLを見てデータの開始位置を特定します。 -
Type of Service
パケットの優先度を指定します。現在はDSCP(Differentiated Services Code Point)として再定義されており、VoIPなどリアルタイム通信で低遅延を要求する際に使われます。一般的な通信では0です。 -
Total Length
IPヘッダ+データの合計バイト数です。最大65535バイトですが、Ethernetの最大フレームサイズ(1500バイト)を超える場合はフラグメント分割されます。 -
Identification / Flags / Fragment Offset
フラグメント(分割)制御に使われます。大きなパケットを分割する際、同じIdentificationを持つフラグメントを再組み立て時に紐付けます。DFフラグ(Don't Fragment)を立てると分割を禁止でき、MFフラグ(More Fragments)は「続きがある」ことを示します。Fragment Offsetは元データのどの位置かを示します。 -
Time to Live(TTL)
ルータを1つ通過するたびに1減算され、0になるとパケットを破棄してICMP Time Exceededを送信元に返します。無限ループを防ぐためのもので、初期値は通常64や128が設定されます。tracerouteはこのTTLを1から順に増やすことで経路上のルータを特定します。 -
Protocol
ペイロードのプロトコルを示します。TCPなら6、UDPなら17、ICMPなら1です。受信側はこの値を見て上位層のどのプロトコルに渡すかを決定します。 -
Header Checksum
IPヘッダのみを対象に計算します(データ部分は対象外)。TTLはルータを通過するたびに変わるため、ルータは毎回チェックサムを再計算します。 -
Source Address / Destination Address
送信元・宛先のIPアドレスです。ルータはDestination Addressを見てルーティングテーブルと照合し、次の転送先を決めます。
ICMP (Internet Control Message Protocol)
ICMPはIPと組み合わせて使われる制御用プロトコルで、主にネットワークの疎通確認やエラー通知に使われます。pingでは宛先との疎通確認や応答時間の計測、tracerouteでは通信経路の調査に利用されます。ICMPのメッセージは大きく2種類に分類されます。
| 種類 | 説明 | 例 |
|---|---|---|
| Query(問い合わせ) | 情報の問い合わせと応答のやりとり | Echo / Echo Reply(ping) |
| Error(エラー) | パケットが届かなかった原因をIPの送信元に通知 | Destination Unreachable、Time Exceeded |
Echo / Echo Reply メッセージのデータ構造(pingで使用)
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Type | Code | Checksum |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Identifier | Sequence Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Data ...
フィールドと説明
| フィールド | サイズ | 説明 |
|---|---|---|
| Type | 8bit | メッセージの種別(Echo Request = 8、Echo Reply = 0) |
| Code | 8bit | Typeの補足情報(Echo/Echo Replyは常に0) |
| Checksum | 16bit | ICMPヘッダ+データのエラー検出用チェックサム |
| Identifier | 16bit | 送受信のペアを識別するID(複数のpingを区別するために使用) |
| Sequence Number | 16bit | 送信ごとにインクリメントされる番号(パケットの順序・消失検出に使用) |
| Data | 可変長 | 任意のペイロード(pingでは往復時間計測用のタイムスタンプなどが入る) |
Type一覧
| Type | 名称 |
|---|---|
| 0 | Echo Reply |
| 3 | Destination Unreachable |
| 4 | Source Quench |
| 5 | Redirect |
| 8 | Echo |
| 11 | Time Exceeded |
| 12 | Parameter Problem |
| 13 | Timestamp |
| 14 | Timestamp Reply |
| 15 | Information Request |
| 16 | Information Reply |
各フィールドの使われ方
-
Type / Code
TypeはICMPメッセージの大分類、CodeはTypeの中のさらに詳細な種別です。例えばType3(Destination Unreachable)のCode3は「指定ポートが閉じている」を意味します。pingはType8(Echo Request)を送り、相手からType0(Echo Reply)が返ってくるかを確認します。 -
Checksum
ICMPヘッダ+データ全体を対象に計算します。IPと違い、データ部分も含まれる点が特徴です。 -
Identifier / Sequence Number
pingで複数のリクエストを区別するために使います。Identifierはプロセスごとに固定値(通常はPID)、Sequence Numberは送信のたびにインクリメントします。返ってきたEcho ReplyのIdentifierとSequence Numberを照合することで、どのリクエストへの返答かを判別します。 -
Data
pingでは往復時間(RTT)を計測するために送信時のタイムスタンプを格納します。Echo Replyはこのデータをそのままコピーして返すため、受信時刻と比較することでRTTを算出できます。
Destination Unreachable メッセージのデータ構造
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Type | Code | Checksum |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| unused |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Internet Header + 64 bits of Original Data Datagram |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
フィールドと説明
| フィールド | サイズ | 説明 |
|---|---|---|
| Type | 8bit | メッセージの種別(Destination Unreachable = 3固定) |
| Code | 8bit | 到達不能の理由を示すコード |
| Checksum | 16bit | ICMPヘッダ+データのエラー検出用チェックサム |
| unused | 32bit | 未使用フィールド(0埋め) |
| Internet Header + 64 bits of Original Data Datagram | 可変長 | エラーの原因となった元のIPヘッダ+元データの先頭64bit |
各フィールドの使われ方
-
Type / Code
Type3固定で「宛先に到達できなかった」ことを示します。Codeで原因を細かく分類しており、受信したアプリケーションはCodeを見て適切なエラー処理を行います。例えばCode3(Port Unreachable)はUDPで存在しないポートにパケットを送った際に返ってきます。 -
Checksum
Echo/Echo Replyと同様にICMPヘッダ+データ全体を対象に計算します。 -
unused
現在は使用されておらず0埋めですが、将来の拡張のために確保されているフィールドです。受信側は値を無視して処理します。 -
Internet Header + 64 bits of Original Data Datagram
エラーの原因となった元のパケットの情報です。IPヘッダ(20バイト)+元データの先頭64bit(8バイト)が含まれます。送信元はこの情報を見て「どのパケットが届かなかったか」を特定し、適切な再送処理やエラー処理を行います。先頭64bitだけで十分な理由は、TCPやUDPのヘッダに含まれるポート番号がこの範囲に収まっているためです。
Code一覧(Type=3の時Codeが入る)
| Code | 意味 |
|---|---|
| 0 | Net Unreachable(宛先ネットワークに到達不能) |
| 1 | Host Unreachable(宛先ホストに到達不能) |
| 2 | Protocol Unreachable(指定プロトコルが使用不能) |
| 3 | Port Unreachable(指定ポートが使用不能) |
| 4 | Fragmentation Needed(フラグメント必要だがDFフラグが立っている) |
| 5 | Source Route Failed(ソースルートが失敗) |
ARP (Address Resolution Protocol)
ARPはIPアドレスからMACアドレスを解決するプロトコルです。
同一ネットワーク内でIPパケットを送るとき、最終的にはEthernetフレームに乗せる必要があるため宛先MACアドレスが必要になります。
ARPはブロードキャストで「このIPアドレスを持っているのは誰?」と問い合わせ、該当するホストがMACアドレスを返答します。取得したMACアドレスは一定時間ARPキャッシュに保存されます。
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Hardware Type | Protocol Type |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| HL | PL | Operation |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Sender MAC Address (32bit/48bit) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Sender MAC Addr(上の残り16bit) | Sender IP Addr (16bit/32bit) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Sender IP Addr(上の残り16bit) | Target MAC Addr(16bit/48bit) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Target MAC Address (上の残り32bit) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Target IP Address |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
フィールドと説明
| フィールド | サイズ | 説明 |
|---|---|---|
| Hardware Type | 16bit | ハードウェアの種類(Ethernet = 1) |
| Protocol Type | 16bit | プロトコルの種類(IPv4 = 0x0800) |
| HL (Hardware Length) | 8bit | MACアドレスの長さ(Ethernetは6バイト)例: 00:1a:2b:3c:4d:5e
|
| PL (Protocol Length) | 8bit | IPアドレスの長さ(IPv4は4バイト) |
| Operation | 16bit | リクエスト=1 / リプライ=2
|
| Sender MAC Address | 48bit | 送信元MACアドレス |
| Sender IP Address | 32bit | 送信元IPアドレス |
| Target MAC Address | 48bit | 宛先MACアドレス(解決したいMACアドレス) |
| Target IP Address | 32bit | 宛先IPアドレス |
ARPは厳密にはどの層に属するか定義が難しいプロトコルです。動作としてはIPアドレス(ネットワーク層)をMACアドレス(ネットワークインターフェース層)に変換するという2つの層をまたぐ橋渡し役を担っています。
TCP/IPモデルでは明確な位置づけがされていないことが多く、書籍や資料によって「インターネット層」「ネットワークインターフェース層」どちらに分類されるか異なります。OSI参照モデルで対応させるとデータリンク層(第2層)と捉えられることが多いです。
今回の記事ではIPと密接に連携するという観点からインターネット層のセクションで紹介しています。
MACアドレス(Media Access Control address)
MACアドレスは48bit(6bytes)で構成されておりネットワーク機器(NIC: Network Interface Card)の物理インターフェースに製造時に付与される世界唯一の12桁の16進数識別番号のことです。IPアドレスが論理的(仮想)な住所なら、MACアドレスは機器固有の物理的IDであり、主に同一ネットワーク内(データリンク層)の通信相手を特定するために使用されます。
前半24ビットはOUI(Organizationally Unique Identifier:ベンダー識別子)と呼ばれ、IEEEがネットワーク機器メーカーごとに割り当てた固有の番号です。この部分で製品の製造メーカーを判別でき、残りの後半24ビットはメーカーが機器ごとに割り当てる個別番号です。
こちらからメーカーの所持するOUIを確認できます。
ちなみに
世界で使用できるMACアドレスは約281兆(16^12)個。
現在世界で使用されているMACアドレスは約70兆個。
Appleが使用できるMACアドレスは約250億(16^6 * 1493)個。
現在Appleが使用しているMACアドレスは約23億個。
らしいです。
7. ネットワークインターフェース層
Ethernet
Ethernetはネットワークインターフェース層のプロトコルで、
同一ネットワーク内のデバイス間でデータを届ける役割を担います。
IPが異なるネットワーク間の転送を担うのに対し、Ethernetは同じLAN内での「最後の一区間」を担当します。
宛先の特定にはIPアドレスではなくMACアドレスを使用するため、ARPでMACアドレスを解決してからフレームを組み立てます。
データはフレームという単位でやり取りされます(IEEE 802.3で規定)。
データ構造
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Preamble (7 bytes) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| SFD |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Destination MAC Address (6 bytes) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source MAC Address (6 bytes) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| EtherType / Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Data / Payload (46~1500 bytes) |
| ... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| FCS (4 bytes) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Ethernet Frame Format
フィールドと説明
| フィールド | サイズ | 説明 |
|---|---|---|
| Preamble | 7バイト | フレームの開始を通知し、送受信間のビット同期を確立するための 10101010... パターン |
| SFD (Start Frame Delimiter) | 1バイト | フレーム本体の開始を示す区切り文字(値は 0xD5 固定) |
| Destination Address | 6バイト | 宛先のMACアドレス(ユニキャスト / マルチキャスト / ブロードキャスト) |
| Source Address | 6バイト | 送信元のMACアドレス(ユニキャストのみ) |
| EtherType / Length | 2バイト | 値が1500以下ならペイロード長、1536以上なら上位プロトコルの種別(IPv4=0x0800、ARP=0x0806、IPv6=0x86DD) |
| Data / Payload | 46〜1500バイト | 実際のデータ(IPパケットなど)。46バイト未満の場合はゼロパディングで補う |
| FCS (Frame Check Sequence) | 4バイト | フレームの破損検出用CRC(32bit巡回冗長検査) |
補足
PreambleとSFDは物理層の役割で、フレームサイズの計算には含まれません。フレームの最小サイズは 64バイト、最大サイズは 1518バイトです。
8. おまけ:IPv5はどこへ?
IPv4, IPv6と使われていますが、IPv5は実は存在します。ただし、一般には普及しませんでした。
IPv5は1970年代後半〜1980年代に ST (Stream Protocol / Internet Stream Protocol) として実験的に開発されたプロトコルです。音声や動画などのリアルタイムストリーミング向けに設計されており、IPヘッダのVersionフィールドに5が割り当てられていました。
しかし以下の理由で普及せず、事実上の欠番となりました。
- リアルタイム通信の需要はその後 RTP (Real-time Transport Protocol) が担うようになった
- インターネットの爆発的な普及に伴いアドレス枯渇問題が深刻化し、次世代IPとして IPv6 の開発が優先された
- STはあくまで実験的なプロトコルであり、広くデプロイされることがなかった
結果として「IPv4の次はIPv6」という形になり、IPv5は歴史の隅に静かに残っています。
IPv5 は今も RFC 1190 / RFC 1819 に記録されており、他のプロトコルに再利用されることもないため、永遠に「幻の番号」として存在し続けます。
勉強になったこと
TCPやIPという単語は聞いたことがあっても、プロトコルとしてどういう処理をしているかはブラックボックス状態でした。しかし、実際に実装してみて以下の気づきを得ることができました。
-
パケットの処理フローが明確に
データを受け取り、処理し、下の階層へ渡すという一連の流れを知ることで、ネットワーク通信の仕組みを根本から理解できました。 -
ICMPの実装経験
pingの再実装の肝となるICMPを実際にコーディングして処理の中身を理解できたのは、今後の開発における大きな自信に繋がりました。 -
裏方プロトコルへの感謝
ARPやEthernetといった、普段意識しないけれど非常にお世話になっているプロトコルの存在を知り、ネットワークの奥深さに気付かされました。
次にやりたいこと
割り込み処理やタスクスケジュール、タイマーなどOSと深く関わる部分もちらほら紹介されていたので、OSの仕組みについてもとても興味が出てきました。自作OS入門(通称みかん本)もあるので次はこれに取り組んでみたいです。
ただ作ったOSをインストールできるPCが必要そうなのでPCを買うか検討中です🤔
(仮想環境でもできるそうですがMac以外のPCが欲しくなってきました、、、)
OS自作したことある方いらっしゃったら、おすすめの参考書や記事など(PCなども)についてコメント欄で教えて頂けますと幸いです🙇♂️
出典・参考文献
本記事は、『ゼロからのTCP/IPプロトコルスタック自作入門』(山本雅也 著 / マイナビ出版)を学習した際のアウトプットとして執筆しています。



