netlink 是 Linux 系统里用户态程序、内核模块之间的一种 IPC 方式,特别是用户态程序和内核模块之间的 IPC 通信。比如在 Linux 终端里常用的 ip 命令,就是使用 netlink 去跟内核进行通信的。

TL,DR 使用 Go 通过 netlink 向内核模块发送消息,内核模块响应一条消息,源代码:内核模块Go 程序

内核模块

注册 netlink

insmod custom-netlink.ko 加载内核模块时,注册自定义的 netlink 处理函数。在 rmmod custom_netlink 卸载内核模块时,则注销自定义的 netlink 处理函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#define NETLINK_CUSTOM 31

static int __init custom_nl_init(void)
{
    //This is for 3.6 kernels and above.
    struct netlink_kernel_cfg cfg = {
        .input = custom_nl_recv_msg,
    };

    nl_sk = netlink_kernel_create(&init_net, NETLINK_CUSTOM, &cfg);
    ......
    return 0;
}

static void __exit custom_nl_exit(void)
{

    netlink_kernel_release(nl_sk);
    printk(KERN_INFO "[-] unregistered custom_netlink module.\n");
}

module_init(custom_nl_init);
module_exit(custom_nl_exit);

接收 netlink 消息

消息格式如下:

1
2
3
4
5
6
0       4       8
+-+-+-+-+-+-+-+-+
|msg len|  data |
+-+-+-+-+-+-+-+-+
|     data      |
+-+-+-+-+-+-+-+-+

前 4 个字节代表后面的数据长度,后跟着一段字符串。为了防止在打印字符串的时候内存越界了,将字符串的最后一个字符置为 \0

打印字符串后,将该字符响应回去。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
enum
{
    nlresp_result_unspec,
    nlresp_result_ok,
    nlresp_result_invalid
};

static void custom_nl_recv_msg(struct sk_buff *skb)
{
    struct nlmsghdr *nlh;
    unsigned char *nl_data;
    unsigned char *msg;
    __u32 msg_size;

    nlh = nlmsg_hdr(skb);
    nl_data = (unsigned char *)NLMSG_DATA(nlh);
    msg_size = *(__u32*)nl_data;
    if (msg_size > 1024) {
        custom_nl_send_msg(nlh, nlresp_result_invalid, NULL, 0);
        return;
    }

    msg = nl_data+4;
    msg[msg_size-1] = '\0';
    printk(KERN_INFO "[Y] [custom netlink] receive msg from user: %s\n", msg);

    custom_nl_send_msg(nlh, nlresp_result_ok, msg, msg_size);
}

响应 netlink 消息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
typedef struct
{
    __u32 result;
    unsigned char data[0];
} custom_nl_resp_data_t;

static int custom_nl_send_msg(struct nlmsghdr *nlh, __u32 result, const unsigned char *data, __u32 data_size)
{
    custom_nl_resp_data_t *resp;
    struct nlmsghdr *nlh_resp;
    struct sk_buff *skb;
    int pid = nlh->nlmsg_pid, res = -1;
    const unsigned char *resp_msg = "Echo from kernel: ";

    data_size += 4+18; // 4 是 result 的长度,18 是 resp_msg 的长度
    resp = kzalloc(data_size + 18, GFP_KERNEL);
    memcpy(resp->data, resp_msg, 18); // 复制 resp_msg
    memcpy(resp->data + 18, data, data_size-4); // 复制 Go 程序发送过来的字符串
    resp->result = result;

    skb = nlmsg_new(data_size, GFP_KERNEL); // 内核自动释放内存,不需要手动释放
    if (!skb)
        goto out;

    nlh_resp = nlmsg_put(skb, pid, nlh->nlmsg_seq, NLMSG_DONE, data_size, 0);
    memcpy(NLMSG_DATA(nlh_resp), resp, data_size); // 填充 netlink 消息内容
    res = nlmsg_unicast(nl_sk, skb, pid); // 将消息响应给指定 pid 的程序

out:
    kfree(resp); // 用后释放内存
    return res;
}

Go 程序

发起 netlink 连接

使用跟内核模块一致的 netlink family 发起连接。

1
2
3
4
5
const (
    netlinkCustom = 31
)

conn, err := netlink.Dial(netlinkCustom, nil)

发送 netlink 消息

封装 netlink 消息,并将提供的字符串复制到消息中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
  // msg 是前面提供的字符串
    data := make([]byte, 4+len(msg)+1)
    nlenc.PutUint32(data[:4], uint32(len(msg)+1))
    copy(data[4:], msg)

    fmt.Println("Send to kernel:", msg)

    var nlmsg netlink.Message
    nlmsg.Data = data

    msgs, err := conn.Execute(nlmsg)

接收 netlink 消息

按如下格式接收消息:

1
2
3
4
5
6
0       4       8
+-+-+-+-+-+-+-+-+
| result|  data |
+-+-+-+-+-+-+-+-+
|     data      |
+-+-+-+-+-+-+-+-+

前 4 个字节是内核模块响应的结果,后跟着一段字符串。

1
2
3
4
5
6
7
  msgs, err := conn.Execute(nlmsg)
    ......

    nlmsg = msgs[0]
    ......

    fmt.Println(string(nlmsg.Data[4:]))

实验效果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
➜  kernel-module-fun make
make -C /lib/modules/4.19.0/build M=/root/Projects/kernel-module-fun modules
make[1]: Entering directory '/usr/src/linux-headers-4.19.0'
  CC [M]  /root/Projects/kernel-module-fun/custom-netlink.o
  Building modules, stage 2.
  MODPOST 4 modules
  CC      /root/Projects/kernel-module-fun/custom-netlink.mod.o
  LD [M]  /root/Projects/kernel-module-fun/custom-netlink.ko
make[1]: Leaving directory '/usr/src/linux-headers-4.19.0'
➜  kernel-module-fun insmod custom-netlink.ko
➜  kernel-module-fun ./custom-netlink/custom-netlink
Send to kernel: Hello from Go
Echo from kernel: Hello from Go
➜  kernel-module-fun rmmod custom_netlink
➜  kernel-module-fun dmesg
[2424642.636765] [+] registered custom_netlink module!
[2424644.929387] [Y] [custom netlink] receive msg from user: Hello from Go
[2424647.822184] [-] unregistered custom_netlink module.

小结

使用 netlink 进行用户态程序和内核模块的 IPC 通信,能够动态地变更内核模块的配置,甚至动态地去开启、关闭内核模块里的功能。比如在收到配置后,再挂载 netfilter hook

使用 netlink 动态传配置的功能,可以取代 insmod 传参这一笨拙的传配置方式。

P.S. go-iproute:使用 Go 基于 netlink 实现的 iproute2 工具库。