Linux内核工程导论–网络:TCP:netlink与tcp_diag编程


概览

http://m.oschina.net/blog/351007有一个示例程序,但是它用的v1的接口。

http://kristrev.github.io/2013/07/26/passive-monitoring-of-sockets-on-linux/

教了怎么用v2的接口。

inet_diag和tcp_diag是两个模块,但是统一使用inet_diag的接口,inet_diag又是使用netlink的接口。要使用你得加载这两个模块,大部分的发行版都是默认加载的(ss命令就是用这个)。

使用这套接口去获得tcp信息,涉及到两个问题:请求格式和返回格式。

请求格式是这样的:

struct

{

struct nlmsghdr nlh;

struct inet_diag_req_v2 r;

} req;

因为netlink要求一个通用的netlink头部后面跟具体请求类型对应的数据头部。这里的数据头部使用inet_diag_req_v2或者inet_diag_req都可以。是两种版本的实现,inet_diag_req_v2更友好一些。

netlink头部填充

http://stuff.onse.fi/man?program=netlink§ion=7

netlink使用通用的socket接口,只是添加了一个新的类型。创建netlink的socket的方法:

#include

#include

#include

netlink_socket =socket(AF_NETLINK, socket_type, netlink_family);

socket_type只可以是SOCK_RAW或者SOCK_DGRAM,内核并不区分这两种,所以用户使用哪个都可以。而netlink_family就是用来选择具体netlink在内核端沟通的模块了:

netlink请求头部

netlink的请求头部结构体是

struct nlmsghdr{

__u32 nlmsg_len; /* Length of message including header. */

__u16 nlmsg_type; /* Type of message content. */

__u16 nlmsg_flags; /* Additional flags. */

__u32 nlmsg_seq; /* Sequence number. */

__u32 nlmsg_pid; /* Sender port ID. */

};

由于netlink要求一个netlink请求头部后面要跟具体的请求类型的头部(例如inet_diag

的请求就需要跟inet_diag的头部),所以这里有个nlmsg_len域,用来表示netlink头部加上请求头部一起的长度。

nlmsg_type就是后端对应的功能模块,随着内核功能的完善,这个支持的模块也在增长。见下节。nlmsg_flags就是针对操作的后端的操作flag,见下下节。

一个netlink请求的头部允许有多个nlmsghdr,每个的nlmsg_flags域要设置NLM_F_MULTI,最后一个设置NLMSG_DONE。这种多个nlmsghdr结构体的情况,每个头部的数据都紧跟在这个头部的后面。

nlmsg_pid用来表示发送这个请求的进程pid(所以你可以伪造为其他进程发送),nlmsg_seq是用户自己设置的,内核的返回也会回复这个,可以让用户用来追踪任何一个请求。如果你嫌烦,可以在bind的时候填好pid,这里可以直接设个0,没人会怪你。

netlink后端功能模块

NETLINK_ROUTE用来与邻居表路由表,数据包分类器等路由子系统通信,获取信息或者设置。

NETLINK_W1就是GPIO用来拉高或者拉低某一根线的内核子系统,所以用户如果使用GPIO就可以不用动内核,直接在用户空间操作GPIO了。

NETLINK_USERSOCK就是用户端socket,使用这个处理netlink请求的单位就不是内核了,而是用户空间的的另外一头的某个进程。恩,你想的没错,这个就是进程间通信的又一种方案。由于是socket,一端可以监听,另一端发送的只要将发送的目标地址填充为目标进程的pid就好(netlink的发送地址不是ip编码的,而是pid等编码的)。

这种IPC最牛逼的地方在于可以支持multicast,多播的通信。一个消息同时发送给多个接受者,但是普通的回环地址lo的socket通信也可以做到这一点。

NETLINK_FIREWALL这个是跟内核的netfilter的ip_queue模块沟通的选项。ip_queue是netfilter提供的将网络数据包从内核传递到用户空间的方法,内核中要提供ip_queue支持,在用户层空间打开一个netlink的socket后就可以接受内核通过ip_queue所传递来的网络数据包,具体数据包类型可由iptables命令来确定,只要将规则动作设置为“-j QUEUE”即可。

之所以要命名为ip_queue,是因为这是一个队列处理过程,iptables规则把指定的包发给QUEUE是一个数据进入队列的过程,而用户空间程序通过netlink socket获取数据包进行裁定,结果返回内核,进行出队列的操作。

在iptables代码中,提供了libipq库,封装了对ipq的一些操作,用户层程序可以直接使用libipq库函数处理数据。

NETLINK_IP6_FW与NETLINK_FIREWALL的功能一样,只是是专门针对ipv6的。

NETLINK_INET_DIAG就是同网络诊断模块通信使用的,最常用的是tcp_diag模块,可以获得tcp连接的最详细信息。

NETLINK_NFLOG是内核用来将netfilter的日志发送到用户空间的方法。

NETLINK_XFRM就是与内核的ipsec子模块通信的机制。

NETLINK_SELINUX与内核的selinux通信。

