NTTドコモR&Dの技術ブログです。

Linuxカーネルを読解してパケットキャプチャツールを作ろう!カーネルモジュール開発入門!

はじめに


こんにちは!この記事は、ドコモアドベントカレンダー2022の25日目の記事になります。
投稿者はNTTドコモ クロステック開発部の藤枝です!
皆さん突然ですが「メタバース」や「デジタルツイン」というワードを聞いたことありますでしょうか??
メタバースは、最近旧Facebookが社名をMetaに変えて話題になりましたが、「超越した」という意味のメタ(Meta)に、世界を意味する「バース(verse)」をつなげて作られた言葉です。 いわゆるゲームの世界の様な仮想空間でもう一人の自分(アバター)を生活させるといった世界観のものです。
コミュニケーションやビジネスなどの様々なものが提供価値として考えられていますが、まだまだこれといった王道なサービスが台頭してきていないので面白い領域なのです。 また、デジタルツインとは現実世界に実在しているものを、バーチャル世界でリアルに表現したものをさしていて、現実空間の写像を仮想空間に作ってそこでインフラの管理やトラフィックのシュミレーションなどをやるといったものになります。

僕は、ドコモ内でこういったメタバース・デジタルツインを実現する担当にいます。 どうでしょう、誰も実現できてない分野の開拓は楽しそうじゃないですか?
人と違うことをするのが大好きなので今やっていることもドコモってそんなことやってるの?というような分野で楽しいです!

前置きが長くなりましたが、今回のアドベントカレンダーはドコモでこれまでやってなさそうな分野を攻めたいそういった気持ちでテーマを決めました。 タイトルをちょっと大げさに書きましたが、皆さん大好きLinuxの回です。 Linuxと言われれば、Ubuntu使ったことあるよだとかCentOSをサーバとして運用してましたという人もいるかと思います。
今回の目指すところはLinuxのソースコードを読んだりLinuxのカーネルモジュールという特殊なプログラムを作ることです。
Linuxの深部をちらっと覗いてカーネルモジュールというものを作っていきたいと思います。
具体的には、カーネルモジュールの作り方の説明とその知識を応用して簡単なパケットキャプチャツールを作りたいと思います。

アジェンダ


1. 本記事の対象者
2. OSとLinuxについて
3. カーネルモジュールを作ってみよう
4. なんちゃってパケットキャプチャツールを作ってみよう
5. おわりに

1.本記事の対象者


  • Linuxに興味がある方
  • OSについて知ってみたい方
  • カーネル?何それ知ってみたいと思った方

2.OSとLinuxについて


そもそもLinuxって何なんだという方や聞いたことあるけど触ったことないという方向けにLinuxについて説明したいと思います。
Linuxとは皆さんが普段使われているWindowsやMac OSと同じOS(オペレーティングシステム)の一種で、 親近感がわくように身近なものでお話すると、AndroidはLinuxを移動機向けに改良したものなのです。

2.1OSについて

OSというとパソコンを買うと勝手に入っていて何となく使っている人が多いと思うのですが、実は偉大な役割を果たしています。
例えば、OSなしでHDDにアクセスしようと思うとどうしたらよいでしょうか。
HDDの中身は以下図のようになっています。

普段、エクスプローラやFinderなどを使って簡単にファイルにアクセスできますが、OSがなければ図のような色々なパーツを制御して以下のようなことをしないといけません。(厳密にはファイルをメモリに展開しないといけないのでメモリの問題もありますがここでは無視します。)

  1. プラッタと呼ばれる何枚もある磁気ディスクから保存したいディスクを選択
  2. 円形のディスクの中から横軸方面のトラックと呼ばれる場所を選択
  3. トラックの中からセクタを選択
  4. 複数のセクタにデータがまたぐ場合にはどのセクタにどのデータを保存するかを選択
  5. etc

これらをファイルごとに実施し、かつきちんとアクセスできるよう自分で管理して覚えておかないといけないわけです。 非常にめんどくさいですよね。しかも、ミスをするとファイルの一部が上書きされたりなど大変なことが起きます。
そこで、OSの出番です。OSは各ハードウェアへのアクセスやそのほかの機能等々において、抽象化してくれる役割を持っています。
つまり、今回の場合で言うとHDDにアクセスするにしてもHDDのハードウェアの特性を無視して簡単にアクセスさせてくれます。 こういった僕達がPCを使う上でなくてはならない機能を提供してくれるOSですが、 どうやったらみんなが使いやすいかなといった考え方の違いなどでLinuxやWindows、Macなど様々な種類に分かれています。

