From Raw Sockets to eBPF: Watching the Kernel Undress a Packet
I usually play with sockets by sending data. Just call `send()`, pass a buffer, done. Simple. But one day I actually paused and thought —…
From Raw Sockets to eBPF: Watching the Kernel Undress a Packet
I usually play with sockets by sending data. Just call send(), pass a buffer, done. Simple. But one day I actually paused and thought — wait, I’m only giving the kernel my payload. Who is building all those headers around it?
The kernel was. Quietly. Every time. Constructing Ethernet headers, IP headers, Transportheaders — layering them on top of my data and firing it out onto the wire, without me ever asking or even noticing.

That got me curious. If the kernel is building all those layers on my behalf, can I do it myself?
So I tried. I opened a raw socket(SOCK_RAW), and manually constructed every header — byte by byte, ofcourse with AI. Destination MAC, source MAC, EtherType. Then the IP header — version, TTL, protocol, checksum. Then UDP — source port, destination port, length. Then finally, my payload at the end.
BYTE l2_hdr[ETHSIZE] = {
0xdc, 0xa6, 0x32, 0x4f, 0xb9, 0x75, /* dst mac */
0xdc, 0xa6, 0x32, 0x52, 0xf8, 0x07, /* src mac */
0x08, 0x00 /* EtherType IPv4 */
};
/* ── L3 : IP header ── */
struct iphdr ip = {0};
ip.version = 4;
ip.ihl = IPHSIZE >> 2;
ip.tos = 0;
ip.id = htons(1675);
ip.frag_off = 0;
ip.ttl = 64;
ip.protocol = IPPROTO_UDP;
ip.saddr = inet_addr("10.6.6.63");
ip.daddr = inet_addr("10.6.6.87");
ip.tot_len = htons(IPHSIZE + UDPHSIZE + PAYLOADSIZE);
ip.check = (unsigned short)in_cksum((unsigned short *)&ip, IPHSIZE);
/* ── L4 : UDP header ── */
struct udphdr udp = {0};
udp.uh_sport = htons(1112);
udp.uh_dport = htons(2223);
udp.uh_ulen = htons(UDPHSIZE + PAYLOADSIZE);
/* ── assemble frame ── */
memcpy(buf, l2_hdr, ETHSIZE);
memcpy(buf + ETHSIZE, &ip, IPHSIZE);
memcpy(buf + ETHSIZE + IPHSIZE, &udp, UDPHSIZE);
memcpy(buf + ETHSIZE + IPHSIZE + UDPHSIZE, payload, PAYLOADSIZE);
int sock_fd = create_socket(argv[1]);
int sent = write(sock_fd, buf, total);
I sent it. The receiver got it.

PAcket captured at receiver end
That moment felt huge. Like I had touched something fundamental. The OSI model had always been a diagram in a textbook — seven clean boxes stacked on top of each other. But now it was bytes in my hands.
But Then The Other Side
I could construct the layers going out. But what about coming in?
On the receiver side, my application (receiver)only ever sees the payload. The Ethernet header — gone. The IP header — gone. The UDP header — gone. Just the raw data lands in my buffer, clean and stripped, as if the headers never existed.
Who is removing them? Where exactly does that happen?
I wanted to watch it. Not do it myself — the kernel was doing a fine job of that. I wanted to peek inside — to see the packet as it moves through the kernel, to catch it at each layer while the headers are still attached, to watch them get peeled away one by one.
And then a deeper question hit me: are these “layers” even separate things inside the kernel? Is Layer 2 a different module from Layer 3?
Or is it all just… one big interconnected piece of the kernel’s network subsystem?
I couldn’t answer it. I had no tool to look inside. So I left it. Filed it under things I don’t know how to do yet and moved on.
The Message From My Guru
Then one day, out of nowhere, I got a message.
From my GURU, It was about eBPF.
I read it. Then read it again. You can attach programs to live kernel functions. You can observe kernel internals in real time. No kernel module. No recompile. No reboot.
I could hook into the exact functions where the kernel processes each network layer — and print the raw bytes of the packet at every stage. I could finally watch the kernel undress a packet, layer by layer, in real time.
I asked Claude to help me write the bpftrace script, and got to work.
The Kernel, Caught in the Act
I started the receiver and then triggered the sender to send a UDP packet with my name as the payload: "Rajagopalaswamy". At the same time, I captured the packets using Wireshark.
On the receiver side, I ran bpftrace and attached probes at four key points in the kernel’s network stack — eth_type_trans, ip_rcv_core, __udp4_lib_rcv, and sys_exit_recvfrom to observe.
From wireshark

packet capture at receiver
This is the frame in hex —
dc a6 32 4f b9 75 dc a6 32 52 f8 07 08 00 45 00 00 2b 06 8b 00 00 40 11 53 96 0a 06 06 3f 0a 06 06 57 04 58 08 af 00 17 83 3d 52 61 6a 61 67 6f 70 61 6c 61 73 77 61 6d 79 00 00 00
It shows that,
Total length of packet is 60 bytes (14+20+8+18)
L2 Header — dc a6 32 4f b9 75 dc a6 32 52 f8 07 08 00 L3 Header— 45 00 00 2b 06 8b 00 00 40 11 53 96 0a 06 06 3f 0a 06 06 57
L4 Header — 04 58 08 af 00 17 83 3d
Actual data — 52 61 6a 61 67 6f 70 61 6c 61 73 77 61 6d 79 00 00 00
From eBPF (bpftrace)
And this is what the kernel had been silently doing all along:

BPFtrace output on receiver side — Decapsulation
Fig above — Sixty bytes arrive at the NIC.
eth_type_trans : The skb->data contains the whole packet
at — 0xffffff8043d9d982
ip_rcv_core : The skb->data shifts to 14 bytes forward(Eth header size)now it points to IP header + remaining at — 0xffffff8043d9d990
__udp4_lib_rcv : The skb->data shifts to 20 bytes forward (IP header size) now it points to Transport header + remaining at — 0xffffff8043d9d9a4
recvfrom : At last the kernel handover the data to receiver application running on port specified in UDP header
And to answer my own question — no, it isn’t separate modules handing off to each other. It’s one continuous network subsystem, one socket buffer (sk_buff) flowing through, with a pointer advancing forward as each layer is consumed not removed. The "layers" are just perspectives on the same memory.
eBPF is genuinely extraordinary. Without touching a single line of kernel code, without rebooting, I attached observers to live kernel functions and watched them run.
Source code : https://github.com/Raju-krish/Learnings/tree/main/ebpf
메타데이터
- post_id
- f92722aeee7f
- slug
- from-raw-sockets-to-ebpf-watching-the-kernel-undress-a-packet-f92722aeee7f
- url
- https://medium.com/@mrajagopalaswamy/from-raw-sockets-to-ebpf-watching-the-kernel-undress-a-packet-f92722aeee7f
- canonical_url
- https://medium.com/@mrajagopalaswamy/from-raw-sockets-to-ebpf-watching-the-kernel-undress-a-packet-f92722aeee7f
- author_url
- https://medium.com/@mrajagopalaswamy
- status
- ok
- fetched_at
- 2026-06-14 11:28:49