NETLINK_ISCSI是open iscsi的内核部分,通过iscsi可以组成iscsi网络,让你的网路存储系统high起来。

NETLINK_AUDIT与内核的audit模块通信。记录了一大堆事件。

NETLINK_FIB_LOOKUP用户可以自由的查询fib路由表了。fib是快速转发表,里面量很大,刷新比较快,服务于快速查找和快速转发,而不是服务于用户空间设置,用户空间设置使用的路由表是rib,在内核中rib会转化为fib。

NETLINK_CONNECTOR是内核端的模块如果想要使用netlink接口对用户提供服务,这个模块可以去注册一个netlink回调,用户空间使用这个子系统就可以连接到特定的内核模块。

NETLINK_NETFILTER用于控制netfilter的。

NETLINK_DNRTMSG:DECnet的,大部分人用不到

NETLINK_KOBJECT_UEVENT:sys子系统使用的uevent事件。内核内所有设备的uevent事件都会通过这个接口发送到用户空间

NETLINK_GENERIC:这个也是内核模块用来提供netlink接口的方式。通过这种方式提供的接口都可以复用这一个子系统。

NETLINK_CRYPTO:可以使用内核的加密系统或者修改查询内核的加密系统参数。

netlink请求nlmsg_flags

由于涉及到具体的后端请求类型,所以这个flag的设计时尽可能通用的,在不同的后端的时候会有不同的表现。大家可以大概了解一下每个flag的意思,但是在使用的使用要根据不同的用途区别对待。

与具体模块无关的通用设置:

NLM_F_REQUEST:所有请求类型的netlink都会设置

NLM_F_MULTI:用于表示多个netlink请求在同一个包的NLMSG_DONE结尾最后一个头部

NLM_F_ACK:由于netlink是不可靠的,可以通过让内核回复ack模拟的实现可靠(其实绝大多数情况下是可靠的,如果不可靠说明内存不够了)

NLM_F_ECHO:这是让内核响应这个请求,一般需要内核响应的(但是也不是所有的内核子系统都是按照这个模型设计的),如果不设置,很可能只有ack(如果设置了NLM_F_ACK的话)

专门为GET类的请求附带的flag:

NLM_F_ROOT:返回满足条件的整个表,而不是单个的entry

NLM_F_MATCH:返回所有匹配的,这个在内核中只是提供了一个接口,并没有具体的实现。所以目前设不设都无所谓。但是比如tcp_diag的根据sockid获取单条tcp连接信息的功能,就可以使用这个标志,只是目前还没有实现而已。

NLM_F_ATOMIC:请求返回表的时候,返回的是一个快照

NLM_F_DUMP:这个是(NLM_F_ROOT|NLM_F_MATCH)的组合,意思是返回全部满足指定条件的条目。

专为SET类的请求附带的flag

NLM_F_REPLACE:取代已经存在的匹配条目

NLM_F_EXCL:如果条目已经存在就不取代

NLM_F_CREATE:如果不存在就创建

NLM_F_APPEND:加在对象列表的最后

netlink的请求类型nlmsgs_type

这个与具体的后端模块相关的,不是netlink还是提供了集中通用的消息类型(但是实际使用的使用一般要按照情况使用对应的后端模块定义的type,例如inet_diag就定义了TCPDIAG_GETSOCK,DCCPDIAG_GETSOCK这两种类型的type)。这些通用的在include/uapi/rtnetlink.h中定义了一坨。

inet_diag模块

概览

inet_diag是diag系统中的一部分,他的上面还有sock_diag,下面有tcp_diag。所有的inet_diag都被注册到sock_diag内部的静态数据结构,每一个inet_diag都是一个方法调用的列表,登记了各种需要的操作。主要有三个:destroy、dump和get_info,get_info用于销毁的时候,实际使用的时候只有dump和detroy。

netlink请求头部

inet_diag模块是netlink后端的一个子系统,他的请求头部如下

 struct inet_diag_req_v2 {
 38 __u8 sdiag_family;
 39 __u8 sdiag_protocol;
 40 __u8 idiag_ext;
 41 __u8 pad;
 42 __u32 idiag_states;
 43 struct inet_diag_sockidid;

44 };

这个是请求inet_diag的请求,sdiag_family,sdiag_protocol这些就和正常的socket一样的设置AF_INET,IPPROTO_TCP。idiag_states就是指tcp的连接状态(如果是UDP的话就是UDP,这取决于你填充的netlink的.nlh.nlmsg_type= TCPDIAG_GETSOCK;)。我们这里关注tcp,这就就填你关注的tcp连接状态,内核对tcp连接状态的定义有两套:

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, /* Now a valid state */

TCP_NEW_SYN_RECV,

 

TCP_MAX_STATES /* Leave at the end! */

};