2.2Linuxについて

LinuxはAT&Tがかつて開発したUnixというOSの考え方を踏襲したOSです。Unixは当時、1つのOSで複数のタスクを制御でき、複数ユーザで動かせる画期的なOSだったためFreeBSD(PS4のOSのベース)・Mac OS・Solaris(SunOS)など様々なOSが影響を受けています。
そして、Linuxを最初に書き始めたLinusさんがUnixを踏襲したのでLinuxというわけです。 ここまでで、Linuxの生い立ちとOSについて分かったと思いますが、いざLinuxと検索をかけて調べたりするとUbuntuだとかCentOSなどのワードが出てきて「ん?Linuxって複数あるの?」となると思います。
Ubuntuなどは、Linuxディストリビューションといわれるものです。

LinuxとはOSに必要な基本的な機能を用意しており、配布される際にはユーザが使いやすいようにUIやアプリケーション群をカスタマイズして配布されます。 そのカスタマイズの違いがディストリビューションで用途別に分けられているため複数存在します。 Linuxの構成をざっくり分けると以下のLinuxの構成の図のように分けられます。
この図では下の方に行くにつれてハードウェアの特性を考慮した複雑な仕組みになっており、上の方に行くにつれて抽象化されユーザが簡単に扱えるようになっています。 こういった階層構造をレイヤといい、OS周りを触っている人たちは低いレイヤを触ることになるので低レイヤと言ったりします。

Linuxの構成

図のカーネル(LinuxではLinuxカーネル言う)という部分が上記の必要な基本的な機能のことで、身近なもので言うとデバイスドライバもこのカーネルの中に含まれます。

本記事ではアプリケーションではなくこのLinuxカーネルに機能を追加するためのカーネルモジュールというものを作っていきます。

3.カーネルモジュールを作ってみよう


本章では、先ほどお話ししたLinuxカーネルに機能を追加できるカーネルモジュールというものを作っていきます。 普段、GUIのグラフィカルなアプリケーションを使う機会が多いかと思います。 それらは、すべてアプリケーションであり、先ほどの図から見るとカーネルより上位レイヤにあるのでOSの基幹的な機能追加はできないです。 なので、今回はカーネルモジュールによってカーネルに機能追加するようなプログラムを書いていきたいと思います。 目指せ、Linuxカーネルマスター!

3.1環境構築

普段からLinuxを使っている人は少ないと思います。 なので、今回はVirtualBoxというOSの上でOSを立ち上げられるアプリケーションをつかってWindows上でLinuxを立ち上げてそのLinuxで開発を進めていきます。 こちらについてはAWSのEC2でも大丈夫です。AWSでやるのであればt2.microあたりのインスタンスを立ててUbuntu ServerのAMI選択後、SSHなどを使ってシェルにログイン後、3.2章から初めていただければ大丈夫です。

まずは、こちらのページからVirtualBoxをダウンロードしてダウンロードしてインストールをしてください。

www.oracle.com

インストールが完了したら次はこちらからUbuntuのイメージをダウンロードしてください。

www.ubuntulinux.jp

上記のファイルがそろったらVirtualBoxを起動して、新規というボタンを押してください。
すると、起動したいOSを指定する画面が出るのでこちらをLinux、Ubuntuと指定して次へを押してください。 それ以降はストレージサイズ等、適宜調節してください。わからなければデフォルトで大丈夫です。

以上、作業が完了すると新しく仮想環境が追加されるので選択し、起動ボタンを押します。

起動ボタンを押すとイメージファイルの指定画面が出てくるのでここで先ほどのUbuntuのイメージファイルを指定します。

この後はインストール画面が立ち上がって通常のインストールと同じ手順なので、割愛します。

3.2開発者ツールのインストール

