うたカモ技術ブログ

Linux OpenWrt ネットワーク

【第 5 回】 OpenWrt開発入門   作ってみようパケットキャプチャソフト

post:     update: 

この記事で紹介するC言語のソースコードは、基本的にどのLinuxディストリビューションでもコンパイル・実行できるものです。そのため、 この記事に限り、パケットキャプチャソフトの作り方のみを知りたいという方も対象としています。

OpenWrtソフトウェア開発として、lipcapを使用したIPv4通信のパケットキャプチャソフトの作り方を紹介します。

libpcapとは、Linuxのパケットキャプチャソフトとして有名なTCPDUMPのコアライブラリです。

今回は、このライブラリを利用して特定のNIC上に流れるEthernetフレームのMACアドレスとIPv4パケットのIPアドレスを抽出し、RAM領域の一時ファイルに内容を出力する 簡単なパケットキャプチャソフト(duckdump)を開発します。

開発にあたって、まずはプロトコルやパケットの仕組みを説明し、ネットワークインタフェース層(OSI参照モデルではデータリンク層)の生の送受信データ(RAWデータ)から必要とする情報を抽出するC言語のプログラムを作成していきます。

ここを理解することで、応用としてニッチな独自プロトコルのパケットキャプチャや送信パケットの作成もできるようになるでしょう。

最後に、完成したプログラムを開発用PCのUbuntu 22.04 LTS 64bit上でコンパイル・実行してみます。

それでは始めましょう。

第6回では、今回のパケットキャプチャソフトをOpenWrtのターゲットデバイス(Raspberry Pi3B)用 にパッケージ化してインストールします。

目次

  1. 実施環境と補足事項
  2. ネットワーク上に流れるデータとパケット理解
  3. パケットキャプチャとは
  4. Ethernetフレーム構造
  5. IPv4パケット構造
  6. ソースコード紹介
  7. 開発用PC(Ubuntu)でパケットキャプチャソフトを実行してみる
  8. おわりに
  9. 参考文献

実施環境と補足事項

この記事は以下の環境で実施した結果を元に作成しています。

#開発用PCの実行環境OS
ubuntu 22.04 LTS 64bit
プログラミング言語
C言語
#Cコンパイラ
gcc version 11.3.0
#パケットキャプチャライブラリ
libpcap-dev version 1.10.1-4build1_amd64

また、記事内で紹介するコンソール表記は次の通りです。

開発用PCのコンソール表記
ユーザー名とカレントディレクトリを色付けして表記します。
user:~/openwrt$ command

その他の補足事項は下記の通りです。

  • ・この記事で作成するパケットキャプチャソフトはIPv4のみ対応です。
  • ・プロトコルやパケットはTCP/IP階層モデルをベースに説明します。
  • ・この記事で紹介するEthernetフレームとは、現在主流の「EthernetⅡフレーム」のことを指します。
  • ・この記事で紹介するHTTPパケットのバージョンはHTTP/2です。

ネットワーク上に流れるデータとパケット理解

はじめに、ネットワーク上の送信ホストから受信ホストへデータがやり取りされる仕組みについて説明します。

ネットワークには膨大な技術知識があり、全てを細かく説明していると底無し沼にハマります。 そのため、この記事ではパケットキャプチャソフトを作る上で必要な部分について、ざっくりと説明します。

まず、ネットワーク上に流れるデータがいくつかのプロトコル層(レイヤー)のパケットで構成され、それらには入れ子の関係性があることを理解する必要があります。

ネットワーク上のデータのやり取りには、いくつかのプロトコルが関係しており、それらのパケットが組み合わさってデータが送受信されます。

現在のインターネット上で送受信されるデータは次の表のように、いくつかのプロトコルのパケット組み合わせで構成されます。 ※表はデータの種類によって異なる組み合わせが存在するため、あくまで1つの例です。

