手动清理 TCP TIME-WAIT 套接字:Linux 内核模块的实现与原理

日常运维Linux服务器时,你可能会遇到一个头疼的问题:服务器上堆积了大量处于TIME-WAIT状态的TCP连接,导致可用端口被占满,新的连接建立失败,服务响应变慢。常规的系统参数调整(比如修改net.ipv4.tcp_tw_reusetcp_tw_recycle)虽然能缓解,但有时我们需要更精准的方式------直接手动终止指定的TIME-WAIT连接。今天我们就聊聊如何通过一个Linux内核模块,实现对TCP连接(尤其是TIME-WAIT套接字)的精准"清理"。

一、先搞懂:TCP TIME-WAIT到底是个啥?

要理解这个模块的价值,得先明白TIME-WAIT是怎么来的。

TCP是个"靠谱"的协议,通信结束时要确保双方都正确收到了断开信号。当一方主动关闭连接(比如客户端),发送FIN包并收到对方的ACK后,不会立刻释放连接资源,而是进入TIME-WAIT状态,默认会停留2MSL(最大报文生存时间,Linux下约1分钟)。

这个设计的目的很简单:

  1. 防止延迟的报文被后续新建的同名连接接收,导致数据错乱;
  2. 确保对方能收到最后的ACK包,避免对方重发FIN包时没人回应。

但问题来了:如果服务器短时间内处理大量短连接(比如高频的HTTP短连接),就会堆积大量TIME-WAIT连接。每个连接都会占用一个端口,端口资源是有限的(默认0-65535),堆积多了就会出现"端口耗尽",新连接建不起来。

常规的系统参数调整是"全局生效"的,而我们今天聊的这个内核模块,能实现"精准打击"------只终止你指定的那些TIME-WAIT连接,不影响其他正常连接。

二、核心功能:这个内核模块能做什么?

简单说,这个模块的核心能力就是:

  1. 精准定位并终止指定的TCP连接(支持IPv4和IPv6);
  2. 对TIME-WAIT状态的套接字做专门处理,清理效率更高;
  3. 支持批量终止多个连接,不用一个个手动操作;
  4. 适配Linux 2.6.32及以上版本,还支持网络命名空间(容器环境也能用);
  5. 提供简单的交互方式:通过/proc文件系统写入要清理的连接信息,就能触发清理。

比如你查到有几个127.0.0.1:50866 → 127.0.0.1:22的TIME-WAIT连接,只需把这个地址端口对写入指定的/proc文件,模块就会自动找到并终止这个连接。

三、设计思路与实现原理

这个内核模块的实现逻辑并不复杂,核心是"接收用户指令 → 查找对应套接字 → 终止连接",我们一步步拆解:

c 复制代码
...
static int dts_pton(struct dts_inet *in)
{
	char *p, *end;
	if (in4_pton(in->p, -1, (void *)in->addr, -1, (const char **)&end)) {
		in->ipv6 = 0;
	} else if (in6_pton(in->p, -1, (void *)in->addr, -1, (const char **)&end)) {
		in->ipv6 = 1;
	} else return -EINVAL;

	p = (end += 1);
	while (*p && isdigit(*p)) p++;
	*p = 0; // kstrtoXX requires 0 at the end

	return kstrtou16(end, 10, &in->port);
}

static void dts_process(struct dts_pernet *dts, struct dts_data *d)
{
	char *p = d->data;
	struct dts_inet src, dst;

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wmisleading-indentation"
	while (*p && p < d->data + d->len) {
		while (*p && isspace(*p)) p++; if (!*p) return; // skip spaces
		src.p = p;
		while (*p && !isspace(*p)) p++; if (!*p) return; // skip non-spaces

		while (*p && isspace(*p)) p++; if (!*p) return; // skip spaces
		dst.p = p;
		while (*p && !isspace(*p)) p++; if (!*p) return; // skip non-spaces

		if ((dts_pton(&src) || dts_pton(&dst)) || (src.ipv6 != dst.ipv6))
			break;

		dts_kill(dts->net, &src, &dst), p++;
	}
#pragma GCC diagnostic pop
}

