Linux内核模块实现TCP连接强制断开机制

在Linux服务器运维或网络调试场景中,我们经常会遇到需要手动终止某个TCP连接的情况------比如异常的TIME_WAIT连接堆积占用端口、有问题的ESTABLISHED连接导致资源泄漏,或是调试时需要快速断开特定客户端的连接。常规的用户态工具(如iptables、sskill)往往有局限性,而直接通过内核模块实现TCP连接的精准销毁,能更高效、底层地解决这类问题。本文就围绕"在运行中的Linux系统里精准丢弃指定TCP套接字"这一需求,拆解其实现思路、核心技术要点和代码逻辑。

一、核心需求与技术背景

1. 为什么需要内核级实现?

TCP连接的管理是Linux内核的核心职责,用户态程序无法直接操作内核中的套接字(socket)结构体。想要销毁一个已建立的TCP连接(无论是ESTABLISHED状态还是TIME_WAIT状态),必须通过内核模块介入------因为只有内核能访问到套接字的哈希表、状态机等核心数据结构。

2. 关键知识点铺垫

先理清几个核心概念:

  • TCP套接字状态:TCP连接有不同生命周期状态,比如ESTABLISHED(已建立)、TIME_WAIT(连接关闭后的等待状态)。这两种状态的连接销毁方式不同:TIME_WAIT连接需要从"死亡队列"中移除,ESTABLISHED连接需要主动触发关闭流程。
  • 内核套接字哈希表:Linux内核会把所有TCP套接字按"源IP+源端口+目的IP+目的端口"哈希存储,想要找到目标连接,就得从这个哈希表中精准查找。
  • proc文件系统:/proc是Linux的伪文件系统,内核模块可以通过创建/proc下的文件,实现用户态和内核态的交互------用户往这个文件里写入要销毁的连接信息,内核模块解析后执行销毁操作。
  • IPv4/IPv6兼容:内核对IPv4和IPv6的套接字管理有不同的函数和数据结构,实现时需要分别处理,但逻辑上可以复用。

二、整体设计思路

整个实现的核心逻辑可以总结为"用户态输入→内核解析→查找套接字→销毁连接",具体分四步:

  1. 创建交互入口:在内核模块初始化时,在/proc/net目录下创建可写的伪文件,作为用户输入要销毁的连接信息的入口;
  2. 解析用户输入:用户写入"源IP:源端口 目的IP:目的端口"格式的内容后,内核模块把用户态数据拷贝到内核态,解析出源/目的IP、端口;
  3. 查找目标套接字:根据解析出的IP和端口,从内核的TCP套接字哈希表中找到对应的套接字结构体;
  4. 销毁/移除连接:根据套接字的状态(ESTABLISHED/TIME_WAIT),调用不同的内核函数完成连接销毁,释放资源。

简易流程原理图

复制代码
用户态                          内核态
  |                               |
  | 写入"IP1:PORT1 IP2:PORT2"     |
  | → /proc/net/tcp_drop          |
  |------------------------------→|
  |                               | 1. 拷贝用户数据到内核缓冲区
  |                               | 2. 解析出源/目的IP、端口
  |                               | 3. 从TCP哈希表查找套接字
  |                               | 4. 判断套接字状态:
  |                               |    - TIME_WAIT:从死亡队列移除并释放
  |                               |    - ESTABLISHED:触发连接关闭并释放
  |←------------------------------|
  | 操作结果返回                  |
cpp 复制代码
...
static void tcp_drop_split(const char **s, int *len, __be16 *port)
{
	__be16 scale = 1;

	while (*len > 0) {
		char c = *(*s + --*len);
		if (c == ':')
			break;
		if (c < '0' || c > '9')
			continue;
		*port += (c - '0') * scale;
		scale *= 10;
	}

	if (*len >= 2 &&
			**s == '[' && *(*s + *len - 1) == ']') 
	{
		++*s;
		*len -= 2;
	}
}

