ローファイ日記

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

PyRoute2 + PyCall.rb でネットワークインタフェースの操作をする

前回、BCCPythonバインディングで、TCにBPFを引っ掛けてegressのIPを表示した。

udzura.hatenablog.jp

ところで私はRubyBCCを開発しているわけだが、上記プログラムは一部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

rubykaigi.org


RbBCCもPyCall経由でいいんじゃ、という気持ちにもなったけど、二重FFIトラブルシューティング大変そうだし、 libbcc.so にだけ依存するという方がバージョンを変えるなどやりやすいだろうからここはこのままでいいかな...。

*1:なお裏番組が、このPyCall.rbの作者のmrknさん(PyCallの話ではないが)という偶然。