基于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_hdr和udp_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_hook和nf_unregister_hook即可。 - skb操作安全:修改skb时一定要注意调整
data、len、tail等指针,避免内核崩溃。如果是克隆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




