AF_NetLink結構體及例程
一、AF_NETLINK結構體基礎
我們從一個實際的資料包傳送的例子入手,來看看其傳送的具體流程,以及過程中涉及到的相關資料結構。在我們的虛擬機器上傳送icmp回顯請求包,ping另一臺主機172.16.48.1。我們使用系統呼叫sendto傳送這個icmp包。
ssize_t sendto(int s, const void *buf, size_t len, int flags,
const struct sockaddr *to, socklen_t tolen);
系統呼叫sendto最終呼叫核心函式asmlinkage long sys_sendto(int fd, void __user * buff, size_t len,unsigned flags, struct sockaddr __user *addr, int addr_len)
sys_sendto構建一個結構體struct msghdr,用於接收來自應用層的資料包 ,下面是結構體struct msghdr的定義:
struct msghdr {
void *msg_name;//存資料包的目的地址,網路包指向sockaddr_in
//向核心發資料時,指向sockaddr_nl
int msg_namelen;//地址長度
struct iovec *msg_iov;
__kernel_size_t msg_iovlen;
void *msg_control;
__kernel_size_t msg_controllen;
unsigned msg_flags;
};
這個結構體的內容可以分為四組:
第一組是msg_name和msg_namelen,記錄這個訊息的名字,其實就是資料包的目的地址 。msg_name是指向一個結構體struct sockaddr的指標。長度為16:
structsockaddr{
sa_family_t sa_family;
char sa_addr[14];
}
所以,msg_namelen的長度為16。需要注意的是,結構體struct sockaddr只在進行引數傳遞時使用,無論是在使用者態還是在核心態,我們都把其強制轉化為結構體struct sockaddr_in:
strcutsockaddr_in{
sa_family_t sin_family;
unsigned short int sin_port;
struct in_addr sin_addr;
unsigned char __pad[__SOCK_SIZE__ -sizeof(short int) -
sizeof(unsigned short int) - sizeof(struct in_addr)];
};
struct in_addr{
__u32s_addr;
}
__SOCK_SIZE__的值為16,所以,struct sockaddr中真正有用的資料只有8bytes。在我們的ping例子中,傳入到核心的msghdr結構中:
msg.msg_name = { sa_family_t = MY_AF_INET, sin_port = 0, sin_addr.s_addr= 172.16.48.1 }
msg_msg_namelen = 16。
請求回顯icmp包沒有目的端地址的埠號。
第二組是msg_iov和msg_iovlen,記錄這個訊息的內容。msg_iov是一個指向結構體struct iovec的指標,實際上,確切地說,應該是一個結構體strcut iovec的陣列 。下面是該結構體的定義:
struct iovec{
void__user *iov_base;
__kernel_size_t iov_len;
};
iov_base指向資料包緩衝區,即引數buff,iov_len是buff的長度。msghdr中允許一次傳遞多個buff,以陣列的形式組織在 msg_iov中,msg_iovlen就記錄陣列的長度 (即有多少個buff)。在我們的ping程式的例項中:
msg.msg_iov = { struct iovec = { iov_base= { icmp頭+填充字元'E' }, iov_len = 40 } }
msg.msg_len = 1
第三組是msg_control和msg_controllen,它們可被用於傳送任何的控制資訊,在我們的例子中,沒有控制資訊要傳送。暫時略過。
第四組是msg_flags。其值即為傳入的引數flags。raw協議不支援MSG_OOB向標誌,即帶外資料。向向核心傳送msg時使用msghdr,netlink socket使用自己的訊息頭nlmsghdr和自己的訊息地址sockaddr_nl:
struct sockaddr_nl
{
sa_family_t nl_family;
unsigned short nl_pad;
__u32 nl_pid;
__u32 nl_groups;
};
struct nlmsghdr
{
__u32 nlmsg_len; /* Length of message */
__u16 nlmsg_type; /* Message type*/
__u16 nlmsg_flags; /* Additional flags */
__u32 nlmsg_seq; /* Sequence number */
__u32 nlmsg_pid; /* Sending process PID */
};
其中,nlmsg_flags:訊息標記,它們用以表示訊息的型別;
nlmsg_seq:訊息序列號,用以將訊息排隊,有些類似TCP協議中的序號(不完全一樣),但是netlink的這個欄位是可選的,不強制使用;
nlmsg_pid:傳送埠的ID號,對於核心來說該值就是0,對於使用者程序來說就是其socket所繫結的ID號。
過程如下:
struct msghdr msg;
memset(&msg, 0,sizeof(msg));
msg.msg_name =(void *)&(nladdr); //繫結目的地址
msg.msg_namelen = sizeof(nladdr);
{
/*初始化一個strcut nlmsghdr結構存,nlmsghdr為netlink socket自己的訊息頭部,並使iov->iov_base指向在這個結構*/
char buffer[] = "An example message";
struct nlmsghdr nlhdr;
nlhdr = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_MSGSIZE));
strcpy(NLMSG_DATA (nlhdr),buffer);//將資料存放在訊息頭指向的資料地址
nlhdr->nlmsg_len = NLMSG_LENGTH(strlen(buffer));
nlhdr->nlmsg_pid = getpid(); /* self pid */
nlhdr->nlmsg_flags = 0;
iov.iov_base = (void *)nlhdr;
iov.iov_len = nlh->nlmsg_len;
}
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
fd=socket(AF_NETLINK, SOCK_RAW, netlink_type);
sendmsg(fd,&msg,0)
二、 netlink 核心資料結構、常用巨集及函式
#define NETLINK_ROUTE 0 /* Routing/device hook */
#define NETLINK_UNUSED 1 /* Unused number */
#define NETLINK_USERSOCK 2 /* Reserved for user mode socket protocols */
#define NETLINK_FIREWALL 3 /* Unused number, formerly ip_queue */
#define NETLINK_SOCK_DIAG 4 /* socket monitoring */
#define NETLINK_NFLOG 5 /* netfilter/iptables ULOG */
#define NETLINK_XFRM 6 /* ipsec */
#define NETLINK_SELINUX 7 /* SELinux event notifications */
#define NETLINK_ISCSI 8 /* Open-iSCSI */
#define NETLINK_AUDIT 9 /* auditing */
#define NETLINK_FIB_LOOKUP 10
#define NETLINK_CONNECTOR 11
#define NETLINK_NETFILTER 12 /* netfilter subsystem */
#define NETLINK_IP6_FW 13
#define NETLINK_DNRTMSG 14 /* DECnet routing messages */
#define NETLINK_KOBJECT_UEVENT 15 /* Kernel messages to userspace */
#define NETLINK_GENERIC 16
/* leave room for NETLINK_DM (DM Events) */
#define NETLINK_SCSITRANSPORT 18 /* SCSI Transports */
#define NETLINK_ECRYPTFS 19
#define NETLINK_RDMA 20
#define NETLINK_CRYPTO 21 /* Crypto layer */
#define NETLINK_INET_DIAG NETLINK_SOCK_DIAG
#define MAX_LINKS 32
netlink常用巨集:
#define NLMSG_ALIGNTO 4U
/* 巨集NLMSG_ALIGN(len)用於得到不小於len且位元組對齊的最小數值 */
#define NLMSG_ALIGN(len) ( ((len)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1) )
/* Netlink 頭部長度 */
#define NLMSG_HDRLEN ((int) NLMSG_ALIGN(sizeof(struct nlmsghdr)))
/* 計算訊息資料len的真實訊息長度(訊息體 + 訊息頭)*/
#define NLMSG_LENGTH(len) ((len) + NLMSG_HDRLEN)
/* 巨集NLMSG_SPACE(len)返回不小於NLMSG_LENGTH(len)且位元組對齊的最小數值 */
#define NLMSG_SPACE(len) NLMSG_ALIGN(NLMSG_LENGTH(len))
/* 巨集NLMSG_DATA(nlh)用於取得訊息的資料部分的首地址,設定和讀取訊息資料部分時需要使用該巨集 */
#define NLMSG_DATA(nlh) ((void*)(((char*)nlh) + NLMSG_LENGTH(0)))
/* 巨集NLMSG_NEXT(nlh,len)用於得到下一個訊息的首地址, 同時len 變為剩餘訊息的長度 */
#define NLMSG_NEXT(nlh,len) ((len) -= NLMSG_ALIGN((nlh)->nlmsg_len),
(struct nlmsghdr*)(((char*)(nlh)) + NLMSG_ALIGN((nlh)->nlmsg_len)))
/* 判斷訊息是否 >len */
#define NLMSG_OK(nlh,len) ((len) >= (int)sizeof(struct nlmsghdr) && \(nlh)->nlmsg_len >= sizeof(struct nlmsghdr) && \(nlh)->nlmsg_len <= (len))
/* NLMSG_PAYLOAD(nlh,len) 用於返回payload的長度*/
#define NLMSG_PAYLOAD(nlh,len) ((nlh)->nlmsg_len - NLMSG_SPACE((len)))
Linux下雖然也有AF_ROUTE族套接字,但是這個定義只是個別名,請看 /usr/include/linux/socket.h, line 145: #define AF_ROUTE AF_NETLINK /* Alias to emulate 4.4BSD */ 可見在Linux核心當中真正實現routing socket的是AF_NETLINK族套接字。AF_NETLINK族套接字像一個連線使用者空間和核心的雙工管道,通過它,使用者程序可以修改核心執行引數、讀取和設定路由資訊、控制特定網絡卡的up/down狀態等等,可以說是一個管理網路資源的絕佳途徑。
上面提到的nlmsghdr,它只是一個資訊頭,後面可以接任意長的資料,上面的例子我們只是填充了一個字串,實際上這裡是針對某一需求所採用的特定資料結構。先來看nlmsghdr:
struct nlmsghdr { _u32 nlmsg_len; /* Length of msg including header */ _u32 nlmsg_type; /* 操作命令 */ _u16 nlmsg_flags; /* various flags */ _u32 nlmsg_seq; /* Sequence number */ _u32 nlmsg_pid; /* 程序PID */ }; /* 緊跟著是實際要傳送的資料,長度可以任意 */
其中nlmsg_type決定這次要執行的操作,如查詢當前路由表資訊,所使用的就是RTM_GETROUTE。標準nlmsg_type包括:NLMSG_NOOP, NLMSG_DONE, NLMSG_ERROR等。根據採用的nlmsg_type不同,還要選取不同的資料結構來填充到nlmsghdr後面: 操作 資料結構 RTM_NEWLINK ifinfomsg RTM_DELLINK RTM_GETLINK RTM_NEWADDR ifaddrmsg RTM_DELADDR RTM_GETADDR RTM_NEWROUTE rtmsg RTM_DELROUTE RTM_GETROUTE RTM_NEWNEIGH ndmsg/nda_chcheinfo RTM_DELNEIGH RTM_GETNEIGH RTM_NEWRULE rtmsg RTM_DELRULE RTM_GETRULE RTM_NEWQDISC tcmsg RTM_DELQDISC RTM_GETQDISC RTM_NEWTCLASS tcmsg RTM_DELTCLASS RTM_GETTCLASS RTM_NEWTFILTER tcmsg RTM_DELTFILTER
由於情形眾多,這裡以從核心讀取IPV4路由表資訊為例。從上面表看,nlmsg_type一定使用RTM_xxxROUTE操作,對應的資料結構是rtmsg。既然是讀取,那麼應該是RTM_GETROUTE了。
structrtmsg {
unsigned char rtm_family; /* 路由表地址族 */
unsigned char rtm_dst_len; /* 目的長度 */
unsigned char rtm_src_len; /* 源長度 */ (2.4.10標頭檔案的註釋標反了?)
unsigned char rtm_tos; /* TOS */
unsigned char rtm_table; /* 路由表選取 */
unsigned char rtm_protocol; /* 路由協議 */
unsigned char rtm_scope;
unsigned char rtm_type;
unsigned int rtm_flags;
};
對於RTM_GETROUTE操作來說,我們只需指定兩個成員:rtm_family:AF_INET, rtm_table: RT_TABLE_MAIN。其他成員都初始化為0即可。將這個結構體跟nlmsghdr結合起來,得到我們自己的新結構體:
struct {
struct nlmsghdr nl;
struct rtmsg rt;
}req;
填充好rt結構之後,還要調整nl結構相應成員的值。Linux定義了多個巨集來處理nlmsghdr成員的值,我們這裡用到的是NLMSG_LENGTH(size_tlen);
req.nl.nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg));
這將計算nlmsghdr長度與rtmsg長度的和(其中包括了將rtmsg進行4位元組邊界對齊的調整),並存儲到nlmsghdr的nlmsg_len成員中。
接下來要做的就是將這個新結構體req放到sendmsg()函式的msghdr.iov處,並呼叫函式:sendmsg(sockfd, &msg, 0);
接下來的操作是recv()操作,從該套接字讀取核心返回的資料,並進行分析處理。
recv(sockfd, p, sizeof(buf) - nll, 0);
其中p是指向一個緩衝區buf的指標,nll是已接收到的nlmsghdr資料的長度。
由於核心返回資訊是一個位元組流,需要呼叫者檢查訊息結尾。這是通過檢查返回的nlmsghdr的nlmsg_type是否等於NLMSG_DONE來完成的。返回的資料格式如下:
-----------------------------------------------------------
| nlmsghdr+route entry | nlmsghdr+route entry | .........
-----------------------------------------------------------
| 解出routeentry
V
-----------------------------------------------------------
| dst_addr | gateway | Output interface| ...............
-----------------------------------------------------------
可以看出,返回訊息由多個(nlmsghdr+ route entry)組成,當某個nlmsghdr的nlmsg_type == NLMSG_DONE時就表示資訊輸出已經完畢。而每一個routeentry由多個rtattr結構體組成,每個結構體表示該路由項的某個屬性,如目的地址,閘道器等等。根據這個示意圖我們就能夠輕鬆解析需要的資料了。
本節通過詳解一個簡單的例項程式來說明使用者程序通過netlink機制如何主動向核心發起會話。在該程式中,使用者程序向核心傳送一段字串,核心接收到後再將該字串後再重新發給使用者程序。使用者態程式netlink是一種特殊的套接字,在使用者態除了一些引數的傳遞對其使用的方法與一般套接字無較大差異。
1.巨集與資料結構的定義
在使用netlink進行使用者程序和核心的資料互動時,最重要的是定義好通訊協議。協議一詞直白的說就是使用者程序和核心應該以什麼樣的形式傳送資料,以什麼樣的形式接收資料。而這個“形式”通常對應程式中的一個特定資料結構。
本文所演示的程式並沒有使用netlink已有的通訊協議,因此我們自定義一種協議型別NETLINK_TEST。
1 #defineNETLINK_TEST 18
2 #define MAX_PAYLOAD 1024
3
4 struct req {
5 struct nlmsghdr nlh;
6 char buf[MAX_PAYLOAD];
7 };
除此之外,我們應該再自定義一個數據報型別req,該結構包含了netlink資料包頭結構的變數nlh和一個MAX_PAYLOAD大小的緩衝區。這裡我們為了演示簡單,並沒有像上文中描述的那樣將一個特定資料結構與nlmsghdr封裝起來。
2.建立netlink套接字
要使用netlink,必須先建立一個netlink套接字。建立方法同樣採用socket(),只是這裡需要注意傳遞的引數:1 int sock_fd;
2 sock_fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_TEST);
3 if (sock_fd < 0) {
4 eprint(errno, "socket", __LINE__);
5 return errno;
6 }第一個引數必須指定為PF_NETLINK或AF_NETLINK。第二個引數必須指定為SOCK_RAW或SOCK_DGRAM,因為netlink提供的是一種無連線的資料報服務。第三個引數則指定具體的協議型別,我們這裡使用自定義的協議型別NETLINK_TEST。另外,eprint()是一個自定義的出錯處理函式,實現如下:1 void eprint(int err_no, char *str, int line)
2 {
3 printf("Error %d in line %d:%s() with %s\n",err_no, line, str, strerror(errno));
4 }
3.將本地套接字與源地址繫結
將本地的套接字與源地址進行繫結通過bind()完成。在繫結之前,需要將源地址進行初始化,nl_pid欄位指明發送訊息一方的pid,nl_groups表示多播組的掩碼,這裡我們並沒有涉及多播,因此預設為0。
1 struct sockaddr_nl src_addr;
2 memset(&src_addr, 0, sizeof(src_addr));
3 src_addr.nl_family = AF_NETLINK;
4 src_addr.nl_pid = getpid();
5 src_addr.nl_groups = 0;
6
7 if (bind(sock_fd, (struct sockaddr *)&src_addr, sizeof(src_addr)) < 0){
8 eprint(errno, "bind", __LINE__);
9 return errno;
10 }
4.初始化msghdr結構
使用者程序最終傳送的是msghdr結構的訊息,因此必須對這個結構進行初始化。而此結構又與sockaddr_nl,iovec和nlmsghdr三個結構相關,因此必須依次對這些資料結構進行初始化。首先初始化目的套接字的地址結構,該結構與源套接字地址結構初始化的方法稍有不同,即nl_pid必須為0,表示接收方為核心。
1 struct sockaddr_nl dest_addr;
2 memset(&dest_addr, 0, sizeof(dest_addr));
3 dest_addr.nl_family = AF_NETLINK;
4 dest_addr.nl_pid = 0;
5 dest_addr.nl_groups = 0;
接下來對req型別的資料報進行初始化,即依次對其封裝的兩個資料結構初始化:1 struct req r;
2 r.nlh.nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
3 r.nlh.nlmsg_pid = getpid();
4 r.nlh.nlmsg_flags = 0;
5 memset(r.buf, 0, MAX_PAYLOAD);
6 strcpy(NLMSG_DATA(&(r.nlh)), "hello, I am edsionte!");
這裡的nlmsg_len為為sizeof(struct nlmsghdr)+MAX_PAYLOAD的總和。巨集NLMSG_SPACE會自動將兩者的長度相加。接下來對緩衝區向量iov進行初始化,讓iov_base欄位指向資料報結構,而iov_len為資料報長度。1 struct iovec iov;
2 iov.iov_base = (void *)&r;
3 iov.iov_len = sizeof(r);一切就緒後,將目的套接字地址與當前要傳送的訊息msg繫結,即將目的套接字地址複製給msg_name。再將要傳送的資料iov與msg_iov繫結,如果一次性要傳送多個數據包,則建立一個iovec型別的陣列。
1 struct msghdr msg;
2 msg.msg_name = (void *)&dest_addr;
3 msg.msg_namelen = sizeof(dest_addr);
4 msg.msg_iov = &iov;5msg.msg_iovlen = 1;
5.
向核心傳送訊息傳送訊息則很簡單,通過sendmsg函式即可完成,前提是正確的建立netlink套接字和要傳送的訊息。1 if (sendmsg(sock_fd, &msg, 0) < 0) {2 eprint(errno, "sendmsg", __LINE__);
3 return errno;
4 }
6.接受核心發來的訊息如果使用者程序需要接收核心傳送的訊息,則需要通過recvmsg完成,只不過在接收之前需要將資料報r重新初始化,因為傳送和接收時傳遞的資料結構可能是不同的。為了簡單演示netlink的用法,本文所述的使用者程序傳送的是一段字串,這一點從資料報結構req的定義可以看出。而核心向用戶程序傳送的也是一段字串,具體情況下面將會具體說明。
1 memset(&r.nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));
2 if (recvmsg(sock_fd, &msg, 0) < 0) {
3 eprint(errno,"recvmsg", __LINE__);
4 return errno;
5 }
6
7 printf("Received message payload:%s\n", (char*)NLMSG_DATA(&r.nlh));
8 close(sock_fd);
接收完畢後,通過專門的巨集NLMSG_DATA對資料報進行操作。 netlink對資料報的的訪問和操作都是通過一系列標準的巨集NLMSG_XXX來完成的,具體的說明可以通過man
netlink檢視。這裡的NLMSG_DATA傳遞進去的是nlh,但它獲取的是緊鄰nlh的真正資料。本程式中傳遞的是字串,所以取資料時候用char
*強制型別轉換,如果傳遞的是其他資料結構,則相應轉換資料型別即可。
核心模組netlink既然是一種使用者態和核心態之間的雙向通訊機制,那麼除了編寫使用者程式還要編寫核心模組,也就是說使用者程序和核心模組之間對資料的處理要彼此對應起來。
1.核心模組載入和解除安裝函式核心模組載入函式主要通過netlink_kernel_create函式申請伺服器端的套接字nl_sk,核心中對套接字表示為sock結構。另外,在建立套接字時還需要傳遞和使用者程序相同的netlink協議型別NETLINK_TEST。建立套接字函式的第一個引數預設為init_net,第三個引數為多播時使用,我們這裡不使用多播因此預設值為0。nl_data_handler是一個鉤子函式,每當核心接收到一個訊息時,這個鉤子函式就被回撥對使用者資料進行處理。
1 #define NETLINK_TEST 17
2 struct sock *nl_sk = NULL;
3 static int __init hello_init(void)
4 {
5 printk("hello_init is starting..\n");
6 nl_sk = netlink_kernel_create(&init_net,NETLINK_TEST, 0, nl_data_ready, NULL, THIS_MODULE);
7 if (nl_sk == 0)
8 {
9 printk("can not createnetlink socket.\n");
10 return -1;
11 }
12 return 0;
13 }
核心模組解除安裝函式所做的工作與載入函式相反,通過sock_release函式釋放一開始申請的套接字。1 static void __exit hello_exit(void)2 {
3 sock_release(nl_sk->sk_socket);
4 printk("hello_exit is leaving..\n");
5 }
2.鉤子函式的實現在核心建立netlink套接字時,必須繫結一個鉤子函式,該鉤子函式原型為:
1 void (*input)(struct sk_buff *skb);
鉤子函式的實現主要是先接收使用者程序傳送的訊息,接收以後核心再發送一條訊息到使用者程序。在鉤子函式中,先通過skb_get函式對套接字緩衝區增加一次引用值,再通過nlmsg_hdr函式獲取netlink訊息頭指標nlh。接著使用NLMSG_DATA巨集獲取使用者程序傳送過來的資料str。除此之外,再打印發送者的pid。
1 void nl_data_handler(struct sk_buff *__skb)
2 {
3 struct sk_buff *skb;
4 struct nlmsghdr *nlh;
5 u32 pid;
6 int rc;
7 char str[100];
8 int len = NLMSG_SPACE(MAX_PAYLOAD);
9
10 printk("read data..\n");
11 skb = skb_get(__skb);
12
13 if (skb->len >= NLMSG_SPACE(0)) {
14 nlh = nlmsg_hdr(skb);
15 printk("Recv:%s\n", (char *)NLMSG_DATA(nlh));
16 memcpy(str, NLMSG_DATA(nlh),sizeof(str));
17 pid = nlh->nlmsg_pid;
18 printk("pid is%d\n", pid);
19 kfree_skb(skb);
接下來重新申請一個套接字緩衝區,為核心傳送訊息到使用者程序做準備,nlmsg_put函式將填充netlink資料報頭。接下來將使用者程序傳送的字串複製到nlh緊鄰的資料緩衝區中,等待核心傳送。netlink_unicast函式將以非阻塞的方式傳送資料包到使用者程序,pid具體指明瞭接收訊息的程序。
1 skb = alloc_skb(len,GFP_ATOMIC);
2 if (!skb){
3 printk(KERN_ERR"net_link: allocate failed.\n");
4 return;
5 }
6 nlh = nlmsg_put(skb, 0, 0, 0,MAX_PAYLOAD, 0);
7 NETLINK_CB(skb).pid = 0;
8
9 memcpy(NLMSG_DATA(nlh), str,sizeof(str));
10 printk("net_link: goingto send.\n");
11 rc = netlink_unicast(nl_sk,skb, pid, MSG_DONTWAIT);
12 if (rc < 0) {
13 printk(KERN_ERR"net_link: can not unicast skb (%d)\n", rc);
14 }
15 printk("net_link: sendis ok.\n");
16 }
17 }
這樣就完成了核心模組的編寫,它與使用者程序通訊共同完成資料互動。