Ubuntu Weekly Recipe

第585回SystemTapを活用してネットワークの動作を確認する

第584回ではカーネルデバッグツールのひとつSystemTapを紹介しました。今回はより実践的な例として、Ethernetフレームがドロップした際のMACアドレスを出力してみましょう。

droppedになったフレームのMACアドレスを調べる

Linuxは予期しないEthernetフレームを受け取ったとき、その内容によっては意図的に破棄し、ネットワークインターフェースのdroppedカウンターをインクリメントします。通常のネットワーク環境だとほぼ0なのですが、特定の機器や構成によっては恒常的にdroppedカウンターが増えることもあります。

そこでこのdroppedカウンターが上昇した理由を調べるために、droppedカウンター増加時のEthernetフレームの送信元・送信先のMACアドレスを、今回はSystemTapを用いて確認してみましょう。

予期しないEthernetフレームを送る仕組みを作る

まずはテスト用に予期しないEthernetフレームを送る仕組みを作ります。今回はテスト用なのでloインターフェースを対象にします。

droppedカウンターはip -s link showコマンドで確認できます。たとえばローカルのループバックインターフェースは次のとおりです。

$ ip -s link show lo
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    RX: bytes  packets  errors  dropped overrun mcast
    25919416   262042   0       0       0       0
    TX: bytes  packets  errors  dropped carrier collsns
    25919416   262042   0       0       0       0

上記の「dorpped」の部分ですね。今回の例では受信フレームのみを扱いますので、⁠RX:」で始まっているほうを確認してください。現時点では0です。

「予期しないEthernetフレーム」にはいろいろありますが、代表的なのはEtherTypeフィールド「サポートしていないEtherType」だった場合です。つまりカーネル側が届いたフレームのEtherTypeをどう扱うか不明な場合にdroppedとなります。IPv6のフレームならEtherTypeには0x86DDが入ります。このEtherTypeが使用しているカーネルでサポートしていない値(たとえば0x4321など)になっているフレームを受信するとdroppedが増えるというわけです[1]⁠。

任意のバイト列のEthernetフレームを送信する方法はいろいろ存在しますが、今回は次のようなPythonスクリプト(errframe.py)を作成し、利用することにします。

#!/usr/bin/env python3

from socket import *

dst = b'\x00\x00\x00\x00\x00\x00'
src = b'\x00\x00\x5E\x00\x53\x00'
etype = b'\x43\x21'
payload = bytes(46)
interface = 'lo'

sock = socket(AF_PACKET, SOCK_RAW)
sock.bind((interface, 0))
sock.send(dst + src + etype + payload)

コード自体は特に難しいものではないと思います。送信元のMACアドレスsrcとしてRFC7042に記載のある文書用のMACアドレスを指定しているのは、特に大きな理由はありません。今回は送信元も送信先もローカルループバックなので、送信先MACアドレスであるdstも送信元インターフェースであるinterfaceもそれに合わせています。payloadには最小フレームサイズを満たすよう0で埋めたバイト列を作っているだけです。

後半はSOCK_RAWsocket()を作成して、インターフェースにbind()し、send()で生のEthernetフレームを送っています。送信元と送信先が一緒になっているため多少わかりにくいかもしれません。実際にdrooppedが起きるのは実デバイスに紐付いたインターフェースのはずなので、srcdstinterfaceはそれらに合わせて適宜変更してください。

実際にこのコードを実行してから、再度loの情報を表示してみましょう。

$ chmod +x errframe.py
$ sudo ./errframe.py
$ ip -s link show lo
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    RX: bytes  packets  errors  dropped overrun mcast
    25959740   262444   0       1       0       0
    TX: bytes  packets  errors  dropped carrier collsns
    25959740   262444   0       0       0       0

無事にdroppedが1増えました。

droppedカウンターが更新されたことを表示する

この受信用のdroppedカウンターはいくつかの箇所で更新されうるのですが、今回はそのあたりの話をすっとばして、具体的なコードの場所に移動します。

net/core/dev.cの__netif_receive_skb_core()では、いくつかの条件において関数内のdropラベルにジャンプし、ネットワークインターフェースのrx_droppedを更新します。