プロトコル レイヤー 説明
HTTP アプリケーション層 Webブラウザーが画面に表示するデータを持っています。
TCP トランスポート層 送信先ホストのアプリケーションの場所(ポート番号)とデータの信頼性を持たせるルールを持っています。
IPv4 インターネット層 IPv4ネットワーク上に配置されている特定端末の住所をIPアドレスとして持っています。
Ethernet ネットワークインタフェース層 隣接機器のMACアドレスや電気的な情報を解釈するための物理情報を持っています。 データの実体はパケットではなく、フレームと呼びます。

パケットとは、ネットワーク上の通信ルール(プロトコル)に則ったデータそのものを指します。プロトコルとはハードウェア回路(低層) やOSの通信機能であるプロトコルスタック(中層)、アプリケーション(上層)がデータを解釈するためのルールをまとめたものです。 このルールを共通のものとして各ネットワーク製品が使用していれば、異なる種類の機器やアプリケーションでも、互いにデータをやり取りできるようになります。

ここで重要なのは、上の表の各パケットはマトリョーシカのように入れ子状の関係になっていることです。 この入れ子構造は、各パケットがヘッダー部とペイロード部で構成され、ペイロード部には、別の上位のパケットが格納されることで成り立ちます。

対してヘッダー部は、そのパケット自身の役割に応じた情報が格納されている領域です。この情報は一般的に受信ホスト側のプロトコルスタックと呼ばれる 処理システムで解釈され、データ受信時の処理に使用されます。

データ送信の際には、上記のパケット構造を利用して、上位のパケットを低層のパケットが持つペイロード部に順次格納していきます(①→②→③→④)。

そして、ネットワークインタフェース層のEthenertフレームのペイロード部に、それより上のパケットが全て詰め終わると、 最後に物理層ヘッダーを付けて(上図では省略)、ネットワーク上にデータ(マトリョーシカ構造になったパケット本体)が送信されます。

送信されたデータは、受信ホストのプロトコルスタックにより低層から上層にかけて段階的に処理されます。

イメージを付けるために送信ホストからHTTP、TCP、IPv4、Ethernetが詰め込まれたデータが受信ホストに届くと 仮定して考えてみましょう。

これはWebサーバーがWebブラウザーからのHTTPリクエスト(ページ要求)を受けて、該当ページをユーザーに渡すときのパケット構成になります。 そのため、送信ホストはWebサーバー、受信ホストはページ要求をした皆さんのPCだと考えて下さい。

まず、受信ホストのユーザーPCにWebサーバーからのレスポンスデータが到達すると、物理フレーム、Ethernetフレームにあるヘッダー部の情報が電気的なデータ変調・復調を処理する回路やMACアドレスを解釈するデバイスドライバー(⑤)により処理されます。

これによって、Ethernetフレームが受信ホスト宛てのデータであることが判明した場合は、Ethernetフレームのペイロード部 が持つIPv4パケットが取り出され、次はOSの機能であるIPプロトコルスタックに渡されます(⑥)。そして、今度は IPv4パケットのヘッダー部に記載されているIPアドレスをIPプロトコルスタックが解釈し、そのアドレスが受信ホスト自身のものであった場合は、IPパケットのペイロード部に入っている TCPパケットが上層のTCPプロトコルスタックに渡されます(⑦)。

TCPパケットのヘッダー部には、最終的なデータの届け先であるアプリケーションのポート番号が記載されていますので、それを 読み取ったTCPプロトコルスタックは、TCPパケットのペイロード部が持つHTTPパケットをそのポート番号を管理するアプリケーションに渡します(⑧)。

このようにして、受信ホスト側は受け取ったデータをマトリョーシカのように開けていき、今回の例ではアプリケーション層のHTTPパケットが最後に取り出されます。 パケットは今までプロトコルスタックが処理を担当していましたが、アプリケーション層のHTTPパケットはGoogle Chromeやsafari、Edge、firefoxなどのアプリケーション(Webブラウザ)によってヘッダー部とペイロード部が解釈され、ブラウザー 画面上にWebページが表示されます。このとき、Webサーバー情報やプロトコルバージョンはヘッダー部、Webページのコンテンツはペイロードに格納されています。

