原文地址:http://people.redhat.com/nhorman/papers/netlink.pdf
译文:
1 介绍
在Linux和Unix的众多发行版中的网络配置功能, 都是编程者事后需求的功能, 导致像添加路由、邻居表条目和配置接口等功能有着很多杂乱的方法, 比如raw socket, ioctl调用以及专门的伪网络协议等方法。在Linux 2.4内核中, 开发者努力实现了一种更标准化的配置网络的方法。这种方法被命名为netlink sockets, 它旨在创建一个适合所有网络控制方面的通信框架,虽然建立的netlink子系统不是完善的, 但是这是一种新的网络配置方法, 也是可靠的基础。此文档旨在介绍如何使用netlink socket族和其实现的协议。
本文假设读者有C和socket编程的基础。
2 Netlink 地址族
2.1 socket创建
netlink地址族使用标准的BSD socket API作为用户空间程序和内核交互的使者。创建一个netlink套接字和创建其它套接字是类似的方式。
socket fd=socket(AF_NETLINK, SOCK_RAW, protocol);
地址族参数总是AF_NETLINK, 并且类型值总是SOCK_RAW, 唯一可变的参数 就是协议protocol域, 此域将会继续添加可选项, 增加了它的可配置性, 下表是protocol的可选项(来自linux-2.6.32 kernel)。
#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 /* Firewalling hook */ #define NETLINK_INET_DIAG 4 /* INET 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_L2TP 20 #if defined(CONFIG_RTL_819X) #define NETLINK_RTK_DEBUG 21 #define NETLINK_RTK_FILTER 22 #define NETLINK_MULTICAST_DELETE 23 #define NETLINK_RTK_FB 24 #define NETLINK_RTK_HW_QOS 25 #endif
2.2 发送和接收数据包
netlink套接字是无连接的, 收发的数据报就表示类似UDP套接字, 发送数据报通过sendto或者sendmsg系统调用, 用recvfrom或者recvmsg接收数据报。 注意, netlink套接字不使用send和recv交互, 这是因为netlink套接字是无连接的。就像UDP套接字, netlink消息是数据报格式, 虽然netlink消息头部有一些机制设计用于编程者增加协议的可靠性, 但仍旧不能保证连接是可靠的。
2.3 netlink套接字地址结构
struct sockaddr_nl 是它的地址结构, 用于netlink套接字的接收和发送, 定义如下:
struct sockaddr_nl { sa_family_t nl_family; /* AF_NETLINK */ unsigned short nl_pad; /* zero */ __u32 nl_pid; /* port ID */ __u32 nl_groups; /* multicast groups mask */ };
- nl_family:此域定义了消息的地址族, 应该总是AF_NETLINK
- nl_pad : 总是为0
- nl_pid : 一般设置为本进程的pid或者填0理, 如果是进程接收来自内核netlink消息, 此域应为本进程PID, 如果是向内核发送netlink消息, 此域应置0
- nl_groups :用于指定多播组, 如对接收来自内核的netlink消息来说, 内核可将要发送的消息指定一个多播组, 那么此消息就会发向同一多播组的接收端。而对于进程发送消息来讲, 设置了多播组就只会发送到此多播组的内核接收端。此域是32位, 最多可支持32个多播组。
3 Netlink 消息格式
与每个IP消息头一样, netlink消息也有类似的头部, 然而和其他协议不同的是, 编程者需要为每个数据包构建这个头部(一般的TCP/UDP socket都是直接操作报文的payload部分), 这个头部用来保存每个消息和格式的元数据, 这个头部也是netlink协议的基础。
struct nlmsghdr { u32 nlmsg_len; u16 nlmsg_type; u16 nlmsg_flags; u32 nlmsg_seq; u32 nlmsg_pid; }
- nlmsg_len:每个netlink头后面跟随者0个或者多个字节的辅助数据, 此域记录了消息的整个长度, 包括了头部在内。
- nlmsg_type:此域标识了头部后面数据的格式。此域的取值和2.1中的protocol有关。
- nlmsg_flags:此域标识了消息由谁进行处理和解析, 有如下取值
NLM_F_REQUEST - 这个标志暗示这是一个请求消息, 它应该被设置到大多数应用程序的初始化消息中。 NLM_F_ACK - 这个标志暗示对前一个请求消息的回应, 序列号和pid值能够辨别请求的回应报文。 NLM_F_ECHO - 这个标志表示发出的报文将回响给发送进程一份。 NLM_F_MULTI - 这个标志表示此消息是多个消息的一部分, 可用宏NLMSG_NEXT获得下一个消息。 NLM_F_ROOT - 用于请求多个netlink消息, 有此标志的请求消息表示请求回复整个条目表而不是一条, 回复的报文通常是 NLM_F_MULTI标志的。注意:此标志只适合特定的nlmsg_type才有效。 NLM_F_ATOMIC - 标志任何通过get-->回复报文的过程都是原子的, 防止在中间有资源改变引起歧义。 NLM_F_REPLACE - 替换条目表中的一条, 可用于覆盖条目表。 NLM_F_CREATE - 在条目表中设置一个新的条目(比如添加一条新路由) NLM_F_APPEND - 在条目表末尾添加一个条目 NLM_F_EXCL - 结合了CREATE和APPEND, 如果要添加的条目已存在将返回错误(推荐使用)
-
nlmsg_seq:seq用来联系请求和回复报文, 顺序标志的作用
- nlmsg_pid:和seq类似
3.2 netlink 实用宏
#define NLMSG_ALIGNTO 4
/* 返回不小于len的以4字节对齐的最近数字 (ex: len=9 --> 返回值为12)*/ #define NLMSG_ALIGN(len) ( ((len)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1) )
/* 返回netlink消息头的字节数, 且务必是以4字节对齐的 */ #define NLMSG_HDRLEN ((int) NLMSG_ALIGN(sizeof(struct nlmsghdr)))
/* 返回'len + netlink消息头长度' 一般len指定为消息的payload长度, 此宏可用来填充nlmsg_len域 */ #define NLMSG_LENGTH(len) ((len)+NLMSG_ALIGN(NLMSG_HDRLEN))
/* 返回值同 NLMSG_LENGTH 宏一样有效, 主要是确保整个netlink消息长度4字节对齐(nlmsg_len域是否最好用此域来填充) */ #define NLMSG_SPACE(len) NLMSG_ALIGN(NLMSG_LENGTH(len))
/* 返回指向netlink消息payload处的指针(nlh 参数一般是指向struct nlmsghdr结构的指针) */
#define NLMSG_DATA(nlh) ((void*)(((char*)nlh) + NLMSG_LENGTH(0))) ----------------------------- | struct nlmsghdr | payload | ----------------------------- | NLMSG_DATA(nlh)
/* 返回指向下一个netlink消息的指针 */ /* 很多netlink回应由多个netlink消息组成 */ *参数: nlh --> 指向struct nlmsghdr结构的指针 len --> 一般为recvmsg函数的返回值(整个消息的长度) #define NLMSG_NEXT(nlh,len) ((len) -= NLMSG_ALIGN((nlh)->nlmsg_len), (struct nlmsghdr*)(((char*)(nlh)) + NLMSG_ALIGN((nlh)->nlmsg_len))) --------------------------------------------------------- | struct nlmsghdr | payload | struct nlmsghdr | payload | --------------------------------------------------------- | | nlh | NLMSG_NEXT(nlh,len) |--------------------------len--------------------------| *此宏先用len(整个消息的长度)减掉第一个消息的长度, 如果为0, 表示没有下一个消息了, 返回空, 若为非0, 表示后边还有消息, 指针移动指向下一个消息处并返回.
/* 确保nlh指向的消息大小不大于len */ *参数: nlh --> 指向struct nlmsghdr结构的指针 len --> 一般为recvmsg函数的返回值 #define NLMSG_OK(nlh,len) ((len) >= (int)sizeof(struct nlmsghdr) && (nlh)->nlmsg_len >= sizeof(struct nlmsghdr) && (nlh)->nlmsg_len <= (len))
3.3 总结
下图是netlink消息的内存分布图:
4 NETLINK_FIREWALL 协议
由3可知, netlink消息头nlmsg_type成员的取值和2.1中的protocol有关, 现在就介绍protocol为NETLINK_FIREWALL时的情况。
这个协议是非常有用的开发协议。首先它是有意被设计来在用户空间来调试iptables模块的架构,这个协议与很多iptables模块相关联。ip-queue模块就是其中一个(详见:http://blog.csdn.net/u010807313/article/details/9236581),在创建此协议的netlink套接字之前都需要先安装相关模块,发往相关模块的报文都会同样发给由NETLINK_FIREWALL协议创建的netlink套接字, 由此实现在用户空间监听流经netfilter的报文的目的。例如:
iptables -I OUTPUT -j QUEUE -p tcp –destination-port 7551
上面一条命令表示在OUTPUT链上, 发往端口7551的TCP报文都交给QUEUE链来处理, 而ip-queue模块正好和NETLINK_FIREWALL协议的套接字关联(内核实现的), 所以套接字同样也会收到这样的报文, 实现了监听的目的, 自行修改iptables命令可达到监听多种类型报文的目的。
4.1 创建和使用
socket fd=socket(AF_NETLINK, SOCK_RAW, NETLINK_FIREWALL);
此协议没有使用多播组, 所以地址结构struct sockaddr_nl中的nl_groups应该总是设置为0,并且此协议的套接字不需要bind函数,因为报文只是在进程和内核中传输,所以从进程发向内核的报文struct sockaddr_nl中的nl_pid应该设置为0。
4.2 消息类型
NETLINK_FIREWALL协议的套接字有三种消息类型(如3中所述, 成员nlmsg_type的取值),每个消息类型都有它各自的数据结构来描述。
- IPQM_MODE
- IPQM_PACKET < 这个是内核向用户空间返回的报文类型 >
- IPQM_VERDICT
4.2.1 IPQM_MODE
此类型是使用NETLINK_FIREWALL协议需要第一个发向内核的包, 内核收到之后才会将匹配的报文从内核发至用户空间的netlink套接字。此报文的数据结构如下, 它是紧随在struct nlmsghdr之后的:
typedef struct ipq mode msg { unsigned char value; size t range; };
value的取值有三种:
- IPQ_COPY_NONE - 不常用, 设置此值发给内核将导致iptables将所有到QUEUE链的报文丢弃。
- IPQ_COPY_META - 表示我希望内核返回报文的元数据(我理解是struct nlmsghdr + struct ipq_packet_msg 两个头部)部分。
- IPQ_COPY_PACKET - 表示希望内核返回报文, 报文长度由range控制, 若range为0表示返回整个报文。如果你需要在用户空间分析流经QUEUE链的报文应该设置此项并将range设置为0。
range:
此域只在value = IPQ_COPY_PACKET时才有效。
也就是说, 用户进程使用IPQM_MODE类型的报文告诉内核, 我需要你返回给我的报文是什么样的(不需要 or 要元数据 or 要range长的报文)
4.2.2 IPQM_PACKET
这个类型的报文是根据4.2.1之后内核根据需求返回的报文。只要之前设置的value不是IPQ_COPY_NONE, socket就会收到此类型的报文, 结构如下:
typedef struct ipq packet msg { unsigned long packet_id; unsigned long mark; long timestamp sec; long timestamp usec; unsigned int hook; char indev name[IFNAMSIZ]; char outdev name[IFNAMSIZ]; unsigned short hw_protocol; unsigned short hw_type; unsigned char hw_addrlen; unsigned char hw_addr[8]; size t data len; unsigned char payload[0]; };
- packet_id - 这个是内核产生的独一无二的标识, 在4.2.3中发送IPQM_VERDICT报文需要。
- mark - //
- timestap_sec - 报文抵达时间(秒)
- timestap_usec - 报文抵达时间(微秒)
- hook - 报文被重定向到QUEUE的hook number
- indev_name - //
- outdev_name - //
- hw_protocol - 通常是ETH_P_IP
- hw_type - 通常是ARPHDR_ETHER
- hw_addrlen - 通常为6
- hw_addr - 报文的源MAC地址
- data_len - payload数据长度
- payload - 柔性数组头部, 指向了payload数据的头部
4.2.3 IPQM_VERDICT
此类型的报文是在收到内核的回复报文之后, 用户经过自己的检测, 决定对此报文执行何种操作, IPQM_MODE --> IPQM_PACKET <----> IPQM_VERDICT, 是顺序的过程。也就是说, 只有你向内核发送IPQM_VERDICT说明了报文处理方式之后, 你才能recvmsg下一个到达QUEUE链的报文, 否则recvmsg会一直阻塞。结构如下:
typedef struct ipq verdict msg { unsigned int value; unsigned long id; size t data_len; unsigned char payload; };
value 指示了对报文的处理方式:
- NF_DROP - 立即丢弃报文
- NF_ACCEPT - 接收报文(不参与之后的iptables链了)
- NF_STOLEN - //
- NF_QUEUE - 不常使用
- NF_REPEAT - 将报文移入下一个iptables链
id - 指示了要对哪个报文进行处理, 对应4.2.2的packet_id成员, 这个成员唯一关联了一个进入QUEUE的报文
data_len - 指verdict报文的payload数据长度, 因为verdict是用户发向内核的, 此域一般设置为0
payload - //
5 NETLINK_ROUTE 协议
5.1 创建和使用
socket fd=socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
NETLINK_ROUTE协议是netlink套接字最大且是最成熟的协议, 它有它自己消息的处理宏(类似3.2节), 这些NETLINK_ROUTE的宏是为了添加它的辅助数据段和定制化特别的消息类型而做的。
每个族都有同样的命名空间和辅助数据结构, 辅助数据结构后又跟着一个或多个消息。
每个族都包含三个方法, NEW, DEL, GET; 这些方法用于创建、删除和接收路由相关条目。
5.2 NETLINK_ROUTE消息宏
NETLINK_ROUTE消息实际有自己的数据结构, 如下所示。
struct rtattr { unsigned short rta_len; unsigned short rta_type; }
下面是NETLINK_ROUTE的消息内存布局:
对于struct rtattr结构, 与netlink消息头struct nlmsghdr结构相似, 有一些宏进行辅助处理, 参考3.2节。
• int RTA OK(struct rtattr *rta, int rtabuflen); - Verify the data integrity of the data which succedes this rtattr header. • void * RTA DATA(struct rtattr *rta); - Return a pointer to the ancilliary data associated with this rtattr header. • struct rtattr *RTA NEXT(struct rtattr *rta); - Return a pointer to the next rtattr header in the chain. • unsigned int RTA PAYLOAD(struct rtattr *rta); - Return the length of the ancilliary data associated with the passed rtattr header. • unsigned int RTA LENGTH(unsigned int length); - Return the aligned length for the passed payload length. This value is assigned to the rta len field of the rtattr header • unsigned int RTA SPACE(unsigned int length); - Return the length of the ancilliary data, when aligned.
5.3 消息类型
在使用NETLINK_ROUTE协议的情况下, netlink控制块struct nlmsghdr中的nlmsg_type域标识了多种消息类型,举例如下:
5.3.1 LINK消息
LINK消息族允许设置和获取关于系统接口的消息nlmsg_type有如下取值:
- RTM_NEWLINK - 创建一个新接口/有一个新接口被创建
- RTM_DELLINK - 删除一个接口
- RTM_GETLINK - 接收一个接口消息
每个消息的辅助数据结构是struct ifinfomsg:
struct ifinfomsg { unsigned char ifi_family; unsigned short ifi_type; int ifi index; unsigned int ifi_flags; unsigned int ifi_change; };
5.3.2 LINK消息struct rtattr结构rta_type取值
5.3.3 LINK消息内存布局
5.4 其它消息类型
比如ADDR消息, ROUTE消息等和LINK消息类似, 不同的是它们有各自的struct ifinfomsg消息和支持不同的rta_type。