プログラムを書く時、普通はコンパイラ、標準ライブラリ、追加のライブラリなどが必要になると思います。 基本的には同じイメージなのでこれらを今からインストールしていきます。 ただ、カーネルモジュールの場合少し特殊になるため、以下のものを入れていきます。

  • make:ちょっと賢いビルド用のシェルスクリプトのイメージです。依存関係や更新履歴等を考慮して必要部分のみをビルドしてくれる設定をしてくれます。
  • gcc:GNUの出しているCコンパイラです。カーネルモジュールはC言語で書く必要があるので、ビルドに必要なコンパイラ、リンカなどをこれによってインストールします。
  • カーネルヘッダ:こちらが少し特殊と言っていた部分になります。カーネルヘッダとはLinuxカーネルのヘッダになっており、関数の定義がされている部分になります。これによってシンボルの解決がされるため、自分で書いたカーネルモジュールをコンパイルする際に関数が見つからない!と怒られるのを防げます。

それでは作業に移ります。以下のコマンドをUbuntuの端末 or Terminalというアプリケーションで実行してください。

$sudo apt update
$sudo apt upgrade
$sudo apt install -y make gcc linux-headers-$(uname -r)

インストールの確認ができたらいよいよ開発に移っていきたいと思います!

3.3hello worldと呟くカーネルモジュールを作る

3.3.1ファイルの準備

では早速準備を始めていきましょう。 まずは、開発をするディレクトリを作成しましょう。 名前はなんでも良いですが、今回はホームディレクトリにkmodule_devとでも作りましょうか。 以下のコマンドを作成するとTerminal上からディレクトリを作成することができます

$mkdir ~/kmodule_dev

また、今回必要なファイルは以下の二つとなります。 ディレクトリは先ほど作ったものを利用し、その配下に以下2ファイルを作成してください。

~/kmodule_dev/Makefile

obj-m := hello.o

all:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

~/kmodule_dev/hello.c

#include <linux/module.h>
#include <linux/init.h>

MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("Taro Docomo<taro.docomo@docomo.abc>");
MODULE_DESCRIPTION("Hello world kernel module");

/*
* わかりやすいようにKERN_ALERTをつけて赤くします
*/
static int mymodule_init(void) {
        printk(KERN_ALERT "Hello world!\n");
        return 0;
}

static void mymodule_exit(void) {
        printk(KERN_ALERT "GoodBye world!\n");
}

module_init(mymodule_init);
module_exit(mymodule_exit);

Makefileの説明は今回本質ではないので割愛します。 では早速、hello.cの中身を見ていきましょう。

3.3.2hello.cの探検

~/kmodule_dev/hello.cについて見てみると、色々と普通のC言語によるプログラム開発とは異なる点があるのが見受けられます。 例えば、いきなり冒頭のヘッダの読み込みから標準Cライブラリには存在しないlinux/module.hを読み込んでいますね。 C言語を書いたことがある人からするとすごい違和感だと思います。 カーネルモジュールはカーネルにロードされるため、アプリケーションが利用する標準Cライブラリの関数は使えず、カーネルで定義されている関数のみ使えます。そのため、普通のC言語のアプリケーションの開発とヘッダの種類や関数が変わってきます。 通常、「HelloWorld」と表示するアプリケーションをC言語で書くと以下のようになります。

#include <stdio.h>

int main(void)
{
  printf("Hello World\n");
  return 0;
}

~/kmodule_dev/hello.cとは違い、stdio.hを読み込んでいますね。
これはprintf関数などを定義している「スタンダードIO=標準入出力」のヘッダファイルでこれを読み込むことでprinf関数などが使えるわけです。
また、main関数というのがいてこのファイルを実際実行するとこのmain関数が一番最初に実行されます。
なので、printf(標準出力へ文字列を渡す関数)に書かれているHelloWorldが表示されます。

ここでまた、~/kmodule_dev/hello.cに戻りましょう。 つまり、上記の話を考えるとlinux/module.hは何かの関数を使うために読み込まれていることになります。 見てみるとファイル内で未定義だけど使えている関数は、

  • module_init(mymodule_init);
  • module_exit(mymodule_exit);

これらの関数ですね。なのでmodule_initとmodule_exitを使うためにlinux/module.hを読み込んでいることがわかります。
このmodule_init、module_exitはそれぞれ作ったカーネルモジュールをカーネル本体にロード、アンロードされた際に呼び出す関数を登録してあげる関数です。 これにそれぞれ自分の作った関数を食わせることで、モジュールがロードされた時にはmymodule_init関数が、アンロードされた時にはmymodule_exitが呼び出されます。 なので、これらを踏まえるとこのhello.cが実行されると以下の挙動が予測されます。

  1. カーネルモジュールをロードするとHello world!と表示される
  2. アンロードするとGoodBye world!と表示される

