Linux Netlink 深度工程:内核与用户空间双向通信全栈实战

本文深入解析 Linux Netlink 协议族——内核与用户空间的"系统总线",涵盖协议原理、消息机制、libmnl 编程接口变化监听、路由操作、Netlink Generic 自定义协议扩展以及与 eBPF 的深度集成。


在 Linux 内核演进过程中,用户空间获取内核状态的主流方式经历了三个阶段:

  • 系统调用(syscall):单向、简单,如 gethostname()、sysinfo()。扩展性差,每次新增功能需修改内核 ABI。
  • ioctl:弱类型,参数即 unsigned long,不同驱动私有定义,内核 5.15 后已标记为"legacy interface"。
  • 虚拟文件系统(procfs/sysfs):文件语义导致状态一致性差——read() 单次调用可能返回不完整快照,大结构需要 seek 重试。

Netlink 的设计解决了所有上述问题:Socket 风格双向通信、强类型嵌套属性、多播组推送、事件驱动、可扩展的 Generic Netlink 协议族,且不需要修改系统调用表。

Netlink 基于 Linux 的 AF_NETLINK 地址族,使用 SOCK_DGRAM (或 SOCK_RAW) 类型的 Socket。与 UDS (Unix Domain Socket) 类似,它是本机通信,但目标地址是内核而非用户空间进程:

┌──────────────────────────────────────────────────────┐
│                  用户空间进程                          │
│                                                      │
│  iproute2    NetworkManager    systemd    wireguard  │
│     │              │             │            │       │
│     └──────┬───────┴──────┬──────┴─────┬──────┘      │
│            │              │            │             │
│      NETLINK_ROUTE   NETLINK_GENERIC  NETLINK_UEVENT │
│            │              │            │             │
├────────────┼──────────────┼────────────┼─────────────┤
│      ┌─────┴──────────────┴────────────┴─────┐       │
│      │     Netlink 协议层 (af_netlink.c)      │       │
│      │    nl_table[MAX_LINKS] 多播分发         │       │
│      └─────┬──────────────┬────────────┬─────┘       │
│            │              │            │             │
│      rtnetlink.c    genetlink.c    uevent.c          │
│     (路由/接口/地址)  (Generic框架)   (热插拔事件)     │
│                                                      │
│                    Linux Kernel                      │
└──────────────────────────────────────────────────────┘

关键点:

  • 内核的 struct sock 与用户空间的 struct sockaddr_nl 通过 AF_NETLINK 族绑定。
  • 每个 Netlink 协议族在 nl_table 数组中注册一个槽位。
  • 多播组最多 32 个(NL_CFG_F_NONROOT_RECV 限制非 root 监听)。
  • 消息通过 netlink_kernel_create() (旧内核) 或 netlink_register() (新内核) 在内核态注册回调。
协议族 编号 主要使用者 功能
NETLINK_ROUTE 0 iproute2, NetworkManager 路由、接口、地址、邻居、TC、队列规则
NETLINK_UNUSED 1 - 历史上 TCPDIAG,已弃用
NETLINK_USERSOCK 2 用户自定义 用户空间通信
NETLINK_FIREWALL 3 已弃用 旧版 netfilter 日志(被 NFLOG 替代)
NETLINK_SOCK_DIAG 4 ss, netstat socket 状态诊断(inet_diag, tcp_diag)
NETLINK_NFLOG 5 ulogd netfilter 日志投递
NETLINK_XFRM 6 setkey, racoon IPSec SA/SP 管理
NETLINK_SELINUX 7 auditd SELinux 事件通知
NETLINK_ISCSI 8 open-iscsi iSCSI 会话管理
NETLINK_AUDIT 9 auditd Linux Audit 事件流
NETLINK_FIB_LOOKUP 10 - FIB 前缀查询
NETLINK_CONNECTOR 11 process_events 进程/内核事件回调
NETLINK_NETFILTER 12 conntrackd netfilter 子系统的用户态事件
NETLINK_IP6_FW 13 - IPv6 旧版防火墙(已弃用)
NETLINK_DNRTMSG 14 - DECnet 路由消息
NETLINK_KOBJECT_UEVENT 15 udev, systemd-udevd 设备热插拔 uevent 广播
NETLINK_GENERIC 16 所有自定义协议 Generic Netlink,可扩展的"Netlink 的 Netlink"

