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
#include
#include
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就放在這些數據後面,如果是其他的請求也是一樣的道理。