Updated 29 July 2021
I’m not a fan of Uncomplicated Fire Wall (ufw(8)). One needs to map out the rules needed, then apply those rules one at a time. (My attempt to build a shell script failed, for unknown reasons; I didn’t take the time to find out why.) I have had a parameterized firewall system based on iptables(8) for years. It was time to port the thing over to Ubuntu.
Necessity is the mother of invention, right? One fine morning, I arrived at my desk to find that my access to the Internet was gone. Not everything. Mail was coming in and going out just fine. My web browser couldn't do anything, and when I tried to update software on my various computers, it was no go. My smart phone was affected, too, because I had it configured for Internet calling.
The problem was simple. I was using a Ubiquity Inc. Unify Security Gateway as the bridge between my LAN and the Internet service, AT&T fiber. The poor thing somehow forgot how to route packets in and out. To my horror, my access to the device was gone with the loss of the virtual machine that I had used as the Unify controller. My $DAYJOB is performed via the Internet. The curse of running a proprietary product in a critical function. What to do?
Build a replacement firewall, of course! Using Ubuntu 20.04 LTS server edition, plus the code I had written a long time ago on Red Hat CentOS 4. The code in question implemented a parameterized firewall service, using shell scripts and iptables(8).
The parameterized firewall came into being more than 20 years ago. I was the first commercial customer for DSL service in northern Nevada. When I got the service, I realized very quickly that I needed to protect my LAN from outside attack. I did what most DIY types do: bought books on Linux firewalls. (I still do!) From that, I figured out in outline what I needed.
And like most quick and dirty DevOps jobs, I wrote a shell script. Now, unlike other shell scripts I found on-line during my research phase, I decide to make extensive use of shell functions. Much of the heavy lifting is done with those functions, which meant writing (hopefully) bug-free code meant elimination of duplication, and making for easy unit testing.
The result was a facility that plugged into the rc(8) system initialization system, to rebuild the firewall on every reboot. My internal network was protected because sysctl.conf turned all forwarding off; rc.local ran my script, and the script turned on forwarding when it was safe to do so. Saved time and headaches, without compromising security overmuch. (But it was not the most secure thing in the world, but “good enough for government work.”)
Fast-forward 20 years. The firewall was running on CentOS 4. Bad – that distribution had gone end-of-support years ago. It was also in a computer running a number of services. Bad – a single error meant that a ne’er-do-well could do damage even if he/she/it didn’t explore the LAN. Moving everything to CentOS 8 did not go well, as described elsewhere on this web site.
PORTING THE SCRIPT:
One of the issues facing me was that Ubuntu 20.04 LTS server edition uses systemd(8). The usual old way of interfacing with PID 1 were out the window. I moved the scripts to usr/lib/fw/ so that it was out of /etc and more-or-less like other packages. When I tried to run the script manually, I discovered that iptables(8) had changed command syntax in interesting but simple ways. Bring the script up to date, it ran fine. It worked, too.
So now it was time to explore systemd(8). Long story short, I built a “stop” script and a unit file. Here is the unit file:
[Unit] Before=network.target [Service] Type=oneshot ExecStart=/usr/lib/fw/rc.iptables # ExecStop=/usr/lib/fw/rc.iptables.stop RemainAfterExit=yes [Install] WantedBy=multi-user.target
And that’s it.
PARAMETERIZED FIREWALL, What Is That?
It’s a firewall that accept parameters to determine what to do. Instead of saying “turn on these services” from a GUI, you add them to a series of parameter lists. Here is my current set of parameters:
USE_STATEFUL="YES" USE_NAT="YES" USE_LOG="YES" LOCAL_DNS_SERVER="YES" LOCAL_DHCP_SERVER="YES" INTDEV="enp2s0" INTIP="10.1.1.31" INTBCST="10.1.1.255" INTNET="10.1.1.0/24" EXTDEV="enp1s0" EXTIPS=[redacted] # TRUSTED = Internet addresses/nets with unfettered access to the LAN (dangerous) # FWADMIN = Internet addresses/nets with unfettered access to the firewall # MYNETS = External nets used in RFC1918 # NETMON = Internet addresses/nets with unfettered PING access to the firewall # NAMESERVER = Internet addresses of trusted domain name servers TRUSTED="" FWADMIN="" MYNETS="" NETMON="" NAMESERVER="" # FORWARD = exchanged between networks # INTERNET = Internet-initiated traffic to the firewall (service) # LAN = LAN-initiated traffic to the firewall (service) # EXTERNAL = server-initiated traffic to the Internet # INTERNAL = server-initiated traffic to the LAN # # Use the names contained in /etc/services or the port number. Note that # ftp, ssh, and domain are handled specially. # # NOTE: FORWARD only allows LAN systems to initiate connections. FORWARD # never forwards connection attempts from the Internet to the LAN. # In order for an outsider to connect to a LAN host, it must be a # member of the TRUSTED list, which bypasses all this. # ---originated by LAN host to Internet FORWARD_TCP="ftp ssh snmp telnet smtp smtps submission domain http https nicname pop3 pop3s imap imaps radius" FORWARD_TCP="$FORWARD_TCP 465 3389 8008 webcache 8443 8888 snpp rsync" # xmpp-client FORWARD_TCP="$FORWARD_TCP 5222 5223 8002" # Microsoft Notification Protocol (msnp) [Messenger] FORWARD_TCP="$FORWARD_TCP 1863" # Microsoft PPTP FORWARD_TCP="$FORWARD_TCP 1723" # Timbuktu client, Service Ports 1-4 FORWARD_TCP="$FORWARD_TCP 407 1417:1420" # pyramid.net radios FORWARD_TCP="$FORWARD_TCP 47000:47014 47101:47120 51001:51012" # memoq FORWARD_TCP="$FORWARD_TCP 2705" # INE SSH to *.ine.com FORWARD_TCP="$FORWARD_TCP 6022" # HP signature server FORWARD_TCP="$FORWARD_TCP 11371" # CDDB FORWARD_TCP="$FORWARD_TCP 888 8880" # surge webmail FORWARD_TCP="$FORWARD_TCP 7080" # this is bob's fault #FORWARD_TCP="$FORWARD_TCP 3390" # GIT port FORWARD_TCP="$FORWARD_TCP 9418" # Cary Carl's bootcamp portal FORWARD_TCP="$FORWARD_TCP 8123" # Cary L2TP VPN FORWARD_TCP="$FORWARD_TCP 1701" # ZOOM FORWARD_TCP="$FORWARD_TCP 8801 802" # FORWARD_UDP="domain ntp snmp 407 443 500 1419 1701 1812 4500 snmp 3389 10000 55555 " # ---inbound to firewall LAN_TCP="ssh domain smtp imap imaps submission pop3 http 465 5222 25900 1935 5900 873 3333 2705" LAN_UDP="ntp domain snmp traceroute" LAN_UDP_BCST="" INTERNET_TCP="ssh smtp smtps" INTERNET_UDP="domain ntp" # ---originated by firewall INTERNAL_TCP="ftp telnet pop3 imap ssh 137 138 139" INTERNAL_UDP="tftp domain ntp traceroute 137 138 139" INTERNAL_UDP_BCST="137 138 139" EXTERNAL_TCP="ssh smtp smtps submission domain http https" EXTERNAL_UDP="domain ntp isakmp traceroute" # ---ports to completely ignore (to avoid cluttering the log) COMPLETELY_IGNORE_PORTS="135:139 161:162 445 843 1433 5228 5353 2323 6789 5358 7547 11211" COMPLETELY_IGNORE_NETS=`cut -f1 </usr/lib/fw/fw_killnets` echo kill networks: $COMPLETELY_IGNORE_NETS | sed 's/ /\n/g' | column -c 78
All that happens is that the parameters, as shown, are inserted into the proper places in the iptables(8) command executed by the script. The script functions take the lists and build the commands. Testing the original code was easy, because I could feed single values into the various functions and examine what came out.
The lifetime of this little Q&D project is limited. I’m building version 2, which will target nftables(8) instead of iptables(8), and put a more general framework around the process. The first effort will mimic this version 1 effort; later additions will handle multiple interfaces instead of just two.
Comments, suggestions, and error reports are welcome.
Send them to:
spamfilter (at) satchell (dot)
© 2021 Stephen Satchell, Reno NV