其中 NETLINK_ROUTE 是最常用的——你每天用到的 ip addr、ip route、ip link 都是通过它完成的。NETLINK_GENERIC 则是最高效的自定义扩展协议,所有较新的内核子系统(nftables、tc flower、wireguard、LSM BPF 等)都迁移到了 Generic Netlink。


2.1 nlmsghdr 头部

Netlink 消息由固定头部 + 载荷组成,头部定义在 <linux/netlink.h>:

struct nlmsghdr {
    __u32 nlmsg_len;    /* 整个消息长度(含头部) */
    __u16 nlmsg_type;   /* 消息内容类型 */
    __u16 nlmsg_flags;  /* 附加标志 */
    __u32 nlmsg_seq;    /* 序列号 */
    __u32 nlmsg_pid;    /* 发送端端口 ID(内核为 0) */
};

常见 nlmsg_flags:

Flag 含义 使用场景
NLM_F_REQUEST 请求消息 用户→内核的请求
NLM_F_MULTI 多部分大消息 dump 操作返回多条
NLM_F_ACK 请求 ACK 需要明确确认时
NLM_F_ROOT dump 根 RTM_GETLINK dump 全部接口
NLM_F_MATCH dump 匹配 仅返回匹配条目
NLM_F_ATOMIC 原子 dump 快照一致性
NLM_F_REPLACE 替换 存在则覆盖
NLM_F_EXCL 排他 存在则失败
NLM_F_CREATE 创建 不存在则创建
NLM_F_APPEND 追加 _append 语义
NLM_F_DUMP dump 全部 NLM_F_ROOT \| NLM_F_MATCH

2.2 嵌套属性(nlattr)

Netlink 使用 Type-Length-Value (TLV) 编码承载扩展属性:

struct nlattr {
    __u16 nla_len;      /* 属性长度(含头部 4 字节) */
    __u16 nla_type;     /* 属性类型(高位含 NLA_F_NESTED 标志) */
    /* 载荷数据紧跟其后 */
};

属性对齐到 4 字节边界(NLA_ALIGNTO = 4),这是手动编码时最常见的错误来源。强烈建议使用 libmnl 而非手动编码——手动编码的 4 字节对齐陷阱导致了不少 CVE。

nla_type 标志位:

标志 含义
NLA_F_NESTED (1<<15) 属性值为嵌套子属性
NLA_F_NET_BYTEORDER (1<<14) 网络字节序(大端)
NLA_TYPE_MASK 掩码,提取纯类型值

2.3 消息对齐规则

┌───────────────────────────────────────────────────────┐
│                  nlmsghdr (aligned to NLMSG_ALIGNTO)   │
├──────────────┬────────────────────────────────────────┤
│ nlmsg_len    │ 从消息起点到末尾的总长度                 │
│ nlmsg_type   │ RTM_NEWLINK / RTM_DELLINK / ...         │
│ nlmsg_flags  │ NLM_F_REQUEST | NLM_F_ACK             │
│ nlmsg_seq    │ 每消息递增,用于应答匹配                  │
│ nlmsg_pid    │ 发送者 port_id,内核为 0                 │
├──────────────┴────────────────────────────────────────┤
│ nlattr#1: ifla_family(1B) + ifla_pad(1B) + ...      │
│ nlattr#2: ifla_ifname → "eth0"                      │
│ nlattr#3: nested(IFLA_LINKINFO) → nested attr...    │
│ ...                                                   │
│ NLMSG_NEXT() 循环遍历所有属性                        │
└───────────────────────────────────────────────────────┘

手动对齐宏 NLMSG_ALIGN(x)、NLA_ALIGN(x) 在 <linux/netlink.h> 中定义。


三、libmnl 实战:监听网络接口变化

libmnl(Mini-Netlink library)是 netfilter 团队维护的轻量 Netlink 库(单 .h + .c 文件),相比 libnl3 更小更快。

