DEV Community

Shrijith Venkatramana
Shrijith Venkatramana

Posted on

Story of a TCP Packet: An Intuitive Entrypoint To Linux Networking

Hi, I'm Shrijith Venkatramana. Right now, I'm building on an aggregation of 1,25,000+ resources on my site Free DevTools. This site hosts many free developer tools, manuals, cheatsheets and icon sets - which you can download or use without any login. Do give it a try here

Ever wondered what eth0, iptables, nat, or FORWARD actually mean in Linux networking? As engineers, we use these terms, but these terms—interfaces, iptables tables and chains, the networking stack—often feel like a black box. What are interfaces, are they hardware or software? Do they always need an IP? What’s the deal with iptables’ tables versus chains? How does a packet even flow through all this?

This post follows a TCP packet from your browser to a server running nginx, into a Docker container with a Python app, and back. We’ll use an analogy to ease in, a real-world example for context, plain English to clarify, and technical details to satisfy your curiosity. By the end, you’ll understand how Linux networking fits together and why it matters for debugging and building systems.

A Building Analogy: Interfaces as Doors, iptables as Gatekeepers

Picture your server’s network as a busy office building:

  • Interfaces are doors or hallways. The front entrance (eth0) welcomes external packets, while internal paths (docker0, lo) connect departments. Some doors are physical (like Ethernet ports); others are virtual (software-only).
  • iptables is the security team, with rulebooks (tables) for different tasks—filter for blocking or allowing, nat for rewriting addresses. Each rulebook has checkpoints (chains) like reception desks or mailrooms.
  • The networking stack is the building’s delivery system, ensuring letters (packets) reach the right desk through doors and guards.

From outside, it’s just sending a letter and getting a reply. Inside, a complex system makes it happen.