もし、アプリケーション層プロトコルのパケットがSMTPやIMAP、POPであればOutlookやthunderbird、Gmailなどがヘッダー部とペイロード部 を解釈し、メールを送信したり、受信したりします。

現在のネットワーク通信はこのようにしてパケットデータを階層化し、プロトコルスタックや最上位のアプリケーションに段階的にデータを解釈させることで パケットの受信を完了します。そのため、データを送信する側は受信処理の仕組みに則り、パケットを入れ子状にしてパケットデータを 構築します。

パケットキャプチャとは

ここでは、パケットキャプチャの仕組みについて説明します。

前節では、ネットワーク上で送受信されるデータはマトリョーシカのような入れ子構造を持ったパケットの組み合わせであることを 説明しました。

そして、送信ホストは各プロトコルのパケットを①→②→③→④の順で詰めて送信し、受信ホストは送信ホストとは逆順の⑤→⑥→⑦→⑧にパケットを 段階的に解釈してデータ受信が完了することを説明しました。

ここで重要なのは、送信ホストと受信ホストはプロトコルスタックと呼ばれる処理系にパケットを詰めてもらったり、開けて解釈してもらったりしているということです。

中でも、各プロトコルスタックが管理するパケットを意識することがパケットキャプチャソフトを作るためのライブラリ選びで重要です。 例えば、インターネット層のIPv4パケットを処理するIPプロトコルスタックは自身が処理するIPv4ヘッダーと上位パケットを持ちます。

IPプロトコルスタックはデータ送信の準備のときに、上層のTCPプロトコルスタックなどからパケットが渡されると、IPv4ヘッダーを付加した上で低層のEthernetスタック(デバイスドライバー)にIPv4パケットをインタフェースを介して渡します。

逆にデータ受信時では、Ethernetフレームを処理するデバイスドライバーから、そのペイロード部分であるIPv4パケットが提供されますので、それを解釈して上層のトランスポート層のTCPプロトコルスタックにIPv4パケットのペイロード部を渡します。

このようなプロトコルスタック間でのパケットやり取りがある中で、パケットキャプチャをしようと考えたとき、どこのプロトコルスタックのデータの流れを見る(スニファまたはキャプチャする)かが重要になります。

キャプチャしたいパケットがIPv4だけであれば、IPプロトコルスタック層からのデータのやり取りをキャプチャするライブラリを使うことで目的が果たせます。

しかし、キャプチャ対象がEthernetフレームであれば、インターネット層からのキャプチャライブラリでは不十分です。
Ethernetフレームを見るには、ネットワークインタフェース層のキャプチャライブラリが必要です。

このようにしてライブラリ選びをすると、ネットワークインタフェース層のキャプチャができるlibpcapを使用するのが望ましいと言えます。 libpcapを使用することでEthernetフレームとそれより上層のパケットを見ることができます。少なくとも、アプリケーション層などの上層のみを見ることができるソケットAPI(上図に記載)では事足りません。

以上から、この記事ではlibpcapを使用して、送信ホストと受信ホストのIPv4アドレスとMACアドレス(※)を表示するパケットキャプチャソフトを開発します。

パケットキャプチャソフトの処理は、はじめにlibpcapによってネットワークインタフェース層のEthernetフレーム(以下、RAWデータ)を取得します(①)。 そして、得られたRAWデータに対してEthernetヘッダーとIPv4ヘッダー定義をマッピング(②)して、そこから送信ホストと受信ホストのIPアドレスとMACアドレス(※) を明示的に抽出します。最後に抽出内容をRAM領域の一時ファイルに出力(③)することでパケットキャプチャを実現します。

※MACアドレスは隣接機器のアドレスと受信ホストのアドレスのものになります。

次節では、今回のキャプチャ対象であるEthernetフレームとIPv4パケットの構造について説明します。
ここで紹介する内容が理解できれば、構造体でどのようにヘッダー定義を作れば良いか分かります。

