在我们开始利用 netlink 套接字、实现与内核通信的应用程序之前,先来分析一下内核空间的 rtnetlink 模块是如何工作的。
}
从清单 1 中可以看到:
在 rtnetlink 进行初始化的时候,首先会调用 netlink_kernel_create 来创建一个 NETLINK_ROUTE 类型的 netlink 套接字,并指定接收函数为 rtnetlink_rcv,有关 rtnetlink_rcv 的实现细节可以查阅内核 net/core/rtnetlink.c 文件。这里需要指出的是,netlink 提供了包括 NETLINK_ROUTE、NETLINK_FIREWALL、NETLINK_INET_DIAG 等在内的多种协议簇(详细列表及各协议簇的含义可以自行查看),其中 NETLINK_ROUTE 类型提供了网络地址发生变化的消息,这正是 DDNS 需要用到的。
引起主机 IP 地址变化的原因有很多种,如:DHCP 分配的 IP 过期、用户手动修改了 IP 等等。无论何种原因,最终都会触发内核空间对相应事件的通知机制,这里以最常用的修改 IPV4 地址的工具 ifconfig 为例。
ifconfig 先是创建一个 AF_INET 的 socket,然后通过系统调用 ioctl 来完成配置的,ioctl 在内核中对应的函数是 sys_ioctl,对于 IP 地址、子网掩码、默认网关等配置的修改,其最终会调用 devinet_ioctl。devinet_ioctl 函数处理包括 get、set 在内的多种命令,与 DDNS 应用有关的是 set 类命令,图 2 给出了 SIOCSIFADDR 命令(设置网络地址)的 ifconfig 调用树:

