💎

Ruby / Netlink でルーティングテーブルを表示する

2021/03/16に公開

背景

Ruby(プログラム)でルーティングテーブルを取得する方法を調べたところ、Netlinkという仕組みがあるようなので、調べて簡単なプログラムを書いてみました。他の方法としては ip コマンドをラップしたり、 /proc ファイルを読み込んでパースしたり、といった方法があるみたいです。

Netlinkについてはあまり解説されたまとまったものが見つからず、書籍だと Linuxネットワークプログラミング にまとまった解説がありました。以下記載の「メモ」の内容も、基本的に本書の内容を参考にさせて頂いています。

(動作的には問題なさそうなので多分大きな間違いは無いと思うのですが、ドキュメント類が少ないということもあり、メモ部分の説明等もしかしたら細かいところで間違っている部分があるかもしれません)

環境

  • Ruby 2.7
  • Linux 4.14.219-164.354.amzn2.aarch64

作ったもの

ルーティングテーブルを取得/表示します。
https://github.com/kuredev/simple_routing

test.rb
require_relative "./lib/simple_routing"

rt = SimpleRouting::RouteHandler.new
# rt.show_routes
rt.each_route do |route|
  route.show
end
console
% 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 が定義されていたので、その値を指定します。

/usr/include/bits/socket.h
#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 で定義されていたので、そのように指定します。

/usr/include/linux/netlink.h
#define NETLINK_ROUTE           0       /* Routing/device hook                          */

メッセージのフォーマットは [nlmsghdr構造体] + [NLMSGデータ] が基本になるようです。

②nlmsghdr構造体 の定義はこちらのManページ に記載がある通り以下です。この構造体に添うように、 pack を用いてRubyの文字列としてパラメータを準備します。

nlmsghdr構造体
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 構造体なのでそれに応じたデータを送信します。
https://linuxjm.osdn.jp/html/LDP_man-pages/man7/rtnetlink.7.html

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 */
};

https://linuxjm.osdn.jp/html/LDP_man-pages/man7/rtnetlink.7.html

rta_len (2Byte) と rta_type (2Byte) で4Byteなのでそこを読み込みつつ③、あとに続いているデータ部を読み込みます④。

rta_type は以下ドキュメントに記載があり、 RTA_DST(行き先アドレス) や RTA_GATEWAY(経路のゲートウェイ)等が格納されます。
https://linuxjm.osdn.jp/html/LDP_man-pages/man7/rtnetlink.7.html

データ部の構造は rta_type 次第なようですが、対応表のようなドキュメントは見つけられなかったので、動かしながら確認しました。アドレスが返ってくるような RTA_GATEWAY 等であれば、 IPAddr.new_ntoh(@data) のような形で値を取り出すことができました。

rtattr は複数返されるため、すべて取り出すまで繰り返し処理を行います。⑤

以上です。

参考

https://amzn.to/2OnpQkY

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