想象这样一个场景:你是一个网络管理员,需要把经过网关的所有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长度必须相同。这不是偷懒,而是权衡后的设计:
| | |
|---|
| 等长替换 | | |
| | 需要调整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);
}
这是因为:
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网络栈的灵活性:
- 内核能力用户化:通过Netfilter Queue,普通用户程序也能"插手"内核网络流程
- 透明性:对通信双方无感知,不需要修改客户端或服务端配置
- 局限性:等长替换、仅支持TCP/UDP、单线程处理(可优化)
适合场景:网关内容过滤、安全测试、协议调试、透明代理。
不适合场景:高吞吐生产环境(需要多线程/零拷贝优化)、需要修改包长的场景、加密流量(HTTPS等需要先解密)。
理解nfq_sed的原理,不仅能帮你解决实际网络问题,更能深入理解Linux网络栈的工作机制——这对任何后端开发或运维工程师都是宝贵的知识储备。
Welcome to follow WeChat official account【程序猿编码】