Can the NETLINK/SOCK_DIAG interface be used to listen for `listen()` and `close()` events of said socket?
Asked Answered
C

0

0

I've been gleaning information about the NETLINK socket which allows us to listen on what is happening in socket land. It seems to very partially work, but I'm wondering whether I missed something.

I'm using C++, although of course all the NETLINK is pure C, so here we have mainly C headers. The following code has three main parts:

  1. Binding

    First I create a socket() and bind() it.

    The bind() is kind of magical. When using a bound NETLINK socket, you start receiving events without having to have a polling setup (which is why I'm trying to do this, to avoid polling for when a socket starts listening for connections).

    I put -1 in the nl_groups so that way all events are sent to my socket. But, at this point, I seem to only receive two of them: TCP_ESTABLISHED and TCP_CLOSE. The one I really would like to receive is the TCP_LISTEN and "not listening" (which apparently is not going to be available...)

  2. Explicit Request

    I tried with an explicit request. I have it in the code below so you can see how I've done it. That request works as expected. I get an explicit reply if the socket exists or an error "No such file or directory" when the socket is closed. Great, except that mechanism means I'd be using a poll (i.e. I need my process to try over and over again on a timer until the socket is visible).

    Note: the error when no one is listening is happening because the request is explicit, i.e. it includes the expected IP address and port that I'm interested in.

  3. Response

    The next part is a loop, which sits until a response is received. The recvmsg() call is blocking in this version, which is why it sits around in this test.

    If I sent my explicit request (see point 2. above), then, as I mentioned, I get a reply if another process is listening, otherwise I get an error saying it's not listening. The state is clearly set to 10 (TCP_LISTEN), so everything works as expected.

    When listening to all the events (-1 in the bind), the process will go on and receive more data as events happen. However, so far, the only events I've received are 1 and 7 (i.e. TCP_ESTABLISHED and TCP_CLOSE).

I used the following to compile my code:

g++ -Wall -o a test.cpp

Here is my test code with which I can reproduce my current results:

#include    <iostream>
#include    <linux/netlink.h>
#include    <errno.h>
#include    <stdio.h>
#include    <string.h>
#include    <unistd.h>
#include    <sys/socket.h>
#include    <netinet/in.h>
#include    <linux/netlink.h>
#include    <linux/rtnetlink.h>
#include    <linux/sock_diag.h>
#include    <linux/inet_diag.h>

int
main(int argc, char ** argv)
{
    // socket / bind

    int d = socket(AF_NETLINK, SOCK_RAW, NETLINK_SOCK_DIAG);
    if(d < 0)
    {
        std::cerr << "error: could not create RAW socket.\n";
        return 1;
    }

    struct sockaddr_nl addr = {};
    addr.nl_family = AF_NETLINK;
    addr.nl_pid = getpid();
    addr.nl_groups = -1;
           // You can find these flags in Linux source:
           //
           //     "/usr/src/linux-headers-4.15.0-147/include/net/tcp_states.h
           //
           //   (1 <<  7)   // TCPF_CLOSE
           // | (1 <<  8)   // TCPF_CLOSE-WAIT
           // | (1 << 10)   // TCPF_LISTEN
           // | (1 << 11)   // TCPF_CLOSING
           // ;
    if(bind(d, reinterpret_cast<struct sockaddr *>(&addr), sizeof(addr)) != 0)
    {
        perror("bind failure\n");
        return 1;
    }

    // request

    struct sockaddr_nl nladdr = {};

    nladdr.nl_family = AF_NETLINK;

    struct nl_request
    {
        struct nlmsghdr         f_nlh;
        struct inet_diag_req_v2 f_inet;
    };

    nl_request req = {};

    req.f_nlh.nlmsg_len = sizeof(req);
    req.f_nlh.nlmsg_type = SOCK_DIAG_BY_FAMILY;
    req.f_nlh.nlmsg_flags = NLM_F_REQUEST;
    req.f_inet.sdiag_family = AF_INET;
    req.f_inet.sdiag_protocol = IPPROTO_TCP;
    req.f_inet.idiag_ext = 0;
    req.f_inet.pad = 0;
    req.f_inet.idiag_states = 0;
    req.f_inet.id.idiag_sport = htons(4998);
    req.f_inet.id.idiag_dport = 0;
    req.f_inet.id.idiag_src[0] = htonl(0x0A00020A);
    req.f_inet.id.idiag_dst[0] = 0;
    req.f_inet.id.idiag_if = 0;
    req.f_inet.id.idiag_cookie[0] = INET_DIAG_NOCOOKIE;
    req.f_inet.id.idiag_cookie[1] = INET_DIAG_NOCOOKIE;

    struct iovec vector = {};

    vector.iov_base = &req;
    vector.iov_len = sizeof(req);

    struct msghdr msg = {};

    msg.msg_name = &nladdr;
    msg.msg_namelen = sizeof(nladdr);
    msg.msg_iov = &vector;
    msg.msg_iovlen = 1;

    int const r(sendmsg(d, &msg, 0));
    if(r < 0)
    {
        perror("sendmsg");
        return 1;
    }

    // response

    struct sockaddr_nl r_nladdr = {};

    r_nladdr.nl_family = AF_NETLINK;

    struct iovec r_vector = {};

    long buf[8192 / sizeof(long)];
    r_vector.iov_base = buf;
    r_vector.iov_len = sizeof(buf);

    for(int i(1);; ++i)
    {
        struct msghdr r_msg = {};

        r_msg.msg_name = &r_nladdr;
        r_msg.msg_namelen = sizeof(r_nladdr);
        r_msg.msg_iov = &r_vector;
        r_msg.msg_iovlen = 1;

        //std::cout << "wait for message...\n";

        ssize_t size(recvmsg(d, &r_msg, 0));
        if(size < 0)
        {
            perror("recvmsg");
            return 1;
        }

        if(size == 0)
        {
            std::cout << "end of message stream received." << std::endl;
            break;
        }

        //std::cout << "got message #" << i << ": size = " << size << std::endl;

        struct nlmsghdr const * h(reinterpret_cast<struct nlmsghdr *>(buf));

        if(!NLMSG_OK(h, size))
        {
            std::cerr << "NLMSG_OK() says there is an error." << std::endl;
            return 1;
        }

        do
        {
            if(h->nlmsg_type == NLMSG_DONE)
            {
                std::cout << "explicit end of message stream received (NLMSG_DONE)." << std::endl;
                break;
            }

            if(h->nlmsg_type == NLMSG_ERROR)
            {
                struct nlmsgerr const * err(reinterpret_cast<struct nlmsgerr const *>(NLMSG_DATA(h)));

                if(h->nlmsg_len < NLMSG_LENGTH(sizeof(*err)))
                {
                    std::cerr << "unknown NLMSG_ERROR received." << std::endl;
                }
                else
                {
                    // here is the location display an error when trying to get an
                    // event about the LISTEN and no one is listening on that port.
                    //
                    errno = -err->error;
                    perror("NLMSG_ERROR:");
                }
                return 1;
            }

            if(h->nlmsg_type != SOCK_DIAG_BY_FAMILY)
            {
                std::cerr << "unexpected message type (h->nlmsg_type) "
                    << h->nlmsg_type
                    << std::endl;
                return 1;
            }

            //std::cout << "------- sock_diag info!\n";

            struct inet_diag_msg const * k_msg(reinterpret_cast<struct inet_diag_msg const *>(NLMSG_DATA(h)));
            if(h->nlmsg_len < NLMSG_LENGTH(sizeof(*k_msg)))
            {
                std::cerr << "unexpected message length (h->nlmsg_len) "
                    << h->nlmsg_type
                    << std::endl;
                return 1;
            }

            switch(k_msg->idiag_state)
            {
            case 1:
            case 7:
                break;

            default:
            {
                std::uint32_t const src_ip(ntohl(k_msg->id.idiag_src[0]));
                std::uint32_t const dst_ip(ntohl(k_msg->id.idiag_dst[0]));
                std::cout << "inet_diag_msg->idiag_family = " << static_cast<int>(k_msg->idiag_family) << "\n"
                    << "inet_diag_msg->idiag_state = " << static_cast<int>(k_msg->idiag_state) << "\n"
                    << "inet_diag_msg->idiag_timer = " << static_cast<int>(k_msg->idiag_timer) << "\n"
                    << "inet_diag_msg->idiag_retrans = " << static_cast<int>(k_msg->idiag_retrans) << "\n"
                    << "inet_diag_msg->id.idiag_sport = " << ntohs(k_msg->id.idiag_sport) << "\n"
                    << "inet_diag_msg->id.idiag_dport = " << ntohs(k_msg->id.idiag_dport) << "\n"
                    << "inet_diag_msg->id.idiag_src[0] = " << ((src_ip >> 24) & 255)
                                << "." << ((src_ip >> 16) & 255) << "." << ((src_ip >> 8) & 255) << "." << (src_ip & 255) << "\n"
                    << "inet_diag_msg->id.idiag_dst[0] = " << ((dst_ip >> 24) & 255)
                                << "." << ((dst_ip >> 16) & 255) << "." << ((dst_ip >> 8) & 255) << "." << (dst_ip & 255) << "\n"
                    << "inet_diag_msg->id.idiag_if = " << k_msg->id.idiag_if << "\n"
                    << "inet_diag_msg->id.idiag_cookie[0] = " << k_msg->id.idiag_cookie[0] << "\n"
                    << "inet_diag_msg->id.idiag_cookie[1] = " << k_msg->id.idiag_cookie[1] << "\n"
                    << "inet_diag_msg->idiag_expires = " << k_msg->idiag_expires << "\n"
                    << "inet_diag_msg->idiag_rqueue = " << k_msg->idiag_rqueue << "\n"
                    << "inet_diag_msg->idiag_wqueue = " << k_msg->idiag_wqueue << "\n"
                    << "inet_diag_msg->idiag_uid = " << k_msg->idiag_uid << "\n"
                    << "inet_diag_msg->idiag_inode = " << k_msg->idiag_inode << "\n"
                    << "\n";
            }
                break;

            }

            // next message
            //
            h = NLMSG_NEXT(h, size);
        }
        while(NLMSG_OK(h, size));
    }

    return 0;
}

To test that IP:port combo, I simply used the nc command like so:

nc -l 10.0.2.10 4998

You of course need the 10.0.2.10 IP on one of your interfaces for this to work.

My question is:

Did I do something wrong that I do not receive TCP_LISTEN events on that socket unless explicitly requested?

P.S. Just in case, I tried to run this test app as root. Same results.

Correna answered 18/7, 2021 at 0:46 Comment(6)
My socket foo is very out of date, but I think you have shown us the receiving/snooping bits here, is anything actually trying to send data to that port?Ametropia
I would add that, when in doubt, do not try running as root especially with untested network code.Ametropia
@Ametropia Note that's just a NETLINK which talks to the kernel, so very local. Also I wrote the code so I trust it enough. Now, if I remove the switch(k_msg->idiag_state) and let all cases go through the default: ... case, then I see the established and close events. So I think the loop generally works. But somehow doesn't want to send messages for the new TCP_LISTEN in said loop.Correna
@AlexisWilke did you manage to find a solution to this issue?Dewie
@Dewie Unfortunately, I have not seen any movement on that. I am thinking that newer versions of Linux may get better. I tested on 22.10 and it still fails. Same way.Correna
@AlexisWilke try to follow the ss source code (might be dated because it doesn't compile) but I have found it very useful for my usecase --> ss.cDewie

© 2022 - 2024 — McMap. All rights reserved.