3.1 环境准备

# Debian/Ubuntu
apt install libmnl-dev

# 或者直接从源码编译
git clone git://git.netfilter.org/libmnl
cd libmnl && autoreconf -fi && ./configure && make && sudo make install

3.2 完整示例:实时接口监控

/* netlink_iface_monitor.c
 * 功能:监听 RTM_NEWLINK/RTM_DELLINK,实时打印接口的 create/delete/MTU/IP 变化
 * 编译:gcc -o monitor netlink_iface_monitor.c $(pkg-config --cflags --libs libmnl)
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <errno.h>

#include <sys/select.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
#include <libmnl/libmnl.h>

/* ---------- 工具函数 ---------- */

static const char *op_type_str(__u16 type)
{
    switch (type) {
    case RTM_NEWLINK: return "NEWLINK";
    case RTM_DELLINK: return "DELLINK";
    case RTM_NEWADDR: return "NEWADDR";
    case RTM_DELADDR: return "DELADDR";
    case RTM_NEWROUTE: return "NEWROUTE";
    case RTM_DELROUTE: return "DELROUTE";
    default:          return "UNKNOWN";
    }
}

static void print_mac(struct nlattr *attr)
{
    unsigned char *addr = mnl_attr_get_payload(attr);
    printf("MAC=%02x:%02x:%02x:%02x:%02x:%02x",
           addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]);
}

/* ---------- RTM_NEWLINK 消息解析 ---------- */

static int parse_newlink_cb(const struct nlmsghdr *nlh, void *data)
{
    (void)data;
    struct ifinfomsg *ifi = mnl_nlmsg_get_payload(nlh);
    struct nlattr *attr;
    char ifname[IFNAMSIZ] = "(unknown)";

    const char *type = op_type_str(nlh->nlmsg_type);
    time_t now = time(NULL);
    struct tm *tm_info = localtime(&now);
    char buf[26];
    strftime(buf, 26, "%Y-%m-%d %H:%M:%S", tm_info);

    /* 遍历属性 */
    mnl_attr_for_each(attr, nlh, sizeof(*ifi)) {
        int type_a = mnl_attr_get_type(attr);

        switch (type_a) {
        case IFLA_IFNAME:
            snprintf(ifname, sizeof(ifname), "%s",
                     mnl_attr_get_str(attr));
            break;
        case IFLA_ADDRESS:
            print_mac(attr);
            printf(" ");
            break;
        case IFLA_MTU:
            printf("MTU=%u ", mnl_attr_get_u32(attr));
            break;
        case IFLA_LINK:
            printf("LINK=%d ", mnl_attr_get_u32(attr));
            break;
        case IFLA_MASTER:
            printf("MASTER=%d ", mnl_attr_get_u32(attr));
            break;
        default:
            break;
        }
    }

    printf("[%s] %s ifindex=%d flags=0x%x family=%d\n",
           buf, type, ifi->ifi_index, ifi->ifi_flags, ifi->ifi_family);

    return MNL_CB_OK;
}

/* ---------- 初始化并订阅 ---------- */

static struct mnl_socket *nl_subscribe(unsigned int grp)
{
    struct mnl_socket *nl = mnl_socket_open(NETLINK_ROUTE);
    if (!nl) {
        perror("mnl_socket_open");
        return NULL;
    }

    /* 设置接收缓冲区(默认太小会丢多播消息) */
    mnl_socket_setsockopt(nl, NETLINK_EXT_ACK, &(int){1}, sizeof(int));

    /* 绑定 port_id 并订阅多播组 */
    if (mnl_socket_bind(nl, grp, MNL_SOCKET_AUTOPID) < 0) {
        perror("mnl_socket_bind");
        mnl_socket_close(nl);
        return NULL;
    }

    return nl;
}

/* ---------- 主循环 ---------- */