Ethernetフレーム構造

ここでは、Ethernetヘッダー部の詳細を見ていきます。
これは、C言語でEthernetヘッダー定義を構造体として作成するための準備になります。
※ここで取り上げるEthernetフレームは現在主流の「EthernetⅡフレーム」です。

Ethernetのヘッダー部は次の図に示すデータにより構成されます。

各データの説明は次の通りです。

MACアドレス [6byte]
機器固有のアドレス情報です。 重複のない世界で唯一のアドレスです。主にネットワークインタフェース層(OSI参照モデルではデータリンク層)で 使用され、同一ネットワークに所属する機器同士が通信対象を特定するために使用します。
Ethernetのヘッダー部では送信元と宛先ホストのMACアドレスの組が内包されます。
ただし、ここで言う送信元ホストとは通信経路上にいて、宛先ホストに直接データを送ってきた機器のことです。
MACアドレスを詳細にみると、上位3byteはベンダーIDと呼ばれる区分であり、メーカーに割り振られる固有番号です。 下位3byteはその製品のシリアル番号を指します。このような割り振りのため、MACアドレスからその機器の製造メーカーなどが 特定できます。なので、WebサイトではMACアドレスが塗りつぶされた形で製品画像やコンソール結果が掲載されてたりします。
通常、MACアドレスのデータは1byte(8bit)区切りで、上位から第1オクテット~第6オクテットと呼びます。
Ethernetヘッダーでは先頭に宛先MACアドレス、続いて送信元MACアドレスを配置していることが分かります。
タイプフィールド [2byte]
そのEthernetフレームのペイロード部がどんな上位パケットを持っているかを示すデータです。 実に様々なプロトコルのパケットをペイロード部に積むことができます。
今回は、IPv4通信だけをキャプチャしますので、タイプフィールドの中身が0x0800(IPv4) に限り、そのヘッダー情報(IPv4アドレス)をRAM領域の一時ファイルに出力するようにします。
識別値(HEX) プロトコルタイプ
0x0800Internet Protocol Version 4
0x86ddInternet Protocol Version 6
-------その他のプロトコルも知りたい方はこちら

このように、ヘッダー部を細かく見ていくと、上位6byteが宛先、次の6byteが送信元MACアドレス、 そして最後の2byteがタイプフィールドで構成されていることが分かります。

これら各データの配置構成理解は、Ethernetヘッダーを抽出するためのC言語構造体を作るために必須です。
正しい配置関係のEthernetヘッダー定義を構造体として作ることで、それをlibpcapによって得られたRAWデータに被せ(マッピング)、 そこから明示的に対象データ(ヘッダー情報など)を抽出することができます。

IPv4パケット構造

次はIPv4パケットのヘッダー部について説明します。
IPv4パケットのヘッダー部は、Ethernetよりも多くのデータによって構成されます。

今回のパケットキャプチャは、IPv4ヘッダーの中のIPv4アドレスを抽出してそれを一時ファイルに出力すれば良いので、そこだけを紹介します。 一応、その他のヘッダー情報も抽出できるようにC言語の構造体を作成しますので、興味があればこの記事の掲載ソースコードを改造して中身を 表示させて見てください。

なお、IPv4パケットは前節で既に説明したように、Ethernetフレームのペイロード部に積まれる情報でもあります。

そのため、C言語の構造体として定義するIPv4ヘッダーの構造体は、Ethernetフレーム構造体のペイロード用メンバ変数として宣言されます。 (何を言っているか分からなかった場合は、この記事の掲載ソースコードを見たほうが早いです。)

それでは、IPv4アドレスについて説明します。

IPv4アドレス [4byte]
IPv4ネットワークにおけるホスト住所を表すアドレスです。IPv4アドレスを基に、ルーターやL3スイッチは 通信経路を決定し、宛先に間接的・直接的に繋がる機器にパケットを転送します(これをルーティングと呼びます)。
IPv4アドレスのデータは1byte(8bit)区切りで、上位から第1オクテット~第4オクテットと呼びます。

