1. 程式人生 > >DDNS 的工作原理及其在 Linux 上的實現

DDNS 的工作原理及其在 Linux 上的實現

http://www.ibm.com/developerworks/cn/linux/1305_wanghz_ddns/index.html

DDNS (Dynamic DNS) 擴充套件了 DNS 將客戶端 IP 與其域名進行靜態對映的功能,它可以將同一域名實時地解析為不同的動態 IP,而不需要額外的人工干預。這在客戶端 IP 地址不斷髮生變化的情況下,尤其是在無線網路和 DHCP 環境中,都有著極其重要的意義。本文通過分析 DDNS 的工作原理,簡單演示了其在 Linux 網路協議棧的核心空間及使用者空間建立 netlink 套接字、進行資料交換、並最終通過 nsupate 工具將更新訊息傳送給 DNS 伺服器的過程。

DDNS 工作原理的分析

DDNS 的實現最根本的一點是當主機的 IP 地址發生變化的時候,實現 DNS 對映資訊的及時更新,應用程式需要及時地獲得這一資訊,主要的方法可分為兩大類:

  • 一類是輪詢機制,即:應用程式每隔一定的時間,去從查詢主機當前的 IP 地址,並與之前的進行比較,從而判斷網路地址是否發生了變化。顯然,這種方法不僅效率低下,而且對每次查詢 IP 地址的時間間隔很難得到一個折中的數值。
  • 第二類方法是非同步實現方式,即:每當主機的 IP 地址發生變化的時候,應用程式能夠被及時地通知到。這的確是一個簡單而又高效的方法,但與此同時,另一個問題又產生了,那就是:通知源又應該由誰來擔當呢?顯然,這是處於使用者空間的應用程式無法勝任的。於是,我們想到了讓核心來充當這一訊息源。這樣,在核心空間和使用者空間之間就需要通過訊息來進行通訊了。

在 Linux 下使用者空間與核心空間的資訊互動方式有許多種,比如:軟中斷、系統呼叫、netlink 等等。關於這些通訊方式的介紹以及其各自的優缺點並不在本文的討論範圍內,您可以自行檢視參考資源

在這許多種通訊方式中,netlink 憑藉其標準的 socket API、模組化實現、非同步通訊機制、多播機制等等多種優勢,成為了核心與越來越多應用程式之間互動的主要方式。在 Linux 的核心中,已經為我們封裝了使用 netlink 對特定網路狀態變化進行訊息通知的功能,這就是著名的 rtnetlink。有關 netlink 在核心空間實現的詳細程式碼以及其 API 引數的介紹,您可以自行檢視

參考資源,本文在此不作過多的贅述。

本文討論的重點是針對 DDNS 這一特定的應用,演示 rtnetlink 檢測到 IP 地址發生了變化、並將訊息告知使用者空間的應用程式的整個過程,以及應用程式利用 netlink 套接字接收訊息、並告知 DNS 伺服器的實現方法。

 

 

DDNS 工作流程的簡單介紹

結合上述對 DDNS 工作原理的分析,我們可以將 DDNS 的工作流程簡單地用圖 1 來表示:

圖 1. DDNS 的工作流程圖

DDNS 的工作流程圖

從圖 1 中可以看到,DDNS 的工作流程主要有三個部分:

  1. 應用程式實時感知到 IP 地址發生了變化,如上介紹,利用基於 netlink 的非同步通知機制可以讓應用程式及時得到核心空間對這些事件的“通知”,具體可以分為如下 5 個步驟:
    • 1、核心空間初始化 rtnetlink 模組,建立 NETLINK_ROUTE 協議簇型別的 netlink 套接字;
    • 2、使用者空間建立 NETLINK_ROUTE 協議簇型別的 netlink 套接字,並且繫結到 RTMGRP_IPV4_IFADDR 組播 group 中;
    • 3、使用者空間接收從核心空間發來的訊息,如果沒有訊息,則阻塞自身;
    • 4、當主機被分配了新的 IPV4 地址,核心空間通過 netlink_broadcast,將 RTM_NEWADDR 訊息傳送到 RTNLGRP_IPV4_IFADDR 組播 group 中 ;
    • 5、使用者空間接收訊息,進行驗證、處理;
  2. 應用程式接收到“通知”後,把 DNS update 資訊傳送給 DNS 伺服器,目的是將更新後的 IP 地址及時地通知 DNS 伺服器,以便網路上的主機仍然能夠通過原來的域名訪問到自己,通用的做法是利用開源軟體 nsupdate 傳送 DNS update 資訊給 DNS 伺服器以實現 DNS 資訊的動態更新。
  3. 最後,對應於第一部分 netlink 套接字的建立,使用者空間和核心空間關閉所建立的 netlink 套接字。