int main(int argc, char *argv[])
{
    (void)argc; (void)argv;
    char buf[MNL_SOCKET_BUFFER_SIZE];
    fd_set rfds;
    int ret, fd;

    struct mnl_socket *nl = nl_subscribe(RTMGRP_LINK); /* 订阅接口事件多播 */
    if (!nl)
        return EXIT_FAILURE;

    fd = mnl_socket_get_fd(nl);
    printf("监听网络接口变化(Linux Netlink RTMGRP_LINK)...\n");

    for (;;) {
        FD_ZERO(&rfds);
        FD_SET(fd, &rfds);

        ret = select(fd + 1, &rfds, NULL, NULL, NULL);
        if (ret < 0) {
            if (errno == EINTR)
                continue;
            perror("select");
            break;
        }

        ret = mnl_socket_recvfrom(nl, buf, sizeof(buf));
        if (ret <= 0) {
            if (ret == -1 && errno == ENOBUFS) {
                /* 内核丢消息:接收缓冲区溢出 */
                fprintf(stderr, "ENBUFS: consider increasing rcvbuf\n");
                continue;
            }
            break;
        }

        ret = mnl_cb_run(buf, ret, 0, 0, parse_newlink_cb, NULL);
        if (ret == MNL_CB_ERROR)
            break;
    }

    mnl_socket_close(nl);
    return 0;
}

编译与运行:

gcc -O2 -Wall -o netmon netlink_iface_monitor.c $(pkg-config --cflags --libs libmnl)
sudo ./netmon
# 输出示例:
# 监听网络接口变化(Linux Netlink RTMGRP_LINK)...
# [2025-05-15 10:23:01] NEWLINK ifindex=3 flags=0x11043 family=0  MAC=aa:bb:cc:dd:ee:ff MTU=1500
# [2025-05-15 10:23:45] DELLINK ifindex=3 flags=0x1 family=0

将另一个终端执行 ip link add veth0 type veth peer name veth1,可立即看到 Newlink 事件。

3.3 关键细节:ENOBUFS 的处理

生产环境中常见 ENOBUFS 错误——用户空间 recvfrom() 来不及消费,内核消息队列溢出。Netlink 多播有广播特性,网络变化风暴时消息速率可以很高。

解决方案:

/* 增加接收缓冲区 */
#define RECVBUF_SIZE (2 * 1024 * 1024)  /* 2MB */
setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &RECVBUF_SIZE, sizeof(RECVBUF_SIZE));

/* 或者 UNIX 非阻塞模式 + epoll 模式 */
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);

注意:Linux 的 SO_RCVBUF 会做"double up"处理(实际分配 2×你设定的值),但系统最大值通过 /proc/sys/net/core/rmem_max 限制。


4.1 查询路由表

这是 route_monitor_dump.c —— 获取 IPv4/IPv6 默认路由的网关和出接口:

/* get_default_route.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
#include <libmnl/libmnl.h>

struct route_request {
    struct mnl_nlmsg_batch *batch;
    unsigned int seq;
    unsigned int portid;
};

/* 构建 RTM_GETROUTE dump 请求 */
static void build_route_request(struct route_request *req)
{
    struct nlmsghdr *nlh;
    struct rtmsg *rtm;

    nlh = mnl_nlmsg_put_header(req->batch->buf);
    nlh->nlmsg_type = RTM_GETROUTE;
    nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP;
    nlh->nlmsg_seq = req->seq++;

    rtm = mnl_nlmsg_put_extra_header(nlh, sizeof(struct rtmsg));
    rtm->rtm_family = AF_INET; /* AF_INET6 for IPv6 */
    rtm->rtm_table = RT_TABLE_MAIN;

    mnl_nlmsg_batch_next(req->batch);
}

