RHCE Series: Use iptables to implement packet filtering and configure network address translation (NAT): Part 1

This section is on using IPTables to create a packet filtering firewall as well as implementing NAT with IPTables. My test environment are two stock installs of CentOS 6.3 in a virtualized environment.

The VM's:

The first thing that I did on server1 was make sure that I had a clean slate to work with. That included making sure that my firewall had a default setting of allowing all traffic, but wasn't forwarding any traffic. I also verified my interfaces and routing table.


[[email protected] ~]# iptables -F
[[email protected] ~]# service iptables save
iptables: Saving firewall rules to /etc/sysconfig/iptables:[ OK ]
[[email protected] ~]# iptables -L
Chain INPUT (policy ACCEPT)
target prot opt source destination

Chain FORWARD (policy ACCEPT)
target prot opt source destination

Chain OUTPUT (policy ACCEPT)
target prot opt source destination
[[email protected] ~]# ip addr show
1: lo: mtu 16436 qdisc noqueue state UNKNOWN
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: eth0: mtu 1500 qdisc pfifo_fast state UP qlen 1000
link/ether 52:54:00:f0:36:20 brd ff:ff:ff:ff:ff:ff
inet 192.168.122.74/24 brd 192.168.122.255 scope global eth0
inet6 fe80::5054:ff:fef0:3620/64 scope link
valid_lft forever preferred_lft forever
3: eth1: mtu 1500 qdisc pfifo_fast state UP qlen 1000
link/ether 52:54:00:b3:38:d1 brd ff:ff:ff:ff:ff:ff
inet 192.168.101.1/24 brd 192.168.101.255 scope global eth1
inet6 fe80::5054:ff:feb3:38d1/64 scope link
valid_lft forever preferred_lft forever
[[email protected] ~]# ip route show
192.168.101.0/24 dev eth1 proto kernel scope link src 192.168.101.1
192.168.122.0/24 dev eth0 proto kernel scope link src 192.168.122.74
169.254.0.0/16 dev eth0 scope link metric 1002
169.254.0.0/16 dev eth1 scope link metric 1003
default via 192.168.122.1 dev eth0

I did the same thing on client1 and client2:


[[email protected] ~]# iptables -F
[[email protected] ~]# service iptables save
iptables: Saving firewall rules to /etc/sysconfig/iptables:[ OK ]
[[email protected] ~]# ip addr show
1: lo: mtu 16436 qdisc noqueue state UNKNOWN
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: eth0: mtu 1500 qdisc pfifo_fast state UP qlen 1000
link/ether 52:54:00:38:28:0a brd ff:ff:ff:ff:ff:ff
inet 192.168.101.101/24 brd 192.168.101.255 scope global eth0
inet6 fe80::5054:ff:fe38:280a/64 scope link
valid_lft forever preferred_lft forever
[[email protected] ~]# ip route show
192.168.101.0/24 dev eth0 proto kernel scope link src 192.168.101.101
169.254.0.0/16 dev eth0 scope link metric 1002
default via 192.168.101.1 dev eth0
[[email protected] ~]# ping -c 1 192.168.101.1 && ping -c 1 4.2.2.2
PING 192.168.101.1 (192.168.101.1) 56(84) bytes of data.
64 bytes from 192.168.101.1: icmp_seq=1 ttl=64 time=0.333 ms

--- 192.168.101.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.333/0.333/0.333/0.000 ms
PING 4.2.2.2 (4.2.2.2) 56(84) bytes of data.

--- 4.2.2.2 ping statistics ---
1 packets transmitted, 0 received, 100% packet loss, time 10000ms

Here is some of the functionality that IPTables provides:


[[email protected] ~]# iptables --help
iptables v1.4.7

Usage: iptables -[AD] chain rule-specification [options]
iptables -I chain [rulenum] rule-specification [options]
iptables -R chain rulenum rule-specification [options]
iptables -D chain rulenum [options]
iptables -[LS] [chain [rulenum]] [options]
iptables -[FZ] [chain] [options]
iptables -[NX] chain
iptables -E old-chain-name new-chain-name
iptables -P chain target [options]
iptables -h (print this help information)

