在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的套接字管理有不同的函数和数据结构,实现时需要分别处理,但逻辑上可以复用。
二、整体设计思路
整个实现的核心逻辑可以总结为"用户态输入→内核解析→查找套接字→销毁连接",具体分四步:
- 创建交互入口:在内核模块初始化时,在/proc/net目录下创建可写的伪文件,作为用户输入要销毁的连接信息的入口;
- 解析用户输入:用户写入"源IP:源端口 目的IP:目的端口"格式的内容后,内核模块把用户态数据拷贝到内核态,解析出源/目的IP、端口;
- 查找目标套接字:根据解析出的IP和端口,从内核的TCP套接字哈希表中找到对应的套接字结构体;
- 销毁/移除连接:根据套接字的状态(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包),再释放资源。
四、关键技术要点总结
- 内核态与用户态交互:通过proc文件系统实现,这是Linux内核模块和用户态程序交互的常用方式,优点是简单、无需额外的系统调用;
- 套接字查找机制:依赖内核的inet_lookup/inet6_lookup函数,这是内核管理TCP套接字的核心接口,必须精准匹配四元组(源IP、源端口、目的IP、目的端口);
- TCP状态机处理:不同状态的套接字销毁逻辑不同,尤其是TIME_WAIT状态需要特殊处理,否则会导致内核资源泄漏;
- 内存安全:用户态数据拷贝到内核态时,必须检查长度、使用copy_from_user(避免直接访问用户态内存导致内核崩溃),同时用完内核缓冲区要及时释放(kfree);
- 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【程序猿编码】