/* 解析路由响应 */
static int parse_route_cb(const struct nlmsghdr *nlh, void *data)
{
    struct rtmsg *rtm = mnl_nlmsg_get_payload(nlh);
    struct nlattr *attr;
    char dst[INET_ADDRSTRLEN] = "default";
    char gw[INET_ADDRSTRLEN] = "-";
    char src[INET_ADDRSTRLEN] = "-";
    int oif = -1;

    if (nlh->nlmsg_type != RTM_NEWROUTE)
        return MNL_CB_OK;

    mnl_attr_for_each(attr, nlh, sizeof(*rtm)) {
        int type = mnl_attr_get_type(attr);

        /* 跳过无法识别的属性(向前兼容关键) */
        if (mnl_attr_type_valid(attr, RTA_MAX) < 0)
            continue;

        switch (type) {
        case RTA_DST: {
            struct in_addr *addr = mnl_attr_get_payload(attr);
            inet_ntop(AF_INET, addr, dst, sizeof(dst));
            break;
        }
        case RTA_GATEWAY: {
            struct in_addr *addr = mnl_attr_get_payload(attr);
            inet_ntop(AF_INET, addr, gw, sizeof(gw));
            break;
        }
        case RTA_PREFSRC: {
            struct in_addr *addr = mnl_attr_get_payload(attr);
            inet_ntop(AF_INET, addr, src, sizeof(src));
            break;
        }
        case RTA_OIF:
            oif = mnl_attr_get_u32(attr);
            break;
        default:
            break;
        }
    }

    printf("dst=%-18s gw=%-16s src=%-16s oif=%d table=%d\n",
           dst, gw, src, oif, rtm->rtm_table);

    return MNL_CB_OK;
}

int main(void)
{
    struct mnl_socket *nl;
    struct route_request req;
    char buf[MNL_SOCKET_BUFFER_SIZE];
    unsigned int portid;
    int ret;

    nl = mnl_socket_open(NETLINK_ROUTE);
    if (!nl) {
        perror("mnl_socket_open");
        return EXIT_FAILURE;
    }

    if (mnl_socket_bind(nl, 0, MNL_SOCKET_AUTOPID) < 0) {
        perror("mnl_socket_bind");
        mnl_socket_close(nl);
        return EXIT_FAILURE;
    }

    portid = mnl_socket_get_portid(nl);

    /* 初始化消息批处理缓冲区 */
    req.batch = mnl_nlmsg_batch_start(buf, sizeof(buf));
    req.seq = time(NULL);

    build_route_request(&req);

    ret = mnl_socket_sendto(nl, mnl_nlmsg_batch_head(req.batch),
                            mnl_nlmsg_batch_size(req.batch));
    if (ret < 0) {
        perror("mnl_socket_sendto");
        return EXIT_FAILURE;
    }

    /* 持续接收多部分响应 */
    while ((ret = mnl_socket_recvfrom(nl, buf, sizeof(buf))) > 0) {
        ret = mnl_cb_run(buf, ret, req.seq - 1,
                         portid, parse_route_cb, NULL);
        if (ret <= MNL_CB_STOP)
            break;
    }

    mnl_nlmsg_batch_stop(req.batch);
    mnl_socket_close(nl);
    return 0;
}

4.2 添加/删除路由

/* add_route.c: 添加一条静态路由 */
static int add_route_v4(struct mnl_socket *nl, const char *dest, int prefix,
                        const char *gw, int oif)
{
    char buf[MNL_SOCKET_BUFFER_SIZE];
    struct nlmsghdr *nlh;
    struct rtmsg *rtm;
    struct in_addr gw_addr, dst_addr;
    unsigned int portid = mnl_socket_get_portid(nl);

    nlh = mnl_nlmsg_put_header(buf);
    nlh->nlmsg_type = RTM_NEWROUTE;
    nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_REPLACE;
    nlh->nlmsg_seq = time(NULL);

    rtm = mnl_nlmsg_put_extra_header(nlh, sizeof(struct rtmsg));
    rtm->rtm_family = AF_INET;
    rtm->rtm_dst_len = prefix;
    rtm->rtm_src_len = 0;
    rtm->rtm_tos = 0;
    rtm->rtm_table = RT_TABLE_MAIN;
    rtm->rtm_protocol = RTPROT_STATIC;
    rtm->rtm_scope = gw ? RT_SCOPE_UNIVERSE : RT_SCOPE_LINK;
    rtm->rtm_type = RTN_UNICAST;

    /* 目的地址 */
    inet_pton(AF_INET, dest, &dst_addr);
    mnl_attr_put(nlh, RTA_DST, sizeof(dst_addr), &dst_addr);

    /* 下一跳网关 */
    if (gw) {
        inet_pton(AF_INET, gw, &gw_addr);
        mnl_attr_put(nlh, RTA_GATEWAY, sizeof(gw_addr), &gw_addr);
    }

    /* 出接口 */
    mnl_attr_put_u32(nlh, RTA_OIF, oif);

    return mnl_socket_sendto(nl, nlh, nlh->nlmsg_len);
}

