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 網絡編程愛好者提供有益的參考。