宛先と送信元のIPv4アドレスはIPv4ヘッダーの先頭から96byte目ですので、正しくデータを抽出できるように、 IPv4ヘッダーの他データ定義もしっかり行います(96byte分のオフセットを利用するのでも良いのですが、パケット構造を理解しながらプログラミングをするなら 全部定義した方がいいだろうとなりました)。

それでは次節からパケットキャプチャソフトのソースコードを紹介します。

ソースコード紹介

この記事で紹介するソースコードは自由に使っていただいて構いません。 アプリケーションの開発や自己学習にお役立て下さい。ただし、当ブログでは掲載するソースコードを流用・利用したことによる損害等につきましては 一切の責任を負いません。自己責任で利用お願いします。

パケットキャプチャソフト(duckdump)のソースコードを紹介します。
このパケットキャプチャソフトはLinuxのデーモンとして動作し、引数として与えられたNIC上のIPv4通信をキャプチャ、ログファイルに出力します。 ただし、ログファイルを書き込み続けるわけには行きませんので、最新のキャプチャログ100件のみを残します(古いログは上書きされます)。

実際に試してみたい方は、掲載ソースコードを自身のユーザーホームディレクトリの好きな場所にコピペするか、GitHubのリポジトリからクローンまたはダウンロードして、次節で紹介する実行方法に従ってください。 ※ただし、この記事で紹介する実行方法の想定環境はUbuntu 22.04 LTS 64bitです。

ここでは最初に、EthernetとIPv4ヘッダーの構造体を定義したヘッダファイル(duckdump.h)から掲載します。

前節のEthernetフレーム構造IPv4パケット構造にある図の#番号に対応する 構造体定義の該当箇所に、コメントで同じ番号を割り振っています。前節の図を都度参照して、構造体定義を理解してください。

これらの構造体を利用し、libpcapによって取得したEthernetフレームのRAWデータに対してEthernetとIPv4ヘッダー定義をマッピングすれば、それらが内包するヘッダー情報を明示して抽出することが可能になります。

※これで構造体の仕組みが理解できれば、ニッチな装置やシステム上で使用される独自プロトコルの構造体定義も仕様書を読みながら作成 できるでしょう。テストツール開発とかでも利用できそうです。

/* duckdump.h */
/* libpcapには予めプロトコルの定義(netinet/ether.hなど)が存在しますが今回は使用しません。 */
/* パケット構造を理解することも目的に含め、今回は以下のように自作しています。 */
#ifndef _DUCKDUMP_H
#define _DUCKDUMP_H

#define INTERNET_PROTOCOL_VERSION_4 0x0800

/*******************************/
/*  Address Structure Define   */
/*******************************/

/* Macアドレス */
typedef struct mac_address 
{
    /* TOTAL SIZE = 6byte [48bit] */
    unsigned char octet1;
    unsigned char octet2;
    unsigned char octet3;
    unsigned char octet4;
    unsigned char octet5;
    unsigned char octet6;  
} mac_address;

/* IPv4アドレス */
typedef struct ipv4_address 
{
    /* TOTAL SIZE = 4byte [32bit] */
    unsigned char octet1;
    unsigned char octet2;
    unsigned char octet3;
    unsigned char octet4;
} ipv4_address;

/****************************/
/*  Protocol Packet Define  */
/****************************/

/* Internet Protocol Version 4 [IPv4] */
/* 1byte未満のデータはビットフィールドで指定しています。 */
/* ビットフィールドは「型 変数名 : ビット数」で指定可能です。*/
typedef struct ipv4 
{
    /* IPv4 Header #3 */
    unsigned char version : 4;      /* #3-1 */
    unsigned char ihl : 4;          /* #3-2 */
    unsigned char type_of_service;  /* #3-3 */
    ushort total_length;            /* #3-4 */
    ushort identification;          /* #3-5 */
    ushort flags : 3;               /* #3-6 */
    ushort offset_flagment : 13;    /* #3-7 */
    unsigned char time_of_live;     /* #3-8 */
    unsigned char upper_protocol;   /* #3-9 */
    ushort header_checksum;         /* #3-10 */
    ipv4_address src_ip_addr;       /* #3-11*/
    ipv4_address dst_ip_addr;       /* #3-12 */

    /* IPv4 ペイロード部 #4 */
    /* 今回は抽出しないので未定義 */
} ipv4;