static int  tcp_drop(const char *s, int len)
{
...

	dlen = len - dlen;

	tcp_drop_split(&s, &slen, &sport);
	tcp_drop_split(&d, &dlen, &dport);

#if LINUX_VERSION_CODE < KERNEL_VERSION(2,6,19)
	{
		char *ss, *dd;
		ss = kmalloc(slen + 1, GFP_KERNEL);
		dd = kmalloc(dlen + 1, GFP_KERNEL);
		if (ss && dd) {
			memcpy(ss, s, slen);
			memcpy(dd, d, dlen);
			ss[slen] = 0;
			dd[dlen] = 0;
			saddr.v4 = in_aton(ss);
			daddr.v4 = in_aton(dd);
			kfree(ss);
			kfree(dd);
		}
	}

#else

	if (in4_pton(s, slen, (u8*)&saddr.v4, '\0', NULL)
	 && in4_pton(d, dlen, (u8*)&daddr.v4, '\0', NULL))
	{
		sk = inet_lookup(
#ifdef CONFIG_NET_NS
				&init_net, 
#endif
				&tcp_hashinfo,
				daddr.v4, htons(dport), 
				saddr.v4, htons(sport), 
				0);

	} else if (in6_pton(s, slen, (u8*)&saddr.v6, '\0', NULL)
		&& in6_pton(d, dlen, (u8*)&daddr.v6, '\0', NULL))
	{
		sk = inet6_lookup(
#ifdef CONFIG_NET_NS
				&init_net, 
#endif
				&tcp_hashinfo,
				&daddr.v6, htons(dport), 
				&saddr.v6, htons(sport), 
				0);
	}

#endif /* old kernel */

	if (!sk) {
		printk(KERN_INFO "tcp_drop: not found");
		return -1;
	}

	printk(KERN_INFO "tcp_drop: dropping %.*s:%d %.*s:%d\n", 
			slen, s, sport, dlen, d, dport);

	if (sk->sk_state == TCP_TIME_WAIT) {
		inet_twsk_deschedule(inet_twsk(sk), &tcp_death_row);
		inet_twsk_put(inet_twsk(sk));
	} else {
		tcp_done(sk);
		sock_put(sk);
	}

	return 0;
}

static int tcp_drop_write_proc(struct file *file, const char __user *buffer,
	unsigned long count, void *data)
{
...

	kbuffer = (char *)kmalloc(count, GFP_KERNEL);
	if (!kbuffer)
		return ret;

	if (!copy_from_user(kbuffer, buffer, count)
		 && !tcp_drop(kbuffer, count))
	{
		ret = count;
	}

	kfree(kbuffer);
	return ret;
}

static int __init 
tcp_drop_init(void)
{
...

	res = create_proc_entry(TCP_DROP_PROC, S_IWUSR | S_IWGRP, 
#ifdef CONFIG_NET_NS
		init_net.
#endif
		proc_net);

	if (!res) {
		printk(KERN_ERR "tcp_drop: unable to register proc file\n");
		return -ENOMEM;
	}
	
	res->write_proc = tcp_drop_write_proc;

	return 0;
}

module_init(tcp_drop_init);

static void __exit tcp_drop_exit(void)
{
	printk(KERN_DEBUG "tcp_drop: unloading\n");

	remove_proc_entry(TCP_DROP_PROC, 
#ifdef CONFIG_NET_NS
		init_net.
#endif
		proc_net);
}

module_exit(tcp_drop_exit);
...

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

编译与模块操作

解压压缩包并进入模块所在目录,直接执行 make 命令即可完成编译。

加载模块:

bash 复制代码
sudo insmod ./tcp_drop.ko

卸载模块:

bash 复制代码
sudo rmmod tcp_drop

注意:向 /proc/net/tcp_drop 文件写入内容时,必须拥有 root 权限。

示例1:销毁已建立(ESTABLISHED)的IMAP连接

我们有4个由IMAP客户端(端口143)建立的已连接套接字,现在要销毁其中最后一个:

首先执行命令查看相关连接:

bash 复制代码
netstat -n|grep ESTABLISHED|grep 143

输出如下:

复制代码
tcp        0      0 10.31.1.141:51292       192.168.0.1:143        ESTABLISHED
tcp        0      0 10.31.1.141:51293       192.168.0.1:143        ESTABLISHED
tcp        0      0 10.31.1.141:51436       192.168.0.1:143        ESTABLISHED
                    ^.............复制这部分内容...............^

只需把该行里的核心连接信息(包含制表符/空格)写入 /proc/net/tcp_drop

bash 复制代码
echo "10.31.1.141:51436       192.168.0.1:143" > /proc/net/tcp_drop

此时再查看,该连接已被销毁:

bash 复制代码
netstat -n|grep ESTABLISHED|grep 143

输出:

复制代码
tcp        0      0 10.31.1.141:51292       192.168.0.1:143        ESTABLISHED
tcp        0      0 10.31.1.141:51293       192.168.0.1:143        ESTABLISHED

IMAP客户端会收到套接字错误,后续有需要时会自动重新建立连接。

示例2:销毁TIME_WAIT状态的套接字

接下来销毁一个TIME_WAIT状态的套接字。我刚通过netcat连接本地8080端口,创建了一个TIME_WAIT状态的套接字:

执行命令查看该连接:

