Cheat for iptables / nftables / firewalld


Nice Borrowed Diagram.

A bit of theory

Iptables have been in use for a long period of time. This tool has become very familiar and well documented. You can find many recipes for its use to follow, and use without even understanding its mechanism.

Nftables is replacing iptables for reasons beyond the scope of this article.

The firewalld tool is used to create a custom abstract layer, and can use both iptables and nftables as its engine. This tool should bring simplicity of action and no need for deep knowledge of the syntax and parameters of the engine used.

The Nice Borrowed Diagram on the left shows the flow of packets and the exact location of tables and rules along the way. This diagram can help explain the difference between the desired and the result.

For example, I saw someone try to block incoming traffic for a docker container running on the host. It seems obvious that the running application is shown as "local application" in this diagram. So the usual blocking rule in the INPUT table was tried without success.

However, the docker network infrastructure uses its own address space and split its traffic in the PREROUTING phase. This may explain why the INPUT blocks didn't work. The required block had to be implemented on the FORWARD table, which was not so obvious at first glance.

The idea of tables and packet paths remains the same for both engines, so this diagram can be really helpful. The diagram does not needed for firewalld understanding, the firewalld tool tries to isolate the user from understanding the work of engine and operates with the concepts of service and zone.

This cheat sheet will demonstrate how to solve common tasks using all three tools.

Iptables works with tables and chains. The "filter" table has INPUT, FORWARD and OUTPUT chains and is used to filter (ACCEPT/DROP/REJECT) traffic. The "nat" table has PREROUTING, OUTPUT and POSTROUTING chains and is usually used to alternate source or destination in packets. The "mangle" table has all the mentioned chains and is usually used to mark packets according to some rules. Marked packets could be then processed by other rules and even by other tools. Any other chains groupping together rules can be created in iptables. The basic idea for creating custom named chains is to define the source of authority for that particular set of rules.

Nftables can see the same tables and chains that iptables manages. Additional tables and chains can be created. There is a concept of priority, with the help of which the desired rule is selected. Nftables tables and rules cannot be viewed using the iptables tool.

Firewalld operates with services and zones. Predefined zones: drop, block, public, external, dmz, work, home, internal, trusted (sorted from least to most trusted zones). NetworkManager can determine its "connections" to belong to a particular zone, making it easier to cooperate with firewalld. The configuration file /etc/firewalld/firewalld.conf configures the default zone for operation and the backend engine. Firewalld communicates through D-bus and its command line interface called firewall-cmd.

Getting current state info

Check the firewall status with iptables:

# iptables -L -n -v
Chain INPUT (policy ACCEPT 37 packets, 3202 bytes)
 pkts bytes target     prot opt in     out     source               destination         

Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         

Chain OUTPUT (policy ACCEPT 18 packets, 1584 bytes)
 pkts bytes target     prot opt in     out     source               destination

The default table used for this output is "filter" table. You can use option -t tablename to check all tables or use "iptables-save" command instead:

# iptables-save
 ..

As you remember from chapter above, the iptables could not see NFT tables, let's see NFT tables:

# nft list tables
table ip filter
table ip6 filter
table bridge filter
table ip security
table ip raw
table ip mangle
table ip nat
table ip6 security
table ip6 raw
table ip6 mangle
table ip6 nat
table bridge nat
table inet firewalld
table ip firewalld
table ip6 firewalld

This is a list of existing tables, while filter, security, raw, mangle and nat tables were in iptables-save output. The existence of the "firewalld" table may hint that firewalld may be in use and be active. Lets see the content of table itself:

