Firewall zones (profiles) in Linux, and how to switch them automatically if you use UFW
October 4, 2019 4 Comments
Firstly, a note on terminology: UFW (Uncomplicated Firewall) and its two GUI front-ends Gufw and UFW Frontends use the term ‘application profile’ to refer to a pre-configured set of rules specified in a file. Files containing UFW application profiles are placed in the directory /etc/ufw/applications.d/
. An application profile for SMB, for example, enables the root user to use the UFW command ‘ufw allow Samba
‘ (‘ufw allow CIFS
‘ in Gentoo Linux) rather than having to enter UFW commands specifying the precise ports and network protocols that SMB uses. However, this blog post is not about UFW’s application profiles; it is about what Gufw calls ‘profiles’ and firewalld calls ‘zones’.
In essence a profile/zone is a collection of firewall policies and rules. Both Gufw and firewalld include the concept of a ‘zone’, although Gufw uses the term ‘profile’ rather than ‘zone’. UFW Frontends does not have the concept of a ‘zone’; rules entered via UFW Frontends apply to any network to which you connect your laptop. The ability to define different zones for different networks is handy. For example, you can have certain policies and rules when your laptop is connected to your home network, and different policies and rules when your laptop is connected to the network in a café, hotel, airport or other public place.
An attractive feature of firewalld when used in conjunction with NetworkManager and KDE Plasma is that it is possible to use the desktop environment’s network management module (‘System Settings’ > ‘Connections’) to specify a particular firewalld zone for a particular network connection. For example, let’s say you used firewalld to specify certain policies and rules for a zone you named ‘office’, and you then specified in the System Settings – Connections GUI that a connection named ‘ACM’ should use the zone ‘office’. Thereafter, whenever you connect your laptop to the network named ‘ACM’, firewalld will use the policies and rules you previously configured for the zone ‘office’.
Unlike firewalld, Gufw does not have the ability to switch profiles automatically according to which network the laptop is connected. You have to select manually the profile you wish to use. You would launch Gufw prior to connecting to, for example, your office’s network, select the profile ‘Office’ (or whatever you have named it), then connect your laptop to that network.
I think many people would be satisfied with the functionality currently provided by Gufw. I could use the Gufw GUI to create Gufw profiles with names such as ‘Home’, ‘HomeDave’, ‘Public’, ‘HQoffice’, ‘USoffice’, ‘PestanaRio’ and so on, and specify the different policies and rules I want for each profile. At home I would launch Gufw on my laptop and select the Home profile then connect to my home network; in the office at work I would launch Gufw on my laptop and select the HQoffice profile then connect to the office network; at my friend Dave’s house I would launch Gufw on my laptop and select the HomeDave profile then connect to the house network; and so on. Nevertheless I do see the attraction of automated zone switching, as provided by firewalld in conjunction with NetworkManager and KDE. It would be handy if my laptop could switch automatically to the Home profile when my laptop connected to the network at my home with the name ‘BTHub5-8EUQ’, automatically switch to the HQoffice profile when my laptop connected to the network named ‘HQ-Office2’ in the office, and so on.
I use UFW on my two laptops running Gentoo Linux. The package ufw-frontends is also installed but normally I use UFW directly via the command line. However I wanted to learn about zones/profiles while using UFW, and I also wanted to see if I could automate the switching of zones without resorting to installing firewalld. NetworkManager has the ability to launch ‘hook’ scripts when certain things happen — when a network connection changes, for example — and this seemed to me to be a way of switching profiles automatically.
I had not used Gufw before, so I decided to install it. A package is available in many Linux distributions but there is no ebuild for Gufw in Gentoo’s main Portage tree and I could not find an up-to-date ebuild for it in any Portage overlays. Therefore I created the ebuild for net-firewall/gufw-19.10.0 shown below. It probably needs improving, but it does install a working Gufw in Gentoo Linux.
# Copyright 1999-2019 Gentoo Foundation # Distributed under the terms of the GNU General Public License v2 # $Header: $ EAPI=7 PYTHON_COMPAT=( python3_{5,6,7} ) DISTUTILS_IN_SOURCE_BUILD=1 inherit distutils-r1 MY_PN="gui-ufw" MY_PV="$(ver_cut 1-2)" DESCRIPTION="GUI frontend for managing ufw." HOMEPAGE="https://gufw.org/" SRC_URI="https://launchpad.net/${MY_PN}/trunk/${MY_PV}/+download/${MY_PN}-${PV}.tar.gz" LICENSE="GPL-3" SLOT="0" KEYWORDS="~amd64" IUSE="" DEPEND="dev-python/python-distutils-extra" RDEPEND="net-firewall/ufw dev-python/netifaces dev-python/pygobject:3 net-libs/webkit-gtk[introspection] sys-auth/elogind sys-auth/polkit x11-libs/gtk+:3[introspection] x11-themes/gnome-icon-theme-symbolic " S=${WORKDIR}/${MY_PN}-${PV} pkg_postinst() { sed '/dist-packages/d' -i /usr/bin/gufw-pkexec sed -E '/\/share\//d' -i /usr/bin/gufw-pkexec local PYTHONVERSION="$(python -c 'import sys; print("{}.{}".format(sys.version_info.major, sys.version_info.minor))')" sed -E "s|python3\.[0-9]|python${PYTHONVERSION}|g" -i /usr/bin/gufw-pkexec sed -E 's|\/lib\/|\/lib64\/|g' -i /usr/bin/gufw-pkexec }
‘How To Set Up a Firewall with GUFW on Linux‘ is a good tutorial on Gufw.
As I had not used Gufw previously, I had to play around with it to understand better its functional design. I found that if I configure rules directly via UFW on the command line without using Gufw, Gufw does not allow me to edit those rules (but does allow me to delete them) and those rules exist whichever Gufw profile is selected in the Gufw GUI. Gufw profiles are stored in files named ‘/etc/gufw/*.profile
‘ (e.g. /etc/gufw/Home.profile
) and these files will not include UFW rules entered via the command line. On the other hand, UFW rules created via the Gufw GUI apply solely to the currently-selected Gufw profile, which is what I would have expected. In other words, I can create a different set of policies and rules in each Gufw profile. Therefore I believe Gufw profiles (as distinct from UFW application profiles) are basically analogous to firewalld’s zones. It also appears to me that Gufw maintains configuration files specifying policies and rules independently of UFW, which Gufw then applies to UFW. In other words, if you are a Gufw user you should not use UFW directly to configure policies and rules, otherwise Gufw’s configuration files will not include what you did directly using UFW. To reiterate, use only Gufw or only UFW, not both.
The current Gufw profile’s name is listed in the file /etc/gufw/gufw.cfg
. For example, I currently have the Home profile selected in the Gufw GUI, and the file gufw.cfg
contains the following:
[GufwConfiguration] profile = Home windowwidth = 542 windowheight = 530 confirmdetelerule = yes
If I examine the contents of the file /etc/gufw/Home.profle
I see that it contains the UFW policies and rules I specified for the Gufw Home profile:
[fwBasic] status = enabled incoming = deny outgoing = allow routed = disabled [Rule0] ufw_rule = 137,138/udp ALLOW IN 192.168.1.0/24 description = Samba command = /usr/sbin/ufw allow in proto udp from 192.168.1.0/24 to any port 137,138 policy = allow direction = in protocol = from_ip = 192.168.1.0/24 from_port = to_ip = to_port = 137,138/udp iface = routed = logging = [Rule1] ufw_rule = 139,445/tcp ALLOW IN 192.168.1.0/24 description = Samba command = /usr/sbin/ufw allow in proto tcp from 192.168.1.0/24 to any port 139,445 policy = allow direction = in protocol = from_ip = 192.168.1.0/24 from_port = to_ip = to_port = 139,445/tcp iface = routed = logging =
I also notice that the other Gufw profiles can differ. For example, my Office.profile
file contains the following:
[fwBasic] status = enabled incoming = deny outgoing = allow routed = allow
The profile name listed in gufw.cfg
gets changed when the user changes the profile using the Gufw GUI. It appears to me that only at the point in time when the user selects a certain Gufw profile in the Gufw GUI does Gufw parse the applicable *.profile
file and issue commands to UFW to implement the policies and rules specified in the *.profile
file.
Initially I tried to automate the process of changing the Gufw profile by doing the following:
- I created a NetworkManager Dispatcher hook script to:
- detect when the laptop connects to a network;
- determine whether the network is at my home, at my workplace or in a public place (café, airport or wherever) by looking at the connection name;
- edit
gufw.cfg
to change the name of the Gufw profile according to the network connected.
- I configured KDE to launch Gufw automatically at login, hoping that would implement the Gufw profile specified in
gufw.cfg
.
When I connected the laptop to various networks, Gufw did indeed show the name of the profile selected by the NetworkManager Dispatcher hook script, but the associated Gufw profile’s rules had not been applied. They were only applied if I clicked on the ‘Profile’ pull-down menu in Gufw, selected a different Gufw profile, then re-selected the desired Gufw profile. Therefore driving Gufw from a NetworkManager Dispatcher hook script is not possible. This is a pity, as Gufw is an easy way to manage UFW from a GUI; it allows the user to create, delete and edit zones (Gufw profiles) and to select them manually. What Gufw doesn’t do is enable the user to associate those zones with connection names, nor trigger specific zone automatically based on the selected network connection. firewalld, on the other hand, does enable the user to do both those things.
As my attempt at automating the switching of zones in Gufw had failed, I decided to create a NetworkManager Dispatcher hook script to switch zones automatically by using UFW commands. Initially I though about creating a bespoke UFW application profile for each zone and allowing/denying those in the script, but it is actually easier to use the fundamental UFW commands in the script, especially as UFW commands are relatively easy to understand. Also, this approach means everything is in a single file, which facilitates configuration. I can simply edit the script in order to: a) add or delete a zone; b) change a zone’s name; c) change policies and rules for a zone; d) add or delete a connection; e) change the name of a connection; f) change the zone a connection uses. Granted, editing a script is not as user-friendly as using the firewalld GUI to configure a zone and then using KDE Plasma’s system settings module Connections to specify that zone for a specific connection, but my script is not particularly difficult to understand and edit. And by using such a script I can continue to use UFW rather than installing firewalld and having to learn how to use it.
My NetworkManager Dispatcher hook script /etc/NetworkManager/dispatcher.d/20_ufw-zones
is listed below. In the main body of the script I define the zone I wish to use for each connection, and in the function select_zone
I define the policies and rules I want each zone to use.
#!/bin/bash INTERFACE=$1 STATUS=$2 WIRED=enp4s0f1 WIFI=wlp3s0 CT_helper_rule() { echo "# The following is needed to enable Samba commands to" >> /etc/ufw/before.rules echo "# work properly for broadcast NetBIOS name resolution" >> /etc/ufw/before.rules echo "#" >> /etc/ufw/before.rules echo "# raw table rules" >> /etc/ufw/before.rules echo "*raw" >> /etc/ufw/before.rules echo ":OUTPUT ACCEPT [0:0]" >> /etc/ufw/before.rules echo "-F OUTPUT" >> /etc/ufw/before.rules echo "-A OUTPUT -p udp -m udp --dport 137 -j CT --helper netbios-ns" >> /etc/ufw/before.rules echo "COMMIT" >> /etc/ufw/before.rules } select_zone() { ufw --force reset ufw --force enable ZONE=$1 case "$ZONE" in 'Home') ufw default deny incoming ufw default allow outgoing # # Rules for SMB ufw allow from 192.168.1.0/24 to any port 137,138 proto udp ufw allow from 192.168.1.0/24 to any port 139,445 proto tcp CT_helper_rule # # Rules for KDEConnect ufw allow from 192.168.1.0/24 to any port 1714:1764 proto udp ufw allow from 192.168.1.0/24 to any port 1714:1764 proto tcp ;; 'Office') ufw default deny incoming ufw default allow outgoing ;; 'Public') ufw default reject incoming ufw default allow outgoing ;; 'JohnsHouse') ufw default deny incoming ufw default allow outgoing # # Rules for SMB ufw allow from 192.168.42.0/24 to any port 137,138 proto udp ufw allow from 192.168.42.0/24 to any port 139,445 proto tcp CT_helper_rule # # Rules for KDEConnect ufw allow from 192.168.42.0/24 to any port 1714:1764 proto udp ufw allow from 192.168.42.0/24 to any port 1714:1764 proto tcp ;; esac ufw --force reload rm /etc/ufw/*.rules.20* # Delete backups of *.rules files ufw makes every time it is reset echo -n `date +"[%F %T %Z]"` >> /var/log/ufw-zones.log echo " Zone $ZONE selected for connection $ACTIVE on interface $INTERFACE." >> /var/log/ufw-zones.log } # Check if either the wired or wireless interface is up if [ "$INTERFACE" = "$WIRED" -o "$INTERFACE" = "$WIFI" ] && [ "$STATUS" = "up" ]; then # Check if a single connection is active if [ `nmcli c | grep -v "\-\-" | grep -v "NAME.*UUID.*TYPE.*DEVICE" | wc -l` -eq 1 ]; then # Ascertain the name of the active connection ACTIVE=`nmcli c | grep -v "\-\-" | grep -v "NAME.*UUID.*TYPE.*DEVICE" | awk -F' ' '{print $1}'` case "$ACTIVE" in 'eth0') ZONE="Home" ;; 'POR1-wired') ZONE="Office" ;; 'BTHub5-8EUQ') ZONE="Home" ;; 'BTHub5-8EUQ-5GHz') ZONE="Home" ;; 'John1') ZONE="JohnsHouse" ;; 'GRAND MERCURE') ZONE="Public" ;; *) # If connection name is not in above list ZONE="Public" ;; esac select_zone $ZONE exit $? fi fi
The log file that the script uses contains a chronological record of the connections made and the zones selected:
$ cat /var/log/ufw-zones.log
[2019-09-30 20:13:52 BST] Zone Home selected for connection eth0 on interface enp4s0f1.
[2019-10-01 22:59:18 BST] Zone Home selected for connection BTHub5-8EUQ-5GHz on interface wlp3s0.
[2019-10-02 17:59:23 EDT] Zone Public selected for connection loganwifi on interface wlp3s0.
[2019-10-03 10:12:46 EDT] Zone Office selected for connection POR1-wired on interface enp4s0f1.
Wow nice work! Just wanted to share with you. I based the rules on gufw profiles and it’s faster to add rules to profiles 😉 (sorry for the Interface mess, I only use wifi)
#!/bin/bash
INTERFACE=$1
STATUS=$2
CT_helper_rule() {
echo “# The following is needed to enable Samba commands to” >> /etc/ufw/before.rules
echo “# work properly for broadcast NetBIOS name resolution” >> /etc/ufw/before.rules
echo “#” >> /etc/ufw/before.rules
echo “# raw table rules” >> /etc/ufw/before.rules
echo “*raw” >> /etc/ufw/before.rules
echo “:OUTPUT ACCEPT [0:0]” >> /etc/ufw/before.rules
echo “-F OUTPUT” >> /etc/ufw/before.rules
echo “-A OUTPUT -p udp -m udp –dport 137 -j CT –helper netbios-ns” >> /etc/ufw/before.rules
echo “COMMIT” >> /etc/ufw/before.rules
}
select_zone() {
ZONE=$1
echo -n `date +”[%F %T %Z]”` >> /var/log/ufw-zones.log
echo ” Zone $ZONE selected for connection $ACTIVE on interface $INTERFACE.” >> /var/log/ufw-zones.log
if [ -f “/etc/gufw/$ZONE.profile” ]; then
echo ” /etc/gufw/$ZONE.profile exist.” >> /var/log/ufw-zones.log
ufw –force reset
ufw –force enable
line=`grep -m1 “incoming = ” /etc/gufw/$ZONE.profile`; cmd=”${line/incoming = /}”; echo ” cmd = ‘ufw default $cmd incoming'” >> /var/log/ufw-zones.log; eval “ufw default $cmd incoming”
line=`grep -m1 “outgoing = ” /etc/gufw/$ZONE.profile`; cmd=”${line/outgoing = /}”; echo ” cmd = ‘ufw default $cmd outgoing'” >> /var/log/ufw-zones.log; eval “ufw default $cmd outgoing”
line=`grep -m1 “routed = ” /etc/gufw/$ZONE.profile`; cmd=”${line/routed = /}”; echo ” cmd = ‘ufw default $cmd routed'” >> /var/log/ufw-zones.log; eval “ufw default $cmd routed”
grep “command = ” /etc/gufw/$ZONE.profile | while read -r line ; do
cmd=”${line/command = /}”
echo ” cmd = ‘$cmd'” >> /var/log/ufw-zones.log
eval “$cmd”
done
sed -i “s/\(^profile = \).*/\1$ZONE/” /etc/gufw/gufw.cfg
ufw –force reload
rm /etc/ufw/*.rules.20* # Delete backups of *.rules files ufw makes every time it is reset
fi
}
# Check if either the wired or wireless interface is up
if [ “$STATUS” = “up” ]; then
# Check if a single connection is active
if [ `nmcli c | grep -v “\-\-” | grep -v “NAME.*UUID.*TYPE.*DEVICE” | wc -l` -eq 1 ]; then
# Ascertain the name of the active connection
ACTIVE=`nmcli c | grep -v “\-\-” | grep -v “NAME.*UUID.*TYPE.*DEVICE” | awk -F’ ‘ ‘{print $1}’`
case “$ACTIVE” in
‘HOMESSID’)
ZONE=”Home”
;;
‘WORKSSID’)
ZONE=”Office”
;;
‘WORK2SSID’)
ZONE=”Office”
;;
*)
# If connection name is not in above list
ZONE=”Public”
;;
esac
select_zone $ZONE
exit $?
fi
fi
Pingback: Using WS-Discovery to enable Windows 10 to browse SMB shares in my home network of Linux computers | Fitzcarraldo's Blog
The syntax of the
grep
command has changed since I wrote the above post. Instead ofgrep -v "\-\-"
you now have to usegrep -v "\--"
in the two lines in my original script that include thatgrep
command.Furthermore, I have found that, on my laptops running Gentoo Linux, the
nmcli
commands in my original script now return the connection name ‘lo
‘ in addition to the active connection name, so the two lines also need to be modified so their output excludes the ‘lo
‘ connection name. Additionally, thenmcli
command has options and arguments that can be used to simplify the two lines in the script further. So, replace the two lines in the original script that contain thenmcli
command with the following two lines:# Check if a single connection is active
if [ `nmcli -t -f NAME c show --active | grep -v "^lo$" | wc -l` -eq 1 ]; then
# Ascertain the name of the active connection
ACTIVE=`nmcli -t -f NAME c show --active | awk -F' ' '{print $1}' | grep -v "^lo$"`
Another thing I observed recently is that the variable
$STATUS
does not always contain"up"
once the connection is made, so I modified the line that checks if the interface is up as follows:# Check if either the wired or wireless interface is up
if [ "$INTERFACE" = "$WIRED" -o "$INTERFACE" = "$WIFI" ] && [ "$STATUS" = "up" -o "$STATUS" = "dhcp4-change" -o "$STATUS" = "dhcp6-change" ]; then