This shows interfaces can be hardware or software, and not all need IPs—some just pass traffic. iptables handles both security and routing (and property modification/mangling, but that's less important for now), and the stack ties it all together.

The Setup: From Browser to Dockerized Python App

Here, we will get a high level view of an interaction - which will form the basis of the rest of the post.

Imagine you visit https://example.com. Here’s the server setup:

  • nginx listens on the server’s public IP 203.0.113.10:443 via interface eth0.
  • nginx proxies requests to a Python app inside a Docker container at 172.17.0.2:8000, connected through a bridge interface docker0.

The packet’s journey:

  • Browser → Internet → Server (eth0, nginx) → docker0 → Container → Python app → Back.

This mirrors real-world web app setups. Let’s break it down in plain English from a slighly different angle.

Plain English: How Packets Enter and Move

When your browser sends a request, the packet arrives at eth0, a network interface. Interfaces are where packets enter or leave. Some are tied to physical hardware (like an Ethernet card for eth0), while others, like docker0 or lo (loopback), are software-based, existing only in the kernel. Not every interface needs an IP— althoug in practice bridges like docker0 often have one and help with forward packets from the server to the containers and back (act as gateways).

The kernel decides: deliver locally (to nginx on port 443) or forward (to a container)? Before that, iptables steps in. It’s the kernel’s tool for managing packets, handling both security (via the filter table to allow/deny) and routing (via the nat table to rewrite addresses). Tables are the big categories; chains are specific checkpoints within them, like INPUT for local delivery or FORWARD for routing. Each chain holds rules, like “allow TCP port 443.”

To see interfaces on your system:

#!/bin/bash
# List network interfaces and IPs
ip addr show
# Example output (shortened):
# 1: lo: <LOOPBACK> inet 127.0.0.1/8
# 2: eth0: <BROADCAST> inet 203.0.113.10/24
# 3: docker0: <BROADCAST> inet 172.17.0.1/16
Enter fullscreen mode Exit fullscreen mode

iptables Explained: Tables, Chains, and Their Roles

iptables is the kernel’s packet-processing engine, controlling both security and routing, among other things. Tables are the top-level categories, each with a purpose:

  • filter: Decides to accept or drop packets (security).
  • nat: Rewrites source/destination addresses (routing).
  • mangle: Modifies packet headers (less common).

Chains are checkpoints within tables, like steps in a process:

  • PREROUTING: First stop for incoming packets, before routing decisions.
  • INPUT: For packets destined locally (e.g., to nginx).
  • FORWARD: For packets passing through to another destination (e.g., container).
  • OUTPUT: For packets leaving the host.
  • POSTROUTING: Final step before packets exit.

Tables are bigger than chains—each table contains multiple chains, and chains hold specific rules, like “accept TCP to port 443.”

Here’s the structure:

Concept Purpose Examples
Table Groups tasks filter (security), nat (routing)
Chain Checkpoint in table PREROUTING, INPUT, FORWARD
Rule Action to take Accept port 443, rewrite to 172.17.0.2

To add a rule allowing HTTPS:

#!/bin/bash
# Add INPUT rule for HTTPS (run as root)
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
# No output; verify with:
# iptables -L INPUT -v -n
# Example: Chain INPUT ... ACCEPT tcp dpt:443
Enter fullscreen mode Exit fullscreen mode

Note that we only reference the INPUT chain here; the table is implicit. The default table is filter, so the rule above is shorthand for:

iptables -t filter -A INPUT -p tcp --dport 443 -j ACCEPT
Enter fullscreen mode Exit fullscreen mode

For iptables details, see netfilter documentation.

Step 1: Packet Hits eth0 and PREROUTING

The packet arrives at eth0:

  • Source: 192.168.1.10:50500 (browser)
  • Destination: 203.0.113.10:443 (server)

The kernel sees the destination IP matches eth0 (local delivery).

First stop: nat:PREROUTING for potential Destination NAT (DNAT), rewriting the destination. Here, nginx is on the host, so no rewrite occurs. In Docker setups with -p 443:8000, DNAT would change the destination to the container’s IP.

Capture the packet:

#!/bin/bash
# Capture HTTPS traffic on eth0 (run as root)
tcpdump -i eth0 tcp port 443 -c 1 -nn
# Example: IP 192.168.1.10.50500 > 203.0.113.10.443: Flags [S]...
Enter fullscreen mode Exit fullscreen mode

Step 2: filter:INPUT Delivers to nginx

Next, the filter:INPUT chain checks if the packet can reach nginx. A rule like above (--dport 443 -j ACCEPT) allows it. This is also where tools like ufw (Uncomplicated Firewall) add their rules—ufw is a user-friendly frontend for iptables that primarily manages the filter table to block or allow traffic.

nginx processes the request and opens a new connection to the container:

  • Source: 172.17.0.1:random (docker0 on host)
  • Destination: 172.17.0.2:8000 (container)

iptables acts before apps—it filters or rewrites packets before nginx sees them.

Step 3: Routing Through docker0 to the Container

The kernel routes the new packet via docker0, a software bridge interface (no IP, just forwards). Containers use virtual Ethernet (veth) pairs: one end (vethXXXX) on the host, the other (eth0) in the container.

Path: nginx → docker0vethXXXX → container’s eth0.

Check the bridge:

#!/bin/bash
# Show Docker bridge (needs bridge-utils)
brctl show docker0
# Example: docker0 ... interfaces: veth1234
Enter fullscreen mode Exit fullscreen mode

Docker networking: Docker bridge docs.

Step 4: Container App and Return Trip

In the container, the packet hits its eth0 (the one inside the container, this one is virtual) and reaches the Python app on port 8000. The app responds, and the packet reverses:

  • Container eth0vethXXXXdocker0 → nginx → eth0 → browser.

On return, filter:FORWARD ensures forwarding is allowed, and nat:POSTROUTING applies Source NAT (SNAT), rewriting the source from 172.17.0.2 to 203.0.113.10.

Add SNAT:

#!/bin/bash
# Add SNAT for outbound traffic (run as root)
iptables -t nat -A POSTROUTING -s 172.17.0.0/16 -o eth0 -j MASQUERADE
# Verify: iptables -t nat -L POSTROUTING -v -n
Enter fullscreen mode Exit fullscreen mode

Simple Python app:

# app.py - Run: python app.py
from http.server import BaseHTTPRequestHandler, HTTPServer

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.end_headers()
        self.wfile.write(b"Hello from container!")

server = HTTPServer(('0.0.0.0', 8000), Handler)
server.serve_forever()
# Run; test with: curl localhost:8000
# Output: Hello from container!
Enter fullscreen mode Exit fullscreen mode

Save and run with python app.py.

Why This Matters for Engineers

The Linux networking stack is the kernel’s machinery: interfaces receive packets, iptables’ tables (filter for security, nat for routing) and chains (INPUT, FORWARD) process them, and routing delivers them. It’s what powers every curl or server.

Understanding this helps you:

  • Debug with ip addr, iptables -L, or tcpdump.
  • Secure systems by crafting precise iptables rules.
  • Design networks, like Docker setups, with confidence.

Next time you see veth1234 or a cryptic iptables rule, you’ll know its place in the packet’s journey—and how to control it.

Top comments (0)