qvm-firewall 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. #!/usr/bin/python2
  2. # -*- encoding: utf8 -*-
  3. #
  4. # The Qubes OS Project, http://www.qubes-os.org
  5. #
  6. # Copyright (C) 2012 Marek Marczykowski <marmarek@invisiblethingslab.com>
  7. #
  8. # This program is free software; you can redistribute it and/or
  9. # modify it under the terms of the GNU General Public License
  10. # as published by the Free Software Foundation; either version 2
  11. # of the License, or (at your option) any later version.
  12. #
  13. # This program is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. # GNU General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU General Public License
  19. # along with this program; if not, write to the Free Software
  20. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  21. #
  22. #
  23. import datetime
  24. from qubes.qubes import QubesVmCollection
  25. from optparse import OptionParser;
  26. import subprocess
  27. import sys
  28. import re
  29. import os
  30. import socket
  31. def parse_rule(args):
  32. if len(args) < 2:
  33. print >>sys.stderr, "ERROR: Rule must have at least address and protocol"
  34. return None
  35. address = args[0]
  36. netmask = 32
  37. proto = args[1]
  38. port = args[2] if len(args) > 2 else None
  39. port_end = None
  40. unmask = address.split("/", 1)
  41. if len(unmask) == 2:
  42. address = unmask[0]
  43. netmask = unmask[1]
  44. if netmask.isdigit():
  45. if re.match("^([0-9]{1,3}\.){3}[0-9]{1,3}$", address) is None:
  46. print >>sys.stderr, "ERROR: Only IP is allowed when specyfying netmask"
  47. return None
  48. if netmask != "":
  49. netmask = int(unmask[1])
  50. if netmask < 0 or netmask > 32:
  51. print >>sys.stderr, "ERROR: Invalid netmask"
  52. return None
  53. else:
  54. print >>sys.stderr, "ERROR: Invalid netmask"
  55. return None
  56. if address[-1:] == ".":
  57. address = address[:-1]
  58. allowed = re.compile("(?!-)[A-Z\d-]{1,63}(?<!-)$", re.IGNORECASE)
  59. if not all(allowed.match(x) for x in address.split(".")):
  60. print >>sys.stderr, "ERROR: Invalid hostname"
  61. return None
  62. proto_split = proto.split('/', 1)
  63. if len(proto_split) == 2:
  64. proto = proto_split[0]
  65. port = proto_split[1]
  66. if proto not in ['tcp', 'udp', 'any']:
  67. print >>sys.stderr, "ERROR: Protocol must be one of: 'tcp', 'udp', 'any'"
  68. return None
  69. if proto != "any" and port is None:
  70. print >>sys.stderr, "ERROR: Port required for protocol %s" % args[1]
  71. return None
  72. if port is not None:
  73. port_range = port.split('-', 1)
  74. if len(port_range) == 2:
  75. port = port_range[0]
  76. port_end = port_range[1]
  77. if port.isdigit():
  78. port = int(port)
  79. else:
  80. try:
  81. port = socket.getservbyname(port)
  82. except socket.error:
  83. print >>sys.stderr, "ERROR: Invalid port/service name '%s'" % port
  84. return None
  85. if port_end is not None and not port_end.isdigit():
  86. print >>sys.stderr, "ERROR: Invalid port '%s'" % port_end
  87. return None
  88. if port_end is not None:
  89. port_end = int(port_end)
  90. rule = {}
  91. rule['address'] = address
  92. rule['netmask'] = netmask
  93. rule['proto'] = proto
  94. rule['portBegin'] = port
  95. rule['portEnd'] = port_end
  96. return rule
  97. def list_rules(rules, numeric=False):
  98. fields = [ "num", "address", "proto", "port(s)" ]
  99. rules_to_display = list()
  100. counter = 1
  101. for rule in rules:
  102. parsed_rule = {
  103. 'num': "{0:>2}".format(counter),
  104. 'address': rule['address'] + ('/' + str(rule['netmask']) if rule['netmask'] < 32 else ""),
  105. 'proto': rule['proto'],
  106. 'port(s)': '',
  107. }
  108. if rule['proto'] in ['tcp', 'udp']:
  109. parsed_rule['port(s)'] = str(rule['portBegin']) + \
  110. ('-' + str(rule['portEnd']) if rule['portEnd'] is not None else '')
  111. if not numeric and rule['portBegin'] is not None and rule['portEnd'] is None:
  112. try:
  113. parsed_rule['port(s)'] = str(socket.getservbyport(rule['portBegin']))
  114. except socket.error:
  115. pass
  116. if 'expire' in rule:
  117. parsed_rule['expire'] = str(datetime.datetime.fromtimestamp(rule[
  118. 'expire']))
  119. rules_to_display.append(parsed_rule)
  120. counter += 1
  121. fields_width = {}
  122. for f in fields:
  123. fields_width[f] = len(f)
  124. for r in rules_to_display:
  125. if len(r[f]) > fields_width[f]:
  126. fields_width[f] = len(r[f])
  127. # Display the header
  128. s = ""
  129. for f in fields:
  130. fmt="{{0:-^{0}}}-+".format(fields_width[f] + 1)
  131. s += fmt.format('-')
  132. print s
  133. s = ""
  134. for f in fields:
  135. fmt=" {{0:^{0}}} |".format(fields_width[f])
  136. s += fmt.format(f)
  137. print s
  138. s = ""
  139. for f in fields:
  140. fmt="{{0:-^{0}}}-+".format(fields_width[f] + 1)
  141. s += fmt.format('-')
  142. print s
  143. # And the content
  144. for r in rules_to_display:
  145. s = ""
  146. for f in fields:
  147. fmt=" {{0:<{0}}} |".format(fields_width[f])
  148. s += fmt.format(r[f])
  149. if 'expire' in r:
  150. s += " <-- expires at %s" % r['expire']
  151. print s
  152. def display_firewall(conf, numeric=False):
  153. print "Firewall policy: %s" % (
  154. "ALLOW all traffic except" if conf['allow'] else "DENY all traffic except")
  155. print "ICMP: %s" % ("ALLOW" if conf['allowIcmp'] else 'DENY')
  156. print "DNS: %s" % ("ALLOW" if conf['allowDns'] else 'DENY')
  157. print "Qubes yum proxy: %s" % ("ALLOW" if conf['allowYumProxy'] else 'DENY')
  158. list_rules(conf['rules'], numeric)
  159. def add_rule(conf, args):
  160. rule = parse_rule(args)
  161. if rule is None:
  162. return False
  163. conf['rules'].append(rule)
  164. return True
  165. def del_rule(conf, args):
  166. if len(args) == 1 and args[0].isdigit():
  167. rulenum = int(args[0])
  168. if rulenum < 1 or rulenum > len(conf['rules']):
  169. print >>sys.stderr, "ERROR: Rule number out of range"
  170. return False
  171. conf['rules'].pop(rulenum-1)
  172. else:
  173. rule = parse_rule(args)
  174. #print "PARSED: %s" % str(rule)
  175. #print "ALL: %s" % str(conf['rules'])
  176. if rule is None:
  177. return False
  178. try:
  179. conf['rules'].remove(rule)
  180. except ValueError:
  181. print >>sys.stderr, "ERROR: Rule not found"
  182. return False
  183. return True
  184. def allow_deny_value(s):
  185. value = None
  186. if s == "allow":
  187. value = True
  188. elif s == "deny":
  189. value = False
  190. else:
  191. print >>sys.stderr, 'ERROR: Only "allow" or "deny" allowed'
  192. exit(1)
  193. return value
  194. def main():
  195. usage = "usage: %prog [-n] <vm-name> [action] [rule spec]\n"
  196. usage += " rule specification can be one of:\n"
  197. usage += " address|hostname[/netmask] tcp|udp port[-port]\n"
  198. usage += " address|hostname[/netmask] tcp|udp service_name\n"
  199. usage += " address|hostname[/netmask] any\n"
  200. parser = OptionParser (usage)
  201. parser.add_option ("-l", "--list", dest="do_list", action="store_true", default=True,
  202. help="List firewall settings (default action)")
  203. parser.add_option ("-a", "--add", dest="do_add", action="store_true", default=False,
  204. help="Add rule")
  205. parser.add_option ("-d", "--del", dest="do_del", action="store_true", default=False,
  206. help="Remove rule (given by number or by rule spec)")
  207. parser.add_option ("-P", "--policy", dest="set_policy", action="store", default=None,
  208. help="Set firewall policy (allow/deny)")
  209. parser.add_option ("-i", "--icmp", dest="set_icmp", action="store", default=None,
  210. help="Set ICMP access (allow/deny)")
  211. parser.add_option ("-D", "--dns", dest="set_dns", action="store", default=None,
  212. help="Set DNS access (allow/deny)")
  213. parser.add_option ("-Y", "--yum-proxy", dest="set_yum_proxy", action="store", default=None,
  214. help="Set access to Qubes yum proxy (allow/deny)")
  215. parser.add_option ("-r", "--reload", dest="reload", action="store_true",
  216. default=False, help="Reload firewall (implied by any "
  217. "change action")
  218. parser.add_option ("-n", "--numeric", dest="numeric", action="store_true", default=False,
  219. help="Display port numbers instead of services (makes sense only with --list)")
  220. parser.add_option ("--force-root", action="store_true", dest="force_root", default=False,
  221. help="Force to run, even with root privileges")
  222. (options, args) = parser.parse_args ()
  223. if (len (args) < 1):
  224. parser.error ("You must specify VM name!")
  225. vmname = args[0]
  226. args = args[1:]
  227. if hasattr(os, "geteuid") and os.geteuid() == 0:
  228. if not options.force_root:
  229. print >> sys.stderr, "*** Running this tool as root is strongly discouraged, this will lead you in permissions problems."
  230. print >> sys.stderr, "Retry as unprivileged user."
  231. print >> sys.stderr, "... or use --force-root to continue anyway."
  232. exit(1)
  233. if options.do_add or options.do_del or options.set_policy or \
  234. options.set_icmp or options.set_dns or options.set_yum_proxy:
  235. options.do_list = False
  236. qvm_collection = QubesVmCollection()
  237. if options.do_list:
  238. qvm_collection.lock_db_for_reading()
  239. qvm_collection.load()
  240. qvm_collection.unlock_db()
  241. else:
  242. qvm_collection.lock_db_for_writing()
  243. qvm_collection.load()
  244. vm = qvm_collection.get_vm_by_name(vmname)
  245. if vm is None:
  246. print >> sys.stderr, "A VM with the name '{0}' does not exist in the system.".format(vmname)
  247. exit(1)
  248. changed = False
  249. conf = vm.get_firewall_conf()
  250. if options.set_policy:
  251. conf['allow'] = allow_deny_value(options.set_policy)
  252. changed = True
  253. if options.set_icmp:
  254. conf['allowIcmp'] = allow_deny_value(options.set_icmp)
  255. changed = True
  256. if options.set_dns:
  257. conf['allowDns'] = allow_deny_value(options.set_dns)
  258. changed = True
  259. if options.set_yum_proxy:
  260. conf['allowYumProxy'] = allow_deny_value(options.set_yum_proxy)
  261. changed = True
  262. if options.do_add:
  263. changed = add_rule(conf, args)
  264. elif options.do_del:
  265. changed = del_rule(conf, args)
  266. elif options.do_list and not options.reload:
  267. if not vm.has_firewall():
  268. print "INFO: This VM has no firewall rules set, below defaults are listed"
  269. display_firewall(conf, options.numeric)
  270. if changed:
  271. vm.write_firewall_conf(conf)
  272. if changed or options.reload:
  273. if vm.is_running():
  274. if vm.netvm is not None and vm.netvm.is_proxyvm():
  275. vm.netvm.write_iptables_qubesdb_entry()
  276. qvm_collection.save()
  277. if not options.do_list:
  278. qvm_collection.unlock_db()
  279. main()