Netlink Multicast Kernel Group
Asked Answered
D

2

5

The task I am trying to achieve is actually quite simple (multicast the string "TEST" to a userland daemon), but the kernel module doesn't compile. It stops with the error:

passing argument 4 of ‘genlmsg_multicast_allns’ makes integer from pointer without a cast [enabled by default]

But shouldn't it just be the multicast group I defined?

Here is the code for "clarification":

#include <linux/module.h>
#include <net/sock.h>
#include <linux/netlink.h>
#include <linux/skbuff.h>
#include <linux/string.h>
#include <net/netlink.h>
#include <net/genetlink.h>

struct sock *nl_sk = NULL;

static void daemon(void){
        struct sk_buff *skb;
        void* msg_head;
        unsigned char *msg;

        struct genl_family my_genl_family = {
                .id = GENL_ID_GENERATE,
                .hdrsize = 0,
                .name = "family_name",
                .version = 1,
                .maxattr = 5
        };

        struct genl_multicast_group my_mc_group = {
                .name = "mc_group",
        };


        msg = "TEST";
        skb = genlmsg_new(NLMSG_GOODSIZE, GFP_KERNEL);

        msg_head = genlmsg_put(skb, 0, 0, &my_genl_family, 0, 21);

        nla_put(skb, 0, sizeof(msg), msg);

        genlmsg_end(skb, msg_head);

        genlmsg_multicast_allns( &my_genl_family, skb, 0, my_mc_group, GFP_KERNEL);

}

static int __init hello_init(void)
{
    printk("Entering: %s\n", __FUNCTION__);

    printk(KERN_INFO "Calling main function with sockets\n");

      struct netlink_kernel_cfg cfg = {
        .groups = 1,
        .flags  = NL_CFG_F_NONROOT_RECV,
      };

      nl_sk = netlink_kernel_create(&init_net, NETLINK_GENERIC, &cfg);


    daemon();

    return 0;
}

