DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Iptables Basic Commands for Novice
  • Building Threat Intelligence Pipelines Using Python, APIs, and Elasticsearch
  • Identity in Action
  • 5 AI Security Incidents That Broke Things in Production (and What They Have in Common)

Trending

  • RAG Is Not Enough: Advanced Retrieval Architectures Using Vertex AI Search on GCP
  • Mocking Kafka for Local Spring Development
  • Introduction to Tactical DDD With Java: Steps to Build Semantic Code
  • Ingesting Fixed-Width Mainframe Files Into Delta Lake: The Details Nobody Writes Down
  1. DZone
  2. Software Design and Architecture
  3. Security
  4. Unpack IPTables: Its Inner Workings With Commands and Demos

Unpack IPTables: Its Inner Workings With Commands and Demos

Iptables is a technology used in Linux subsystems to filter packets. This was the third attempt, and it proved to be very successful. Let's unpack how they work.

By 
Ramesh Sinha user avatar
Ramesh Sinha
·
Oct. 02, 25 · Analysis
Likes (3)
Comment
Save
Tweet
Share
2.1K Views

Join the DZone community and get the full member experience.

Join For Free

We all know that the internet works by sending and receiving small chunks of data called packets. Back in the early days, when the internet was still in its infancy, packets were allowed to transfer freely across a connected world, however small that world was. Anyone could send packets to your system, and you could send packets to other connected systems. All services running on systems were exposed by default. 

As the internet started to grow, problems started to emerge, problems related to security. There are worms, viruses, unauthorized access, denial-of-service (DoS) attacks, IP spoofing, etc. Iptables is an attempt to deal with some of these problems.

Some History

In early Linux systems, there were basic tools to handle networking, allowing users to connect to remote machines, share data, and run servers. However, it quickly became clear that raw networking capabilities weren't enough. The growing demand called for:

  • Firewalls – Blocking or allowing network traffic based on defined rules.
  • Network Address Translation (NAT) – Allowing multiple devices to share a single IP address (we were running out of IPV4 addresses )

Initially, Linux introduced a tool called ipfwadm into the kernel to support a firewall. But it had significant limitations 

  • Only basic packet filtering capabilities
  • No support for stateful inspection; if outbound traffic was allowed, there was no automatic way to allow the related inbound response. For example, if you pinged, you wouldn't automatically receive the reply unless you manually configured it.

Things got a little better somewhat with the introduction of ipchains. It offered certain advantages over ipfwadm, such as 

  • More flexible rule matching 
  • Basic support for connection tracking.

However, it still fell short. The connection tracking wasn't fully stateful, there was no support for IPv6, and the design wasn't scalable — meaning rule sets became slower and harder to manage as they grew.

NetFilter

Before getting to iptables, we should talk about NetFilter, after all, that is the foundation of iptables. Initial packet filtering in Linux systems was linear, and there was no easy way to intercept, modify, or drop packets. Netfilter provided the kernel-level foundation to solve this problem.

Netfilter is a framework that solves this problem and adds powerful new capabilities. Very cleverly, Netfilter developers introduced hook points at critical stages of packet processing within the Linux networking stack. These stages are: 

  • PREROUTING 
  • INPUT
  • FORWARD
  • OUTPUT
  • POSTROUTING 

They also implemented a mechanism that allows kernel modules to register callbacks (also known as hook functions ) at these points.

With these changes, the Linux networking stack began calling the registered hook functions during packet traversal, effectively enabling features like firewall, NAT, packet mangling, and many others.

Netfilter also provides an API for registering custom hooks. Iptables uses this registration API to register its function for actions such as ACCEPT, DROP, and LOG.

The api interface looks like:

C
 
//for single hook registration 
int nf_register_net_hook(struct net *net, const struct nf_hook_ops *ops);

//for multiple hooks
int nf_register_net_hooks(struct net *net,
                          const struct nf_hook_ops *ops,
                          unsigned int n);

//ops is pointer to the struct that would define the hook function, priority etc 
// net is the pointed to network namespace


Iptables 

You can think of iptables as the frontend and Netfilter as the backend. Using Netfilters, iptables gives the capability to filter or mangle packets, perform NAT, etc. Instead of writing kernel modules for custom requirements, you could just configure iptables according to your requirements.

Table

Iptables performs multiple functions such as packet filtering, NAT, routing, and packet marking. These processing functions operate on packets in parallel but involve different processing logic, making them conceptually distinct. To support this, iptables uses tables that represent different functions. Below are some examples of different tables in iptables:

