ローファイ日記

出てくるコード片、ぼくが書いたものは断りがない場合 MIT License としています http://udzura.mit-license.org/

BCCでTCにアタッチし、egressのIPを表示する

サーバではいろいろな通信があるけれど、実際どんな通信が行われているか全貌を確認するのはなかなか難しい。

blog.ssrf.in

上記のソリューションはかっこいいが、KRSIなんで、「うちの環境のカーネルじゃちょっと...」となりがち。BPFあるあるだと思う。

一方でBPF自体は、カーネルでサポートされて随分時間が経ってきており、徐々に使えるカーネルが普及してきた。

とりあえず通信のログを残したい、というだけであればTCにBPFをアタッチする方法も使えて、これはUbuntu 20.04のカーネル(5.4 〜 5.8)なら十分使える。

// よく考えたらTCなのでブロックもできるのかな?

まあ今度調べるとして、egressの通信をキャプチャしてdst IP(今回はv4だけです)を集計するサンプルを残す。

動作環境(主にカーネルバージョン)について

今回は別の環境カーネル5.4でも動作確認しま。もう少し低くても行けるかもしれない。

しかしこのブログの環境は以下。

ubuntu@udzura.local:~$ uname -a
Linux kondo-ke-focal 5.8.0-63-generic #71~20.04.1-Ubuntu SMP Thu Jul 15 17:46:44 UTC 2021 aarch64 aarch64 aarch64 GNU/Linux

BCC の環境を作る

BCCといえば自作ビルド... という時代もあったが、今回はパッケージマネージャー経由のもので十分。

$ apt install libbpfcc python3-bpfcc python3-pyroute2

Ubuntu 20.04 では libbcc 0.12.0 が入ってくる。

自分の環境は全然違うディストリです!という場合はいっそ --host=netdebianを立ち上げてもいいと思う。

$ docker run -ti --rm -v /lib/modules:/lib/modules -v /usr/src:/usr/src --privileged --net=host debian:11-slim
~# apt update && apt install libbpfcc python3-bpfcc python3-pyroute2

多分動く。debian 11 では libbcc 0.18.0 が入ってくる。

Pythonスクリプト

いろいろな実装のつぎはぎだが、これを動かす。

from bcc import BPF, libbcc
from struct import unpack
import pyroute2
import time
import sys

code = """
// SPDX-License-Identifier: GPL-2.0+
#define BPF_LICENSE GPL

#include <linux/pkt_cls.h>
#include <uapi/linux/bpf.h>
#include <bcc/proto.h>

BPF_HASH(ip_counter, u32, u64);

int counter(struct __sk_buff *skb) {
    u32 key = 0;
    u8 *cursor = 0;

    struct ethernet_t *ethernet = cursor_advance(cursor, sizeof(*ethernet));
    // 0x0800 = IPv4
    if (ethernet->type != 0x0800)
        return TC_ACT_OK;

    struct ip_t *ip = cursor_advance(cursor, sizeof(*ip));
    key = ip->dst;

    if (key == 0)
        return TC_ACT_OK;

    // 192.168.0.0 ~ 192.168.255.255 のログは取らない
    // 他のプライベートIPレンジは宿題とします。
    if (3232235520 <= key && key <= 3232301055)
        return TC_ACT_OK;

    u64 zero = 0;
    u64 *value = ip_counter.lookup_or_try_init(&key, &zero);
    if (value)
        *value += 1;

    return TC_ACT_OK;
}
"""

# ライブラリを知らないので頑張って自作した
def int_to_ip(nr_):
    nr = unpack('I', nr_)[0]
    return '{}.{}.{}.{}'.format(
                (nr >> 24 & 0b11111111),
                (nr >> 16 & 0b11111111),
                (nr >> 8  & 0b11111111),
                (nr       & 0b11111111)
            )

def count(nr_):
    return unpack('L', nr_)[0]

