firewall.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. #
  2. # The Qubes OS Project, http://www.qubes-os.org
  3. #
  4. # Copyright (C) 2011 Tomasz Sterna <tomek@xiaoka.com>
  5. #
  6. # This program is free software; you can redistribute it and/or
  7. # modify it under the terms of the GNU General Public License
  8. # as published by the Free Software Foundation; either version 2
  9. # of the License, or (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program; if not, write to the Free Software
  18. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. #
  20. #
  21. import datetime
  22. import ipaddress
  23. import os
  24. import re
  25. import sys
  26. import xml.etree.ElementTree
  27. from PyQt4.QtCore import *
  28. from PyQt4.QtGui import *
  29. import qubesadmin.firewall
  30. from . import ui_newfwruledlg
  31. class FirewallModifiedOutsideError(ValueError):
  32. pass
  33. class QIPAddressValidator(QValidator):
  34. def __init__(self, parent = None):
  35. super (QIPAddressValidator, self).__init__(parent)
  36. def validate(self, input, pos):
  37. hostname = str(input)
  38. if len(hostname) > 255 or len(hostname) == 0:
  39. return (QValidator.Intermediate, input, pos)
  40. if hostname == "*":
  41. return (QValidator.Acceptable, input, pos)
  42. unmask = hostname.split("/", 1)
  43. if len(unmask) == 2:
  44. hostname = unmask[0]
  45. mask = unmask[1]
  46. if mask.isdigit() or mask == "":
  47. if re.match("^([0-9]{1,3}\.){3}[0-9]{1,3}$", hostname) is None:
  48. return (QValidator.Invalid, input, pos)
  49. if mask != "":
  50. mask = int(unmask[1])
  51. if mask < 0 or mask > 32:
  52. return (QValidator.Invalid, input, pos)
  53. else:
  54. return (QValidator.Invalid, input, pos)
  55. if hostname[-1:] == ".":
  56. hostname = hostname[:-1]
  57. if hostname[-1:] == "-":
  58. return (QValidator.Intermediate, input, pos)
  59. allowed = re.compile("(?!-)[A-Z\d-]{1,63}(?<!-)$", re.IGNORECASE)
  60. if all(allowed.match(x) for x in hostname.split(".")):
  61. return (QValidator.Acceptable, input, pos)
  62. return (QValidator.Invalid, input, pos)
  63. class NewFwRuleDlg (QDialog, ui_newfwruledlg.Ui_NewFwRuleDlg):
  64. def __init__(self, parent = None):
  65. super (NewFwRuleDlg, self).__init__(parent)
  66. self.setupUi(self)
  67. self.set_ok_enabled(False)
  68. self.addressComboBox.setValidator(QIPAddressValidator())
  69. self.addressComboBox.editTextChanged.connect(self.address_editing_finished)
  70. self.serviceComboBox.setValidator(QRegExpValidator(QRegExp("[a-z][a-z0-9-]+|[0-9]+(-[0-9]+)?", Qt.CaseInsensitive), None))
  71. self.serviceComboBox.setEnabled(False)
  72. self.serviceComboBox.setInsertPolicy(QComboBox.InsertAtBottom)
  73. self.populate_combos()
  74. self.serviceComboBox.setInsertPolicy(QComboBox.InsertAtTop)
  75. def accept(self):
  76. if self.tcp_radio.isChecked() or self.udp_radio.isChecked():
  77. if len(self.serviceComboBox.currentText()) == 0:
  78. msg = QMessageBox()
  79. msg.warning(self, self.tr("Firewall rule"),
  80. self.tr("You need to fill service name/port for TCP/UDP rule"))
  81. return
  82. QDialog.accept(self)
  83. def populate_combos(self):
  84. example_addresses = [
  85. "", "www.example.com",
  86. "192.168.1.100", "192.168.0.0/16",
  87. "*"
  88. ]
  89. displayed_services = [
  90. '',
  91. 'http', 'https', 'ftp', 'ftps', 'smtp',
  92. 'smtps', 'pop3', 'pop3s', 'imap', 'imaps', 'odmr',
  93. 'nntp', 'nntps', 'ssh', 'telnet', 'telnets', 'ntp',
  94. 'snmp', 'ldap', 'ldaps', 'irc', 'ircs', 'xmpp-client',
  95. 'syslog', 'printer', 'nfs', 'x11',
  96. '1024-1234'
  97. ]
  98. for address in example_addresses:
  99. self.addressComboBox.addItem(address)
  100. for service in displayed_services:
  101. self.serviceComboBox.addItem(service)
  102. def address_editing_finished(self):
  103. self.set_ok_enabled(True)
  104. def set_ok_enabled(self, on):
  105. ok_button = self.buttonBox.button(QDialogButtonBox.Ok)
  106. if ok_button is not None:
  107. ok_button.setEnabled(on)
  108. def on_tcp_radio_toggled(self, checked):
  109. if checked:
  110. self.serviceComboBox.setEnabled(True)
  111. def on_udp_radio_toggled(self, checked):
  112. if checked:
  113. self.serviceComboBox.setEnabled(True)
  114. def on_any_radio_toggled(self, checked):
  115. if checked:
  116. self.serviceComboBox.setEnabled(False)
  117. class QubesFirewallRulesModel(QAbstractItemModel):
  118. def __init__(self, parent=None):
  119. QAbstractItemModel.__init__(self, parent)
  120. self.__columnNames = {0: "Address", 1: "Service", 2: "Protocol", }
  121. self.__services = list()
  122. pattern = re.compile("(?P<name>[a-z][a-z0-9-]+)\s+(?P<port>[0-9]+)/(?P<protocol>[a-z]+)", re.IGNORECASE)
  123. f = open('/etc/services', 'r')
  124. for line in f:
  125. match = pattern.match(line)
  126. if match is not None:
  127. service = match.groupdict()
  128. self.__services.append( (service["name"], int(service["port"]),) )
  129. f.close()
  130. self.fw_changed = False
  131. def sort(self, idx, order):
  132. from operator import attrgetter
  133. rev = (order == Qt.AscendingOrder)
  134. self.children.sort(key = lambda x: self.get_column_string(idx, x)
  135. , reverse = rev)
  136. index1 = self.createIndex(0, 0)
  137. index2 = self.createIndex(len(self)-1, len(self.__columnNames)-1)
  138. self.dataChanged.emit(index1, index2)
  139. def get_service_name(self, port):
  140. for service in self.__services:
  141. if str(service[1]) == str(port):
  142. return service[0]
  143. return str(port)
  144. def get_service_port(self, name):
  145. for service in self.__services:
  146. if service[0] == name:
  147. return service[1]
  148. return None
  149. def get_column_string(self, col, rule):
  150. # Address
  151. if col == 0:
  152. if rule.dsthost is None:
  153. return "*"
  154. else:
  155. if rule.dsthost.type == 'dst4'\
  156. and rule.dsthost.prefixlen == '32':
  157. return str(rule.dsthost)[:-3]
  158. elif rule.dsthost.type == 'dst6'\
  159. and rule.dsthost.prefixlen == '128':
  160. return str(rule.dsthost)[:-4]
  161. else:
  162. return str(rule.dsthost)
  163. # Service
  164. if col == 1:
  165. if rule.dstports is None:
  166. return "any"
  167. elif rule.dstports.range[0] != rule.dstports.range[1]:
  168. return str(rule.dstports)
  169. else:
  170. return self.get_service_name(rule.dstports)
  171. # Protocol
  172. if col == 2:
  173. if rule.proto is None:
  174. return "any"
  175. else:
  176. return str(rule.proto)
  177. return "unknown"
  178. def get_firewall_conf(self, vm):
  179. conf = {
  180. 'allow': None,
  181. 'expire': 0,
  182. 'rules': [],
  183. }
  184. allowDns = False
  185. allowIcmp = False
  186. common_action = None
  187. reversed_rules = list(reversed(vm.firewall.rules))
  188. last_rule = reversed_rules.pop(0)
  189. if last_rule == qubesadmin.firewall.Rule('action=accept') \
  190. or last_rule == qubesadmin.firewall.Rule('action=drop'):
  191. common_action = last_rule.action
  192. else:
  193. FirewallModifiedOutsideError('Last rule must be either '
  194. 'drop all or accept all.')
  195. dns_rule = qubesadmin.firewall.Rule(None,
  196. action='accept', specialtarget='dns')
  197. icmp_rule = qubesadmin.firewall.Rule(None,
  198. action='accept', proto='icmp')
  199. while reversed_rules:
  200. rule = reversed_rules.pop(0)
  201. if rule == dns_rule:
  202. allowDns = True
  203. continue
  204. if rule.proto == icmp_rule:
  205. allowIcmp = True
  206. continue
  207. if rule.specialtarget is not None or rule.icmptype is not None:
  208. raise FirewallModifiedOutsideError("Rule type unknown!")
  209. if (rule.dsthost is not None or rule.proto is not None) \
  210. and rule.expire is None:
  211. if rule.action == 'accept':
  212. conf['rules'].insert(0, rule)
  213. continue
  214. else:
  215. raise FirewallModifiedOutsideError('No blacklist support.')
  216. if rule.expire is not None and rule.dsthost is None \
  217. and rule.proto is None:
  218. conf['expire'] = int(str(rule.expire))
  219. continue
  220. raise FirewallModifiedOutsideError('it does not add up.')
  221. conf['allow'] = (common_action == 'accept')
  222. if not allowIcmp and not conf['allow']:
  223. raise FirewallModifiedOutsideError('ICMP must be allowed.')
  224. if not allowDns and not conf['allow']:
  225. raise FirewallModifiedOutsideError('DNS must be allowed')
  226. return conf
  227. def write_firewall_conf(self, vm, conf):
  228. rules = []
  229. for rule in conf['rules']:
  230. rules.append(rule)
  231. if not conf['allow']:
  232. rules.append(qubesadmin.firewall.Rule(None,
  233. action='accept', specialtarget='dns'))
  234. if not conf['allow']:
  235. rules.append(qubesadmin.firewall.Rule(None,
  236. action='accept', proto='icmp'))
  237. if conf['allow']:
  238. rules.append(qubesadmin.firewall.Rule(None,
  239. action='accept'))
  240. else:
  241. rules.append(qubesadmin.firewall.Rule(None,
  242. action = 'drop'))
  243. vm.firewall.rules = rules
  244. def set_vm(self, vm):
  245. self.__vm = vm
  246. self.clearChildren()
  247. conf = self.get_firewall_conf(vm)
  248. self.allow = conf["allow"]
  249. self.tempFullAccessExpireTime = conf['expire']
  250. for rule in conf["rules"]:
  251. self.appendChild(rule)
  252. def get_vm_name(self):
  253. return self.__vm.name
  254. def apply_rules(self, allow, tempFullAccess=False,
  255. tempFullAccessTime=None):
  256. assert self.__vm is not None
  257. if self.allow != allow or \
  258. (self.tempFullAccessExpireTime != 0) != tempFullAccess:
  259. self.fw_changed = True
  260. conf = { "allow": allow,
  261. "rules": list()
  262. }
  263. conf['rules'].extend(self.children)
  264. if tempFullAccess and not allow:
  265. conf["rules"].append(qubesadmin.firewall.Rule(None,action='accept'
  266. , expire=int(datetime.datetime.now().strftime("%s"))+\
  267. tempFullAccessTime*60))
  268. if self.fw_changed:
  269. self.write_firewall_conf(self.__vm, conf)
  270. def populate_edit_dialog(self, dialog, row):
  271. address = self.get_column_string(0, self.children[row])
  272. dialog.addressComboBox.setItemText(0, address)
  273. dialog.addressComboBox.setCurrentIndex(0)
  274. service = self.get_column_string(1, self.children[row])
  275. if service == "any":
  276. service = ""
  277. dialog.serviceComboBox.setItemText(0, service)
  278. dialog.serviceComboBox.setCurrentIndex(0)
  279. protocol = self.get_column_string(2, self.children[row])
  280. if protocol == "tcp":
  281. dialog.tcp_radio.setChecked(True)
  282. elif protocol == "udp":
  283. dialog.udp_radio.setChecked(True)
  284. else:
  285. dialog.any_radio.setChecked(True)
  286. def run_rule_dialog(self, dialog, row = None):
  287. if dialog.exec_():
  288. address = str(dialog.addressComboBox.currentText())
  289. service = str(dialog.serviceComboBox.currentText())
  290. rule = qubesadmin.firewall.Rule(None,action='accept')
  291. if address is not None and address != "*":
  292. try:
  293. rule.dsthost = address
  294. except ValueError:
  295. QMessageBox.warning(None, self.tr("Invalid address"),
  296. self.tr("Address '{0}' is invalid.").format(address))
  297. if dialog.tcp_radio.isChecked():
  298. rule.proto = 'tcp'
  299. elif dialog.udp_radio.isChecked():
  300. rule.proto = 'udp'
  301. if '-' in service:
  302. try:
  303. rule.dstports = service
  304. except ValueError:
  305. QMessageBox.warning(None, self.tr("Invalid port or service"),
  306. self.tr("Port number or service '{0}' is invalid.")
  307. .format(service))
  308. elif service is not None:
  309. try:
  310. rule.dstports = service
  311. except (TypeError, ValueError) as ex:
  312. if self.get_service_port(service) is not None:
  313. rule.dstports = self.get_service_port(service)
  314. else:
  315. QMessageBox.warning(None,
  316. self.tr("Invalid port or service"),
  317. self.tr("Port number or service '{0}' is invalid.")
  318. .format(service))
  319. if row is not None:
  320. self.setChild(row, rule)
  321. else:
  322. self.appendChild(rule)
  323. def index(self, row, column, parent=QModelIndex()):
  324. if not self.hasIndex(row, column, parent):
  325. return QModelIndex()
  326. return self.createIndex(row, column, self.children[row])
  327. def parent(self, child):
  328. return QModelIndex()
  329. def rowCount(self, parent=QModelIndex()):
  330. return len(self)
  331. def columnCount(self, parent=QModelIndex()):
  332. return len(self.__columnNames)
  333. def hasChildren(self, index=QModelIndex()):
  334. parentItem = index.internalPointer()
  335. if parentItem is not None:
  336. return False
  337. else:
  338. return True
  339. def data(self, index, role=Qt.DisplayRole):
  340. if index.isValid() and role == Qt.DisplayRole:
  341. return self.get_column_string(index.column()
  342. ,self.children[index.row()])
  343. def headerData(self, section, orientation, role=Qt.DisplayRole):
  344. if section < len(self.__columnNames) \
  345. and orientation == Qt.Horizontal and role == Qt.DisplayRole:
  346. return self.__columnNames[section]
  347. @property
  348. def children(self):
  349. return self.__children
  350. def appendChild(self, child):
  351. row = len(self)
  352. self.beginInsertRows(QModelIndex(), row, row)
  353. self.children.append(child)
  354. self.endInsertRows()
  355. index = self.createIndex(row, 0, child)
  356. self.dataChanged.emit(index, index)
  357. self.fw_changed = True
  358. def removeChild(self, i):
  359. if i >= len(self):
  360. return
  361. self.beginRemoveRows(QModelIndex(), i, i)
  362. del self.children[i]
  363. self.endRemoveRows()
  364. index = self.createIndex(i, 0)
  365. self.dataChanged.emit(index, index)
  366. self.fw_changed = True
  367. def setChild(self, i, child):
  368. self.children[i] = child
  369. index = self.createIndex(i, 0, child)
  370. self.dataChanged.emit(index, index)
  371. self.fw_changed = True
  372. def clearChildren(self):
  373. self.__children = list()
  374. def __len__(self):
  375. return len(self.children)