4523 | drop:
4524 |         if (!deliver_exact)
4525 |             atomic_long_inc(&skb->dev->rx_dropped);
4526 |         else
4527 |             atomic_long_inc(&skb->dev->rx_nohandler);
4528 |         kfree_skb(skb);
4529 |         /* Jamal, now you will not able to escape explaining
4530 |          * me how you were going to use this. :-)
4531 |          */
4532 |         ret = NET_RX_DROP;

atomic_long_inc(&skb->dev->rx_dropped)がまさにその場所です。コードを見るとわかるように、条件によってはrx_droppedではなくrx_nohandlerが上がることもあるようです。というわけでこのラベルにジャンプするかどうかを確認してみましょう。幸い、SystemTapには.label("FOO")というフィルタリングルールがあるため、簡単にプローブポイントを指定できます。

#!/usr/bin/env stap

probe kernel.function("__netif_receive_skb_core").label("drop")
{
  printf("pakcet dropped\n");
}

早速、このSTPスクリプトを実行しつつ、先ほどのPythonスクリプトを動かしてください。ちなみにこれ以降、STPスクリプトのファイル名は「drop.stp」であるとします。

$ sudo stap drop.stp
pakcet dropped

裏でerrframe.pyを実行するたびに、上記が表示されるはずです。Ctrl-Cを入力してstapコマンドを終了しておきましょう。

rx_droppedの値を表示する

次にrx_droppedの内容を表示してみましょう。

SystemTapではいくつかのローカル変数にアクセスできます。たとえばdropラベル付近の変数であれば、次のように確認できます。

$ stap -L 'kernel.function("__netif_receive_skb_core").label("drop")'
kernel.function("__netif_receive_skb_core@/build/linux-hwVdeu/linux-4.15.0/net/core/dev.c:4523").label("drop")
  $skb:struct sk_buff* $pfmemalloc:bool $orig_dev:struct net_device* $ret:int $type:__be16

上記の結果だと、$skbstruct sk_buff*の変数にアクセスできるわけです。

さらに@cast()を使うことで、特定の変数を指定した構造体にキャストしてそのメンバーにアクセスしたり、その値を読むことが可能です。具体的に見ていきましょう。

#!/usr/bin/env stap

probe kernel.function("__netif_receive_skb_core").label("drop")
{
  net_device = @cast($skb, "struct sk_buff", "kernel<linux/skbuff.h>")->dev
  dropped = atomic_long_read(&@cast(net_device, "struct net_device", "kernel<linux/netdevice.h>")->rx_dropped)
  printf("dropped1 = %lu\n", dropped)
}

上記では$skblinux/skbuff.hstruct sk_buffであるという情報を与えた上で、そのdevメンバーにアクセスしています。さらにdevstruct net_deviceであり、そのrx_droppedメンバーの値をatomic_long_read()dropped変数に保存、表示しています。

実際に実行してみると次のような結果になります。

$ sudo stap drop.stp
dropped1 = 2

どうやら値が読めているようです。

ちなみにip -s link showの結果と比べてみると、dropped1の値は1つ少ないことがわかります。これは「dropラベル」rx_droppedカウンターの内容を見てしまっているためです。コードをよく見るとわかりますが、rx_droppedはdropラベルにジャンプしたあとに更新されるので、本来は更新されたあとにチェックする必要があります。

ラベルのある箇所と違って、コード上の任意の場所をプローブポイントにするのは少し調整が必要です。先ほどstap -Lコマンドを実行したときに、ラベルのある具体的なコードの箇所が表示されていました。今回はその表示を元にコードの行番号をkfree_skb(skb);が実行される4528行目「付近の」プローブポイントに設定してみましょう。STPスクリプトの末尾に、次のコードブロックを追加してください。

probe kernel.statement("__netif_receive_skb_core@/build/linux-hwVdeu/linux-4.15.0/net/core/dev.c:4532").nearest
{
  net_device = @cast($skb, "struct sk_buff", "kernel<linux/skbuff.h>")->dev
  dropped = atomic_long_read(&@cast(net_device, "struct net_device", "kernel<linux/netdevice.h>")->rx_dropped)
  printf("dropped2 = %lu\n", dropped)
}

statement("関数名@ファイル名:行番号").nearestを使うと、指定した行番号付近のもっとも近いところをプローブポイントに設定してくれます。必ずしも期待通りの場所になるとは限りませんが、うまくプローブポイントを指定できないときに役に立つでしょう。

$ sudo stap drop.stp
dropped1 = 3
dropped2 = 4

