💬

LinuxのNetlinkでssやipコマンドみたいなものを作って遊ぼう

に公開

皆さんはLinuxのネットワークの状況を可視化するツールを自分で作ってみたいと思ったことはありますか?
多分無いのではないでしょうか。。
ですが、作ろうとまでは思わずとも、普段自分が叩いているssipといったコマンド群の裏側にちょっと興味がある方がいるかもしれません。
そんな方がきっといるだろうという希望を持ってこのブログを書こうと思います。

なお、このブログではNetlinkの詳細な仕様を解説するというよりは、機能の概要を解説して、実際にミニssやipコマンドを作ることを目標としていきます。
詳しい仕様に関しては、最後に参考URLを記載するのでそちらを参照ください。

Netlinkとは

Netlinkとは、Linuxカーネルとユーザスペースにある(私たちが普段開発している)プロセスとの間で通信するためのIPCメカニズムです。
ioctlと呼ばれるデバイスドライバを制御するシステムコールの機能を置き換える目的で作られました。
Netlinkは、専用のプロトコルが用意されており、その形式でBSDソケットを通じて通信します。

netlink_overview

Netlinkはバイナリでカーネルとやりとりするため、/procのファイルを読み取ったり、あるいはssなどのコマンドを自分のアプリケーションから別プロセスで実行するよりも効率に優れています。
なお、Netlinkはカーネルモジュールに対してもAPIを公開しており、それを活用することでNetlinkの機能を拡張することもできます。

Netlinkでこんなことができる

冒頭でも伝えた通り、Netlinkはssipnftablesコマンドの裏側で使用されています。
では、ざっくりとNetlinkでどんなことができるのかを箇条書きにしてみます。

  • ルーティングテーブルの操作(経路の追加・削除・取得)
  • ネットワークインターフェースの操作
  • ネットワークインターフェースが操作された旨の通知
  • TCP/UDP/UNIX/DCCP/SCTP ソケットの状態取得
  • 接続中ソケットのアドレス・ポート一覧取得
  • TCP 内部統計情報(RTT, cwnd, retransmission など)の取得
  • iptables/nftables のルール管理
  • SELinux のイベント通知
  • NATテーブルの操作

ちなみに、これはほんの一例です。
普段私たちがお世話になっているネットワーク系のコマンドの裏側というだけあり、ネットワークに関連する操作は非常に多彩です。

Netlinkはカーネルとユーザースペースとの間で双方向に通信が可能なので、カーネルから情報を取得したり操作するだけでなく、カーネルのイベントをサブスクライブしておいて通知させることも可能です。

Netlinkの基本的な仕組み

まず、Netlink通信はソケットを介して行われるため、socketシステムコールの呼び出す場面から見ていきます。

fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_GENERIC);

socket(2)は第一引数にdomain(どのプロトコルを使うか)をとります。
NetLinkを使用するには、まずここにAF_NETLINKというアドレスファミリーを指定します。
続けて、第二引数のtypeにはSOCK_RAWもしくはSOCK_DGRAMを指定可能です。
第三引数では使用するNetlink Familyをを伝えています(利用できるFamily)。
どのNetlink Familyを選択するかでカーネルから取得できる情報が変わります。
例えば、NETLINK_ROUTEならネットワークインターフェースやルーティングテーブルの取得・操作、NETLINK_SOCK_DIAGならソケットの状況の確認などを実施できます。

ソケットを作成したら、send(2)recv(2)を使用してNetlinkメッセージをやり取りします。
簡略化した実装のイメージを下記に記します。

fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);

/* Netlink Messageを作成 */
send(fd, &request, sizeof(request));
/* 返信を受信 */
n = recv(fd, &response, RSP_BUFFER_SIZE);

ここで、requestとresponseの形式として使用されているNetlinkのプロトコルの中身を見てみます。
Netlinkでは、固定長のヘッダーと可変長のペイロードで構成された、Netlinkメッセージと呼ばれる形式のデータでやりとりします。

nlmsg_hdr

ヘッダーの各フィールドの意味は以下の通りです:

  • Length: メッセージ全体の長さ(ヘッダー + ペイロード)
  • Type: メッセージの種類
  • Flags: リクエストフラグやACKフラグなど
  • Sequence: メッセージの識別番号。以前のメッセージを参照できるようにするために使用できる。
  • Port Number: メッセージを配信するピアを指定します。

これだけ見せられてもよくわからないと思います。
ただ、このブログではNetlinkの詳細な仕様を解説したいわけではないため、わかりづらいであろうTypeとFlagsの実例をrtnetlink(7)に見てみます。

rtnetlinkはLinuxのルーティングソケットです。Protocol FamiliyとしてNETLINK_ROUTEを指定することで作成できます。

fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);

