内核空间 rtnetlink 检测 IP 地址变化的实现与分析
在我们开始利用 netlink 套接字、实现与内核通信的应用程序之前,先来分析一下内核空间的 rtnetlink 模块是如何工作的。
内核空间 rtnetlink 的初始化
清单 1. rtnetlink 的初始化
/*
以下代码摘自 Linux kernel 2.6.18, net/core/rtnetlink.c 文件,
并只选择了与本主题相关的最重要的部分,其他的都用省略号略过,之后的各清单也一样。
*/
void __init rtnetlink_init(void)
{
......
rtnl = netlink_kernel_create(NETLINK_ROUTE, RTNLGRP_MAX, rtnetlink_rcv, THIS_MODULE);
if (rtnl == NULL)
panic("rtnetlink_init: cannot initialize rtnetlink\n");
......
}
从清单 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 地址变化事件的通知过程
引起主机 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. SIOCSIFADDR 命令的 ifconfig 调用树
从图 2 中可以看到,当用户使用 ifconfig 对主机的 IP 地址作了修改,内核在进行了新地址的设置之后,会调用 rtmsg_ifa,传递的事件为 RTM_NEWADDR。
清单 2. rtmsg_ifa 发送 IP 地址变化消息
/*
以下代码摘自 Linux kernel 2.6.18, net/ipv4/devinet.c 文件
*/
static void rtmsg_ifa(int event, struct in_ifaddr* ifa)
{
int size = NLMSG_SPACE(sizeof(struct ifaddrmsg) + 128);
struct sk_buff *skb = alloc_skb(size, GFP_KERNEL);
if (!skb)
netlink_set_err(rtnl, 0, RTNLGRP_IPV4_IFADDR, ENOBUFS);
else if (inet_fill_ifaddr(skb, ifa, 0, 0, event, 0) < 0) {
kfree_skb(skb);
netlink_set_err(rtnl, 0, RTNLGRP_IPV4_IFADDR, EINVAL);
} else {
netlink_broadcast(rtnl, skb, 0, RTNLGRP_IPV4_IFADDR, 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 的对应关系:
清单 3. 内核空间组播 group 与用户空间组播 group 的对应关系
/*
以下代码摘自 Linux kernel 2.6.18, include/linux/rtnetlink.h 文件
*/
/* RTnetlink multicast groups */
enum rtnetlink_groups {
RTNLGRP_NONE,
#define RTNLGRP_NONE RTNLGRP_NONE
RTNLGRP_LINK,
#define RTNLGRP_LINK RTNLGRP_LINK
.....
RTNLGRP_IPV4_IFADDR,
#define RTNLGRP_IPV4_IFADDR RTNLGRP_IPV4_IFADDR
......
};
#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 应用程序能够异步地收到内核空间发来的通知消息了。