/* Ethernetフレーム */
typedef struct ethernet 
{
    /* Ethernet ヘッダー部 #1 */
    mac_address dst_mac_addr;  /* #1-1 */
    mac_address src_mac_addr;   /* #1-2 */
    ushort upper_protocol_type; /* #1-3 */

    /* Ethernet ペイロード部 #2 */
    ipv4 payload;   /* #3 & #4 */
} ethernet;

#endif

次は、パケットキャプチャ処理が記述されたソースコード(duckdump.c)を掲載します。

このソースコードはEthernetフレームのRAWデータ取得に、libpcapのpcap_next関数を利用しています。
コードを読んで使い方を理解して下さい。※コメントアウトで補足を入れています。

ちなみに、このソースコードはGigazine様の「UNIX/Linuxの「デーモン」はこうやって作る」で紹介されていた デーモンプログラムのテンプレートを使用しています。興味があれば、そちらも参照してください。

/* duckdump.c */
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <syslog.h>
#include <time.h>
#include <ctype.h>
#include <pcap.h>  /* libpcapのヘッダーファイル */
#include "duckdump.h"

#define MAX_LINE_FOR_OUTPUTFILE	100

/* PROTOTYPE DECLARATION */
void output_logfile(FILE *fp, const u_char *raw_data, struct tm *localtime);

/* GLOBAL VARIABLE DECLARATION */
bool terminate_flg = false;

static void create_daemon()
{
    pid_t pid;

    pid = fork();

    if (pid < 0)
        exit(EXIT_FAILURE);

    if (pid > 0)
        exit(EXIT_SUCCESS);

    if (setsid() < 0)
        exit(EXIT_FAILURE);

    signal(SIGCHLD, SIG_IGN);

    signal(SIGHUP, SIG_IGN);

    pid = fork();

    if (pid < 0)
        exit(EXIT_FAILURE);

    if (pid > 0)
        exit(EXIT_SUCCESS);

    umask(0);
    
    chdir("/");

    int x;
     for (x = sysconf(_SC_OPEN_MAX); x>=0; x--)
    {
        close(x);
    }
}

void sigterm_handler(int signum)
{
    terminate_flg = true;
}

