You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

基于Linux 4.13.x内核NetFilter实现无用户态服务器的数据包响应

在Linux 4.13.x内核中通过NetFilter绕过用户态服务器直接发送TCP/UDP响应包

刚好做过类似的内核级数据包处理需求,用NetFilter完全能实现你要的“绕开用户态回显服务,直接给客户端发响应”的目标,而且适配4.13.x这个版本的内核API也没什么大问题。下面我分TCP和UDP两种场景,给你梳理具体的实现思路和关键细节:

TCP场景处理

TCP是面向连接的协议,构造响应包时要严格遵守TCP的序列号、确认号规则,不然客户端会认为数据包非法而丢弃。

1. 选对NetFilter Hook点

我建议把钩子挂在NF_INET_LOCAL_IN这个hook点——这个阶段是数据包已经到达本地协议栈,但还没递交给用户态进程的环节,刚好适合我们拦截请求、直接构造响应,同时丢弃原始包不让用户态服务收到。记得把钩子优先级设为NF_IP_PRI_FIRST,确保我们的逻辑比用户态进程的接收逻辑先执行。

2. 构造TCP响应包的核心步骤

  • 解析原始包:用ip_hdr(skb)获取IP头部指针,tcp_hdr(skb)获取TCP头部指针,先确认数据包是目标端口为你的回显服务(比如默认的7端口)的请求。
  • 克隆/创建响应skb:可以用skb_clone(skb, GFP_ATOMIC)克隆原始包,这样能复用原始的数据部分(回显需求刚好能用),注意要用GFP_ATOMIC因为钩子函数可能运行在软中断上下文,不能阻塞。
  • 修改IP头部:交换源IP和目的IP,重置IP校验和(用ip_send_check重新计算),调整IP总长度(如果有数据变化的话)。
  • 修改TCP头部
    • 交换源端口和目的端口;
    • 调整序列号和确认号:响应包的seq等于请求包的ack_seq,响应包的ack_seq等于请求包的seq加上请求数据的长度(也就是skb->len减去IP头长度和TCP头长度);
    • 设置正确的TCP标志位:如果是客户端的SYN握手包,响应SYN+ACK;如果是带数据的PSH包,响应PSH+ACK;
    • 重新计算TCP校验和:用tcp_v4_check函数,要包含IP伪头部的信息,这个函数在4.13.x内核是可用的。
  • 发送响应包:调用ip_local_out把构造好的skb发出去,这个函数会帮我们处理路由等后续逻辑。
  • 丢弃原始包:最后返回NF_DROP,阻止原始请求包到达用户态服务。

UDP场景处理

UDP是无连接协议,处理起来比TCP简单很多,不需要关心序列号和握手状态。

1. Hook点选择

同样用NF_INET_LOCAL_IN hook点,逻辑和TCP一致:拦截即将到用户态的UDP请求,构造响应后丢弃原始包。

2. 构造UDP响应包的核心步骤

  • 解析原始包:还是用ip_hdrudp_hdr获取头部指针,过滤目标端口为回显服务的包。
  • 克隆skb并修改头部:交换IP源/目的地址、UDP源/目的端口,调整UDP长度(回显的话和原始包一致)。
  • 重新计算校验和:用udp_v4_check函数计算新的UDP校验和。
  • 发送并丢弃原始包:调用ip_local_out发送响应,返回NF_DROP阻止原始包到用户态。

4.13.x内核的关键注意事项

  • API兼容性:4.13.x的NetFilter还是用传统的struct nf_hook_ops结构体注册钩子,不需要后续版本的nf_hook_ops_alloc等函数,直接用nf_register_hooknf_unregister_hook即可。
  • skb操作安全:修改skb时一定要注意调整datalentail等指针,避免内核崩溃。如果是克隆skb,记得要重新设置各层头部的偏移量。
  • 上下文限制:钩子函数可能运行在软中断上下文,所以不能调用任何阻塞式函数(比如用GFP_KERNEL分配内存),必须用GFP_ATOMIC
  • 避免重复处理:可以通过端口过滤或者设置skb的标记(skb->mark)来避免同一个数据包被多次拦截处理。

简单伪代码示例(TCP场景)

#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>
#include <linux/ip.h>
#include <linux/tcp.h>

static unsigned int tcp_echo_hook(void *priv, struct sk_buff *skb, const struct nf_hook_state *state) {
    struct iphdr *iph;
    struct tcphdr *tcph;
    struct sk_buff *resp_skb;
    __be32 temp_ip;
    __be16 temp_port;
    unsigned int data_len;

    // 确保skb能被解析
    if (!skb || !pskb_may_pull(skb, sizeof(struct iphdr) + sizeof(struct tcphdr)))
        return NF_ACCEPT;

    iph = ip_hdr(skb);
    tcph = tcp_hdr(skb);

    // 只处理目标端口为回显服务(7端口)的TCP包
    if (tcph->dest != htons(7))
        return NF_ACCEPT;

    // 克隆skb,GFP_ATOMIC适配软中断上下文
    resp_skb = skb_clone(skb, GFP_ATOMIC);
    if (!resp_skb)
        return NF_ACCEPT;

    // 交换IP源/目的地址
    temp_ip = iph->saddr;
    iph->saddr = iph->daddr;
    iph->daddr = temp_ip;
    // 重置并重新计算IP校验和
    iph->check = 0;
    iph->check = ip_send_check(iph);

    // 交换TCP源/目的端口
    temp_port = tcph->source;
    tcph->source = tcph->dest;
    tcph->dest = temp_port;

    // 计算请求数据长度:总长度 - IP头长度 - TCP头长度
    data_len = skb->len - (iph->ihl << 2) - (tcph->doff << 2);
    // 调整序列号和确认号
    tcph->seq = tcph->ack_seq;
    tcph->ack_seq = htonl(ntohl(tcph->seq) + data_len);

    // 设置TCP标志位:PSH+ACK(针对带数据的请求)
    tcph->psh = 1;
    tcph->ack = 1;
    tcph->syn = 0;
    tcph->fin = 0;

    // 重置并重新计算TCP校验和
    tcph->check = 0;
    tcph->check = tcp_v4_check(tcph, resp_skb->len - (iph->ihl << 2),
                               iph->saddr, iph->daddr,
                               csum_partial((char *)tcph, resp_skb->len - (iph->ihl << 2), 0));

    // 发送响应包
    ip_local_out(state->net, state->sk, resp_skb);

    // 丢弃原始请求包,不让用户态服务收到
    return NF_DROP;
}

static struct nf_hook_ops tcp_echo_ops = {
    .hook       = tcp_echo_hook,
    .pf         = PF_INET,
    .hooknum    = NF_INET_LOCAL_IN,
    .priority   = NF_IP_PRI_FIRST, // 最高优先级,确保先处理
};

static int __init echo_init(void) {
    return nf_register_hook(&tcp_echo_ops);
}

static void __exit echo_exit(void) {
    nf_unregister_hook(&tcp_echo_ops);
}

module_init(echo_init);
module_exit(echo_exit);
MODULE_LICENSE("GPL");

这个示例是核心逻辑,实际使用时需要完善错误处理(比如skb克隆失败的情况),以及针对SYN、FIN等不同TCP包类型的分支处理。UDP的实现可以参照这个逻辑,把TCP相关的部分换成UDP的即可。

内容的提问来源于stack exchange,提问作者Alireza Sanaee

火山引擎 最新活动