关键:NLM_F_CREATE | NLM_F_REPLACE 组合实现"不存在则创建,存在则覆盖"的幂等语义,这正是网络配置管理工具(NetworkManager、systemd-networkd)所依赖的行为。


Generic Netlink 是"Netlink 的 Netlink"——它本身占用一个 Netlink 族(NETLINK_GENERIC = 16),但通过家族名/命令号的二级寻址,将单个族扩展为可容纳 1..65535 个子协议。

这与 NETLINK_ROUTE 的每个子功能单独占用消息类型的方式相比,避免了大量重复的 rtnetlink 协议头实现。

Generic Netlink 消息结构:
┌────────────┬───────────────┬─────────────────┐
│ nlmsghdr   │ genlmsghdr    │ nlattr[]        │
│ (eth=16)   │ cmd/version   │ 协议特定属性    │
└────────────┴───────────────┴─────────────────┘

5.2 获取家族 ID

Generic Netlink 家族 ID 是动态分配的,需要运行时解析:

#include <linux/genetlink.h>

/* 通过 CTRL_CMD_GETFAMILY 查询家族 ID */
static int resolve_family_id(struct mnl_socket *nl, const char *family_name)
{
    char buf[MNL_SOCKET_BUFFER_SIZE];
    struct nlmsghdr *nlh;
    struct genlmsghdr *genl;
    unsigned int seq = time(NULL);
    unsigned int portid = mnl_socket_get_portid(nl);

    nlh = mnl_nlmsg_put_header(buf);
    nlh->nlmsg_type = GENL_ID_CTRL;       /* 内置 ctrl 家族 */
    nlh->nlmsg_flags = NLM_F_REQUEST;
    nlh->nlmsg_seq = seq;

    genl = mnl_nlmsg_put_extra_header(nlh, sizeof(struct genlmsghdr));
    genl->cmd = CTRL_CMD_GETFAMILY;
    genl->version = 1;

    mnl_attr_put_strz(nlh, CTRL_ATTR_FAMILY_NAME, family_name);

    if (mnl_socket_sendto(nl, nlh, nlh->nlmsg_len) < 0)
        return -1;

    int ret = mnl_socket_recvfrom(nl, buf, sizeof(buf));
    if (ret < 0)
        return -1;

    /* 解响应,从 CTRL_ATTR_FAMILY_ID 属性提取 ID */
    ret = mnl_cb_run(buf, ret, seq, portid, family_id_cb, &family_id);
    return family_id;
}

如果你在编写内核模块,通过以下 API 注册自定义协议:

/* 内核侧 */
static const struct genl_ops my_gnl_ops[] = {
    {
        .cmd = MY_CMD_ECHO,
        .doit = my_nl_echo_handler,
        .flags = GENL_ADMIN_PERM, /* 需要 CAP_NET_ADMIN */
    },
    {
        .cmd = MY_CMD_DUMP,
        .start = my_nl_dump_start,
        .dumpit = my_nl_dump,
        .done  = my_nl_dump_done,
    },
};

static struct genl_family my_gnl_family = {
    .name = "my-custom",
    .version = 1,
    .maxattr = MY_ATTR_MAX,
    .module = THIS_MODULE,
    .ops = my_gnl_ops,
    .n_ops = ARRAY_SIZE(my_gnl_ops),
    .resizable = true, /* 6.4+ 使用 genl_family_rcv_msg */
};

/* 注册 */
genl_register_family(&my_gnl_family);

eBPF 自身也使用 Netlink 进行程序加载(bp() 系统调用实际通过 Netlink 协议通信)和 Map 操作。不过更常见的交叉点是 tc (Traffic Control) 通过 Netlink 操作的 clsact qdisc + eBPF classifier。

