NATflow works by matching packets against a hash table to quickly determine forwarding information, performing necessary NAT and MAC modifications, and directly sending matched packets to the NIC, while unmatched packets follow the traditional slow path for processing.
NATflow 是一个 Linux 内核模块,用 Netfilter、conntrack、NAT、ipset、字符设备和可选硬件 NAT/WED offload 实现路由/NAT 快速转发、用户认证、QoS、URL/SNI 记录和主机访问控制。
Fast Path with natflow:
graph TB
A[NIC] ==> B[nf_ingress]
B ==> K[natflow: Match hash table for info]
K --> |If not matched| C[PRE_ROUTING]
subgraph SlowPath
direction TB
C --> D[Routing Decision]
D --> E[FORWARD]
E --> F[POST_ROUTING]
D -.-> I[LOCAL_IN]
J[LOCAL_OUT] -.-> F
end
F --> G[nf_hook_egress]
G --> H[NIC]
K ==> |If matched| L["Modify packet (NAT & MAC)"]
L ==> |sends directly to NIC| H
A --> |ppe: hardware offload forward| H
- README.md: human-oriented user manual and external interface reference.
- SYSTEM_DESIGN_SPEC.md: implementation-oriented system design, internal state, constraints, and compatibility notes.
- AGENTS.md: startup instructions and guardrails for AI agents working in this repository.
- docs/agent/MEMORY.md: compressed long-term context for agent work.
- docs/agent/ROADMAP.md: current development goals and priorities for future agent work.
- docs/agent/WORKFLOW.md: repository-as-agent-memory workflow and handoff protocol.
- docs/agent/DECISIONS.md: durable AI/agent repository decisions.
- docs/agent/TASK_TEMPLATE.md: structured task template for future agent work.
natflow is a versatile and high-performance network acceleration solution that provides the following key features:
- Fastpath for High-Speed Packet Forwarding
- Implements a software-based fast path for rapid packet forwarding.
- Works on any platform, delivering exceptional forwarding performance.
- Hardware NAT (hwnat) Support
- For specific platforms like MT7621, MT7622, MT7981, MT7986, and others, natflow provides hardware NAT support, enabling hardware-based acceleration for even higher performance.
- Requires kernel patches for proper integration.
- User Identification and Traffic Auditing
- Identifies individual IP users and monitors their traffic and speed.
- Provides detailed traffic auditing for user-level insights.
- Traffic Control (QoS)
- Enables bandwidth management and traffic shaping for users.
- Ensures fair usage and optimized network performance.
- Internet Access Control
- Allows or blocks internet access for specific users based on policies.
- URL Auditing (urllogger)
- Monitors and logs the domains or URLs accessed by users.
- Offers visibility into user browsing behavior.
- Website Access Control
- Matches user traffic against defined rules to restrict access to specific websites.
natflow combines software fast path, hardware acceleration on supported platforms, and advanced user management and auditing features, making it suitable for performance-critical and policy-driven network environments.
Natflow supports hardware acceleration on X-WRT, providing high-performance NAT and packet forwarding capabilities.
Hardware NAT (Hwnat) support:
- Platforms: MT7621, MT7622, MT7981, MT7986
- Enables efficient hardware-based NAT forwarding.
Hwnat with WED support:
- Platforms: MT7622, MT7981, MT7986
- Combines hardware NAT acceleration with WED (Wireless Ethernet Dispatch) support to optimize both WiFi and wired traffic.
Port-to-port Hwnat forwarding:
Port --> PPE --> PortPort <-- PPE <-- Port
WiFi-to-port Hwnat forwarding:
WiFi --> CPU --> PPE --> PortWiFi <-- CPU <-- PPE <-- Port
WiFi-to-port Hwnat forwarding with WED support:
WiFi --> CPU --> PPE --> PortWiFi <-- PPE <-- Port
本节面向部署和对接人员。更完整的内部实现、数据结构、状态位和兼容性限制见 SYSTEM_DESIGN_SPEC.md。
常用构建:
make EXTRA_CFLAGS="-DCONFIG_NATFLOW_PATH -DCONFIG_NATFLOW_URLLOGGER"只构建基础控制面时可以直接:
make常用编译宏:
| 宏 | 作用 |
|---|---|
CONFIG_NATFLOW_PATH |
启用 fast path、vline/relay、硬件 offload 相关控制。 |
CONFIG_NATFLOW_URLLOGGER |
启用 URL logger、Host ACL 和 /proc/sys/urllogger_store。 |
CONFIG_NATFLOW_DPI |
启用 DPI 控制/事件接口、19 个固定应用、DNS QNAME 查询意图、26 个固定原生协议状态机和 /dev/natflow_dpi_queue;默认关闭。DPI enabled 即激活 host/packet consumer,不依赖规则或 /proc/sys/urllogger_store/enable。 |
CONFIG_HWNAT_EXTDEV_USE_VLAN_HASH |
MTK 外部设备硬件 offload 使用 VLAN hash 模式;会影响 bridge VLAN filter。 |
CONFIG_HWNAT_EXTDEV_DISABLED |
禁用部分外部设备硬件 offload 分支。 |
NO_DEBUG=1 |
追加 -DNO_DEBUG -Os,编译期关闭日志宏。 |
启用 CONFIG_NATFLOW_URLLOGGER 或 CONFIG_NATFLOW_DPI 时,目标内核的
THREAD_SIZE 必须至少为 8192 字节,否则构建会失败。L7、DPI 和 URL
consumer 数据面源文件同时把单函数栈帧限制为 512 字节;该限制不能替代对
入口到 consumer 的累计调用栈检查。
示例:
make NO_DEBUG=1 EXTRA_CFLAGS="-DCONFIG_NATFLOW_PATH -DCONFIG_NATFLOW_URLLOGGER"为非当前运行内核构建时,主 Makefile 使用 KERNELRELEASE 选择内核目录:
make KERNELRELEASE=6.6.1 EXTRA_CFLAGS="-DCONFIG_NATFLOW_PATH -DCONFIG_NATFLOW_URLLOGGER"DKMS 入口:
make -f Makefile.dkms install
make -f Makefile.dkms uninstall加载模块时内核日志会打印 version=<NATFLOW_VERSION> 和 major/minor。设备节点通常由内核 device/class 机制创建;如果系统没有自动创建设备节点,请根据 dmesg 中打印的 major/minor 手动处理。
Since kernel < 4.10 cannot handle NF_STOLEN in ingress hook correctly, a kernel patch is needed:
diff --git a/include/linux/netfilter_ingress.h b/include/linux/netfilter_ingress.h
index 5fcd375ef175..b407128a35c0 100644
--- a/include/linux/netfilter_ingress.h
+++ b/include/linux/netfilter_ingress.h
@@ -17,11 +17,15 @@ static inline bool nf_hook_ingress_active(const struct sk_buff *skb)
static inline int nf_hook_ingress(struct sk_buff *skb)
{
struct nf_hook_state state;
+ int ret;
nf_hook_state_init(&state, &skb->dev->nf_hooks_ingress,
NF_NETDEV_INGRESS, INT_MIN, NFPROTO_NETDEV,
skb->dev, NULL, NULL, dev_net(skb->dev), NULL);
- return nf_hook_slow(skb, &state);
+ ret = nf_hook_slow(skb, &state);
+ if (ret == 0)
+ return -1;
+ return ret;
}
static inline void nf_hook_ingress_init(struct net_device *dev)
diff --git a/net/netfilter/core.c b/net/netfilter/core.c
index f39276d1c2d7..905597547b08 100644
--- a/net/netfilter/core.c
+++ b/net/netfilter/core.c
@@ -320,6 +320,8 @@ next_hook:
goto next_hook;
kfree_skb(skb);
}
+ } else if (verdict == NF_STOLEN) {
+ ret = 0;
}
rcu_read_unlock();
return ret;典型流程:
# 1. 加载模块后查看主控制面
cat /dev/natflow_ctl
# 2. 设置 zone
echo 'lan_zone 1=br-lan' >/dev/natflow_zone_ctl
echo 'wan_zone 2=pppoe-wan' >/dev/natflow_zone_ctl
echo 'update_match' >/dev/natflow_zone_ctl
# 3. 开启 fast path
echo 'disabled=0' >/dev/natflow_ctl
# 4. 可选:开启用户认证/URL logger
echo 'disabled=0' >/dev/natflow_user_ctl
echo 1 >/proc/sys/urllogger_store/enable所有写入字符设备的命令都必须以换行结束。echo 默认带换行,echo -n 不适合直接写控制命令。
这些字符设备大多采用相同的控制协议:
- 单行命令最大
256字节。 - 一条命令必须以
\n结束。 cat /dev/*_ctl通常会输出 usage 和可重放配置。- 未识别命令多数情况下只写内核日志并返回已消费字节;三个
natflow_*_queue的写接口只接受cache=N,未识别命令返回-EINVAL。 natflow_userinfo_ctl支持 partial read,用户 buffer 小于单条记录时会分多次读取完成。三个natflow_*_queue不会拆分单条记录;用户 buffer 小于单条记录时返回-EINVAL,buffer 足够时一次read()可返回多条完整记录。/dev/natflow_userinfo_queue、/dev/natflow_urllogger_queue和/dev/natflow_dpi_queue都只允许一个 reader。长期采集程序应以O_RDWR打开并保持 fd,写入cache=N\n设置最多缓存 N 条事件后才会缓存新事件;写入cache=0\n会关闭缓存并清空未读事件。- 多个 writer 并发写同一控制设备时,半行缓存可能互相干扰;生产脚本应串行写入。
| 接口 | 类型 | 用途 |
|---|---|---|
/dev/natflow_ctl |
char device | 全局 fast path、debug、HWNAT、ifname group、vline/relay。 |
/dev/natflow_zone_ctl |
char device | LAN/WAN zone 配置和刷新。 |
/dev/natflow_user_ctl |
char device | 认证规则、认证开关、portal 重定向和 bypass ipset。 |
/dev/natflow_userinfo_ctl |
char device | 用户状态读取、踢用户、设置认证状态、单用户限速。 |
/dev/natflow_userinfo_queue |
char device | 认证二进制事件队列,只允许一个 reader,默认不缓存,空读返回 0,支持 poll()。 |
/dev/natflow_qos_ctl |
char device | 全局 QoS 规则和 tc_classid_mode。 |
/dev/hostacl_ctl |
char device | Host ACL 规则和默认动作。 |
/dev/natflow_urllogger_queue |
char device | URL/SNI/ACL 命中二进制事件队列,只允许一个 reader,默认不缓存。 |
/dev/natflow_dpi_ctl |
char device | DPI enable 状态、固定 catalog 信息、统计和事件清理。 |
/dev/natflow_dpi_queue |
char device | DPI 二进制事件队列,只允许一个 reader,默认不缓存,当前输出 domain/fixed-protocol match v3 固定头事件、original tuple 和 evidence direction。 |
/dev/natflow_conntrackinfo_ctl |
char device | conntrack 文本快照。 |
/proc/sys/urllogger_store/* |
sysctl | URL logger 开关、合并窗口和当前队列条数。 |
读取:
cat /dev/natflow_ctl常用命令:
| 命令 | 说明 |
|---|---|
debug=<num> |
设置日志 bitmask:1=error、2=warn、4=info、8=debug、16=fixme、32=debug_ratelimited。 |
disabled=0/1 |
开启或关闭 fast path。模块加载后默认关闭。 |
hwnat=0/1 |
支持 HWNAT 的平台上开启或关闭硬件 offload。 |
hwnat_wed_disabled=0/1 |
支持 WED 的 MTK 平台上控制 WED 分支。 |
delay_pkts=<n> |
fastnat 建立前延迟若干包。 |
go_slowpath_if_no_qos=0/1 |
无 QoS 命中时是否走慢路径。 |
ifname_group_type=<n> |
接口组过滤模式。 |
ifname_group_clear |
清空接口组标记。 |
ifname_group_add=<ifname> |
把接口加入接口组。 |
list_net_device |
把当前 netdev 信息打印到内核日志。 |
update_magic |
递增 path magic,使已有 fastnat 条目失效并重新学习。 |
vline/relay:
echo 'vline_add=<src_ifname>,<dst_ifname>,<ipv4|ipv6|all>' >/dev/natflow_ctl
echo 'relay_add=<src_ifname>,<dst_ifname>,<ipv4|ipv6|all>' >/dev/natflow_ctl
echo 'vline_apply' >/dev/natflow_ctl
echo 'vline_clear' >/dev/natflow_ctl使用限制:
- vline/relay 只在启用
CONFIG_NETFILTER_INGRESS的 fast path 路径中生效。 - 最多缓存 8 条配置。
- 接口名最长 15 个可见字符,不允许逗号。
- 两端设备必须在
init_net中存在;桥场景应配置 bridge master,不要配置桥下挂端口。 - 实际 ingress 设备的
ifindex必须小于 64。 family只能是ipv4、ipv6或all。
参考 X-WRT IPv6 虚拟桥接配置指南,vline 可以用于 WAN 侧只能拿到 IPv6 /64、没有 IPv6-PD 的场景。典型情况包括 4G/5G 上网卡、光猫拨号后下挂路由器、二级路由等:WAN 设备本身有公网 IPv6,但 LAN 侧无法通过前缀委派给下游设备分配公网 IPv6。
示例拓扑:
clients ---> br-lan (NATflow Router) usb0 ---> Internet
其中 br-lan 是 LAN bridge,usb0 是 WAN 侧网卡设备名。不同 4G/5G 模块或上联方式下,WAN 设备也可能叫 eth1、wwan0 等,应以系统实际 netdev 名称为准。
配置命令:
echo 'vline_clear' >/dev/natflow_ctl
echo 'vline_add=br-lan,usb0,ipv6' >/dev/natflow_ctl
echo 'vline_apply' >/dev/natflow_ctl如果 WAN 设备是 eth1:
echo 'vline_clear' >/dev/natflow_ctl
echo 'vline_add=br-lan,eth1,ipv6' >/dev/natflow_ctl
echo 'vline_apply' >/dev/natflow_ctl开机自动启用时,可把上述命令加入 /etc/rc.local 或系统等价的启动脚本。虚拟机环境或部分网卡驱动下,WAN 侧设备可能需要开启混杂模式后才能正常转发。
安全注意事项:
- 启用 IPv6 虚拟桥接后,下游设备的 IPv6 流量可能绕过路由器原有 IPv6 防火墙策略;部署前应确认上游、防火墙和终端侧的安全边界。
vline_add=br-lan,<wan-dev>,ipv6只处理 IPv6 虚拟桥接;IPv4 仍应按原 NAT/路由路径处理。vline_apply前两端设备必须已存在;WAN 设备名变化时需要重新下发配置。
读取:
cat /dev/natflow_zone_ctl命令:
echo 'lan_zone <id>=<if_name>' >/dev/natflow_zone_ctl
echo 'wan_zone <id>=<if_name>' >/dev/natflow_zone_ctl
echo 'update_match' >/dev/natflow_zone_ctl
echo 'print_zone' >/dev/natflow_zone_ctl
echo 'clean' >/dev/natflow_zone_ctl说明:
- zone id 有效范围是
0..126。 - 同一个 zone id 只能属于一种类型;如果某个 id 已经用于
lan_zone,不能再用于wan_zone,反之亦然。 <if_name>支持用+做前缀匹配,例如eth+。update_match会刷新当前所有 netdev 的 zone 标记。- 当前实现中
clean只清规则;为了让已有设备的缓存标记失效,清理后应执行一次update_match。
读取:
cat /dev/natflow_user_ctl命令:
| 命令 | 说明 |
|---|---|
disabled=0/1 |
开启或关闭用户认证/控制路径。 |
clean |
清空 auth 规则和 bypass 名称。 |
update_magic |
递增认证规则代际,使用户重新匹配规则。 |
dst_bypasslist_name=<ipset> |
设置目的地址 bypass ipset;空值清除。 |
src_bypasslist_name=<ipset> |
设置源地址 bypass ipset;空值清除。 |
| `auth id=,szone=,type=<web | auto>,sipgrp=[,ipwhite=][,macwhite=]` |
redirect_ip=<a.b.c.d> |
设置 portal/redirect 目的 IPv4。 |
redirect_ip6=<ipv6_addr> |
设置 portal/redirect 目的 IPv6。如果未设置(默认为 ::),IPv6 重定向请求将回滚使用 redirect_ip 提供的 IPv4 地址作为目标。 |
no_flow_timeout=<seconds> |
设置无流量用户超时。 |
https_redirect_en=0/1 |
开启或关闭 HTTPS redirect。 |
https_redirect_port=<port> |
设置 HTTPS redirect 端口,合法范围 0..65535。 |
auth_open_weixin_reply=0/1 |
控制微信相关自动 portal 回复逻辑。 |
认证规则限制:
- 最多 16 条 auth 规则。
id是业务规则 ID;szone匹配/dev/natflow_zone_ctl中的 LAN zone id。type=auto命中后直接进入通过状态;type=web命中后进入待认证状态。sipgrp、ipwhite、macwhite都是 ipset 名称。
认证状态值:
| 名称 | 值 |
|---|---|
AUTH_NONE |
0 |
AUTH_OK |
1 |
AUTH_BYPASS |
2 |
AUTH_REQ |
3 |
AUTH_NOAUTH |
4 |
AUTH_VIP |
5 |
AUTH_BLOCK |
6 |
AUTH_UNKNOWN |
15 |
认证类型值:
| 名称 | 值 |
|---|---|
AUTH_TYPE_UNKNOWN |
0 |
AUTH_TYPE_AUTO |
1 |
AUTH_TYPE_WEB |
2 |
读取当前用户:
cat /dev/natflow_userinfo_ctl输出格式:
ip_or_ipv6,mac,auth_type,auth_status,rule_id,idle_time,rx_pkts:rx_bytes,tx_pkts:tx_bytes,rx_speed_pkts:rx_speed_bytes,tx_speed_pkts:tx_speed_bytes,ifname
命令:
echo 'kickall' >/dev/natflow_userinfo_ctl
echo 'kick <ip_or_ipv6>' >/dev/natflow_userinfo_ctl
echo 'set-status <ip_or_ipv6> <status>' >/dev/natflow_userinfo_ctl
echo 'set-token-ctrl <ip_or_ipv6> <rxbytes> <txbytes>' >/dev/natflow_userinfo_ctl说明:
idle_time是该 fakeuser 内部活动时间戳至今经过的秒数;该时间戳在 fakeuser 创建/获取时写入,普通活动最多每 32 秒刷新一次,新连接包距离上次刷新超过 2 秒也会刷新。ifname由 user 模块维护并依赖 path。普通 TCP/UDP 单播从当前连接 path 缓存中与包方向相反的rroute[!dir].outdev同步设备名:用户主动连接使用 original 方向,外网主动进入并已在 post hook 关联用户的连接使用 LAN 应答的 reply 方向;reply 分支仅在 fakeuser 地址与当前 reply 源地址一致时刷新已有用户来源,避免 LAN-to-LAN/hairpin 应答污染原用户,且不执行认证和重定向。字段为空时每个用户侧包都会先尝试补齐 ifname,不受普通活动 32 秒、新连接 2 秒节流限制。对于 path 不建立 route 的广播、组播和 ICMP/ICMPv6,启用的 path 保持原有校验和早退顺序,仅在调用 user 专用入口前由局部包装器独立验证 IPv4 长度/checksum 或 IPv6 长度;入口按源地址只查找已有 fakeuser,用户不存在时不创建,入口不更新 MAC,已有非零用户 MAC 必须与 Ethernet 源 MAC 一致,ifname 变化时发布 userinfo 事件。ARP 不经过该 IP 入口。path 未启用、未启用 NETDEV ingress 或没有可用入口信息时该字段可以为空。kickall清理所有用户认证状态和统计。kick、set-status、set-token-ctrl找不到用户时返回-ENOENT。set-token-ctrl单位是 Bytes/s;rx 或 tx 非 0 时启用该用户 token control,两者都为 0 时关闭。
读取方式:
用户态应以 O_RDWR 打开并保持 fd,先向同一个 fd 写入 cache=N\n 设置最多缓存 N 条事件,再使用 poll()、select() 或 epoll 等待可读后循环 read() 固定头事件;如果读缓冲能容纳多条固定头,单次 read() 会尽量返回多条完整事件。不要用 cat 作为长期采集程序。
行为:
- 队列为空时
read()返回 0;poll()在有事件时返回 readable。 read()不返回半条事件;用户 buffer 小于sizeof(struct natflow_userinfo_event_hdr)时返回-EINVAL。- 同一时间只允许一个 reader,第二个打开会返回
-EBUSY。 - 默认不缓存;reader 打开时会清空残留事件,事件只在 reader 已打开且写入正数
cache=N\n后入队,队列满时丢弃新事件。 - 写入
cache=0\n会关闭缓存并清空未读事件;reader 关闭时也会清空未读事件。 - 写接口只接受
cache=N,N 为十进制无符号整数;未知命令返回-EINVAL。
固定头为:
struct natflow_userinfo_event_hdr {
__u16 version;
__u16 header_len;
__u16 record_len;
__u16 family;
__u32 idle_time;
__u8 ip[16];
__u8 mac[6];
__u8 auth_type;
__u8 auth_status;
__u16 auth_rule_id;
__u64 rx_packets;
__u64 rx_bytes;
__u64 tx_packets;
__u64 tx_bytes;
__u32 rx_speed_packets;
__u32 rx_speed_bytes;
__u32 tx_speed_packets;
__u32 tx_speed_bytes;
__u8 ifname[16];
} __packed;字段说明:
version=3,header_len=record_len=sizeof(struct natflow_userinfo_event_hdr)。- 除地址字节数组外,整数按内核本机端序输出;用户态 reader 与内核运行在同一机器时直接按结构体读取即可。
family是AF_INET或AF_INET6;IPv4 地址放在ip[0..3],IPv6 地址使用完整 16 字节。idle_time是该 fakeuser 内部活动时间戳至今经过的秒数。ifname是以 NUL 结尾的用户侧三层入口设备名,更新规则与文本接口一致。- 计数字段与
/dev/natflow_userinfo_ctl文本输出一致;速度字段来自 4 个 2 秒窗口,超过 8 秒无更新时为 0。
C 读者样例:
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <poll.h>
#include <stdint.h>
#include <stdio.h>
#include <sys/socket.h>
#include <unistd.h>
#define USERINFO_QUEUE "/dev/natflow_userinfo_queue"
struct natflow_userinfo_event_hdr {
uint16_t version;
uint16_t header_len;
uint16_t record_len;
uint16_t family;
uint32_t idle_time;
uint8_t ip[16];
uint8_t mac[6];
uint8_t auth_type;
uint8_t auth_status;
uint16_t auth_rule_id;
uint64_t rx_packets;
uint64_t rx_bytes;
uint64_t tx_packets;
uint64_t tx_bytes;
uint32_t rx_speed_packets;
uint32_t rx_speed_bytes;
uint32_t tx_speed_packets;
uint32_t tx_speed_bytes;
uint8_t ifname[16];
} __attribute__((packed));
#define CACHE_LIMIT 256
static int set_cache_limit(int fd)
{
char cmd[32];
int cmd_len = snprintf(cmd, sizeof(cmd), "cache=%u\n", CACHE_LIMIT);
ssize_t len;
if (cmd_len < 0 || (size_t)cmd_len >= sizeof(cmd)) {
fprintf(stderr, "invalid cache command\n");
return -1;
}
len = write(fd, cmd, cmd_len);
if (len < 0) {
perror("write cache limit");
return -1;
}
if (len != cmd_len) {
fprintf(stderr, "short write cache limit\n");
return -1;
}
return 0;
}
static int wait_queue_readable(int fd, int timeout_ms)
{
for (;;) {
struct pollfd pfd = {
.fd = fd,
.events = POLLIN | POLLRDNORM,
};
int ret = poll(&pfd, 1, timeout_ms);
if (ret < 0) {
if (errno == EINTR)
continue;
perror("poll");
return -1;
}
if (ret == 0)
return 0;
if (pfd.revents & POLLNVAL) {
fprintf(stderr, "poll: invalid queue fd\n");
return -1;
}
if (pfd.revents & (POLLERR | POLLHUP)) {
fprintf(stderr, "poll: queue error revents=0x%x\n",
pfd.revents);
return -1;
}
if (pfd.revents & (POLLIN | POLLRDNORM))
return 1;
}
}
int main(void)
{
int fd = open(USERINFO_QUEUE, O_RDWR | O_CLOEXEC);
if (fd < 0) {
perror("open " USERINFO_QUEUE);
return 1;
}
if (set_cache_limit(fd) != 0) {
close(fd);
return 1;
}
for (;;) {
int ready = wait_queue_readable(fd, -1);
if (ready < 0)
break;
if (ready == 0)
continue;
for (;;) {
struct natflow_userinfo_event_hdr events[32];
size_t i;
size_t event_count;
ssize_t len = read(fd, events, sizeof(events));
if (len < 0) {
if (errno == EINTR)
continue;
perror("read");
close(fd);
return 1;
}
if (len == 0)
break;
if ((size_t)len % sizeof(events[0]) != 0) {
fprintf(stderr, "skip partial userinfo batch: %zd\n", len);
continue;
}
event_count = (size_t)len / sizeof(events[0]);
for (i = 0; i < event_count; i++) {
const struct natflow_userinfo_event_hdr *ev = &events[i];
char ip[INET6_ADDRSTRLEN];
if (ev->version != 3 ||
ev->header_len != sizeof(*ev) ||
ev->record_len != sizeof(*ev)) {
fprintf(stderr, "skip unsupported userinfo event\n");
continue;
}
if (ev->family == AF_INET6) {
if (!inet_ntop(AF_INET6, ev->ip, ip, sizeof(ip)))
snprintf(ip, sizeof(ip), "?");
} else if (ev->family == AF_INET) {
if (!inet_ntop(AF_INET, ev->ip, ip, sizeof(ip)))
snprintf(ip, sizeof(ip), "?");
} else {
snprintf(ip, sizeof(ip), "family-%u", ev->family);
}
printf("%s %02x:%02x:%02x:%02x:%02x:%02x "
"auth=0x%x status=0x%x rule=%u idle=%u "
"rx=%" PRIu64 ":%" PRIu64 " tx=%" PRIu64 ":%" PRIu64 " "
"rx_speed=%u:%u tx_speed=%u:%u ifname=%.*s\n",
ip,
ev->mac[0], ev->mac[1], ev->mac[2],
ev->mac[3], ev->mac[4], ev->mac[5],
ev->auth_type, ev->auth_status, ev->auth_rule_id,
ev->idle_time, ev->rx_packets, ev->rx_bytes,
ev->tx_packets, ev->tx_bytes,
ev->rx_speed_packets, ev->rx_speed_bytes,
ev->tx_speed_packets, ev->tx_speed_bytes,
(int)sizeof(ev->ifname), (const char *)ev->ifname);
}
}
}
close(fd);
return 1;
}读取:
cat /dev/natflow_qos_ctl命令:
echo 'clear' >/dev/natflow_qos_ctl
echo 'tc_classid_mode=1' >/dev/natflow_qos_ctl
echo 'add user=<user>,user_port=<user_port>,remote=<remote>,remote_port=<remote_port>,proto=<tcp|udp|>,rxbytes=<Bytes>,txbytes=<Bytes>' >/dev/natflow_qos_ctl字段:
user、remote支持 IPv4、IPv4 CIDR、IPv6、IPv6 CIDR 或 ipset 名称。user_port、remote_port支持端口号或 ipset 端口集合名;空字段表示任意。proto支持tcp、udp或空字段。rxbytes、txbytes单位是 Bytes/s。- 最多 64 条规则。
示例:
echo 'add user=192.168.1.0/24,user_port=,remote=,remote_port=,proto=tcp,rxbytes=1310720,txbytes=655360' >/dev/natflow_qos_ctl
echo 'add user=2001:db8::/64,user_port=,remote=2001:4860:4860::8888,remote_port=443,proto=tcp,rxbytes=1310720,txbytes=655360' >/dev/natflow_qos_ctltc_classid_mode=1 时,匹配到的 qos_id 会写入 skb->mark,可配合 tc filter fw 使用。
读取:
cat /dev/hostacl_ctl命令:
echo 'clear' >/dev/hostacl_ctl
echo 'acl_action_default=accept' >/dev/hostacl_ctl
echo 'redirect_url=http://1.1.1.1/blocked.html' >/dev/hostacl_ctl
echo 'add acl=<id>,<act>,<host>' >/dev/hostacl_ctl动作:
act |
名称 | 行为 |
|---|---|---|
| 0 | accept / record |
记录并放行。 |
| 1 | drop |
丢弃。 |
| 2 | reset |
对 TCP 尝试 reset。 |
| 3 | redirect |
HTTP 请求(GET/POST)返回 302 重定向;HTTPS/QUIC 则退化为 TCP reset 或丢弃。 |
说明:
- ACL 槽位范围是
0..31。 - 同一槽位可追加多个 host。
- 可选 ipset 过滤集合名:
host_acl_rule<id>_ipv4、host_acl_rule<id>_ipv6、host_acl_rule<id>_mac。 - Host ACL 依赖 URL logger 解析,排障时先开启
/proc/sys/urllogger_store/enable。 - Host ACL 使用解析出的最小 host 视图执行;即使 URL store 记录分配失败,也会尽量执行 ACL 动作,但不会生成对应
/dev/natflow_urllogger_queue记录。
sysctl:
| 路径 | 默认值 | 说明 |
|---|---|---|
/proc/sys/urllogger_store/enable |
0 | 是否启用 URL logger/Host ACL 处理。 |
/proc/sys/urllogger_store/count |
0 | 当前已缓存待读 URL 记录数,只读。 |
/proc/sys/urllogger_store/timestamp_freq |
10 | 相同 URL 合并窗口,也是读出前的最小老化秒数。 |
/proc/sys/urllogger_store/tuple_type |
0 | 记录 tuple 方向:0=dir0-src dir0-dst,1=dir0-src dir1-src,2=dir1-dst dir1-src。 |
开启流程:
# 1. 先启动并保持下面的 reader 程序
# 2. 再启用 URL logger/Host ACL
echo 1 >/proc/sys/urllogger_store/enable/dev/natflow_urllogger_queue 只允许一个 reader,第二个 reader 打开会返回 -EBUSY。没有 reader 或 reader 未写入正数 cache=N\n 时,URL/SNI record 直接丢弃,不缓存到 URL store;reader 打开时 cache 默认为 0 并会先清空残留记录,写入 cache=N\n 后最多缓存 N 条新记录,队列满时丢弃新记录;写入 cache=0\n 或关闭 fd 会关闭缓存并清空未读记录。read() 在没有可读记录时返回 0;因为 timestamp_freq 同时是相同 URL 合并窗口和读出前的最小老化秒数,用户态应以 O_RDWR 打开并保持 fd,先写入 cache=N\n,再使用 poll()、select() 或 epoll 等待可读后读取。不要用 cat /dev/natflow_urllogger_queue 做长期采集;空队列会让 cat 退出,后续记录会因没有 reader 或未开启缓存而被丢弃。
如果用户 buffer 能容纳多条版本化二进制记录,单次 read() 会按 record_len 拼接返回多条完整记录;不会返回半条记录。如果同一个 fd 上 read() 返回 0,表示当前没有已老化到可读状态的记录;reader 应继续保持 fd 打开并重新进入 poll() 等待,而不是关闭后反复重开。使用无限期 poll() 时需要注意:已有记录只会在新记录入队或清理事件发生时唤醒;如果业务依赖 timestamp_freq 到期后立刻读出,应在用户态给 poll() 设置不大于 timestamp_freq 的超时并定期重试。
固定头为:
struct natflow_urllogger_event_hdr {
__u16 version;
__u16 header_len;
__u16 record_len;
__u16 family;
__u32 timestamp;
__u16 sport;
__u16 dport;
__u8 sip[16];
__u8 dip[16];
__u8 mac[6];
__u16 hits;
__u16 host_len;
__u8 method;
__u8 source;
__u8 acl_idx;
__u8 acl_action;
} __packed;字段说明:
version=2,header_len=sizeof(struct natflow_urllogger_event_hdr),record_len是固定头加 payload 的总长度。- 除地址字节数组外,整数按内核本机端序输出;用户态 reader 与内核运行在同一机器时直接按结构体读取即可。
family是AF_INET或AF_INET6;IPv4 地址放在sip[0..3]、dip[0..3],IPv6 地址使用完整 16 字节。timestamp是基于系统 uptime 的秒数,不是 Unix epoch。method:0=NONE,1=GET,2=POST,3=HEAD;非 HTTP 通常为 0。source:1=HTTP,2=TLS/HTTPS SNI,3=QUIC。acl_idx=64表示未命中 ACL。acl_action:0=record/accept,1=drop,2=reset,3=redirect。- payload 紧跟固定头,长度为
record_len - header_len,内容是host + uri,不带结尾NUL;host_len给出 host 部分长度,剩余部分是 HTTP URI。TLS/QUIC 记录通常只有 host,没有 URI。
C 读者样例:
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <poll.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#define URLLOGGER_QUEUE "/dev/natflow_urllogger_queue"
#define READ_BUF_LEN 65536
struct natflow_urllogger_event_hdr {
uint16_t version;
uint16_t header_len;
uint16_t record_len;
uint16_t family;
uint32_t timestamp;
uint16_t sport;
uint16_t dport;
uint8_t sip[16];
uint8_t dip[16];
uint8_t mac[6];
uint16_t hits;
uint16_t host_len;
uint8_t method;
uint8_t source;
uint8_t acl_idx;
uint8_t acl_action;
} __attribute__((packed));
#define CACHE_LIMIT 256
static int set_cache_limit(int fd)
{
char cmd[32];
int cmd_len = snprintf(cmd, sizeof(cmd), "cache=%u\n", CACHE_LIMIT);
ssize_t len;
if (cmd_len < 0 || (size_t)cmd_len >= sizeof(cmd)) {
fprintf(stderr, "invalid cache command\n");
return -1;
}
len = write(fd, cmd, cmd_len);
if (len < 0) {
perror("write cache limit");
return -1;
}
if (len != cmd_len) {
fprintf(stderr, "short write cache limit\n");
return -1;
}
return 0;
}
static int wait_queue_readable(int fd, int timeout_ms)
{
for (;;) {
struct pollfd pfd = {
.fd = fd,
.events = POLLIN | POLLRDNORM,
};
int ret = poll(&pfd, 1, timeout_ms);
if (ret < 0) {
if (errno == EINTR)
continue;
perror("poll");
return -1;
}
if (ret == 0)
return 0;
if (pfd.revents & POLLNVAL) {
fprintf(stderr, "poll: invalid queue fd\n");
return -1;
}
if (pfd.revents & (POLLERR | POLLHUP)) {
fprintf(stderr, "poll: queue error revents=0x%x\n",
pfd.revents);
return -1;
}
if (pfd.revents & (POLLIN | POLLRDNORM))
return 1;
}
}
static const char *source_name(uint8_t source)
{
switch (source) {
case 1: return "HTTP";
case 2: return "HTTPS";
case 3: return "QUIC";
default: return "UNKNOWN";
}
}
int main(void)
{
int fd = open(URLLOGGER_QUEUE, O_RDWR | O_CLOEXEC);
if (fd < 0) {
perror("open " URLLOGGER_QUEUE);
return 1;
}
if (set_cache_limit(fd) != 0) {
close(fd);
return 1;
}
for (;;) {
int ready = wait_queue_readable(fd, 1000);
if (ready < 0)
break;
if (ready == 0)
continue;
for (;;) {
unsigned char buf[READ_BUF_LEN];
size_t off = 0;
ssize_t len = read(fd, buf, sizeof(buf));
if (len < 0) {
if (errno == EINTR)
continue;
perror("read");
close(fd);
return 1;
}
if (len == 0)
break;
while (off < (size_t)len) {
struct natflow_urllogger_event_hdr *h;
char sip[INET6_ADDRSTRLEN];
char dip[INET6_ADDRSTRLEN];
unsigned char *payload;
unsigned int payload_len;
unsigned int host_len;
size_t remaining = (size_t)len - off;
if (remaining < sizeof(*h)) {
fprintf(stderr, "skip partial urllogger tail: %zu\n",
remaining);
break;
}
h = (struct natflow_urllogger_event_hdr *)(buf + off);
if (h->version != 2 || h->header_len < sizeof(*h) ||
h->record_len < h->header_len ||
h->record_len > remaining) {
fprintf(stderr, "skip unsupported urllogger event\n");
break;
}
payload = buf + off + h->header_len;
payload_len = h->record_len - h->header_len;
host_len = h->host_len <= payload_len ? h->host_len : payload_len;
if (h->family == AF_INET6) {
if (!inet_ntop(AF_INET6, h->sip, sip, sizeof(sip)))
snprintf(sip, sizeof(sip), "?");
if (!inet_ntop(AF_INET6, h->dip, dip, sizeof(dip)))
snprintf(dip, sizeof(dip), "?");
} else if (h->family == AF_INET) {
if (!inet_ntop(AF_INET, h->sip, sip, sizeof(sip)))
snprintf(sip, sizeof(sip), "?");
if (!inet_ntop(AF_INET, h->dip, dip, sizeof(dip)))
snprintf(dip, sizeof(dip), "?");
} else {
snprintf(sip, sizeof(sip), "family-%u", h->family);
snprintf(dip, sizeof(dip), "family-%u", h->family);
}
printf("%u %02x:%02x:%02x:%02x:%02x:%02x %s:%u -> %s:%u "
"hits=%u method=%u source=%s acl=%u/%u host=%.*s uri=%.*s\n",
h->timestamp,
h->mac[0], h->mac[1], h->mac[2],
h->mac[3], h->mac[4], h->mac[5],
sip, h->sport, dip, h->dport, h->hits, h->method,
source_name(h->source), h->acl_idx, h->acl_action,
(int)host_len, (char *)payload,
(int)(payload_len - host_len), (char *)(payload + host_len));
off += h->record_len;
}
}
}
close(fd);
return 1;
}关闭缓存并清空未读记录时,对长期 reader 已打开的同一个 O_RDWR fd 写入 cache=0\n,例如 write(fd, "cache=0\n", 8)。
需要编译 CONFIG_NATFLOW_DPI。当前 DPI 默认关闭,支持 19 个固定应用、DNS QNAME 查询意图统计和 26 个编译期固定原生协议状态机。enable=1 会直接激活 DPI host/packet consumer 并运行全部内置分类器,不需要任何运行时规则。HTTP Host、TLS SNI 或 QUIC v1 Initial SNI 命中静态域名时直接写固定 app_id 和 category;钉钉、QQ/OICQ、爱奇艺、WhatsApp、Discord、Spotify 和 Zoom 还可由 nDPI 来源的直接 payload 签名终态。/proc/sys/urllogger_store/enable=0 仍只表示 URL logger 事件和 Host ACL 不执行。URL、DPI domain 和 DPI packet 的 L7 终态分别记录在 natflow_t.status 中:URL 失败不会关闭 DPI,DPI packet 结束不会关闭仍在等待 Host/SNI 的 DPI domain,DPI domain 完成也不会影响 URL;当前 active consumer 全部完成后才释放 fast path,并设置 IPS_NATFLOW_L7_HANDLED 作为后续包的 L7_SKIP 快速短路 hint。
完整 L7 pipeline 继续只运行在 IPv4/IPv6/bridge FORWARD。DPI 另有
IPv4/IPv6 LOCAL_IN DNS-only 入口,用于发往本机 dnsmasq/unbound 的
original-direction TCP/UDP DNS query;它不运行 URL logger、Host ACL、HTTP、
TLS、QUIC 或其他协议机器。QUIC 443 candidate 和所有依赖逻辑端口的 DPI 状态机统一使用
conntrack original tuple 的 client/server port,因此 DNAT/REDIRECT 把当前
报文目的端口从 53 改到本机其他端口后仍可识别,事件也继续输出 original
tuple。TCP SYN、ACK 等零 payload 包不会提前终态,LOCAL_OUT 应答当前不
进入该入口。
运行时 enable=0 只改变后续数据包看到的 DPI consumer,不扫描或清理已经标记为 L7 处理中的连接,也不会重新武装已经设置 L7_SKIP 的连接。已标记连接可以由后续数据包自然完成,也可以保留原 L7 状态直到 conntrack 生命周期结束;配置切换不保证立即释放这些既有连接的 fast path gate。
原生协议机器未命中时会在 natflow_t 尾部保存 8 字节瞬态双向预算 context,并设置 NF_FF_DPI_USE;app_id 仍是唯一分类结果。context 内的 16 位 dpi_automaton 在 discovery 阶段以低 8 位保存 machine-class mask,RDP、SOCKS 或 WhatsApp 认领后原子保存 machine/state。源码没有 detector metadata 或 detector 数组:固定 dispatcher 按编译期顺序直接调用对应 parser/机器分支。同一 conntrack 的 packet machine、automaton、双向预算和 app/context 终态由 conntrack lock 串行;任何固定 app 终态都会在同一临界区清除 context 并写 DPI packet done,避免终态后重新武装 owner bit。当前每方向硬限制最多观察 4 个 payload 包,不设置时间 deadline。若 conntrack acct 扩展存在,双向累计包数超过 256 时会清除仍活跃的 DPI context 并写 packet done;第 256 包仍允许等待,第 257 包触发兜底。conntrack accounting 实现随 CONFIG_NF_CONNTRACK 内建,不存在 CONFIG_NF_CONNTRACK_ACCT 编译开关,但是否给新连接挂 acct 扩展由运行时 net.netfilter.nf_conntrack_acct 控制;OpenWrt 默认设为 1。管理员关闭 accounting 或连接创建时未获得 acct 扩展时,所需方向始终没有 payload 的 context 仍可保留到 conntrack 生命周期结束。
reply 方向只进入 DPI packet consumer;URL logger、Host ACL、HTTP/TLS/QUIC host 和 DNS QNAME domain 仍只处理 original。DNS reply 必须通过 response header 和第一问结构校验,其他原生协议机器也必须匹配 payload 证据,端口不会直接产生分类。
TCP 只有 DPI packet consumer 时,为 App HTTP 输入最多 pull 512 字节;通用原生协议 parser 仍只检查其中前 96 字节。TCP SYN/ACK/keepalive 等零负载包只进入 app/transport/conntrack 包数的轻量生命周期检查,不运行 payload parser,也不增加 packet_inspect_*。UDP 通常最多 pull 96 字节,payload 总长为 121..299 字节的爱奇艺候选会 pull 完整 datagram 以执行 PPStream 有界搜索。URL 或 DPI domain host consumer 激活时仍按 HTTP/TLS producer 的既有需求准备完整 payload。
当前 DPI 仍是 audit-only:不执行 drop/reset/QoS,不覆盖 Host ACL、认证或 conntrack drop 结果;未命中、禁用、无对应 parser 或无法创建 natflow session 时 fail-open。L7 shared hook 在解析前会统一调用 natflow_session_in() 确保 URL/DPI 共享同一个 natflow_t.status 终态存储;若 confirmed、内存或布局限制导致 session 不存在,则跳过本次 L7 解析,不输出无状态 DPI match event,也不写入 app_id。protocol-only 命中要求 app_id=0,用于避免每包重复事件。
控制:
cat /dev/natflow_dpi_ctl
echo enable=1 >/dev/natflow_dpi_ctl
echo enable=0 >/dev/natflow_dpi_ctl
echo events_clear >/dev/natflow_dpi_ctl规则说明:
events_clear会临时暂停 DPI producer、等待在途 Netfilter hook 退出,再清空/dev/natflow_dpi_queue中已排队事件并把全部 match、event、domain、packet、context 和proto_*shadow counters 归零,最后恢复原 enable 状态;固定 catalog revision 不变。复位窗口内经过的包不做 DPI,命令返回后旧 producer 不会再写入新统计,但持续流量可能立即产生新的计数和事件。rules_begin、domain ...、proto ...、rules_commit、rules_abort和rules_clear已全部删除,写入返回-EINVAL。- Host/SNI 会转小写、去掉末尾点,并校验 DNS label;HTTP Host 中的端口由 URL logger normalize 时剥离。静态 matcher 固定使用 exact 优先、suffix 长度降序和 label-boundary 语义。
kind=suffix同时匹配完全相同的 host 和带点边界的子域名,例如规则example.net可匹配example.net与www.example.net。- 固定 protocol app ID 为:DNS=1、SSH=2、WireGuard=3、STUN=4、TURN=5、BitTorrent=6、FTP=7、SMTP=8、POP3=9、IMAP=10、SIP=11、RTSP=12、MQTT=13、RESP=14、MySQL=15、PostgreSQL=16、RDP=17、SMB=18、NTP=19、SNMP=20、RADIUS=21、TFTP=22、LDAP=23、NFS=24、SOCKS=25、CoAP=26。已发布 ID 不改号或复用。
- 固定应用 ID 在原有 9 项上追加腾讯视频=
0x1004、Spotify=0x1005、WhatsApp=0x2005、Messenger=0x2006、Discord=0x2007、Zoom=0x2008、Facebook=0x5002、Instagram=0x5003、X/Twitter=0x5004、微博=0x5005。catalog revision 为 3,共 45 个 protocol/app 项;现有 category 编号不变。 - 静态域名表共有 94 项。新增项直接提取自本地 nDPI
ndpi_content_match.c.inc,仍使用 exact 或严格 label-boundary suffix;v.qq.com和 Meta 具体子域先于qq.com、facebook.com父域。nDPI 的宽泛wx.、weixin.、instagram.、twitter.、whatsapp.、fbcdn-等 substring 没有采用;共享 CDN 只登记完整 hostname exact。 - App HTTP step 可在 original request 或 reply response 的当前有界 payload 中解析 request/status line、最多 32 个完整 header、大小写不敏感的
Host/User-Agent/Content-Type和 header 后可见 body。第一批 nDPI 规则只使用 request Host;没有可追溯来源的 header/body 关键字不会单独终态。当前不做 TCP stream reassembly、chunked 解码、gzip/br 解压或跨包 body 拼接。 - DNS QNAME 路径:original direction TCP/UDP 53 标准 query 的第一问 QNAME 会经过同一静态 matcher,但只增加
dns_app_intents,不会把查询目标应用写入 DNS 连接的app_id;该连接仍终态为 DNS。FORWARD 和 DNS-only LOCAL_IN 都按 conntrack original tuple 的目的端口选择 DNS 候选,支持目的端口已被 DNAT/REDIRECT 改写的流量。parser 支持 compression pointer,最多跳转 16 次并拒绝指针环、越界和展开后超长名称。FORWARD reply 只用于 DNS protocol 证据;本机 DNS 应答不挂 LOCAL_OUT。 - 端口只用于选择有界解析候选和 payload pull budget,不直接写入
app_id;当前只有 TCP/UDP 53 会触发 DNS 候选解析,TCP 22 和 UDP 51820 不再作为 SSH/WireGuard 的独立分类证据。 - 有界 payload 机器:TCP 任一方向的 SSH banner 识别
SSH-<version>-identification string;WireGuard、STUN/TURN 和 BitTorrent 机器也在任一方向匹配直接 payload 证据。uTP 会校验 version/type、最多 4 段的有界 extension chain;为避免与 WireGuard type 1 重叠,DATA packet 的 connection ID 为 0 时不分类。DPI 启用后执行全部内置机器,但每包仍只运行当前 L4、方向和 discovery machine-class mask 允许且预算未耗尽的 parser。 - B 级原生协议机器仍为 audit-only:除原有文本、数据库和 Microsoft 协议外,NTP、SNMP、RADIUS、LDAP 和 CoAP 使用任一端点端口加结构/长度证据;TFTP RRQ/WRQ 需要端点 69,严格 OACK 支持动态 TID;NFS 使用 RPC record/program/version,不限制端口。强结构 network/NFS parser 先于 WireGuard/uTP,避免合法 RPC 或基础协议被较弱的 uTP 头抢先分类。BER length 拒绝 indefinite、超过 4 字节、非最短编码和越界。SOCKS4/5 必须先看到 original negotiation,再由 reply 的固定应答终态,claimed 后只运行 SOCKS machine。RDP 仍要求 original request 与 reply confirm 两个事实汇合。新增协议复用现有 TEXT/BINARY machine class,不扩大 context。
- nDPI payload App 机器在任一方向识别:钉钉 TCP 固定前缀、QQ/OICQ UDP、爱奇艺 UDP
PPStream、Discord UDP magic、Spotify TCP/双端 57621 UDP、Zoom 任一端点 8801..8810 的 UDP SFU 前缀;WhatsApp TCP 的新前缀可在同一方向的既有 4 包预算内连续匹配,首段至少需要 2 个匹配字节才认领 machine,旧前缀单包终态。它们不新增 detector 表,也不扩大natflow_t的 8 字节 context。 cat /dev/natflow_dpi_ctl中,matches/matches_*统计全部分类终态,不依赖 queue reader;events/events_*只统计成功入队,events_suppressed表示没有 reader 或cache=0,events_lost表示分配失败或队列已满。稳定采样区间内应满足matches = events + events_suppressed + events_lost;新的并发 producer 在 match 计数和最终入队结果之间仍允许短暂不一致,events_clear返回后不会混入复位前 producer 的延迟结果。domain_lookups/domain_matches统计 hostname 静态/迁移期规则查找和产生应用终态的命中;dns_app_intents统计 QNAME 命中静态应用域名但未写 resident app 的次数;packet_inspect_original/reply按实际进入有界 App/协议 parser 的 packet 计数,每包最多增加一次,不按机器数量累加;packet_match_original/reply统计直接 App/协议证据方向。context_armed和各context_cleared_*记录 bounded context 的累计状态转换;context_cleared_acct_limit表示 acct 双向包数首次超过 256 时清除了活跃 context,events_clear会把它复位。context_aborted表示 L7 强制终态清理。conntrack 自然销毁不会回调 DPI,因此这些累计值不能相减推导当前活跃 context 数。proto_no_session和proto_app_exists解释原生协议机器未产生新分类结果的原因;固定映射不存在proto_no_rule。
可运行 tools/natflow-dpi-ctl-smoke.sh 验证 enable、catalog、events_clear、未知命令以及全部已删除规则命令。脚本会清空事件统计并临时切换 enable,退出时恢复原 enable 状态。
/dev/natflow_dpi_queue 使用版本化二进制记录,只允许一个 reader,第二个 reader 打开会返回 -EBUSY。没有 reader 或 reader 未写入正数 cache=N\n 时,match event 直接丢弃,不分配、不缓存,也不增加 events_lost;reader 打开时 cache 默认为 0 并会先清空残留事件,写入 cache=N\n 后最多缓存 N 条新事件,队列满、溢出或分配失败会丢弃新事件并增加 events_lost;写入 cache=0\n 或关闭 fd 会关闭缓存并清空未读事件。当前 record 是 v3 固定头,包含规则命中摘要、original direction tuple 和实际证据方向;read() 在队列为空时返回 0,用户 buffer 小于固定头时返回 -EINVAL,poll() 在有事件时返回 readable。
读者用法:
- 用户态应以
O_RDWR先打开并保持/dev/natflow_dpi_queuefd,写入正数cache=N\n后再启用 DPI 或开始采集流量;fd 关闭或 cache 关闭期间产生的 match event 会被直接丢弃。 - 不建议用
cat /dev/natflow_dpi_queue做长期采集;如果打开时队列为空,read()会返回 0,cat会退出,后续事件又会因没有 reader 或未开启缓存而被丢弃。 - 推荐使用
poll()、select()或epoll等待 fd 可读;可读后用足够大的 buffer 读取记录。单次read()可返回多条sizeof(struct natflow_dpi_event_hdr)固定头事件,不返回半条事件。 - 如果同一个 fd 上
read()返回 0,表示当前队列已空;reader 应继续保持 fd 打开并重新进入poll()等待,而不是关闭后反复重开。
固定头为:
struct natflow_dpi_event_hdr {
__u16 version;
__u16 header_len;
__u16 record_len;
__u16 family;
__u64 timestamp;
__u8 l4proto;
__u8 tuple_dir;
__u8 evidence_dir;
__u8 reserved;
__u16 reason;
__u16 sport;
__u16 dport;
__u8 sip[16];
__u8 dip[16];
__u32 generation;
__u32 app_id;
__u32 category_id;
__u32 rule_id;
__u32 flags;
} __packed;当前 match event 字段含义:
version=3,header_len=record_len=sizeof(struct natflow_dpi_event_hdr)=78。reason=6表示 rule matched。generation固定为 catalog revision;当前为 3。- 所有事件的
app_id和category_id来自静态 metadata,rule_id=0。 category_id=0预留。flags当前记录事件来源:1..24 保持原有 HTTP、TLS、QUIC、DNS 至 iQIYI 编号;25=NTP,26=SNMP,27=RADIUS,28=TFTP,29=LDAP,30=NFS,31=SOCKS,32=CoAP,33=WhatsApp,34=Discord,35=Spotify,36=Zoom。timestamp是基于系统 uptime 的秒数,不是 Unix epoch,与 URL logger 事件语义一致。family是 original tuple 的 L3 family,当前为AF_INET或AF_INET6;l4proto是 original tuple 的 L4 protocol;tuple_dir=0表示 tuple 固定取IP_CT_DIR_ORIGINAL。evidence_dir=0/1分别表示命中证据来自IP_CT_DIR_ORIGINAL/IP_CT_DIR_REPLY;domain host event 固定为 original,protocol-only event 记录实际命中 packet 的方向。reserved当前必须忽略。sport和dport是 original tuple 的源/目的端口,按主机字节序输出;非端口型协议为 0。sip和dip是 original tuple 的源/目的地址字节数组;IPv4 使用前 4 字节,IPv6 使用完整 16 字节。
C 读者样例:
仓库提供可直接编译的维护版本 tools/natflow-dpi-reader.c,支持指定设备、cache 上限和读取条数:
cc -std=c11 -O2 -Wall -Wextra -Werror \
-o natflow-dpi-reader tools/natflow-dpi-reader.c
./natflow-dpi-reader -c 256tools/natflow-dpi-queue-smoke.c 用于真机 ABI 冒烟。默认模式不等待流量,验证单 reader、不可 seek、小 buffer、空队列 poll/read、未知命令、cache 开关和关闭后重开清理;-w 模式还要求在超时前收到至少一条匹配事件并严格校验当前 v3 固定头:
cc -std=c11 -O2 -Wall -Wextra -Werror \
-o natflow-dpi-queue-smoke tools/natflow-dpi-queue-smoke.c
./natflow-dpi-queue-smoke
./natflow-dpi-queue-smoke -c 256 -w 10000queue smoke 打开设备时会按 ABI 清空残留事件并独占 reader;不要与生产 reader 同时运行。-w 模式运行前应启用 DPI,并在等待窗口内生成内置分类器可识别的流量。
原生协议机器黑盒 corpus 入口为 tests/dpi/run-corpus.sh。它在 root namespace 中建立两个 network namespace,让 TCP/UDP fixture 经过真实 FORWARD hook,并对 queue event 的 original tuple、source、app_id、rule_id 和 evidence_dir 做断言。runner 要求 root 权限、ip、对应 family 的 iptables/ip6tables、C 编译器和已加载的 DPI 模块;临时 FORWARD 规则带 conntrack state match,确保所选地址族不依赖系统已有 NAT/firewall 或 natflow path 开关获得 conntrack。它会临时修改对应 forwarding、FORWARD 规则、DPI enable 和事件统计,只能用于隔离测试环境。最终 PASS 仅在 DPI 状态、FORWARD 规则、namespace/veth 和 forwarding 清理结果均核验通过后输出。样本格式和清理边界见 tests/dpi/README.md。
本机 DNS-only hook 的 IPv4 直连与 REDIRECT 真机回归入口为:
sudo tests/dpi/run-local-dns.sh--ipv6 使用两个 IPv6 /64、ip6tables 和 IPv6 forwarding 运行同一批 fixture,并验证 event 中完整 16 字节 original tuple;它覆盖基础 IPv6 TCP/UDP,DPI 不解析 IPv6 extension header:
sudo tests/dpi/run-corpus.sh --ipv6 tests/dpi/cases/*.cases--packet-limit 模式验证包数兜底及零负载路径。它临时把 net.netfilter.nf_conntrack_acct 设为 1,使用两个独立 UDP flow 分别断言同一 flow 的第 256 包不会清理、第 257 包触发清理,再通过 TCP repair/raw ACK 构造保持连接打开的纯 ACK 流,确认零负载 TCP 包能触发兜底但不会进入 payload parser;退出时恢复原 sysctl、DPI 和网络状态。该模式当前只支持 IPv4,并要求 root 具有 network namespace、CAP_NET_ADMIN 和 CAP_NET_RAW 能力:
sudo tests/dpi/run-corpus.sh --packet-limit同一 runner 的 --queue-pressure [cache [generated]] 模式用于 queue 满载和并发 producer 回归,默认以 cache=8 并发生成 32 条独立 STUN 流。测试期间单一 reader 不读取事件,注入完成后断言只保留 8 条合法 v3 event,并核对 matches=32、events=8、events_lost=24、events_suppressed=0 及 STUN 分项。该模式同样要求隔离测试环境:
sudo tests/dpi/run-corpus.sh --queue-pressure
sudo tests/dpi/run-corpus.sh --queue-pressure 16 64--queue-stream [cache [generated [parallel]]] 模式让单一 reader 在 producer 分批并发注入期间持续执行 poll() 和批量 read(),默认 cache=64、总流量 128、每批并发 16。每个测试端口必须恰好读到一次,结束后 queue 必须为空,并要求 matches=events=128、events_lost=events_suppressed=0:
sudo tests/dpi/run-corpus.sh --queue-stream
sudo tests/dpi/run-corpus.sh --queue-stream 64 128 16下面代码保留为接口示例;实际测试优先使用上述维护版本。
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <netinet/in.h>
#include <poll.h>
#include <stdint.h>
#include <stdio.h>
#include <unistd.h>
#define DPI_QUEUE "/dev/natflow_dpi_queue"
struct natflow_dpi_event_hdr {
uint16_t version;
uint16_t header_len;
uint16_t record_len;
uint16_t family;
uint64_t timestamp;
uint8_t l4proto;
uint8_t tuple_dir;
uint8_t evidence_dir;
uint8_t reserved;
uint16_t reason;
uint16_t sport;
uint16_t dport;
uint8_t sip[16];
uint8_t dip[16];
uint32_t generation;
uint32_t app_id;
uint32_t category_id;
uint32_t rule_id;
uint32_t flags;
} __attribute__((packed));
#define CACHE_LIMIT 256
static int set_cache_limit(int fd)
{
char cmd[32];
int cmd_len = snprintf(cmd, sizeof(cmd), "cache=%u\n", CACHE_LIMIT);
ssize_t len;
if (cmd_len < 0 || (size_t)cmd_len >= sizeof(cmd)) {
fprintf(stderr, "invalid cache command\n");
return -1;
}
len = write(fd, cmd, cmd_len);
if (len < 0) {
perror("write cache limit");
return -1;
}
if (len != cmd_len) {
fprintf(stderr, "short write cache limit\n");
return -1;
}
return 0;
}
static int wait_queue_readable(int fd, int timeout_ms)
{
for (;;) {
struct pollfd pfd = {
.fd = fd,
.events = POLLIN | POLLRDNORM,
};
int ret = poll(&pfd, 1, timeout_ms);
if (ret < 0) {
if (errno == EINTR)
continue;
perror("poll");
return -1;
}
if (ret == 0)
return 0;
if (pfd.revents & POLLNVAL) {
fprintf(stderr, "poll: invalid queue fd\n");
return -1;
}
if (pfd.revents & (POLLERR | POLLHUP)) {
fprintf(stderr, "poll: queue error revents=0x%x\n",
pfd.revents);
return -1;
}
if (pfd.revents & (POLLIN | POLLRDNORM))
return 1;
}
}
static const char *source_name(uint32_t source)
{
switch (source) {
case 1: return "HTTP";
case 2: return "TLS";
case 3: return "QUIC";
case 4: return "DNS";
case 5: return "SSH";
case 6: return "WireGuard";
case 7: return "STUN";
case 8: return "TURN";
case 9: return "BitTorrent";
default: return "UNKNOWN";
}
}
static const char *l4proto_name(uint8_t proto)
{
switch (proto) {
case IPPROTO_TCP: return "tcp";
case IPPROTO_UDP: return "udp";
default: return "l4";
}
}
static const char *addr_text(uint16_t family, const uint8_t addr[16],
char *buf, size_t len)
{
const void *src = NULL;
int af = 0;
if (family == AF_INET) {
af = AF_INET;
src = addr;
} else if (family == AF_INET6) {
af = AF_INET6;
src = addr;
} else {
snprintf(buf, len, "family-%u", family);
return buf;
}
if (!inet_ntop(af, src, buf, len))
snprintf(buf, len, "?");
return buf;
}
int main(void)
{
int fd = open(DPI_QUEUE, O_RDWR | O_CLOEXEC);
if (fd < 0) {
perror("open " DPI_QUEUE);
return 1;
}
if (set_cache_limit(fd) != 0) {
close(fd);
return 1;
}
for (;;) {
int ready = wait_queue_readable(fd, -1);
if (ready < 0)
break;
if (ready == 0)
continue;
for (;;) {
struct natflow_dpi_event_hdr events[32];
size_t event_count;
size_t i;
ssize_t len = read(fd, events, sizeof(events));
if (len < 0) {
if (errno == EINTR)
continue;
perror("read");
close(fd);
return 1;
}
if (len == 0)
break;
if ((size_t)len % sizeof(events[0]) != 0) {
fprintf(stderr, "skip partial dpi batch: %zd\n", len);
continue;
}
event_count = (size_t)len / sizeof(events[0]);
for (i = 0; i < event_count; i++) {
const struct natflow_dpi_event_hdr *ev = &events[i];
char sip[INET6_ADDRSTRLEN];
char dip[INET6_ADDRSTRLEN];
if (ev->version != 3 ||
ev->header_len != sizeof(*ev) ||
ev->record_len != sizeof(*ev)) {
fprintf(stderr, "skip unsupported dpi event\n");
continue;
}
printf("ts=%" PRIu64 " generation=%u app=%u rule=%u "
"reason=%u source=%s category=%u "
"tuple=%s %s:%u -> %s:%u tuple_dir=%u evidence_dir=%u\n",
ev->timestamp, ev->generation, ev->app_id,
ev->rule_id, ev->reason, source_name(ev->flags),
ev->category_id, l4proto_name(ev->l4proto),
addr_text(ev->family, ev->sip, sip, sizeof(sip)),
ev->sport,
addr_text(ev->family, ev->dip, dip, sizeof(dip)),
ev->dport, ev->tuple_dir, ev->evidence_dir);
}
}
}
close(fd);
return 1;
}读取:
cat /dev/natflow_conntrackinfo_ctl该接口输出 conntrack 文本快照,包含 L3/L4 协议、源/目的地址端口、timeout、计数、状态标记等。它支持 partial read,适合用常规 cat 或脚本持续读取完整快照。
写入:
echo 'kickall' >/dev/natflow_conntrackinfo_ctlkickall 等价于对 init_net 执行一次带过滤条件的 conntrack -F:删除当前
conntrack 表中除 fakeuser (IPS_NATFLOW_USER) 和 NATCAP peer
(IPS_NATCAP_PEER) 之外的所有已确认连接。命令要求调用进程在 init_net
所属 user namespace 中具有 CAP_NET_ADMIN,否则返回 -EPERM。这是同步且
破坏性的操作,会立即中断被删除连接的 NAT 和状态跟踪;执行期间新建的连接
可能不在本次遍历快照内。
| 名称 | 用途 |
|---|---|
dst_bypasslist_name=<ipset> |
目的地址认证旁路。 |
src_bypasslist_name=<ipset> |
源地址认证旁路。 |
sipgrp=<ipset> |
auth 规则的源用户匹配集合。 |
ipwhite=<ipset> |
auth 规则源 IP 白名单。 |
macwhite=<ipset> |
auth 规则源 MAC 白名单。 |
host_acl_rule<id>_ipv4 |
Host ACL 对 IPv4 源过滤。 |
host_acl_rule<id>_ipv6 |
Host ACL 对 IPv6 源过滤。 |
host_acl_rule<id>_mac |
Host ACL 对 MAC 源过滤。 |
vline_filter_dst_netport、vline_filter_dst、vline_filter_src、vline_filter_src_mac |
IPv4 vline 过滤。 |
vline_filter6_dst_netport、vline_filter6_dst、vline_filter6_src、vline_filter_src_mac |
IPv6 vline 过滤。 |
- 先确认模块是否加载、设备节点是否存在:
ls -l /dev/*natflow* /dev/*info* /dev/*acl*。 - 写命令无效时,确认命令带换行,且没有超过 256 字节。
- fast path 不生效时,检查
disabled=0、zone 是否刷新、debug日志、conntrack 是否存在。 - URL/Host ACL 不生效时,确认
echo 1 >/proc/sys/urllogger_store/enable,并让 reader 以O_RDWR打开/dev/natflow_urllogger_queue后写入正数cache=N,再看队列是否输出目标 host。 - QoS 不生效时,先
cat /dev/natflow_qos_ctl确认规则已加载,再检查是否已有连接缓存了旧规则;生产变更建议配合重新建连或刷新相关连接状态。 - 老内核如果不能正确处理 ingress hook 的
NF_STOLEN,需要内核侧补丁;详细实现约束见SYSTEM_DESIGN_SPEC.md。
Buy me a beer!
BITCOIN ADDR: 3CJ5VwxL8ageKpA3jJ561rvhkFW4FmZiqc
