当前位置:首页>Linux>用Linux内核"拦截器"实时修改网络流量(C/C++代码实现)

用Linux内核"拦截器"实时修改网络流量(C/C++代码实现)

  • 2026-09-10 09:21:44
用Linux内核"拦截器"实时修改网络流量(C/C++代码实现)

想象这样一个场景:你是一个网络管理员,需要把经过网关的所有HTTP请求里的某个关键词替换掉;或者你在做安全测试,想实时修改传输中的数据包内容。传统的做法需要写复杂的内核模块或者用iptables做重定向,门槛很高。

nfq_sed就是为解决这类需求而生的。它是一个轻量级命令行工具,能在Linux系统上透明地修改经过的网络流量。你可以把它理解为网络世界的"查找替换"功能——就像你在Word里按Ctrl+H替换文字一样,只不过它操作的是网络数据包。

和类似的工具netsed相比,nfq_sed有个独特优势:它支持在以太网桥(ethernet bridge)上工作,这意味着它可以保持源MAC地址不变,特别适合做透明代理或网关场景。


一、核心原理:Linux的"网络过滤器队列"

nfq_sed的名字里藏着它的核心技术:nfq(netfilter_queue)+ sed(流编辑器)。

1.1 Netfilter是什么?

Netfilter是Linux内核的网络过滤框架,也是iptables的底层基础。你可以把它想象成内核里的一排"检查站":

数据包流入 → [PREROUTING检查站] → [路由决策] → [FORWARD检查站] → [POSTROUTING检查站] → 发出
                    ↓                                        ↓
               本机进程接收                              转发到其他机器

传统的iptables只能决定"放行"或"丢弃",但Netfilter Queue(NFQUEUE)给了用户态程序一个"插手"的机会——它能把数据包复制一份送到用户空间,让用户程序检查、修改后再决定怎么处理。

1.2 工作流程图

┌─────────────────────────────────────────────────────────────────┐
│                         用户空间 (User Space)                      │
│  ┌──────────────┐    ┌──────────────┐    ┌──────────────────┐   │
│  │   iptables   │───→│  nfq_sed程序   │───→│  修改后的数据包   │   │
│  │  (规则匹配)   │    │ (查找替换payload)│   │  ( verdict回内核) │   │
│  └──────────────┘    └──────────────┘    └──────────────────┘   │
│           ↑                              ↓                      │
│           └──────────────────────────────┘                      │
│                    Netlink Socket通信                            │
└─────────────────────────────────────────────────────────────────┘
                              ↑↓
┌─────────────────────────────────────────────────────────────────┐
│                         内核空间 (Kernel Space)                    │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │              Netfilter Hook (NF_INET_FORWARD等)          │    │
│  │                      ↓                                  │    │
│  │              NFQUEUE目标 (数据包入队)                    │    │
│  │                      ↓                                  │    │
│  │              数据包复制到用户空间队列                     │    │
│  └─────────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────────┘

二、代码架构解析

...
intmain(int argc, char *argv[])
{
...
while ((opt = getopt(argc, argv, "vs:x:f:q:")) != -1) {
switch (opt) {
case'v':
                verbose = 1;
break;
case's':
                add_rule(optarg);
break;
case'x':
                add_hex_rule(optarg);
break;
case'f':
                load_rules(optarg);
break;
case'q':
                queue_num = atoi(optarg);
break;
default:
                usage();
        }
    }
if (!rules) {
fprintf(stderr, "no rules defined, exiting\n");
return1;
    }
if (verbose) {
structrule_t *rule = rules;
printf("Rules (in hex):\n");
while (rule) {
printf("  ");
            print_rule(rule);
            rule = rule->next;
        }
    }
    read_queue();
return0;
}


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

nfq_sed的代码结构非常清晰,主要分为三大模块:

2.1 规则管理模块

structrule_t {
uint8_t *val1;      // 要查找的内容
uint8_t *val2;      // 替换成的内容
int length;         // 内容长度(val1和val2必须等长)
structrule_t *next;// 链表指针,支持多条规则
};

设计要点:

  • 使用单向链表管理多条替换规则
  • 支持字符串模式(-s参数)和十六进制模式(-x参数)
  • 查找值和替换值长度必须相同——这是为了避免修改后数据包长度变化导致的复杂问题(TCP序列号调整、分片重组等)

规则解析示例:

# 字符串规则:把"foo"替换成"bar"
-s /foo/bar

# 十六进制规则:把0x010203替换成0x414243(即ABC)
-x /010203/414243

2.2 数据包处理回调函数

这是整个程序的核心,代码精简但五脏俱全:

staticintcb(struct nfq_q_handle *qh, struct nfgenmsg *nfmsg,
              struct nfq_data *nfa, void *data)

