Ruby / Netlink でルーティングテーブルを表示する
背景
Ruby(プログラム)でルーティングテーブルを取得する方法を調べたところ、Netlinkという仕組みがあるようなので、調べて簡単なプログラムを書いてみました。他の方法としては ip
コマンドをラップしたり、 /proc
ファイルを読み込んでパースしたり、といった方法があるみたいです。
Netlinkについてはあまり解説されたまとまったものが見つからず、書籍だと Linuxネットワークプログラミング にまとまった解説がありました。以下記載の「メモ」の内容も、基本的に本書の内容を参考にさせて頂いています。
(動作的には問題なさそうなので多分大きな間違いは無いと思うのですが、ドキュメント類が少ないということもあり、メモ部分の説明等もしかしたら細かいところで間違っている部分があるかもしれません)
環境
- Ruby 2.7
- Linux 4.14.219-164.354.amzn2.aarch64
作ったもの
ルーティングテーブルを取得/表示します。
require_relative "./lib/simple_routing"
rt = SimpleRouting::RouteHandler.new
# rt.show_routes
rt.each_route do |route|
route.show
end
% ruby test.rb
family: 2, dst_len: 0, src_len: 0, tos: 0, table: 254, protocol: 3, scope: 0, type: 1, flags: 0
type: RTA_TABLE, data: 254
type: RTA_GATEWAY, data: 172.31.0.1
type: RTA_OIF, data: eth0
family: 2, dst_len: 0, src_len: 0, tos: 0, table: 254, protocol: 3, scope: 0, type: 1, flags: 0
type: RTA_TABLE, data: 254
type: RTA_PRIORITY, data: 10001
type: RTA_GATEWAY, data: 172.31.0.1
type: RTA_OIF, data: eth1
family: 2, dst_len: 0, src_len: 0, tos: 0, table: 254, protocol: 3, scope: 0, type: 1, flags: 0
type: RTA_TABLE, data: 254
type: RTA_PRIORITY, data: 10002
type: RTA_GATEWAY, data: 172.31.0.1
type: RTA_OIF, data: eth2
メモ
リクエスト
リクエストを送信するためのサンプルプログラムを以下に抜粋します。
NLM_F_REQUEST = 1
NLM_F_ROOT = 0x100
NETLINK_ROUTE = 0
AF_NETLINK = 16
RTM_GETROUTE = 26
socket = Socket.new(AF_NETLINK, Socket::SOCK_RAW, NETLINK_ROUTE) # ①
header = [24, RTM_GETROUTE, NLM_F_REQUEST | NLM_F_ROOT, 0, 0].pack('ISSII') # ②
body = [0, 0, 0, 0, 0, 0, 0, 0, 0].pack('CCCCCCCCI') # ③
socket.send(header + body, 0)
socket.recv(4096)
①Netlinkソケットの作成には AF_NETLINK
というソケットファミリを指定します。Rubyには定数が用意されていないようだったので、 /usr/include/bits/socket.h
に以下のように 16 が定義されていたので、その値を指定します。
#define PF_NETLINK 16
#define PF_ROUTE PF_NETLINK /* Alias to emulate 4.4BSD. */
#define AF_NETLINK PF_NETLINK
第3引数には netlink_family を指定します。ルーティング情報等を扱うには NETLINK_ROUTE
を指定します。この定数もRubyには無いようだったので、 /usr/include/linux/netlink.h
を確認すると 0 で定義されていたので、そのように指定します。
#define NETLINK_ROUTE 0 /* Routing/device hook */
メッセージのフォーマットは [nlmsghdr構造体] + [NLMSGデータ]
が基本になるようです。
②nlmsghdr構造体 の定義はこちらのManページ に記載がある通り以下です。この構造体に添うように、 pack
を用いてRubyの文字列としてパラメータを準備します。
struct nlmsghdr {
__u32 nlmsg_len; /* ヘッダーを含むメッセージの長さ */
__u16 nlmsg_type; /* メッセージの内容のタイプ */
__u16 nlmsg_flags; /* 追加フラグ */
__u32 nlmsg_seq; /* シーケンス番号 */
__u32 nlmsg_pid; /* 送信者のポート ID */
};
nlmsg_type
に今回はルーティングテーブルを表示するための RTM_GETROUTE
を指定するします。この定義は /usr/include/linux/rtnetlink.h
にあり、値は 26 なのでそれを指定します。
③続くNLMSGデータ部は RTM_GETROUTE の場合は RTMSG 構造体なのでそれに応じたデータを送信します。
struct rtmsg {
unsigned char rtm_family; /* Address family of route */
unsigned char rtm_dst_len; /* Length of destination */
unsigned char rtm_src_len; /* Length of source */
unsigned char rtm_tos; /* TOS filter */
unsigned char rtm_table; /* Routing table ID */
unsigned char rtm_protocol; /* Routing protocol; see below */
unsigned char rtm_scope; /* See below */
unsigned char rtm_type; /* See below */
unsigned int rtm_flags;
};
リクエスト時には特に値は指定しなくても大丈夫なようなのですべて 0
とします。先程同様、パラメータを文字列で指定するためpackして文字列にします。
レスポンス
次にレスポンス部を見ていきます。ルーティングテーブルを取得する部分の大まかな流れを確認していきたいと思います。
単一のメソッドとしては少し長いですが、ご容赦ください。
def fetch_routes
resp = request_and_recv
resp_len = resp.length
routes = []
while resp_len > 0 # ⑤
nlmshdhr = Nlmshdhr.new(resp.slice!(0, 16)) # ①
rtmsg = Rtmsg.new(resp.slice!(0, 12)) # ②
route = build_route_from_rtmsg(rtmsg)
rtattr_all_len = nlmshdhr.len - 28
while rtattr_all_len > 0
rtattr = Rtattr.new(resp.slice!(0, 4)) # ③
rtattr.add_data(resp.slice!(0, rtattr.len.to_i - 4)) # ④
route.add_rtattr(rtattr) # ④
rtattr_all_len -= rtattr.len.to_i
end
routes.push(route)
resp_len = resp.length
end
routes
end
レスポンスは、全体として以下のような構造となります。
[nlmsghdr] + [rtmsg] + [rtattr] + [rtattr] ...
ヘッダ部となる ①nlmsghdr と ②rtmsg は既出なので割愛します。②rtmsgはタイプ(今回はRTM_GETROUTE)によっては異なるデータ構造のものとなります。
次は rtattr 構造体によって属性値が複数返答されます。rtattr構造体は以下の形です。
struct rtattr {
unsigned short rta_len; /* Length of option */
unsigned short rta_type; /* Type of option */
/* Data follows */
};
rta_len
(2Byte) と rta_type
(2Byte) で4Byteなのでそこを読み込みつつ③、あとに続いているデータ部を読み込みます④。
rta_type
は以下ドキュメントに記載があり、 RTA_DST
(行き先アドレス) や RTA_GATEWAY
(経路のゲートウェイ)等が格納されます。
データ部の構造は rta_type
次第なようですが、対応表のようなドキュメントは見つけられなかったので、動かしながら確認しました。アドレスが返ってくるような RTA_GATEWAY
等であれば、 IPAddr.new_ntoh(@data)
のような形で値を取り出すことができました。
rtattr は複数返されるため、すべて取り出すまで繰り返し処理を行います。⑤
以上です。
参考
Ruby で Linux の NETLINKを使う - Qiita https://qiita.com/eggman/items/9848606a87c74d5dfcdd
Man page of RTNETLINK https://linuxjm.osdn.jp/html/LDP_man-pages/man7/rtnetlink.7.html
Getting Linux routing table using netlink – Oleg Kutkov personal blog https://olegkutkov.me/2019/03/24/getting-linux-routing-table-using-netlink/
Netlink IPCを使ってLinuxカーネルのネットワーク情報にアクセスする - Hash λ Bye http://ilyaletre.hatenablog.com/entry/2019/09/01/205432
Discussion