Ethernetフレームを自前で実装する必要があるため、Ethernetフレームの構造を実装例とともに解説して欲しい。
こんなお悩みを解決します。
今回は、Ethernetフレームの構造を解説し、同時に実装例を紹介します。
現在では、宛先、ポート、送信するデータを用意して関数を呼び出せば、Ethernetフレームの構造を知らなくても通信ができます。
実際、通信する際は、通信プロトコルに合わせて様々なヘッダーが付与されるのですが、このヘッダー付与は、kernelが担っています。
一方、kernelが担っている部分に変更を加える場合は、途端に対応が困難になります。
そこで、Ethernetフレームの構造を理解し、自前で構築することで、問題を解決したいと思います。
Ethernetフレームの構造
Ethernetフレームは、以下のような構造になります。
上記に登場するMACアドレスについて説明します。
MACアドレスとは、Ethernet通信において使用される物理アドレスのことを指します。
PCには、LANケーブルを差し込む部分があると思いますが、その物理的なポートに割り当てられた固有のIDとなります。
また、上記のうち、オレンジ色のフィールドは、Ethernetヘッダと呼ばれ、以下のような内訳となります。
フィールド | 内容 |
---|---|
宛先MACアドレス | Ethernet通信する際の宛先のMACアドレス |
送信元MACアドレス | Ethernet通信する際の送信元のMACアドレス |
タイプ | データフィールドに格納する上位層のプロトコルを識別するための情報 |
タイプで設定できる情報は決まっており、主なものとして以下が利用されます。
ここで、0xで始まる数値は、16進数であることを意味します。
タイプ | プロトコル |
---|---|
0x0800 | IPv4 |
0x0806 | ARP |
0x8137 | IPX |
0x86dd | IPv6 |
以降では、IPv4を対象に、TCP/UDPについて解説をします。
IPヘッダの構造
IPv4で通信する際は、Ethernetフレームのデータ部分にIPヘッダというものが付与されます。
この時、Ethernetフレームの構造をさらに細かく見ると以下のようになります。
このIPヘッダは、以下のような構造になっています。
各フィールドの内訳は、以下のようになります。
フィールド | 内容 | 備考 |
---|---|---|
バージョン | Internet Protocol(IP)のバージョン | IPv4の場合は「4」となる |
ヘッダ長 | 4バイト単位としたIPヘッダのヘッダ長 | オプションなしの場合、IPヘッダは20バイトとなるため、\(20/4=5\)となる。 |
サービスタイプ(ToS) | IPパケットの優先度 | - |
全データ長 | IPヘッダとデータ長を含めたパケット全体の長さ | IPヘッダのオプションなしで、UDPで12バイトのデータを送信する場合、全データ長は52バイトとなる。内訳は以下。 ・IPヘッダ:20バイト ・UDPヘッダ:20バイト ・データ:12バイト |
ID | IPパケット送信時に付与される識別子 | 大きなデータを送信する際は、複数のIPパケットを分けて送信する仕組みとなっている。分割した個々のデータをパケットと呼ぶ。 |
フラグ | IPパケット分割時の制御情報 | 3ビットごとに分かれており、内訳は以下のようになる。 ・ビット0:予約済み(未使用) ・ビット1:分割を許可する/許可しないを表す。 値が0の場合:分割可能、値が1の場合:分割不可能 ・ビット2:フラグメントの終端を表す。 値が0の場合:最後のフラグメント、値が1の場合:後続パケットが存在 |
フラグメントオフセット | 分割されたパケットに対する元のデータの位置 | 単位は8bit単位で、最大65536bitとなる。 |
TTL(Time To Live) | パケットが通過可能なルータ数 | ルータを経由するごとに1ずつ減っていき、0になった時点でパケットは破棄される。 |
プロトコル番号 | 通信時に使用される上位のプロトコル | 今回の例では、以下の2つを用いる。 ・TCP:0x06 ・UDP:0x11 |
チェックサム | IPパケットのチェックサム | IPパケットの伝送エラーの有無を確認するための情報。ルータを経由する度にTTLの値が変わるため、ルータ内で再計算される。 |
送信元IPアドレス | 送信元のIPアドレス | - |
宛先IPアドレス | 宛先のIPアドレス | - |
オプション(可変) | IPパケットに付加するオプション | 32bit単位で設定する。32bitに満たない場合は32bitになるようにpadding(0埋め)する。 今回の例では、オプションは利用しない。 |
今回、チェックサムを計算するため、以下のような補助関数を定義しています。(非公開関数としてutils.c
にて定義)
この関数の出力結果をビット反転したものがチェックサムの値となります。
#define CHECKSUM_MSB (0x80000000)
#define CHECKSUM_MASK (0x0000FFFF)
#define SHIFT_CHECKSUM(x) (((x) & (uint32_t)CHECKSUM_MASK) + (((x) >> 16) & (uint32_t)CHECKSUM_MASK))
uint16_t calcTotal(const uint8_t *data, int32_t size, uint16_t initValue) {
register uint32_t sum;
register const uint16_t *ptr;
register int32_t idx;
uint16_t val;
sum = (uint32_t)initValue & (uint32_t)CHECKSUM_MASK;
ptr = (const uint16_t *)data;
for (idx = size; idx > 1; idx -= 2) {
sum += (*ptr);
// check overflow
if (sum & (uint32_t)CHECKSUM_MSB) {
sum = SHIFT_CHECKSUM(sum);
}
ptr++;
}
if (1 == idx) {
val = 0;
memcpy(&val, ptr, sizeof(uint8_t));
sum += (uint32_t)(val & (uint16_t)0x00FF);
}
while (sum >> 16) {
sum = SHIFT_CHECKSUM(sum);
}
return (uint16_t)(sum & (uint32_t)CHECKSUM_MASK);
}
IPヘッダの場合、以下のような実装をしています。
ip.check = 0;
ip.check = ~calcTotal((const uint8_t *)&ip, (int32_t)ipHeaderSize, 0);
また、フラグのビット0~ビット2は、以下のように対応します。
UDPヘッダの構造
UDPで通信する場合、IPヘッダのデータ部分にUDPヘッダというものが付与されます。
この時、Ethernetフレームの構造をさらに細かく見ると以下のようになります。
このUDPヘッダは、以下のような構造になっています。
各フィールドの内訳は、以下のようになります。
フィールド | 内容 | 備考 |
---|---|---|
発信元ポート番号 | 発信元のポート番号 | - |
宛先ポート番号 | 宛先のポート番号 | - |
UDPデータ長 | UDPパケットの長さ | UDPヘッダとデータ長の和となる。 |
チェックサム | UDPパケットのチェックサム | 疑似ヘッダを用いて計算することが多い。 |
UDPヘッダの場合、下記のような疑似ヘッダを付与して、チェックサムを計算します。
今回は、上記の疑似ヘッダを利用して、UDPの疑似ヘッダを以下のように計算しています。
IPヘッダで定義した補助関数を利用しています。
static uint16_t calcChksum(const uint8_t *header, const uint8_t *data, size_t headerSize, uint16_t dataLength) {
register uint16_t sum;
// calculate header
sum = calcTotal(header, (int32_t)headerSize, 0);
// calculate data
sum = calcTotal(data, (int32_t)dataLength, sum);
return (~sum);
}
利用する際は、下記のような構造体を定義し、上記の関数を呼び出します。
struct pseudo_header_t {
uint32_t srcIPAddr;
uint32_t dstIPAddr;
uint8_t reserved;
uint8_t protocol;
uint16_t dataLength;
uint16_t srcPort;
uint16_t dstPort;
uint16_t len;
uint16_t checksum;
};
struct udp_header_arg_t {
uint32_t srcIPAddr;
uint32_t dstIPAddr;
uint16_t srcPort;
uint16_t dstPort;
uint16_t dataLength;
const uint8_t *data;
};
// 中略
ph.reserved = 0;
ph.checksum = 0;
// calculate checksum
udp.uh_sum = calcChksum((const uint8_t *)&ph, (const uint8_t *)(arg->data), sizeof(struct pseudo_header_t), arg->dataLength);
TCPヘッダの構造
UDPと同様に、TCPで通信する場合、IPヘッダのデータ部分にTCPヘッダというものが付与されます。
この時、Ethernetフレームの構造をさらに細かく見ると以下のようになります。
このTCPヘッダは、以下のような構造になっています。
各フィールドの内訳は、以下のようになります。
フィールド | 内容 | 備考 |
---|---|---|
発信元ポート番号 | 発信元のポート番号 | - |
宛先ポート番号 | 宛先のポート番号 | - |
シーケンス番号 | 受信側(宛先)にデータの並び順を伝えるための情報 | 本ライブラリでは、シーケンス番号の計算・設定機能は提供していない |
確認応答番号 | 送信側に次に欲しいシーケンス番号を伝えるための情報 | 本ライブラリでは、確認応答番号の計算・設定機能は提供していない |
ヘッダ長 | 4バイト単位としたTCPヘッダのヘッダ長 | オプションなしの場合、IPヘッダは20バイトとなるため、\(20/4=5\)となる。 |
セッションフラグ | TCP通信におけるコネクションを制御するための情報 | 以降で、詳細を図解する。 |
ウィンドウサイズ | データ転送時における、受信側が受信可能なデータサイズ | TCP通信時の効率を上げるために利用される。 |
チェックサム | TCPパケットのチェックサム | 疑似ヘッダを用いて計算することが多い。 |
緊急ポインタ | セッションフラグのURGが有効になっている場合に使用する情報 | 緊急に処理する必要があるデータの場所を示す。 |
オプション | TCPパケットに付加するオプション | 32bit単位で設定する。32bitに満たない場合は32bitになるようにpadding(0埋め)する。 今回の例では、オプションは利用できるが、内容のチェックまでは行っていない。 |
また、セッションフラグの内訳は以下のようになっております。
それぞれの内容を以下に示します。
フラグ名 | 内容 |
---|---|
URG | このフラグが1の場合、パケット内に緊急で処理する必要あるデータが含まれていることを表す。 |
ACK | このフラグが1の場合、確認応答番号が有効であることを表す。 |
PSH | このフラグが1の場合、受信したデータをすぐに上位のアプリケーションに渡すことを表す。 通常のTCP通信では、受信側である程度データをバッファに溜めてから、 適当なタイミングで上位のアプリケーションに渡される。 |
RST | このフラグが1の場合、TCP通信を強制的に切断することを表す。 |
SYN | このフラグが1の場合、TCPの接続要求を表す。 |
FIN | このフラグが1の場合、TCPコネクション切断要求であることを表す。 |
TCPヘッダの場合、下記のような疑似ヘッダを付与して、チェックサムを計算します。
今回は、上記の疑似ヘッダを利用して、下記のような構造体と関数を定義し、TCPの疑似ヘッダを以下のように計算しています。
IPヘッダで定義した補助関数を利用しています。
struct pseudo_header_t {
uint32_t srcIPAddr;
uint32_t dstIPAddr;
uint16_t protocol; // reserved(uint8_t) + protocol(uint8_t)
uint16_t dataLength;
uint16_t srcPort;
uint16_t dstPort;
uint32_t seqNum;
uint32_t ackNum;
uint8_t dataOffset;
uint8_t flags;
uint16_t windowSize;
uint16_t checksum;
uint16_t urgentPointer;
};
struct checksum_arg_t {
struct pseudo_header_t pseudoHeader;
uint16_t optionLength;
uint16_t dataLength;
const uint8_t *options;
const uint8_t *data;
};
static uint16_t calcChksum(const struct checksum_arg_t *arg) {
register uint16_t sum;
// calculate header
sum = calcTotal((const uint8_t *)&(arg->pseudoHeader), (int32_t)sizeof(struct pseudo_header_t), 0);
// calculate options
sum = calcTotal(arg->options, (int32_t)(arg->optionLength), sum);
// calculate data
sum = calcTotal(arg->data, (int32_t)(arg->dataLength), sum);
return (~sum);
}
利用する際は、下記のようにして上記の関数を呼び出します。
chksumArg.pseudoHeader.checksum = 0;
// calculate checksum
tcp.check = calcChksum((const struct checksum_arg_t *)&chksumArg);
使い方
以降では、今回実装したプログラムの使い方を説明します。
また、実装結果は、以下に格納してあります。
https://github.com/yuruto-free/raw-ethernet-frame/tree/v1.0.0
Ethernetフレームの構築
まず、C言語のプログラムで、下記のヘッダーを読み込みます。
#include "rawEthernetFrame.h"
#include "packetStructure.h"
次に、Ethernetフレームを扱う変数を定義、領域を確保します。
ここで、struct REF_param_t
は、rawEthernetFrame.h
で定義されています。
そして、Ethernetフレームを構築するAPIであるREF_createRawFrame
関数を呼び出します。
最後に、確保した領域を解放します。
int main(void) {
int32_t ret;
struct REF_param_t params;
struct REF_rawFrame_t *frame = NULL;
// 領域確保
ret = REF_mallocRawFrame(&frame);
if ((int32_t)REF_SUCCESS != ret) {
// エラー処理
// 省略
}
// パラメータの設定
memset(¶ms, 0x00, sizeof(struct REF_param_t));
// 具体的な設定内容は省略
//
// Ethernetフレームの構築
ret = REF_createRawFrame((const struct REF_param_t *)¶ms, frame);
if ((int32_t)REF_SUCCESS != ret) {
// エラー処理
// 省略
}
// 確保した領域の開放
(void)REF_freeRawFrame(&frame);
}
Ethernetフレームの解析
Ethernetフレームの構築とほとんど同じ使い方となります。
差分としては、以下の2点があります。
- callback関数を定義する。
- REF_dumpRawFrame関数を呼び出す。
差分部分を抽出した例を以下に示します。
// callback関数の定義
static int32_t callback(uint8_t packetType, void *data) {
struct ether_header_t *eth;
struct ip_header_t *ip;
struct udp_header_t *udp;
struct tcp_header_t *tcp;
switch (packetType) {
case REF_ETHER_PACKET:
eth = (struct ether_header_t *)data;
// Ether headerに対する処理
break;
case REF_IP_PACKET:
ip = (struct ip_header_t *)data;
// IP headerに対する処理
break;
case REF_UDP_PACKET:
udp = (struct udp_header_t *)data;
// UDP headerに対する処理
break;
case REF_TCP_PACKET:
tcp = (struct tcp_header_t *)data;
// TCP headerに対する処理
break;
default:
break;
}
return 0;
}
int main(void) {
// 前処理:省略
// Ethernetフレームの解析
(void)REF_dumpRawFrame((const struct REF_rawFrame_t *)frame, callback);
// 後処理:省略
}
Ethernetフレームの解析結果は、callback
関数を通して通知されます。
上記のサンプルは、下記に格納しています。
https://github.com/yuruto-free/raw-ethernet-frame/blob/v1.0.0/src/main.c
まとめ
実装結果は、以下に格納してあります。
https://github.com/yuruto-free/raw-ethernet-frame/tree/v1.0.0
Ethernetフレームの構造
Ethernetフレームは、以下のような構造になっています。
データの部分は、IP、TCP/UPDなど扱うプロトコルに応じて変化します。
IPヘッダの構造
IPヘッダは、以下のような構造になっています。
UDPヘッダの構造
UDPヘッダは、以下のような構造になっています。
TCPヘッダの構造
TCPヘッダは、以下のような構造になっています。
また、セッションフラグの内訳は、以下のようになります。
使い方 | Ethernetフレームの構築
まず、C言語のプログラムで、下記のヘッダーを読み込みます。
#include "rawEthernetFrame.h"
#include "packetStructure.h"
次に、Ethernetフレームを扱う変数を定義、領域を確保します。
ここで、struct REF_param_t
は、rawEthernetFrame.h
で定義されています。
そして、Ethernetフレームを構築するAPIであるREF_createRawFrame
関数を呼び出します。
最後に、確保した領域を解放します。
int main(void) {
int32_t ret;
struct REF_param_t params;
struct REF_rawFrame_t *frame = NULL;
// 領域確保
ret = REF_mallocRawFrame(&frame);
if ((int32_t)REF_SUCCESS != ret) {
// エラー処理
// 省略
}
// パラメータの設定
memset(¶ms, 0x00, sizeof(struct REF_param_t));
// 具体的な設定内容は省略
//
// Ethernetフレームの構築
ret = REF_createRawFrame((const struct REF_param_t *)¶ms, frame);
if ((int32_t)REF_SUCCESS != ret) {
// エラー処理
// 省略
}
// 確保した領域の開放
(void)REF_freeRawFrame(&frame);
}
使い方 | Ethernetフレームの解析
Ethernetフレームの構築とほとんど同じ使い方となります。
差分としては、以下の2点があります。
- callback関数を定義する。
- REF_dumpRawFrame関数を呼び出す。
差分部分を抽出した例を以下に示します。
// callback関数の定義
static int32_t callback(uint8_t packetType, void *data) {
struct ether_header_t *eth;
struct ip_header_t *ip;
struct udp_header_t *udp;
struct tcp_header_t *tcp;
switch (packetType) {
case REF_ETHER_PACKET:
eth = (struct ether_header_t *)data;
// Ether headerに対する処理
break;
case REF_IP_PACKET:
ip = (struct ip_header_t *)data;
// IP headerに対する処理
break;
case REF_UDP_PACKET:
udp = (struct udp_header_t *)data;
// UDP headerに対する処理
break;
case REF_TCP_PACKET:
tcp = (struct tcp_header_t *)data;
// TCP headerに対する処理
break;
default:
break;
}
return 0;
}
int main(void) {
// 前処理:省略
// Ethernetフレームの解析
(void)REF_dumpRawFrame((const struct REF_rawFrame_t *)frame, callback);
// 後処理:省略
}
Ethernetフレームの解析結果は、callback
関数を通して通知されます。