それでは、ビルドして上記の予測する動きを見ていきましょう。

3.3.3カーネルモジュールのビルド(コンパイル)

ではカーネルモジュールをビルドしていきましょう。 今回は、Makefileというビルドの手順を事前に設定しておけるスクリプトを配置したので至ってシンプルです。 以下のコマンドをTerminalで~/kmodule_devのディクレトリ上に移動してから実行してください。

$make

エラー等が吐き出されていなければ成功です。 例として僕が実行した結果を表示します。(AWSで実行しているのがバレますね)

make -C /lib/modules/5.15.0-1026-aws/build M=/home/ubuntu modules
make[1]: Entering directory '/usr/src/linux-headers-5.15.0-1026-aws'
  CC [M]  /home/ubuntu/hello.o
  MODPOST /home/ubuntu/Module.symvers
  CC [M]  /home/ubuntu/hello.mod.o
  LD [M]  /home/ubuntu/hello.ko
  BTF [M] /home/ubuntu/hello.ko
Skipping BTF generation for /home/ubuntu/hello.ko due to unavailability of vmlinux
make[1]: Leaving directory '/usr/src/linux-headers-5.15.0-1026-aws'

最後に、lsコマンドを実行してたくさんファイルが吐き出されていて その中に、hello.koが存在するのを確認してください。

3.3.4カーネルモジュールのロード

前の手順にてカーネルモジュールが出来上がったのでついにロードしていきましょう。 ロードでは、insmodというコマンドを使います。 ビルドと同様に、~/kmodule_devというディレクトリ上で以下コマンドを打ってみましょう。

$sudo insmod hello.ko

うまくいくと何も表示されずに終了します。 では、僕たちが予想したHello world!!という文字を見にいきましょう。 カーネルモジュールはdmesgコマンドというコマンドを利用することで出力を確認することができます。

$sudo dmesg

すると、大量のメッセージの中に、頑張って探すと。。。。

[   16.079593] audit: type=1400 audit(1671609678.468:3): apparmor="STATUS" operation="profile_load" profile="unconfined" name="nvidia_modprobe" pid=348 comm="apparmor_parser"
[   16.080823] audit: type=1400 audit(1671609678.468:4): apparmor="STATUS" operation="profile_load" profile="unconfined" name="nvidia_modprobe//kmod" pid=348 comm="apparmor_parser"
[   16.212193] audit: type=1400 audit(1671609678.600:5): apparmor="STATUS" operation="profile_load" profile="unconfined" name="/usr/bin/man" pid=350 comm="apparmor_parser"
[   16.213409] audit: type=1400 audit(1671609678.604:6): apparmor="STATUS" operation="profile_load" profile="unconfined" name="man_filter" pid=350 comm="apparmor_parser"
[   16.214860] audit: type=1400 audit(1671609678.604:7): apparmor="STATUS" operation="profile_load" profile="unconfined" name="man_groff" pid=350 comm="apparmor_parser"
[   16.343528] audit: type=1400 audit(1671609678.732:8): apparmor="STATUS" operation="profile_load" profile="unconfined" name="tcpdump" pid=351 comm="apparmor_parser"
[   16.403908] audit: type=1400 audit(1671609678.792:9): apparmor="STATUS" operation="profile_load" profile="unconfined" name="/usr/lib/NetworkManager/nm-dhcp-client.action" pid=349 comm="apparmor_parser"
[   16.405571] audit: type=1400 audit(1671609678.796:10): apparmor="STATUS" operation="profile_load" profile="unconfined" name="/usr/lib/NetworkManager/nm-dhcp-helper" pid=349 comm="apparmor_parser"
[   16.407104] audit: type=1400 audit(1671609678.796:11): apparmor="STATUS" operation="profile_load" profile="unconfined" name="/usr/lib/connman/scripts/dhclient-script" pid=349 comm="apparmor_parser"
[   44.859300] loop5: detected capacity change from 0 to 8
[ 4209.441886] hello: loading out-of-tree module taints kernel.
[ 4209.441915] hello: module verification failed: signature and/or required key missing - tainting kernel
[ 4209.442449] Hello world!

