使用 RTNETLINK 操作网络环境
NETLINK 是 Linux 操作系统中的一种机制,允许用户空间应用程序与内核进行通信。NETLINK 是标准套接字实现的扩展。使用 NETLINK,应用程序可以向/从不同的内核特性(例如网络)发送/接收信息,以检查当前状态并对其进行控制。
在本文中,我将介绍程序员如何使用 NETLINK 的网络环境操作功能,即 RTNETLINK。我将讨论 RTNETLINK 的一些应用领域、相关的套接字操作、功能、RTNETLINK 消息的形成方式,并最后提供一组使用 RTNETLINK 的示例代码。用于 IP 版本 4 环境的 RTNETLINK 被称为 NETLINK_ROUTE,用于 IP 版本 6 环境的 RTNETLINK 被称为 NETLINK_ROUTE6。这里给出的解释适用于 IP 版本 4 和 6。
网络层协议处理程序的开发人员可以使用 RTNETLINK 来修改和监控网络的不同组件,例如路由表和网络接口。互联网工程任务组 (IETF) 有许多现有和即将到来的协议标准可以在用户空间中实现。这些实现将需要操作路由,并了解其他进程正在修改什么。以下是一些协议类别:
动态路由协议:此类协议,包括路由信息协议 (RIP)、开放最短路径优先 (OSPF) 和外部网关协议 (EGP),在与网络或互联网中其他同等能力的主机或路由器通信时,积极管理主机的路由环境。
移动性协议:移动主机在不同时间连接到不同网络时,使用移动 IP (MIP)、会话发起协议 (SIP) 和网络移动性 (NEMO) 等协议来管理路由,以保持连接性和通信的连续性。
Ad hoc 网络协议:移动主机位于没有网络基础设施(例如路由器和 WLAN 接入点)的地方,需要与配置不同的主机进行对等通信。地震灾区或其他此类紧急情况下的救援人员的移动计算机可以使用 ad hoc 网络协议。这些协议,例如 Ad hoc 按需距离矢量 (AODV) 和优化链路状态路由 (OLSR),需要管理路由以查找其他主机并使用邻近主机作为路由器和网关进行通信。
如果在用户空间中实现这些协议,则有助于降低内核代码的复杂性。此外,由于许多用户空间开发工具的可用性,它简化了这些协议的开发和测试。当测试或最终用户使用时,基于内核的代码可能出现内核崩溃等问题,但在用户空间协议处理程序中不会发生。
Linux 的套接字实现允许两个端点进行通信。套接字 API 提供了一组标准的函数和数据结构。对于 RTNETLINK,通信中的两个端点是用户空间和内核空间。通过 RTNETLINK 操作网络环境时,必须进行以下套接字调用序列:
打开套接字。
将套接字绑定到本地地址(使用进程 ID)。
向另一端点发送消息。
从另一端点接收消息。
关闭套接字。
socket() 函数打开一个未连接的端点以与内核通信。此调用的函数原型如下:
int socket(int domain, int type, int protocol);
domain 指的是正在使用的套接字类型。对于 RTNETLINK,我们使用 AF_NETLINK (PF_NETLINK)。type 指的是通信时使用的协议类型。这可以是原始 (SOCK_RAW) 或数据报 (SOCK_DGRAM)。这与 RTNETLINK 套接字无关,可以使用任何一种。protocol 指的是我们使用的确切 NETLINK 功能;在我们的例子中,它是 NETLINK_ROUTE。如果套接字打开成功,此函数将返回一个整数,其中包含一个称为套接字描述符的正数。此描述符将用于所有未来的 RTNETLINK 调用,直到套接字关闭。如果出现故障,则返回负值,并且 errno.h 中包含的系统错误变量 errno 将设置为相应的错误代码。
以下是打开 RTNETLINK 套接字的调用示例:
int fd; ... fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
一旦套接字打开,就必须将其绑定到本地地址。用户应用程序可以使用唯一的 32 位 ID 来标识本地地址。bind 的函数原型如下:
int bind(int fd, struct sockaddr *my_addr, socklen_t addrlen);
要绑定,调用者必须使用 sockaddr_nl 结构提供本地地址。linux/netlink.h #include 文件中的此结构具有以下格式:
struct sockaddr_nl { sa_family_t nl_family; // AF_NETLINK unsigned short nl_pad; // zero __u32 nl_pid; // process pid __u32 nl_groups; // multicast grps mask };
nl_pid 必须包含唯一的 ID,可以使用 getpid() 函数的返回值创建该 ID。此函数返回打开 RTNETLINK 套接字的当前用户进程的进程 ID。但是,如果我们的进程由多个线程组成,每个线程打开不同的 RTNETLINK 套接字,则可以使用修改后的进程 ID。
一旦填充此结构,就可以进行绑定。如果操作成功,bind 函数返回零。如果失败,则返回负数,并设置系统错误变量。以下是调用 bind 的示例:
struct sockaddr_nl la; ... bzero(&la, sizeof(la)); la.nl_family = AF_NETLINK; la.nl_pad = 0; la.nl_pid = getpid(); la.nl_groups = 0; rtn = bind(fd, (struct sockaddr*) &la, sizeof(la));
如果您需要的操作是基于多播的,则必须设置 nl_groups 以加入与所需 RTNETLINK 操作关联的多播组。例如,如果您希望收到其他进程对路由表更改的通知,则必须对 RTMGRP_IPV4_ROUTE 和 RTMGRP_NOTIFY 进行 OR (|) 运算。
将路由 RTNETLINK 消息发送到内核是通过使用套接字接口的标准 sendmsg() 函数完成的。以下是此函数的原型:
ssize_t sendmsg(int fd, const struct msghdr *msg, int flags);
msg 是指向 msghdr 结构的指针。以下是此结构的格式:
struct msghdr { void *msg_name; //Address to send to socklen_t msg_namelen; //Length of address data struct iovec *msg_iov; //Vector of data to send size_t msg_iovlen; //Number of iovec entries void *msg_control; //Ancillary data size_t msg_controllen; //Ancillary data buf len int msg_flags; //Flags on received msg };
msg_name 是指向 struct sockaddr_nl 类型变量的指针。这是 sendmsg() 函数的目标地址。由于此消息定向到内核,因此 sockaddr_nl 的所有变量都将初始化为零,但 nl_family 成员变量除外。msg_namelen 字段应包含 struct sockaddr_nl 的大小。
msg_iov 应包含指向 struct iovec 的指针,该结构填充了与正在发出的请求相关的 RTNETLINK 消息。如果需要,调用者可以放置多个 RTNETLINK 请求。msg_iovlen 指向放置在 msg_iov 中的 struct iovec 结构的数量。其余变量初始化为零。
要接收 RTNETLINK 消息,请使用 recv() 函数。以下是此函数的原型:
ssize_t recv(int fd, void *buf, size_t len, int flags);
第二个和第三个变量是指向缓冲区的指针,分别用于放置读取的字节和此缓冲区的长度。对于 RTNETLINK,缓冲区将包含一组 RTNETLINK 消息,这些消息必须使用 netlink.h 和 rtnetlink.h #include 文件中提供的一组宏逐个读取。flags 是一组标志,用于指示应如何执行接收。对于 RTNETLINK,只需将其初始化为零即可。
一旦套接字通信完成,必须使用 close() 函数关闭套接字。以下是此函数的原型:
int close(int fd);
开发使用 RTNETLINK 的应用程序的程序员必须至少包含以下 #include 文件:
#include <bits/sockaddr.h> #include <asm/types.h> #include <linux/rtnetlink.h> #include <sys/socket.h>
这些文件包含进行 RTNETLINK 调用所需的各种定义,例如数据类型和结构。以下是与 RTNETLINK 相关的这些文件中定义的简要说明:
bits/sockaddr.h:提供套接字函数使用的地址的定义。
asm/types.h:提供与 NETLINK 和 RTNETLINK 相关的头文件中使用的数据类型的定义。
linux/rtnetlink.h:提供 RTNETLINK 中使用的宏和数据结构。由于 RTNETLINK 基于 NETLINK,因此它也包含 linux/netlink.h。netlink.h 定义了所有基于 NETLINK 的功能中使用的通用宏和结构。
sys/socket.h:提供函数原型和与套接字实现相关的各种数据结构。
可以使用 RTNETLINK 调用的操作在 rtnetlink.h 文件中定义。每个操作都提供三种操作可能性:添加/更新、删除或查询。这三种可能性分别由 NEW、DEL 和 GET 标识。以下是 RTNETLINK 允许的操作:
通用网络环境操作服务
链路层接口设置:由 RTM_NEWLINK、RTM_DELLINK 和 RTM_GETLINK 标识。
网络层 (IP) 接口设置:RTM_NEWADDR、RTM_DELADDR 和 RTM_GETADDR。
网络层路由表:RTM_NEWROUTE、RTM_DELROUTE 和 RTM_GETROUTE。
将网络层和链路层寻址关联的邻居缓存:RTM_NEWNEIGH、RTM_DELNEIGH 和 RTM_GETNEIGH。
流量整形(管理)服务
用于定向网络层数据包的路由规则:RTM_NEWRULE、RTM_DELRULE 和 RTM_GETRULE。
与网络接口关联的排队规则设置:RTM_NEWQDISC、RTM_DELQDISC 和 RTM_GETQDISC。
与队列一起使用的流量类别:RTM_NEWTCLASS、RTM_DELTCLASS 和 RTM_GETTCLASS。
与排队关联的流量过滤器:RTM_NEWTFILTER、RTM_DELTFILTER 和 RTM_GETTFILTER。
RTNETLINK 采用请求-响应机制来发送和接收信息以操作网络环境。RTNETLINK 的请求或响应由消息结构流组成。这些结构在请求的情况下由调用者填充,在响应的情况下由内核填充。为了将信息放入这些结构或检索信息,RTNETLINK 提供了一组宏(使用 #define 语句)。每个请求都必须在开头包含以下结构:
struct nlmsghdr { __u32 nlmsg_len; //Length of msg incl. hdr __u16 nlmsg_type; //Message content __u16 nlmsg_flags; //Additional flags __u32 nlmsg_seq; //Sequence number __u32 nlmsg_pid; //Sending process PID }
此结构提供有关请求的其余部分中指定的 RTNETLINK 消息类型的信息。它也称为 NETLINK 标头。以下是对这些字段的简要说明:
nlmsg_len:应包含整个 RTNETLINK 消息的长度,包括 nlmsghdr 结构的长度。可以使用宏 NLMSG_ALIGN(len) 填充此字段,其中 len 是跟随此结构的消息的长度。
nlmsg_type:一个 16 位标志,用于指示消息中包含的内容,例如 RTM_NEWROUTE。
nlmsg_flags:另一个 16 位标志,用于进一步说明 nlmsg_type 中指定的操作,例如 NLM_F_REQUEST。
nlmsg_seq 和 nlmsg_pid:这两个字段用于唯一标识 RTNETLINK 请求。调用者可以将进程 ID 和序列号放置在这些字段中。
在 nlmsghdr 结构之后是与正在请求的操作相关的结构。根据 RTNETLINK 操作的类型,调用者必须包含以下一个或多个结构。这些结构称为 RTNETLINK 操作标头:
struct rtmsg:检索或修改路由表的条目需要使用此结构。
struct rtnexthop:路由条目中的下一跳是到达目标地址的路径上要考虑的下一个主机。单个路由条目可以有多个下一跳。除了下一跳 IP 地址之外,每个下一跳条目还有许多类型的属性,例如网络接口。
struct rta_cacheinfo:每个路由条目都包含内核定期更新的状态信息,主要是使用信息。使用此结构,用户可以检索此信息。
struct ifaddrmsg:检索或修改与网络接口关联的网络层属性需要使用此结构。
struct ifa_cacheinfo:与路由条目类似,网络接口也包含有关其状态的信息,该信息由内核更新。此结构用于检索此信息。
struct ndmsg:检索或修改邻居的链路层寻址和网络层寻址之间的关联信息(称为邻居发现)是通过此结构指定的。
struct nda_cacheinfo:保存与每个邻居发现条目相关的内核更新信息。
struct ifinfomsg:检索或修改与网络接口相关的链路层属性需要使用此结构。
struct tcmsg:检索或修改流量整形属性是使用此结构提供的。
在 RTNETLINK 操作标头之后是与操作相关的属性,例如接口号和 IP 地址。这些属性使用 struct rtattr 指定。每个属性都有一个结构。此结构具有以下格式:
struct rtattr { unsigned short rta_len; unsigned short rta_type; };
紧随此结构之后是属性的值。诸如 IP 版本 4 地址之类的属性将占用 4 字节的区域。变量 rta_len 应包含此结构的大小加上属性的大小。rta_type 应包含标识属性的值,这些值在 rtnetlink.h 中定义的枚举中给出。enum rtattr_type_t 和其他枚举提供属性标识符,例如 IFA_ADDRESS 和 NDA_DST,用于此字段。您可以附加的最大属性数仅为宏 RTATTR_MAX。以下是附加属性的示例:
rtap->rta_type = RTA_DST; rtap->rta_len = sizeof(struct rtattr) + 4; inet_pton(AF_INET, dsts, ((char *)rtap) + sizeof(struct rtattr));
从 RTNETLINK 套接字接收的信息再次是结构流。程序员必须通过沿此字节流移动指针来识别和提取信息。为了简化此过程,RTNETLINK 提供了一组宏,以简化缓冲区定位:
NLMSG_NEXT(nlh, len):返回指向下一个 NETLINK 标头的指针。nlh 是先前返回的标头,len 是消息的总长度。这将在循环中调用以读取每条消息。
NLMSG_DATA(nlh):给定 NETLINK 标头,返回指向与请求的操作相关的 RTNETLINK 标头的指针。如果要操作路由条目,这将返回指向 struct rtmsg 的指针。
RTM_RTA(r)、IFA_RTA(r)、NDA_RTA(r)、IFLA_RTA(r) 和 TCA_RTA(r):给定 RTNETLINK 消息的标头 (r),返回指向相应 RTNETLINK 操作的属性开头的指针。
RTM_PAYLOAD(n)、IFA_PAYLOAD(n)、NDA_PAYLOAD(n)、IFLA_PAYLOAD(n) 和 TCA_PAYLOAD(n):给定指向 NETLINK 标头 (n) 的指针,返回跟随 RTNETLINK 操作标头的属性的总长度。
RTA_NEXT(rta, attrlen):给定上次返回的属性 (rta) 和属性的剩余大小 (attrlen),返回指向下一个属性开头的指针。
考虑一个简单的示例,其中发送了检索路由表的 RTNETLINK 请求,回复的处理方式如下:
char *buf; // ptr to RTNETLINK data int nll; // byte length of all data struct nlmsghdr *nlp; struct rtmsg *rtp; int rtl; struct rtattr *rtap; nlp = (struct nlmsghdr *) buf; for(;NLMSG_OK(nlp, nll); nlp=NLMSG_NEXT(nlp, nll)) { // get RTNETLINK message header rtp = (struct rtmsg *) NLMSG_DATA(nlp); // get start of attributes rtap = (struct rtattr *) RTM_RTA(rtp); // get length of attributes rtl = RTM_PAYLOAD(nlp); // loop & get every attribute for(;RTA_OK(rtap, rtl); rtap=RTA_NEXT(rtap, rtl)) { // check and process every attribute } }
此处提供的示例代码侧重于可以在路由表上执行的三个操作:
get_routing_table:读取系统中的主路由表。
set_routing_table:向表中插入新的路由条目。
mon_routing_table:监控路由表更改。
所有三个示例都使用类似的 main() 函数,该函数调用一组子函数来形成 RTNETLINK 消息并发送、接收和处理接收到的消息。为了简化说明,不考虑错误处理。这些示例在系统的 IP 版本 4 环境 (AF_INET) 中执行。以下是 main() 函数:
int main(int argc, char *argv[]) { // open socket fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); // setup local address & bind using // this address bzero(&la, sizeof(la)); la.nl_family = AF_NETLINK; la.nl_pid = getpid(); bind(fd, (struct sockaddr*) &la, sizeof(la)); // sub functions to create RTNETLINK message, // send over socket, receive reply & process // message form_request(); send_request(); recv_reply(); read_reply(); // close socket close(fd); }
与上述函数类似,执行套接字通信的两个函数几乎对所有示例都是通用的。这两个函数只是将形成的消息发送到内核,并接收内核发送的消息。这里的例外是 set_routing_table 和 mon_routing_table 示例。在 set_routing_table 中,不考虑接收阶段。在 mon_routing_table 中,由于它尝试仅监控路由环境的状态以查看正在更改的内容,因此不存在发送阶段。此信息由内核多播到所有处于适当接收状态的 RTNETLINK 套接字。
首先,这是 send_request() 的代码:
void send_request() { // create the remote address // to communicate bzero(&pa, sizeof(pa)); pa.nl_family = AF_NETLINK; // initialize & create the struct msghdr supplied // to the sendmsg() function bzero(&msg, sizeof(msg)); msg.msg_name = (void *) &pa; msg.msg_namelen = sizeof(pa); // place the pointer & size of the RTNETLINK // message in the struct msghdr iov.iov_base = (void *) &req.nl; iov.iov_len = req.nl.nlmsg_len; msg.msg_iov = &iov; msg.msg_iovlen = 1; // send the RTNETLINK message to kernel rtn = sendmsg(fd, &msg, 0); }
其次,这是 recv_reply():
void recv_reply() { char *p; // initialize the socket read buffer bzero(buf, sizeof(buf)); p = buf; nll = 0; // read from the socket until the NLMSG_DONE is // returned in the type of the RTNETLINK message // or if it was a monitoring socket while(1) { rtn = recv(fd, p, sizeof(buf) - nll, 0); nlp = (struct nlmsghdr *) p; if(nlp->nlmsg_type == NLMSG_DONE) break; // increment the buffer pointer to place // next message p += rtn; // increment the total size by the size of // the last received message nll += rtn; if((la.nl_groups & RTMGRP_IPV4_ROUTE) == RTMGRP_IPV4_ROUTE) break; } }
上述函数和以下函数使用一组全局定义的变量。这些变量用于所有套接字操作以及形成和处理 RTNETLINK 消息:
// buffer to hold the RTNETLINK request struct { struct nlmsghdr nl; struct rtmsg rt; char buf[8192]; } req; // variables used for // socket communications int fd; struct sockaddr_nl la; struct sockaddr_nl pa; struct msghdr msg; struct iovec iov; int rtn; // buffer to hold the RTNETLINK reply(ies) char buf[8192]; // RTNETLINK message pointers & lengths // used when processing messages struct nlmsghdr *nlp; int nll; struct rtmsg *rtp; int rtl; struct rtattr *rtap;
get_routing_table 示例检索 IPv4 环境的主路由表。form_request() 函数如下:
void form_request() { // initialize the request buffer bzero(&req, sizeof(req)); // set the NETLINK header req.nl.nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg)); req.nl.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP; req.nl.nlmsg_type = RTM_GETROUTE; // set the routing message header req.rt.rtm_family = AF_INET; req.rt.rtm_table = RT_TABLE_MAIN; }
在 buf 变量中接收到的用于检索路由表的 RTNETLINK 请求的消息由 read_reply() 函数处理。以下是此函数的代码:
void read_reply() { // string to hold content of the route // table (i.e. one entry) char dsts[24], gws[24], ifs[16], ms[24]; // outer loop: loops thru all the NETLINK // headers that also include the route entry // header nlp = (struct nlmsghdr *) buf; for(;NLMSG_OK(nlp, nll);nlp=NLMSG_NEXT(nlp, nll)) { // get route entry header rtp = (struct rtmsg *) NLMSG_DATA(nlp); // we are only concerned about the // main route table if(rtp->rtm_table != RT_TABLE_MAIN) continue; // init all the strings bzero(dsts, sizeof(dsts)); bzero(gws, sizeof(gws)); bzero(ifs, sizeof(ifs)); bzero(ms, sizeof(ms)); // inner loop: loop thru all the attributes of // one route entry rtap = (struct rtattr *) RTM_RTA(rtp); rtl = RTM_PAYLOAD(nlp); for(;RTA_OK(rtap, rtl);rtap=RTA_NEXT(rtap,rtl)) { switch(rtap->rta_type) { // destination IPv4 address case RTA_DST: inet_ntop(AF_INET, RTA_DATA(rtap), dsts, 24); break; // next hop IPv4 address case RTA_GATEWAY: inet_ntop(AF_INET, RTA_DATA(rtap), gws, 24); break; // unique ID associated with the network // interface case RTA_OIF: sprintf(ifs, "%d", *((int *) RTA_DATA(rtap))); default: break; } } sprintf(ms, "%d", rtp->rtm_dst_len); printf("dst %s/%s gw %s if %s\n", dsts, ms, gws, ifs); } }
set_routing_table 示例发送 RTNETLINK 请求以在路由表中插入条目。插入的路由条目是通过接口号 2 到私有 IP 地址 (192.168.0.100) 的主机路由(32 位网络前缀)。这些值在变量 dsts(目标 IP 地址)、ifcn(接口号)和 pn(前缀长度)中定义。您可以运行 get_routing_table 示例来了解系统中的接口号和 IP 网络。以下是 form_request():
void form_request() { // attributes of the route entry char dsts[24] = "192.168.0.100"; int ifcn = 2, pn = 32; // initialize RTNETLINK request buffer bzero(&req, sizeof(req)); // compute the initial length of the // service request rtl = sizeof(struct rtmsg); // add first attrib: // set destination IP addr and increment the // RTNETLINK buffer size rtap = (struct rtattr *) req.buf; rtap->rta_type = RTA_DST; rtap->rta_len = sizeof(struct rtattr) + 4; inet_pton(AF_INET, dsts, ((char *)rtap) + sizeof(struct rtattr)); rtl += rtap->rta_len; // add second attrib: // set ifc index and increment the size rtap = (struct rtattr *) (((char *)rtap) + rtap->rta_len); rtap->rta_type = RTA_OIF; rtap->rta_len = sizeof(struct rtattr) + 4; memcpy(((char *)rtap) + sizeof(struct rtattr), &ifcn, 4); rtl += rtap->rta_len; // setup the NETLINK header req.nl.nlmsg_len = NLMSG_LENGTH(rtl); req.nl.nlmsg_flags = NLM_F_REQUEST | NLM_F_CREATE; req.nl.nlmsg_type = RTM_NEWROUTE; // setup the service header (struct rtmsg) req.rt.rtm_family = AF_INET; req.rt.rtm_table = RT_TABLE_MAIN; req.rt.rtm_protocol = RTPROT_STATIC; req.rt.rtm_scope = RT_SCOPE_UNIVERSE; req.rt.rtm_type = RTN_UNICAST; // set the network prefix size req.rt.rtm_dst_len = pn; }
mon_routing_table 示例读取在其他进程更改系统的主路由表时接收到的 RTNETLINK 消息。此函数将使用相同的 read_reply() 函数来处理消息。main() 函数需要稍作更改。由于此操作涉及侦听内核的多播消息,因此我们绑定的本地地址也必须包含两个标志 RTMGRP_IPV4_ROUTE 和 RTMGRP_NOTIFY。以下是所需的更改:
la.nl_groups = RTMGRP_IPV4_ROUTE | RTMGRP_NOTIFY;
执行 mon_routing_table 后,运行:route add或:route del来自另一个 shell 提示符的命令以查看结果。
RTNETLINK 是一种简单而通用的方式,用于操作 Linux 主机的网络环境。用户空间网络协议处理程序是使用 RTNETLINK 的理想选择。高级 IP 路由命令套件(称为 IPROUTE2)基于 RTNETLINK。有关 RTNETLINK 的不同操作和标志的更多信息,请参见 NETLINK(7) 和 RTNETLINK(7)。
本文的示例代码可在 ftp.ssc.com/pub/lj/listings/issue145/8498.tgz 获取。
Asanga Udugama (adu@comnets.uni-bremen.de) 是德国不莱梅大学 ComNets 的研究员/软件开发人员。他目前致力于实施和改进 IETF 关于移动性相关网络层协议的标准。他有几个参考实现归功于他。目前,他正在等待他梦寐以求的轮椅(带有 Servomatic 的 Meyra X3)的到来。