Firewalld Rich and Direct Rules: Setting up RHEL 7 Server as a Router

We are going to configure RHEL server as a router. Masquerading, port forwarding, rich and direct rules will be covered. 

The Lab

We have three RHEL 7.0 servers available in our lab:

  1. ipa (10.10.1.79, 10.8.8.70) – will be configured as a router,
  2. srv1 (10.8.8.71) – a server on our DMZ network, will be used to test masquerading,
  3. pub (10.10.1.10) – a server on our public network, will be used to test port forwarding.

The ipa server has SELinux set to enforcing mode.

Network Interfaces on the Router Server

The ipa server has two network interfaces attached and configured:

# nmcli d
DEVICE   TYPE      STATE      CONNECTION 
enp0s17  ethernet  connected  enp0s17    
enp0s8   ethernet  connected  enp0s8     
lo       loopback  unmanaged  --

The enp0s8 is connected to our “public” network 10.10.1.0/24, where enp0s17 is connected to DMZ 10.8.8.0/24.

# ip -4 ad
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN 
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
2: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
    inet 10.10.1.79/24 brd 10.10.1.255 scope global dynamic enp0s8
       valid_lft 86376sec preferred_lft 86376sec
3: enp0s17: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
    inet 10.8.8.70/24 brd 10.8.8.255 scope global dynamic enp0s17
       valid_lft 3583sec preferred_lft 3583sec

Routing table can be seen below:

# ip ro
default via 10.10.1.1 dev enp0s8  proto static  metric 1024 
10.8.8.0/24 dev enp0s17  proto kernel  scope link  src 10.8.8.70 
10.10.1.0/24 dev enp0s8  proto kernel  scope link  src 10.10.1.79

Exclude all iptables-based services from ever being started:

# systemctl mask iptables ip6tables ebtables
ln -s '/dev/null' '/etc/systemd/system/iptables.service'
ln -s '/dev/null' '/etc/systemd/system/ip6tables.service'
ln -s '/dev/null' '/etc/systemd/system/ebtables.service'

There is apparently a bug in RHEL 7.1 and RHEL 7.2 that prevents the iptables service from being masked if the package iptables-services is not installed:

[rhel7.1]# systemctl mask iptables
Failed to issue method call: Access denied
[rhel7.1]# cat /var/log/audit/audit.log
type=USER_AVC msg=audit(1468920066.358:447): pid=1 uid=0 auid=4294967295 ses=4294967295 subj=system_u:system_r:init_t:s0 msg='avc:  denied  { disable } for auid=0 uid=0 gid=0 cmdline="systemctl mask iptables ip6tables ebtables" scontext=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 tcontext=system_u:system_r:init_t:s0 tclass=service  exe="/usr/lib/systemd/systemd" sauid=0 hostname=? addr=? terminal=?'

The version of the policy that has a bug is this:

[rhel7.1]# rpm -q selinux-policy-targeted
selinux-policy-targeted-3.13.1-23.el7.noarch
[rhel7.2]# rpm -q selinux-policy-targeted
selinux-policy-targeted-3.13.1-60.el7.noarch

Installing the iptables-services package, or putting SELinux in to permissive mode, allows masking of iptables service on RHEL 7.1 and RHEL 7.2.

Firewalld Default Zone

Both network interfaces should be added to the zone public:

# firewall-cmd --get-active-zones
public
  interfaces: enp0s17 enp0s8

It should be the default zone:

# firewall-cmd --get-default-zone
public

We still want to set the default zone to public (so that we know how to change it), and assign the interface enp0s17 to the zone dmz.

Set the default firewall zone to public, remove the network interface enp0s17 from the zone public and add to the zone dmz.

# firewall-cmd --set-default-zone=public
# firewall-cmd --remove-interface=enp0s17 --zone=public
# firewall-cmd --permanent --add-interface=enp0s17 --zone=dmz

A permanent configuration requires a reload of the firewall configuration to work as expected:

# firewall-cmd --reload

Let us see the actives zones:

# firewall-cmd --get-active-zones
dmz
  interfaces: enp0s17
public
  interfaces: enp0s8

Make sure that zones are correctly set in the network scripts:

# nmcli con mod enp0s8 connection.zone public
# nmcli con mod enp0s17 connection.zone dmz
# nmcli c reload

Custom Firewalld Services

This part shows the way we can create custom firewalld services. We are going to create one for iSCSI target.

Copy one of the existing firewalld service configuration files, say for SSH:

# cp /usr/lib/firewalld/services/ssh.xml /etc/firewalld/services/iscsi-target.xml

Open the file /etc/firewalld/services/iscsi-target.xml for editing, and put the following:

<?xml version="1.0" encoding="utf-8"?>
<service>
 <short>iSCSI Target</short>
 <description>iSCSI target.</description>
 <port protocol="tcp" port="3260"/>