static int dts_proc_open(struct inode *inode, struct file *file)
{
	struct dts_data *d = kzalloc(PAGE_SIZE, GFP_KERNEL);
	if (!d) return -ENOMEM;
	d->available = PAGE_SIZE - (sizeof(*d)+1);
	file->private_data = d;
	return 0;
}

static ssize_t dts_proc_write(struct file *file, const char __user *buf, size_t size, loff_t *pos)
{
	struct dts_data *d = file->private_data;

	if (d->len + size > d->available) {
		size_t new_available = d->available + roundup(size, PAGE_SIZE);
		struct dts_data *dnew = krealloc(d, new_available + (sizeof(*d)+1), GFP_KERNEL);
		if (!dnew) {
			kfree(d), file->private_data = NULL;
			return -ENOMEM;
		}
		(d = dnew)->available = new_available;
		file->private_data = d; // update
	}

	if (copy_from_user(d->data + d->len, buf, size))
		return -EFAULT;
	d->data[(d->len += size)] = 0;

	return size;
}

static int dts_proc_release(struct inode *inode, struct file *file)
{
	struct dts_data *d = file->private_data;
	if (d) {
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 17, 0)
		dts_process(pde_data(file_inode(file)), d);
#elif LINUX_VERSION_CODE >= KERNEL_VERSION(3, 10, 0)
		dts_process(PDE_DATA(file_inode(file)), d);
#else
		dts_process(PDE(file->f_path.dentry->d_inode)->data, d);
#endif
		kfree(d), file->private_data = NULL;
	} return 0;
}

#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 6, 0)
static const struct proc_ops dts_proc_fops = {
	.proc_open = dts_proc_open,
	.proc_write = dts_proc_write,
	.proc_release = dts_proc_release,
};
#else
static const struct file_operations dts_proc_fops = {
	.owner = THIS_MODULE,
	.open = dts_proc_open,
	.write = dts_proc_write,
	.release = dts_proc_release,
};
#endif

static int dts_pernet_id = 0;

#if LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 33)
static int dts_pernet_init(struct net *net)
{
	struct dts_pernet *dts = net_generic(net, dts_pernet_id);
	dts->net = net;
	dts->pde = proc_create_data(DTS_PDE_NAME, 0600, net->proc_net, &dts_proc_fops, dts);
	return !dts->pde;
}
static void dts_pernet_exit(struct net* net)
{
	struct dts_pernet *dts = net_generic(net, dts_pernet_id);
	BUG_ON(!dts->pde);
	remove_proc_entry(DTS_PDE_NAME, net->proc_net);
}
#else
static int dts_pernet_init(struct net *net)
{
	struct dts_pernet *dts = NULL;

	dts = kzalloc(sizeof(*dts), GFP_KERNEL);
	if (!dts) return -ENOMEM;

	dts->net = net;
	dts->pde = proc_create_data(DTS_PDE_NAME, 0600, net->proc_net, &dts_proc_fops, dts);
	if (!dts->pde) {
		kfree(dts);
		return -ENOMEM;
	}

	if (net_assign_generic(net, dts_pernet_id, dts)) {
		kfree(dts);
		return -ENOMEM;
	}

	return 0;
}
static void dts_pernet_exit(struct net* net)
{
	struct dts_pernet *dts = net_generic(net, dts_pernet_id);
	net_assign_generic(net, dts_pernet_id, NULL);
	BUG_ON(!dts->pde);
	remove_proc_entry(DTS_PDE_NAME, net->proc_net);
	kfree(dts);
}
#endif

static struct pernet_operations dts_pernet_ops = {
	.init = dts_pernet_init,
	.exit = dts_pernet_exit,
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 33)
	.id = &dts_pernet_id,
	.size = sizeof(struct dts_pernet),
#endif
};

static inline int dts_register_pernet(void)
{
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 33)
	return register_pernet_subsys(&dts_pernet_ops);
