| // SPDX-License-Identifier: GPL-2.0-or-later | |
| /* | |
| * tcp_diag.c Module for monitoring TCP transport protocols sockets. | |
| * | |
| * Authors: Alexey Kuznetsov, <kuznet@ms2.inr.ac.ru> | |
| */ | |
| #include <linux/module.h> | |
| #include <linux/net.h> | |
| #include <linux/sock_diag.h> | |
| #include <linux/inet_diag.h> | |
| #include <linux/tcp.h> | |
| #include <net/inet_hashtables.h> | |
| #include <net/inet6_hashtables.h> | |
| #include <net/inet_timewait_sock.h> | |
| #include <net/netlink.h> | |
| #include <net/tcp.h> | |
| static void tcp_diag_get_info(struct sock *sk, struct inet_diag_msg *r, | |
| void *_info) | |
| { | |
| struct tcp_info *info = _info; | |
| if (inet_sk_state_load(sk) == TCP_LISTEN) { | |
| r->idiag_rqueue = READ_ONCE(sk->sk_ack_backlog); | |
| r->idiag_wqueue = READ_ONCE(sk->sk_max_ack_backlog); | |
| } else if (sk->sk_type == SOCK_STREAM) { | |
| const struct tcp_sock *tp = tcp_sk(sk); | |
| r->idiag_rqueue = max_t(int, READ_ONCE(tp->rcv_nxt) - | |
| READ_ONCE(tp->copied_seq), 0); | |
| r->idiag_wqueue = READ_ONCE(tp->write_seq) - tp->snd_una; | |
| } | |
| if (info) | |
| tcp_get_info(sk, info); | |
| } | |
| #ifdef CONFIG_TCP_MD5SIG | |
| static void tcp_diag_md5sig_fill(struct tcp_diag_md5sig *info, | |
| const struct tcp_md5sig_key *key) | |
| { | |
| info->tcpm_family = key->family; | |
| info->tcpm_prefixlen = key->prefixlen; | |
| info->tcpm_keylen = key->keylen; | |
| memcpy(info->tcpm_key, key->key, key->keylen); | |
| if (key->family == AF_INET) | |
| info->tcpm_addr[0] = key->addr.a4.s_addr; | |
| #if IS_ENABLED(CONFIG_IPV6) | |
| else if (key->family == AF_INET6) | |
| memcpy(&info->tcpm_addr, &key->addr.a6, | |
| sizeof(info->tcpm_addr)); | |
| #endif | |
| } | |
| static int tcp_diag_put_md5sig(struct sk_buff *skb, | |
| const struct tcp_md5sig_info *md5sig) | |
| { | |
| const struct tcp_md5sig_key *key; | |
| struct tcp_diag_md5sig *info; | |
| struct nlattr *attr; | |
| int md5sig_count = 0; | |
| hlist_for_each_entry_rcu(key, &md5sig->head, node) | |
| md5sig_count++; | |
| if (md5sig_count == 0) | |
| return 0; | |
| attr = nla_reserve(skb, INET_DIAG_MD5SIG, | |
| md5sig_count * sizeof(struct tcp_diag_md5sig)); | |
| if (!attr) | |
| return -EMSGSIZE; | |
| info = nla_data(attr); | |
| memset(info, 0, md5sig_count * sizeof(struct tcp_diag_md5sig)); | |
| hlist_for_each_entry_rcu(key, &md5sig->head, node) { | |
| tcp_diag_md5sig_fill(info++, key); | |
| if (--md5sig_count == 0) | |
| break; | |
| } | |
| return 0; | |
| } | |
| #endif | |
| static int tcp_diag_put_ulp(struct sk_buff *skb, struct sock *sk, | |
| const struct tcp_ulp_ops *ulp_ops, bool net_admin) | |
| { | |
| struct nlattr *nest; | |
| int err; | |
| nest = nla_nest_start_noflag(skb, INET_DIAG_ULP_INFO); | |
| if (!nest) | |
| return -EMSGSIZE; | |
| err = nla_put_string(skb, INET_ULP_INFO_NAME, ulp_ops->name); | |
| if (err) | |
| goto nla_failure; | |
| if (ulp_ops->get_info) | |
| err = ulp_ops->get_info(sk, skb, net_admin); | |
| if (err) | |
| goto nla_failure; | |
| nla_nest_end(skb, nest); | |
| return 0; | |
| nla_failure: | |
| nla_nest_cancel(skb, nest); | |
| return err; | |
| } | |
| static int tcp_diag_get_aux(struct sock *sk, bool net_admin, | |
| struct sk_buff *skb) | |
| { | |
| struct inet_connection_sock *icsk = inet_csk(sk); | |
| const struct tcp_ulp_ops *ulp_ops; | |
| int err = 0; | |
| #ifdef CONFIG_TCP_MD5SIG | |
| if (net_admin) { | |
| struct tcp_md5sig_info *md5sig; | |
| rcu_read_lock(); | |
| md5sig = rcu_dereference(tcp_sk(sk)->md5sig_info); | |
| if (md5sig) | |
| err = tcp_diag_put_md5sig(skb, md5sig); | |
| rcu_read_unlock(); | |
| if (err < 0) | |
| return err; | |
| } | |
| #endif | |
| ulp_ops = icsk->icsk_ulp_ops; | |
| if (ulp_ops) { | |
| err = tcp_diag_put_ulp(skb, sk, ulp_ops, net_admin); | |
| if (err < 0) | |
| return err; | |
| } | |
| return 0; | |
| } | |
| static size_t tcp_diag_get_aux_size(struct sock *sk, bool net_admin) | |
| { | |
| struct inet_connection_sock *icsk = inet_csk(sk); | |
| size_t size = 0; | |
| #ifdef CONFIG_TCP_MD5SIG | |
| if (net_admin && sk_fullsock(sk)) { | |
| const struct tcp_md5sig_info *md5sig; | |
| const struct tcp_md5sig_key *key; | |
| size_t md5sig_count = 0; | |
| rcu_read_lock(); | |
| md5sig = rcu_dereference(tcp_sk(sk)->md5sig_info); | |
| if (md5sig) { | |
| hlist_for_each_entry_rcu(key, &md5sig->head, node) | |
| md5sig_count++; | |
| } | |
| rcu_read_unlock(); | |
| size += nla_total_size(md5sig_count * | |
| sizeof(struct tcp_diag_md5sig)); | |
| } | |
| #endif | |
| if (sk_fullsock(sk)) { | |
| const struct tcp_ulp_ops *ulp_ops; | |
| ulp_ops = icsk->icsk_ulp_ops; | |
| if (ulp_ops) { | |
| size += nla_total_size(0) + | |
| nla_total_size(TCP_ULP_NAME_MAX); | |
| if (ulp_ops->get_info_size) | |
| size += ulp_ops->get_info_size(sk, net_admin); | |
| } | |
| } | |
| return size | |
| + nla_total_size(sizeof(struct tcp_info)) | |
| + nla_total_size(sizeof(struct inet_diag_msg)) | |
| + inet_diag_msg_attrs_size() | |
| + nla_total_size(sizeof(struct inet_diag_meminfo)) | |
| + nla_total_size(SK_MEMINFO_VARS * sizeof(u32)) | |
| + nla_total_size(TCP_CA_NAME_MAX) | |
| + nla_total_size(sizeof(struct tcpvegas_info)) | |
| + 64; | |
| } | |
| static int tcp_twsk_diag_fill(struct sock *sk, | |
| struct sk_buff *skb, | |
| struct netlink_callback *cb, | |
| u16 nlmsg_flags, bool net_admin) | |
| { | |
| struct inet_timewait_sock *tw = inet_twsk(sk); | |
| struct inet_diag_msg *r; | |
| struct nlmsghdr *nlh; | |
| long tmo; | |
| nlh = nlmsg_put(skb, NETLINK_CB(cb->skb).portid, | |
| cb->nlh->nlmsg_seq, cb->nlh->nlmsg_type, | |
| sizeof(*r), nlmsg_flags); | |
| if (!nlh) | |
| return -EMSGSIZE; | |
| r = nlmsg_data(nlh); | |
| DEBUG_NET_WARN_ON_ONCE(tw->tw_state != TCP_TIME_WAIT); | |
| inet_diag_msg_common_fill(r, sk); | |
| r->idiag_retrans = 0; | |
| r->idiag_state = READ_ONCE(tw->tw_substate); | |
| r->idiag_timer = IDIAG_TIMER_TIMEWAIT; | |
| tmo = tw->tw_timer.expires - jiffies; | |
| r->idiag_expires = jiffies_delta_to_msecs(tmo); | |
| r->idiag_rqueue = 0; | |
| r->idiag_wqueue = 0; | |
| r->idiag_uid = 0; | |
| r->idiag_inode = 0; | |
| if (net_admin && nla_put_u32(skb, INET_DIAG_MARK, | |
| tw->tw_mark)) { | |
| nlmsg_cancel(skb, nlh); | |
| return -EMSGSIZE; | |
| } | |
| nlmsg_end(skb, nlh); | |
| return 0; | |
| } | |
| static int tcp_req_diag_fill(struct sock *sk, struct sk_buff *skb, | |
| struct netlink_callback *cb, | |
| u16 nlmsg_flags, bool net_admin) | |
| { | |
| struct request_sock *reqsk = inet_reqsk(sk); | |
| struct inet_diag_msg *r; | |
| struct nlmsghdr *nlh; | |
| long tmo; | |
| nlh = nlmsg_put(skb, NETLINK_CB(cb->skb).portid, cb->nlh->nlmsg_seq, | |
| cb->nlh->nlmsg_type, sizeof(*r), nlmsg_flags); | |
| if (!nlh) | |
| return -EMSGSIZE; | |
| r = nlmsg_data(nlh); | |
| inet_diag_msg_common_fill(r, sk); | |
| r->idiag_state = TCP_SYN_RECV; | |
| r->idiag_timer = IDIAG_TIMER_ON; | |
| r->idiag_retrans = READ_ONCE(reqsk->num_retrans); | |
| BUILD_BUG_ON(offsetof(struct inet_request_sock, ir_cookie) != | |
| offsetof(struct sock, sk_cookie)); | |
| tmo = READ_ONCE(inet_reqsk(sk)->rsk_timer.expires) - jiffies; | |
| r->idiag_expires = jiffies_delta_to_msecs(tmo); | |
| r->idiag_rqueue = 0; | |
| r->idiag_wqueue = 0; | |
| r->idiag_uid = 0; | |
| r->idiag_inode = 0; | |
| if (net_admin && nla_put_u32(skb, INET_DIAG_MARK, | |
| inet_rsk(reqsk)->ir_mark)) { | |
| nlmsg_cancel(skb, nlh); | |
| return -EMSGSIZE; | |
| } | |
| nlmsg_end(skb, nlh); | |
| return 0; | |
| } | |
| static int sk_diag_fill(struct sock *sk, struct sk_buff *skb, | |
| struct netlink_callback *cb, | |
| const struct inet_diag_req_v2 *r, | |
| u16 nlmsg_flags, bool net_admin) | |
| { | |
| if (sk->sk_state == TCP_TIME_WAIT) | |
| return tcp_twsk_diag_fill(sk, skb, cb, nlmsg_flags, net_admin); | |
| if (sk->sk_state == TCP_NEW_SYN_RECV) | |
| return tcp_req_diag_fill(sk, skb, cb, nlmsg_flags, net_admin); | |
| return inet_sk_diag_fill(sk, inet_csk(sk), skb, cb, r, nlmsg_flags, | |
| net_admin); | |
| } | |
| static void twsk_build_assert(void) | |
| { | |
| BUILD_BUG_ON(offsetof(struct inet_timewait_sock, tw_family) != | |
| offsetof(struct sock, sk_family)); | |
| BUILD_BUG_ON(offsetof(struct inet_timewait_sock, tw_num) != | |
| offsetof(struct inet_sock, inet_num)); | |
| BUILD_BUG_ON(offsetof(struct inet_timewait_sock, tw_dport) != | |
| offsetof(struct inet_sock, inet_dport)); | |
| BUILD_BUG_ON(offsetof(struct inet_timewait_sock, tw_rcv_saddr) != | |
| offsetof(struct inet_sock, inet_rcv_saddr)); | |
| BUILD_BUG_ON(offsetof(struct inet_timewait_sock, tw_daddr) != | |
| offsetof(struct inet_sock, inet_daddr)); | |
| #if IS_ENABLED(CONFIG_IPV6) | |
| BUILD_BUG_ON(offsetof(struct inet_timewait_sock, tw_v6_rcv_saddr) != | |
| offsetof(struct sock, sk_v6_rcv_saddr)); | |
| BUILD_BUG_ON(offsetof(struct inet_timewait_sock, tw_v6_daddr) != | |
| offsetof(struct sock, sk_v6_daddr)); | |
| #endif | |
| } | |
| static void tcp_diag_dump(struct sk_buff *skb, struct netlink_callback *cb, | |
| const struct inet_diag_req_v2 *r) | |
| { | |
| bool net_admin = netlink_net_capable(cb->skb, CAP_NET_ADMIN); | |
| struct inet_diag_dump_data *cb_data = cb->data; | |
| struct net *net = sock_net(skb->sk); | |
| u32 idiag_states = r->idiag_states; | |
| struct inet_hashinfo *hashinfo; | |
| int i, num, s_i, s_num; | |
| struct sock *sk; | |
| hashinfo = net->ipv4.tcp_death_row.hashinfo; | |
| if (idiag_states & TCPF_SYN_RECV) | |
| idiag_states |= TCPF_NEW_SYN_RECV; | |
| s_i = cb->args[1]; | |
| s_num = num = cb->args[2]; | |
| if (cb->args[0] == 0) { | |
| if (!(idiag_states & TCPF_LISTEN) || r->id.idiag_dport) | |
| goto skip_listen_ht; | |
| for (i = s_i; i <= hashinfo->lhash2_mask; i++) { | |
| struct inet_listen_hashbucket *ilb; | |
| struct hlist_nulls_node *node; | |
| num = 0; | |
| ilb = &hashinfo->lhash2[i]; | |
| if (hlist_nulls_empty(&ilb->nulls_head)) { | |
| s_num = 0; | |
| continue; | |
| } | |
| spin_lock(&ilb->lock); | |
| sk_nulls_for_each(sk, node, &ilb->nulls_head) { | |
| struct inet_sock *inet = inet_sk(sk); | |
| if (!net_eq(sock_net(sk), net)) | |
| continue; | |
| if (num < s_num) { | |
| num++; | |
| continue; | |
| } | |
| if (r->sdiag_family != AF_UNSPEC && | |
| sk->sk_family != r->sdiag_family) | |
| goto next_listen; | |
| if (r->id.idiag_sport != inet->inet_sport && | |
| r->id.idiag_sport) | |
| goto next_listen; | |
| if (!inet_diag_bc_sk(cb_data, sk)) | |
| goto next_listen; | |
| if (inet_sk_diag_fill(sk, inet_csk(sk), skb, | |
| cb, r, NLM_F_MULTI, | |
| net_admin) < 0) { | |
| spin_unlock(&ilb->lock); | |
| goto done; | |
| } | |
| next_listen: | |
| ++num; | |
| } | |
| spin_unlock(&ilb->lock); | |
| s_num = 0; | |
| } | |
| skip_listen_ht: | |
| cb->args[0] = 1; | |
| s_i = num = s_num = 0; | |
| } | |
| /* Process a maximum of SKARR_SZ sockets at a time when walking hash buckets | |
| * with bh disabled. | |
| */ | |
| #define SKARR_SZ 16 | |
| /* Dump bound but inactive (not listening, connecting, etc.) sockets */ | |
| if (cb->args[0] == 1) { | |
| if (!(idiag_states & TCPF_BOUND_INACTIVE)) | |
| goto skip_bind_ht; | |
| for (i = s_i; i < hashinfo->bhash_size; i++) { | |
| struct inet_bind_hashbucket *ibb; | |
| struct inet_bind2_bucket *tb2; | |
| struct sock *sk_arr[SKARR_SZ]; | |
| int num_arr[SKARR_SZ]; | |
| int idx, accum, res; | |
| resume_bind_walk: | |
| num = 0; | |
| accum = 0; | |
| ibb = &hashinfo->bhash2[i]; | |
| if (hlist_empty(&ibb->chain)) { | |
| s_num = 0; | |
| continue; | |
| } | |
| spin_lock_bh(&ibb->lock); | |
| inet_bind_bucket_for_each(tb2, &ibb->chain) { | |
| if (!net_eq(ib2_net(tb2), net)) | |
| continue; | |
| sk_for_each_bound(sk, &tb2->owners) { | |
| struct inet_sock *inet = inet_sk(sk); | |
| if (num < s_num) | |
| goto next_bind; | |
| if (sk->sk_state != TCP_CLOSE || | |
| !inet->inet_num) | |
| goto next_bind; | |
| if (r->sdiag_family != AF_UNSPEC && | |
| r->sdiag_family != sk->sk_family) | |
| goto next_bind; | |
| if (!inet_diag_bc_sk(cb_data, sk)) | |
| goto next_bind; | |
| sock_hold(sk); | |
| num_arr[accum] = num; | |
| sk_arr[accum] = sk; | |
| if (++accum == SKARR_SZ) | |
| goto pause_bind_walk; | |
| next_bind: | |
| num++; | |
| } | |
| } | |
| pause_bind_walk: | |
| spin_unlock_bh(&ibb->lock); | |
| res = 0; | |
| for (idx = 0; idx < accum; idx++) { | |
| if (res >= 0) { | |
| res = inet_sk_diag_fill(sk_arr[idx], | |
| NULL, skb, cb, | |
| r, NLM_F_MULTI, | |
| net_admin); | |
| if (res < 0) | |
| num = num_arr[idx]; | |
| } | |
| sock_put(sk_arr[idx]); | |
| } | |
| if (res < 0) | |
| goto done; | |
| cond_resched(); | |
| if (accum == SKARR_SZ) { | |
| s_num = num + 1; | |
| goto resume_bind_walk; | |
| } | |
| s_num = 0; | |
| } | |
| skip_bind_ht: | |
| cb->args[0] = 2; | |
| s_i = num = s_num = 0; | |
| } | |
| if (!(idiag_states & ~TCPF_LISTEN)) | |
| goto out; | |
| for (i = s_i; i <= hashinfo->ehash_mask; i++) { | |
| struct inet_ehash_bucket *head = &hashinfo->ehash[i]; | |
| spinlock_t *lock = inet_ehash_lockp(hashinfo, i); | |
| struct hlist_nulls_node *node; | |
| struct sock *sk_arr[SKARR_SZ]; | |
| int num_arr[SKARR_SZ]; | |
| int idx, accum, res; | |
| if (hlist_nulls_empty(&head->chain)) | |
| continue; | |
| if (i > s_i) | |
| s_num = 0; | |
| next_chunk: | |
| num = 0; | |
| accum = 0; | |
| spin_lock_bh(lock); | |
| sk_nulls_for_each(sk, node, &head->chain) { | |
| int state; | |
| if (!net_eq(sock_net(sk), net)) | |
| continue; | |
| if (num < s_num) | |
| goto next_normal; | |
| state = (sk->sk_state == TCP_TIME_WAIT) ? | |
| READ_ONCE(inet_twsk(sk)->tw_substate) : sk->sk_state; | |
| if (!(idiag_states & (1 << state))) | |
| goto next_normal; | |
| if (r->sdiag_family != AF_UNSPEC && | |
| sk->sk_family != r->sdiag_family) | |
| goto next_normal; | |
| if (r->id.idiag_sport != htons(READ_ONCE(sk->sk_num)) && | |
| r->id.idiag_sport) | |
| goto next_normal; | |
| if (r->id.idiag_dport != sk->sk_dport && | |
| r->id.idiag_dport) | |
| goto next_normal; | |
| twsk_build_assert(); | |
| if (!inet_diag_bc_sk(cb_data, sk)) | |
| goto next_normal; | |
| if (!refcount_inc_not_zero(&sk->sk_refcnt)) | |
| goto next_normal; | |
| num_arr[accum] = num; | |
| sk_arr[accum] = sk; | |
| if (++accum == SKARR_SZ) | |
| break; | |
| next_normal: | |
| ++num; | |
| } | |
| spin_unlock_bh(lock); | |
| res = 0; | |
| for (idx = 0; idx < accum; idx++) { | |
| if (res >= 0) { | |
| res = sk_diag_fill(sk_arr[idx], skb, cb, r, | |
| NLM_F_MULTI, net_admin); | |
| if (res < 0) | |
| num = num_arr[idx]; | |
| } | |
| sock_gen_put(sk_arr[idx]); | |
| } | |
| if (res < 0) | |
| break; | |
| cond_resched(); | |
| if (accum == SKARR_SZ) { | |
| s_num = num + 1; | |
| goto next_chunk; | |
| } | |
| } | |
| done: | |
| cb->args[1] = i; | |
| cb->args[2] = num; | |
| out: | |
| ; | |
| } | |
| static struct sock *tcp_diag_find_one_icsk(struct net *net, | |
| const struct inet_diag_req_v2 *req) | |
| { | |
| struct sock *sk; | |
| rcu_read_lock(); | |
| if (req->sdiag_family == AF_INET) { | |
| sk = inet_lookup(net, NULL, 0, req->id.idiag_dst[0], | |
| req->id.idiag_dport, req->id.idiag_src[0], | |
| req->id.idiag_sport, req->id.idiag_if); | |
| #if IS_ENABLED(CONFIG_IPV6) | |
| } else if (req->sdiag_family == AF_INET6) { | |
| if (ipv6_addr_v4mapped((struct in6_addr *)req->id.idiag_dst) && | |
| ipv6_addr_v4mapped((struct in6_addr *)req->id.idiag_src)) | |
| sk = inet_lookup(net, NULL, 0, req->id.idiag_dst[3], | |
| req->id.idiag_dport, req->id.idiag_src[3], | |
| req->id.idiag_sport, req->id.idiag_if); | |
| else | |
| sk = inet6_lookup(net, NULL, 0, | |
| (struct in6_addr *)req->id.idiag_dst, | |
| req->id.idiag_dport, | |
| (struct in6_addr *)req->id.idiag_src, | |
| req->id.idiag_sport, | |
| req->id.idiag_if); | |
| #endif | |
| } else { | |
| rcu_read_unlock(); | |
| return ERR_PTR(-EINVAL); | |
| } | |
| rcu_read_unlock(); | |
| if (!sk) | |
| return ERR_PTR(-ENOENT); | |
| if (sock_diag_check_cookie(sk, req->id.idiag_cookie)) { | |
| sock_gen_put(sk); | |
| return ERR_PTR(-ENOENT); | |
| } | |
| return sk; | |
| } | |
| static int tcp_diag_dump_one(struct netlink_callback *cb, | |
| const struct inet_diag_req_v2 *req) | |
| { | |
| struct sk_buff *in_skb = cb->skb; | |
| struct sk_buff *rep; | |
| struct sock *sk; | |
| struct net *net; | |
| bool net_admin; | |
| int err; | |
| net = sock_net(in_skb->sk); | |
| sk = tcp_diag_find_one_icsk(net, req); | |
| if (IS_ERR(sk)) | |
| return PTR_ERR(sk); | |
| net_admin = netlink_net_capable(in_skb, CAP_NET_ADMIN); | |
| rep = nlmsg_new(tcp_diag_get_aux_size(sk, net_admin), GFP_KERNEL); | |
| if (!rep) { | |
| err = -ENOMEM; | |
| goto out; | |
| } | |
| err = sk_diag_fill(sk, rep, cb, req, 0, net_admin); | |
| if (err < 0) { | |
| WARN_ON(err == -EMSGSIZE); | |
| nlmsg_free(rep); | |
| goto out; | |
| } | |
| err = nlmsg_unicast(net->diag_nlsk, rep, NETLINK_CB(in_skb).portid); | |
| out: | |
| if (sk) | |
| sock_gen_put(sk); | |
| return err; | |
| } | |
| #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; | |
| int err; | |
| sk = tcp_diag_find_one_icsk(net, req); | |
| if (IS_ERR(sk)) | |
| return PTR_ERR(sk); | |
| err = sock_diag_destroy(sk, ECONNABORTED); | |
| sock_gen_put(sk); | |
| return err; | |
| } | |
| #endif | |
| static const struct inet_diag_handler tcp_diag_handler = { | |
| .owner = THIS_MODULE, | |
| .dump = tcp_diag_dump, | |
| .dump_one = tcp_diag_dump_one, | |
| .idiag_get_info = tcp_diag_get_info, | |
| .idiag_get_aux = tcp_diag_get_aux, | |
| .idiag_type = IPPROTO_TCP, | |
| .idiag_info_size = sizeof(struct tcp_info), | |
| #ifdef CONFIG_INET_DIAG_DESTROY | |
| .destroy = tcp_diag_destroy, | |
| #endif | |
| }; | |
| static int __init tcp_diag_init(void) | |
| { | |
| return inet_diag_register(&tcp_diag_handler); | |
| } | |
| static void __exit tcp_diag_exit(void) | |
| { | |
| inet_diag_unregister(&tcp_diag_handler); | |
| } | |
| module_init(tcp_diag_init); | |
| module_exit(tcp_diag_exit); | |
| MODULE_LICENSE("GPL"); | |
| MODULE_DESCRIPTION("TCP socket monitoring via SOCK_DIAG"); | |
| MODULE_ALIAS_NET_PF_PROTO_TYPE(PF_NETLINK, NETLINK_SOCK_DIAG, 2-6 /* AF_INET - IPPROTO_TCP */); |