table name purpose used for

Filter (default)

Packet Filtering

Allow/deny traffic

nat

Network Address Translation

Port forwarding, source NAT

mangle

Packet modification

Qos, TTL, TOS, packet marking


Chains 

Iptables works based on rules, which are processed sequentially. For example, you might want to :

  • Drop Ping (ICMP) connection from a particular IP
  • Accept SSH connections from an internal subnet 
  • Log SSH connection from any other location
  • Reject Ping connection from a particular source

You could create a flat list of rules, but managing them over time can become painful and prone to errors— for instance, accidentally allowing all IPS when you intended to allow only a specific one outside of your internal network. 

To keep rules sequential and modular, iptables uses chains. A packet passes through each rule in a chain, and rules are applied depending on whether there is a match. So an SSH packer will be checked for each rule. If there is a match, action will be taken; otherwise, the next rule in the chain will be checked.

Some chains are built-in, but you can also create your own custom chains. The following are built-in chains:

  • The INPUT chain handles packets destined for the local machine
  • The OUTPUT chain manages packets generated by the local machine and sent out
  • The FORWARD chain deals with packets being routed through the system (e.g., a router )

Fun fact : you can create your own custom chain and have a built-in chain call it using JUMP (-j) target. We will see how to do that in a little bit.

Enough of the blabbering, let's dive into some demos to see how things actually work. Before we get started, we need to set up our environment by creating two Linux VMs, one acting as the server and the other as the client. Follow along! 

Virtualization 

It's easier to demonstrate the example on virtual machines. You could create multiple VMs on your Mac/Windows using free virtualization software like VMware Fusion or Oracle VirtualBox.

If you are on a Mac with an Apple M-series chip, you'll need to use VMware Fusion, as VirtualBox doesn't support ARM-based Macs.

You could download VMware Fusion from Broadcom using the following link.

Note: You'll need to register, and they require your physical address. Honestly, I found their website experience a bit clunky; it took me quite a while just to get to the actual download link.

The following steps can be followed to create VMs using VMware Fusion:

  1. Open VMware Fusion.
  2. Click on + at the top to create a new VM.
  3. Select "Install from disc or image". Note: You will need to download the image beforehand. You can download Ubuntu from the provided link. 
  4. Click continue or done multiple times.

A rather simpler way to manage VM Lifecycle is to use Vagrant.

Vagrant 

Vagrant is a tool that helps create and manage virtual machines. To some extent, Vagrant is to VMs what Docker is to containers; it simplifies the creation and management of consistent developer environments.

Vagrant uses a configuration file called Vagrantfile, which allows you to define and share VM configurations across teams or systems. This ensures that virtual machines are created consistently across different environments. Vagrant has base images for VM available at the link.

The following steps can be followed to set up VMs using vagrant:

  1. Install Vagrant. On Mac, the command is brew install --cask vagrant.
  2. This link can be referenced for other OS.
  3. Create a Vagrant Project and Initialize, some commands are:
    Shell
     
    mkdir iptables-demo
    cd iptables-demo
    -- vagrant init will create a vagrant file
    Vagrant init 
  4. Modify the vagrant file to look like below:
    Shell
     
    Vagrant.configure("2") do |config|
     config.vm.define "server" do |server|
       server.vm.box = "ubuntu/bionic64"
       server.vm.hostname = "server"
       server.vm.network "private_network", ip: "192.168.212.134"
     end
    
    
     config.vm.define "client" do |client|
       client.vm.box = "ubuntu/bionic64"
       client.vm.hostname = "client"
       client.vm.network "private_network", ip: "192.168.212.135"
     end
    end
  5. You will also need to install the vagrant vmware desktop plugin and the vagrant vmware utility. The plugin can be installed using vagrant plugin install vagrant-vmware-desktop and vmware utility can be downloaded from here.
  6. Now run the command vagrant up --provider=vmware_desktop to create the VMs
    Note: I had to give my terminal access to "input monitoring" on Mac, so I recommend running under elevated privilege to catch and fix any permission issues.

Now, that the VMs are up, you should SSH into them either directly using their IP or using vagrant ssh <VMName>.

The Demo

Hopefully, by now you have two VMs like : 

  • Server with IP address 192.168.212.134
  • Client with IP address 192.168.212.135

Note: IP addresses may differ if you used VMware Fusion UI to create the VMs.