#else
	return register_pernet_gen_subsys(&dts_pernet_id, &dts_pernet_ops);
#endif
}

static inline void dts_unregister_pernet(void)
{
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 33)
	unregister_pernet_subsys(&dts_pernet_ops);
#else
	unregister_pernet_gen_subsys(dts_pernet_id, &dts_pernet_ops);
#endif
}

////////////////////////////////////////////////////////////////////////////////

static int __init dts_module_init(void)
{
	int res = 0;

	if (!tcp_hashinfop()) {
		printk("DTS: no tcp_hashinfo found\n");
		return -EINVAL;
	}

	res = dts_register_pernet();
	if (res) return res;

	return 0;
}

module_init(dts_module_init);

static void __exit dts_module_exit(void)
{
	dts_unregister_pernet();
}

module_exit(dts_module_exit);
...

If you need the complete source code, please add the WeChat number (c17865354792)

1. 整体流程(可视化)

先看一张流程图,能快速理解模块的工作全过程:


TIME-WAIT
其他状态
用户加载内核模块
模块初始化:创建/proc/net下的交互文件
用户向/proc文件写入待清理的连接信息(如127.0.0.1:50866 127.0.0.1:22)
模块接收写入的数据,解析地址和端口(区分IPv4/IPv6)
根据解析出的源/目的地址+端口,在内核中查找对应的TCP套接字(sock结构体)
找到套接字?
结束,无操作
套接字状态?
调用内核接口释放TIME-WAIT套接字资源
调用tcp_done终止连接,释放套接字
输出日志,完成清理

2. 关键步骤拆解

我们用大白话讲每个核心步骤的实现逻辑:

(1)与用户交互:借助proc文件系统

Linux内核模块不能直接和用户态程序通信,所以模块选择了proc文件系统 作为交互桥梁------在/proc/net目录下创建一个专属文件,用户向这个文件写入内容,内核就能接收到。

模块里实现了proc文件的"打开(open)""写入(write)""释放(release)"三个核心接口:

  • 打开文件时:内核分配一块内存,用来存放用户后续写入的连接信息;
  • 写入文件时:把用户态的内容拷贝到内核内存中(还会自动扩容,支持批量写入);
  • 释放文件时(比如echo命令执行完):触发核心的清理逻辑,处理内存里的连接信息。
(2)解析用户输入:识别地址和端口

用户写入的内容是"源地址:端口 目的地址:端口"(比如127.0.0.1:50866 127.0.0.1:22),模块需要把这些字符串解析成内核能识别的格式:

  • 先区分是IPv4还是IPv6地址(用in4_pton/in6_pton函数);
  • 再把端口号从字符串转成数字(kstrtou16);
  • 把地址转换成内核网络栈使用的大端序格式(网络字节序)。

这里要注意一个细节:必须保证源和目的地址都是IPv4或都是IPv6,不能混着来,否则会直接跳过处理。

(3)查找内核中的TCP套接字

解析完地址和端口后,模块要在内核的TCP哈希表中找到对应的套接字(sock结构体)------这是最核心的一步。

Linux内核会把所有TCP套接字存在哈希表中(tcp_hashinfo),模块通过inet_lookup(IPv4)或inet6_lookup(IPv6)函数,传入源/目的地址和端口,就能精准查到对应的套接字。

这里有个兼容技巧:不同Linux内核版本中,tcp_hashinfo的获取方式不一样(6.15以上版本需要通过kallsyms查找符号地址),模块做了版本适配,确保在2.6.32以上版本都能正常工作。

(4)终止连接:分状态处理

找到套接字后,模块会根据套接字的状态做不同处理:

  • 如果是TIME-WAIT状态:调用inet_twsk_deschedule_put(新内核)或inet_twsk_deschedule + inet_twsk_put(旧内核),直接释放TIME-WAIT套接字的资源(这是清理TIME-WAIT的关键,比普通关闭更高效);
  • 如果是其他状态(比如ESTABLISHED):调用tcp_done终止连接,再调用sock_put释放套接字资源。