ちなみに、NETLINK_ROUTEはルーティングテーブルやネットワークインターフェースの操作ができます。
manの説明文を転記しておきます。

NETLINK_ROUTE
Receives routing and link updates and may be used to modify
the routing tables (both IPv4 and IPv6), IP addresses, link
parameters, neighbor setups, queueing disciplines, traffic
classes, and packet classifiers (see rtnetlink(7)).

では、次にrtnetlink(7)のmanページを見てみると、RTM_NEWLINK, RTM_DELLINK, RTM_GETLINKといった、RTM_*という形式のメッセージタイプが紹介されています。
これらがNetlink MessageのヘッダのTypeに該当します。
例えばRTM_**LINKのTypeは指定したネットワークインターフェースの情報を、生成・削除・取得します。
このように、TypeではそのNetlink Messageが何をさせるものなのかを指定しています。

Flagsの記述はnetlink(7)にあります。
そこまで記述量が多くないのでここに貼ってしまいます。

  Standard flag bits in nlmsg_flags
  ───────────────────────────────────────────────────────────────────
  NLM_F_REQUEST           Must be set on all request messages.
  NLM_F_MULTI             The message is part of a multipart message
                          terminated by NLMSG_DONE.
  NLM_F_ACK               Request for an acknowledgement on success.
  NLM_F_ECHO              Echo this request.
  Additional flag bits for GET requests
  ───────────────────────────────────────────────────────────────────
  NLM_F_ROOT               Return the complete table instead of a
                          single entry.
  NLM_F_MATCH              Return all entries matching criteria
                          passed in message content.  Not
                          implemented yet.
  NLM_F_ATOMIC             Return an atomic snapshot of the table.
  NLM_F_DUMP               Convenience macro; equivalent to
                          (NLM_F_ROOT|NLM_F_MATCH).

  Note that NLM_F_ATOMIC requires the CAP_NET_ADMIN capability or an
  effective UID of 0.
  Additional flag bits for NEW requests
  ───────────────────────────────────────────────────────────────────
  NLM_F_REPLACE             Replace existing matching object.
  NLM_F_EXCL                Don't replace if the object already
                            exists.
  NLM_F_CREATE              Create object if it doesn't already
                            exist.
  NLM_F_APPEND              Add to the end of the object list.

NLM_F_*と記載されたものがFlagsです。よく使うのはリクエストであることを示すNLM_F_REQUESTです。
このように、FlagsはそのNetlink Messageがどのような性質を持つかを表します。


ここまでNetlinkの基本的な仕様について語ってきましたが、簡単にまとめると、sockect作成時にNetlink Familyを指定することで使用するNetlinkプロトコルのカテゴリを指定し、次に、Typeで実際に行いたい操作の種別を、Flagsで送信するメッセージの性質を伝えています。
実は、Netlink Familyごとにさらに追加のヘッダ情報が存在するのですが、ここでは割愛します。

NetlinkにはClassic NetlinkとGenelic Netlinkがあります。
名前の通り、Classic NetlinkはNetlinkの初期実装で、静的にサブシステムにIDが割り当てられているのが特徴です。
先ほど紹介したNETLINK_ROUTEや、iSCSIを司るNETLINK_ISCSI、監査プロトコルであるNETLINK_AUDITなど多くのNetlink Familiyが該当します。
これらはLinuxのinclude/uapi/linux/netlink.hにて直接的に定義されているため、動的にサブシステムを追加することができません。

そこで、Generic Netlinkが登場しました。
Generic Netlinkは、カーネル開発者に対し、サブシステムを動的に追加するAPIを提供しています(このブログではこの辺は割愛)。
そのAPIを使用して登録されたNetlink Familyを使用するにはまず、socket(7)の際にファミリにNETLINK_GENERICを指定します。
ただ、これではClassic Netlinkで判別できていたサブシステムのカテゴリがGenelic Netlinkであることしかわかりません。
そこで、Genelic Netlinkでは、Netlink MessageのヘッダーのTypeに、Genelic Netlink FamilyのサブシステムのIDを指定させます。
さらに、Genelic Netlink用のヘッダーデータを設け、その中のcmdというデータ部分で実際に行う操作内容を指定します。

struct nlmsghdr {
      __u32   nlmsg_len;      /* Length of message including headers */
      __u16   nlmsg_type;     /* Generic Netlink Family (subsystem) ID */
      __u16   nlmsg_flags;    /* Flags - request or dump */
      __u32   nlmsg_seq;      /* Sequence number */
      __u32   nlmsg_pid;      /* Port ID, set to 0 */
};
struct genlmsghdr {
      __u8    cmd;            /* Command, as defined by the Family */
      __u8    version;        /* Irrelevant, set to 1 */
      __u16   reserved;       /* Reserved, set to 0 */
};
/* TLV attributes follow... */