int main(int argc, char** argv)
{
    create_daemon();

    FILE *fp;
    pcap_t *pcap_handle = NULL;
    pcap_if_t *ift = NULL;
    struct pcap_pkthdr header;
    const u_char *raw_data;
    char errbuf[PCAP_ERRBUF_SIZE];
    struct stat st = {0};

    /* シグナルハンドラ登録 killコマンドによるプロセス終了時にsigterm_handler関数を呼び出す */
    signal(SIGTERM, sigterm_handler);

    /* tmpfファイルシステム上にduckdumpディレクトリがなければ作成 */
    if (stat("/tmp/duckdump", &st) == -1)
        mkdir("/tmp/duckdump", 0777);
    
    /* 一時ファイルcap.logを書き込みモードでオープン */
    if((fp = fopen("/tmp/duckdump/cap.log", "w")) == NULL)
    {
        return EXIT_FAILURE;
    }

    /* 引数として何も指定していなかった場合は終了(cap.logには使用可能なNIC一覧が出力されている) */
    if(argv[1] == NULL)
    {
        fprintf(fp, "please specify target nic. (ex eth0, wlan0\n");
        fclose(fp);
        return EXIT_FAILURE;
    }

    bool exist_nic = false;

    /* システムが管理するNIC情報を取得 */
    if(pcap_findalldevs(&ift, errbuf) == 0) {
        pcap_if_t *it = ift;
        
        fprintf(fp, "NIC LIST\n");
        
        while (it)
        {
            fprintf(fp, "Device: %s - %s\n", it->name, it->description);

            if(strcmp(argv[1], it->name) == 0)
                exist_nic = true;

            it = it->next;
        }
        pcap_freealldevs(ift);
    }
    else {
        fprintf(fp, "error: %s\n", errbuf);
        fclose(fp);
        return EXIT_FAILURE;
    }

    /* 引数で指定したNICと一致するものがなかった場合は終了 */
    if(exist_nic == false)
    {
        fprintf(fp, "Target NIC not found.\n");
        fclose(fp);
        return EXIT_FAILURE;
    }

    /* パケットキャプチャハンドルを取得します。これはパケットキャプチャ処理の前準備です。 */
    /* インタフェースの起動に最大30秒待ちます。 */
    for (int retry = 0; retry < 5; retry++)
    {
        pcap_handle = pcap_open_live(argv[1], BUFSIZ, 1, 1000, errbuf);
        
        if (pcap_handle != NULL)
            break;

        sleep(6);
    }

    if (pcap_handle == NULL)
    {
        fprintf(fp, "Couldn't open device %s: %s\n", argv[1], errbuf);
        fclose(fp);
        return EXIT_FAILURE;
    }

    time_t current_raw_time;
    struct tm localtime; 
    int line_num = 0;

    /* パケットキャプチャのメイン処理 */
    for (;;)
    {
        current_raw_time = time(NULL);
        localtime_r(&current_raw_time, &localtime);

        /* 一時ファイルに最大100個のパケットログを出力します */
        /* 上限に達すると、出力位置を先頭行に戻して出力を続けます */
        if(line_num >= MAX_LINE_FOR_OUTPUTFILE)
        {
            //fseek(fp, 0L, SEEK_SET);
            if (freopen("/tmp/duckdump/cap.log","w", fp) == NULL)
                return EXIT_FAILURE;
                
            line_num = 0;
        }

        /* パケットキャプチャにより、EthernetフレームのRAWデータを取得します */
        raw_data = pcap_next(pcap_handle, &header);

        if(raw_data != NULL)
        {
            /* キャプチャ情報を一時ファイルに出力します */
            output_logfile(fp, raw_data, &localtime);
        }

        /* killコマンドによってSIGTERMシグナルが通知されたときに、フラグが立ちます */
        /* パケットキャプチャループを抜け、終了処理に移行します */
        if(terminate_flg == true)
        {
            fprintf(fp, "This program is terminated.");
            break;
        }

        line_num++;
    }

    /* プロセス終了時の後処理 */
    pcap_close(pcap_handle);
    fclose(fp);
    return EXIT_SUCCESS;
}