一番最後の行にいました!!見つけることができましたね。 これはどうなっているかというと、以下の図のようになっています。

  1. カーネルがロードされたことによりprink関数がカーネルリングバッファという場所にHelloWorld!!を書き込む。
  2. dmesgコマンドの実行により、readシステムコールというファイルを読み出す関数が/dev/kmsgというファイルを対象に実行される。
  3. /dev/kmsgは実はハリボテのファイルになっていて、readシステムコールが呼び出されるとカーネルリングバッファの値を読み出してファイルの中身かのように見せてくる。
  4. readシステムコールの返り値としてカーネルリングバッファの中身がdmesgに渡される。
  5. dmesgが標準出力にカーネルリングバッファの中身を出力する

このように、カーネルモジュール内でprinkによって文字出力をすると、 カーネルリングバッファと呼ばれるカーネル内でのシステム情報のストア、つまりカーネルモジュール君や他のカーネル内の機能さんたちのTwitterに書き込みが行われ、僕たちはそれをdmesgというコマンドを用いて/dev/kmsgというファイル越しに見ていたわけです。

3.3.5カーネルモジュールのアンロード

最後にロードしたカーネルモジュールちゃんとアンロードしておきましょう。
カーネルモジュールはカーネル空間で動きます。つまり、OSによるプロセス管理の恩恵が受けられず、 エラー等が起きるとカーネルパニックというOS自体が固まってしまう現象に見舞われます。 そのためにも、ずっとロードしっぱなしでなくきちんと不要なものはアンロードしておく様にしましょう。
以下が、アンロードのコマンドになります。

$rmmod hello

insmodコマンドの時はファイル名の指定だったので、hello.koが引数に来ましたが、 今度はモジュールが対象になるので、.koがいらないのが注意です。

[   16.079593] audit: type=1400 audit(1671609678.468:3): apparmor="STATUS" operation="profile_load" profile="unconfined" name="nvidia_modprobe" pid=348 comm="apparmor_parser"
[   16.080823] audit: type=1400 audit(1671609678.468:4): apparmor="STATUS" operation="profile_load" profile="unconfined" name="nvidia_modprobe//kmod" pid=348 comm="apparmor_parser"
[   16.212193] audit: type=1400 audit(1671609678.600:5): apparmor="STATUS" operation="profile_load" profile="unconfined" name="/usr/bin/man" pid=350 comm="apparmor_parser"
[   16.213409] audit: type=1400 audit(1671609678.604:6): apparmor="STATUS" operation="profile_load" profile="unconfined" name="man_filter" pid=350 comm="apparmor_parser"
[   16.214860] audit: type=1400 audit(1671609678.604:7): apparmor="STATUS" operation="profile_load" profile="unconfined" name="man_groff" pid=350 comm="apparmor_parser"
[   16.343528] audit: type=1400 audit(1671609678.732:8): apparmor="STATUS" operation="profile_load" profile="unconfined" name="tcpdump" pid=351 comm="apparmor_parser"
[   16.403908] audit: type=1400 audit(1671609678.792:9): apparmor="STATUS" operation="profile_load" profile="unconfined" name="/usr/lib/NetworkManager/nm-dhcp-client.action" pid=349 comm="apparmor_parser"
[   16.405571] audit: type=1400 audit(1671609678.796:10): apparmor="STATUS" operation="profile_load" profile="unconfined" name="/usr/lib/NetworkManager/nm-dhcp-helper" pid=349 comm="apparmor_parser"
[   16.407104] audit: type=1400 audit(1671609678.796:11): apparmor="STATUS" operation="profile_load" profile="unconfined" name="/usr/lib/connman/scripts/dhclient-script" pid=349 comm="apparmor_parser"
[   44.859300] loop5: detected capacity change from 0 to 8
[ 4209.441886] hello: loading out-of-tree module taints kernel.
[ 4209.441915] hello: module verification failed: signature and/or required key missing - tainting kernel
[ 4209.442449] GoodBye world!

これで、きちんとアンロードできたことが確認できました!と言いたいところですが、 これはprinkが動作しているかは確認できますが、直接的にアンロードができた証拠にはならないのです、、 なので、アンロードされたかどうかを確認しましょう。

$lsmod | grep hello

このコマンドで検索してみて出て来なければ来なければアンロードが完了しています。

4.なんちゃってパケットキャプチャツールを作ろう