/* 通过 Netlink 将 eBPF 程序附加到网络接口的 clsact */
static int tc_attach_bpf(const char *ifname, int ifindex,
                         const char *bpf_obj_path)
{
    struct mnl_socket *nl = mnl_socket_open(NETLINK_ROUTE);
    char buf[MNL_SOCKET_BUFFER_SIZE];
    struct nlmsghdr *nlh;
    struct tcmsg *tcm;

    /* 1. 加载 BPF 对象 */
    struct bpf_object *obj = bpf_object__open_file(bpf_obj_path, NULL);
    bpf_object__load(obj);
    int prog_fd = bpf_program__fd(bpf_object__find_program_by_name(obj, "tc_ingress"));

    /* 2. 创建 clsact qdisc */
    nlh = mnl_nlmsg_put_header(buf);
    nlh->nlmsg_type = RTM_NEWQDISC;
    nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_REPLACE;
    tcm = mnl_nlmsg_put_extra_header(nlh, sizeof(struct tcmsg));
    tcm->tcm_family = AF_UNSPEC;
    tcm->tcm_ifindex = ifindex;
    tcm->tcm_handle = TC_H_MAKE(TC_H_INGRESS, 0);
    tcm->tcm_parent = TC_H_INGRESS;

    struct nlattr *opts = mnl_attr_nest_start(nlh, TCA_OPTIONS);
    mnl_attr_put_strz(nlh, TCA_KIND, "clsact");
    mnl_attr_nest_end(nlh, opts);

    mnl_socket_sendto(nl, nlh, nlh->nlmsg_len);

    /* 3. 添加 BPF filter */
    /* ... 省略 BPF 挂载的详细 netlink 属性编码 ... */

    mnl_socket_close(nl);
    return 0;
}

NETLINK_CGROUP 和 LSM BPF 结合使用 Netlink 传递策略变更事件。例如当容器运行时需要为每个新容器注入独立的 LSM 策略时,它通过 Netlink 发送 BPF 加载请求和关联的 cgroup 路径。

# 使用 bpftool (通过 Netlink) 查看当前附加的 LSM BPF 程序
bpftool net show

# 输出示例:
# netns cgroup     sk_msg    my_lsm_prog    cgroup:/system.slice/container-abc123
# netns cgroup     sock_ops  my_sec_sock    cgroup:/system.slice/container-def456

七、生产环境最佳实践

7.1 错误处理

Netlink 内核响应可能携带 NLMSG_ERROR 消息(错误码为负的 errno):

static int error_cb(const struct nlmsghdr *nlh, void *data)
{
    struct nlmsgerr *err = mnl_nlmsg_get_payload(nlh);

    if (nlh->nlmsg_type == NLMSG_ERROR) {
        if (err->error == 0) {
            /* ACK 消息:操作成功 */
        } else {
            /* 错误码为负值(如 -EPERM) */
            fprintf(stderr, "Netlink error: %s\n",
                    strerror(-err->error));

            /* 可以通过 NLMSGERR_ATTR_MSG 获取内核错误描述 */
            struct nlattr *attr;
            mnl_attr_for_each(attr, nlh, sizeof(*err)) {
                if (mnl_attr_get_type(attr) == NLMSGERR_ATTR_MSG)
                    fprintf(stderr, "  kernel says: %s\n",
                            mnl_attr_get_str(attr));
            }
        }
    }
    return MNL_CB_OK;
}

7.2 接收大消息(Dump 优化)

当 dump 路由表或接口列表时,单个 NLM_F_MULTI 响应可能超出缓冲区。内核会将其拆分为多条 RTM_NEWROUTE,最后以 NLMSG_DONE 发送一条空消息标记结束:

/* 正确做法:使用 mnl_socket_recvfrom + mnl_cb_run 循环 */
while ((recv_len = mnl_socket_recvfrom(nl, buf, bufsize)) > 0) {
    ret = mnl_cb_run(buf, recv_len, seq, portid, data_cb, NULL);
    if (ret <= MNL_CB_STOP)
        break;  /* NLMSG_DONE 触发 STOP */
    if (ret == MNL_CB_OK)
        continue;
}

