💾 Archived View for thrig.me › blog › 2024 › 05 › 26 › bobby-drop-connections.gmi captured on 2024-07-09 at 00:25:21. Gemini links have been rewritten to link to archived content

View Raw

More Information

⬅️ Previous capture (2024-05-26)

-=-=-=-=-=-=-

Bobby Drop Connections

    # netstat -np tcp
    ...
    tcp          0      0  127.0.0.1.6379         127.0.0.1.12808        TIME_WAIT
    Active Internet connections
    Proto   Recv-Q Send-Q  Local Address          Foreign Address        TCP-State
    tcp6         0      0  fda5:e7e0:793c:8.1965  fda5:e7e0:793c:8.25642 ESTABLISHED

One problem here is that some IPv6 addresses are too long to fit into the usual terminal width. Another problem is that the bytes, even if fully revealed, are not directly usable with other tools such as tcpdrop(8).

    # fstat | grep fda5
    _gemini  gmid       28482   10* internet6 stream tcp 0xffff800000987ce8 [fda5:e7e0:793c:80b9:ff13:e69c:9460:1]:1965 <-- [fda5:e7e0:793c:80b9:ff13:e69c:9460:6704]:25642

One could use awk or something to hopefully extract out the bits you are interested in, or maybe add some XML or JSON libraries so there can be --with-output=json and --filter=ipv4,ipv6 and so forth flags, or maybe instead with a trip through jq(1). Another method is to write a new tool that emits only what you are interested in (and maybe to put that code into a library, if both CLI and daemon code would be using it). So, a tool that prints just the IP and ports of any active TCP connections.

    # malinger
    192.168.92.1 22 192.168.92.2 20649
    [fda5:e7e0:793c:80b9:ff13:e69c:9460:1] 1965 [fda5:e7e0:793c:80b9:ff13:e69c:9460:6704] 25642

These lines can be passed directly to tcpdrop(8), but hopefully you do not kill your own SSH connection in the process.

The custom tool is a small subset of fstat(1) so one way to make a tool like this is to study the source code for fstat(1) and pull out the bits you need. Usually such tools are unportable, unless someone has made a cross platform library that abstracts the platform specific details away, but then you'd maybe also need an abstraction for tcpdrop(8), etc. Most folks will instead sit in a single OS silo, as it usually does not make sense to diversify the stacks one must support. IT is a cost center.

    #include <sys/types.h>

    #include <sys/file.h>
    #include <sys/socket.h>
    #include <sys/sysctl.h>

    #include <arpa/inet.h>
    #include <netinet/in.h>

    #include <err.h>
    #include <fcntl.h>
    #include <kvm.h>
    #include <limits.h>
    #include <netdb.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>

    static void socktrans(struct kinfo_file *kf);
    static const char *inet6_addrstr(struct in6_addr *);

    int
    main(int argc, char *argv[])
    {
        char *optstr = "fnop:su:vN:M:";

        char *memf, *nlistf;
        nlistf = memf = NULL;
        int flags     = KVM_NO_FILES;

        char buf[_POSIX2_LINE_MAX];
        kvm_t *kd;
        if ((kd = kvm_openfiles(nlistf, memf, NULL, flags, buf)) == NULL)
            errx(1, "%s", buf);

        struct kinfo_file *kf, *kflast;
        int cnt;
        if ((kf = kvm_getfiles(kd, KERN_FILE_BYPID, -1, sizeof(*kf), &cnt)) ==
            NULL)
            errx(1, "%s", kvm_geterr(kd));

        if (pledge("stdio", NULL) == -1) err(1, "pledge");

        for (kflast = &kf[cnt]; kf < kflast; ++kf) {
            if (kf->f_type == DTYPE_SOCKET &&
                kf->so_protocol == IPPROTO_TCP && kf->inp_fport)
                socktrans(kf);
        }
        exit(EXIT_SUCCESS);
    }

    static const char *
    inet6_addrstr(struct in6_addr *p)
    {
        struct sockaddr_in6 sin6;
        static char hbuf[NI_MAXHOST];
        const int niflags = NI_NUMERICHOST;

        memset(&sin6, 0, sizeof(sin6));
        sin6.sin6_family = AF_INET6;
        sin6.sin6_len    = sizeof(struct sockaddr_in6);
        sin6.sin6_addr   = *p;
        if (IN6_IS_ADDR_LINKLOCAL(p) &&
            *(u_int16_t *) &sin6.sin6_addr.s6_addr[2] != 0) {
            sin6.sin6_scope_id =
              ntohs(*(u_int16_t *) &sin6.sin6_addr.s6_addr[2]);
            sin6.sin6_addr.s6_addr[2] = sin6.sin6_addr.s6_addr[3] = 0;
        }

        if (getnameinfo((struct sockaddr *) &sin6, sin6.sin6_len, hbuf,
                        sizeof(hbuf), NULL, 0, niflags))
            return "invalid";

        return hbuf;
    }

    inline static void
    socktrans(struct kinfo_file *kf)
    {
        switch (kf->so_family) {
        case AF_INET: {
            struct in_addr laddr, faddr;
            memcpy(&laddr, kf->inp_laddru, sizeof(laddr));
            memcpy(&faddr, kf->inp_faddru, sizeof(faddr));
            // skip 127.0.0.0/8 addresses
            if ((laddr.s_addr & 0xFF) == 127 ||
                (faddr.s_addr & 0xFF) == 127)
                return;
            printf("%s %d", inet_ntoa(laddr), ntohs(kf->inp_lport));
            printf(" %s %d\n", inet_ntoa(faddr), ntohs(kf->inp_fport));
            break;
        }
        case AF_INET6: {
            struct in6_addr laddr6, faddr6;
            memcpy(&laddr6, kf->inp_laddru, sizeof(laddr6));
            memcpy(&faddr6, kf->inp_faddru, sizeof(faddr6));
            // NOTE one may also want to exclude
            // IN6_IS_ADDR_LINKLOCAL connections
            if (IN6_IS_ADDR_LOOPBACK(&laddr6) ||
                IN6_IS_ADDR_LOOPBACK(&faddr6))
                return;
            printf("[%s] %d", inet6_addrstr(&laddr6), ntohs(kf->inp_lport));
            printf(" [%s] %d\n", inet6_addrstr(&faddr6),
                   ntohs(kf->inp_fport));
            break;
        }
        default:;
        }
    }

It turns out that gmid lacks a timeout option for clients that connect but do nothing. So here it might have been better to ask questions about the problem first and write the code, if at all, later.

Why do you need a tool like tcpdrop(8), and other tools to check for things that are stuck open? You could ignore the issue, but maybe you have firewall rules that limit the number of connections to a service, which would make the service easier to denial-of-service if someone holds connections open. Allowing all the connections may mean there would not be enough to allow SSH into the system, so you may want to reserve a socket or two for administrative needs. tcpdrop(8) or similar is necessary to close already open connections, as most firewalls use stateful rules that bypass the (possibly expensive) firewall rules traverse for each packet when the connection is already known. This means a naughty host getting put into a blacklist will do nothing for the already open connections, so either you need a tool to drop those, or to restart the daemon involved, or to reboot the host involved, or to reboot the civilization involved, etc.