ここからはかなり内容がヘビーになります。 根気が必要ですが、この章を乗り切った人は自分である程度カーネルモジュールが書けるようになっているはずです。 この章ではカーネルモジュールのソースコード解説しながらLinuxカーネルの中身を追っていくことで、 理解度を高めるとともに、この記事を読み終わった後には自分で独自のコードが書ける状態になっていただければと思っています。 では、さっそくパケットキャプチャツールの中身を見ていきましょう。

4.1 パケットキャプチャツールの実装

やりたい事は、以下の2点です。

  • UDPパケット送信時に送信先と送信元のIPアドレスを取得
  • UDPヘッダの中身を表示

では、実際にソースコードを見ていきたいですが、その前に注意点です。
今回の実装では全ての通信でなく、UDPのみを対象としてます。 TCPではUDPより処理が複雑化しており、今回の実装では外しました。ですが、TCPもUDPに倣って実装することが可能なので やってみてください!
また、ネットワーク系の処理はネットワークバイトオーダというビックエンディアンで処理されているため、 しかし、計算機側ではホストバイトオーダのリトルエンディアンに変換して置かないとprintk等でフォーマット指定子で表示したときに何が書いてあるか分からなくなってしまいます。

上記、確認できましたらソースコードを見ていきましょう。

#include <linux/module.h> /* Needed by all modules */
#include <linux/kernel.h>    /* Needed for KERN_INFO */
#include <linux/init.h>      /* Needed for the macros */
#include <linux/skbuff.h> /* Needed for skbuff struct */
#include <linux/netfilter.h> /* Needed for hook function */
#include <linux/netfilter_ipv4.h>
#include <linux/ip.h> /* Needed for ip header */
#include <linux/if_ether.h>
//#include <linux/tcp.h>
#include <linux/udp.h>
#include <linux/byteorder/generic.h>

MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("Taro Docomo<taro.docomo@docomo.abc>");
MODULE_DESCRIPTION("Packet Capture kernel module");

static struct nf_hook_ops nfhook;

static unsigned int traffic_hook(void *priv, struct sk_buff *skb, const struct nf_hook_state *state)
{
    struct iphdr *ip_header;
    struct udphdr *udp_header;

    ip = (struct iphdr *)ip_hdr(skb);
    printk("s_addr :%x\n",be32_to_cpu(ip->saddr));
    printk("d_addr :%x\n",be32_to_cpu(ip->daddr));
    if(ip->protocol == 17) {
        udp_header = (struct udphdr *)ipip_hdr(skb);
        printk(KERN_ALERT "\n---- UDP Header -------------------\n");
        printk("source port : %7u\t",ntohs(udp_header->source));
        printk("dest port   : %7u\t",ntohs(udp_header->dest));
        printk("UDP_packet_length    : %7u\t",ntohl(udp_header->len));
        printk("check     : %7u\n",ntohl(udp_header->check));
        }
        return NF_ACCEPT;
}

static int __init mymodule_init(void)
{
    int rv = 0;
    nfhook.hook     = traffic_hook;
    nfhook.hooknum  = 0;
    nfhook.pf       = PF_INET;
    nfhook.priority = NF_IP_PRI_FIRST;
    
    nf_register_net_hook(&init_net, &nfhook);

    return rv;
}

static void __exit mymodule_exit(void)
{
    nf_unregister_net_hook(&nfhook);
}

module_init(mymodule_init);
module_exit(mymodule_exit);

基本的には、先ほどのhelloカーネルモジュールに機能を追加した形になっています。 中身を見ると、traffic_hookという関数が追加されていること、mymodule_initとmymodule_exitのそれぞれの関数に謎の関数が追加されていますね。
説明すると、traffic_hookという関数がパケットが送信される際に呼び出される関数になっており、 中で、printk関数を使って観測したパケットの送受信先IPアドレスやUDPヘッダを表示という内容になっています。 また、この関数をカーネル上で登録、解除する処理がmymodule_initとmymodule_exitに記載されています。

では、このトラフィックの観測はどのように実現しているのでしょうか。
このパケットキャプチャツールを作るためにはNetfliterという機能を使っています。 Netfliterはiptablesにもを使われていたネットワークレイヤの各処理前後でソケットバッファをフックできる機能になっていて、 ネットワークレイヤの以下の状態でフック可能です。

