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的接口。

http://www.infradead.org/~tgr/libnl/doc/core.html#core_msg_attr最全面的netlink教程

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中定义了一坨。

 

netlink策略:nla_policy

         这个netlink比较少被其他模块用到,在genetlink中有提供使用的接口,但是genetlink的使用者也仍然可以选择不使用它。

Linux内核工程导论–网络:TCP:netlink与tcp_diag编程_第1张图片

         定义policy是一系列的nla_policy数组,每一个nla_policy结构体都是一个属性。这个数组就是这个netlink请求的属性集。netlink系统会根据这个预定义的属性集去自动的解析这些属性为已知的格式。就省去了用户去自己解析的烦恼。属性不是必须的,但是尽量的使用属性可以让程序更容易修改和维护。

         这张图是netlink的请求格式图,可以看到首先是nlmsghdr,然后是payload。而payload又可以进一步划分,先是各种netlink  family的头部,然后是属性列表,中间都是有pad的,这个pad是4字节对齐的。属性列表的格式如下:

          Linux内核工程导论–网络:TCP:netlink与tcp_diag编程_第2张图片

Linux内核工程导论–网络:TCP:netlink与tcp_diag编程_第3张图片


         例如genetlink的family头部就是structgenlmsghdr,然后再是用户自己的header,然后就是属性列表。

genetlink的使用

请求流程

         前面说过netlink有一种通用的用法叫NETLINK_GENERIC类型。可以供大家自由的扩展使用netlink功能。内核模块开发者可以使用这个直接提供netlink能力。

         这一类的netlink内核有专门的头部定义,用户发来的netlink消息,先是netlink头部,然后要跟

struct genlmsghdr {

         __u8         cmd;

         __u8         version;

         __u16       reserved;

};

这个genetlink头部。然后在这个头部之后跟用户自己定义的头部。

         这个消息传递到内核的时候,genetlink的内核部分会自动解析,将这个头部对应的解析为

struct genl_info {

         u32                     snd_seq;

         u32                     snd_portid;

         structnlmsghdr *    nlhdr;

         structgenlmsghdr *         genlhdr;

         void*                          userhdr;

         structnlattr **         attrs;

         possible_net_t                   _net;

         void*                          user_ptr[2];

         structsock *             dst_sk;

};

这个结构体交给使用genetlink架构处理genetlink消息的内核模块使用。这些都是从用户提交的请求中解析出来的,形成的struct genl_info结构体,并且只会传递给doit调用(后面讲)。

         nlhdr,genlhdr,userhdr分别是netlink头部,genetlink头部,用户自定义头部,attrs是netlink的属性机制。如果检测到消息中有属性,程序就会解析,然后放到这个位置传递给实际的处理函数进行处理(可以用属性也可以直接用用户自定义的请求头部,官方推荐用属性,认为可以增加可扩展性,方便维护)。

 

概念概览

         genetlink既然是提供给多个内核模块使用的,就一定会提供区分不同内核模块的方法机制。genetlink通过family和ops两个维度来定位到最终的处理函数。

family

         family一般用来确定模块,ops一般用来确定模块内的不同操作(当然可以变通使用,概念上就是这么设计的)。

         要使用genetlink的内核模块首先定义一个family:

static struct genl_family yy_gnl_family = {

     .id= NETLINK_FAMILY_ID,

    .hdrsize = 0,

    .name = “my_netlink",

    .version = 1,

    .maxattr = A_MAX,

};

         其中id是用户自己指定的数字,用户端在填充netlink的请求头部的时候要填写这个nlmsg_type作为netlink请求的目标类型。这个指定的数字不要与已有的相冲突,否则内核就无法找到你了。

         hdrsize是用户自定义头部的大小,你其实可以把它填0,系统还是可以正常的找到用户的头部的指针的,然后你在你自己的函数中处理也是可以的。但是内核会报个警告出来告诉你netlink多了一些字节。所以按照他们的设计来吧。你定义好你的头部,在这里填上你定义的头部的大小。另外由于属性列表是放在用户头部的后面,所以如果你想使用属性列表,这个值就必须得准确的填充,否则netlink模块找不到对应的属性也就无法解析了。

         name就是给人读的了,对程序没有影响。

         genlmsghdr这个头部需要填充一个version,这个version就对应着structgenl_family的version域。从这里你可以发现,你可以同时存在同一个family的不同个version,从而也可以用这种手法实现family的复用和提供不同维度的功能。

         maxattr就是该模块支持的最大属性数。每一个属性都是nla_policy。这里这个familuy可以定义一堆属性,请求的时候请求者应该把这些属性放在用户头的后面。netlink会自动解析传递到doit调用。

每一个family在定以后都应该调用genl_register_family向genetlink模块注册自己,这样才可以被找到。

我们可以看到在定义family的时候没有指定policy(属性),也就是说属性是不是family相关的,而是ops相关的。

ops

         有了family了还没有定义这个类别下的操作。每一个family可以定义多个操作,一个操作并不是一个函数,而是一个函数指针的结构体。因为netlink在设计的时候就把操作分成了两类:set和get,或者说是3类,还有一个dump,再或者还加上destroy。所以定义一个family的操作就得实现多个函数。

         genetlink为我们封装了细节,但是一个操作还是要实现两个函数(也可以不实现,不实现就不支持对应的netlink的请求flag,例如dump)。

static struct genl_opsyy_gnl_ops_tcp_getinfo = {

    .cmd = _C_GETINFO,

    .flags = 0,

    .policy = genl_policy,

    .doit =  _doit_getinfo,

    .dumpit = dump_getinfo,

 };

         这个结构体就是一个ops操作的结构体。一个family可以定义多个这样的结构体,每个结构体都需要调用genl_register_ops将这个ops注册到对应的family(所以如果family没有注册,这个操作就不可能被执行)。

         cmd是一个整数,任意定义,只要保证在一个family中这个数不冲突就好。这就是唯一的确定一个family中的某个ops。

         policy就是属性集,这个操作对应的属性集,由于属性列表会传输给doit调用,而doit也是这个ops定义的,所以就相当于给每一个ops定义一系列的属性参数。

         doit相当于set函数,这个函数的参数是用户发来的请求(被解析好了),模块只需要实现这个函数,但是不能返回值。这个函数当用户请求没有NLM_F_DUMP标志的时候会被触发调用。

         当用户的请求flag有NLM_F_DUMP标志时,调用的就是dumpit函数,这个函数的没有传入参数,但是可以返回内容。也就是说你可以先调用doit来设置,然后调用dumpit来返回。或者是直接使用dumpit返回一个列表。用户端的一系列程序,例如ss命令都是直接返回列表的。

         flags有4种:GENL_ADMIN_PERM,表示执行这个ops需要CAP_NET_ADMIN的权限。GENL_CMD_CAP_DO表示本模块实现了doit操作,GENL_CMD_CAP_DUMP表示本模块实现了dump操作,GENL_CMD_CAP_HASPOL表示本模块实现了属性集。这些flag不设程序也能正常工作,但是设置了符合标准,是一种符合内核capabilities权限控制系统的补充实现。

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就放在这些数据后面,如果是其他的请求也是一样的道理。


你可能感兴趣的:(linux,linux内核原理)