处理完成后,模块还会打印日志,告诉你清理了哪个套接字、源目的地址是什么、原来的状态是多少,方便排查。

3.测试运行完整步骤

步骤1:编译内核模块

进入代码所在目录,执行编译命令:

bash 复制代码
cd ~/tcp_clean_module
make
  • 编译成功:目录下会生成 tcp_clean.ko(核心的内核模块文件)、tcp_clean.o 等文件;
  • 编译失败:大概率是内核版本不匹配或依赖没装全,核对前面的环境准备步骤。
步骤2:加载内核模块
bash 复制代码
# 加载模块(root权限)
sudo insmod tcp_clean.ko

# 验证模块是否加载成功
lsmod | grep tcp_clean  # 能看到tcp_clean模块名,说明加载成功
步骤3:生成测试用的 TIME-WAIT 连接(模拟场景)

我们先手动造一个 TIME-WAIT 连接,方便测试:

bash 复制代码
# 1. 打开一个终端,启动一个临时的TCP监听端口(比如2222)
nc -l 2222

# 2. 打开第二个终端,连接这个监听端口,然后主动断开(会生成TIME-WAIT)
telnet 127.0.0.1 2222  # 连接后,按 Ctrl + ] 再按 q 退出,或直接关终端

# 3. 查看生成的TIME-WAIT连接
netstat -ant | grep TIME-WAIT | grep 2222
# 会看到类似输出:tcp  0  0 127.0.0.1:2222  127.0.0.1:54321  TIME-WAIT

记下输出里的「源地址:端口」和「目的地址:端口」(比如 127.0.0.1:54321 127.0.0.1:2222)。

步骤4:使用模块清理 TIME-WAIT 连接

模块通过 /proc/net/tcpdropsock 文件接收清理指令,执行清理:

bash 复制代码
# 替换成你实际查到的源/目的地址+端口(格式:源地址:端口 目的地址:端口)
echo "127.0.0.1:54321 127.0.0.1:2222" | sudo tee /proc/net/tcpdropsock
步骤5:验证清理效果

再次执行查看 TIME-WAIT 连接的命令,确认目标连接已消失:

bash 复制代码
netstat -ant | grep TIME-WAIT | grep 2222
# 没有输出 → 清理成功;有输出 → 检查地址/端口是否填错,或模块加载是否正常
步骤6:批量清理测试

如果想测试批量清理,先生成多个 TIME-WAIT 连接,再批量写入指令:

bash 复制代码
# 生成多个测试连接(循环执行多次连接+断开)
for i in {1..3}; do telnet 127.0.0.1 2222 < /dev/null; done

# 批量提取TIME-WAIT连接并清理
netstat -ant | grep TIME-WAIT | grep 2222 | awk '{print $4"\t"$5}' | sudo tee /proc/net/tcpdropsock

# 验证批量清理结果
netstat -ant | grep TIME-WAIT | grep 2222
步骤7:卸载内核模块(测试完成后)
bash 复制代码
# 卸载模块
sudo rmmod tcp_clean

# 验证卸载
lsmod | grep tcp_clean  # 无输出 → 卸载成功

排错小技巧(测试失败时用)

  1. 模块加载失败:执行 dmesg | tail 查看内核日志,会提示失败原因(比如内核版本不兼容、符号查找失败);
  2. 清理连接没效果:
    • 核对地址/端口是否写反(源和目的要和 netstat 输出一致);
    • 确认连接确实是 TIME-WAIT 状态(模块对其他状态的连接也能清理,但优先针对 TIME-WAIT);
  3. /proc/net/tcpdropsock 文件不存在:模块加载失败,重新编译/加载,或检查内核日志。

四、背后的核心内核知识点

这个模块的实现,用到了几个Linux内核开发的关键知识点,搞懂这些能帮你理解更多内核网络编程的逻辑:

1. proc文件系统的内核编程