从图 2 中可以看到,当用户使用 ifconfig 对主机的 IP 地址作了修改,内核在进行了新地址的设置之后,会调用 rtmsg_ifa,传递的事件为 RTM_NEWADDR。
if (!skb)
netlink_set_err(rtnl,RTNLGRP_IPV4_IFADDR,ENOBUFS);
else if (inet_fill_ifaddr(skb,ifa,event,0) < 0) {
kfree_skb(skb);
netlink_set_err(rtnl,EINVAL);
} else {
netlink_broadcast(rtnl,skb,GFP_KERNEL);
}
}
从清单 2 中可以看到,rtmsg_ifa 的实现主要包括:
首先分配了一块类型为 struct sk_buff 的空间用于存放需要发送的消息内容。
- 随后,调用 inet_fill_ifaddr 将消息填充至上述缓存(有关消息的格式,您可以自行查看)。值得注意的是,RTM_NEWADDR 被作为 nlmsg_type 封装到了内核发送给应用程序的 netlink 消息头 nlmsghdr 中,这样用户空间的应用程序在接收后就能够根据 type 来分别处理不同类型的消息了。
- rtmsg_ifa 的最后是调用了 netlink_broadcast 将上述封装完毕的 sk_buff 结构广播到 RTNLGRP_IPV4_IFADDR 这个 group,以下是内核空间组播 group 与用户空间组播 group 的对应关系:
ifndef
KERNEL
/ RTnetlink multicast groups - backwards compatibility for userspace /
define RTMGRP_LINK 1
define RTMGRP_NOTIFY 2
......
define RTMGRP_IPV4_IFADDR 0x10
......
endif
综上所述,当主机的 IP 地址发生变化时,内核会向所有 RTNLGRP_IPV4_IFADDR 组播成员发送 RTM_NEWADDR 消息。因此,在用户空间创建 netlink 套接字时,只需要加入到 RTMGRP_IPV4_IFADDR 这个组播 group 中,就可以实现当本机 IP 地址有更新的时候,DDNS 应用程序能够异步地收到内核空间发来的通知消息了。
用户空间的 netlink socket 相关操作与标准 socket API 完全一致,因此可以像使用标准 socket 来进行两台主机间的 IP 协议通信一样地来使用它,这也是 netlink 之所以能够得到越来越广泛应用的一个重要原因。
#include
#include
#include
......
int main(void)
{
......
if((nl_socket = socket(PF_NETLINK,SOCK_DGRAM,NETLINK_ROUTE))==-1)
// 指定通信域、通信方式以及通信协议
exit(1);
......
}
在创建 netlink 套接字时:
我们指定了通信域为 PF_NETLINK,表明这是一个 netlink 套接字。其定义可以在如下所示的内核 include/linux/socket.h 文件中找到。从中我们也可以看到自己非常熟悉的 AF_INET:
对于通信方式,我们选择了 SOCK_DGRAM。事实上对于 netlink 这种基于无连接的 socket,使用 SOCK_DGRAM 或者 SOCK_RAW 都是可以的。
对于通信协议,我们使用了 NETLINK_ROUTE。这是因为在清单 1 中,内核空间创建 netlink 套接字、用于发送 IP 地址发生变化的消息时使用的是它,所以这里需要保持一致以进行双方间的通信。
与标准的 socket 使用方法相似,在建立 netlink 套接字之后,也需要绑定到一个 netlink 地址才能够进行消息的发送与接收。netlink 地址在 struct sockaddr_nl 结构中定义,各结构成员的含义可参见附录 3。
#include
#include
#include
......
int main(void)
{
......
struct sockaddr_nl addr // 在 include/linux/netlink.h 中定义,结构各成员的含义可参见附录 3
memset(&addr,sizeof(addr));
addr.nl_family = PF_NETLINK; // 定义协议簇为 PF_NETLINK
addr.nl_groups = RTMGRP_IPV4_IFADDR // 加入到 RTMGRP_IPV4_IFADDR 组播 group 中
addr.nl_pid = 0; // 让 kernel 来分配 pid
......
// 将清单 5 中创建的 netlink 套接字与上述协议地址进行绑定
if(bind(nl_socket,(struct sockaddr *) &addr,sizeof(addr)) == -1)
{
close(nl_socket);
exit(1);
}
......
}
从清单 6 中可以看到,在绑定应用程序的 netlink 套接字时,我们将自己加入到了 RTMGRP_IPV4_IFADDR 组播 group 中,这与前文我们对内核空间 IP 地址变化事件的通知过程的分析是一致的。
同样与标准的 socket 使用方法类似,用户空间接收内核空间发来的 netlink 消息可以使用 recv、recvfrom 或 recvmsg。值得一提的是,netlink 套接字有自己的消息头:nlmsghdr 结构(该结构具体各成员变量的含义请查看),而其中的 nlmsg_type 正是我们需要用到的包含了消息类型的字段。
#include
#include
#include
......
struct if_info
{
int index; //interface 的序号
char name[IFNAMSIZ]; //interface 的名称,Linux 内核 include/linux/if.h 中定义了 IFNAMSIZ
uint8_t mac[ETH_ALEN];
//interface 的 mac 地址,Linux 内核 include/linux/if_ether.h 中定义了 ETH_ALEN
...... //interface 的其他信息
struct if_info *next; // 指向下一个 if_info 结构的指针
};
static struct if_info *if_list = NULL; // 存放现有的 interface 列表,在每次程序初始化时更新
int receive_netlink_message(struct nlmsghdr *nl); // 用于接收内核空间发来的消息的函数
handle_newaddr(struct ifinfomsg *ifi,int len); // 用于处理向 DNS 服务器发送更新的函数
......
int main(void)
{
......
int len = 0;
struct nlmsghdr *nl; // 结构体定义可以参考内核 include/linux/netlink.h 文件
while((len = receive_netlink_message(&nl)) > 0)
{
while(NLMSG_OK(nl,len)) //NLMSG 相关的宏定义可以参考内核 include/linux/netlink.h 文件
{
switch(nl->nlmsg_type)
{
case RTM_NEWADDR: // 处理 RTM_NEWADDR 的 netlink 消息类型
//ifinfomsg 结构可以参考内核 include/linux/rtnetlink.h 文件
handle_newaddr((struct ifinfomsg *)NLMSG_DATA(nl),NLMSG_PAYLOAD(nl,sizeof(struct ifinfomsg)));
break;
...... // 处理其他 netlink 消息类型,如:RTM_NEWLINK,这里略过
default:
printf("Unknown netlink message type : %d",nl->nlmsg_type);
}
nl = NLMSG_NEXT(nl,len);
}
if( nl != NULL )
free(nl);
}
......
}
int receive_netlink_message(struct nlmsghdr **nl)
{
struct iovec iov; // 使用 iovec 进行接收
struct msghdr msg = {NULL,&iov,1,NULL,0}; // 初始化 msghdr
int length;
nl = NULL;
if ((nl = (struct nlmsghdr *) malloc(MAX_MSG_SIZE)) == NULL )
return 0;
iov.iov_base = *nl; // 封装 nlmsghdr
iov.iov_len = MAX_MSG_SIZE; // 指定长度
length = recvmsg(nl_socket,&msg,0);
if(length <= 0)
FREE(*nl);
return length;
}
应用程序在收到了 RTM_NEWADDR 类型的 netlink 消息后,需要根据 IP 的变化进行处理。这里使用了 handle_newaddr 函数,对 IP 的变化分为了两种情况:一种是 interface 已经存在、仅仅是 IP 发生了变化;另一种是 interface 是新添加的。无论是哪种情况,handle_newaddr 函数在进行了相应的处理之后,都需要调用 update_dns.sh 这个脚本通知 DNS 服务器。关于 update_dns.sh 的实现参见下一章。
for(i = if_list ; i ; i = i->next) // 遍历 in_list,找到 ip 发生变化的 interface
if(i->index == ifinfo->ifi_index)
break;
if(i != NULL){ // 找到了相应的 interface,执行 update_dns.sh
system(update_dns.sh);
return;
}
// 没有找到对应的 interface,说明该 interface 是新添加的
if((i = calloc(sizeof(struct if_info),1)) == NULL)// 分配一个 if_info 结构用于添加新的 interface
exit(1);
// 根据 ifinfo->ifi_index 等信息更新 if_info 结构 i,考虑到与 ddns 应用关系不大,限于篇幅,这里略过
......
system(update_dns.sh); // 执行 update_dns.sh
i->next = if_list; // 在 if_list 的末尾添加新发现的 interface
if_list = i;
}
应用程序可以利用开源工具 nsupdate 来向 DNS 服务器发送 DNS update 消息。nsupdate 的详细用法及特性可以请查看,受篇幅所限,本章将会结合例子简单介绍这个工具的基本用法。
nsupdate 可以从终端或文件中读取命令,每个命令一行。一个空行或一个"send"命令,则会将先前输入的命令发送到 DNS 服务器上,典型的使用方法如清单 9 所示。nsupdate 默认从文件 /etc/resolv.conf 中解析 DNS 服务器和域名,在实际应用中,我们可以首先解析网络参数,生成 nsupdate 的输入文件,最后调用 nsupdate。update_dns.sh 的实现流程如图 3 所示。
server 9.0.148.50 //DNS 服务器地址 9.0.148.50,默认端口 53
> update delete oldhost.example.com A
// 删除域名 oldhost.example.com 的任何 A 类型记录
> update add newhost.example.com 86400 A 172.16.1.1
// 添加一条 172.16.1.1<----->newhost.example.com A 类型的记录,
// 记录的 TTL 是 24 小时(86400 秒)
> send // 发送命令