下文將詳細闡述其中的每一環節及其實現。

 

 

核心空間 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. 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 的實現主要包括:

  1. 首先分配了一塊型別為 struct sk_buff 的空間用於存放需要傳送的訊息內容。
  2. 隨後,呼叫 inet_fill_ifaddr 將訊息填充至上述快取(有關訊息的格式,您可以自行檢視參考資源)。值得注意的是,RTM_NEWADDR 被作為 nlmsg_type 封裝到了核心傳送給應用程式的 netlink 訊息頭 nlmsghdr 中,這樣使用者空間的應用程式在接收後就能夠根據 type 來分別處理不同型別的訊息了。
  3. 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 應用程式能夠非同步地收到核心空間發來的通知訊息了。

 

使用者空間 netlink socket 的建立、繫結與訊息接收處理

使用者空間建立 netlink 套接字

使用者空間的 netlink socket 相關操作與標準 socket API 完全一致,因此可以像使用標準 socket 來進行兩臺主機間的 IP 協議通訊一樣地來使用它,這也是 netlink 之所以能夠得到越來越廣泛應用的一個重要原因。

清單 4. 使用者空間建立 netlink socket

 #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:

清單 5. 清單 4 中使用到的巨集定義

 /* 以下程式碼摘自 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 地址發生變化的訊息時使用的是它,所以這裡需要保持一致以進行雙方間的通訊。

使用者空間繫結 netlink 套接字

與標準的 socket 使用方法相似,在建立 netlink 套接字之後,也需要繫結到一個 netlink 地址才能夠進行訊息的傳送與接收。netlink 地址在 struct sockaddr_nl 結構中定義,各結構成員的含義可參見附錄 3。

清單 6. 使用者空間 bind netlink socket

 #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 正是我們需要用到的包含了訊息型別的欄位。

清單 7. 使用者空間接收核心空間訊息

 #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 的實現參見下一章。

清單 8. 使用者空間處理核心空間訊息

 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; 
 }

 

 

應用程式與 DNS 伺服器的互動

應用程式可以利用開源工具 nsupdate 來向 DNS 伺服器傳送 DNS update 訊息。nsupdate 的詳細用法及特性可以請檢視參考資源,受篇幅所限,本章將會結合例子簡單介紹這個工具的基本用法。

nsupdate 可以從終端或檔案中讀取命令,每個命令一行。一個空行或一個"send"命令,則會將先前輸入的命令傳送到 DNS 伺服器上,典型的使用方法如清單 9 所示。nsupdate 預設從檔案 /etc/resolv.conf 中解析 DNS 伺服器和域名,在實際應用中,我們可以首先解析網路引數,生成 nsupdate 的輸入檔案,最後呼叫 nsupdate。update_dns.sh 的實現流程如圖 3 所示。

清單 9. nsupdate 的使用例子

 # 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          // 傳送命令

圖 3. update_dns.sh 的實現流程

圖 3. update_dns.sh 的實現流程

 

 

netlink socket 的關閉

使用者空間關閉 netlink socket

同標準的 socket API 一樣,使用者空間關閉 netlink socket 使用的也是 close 函式,而且用法完全一致。您可以參考清單 6 中 close 函式在 DDNS 應用程式中的使用。

核心空間關閉 netlink socket

核心空間關閉 netlink socket 使用 sock_release 函式,函式原型如下所示:

清單 10. 核心空間關閉 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 介面,函式原型如下所示:

清單 11. 核心空間關閉 netlink socket —— 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 應用實現的擴充套件啟示

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 網路程式設計愛好者提供有益的參考。