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

发表于2016-09-03
评论0 3.5k浏览
概览
  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信息,涉及到两个问题:请求格式和返回格式。
  请求格式是这样的:
1
2
3
4
5
6
7
8
9
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的方法:
1
2
3
4
5
6
7
#include
 
#include
 
#include
 
netlink_socket =socket(AF_NETLINK, socket_type, netlink_family);
  socket_type只可以是SOCK_RAW或者SOCK_DGRAM,内核并不区分这两种,所以用户使用哪个都可以。而netlink_family就是用来选择具体netlink在内核端沟通的模块了:

netlink请求头部
  netlink的请求头部结构体是
1
2
3
4
5
6
7
8
9
10
11
12
13
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后端的一个子系统,他的请求头部如下
1
2
3
4
5
6
7
8
struct inet_diag_req_v2 {
__u8 sdiag_family;
__u8 sdiag_protocol;
__u8 idiag_ext;
__u8 pad;
__u32 idiag_states;
struct inet_diag_sockidid;
};
  这个是请求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连接状态的定义有两套:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
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域,
1
2
3
4
5
6
7
8
9
10
11
enum {
INET_DIAG_NONE,
INET_DIAG_MEMINFO,
INET_DIAG_INFO,
INET_DIAG_VEGASINFO,
INET_DIAG_CONG,
INET_DIAG_TOS,
INET_DIAG_TCLASS,
INET_DIAG_SKMEMINFO,
INET_DIAG_SHUTDOWN,
};
  这个ext可以获得更多种类的信息,包括内存(ss –m参数),如果不填(就是填0)就是INET_DIAG_NONE,表示啥都不要。也可以看出来,同一个请求只能请求一种数据。我们比较关注tcp连接的信息,所以使用INET_DIAG_INFO。
  还有一个是唯一标识一个socket的域,
1
2
3
4
5
6
7
8
9
struct inet_diag_sockid {
__be16 idiag_sport;
__be16 idiag_dport;
__be32 idiag_src[4];
__be32 idiag_dst[4];
__u32 idiag_if;
__u32 idiag_cookie[2];
#define INET_DIAG_NOCOOKIE (~0U)
};
  可以看到,标识一个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域指定):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct inet_diag_msg {
__u8 idiag_family;
__u8 idiag_state;
__u8 idiag_timer;
__u8 idiag_retrans;
  
struct inet_diag_sockidid;
  
__u32 idiag_expires;
__u32 idiag_rqueue;
__u32 idiag_wqueue;
__u32 idiag_uid;
__u32 idiag_inode;
};
  这个是基础的所能获得的信息,tcp_info就放在这些数据后面,如果是其他的请求也是一样的道理。

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引