diff --git a/qubesmanager/firewall.py b/qubesmanager/firewall.py index d482b3f..fa61186 100644 --- a/qubesmanager/firewall.py +++ b/qubesmanager/firewall.py @@ -143,17 +143,6 @@ class QubesFirewallRulesModel(QAbstractItemModel): def __init__(self, parent=None): QAbstractItemModel.__init__(self, parent) - self.__columnValues = { - 0: lambda x: "*" if self.children[x]["address"] == "0.0.0.0" and - self.children[x]["netmask"] == 0 else - self.children[x]["address"] + ("" if self.children[x][ "netmask"] == 32 else - " /{0}".format(self.children[x][ - "netmask"])), - 1: lambda x: "any" if self.children[x]["portBegin"] == 0 else - "{0}-{1}".format(self.children[x]["portBegin"], self.children[x][ - "portEnd"]) if self.children[x]["portEnd"] is not None else \ - self.get_service_name(self.children[x]["portBegin"]), - 2: lambda x: self.children[x]["proto"], } self.__columnNames = {0: "Address", 1: "Service", 2: "Protocol", } self.__services = list() pattern = re.compile("(?P[a-z][a-z0-9-]+)\s+(?P[0-9]+)/(?P[a-z]+)", re.IGNORECASE) @@ -171,23 +160,17 @@ class QubesFirewallRulesModel(QAbstractItemModel): from operator import attrgetter rev = (order == Qt.AscendingOrder) - if idx==0: - self.children.sort(key=lambda x: x['address'], reverse = rev) - if idx==1: - self.children.sort(key=lambda x: self.get_service_name(x[ - "portBegin"]) if x["portEnd"] == None else x["portBegin"], - reverse = rev) - if idx==2: - self.children.sort(key=lambda x: x['proto'], reverse - = rev) + self.children.sort(key = lambda x: self.get_column_string(idx, x) + , reverse = rev) + index1 = self.createIndex(0, 0) - index2 = self.createIndex(len(self)-1, len(self.__columnValues)-1) + index2 = self.createIndex(len(self)-1, len(self.__columnNames)-1) self.dataChanged.emit(index1, index2) def get_service_name(self, port): for service in self.__services: - if service[1] == port: + if str(service[1]) == str(port): return service[0] return str(port) @@ -197,129 +180,122 @@ class QubesFirewallRulesModel(QAbstractItemModel): return service[1] return None - def get_column_string(self, col, row): - return self.__columnValues[col](row) + def get_column_string(self, col, rule): + # Address + if col == 0: + if rule.dsthost is None: + return "*" + else: + if rule.dsthost.type == 'dst4'\ + and rule.dsthost.prefixlen == '32': + return str(rule.dsthost)[:-3] + elif rule.dsthost.type == 'dst6'\ + and rule.dsthost.prefixlen == '128': + return str(rule.dsthost)[:-4] + else: + return str(rule.dsthost) - - def rule_to_dict(self, rule): - if rule.dsthost is None: - raise FirewallModifiedOutsideError('no dsthost') - - d = {} - - if not rule.proto: - d['proto'] = 'any' - d['portBegin'] = 'any' - d['portEnd'] = None - - else: - d['proto'] = rule.proto + # Service + if col == 1: if rule.dstports is None: - raise FirewallModifiedOutsideError('no dstport') - d['portBegin'] = rule.dstports.range[0] - d['portEnd'] = rule.dstports.range[1] \ - if rule.dstports.range[0] != rule.dstports.range[1] \ - else None + return "any" + elif rule.dstports.range[0] != rule.dstports.range[1]: + return str(rule.dstports) + else: + return self.get_service_name(rule.dstports) - if rule.dsthost.type == 'dsthost': - d['address'] = str(rule.dsthost) - d['netmask'] = 32 - elif rule.dsthost.type == 'dst4': - network = ipaddress.IPv4Network(rule.dsthost) - d['address'] = str(network.network_address) - d['netmask'] = int(network.prefixlen) - else: - raise FirewallModifiedOutsideError( - 'cannot map dsthost.type={!s}'.format(rule.dsthost)) - - if rule.expire is not None: - d['expire'] = int(rule.expire) - - return d + # Protocol + if col == 2: + if rule.proto is None: + return "any" + else: + return str(rule.proto) + return "unknown" def get_firewall_conf(self, vm): conf = { 'allow': None, - 'allowDns': False, - 'allowIcmp': False, - 'allowYumProxy': False, + 'expire': 0, 'rules': [], } + allowDns = False + allowIcmp = False common_action = None - tentative_action = None reversed_rules = list(reversed(vm.firewall.rules)) + last_rule = reversed_rules.pop(0) + if last_rule == qubesadmin.firewall.Rule('action=accept') \ + or last_rule == qubesadmin.firewall.Rule('action=drop'): + common_action = last_rule.action + else: + FirewallModifiedOutsideError('Last rule must be either ' + 'drop all or accept all.') + + dns_rule = qubesadmin.firewall.Rule(None, + action='accept', specialtarget='dns') + icmp_rule = qubesadmin.firewall.Rule(None, + action='accept', proto='icmp') while reversed_rules: - rule = reversed_rules[0] - if rule.dsthost is not None or rule.proto is not None: - break - tentative_action = reversed_rules.pop(0).action + rule = reversed_rules.pop(0) - if not reversed_rules: - conf['allow'] = tentative_action == 'accept' - return conf - - for rule in reversed_rules: - if rule.specialtarget == 'dns': - conf['allowDns'] = (rule.action == 'accept') + if rule == dns_rule: + allowDns = True continue - if rule.proto == 'icmp': - if rule.icmptype is not None: - raise FirewallModifiedOutsideError( - 'cannot map icmptype != None') - conf['allowIcmp'] = (rule.action == 'accept') + if rule.proto == icmp_rule: + allowIcmp = True continue - if common_action is None: - common_action = rule.action - elif common_action != rule.action: - raise FirewallModifiedOutsideError('incoherent action') + if rule.specialtarget is not None or rule.icmptype is not None: + raise FirewallModifiedOutsideError("Rule type unknown!") - conf['rules'].insert(0, self.rule_to_dict(rule)) + if (rule.dsthost is not None or rule.proto is not None) \ + and rule.expire is None: + if rule.action == 'accept': + conf['rules'].insert(0, rule) + continue + else: + raise FirewallModifiedOutsideError('No blacklist support.') - if common_action is None or common_action != tentative_action: - # we've got only specialtarget and/or icmp - conf['allow'] = tentative_action == 'accept' - return conf + if rule.expire is not None and rule.dsthost is None \ + and rule.proto is None: + conf['expire'] = int(str(rule.expire)) + continue - raise FirewallModifiedOutsideError('it does not add up') + raise FirewallModifiedOutsideError('it does not add up.') + + conf['allow'] = (common_action == 'accept') + + if not allowIcmp and not conf['allow']: + raise FirewallModifiedOutsideError('ICMP must be allowed.') + + if not allowDns and not conf['allow']: + raise FirewallModifiedOutsideError('DNS must be allowed') + + return conf def write_firewall_conf(self, vm, conf): - common_action = qubesadmin.firewall.Action( - 'drop' if conf['allow'] else 'accept') - rules = [] for rule in conf['rules']: - kwargs = {} - if rule['proto'] != 'any': - kwargs['proto'] = rule['proto'] - if rule['portBegin'] != 'any': - kwargs['dstports'] = '-'.join(map(str, filter((lambda x: x), - (rule['portBegin'], rule['portEnd'])))) + rules.append(rule) - netmask = str(rule['netmask']) if rule['netmask'] != 32 else None - - rules.append(qubesadmin.firewall.Rule(None, - action=common_action, - dsthost='/'.join(map(str, filter((lambda x: x), - (rule['address'], netmask)))), - **kwargs)) - - if conf['allowDns']: + if not conf['allow']: rules.append(qubesadmin.firewall.Rule(None, action='accept', specialtarget='dns')) - if conf['allowIcmp']: + if not conf['allow']: rules.append(qubesadmin.firewall.Rule(None, action='accept', proto='icmp')) - if common_action == 'drop': + if conf['allow']: rules.append(qubesadmin.firewall.Rule(None, action='accept')) + else: + rules.append(qubesadmin.firewall.Rule(None, + action = 'drop')) vm.firewall.rules = rules @@ -331,58 +307,98 @@ class QubesFirewallRulesModel(QAbstractItemModel): conf = self.get_firewall_conf(vm) self.allow = conf["allow"] - self.allowDns = conf["allowDns"] - self.allowIcmp = conf["allowIcmp"] - self.allowYumProxy = conf["allowYumProxy"] - self.tempFullAccessExpireTime = 0 + + self.tempFullAccessExpireTime = conf['expire'] for rule in conf["rules"]: self.appendChild(rule) - if "expire" in rule and rule["address"] == "0.0.0.0": - self.tempFullAccessExpireTime = rule["expire"] def get_vm_name(self): return self.__vm.name - def apply_rules(self, allow, dns, icmp, yumproxy, tempFullAccess=False, + def apply_rules(self, allow, tempFullAccess=False, tempFullAccessTime=None): assert self.__vm is not None - if self.allow != allow or self.allowDns != dns or \ - self.allowIcmp != icmp or self.allowYumProxy != yumproxy or \ + if self.allow != allow or \ (self.tempFullAccessExpireTime != 0) != tempFullAccess: self.fw_changed = True conf = { "allow": allow, - "allowDns": dns, - "allowIcmp": icmp, - "allowYumProxy": yumproxy, "rules": list() } - for rule in self.children: - if "expire" in rule and rule["address"] == "0.0.0.0" and \ - rule["netmask"] == 0 and rule["proto"] == "any": - # rule already present, update its time - if tempFullAccess: - rule["expire"] = \ - int(datetime.datetime.now().strftime("%s")) + \ - tempFullAccessTime*60 - tempFullAccess = False - conf["rules"].append(rule) + conf['rules'].extend(self.children) if tempFullAccess and not allow: - conf["rules"].append({"address": "0.0.0.0", - "netmask": 0, - "proto": "any", - "expire": int( - datetime.datetime.now().strftime("%s"))+\ - tempFullAccessTime*60 - }) + conf["rules"].append(qubesadmin.firewall.Rule(None,action='accept' + , expire=int(datetime.datetime.now().strftime("%s"))+\ + tempFullAccessTime*60)) if self.fw_changed: self.write_firewall_conf(self.__vm, conf) + def populate_edit_dialog(self, dialog, row): + address = self.get_column_string(0, self.children[row]) + dialog.addressComboBox.setItemText(0, address) + dialog.addressComboBox.setCurrentIndex(0) + service = self.get_column_string(1, self.children[row]) + if service == "any": + service = "" + dialog.serviceComboBox.setItemText(0, service) + dialog.serviceComboBox.setCurrentIndex(0) + protocol = self.get_column_string(2, self.children[row]) + if protocol == "tcp": + dialog.tcp_radio.setChecked(True) + elif protocol == "udp": + dialog.udp_radio.setChecked(True) + else: + dialog.any_radio.setChecked(True) + + def run_rule_dialog(self, dialog, row = None): + if dialog.exec_(): + + address = str(dialog.addressComboBox.currentText()) + service = str(dialog.serviceComboBox.currentText()) + + rule = qubesadmin.firewall.Rule(None,action='accept') + + if address is not None and address != "*": + try: + rule.dsthost = address + except ValueError: + QMessageBox.warning(None, self.tr("Invalid address"), + self.tr("Address '{0}' is invalid.").format(address)) + + if dialog.tcp_radio.isChecked(): + rule.proto = 'tcp' + elif dialog.udp_radio.isChecked(): + rule.proto = 'udp' + + if '-' in service: + try: + rule.dstports = service + except ValueError: + QMessageBox.warning(None, self.tr("Invalid port or service"), + self.tr("Port number or service '{0}' is invalid.") + .format(service)) + elif service is not None: + try: + rule.dstports = service + except (TypeError, ValueError) as ex: + if self.get_service_port(service) is not None: + rule.dstports = self.get_service_port(service) + else: + QMessageBox.warning(None, + self.tr("Invalid port or service"), + self.tr("Port number or service '{0}' is invalid.") + .format(service)) + + if row is not None: + self.setChild(row, rule) + else: + self.appendChild(rule) + def index(self, row, column, parent=QModelIndex()): if not self.hasIndex(row, column, parent): return QModelIndex() @@ -396,7 +412,7 @@ class QubesFirewallRulesModel(QAbstractItemModel): return len(self) def columnCount(self, parent=QModelIndex()): - return len(self.__columnValues) + return len(self.__columnNames) def hasChildren(self, index=QModelIndex()): parentItem = index.internalPointer() @@ -407,7 +423,8 @@ class QubesFirewallRulesModel(QAbstractItemModel): def data(self, index, role=Qt.DisplayRole): if index.isValid() and role == Qt.DisplayRole: - return self.__columnValues[index.column()](index.row()) + return self.get_column_string(index.column() + ,self.children[index.row()]) def headerData(self, section, orientation, role=Qt.DisplayRole): if section < len(self.__columnNames) \ diff --git a/qubesmanager/settings.py b/qubesmanager/settings.py index 79d9a18..a36ba89 100755 --- a/qubesmanager/settings.py +++ b/qubesmanager/settings.py @@ -80,7 +80,8 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog): self.tabWidget.currentChanged.connect(self.current_tab_changed) -# self.tabWidget.setTabEnabled(self.tabs_indices["firewall"], vm.is_networked() and not vm.provides_network) + self.tabWidget.setTabEnabled(self.tabs_indices["firewall"], + vm.netvm is not None and not vm.provides_network) ###### basic tab self.__init_basic_tab__() @@ -96,8 +97,12 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog): ###### firewall tab if self.tabWidget.isTabEnabled(self.tabs_indices['firewall']): model = QubesFirewallRulesModel() - model.set_vm(vm) - self.set_fw_model(model) + try: + model.set_vm(vm) + self.set_fw_model(model) + self.firewallModifiedOutsidelabel.setVisible(False) + except FirewallModifiedOutsideError as ex: + self.disable_all_fw_conf() self.newRuleButton.clicked.connect(self.new_rule_button_pressed) self.editRuleButton.clicked.connect(self.edit_rule_button_pressed) @@ -175,11 +180,8 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog): ret.append(self.tr('Error while saving changes: ') + str(ex)) try: - if self.tabWidget.isTabEnabled(self.tabs_indices["firewall"]): + if self.policyAllowRadioButton.isEnabled(): self.fw_model.apply_rules(self.policyAllowRadioButton.isChecked(), - self.dnsCheckBox.isChecked(), - self.icmpCheckBox.isChecked(), - self.yumproxyCheckBox.isChecked(), self.tempFullAccess.isChecked(), self.tempFullAccessTime.value()) if self.fw_model.fw_changed: @@ -773,114 +775,58 @@ class VMSettingsWindow(Ui_SettingsDialog, QDialog): self.rulesTreeView.header().setResizeMode(QHeaderView.ResizeToContents) self.rulesTreeView.header().setResizeMode(0, QHeaderView.Stretch) self.set_allow(model.allow) - self.dnsCheckBox.setChecked(model.allowDns) - self.icmpCheckBox.setChecked(model.allowIcmp) - self.yumproxyCheckBox.setChecked(model.allowYumProxy) if model.tempFullAccessExpireTime: self.tempFullAccess.setChecked(True) self.tempFullAccessTime.setValue( (model.tempFullAccessExpireTime - int(datetime.datetime.now().strftime("%s")))/60) + def disable_all_fw_conf(self): + self.firewallModifiedOutsidelabel.setVisible(True) + self.policyAllowRadioButton.setEnabled(False) + self.policyDenyRadioButton.setEnabled(False) + self.rulesTreeView.setEnabled(False) + self.newRuleButton.setEnabled(False) + self.editRuleButton.setEnabled(False) + self.deleteRuleButton.setEnabled(False) + self.firewalRulesLabel.setEnabled(False) + self.tempFullAccessWidget.setEnabled(False) + def set_allow(self, allow): self.policyAllowRadioButton.setChecked(allow) self.policyDenyRadioButton.setChecked(not allow) self.policy_changed(allow) def policy_changed(self, checked): - self.tempFullAccessWidget.setEnabled(self.policyDenyRadioButton.isChecked()) + self.rulesTreeView.setEnabled(self.policyDenyRadioButton.isChecked()) + self.newRuleButton.setEnabled(self.policyDenyRadioButton.isChecked()) + self.editRuleButton.setEnabled(self.policyDenyRadioButton.isChecked()) + self.deleteRuleButton.setEnabled(self.policyDenyRadioButton.isChecked()) + self.firewalRulesLabel.setEnabled( + self.policyDenyRadioButton.isChecked()) + self.tempFullAccessWidget.setEnabled( + self.policyDenyRadioButton.isChecked()) def new_rule_button_pressed(self): dialog = NewFwRuleDlg() - self.run_rule_dialog(dialog) + self.fw_model.run_rule_dialog(dialog) def edit_rule_button_pressed(self): - dialog = NewFwRuleDlg() - dialog.set_ok_enabled(True) - selected = self.rulesTreeView.selectedIndexes() - if len(selected) > 0: - row = self.rulesTreeView.selectedIndexes().pop().row() - address = self.fw_model.get_column_string(0, row).replace(' ', '') - dialog.addressComboBox.setItemText(0, address) - dialog.addressComboBox.setCurrentIndex(0) - service = self.fw_model.get_column_string(1, row) - if service == "any": - service = "" - dialog.serviceComboBox.setItemText(0, service) - dialog.serviceComboBox.setCurrentIndex(0) - protocol = self.fw_model.get_column_string(2, row) - if protocol == "tcp": - dialog.tcp_radio.setChecked(True) - elif protocol == "udp": - dialog.udp_radio.setChecked(True) - else: - dialog.any_radio.setChecked(True) - self.run_rule_dialog(dialog, row) + selected = self.rulesTreeView.selectedIndexes() + + if len(selected) > 0: + dialog = NewFwRuleDlg() + dialog.set_ok_enabled(True) + row = self.rulesTreeView.selectedIndexes().pop().row() + self.fw_model.populate_edit_dialog(dialog, row) + self.fw_model.run_rule_dialog(dialog, row) def delete_rule_button_pressed(self): - for i in set([index.row() for index in self.rulesTreeView.selectedIndexes()]): + for i in set([index.row() for index + in self.rulesTreeView.selectedIndexes()]): self.fw_model.removeChild(i) - def run_rule_dialog(self, dialog, row = None): - if dialog.exec_(): - address = str(dialog.addressComboBox.currentText()) - service = str(dialog.serviceComboBox.currentText()) - port = None - port2 = None - - unmask = address.split("/", 1) - if len(unmask) == 2: - address = unmask[0] - netmask = int(unmask[1]) - else: - netmask = 32 - - if address == "*": - address = "0.0.0.0" - netmask = 0 - - if dialog.any_radio.isChecked(): - protocol = "any" - port = 0 - else: - if dialog.tcp_radio.isChecked(): - protocol = "tcp" - elif dialog.udp_radio.isChecked(): - protocol = "udp" - else: - protocol = "any" - - try: - range = service.split("-", 1) - if len(range) == 2: - port = int(range[0]) - port2 = int(range[1]) - else: - port = int(service) - except (TypeError, ValueError) as ex: - port = self.fw_model.get_service_port(service) - - if port is not None: - if port2 is not None and port2 <= port: - QMessageBox.warning(None, self.tr("Invalid service ports range"), - self.tr("Port {0} is lower than port {1}.").format( - port2, port)) - else: - item = {"address": address, - "netmask": netmask, - "portBegin": port, - "portEnd": port2, - "proto": protocol, - } - if row is not None: - self.fw_model.setChild(row, item) - else: - self.fw_model.appendChild(item) - else: - QMessageBox.warning(None, self.tr("Invalid service name"), - self.tr("Service '{0}' is unknown.").format(service)) - # Bases on the original code by: # Copyright (c) 2002-2007 Pascal Varet diff --git a/ui/newfwruledlg.ui b/ui/newfwruledlg.ui index 11da4ed..391e16c 100644 --- a/ui/newfwruledlg.ui +++ b/ui/newfwruledlg.ui @@ -31,10 +31,22 @@ 6 - - + + + + + 0 + 0 + + + + + 0 + 0 + + - Protocol + UDP @@ -45,13 +57,6 @@ - - - - true - - - @@ -59,6 +64,31 @@ + + + + + 0 + 0 + + + + + 71 + 0 + + + + + + + Any + + + true + + + @@ -66,6 +96,13 @@ + + + + true + + + @@ -85,44 +122,10 @@ - - - - - 0 - 0 - - - - - 0 - 0 - - + + - UDP - - - - - - - - 0 - 0 - - - - - 71 - 0 - - - - Any - - - true + Protocol diff --git a/ui/settingsdlg.ui b/ui/settingsdlg.ui index ee24036..1d711c1 100644 --- a/ui/settingsdlg.ui +++ b/ui/settingsdlg.ui @@ -29,7 +29,7 @@ - 1 + 2 @@ -655,48 +655,101 @@ Firewall rules - - - - Allow network access except... - - - - - - - - 323 - 0 - - - - Allow ICMP traffic - - - true - - - - - - Deny network access except... - - - - - - - Allow DNS queries - - - true - - + + + + + Allow all outgoing Internet connections + + + + + + + Limit outgoing Internet connections to ... + + + + - + + + Qt::Horizontal + + + + + + + NOTE: To block all network access, set Networking to (none) on the Basic settings tab. This tab provides a very simplified firewall configuration. All DNS requests and ICMP (pings) will be allowed. For more granular control, use the command line tool qvm-firewall. + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + List of allowed (whitelisted) addresses: + + + + + + + true + + + + 0 + + + 0 + + + 0 + + + + + Allow full access for + + + + + + + min + + + 5 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + QLayout::SetMaximumSize @@ -805,48 +858,57 @@ - - + + + + + + + + + 255 + 0 + 0 + + + + + + + + + 255 + 0 + 0 + + + + + + + + + 139 + 142 + 142 + + + + + + + + + 75 + true + true + + - Allow connections to Updates Proxy + Firewall has been modified manually - please use qvm-firewall for any further configuration. - - - - true - - - - 0 - - - 0 - - - 0 - - - - - Allow full access for - - - - - - - min - - - 5 - - - - - - @@ -1035,11 +1097,6 @@ vcpus include_in_balancing kernel - policyAllowRadioButton - policyDenyRadioButton - icmpCheckBox - dnsCheckBox - yumproxyCheckBox newRuleButton rulesTreeView editRuleButton