</service>

The newly created firewalld service can be added to the zone dmz this way:

# firewall-cmd --permanent --zone=dmz --add-service iscsi-target

More services depending on requirements can be also added, for example:

# firewall-cmd --permanent --zone=dmz --add-service={http,https,ldap,ldaps,kerberos,dns,kpasswd,ntp,ftp}
# firewall-cmd --reload

List all services that are added to the zone dmz:

# firewall-cmd --list-services --zone=dmz
dns ftp http https iscsi-target kerberos kpasswd ldap ldaps ntp ssh

List all services that are added to the zone public:

# firewall-cmd --list-services --zone=public
dhcpv6-client

Packet Forwarding

To configure routing, the server needs to forward incoming packets from one interface to another interface.

Create a new file /etc/sysctl.d/ip_forward.conf and add the following:

net.ipv4.ip_forward=1

The above makes the change permanent. Now change the runtime value:

# sysctl -w net.ipv4.ip_forward=1

Routing with Rich Rules

In general, all we have to do is to enable masquerading on the public interface. The command below masquerades packets coming from all hosts in the zone public:

# firewall-cmd --permanent --zone=public --add-masquerade
# firewall-cmd --reload

In other words, the above allows all traffic to leave from the zone dmz to the zone public.

The easy way to test this is to try to access some public website from any VM that resides in the dmz network 10.8.8.0/24. We have a server srv1 that’s on 10.8.8.71, and it has the default route going via 10.8.8.70:

[srv1]# ip ro
default via 10.8.8.70 dev mybond0  proto static  metric 1024 
10.8.8.0/24 dev mybond0  proto kernel  scope link  src 10.8.8.71

If we were to try to get access to example.com with masquerading disabled, we would have the following:

[srv1]# wget -O - http://example.com
--2016-06-18 17:47:30--  http://example.com/
Resolving example.com (example.com)... 93.184.216.34
Connecting to example.com (example.com)|93.184.216.34|:80... failed: No route to host.

When masquerading on the zone public is enabled, we can access the site:

[srv1]# wget http://example.com
Resolving example.com (example.com)... 93.184.216.34
Connecting to example.com (example.com)|93.184.216.34|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 1270 (1.2K) [text/html]
Saving to: 'index.html'

100%[================================>] 1,270  --.-K/s   in 0s

2016-06-18 17:50:19 (129 MB/s) - 'index.html' saved [1270/1270]

If we need some more control over which packets exactly are masqueraded, we can add a rich rule that matches packets coming from specific source addresses only, for example:

# firewall-cmd --permanent --zone=public --add-rich-rule='rule family=ipv4 source address=10.8.8.0/24 masquerade'
# firewall-cmd --reload

Let us check the rich rule:

# firewall-cmd --zone=public --list-all
public (default, active)
  interfaces: enp0s8
  sources:
  services: dhcpv6-client
  ports:
  masquerade: yes
  forward-ports:
  icmp-blocks:
  rich rules:
      rule family="ipv4" source address="10.8.8.0/24" masquerade

Routing with Direct Rules

Routing can also be achieved with direct rules. However, the firewalld man page says that direct options should be used only as a last resort when it’s not possible to use for example –add-rich-rule=’rule’.

The syntax is below:

[--permanent] --direct --add-rule { ipv4 | ipv6 | eb } table chain priority args
    Add a rule with the arguments args to chain chain in table table with priority priority.

    The priority is used to order rules. Priority 0 means add rule on top of the chain, with a higher priority
    the rule will be added further down. Rules with the same priority are on the same level and the order of
    these rules is not fixed and may change. If you want to make sure that a rule will be added after another
    one, use a low priority for the first and a higher for the following.

To allow our dmz (enp0s17) network VMs with private IP addresses to communicate with external networks, we have to configure firewall for IP masquerading:

# firewall-cmd --permanent --direct --add-rule ipv4 nat POSTROUTING 0 -o enp0s8 -j MASQUERADE

Forward all ICMP requests from the zone dmz (enp0s17) to the zone public (enp0s8):

# firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 0 -i enp0s17 -o enp0s8 \
  -p icmp -m state --state NEW,RELATED,ESTABLISHED -j ACCEPT

Do the same for all HTTP and HTTPS traffic:

# firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 0 -i enp0s17 -o enp0s8 \
  -p tcp -m multiport --dport 80,443 -m state --state NEW,RELATED,ESTABLISHED -j ACCEPT

Also allow access to public SMTP and SMTPS servers:

# firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 0 -i enp0s17 -o enp0s8 \
  -p tcp -m multiport --dport 25,465 -m state --state NEW,RELATED,ESTABLISHED -j ACCEPT

Allow to SSH into public servers:

# firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 0 -i enp0s17 -o enp0s8 \
  -p tcp --dport 22 -m state --state NEW,RELATED,ESTABLISHED -j ACCEPT

Log everything else:

# firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 0 -i enp0s17 -o enp0s8 \
  -j LOG --log-prefix "forward_fw "

Optional, but not really required:

# firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 0 -i enp0s17 -o enp0s8 -j REJECT

Reload:

# firewall-cmd --reload

Check the direct rules:

# firewall-cmd --direct --get-all-rules
ipv4 nat POSTROUTING 0 -o enp0s8 -j MASQUERADE
ipv4 filter FORWARD 0 -i enp0s17 -o enp0s8 -p icmp -m state --state NEW,RELATED,ESTABLISHED -j ACCEPT
ipv4 filter FORWARD 0 -i enp0s17 -o enp0s8 -p tcp -m multiport --dport 80,443 -m state --state NEW,RELATED,ESTABLISHED -j ACCEPT
ipv4 filter FORWARD 0 -i enp0s17 -o enp0s8 -p tcp -m multiport --dport 25,465 -m state --state NEW,RELATED,ESTABLISHED -j ACCEPT
ipv4 filter FORWARD 0 -i enp0s17 -o enp0s8 -p tcp --dport 22 -m state --state NEW,RELATED,ESTABLISHED -j ACCEPT
ipv4 filter FORWARD 0 -i enp0s17 -o enp0s8 -j LOG --log-prefix 'forward_fw '

We can use the same server srv1 to test the rules above. Anything that doesn’t match should be blocked and logged. For example, let us try to access example.com via FTP:

[srv1]# wget -O - ftp://example.com
--2016-06-18 18:13:49--  ftp://example.com/
           => ‘.listing’
Resolving example.com (example.com)... 93.184.216.34
Connecting to example.com (example.com)|93.184.216.34|:21... failed: No route to host.

And the log entry should be:

forward_fw IN=enp0s17 OUT=enp0s8 MAC=08:00:27:ff:70:00:08:00:27:ff:81:00:08:00 SRC=10.8.8.71 DST=93.184.216.34 LEN=60 TOS=0x00 PREC=0x00 TTL=63 ID=10304 DF PROTO=TCP SPT=34579 DPT=21 WINDOW=14600 RES=0x00 SYN URGP=0

Rich Rules and Port Forwarding

We may want to access servers that reside on the dmz network (10.8.8.0/24) from the public (10.10.1.0/24) over SSH. To do so, we need to put some port forwarding rules.

The rules below configure port forwarding so that connections to 10.10.1.79:2271 are forwarded to 10.8.8.71:22, and SSH logging.

# firewall-cmd --permanent --zone=public --add-forward-port='port=2271:proto=tcp:toport=22:toaddr=10.8.8.71'
# firewall-cmd --permanent --zone=public --add-rich-rule='rule service name=ssh log prefix="SSH_" level="debug" limit value=1/m reject'
# firewall-cmd --reload

Let us see the rules:

# firewall-cmd --list-all --zone=public
public (default, active)
  interfaces: enp0s8
  sources:
  services: dhcpv6-client
  ports:
  masquerade: yes
  forward-ports: port=2271:proto=tcp:toport=22:toaddr=10.8.8.71
  icmp-blocks:
  rich rules:
        rule service name="ssh" log prefix="SSH_" level="debug" limit value="1/m" reject

To test port forwarding, we need some server that’s in the public network (10.10.1.0/24):

[pub]$ ssh [email protected] -p2271
The authenticity of host '[10.10.1.79]:2271 ([10.10.1.79]:2271)' can't be established.
ECDSA key fingerprint is 32:e6:80:ec:ec:3e:d0:6a:7b:78:bf:6e:79:90:8f:2d.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '[10.10.1.79]:2271' (ECDSA) to the list of known hosts.
[email protected]'s password: 
Last login: Sat Jun 18 17:29:59 2016 from 10.8.8.1

2 thoughts on “Firewalld Rich and Direct Rules: Setting up RHEL 7 Server as a Router

  1. Hi Tomas, sorry to bother you again on another topic…
    Port forwarding – from the firewall man page –
    –add-forward-port=port=portid[-portid]:proto=protocol[:toport=portid[-portid]][:toaddr=address[/mask]]
    It seems to me the “toaddr=address/mask” is conflicting with line below in the same man page
    “The port can either be a single port number portid or a port range portid-portid. The protocol can
    either be tcp or udp. The destination address is a simple IP address.”
    :::::
    So for example, is this an invalid destination address “10.10.1.0/24” ?
    Should it be a specific simple IP address?

    • It shouldn’t even allow you to create a port forwarding rule if I use a mask of anything but /32.

      The man page is correct, it is a simple IP address.

Leave a Reply

Your email address will not be published. Required fields are marked *