Commands:
Either long or short options are allowed.
--append -A chain Append to chain
--delete -D chain Delete matching rule from chain
--delete -D chain rulenum
Delete rule rulenum (1 = first) from chain
--insert -I chain [rulenum]
Insert in chain as rulenum (default 1=first)
--replace -R chain rulenum
Replace rule rulenum (1 = first) in chain
--list -L [chain [rulenum]]
List the rules in a chain or all chains
--list-rules -S [chain [rulenum]]
Print the rules in a chain or all chains
--flush -F [chain] Delete all rules in chain or all chains
--zero -Z [chain [rulenum]]
Zero counters in chain or all chains
--new -N chain Create a new user-defined chain
--delete-chain
-X [chain] Delete a user-defined chain
--policy -P chain target
Change policy on chain to target
--rename-chain
-E old-chain new-chain
Change chain name, (moving any references)
Options:
[!] --proto -p proto protocol: by number or name, eg. `tcp'
[!] --source -s address[/mask][...]
source specification
[!] --destination -d address[/mask][...]
destination specification
[!] --in-interface -i input name[+]
network interface name ([+] for wildcard)
--jump -j target
target for rule (may load target extension)
--goto -g chain
jump to chain with no return
--match -m match
extended match (may load extension)
--numeric -n numeric output of addresses and ports
[!] --out-interface -o output name[+]
network interface name ([+] for wildcard)
--table -t table table to manipulate (default: `filter')
--verbose -v verbose mode
--line-numbers print line numbers when listing
--exact -x expand numbers (display exact values)
[!] --fragment -f match second or further fragments only
--modprobe= try to insert modules using this command
--set-counters PKTS BYTES set the counter during insert/append
[!] --version -V print package version.

The remaining functionality can be found in 'man iptables'. There is A LOT of functionality! However, the RHCE objectives state that you should be able to create a packet filtering firewall. Note that it doesn't state anything about needing to create a modern stateful packet inspection firewall. It also states that you should be able to create a NAT.

Let's start with a packet filtering firewall. IPtables reads the rules from the top down and will process it's responses based upon the first matching rule.

In this first example. I created two rules. The first one blocks all ICMP traffic from 192.168.101.101 (client1), while the second permits all ICMP traffic from 192.168.101.102 (client2).


[[email protected] ~]# iptables -L
Chain INPUT (policy ACCEPT)
target prot opt source destination

Chain FORWARD (policy ACCEPT)
target prot opt source destination

Chain OUTPUT (policy ACCEPT)
target prot opt source destination
[[email protected] ~]# ping -c 1 192.168.101.101
PING 192.168.101.101 (192.168.101.101) 56(84) bytes of data.
64 bytes from 192.168.101.101: icmp_seq=1 ttl=64 time=0.434 ms

--- 192.168.101.101 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.434/0.434/0.434/0.000 ms
[[email protected] ~]# ping -c 1 192.168.101.102
PING 192.168.101.102 (192.168.101.102) 56(84) bytes of data.
64 bytes from 192.168.101.102: icmp_seq=1 ttl=64 time=0.263 ms

--- 192.168.101.102 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.263/0.263/0.263/0.000 ms
[[email protected] ~]# iptables -I INPUT -s 192.168.101.101/32 -p icmp -j REJECT
[[email protected] ~]# iptables -I INPUT -s 192.168.101.102/32 -p icmp -j ACCEPT
[[email protected] ~]# iptables -L
Chain INPUT (policy ACCEPT)
target prot opt source destination
ACCEPT icmp -- 192.168.101.102 anywhere
REJECT icmp -- 192.168.101.101 anywhere reject-with icmp-port-unreachable

Chain FORWARD (policy ACCEPT)
target prot opt source destination

Chain OUTPUT (policy ACCEPT)
target prot opt source destination
[[email protected] ~]# ping -c 1 192.168.101.101
PING 192.168.101.101 (192.168.101.101) 56(84) bytes of data.

--- 192.168.101.101 ping statistics ---
1 packets transmitted, 0 received, 100% packet loss, time 10000ms

[[email protected] ~]# ping -c 1 192.168.101.102
PING 192.168.101.102 (192.168.101.102) 56(84) bytes of data.
64 bytes from 192.168.101.102: icmp_seq=1 ttl=64 time=0.569 ms

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

As you can see in the example above, I started out with a default, permit anything, rule set. I then verified that I could ping both client1 and client2. Finally, I applied the two rules and attempted to ping the hosts again. The ping request to client1 failed, while was successful on client2.

Now, why did the server fail to ping client1 when we wanted pings from client1 to server1 to fail? The answer is that when server1 sent a ping (echo-request), client1 responded to the ping (echo). Since the rule blocks ALL icmp types. The rule blocked the response (echo) from client1, but allowed the echo-request from server1.

Below are the attempted ping requests from client1 and client2 before and after I applied the firewall rules to block icmp.


###Before###
[[email protected] ~]# ping -c 1 192.168.101.1
PING 192.168.101.1 (192.168.101.1) 56(84) bytes of data.
64 bytes from 192.168.101.1: icmp_seq=1 ttl=64 time=0.320 ms