Genelic Netlinkとして使用できるFamilyの代表例としてはnl80211(WiFi管理)、taskstats(プロセス統計)などがあります。
使用できるFamilyの一覧を取得するには、genl-ctrl-listコマンドを実行して下さい。
genl-ctrl-listはubuntuであればapt-get install -y libnl-utilsでインストールできます。
より詳細なGenelic Netlinkの使用方法に興味がある方はこちらを参照ください。

ちなみに、Generic Netlinkの方が新しいからといって、Classic Netlinkが非推奨になっているとかそんなことはないです。
用途に合わせて使い分けて下さい。

Netlinkを使ったサンプルプログラム

ここからは、簡単に作成したCのサンプルプログラムを2つほど紹介します。
リポジトリはこちらです。
Dockerを使用して簡単に実行できるようにしてあるので、Netlinkに興味がある方は是非実際に動かしてみて下さい。

なお、Netlinkの使用に際してはlibnlというライブラリを使用しています。

Network Interfaceリスト取得ツール

ip linkの簡易版のようなツールです。
名前の通り、Network Interfaceの一覧を取得して表示します。

ソースコードはこちらです。

#include <stdio.h>
#include <linux/if.h>
#include <linux/rtnetlink.h>
#include <netlink/netlink.h>
#include <netlink/socket.h>
#include <netlink/cache.h>
#include <netlink/route/link.h>

static const char* updown(const unsigned int flags) { return (flags & IFF_UP) ? "UP" : "DOWN"; }

void cb(struct nl_object * nlobj, void * _){
        struct rtnl_link *link = (struct rtnl_link *)nlobj;
        const char *name = rtnl_link_get_name(link);
        int ifindex      = rtnl_link_get_ifindex(link);
        unsigned int fl  = rtnl_link_get_flags(link);
        int mtu          = rtnl_link_get_mtu(link);
        const char *qdisc= rtnl_link_get_qdisc(link);
        int oper         = rtnl_link_get_operstate(link);
        printf("ifindex=%d name=%s flags=0x%x (%s) mtu=%d qdisc=%s operstate=%d\n",
               ifindex, name ? name : "(null)", fl, updown(fl), mtu,
               qdisc ? qdisc : "-", oper);
    }

int main(void) {
    int err;
    struct nl_sock *sk = nl_socket_alloc();
    if (!sk) { perror("nl_socket_alloc"); return 1; }
    if ((err = nl_connect(sk, NETLINK_ROUTE)) < 0) {
        fprintf(stderr, "nl_connect: %s\n", nl_geterror(err));
        nl_socket_free(sk);
        return 1;
    }

    struct nl_cache *link_cache = NULL;
    if ((err = rtnl_link_alloc_cache(sk, AF_UNSPEC, &link_cache)) < 0) {
        fprintf(stderr, "rtnl_link_alloc_cache: %s\n", nl_geterror(err));
        nl_socket_free(sk);
        return 1;
    }

    printf("=== links ===\n");
    nl_cache_foreach(link_cache, cb, NULL); 

    nl_cache_free(link_cache);
    nl_socket_free(sk);
    return 0;
}

libnlでは、nl_socket_allocでNetlink socketを作成し、nl_connectでカーネルとやりとりできるようになります。
rtnl_link_alloc_cacheは内部的にNETLINK_ROUTE Familyを使用してネットワークインターフェースの情報を取得しており、あとはそれをnl_cache_foreachで指定したコールバック関数を使用して出力しているだけとなります。

たったこれだけのコードですが、動かしてみると下記のような出力を得られます。

$root@fd1cb7a9c15c:/app# make listif
Running sample: listif
=== links ===
ifindex=1 name=lo flags=0x10049 (UP) mtu=65536 qdisc=noqueue operstate=0
ifindex=2 name=tunl0 flags=0x80 (DOWN) mtu=1480 qdisc=noop operstate=2
ifindex=3 name=gre0 flags=0x80 (DOWN) mtu=1476 qdisc=noop operstate=2
ifindex=4 name=gretap0 flags=0x1002 (DOWN) mtu=1462 qdisc=noop operstate=2
ifindex=5 name=erspan0 flags=0x1002 (DOWN) mtu=1450 qdisc=noop operstate=2
ifindex=6 name=ip_vti0 flags=0x80 (DOWN) mtu=1480 qdisc=noop operstate=2
ifindex=7 name=ip6_vti0 flags=0x80 (DOWN) mtu=1428 qdisc=noop operstate=2
ifindex=8 name=sit0 flags=0x80 (DOWN) mtu=1480 qdisc=noop operstate=2
ifindex=9 name=ip6tnl0 flags=0x80 (DOWN) mtu=1452 qdisc=noop operstate=2
ifindex=10 name=ip6gre0 flags=0x80 (DOWN) mtu=1448 qdisc=noop operstate=2
ifindex=19 name=eth0 flags=0x11043 (UP) mtu=65535 qdisc=noqueue operstate=6