def count_packet(iface):
    bpf = BPF(text=code, cflags=["-w"])
    func = bpf.load_func("counter", BPF.SCHED_CLS)
    counter = bpf.get_table("ip_counter")
    
    print("map fd: {}".format(counter.map_fd))
    ip = pyroute2.IPRoute()
    ipdb = pyroute2.IPDB(nl=ip)
    idx = ipdb.interfaces[iface].index
    ip.tc("add", "clsact", idx)

    # ffff:fff3 というのが、 egress のことらしい。
    # ingress なら ffff:fff2 らしい。
    ip.tc("add-filter", "bpf", idx, ":1", fd=func.fd, name=func.name,
          parent="ffff:fff3", classid=1, direct_action=True)

    print("Hit CTRL+C to stop")
    while True:
        try:
            print("outgoing dest ips in 1s:")
            for k, v in counter.items():
                print(f'k: {int_to_ip(k)}, v: {count(v)}')
            counter.clear()
            time.sleep(1)
        except KeyboardInterrupt:
            print("Removing filter from device")
            break
    ip.tc("del", "clsact", idx)
    ipdb.release()

if __name__ == '__main__':
    iface = sys.argv[1]
    count_packet(iface)

※ ところで拙作 RbBCC ではこれと同じことができない。libbccのFFIなので本当はできるはずだが、pyroute2のRuby版がないのでBPFプログラムを作れてもアタッチできない。pyroute2は巨大なので流石に僕は移植する時間が...。

pyroute2がRubyにあると嬉しいなあという願いだけ残そう。

tcにアタッチする

上記をsudoで実行するとデバイスにアタッチできる。

$ sudo python3 filter.py enp0s1
map fd: 4
Hit CTRL+C to stop
outgoing dest ips in 1s:
outgoing dest ips in 1s:
outgoing dest ips in 1s:
outgoing dest ips in 1s:

この状態で、 enp0s1 を通って外に行くような通信をすると、ちゃんと記録されることがわかる。

$ ping hatena.ne.jp
PING hatena.ne.jp (54.65.114.10) 56(84) bytes of data.
64 bytes from ec2-54-65-114-10.ap-northeast-1.compute.amazonaws.com (54.65.114.10): icmp_seq=1 ttl=232 time=36.1 ms
64 bytes from ec2-54-65-114-10.ap-northeast-1.compute.amazonaws.com (54.65.114.10): icmp_seq=2 ttl=232 time=32.1 ms

...

outgoing dest ips in 1s:
k: 54.65.114.10, v: 1
outgoing dest ips in 1s:
k: 54.65.114.10, v: 1
...

今回は雑然とMapに保存しているが、定期的に取り出して監査ログにするなり、怪しい通信を確認するなりやろうと思えばできそうかな?

トラブルシュート

tc にアタッチする前に、以下のコマンドで現在何もクラスがないことが確認できる。

$ tc qdisc show dev enp0s1 clsact
qdisc fq_codel 0: root refcnt 2 limit 10240p flows 1024 quantum 1514 target 5.0ms interval 100.0ms memory_limit 32Mb ecn

上のスクリプトの実行が失敗すると、中途半端なクラスやフィルターが残る。

$ tc qdisc show dev enp0s1 clsact
qdisc fq_codel 0: root refcnt 2 limit 10240p flows 1024 quantum 1514 target 5.0ms interval 100.0ms memory_limit 32Mb ecn 
qdisc clsact ffff: parent ffff:fff1 <- これ

削除は以下のコマンドで。

$ sudo tc qdisc del dev enp0s1 clsact

(実はこの記事の主題こと)宣伝

RubyKaigi 2022 で BPF の話をします。と言いつつこの記事でしているネットワーク関係の機能の話はしなくて、Observability中心です。

rubykaigi.org

思えば2年以上前にとりあえず完成させたものですが、RbBCCの話中心に色々やっていくような気がします。正直、現場で運用にも携わる人としては、CO-REが普通に使えるカーネルに全部置き換えよう! ってすぐにはいかないので、全然BCC使っていこうと思っていますから...。

udzura.hatenablog.jp

今年はハイブリッド開催だそうです。できる形で是非ご参加くださいませ。赤福

(今気づいたけど1行もRubyのコードがない記事で宣伝してるw)

参考にしたページ

atmarkit.itmedia.co.jp

yunazuno.hatenablog.com

github.com