Sending Raw TCP Packets from Python

When you want to experiment with low-level networking protocols, one of the first obstacles is actually getting packets out of your machine without the kernel interfering. The Linux kernel already has its own TCP stack, so if you try to send crafted packets over a normal network interface, the kernel reacts badly: when a SYN ACK comes back for a connection it never initiated, it tears the connection down. That makes it nearly impossible to talk to a remote host with your own homemade protocol implementation.

The solution is tun/tap — a virtual network device that hands raw packets to userspace. Instead of the kernel managing the connection, your Python program becomes the receiver and sender of everything crossing that interface. This is especially useful for building educational mini-implementations of networking protocols, as the author of this technique explains in their ongoing project on toy protocol implementations.

The tun/tap Difference

The tun/tap system offers two types of virtual interfaces: tun handles IP-layer packets, while tap deals with full Ethernet frames. For this approach, tun is the practical choice — it's simpler to get working.

Setting up a tun interface involves a few shell commands: first creating the tun0 device with an IP address (the author uses 192.0.2.2), granting your user write access, and then adding an iptables NAT rule so packets from the virtual device can reach the broader internet:

When the interface is up, packets flow between your Python script and the real network. You'll want NAT enabled, because without it those crafted packets never make it past your machine's loopback circuitry — they stay entirely local and never touch actual remote hosts.

Opening a Tun Device in Python

Connecting to the tun interface from Python is straightforward. The function below opens /dev/net/tun in binary mode and calls an ioctl to attach it to the device named tun0 (or whatever string you pass in):

Once the file descriptor is open, you can read from and write to it as if it were any other file. To send a SYN packet, you write a raw byte string representing the handcrafted TCP segment — in this case, a pre-encoded packet with the proper IP and TCP headers.

When run with sudo python3 syn.py, the script outputs exactly what it received from example.com:

The reply arrives, but this is a hardcoded packet rather than a real implementation — no code generates those bytes or parses the response. That's the point: this is just enough to see traffic actually move through your virtual interface.

Observing the Traffic in tcpdump

Running tcpdump on the tun0 interface shows the full exchange. The outgoing packet appears with Flags [S] — the SYN — and the response with Flags [S.], the SYN ACK. It's a successful round trip, and the Linux kernel didn't interfere at all.

Looking at your physical network interface instead (like the wireless card wlp3s0) reveals the NAT in action. The source address 192.0.2.2 has been rewritten to 192.168.1.181 by that iptables rule. You'll also notice example.com retrying its response in exponential backoff — after 4, 8, then 16 seconds — because the TCP handshake was never completed. That's similar in form to SYN flooding, though sending one or two packets isn't a concern.

The replies appear more often on the physical interface than on tun0 — likely because the Python code reads only one reply from the virtual device before exiting.

Why This Beats the Alternatives

Some earlier attempts at crafting TCP packets used ARP spoofing — a technique that involves impersonating another machine on the local network to intercept traffic. Not only is that less reliable, it's also ethically questionable if you're not on a network you own. Using tun/tap is far more predictable: you control the virtual interface completely without needing to hijack anyone's traffic.

The approach has its limitations — it works on Linux, and while there's a way to set up equivalent functionality on Mac, this particular code is Linux-only. But for experimenting with protocol implementations, it's a solid foundation that's far simpler to debug than trying to work around the kernel's own TCP stack.

For people who don't want to write their own packet assembly code, the scapy library is a great companion — it handles much of that heavy lifting while still giving you the same low-level access. If you want to go all the way down to raw bytes, however, this tun approach is a clean starting point. All the code used here is available in this gist.