libnlは割とドキュメントが充実しているので助かります。

ミニss

続いては、ssコマンドっぽいものを作ります。
コマンド実行時点のソケットの状況を確認し、src->dest の形式で接続先と接続元のアドレス情報を表示します。

#include <stdio.h>
#include <arpa/inet.h>
#include <netlink/netlink.h>
#include <netlink/socket.h>
#include <netlink/msg.h>
#include <netlink/idiag/idiagnl.h>
#include <netlink/idiag/idiagnl.h>
#include <netlink/idiag/req.h>
#include <netlink/idiag/msg.h>
#include <linux/inet_diag.h>
#include <netinet/tcp.h>

static int cb_diag(struct nl_msg *nlmsg, void *_) {
    struct inet_diag_msg *m = nlmsg_data(nlmsg_hdr(nlmsg));
    char src[INET6_ADDRSTRLEN], dst[INET6_ADDRSTRLEN];

    if (m->idiag_family == AF_INET) {
        inet_ntop(AF_INET, m->id.idiag_src, src, sizeof(src));
        inet_ntop(AF_INET, m->id.idiag_dst, dst, sizeof(dst));
        printf("%s:%u -> %s:%u state=%u inode=%u\n",
               src, ntohs(m->id.idiag_sport),
               dst, ntohs(m->id.idiag_dport),
               m->idiag_state, m->idiag_inode);
    }
    return NL_OK;
}

enum
{
  TCP_ESTABLISHED = 1,
  TCP_SYN_SENT,
  TCP_SYN_RECV,
  TCP_FIN_WAIT1,
  TCP_FIN_WAIT2,
  TCP_TIME_WAIT,
  TCP_CLOSE,
  TCP_CLOSE_WAIT,
  TCP_LAST_ACK,
  TCP_LISTEN,
  TCP_CLOSING
};

int main() {

    struct nl_sock *sock = nl_socket_alloc();
    nl_connect(sock, NETLINK_SOCK_DIAG);

    nl_socket_modify_cb(sock, NL_CB_VALID, NL_CB_CUSTOM, cb_diag, NULL);

    // ESTABLISHED なTCPコネクションだけを取得する
    idiagnl_send_simple(sock, (NLM_F_REQUEST|NLM_F_DUMP), AF_INET, 1 << TCP_ESTABLISHED, INET_DIAG_MEMINFO);

    nl_recvmsgs_default(sock);

    nl_socket_free(sock);
    return 0;
}

こちらも、nl_socket_allocnl_connectを実行するところまでは一つ目のツールと変わりません。
しかし、nl_socket_modify_cbを使用することで、のちにnl_recvmsgs_default実行時に実行されるコールバック関数を上書きしています。
そして、肝心のソケット情報の確認にはidiagnl_send_simpleを使用しています。
inet_diagはsock_diag(7)の前身にあたります。
sock_diagをlibnlではなぜ使っていないのかと思いましたが、libnlのコードを見ると#define NETLINK_INET_DIAG NETLINK_SOCK_DIAG というマクロが定義されていたので、内部的にはsock_diagが使われているようです。
さて、sock_diagでは、ソケットの診断情報を取得することができるので、idiagnl_send_simpleで取得したソケット情報をnl_recvmsgs_defaultで受信し、コールバック関数を実行して出力をします。
ここで気をつけなくてはならないポイントは、アドレス情報に対して実行しているinet_ntopと、ポートに対して実行しているntohsです。
これらを実行しないと、前者は正しく文字列でIPアドレスが表示されないですし、ポートはネットワークバイトオーダーで表示されるので意図したポート番号になりません。

$root@fd1cb7a9c15c:/app# make miniss
Running sample: miniss
127.0.0.1:39852 -> 127.0.0.1:34629 state=1 inode=301517
127.0.0.1:39840 -> 127.0.0.1:34629 state=1 inode=303851
172.17.0.2:45890 -> 140.82.113.21:443 state=1 inode=537792
127.0.0.1:34629 -> 127.0.0.1:39840 state=1 inode=307205
127.0.0.1:34629 -> 127.0.0.1:39852 state=1 inode=306199
172.17.0.2:37534 -> 140.82.112.21:443 state=1 inode=644179
172.17.0.2:52380 -> 20.27.177.116:443 state=1 inode=635832
172.17.0.2:42352 -> 52.175.140.176:443 state=1 inode=623728

本当はinodeからpidを導出したかったのですがサンプルなのでここまで。

終わりに

今後はNetlinkを使用して自分用のネットワーク診断ツールを作ろうと思っています。
少しディープな話ではありますが、皆さんももし興味があればNetlinkに触れてみて下さい。
ではまた。

参考URL

Discussion