├── LICENSE ├── Makefile ├── README.md ├── THANKS ├── ipfwGUI.desktop ├── ipfwGUI.py └── screenshots └── screenshot1.jpg /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2020, Lars E 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROGRAM = ipfwGUI.py 2 | DESKTOP_FILE = ${PROGRAM:C/.py//}.desktop 3 | PREFIX ?= /usr/local 4 | BINDIR = ${DESTDIR}${PREFIX}/bin 5 | APPSDIR = ${DESTDIR}${PREFIX}/share/applications 6 | BSD_INSTALL_DATA ?= install -m 0644 7 | BSD_INSTALL_SCRIPT ?= install -m 555 8 | 9 | all: 10 | 11 | install: 12 | ${BSD_INSTALL_SCRIPT} ${PROGRAM} ${BINDIR}/${PROGRAM:C/.py//} 13 | if [ ! -d ${APPSDIR} ]; then mkdir -p ${APPSDIR}; fi 14 | ${BSD_INSTALL_DATA} ${DESKTOP_FILE} ${APPSDIR} 15 | clean: 16 | -rm -rf __pycache__ 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ipfwGUI 2 | #### A very simple GUI for the ipfw packet filter (AKA firewall) for FreeBSD 3 | 4 | With ipfwGUI it is possible to create a "workstation" type [firewall](https://www.freebsd.org/cgi/man.cgi?firewall) and allow incoming traffic to your host. 5 | The ports to be allowed can be only be selected from a list of ports in LISTEN status for now. You need to start the listening daemon before you start ipfwGUI. 6 | ipfwGUI always permits both IPv4 and IPv6 at the same time. 7 | 8 | #### Installation 9 | 10 | ipfwGUI needs Python3 and PyQt6 to run. To install it run: 11 | 12 | ``` 13 | pkg install python3 py311-qt6-pyqt 14 | # optional: 15 | pkg install dsbsu|sudo|doas 16 | ``` 17 | 18 | #### Running ipfwGUI 19 | 20 | If you run ipfwGUI as a user you can only view the current settings. To actually be able to change settings you need to run ipfwGUI with doas or sudo or dsbsu. 21 | 22 | #### Screenshot 23 | 24 | ![screenshot](https://github.com/bsdlme/ipfwGUI/blob/main/screenshots/screenshot1.jpg?raw=true) 25 | -------------------------------------------------------------------------------- /THANKS: -------------------------------------------------------------------------------- 1 | Thanks to 2 | 3 | Marcel Kaiser for testing and make good suggestions 4 | -------------------------------------------------------------------------------- /ipfwGUI.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=ipfwGUI 3 | Comment=Simple configuration GUI for the ipfw firewall 4 | Exec=dsbsu ipfwGUI 5 | Icon=firewall-config 6 | Terminal=false 7 | StartupNotify=false 8 | Type=Application 9 | Categories=Network; 10 | Actions=StartFirewall;StopFirewall 11 | 12 | [Desktop Action StartFirewall] 13 | Name=Start the firewall 14 | Comment=Start the firewall with current rules 15 | Icon=firewall-applet-shields_up 16 | Exec=dsbsu "/usr/sbin/service ipfw onestart" 17 | 18 | [Desktop Action StopFirewall] 19 | Name=Stop the firewall 20 | Comment=Stop the firewall 21 | Icon=firewall-applet-symbolic 22 | Exec=dsbsu "/usr/sbin/service ipfw forcestop" 23 | -------------------------------------------------------------------------------- /ipfwGUI.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (c) 2020 Lars Engels. All rights reserved. 4 | # 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted provided that the following conditions 7 | # are met: 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 16 | # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 17 | # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 18 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 19 | # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 20 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 21 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 23 | # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | import os 26 | import sys 27 | import re 28 | 29 | from subprocess import check_output, CalledProcessError 30 | from PyQt6.QtCore import Qt 31 | from PyQt6.QtWidgets import ( 32 | QApplication, QMainWindow, QMessageBox, QWidget, QGridLayout, 33 | QCheckBox, QLabel, QPushButton, QTableWidget, QTableWidgetItem, 34 | QStatusBar, QStyle 35 | ) 36 | 37 | # Constants for system binaries 38 | SYSRC_BIN = '/usr/sbin/sysrc' 39 | SERVICE_BIN = '/usr/sbin/service' 40 | SOCKSTAT_BIN = '/usr/bin/sockstat' 41 | 42 | 43 | class SimpleIpfwGui(QMainWindow): 44 | def __init__(self): 45 | super().__init__() 46 | self.setWindowTitle("Simple Firewall GUI for FreeBSD") 47 | self.centralWidget = QWidget(self) 48 | self.setCentralWidget(self.centralWidget) 49 | 50 | self.layout = QGridLayout() 51 | self.centralWidget.setLayout(self.layout) 52 | 53 | self.firewallEnabled, self.firewallRunningString, self.firewallRunningBool = self.getFirewallState() 54 | self.allowedPorts = self.getAllowedPorts() 55 | 56 | self.setupWidgets() 57 | self.checkPrivileges() 58 | 59 | def setupWidgets(self): 60 | self.labelTitle = QLabel("Simple Firewall GUI for FreeBSD") 61 | self.labelTitle.setAlignment(Qt.AlignmentFlag.AlignCenter) 62 | 63 | self.checkBoxIpfwEnable = QCheckBox() 64 | self.checkBoxIpfwEnable.setToolTip('Check this to enable the firewall.') 65 | self.checkBoxIpfwEnable.setChecked(self.firewallEnabled.lower() == "yes") 66 | 67 | self.buttonApply = QPushButton("Apply") 68 | self.buttonApply.setFixedWidth(120) 69 | self.buttonApply.clicked.connect(self.applyChanges) 70 | 71 | self.buttonQuit = QPushButton("Quit") 72 | self.buttonQuit.setFixedWidth(120) 73 | self.buttonQuit.clicked.connect(QApplication.instance().quit) 74 | 75 | self.createTable() 76 | 77 | self.statusBar = QStatusBar() 78 | self.statusBar.showMessage(self.firewallRunningString) 79 | self.updateStatusBar() 80 | self.setStatusBar(self.statusBar) 81 | 82 | self.layout.addWidget(self.labelTitle, 0, 1) 83 | self.layout.addWidget(QLabel("Enable Firewall? "), 1, 0) 84 | self.layout.addWidget(self.checkBoxIpfwEnable, 1, 1) 85 | self.layout.addWidget(self.tableWidget, 2, 0, 1, 3) 86 | self.layout.addWidget(self.buttonApply, 5, 1, alignment=Qt.AlignmentFlag.AlignRight) 87 | self.layout.addWidget(self.buttonQuit, 5, 2) 88 | 89 | def createTable(self): 90 | self.tableWidget = QTableWidget() 91 | self.tableContent = self.getListenPorts() 92 | 93 | self.tableWidget.setRowCount(len(self.tableContent)) 94 | self.tableWidget.setColumnCount(4) 95 | self.tableWidget.setHorizontalHeaderLabels(["Process", "Protocol", "Port", "Allow"]) 96 | 97 | for lineNum, line in enumerate(self.tableContent): 98 | proc, proto, port = line 99 | self.tableWidget.setItem(lineNum, 0, QTableWidgetItem(proc)) 100 | self.tableWidget.setItem(lineNum, 1, QTableWidgetItem(proto)) 101 | self.tableWidget.setItem(lineNum, 2, QTableWidgetItem(port)) 102 | 103 | checkbox = QTableWidgetItem() 104 | checkbox.setFlags(checkbox.flags() | Qt.ItemFlag.ItemIsUserCheckable) 105 | checkbox.setCheckState(Qt.CheckState.Checked if f"{port}/{proto.rstrip('46')}" in self.allowedPorts else Qt.CheckState.Unchecked) 106 | self.tableWidget.setItem(lineNum, 3, checkbox) 107 | 108 | def applyChanges(self): 109 | if not self.checkPrivileges(): 110 | return 111 | 112 | self.fwEnable = "YES" if self.checkBoxIpfwEnable.isChecked() else "NO" 113 | self.serviceAction = "start" if self.fwEnable == "YES" else "onestop" 114 | 115 | allowedPortsNew = [ 116 | f"{self.tableWidget.item(i, 2).text()}/{self.tableWidget.item(i, 1).text().rstrip('46')}" 117 | for i in range(self.tableWidget.rowCount()) 118 | if self.tableWidget.item(i, 3).checkState() == Qt.CheckState.Checked 119 | ] 120 | 121 | 122 | allowedPortsNew = sorted(set(allowedPortsNew), key=self.natural_keys) 123 | 124 | try: 125 | check_output([SYSRC_BIN, f'firewall_enable={self.fwEnable}']) 126 | check_output([SYSRC_BIN, 'firewall_type=workstation']) 127 | check_output([SYSRC_BIN, 'firewall_allowservices=any']) 128 | check_output([SYSRC_BIN, f'firewall_myservices={" ".join(allowedPortsNew)}']) 129 | check_output([SERVICE_BIN, 'ipfw', self.serviceAction]) 130 | except CalledProcessError as e: 131 | QMessageBox.critical(self, "Error", f"Failed to apply changes: {e}") 132 | return 133 | 134 | self.firewallEnabled, self.firewallRunningString, self.firewallRunningBool = self.getFirewallState() 135 | self.updateStatusBar() 136 | 137 | def getFirewallState(self): 138 | try: 139 | firewallEnabled = check_output([SYSRC_BIN, '-n', 'firewall_enable']).strip().decode("utf-8") 140 | firewallRunningString = check_output(f"{SERVICE_BIN} ipfw forcestatus; exit 0", shell=True).strip().decode("utf-8") 141 | firewallRunningBool = firewallRunningString == "ipfw is enabled" 142 | firewallRunningString = firewallRunningString.replace('enabled', 'running').replace('ipfw', 'Firewall') 143 | return firewallEnabled, firewallRunningString, firewallRunningBool 144 | except CalledProcessError: 145 | return "NO", "Firewall status unknown", False 146 | 147 | def getAllowedPorts(self): 148 | try: 149 | return check_output([SYSRC_BIN, '-n', 'firewall_myservices']).strip().decode("utf-8").split() 150 | except CalledProcessError: 151 | return [] 152 | 153 | def checkPrivileges(self): 154 | if os.geteuid() != 0: 155 | QMessageBox.information(self, "Not enough privileges", 156 | "You started the firewall management GUI as a regular user and can only read the firewall settings.\n\nIf you want to edit settings, run the firewall management GUI as root.") 157 | return False 158 | return True 159 | 160 | def updateStatusBar(self): 161 | self.statusBar.setStyleSheet("background-color : lightgreen" if self.firewallRunningBool else "background-color : pink") 162 | self.statusBar.showMessage(self.firewallRunningString) 163 | 164 | def getListenPorts(self): 165 | try: 166 | # Get the output from sockstat 167 | sockstat_output = check_output([SOCKSTAT_BIN, '-46s']).decode('utf-8') 168 | 169 | # Process the output 170 | lines = sockstat_output.strip().splitlines() 171 | connections = [] 172 | 173 | for line in lines: 174 | cols = line.split() 175 | if cols[-1] != "LISTEN": 176 | continue 177 | if len(cols) >= 6: 178 | proc = cols[1] # Process name 179 | proto = cols[4] # Protocol 180 | port = cols[5] # Port 181 | port = port.rsplit(':', -1)[-1] 182 | if port == "*": 183 | continue 184 | # Append the cleaned data 185 | connections.append((proc, proto, port)) 186 | 187 | # Sort connections by port 188 | connections.sort(key=lambda x: int(x[2])) # Sort by port 189 | 190 | return connections 191 | 192 | except CalledProcessError as e: 193 | print(f"Error retrieving listening ports: {e}") 194 | return [] 195 | 196 | def natural_keys(self, text): 197 | return [int(c) if c.isdigit() else c for c in re.split('(\d+)', text)] 198 | 199 | def main(): 200 | app = QApplication(sys.argv) 201 | gui = SimpleIpfwGui() 202 | gui.show() 203 | gui.setGeometry(QStyle.alignedRect(Qt.LayoutDirection.LeftToRight, Qt.AlignmentFlag.AlignCenter, gui.size(), app.primaryScreen().geometry())) 204 | sys.exit(app.exec()) 205 | 206 | if __name__ == '__main__': 207 | main() 208 | -------------------------------------------------------------------------------- /screenshots/screenshot1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bsdlme/ipfwGUI/018abc06f3e814824fc8e7341ce3cd56ef1701c3/screenshots/screenshot1.jpg --------------------------------------------------------------------------------