enum {

TCPF_ESTABLISHED= (1 << 1),

TCPF_SYN_SENT = (1 << 2),

TCPF_SYN_RECV = (1 << 3),

TCPF_FIN_WAIT1 = (1 << 4),

TCPF_FIN_WAIT2 = (1 << 5),

TCPF_TIME_WAIT = (1 << 6),

TCPF_CLOSE = (1 << 7),

TCPF_CLOSE_WAIT = (1 << 8),

TCPF_LAST_ACK = (1 << 9),

TCPF_LISTEN = (1 << 10),

TCPF_CLOSING = (1 << 11),

TCPF_NEW_SYN_RECV= (1 << 12),

};

 

所以,你可以很明显的看出来应该用第二套。第一套是用来给内部使用的,第二套使用来给外部使用的。第二套可以轻松的实现不同状态的组合设置。

所以,我们这里的idiag_states就用第二套来组合设置。如果想要全部,你就可以任性的使用0xff来搞定。还有一个idiag_ext域,

enum {
104 INET_DIAG_NONE,
105 INET_DIAG_MEMINFO,
106 INET_DIAG_INFO,
107 INET_DIAG_VEGASINFO,
108 INET_DIAG_CONG,
109 INET_DIAG_TOS,
110 INET_DIAG_TCLASS,
111 INET_DIAG_SKMEMINFO,
112 INET_DIAG_SHUTDOWN,
113 };

这个ext可以获得更多种类的信息,包括内存(ss –m参数),如果不填(就是填0)就是INET_DIAG_NONE,表示啥都不要。也可以看出来,同一个请求只能请求一种数据。我们比较关注tcp连接的信息,所以使用INET_DIAG_INFO。

 

还有一个是唯一标识一个socket的域,

struct inet_diag_sockid {
 14 __be16 idiag_sport;
 15 __be16 idiag_dport;
 16 __be32 idiag_src[4];
 17 __be32 idiag_dst[4];
 18 __u32 idiag_if;
 19 __u32 idiag_cookie[2];
 20 #define INET_DIAG_NOCOOKIE (~0U)
 21 };

可以看到,标识一个socket不是用的五元组,而是源ip:源端口,目的ip:目的端口,从哪个设备获得的,还有唯一的标示内核中的一个socket的cookie,这个cookie值是在内核中计算sock结构体的sk_cookie域得出来的,一般用户端不需要填充这个域,在两个字节都放个INET_DIAG_NOCOOKIE就去就可以了。

内核内部在连接表中查找:

 

而内核的这个实现只会查找ESTABLISHED状态和LISTEN状态的连接,所以想要查询其他状态的tcp连接信息的可以洗洗睡了。最后那个socket绑定的设备也是必须的,因为内核中的查找也要使用这个信息。

但是不是所有的请求都需要填充所有的头部,例如如果你想要全部dump整个tcp连接表,就可以不填sockid域(置0)。

你会发现idiag_src和idiag_dst都是4个字节的,这并不是要你输入字符串,而是要兼容ipv6,所以这个接口是ipv6和ipv4通用的。ipv4的话只需要填充第一个单位就可以了。

注意的是这里地址和端口是网络序的,idiag_if一般是0,如果你不确定,先全部填0,选项上用NLM_F_DUMP就可以看到现有的都是怎么存储的了。但是要获得单个的socket的信息需要使用NLM_F_ATOMIC,当然NLM_F_REQUEST都是必须的。

操作种类

总体来说,所有的sock_diag都只提供一种对外接口,那就是dump。但是显然的只有这么一种是不够的。inet_diag就用这个dump接口实现了dump和对其他操作的封装。这个dump对应的inet_diag模块内部的操作是inet_diag_handler_cmd函数。想要获得netlink本身的dump信息,必须得设置NLM_F_DUMP这个flag(#define NLM_F_DUMP (NLM_F_ROOT|NLM_F_MATCH)),但是执行功能时我们是希望获得tcp连接的信息,由于内核保存tcp连接信息的方式是使用tcp_hashinfo全局结构体,所以本质上,就是查询的这个哈希表,而这个哈希表中只有ESTABLISHED和LISTEN状态,所以,你也查不到别的状态。

内核还有一个get_info接口可以获得很多数据,但是sock_diag没有对外提供,其实完全可以对外提供的,就可以获得tcp最详细的数据。也就是说现在inet_diag和tcp_diag都支持获得tcp_info,只是sock_diag没有对外提供。而tcp通过getsockopt对外提供了获得tcp_info结构体的能力。

获得数据内容

其实tcp_diag能获得很多数据,除了tcp_info之外,还可以获得一些拥塞控制算法和内存上的信息(都在idiag_ext域指定):

struct inet_diag_msg {
 87 __u8 idiag_family;
 88 __u8 idiag_state;
 89 __u8 idiag_timer;
 90 __u8 idiag_retrans;
 91 
92 struct inet_diag_sockidid;
 93 
94 __u32 idiag_expires;
 95 __u32 idiag_rqueue;
 96 __u32 idiag_wqueue;
 97 __u32 idiag_uid;
 98 __u32 idiag_inode;
 99 };

这个是基础的所能获得的信息,tcp_info就放在这些数据后面,如果是其他的请求也是一样的道理。

相关内容