Netlinkと友達になろう
Netlinkを学び始めました。記録です。 今回はNetlink メッセージをコードで書く内容であったり、そのメッセージをキャプチャしてバイナリを読みながらNetlinkを理解する記事になります。
Netlinkとは?
ユーザー空間とカーネル空間のやりとりを行う事が出来るLinux kernelのサブシステムです。 Netlinkは、socket通信を利用してユーザー側はカーネル空間との通信を行う事が出来るため通常のネットワークプログラミングと同じように、Netlinkに関する様々な要素を持ったヘッダーを付加し送信を行った後、recv関数といったソケットからメッセージを受け取る関数を使用する事でカーネル空間とユーザ空間の通信を実現しています。
ネットワーク経由でNetlinkを使用する主な要素としては以下が挙げられます。
- 経路テーブルに関する操作(例:追加, 削除, 更新).
- インターフェースに関する操作(例:デバイスの追加, 削除, 更新).
- ネットワークの監視(例:経路追加, 削除, 更新が行われた際の通知、インターフェースも同じく).
またNetlinkが他の用途でどのように使用されているかを調べたい場合では、NetlinkのFamilyが定義されているヘッダーファイルを見れば良いです。
パケットフィルタリングで使用されるnetfilterやファイルの改竄検知を行う事が出来るauditなどがあります。
はじめに
前述したように、Netlinkはソケット通信を使用してユーザ空間とカーネル空間とのやり取りを行います。 今回はネットワーク関連で使用されるrtnetlinkを使用してNetlinkの通信の仕組みを理解します。 また今回はNetlinkを主に使用しているiproute2と呼ばれるLinux networkの設定を行う事が出来るソフトウェアのコマンドを分解しながら説明を行い、iproute2におけるシンプルなネットワーク経路情報の追加のコマンドと同じ様なnetlinkのメッセージを送信します。
ip route add ○.○.○.○ via ○.○.○.○
ここからはNetlinkの仕組みとC言語を用いてのコードの作成方法を説明します。
ソケットオープン
Netlinkが基本的に扱いやすいC言語を使用して説明を行っていきます。 また記事で紹介する関数は複雑なnetlink msgを処理するために一部libnetlinkの関数を使用したり、マクロを使用したりしています。(基本的にこれを使う!!!!!!!)
まずはソケットの作成を行います。 Netlinkのmanページにあるように、第一引数はAF_NETLINK 第二引数はSOCK_RAW 第三引数はNETLINK_ROUTEと指定します。
int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
引数を見れば分かる通り、socket domainがAF_NETLINKと見たことがないdomainがありますね。 通常ネットワーク通信を行いたい場合だとAF_INETを使用しますがNetlinkはこのdomainを使用して、カーネル空間とのやり取りを行う事が出来ます。
またsocketのtypeを指定する引数にはNETLINK_ROUTEがあります。rtnetlink を利用する場合にはNETLINK_ROUTE を指定します。
netlink msgのフォーマット
次にNetlinkのメッセージを定義します。 Netlinkはメッセージのフォーマットとして、nlmsghdrと呼ばれるNetlinkの基本的ヘッダー情報を元にTCPやICMPと同じくデータが続いていく構成になっています。 まず必ず付加する事が定められているNetlink メッセージ ヘッダー、nlmsghdrを見てみます。
struct nlmsghdr { __u32 nlmsg_len; /* ヘッダーを含むメッセージの長さ */ __u16 nlmsg_type; /* メッセージの内容のタイプ */ __u16 nlmsg_flags; /* 追加フラグ */ __u32 nlmsg_seq; /* シーケンス番号 */ __u32 nlmsg_pid; /* 送信者のポート ID */ };
定義されているnlmsghdr構造体のメンバの中にはnlmsg_typeというメンバがあります。 これはNetlinkのメッセージがどのような要素を含むかを識別するものであり、今回経路の追加であれば以下のtypeを主に使用します。
RTM_NEWLINK RTM_DELLINK RTM_GETLINK RTM_NEWROUTE RTM_DELROUTE RTM_GETROUTE ...
例えば、RTM_NEWROUTEはlinux kernel上にある経路テーブルに対して新しい経路を追加する場合に指定します。 またNetlinkは基本的、このnlmsg_typeの値を元にnlmsghdrとtypeごとに決められたヘッダを付与する事でやりとりを行う事が出来ます。 前述したRTM_NEWROUTEを指定したい場合、manページには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; see RTA_TABLE below */ unsigned char rtm_protocol; /* Routing protocol; see below */ unsigned char rtm_scope; /* See below */ unsigned char rtm_type; /* See below */
漠然とした説明ですが、まとめると
- Netlink メッセージはnlmsghdrを必ず含めた構成になっている
- nlmsg_typeを元に続くペイロードのデータが構築される
という事になります。 またこの説明はnetlink(7)のmanページの説明を見ると理解が深まると思います。
ここからはコードを書きながらNetlink メッセージをどう構築すれば良いのかを説明します。
コードを書く
ではここまでの流れをコードに書いてみます。
struct netlink_msg{ struct nlmsghdr n; struct rtmsg r; char buf[4096]; }; struct netlink_req req; int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); if (fd < 0) { return 0; } req.n.nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg)); req.n.nlmsg_flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_ACK | NLM_F_REPLACE; req.n.nlmsg_type = RTM_NEWROUTE; req.r.rtm_family = AF_INET; req.r.rtm_dst_len = 32; req.r.rtm_src_len = 0; req.r.rtm_tos = 0; req.r.rtm_table = RT_TABLE_MAIN; req.r.rtm_protocol = 0x03; req.r.rtm_scope = 0xFD; req.r.rtm_type = RTN_UNICAST; req.r.rtm_flags = 0;
Netlinkプログラミングを行う際には、定義してあるnetlink_reqのようにNetlinkのメッセージヘッダであるnlmsghdr構造体を必ずメンバに入れ、それに対応するペイロードを定義します。 また後ろのbuf部分はNetlinkの付加部分であるrtattrという属性を追加するために定義します。rtattrは以下のように定義されています。
struct rtattr { unsigned short rta_len; /* Length of option */ unsigned short rta_type; /* Type of option */ /* Data follows */ };
rtattrはカーネルが定義しているrta_typeを元に対応したデータを入れる事が可能であり、rtnetlink.hではrta_typeの一覧を見る事が出来ます。
rta_typeには、例えばRTA_DSTは経路に対するDestination(行き先)を決める事が出来ます。 この部分は後述にしっかり記載します。
rtattrの説明が少し長くなりましたが、ここからメッセージヘッダに入っているnlmsghdr構造体のメンバの値やrtmsg構造体の値の説明を行います。
nlmsghdr :nlmsg_len
nlmsg_lenはNetlinkメッセージヘッダの長さを定義するものです。これは定義するnlmsghdr、rtmsg、rtattrのヘッダの長さが値となります。 またここでは便利なマクロとしてNLMSG_LENGTHと呼ばれるマクロが存在します。 このマクロは予め定義するヘッダ(今回はrtmsg)を元にヘッダの長さを計算してもらいます。
req.n.nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg));
またNLMSG_LENGTHはnetlink.hで以下のように定義されています。
#define NLMSG_LENGTH(len) ((len) + NLMSG_HDRLEN)
nlmshdr: nlmsg_flags
nlmsg_flagsは構築したNetlinkメッセージをカーネルに対して様々な要求が行えるflagです。 例えば以下に一部定義しているNLM_F_ACKは、カーネルに対してメッセージの返答を貰うためのflagです。
req.n.nlmsg_flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_ACK | NLM_F_REPLACE;
rtmsg: rtm_family
rtm_familyはその経路情報が持つaddress familyを認識します。 今回はIPv4形式の経路を追加するため、Linux上のIPv4形式の識別子であるAF_INET定義します。
IPv4 = AF_INET IPv6 = AF_INET6
req.r.rtm_family = AF_INET;
rtmsg: rtm_dst_len rtm_src_len
rtm_dst_lenはDestination Adressに対するマスク(サブネットマスク)になります。rtm_src_lenは今回のような経路作成で使用する事はありませんが、例えば経路情報の取得を行いたい場合はフィルタとして使用する事があります。
req.r.rtm_dst_len = 32; req.r.rtm_src_len = 0;
rtmsg: rtm_tos
QoS制御などで使用されるDSCPという仕組みがあります。DSCPはToSの値を使用して制御信号としての役割を果たします。rtm_tosはそのToSの値を入れるためのフィールドと言えます。 例えば経路情報に登録されているToSの値付きの経路があるとして、その経路のパケットが受信されるとToSの値を確認した後に値を見比べてフィルタを掛ける事が出来ます。
req.r.rtm_tos = 0;
rtmsg: rtm_table
Linuxでは登録される経路をルーティングテーブルに登録する際に、特定の識別子を用いて登録する事が出来ます。 Linuxのメインルーティングテーブルの値である基本的にRT_TABLE_MAINか指定無しのRT_TABLE_UNSPECを使用します。
enum rt_class_t { RT_TABLE_UNSPEC=0, /* User defined values */ RT_TABLE_COMPAT=252, RT_TABLE_DEFAULT=253, RT_TABLE_MAIN=254, RT_TABLE_LOCAL=255, RT_TABLE_MAX=0xFFFFFFFF };
req.r.rtm_table = RT_TABLE_MAIN;
rtmsg: rtm_protocol
登録される経路がどこから来たかを示す値です。例えばBGPやOSPFなど用いられて登録された経路は個々の値が経路に付加されています。
#define RTPROT_UNSPEC 0 #define RTPROT_REDIRECT 1 /* Route installed by ICMP redirects; not used by current IPv4 */ #define RTPROT_KERNEL 2 /* Route installed by kernel */ #define RTPROT_BOOT 3 /* Route installed during boot */ #define RTPROT_STATIC 4 /* Route installed by administrator */ ... #define RTPROT_BGP 186 /* BGP Routes */ #define RTPROT_ISIS 187 /* ISIS Routes */ #define RTPROT_OSPF 188 /* OSPF Routes */
登録する経路はルーティングプロトコルを使用して登録されているものではないのでRTPROT_STATICやRTPROT_KERNELを指定します。
req.r.rtm_protocol = RTPROT_STATIC;
rtmsg: rtm_scope
経路の種類を識別するためのフィールドです。登録する経路に対してその経路がグローバルなものなのかローカルなものなのかを判別する事が出来ます。
enum rt_scope_t { RT_SCOPE_UNIVERSE=0, /* User defined values */ RT_SCOPE_SITE=200, RT_SCOPE_LINK=253, RT_SCOPE_HOST=254, RT_SCOPE_NOWHERE=255 };
登録する経路はグローバルなものにしたいのでRT_SCOPE_UNIVERSEを指定します。
req.r.rtm_scope = RT_SCOPE_UNIVERSE;
rtmsg: rtm_type
経路のタイプを判別するためのフィールドです。
enum { RTN_UNSPEC, RTN_UNICAST, /* Gateway or direct route */ RTN_LOCAL, /* Accept locally */ RTN_BROADCAST, /* Accept locally as broadcast, send as broadcast */ RTN_ANYCAST, /* Accept locally as broadcast, but send as unicast */ RTN_MULTICAST, /* Multicast route */ RTN_BLACKHOLE, /* Drop */ RTN_UNREACHABLE, /* Destination is unreachable */ RTN_PROHIBIT, /* Administratively prohibited */ RTN_THROW, /* Not in this table */ RTN_NAT, /* Translate this address */ RTN_XRESOLVE, /* Use external resolver */ __RTN_MAX };
今回はマルチキャストな経路でもないので、RTN_UNICASTを指定します。
req.r.rtm_type = RTN_UNICAST;
rtmsg: rtm_flags
その経路に対する通知などをユーザーに知らせるためのフィールドです。
#define RTM_F_NOTIFY 0x100 /* Notify user of route change */ #define RTM_F_CLONED 0x200 /* This route is cloned */ #define RTM_F_EQUALIZE 0x400 /* Multipath equalizer: NI */ #define RTM_F_PREFIX 0x800 /* Prefix addresses */ #define RTM_F_LOOKUP_TABLE 0x1000 /* set rtm_table to FIB lookup result */ #define RTM_F_FIB_MATCH 0x2000 /* return full fib lookup match */ #define RTM_F_OFFLOAD 0x4000 /* route is offloaded */ #define RTM_F_TRAP 0x8000 /* route is trapping packets */ #define RTM_F_OFFLOAD_FAILED 0x20000000 /* route offload failed, this value * is chosen to avoid conflicts with * other flags defined in * include/uapi/linux/ipv6_route.h */
req.r.rtm_flags = 0;
以上がnlmsghdrにあるフィールドの説明とrtmsgのフィールドの説明になります。 またここからはNetlinkの追加要素であるrtattrの説明とメッセージ作成に使用する関数の説明を行います。
rtattr
rtattrはrtnetlinkの付加的要素であり、rtnetlinkはこのrtattrを使用して様々な経路を登録する事が可能になります。 具体的にはrta_typeとrta_lenを指定した後、構造体にあるバッファにカーネルが対応するデータを書き込む事で経路の追加を行う事が出来ます。 しかし、rtattrの構造体をよく見てみるとrta_typeとrta_lenのフィールド以外は定義されていないように思えます。
struct rtattr { unsigned short rta_len; /* Length of option */ unsigned short rta_type; /* Type of option */ /* Data follows */ };
ではここからどうやってIPv4 PrefixやIPv6 Prefixの値を書き込めばいいのでしょうか? 次の章では、rtattrのメッセージ作成のテクニックと、関数の説明、rta_typeの説明を行います。
rtattr : RTA_DST
経路の行き先のアドレスを指定するためのrta_typeです。 iproute2で経路を追加しようとすると以下のような指定があるはずです。
via ○.○.○.○
この部分がRTA_DSTとなります。このRTA_DSTを使用してどのようにバッファにデータを書き込むかを説明する前にrtattrの操作方法について説明します。
libnetlinkを使用したrtattrメッセージの作成
libnetlinkはNetlinkメッセージの作成を補助するためのライブラリです。主にrtattrのメッセージを作成するために必要な関数やマクロが用意されています。 またlibnetlinkが操作を行うための関数を使用しているか、マクロを使用しているかを調べるためにはiproute2にあるlibnetlink.hを見るのが早いと思います。
また主に操作に使用するマクロや関数は以下のようなものがあります。
RTA_DATA RTA_LENGTH RTA_TAIL
int addattr_l(struct nlmsghdr *n, int maxlen, int type, const void *data, int alen); int rta_addattr8(struct rtattr *rta, int maxlen, int type, __u8 data); int rta_addattr16(struct rtattr *rta, int maxlen, int type, __u16 data); int rta_addattr32(struct rtattr *rta, int maxlen, int type, __u32 data); int rta_addattr64(struct rtattr *rta, int maxlen, int type, __u64 data); int rta_addattr_l(struct rtattr *rta, int maxlen, int type, const void *data, int alen);
ではここから戻ってRTA_DSTのメッセージ作成を行います。
RTA_DSTの書き込み
RTA_DSTは経路の行き先を指定するためのIPv4 Prefixが必要になります。 CでIPv4 Prefixを定義したい場合は、struct in_addr構造体が有名な定義するための構造体でしょう。
struct in_addr via_v4prefix; inet_pton(AF_INET, "1.1.1.1", &via_v4prefix);
inet_pton()を使用して文字列からin_addr型に変換した後、データを入れるために都合が良い関数は、データがlibnetlink内で定義されておらず、またデータの長さが可変長でも良い引数を持つaddattr_l()が良いでしょう。
int addattr_l(struct nlmsghdr *n, int maxlen, int type, const void *data, int alen) { int len = RTA_LENGTH(alen); struct rtattr *rta; if (NLMSG_ALIGN(n->nlmsg_len) + RTA_ALIGN(len) > maxlen) { fprintf(stderr, "addattr_l ERROR: message exceeded bound of %d\n", maxlen); return -1; } rta = NLMSG_TAIL(n); rta->rta_type = type; rta->rta_len = len; if (alen) memcpy(RTA_DATA(rta), data, alen); n->nlmsg_len = NLMSG_ALIGN(n->nlmsg_len) + RTA_ALIGN(len); return 0; }
addattr_l()の説明を簡単に行うと、RTA_LENGTHでデータの長さをrtattrヘッダとデータの長さを足す事によって、rta_lenを定義します。 rtattr型の構造体を定義した後、NLMSG_TAIL()で現在のバッファの一番後ろまで移動します。 またrta_typeにはRTA_DSTを定義します。 memcpyを使用してdataをコピーし、最後にnlmsghdrのヘッダの長さをデータの長さに合わせた長さに変更します。
では、RTA_DSTを定義したい場合は自然と以下のような定義になるでしょう。
addattr_l(&req.n, sizeof(req), RTA_DST, &via_v4prefix, sizeof(struct in_addr));
このように、libnetlinkの関数を使用してメッセージの操作を行う事が出来るというのが理解出来たと思います。
rtattr: RTA_GATEWAY
RTA_GATEWAYは該当する経路を登録するためのrta_typeです。 またiproute2では以下の部分がRTA_GATEWAYに該当するでしょう。
ip route add ○.○.○.○ ...
RTA_DSTと同じくIPv4 Prefixがデータとして必要になるので、同じようにaddattr_l()を使用してデータを定義します。
addattr_l(&req.n, sizeof(req), RTA_GATEWAY, &dst_addr, sizeof(struct in_addr));
rtattr: RTA_OIF
RTA_OIFは、登録される経路のinterface indexを入れるためのrta_typeです。 通常、経路を登録する際にはその経路がどこのinterfaceから送信されるかを示すための情報が必要になると思います。 またルーティングテーブルを確認してみると、以下のように登録されている事があると思います。
... dev ○○○ proto static
このdev という部分がinterfaceであり、数値として指定する場合にはip aなどのinterface情報が分かるコマンドを使用してindex値を確認します。
uint32_t oif_idx = index; addattr32(&req.n, sizeof(req), RTA_OIF, oif_idx);
index値はuint_32_tで良いという事になっているため、addattr32()を使用してデータを書き込みます。
int addattr32(struct nlmsghdr *n, int maxlen, int type, __u32 data) { return addattr_l(n, maxlen, type, &data, sizeof(__u32)); }
addattr32()は32bitのデータを扱うためのaddattr_lのラッパ関数です。
メッセージの送信
以上がメッセージの完成なのですが、このメッセージを送るための送信部分を作成します。 Netlinkはソケットを通じて、カーネルとのやり取りを行うため通常の送信関数や受信関数などを用いて送受信を行います。 またNetlinkのメッセージを送受信する際には、変換を行ったり、受信メッセージのパースなどのテクニックも存在するので説明します。
送信部分の作成
sockaddr_nl構造体というNetlink専用のソケットである事を定義する構造体を使用します。
struct sockaddr_nl { sa_family_t nl_family; /* AF_NETLINK */ unsigned short nl_pad; /* 0 である */ pid_t nl_pid; /* ポート ID */ __u32 nl_groups; /* マルチキャストグループマスク */ };
この構造体はfamilyを入れるフィールドやNetlinkのソケットなどを監視したい際にイベントIDなどを入れて監視を行う事が出来るnl_groupsなどがあります。(ipmonitor)
今回は送信関数として、構造体msghdrを用いた送信が行えるsendmsg()を使用して送信を行います。 またmsghdrのフィールドの中にmsg_iovというフィールドがあります。
struct sockaddr_nl nladdr = { .nl_family = AF_NETLINK }; struct msghdr msg = { .msg_name = &nladdr, .msg_namelen = sizeof(nladdr), .msg_iov = iov, .msg_iovlen = 1, }; int send_len = sendmsg(fd, &msg, 0); if (send_len < 0) { perror("Receiving message failed"); }
struct iovec iov = {&req, req.n.nlmsg_len };
このiovec構造体は。Linux上でデータ互換性と扱いやすくするための構造体です。 この構造体に今まで作ったデータ(req)とその長さを入れるとmsghdrとして送信する事が出来ます。
受信部分の作成
char recv_buf[MAX_RECV_BUF_LEN]; struct iovec riov = { .iov_base = &recv_buf, .iov_len = MAX_RECV_BUF_LEN}; msg.msg_iov = &riov; msg.msg_iovlen = 1; int recv_len = recvmsg(fd, &msg, 0); if (recv_len < 0) { perror("Receiving message failed"); }
送信したNetlink メッセージを受信する場合はmsghdrを受信出来るrecvmsg()を使用します。 また同じく受信する場合もiovecを使用した受信方法にします。
struct nlmsghdr *rnh; for (rnh = (struct nlmsghdr *)recv_buf; NLMSG_OK(rnh, recv_len); rnh = NLMSG_NEXT(rnh, recv_len)) { if (rnh->nlmsg_type == NLMSG_ERROR) { struct nlmsgerr *errmsg; errmsg = NLMSG_DATA(rnh); printf("%d, %s\n", errmsg->error, strerror(-errmsg->error)); break; } }
受け取ったNetlink メッセージはそのままrecv_bufにバイナリとして残っているため、Netlink メッセージとして扱うためのnlmsghdr型にキャストする事が出来ます、またNLMSG_OK()とNLMSG_NEXT()マクロでループさせます。
ここでNLMSG_ERRORを使用した判定式を作っていますが、これは正常にNetlinkメッセージが送信出来たかを確認することができる識別子です。 またはnlmsgerrはnlmsg_typeのNLMSG_ERRORに対応した構造体で、Linux上でのエラー原因を教えてくれるerrnoの形に出来ます。
struct nlmsgerr { int error; /* 負または 0 の errno は応答を表す */ struct nlmsghdr msg; /* エラーを起こしたメッセージのヘッダー */ };
完成
ここまで出来たのでコードにしてまとめてみました
main code
#include "libnetlink.h" #define DST_MASK_LEN 32 int ipv4_route_add(struct in_addr src_addr, struct in_addr dst_addr, uint32_t index) { struct netlink_msg req; int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); if (fd < 0) { return 0; } req.n.nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg)); req.n.nlmsg_flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_ACK | NLM_F_REPLACE; req.n.nlmsg_type = RTM_NEWROUTE; req.r.rtm_family = AF_INET; req.r.rtm_dst_len = DST_MASK_LEN; req.r.rtm_src_len = 0; req.r.rtm_tos = 0; req.r.rtm_table = RT_TABLE_MAIN; // 0xFE req.r.rtm_protocol = RTPROT_STATIC; //0x04 req.r.rtm_scope = RT_SCOPE_UNIVERSE; // 0x00 req.r.rtm_type = RTN_UNICAST; // 0x01 req.r.rtm_flags = 0; addattr_l(&req.n, sizeof(req), RTA_DST, &src_addr, sizeof(struct in_addr)); addattr_l(&req.n, sizeof(req), RTA_GATEWAY, &dst_addr, sizeof(struct in_addr)); uint32_t oif_idx = index; addattr32(&req.n, sizeof(req), RTA_OIF, oif_idx); struct iovec iov = {&req, req.n.nlmsg_len }; nl_talk_iov(fd, &iov); return 1; } int main(int argc, char *argv[]) { char *cmd_ip_str; char *cmd_via_ip_str; char *dev; cmd_ip_str = argv[1]; cmd_via_ip_str = argv[2]; uint32_t index = if_nametoindex(dev); struct in_addr add_v4prefix; struct in_addr via_v4prefix; inet_pton(AF_INET, cmd_ip_str, &add_v4prefix); inet_pton(AF_INET, cmd_via_ip_str, &via_v4prefix); ipv4_route_add(add_v4prefix, via_v4prefix, index); }
libnetlink.h
#pragma once #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <stddef.h> #include <stdbool.h> #include <unistd.h> #include <sys/socket.h> #include <asm/types.h> #include <arpa/inet.h> #include <netinet/in.h> #include <linux/netlink.h> #include <linux/rtnetlink.h> #include <sys/ioctl.h> #include <sys/select.h> #define NLMSG_TAIL(nmsg) \ ((struct rtattr *) (((void *) (nmsg)) + NLMSG_ALIGN((nmsg)->nlmsg_len))) int addattr_l(struct nlmsghdr *n, int maxlen, int type, const void *data, int alen); int addattr32(struct nlmsghdr *n, int maxlen, int type, __u32 data); int nl_talk_iov(int fd, struct iovec *iov); void hexdump(const void *buffer, size_t bufferlen);
libnetlink.c
#include "libnetlink.h" void hexdump(const void *buffer, size_t bufferlen) { const unsigned char *data = (const unsigned char *)(buffer); for (size_t i = 0; i < bufferlen; i++) { if (i % 4 == 0) { printf(" "); } if (i % 16 == 0) { printf(" \n"); } printf("%02X", data[i]); } } int addattr_l(struct nlmsghdr *n, int maxlen, int type, const void *data, int alen) { int len = RTA_LENGTH(alen); struct rtattr *rta; if (NLMSG_ALIGN(n->nlmsg_len) + RTA_ALIGN(len) > maxlen) { fprintf(stderr, "addattr_l ERROR: message exceeded bound of %d\n", maxlen); return -1; } rta = NLMSG_TAIL(n); rta->rta_type = type; rta->rta_len = len; if (alen) memcpy(RTA_DATA(rta), data, alen); n->nlmsg_len = NLMSG_ALIGN(n->nlmsg_len) + RTA_ALIGN(len); return 0; } int addattr32(struct nlmsghdr *n, int maxlen, int type, __u32 data) { return addattr_l(n, maxlen, type, &data, sizeof(__u32)); } int nl_talk_iov(int fd, struct iovec *iov) { struct sockaddr_nl nladdr = { .nl_family = AF_NETLINK }; struct msghdr msg = { .msg_name = &nladdr, .msg_namelen = sizeof(nladdr), .msg_iov = iov, .msg_iovlen = 1, }; sendmsg(fd, &msg, 0); char recv_buf[MAX_RECV_BUF_LEN]; struct iovec riov = { .iov_base = &recv_buf, .iov_len = MAX_RECV_BUF_LEN}; msg.msg_iov = &riov; msg.msg_iovlen = 1; int recv_len = recvmsg(fd, &msg, 0); if (recv_len < 0) { perror("Receiving message failed"); } struct nlmsghdr *rnh; for (rnh = (struct nlmsghdr *)recv_buf; NLMSG_OK(rnh, recv_len); rnh = NLMSG_NEXT(rnh, recv_len)) { if (rnh->nlmsg_type == NLMSG_ERROR) { struct nlmsgerr *errmsg; errmsg = NLMSG_DATA(rnh); printf("%d, %s\n", errmsg->error, strerror(-errmsg->error)); break; } } return 1; }
これでNetlinkにおける経路追加の方法や基本的な概要、どうやってメッセージを作っていくのかが理解出来たと思います。 ここからは応用として、iproute2を使用したdebug方法やNetlinkのバイナリの読み方、またNetlink関係のモジュールを使用したNetlink メッセージの作成方法を記述します。
iproute2のdebug
iproute2はrtnetlinkの機能を使用したNetlinkコードの宝庫です。iproute2のソースコードにはNetlinkコードの捌き方やどうメッセージを構築しているかなどをみることが出来ます。 またiproute2で構築されるメッセージをバイナリで読む方法があるので紹介します。
iproute2の送信部分は__rtnl_talk_iov()という関数からNetlinkメッセージのやり取りが行われています。
struct sockaddr_nl nladdr = { .nl_family = AF_NETLINK }; struct iovec riov; struct msghdr msg = { .msg_name = &nladdr, .msg_namelen = sizeof(nladdr), .msg_iov = iov, .msg_iovlen = iovlen, }; unsigned int seq = 0; struct nlmsghdr *h; int i, status; char *buf;
一部抜粋ですが、この関数は出来上がったNetlink メッセージを最後に送信する部分の関数として機能しているため、msghdrを見ればiproute2内でどんなメッセージが構築されているのかが見ることが可能になります。 バイナリとして出力するために、hexdump()関数を作成します。
void hexdump(FILE* fp, const void *buffer, size_t bufferlen) { const uint8_t *data = (const uint8_t*)(buffer); size_t row = 0; while (bufferlen > 0) { fprintf(fp, "%04zx: ", row); size_t n; if (bufferlen < 16) n = bufferlen; else n = 16; for (size_t i = 0; i < n; i++) { if (i == 8) fprintf(fp, " "); fprintf(fp, " %02x", data[i]); } for (size_t i = n; i < 16; i++) { fprintf(fp, " "); } fprintf(fp, " "); for (size_t i = 0; i < n; i++) { if (i == 8) fprintf(fp, " "); uint8_t c = data[i]; if (!(0x20 <= c && c <= 0x7e)) c = '.'; fprintf(fp, "%c", c); } fprintf(fp, "\n"); bufferlen -= n; data += n; row += n; } }
メッセージをvoid型で受け取った後に、uint8_tでもunsigned charでもなんでも良いですが扱いやすい型にキャストをします。 それで見やすいように色々と処理を行なって、__rtnl_talk_iov()の関数内にこのhexdump()を記述します。
struct sockaddr_nl nladdr = { .nl_family = AF_NETLINK }; struct iovec riov; struct msghdr msg = { .msg_name = &nladdr, .msg_namelen = sizeof(nladdr), .msg_iov = iov, .msg_iovlen = iovlen, }; unsigned int seq = 0; struct nlmsghdr *h; int i, status; char *buf; hexdump(stdout, &msg, 1000);
試しに、iproute2をbuildしてip r add 1.1.1.1 via 2.2.2.2
と打ち込んでみます
また、iproute2のdebug方法はとても簡単で、iproute2のGithubリポジトリからgit cloneしてmakeするだけです。
root@ubuntu10:/home/ubuntu/iproute2# ip/ip r add 1.1.1.1 via 2.2.2.2 0010: 08 53 b0 c7 ff ff 00 00 01 00 00 00 00 00 00 00 .S...... ........ ....
成功するとこういった感じで、hexdump関数が働いているのが見えると思います。 ではここから250番目のバイナリを見てみます。
0240: 00 00 00 00 00 00 00 00 2c 00 00 00 18 00 01 06 ........ ,....... 0250: 00 00 00 00 00 00 00 00 02 20 00 00 fe 03 00 01 ........ . ...... 0260: 00 00 00 00 08 00 01 00 01 01 01 01 08 00 05 00 ........ ........ 0270: 02 02 02 02 00 00 00 00 00 00 00 00 00 00 00 00 ........ ........
それっぽいバイナリがあるのが理解出来ますね。こういったバイナリ読みの部分はまず読める部分から読んでいきます。
読み大会開始
0260番目の0x01 0x01 0x01 0x01
となっているわかりやすいバイナリがありますね。
今回試したipコマンドは経路の宛先に1.1.1.1
と記述しました。
もう気づいたかもしれませんが、このバイナリの部分がRTA_DSTの部分となります。
よく前のバイナリを見てみると、0x08 0x00 0x01 0x00
の部分がありますね。
では、RTA_DSTの値を見てみます。
enum rtattr_type_t { RTA_UNSPEC, RTA_DST, RTA_SRC,
RTA_DSTはrtattr_type_tの列挙型であり、その値は1になります。 またNetlinkではrtattrのrta_typeを指定するフィールドではunsigned shortとなっており、大きさは16bitです。 なので2byte分のデータが入る事になります。またrta_lenも同じくunsigned shortで2byte分のデータを入れる事になります。
そうした事を踏まえると
0x08 0x00 0x01 0x00 0x01 0x01 0x01 0x01 |--len--| |--type--| |--RTA_DST data--|
と解釈することが出来ます。
また、次に続くRTA_GATEWAYも同じように解釈する事が出来ますね。
0x08 0x00 0x05 0x00 0x02 0x02 0x02 0x02 |--len--| |--type--| |--RTA_GATEWAY data--|
このようにNetlinkメッセージをバイナリにして読むのですが、少し他の部分も見てみましょう。
0240: 00 00 00 00 00 00 00 00 2c 00 00 00 18 00 01 06 ........ ,....... 0250: 00 00 00 00 00 00 00 00 02 20 00 00 fe 03 00 01 ........ . ......
この部分はnlmsghdrとrtmsgを定義している場所です。 例えば0240に0x18がありますが、この値はnlmsghdrでnlmsg_typeを定義する場所であり、経路追加するためのnlmsg_type RTM_NEWROUTEになります。
24と定義されていますが、16進数に変換すると0x18になるのでこの部分がnlmsg_typeの領域だと分かります。 またここでは一部を紹介していますが、バイナリを見ていくと前章で行なったメッセージ作成通りになっているのが確認出来ると思います。
Netlinkメッセージをパケットキャプチャしよう
ここまでバイナリを読んだりしていますが、割とエスパーの世界だと感じる事もあると思います。 hexdumpを仕込んでメッセージを見るのも良いですが世の中にはNetlinkに関する大変便利なカーネルモジュールが存在します。
このnlmonというカーネルモジュールはソケット通信を通じて流れるNetlink メッセージだけを取得するインターフェースで、libpcap系のツール(wiresharkやtshark)などを使用するとなんとNetlink メッセージをパケットキャプチャ風に見る事が出来ます。
obj-m := test.o KDIR := /lib/modules/$(shell uname -r)/build VERBOSE = 0 all: $(MAKE) -C $(KDIR) M=$(PWD) KBUILD_VERBOSE=$(VERBOSE) CONFIG_DEBUG_INFO=y modules
カーネルモジュールをビルドする時のいつもの魔法です。 またビルド出来たら以下の様にします。
insmod test.ko ip link add nlmon0 type nlmon ip link set nlmon0 up tshark -i nlmon0 -V -x
では早速、ipコマンドを実行してtsharkで見てみます。
Running as user "root" and group "root". This could be dangerous. Capturing on 'nlmon0' Frame 1: 60 bytes on wire (480 bits), 60 bytes captured (480 bits) on interface nlmon0, id 0 Interface id: 0 (nlmon0) Interface name: nlmon0 Encapsulation type: Linux Netlink (158) Arrival Time: Nov 23, 2022 17:05:03.552239744 JST [Time shift for this packet: 0.000000000 seconds] Epoch Time: 1669190703.552239744 seconds [Time delta from previous captured frame: 0.000000000 seconds] [Time delta from previous displayed frame: 0.000000000 seconds] [Time since reference or first frame: 0.000000000 seconds] Frame Number: 1 Frame Length: 60 bytes (480 bits) Capture Length: 60 bytes (480 bits) [Frame is marked: False] [Frame is ignored: False] [Protocols in frame: netlink:netlink-route] Linux netlink (cooked header) Link-layer address type: Netlink (824) Family: Route (0x0000) Linux rtnetlink (route netlink) protocol Netlink message header (type: Add network route) Length: 44 Message type: Add network route (24) Flags: 0x0605 .... .... .... ...1 = Request: 1 .... .... .... ..0. = Multipart message: 0 .... .... .... .1.. = Ack: 1 .... .... .... 0... = Echo: 0 .... .... ...0 .... = Dump inconsistent: 0 .... .... ..0. .... = Dump filtered: 0 .... ...0 .... .... = Specify tree root: 0 .... ..1. .... .... = Return all matching: 1 .... .1.. .... .... = Atomic: 1 Flags: 0x0605 .... .... .... ...1 = Request: 1 .... .... .... ..0. = Multipart message: 0 .... .... .... .1.. = Ack: 1 .... .... .... 0... = Echo: 0 .... .... ...0 .... = Dump inconsistent: 0 .... .... ..0. .... = Dump filtered: 0 .... ...0 .... .... = Replace: 0 .... ..1. .... .... = Excl: 1 .... .1.. .... .... = Create: 1 .... 0... .... .... = Append: 0 Sequence: 1669190704 Port ID: 0 Address family: AF_INET (2) Length of destination: 32 Length of source: 0 TOS filter: 0x00 Routing table ID: 254 Routing protocol: boot (0x03) Route origin: global route (0x00) Route type: Gateway or direct route (0x01) Route flags: 0x00000000 Attribute: Route destination address Len: 8 Type: 0x0001, Route destination address (1) 0... .... .... .... = Nested: False .0.. .... .... .... = Network byte order: False Attribute type: Route destination address (1) Data: 01010101 Attribute: Gateway of the route Len: 8 Type: 0x0005, Gateway of the route (5) 0... .... .... .... = Nested: False .0.. .... .... .... = Network byte order: False Attribute type: Gateway of the route (5) Data: 02020202
綺麗にパケットキャプチャ風にNetlink メッセージを取得出来ていますね。 またnlmonはNetlinkメッセージなら全てをリアルタイムで取得する事が出来るので、自作のNetlink メッセージを流す時やiproute2のコマンドと見比べたい時に非常に便利なカーネルモジュールです。
これから何をするか
次は何をすれば良いかを記述します。
Netlinkシステムの改善やツールの作成(参考にptraceを用いたNetlink msg trace tool : GitHub - socketpair/nltrace: nltrace )
iproute2を見ながらencapが行われる特殊な経路追加にチャレンジしてみる(Segment Routingなど)
この辺にチャレンジしてみると、カーネルの事を知りながらiproute2で行われているrtnetlinkの全貌が見えてくるはずです。
締め
ここまで読んでくださりありがとうございました。Netlinkの仕組みやメッセージの作成方法などをつらつらと説明していきました。 割とネットワークプログラミングの感覚で構築が出来たりするのが楽しい所です。 まだまだ自分自身もLinux network dplaneに足を突っ込んだばかりの素人ですが、これからも色んな事をやってみようと考えています。