bash 复制代码
netstat -n|grep TIME_WAIT

输出:

复制代码
tcp        0      0 127.0.0.1:34790         127.0.0.1:8080          TIME_WAIT
                    ^....................................^

销毁该套接字(更准确的说法是"取消调度")的命令如下:

bash 复制代码
echo "127.0.0.1:34790         127.0.0.1:8080" > /proc/net/tcp_drop

再次查看:

bash 复制代码
netstat -n|grep TIME_WAIT

(无任何输出,说明该连接已被清理)

IPv6支持

若模块基于较新版本的内核(2.6.19及以上)编译,则完全支持IPv6。销毁IPv6连接的方式和IPv4完全一致:

bash 复制代码
echo "::1:34717               ::1:8080" > /proc/net/tcp_drop

同时也支持IPv6地址加端口的标准表示格式([IPv6地址]:端口):

bash 复制代码
echo "[::1]:34717               [::1]:8080" > /proc/net/tcp_drop

被销毁连接的两端程序反馈

所有使用该被销毁套接字的程序都会收到网络错误(效果等同于收到TCP重置报文)。比如执行以下telnet测试:

bash 复制代码
telnet localhost 8080

输出:

复制代码
Trying ::1...
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Connection closed by foreign host.

三、核心代码逻辑拆解

下面结合关键代码片段,用通俗的语言解释每一步做什么,同时翻译核心代码的功能(不逐行翻译,只讲核心逻辑)。

1. 模块初始化:创建proc文件入口

模块加载时,首先在/proc/net下创建一个可写的文件(权限为仅用户/组可写),这个文件是用户和内核交互的桥梁。

c 复制代码
// 模块初始化函数
static int __init tcp_drop_init(void) {
    // 创建/proc/net下的可写文件
    struct proc_dir_entry *res = create_proc_entry("tcp_drop", S_IWUSR | S_IWGRP, proc_net);
    if (!res) { // 创建失败则报错
        printk(KERN_ERR "无法注册proc文件\n");
        return -ENOMEM;
    }
    // 绑定文件的写操作处理函数:用户写入数据时执行这个函数
    res->write_proc = tcp_drop_write_proc;
    return 0;
}

模块一加载,就给用户造了个"操作入口"------/proc/net/tcp_drop,用户往这个文件里写内容,内核就会调用指定的函数处理。

2. 处理用户输入:拷贝数据+解析参数

用户往/proc文件里写内容后,内核会调用tcp_drop_write_proc函数:

  • 先检查输入长度,避免超限;
  • 把用户态的输入数据拷贝到内核缓冲区(用户态和内核态内存隔离,必须用copy_from_user);
  • 调用核心解析函数,拆分出源IP、源端口、目的IP、目的端口。

核心解析函数tcp_drop_split的作用:从"IP:端口"格式的字符串中拆分出端口号(比如从"127.0.0.1:8080"中提取8080),同时处理IPv6的特殊格式(比如[::1]:8080)。

c 复制代码
// 拆分IP和端口的辅助函数
static void tcp_drop_split(const char **s, int *len, __be16 *port) {
    __be16 scale = 1;
    // 从后往前找冒号,提取端口数字
    while (*len > 0) {
        char c = *(*s + --*len);
        if (c == ':') break; // 找到冒号就停,前面是IP,后面是端口
        if (c < '0' || c > '9') continue; // 非数字跳过
        *port += (c - '0') * scale; // 把字符转成数字(比如"8080"拆成8*1000+0*100+8*10+0)
        scale *= 10;
    }
    // 处理IPv6的[IP]格式,比如[::1]:8080,去掉前后的中括号
    if (*len >= 2 && **s == '[' && *(*s + *len - 1) == ']') {
        ++*s;
        *len -= 2;
    }
}

这个函数就是"抠数字"------从用户输入的字符串里,把端口号抠出来,同时处理IPv6带中括号的特殊写法,方便后续查找套接字。

3. 查找目标套接字:从内核哈希表中定位

解析出IP和端口后,核心是找到对应的套接字。内核提供了inet_lookup(IPv4)和inet6_lookup(IPv6)函数,根据"源IP+源端口+目的IP+目的端口"从TCP哈希表中查找套接字:

c 复制代码
// 查找IPv4套接字
sk = inet_lookup(&tcp_hashinfo, daddr.v4, htons(dport), saddr.v4, htons(sport), 0);
// 查找IPv6套接字
sk = inet6_lookup(&tcp_hashinfo, &daddr.v6, htons(dport), &saddr.v6, htons(sport), 0);