dropped2のほうは、きちんと「増えたあと」rx_droppedが表示されましたね。

関数を定義する

SystemTapではfunctionを指定することで自前の関数を定義します。さきほどの例ではrx_droppedの増加前後で同じようなコードをコピーしていたので、関数化してしまいましょう。

#!/usr/bin/env stap

function print_dropped(skb, label)
{
  net_device = @cast(skb, "struct sk_buff", "kernel<linux/skbuff.h>")->dev
  dropped = atomic_long_read(&@cast(net_device, "struct net_device", "kernel<linux/netdevice.h>")->rx_dropped)
  printf("%s = %lu\n", label, dropped)
}

probe kernel.function("__netif_receive_skb_core").label("drop")
{
  print_dropped($skb, "dropped1")
}
probe kernel.statement("__netif_receive_skb_core@/build/linux-hwVdeu/linux-4.15.0/net/core/dev.c:4528").nearest
{
  print_dropped($skb, "dropped2")
}

これだけです。説明するまでもないぐらいわかりやすいですね。

MACアドレスを表示する

最後にMACアドレスを表示してみましょう。

skbからMACアドレスを取り出すには、カーネルの中だとskb_mac_header(struct sk_buff *)でEthernetフレームのヘッダーを示すポインタを取り出します。そのメンバーであるh_desth_sourceが送信先・送信元のMACアドレスです。

実はSystemTapのスクリプトの中でC言語の関数を呼び出すことも可能です。先に完成形のスクリプトを提示しておきましょう。

#!/usr/bin/env stap
%{
#include <linux/skbuff.h>
#include <uapi/linux/if_ether.h>
%}

function get_ethhdr:string(skb:long)
%{
  struct sk_buff *skb;
  struct ethhdr *ehdr;
  skb = (struct sk_buff *)(long)STAP_ARG_skb;
  ehdr = (struct ethhdr *)skb_mac_header(skb);
  snprintf(STAP_RETVALUE, MAXSTRINGLEN, "  src=%pM\n  dst=%pM\n  proto=%#x\n",
           ehdr->h_source, ehdr->h_dest, ntohs(ehdr->h_proto));
  CATCH_DEREF_FAULT();
%}

function print_dropped(skb, label)
{
  net_device = @cast(skb, "struct sk_buff", "kernel<linux/skbuff.h>")->dev
  dropped = atomic_long_read(&@cast(net_device, "struct net_device", "kernel<linux/netdevice.h>")->rx_dropped)
  printf("%s = %lu\n", label, dropped)
}

probe kernel.function("__netif_receive_skb_core").label("drop")
{
  print_dropped($skb, "dropped1")
}
probe kernel.statement("__netif_receive_skb_core@/build/linux-hwVdeu/linux-4.15.0/net/core/dev.c:4528").nearest
{
  print_dropped($skb, "dropped2")
  printf("%s", get_ethhdr($skb))
}

最初のポイントはC言語の部分は%{...%}でかこうことです。またC言語部分については別途カーネルヘッダーが必要になりますので、明示的に必要なヘッダーファイルをインクルードしています。

get_ethhdr()では、skbのポインタを渡して、Ethernetフレームのヘッダーを示すポインターに変換し、文字列として送信元・送信先のMACアドレスとEtherTypeを保存します。STAP_ARG_fooはSTPスクリプト上の変数をC言語で解釈するためのマクロであり、STAP_RETVALUEは関数から値を返すためのマクロです。

カーネルのprintf関連のフォーマットには便利なフォーマット変換子(上記で言うとMACアドレス表記にする%pMなど)があるため、C言語側で文字列に変換しています。ただしMAXSTRINGLENは128バイトと小さめなので、もしいろいろ出力するなら別途調整したほうが良いでしょう。

あとは作成した文字列をそのまま出力しているだけです。実際の出力結果は次のようになります。

$ sudo stap -g drop.stp
dropped1 = 4
dropped2 = 5
  src=00:00:5e:00:53:00
  dst=00:00:00:00:00:00
  proto=0x4321

stapコマンドは-gオプションを付けることでguruモードで動作します。C言語を埋め込む場合はその内容によって-gが必要になるようです。

errframe.pyで設定した値に従って、MACアドレスやEtherTypeが設定されていることがわかります。

おすすめ記事

記事・ニュース一覧