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.