同标准的 socket API 一样,用户空间关闭 netlink socket 使用的也是 close 函数,而且用法完全一致。您可以参考清单 6 中 close 函数在 DDNS 应用程序中的使用。
内核空间关闭 netlink socket 使用 sock_release 函数,函数原型如下所示:
其中 sock 为 netlink_kernel_create 创建的 netlink 套接字。
值得一提的是,在最新的 Linux kernel 中,还提供了 netlink_kernel_release 接口,函数原型如下所示:
其中 sk 为 netlink_kernel_create 创建的 netlink 套接字。
DDNS 利用 rtnetlink 的 NETLINK_ROUTE 协议簇套接字来监听 Linux 内核网络事件“RTM_NEWADDR”,实时更新 DNS 映射信息,从而实现 DNS 信息的动态更新。除了 NETLINK_ROUTE,netlink_family 还提供了多种协议簇来实现多种信息的报告,比如 SELinux、防火墙、Netfilter、IPV6 等。就 NETLINK_ROUTE 协议簇而言,也提供了多个组播 group 对应多种网络连接、网络参数、路由信息、网络流量类别等等变化的事件。
这就启示我们可以利用 netlink,特别是 rtnetlink,实现许多其他的与网络相关的应用。比如:应用程序如果需要实时地监控本机路由表的变化,就可以在用户空间创建 NETLINK_ROUTE 协议簇的 netlink 套接字时把自己加到 RTMGRP_IPV4_ROUTE 及 RTMGRP_NOTIFY 的多播组中(即:addr.nl_groups = RTMGRP_IPV4_ROUTE | RTMGRP_NOTIFY;)通过这种方式,可以实现包括 OSPF、RIPv2、BGP 等在内的多种现行路由协议;再比如:也可以利用 rtnetlink 来监听网络的连接情况,rtnetlink 在初始化的时候将 rtnetlink 消息处理函数 rtnetlink_event 挂到了通知链 netdev_chain 上,网络设备的启动,关闭,更名等事件都能触发通知链并回调消息处理函数,从而组播 RTM_NEWLINK 或者 RTM_DELLINK 信息,向用户程序通知网络的连接情况。
本文结合 DDNS 的工作原理,简单阐释了 DDNS 的实现流程,并在此基础之上,进一步演示了利用 Linux rtnetlink 套接字实现内核空间与用户空间的网络状态 IP 地址变化信息的交互、以及利用 nsupdate 实现 DDNS 客户端与服务器端的同步更新,并且在实际的应用中完全实现了 DDNS 的功能,希望能够为使用 DDNS 进行网络管理的人员及 Linux 网络编程爱好者提供有益的参考。
(编辑:李大同)
【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!