http://www.ibm.com/developerworks/cn/linux/1305_wanghz_ddns/index.htmlhtml
DDNS (Dynamic DNS) 扩展了 DNS 将客户端 IP 与其域名进行静态映射的功能,它能够将同一域名实时地解析为不一样的动态 IP,而不须要额外的人工干预。这在客户端 IP 地址不断发生变化的状况下,尤为是在无线网络和 DHCP 环境中,都有着极其重要的意义。本文经过分析 DDNS 的工做原理,简单演示了其在 Linux 网络协议栈的内核空间及用户空间建立 netlink 套接字、进行数据交换、并最终经过 nsupate 工具将更新消息发送给 DNS 服务器的过程。linux
DDNS 的实现最根本的一点是当主机的 IP 地址发生变化的时候,实现 DNS 映射信息的及时更新,应用程序须要及时地得到这一信息,主要的方法可分为两大类:编程
在 Linux 下用户空间与内核空间的信息交互方式有许多种,好比:软中断、系统调用、netlink 等等。关于这些通讯方式的介绍以及其各自的优缺点并不在本文的讨论范围内,您能够自行查看参考资源。缓存
在这许多种通讯方式中,netlink 凭借其标准的 socket API、模块化实现、异步通讯机制、多播机制等等多种优点,成为了内核与愈来愈多应用程序之间交互的主要方式。在 Linux 的内核中,已经为咱们封装了使用 netlink 对特定网络状态变化进行消息通知的功能,这就是著名的 rtnetlink。有关 netlink 在内核空间实现的详细代码以及其 API 参数的介绍,您能够自行查看参考资源,本文在此不做过多的赘述。服务器
本文讨论的重点是针对 DDNS 这一特定的应用,演示 rtnetlink 检测到 IP 地址发生了变化、并将消息告知用户空间的应用程序的整个过程,以及应用程序利用 netlink 套接字接收消息、并告知 DNS 服务器的实现方法。网络
回页首dom
结合上述对 DDNS 工做原理的分析,咱们能够将 DDNS 的工做流程简单地用图 1 来表示:异步
从图 1 中能够看到,DDNS 的工做流程主要有三个部分:socket
下文将详细阐述其中的每一环节及其实现。模块化
在咱们开始利用 netlink 套接字、实现与内核通讯的应用程序以前,先来分析一下内核空间的 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 地址变化的缘由有不少种,如: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。
/* 如下代码摘自 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 的实现主要包括:
/* 如下代码摘自 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 应用程序可以异步地收到内核空间发来的通知消息了。
用户空间的 netlink socket 相关操做与标准 socket API 彻底一致,所以能够像使用标准 socket 来进行两台主机间的 IP 协议通讯同样地来使用它,这也是 netlink 之因此可以获得愈来愈普遍应用的一个重要缘由。
#include <sys/socket.h> #include <linux/types.h> #include <linux/netlink.h> #include <linux/rtnetlink.h> ...... 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:
/* 如下代码摘自 include/linux/socket.h 文件 */ /* Supported address families. */ #define AF_UNSPEC 0 #define AF_UNIX 1 /* Unix domain sockets */ #define AF_LOCAL 1 /* POSIX name for AF_UNIX */ #define AF_INET 2 /* Internet IP Protocol */ ...... #define AF_NETLINK 16 ...... /* Protocol families, same as address families. */ #define PF_NETLINK AF_NETLINK ......
对于通讯方式,咱们选择了 SOCK_DGRAM。事实上对于 netlink 这种基于无链接的 socket,使用 SOCK_DGRAM 或者 SOCK_RAW 都是能够的。
对于通讯协议,咱们使用了 NETLINK_ROUTE。这是由于在清单 1 中,内核空间建立 netlink 套接字、用于发送 IP 地址发生变化的消息时使用的是它,因此这里须要保持一致以进行双方间的通讯。
与标准的 socket 使用方法类似,在创建 netlink 套接字以后,也须要绑定到一个 netlink 地址才可以进行消息的发送与接收。netlink 地址在 struct sockaddr_nl 结构中定义,各结构成员的含义可参见附录 3。
#include <sys/socket.h> #include <linux/types.h> #include <linux/netlink.h> #include <linux/rtnetlink.h> ...... int main(void) { ...... struct sockaddr_nl addr // 在 include/linux/netlink.h 中定义,结构各成员的含义可参见附录 3 memset(&addr, 0, 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 正是咱们须要用到的包含了消息类型的字段。
#define MAX_MSG_SIZE 1024 ...... #include <sys/socket.h> #include <linux/types.h> #include <linux/netlink.h> #include <linux/rtnetlink.h> ...... 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, 0, &iov, 1, NULL, 0, 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 的实现参见下一章。
void handle_newaddr(struct ifinfomsg *ifinfo, int len) { struct if_info *i; 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 所示。
# nsupdate > 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 函数,函数原型以下所示:
/* 如下代码摘自 Linux kernel 3.4.3, net/socket.c 文件 */ void sock_release(struct socket * sock);
其中 sock 为 netlink_kernel_create 建立的 netlink 套接字。
值得一提的是,在最新的 Linux kernel 中,还提供了 netlink_kernel_release 接口,函数原型以下所示:
/* 如下代码摘自 Linux kernel 3.4.3, net/netlink/af_netlink.c 文件 */ void netlink_kernel_release(struct sock *sk);
其中 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、RIPv二、BGP 等在内的多种现行路由协议;再好比:也能够利用 rtnetlink 来监听网络的链接状况,rtnetlink 在初始化的时候将 rtnetlink 消息处理函数 rtnetlink_event 挂到了通知链 netdev_chain 上,网络设备的启动,关闭,改名等事件都能触发通知链并回调消息处理函数,从而组播 RTM_NEWLINK 或者 RTM_DELLINK 信息,向用户程序通知网络的链接状况。
本文结合 DDNS 的工做原理,简单阐释了 DDNS 的实现流程,并在此基础之上,进一步演示了利用 Linux rtnetlink 套接字实现内核空间与用户空间的网络状态 IP 地址变化信息的交互、以及利用 nsupdate 实现 DDNS 客户端与服务器端的同步更新,而且在实际的应用中彻底实现了 DDNS 的功能,但愿可以为使用 DDNS 进行网络管理的人员及 Linux 网络编程爱好者提供有益的参考。