NetMoon: Raw Sockets and Network Monitoring

NetMoon is a network monitoring tool built on Linux raw sockets. It captures packets, parses TCP headers, and presents connection-level metrics in real time. The implementation is about 700 lines of C and demonstrates how far you can get with nothing more than a well-chosen syscall and a couple of struct definitions.

Raw Socket Setup

Raw sockets on Linux require CAP_NET_RAW (or root). The call is straightforward — socket(AF_PACKET, SOCK_RAW, htons(ETH_P_IP)) — which delivers every IP frame that reaches the interface.

int sock = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_IP));
if (sock < 0) {
    perror("socket");
    return 1;
}

The socket is placed into promiscuous mode via PACKET_ADD_MEMBERSHIP with PACKET_MR_PROMISC. This tells the NIC to forward all frames, not just those addressed to our MAC, so we see traffic from other hosts on the same broadcast domain.

Packet Capture Loop

The capture thread reads from the raw socket into a fixed 64kB buffer and hands the buffer to a parser running in a second thread. The split keeps the capture side lossless — if the parser lags, the kernel buffer fills and drops packets in the NIC ring, not in userspace.

for (;;) {
    ssize_t n = recvfrom(sock, buf, sizeof(buf), 0, NULL, NULL);
    if (n < 0) break;
    parse_frame(buf, n);
}

TCP Header Parsing

parse_frame walks the protocol stack: Ethernet header → IP header → TCP header. Each step checks the relevant length field before advancing.

void parse_frame(uint8_t *buf, size_t len) {
    struct ethhdr *eth = (struct ethhdr *)buf;
    if (ntohs(eth->h_proto) != ETH_P_IP) return;

    struct iphdr *ip = (struct iphdr *)(buf + sizeof(struct ethhdr));
    size_t ip_hlen = ip->ihl * 4;
    if (ip->protocol != IPPROTO_TCP) return;

    struct tcphdr *tcp = (struct tcphdr *)((uint8_t *)ip + ip_hlen);
    // extract src_port, dst_port, seq, ack, flags
    // update connection table
}

Connection Tracking

The parser maintains a hash table of active connections keyed by the 4-tuple (src_ip, src_port, dst_ip, dst_port). Each entry tracks byte counts, packet counts, the current TCP state (from flags), and a rough RTT measured from SYN/SYN-ACK timing. Expired entries — those with no activity for 60 seconds — are evicted on every tenth iteration to keep the table bounded.

Real-Time Display

A curses-based UI refreshes the connection table once per second, printing per-connection bandwidth as a bar chart and flags as human-readable state:

192.168.1.20:44012 → 10.0.0.1:443  (ESTABLISHED)  1.2 MB   ████████░░
192.168.1.20:44013 → 10.0.0.1:443  (ESTABLISHED)  340 kB   ██░░░░░░░░
192.168.1.30:22   → 10.0.0.2:53041 (ESTABLISHED)  4.1 MB   ██████████

Packet capture at 60% with TCP header parsing already gives a useful picture of what crosses the wire. The missing pieces — reassembly, deeper protocol dissection, and a filter language — are natural extensions once the core loop is solid.

Have a comment on this article? Send me an email.

The Segfault Garden

Lu

frgmntedflower@linux.com