7.3 权限模型

不同的 Netlink 族有严格的权限要求:

  • RTM_NEWROUTE、RTM_DELROUTE:需要 CAP_NET_ADMIN
  • RTM_NEWRULE、RTM_DELRULE:需要 CAP_NET_ADMIN
  • NETLINK_AUDIT:需要 CAP_AUDIT_CONTROL
  • NETLINK_SOCK_DIAG:需要 CAP_NET_ADMIN(诊断用户不属于自己的 socket)
  • NETLINK_KOBJECT_UEVENT:仅 root 可发送伪造事件
  • NETLINK_GENERIC 自定义协议:核模块自行定义 ops 的 .flags 字段(GENL_ADMIN_PERM)

7.4 性能调优

问题 1:多播丢消息

增大 rmem_max 临时解决:

sysctl -w net.core.rmem_max=16777216  # 16MB
sysctl -w net.core.rmem_default=2097152  # 2MB

或者在代码中立即 recvfrom + epoll 快速消费:

mnl_socket_setsockopt(nl, NETLINK_NO_ENOBUFS, &(int){1}, sizeof(int));
/* 开启后内核不再发 ENOBUFS,直接丢弃(用户态需自行处理) */

问题 2:大量 dump 操作导致延迟

开启 NLM_F_ATOMIC 获取一致性快照(内核持有读取锁)。注意:原子 dump 在大规模路由表(数十万条 BGP 路由)下可能引发长时间读锁。


对于一个新的内核用户态接口设计,选择 Netlink 而不是 ioctl 的理由:

维度 ioctl Netlink eBPF 辅助系统调用
类型安全 ❌ 任意 unsigned long ✅ TLV 强类型 ✅ 但结构体需兼容
双向通信 ❌ 调用/返回 ✅ 推送 + 请求 ❌ (需 rpc 模式)
多播/事件 ❌ ✅ 多播组 ❌ (需 ringbuf)
可发现性 ❌ 私有头文件 ✅ GENL 家族列表 ❌
权限细粒度 仅 CAP_* nlmsg_flags + per-family BPF token
性能 最优 (单次 syscall) 稍差 (消息拷贝) 近似

实践判断:

  • 简单状态查询/少量数据传输:直接 syscall 或 sysfs。
  • 事件驱动、大批量数据、跨版本兼容:Netlink 是最佳选择。
  • 需要内核内计算逻辑:eBPF + Netlink 混合(Netlink 传递参数,eBPF 执行逻辑)。

九、总结

Netlink 是 Linux 内核与用户空间通信的"系统总线",几乎承载了所有基础设施工具的内核交互。理解 Netlink 不仅是编写高级网络工具的必备技能,更是进行内核观测和性能调优的基础。

关键点回顾:

  1. nlmsghdr + nlattr[] 是核心消息模型,务必注意 4 字节对齐。
  2. 优先选择 libmnl 而非手动编码,避免对齐陷阱。
  3. ENOBUFS 是生产环境常见错误,需增加接收缓冲区和快速消费。
  4. Generic Netlink 是新协议的首选架构,为动态家族 ID 提前处理 CTRL_CMD_GETFAMILY。
  5. 错误码为负值(-EPERM 等),ACK 消息的 error == 0 表示成功。
  6. NLM_F_MULTI 以 NLMSG_DONE 结束,务必以 NLMSG_DONE 为终止条件。
  7. Netlink 与 eBPF 天然互补——Netlink 传递策略和状态,eBPF 执行高效处理。

参考文档: - Linux 内核源码 net/core/rtnetlink.c、net/netlink/genetlink.c、net/netlink/af_netlink.c - libmnl 官方文档:https://www.netfilter.org/projects/libmnl/ - RFC 3549:Linux Netlink as an IP Services Protocol(虽 RFC 但信息较旧,以源码为准) - man 7 netlink、man 7 rtnetlink、man 3 genl

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部