Let’s start by examining what already exists in your iptables configuration and try to understand it. The following command will help you view the current rules:

Shell
 
sudo iptables -L -v -n
-L is for Listing the rules 
-v is for verbose, to show detail like number of packets 
-n show raw ips for source destination instead of using DNS


The output should display the default chains — INPUT, OUTPUT, and FORWARD, along with their associated rules.

Depending on how your VM is set up, you may also see additional chains and rules. For example, in my case, since Docker is installed, there are extra chains and rules related to Docker.

Shell
 
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         
Chain FORWARD (policy DROP 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 DOCKER-USER  0    --  *      *       0.0.0.0/0            0.0.0.0/0           
    0     0 DOCKER-FORWARD  0    --  *      *       0.0.0.0/0            0.0.0.0/0           
Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         
Chain DOCKER (1 references)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 DROP       0    --  !docker0 docker0  0.0.0.0/0            0.0.0.0/0           
Chain DOCKER-BRIDGE (1 references)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 DOCKER     0    --  *      docker0  0.0.0.0/0            0.0.0.0/0           
Chain DOCKER-CT (1 references)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 ACCEPT     0    --  *      docker0  0.0.0.0/0            0.0.0.0/0            ctstate RELATED,ESTABLISHED
Chain DOCKER-FORWARD (1 references)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 DOCKER-CT  0    --  *      *       0.0.0.0/0            0.0.0.0/0           
    0     0 DOCKER-ISOLATION-STAGE-1  0    --  *      *       0.0.0.0/0            0.0.0.0/0           
    0     0 DOCKER-BRIDGE  0    --  *      *       0.0.0.0/0            0.0.0.0/0           
    0     0 ACCEPT     0    --  docker0 *       0.0.0.0/0            0.0.0.0/0           
Chain DOCKER-ISOLATION-STAGE-1 (1 references)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 DOCKER-ISOLATION-STAGE-2  0    --  docker0 !docker0  0.0.0.0/0            0.0.0.0/0           
Chain DOCKER-ISOLATION-STAGE-2 (1 references)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 DROP       0    --  *      docker0  0.0.0.0/0            0.0.0.0/0           
Chain DOCKER-USER (1 references)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 RETURN     0    --  *      *       0.0.0.0/0            0.0.0.0/0


Let's unpack this:

Chain INPUT (policy ACCEPT 0 packets, 0 bytes)

This is the built-in INPUT chain, responsible for handling packets destined for the local system (i.e., the server or the machine you're currently on).

The policy is set to ACCEPT, meaning any packet that doesn’t match a rule in this chain will be accepted by default.

Currently, there are no rules in the INPUT chain, so all incoming packets will be accepted. We’ll modify this behavior shortly.

Here’s a breakdown of the columns you’ll see:

  • pkts – Number of packets that matched this group of rules. Since there are no rules yet, this is currently 0
  • bytes – Total number of bytes from the matched packets
  • target – The action taken if the rule matches (e.g  ACCEPT, DROP, or jump to another chain).
  • prot – The protocol for the rule, example ICMP. 0 means all protocols 
  • opt – IP options. A dash (–) means no special options. An example of a special option could be matching only TCP SYN packets. In most cases, you'll just see – here.
  • in – Input interface (example eth0, * means any )
  • out – output interface (* means any)
  • source – The source IP address or range
  • destination – The destination IP address or range

In my setup, I have a chain created by Docker, for its network to work properly within the defined rules — topic for the other day. 

With this setup, let's also see how ping works from the client, run the following command from the client VM:

Shell
 
ping 192.168.212.134
PING 192.168.212.134 (192.168.212.134) 56(84) bytes of data.
64 bytes from 192.168.212.134: icmp_seq=1 ttl=64 time=1.42 ms
64 bytes from 192.168.212.134: icmp_seq=2 ttl=64 time=0.582 ms
64 bytes from 192.168.212.134: icmp_seq=3 ttl=64 time=0.974 ms
^C
--- 192.168.212.134 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2037ms
rtt min/avg/max/mdev = 0.582/0.992/1.421/0.342 ms


Note that all packets were sent and received just fine. 

Configure a Rule for the DROP Packet

With an understanding of the baseline status of iptables, let's play with it and add some rules.

The first rule we will add is to drop ping (ICMP) packets from our client, remember, previously ping was allowed without any issues. The following command will do that:

Shell
 
sudo iptables -A INPUT -p icmp --icmp-type echo-request -s 192.168.212.135 -j DROP
  
-A is used to append to input chain 
-p is the protocol, ICMP in our case 
–icmp-type echo-request is an extension of ICMP match extension. 
-s is the source ip address, our client vm 
-j is the target action, DROP in our case.


After making the change, you could see the iptables status using sudo iptables -L -v -n

You will see new rule show up in iptables, now lets run ping again and see what happens.

Shell
 
-- on server vm 
  
sudo iptables -L -v -n 
--output 
Chain INPUT (policy ACCEPT 618 packets, 43156 bytes)
 pkts bytes target     prot opt in     out     source               destination         
   14  1176 DROP       1    --  *      *       192.168.212.135      0.0.0.0/0            icmptype 8

-- on client vm 
ping 192.168.212.134
PING 192.168.212.134 (192.168.212.134) 56(84) bytes of data.


You’ll notice that the terminal on the client side appears to hang. The reason is quite interesting: we used the DROP target, which silently discards the packet without sending any response back to the client. As a result, the client continues waiting for a reply and, depending on its configuration, may or may not eventually timeout. If you terminate the terminal session on the client, you’ll see 100% packet loss.

Another interesting observation: If you run tcpdump on the server, you won’t see the dropped packets. So, what’s happening here? Based on what we’ve learned so far, the packets should have been dropped.

In reality, tcpdump operates at the Data Link layer or IP layer, before iptables applies its rules. As long as packets arrive at your network interface card (NIC), tcpdump will capture and display them even if iptables later drops those packets. Tcpdump doesn’t have any visibility into the filtering decisions made by iptables.

It’s fun to see how these different layers interact, right?

Below is my tcpdump result while the client's ping request was hung. 

Shell
 
sudo tcpdump -i ens160  icmp
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on ens160, link-type EN10MB (Ethernet), snapshot length 262144 bytes
22:10:09.944514 IP 192.168.212.135 > rameshvmserver: ICMP echo request, id 3119, seq 596, length 64
22:10:10.968081 IP 192.168.212.135 > rameshvmserver: ICMP echo request, id 3119, seq 597, length 64
22:10:11.993027 IP 192.168.212.135 > rameshvmserver: ICMP echo request, id 3119, seq 598, length 64
22:10:13.016252 IP 192.168.212.135 > rameshvmserver: ICMP echo request, id 3119, seq 599, length 64
22:10:14.040524 IP 192.168.212.135 > rameshvmserver: ICMP echo request, id 3119, seq 600, length 64
22:10:15.065030 IP 192.168.212.135 > rameshvmserver: ICMP echo request, id 3119, seq 601, length 64
22:10:16.088700 IP 192.168.212.135 > rameshvmserver: ICMP echo request, id 3119, seq 602, length 64
22:10:17.113139 IP 192.168.212.135 > rameshvmserver: ICMP echo request, id 3119, seq 603, length 64
22:10:18.137066 IP 192.168.212.135 > rameshvmserver: ICMP echo request, id 3119, seq 604, length 64
22:10:19.161158 IP 192.168.212.135 > rameshvmserver: ICMP echo request, id 3119, seq 605, length 64
22:10:20.184311 IP 192.168.212.135 > rameshvmserver: ICMP echo request, id 3119, seq 606, length 64
22:10:21.208336 IP 192.168.212.135 > rameshvmserver: ICMP echo request, id 3119, seq 607, length 64
22:10:22.232126 IP 192.168.212.135 > rameshvmserver: ICMP echo request, id 3119, seq 608, length 64
13 packets captured
13 packets received by filter
0 packets dropped by kernel


Configure a Rule for the REJECT Packet

The command below will add a rule to reject a ping request from the client:

Shell
 
sudo iptables -A INPUT -p icmp --icmp-type echo-request -s 192.168.212.135 -j REJECT


If you add a REJECT rule without deleting the existing DROP rule, you won’t see packets being rejected. This is because iptables applies rules in order, and the first matching rule’s action is taken.

The commands below will help you delete the rules.

First, let's find out the line numbers for the rules.

Shell
 
sudo iptables -L INPUT --line-numbers
-- my results are 
Chain INPUT (policy ACCEPT)
num  target     prot opt source               destination         
1    DROP       icmp --  192.168.212.135      anywhere             icmp echo-request
2    REJECT     icmp --  192.168.212.135      anywhere             icmp echo-request reject-with icmp-port-unreachable


Delete the DROP rule.

Shell
 
sudo iptables -D INPUT 1


Now, if you see the list again, the drop rule will be gone, and the ping experience on the client side will be:

Shell
 
ping 192.168.212.134
PING 192.168.212.134 (192.168.212.134) 56(84) bytes of data.
From 192.168.212.134 icmp_seq=1 Destination Port Unreachable
From 192.168.212.134 icmp_seq=2 Destination Port Unreachable
From 192.168.212.134 icmp_seq=3 Destination Port Unreachable
From 192.168.212.134 icmp_seq=4 Destination Port Unreachable
From 192.168.212.134 icmp_seq=5 Destination Port Unreachable
From 192.168.212.134 icmp_seq=6 Destination Port Unreachable


Custom Chain  

Let's have more fun. We will create a custom chain to log ping packets from the client and accept them. Before that, though, don't forget to delete existing rules.

Command to create a custom chain:

Shell
 
sudo iptables -N CUSTOM_LOG_AND_DROP_ICMP


Let’s add multiple rules to the chain, one for logging, the other for dropping ICMP packets:

Shell
 
sudo iptables -A CUSTOM_LOG_AND_DROP_ICMP -j LOG --log-prefix "ICMP_DROP: " --log-level 4
sudo iptables -A CUSTOM_LOG_AND_DROP_ICMP -j DROP


A few things to note:

  • log-prefix – This is for convenience, so you could look up (grep) the logs. 
  • log-level – There are various log levels. 4 corresponds to a warning.

The following command will add a rule in the INPUT chain to jump to our custom chain. 

Shell
sudo iptables -A INPUT -p icmp --icmp-type echo-request -s 192.168.212.135 -j CUSTOM_LOG_AND_DROP_ICMP


Now, if we ping again, we will get packet drops, and we will also see the logs. The following commands can be used to see the logs:

Shell
sudo dmesg | grep ICMP_DROP

  -- output 
[22328.664960] ICMP_DROP: IN=ens160 OUT= MAC=00:0c:29:40:3c:6e:00:0c:29:c0:00:27:08:00 SRC=192.168.212.135 DST=192.168.212.134 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=17928 DF PROTO=ICMP TYPE=8 CODE=0 ID=3231 SEQ=1 
[22329.674403] ICMP_DROP: IN=ens160 OUT= MAC=00:0c:29:40:3c:6e:00:0c:29:c0:00:27:08:00 SRC=192.168.212.135 DST=192.168.212.134 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=18496 DF PROTO=ICMP TYPE=8 CODE=0 ID=3231 SEQ=2 
[22330.699224] ICMP_DROP: IN=ens160 OUT= MAC=00:0c:29:40:3c:6e:00:0c:29:c0:00:27:08:00 SRC=192.168.212.135 DST=192.168.212.134 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=18551 DF PROTO=ICMP TYPE=8 CODE=0 ID=3231 SEQ=3 
Shell
sudo tail -f /var/log/kern.log

--output 
2025-09-13T22:53:32.618844+00:00 rameshvmserver kernel: ICMP_DROP: IN=ens160 OUT= MAC=00:0c:29:40:3c:6e:00:0c:29:c0:00:27:08:00 SRC=192.168.212.135 DST=192.168.212.134 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=22971 DF PROTO=ICMP TYPE=8 CODE=0 ID=3231 SEQ=13 
2025-09-13T22:53:33.642762+00:00 rameshvmserver kernel: ICMP_DROP: IN=ens160 OUT= MAC=00:0c:29:40:3c:6e:00:0c:29:c0:00:27:08:00 SRC=192.168.212.135 DST=192.168.212.134 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=23438 DF PROTO=ICMP TYPE=8 CODE=0 ID=3231 SEQ=14 


Conclusion

IPtables is a well-thought-through and designed technology; it is at the foundation of many software-based firewalls and routers , and it is heavily used by cloud platforms and container technologies like Docker. Having said that, no technology is perfect, at this point, iptables is almost considered a legacy, and newer technologies like nftables, eBPF are replacing it. Next time, we will dive into the details for one of those.

Iptables Command (computing) security

Opinions expressed by DZone contributors are their own.

Related

  • Iptables Basic Commands for Novice
  • Building Threat Intelligence Pipelines Using Python, APIs, and Elasticsearch
  • Identity in Action
  • 5 AI Security Incidents That Broke Things in Production (and What They Have in Common)

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook