要在linux下的程序中获取gateway的IP地址,使用netlink是一种直接、可靠的方法,不需要依赖其它命令,直接从linux内核获取信息,netlink编程的中文资料很少,本文试图用尽可能简单的方式讨论使用netlink获取gataway的IP地址的编程方法,并有大量篇幅介绍netlink的相关数据结构和编程方法,假定阅读本文的读者对linux下编程有一定了解,具备一定的C语言能力,熟悉IPv4的socket编程。
在linux编程的资料中,netlink编程的资料并不多,但netlink编程显然是本文无法越过的一道坎,所以下面需要用一定篇幅对netlink编程做个介绍;本文的最终目标是使用netlink这种与linux内核通信的机制,从内核获得路由表并从中找到gateway的IP地址。在具体实践中,获取路由表或者获取gateway的IP地址通常并不需要使用netlink编程实现,这种方法对应用层编程来说显得有些繁琐,本文主要还是作为netlink编程的一个范例,并以此为题介绍一些netlink的编程方法;有关其它获取gateway的IP地址的方法,请参见另一篇文章《从proc文件系统中获取gateway的IP地址》。
1. netlink socket及netlink消息结构
netlink socket
- netlink是一个socket,所以它的编程与普通的socket编程类似,其socket的创建方法如下:
sock = socket(AF_NETLINK, SOCK_DGRAM, NETLINK_ROUTE);
- 第一个参数可以是AF_NETLINK或者PF_NETLINK,表示要创建一个NETLINK socket,其实AF_NETLINK和PF_NETLINK是一样的,可以到
bits/socket.h
中查看相应的定义; - 第二个参数可以是SOCK_RAW或者SOCK_DGRAM,linux内核的netlink子系统并不会区分SOCK_RAW和SOCK_DGRAM(内核5.15,也许更高的版本会区分),所以使用SOCK_RAW和SOCK_DGRAM是一样的;
- 第三个参数是NETLINK协议,这些协议定义在文件
linux/netlink.h
中,在linux 5.15中定义了23种协议,协议数量最多为32个#define NETLINK_ROUTE 0 /* Routing/device hook */ #define NETLINK_UNUSED 1 /* Unused number */ #define NETLINK_USERSOCK 2 /* Reserved for user mode socket protocols */ #define NETLINK_FIREWALL 3 /* Unused number, formerly ip_queue */ #define NETLINK_SOCK_DIAG 4 /* socket monitoring */ #define NETLINK_NFLOG 5 /* netfilter/iptables ULOG */ #define NETLINK_XFRM 6 /* ipsec */ #define NETLINK_SELINUX 7 /* SELinux event notifications */ #define NETLINK_ISCSI 8 /* Open-iSCSI */ #define NETLINK_AUDIT 9 /* auditing */ #define NETLINK_FIB_LOOKUP 10 #define NETLINK_CONNECTOR 11 #define NETLINK_NETFILTER 12 /* netfilter subsystem */ #define NETLINK_IP6_FW 13 #define NETLINK_DNRTMSG 14 /* DECnet routing messages */ #define NETLINK_KOBJECT_UEVENT 15 /* Kernel messages to userspace */ #define NETLINK_GENERIC 16 /* leave room for NETLINK_DM (DM Events) */ #define NETLINK_SCSITRANSPORT 18 /* SCSI Transports */ #define NETLINK_ECRYPTFS 19 #define NETLINK_RDMA 20 #define NETLINK_CRYPTO 21 /* Crypto layer */ #define NETLINK_SMC 22 /* SMC monitoring */
- 尽管定义的协议很多,但常用的协议并不多,本例中仅使用NETLINK_ROUTE;
- 创建netlink socket,netlink协议使用NETLINK_ROUTE时,创建的socket又被称为rtnetlink,可以使用在线手册
man 7 rtnetlink
了解更详细的信息,本文仅讨论rtnetlink,以下将尽可能使用rtnetlink代替netlink socket。
netlink socket本质上是进程间的通信,一个用户进程使用netlink socket不仅可以和内核进程进行通信,两个(多个)用户进程间也可以使用netlink socket进行通信;
- netlink是一个socket,所以它的编程与普通的socket编程类似,其socket的创建方法如下:
netlink除了使用内核定义的协议外,也可以使用自定义协议,在上面定义的netlink协议中的NETLINK_GENERIC就是用于用户自定义协议的,我们可以编写一个内核进程,然后在用户进程中使用NETLINK_GENERIC进行通信;
netlink socket有一个多播的特性,所谓多播,就是一个进程发出的消息可以有多个其它进程接收,netlink允许最多32个多播组,当向一个多播组发出消息时,所有已经加入这个多播组的进程都可以收到这个消息。
当使用rtnetlink与内核进行通信时,用户进程需要先向内核进程发出一个请求,然后接收内核进程返回的消息,从而实现与内核的通信;
netlink socket可以使用send、sendto、sendmsg发送消息,使用recv、recvfrom、recvmsg接收消息,这些和IPv4 socket是一样的;
netlink消息结构
netlink消息是由一个或多个netlink消息头和与其相关的payload组成的一个字节流
- netlink报头的结构为(struct nlmsghdr),在头文件
<linux/netlink.h>
中定义,在下一节中会介绍了这个结构; - 这个字节流只能使用一组标准的以NLMSG_开头的宏进行存取(netlink的手册中是这样说的),这组宏定义在头文件
<linux/netlink.h>
中,可以使用在线手册man 3 netlink
了解更多的信息,本文在后面章节也会简要介绍这些宏; - payload对不同的netlink协议和信息类型而言,其信息结构和长度都是不一样的,本文仅讨论NETLINK_ROUTE协议下的信息类型为RTM_GETROUTE下的消息结构,这个类型的payload结构为(struct rtmsg),定义在头文件
<linux/rtnetlink.h>
中,这个结构也会在下一节介绍;详细信息可以查阅man 7 rtnetlink
; - 使用rtnetlink与内核通信时,需要首先建立一个rtnetlink,然后向这个rtnetlink发送一个请求消息,请求中说明想要的操作,然后从这个rtnetlink上接收内核的回应,解析回应信息获得结果;
- 在多部分消息中(一个字节流中包含多个netlink报头和相关payload),第一个报头和后面所有的报头都会设置NLM_F_MULTI标志,最后一个报头的类型为NLMSG_DONE,表示多部分消息结束;有关NLM_F_MULTI和NLMSG_DONE的含义,下一节会做介绍。
2. rtnetlink常用数据结构
netlink编程涉及的各种结构非常多,这里仅介绍几个与本文讨论的话题相关的结构,但仍然会占很大的篇幅,这几个结构在第5、6节会大量用到,所以必须先介绍一下,可以先大致看一下,在阅读相应章节遇到具体结构时再回来仔细看。
struct sockaddr_nl - 定义在头文件
<linux/netlink.h>
中struct sockaddr_nl { __kernel_sa_family_t nl_family; /* AF_NETLINK */ unsigned short nl_pad; /* zero */ __u32 nl_pid; /* port ID */ __u32 nl_groups; /* multicast groups mask */ };
- 这个结构与IPv4 socket编程中的(struct sockaddr_in)的作用类似,(struct sockaddr_in)表示一个IPv4地址,这个结构表示一个netlink地址
- nl_family字段与(struct sockaddr_in)中的sa_family一样,只是这里要填AF_NETLINK
- nl_pad目前没有使用,填0即可;
- nl_pid是当前进程的ID,可以使用getpid()获得;
- nl_groups表示加入到那个多播组中,前面说过netlink最多允许32个多播组,这个字段每个bit代表一个多播组,为1表示加入这个多播组。
struct nlmsghdr - 定义在头文件
<linux/netlink.h>
中- 这个结构是netlink报头的定义
struct nlmsghdr { __u32 nlmsg_len; /* Length of message including header */ __u16 nlmsg_type; /* Message content */ __u16 nlmsg_flags; /* Additional flags */ __u32 nlmsg_seq; /* Sequence number */ __u32 nlmsg_pid; /* Sending process port ID */ };
- nlmsg_len: netlink报文的长度,按4字节对齐;包括(struct nlmsghdr)的长度和后面payload的长度;
nlmsgtype: netlink报文的类型,不同的类型对应的netlink报文的结构也会不同,这些类型定义在头文件
<linux/rtnetlink.h>
中开头为"RTM"的常数,本例中在发送netlink请求时类型为:RTM_GETROUTE,含义为从内核获取路由表,常用的常数有:NLMSG_NOOP 1 无用,可忽略 NLMSG_ERROR 2 出现错误 NLMSG_DONE 3 数据输出结束 NLMSG_OVERRUN 4 数据丢失 RTM_NEWROUTE 24 新路由 RTM_GETROUTE 26 从内核中获取路由表,发送netlink包请求时填写
nl_flags: 附加标志,在
<linux/netlink.h>
中定义,每一位代表一个标志,与本文相关的定义如下:NLM_F_REQUEST 0x01 这是一个请求信息 NLM_F_MULTI 0x02 分片信息包的一部分,直到收到NLMSG_DONE结束 NLM_F_ROOT 0x100 整体返回数据,而不是一条一条返回 NLM_F_MATCH 0x200 返回所有相符的数据 NLM_F_DUMP (NLM_F_ROOT|NLM_F_MATCH)
- nlmsg_seq: 序列号,信息的唯一性编号,可以自行编号,只要保证唯一性即可,通常可以使用时间戳;
- nlmsg_pid: 发送方的pid,用于识别信息的归属,可以使用getpid()获得;
- 所有netlink报文的报头都是这个结构。
- 这个结构是netlink报头的定义
struct rtmsg - 定义在头文件linux/rtnetlink.h中
这个结构通常是紧跟在(struct nlmsghdr)后面的,当(struct nlmsghdr)中的nlmsg_type为 RTM_NEWROUTE、RTM_DELROUTE和RTM_GETROUTE时,(struct nlmsghdr)后面跟的数据才符合下面的结构,当nlmsg_type为其他值时,(struct nlmsghdr)后面跟的数据结构是不同的;
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 */ unsigned char rtm_scope; /* See below */ unsigned char rtm_type; /* See below */ unsigned int rtm_flags; };
- 以下仅讨论当(struct nlmsghdr)中的nlmsg_type为RTM_GETROUTE时(与本文例子相符),该结构的情况
- 将rtm_dst_len和rtm_src_len设置为0表示想要获取指定路由表(routing table)中的所有项;
- 在发送netlink请求时,只需把rtm_family设为AF_INET,其它全部为0即可;
- 内核发回的回应中,rtm_dst_len、rtm_src_len、rtm_tos均没有意义可以不用管;
- rtm_table表示当前路由表的类型,在linux/rtnetlink.h中有定义:
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 };
- RT_TABLE_UNSPEC表示是一个不明路由表;RT_TABLE_DEFAULT表示是一个默认路由表;RT_TABLE_MAIN表示是一个主路由表;RT_TABLE_LOCAL表示是一个本地路由表;在本例中,我们要得到的就是一个主路由表(RT_TABLE_MAIN)
- rtm_protocol是路由协议,在
<linux/rtnetlink.h>
中有定义:#define RTPROT_UNSPEC 0 不明 #define RTPROT_REDIRECT 1 当前的IPv4下没有使用 #define RTPROT_KERNEL 2 路由由内核设置 #define RTPROT_BOOT 3 路由在启动时设置 #define RTPROT_STATIC 4 路由由管理员设置
- 在头文件
<linux/rtnetlink.h>
中专门有说明,当 rtm_protocol>RTPROT_STATIC 时,Linux内核将不予理会,只能作为用户信息;在本例中,我们得到的路由表应该是由内核或者管理员设置的,当然也可以是在启动中由某个启动例程设置,所以 RTPROT_KERNEL、RTPROT_BOOT或者RTPROT_STATIC 都是可能的; - rtm_scope的值也是定义在
<linux/rtnetlink.h>
中,在这个头文件中说rtm_scope更像是一个到达目的地址的距离,它有下面几个可能的值:enum rt_scope_t { RT_SCOPE_UNIVERSE=0, /* global route */ /* User defined values */ RT_SCOPE_SITE=200, /* interior route in the local autonomous system */ RT_SCOPE_LINK=253, /* route on this link */ RT_SCOPE_HOST=254, /* route on the local host */ RT_SCOPE_NOWHERE=255 /* destination doesn't exist */ };
- 其实这个值没有什么意义,本例中会返回RT_SCOPE_UNIVERSE,表示是一个全球路由;
- rtm_type表示当前路由的类型(rtm_table表示路由表类型,和这个字段是不同的),在
<linux/rtnetlink.h>
中定义: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 };
- 本例中,我们要获取的是gateway IP,所以内核返回的rtm_type为1(RTN_UNICAST)
- rtm_flags在本例中没有作用,其可能的值在
<linux/rtnetlink.h>
中定义:#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 */
struct rtattr - 定义在头文件
<linux/rtnetlink.h>
中- 一个或多个(struct rtattr) + data将跟在(struct rtmsg)后面,data里表达着一个路由中的一项属性;
struct rtattr { unsigned short rta_len; unsigned short rta_type; };
- rta_len字段表示(struct rtattr) + data的总长度,rta_type表示data中的数据类型,在
<linux/rtnetlink.h>
中定义:enum rtattr_type_t { RTA_UNSPEC, RTA_DST, RTA_SRC, RTA_IIF, RTA_OIF, RTA_GATEWAY, RTA_PRIORITY, RTA_PREFSRC, ...... RTA_TABLE, ...... __RTA_MAX };
- 本例中只会用到前面几个定义,如果需要查看全部的rta_type的值,可以去
<linux/rtnetlink.h>
中去查找,以下仅就本文中用到的值做一下解释; - RTA_UNSPEC 表示data数据可以忽略;
- RTA_DST 表示data中的数据为一个目的地址IP,data中数据结构为(struct in_addr),一个32位16进制数字表示的IP地址;
- RTA_SRC 表示data中的数据为一个源地址IP,data中数据结构为(struct in_addr),一个32位16进制数字表示的IP地址;
- RTA_IIF 表示data中的数据为输入接口索引(Input Interface Index),data中数据为一个int;
- RTA_OIF 表示data中的数据为输出接口索引(Onput Interface Index),data中数据为一个int;
- RTA_GATEWAY 表示data中的数据为gateway的IP地址,data中数据结构为(struct in_addr),一个32位16进制数字表示的IP地址;
- RTA_PRIORITY 表示data中的数据为路由优先级,data中数据为一个int;
- RTA_PREFSRC 表示data中的数据为一个优先的源地址IP,data中数据结构为(struct in_addr),一个32位16进制数字表示的IP地址;
- RTA_TABLE 表示data中的数据为路由表的ID,data中数据为一个int;
- 其实RTA_SRC和RTA_PREFSRC到底区别在哪里我也不知道,我以为会返回RTA_SRC,但在我的机器上实际返回的是RTA_PREFSRC;
- 本例中我们会遇到的rta_type有:RTA_DST、RTA_OIF、RTA_GATEWAY、RTA_PRIORITY、RTA_PREFSRC和RTA_TABLE。
- 一个或多个(struct rtattr) + data将跟在(struct rtmsg)后面,data里表达着一个路由中的一项属性;
3. netlink编程中常用的宏定义
按照netlink手册中的要求,对(struct nlmsghdr)的访问要使用一组标准的宏来完成,本节将简单介绍这组宏,也可以使用在线手册
man 3 netlink
来更多地了解这组宏;这些宏定义在头文件<linux/netlink.h>
中,必要时可以查看代码理解其意义。这些宏这样看上去会很枯燥,但在第5、6节和源代码中均会大量出现,可以先大致看一下,等看到相关章节遇到具体的宏时在回来仔细阅读。
NLMSG_ALIGN(len)
- 返回len值按照4字节对齐后的值,比如len=5,则返回8;len=17,返回20;
- 将len按照适当的值进行字节对齐;所谓适当的值,指的是宏NLMSG_ALIGNTO定义的值,目前宏NLMSG_ALIGNTO为4。
NLMSG_LENGTH(len)
- 返回(struct nlmsghdr)按4字节对齐的长度 + len的值;
- 如果len是payload的长度,该宏返回的值应该和存放在(struct nlmsghdr)中的nlmsg_len字段的值一样
NLMSG_SPACE(len)
- 计算(struct nlmsghdr)按4字节对齐的长度 + len的值,返回这个值按4字节对齐后的结果;
- 返回payload长度为len的netlink消息占用的字节数。
NLMSG_DATA(nlh)
- 假定nlh为一个指向(struct nlmsghdr)的指针,该宏将返回(struct nlmsghdr)后面payload的指针。
NLMSG_NEXT(nlh, len)
- 在多部分消息中,收到的报文中会有多个(struct nlmsghdr) + payload,nlh是指向当前(struct nlmsghdr)的指针,len是从nlh开始到报文结束的总字节数,该宏返回指向下一个(struct nlmsghdr)的指针,同时修改len为返回的(struct nlmsghdr)指针到报文结束的总字符数。
- 应用程序在调用前需要自行检查当前的(struct nlmsghdr)是否设置了NLMSG_DONE,该宏不会检查该标志并返回NULL。
NLMSG_OK(nlh, len)
- 当nlh指向的(struct nlmsghdr)完好时会返回true,否则返回false;
- nlh指向当前的(struct nlmsghdr),len是从nlh开始到报文结束的总字节数;
- (struct nlmsghdr)中有一个字段为nlmsg_len,该字段表示(struct nlmsghdr)和后面数据的总长度,
- 该宏要求符合下面三个逻辑:
- len > (struct nlmsghdr)的长度
- (nlmsghdr).nlmsg_len > (struct nlmsghdr)的长度
- (nlmsghdr).nlmsg_len < len
NLMSG_PAYLOAD(nlh, len)
- 返回data的长度;
- 假定nlh指向一个(struct nlmsghdr)的指针,len为data在payload部分的偏移;
- (struct nlmsghdr)中有一个字段为nlmsg_len,该字段表示(struct nlmsghdr)和后面数据的总长度;
- 该宏返回nlmsg_len - (struct nlmsghdr)长度 - len的值;
- 举例来说,当len=0时,该宏将返回payload的长度,这也是这个宏的一种常用的方式。
4. rtnetlink编程中常用的宏
在头文件
<linux/rtnetlink.h>
中定义了一组操作(struct rtmsg)的宏,以RTM开头,只有两个;还定义了一组操作(struct rtattr)的宏,以RTA开头,在这里简单介绍一下;使用在线手册man rtnetlink
可以了解关于操作(struct rtattr)的宏的更详细的信息。这些宏这样看上去会很枯燥,但在第5、6节和源代码中均会大量出现,可以先大致看一下,等看到相关章节遇到具体的宏时在回来仔细阅读。
RTM_RTA(r)
- 返回在r后面指向(struct rtattr)的指针;
- r为指向(struct rtmsg)的指针;
- 该宏认为(struct rtmsg)后面就是(struct rtattr);
RTM_PAYLOAD(n)
- 返回(struct rtmsg)后面的payload的长度
- n为一个指向(struct nlmsghdr)的指针
- 该宏认为报文的结构为(struct nlmsghdr) + (struct rtmsg) + payload
- 该宏认为(struct nlmsghdr)中的nlmsg_len为当前报文的总长度,减去(struct nlmsghdr)的长度,再减去(struct rtmsg)的长度就是payload的长度。
RTA_OK(rta, len)
- 当rta指向的(struct rtattr)完好时返回true,否则返回false;
- rta指向当前(struct rtattr)的指针,len为(struct rtattr) + data的字节数,通常情况下,len不能为0;
- 该宏要求符合下面三个逻辑:
- len >= (struct rtattr)的长度
- (rta).rta_len >= (struct nlmsghdr)的长度
- (rta).rta_len <= len
RTA_DATA(rta)
- 返回一个指向data的指针;rat为一个指向(struct rtattr)的指针;
RTA_PAYLOAD(rta)
- 返回data的长度;rat为一个指向(struct rtattr)的指针;
RTA_NEXT(rta, attrlen)
- 返回rta后面的下一个(struct rtattr)的指针;rta指向当前(struct rtattr),attrlen为所有剩余(struct rtattr) + data的长度,初始值应该是所有(struct rtattr) + data的总长度;
RTA_LENGTH(len)
- 返回(struct rtattr)按4字节对齐的长度 + len的值;
- 当len为data的长度,该宏返回的值应该和(struct rtattr)中的rta_len一样;
RTA_SPACE(len)
- 计算(struct rtattr)按4字节对齐的长度 + len的值,返回这个值按4字节对齐后的结果;
- 返回data长度为len的(struct rtattr) + data占用的字节数。
5. 使用rtnetlink发送请求
前面讨论了一堆预备知识后,现在终于可以进行实际操作了;
- 建立一个rtnetlink
int nl_sock; nl_sock = socket(AF_NETLINK, SOCK_DGRAM, NETLINK_ROUTE);
构建netlink请求报文
请求消息的结构比较简单,就两个结构:(struct nlmsghdr) + (struct rtmsg)
下面代码构建了一个请求报文
struct nlmsghdr *nl_msg_hdr; struct rtmsg *rt_msg; char *msg_buf; int msg_buf_len; msg_buf_len = NLMSG_SPACE(sizeof(struct rtmsg)); msg_buf = malloc(msg_buf_len); memset(msg_buf, 0, msg_buf_len); nl_msg_hdr = (struct nlmsghdr *)msg_buf; rt_msg = (struct rtmsg *)NLMSG_DATA(nl_msg_hdr); nl_msg_hdr->nlmsg_len = msg_buf_len; nl_msg_hdr->nlmsg_type = RTM_GETROUTE; nl_msg_hdr->nlmsg_flags = NLM_F_DUMP | NLM_F_REQUEST; nl_msg_hdr->nlmsg_seq = time(NULL); nl_msg_hdr->nlmsg_pid = getpid(); rt_msg->rtm_family = AF_INET;
- NLM_SPACE()是一个操作(struct nlmsghdr)的宏,前面有介绍;
- (struct nlmsghdr)中的nlmsg_type=RTM_GETROUTE,通知linux内核要获取路由表,这个字段在介绍(struct nlmsghdr)时有介绍;
- (struct nlmsghdr)中的nlmsg_flags是一个组合标志,在介绍(struct nlmsghdr)时有介绍;
- NLM_F_REQUEST表示这条报文是一条请求报文;
- NLM_F_DUMP是NLM_F_ROOT和NLM_F_MATCH的组合,表示需要所有符合条件的记录,所有记录一次性发送过来,而不是一条一条发;
- nlmsg_seq使用时间戳是一种常用的方法,也可以自行编号,但要保证唯一性;
- AF_INET表示只要IPv4的路由表。
发送netlink请求
send(nl_sock, nl_msg_hdr, nl_msg_hdr->nlmsg_len, 0);
6. 接收和解析内核返回的路由表
通常的做法是先把所有的报文接收下来,然后再去解析,接收数据的方法和在IPv4下使用socket接收数据的方法是一样的。
接收数据
接收数据之前并不知道有多少字节的数据需要接收,所以很难确定接收缓冲区的大小,所以最好是先检查一下socket上可以接收到多少字节的数据,然后给接收缓冲区分配内存,再去接收数据;
netlink返回的路由表的报文有点意思,路由表不会只有一条记录,所以这个返回的报文一定是个多部分消息,应该是由多个(struct nlmsghde) + payload组成,最后一个(struct nlmsghdr)中的nlmsg_type=NLMSG_DONE,表示整个路由表传送完毕,实际接收时发现要接收两次,第一次接收到的报文中,没有NLMSG_DONE消息,再接收一次,接收到的一个单独的NLMSG_DONE消息。
强调接收两次是因为我们首先要使用recv()测试一下有多少字节需要接收,然后为接收缓冲区分配内存,但是这个测试只能测试第一次要接收的报文长度,这个长度并不包括第二次接收时的NLMSG_DONE消息的长度,所以获得的字节数实际还要加上(struct nlmsghdr) + (struct rtmsg)的长度,最后加上的这部分就是NLMSG_DONE的消息长度,请看下面的代码。
char *buf_ptr, *p;
int buf_size, msg_len = 0, read_len;
buf_size = recv(nl_sock, NULL, 0, MSG_PEEK|MSG_TRUNC));
buf_size += NLMSG_SPACE(sizeof(struct rtmsg));
buf_ptr = malloc(buf_size);
memset(buf_ptr, 0, buf_size);
p = buf_ptr;
do {
read_len = recv(nl_sock, p, buf_size - msg_len, MSG_DONTWAIT);
if (read_len < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// TODO:
break;
}
// TODO:
} else if (read_len == 0) {
// TODO:
}
p += read_len;
msg_len += read_len;
} while (msg_len < buf_size && read_len > 0);
NLMSG_SPACE(struct rtmsg)
这个宏计算了NLMSG_DONE这个报文所需要的空间;- recv()函数中的 MSG_PEEK|MSG_TRUNC 是一种固定用法,可以预知下一个报文有多少字节可以读取,但是并不会把这个报文标志为已读;
- MSG_DONTWAIT使recv()执行时为无阻塞,按照
man recv
的说明,使用MSG_DONTWAIT时,如果recv()必须阻塞(意即无数据可读,需要等待在socket上),则会返回错误,errno为EAGAIN或者EWOULDBLOCK,所以对这两个错误码一定要做判断; - 实际读取数据时发现NLMSG_DONE报文中的(struct nlmsghdr)是完整的,但后面并没有完整的(struct rtmsg),而且其中并没有找到有效的信息,可以忽略。
解析路由表报文
路由表报文结构(多部分报文)
多部分报文中,每一部分有一个(struct nlmsghdr)和一个(struct rtmsg),在(struct rtmsg)后面有一个或多个(struct rtattr),在(struct rtarrt)后面跟着数据,如下图,我们把这样一块数据叫做多部分报文中的一部分;
- 多部分报文的每一部分表示一条记录,具体到路由表就是路由表中的每条记录将构成多部分报文的一部分;我们可以用命令
cat /proc/net/route
查看实际的路由表,这个路由表中有几条记录,那么linux内核就会返回几个部分,最后在加上一个NLMSG_DONE消息; - 在每个部分中,(struct nlmsghdr)中的nlmsg_len定义了这一部分的总长度;(struct rtattr)中rta_len定义了(struct rtattr) + data的长度,rta_type定义了data中是什么数据,具体可以参阅前面有关数据结构的介绍;
- 在每个部分中,(struct rtmsg)用来说明这条记录的特征,比如协议族,是IPv4还是IPv6;路由表是主路由表还是本地路由表;这条路由是内核设置的还是启动过程中设置的;等等,因为这些特征不能在(struct nlmsghdr)中表示,在(struct rtattr)中表示也不合适;
- 我们需要的数据其实在data中,我们来举一个实际的例子来说明(struct rtattr) + data;假定要表达一个gateway的IP地址为192.168.0.1,则收到的数据如下(按16进制显示):
08 05 c0 a8 00 01
- 其中08是rta_len,表示(struct rtattr) + data的长度为8个字节,rta_type是05,05是RTA_GATEWAY,表示data中的数据为gateway的IP地址,后面的4个字节组成了一个32位的IP地址,其实就是192.168.0.1,0xc0就是十进制的192,0xa8就是十进制的168
- 所以我们在解析报文时应该分成两步,第一步按照(struct nlmsghdr)分开,这样分开的每一部分是路由表的一条记录;第二步是在一个部分中解析出每个结构下每个字段的数据
下面这段程序完成了第一步,并将每部分的数据交给函数parse_message去完成第二步
struct nlmsghdr *nlmsg_hdr; int msg_buf_len; nlmsg_hdr = buf_ptr; msg_buf_len = msg_len; for (; NLMSG_OK(nlmsg_hdr, msg_buf_len); nlmsg_hdr = NLMSG_NEXT(nlmsg_hdr, msg_buf_len)) { if (nlmsg_hdr->nlmsg_type == NLMSG_DONE) { break; } parse_message(nlmsg_hdr); // TODO: }
- 这段程序中的几个宏:NLMSG_OK()、NLMSG_NEXT()和NLMSG_DONE在前面都有介绍;
下面这段程序,完成了上面这段程序中函数parse_message()的功能,将nlmsg_hdr指向多部分报文的其中一部分的(struct nlmsghdr),便可以解析出所有(struct rtattr)下的数据,以本文讨论的话题而言,我们只需要主路由表中gateway的IP,所以其中增加了IPv4和主路由表的判断;
struct rtmsg *rt_msg; struct rtattr *rt_attr; int rt_len; struct in_addr dst_addr; // destination IP address struct in_addr src_addr; // source IP address struct in_addr gateway; // gateway IP address char ifname[IF_NAMESIZE]; // network interface name rt_msg = (struct rtmsg *)NLMSG_DATA(nlmsg_hdr); if ((rt_msg->rtm_family != AF_INET) || (rt_msg->rtm_table != RT_TABLE_MAIN)) { // return if it is not IPv4 or not main routing table. return; } rt_attr = (struct rtattr *)RTM_RTA(rt_msg); rt_len = RTM_PAYLOAD(nlmsg_hdr); unsigned char *p; for (; RTA_OK(rt_attr, rt_len); rt_attr = RTA_NEXT(rt_attr, rt_len)) { switch (rt_attr->rta_type) { case RTA_OIF: // rta_data is index of network interface. converter it to ifterface name here. if_indextoname(*(int *)RTA_DATA(rt_attr), ifname); break; case RTA_GATEWAY: // rta_date is gateway ip in 32bits(struct in_addr). memcpy(&gateway, RTA_DATA(rt_attr), sizeof(gateway)); break; case RTA_PREFSRC: // Preferred source IP address in 32bits(struct in_addr) memcpy(&src_addr, RTA_DATA(rt_attr), sizeof(src_addr)); break; case RTA_DST: // Destination IP address in 32 bits(struct in_addr) memcpy(&dst_addr, RTA_DATA(rt_attr), sizeof(dst_addr)); break; case RTA_TABLE: // Routing table ID. break; case RTA_PRIORITY: // Priority of route. break; default: // Unknown routing attribute break; } }
- 上面这段程序中获得的IP地址都是存放在一个(struct in_addr)中,熟悉IPv4下socket编程的程序员应该了解这是什么。
7. 完整源代码
源代码文件名:get-gateway-netlink.c(点击文件名下载源程序),里面有详细的注释
编译:
gcc -Wall get-gataway-netlink.c -o get-gateway-netlink
- 运行:
./get-gateway-netlink
- 带调试信息运行:
./get-gateway-netlink 1
,可以打印出大量的中间过程信息,对程序的理解将有很大帮助。 - 这个程序的主要部分在前面都已经讨论过了;
- 这个程序其实是可以获取整个路由表的,但程序中做了过滤,一旦找到gateway的IP地址便不再解析下面的信息,所以这个程序稍加修改可以获取整个路由表;
- 获取gateway IP地址的方法不止本文介绍的这一种方法,其实我认为netlink的方法尽管看上去比较"高级",但也十分复杂和繁琐,最好的方法我认为是从proc文件系统中获取,想要了解这种方法的读者可以参考我的另一篇文章《从proc文件系统中获取gateway的IP地址》。
欢迎访问我的博客:whowin.cn
email: hengch@163.com