フックポイントは以下の5点です。

  • NF_INET_PRE_ROUTING
  • NF_INET_LOCAL_IN
  • NF_INET_FORWARD
  • NF_INET_LOCAL_OUT
  • NF_INET_POST_ROUTING

現時点で、ipv6を使っている人はあまりいないと思うので、ipv6は記載しませんがipv6でも利用可能になっています。 今回のソースコードの中でNetfilterは以下の処理で使われています。

  • traffic_hookというudpのヘッダの中身を表示するコールバック関数のフォーマットを定義
  • カーネルモジュールにて、上記のコールバック関数をフックポイントとして登録することができるnf_register_net_hookという関数を定義
  • nf_register_net_hookのオプションにて関数挿入時のプライオリティの設定や対象プロトコルファミリなどの詳細情報を設定可能

ここからは以下の、BootlinというサイトでLinuxカーネルを読んでいき、内容を確認します。 Linuxカーネルのバージョンは5.10ですので以下のサイトからLinuxカーネル5.10を選択してソースコードを見ていきましょう。

elixir.bootlin.com

コールバック関数の引数として設定されているstruct sk_buffはソケットバッファを管理するための構造体です。 ソケットバッファとは、プロトコルスタックを格納しているLinuxカーネル内のバッファです。 プロトコルスタックを入れているバッファということは、先頭からあるオフセット分だけ移動すれば、IPやUDPのヘッダやペイロードまでたどり着けるということです。 それらを可能に関する関数が以下の、ip_hdrやipip_hdrという関数になります。

  • ip_hdr関数:IPレイヤのヘッダまでのオフセットを計算して先頭アドレスを返す
  • ipip_hdr関数:トランスポートレイヤのヘッダまでのオフセットを計算して先頭アドレスを返す

といった機能を持っている関数です。 なので、これらを使うことでバイナリではありますが、フックしたパケットのヘッダを手に入れることができ、 それをprintkで表示させることでなんちゃってパケットキャプチャを実装しております。

include/linux/ip.h 19行目

static inline struct iphdr *ip_hdr(const struct sk_buff *skb)
{
    return (struct iphdr *)skb_network_header(skb);
}
/*間は省略*/
static inline struct iphdr *ipip_hdr(const struct sk_buff *skb)
{
    return (struct iphdr *)skb_transport_header(skb);
}

もっと詳細に説明して行くとキリがないので、今回はここらへんで終わりにしたいと思います! 次の章では、他の関数や構造体についてピックアップした物をのせています。

4.2参考

/include/linux/socket.h 179行目

/* Supported address families. */
#define AF_UNSPEC  0
#define AF_UNIX        1  /* Unix domain sockets      */
#define AF_LOCAL   1  /* POSIX name for AF_UNIX   */
#define AF_INET        2  /* Internet IP Protocol     */
#define AF_AX25        3  /* Amateur Radio AX.25      */
#define AF_IPX     4  /* Novell IPX           */
#define AF_APPLETALK   5  /* AppleTalk DDP        */
#define AF_NETROM  6  /* Amateur Radio NET/ROM    */
#define AF_BRIDGE  7  /* Multiprotocol bridge     */
#define AF_ATMPVC  8  /* ATM PVCs         */
#define AF_X25     9  /* Reserved for X.25 project    */
#define AF_INET6   10 /* IP version 6         */

/include/linux/netfliter.h 77行目

typedef unsigned int nf_hookfn(void *priv,
                   struct sk_buff *skb,
                   const struct nf_hook_state *state);

/include/linux/netfliter.h 85行目

struct nf_hook_ops {
    /* User fills in from here down. */
    nf_hookfn       *hook;
    struct net_device  *dev;
    void           *priv;
    u8          pf;
    enum nf_hook_ops_type  hook_ops_type:8;
    unsigned int      hooknum;
    /* Hooks are ordered in ascending priority. */
    int            priority;
};

5.おわりに


これで、今回のカーネルモジュール開発は終わりとなります。 お疲れ様でした!!意外と簡単なものでもしっかり理解しようとするとこれくらいの情報量となったりします。 このカーネルモジュールを利用すると、実は独自のアンチウイルスやファイアウォールなどを作れます。
みなさんもご自身でいろいろなものを作ってみてください!
僕は今後、GPUのフレームバッファを取得したりなど、このカーネルモジュールを用いて低レイヤでしかできないシステム開発をしていきたいと思います!!
それでは、ありがとうございました!!!