eBPFでTCPパケットのトレース

今回はeBPFの「Kprobe」「Kretprobe」を使いカーネル内の関数が呼び出される前後でeBPFプログラムを実行し、パケットトレースを行います。

https://github.com/iovisor/bcc/blob/master/examples/tracing/tcpv4connect.py

[eBPF入門](https://zenn.dev/masibw/articles/068bdfe5edee17)

を参考に作成します。

c言語を理解する上で解説を載せながら書いています。
まだ理解の浅い部分や読みづらい部分もありましたら、参考にしたサイトへ

# 動作する仕組み

1. カーネル内の関数が呼ばれる
1. eBPFプログラム①実行(C)
1. カーネル内の関数処理終了
1. eBPFプログラム②実行(C)
1. eBPFプログラムからデータを取得し標準出力に表示(Python)


# eBPFプログラムの前に..

eBPF全体で使用される構造体や配列を定義します。
カーネル内の関数処理全後で配列や構造体を活用しデータを取得します。

```c
#include <uapi/linux/ptrace.h>
#include <net/sock.h>
#include <linux/sched.h>
#include <linux/utsname.h>
#include <linux/pid_namespace.h>
struct packet_data{
    u32 pid;
    char comm[TASK_COMM_LEN];
    u32 saddr;
    u32 daddr;
    u16 dport;
};
BPF_HASH(socklist, u32, struct sock *);

BPF_PERF_OUTPUT(events);
```

* まずはパケットから取得したデータを格納するデータ構造を下記のように宣言

|型|データ|
|---|---|
|u32(unsigned int)|プロセスID|
|char|TCP通信を行っているコマンド|
|u32(unsigned int)|送信元アドレス|
|u32(unsigned int)|宛先アドレス|
|u16(unsigned short)|宛先ポート|


* PIDからソケットを特定する為に、PIDをキーとするソケットの構造体の連想配列を作成
* イベントが発生した際に、好きなデータを送信する為、OUTPUTを作成

## eBPFプログラム① ~kprobe~
「kprobe」とはカーネル内の関数が呼び出された際に実行するeBPFプログラムをアタッチします

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

引数はカーネル内の関数と同じ引数が使えます(不要なものは省略可)
PIDを取得し連想配列へ追加する


# eBPFプログラム② ~kretprobe~
「kretprobe」とはカーネル内の関数の処理が終わった後に実行するeBPFプログラムをアタッチします。
関数で処理された値や引数などを取得でき、TCP通信の接続が確立された後のアドレスやソケットの情報を取得する為に使用しています。

```c
int tcp_connect_ret(struct pt_regs *ctx){
    u32 pid = bpf_get_current_pid_tgid();
    struct sock **sock, *sockp;
    struct packet_data data = {};
    sock = socklist.lookup(&pid);
    if(sock == 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;
    u16 dport = sockp->__sk_common.skc_dport;
    data.dport = ntohs(dport);
    events.perf_submit(ctx, &data, sizeof(data));
    socklist.delete(&pid);
    return 0;
}
```

現在のPIDを取得し連想配列からPIDが同じのソケットがあるか探索します。
ここで無かったらretutn0を返します。

socklistの中には、sock構造体のアドレスが入っている為、ポインタのポインタを利用します。

packet_data構造体に値を代入、events.perf_submitを使ってユーザー空間(Python側)へデータを送信します。

## ユーザー空間の処理(Python)

eBPF側のpacket_data構造体を取得し標準出力に表示します。

```py
bpf_text = """ eBPFのプログラム"""
def test(addr):
    bytes =[]
    bytes.append(addr & 0xff)
    bytes.append(addr>>8 & 0xff)
    bytes.append(addr>>16 & 0xff)
    bytes.append(addr>>24 & 0xff)

    a = str(bytes[0])+"."+str(bytes[1])+"."+str(bytes[2])+"."+str(bytes[3])
    return a.encode()


def get_print_event(b: BPF):
    def print_event(a,data, size):
        event = b["events"].event(data)
        printb(b"%-6d %-16s %-16s %-16s %-16d" % (
            event.pid, event.comm, test(event.saddr), ntoa(event.daddr), event.dport))

    return print_event


b = BPF(text=bpf_text)
b.attach_kprobe(event='tcp_v4_connect', fn_name="tcp_connect")
b.attach_kretprobe(event='tcp_v4_connect', fn_name="tcp_connect_ret")


b["events"].open_perf_buffer(get_print_event(b))

print("%-6s %-16s %-16s %-16s %-16s" % (
        "PID","COMMAND", "S-IPADDR", "D-IPADDR", "DPORT"))
while 1:
   try:
      b.perf_buffer_poll()
   except KeyboardInterrupt:
      exit()
```
BPF関数にeBPFのコードを引数に実行する。
関数が呼ばれる前に実行したいコードは"attach_kprobe"
関数が呼ばれた後に実行したいコードは“attach_kretprobe”でアタッチする。

eBPF側でBPF_PERF_OUTPUT(events)で送信したデータを取得するために、b["events"].open_perf_buffer(get_print_event(b))を使い、自分で定義したprint関数で標準出力に表示します。

# 実行

```bash
~/eBPF$ sudo -E python3 tcpv4connect.py 
```
pythonを実行し、裏でcurlを実行をすると

```bash
$ curl google.com

-------------------------------------------------

~/eBPF$ sudo -E python3 tcpv4connect.py 
PID    COMMAND          S-IPADDR         D-IPADDR         DPORT           
346414 curl             111.111.111.111  142.250.207.14   80  
```
パケットがトレース出来てたら成功です。

# 参考文献

https://github.com/iovisor/bcc/blob/master/examples/tracing/tcpv4connect.py

[eBPF入門](https://zenn.dev/masibw/articles/068bdfe5edee17)

# ソースコード
```py
#!/usr/bin/python3

from bcc import BPF
from bcc.utils import printb
bpf_text = """
#include <uapi/linux/ptrace.h>
#include <net/sock.h>
#include <linux/sched.h>
#include <linux/utsname.h>
#include <linux/pid_namespace.h>
struct packet_data{
    u32 pid;
    char comm[TASK_COMM_LEN];
    u32 saddr;
    u32 daddr;
    u16 dport;
};

// create map
BPF_HASH(socklist, u32, struct sock *);
BPF_PERF_OUTPUT(events);
//bpf_trace_printk("text")は文字列限定だけど、BPF_PERF_OUTPUT(events)はポインタで送れる



// kprobe function
int tcp_connect(struct pt_regs *ctx, struct sock *sock){
    u32 pid = bpf_get_current_pid_tgid();
    socklist.update(&pid, &sock);
    return 0;
}

// kretprobe function
int tcp_connect_ret(struct pt_regs *ctx){
    u32 pid = bpf_get_current_pid_tgid();
    struct sock **sock, *sockp;
    struct packet_data data = {};
    sock = socklist.lookup(&pid);
    if(sock == 0){//無かったreturn
        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;
    u16 dport = sockp->__sk_common.skc_dport;
    data.dport = ntohs(dport);
    events.perf_submit(ctx, &data, sizeof(data));
    socklist.delete(&pid);
    return 0;
}
"""
def test(addr):
    bytes =[]
    bytes.append(addr & 0xff)
    bytes.append(addr>>8 & 0xff)
    bytes.append(addr>>16 & 0xff)
    bytes.append(addr>>24 & 0xff)

    a = str(bytes[0])+"."+str(bytes[1])+"."+str(bytes[2])+"."+str(bytes[3])
    return a.encode()

def get_print_event(b: BPF):
    def print_event(a,data, size):
        event = b["events"].event(data)
        printb(b"%-6d %-16s %-16s %-16s %-16d" % (
            event.pid, event.comm, test(event.saddr), ntoa(event.daddr), event.dport))

    return print_event


b = BPF(text=bpf_text)
b.attach_kprobe(event='tcp_v4_connect', fn_name="tcp_connect")
b.attach_kretprobe(event='tcp_v4_connect', fn_name="tcp_connect_ret")


b["events"].open_perf_buffer(get_print_event(b))

print("%-6s %-16s %-16s %-16s %-16s" % (
        "PID","COMMAND", "S-IPADDR", "D-IPADDR", "DPORT"))
while 1:
   try:
      b.perf_buffer_poll()
   except KeyboardInterrupt:
      exit()
```