LinuxのNetlinkでssやipコマンドみたいなものを作って遊ぼう
皆さんはLinuxのネットワークの状況を可視化するツールを自分で作ってみたいと思ったことはありますか?
多分無いのではないでしょうか。。
ですが、作ろうとまでは思わずとも、普段自分が叩いているss
やip
といったコマンド群の裏側にちょっと興味がある方がいるかもしれません。
そんな方がきっといるだろうという希望を持ってこのブログを書こうと思います。
なお、このブログではNetlinkの詳細な仕様を解説するというよりは、機能の概要を解説して、実際にミニssやipコマンドを作ることを目標としていきます。
詳しい仕様に関しては、最後に参考URLを記載するのでそちらを参照ください。
Netlinkとは
Netlinkとは、Linuxカーネルとユーザスペースにある(私たちが普段開発している)プロセスとの間で通信するためのIPCメカニズムです。
ioctlと呼ばれるデバイスドライバを制御するシステムコールの機能を置き換える目的で作られました。
Netlinkは、専用のプロトコルが用意されており、その形式でBSDソケットを通じて通信します。
Netlinkはバイナリでカーネルとやりとりするため、/procのファイルを読み取ったり、あるいはss
などのコマンドを自分のアプリケーションから別プロセスで実行するよりも効率に優れています。
なお、Netlinkはカーネルモジュールに対してもAPIを公開しており、それを活用することでNetlinkの機能を拡張することもできます。
Netlinkでこんなことができる
冒頭でも伝えた通り、Netlinkはss
やip
、nftables
コマンドの裏側で使用されています。
では、ざっくりと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メッセージと呼ばれる形式のデータでやりとりします。
ヘッダーの各フィールドの意味は以下の通りです:
- 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ごとにさらに追加のヘッダ情報が存在するのですが、ここでは割愛します。
Classic NetlinkとGenelic Netlink
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_alloc
とnl_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に触れてみて下さい。
ではまた。
Discussion