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更友好一些。
http://stuff.onse.fi/man?program=netlink§ion=7
netlink使用通用的socket接口,只是添加了一个新的类型。创建netlink的socket的方法:
#include<asm/types.h>
#include <sys/socket.h>
#include <linux/netlink.h>
netlink_socket =socket(AF_NETLINK, socket_type, netlink_family);
socket_type只可以是SOCK_RAW或者SOCK_DGRAM,内核并不区分这两种,所以用户使用哪个都可以。而netlink_family就是用来选择具体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_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:可以使用内核的加密系统或者修改查询内核的加密系统参数。
由于涉及到具体的后端请求类型,所以这个flag的设计时尽可能通用的,在不同的后端的时候会有不同的表现。大家可以大概了解一下每个flag的意思,但是在使用的使用要根据不同的用途区别对待。
NLM_F_REQUEST:所有请求类型的netlink都会设置
NLM_F_MULTI:用于表示多个netlink请求在同一个包的NLMSG_DONE结尾最后一个头部
NLM_F_ACK:由于netlink是不可靠的,可以通过让内核回复ack模拟的实现可靠(其实绝大多数情况下是可靠的,如果不可靠说明内存不够了)
NLM_F_ECHO:这是让内核响应这个请求,一般需要内核响应的(但是也不是所有的内核子系统都是按照这个模型设计的),如果不设置,很可能只有ack(如果设置了NLM_F_ACK的话)
NLM_F_ROOT:返回满足条件的整个表,而不是单个的entry
NLM_F_MATCH:返回所有匹配的,这个在内核中只是提供了一个接口,并没有具体的实现。所以目前设不设都无所谓。但是比如tcp_diag的根据sockid获取单条tcp连接信息的功能,就可以使用这个标志,只是目前还没有实现而已。
NLM_F_ATOMIC:请求返回表的时候,返回的是一个快照
NLM_F_DUMP:这个是(NLM_F_ROOT|NLM_F_MATCH)的组合,意思是返回全部满足指定条件的条目。
NLM_F_REPLACE:取代已经存在的匹配条目
NLM_F_EXCL:如果条目已经存在就不取代
NLM_F_CREATE:如果不存在就创建
NLM_F_APPEND:加在对象列表的最后
这个与具体的后端模块相关的,不是netlink还是提供了集中通用的消息类型(但是实际使用的使用一般要按照情况使用对应的后端模块定义的type,例如inet_diag就定义了TCPDIAG_GETSOCK,DCCPDIAG_GETSOCK这两种类型的type)。这些通用的在include/uapi/rtnetlink.h中定义了一坨。
inet_diag是diag系统中的一部分,他的上面还有sock_diag,下面有tcp_diag。所有的inet_diag都被注册到sock_diag内部的静态数据结构,每一个inet_diag都是一个方法调用的列表,登记了各种需要的操作。主要有三个:destroy、dump和get_info,get_info用于销毁的时候,实际使用的时候只有dump和detroy。
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就放在这些数据后面,如果是其他的请求也是一样的道理。