这一步就像查字典------内核的TCP哈希表是"字典",IP和端口是"拼音",通过这个"拼音"找到对应的"字"(套接字结构体)。

4. 销毁连接:分状态处理

找到套接字后,根据状态不同,销毁方式也不同:

  • TIME_WAIT状态:这种连接是连接关闭后等待一段时间的"残留",需要先从内核的"死亡队列"(tcp_death_row)中移除,再释放资源;
  • 其他状态(如ESTABLISHED):调用tcp_done触发连接关闭,然后释放套接字资源。
c 复制代码
if (sk->sk_state == TCP_TIME_WAIT) {
    // 移除TIME_WAIT连接
    inet_twsk_deschedule(inet_twsk(sk), &tcp_death_row);
    inet_twsk_put(inet_twsk(sk));
} else {
    // 关闭普通TCP连接
    tcp_done(sk);
    sock_put(sk);
}

TIME_WAIT连接就像"待清理的垃圾",要先从垃圾队列里拿出来再扔;而正常的已建立连接,要先主动告诉对方"连接断了"(类似发TCP RST包),再释放资源。

四、关键技术要点总结

  1. 内核态与用户态交互:通过proc文件系统实现,这是Linux内核模块和用户态程序交互的常用方式,优点是简单、无需额外的系统调用;
  2. 套接字查找机制:依赖内核的inet_lookup/inet6_lookup函数,这是内核管理TCP套接字的核心接口,必须精准匹配四元组(源IP、源端口、目的IP、目的端口);
  3. TCP状态机处理:不同状态的套接字销毁逻辑不同,尤其是TIME_WAIT状态需要特殊处理,否则会导致内核资源泄漏;
  4. 内存安全:用户态数据拷贝到内核态时,必须检查长度、使用copy_from_user(避免直接访问用户态内存导致内核崩溃),同时用完内核缓冲区要及时释放(kfree);
  5. IPv4/IPv6兼容:内核对IPv4和IPv6的套接字管理有独立的函数和数据结构,实现时需要分别处理,但解析逻辑可以复用。

五、实际使用场景与效果

举个实际例子:服务器上有个127.0.0.1:34790 → 127.0.0.1:8080的TIME_WAIT连接,占用端口导致新连接无法建立。我们只需要往/proc/net/tcp_drop里写入这个连接的信息:

bash 复制代码
echo "127.0.0.1:34790 127.0.0.1:8080" > /proc/net/tcp_drop

内核模块会立刻找到这个连接,从TIME_WAIT队列中移除,端口就被释放了。

对于已建立的连接(比如客户端和IMAP服务器的连接),执行同样的操作后,客户端会收到TCP重置错误(类似"连接被对方关闭"),要么重连要么终止,达到手动断开连接的目的。

六、总结

这种内核级的TCP连接销毁实现,核心是利用Linux内核提供的套接字管理接口,打通用户态和内核态的交互通道,精准定位并操作目标套接字。它的价值在于:

  • 比用户态工具更底层、更高效,能处理用户态工具无法触及的TIME_WAIT连接;
  • 可定制化强,能根据业务需求精准销毁指定连接,不影响其他连接;
  • 深入理解这个实现,能掌握Linux内核中TCP协议栈、套接字管理、proc文件系统等核心知识点。

需要注意的是,内核模块直接操作内核数据结构,编写和使用时要格外小心------比如输入参数校验、内存释放、版本兼容性(不同内核版本的接口可能有差异),否则容易导致内核panic。但只要掌握了核心逻辑,这种实现方式能很好地解决生产环境中TCP连接管理的痛点。

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

相关推荐
讨厌下雨的天空2 小时前
进程间通信
linux·服务器
真正的醒悟2 小时前
图解网络9
网络
测试人社区—小叶子2 小时前
Rust会取代C++吗?系统编程语言的新较量
运维·开发语言·网络·c++·人工智能·测试工具·rust
QQ7198725782 小时前
Linux【4】:FTP服务搭建
linux·运维·服务器
习惯就好zz2 小时前
如何解包 Android boot.img 并检查 UART 是否启用
android·linux·dtc·3588·dts·解包·dtb
00后程序员张2 小时前
Python 抓包工具全面解析,从网络监听、协议解析到底层数据流捕获的多层调试方案
开发语言·网络·python·ios·小程序·uni-app·iphone
zl_dfq2 小时前
Linux 之 【进程替换】(execl、execlp、execle、execv、execvp、execve)
linux
乌蒙山连着山外山2 小时前
linux中查询多个匹配字段
java·linux·服务器
乘凉~2 小时前
在Ubuntu上部署并使用xianyu-auto-reply
linux·运维·ubuntu