{
// 1. 获取数据包ID和原始数据
    ph = nfq_get_msg_packet_hdr(nfa);
    id = ntohl(ph->packet_id);
    len = nfq_get_payload(nfa, &payload);

// 2. 解析IP头部
    ip = (struct ip_hdr*) payload;
    ip_size = IP_HL(ip)*4;  // IP头部长度(单位:4字节)

// 3. 根据协议类型(TCP/UDP)计算传输层头部大小
if (ip->proto == IPT_TCP) {
        tcp = (struct tcp_hdr*)(payload + ip_size);
        proto_size = TH_OFF(tcp)*4;  // TCP头部长度
    } elseif (ip->proto == IPT_UDP) {
        udp = (struct udp_hdr*)(payload + ip_size);
        proto_size = TH_OFF_UDP;      // UDP头部固定8字节
    }

// 4. 定位到应用层payload
    proto_payload = (uint8_t*)(payload + ip_size + proto_size);

// 5. 遍历所有规则,执行查找替换
while (rule) {
while ((pos = find(rule, proto_payload, payload_length)) != NULL) {
memcpy(pos, rule->val2, rule->length);  // 原地修改
        }
        rule = rule->next;
    }

// 6. 重新计算校验和(关键!)
if (ip->proto == IPT_TCP) {
        tcp->sum = 0;
        tcp->sum = csum(IPT_TCP, len-ip_size, ip->src, ip->dst, (uint8_t*) tcp);
    } else {
        udp->sum = 0;
        udp->sum = csum(IPT_UDP, len-ip_size, ip->src, ip->dst, (uint8_t*) udp);
    }

// 7. 将修改后的数据包返回给内核,并放行
return nfq_set_verdict(qh, id, NF_ACCEPT, len, payload);
}

2.3 校验和计算

为什么必须重新计算校验和?

TCP和UDP都有头部校验和,用于检测传输过程中的数据损坏。当你修改了payload内容,校验和必然失效。如果不重新计算,接收方会丢弃这个数据包。

nfq_sed实现了伪头部校验和(Pseudo-header checksum):

伪头部组成:
┌─────────────────┐
│   源IP地址 (4字节) │
├─────────────────┤
│   目的IP地址 (4字节)│
├─────────────────┤
│   零 (1字节)     │
├─────────────────┤
│   协议号 (1字节)  │
├─────────────────┤
│   TCP/UDP长度(2字节)│
├─────────────────┤
│   TCP/UDP头部+数据 │
└─────────────────┘

代码中的csum()函数就是按RFC 1071标准实现的,采用** one's complement sum**算法。


三、关键设计思路与取舍

3.1 为什么只支持等长替换?

代码里有个硬性限制:val1和val2长度必须相同。这不是偷懒,而是权衡后的设计:

方案
优点
缺点
等长替换
(nfq_sed选择)
实现简单、无序列号问题、性能高
灵活性受限
变长替换
更灵活
需要调整TCP序列号、处理分片、窗口缩放等,复杂度爆炸

对于大部分"关键字屏蔽"、"内容标记"场景,等长替换已经足够。

3.2 只处理TCP/UDP的Payload

代码里明确过滤了非TCP/UDP的包:

if (ip->proto != IPT_TCP && ip->proto != IPT_UDP) {
return nfq_set_verdict(qh, id, NF_ACCEPT, 0, NULL);
}

这是因为:

  • ICMP、GRE等协议的修改逻辑完全不同
  • 大部分"应用层"修改需求都集中在TCP/UDP上
  • 保持代码简洁,避免过度工程化

3.3 链表 vs 数组

规则存储用了单向链表而不是数组:

  • 优点:动态添加规则,无需预分配内存
  • 缺点:查找是O(n),但规则数量通常很少(<100条),性能可忽略

四、使用场景与实战

4.1 基础用法

# 1. 先用iptables把流量引入NFQUEUE
# 例:把所有目的端口554的TCP包送入队列0
iptables -A FORWARD -p tcp --destination-port 554 -j NFQUEUE --queue-num 0

# 2. 启动nfq_sed进行替换
nfq_sed -s /foo/bar -s /good/evil

# 3. 查看详细日志
nfq_sed -v -s /old/new

4.2 从文件加载规则

适合规则较多的场景:

# rules.txt内容:
/foo/bar
/good/evil
/test/prod

nfq_sed -f rules.txt -v

4.3 十六进制模式

处理二进制协议或特殊字符:

# 替换HTTP/1.1为HTTP/1.0(十六进制)
nfq_sed -x /485454502f312e31/485454502f312e30

总结与思考

nfq_sed是一个小而美的网络工具,它展示了Linux网络栈的灵活性:

  1. 内核能力用户化:通过Netfilter Queue,普通用户程序也能"插手"内核网络流程
  2. 透明性:对通信双方无感知,不需要修改客户端或服务端配置
  3. 局限性:等长替换、仅支持TCP/UDP、单线程处理(可优化)

适合场景:网关内容过滤、安全测试、协议调试、透明代理。

不适合场景:高吞吐生产环境(需要多线程/零拷贝优化)、需要修改包长的场景、加密流量(HTTPS等需要先解密)。

理解nfq_sed的原理,不仅能帮你解决实际网络问题,更能深入理解Linux网络栈的工作机制——这对任何后端开发或运维工程师都是宝贵的知识储备。

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

最新文章

随机文章