//EthernetフレームのRAWデータにEthernetヘッダーとIPv4ヘッダ―定義をマッピングします。
//これにより、ヘッダー情報を明示的に抽出(取得)することが可能です。
//得られたIPv4アドレスとMACアドレスは/tmp/duckdump/cap.logに記録されます。
void output_logfile(FILE *fp, const u_char* raw_data, struct tm *localtime)
{
    ethernet *frame = (ethernet*)raw_data; //Ethernetヘッダー定義を被せます(マッピング)。
    ipv4 *packet = &(frame->payload); //Ethernetペイロードの先頭からIPv4ヘッダー定義を被せます。
    //ポイント:IPv4アドレス指定が長くなるのでIPv4ヘッダー構造体をさらに被せています。

    //ガード処理
    //EthernetのタイプフィールドがIPv4(0x0800)以外のものは、ここで終了(キャプチャ情報を出力しない)
    //ネットワークバイトオーダーをホストバイトオーダーに変換するのを忘れずに。
    if( ntohs(frame->upper_protocol_type) != INTERNET_PROTOCOL_VERSION_4 )
        return;

    /************************************/
    /*           ファイル出力処理        */
    /************************************/

    //キャプチャ時刻
    fprintf(fp, "%d/%d/%d %d:%d:%d ",
        localtime->tm_year+1900, localtime->tm_mon+1, localtime->tm_mday,
        localtime->tm_hour, localtime->tm_min, localtime->tm_sec );

    //送信先IPv4アドレス     構造体定義のメンバ変数を利用し、明示的にアドレス情報を指定して出力します。
    fprintf(fp, "[ dst_ip = %d.%d.%d.%d   ",
        packet->dst_ip_addr.octet1, packet->dst_ip_addr.octet2,
        packet->dst_ip_addr.octet3, packet->dst_ip_addr.octet4 );

    //送信先MACアドレス
    fprintf(fp, "dst_mac = %x:%x:%x:%x:%x:%x ]   ",
        frame->dst_mac_addr.octet1, frame->dst_mac_addr.octet2, frame->dst_mac_addr.octet3,
        frame->dst_mac_addr.octet4, frame->dst_mac_addr.octet5, frame->dst_mac_addr.octet6 );

    //送信元IPv4アドレス
    fprintf(fp, "[ src_ip = %d.%d.%d.%d   ",
        packet->src_ip_addr.octet1, packet->src_ip_addr.octet2,
        packet->src_ip_addr.octet3, packet->src_ip_addr.octet4 );

    //送信元MACアドレス
    fprintf(fp, "[ src_mac = %x:%x:%x:%x:%x:%x ]\n",
        frame->src_mac_addr.octet1, frame->src_mac_addr.octet2, frame->src_mac_addr.octet3,
        frame->src_mac_addr.octet4, frame->src_mac_addr.octet5, frame->src_mac_addr.octet6 );
}

開発用PC(Ubuntu)でパケットキャプチャソフトを実行してみる

OpenWrtデバイス上で実行する前に、開発用PCでパケットキャプチャソフトの動作確認をしてみましょう。

コンパイルの準備として、まずはlibpcapライブラリをaptコマンドでインストールします。

kamo@kamo:~/duckdump$ sudo apt-get install libpcap-dev

次に、上記の掲載ソースコードを置いたディレクトリまで行き、gccコマンドでコンパイルします。
私の場合はホームディレクトリ直下にduckdumpディレクトリを作り、そこにソースファイルを置いてみました。

kamo@kamo:~/duckdump$ gcc -o duckdump duckdump.c -lpcap

正常にコンパイルが完了すれば、実行可能ファイルのduckdumpができます。
引数としてパケットキャプチャしたいNICを指定してsudo付きで実行してみましょう。
※NICが分からなかったらifconfigコマンドなどで確認してみてください。

以下は、NIC名:wlan0のIPv4通信をパケットキャプチャするときのduckdump起動例です。

kamo@kamo:~/duckdump$ sudo ./duckdump wlan0

実行すると、バックグラウンドでネットワーク通信のパケットキャプチャが開始されます。Webサイトなどにアクセスしてトラフィックを発生させた後に/tmp/duckdump/cap.logをcatコマンドで見てください。

次のように、キャプチャしたパケットのIPアドレスとMACアドレスが記録されているはずです。

終了方法はpsコマンドでduckdumpのプロセスIDを確認後、killコマンドで終了します。

kamo@kamo:~$ ps -A
kamo@kamo:~$ sudo kill プロセスID

おわりに

今回はlibpcapを用いて、対象NIC上のIPv4通信をパケットキャプチャするソフトウェアを開発してみました。 そして、開発用PCのUbuntu上でコンパイルして実行することで、確かにパケットキャプチャ情報をログファイルに出力できることが確認できました。

次回は、このパケットキャプチャソフトをOpenWrtデバイスのRaspberry Pi 3B用にコンパイルし、インストールパッケージを作成してみたいと思います。 パッケージ作成にあたって、初期化スクリプトやUCIコンフィグレーションファイルの作成・適用方法についても紹介します。

参考文献