広告 プログラミング

【図解】Ethernetフレームの構造

※本ページには、プロモーション(広告)が含まれています。

悩んでいる人

Ethernetフレームを自前で実装する必要があるため、Ethernetフレームの構造を実装例とともに解説して欲しい。

こんなお悩みを解決します。

今回は、Ethernetフレームの構造を解説し、同時に実装例を紹介します。

現在では、宛先、ポート、送信するデータを用意して関数を呼び出せば、Ethernetフレームの構造を知らなくても通信ができます。

実際、通信する際は、通信プロトコルに合わせて様々なヘッダーが付与されるのですが、このヘッダー付与は、kernelが担っています。

一方、kernelが担っている部分に変更を加える場合は、途端に対応が困難になります。

そこで、Ethernetフレームの構造を理解し、自前で構築することで、問題を解決したいと思います。

Ethernetフレームの構造

Ethernetフレームは、以下のような構造になります。

Ethernetフレームの構造

上記に登場するMACアドレスについて説明します。

MACアドレスとは、Ethernet通信において使用される物理アドレスのことを指します。

PCには、LANケーブルを差し込む部分があると思いますが、その物理的なポートに割り当てられた固有のIDとなります。

また、上記のうち、オレンジ色のフィールドは、Ethernetヘッダと呼ばれ、以下のような内訳となります。

フィールド内容
宛先MACアドレスEthernet通信する際の宛先のMACアドレス
送信元MACアドレスEthernet通信する際の送信元のMACアドレス
タイプデータフィールドに格納する上位層のプロトコルを識別するための情報
Ethernetヘッダ

タイプで設定できる情報は決まっており、主なものとして以下が利用されます。

ここで、0xで始まる数値は、16進数であることを意味します。

タイププロトコル
0x0800IPv4
0x0806ARP
0x8137IPX
0x86ddIPv6
タイプとプロトコル

以降では、IPv4を対象に、TCP/UDPについて解説をします。

IPヘッダの構造

IPv4で通信する際は、Ethernetフレームのデータ部分にIPヘッダというものが付与されます。

この時、Ethernetフレームの構造をさらに細かく見ると以下のようになります。

Ethernetフレームの構造(IPヘッダ付与版)

このIPヘッダは、以下のような構造になっています。

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バイト
IDIPパケット送信時に付与される識別子大きなデータを送信する際は、複数の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埋め)する。
今回の例では、オプションは利用しない。
IPヘッダの構造

今回、チェックサムを計算するため、以下のような補助関数を定義しています。(非公開関数として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は、以下のように対応します。

IPヘッダのうち「フラグ」の内訳

UDPヘッダの構造

UDPで通信する場合、IPヘッダのデータ部分にUDPヘッダというものが付与されます。

この時、Ethernetフレームの構造をさらに細かく見ると以下のようになります。

Ethernetフレームの構造(UDPヘッダ付与版)

このUDPヘッダは、以下のような構造になっています。

UDPヘッダの構造

各フィールドの内訳は、以下のようになります。

フィールド内容備考
発信元ポート番号発信元のポート番号-
宛先ポート番号宛先のポート番号-
UDPデータ長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フレームの構造をさらに細かく見ると以下のようになります。

Ethernetフレームの構造(TCPヘッダ付与版)

このTCPヘッダは、以下のような構造になっています。

TCPヘッダの構造

各フィールドの内訳は、以下のようになります。

フィールド内容備考
発信元ポート番号発信元のポート番号-
宛先ポート番号宛先のポート番号-
シーケンス番号受信側(宛先)にデータの並び順を伝えるための情報本ライブラリでは、シーケンス番号の計算・設定機能は提供していない
確認応答番号送信側に次に欲しいシーケンス番号を伝えるための情報本ライブラリでは、確認応答番号の計算・設定機能は提供していない
ヘッダ長4バイト単位としたTCPヘッダのヘッダ長オプションなしの場合、IPヘッダは20バイトとなるため、\(20/4=5\)となる。
セッションフラグTCP通信におけるコネクションを制御するための情報以降で、詳細を図解する。
ウィンドウサイズデータ転送時における、受信側が受信可能なデータサイズTCP通信時の効率を上げるために利用される。
チェックサムTCPパケットのチェックサム疑似ヘッダを用いて計算することが多い。
緊急ポインタセッションフラグのURGが有効になっている場合に使用する情報緊急に処理する必要があるデータの場所を示す。
オプションTCPパケットに付加するオプション32bit単位で設定する。32bitに満たない場合は32bitになるようにpadding(0埋め)する。
今回の例では、オプションは利用できるが、内容のチェックまでは行っていない。
TCPヘッダ構造

また、セッションフラグの内訳は以下のようになっております。

セッションフラグの内訳

それぞれの内容を以下に示します。

フラグ名内容
URGこのフラグが1の場合、パケット内に緊急で処理する必要あるデータが含まれていることを表す。
ACKこのフラグが1の場合、確認応答番号が有効であることを表す。
PSHこのフラグが1の場合、受信したデータをすぐに上位のアプリケーションに渡すことを表す。
通常のTCP通信では、受信側である程度データをバッファに溜めてから、
適当なタイミングで上位のアプリケーションに渡される。
RSTこのフラグが1の場合、TCP通信を強制的に切断することを表す。
SYNこのフラグが1の場合、TCPの接続要求を表す。
FINこのフラグが1の場合、TCPコネクション切断要求であることを表す。
セッションフラグの内訳

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(&params, 0x00, sizeof(struct REF_param_t));
    // 具体的な設定内容は省略 
    // 
    // Ethernetフレームの構築
    ret = REF_createRawFrame((const struct REF_param_t *)&params, 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フレームは、以下のような構造になっています。

Ethernetフレームの構造

データの部分は、IP、TCP/UPDなど扱うプロトコルに応じて変化します。

IPヘッダの構造

IPヘッダは、以下のような構造になっています。

IPヘッダの構造

UDPヘッダの構造

UDPヘッダは、以下のような構造になっています。

UDPヘッダの構造

TCPヘッダの構造

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(&params, 0x00, sizeof(struct REF_param_t));
    // 具体的な設定内容は省略 
    // 
    // Ethernetフレームの構築
    ret = REF_createRawFrame((const struct REF_param_t *)&params, 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関数を通して通知されます。

スポンサードリンク

-プログラミング
-