第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] 。
[1] たとえばBuffaloのスイッチはループ防止機能 として2秒に1回検出用フレームをブロードキャストする機能が存在します。そのフレームには、IEEEにはRealtek用として割り当て られているRRCP(Realtek Remote Control Protocol) 用の「0x8899」なEtherTypeをなぜか設定するようです。結果としてRRCPに対応していないカーネルで、ユーザーランド側でもEtherType=0x8899を見るソフトウェアが存在しない場合、Baffaloのループ防止機能のついたスイッチを接続すると、ひたすら「RX dropped」カウンターが増えていくことになります。また、ユーザーランド側で「SOCK_RAW/ETH_P_ALL
なsocket()
」を作成した場合、すべてのEtherTypeが一度ユーザーランドにあがる(=カーネル側から見るとサポートしていないEtherTypeが存在しない)ため、droppedなフレームとしては表示されません。
任意のバイト列の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_RAW
なsocket()
を作成して、インターフェースにbind()
し、send()
で生のEthernetフレームを送っています。送信元と送信先が一緒になっているため多少わかりにくいかもしれません。実際にdrooppedが起きるのは実デバイスに紐付いたインターフェースのはずなので、src
やdst
、interface
はそれらに合わせて適宜変更してください。
実際にこのコードを実行してから、再度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
上記の結果だと、「 $skb
」で「struct 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)
}
上記では「$skb
」は「linux/skbuff.h
」の「struct sk_buff
」であるという情報を与えた上で、そのdev
メンバーにアクセスしています。さらにdev
はstruct 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_dest
とh_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が設定されていることがわかります。