Disclaimer: Running any of the commands here might break your production systems. As is good practice regardless, always test things before using them, and make sure you understand everything that you're using. Especially when it's from a random stranger on the Internet. You have been warned.
The netfilter Linux kernel-space APIs that have existed since the early 2010s are a really powerful way of filtering TCP/IP traffic.
Not only can they manage every packet of traffic, they do so at an incredible pace, almost never being the bottleneck.
iptables is one common userspace program that allows the interaction with and administration of the netfilter module.
The reader may, at this point, be very well familiar with the
In its most basic form,
iptables accepts a few key arguments, and does a few key things.
iptables-land, operations are centered around the following objects: chains, rules, and targets.
We will now explore each of these to a slight level of detail, however the manual page is unsurprisingly quite helpful here.
- Chains are ordered lists of rules that are used to match a set of packets.
- Rules match specific packets, and define what to do with those packets.
- Targets are destinations that can be used by rules.
Chains, rules, and targets can also belong to specific (routing) tables, which are managed by the kernel.
These include the
security tables, but we’ll just look at the
filter table in this post.
Chains can also have a “policy,” which is just what happens to the packet if it reaches the end of the chain without being matched; it must be either
Users can specify their own rules, chains, and targets, and it is by arranging configurations that users of
iptables can create robust filtering setups.
Implementations may vary from kernel to kernel, however, as netfilter compilations may have different flags turned on. You’ll want to consult
man 8 iptables-extensions to make sure you have a given feature.
Let’s take a look at some chains, now! Consider the following input sequence:
iptables -w --append INPUT --source 188.8.131.52/32 --destination-port 22 --jump ACCEPT iptables -w -A INPUT -s 184.108.40.206/32 -j DROP iptables -w -A INPUT -s 220.127.116.11/24 -j DROP iptables -w -A INPUT -s 0.0.0.0/0 -j ACCEPT
This is a list of four rules, all of which modify the
The first rule,
iptables -w --append INPUT --source 18.104.22.168/32 --destination-port 22 --jump ACCEPT
appends a rule to the INPUT chain that matches all packets coming from the source
22.214.171.124/32(on any port) and with the intended destination port
22. (on any destination host) Each packet that matches this will be sent to the
ACCEPTtarget, so it will be passed on to userspace.
In essence, this rule accepts all input from the source
126.96.36.199/32headed to port
The next rule,
iptables -w -A INPUT -s 188.8.131.52/32 -j DROP
displays one of the powerful corollaries to the chaining model—any packets with the destination port of 22 will have been accepted by the previous rule in the chain, but if we don’t want any additional traffic from
184.108.40.206/32, the second rule here will drop the rest of the packets.
Daisy-chaining and logging
If you like the verbiage “chains,” you may be pleased to know that targets for rules need not be the defaults, and you can forward packets between chains! For example,
# Create a chain called "log-then-drop" iptables -w --new-chain LOG_THEN_DROP # Packets on the LOG_THEN_DROP chain should first be logged, then dropped. iptables -w -A LOG_THEN_DROP -j LOG --log-level info iptables -w -A LOG_THEN_DROP -j DROP # Send INPUT packets from 220.127.116.11 to the LOG_THEN_DROP chain iptables -w -A INPUT -s 18.104.22.168/32 -j LOG_THEN_DROP
22.214.171.124 would see:
$ ping server PING server.krye.io (X.X.X.X) 56(84) bytes of data.
(That is, they would not get ping responses.)
server would see these in the logs:
Jun 17 19:37:21 server kernel: IN=eth0 OUT= MAC=[...] SRC=126.96.36.199 DST=X.X.X.X LEN=84 TOS=0x00 PREC=0x00 TTL=63 ID=29283 DF PROTO=ICMP TYPE=8 CODE=0 ID=[...] SEQ=119
LOG target can be quite helpful for debugging, especially if you’re being aggressive such as using a default-
There seems to be a consensus that
DROP is better if you want people not to even know that your server exists; any packets that reach the
DROP target will not provoke a response by the server!
But, sometimes you want to help your users understand what they are supposed to do.
Taking our previous example and making it a bit more friendly,
# Delete the DROP rule from above iptables -w --delete LOG_THEN_DROP -j DROP # Instead, at the end of our LOG_THEN_DROP chain, reject the packet with "host unreachable" iptables -w -A LOG_THEN_DROP -j REJECT --reject-with icmp-host-unreachable
188.8.131.52 instead sees
$ ping server PING server.krye.io (X.X.X.X) 56(84) bytes of data. From server.krye.io (X.X.X.X) icmp_seq=1 Destination Host Unreachable From server.krye.io (X.X.X.X) icmp_seq=2 Destination Host Unreachable From server.krye.io (X.X.X.X) icmp_seq=3 Destination Host Unreachable From server.krye.io (X.X.X.X) icmp_seq=4 Destination Host Unreachable From server.krye.io (X.X.X.X) icmp_seq=5 Destination Host Unreachable From server.krye.io (X.X.X.X) icmp_seq=6 Destination Host Unreachable
It’s important to note that
DROP imitates the behavior of pinging a completely dead host, which doesn’t help your end-users.
If you want people to know that they are being filtered, it might be advised to instead use the
REJECT target with an appropriate ICMP message.
Faster bulk filtering with
If you, like I do, have automated the process of blocking things, eventually you will end up with some very long
Diving into the Linux kernel source for
nft_do_chain, (which is responsible for actually walking netfilter chains) it’s pretty clear that each rule in the chain gets evaluated one-by-one; there’s no magic hashing or anything going on to speed things up.
This can be problematic if you have a lot of rules, especially multiple corresponding to the same network range.
On one of my production servers, I had around 1000 rules blocking different IPs that belonged to AS4134 (CHINANET-BACKBONE), IPs which had been abusively scanning.
(Side note: If you’re curious, you can execute bulk queries against whois.cymru.com if you want to see what an IP is.)
One might be inclined to think, “should I sort my chains somehow?” Or, “can I reduce the work required to block this entire badly-behaving ISP?” Of course you can!
ipset framework also exists within the Linux kernel, and can store sets of individual IPs, networks, port numbers, MAC addresses, and the like.
According to their site,
If you want to
- store multiple IP addresses or port numbers and match against the collection by iptables at one swoop;
- dynamically update iptables rules against IP addresses or ports without performance penalty;
- express complex IP address and ports based rulesets with one single iptables rule and benefit from the speed of IP sets
then ipset may be the proper tool for you.
Sure enough, you can define
ipsets quite easily after installing the CLI utility.
For example, to block AS396507, after obtaining a list of CIDR prefixes, (e.g.
184.108.40.206/24) all you need is:
ipset create AS396507-v4 hash:net
to create your set. (
hash:net is appropriate for matching multiple ranges, but you may want to explore the other options too)
To add your prefixes,
ipset add AS396507-v4 220.127.116.11/24
and then to finally put your rule in the chain,
iptables -A INPUT -m set --match-set AS396507-v4 src -j DROP
Now, any time that rule gets evaluated against traffic, the IP gets hashed and checked in O(1) time.
These hashes are also tiny; as they fill up, they stay within a certain size in memory.
If you want to block both IPv4 and IPv6 with one
ipset, you might consider the
list:set set type, which lets you make a set of sets, and then make an
ipset infers and pins a set to a specific IP version.)
These are evaluated in O(n) time where n is the number of sets in your list, however.
netfilter is the firewall you never knew about, with
iptables being its most simple manager.
There are management tools out there (like
firewalld) that add a layer of abstraction so you don’t have to think in chains, but at the end of the day,
netfilter is one of many powerful and arguably underused features of the Linux kernel.