前言: 互联网后台的服务器上,通常需要运行多达数百个进程,甚至更多。 有一天,运维兄弟突然找上门,说:xx服务器上为什么要访问yy服务(例如yy服务使用UDP的12345端口)? 开发一脸懵逼:没有呀!并不是我部署的服务访问的。。。 运维兄弟:我不管,这台机器分配给你了,你要负责,要不你抓包看看? 开发兄弟娴熟的一手tcpdump -iany -Xnnls0 udp port 12345:哎呦我去,还真有进程在访问,but,是哪个进程呢?UDP无连接,netstat是没有办法了,tcpdump只能证明有包发送。好吧,这个问题怎么搞@#¥%& 作者曾经就是那个开发,被运维diao的一愣一愣的。于是,就有了这个问题的一个解决方案。 准备知识: netfilter:源代码见linux-4.0.4/net/netfilter目录,并需要参考linux-4.0.4/net/ipv4目录。常用的命令iptables就是基于netfilter实现的。关于netfilter,作者也只是大致了解了一下基本逻辑,大致知道xtable而已。而ipv4协议栈中,会有代码和netfilter相关。在ipv4目录下,grep -i nf_hook *.c,可以看到ipv4实现的几个hook点的位置和大致逻辑。其中,这里要使用的就是NF_INET_LOCAL_OUT这个hook点。 关于NF_INET_LOCAL_OUT:ipv4的hook实现是在linux-4.0.4/net/ipv4/ip_output.c文件的__ip_local_out函数中。因为用户进程使用sendto陷入到kernel mode,再从syscall到__ip_local_out,一直都是同步调用,所以,如果运行到这个hook点,那么就是正在调用sendto的进程。而这,正是解决问题的基础。 代码: 路径:https://github.com/pacepi/port_connection
#include <linux/netfilter.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/netfilter_ipv4.h>
#include <linux/ip.h>
#include <linux/udp.h>
#include <linux/inet.h>
#include <linux/sysctl.h>
static unsigned int min_port = 0;
static unsigned int max_port = 65535;
static unsigned int sysctl_udp_port_connection = 0;
static struct ctl_table_header *ctl_header = NULL;
static struct ctl_table port_conn_table[] = {
{
.procname = "udp_port_connection",//注册sysctl的控制入口
.data = &sysctl_udp_port_connection,
.maxlen = sizeof(sysctl_udp_port_connection),
.mode = 0644,
.proc_handler = proc_dointvec_minmax,
.extra1 = &min_port,
.extra2 = &max_port,
},
{
}
};
unsigned int __port_connection_hookfn(unsigned int hooknum,
struct sk_buff *skb,
const struct net_device *in,
const struct net_device *out,
int (*okfn)(struct sk_buff *))
{
struct iphdr *iph;
struct udphdr *udph;
iph = ip_hdr(skb);
//printk(KERN_INFO"%d, protocol = %d, src IP %pI4\n", __LINE__, iph->protocol, &iph->saddr);
if (iph->protocol == IPPROTO_UDP)//这里也可以选择TCP,TCP短连接的情况下,同样也需要类似的办法抓取
{
udph = udp_hdr(skb);
if (udph->dest == ntohs(sysctl_udp_port_connection))
{
printk(KERN_INFO"UDP : pid = %d, comm = %s\n\n", current->pid, current->comm);//把发送特定UDP端口数据的进程打印出来,dmesg可以看到
}
}
return NF_ACCEPT;//accept不会影响原来的逻辑,如果不想继续发送了,就drop
}
static struct nf_hook_ops port_conn_hook = {
.hook = __port_connection_hookfn,
.pf = PF_INET,
.hooknum = NF_INET_LOCAL_OUT,
.priority = NF_IP_PRI_FIRST,
//.owner = THIS_MODULE,
};
static int __init nf_init(void)
{//ko的八股文,module init的时候,会调用这里
int err = 0;
err = nf_register_hook(&port_conn_hook);//注册netfilter的hook
if (err) {
printk(KERN_ERR"nf_register_hook() failed\n");
goto out;
}
ctl_header = register_net_sysctl(&init_net, "net/ipv4", port_conn_table);//注册/proc/sys/net/ipv4/udp_port_connection
if (ctl_header == NULL)
{
err = -ENOMEM;
goto unregister_hook;
}
goto out;
unregister_hook :
nf_unregister_hook(&port_conn_hook);
out :
return err;
}
static void __exit nf_exit(void)
{
nf_unregister_hook(&port_conn_hook);
unregister_net_sysctl_table(ctl_header);
ctl_header = NULL;
}
module_init(nf_init);
module_exit(nf_exit);
MODULE_AUTHOR("PiZhenwei p_ace@126.com");
MODULE_LICENSE("GPL");
后记:
其实这并不是当时解决问题的原版代码。原版中,作者是把代码写在linux-4.0.4/net/ipv4/udp.c中,需要重新编译kernel才行,并不方便易用。直到后来,大致翻翻netfilter的代码,才想到这个方法来解决。
当然,这里也可以使用kprobe或者systemtap来解决,也同样可以kprobe在__ip_local_out这个函数,核心逻辑也差不多:同样判断udp协议下,满足端口判断就记录一下。不过需要明确的是,二者的原理差别非常非常大。更重要的是,在选择解决方案时,看看当前的kernel是否支持kprobe。