# nft list table inet firewalld
table inet firewalld {
 ..

The very long output does indeed include some rules. Lets now examine a firewalld output:

# firewall-cmd --list-all
public (active)
  target: default
  icmp-block-inversion: no
  interfaces: enp1s0
  sources: 
  services: cockpit dhcpv6-client ssh
  ports: 
  protocols: 
  masquerade: no
  forward-ports: 
  source-ports: 
  icmp-blocks: 
  rich rules:

Following the above, only some ports are opened. We can check this with the nc (netcat) tool. This is the easiest way to open a listening port.

$ nc -l -p 6666

From another Linux, we will try to access our server:

$ echo hello | nc 192.168.120.214 6666
Ncat: No route to host.
$ ping -c1 192.168.120.214
PING 192.168.120.214 (192.168.120.214) 56(84) bytes of data.
64 bytes from 192.168.120.214: icmp_seq=1 ttl=64 time=0.454 ms

--- 192.168.120.214 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.454/0.454/0.454/0.000 ms

This printout is very important to understand. The "No route to host" message has nothing to do with network settings and routes. The next "ping" command demonstrates that there is no problem with an unreachable host. Looking closely at the previous nft list command, you can see:

# nft list table inet firewalld
table inet firewalld {
 ..
	reject with icmpx type admin-prohibited
 ..

The firewall rule rejects access with ICMP message, that cause "No route" error.

Manage firewall rules with firewalld

Recall that the main firewalld configuration file is /etc/firewalld/firewalld.conf. You can check it out to see what backend engine is currently in use. However, this fact does not change the way you use firewalld.

Adding a rule with firewalld is the easiest method. After all, it was designed to simplify this process. Usually you need to open a well-known service port. To get a list of services already known to firewalld, run the command:

# firewall-cmd --get-services
RH-Satellite-6 amanda-client amanda-k5-client amqp amqps apcupsd audit bacula bacula-
 ..

The very long and impressive list. Lets enable an http port:

# firewall-cmd --add-service=http
success
# firewall-cmd --list-all
public (active)
  target: default
  icmp-block-inversion: no
  interfaces: enp1s0
  sources: 
  services: cockpit dhcpv6-client http ssh
  ports: 
  protocols: 
  masquerade: no
  forward-ports: 
  source-ports: 
  icmp-blocks: 
  rich rules:

It is important to say that changes made to the runtime configuration will not persist across service restarts. You can use the --permanent option with the --add-service command, then the service will only be added to the permanent configuration instead of runtime. There is another option, --runtime-to-permanent, which allows changes checked in the runtime configuration to be promoted to the permanent configuration. The permanent configuration file is /etc/firewalld/zones/public.xml where the filename matches the zone name. There is no problem with direct editing of this file if you follow the XML syntax. Firewalld should be "reloaded" to re-read the permanent configuration:

# firewall-cmd --add-service=http --permanent 
success
# firewall-cmd --reload 
success

Lets add our unknown port 6666, that we had tested above with netcat.

# firewall-cmd --add-port=6666/tcp
success
# firewall-cmd --list-all
public (active)
  target: default
  icmp-block-inversion: no
  interfaces: enp1s0
  sources: 
  services: cockpit dhcpv6-client http ssh
  ports: 6666/tcp
  protocols: 
  masquerade: no
  forward-ports: 
  source-ports: 
  icmp-blocks: 
  rich rules: 

Repeating same test with netcat above, you can verify that port is open now.

NAT with firewalld

Another popular task is to configure masquerading or NAT. First you must define one of the network interfaces be the internal zone, then:

# firewall-cmd --zone=internal --add-masquerade

A port forwarding also possible to do in easy way:

# firewall-cmd --zone=public --add-forward-port=port=80:proto=tcp:toport=80:toaddr=192.168.1.10

Removing firewalld rule

You must find out in what configuration, runtime or permanent, the rule you want to remove is located. To compare with the permanent configuration, you must check the status with the command:

# firewall-cmd --list-all
 ..
# firewall-cmd --list-all --permanent
 ..

If you want to remove a rule that was added in the runtime configuration but not in the permanent one, you can simply fall back to the default settings:

# firewall-cmd --reload 
success
# firewall-cmd --list-all
 ..

If you want to remove a specific service or port from a permanent configuration, follow these steps:

# firewall-cmd --remove-service=http --permanent
success
# firewall-cmd --remove-port=6666/tcp --permanent
success
# firewall-cmd --reload 
success

Adding rule using iptables

Firewalld must be disabled regardless of the engine it uses, otherwise it will conflict with your rules and under certain conditions reset them.

# systemctl disable --now firewalld

To start from empty, scratch ruleset, all existing rulesets should be flushed from any tables.

# iptables -F # table "filter" is a default table
# iptables -t nat -F
# iptables -t mangle -F

The next step is to configure the default policy because it affects the rules themselves. Usually (and these are the default settings) any policy sat to ACCEPT, so the last rule in the chain should be DROP or REJECT. A more paranoid default DROP policy setting, such as:

# iptables --policy INPUT DROP

If you issue this command through an open SSH session, you will immediately lose your network because you will not be able to add other rules. You should close doors only after you have left the required ports open. The most convenient way to set up iptables rules is to save the current settings with the iptables-save command, then edit them and reload.

Lets add input HTTP port by command, not by configuration file. The -A option add rule to the end of already defined rules list. This is the only option used in configuration files. We also need to left SSH open, lets add it too. The next command will close rest of incoming ports.

# iptables -A INPUT -p tcp --dport 80 -j ACCEPT
# iptables -A INPUT -p tcp --dport 22 -j ACCEPT
# iptables -A INPUT -j REJECT --reject-with icmp-host-prohibited
# iptables -L -n -v
Chain INPUT (policy ACCEPT 628 packets, 182K bytes)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 ACCEPT     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:80
   44  3044 ACCEPT     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:22
    3  1294 REJECT     all  --  *      *       0.0.0.0/0            0.0.0.0/0            reject-with icmp-host-prohibited

Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         

Chain OUTPUT (policy ACCEPT 257 packets, 40181 bytes)
 pkts bytes target     prot opt in     out     source               destination         
# nc -l -p 6666

The last command starts netcat to listen on port 6666, which is not allowed here. When attempting to get this port from the outside, the same "no route" error occurs due to the --reject-with icmp-host-prohibited option. Let's add a rule for this port 6666 to be open. This rule should be inserted just above the last closing rule, which is number 3.

# iptables -I INPUT 3 -p tcp --dport 6666 -j ACCEPT
# iptables -L -n -v
Chain INPUT (policy ACCEPT 628 packets, 182K bytes)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 ACCEPT     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:80
  292 21908 ACCEPT     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:22
    0     0 ACCEPT     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:6666
   97 93716 REJECT     all  --  *      *       0.0.0.0/0            0.0.0.0/0            reject-with icmp-host-prohibited

Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         

Chain OUTPUT (policy ACCEPT 539 packets, 101K bytes)
 pkts bytes target     prot opt in     out     source

The -I option allows you to insert the rule at the right place in the chain. If the number is not specified, it is inserted as the first rule. This option is only useful for manual configuration via the CLI. Check the port becomes open with netcat test.

Delete rule

# iptables -D INPUT 3
# iptables -L -n -v
Chain INPUT (policy ACCEPT 628 packets, 182K bytes)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 ACCEPT     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:80
  324 24232 ACCEPT     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:22
  137  114K REJECT     all  --  *      *       0.0.0.0/0            0.0.0.0/0            reject-with icmp-host-prohibited

Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         

Chain OUTPUT (policy ACCEPT 590 packets, 114K bytes)
 pkts bytes target     prot opt in     out     source               destination

The option -D also usefull only in interactive way. There is also -R "replace" option to update choosen rule.

NAT

Set quick NAT:

# iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE

Set transparent proxy

# iptables -A PREROUTING -s 192.168.200.0/255.255.255.0 ! -d 192.168.200.50 -p tcp -m tcp --dport 80 -j DNAT --to-destination 192.168.200.50:3128

Firewall rules with nft

As already mentioned, nft can see and manipulate tables managed by iptables.

# nft -a list table ip filter
table ip filter { # handle 1
        chain INPUT { # handle 1
                type filter hook input priority filter; policy accept;
                meta l4proto tcp tcp dport 80 counter packets 0 bytes 0 accept # handle 5
                meta l4proto tcp tcp dport 22 counter packets 541 bytes 39780 accept # handle 6
                counter packets 187 bytes 139878 reject with icmp type host-prohibited # handle 7
        }

        chain FORWARD { # handle 2
                type filter hook forward priority filter; policy accept;
        }

        chain OUTPUT { # handle 3
                type filter hook output priority filter; policy accept;
        }
}

The output is slightly different, but you see the same information that ports 80 and 22 are open to the world and the rest are closed. The -a option marks the lines with # handle X that will help us insert the rules. To add our port 6666:

# nft insert rule ip filter INPUT position 7 tcp dport 6666 accept
# nft -a list table ip filter
table ip filter { # handle 1
        chain INPUT { # handle 1
                type filter hook input priority filter; policy accept;
                meta l4proto tcp tcp dport 80 counter packets 0 bytes 0 accept # handle 5
                meta l4proto tcp tcp dport 22 counter packets 862 bytes 62848 accept # handle 6
                tcp dport 6666 accept # handle 9
                counter packets 209 bytes 152354 reject with icmp type host-prohibited # handle 7
        }

        chain FORWARD { # handle 2
                type filter hook forward priority filter; policy accept;
        }

        chain OUTPUT { # handle 3
                type filter hook output priority filter; policy accept;
        }
}
# iptables -L -n -v
Chain INPUT (policy ACCEPT 628 packets, 182K bytes)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 ACCEPT     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:80
  739 54136 ACCEPT     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:22
    0     0 ACCEPT     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0           
  206  151K REJECT     all  --  *      *       0.0.0.0/0            0.0.0.0/0            reject-with icmp-host-prohibited

Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         

Chain OUTPUT (policy ACCEPT 854 packets, 149K bytes)
 pkts bytes target     prot opt in     out     source               destination    

Interesting. While nft can inspect iptables rules, the opposite is not true. Testing with netcat shows that port 6666 is working correctly, despite strange iptables output.

Delete rule

Lets delete our new rule at "handle 9" position:

# nft delete rule filter INPUT handle 9
# nft -a list table ip filter
table ip filter { # handle 1
        chain INPUT { # handle 1
                type filter hook input priority filter; policy accept;
                meta l4proto tcp tcp dport 80 counter packets 0 bytes 0 accept # handle 5
                meta l4proto tcp tcp dport 22 counter packets 931 bytes 67616 accept # handle 6
                counter packets 231 bytes 164503 reject with icmp type host-prohibited # handle 7
        }

        chain FORWARD { # handle 2
                type filter hook forward priority filter; policy accept;
        }

        chain OUTPUT { # handle 3
                type filter hook output priority filter; policy accept;
        }
}

Conclusion

Looks like it's time to start avoiding iptables. However, the nftables syntax looks richer and more complex. It seems that using the firewalld layer for trivial tasks is preferable.


Updated on Sat Jun 4 16:30:08 IDT 2022 More documentations here