firewall.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  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.__columnValues = {
  121. 0: lambda x: "*" if self.children[x]["address"] == "0.0.0.0" and
  122. self.children[x]["netmask"] == 0 else
  123. self.children[x]["address"] + ("" if self.children[x][ "netmask"] == 32 else
  124. " /{0}".format(self.children[x][
  125. "netmask"])),
  126. 1: lambda x: "any" if self.children[x]["portBegin"] == 0 else
  127. "{0}-{1}".format(self.children[x]["portBegin"], self.children[x][
  128. "portEnd"]) if self.children[x]["portEnd"] is not None else \
  129. self.get_service_name(self.children[x]["portBegin"]),
  130. 2: lambda x: self.children[x]["proto"], }
  131. self.__columnNames = {0: "Address", 1: "Service", 2: "Protocol", }
  132. self.__services = list()
  133. pattern = re.compile("(?P<name>[a-z][a-z0-9-]+)\s+(?P<port>[0-9]+)/(?P<protocol>[a-z]+)", re.IGNORECASE)
  134. f = open('/etc/services', 'r')
  135. for line in f:
  136. match = pattern.match(line)
  137. if match is not None:
  138. service = match.groupdict()
  139. self.__services.append( (service["name"], int(service["port"]),) )
  140. f.close()
  141. self.fw_changed = False
  142. def sort(self, idx, order):
  143. from operator import attrgetter
  144. rev = (order == Qt.AscendingOrder)
  145. if idx==0:
  146. self.children.sort(key=lambda x: x['address'], reverse = rev)
  147. if idx==1:
  148. self.children.sort(key=lambda x: self.get_service_name(x[
  149. "portBegin"]) if x["portEnd"] == None else x["portBegin"],
  150. reverse = rev)
  151. if idx==2:
  152. self.children.sort(key=lambda x: x['proto'], reverse
  153. = rev)
  154. index1 = self.createIndex(0, 0)
  155. index2 = self.createIndex(len(self)-1, len(self.__columnValues)-1)
  156. self.dataChanged.emit(index1, index2)
  157. def get_service_name(self, port):
  158. for service in self.__services:
  159. if service[1] == port:
  160. return service[0]
  161. return str(port)
  162. def get_service_port(self, name):
  163. for service in self.__services:
  164. if service[0] == name:
  165. return service[1]
  166. return None
  167. def get_column_string(self, col, row):
  168. return self.__columnValues[col](row)
  169. def rule_to_dict(self, rule):
  170. if rule.dsthost is None:
  171. raise FirewallModifiedOutsideError('no dsthost')
  172. d = {}
  173. if not rule.proto:
  174. d['proto'] = 'any'
  175. d['portBegin'] = 'any'
  176. d['portEnd'] = None
  177. else:
  178. d['proto'] = rule.proto
  179. if rule.dstports is None:
  180. raise FirewallModifiedOutsideError('no dstport')
  181. d['portBegin'] = rule.dstports.range[0]
  182. d['portEnd'] = rule.dstports.range[1] \
  183. if rule.dstports.range[0] != rule.dstports.range[1] \
  184. else None
  185. if rule.dsthost.type == 'dsthost':
  186. d['address'] = str(rule.dsthost)
  187. d['netmask'] = 32
  188. elif rule.dsthost.type == 'dst4':
  189. network = ipaddress.IPv4Network(rule.dsthost)
  190. d['address'] = str(network.network_address)
  191. d['netmask'] = int(network.prefixlen)
  192. else:
  193. raise FirewallModifiedOutsideError(
  194. 'cannot map dsthost.type={!s}'.format(rule.dsthost))
  195. if rule.expire is not None:
  196. d['expire'] = int(rule.expire)
  197. return d
  198. def get_firewall_conf(self, vm):
  199. conf = {
  200. 'allow': None,
  201. 'allowDns': False,
  202. 'allowIcmp': False,
  203. 'allowYumProxy': False,
  204. 'rules': [],
  205. }
  206. common_action = None
  207. tentative_action = None
  208. reversed_rules = list(reversed(vm.firewall.rules))
  209. while reversed_rules:
  210. rule = reversed_rules[0]
  211. if rule.dsthost is not None or rule.proto is not None:
  212. break
  213. tentative_action = reversed_rules.pop(0).action
  214. if not reversed_rules:
  215. conf['allow'] = tentative_action == 'accept'
  216. return conf
  217. for rule in reversed_rules:
  218. if rule.specialtarget == 'dns':
  219. conf['allowDns'] = (rule.action == 'accept')
  220. continue
  221. if rule.proto == 'icmp':
  222. if rule.icmptype is not None:
  223. raise FirewallModifiedOutsideError(
  224. 'cannot map icmptype != None')
  225. conf['allowIcmp'] = (rule.action == 'accept')
  226. continue
  227. if common_action is None:
  228. common_action = rule.action
  229. elif common_action != rule.action:
  230. raise FirewallModifiedOutsideError('incoherent action')
  231. conf['rules'].insert(0, self.rule_to_dict(rule))
  232. if common_action is None or common_action != tentative_action:
  233. # we've got only specialtarget and/or icmp
  234. conf['allow'] = tentative_action == 'accept'
  235. return conf
  236. raise FirewallModifiedOutsideError('it does not add up')
  237. def write_firewall_conf(self, vm, conf):
  238. common_action = qubesadmin.firewall.Action(
  239. 'drop' if conf['allow'] else 'accept')
  240. rules = []
  241. for rule in conf['rules']:
  242. kwargs = {}
  243. if rule['proto'] != 'any':
  244. kwargs['proto'] = rule['proto']
  245. if rule['portBegin'] != 'any':
  246. kwargs['dstports'] = '-'.join(map(str, filter((lambda x: x),
  247. (rule['portBegin'], rule['portEnd']))))
  248. netmask = str(rule['netmask']) if rule['netmask'] != 32 else None
  249. rules.append(qubesadmin.firewall.Rule(None,
  250. action=common_action,
  251. dsthost='/'.join(map(str, filter((lambda x: x),
  252. (rule['address'], netmask)))),
  253. **kwargs))
  254. if conf['allowDns']:
  255. rules.append(qubesadmin.firewall.Rule(None,
  256. action='accept', specialtarget='dns'))
  257. if conf['allowIcmp']:
  258. rules.append(qubesadmin.firewall.Rule(None,
  259. action='accept', proto='icmp'))
  260. if common_action == 'drop':
  261. rules.append(qubesadmin.firewall.Rule(None,
  262. action='accept'))
  263. vm.firewall.rules = rules
  264. def set_vm(self, vm):
  265. self.__vm = vm
  266. self.clearChildren()
  267. conf = self.get_firewall_conf(vm)
  268. self.allow = conf["allow"]
  269. self.allowDns = conf["allowDns"]
  270. self.allowIcmp = conf["allowIcmp"]
  271. self.allowYumProxy = conf["allowYumProxy"]
  272. self.tempFullAccessExpireTime = 0
  273. for rule in conf["rules"]:
  274. self.appendChild(rule)
  275. if "expire" in rule and rule["address"] == "0.0.0.0":
  276. self.tempFullAccessExpireTime = rule["expire"]
  277. def get_vm_name(self):
  278. return self.__vm.name
  279. def apply_rules(self, allow, dns, icmp, yumproxy, tempFullAccess=False,
  280. tempFullAccessTime=None):
  281. assert self.__vm is not None
  282. if self.allow != allow or self.allowDns != dns or \
  283. self.allowIcmp != icmp or self.allowYumProxy != yumproxy or \
  284. (self.tempFullAccessExpireTime != 0) != tempFullAccess:
  285. self.fw_changed = True
  286. conf = { "allow": allow,
  287. "allowDns": dns,
  288. "allowIcmp": icmp,
  289. "allowYumProxy": yumproxy,
  290. "rules": list()
  291. }
  292. for rule in self.children:
  293. if "expire" in rule and rule["address"] == "0.0.0.0" and \
  294. rule["netmask"] == 0 and rule["proto"] == "any":
  295. # rule already present, update its time
  296. if tempFullAccess:
  297. rule["expire"] = \
  298. int(datetime.datetime.now().strftime("%s")) + \
  299. tempFullAccessTime*60
  300. tempFullAccess = False
  301. conf["rules"].append(rule)
  302. if tempFullAccess and not allow:
  303. conf["rules"].append({"address": "0.0.0.0",
  304. "netmask": 0,
  305. "proto": "any",
  306. "expire": int(
  307. datetime.datetime.now().strftime("%s"))+\
  308. tempFullAccessTime*60
  309. })
  310. if self.fw_changed:
  311. self.write_firewall_conf(self.__vm, conf)
  312. def index(self, row, column, parent=QModelIndex()):
  313. if not self.hasIndex(row, column, parent):
  314. return QModelIndex()
  315. return self.createIndex(row, column, self.children[row])
  316. def parent(self, child):
  317. return QModelIndex()
  318. def rowCount(self, parent=QModelIndex()):
  319. return len(self)
  320. def columnCount(self, parent=QModelIndex()):
  321. return len(self.__columnValues)
  322. def hasChildren(self, index=QModelIndex()):
  323. parentItem = index.internalPointer()
  324. if parentItem is not None:
  325. return False
  326. else:
  327. return True
  328. def data(self, index, role=Qt.DisplayRole):
  329. if index.isValid() and role == Qt.DisplayRole:
  330. return self.__columnValues[index.column()](index.row())
  331. def headerData(self, section, orientation, role=Qt.DisplayRole):
  332. if section < len(self.__columnNames) \
  333. and orientation == Qt.Horizontal and role == Qt.DisplayRole:
  334. return self.__columnNames[section]
  335. @property
  336. def children(self):
  337. return self.__children
  338. def appendChild(self, child):
  339. row = len(self)
  340. self.beginInsertRows(QModelIndex(), row, row)
  341. self.children.append(child)
  342. self.endInsertRows()
  343. index = self.createIndex(row, 0, child)
  344. self.dataChanged.emit(index, index)
  345. self.fw_changed = True
  346. def removeChild(self, i):
  347. if i >= len(self):
  348. return
  349. self.beginRemoveRows(QModelIndex(), i, i)
  350. del self.children[i]
  351. self.endRemoveRows()
  352. index = self.createIndex(i, 0)
  353. self.dataChanged.emit(index, index)
  354. self.fw_changed = True
  355. def setChild(self, i, child):
  356. self.children[i] = child
  357. index = self.createIndex(i, 0, child)
  358. self.dataChanged.emit(index, index)
  359. self.fw_changed = True
  360. def clearChildren(self):
  361. self.__children = list()
  362. def __len__(self):
  363. return len(self.children)