Man page of CHIKU_WAIT(2)

システムソフトウェアとポエムの差が激しいので,高山病に注意

コンテナのtcp_v4_connectをトレースするやつを書いた

最近,色々あって冷やし中華はじめましたみたいなノリでeBPF(extended Berkeley Packet Filter)をはじめました.夏も近づいているのでちょうどいい感じで. 勉強するにしても,チュートリアルそのままだと割とすぐに飽きてしまうので,コンテナのトレースをしてみました.

eBPFとは

eBPFとは,独自の命令セットを持ったカーネル仮想マシンです*1.名前の通り,元々はパケットフィルタリング用としたものでしたが,現在は拡張されてLinuxの様々な機能のトレーシングなどに用いることができます.

eBPFは,検証器というものをもっており,ユーザ空間からアタッチされたコードをカーネル内で安全に実行することができます.検証器では,ループが無いことや到達できない目以来が存在しないなどをチェックしています.これにより,ユーザ空間からアタッチされたコードのバグなどによってカーネルパニックしたりすることを防ぎます.

eBPFのプログラムの記述には,C言語で記述し,ClangでBPFオブジェクトを作る方法や,bcc(BPF Compiler Collection) *2 と呼ばれるツールキット を用いて記述する方法などがあります.bccは,Pythonでのバインディングツールが開発されており,比較的簡単に書くことができます.この記事では,bccを用いてPythonで記述していきます.

実際に作ったもの

GitHubリポジトリはこちら. GitHub - chikuwait/container_tcp_tracer

ユーザランド

...
bpf = BPF(src_file = "trace.c")
bpf.attach_kprobe(event = "tcp_v4_connect", fn_name = "tcp_connect")
bpf.attach_kretprobe(event = "tcp_v4_connect", fn_name = "tcp_connect_ret")
bpf["events"].open_perf_buffer(print_event)
...
while 1:
    bpf.perf_buffer_poll()

attach_kprobeは,カーネルの特定の命令が実行される前に指定したBPFプログラムを挿入することができます.今回は,tcp_v4_connect()をトレースしたいので,eventに指定します. attach_kprobeでは,特定の命令が実行され,値が戻る時に指定したBPFプログラムを指定することができます.

BPFプログラム

...

struct data_t{
    u32 pid;
    char comm[TASK_COMM_LEN];
    u32 saddr;
    u32 daddr;
    int level;
    char nodename[64];
};
BPF_HASH(socklist, u32, struct sock *);
BPF_PERF_OUTPUT(events);


int tcp_connect(struct pt_regs *ctx, struct sock *sock){
    u32 pid = bpf_get_current_pid_tgid();
    socklist.update(&pid, &sock);

    return 0;
}

int tcp_connect_ret(struct pt_regs *ctx){
    u32 pid = bpf_get_current_pid_tgid();
    struct sock **sock, *sockp;
    struct data_t data = {};
    struct task_struct *task = (struct task_struct *)bpf_get_current_task();
    struct pid_namespace *pidns = (struct pid_namespace * )task->nsproxy->pid_ns_for_children;
    struct uts_namespace *uts = (struct uts_namespace * )task->nsproxy->uts_ns;

    sock = socklist.lookup(&pid);    
    if(sock == 0 || pidns->level == 0){
        return 0;
    }

    sockp = *sock;
    data.pid = pid;
    bpf_get_current_comm(&data.comm, sizeof(data.comm));
    data.saddr = sockp->__sk_common.skc_rcv_saddr;
    data.daddr = sockp->__sk_common.skc_daddr;
    data.level = pidns->level;
    bpf_probe_read(&data.nodename, sizeof(data.nodename), (void *)uts->name.nodename);

    events.perf_submit(ctx, &data, sizeof(data));
    socklist.delete(&pid);
    return 0;
}

まず,tcp_v4_connect()が呼ばれる時に,pidをbpf_get_current_pid_tgid()を使って取得します. そして,BPF_HASHで定義したBPF Mapsと呼ばれるカーネル内でデータを保存できる連想配列のようなものに,pidをキーにして,sock構造体のポインタを入れていきます.

次に,tcp_v4_connect()が呼ばれ,値を返す前にコンテナの判定し,宛先IPアドレス等の情報をユーザ空間に送信します. コンテナかどうか判定するために,まず```bpf_get_current_task()を使ってtask_struct構造体へのアドレスを取得します. task_struct構造体には,プロセスに関する情報が詰まっており,その中にあるns_proxyを経由することで,各プロセスの名前空間に関する情報を取得することができます. 今回は,コンテナかどうかの判定に,pid_ns_for_childrenのlevelを使っています. levelは,名前空間の深さを示していて,今回は深さが0以上であるなら,コンテナということにしています.

最後に,送信元,宛先IPアドレス,ホスト名などをPerf Ring Bufferでユーザ空間に渡していったら完成です.

実行例

wgetしているコンテナをトレースしている例. f:id:chikuwa_it:20200531014550p:plain