前回、BCCのPythonバインディングで、TCにBPFを引っ掛けてegressのIPを表示した。
ところで私はRuby版BCCを開発しているわけだが、上記プログラムは一部iproute2的な操作をPythonで行うPyroute2に依存している。RubyにはPyroute2に相当するgemが開発されていないので動かせない。RbBCCの対象範囲はBPFプログラム作成側だけなので。
...と思っていたが、PyCall.rbというものの存在を思い出した(以下単にPyCallというときはRuby版)。PyCallは主にPythonの数値計算や機械学習関連のライブラリを呼び出すときに使われている印象があるが、Pyroute2を呼び出してみる。
インストール
$ gem install pycall
動かしてみる
デバイスの情報を取得する。この際、(Ubuntuなら) python3-pyroute2
パッケージを事前に入れておく必要がある。
irb(main):002:0> require 'pycall/import' => true irb(main):003:1* module Py irb(main):004:1* extend PyCall::Import irb(main):005:1* pyimport :pyroute2 irb(main):006:0> end irb(main):007:0> PyRoute2 = Py.pyroute2 => <module 'pyroute2' from '/usr/lib/python3/dist-packages/pyroute2/__init__.py'> irb(main):008:0> ip = PyRoute2.IPRoute.new => <pyroute2.iproute.linux.IPRoute object at 0xffff83285790> irb(main):009:0> ipdb = PyRoute2.IPDB.new => <pyroute2.ipdb.main.IPDB object at 0xffff8229c130> irb(main):010:0> ipdb.interfaces["lo"] => {'address': '00:00:00:00:00:00', 'broadcast': '00:00:00:00:00:00', 'ifname': 'lo', 'mtu': 65536, 'qdisc': 'sfq', 'txqlen': 1000, 'operstate': 'UNKNOWN', 'linkmode': 0, 'group': 0, 'promiscuity': 0, 'num_tx_queues': 1, 'num_rx_queues': 1, 'carrier': 1, 'carrier_changes': 0, 'proto_down': 0, 'gso_max_segs': 65535, 'gso_max_size': 65536, 'xdp': '05:00:02:00:00:00:00:00', 'carrier_up_count': 0, 'carrier_down_count': 0, 'index': 1, 'flags': 65609, 'ipdb_scope': 'system', 'ipdb_priority': 0, 'vlans': (), 'ipaddr': (('::1', 128), ('127.0.0.1', 8)), 'ports': (), 'family': 0, 'ifi_type': 772, 'state': 'up', 'unknown': {'header': {'length': 8, 'type': 51}}, 'neighbours': ('0.0.0.0',)} irb(main):011:0> ipdb.interfaces["lo"].ipaddr => (('::1', 128), ('127.0.0.1', 8))
相互の引数の変換など、極めてよくできている...とわかった。
TCのbpfフックを登録する
PyCallを使ったプログラムを以下のように書いた。
require 'rbbcc' include RbBCC require 'ipaddr' begin require 'pycall/import' rescue LoadError => e puts "#{e.inspect}: needs pycall installed. run: gem install pycall" exit 127 end unless iface = ARGV[0] puts("USAGE: #{$0} [IFACE]") exit 1 end # IP addressのint表現をハードコードせず、IPAddrで変換しておいておく private_begin = IPAddr.new("192.168.0.0/32").to_i private_end = IPAddr.new("192.168.255.255/32").to_i code = <<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> struct data_t { u32 dest; }; BPF_PERF_OUTPUT(events); int on_egress(struct __sk_buff *skb) { u8 *cursor = 0; struct ethernet_t *ethernet = cursor_advance(cursor, sizeof(*ethernet)); if (ethernet->type != 0x0800) goto ret; struct ip_t *ip = cursor_advance(cursor, sizeof(*ip)); u32 dest = ip->dst; if (dest == 0) goto ret; // 埋め込みもRuby styleで if (#{private_begin} <= dest && dest <= #{private_end}) goto ret; struct data_t data = {0}; data.dest = dest; events.perf_submit(skb, &data, sizeof(data)); ret: return TC_ACT_OK; } CODE module Py extend PyCall::Import pyimport :pyroute2 end PyRoute2 = Py.pyroute2 b = BCC.new(text: code) # BPF::SCHED_CLS という定数自体は定義してあるので func = b.load_func("on_egress", BPF::SCHED_CLS) ip = PyRoute2.IPRoute.new ipdb = PyRoute2.IPDB.new idx = ipdb.interfaces[iface].index # tcメソッドを呼び出せる! ip.tc("add", "clsact", idx) # この辺、PythonとRubyのnamed argumentsの仕様の違いをどう吸収しているのか # わかってないが、そのまま書き換えれば動いた。 ip.tc("add-filter", "bpf", idx, ":1", fd: func[:fd], name: func[:name], parent: "ffff:fff3", classid: 1, direct_action: true) at_exit { ip.tc("del", "clsact", idx) ipdb.release } # perf bufferからのイベントを処理する b["events"].open_perf_buffer do |_cpu, data, _size| event = b["events"].event(data) got = IPAddr.new event.dest, Socket::AF_INET puts "EGRESS IP: #{got}" end # 無限ループ loop do begin b.perf_buffer_poll() rescue Interrupt exit() end end
これを動かすとちゃんと外向きのパケットのdest IPを表示するようになった。
BCCが外部で使ってるPythonライブラリ、RubyにないやつはPyCallで動かせばいいとわかったので、RbBCCのまだ実装していない機能を進めるモチベーションが少し上がった感ある。
[PR] RbBCCの話を9月に三重でする予定です。あと、転職してぶっちゃけどうなん? という話も三重でできます。Lets' have a chat! *1
RbBCCもPyCall経由でいいんじゃ、という気持ちにもなったけど、二重FFIはトラブルシューティング大変そうだし、 libbcc.so
にだけ依存するという方がバージョンを変えるなどやりやすいだろうからここはこのままでいいかな...。
*1:なお裏番組が、このPyCall.rbの作者のmrknさん(PyCallの話ではないが)という偶然。