Kernel Version: 4.6.6
本文分析一下netlink中的一種協定NETLINK_INET_DIAG的實作方法,這裡基本沒有提及userspace的使用方法哦,主要是講解kernel的實作方法。在Android中,會使用到這個netlink socket,對相關的socket進行monitor,會有什麼場景應用到這個功能呢? 一般情況下,如果我們在使用wifi在看視訊的時候,資料包會從server端源源不斷的書送過來,然後送到上層APP,但是如果中途wifi斷掉時候,這個網絡連接配接會發生什麼呢,繼續接收資料,但是收不到任何資料,但是這個時候不能無限的等下去吧,一般的軟體都會設定一個逾時的時間,時間到了,就會收到ETIMEOUT的錯誤,在Android中也做了一些優化,試想一下網絡斷了,其實就意味着這個連結沒什麼用了,即使重連之後,也許Wifi的IP位址會變了,這個連結就不可用了,是以在斷線的時候,Android網絡管理程式就會去dump目前源位址對應的socket,然後destroy掉。
此功能需要kernel支援Patch:Subject: net:diag:Add the ability to destroy a socket
這個patch主要實作了destroy的功能!
Subject: net: diag: Add the ability to destroy a socket.
This patch adds a SOCK_DESTROY operation, a destroy function
pointer to sock_diag_handler, and a diag_destroy function
pointer. It does not include any implementation code.
在逐漸分析之前,先看下如下的時序圖,基本上就是這樣調用的,當然後面的inet_diag完全可以替換成其他協定,這個地方就是使用inet來作為一個執行個體,看了大體流程是不是覺得很簡單,OK,開始細細分析!
1. sock_diag 初始化
Kernel部分 主要集中在 kernel3.18/net/sock_diag.c
這個module的初始化的時候,會使用netlink_kernel_create建立一個協定為NETLINK_SOCK_DIAG的函數,接收函數為sock_diag_rcv
static int __net_init diag_net_init(struct net *net)
{
struct netlink_kernel_cfg cfg = {
.input = sock_diag_rcv,
};
net->diag_nlsk = netlink_kernel_create(net, NETLINK_SOCK_DIAG, &cfg);
return net->diag_nlsk == NULL ? -ENOMEM : 0;
}
不過要注意一點在create socket的時候會有這樣的操作,就是把input給關聯起來..
sk->sk_data_ready = netlink_data_ready;
if (cfg && cfg->input)
nlk_sk(sk)->netlink_rcv = cfg->input;
2. 傳遞消息到kernel
netlink 主要是userspace和kernel space的互動使用的,從上面來看user和kernel的socket都建立完成之後,如果user有消息發送給kernel的話,也是會走netlink的通用的flow到:netlink_sendmsg,這個函數的過程在這裡不做具體分析,直接到最後調用到netlink_unicast,到目前為止,實際上從協定上去區分,userspace發送的kernel是有目的的,專門發送到某個socket,這裡要根據NETLINK_SOCK_DIAG來區分,結合上面初始化的和現在netlink_rev skb,這樣其實我們就直接走到了sock_diag_rcv的函數,已經朝着目的地邁進一大步了!
static int netlink_unicast_kernel(struct sock *sk, struct sk_buff *skb,
struct sock *ssk)
{
int ret;
struct netlink_sock *nlk = nlk_sk(sk);
ret = -ECONNREFUSED;
if (nlk->netlink_rcv != NULL) {
ret = skb->len;
netlink_skb_set_owner_r(skb, sk);
NETLINK_CB(skb).sk = ssk;
netlink_deliver_tap_kernel(sk, ssk, skb);
<span style="color:#ff0000;">nlk->netlink_rcv(skb);</span>
consume_skb(skb);
} else {
kfree_skb(skb);
}
sock_put(sk);
return ret;
}
接着來看 sock_diag_rcv, 上鎖處了解鎖的流程,從netlink_rcv_skb的參數來看,肯定是中間會執行到sock_diag_rcv_msg這個callback的函數
static void sock_diag_rcv(struct sk_buff *skb)
{
mutex_lock(&sock_diag_mutex);
netlink_rcv_skb(skb, &sock_diag_rcv_msg);
mutex_unlock(&sock_diag_mutex);
}
ok,下面來說下netlink_rcv_skb這個函數了,其實任何一種協定的資料包都會走這個函數,kernel分層的思想比比皆是,這個函數的重要工作其實就是在解析skb的資訊,然後去調用協定接收函數,這裡就是指的sock_diag_rcv_msg
int netlink_rcv_skb(struct sk_buff *skb, int (*cb)(struct sk_buff *,
struct nlmsghdr *))
{
struct nlmsghdr *nlh;
int err;
while (skb->len >= nlmsg_total_size(0)) {
int msglen;
//先取出header nlh
nlh = nlmsg_hdr(skb);
err = 0;
//合法性的檢查,如果小于HDRLEN或者skb->len異常 就退出不處理
if (nlh->nlmsg_len < NLMSG_HDRLEN || skb->len < nlh->nlmsg_len)
return 0;
/* Only requests are handled by the kernel */
if (!(nlh->nlmsg_flags & NLM_F_REQUEST))
goto ack;
/* Skip control messages */
if (nlh->nlmsg_type < NLMSG_MIN_TYPE)
goto ack;
//這個地方調用協定相關的函數
err = cb(skb, nlh);
if (err == -EINTR)
goto skip;
ack:
//如果user需要有回複,那麼這個地方調用netlink_ack進行答複動作
if (nlh->nlmsg_flags & NLM_F_ACK || err)
netlink_ack(skb, nlh, err);
skip:
msglen = NLMSG_ALIGN(nlh->nlmsg_len);
if (msglen > skb->len)
msglen = skb->len;
skb_pull(skb, msglen);
}
return 0;
}
接下來再看sock_diag_rcv_msg: 這個函數其實主要是就是對四中msg type進行解析,并執行相關的函數,我們這裡主要關心2中類型的消息
#define SOCK_DIAG_BY_FAMILY 20
#define SOCK_DESTROY 21
其中SOCK_DIAG_BY_FAMILY 是用于dump相關的socket,SOCK_DESTROY是用來destroy相關socket
static int sock_diag_rcv_msg(struct sk_buff *skb, struct nlmsghdr *nlh)
{
int ret;
switch (nlh->nlmsg_type) {
case TCPDIAG_GETSOCK:
case DCCPDIAG_GETSOCK:
if (inet_rcv_compat == NULL)
request_module("net-pf-%d-proto-%d-type-%d", PF_NETLINK,
NETLINK_SOCK_DIAG, AF_INET);
mutex_lock(&sock_diag_table_mutex);
if (inet_rcv_compat != NULL)
ret = inet_rcv_compat(skb, nlh);
else
ret = -EOPNOTSUPP;
mutex_unlock(&sock_diag_table_mutex);
return ret;
/*目前主要是處理這兩種類型的消息,一個是Dump相關的connection的socket info,一個是Destroy相關的
* Socket connect*/
case SOCK_DIAG_BY_FAMILY:
case SOCK_DESTROY:
return __sock_diag_cmd(skb, nlh);
default:
return -EINVAL;
}
}
繼續分析__sock_diag_cmd這個函數,這個函數主要是根據傳入的資料也就是sock_diag_req中的協定類型在注冊的sock_diag_handlers數組中找到對應的sock_diag_handler, 然後再判斷cmd的類型SOCK_DIAG_BY_FAMILY 執行dump鈎子函數,SOCK_DESTROY 執行destroy鈎子函數!
static int __sock_diag_cmd(struct sk_buff *skb, struct nlmsghdr *nlh)
{
int err;
struct sock_diag_req *req = nlmsg_data(nlh);
const struct sock_diag_handler *hndl;
if (nlmsg_len(nlh) < sizeof(*req))
return -EINVAL;
if (req->sdiag_family >= AF_MAX)
return -EINVAL;
if (sock_diag_handlers[req->sdiag_family] == NULL)
request_module("net-pf-%d-proto-%d-type-%d", PF_NETLINK,
NETLINK_SOCK_DIAG, req->sdiag_family);
mutex_lock(&sock_diag_table_mutex);
hndl = sock_diag_handlers[req->sdiag_family];
if (hndl == NULL)
err = -ENOENT;
else if (nlh->nlmsg_type == SOCK_DIAG_BY_FAMILY)
err = hndl->dump(skb, nlh);
else if (nlh->nlmsg_type == SOCK_DESTROY && hndl->destroy)
err = hndl->destroy(skb, nlh);
else
err = -EOPNOTSUPP;
mutex_unlock(&sock_diag_table_mutex);
return err;
}
3.重要的結構體
在進入執行cmd之前,先來看下幾個重要的結構體, 這個是一個 全局靜态的結構體數組指針
static const struct sock_diag_handler *sock_diag_handlers[AF_MAX];
這個結構體,包含3個鈎子和一個family的變量,其中family就是指的協定的類型了比如AF_INET,AF_INET6等,方法有destroy,dump,get_info 3個方法,可想而知每一個協定族需要使用sock_diag的功能的時候,需要提前注冊到sock_diag_handlers中,就拿AF_INET來講,會有一個inet_diag_handler,
static const struct sock_diag_handler inet_diag_handler = {
.family = AF_INET,
.dump = inet_diag_handler_cmd,
.get_info = inet_diag_handler_get_info,
.destroy = inet_diag_handler_cmd,
};
當inet_diag 初始化的時候就會調用sock_diag_register,将傳入的inet_diag_handler給注冊到sock_diag_handlers中,就是按照family放到之前提到的那個全局資料組!
int sock_diag_register(const struct sock_diag_handler *hndl)
{
int err = 0;
if (hndl->family >= AF_MAX)
return -EINVAL;
mutex_lock(&sock_diag_table_mutex);
if (sock_diag_handlers[hndl->family])
err = -EBUSY;
else
sock_diag_handlers[hndl->family] = hndl;
mutex_unlock(&sock_diag_table_mutex);
return err;
}
協定的類型很多,但是注冊形式都是一樣的,如果對于這個協定下還有其他的協定,還可以繼續按照這種分層的思想繼續分下去,例如tcp_diag_handler!
4.Dump指令分析
到此處,在回到之前的__sock_diag_cmd函數,可以看到這個函數會先根據協定的類型family來找到對于的handler,在根據對應的command id來調用handler的鈎子函數,好,假設上層的參數是AF_INET,我們來看下相關flow. 如果是AF_INET,handler肯定是剛才講的inet_diag_handler,如果cmd是SOCK_DIAG_BY_FAMILY的話,那麼接下來走調用dump的hook了。
Code的目錄kernel-4.6/net/ipv4/inet_diag.c,調用的是inet_diag_handler_cmd,這個函數基本直接執行到inet_diag_cmd_exact,這裡的設計有将AF_INET協定抽象了一層,弄了個inet_diag_table,然後TCP/UDP根據協定号存放到數組中!
static int inet_diag_cmd_exact(int cmd, struct sk_buff *in_skb,
const struct nlmsghdr *nlh,
const struct inet_diag_req_v2 *req)
{
const struct inet_diag_handler *handler;
int err;
//繼續從inet_diag_table數組從跟進協定TCP/UDP擷取Handler
handler = inet_diag_lock_handler(req->sdiag_protocol);
if (IS_ERR(handler))
err = PTR_ERR(handler);
//然後再根據cmd,調用相關的執行函數
else if (cmd == SOCK_DIAG_BY_FAMILY)
err = handler->dump_one(in_skb, nlh, req);
else if (cmd == SOCK_DESTROY && handler->destroy)
err = handler->destroy(in_skb, req);
else
err = -EOPNOTSUPP;
inet_diag_unlock_handler(handler);
return err;
}
如果是TCP協定的話,也就意味着調用到了tcp_diag中的tcp_diag_dump_one
static const struct inet_diag_handler tcp_diag_handler = {
.dump = tcp_diag_dump,
.dump_one = tcp_diag_dump_one,
.idiag_get_info = tcp_diag_get_info,
.idiag_type = IPPROTO_TCP,
.idiag_info_size = sizeof(struct tcp_info),
#ifdef CONFIG_INET_DIAG_DESTROY
.destroy = tcp_diag_destroy,
#endif
};
檢視tcp_diag_dump_one函數,實際上就是從tcp_hashinfo中讀取相關的socket,這個地方隻能一次讀一個哦,one....
static int tcp_diag_dump_one(struct sk_buff *in_skb, const struct nlmsghdr *nlh,
const struct inet_diag_req_v2 *req)
{
return inet_diag_dump_one_icsk(&tcp_hashinfo, in_skb, nlh, req);
}
看下内容:根據原位址 目的位址等connection的資訊來找到對應的sk資訊,然後回報回userspace!
int inet_diag_dump_one_icsk(struct inet_hashinfo *hashinfo,
struct sk_buff *in_skb,
const struct nlmsghdr *nlh,
const struct inet_diag_req_v2 *req)
{
struct net *net = sock_net(in_skb->sk);
struct sk_buff *rep;
struct sock *sk;
int err;
//此處根據req資訊中的,dport/dst sport/src if 來找到對應的socket
sk = inet_diag_find_one_icsk(net, hashinfo, req);
if (IS_ERR(sk))
return PTR_ERR(sk);
//create一個skb
rep = nlmsg_new(inet_sk_attr_size(), GFP_KERNEL);
if (!rep) {
err = -ENOMEM;
goto out;
}
//填寫相關的req資訊
err = sk_diag_fill(sk, rep, req,
sk_user_ns(NETLINK_CB(in_skb).sk),
NETLINK_CB(in_skb).portid,
nlh->nlmsg_seq, 0, nlh);
if (err < 0) {
WARN_ON(err == -EMSGSIZE);
nlmsg_free(rep);
goto out;
}
//将dump出的資訊,傳入
err = netlink_unicast(net->diag_nlsk, rep, NETLINK_CB(in_skb).portid,
MSG_DONTWAIT);
if (err > 0)
err = 0;
out:
if (sk)
sock_gen_put(sk);
return err;
}
5. Destroy 分析
分析完dump之後,再來看下destroy函數,其實我們也能夠看到flow都是基本一樣的了,隻不過最後根據cmd不同會選擇到tcp_diag_destroy,仍然也是根據req2的資訊,先找到對應的socket,然後調用sock_diag_destroy,我們看到傳入的參數是sk 和 ECONNABORTED
#ifdef CONFIG_INET_DIAG_DESTROY
static int tcp_diag_destroy(struct sk_buff *in_skb,
const struct inet_diag_req_v2 *req)
{
struct net *net = sock_net(in_skb->sk);
struct sock *sk = inet_diag_find_one_icsk(net, &tcp_hashinfo, req);
if (IS_ERR(sk))
return PTR_ERR(sk);
return <span style="color:#ff0000;">sock_diag_destroy(sk, ECONNABORTED)</span>;
}
#endif
繼續看下sock_diag_destroy,這個函數就是check權限,然後找到sk_prot中的diag_destroy,執行這個hook函數
int sock_diag_destroy(struct sock *sk, int err)
{
if (!ns_capable(sock_net(sk)->user_ns, CAP_NET_ADMIN))
return -EPERM;
if (!sk->sk_prot->diag_destroy)
return -EOPNOTSUPP;
return sk->sk_prot->diag_destroy(sk, err);
}
sk_prot對于tcp協定來講就是tcp_abort函數了
kernel-4.6/net/ipv4/tcp_ipv4.c
struct proto tcp_prot = {
.name = "TCP",
.owner = THIS_MODULE,
.close = tcp_close,
.connect = tcp_v4_connect,
.disconnect = tcp_disconnect,
.accept = inet_csk_accept,
.ioctl = tcp_ioctl,
.init = tcp_v4_init_sock,
.destroy = tcp_v4_destroy_sock,
.shutdown = tcp_shutdown,
.setsockopt = tcp_setsockopt,
.getsockopt = tcp_getsockopt,
.recvmsg = tcp_recvmsg,
.sendmsg = tcp_sendmsg,
.sendpage = tcp_sendpage,
.backlog_rcv = tcp_v4_do_rcv,
.release_cb = tcp_release_cb,
.hash = inet_hash,
.unhash = inet_unhash,
.get_port = inet_csk_get_port,
.enter_memory_pressure = tcp_enter_memory_pressure,
.stream_memory_free = tcp_stream_memory_free,
.sockets_allocated = &tcp_sockets_allocated,
.orphan_count = &tcp_orphan_count,
.memory_allocated = &tcp_memory_allocated,
.memory_pressure = &tcp_memory_pressure,
.sysctl_mem = sysctl_tcp_mem,
.sysctl_wmem = sysctl_tcp_wmem,
.sysctl_rmem = sysctl_tcp_rmem,
.max_header = MAX_TCP_HEADER,
.obj_size = sizeof(struct tcp_sock),
.slab_flags = SLAB_DESTROY_BY_RCU,
.twsk_prot = &tcp_timewait_sock_ops,
.rsk_prot = &tcp_request_sock_ops,
.h.hashinfo = &tcp_hashinfo,
.no_autobind = true,
#ifdef CONFIG_COMPAT
.compat_setsockopt = compat_tcp_setsockopt,
.compat_getsockopt = compat_tcp_getsockopt,
#endif
.diag_destroy = tcp_abort,
};
tcp_abort基本就是将這個sk給destroy掉了,然後置位一個ECONNABORTED錯誤,通過sk_error_report通知上層的user
int tcp_abort(struct sock *sk, int err)
{
//Error ECONNABORTED
if (!sk_fullsock(sk)) {
if (sk->sk_state == TCP_NEW_SYN_RECV) {
struct request_sock *req = inet_reqsk(sk);
local_bh_disable();
inet_csk_reqsk_queue_drop_and_put(req->rsk_listener,
req);
local_bh_enable();
return 0;
}
sock_gen_put(sk);
return -EOPNOTSUPP;
}
/* Don't race with userspace socket closes such as tcp_close. */
lock_sock(sk);
if (sk->sk_state == TCP_LISTEN) {
tcp_set_state(sk, TCP_CLOSE);
inet_csk_listen_stop(sk);
}
/* Don't race with BH socket closes such as inet_csk_listen_stop. */
local_bh_disable();
bh_lock_sock(sk);
if (!sock_flag(sk, SOCK_DEAD)) {
//實際上就是指了一個ECONNABORTED的錯誤
sk->sk_err = err;
/* This barrier is coupled with smp_rmb() in tcp_poll() */
smp_wmb();
//Error report
sk->sk_error_report(sk);
//如果需要發reset的,發RST 包
if (tcp_need_reset(sk->sk_state))
tcp_send_active_reset(sk, GFP_ATOMIC);
//關閉一些socket的狀态
tcp_done(sk);
}
bh_unlock_sock(sk);
local_bh_enable();
release_sock(sk);
sock_put(sk);
return 0;
}
看下error report,就是在喚醒等待隊列的sk了,這個時候在使用select或者poll方法的sk會被喚醒,然後上層收到ECONNABORTED的錯誤!
static void sock_def_error_report(struct sock *sk)
{
struct socket_wq *wq;
rcu_read_lock();
wq = rcu_dereference(sk->sk_wq);
if (skwq_has_sleeper(wq))
wake_up_interruptible_poll(&wq->wait, POLLERR);
sk_wake_async(sk, SOCK_WAKE_IO, POLL_ERR);
rcu_read_unlock();
}
到這裡就把socket monitoring的dump和destroy功能就介紹完了,sock diag的功能還是蠻強大的,不光AF_INET,任何協定的socket都可以擴充支援,換句話說Linux kernel設計上還是擴充性非常強,架構寫的好,擴充新功能也非常簡單!