static void __exit hello_exit(void)
{
    printk(KERN_INFO "exiting hello module\n");
    netlink_kernel_release(nl_sk);
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");

Thanks for your help.

EDIT

This is the client side code:

#include <netlink/netlink.h>
#include <netlink/socket.h>
#include <netlink/msg.h>
#include <netlink/genl/genl.h>
#include <linux/genetlink.h>

/*
 * This function will be called for each valid netlink message received
 * in nl_recvmsgs_default()
 */
static int my_func(struct nl_msg *msg, void *arg)
{
        //struct nl_msg *nlmsg = nlmsg_alloc_size(GENL_HDRLEN+nla_total_size(sizeof(msg))+36);

        printf("Test\n");

        return 0;
}

int main(){
        struct nl_sock *sk;

        int gr_id;

        /* Allocate a new socket */
        sk = nl_socket_alloc();

        /*
         * Notifications do not use sequence numbers, disable sequence number
         * checking.
         */
        nl_socket_disable_seq_check(sk);

        /*
         * Define a callback function, which will be called for each notification
         * received
         */
        nl_socket_modify_cb(sk, NL_CB_VALID, NL_CB_CUSTOM, my_func, NULL);
       /* Connect to netlink generic protocol */
        nl_connect(sk, NETLINK_GENERIC);

        gr_id = genl_family_get_id("family_name");

        /* Subscribe to link notifications group */
        nl_socket_add_memberships(sk, gr_id, 0);

        /*
         * Start receiving messages. The function nl_recvmsgs_default() will block
         * until one or more netlink messages (notification) are received which
         * will be passed on to my_func().
        */
        while (1){
                nl_recvmsgs_default(sk);
        }

        return 0;
}
Dynameter answered 8/10, 2014 at 20:4 Comment(5)
You need to use an integer, not a struct genl_multicast_group in that call. See how it is done elsewhere in the kernel. Also note that this integer has an upper limit, something like 32.Alack
Thanks for your comment. And yes, the upper limit is 32. I will try to compile the corrected version.Dynameter
So the compilation of the kernel module was successful (thanks and sorry, that I cannot upvote your comment..).But I still cannot receive messages in Userland. (and just as info: it did compile the last time, the "error" above was just a warning, and how it seems not the source of the problem.)Dynameter
I had a similar issue recently when using a netlink socket to communicate between a kernel driver and a daemon. It seems that there are limitations on how netlink sockets are used. Some recommend using a multiplexed socket. I just gave up and went with a UDP socket, which worked the first time. If you are interested, I can post sample code.Alack
Yeah, I thought my code should work to just simply send a multicast for certain kernel events occurring. It would be great, if you could supply the code! Thanks a lot!Dynameter
E
6

Your core problem is that before using a Generic Netlink family, you first have to register it (this also applies to normal Netlink families). The kernel cannot handle families it doesn't know. Unless you're using an already existing family, this shapes the way you have to approach Netlink.

A Generic Netlink Family belongs to a kernel module. This means a userspace client cannot create a family. In turn, this means you can't just start the client, then have the module send the message right after it creates the family. This is because the family doesn't exist at the moment the client wanted to bind itself to it.

What you need to do is this:

  1. Create and register the family and the multicast group while you're inserting the module.
  2. Start the userspace client and have it bind itself to the family and multicast group.
  3. Have the kernel module send the message at some point (after the client's bind).
  4. The userspace client now receives the message.
  5. When the module is removed, it should unregister the family.

My version of your code follows. This is driver.c, the kernel module. As you can see, I decided to send the message repeatedly on a timer that runs every two seconds. This gives you time to start the client:

#include <linux/kernel.h>
#include <linux/module.h>
#include <net/genetlink.h>
#include <linux/timer.h>

static struct timer_list timer;
static const int repeat_ms = 2000;

/**
 * This callback runs whenever the socket receives messages.
 * We don't use it now, but Linux complains if we don't define it.
 */
static int hello(struct sk_buff *skb, struct genl_info *info)
{
    pr_info("Received a message in kernelspace.\n");
    return 0;
}

/**
 * Attributes are fields of data your messages will contain.
 * Among other reasons, the designers of Netlink want you to use these instead
 * of dumping raw data to the packet payload because it's designed to prevent
 * alignment problems for you.
 * (http://www.catb.org/esr/structure-packing/)
 */
enum attributes {
    /*
     * The first one has to be a throwaway empty attribute; I don't know
     * why.
     * If you remove it, ATTR_HELLO (the first one) stops working, because
     * it then becomes the throwaway.
     */
    ATTR_DUMMY,
    ATTR_HELLO,
    ATTR_FOO,

    /* This must be last! */
    __ATTR_MAX,
};

/**
 * Here you can define some constraints for the attributes so Linux will
 * validate them for you.
 */
static struct nla_policy policies[] = {
            [ATTR_HELLO] = { .type = NLA_STRING, },
            [ATTR_FOO] = { .type = NLA_U32, },
};

/**
 * Message type codes. All you need is a hello sorta function, so that's what
 * I'm defining.
 */
enum commands {
    COMMAND_HELLO,

    /* This must be last! */
    __COMMAND_MAX,
};

/**
 * Actual message type definition.
 */
struct genl_ops ops[] = {
    {
            .cmd = COMMAND_HELLO,
            .flags = 0,
            .doit = hello, /* The dummy function we defined above. */
            .dumpit = NULL,
    },
};

/**
 * Your multicast group. Choose a likely unique name.
 */
struct genl_multicast_group groups[] = {
    { .name = "PotatoGroup" },
};

/**
 * A Generic Netlink family is a group of listeners who can and want to speak
 * your "language" (ie. your set of Attributes).
 * Anyone who wants to hear your messages needs to register to the same family
 * as you.
 * (And because we're using multicast, they will have to register to the same
 * multicast group as well.)
 */
struct genl_family family = {
            .hdrsize = 0,
            .name = "PotatoFamily",
            .version = 1,
            .maxattr = __ATTR_MAX,
            .policy = policies,
            .ops = ops,
            .n_ops = ARRAY_SIZE(ops),
            .mcgrps = groups,
            .n_mcgrps = ARRAY_SIZE(groups),
};

void send_multicast(struct timer_list *t)
{
    struct sk_buff *skb;
    void *msg_head;
    unsigned char *msg = "TEST";
    int error;

    pr_info("----- Running timer -----\n");

    pr_info("Newing message.\n");
    skb = genlmsg_new(NLMSG_GOODSIZE, GFP_KERNEL);
    if (!skb) {
            pr_err("genlmsg_new() failed.\n");
            goto end;
    }

    pr_info("Adding Generic Netlink header to the message.\n");
    msg_head = genlmsg_put(skb, 0, 0, &family, 0, COMMAND_HELLO);
    if (!msg_head) {
            pr_err("genlmsg_put() failed.\n");
            kfree_skb(skb);
            goto end;
    }

    pr_info("Nla_putting 'hello' attribute.\n");
    error = nla_put_string(skb, ATTR_HELLO, msg);
    if (error) {
            pr_err("nla_put_string() failed: %d\n", error);
            kfree_skb(skb);
            goto end;
    }

    pr_info("Nla_putting 'foo' attribute.\n");
    error = nla_put_u32(skb, ATTR_FOO, 12345);
    if (error) {
            pr_err("nla_put_u32() failed: %d\n", error);
            kfree_skb(skb);
            goto end;
    }

    pr_info("Ending message.\n");
    genlmsg_end(skb, msg_head);

    pr_info("Multicasting message.\n");
    /*
     * The family has only one group, so the group ID is just the family's
     * group offset.
     * mcgrp_offset is supposed to be private, so use this value for debug
     * purposes only.
     */
    pr_info("The group ID is %u.\n", family.mcgrp_offset);
    error = genlmsg_multicast_allns(&family, skb, 0, 0, GFP_KERNEL);
    if (error) {
            pr_err("genlmsg_multicast_allns() failed: %d\n", error);
            pr_err("(This can happen if nobody is listening. "
                            "Because it's not that unexpected, "
                            "you might want to just ignore this error.)\n");
            goto end;
    }

    pr_info("Success.\n");
end:
    // Reschedule the timer to call this function again in repeat_ms
    // milliseconds
    mod_timer(t, jiffies + msecs_to_jiffies(repeat_ms));
}

static int init_socket(void)
{
    int error;

    pr_info("Registering family.\n");
    error = genl_register_family(&family);
    if (error)
            pr_err("Family registration failed: %d\n", error);

    return error;
}

static void initialize_timer(void)
{
    pr_info("Starting timer.\n");

    // Initialize the timer and assign the callback function
    timer_setup(&timer, send_multicast, 0);

    // Set the timer to expire in repeat_ms milliseconds from now
    mod_timer(&timer, jiffies + msecs_to_jiffies(repeat_ms));
}

static int __init hello_init(void)
{
    int error;

    error = init_socket();
    if (error)
            return error;

    initialize_timer();

    pr_info("Hello module registered.\n");
    return 0;
}

static void __exit hello_exit(void)
{
    // Make sure to delete the timer when exiting your module
    del_timer(&timer);
    genl_unregister_family(&family);
    pr_info("Hello removed.\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");

And this is app.c, the userspace client:

#include <netlink/netlink.h>
#include <netlink/socket.h>
#include <netlink/msg.h>
#include <netlink/genl/genl.h>
#include <netlink/genl/ctrl.h>

static struct nl_sock *sk = NULL;

/**
 * Attributes and commands have to be the same as in kernelspace, so you might
 * want to move these enums to a .h and just #include that from both files.
 */
enum attributes {
    ATTR_DUMMY,
    ATTR_HELLO,
    ATTR_FOO,

    /* This must be last! */
    __ATTR_MAX,
};

enum commands {
    COMMAND_HELLO,

    /* This must be last! */
    __COMMAND_MAX,
};

static int fail(int error, char *func_name)
{
    printf("%s() failed.\n", func_name);
    return error;
}

static int nl_fail(int error, char *func_name)
{
    printf("%s (%d)\n", nl_geterror(error), error);
    return fail(error, func_name);
}

/*
 * This function will be called for each valid netlink message received
 * in nl_recvmsgs_default()
 */
static int cb(struct nl_msg *msg, void *arg)
{
    struct nlmsghdr *nl_hdr;
    struct genlmsghdr *genl_hdr;
    struct nlattr *attrs[__ATTR_MAX];
    int error;

    printf("The kernel module sent a message.\n");

    nl_hdr = nlmsg_hdr(msg);
    genl_hdr = genlmsg_hdr(nl_hdr);

    if (genl_hdr->cmd != COMMAND_HELLO) {
            printf("Oops? The message type is not Hello; ignoring.\n");
            return 0;
    }

    error = genlmsg_parse(nl_hdr, 0, attrs, __ATTR_MAX - 1, NULL);
    if (error)
            return nl_fail(error, "genlmsg_parse");

    /* Remember: attrs[0] is a throwaway. */

    if (attrs[1])
            printf("ATTR_HELLO: len:%u type:%u data:%s\n",
                            attrs[1]->nla_len,
                            attrs[1]->nla_type,
                            (char *)nla_data(attrs[1]));
    else
            printf("ATTR_HELLO: null\n");

    if (attrs[2])
            printf("ATTR_FOO: len:%u type:%u data:%u\n",
                            attrs[2]->nla_len,
                            attrs[2]->nla_type,
                            *((__u32 *)nla_data(attrs[2])));
    else
            printf("ATTR_FOO: null\n");

    return 0;
}

static int do_things(void)
{
    struct genl_family *family;
    int group;
    int error;

    /* Socket allocation yadda yadda. */
    sk = nl_socket_alloc();
    if (!sk)
            return fail(-1, "nl_socket_alloc");

    nl_socket_disable_seq_check(sk);

    error = nl_socket_modify_cb(sk, NL_CB_VALID, NL_CB_CUSTOM, cb, NULL);
    if (error)
            return nl_fail(error, "nl_socket_modify_cb");

    error = genl_connect(sk);
    if (error)
            return nl_fail(error, "genl_connect");

    /* Find the multicast group identifier and register ourselves to it. */
    group = genl_ctrl_resolve_grp(sk, "PotatoFamily", "PotatoGroup");
    if (group < 0)
            return nl_fail(group, "genl_ctrl_resolve_grp");

    printf("The group is %u.\n", group);

    error = nl_socket_add_memberships(sk, group, 0);
    if (error) {
            printf("nl_socket_add_memberships() failed: %d\n", error);
            return error;
    }

    /* Finally, receive the message. */
    nl_recvmsgs_default(sk);

    return 0;
}

int main(void)
{
    int error;

    error = do_things();

    if (sk)
            nl_socket_free(sk);

    return error;
}

Here's also a sample Makefile (please verify your directories):

KERNEL_HEADERS := /lib/modules/$(shell uname -r)/build
LIBNL_INC := /usr/include/libnl3
LIBNL_LIB := /usr/lib/x86_64-linux-gnu

obj-m += driver.o

CC := gcc
CFLAGS := -I$(KERNEL_HEADERS)
APPFLAGS := -I$(LIBNL_INC)
LDFLAGS := -L$(LIBNL_LIB)
LDLIBS := -lnl-3 -lnl-genl-3

.PHONY: all driver app clean

all: driver app

driver:
        $(MAKE) -C $(KERNEL_HEADERS) M=$(shell pwd) modules

app: app.c
        $(CC) $(APPFLAGS) $(LDFLAGS) -o app app.c $(LDLIBS)

clean:
        $(MAKE) -C $(KERNEL_HEADERS) M=$(shell pwd) clean
        rm -f app
Energetic answered 7/11, 2015 at 1:11 Comment(1)
This code example solves my problems at work. Note the use of genl_ctrl_resolve_grp() to join multicast groups in userspace, which is a missing piece in either the test code and documentation of libnl.Graduated
A
1

This is not a direct answer to the netlink question, but an alternative solution. See comments above about netlink limitations.

UDP sockets can be used on Linux to communicate between a user mode process, such as a daemon, and a kernel mode component such as a loadable module.

Daemon code my_udp.c:

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <string.h>
#include <errno.h>
#include <pthread.h>

static int rcv_sock;
static int snd_sock;
static struct sockaddr_in rcv_addr_in;
static struct sockaddr_in snd_addr_in;
static pthread_t rcv_thread;

static void *rcv_thread_fn(void *data);

int my_udp_init(void)
{
    int sendlen, receivelen;
    int received = 0;

    if ((rcv_sock = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0) {
            perror("socket");
            return -1;
    }

    if ((snd_sock = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0) {
            perror("socket");
            return -1;
    }

    memset(&rcv_addr_in, 0, sizeof(rcv_addr_in));
    rcv_addr_in.sin_family = AF_INET;
    rcv_addr_in.sin_addr.s_addr = inet_addr("127.0.0.1");
    rcv_addr_in.sin_port = htons(MY_IN_PORT);

    receivelen = sizeof(rcv_addr_in);
    if (bind(rcv_sock, (struct sockaddr *) &rcv_addr_in, receivelen) < 0) {
            perror("bind");
            return -1;
    }

    memset(&snd_addr_in, 0, sizeof(snd_addr_in));
    snd_addr_in.sin_family = AF_INET;
    snd_addr_in.sin_addr.s_addr = inet_addr("127.0.0.1");
    snd_addr_in.sin_port = htons(MY_OUT_PORT);

    if (pthread_create(&rcv_thread, NULL, rcv_thread_fn, (void *)"rcv_thread")) {
            return -ENOMEM;
    }

    return 0;
}

void my_udp_cleanup(void)
{
    pthread_join(rcv_thread, NULL);
    close(rcv_sock);
    close(snd_sock);
}

int my_snd_msg(const char *buf, int size)
{
    sendto(snd_sock, buf, size, 0, (struct sockaddr *)&snd_addr_in, sizeof(snd_addr_in));
    return 0;
}

int my_rcv_msg(char *buf, int size)
{
    int cnt = 0;
    if ((cnt = recv(rcv_sock, buf, size, MSG_DONTWAIT)) < 0) {
            if (errno == EAGAIN) {
                    /* This is ok in the non-blocking case. */
                    sleep(1);
                    return 0;
            } else {
                    perror("recv");
                    return -1;
            }
    }
    return cnt;
}

static void *rcv_thread_fn(void *data)
{
    char buffer[64];
    int cnt;

    while (!g_stop) {
            cnt = my_rcv_msg(buffer, 63);
            if (cnt > 0) {
                    printf("message: %s\n", buffer);
            }
    }

    pthread_exit(0);
}

Kernel module code k_udp.c:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/in.h>
#include <net/sock.h>
#include <linux/skbuff.h>
#include <linux/delay.h>
#include <linux/inet.h>
#include <linux/kthread.h>

static struct work_struct rcv_worker;
static struct socket *in_socket = NULL;
static struct socket *out_socket = NULL;
static struct workqueue_struct *wq = NULL;
static struct task_struct *notify_thread = NULL;

void rcv_work_queue(struct work_struct *data)
{
        int len;
        printk(KERN_INFO "%s: *******\n", __func__);

        while ((len = skb_queue_len(&in_socket->sk->sk_receive_queue)) > 0) {
                struct sk_buff *skb = NULL;

                skb = skb_dequeue(&in_socket->sk->sk_receive_queue);
                printk("message len: %i message: %s\n", skb->len - 8, skb->data+8);
                kfree_skb(skb);
        }
}

static void cb_data(struct sock *sk, int bytes)
{
        printk(KERN_INFO "%s: *******\n", __func__);
        queue_work(wq, &rcv_worker);
}

void send_notification(char *text)
{
        struct sockaddr_in to_addr;
        struct msghdr msg;
        struct iovec iov;
        mm_segment_t oldfs;
        int len = 0;

        if (out_socket->sk == NULL) {
                printk(KERN_ERR "%s: socket skbuff is null\n", __func__);
                return;
        }

        iov.iov_base = text;
        len = strlen(text);
        iov.iov_len = len;

        memset(&to_addr, 0, sizeof(to_addr));
        to_addr.sin_family = AF_INET;
        to_addr.sin_addr.s_addr = in_aton("127.0.0.1");
        to_addr.sin_port = htons(MY_OUT_PORT);

        msg.msg_flags = 0;
        msg.msg_name = &to_addr;
        msg.msg_namelen = sizeof(struct sockaddr_in);
        msg.msg_control = NULL;
        msg.msg_controllen = 0;
        msg.msg_iov = &iov;
        msg.msg_iovlen = 1;
        msg.msg_control = NULL;

        oldfs = get_fs();
        set_fs(KERNEL_DS);
        sock_sendmsg(out_socket, &msg, len);
        set_fs(oldfs);
}

static int k_udp_notify_thread(void *data)
{
        int i = 0;
        while (!kthread_should_stop()) {
                char buf[64];

                sprintf(buf, "test from kernel%d\n", i++);
                send_notification(buf);
                msleep(1000);
        }
        return 0;
}

int k_udp_init(void)
{
        struct sockaddr_in addr_out;
        struct sockaddr_in addr_in;
        int rc = 0;
        printk("%s\n", __func__);
        if (in_socket) {
                printk(KERN_INFO "%s: socket already set up\n", __func__);
                return 0;
        }

        if (sock_create(PF_INET, SOCK_DGRAM, IPPROTO_UDP, &in_socket) < 0) {
                printk( KERN_ERR "%s: failed to create socket\n", __func__);
                return -EIO;
        }
        addr_in.sin_family = AF_INET;
        addr_in.sin_addr.s_addr = in_aton("127.0.0.1");
        addr_in.sin_port = htons( (unsigned short)MY_IN_PORT);
        rc = in_socket->ops->bind(in_socket, (struct sockaddr *)&addr_in, sizeof(addr_in));
        if (rc) {
                printk(KERN_ERR "%s: failed to bind\n", __func__);
                sock_release(in_socket);
                in_socket = NULL;
                return -EIO;
        }
        in_socket->sk->sk_data_ready = cb_data;

        if (sock_create(PF_INET, SOCK_DGRAM, IPPROTO_UDP, &out_socket) < 0) {
                printk( KERN_ERR "%s: failed to create socket\n", __func__);
                sock_release(in_socket);
                in_socket = NULL;
                return -EIO;
        }
        addr_out.sin_family = AF_INET;
        addr_out.sin_addr.s_addr = in_aton("127.0.0.1");
        addr_out.sin_port = htons( (unsigned short)MY_OUT_PORT);
        rc = out_socket->ops->connect(out_socket, (struct sockaddr *)&addr_out, sizeof(addr_out), 0);
        if (rc) {
                printk(KERN_ERR "%s: failed to connect\n", __func__);
                sock_release(in_socket);
                in_socket = NULL;
                sock_release(out_socket);
                out_socket = NULL;
                return -EIO;
        }

        notify_thread = kthread_create(k_udp_notify_thread, NULL, "k_notify_thread");
        if (notify_thread) {
                printk(KERN_INFO "%s: notify thread created\n", __func__);
                wake_up_process(notify_thread);
        } else {
                printk(KERN_ERR "%s: failed to create notify thread\n", __func__);
        }

        INIT_WORK(&rcv_worker, rcv_work_queue);
        wq = create_singlethread_workqueue("k_rcv_wq");
        if (!wq) {
                return -ENOMEM;
        }

        printk(KERN_INFO "%s: success\n", __func__);

        return 0;
}

void k_udp_cleanup(void)
{
        /* Should we check that the thread is still valid (hasn't exited)? */
        if (notify_thread) {
                kthread_stop(notify_thread);
                notify_thread = NULL;
        }

        if (in_socket) {
                sock_release(in_socket);
                in_socket = NULL;
        }

        if (out_socket) {
                sock_release(out_socket);
                out_socket = NULL;
        }
        if (wq) {
                flush_workqueue(wq);
                destroy_workqueue(wq);
                wq = NULL;
        }
}

Note: I've renamed some of the variables and function names from the code I'm using, so you may need to make changes to compile (if I missed something). Be sure to make the ports match between user/kernel components.

The above code was derived from multiple samples on the net and within the Linux kernel.

Alack answered 9/10, 2014 at 18:4 Comment(5)
Thanks for sharing the code. Did you compile it with linux' netlink or with libnl? Sorry, I cant even upvote this answer to thank you for the effort...Dynameter
@Dynameter I'll add the headers. The daemon was compiled with libpthreads. libnl was not needed.Alack
Had to add the defines for in and out ports. Kernel module compiles! Just one more question: did you declare g_stop in the user daemon? (while(!g_stop))Dynameter
@Dynameter In another file I have a global flag that will get toggled on certain signals. This will tell everything to shut down gracefully. You could use a counter to terminate or just run forever.Alack
You are great- thanks a lot. I think you agree, that the netlink multicast (on kernel side) could be a lot easier and better documented. Nevertheless: thanks for sharing! Whoever upvoted your answer: thanks too!Dynameter

© 2022 - 2024 — McMap. All rights reserved.