--- 192.168.101.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.320/0.320/0.320/0.000 ms
[[email protected] ~]# ping -c 1 192.168.101.102
PING 192.168.101.102 (192.168.101.102) 56(84) bytes of data.
64 bytes from 192.168.101.102: icmp_seq=1 ttl=64 time=1.16 ms

--- 192.168.101.102 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 1ms
rtt min/avg/max/mdev = 1.166/1.166/1.166/0.000 ms
###After###
[[email protected] ~]# ping -c 1 192.168.101.1
PING 192.168.101.1 (192.168.101.1) 56(84) bytes of data.
From 192.168.101.1 icmp_seq=1 Destination Port Unreachable

--- 192.168.101.1 ping statistics ---
1 packets transmitted, 0 received, +1 errors, 100% packet loss, time 0ms

[[email protected] ~]# ping -c 1 192.168.101.102
PING 192.168.101.102 (192.168.101.102) 56(84) bytes of data.
64 bytes from 192.168.101.102: icmp_seq=1 ttl=64 time=0.270 ms

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


###Before###
[[email protected] ~]# ping -c 1 192.168.101.1
PING 192.168.101.1 (192.168.101.1) 56(84) bytes of data.
64 bytes from 192.168.101.1: icmp_seq=1 ttl=64 time=0.526 ms

--- 192.168.101.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.526/0.526/0.526/0.000 ms
[[email protected] ~]# ping -c 1 192.168.101.101
PING 192.168.101.101 (192.168.101.101) 56(84) bytes of data.
64 bytes from 192.168.101.101: icmp_seq=1 ttl=64 time=0.236 ms

--- 192.168.101.101 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.236/0.236/0.236/0.000 ms
###After###
[[email protected] ~]# ping -c 1 192.168.101.1
PING 192.168.101.1 (192.168.101.1) 56(84) bytes of data.
64 bytes from 192.168.101.1: icmp_seq=1 ttl=64 time=0.531 ms

--- 192.168.101.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.531/0.531/0.531/0.000 ms
[[email protected] ~]# ping -c 1 192.168.101.101
PING 192.168.101.101 (192.168.101.101) 56(84) bytes of data.
64 bytes from 192.168.101.101: icmp_seq=1 ttl=64 time=0.749 ms

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

Let's break down the IPTables ruleset:


iptables -I INPUT -s 192.168.101.101/32 -p icmp -j REJECT

Directly after the iptables command, you have the '-I' switch followed by the word INPUT. iptables -I INPUT .

The -I option specifies insert and INPUT is the chain name. INPUT is for incoming traffic.

Here are some other options:


  --append  -A chain  Append to chain
--delete -D chain Delete matching rule from chain
--delete -D chain rulenum
Delete rule rulenum (1 = first) from chain
--insert -I chain [rulenum]
Insert in chain as rulenum (default 1=first)
--replace -R chain rulenum
Replace rule rulenum (1 = first) in chain
--list -L [chain [rulenum]]
List the rules in a chain or all chains
--list-rules -S [chain [rulenum]]
Print the rules in a chain or all chains
--flush -F [chain] Delete all rules in chain or all chains
--zero -Z [chain [rulenum]]
Zero counters in chain or all chains
--new -N chain Create a new user-defined chain
--delete-chain
-X [chain] Delete a user-defined chain
--policy -P chain target
Change policy on chain to target
--rename-chain
-E old-chain new-chain
Change chain name, (moving any references)

Be default, in the *filter table there are three chains. Those types are INPUT, FORWARD, and OUTPUT. In the *nat table there are PREROUTING, POSTROUTING, and OUTPUT. You can also create new chains with the -N switch.

The next option was to specify a source with -s 192.168.101.101/32 . With this you can specify entire subnets, individual IP Addresses, hostname, or you can get creative and specify custom chains with groups of addresses. Another common option is the -i option, which lets you specify the incoming interface.

After specifying a source you need to specify a destination. If the -d is left off, all traffic will hit the filter. Next is the protocol with the -p option. In this case, -p icmp . You can use tcp, udp, udplite, icmp, esp, ah, sctp, or all as protocols. With tcp or udp, you will also specify port numbers with the --dport or --sport option, depending on source or destination port. You can specify a single port like --dport 22 , a range of ports like --dport 20:23 , or individual ports like --dport 21,22,25 .

Lastly, you decide what to do with the traffic with the -j option. The most common options are ACCEPT, DROP, and REJECT. In this case I decided to accept the traffic. -j ACCEPT .

As this was a rather long section. I'll split this into two parts. Part two will cover NAT.