Netlink套接字是用于用户与内核间的通信,类似于socket的客户/服务端通信程序,内核与用户之间的通信是全双工的,也即通信既可以从用户端发起,又可以从内核发起,并且netlink可以自定义协议进行通信,但这时需要在内核中相应位置(具体位置忘了)自己定义相应的协议内容,并在netlink头文件定义相应协议的名字。一般使用系统已经定义好的常用协议NETLINK_ROUTE,NETLINK_FIREWALL,NETLINK_APRD,NETLINK_ROUTE6_FW等,这些协议可以直接调用,用于与内核交互,功能强大。
Netlink的使用主要要熟悉netlink消息的封装与解封装,与普通socket不同,netlink套接字的封装与解封装需要自己进行 ,这就需要熟悉netlink套接字消息的结构,熟练掌握一些宏的应用(这些都是有套路的,需要花时间来掌握)。
首先来看netlink socket支持的一些协议:
- NETLINK_ROUTE:用户空间路由damon,如BGP,OSPF,RIP和内核包转发模块的通信信道。用户空间路由damon通过此种netlink协议类型更新内核路由表
- NETLINK_FIREWALL:接收IPv4防火墙代码发送的包
- NETLINK_NFLOG:用户空间iptable管理工具和内核空间Netfilter模块的通信信道
- NETLINK_ARPD:用户空间管理arp表
1.首先创建一个套接字
int socket(int domain,int type, int protocol)
domain指代地址族,填AF_NETLINK,套接字类型不是SOCK_RAW就是SOCK_DGRAM,因为netlink是一个面向数据报的服务。
protocol选择该套接字使用那种netlink特征。以下是几种预定义的协议类型:NETLINK_ROUTE,NETLINK_FIREWALL,NETLINK_APRD,NETLINK_ROUTE6_FW。你也可以非常容易的添加自己的netlink协议。
2.绑定套接字与地址
bind(fd, (struct sockaddr*)&nladdr, sizeof(nladdr));关于nladdr,类似于sockaddr_in,是netlink套接字中特有的地址类型
struct sockaddr_nl
{
sa_family_t nl_family; /* AF_NETLINK */
unsigned short nl_pad; /* zero */
__u32 nl_pid; /* process pid */
__u32 nl_groups; /* mcast groups mask */
} nladdr;
pthread_self() << 16 | getpid();通过这种方法,同一个进程中的不同线程可以有同一种netlink协议类型的netlink套接字字段 nl_pad 当前没有使用,因此要总是设置为 0
字段 nl_groups用于指定多播组,若用于单播则置为0
3.发送netlink消息
使用netlink套接字和内核进行交互时,要向内核发送消息,一般使用sendmsg发送消息,recvmsg接收消息,主要是消息的封装和解封装
对于netlink消息,有一个特定的消息头,它也被称为netlink控制块:
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 */
}nlh;
字段 nlmsg_len指定消息的总长度,包括紧跟该结构的数据部分长度以及该结构的大小,字段 nlmsg_type用于应用内部定义消息的类型,它对 netlink内核实现是透明的,因此大部分情况下设置为 0,字段 nlmsg_flags 用于设置消息标志,可用的标志包括:
标志NLM_F_REQUEST用于表示消息是一个请求,所有应用首先发起的消息都应设置该标志。
标志NLM_F_MULTI用于指示该消息是一个多部分消息的一部分,后续的消息可以通过宏NLMSG_NEXT来获得。
宏NLM_F_ACK表示该消息是前一个请求消息的响应,顺序号与进程ID可以把请求与响应关联起来。
标志NLM_F_ECHO表示该消息是相关的一个包的回传。
标志NLM_F_ROOT被许多 netlink协议的各种数据获取操作使用,该标志指示被请求的数据表应当整体返回用户应用,而不是一个条目一个条目地返回。有该标志的请求通常导致响应消息设置 NLM_F_MULTI标志。注意,当设置了该标志时,请求是协议特定的,因此,需要在字段 nlmsg_type 中指定协议类型。
标志 NLM_F_MATCH表示该协议特定的请求只需要一个数据子集,数据子集由指定的协议特定的过滤器来匹配。
标志 NLM_F_ATOMIC指示请求返回的数据应当原子地收集,这预防数据在获取期间被修改。
标志 NLM_F_DUMP未实现。
标志 NLM_F_REPLACE用于取代在数据表中的现有条目。
标志 NLM_F_EXCL_用于和 CREATE和 APPEND配合使用,如果条目已经存在,将失败。
标志 NLM_F_CREATE指示应当在指定的表中创建一个条目。
标志 NLM_F_APPEND指示在表末尾添加新的条目。
内核需要读取和修改这些标志,对于一般的使用,用户把它设置为 0就可以,只是一些高级应用(如 netfilter和路由 daemon需要它进行一些复杂的操作),字段 nlmsg_seq和 nlmsg_pid用于应用追踪消息,前者表示顺序号,后者为消息来源进程 ID。
结构 struct iovec用于把多个消息通过一次系统调用来发送
struct iovec iov;
iov.iov_base = (void *)nlh;
iov.iov_len = nlh->nlmsg_len; 用于发送消息的msg,真正发送的是msg,前面都是为msg的封装做准备工作struct msghdr msg;msg.msg_name = (void *)&(nladdr); 这个nladdr是目的地址内核的nladdr,ni_pid=0,msg.msg_namelen = sizeof(nladdr); msg.msg_iov = &iov;msg.msg_iovlen = 1;
msg封装完毕后,即可发送出去
sendmsg(fd, &msg, 0); 当信息发送出去,内核会给出回应,此时用户用recvmsg接收消息,并解封装,获取自己想要的消息(解封装有一些固定的流程,尤其是宏的调用)
接收部分:
1: struct sockaddr_nl nladdr;
2: struct msghdr msg;
3: struct iovec iov;
4:
5: iov.iov_base = (void *)nlh; 接收时之前发送的消息头要清空
6: iov.iov_len = MAX_NL_MSG_LEN;
7: msg.msg_name = (void *)&(nladdr); 这个nladdr是发送时的nladdr
8: msg.msg_namelen = sizeof(nladdr);
9:
10: msg.msg_iov = &iov;
11: msg.msg_iovlen = 1;
12: recvmsg(fd, &msg, 0); 接下来是对msg的解封装: 以下是对通过netlink获取主路由网关的源码:
#include <arpa/inet.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <string.h>
#include <asm/types.h>
#include <linux/netlink.h>
#include <linux/socket.h>
#include <errno.h>
#include <netinet/in.h>
#include <linux/rtnetlink.h>
#include <net/if.h>
#define MAX_PAYLOAD 1024 // maximum payload size
int main(int argc, char* argv[])
{
int state;
struct sockaddr_nl src_addr, dest_addr;
struct nlmsghdr *nlh = NULL;
struct iovec iov;
struct msghdr msg;
int sock_fd, retval;
int state_smg = 0;
struct route_info{
u_int dstAddr;
u_int srcAddr;
u_int gateWay;
char ifName[IF_NAMESIZE];
};
int len;
struct rtmsg *rtm;
struct rtattr *tb [RTA_MAX + 1];
u_char flags = 0;
int table;
void *dest;
void *gate;
struct rtattr *rta;
int max;
char gateway[60]={0};
struct in_addr destaddr;
struct in_addr gateaddr;
int rtlen;
struct rtattr *rtAttr;
struct route_info rtInfo;
sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
if(sock_fd == -1){
printf("error getting socket: %s", strerror(errno));
return -1;
}
bzero(&gateaddr,sizeof(gateaddr));
// To prepare binding
memset(&msg,0,sizeof(msg));
memset(&src_addr, 0, sizeof(src_addr));
src_addr.nl_family = AF_NETLINK;
src_addr.nl_pid = getpid(); // 用户进程id src_addr源地址的设定
src_addr.nl_groups = 0; // 单播模式
retval = bind(sock_fd, (struct sockaddr*)&src_addr, sizeof(src_addr)); // netlink套接字与源地址绑定
if(retval < 0)
{
printf("bind failed: %s", strerror(errno));
close(sock_fd);
return -1;
}
// To prepare recvmsg
nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD));
if(!nlh)
{
printf("malloc nlmsghdr error!\n");
close(sock_fd);
return -1;
}
memset(&dest_addr,0,sizeof(dest_addr)); //dest_addr的定义
dest_addr.nl_family = AF_NETLINK;
dest_addr.nl_pid = 0; //pid=0表示内核
dest_addr.nl_groups = 0;
nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD); // nlmsghdr的封装,即netlink消息头
nlh->nlmsg_type=RTM_GETROUTE;
nlh->nlmsg_pid = getpid();
nlh->nlmsg_flags = NLM_F_ROOT | NLM_F_MATCH | NLM_F_REQUEST; //设置netlink消息头的标志位
iov.iov_base = (void *)nlh; //iovec的封装,base部分指向前面的nlmsghdr
iov.iov_len = NLMSG_SPACE(MAX_PAYLOAD);
// iov.iov_len = nlh->nlmsg_len;
memset(&msg, 0, sizeof(msg));
msg.msg_name = (void *)&dest_addr; //用于发送的msg,放入dest_addr
msg.msg_namelen = sizeof(dest_addr);
msg.msg_iov = &iov; //放入iovec
msg.msg_iovlen = 1;
state_smg = sendmsg(sock_fd,&msg,0); //向内核发送路由请求信息
if(state_smg == -1)
{
printf("get error sendmsg = %s\n",strerror(errno));
}
memset(nlh,0,NLMSG_SPACE(MAX_PAYLOAD)); //清空nlmsghdr
// printf("waiting received!\n");
// Read message from kernel
while(1){
state = recvmsg(sock_fd, &msg, 0); //收到内核发送回用户的路由消息
if(state<0)
{
printf("state<1");
return;
}
rtm =(struct rtmsg*) NLMSG_DATA (nlh ); //指向有用数据部分
if((rtm->rtm_family != AF_INET) || (rtm->rtm_table != RT_TABLE_MAIN)) //判断收到的类型和路由表是否为主路由表
return;
rtlen=RTM_PAYLOAD(nlh);
rtAttr=(struct rtattr *)RTM_RTA(rtm);
for(;RTA_OK(rtAttr,rtlen);rtAttr=RTA_NEXT(rtAttr,rtlen)) //for循环固定方法读取收到的信息,如果有网关的信息打印出来
{
switch(rtAttr->rta_type)
{ //RTA_NEXT用于轮循所有路由信息,比如先网关信息,RTA_NEXT之后就会查比如设备名,直到查完这个路由表所有信息再跳出for循环
case RTA_OIF:
if_indextoname(*(int *)RTA_DATA(rtAttr),rtInfo.ifName);
// printf("%s\n",rtInfo.ifName);
break;
case RTA_GATEWAY:
rtInfo.gateWay=*(u_int *)RTA_DATA(rtAttr);
gateaddr.s_addr=rtInfo.gateWay;
sprintf(gateway,"gateway:%s", (char *)inet_ntoa(gateaddr));
printf("%s\n",gateway);
break;
default:
break;
}
}
}
close(sock_fd);
return 0;
}