proc文件系统是内核和用户态交互的"轻量级通道",不需要创建实际文件,数据都存在内存中。实现自定义proc文件需要:

  • 定义文件操作结构体(proc_ops/file_operations),关联open/write/release等回调;
  • proc_create_data创建文件,关联到指定的网络命名空间(/proc/net下);
  • 注意内存管理:用户写入的数据要在内核中分配内存,使用完必须释放,避免内存泄漏。

2. 内核版本兼容性

Linux内核版本迭代快,很多函数和结构体的定义会变(比如tcp_hashinfo的获取、proc文件的操作结构体)。模块通过LINUX_VERSION_CODE宏判断内核版本,适配不同的函数调用方式,这是内核模块开发的必备技巧。

3. 网络命名空间支持

现在容器(Docker、K8s)都用网络命名空间隔离网络环境,模块通过pernet_operations注册每个网络命名空间的初始化/退出回调,确保在不同的网络命名空间中都能创建专属的proc文件,适配容器环境。

4. 套接字(sock)结构体操作

内核中的sock结构体是TCP连接的核心表示,包含连接的状态、地址、端口、哈希表节点等信息。操作sock结构体时要注意:

  • 必须用内核提供的接口(比如inet_lookuptcp_done),不能直接修改结构体成员;
  • 释放套接字时要调用sock_put,避免引用计数泄漏。

五、实际使用价值

这个模块的设计思路和实现,不仅能解决TIME-WAIT堆积的问题,还能给我们带来这些启发:

  1. 精准运维:相比全局调整内核参数,精准清理指定连接能避免影响正常业务;
  2. 内核网络编程参考:模块的代码是学习"内核中操作TCP套接字"的绝佳案例,涵盖了地址解析、套接字查找、连接终止等核心操作;
  3. 兼容性设计:模块对不同内核版本的适配方式,是内核模块开发的通用思路。

需要注意的是:内核模块直接运行在内核态,错误的代码可能导致系统崩溃,使用前一定要在测试环境验证,并且只加载可信的模块。

总结

  1. TCP TIME-WAIT状态是为了保证TCP连接的可靠性,但大量堆积会导致端口耗尽,需要精准清理;
  2. 该内核模块通过proc文件系统与用户交互,解析地址端口后查找并终止指定TCP连接,对TIME-WAIT状态做专门优化;
  3. 模块的实现核心是内核中TCP套接字的查找与释放,同时兼顾了内核版本兼容性和网络命名空间支持。

这个模块的设计思路,本质是"利用内核提供的网络接口,封装成简单的用户交互方式",既体现了Linux内核的灵活性,也给我们解决网络运维问题提供了新的思路。

Welcome to follow WeChat official account【程序猿编码

相关推荐
北京国科云计算2 小时前
什么是IP SSL证书?IP SSL证书和域名SSL证书有什么区别?
tcp/ip·https·ssl
代码游侠2 小时前
应用——HTTP天气查询
网络·笔记·网络协议·算法·http
pwn蒸鱼2 小时前
buuctf中的ciscn_2019_es_2(栈迁移)
linux·安全
牛奶咖啡132 小时前
Linux的实用技巧——终端安全会话、命令提示工具安装使用、端口连通性测试与rm命令无法使用解决方案
linux·tmux·linux实现后台安全运行会话·linux的端口连通性测试·linux的命令提示工具·rm命令无法使用解决方法·tldr
fufu03112 小时前
Linux环境下的C语言编程(五十二)
java·linux·c语言
极客范儿2 小时前
从快手“12·22”事故出发:AI时代,如何构建对抗自动化攻击的动态免疫体系?
网络·人工智能·自动化
Warren982 小时前
MySQL 8 中的保留关键字陷阱:当表名“lead”引发 SQL 语法错误
linux·数据库·python·sql·mysql·django·virtualenv
Hard but lovely2 小时前
linux: pthread库---posix线程创建使用接口&&状态
linux·开发语言·c++
柏木乃一2 小时前
进程(7)命令行参数与环境变量
linux·服务器·shell·环境变量·鸣潮