Netlink编程-使用NETLINK_INET_DIAG协议

本文转自:http://edsionte.com/techblog/archives/4140

Netlink可以使得内核和用户进程进行双向通信,前文已经介绍过用户进程主动发起会话请求的例子。在那个示例程序中,必须同时编写用户态程序和内核模块,因为他们之间通信的协议是我们自己设定的,并没有使用netlink已有的通信协议。如果使用netlink已有的通信协议,那么我们无需编写内核模块,只需编写用户态程序即可。
本文将说明如何在用户态使用NETLINK_INET_DIAG协议。
1.创建netlink套接字Netlink的使用方法与普通套接字并无太大差异,前文已经说明参数的差异,这里不再赘述。

struct sk_req { 
struct nlmsghdr nlh; 
struct inet_diag_req r; 
}; 

int main(int argc, char **argv) 
{ 
int fd; 
struct sk_req req; 
struct sockaddr_nl dest_addr; 
struct msghdr msg; 
char buf[8192]; 
char src_ip[20]; 
char dest_ip[20]; 
struct iovec iov; 

if ((fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_INET_DIAG)) < 0) { 
eprint(__LINE__, errno, "socket"); 
return -1; 
} 
}

2.发送消息到内核用户进程通过msghdr结构将消息发送到内核中,因此必须首先初始化msghdr类型的变量msg。该数据结构与iovec类型的变量iov和sockaddr_nl类型的变量dest_addr关联,iov指向数据缓冲区,dest_addr用于描述目的套接字地址。
这里需要将nlmsghdr结构中的nlmsg_type指定为TCPDIAG_GETSOCK,说明获取的是TCP套接字。同时需要将nlmsg_flags字段指定为NLM_F_REQUEST | NLM_F_ROOT,NLM_F_REQUEST是所有向内核发出消息请求的用户进程所必须所设置的,NLM_F_ROOT则指明返回所有的套接字。

req.nlh.nlmsg_len = sizeof(req); 
req.nlh.nlmsg_type = TCPDIAG_GETSOCK; 
req.nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_ROOT; 
req.nlh.nlmsg_pid = 0; 
memset(&req.r, 0, sizeof(req.r)); 
req.r.idiag_family = AF_INET; 
req.r.idiag_states = ((1 << TCP_CLOSING + 1) - 1); 
iov.iov_base = &req; 
iov.iov_len = sizeof(req); 
memset(&dest_addr, 0, sizeof(dest_addr)); 
dest_addr.nl_family = AF_NETLINK; 
dest_addr.nl_pid = 0; 
dest_addr.nl_groups = 0; 
memset(&msg, 0, sizeof(msg)); 
msg.msg_name = (void *)&dest_addr; 
msg.msg_namelen = sizeof(dest_addr); 
msg.msg_iov = &iov; 
msg.msg_iovlen = 1; 

数据缓冲区通过req结构来表示,它封装了两个数据结构nlmsghdr和inet_diag_req。前者用来表示netlink消息头,它是必须封装的数据结构。后者是NETLINK_INET_DIAG协议所特有的请求会话的数据结构,具体结构如下:

enum 
{ 
TCP_ESTABLISHED = 1, 
TCP_SYN_SENT, 
TCP_SYN_RECV, 
TCP_FIN_WAIT1, 
TCP_FIN_WAIT2, 
TCP_TIME_WAIT, 
TCP_CLOSE, 
TCP_CLOSE_WAIT, 
TCP_LAST_ACK, 
TCP_LISTEN, 
TCP_CLOSING 
}; 

idiag_states字段的每一位表示一个状态,因此通过位偏移可以将具体某个状态位置1。上述的实例程序中,将表示所有状态的位都置1,因此内核将向用户进程反馈所有状态的套接字。

if (sendmsg(fd, &msg, 0) < 0) { 
eprint(__LINE__, errno, "sendmsg"); 
return -1; 
} 

初始化相关的数据结构之后,接下来用户进程通过sendmsg函数发送消息到内核中。
3.用户进程接收消息用户进程通过两层循环来接受并处理内核发送的消息。外层循环通过recvmsg函数不断接收内核发送的数据,在接收数据之前还要将新的数据缓冲区buf与iov进行绑定。内层循环将内核通过一次系统调用所发送的数据进行分批处理。对于本文所描述的NETLINK_INET_DIAG协议,内核每次向用户进程发送的消息通过inet_diag_msg结构描述:

struct inet_diag_req { 
__u8 idiag_family; /* Family of addresses. */ 
__u8 idiag_src_len; 
__u8 idiag_dst_len; 
__u8 idiag_ext; /* Query extended information */ 

struct inet_diag_sockid id; 

__u32 idiag_states; /* States to dump */ 
__u32 idiag_dbs; /* Tables to dump (NI) */ 11 }; 

这里需要特别注意的是inet_diag_req结构中的idiag_states字段,它用来表示内核将要反馈哪些状态的套接字到用户空间。用户空间通过一个枚举类型来表示套接字状态:

struct nlhdrmsg struct inet_idiag_msg || struct nlhdrmsg struct inet_idiag_msg || ……

按照这样的数据存储方式,内层循环要做的就是依次获取这些数据结构。由于每条数据报都至少封装了nlmsghdr结构,因此具体的处理方法通过NLMSG_XXX宏即可完成。

memset(buf, 0 ,sizeof(buf)); 
iov.iov_base = buf; 
iov.iov_len = sizeof(buf); 
while (1) { 
int status; 
struct nlmsghdr *h; 
msg = (struct msghdr) { 
(void *)&dest_addr, sizeof(dest_addr), 
&iov, 1, NULL, 0, 0 
}; 
status = recvmsg(fd, &msg, 0); 
if (status < 0) { 
if (errno == EINTR) 
continue; 
eprint(__LINE__, errno, "recvmsg"); 
continue; 
} 
if (status == 0) { 
printf("EOF on netlink\n"); 
close(fd); 
return 0; 
} 
h = (struct nlmsghdr *)buf; 
while (NLMSG_OK(h, status)) { 
struct inet_diag_msg *pkg = NULL; 
if (h->nlmsg_type == NLMSG_DONE) { 
close(fd); 
printf("NLMSG_DONE\n"); 
return 0; 
} 
if (h->nlmsg_type == NLMSG_ERROR) { 
struct nlmsgerr *err; 
err = (struct nlmsgerr*)NLMSG_DATA(h); 
fprintf(stderr, "%d Error %d:%s\n", __LINE__, -(err->error), strerror(-(err->error))); 
close(fd); 
printf("NLMSG_ERROR\n"); 
return 0; 
} 
pkg = (struct inet_diag_msg *)NLMSG_DATA(h); 
print_skinfo(pkg); 
get_tcp_state(pkg->idiag_state); 
h = NLMSG_NEXT(h, status); 
}//while 
}//while 
close(fd); 
return 0; 

NLMSG_OK宏每次判断buf中的数据是否读取完毕,NLMSG_DATA取当前netlink消息头结构紧邻的inet_diag_msg结构,NLMSG_NEXT则取下一个netlink消息头结构。
pkg指向当前获取到的消息,接下来具体需求处理各个字段即可。上述程序中,print_skinfo函数打印pkg中的各个字段,get_tcp_state则是打印每个套接字连接的状态。